1 Commits

Author SHA1 Message Date
126b48ad3c build 2026-02-10 14:41:22 +09:00
129 changed files with 29 additions and 6054 deletions

View File

@@ -8,7 +8,7 @@ spring:
active: prod # 사용할 프로파일 지정 (ex. dev, prod, test) active: prod # 사용할 프로파일 지정 (ex. dev, prod, test)
datasource: datasource:
url: jdbc:postgresql://172.16.4.56:15432/kamco_cds url: jdbc:postgresql://192.168.2.127:15432/kamco_cds
#url: jdbc:postgresql://localhost:5432/kamco_cds #url: jdbc:postgresql://localhost:5432/kamco_cds
username: kamco_cds username: kamco_cds
password: kamco_cds_Q!W@E#R$ password: kamco_cds_Q!W@E#R$
@@ -59,8 +59,8 @@ management:
file: file:
#sync-root-dir: D:/kamco-nfs/images/ #sync-root-dir: D:/kamco-nfs/images/
sync-root-dir: /data/images/ sync-root-dir: /kamco-nfs/images/
sync-tmp-dir: /data/repo/tmp sync-tmp-dir: ${file.sync-root-dir}/tmp
sync-file-extention: tfw,tif sync-file-extention: tfw,tif
sync-auto-exception-start-year: 2025 sync-auto-exception-start-year: 2025
sync-auto-exception-before-year-cnt: 3 sync-auto-exception-before-year-cnt: 3

View File

@@ -1,687 +0,0 @@
# KAMCO Dataset Generation Batch System
KAMCO 학습 데이터 생성 및 처리를 위한 Spring Batch 시스템입니다.
## 목차
- [시스템 개요](#시스템-개요)
- [배치 작업 구조](#배치-작업-구조)
- [데이터베이스 스키마](#데이터베이스-스키마)
- [실행 흐름](#실행-흐름)
- [설정 방법](#설정-방법)
- [모니터링 및 로그](#모니터링-및-로그)
- [트러블슈팅](#트러블슈팅)
---
## 시스템 개요
### 주요 기능
- 검수 완료된 라벨링 데이터를 GeoJSON 형식으로 변환
- Docker 컨테이너를 통한 학습 데이터 생성 파이프라인 실행
- 생성된 결과물을 ZIP 파일로 압축
- 각 처리 단계별 성공/실패 이력을 DB에 자동 기록
### 기술 스택
- **Java 17+**
- **Spring Boot 3.x**
- **Spring Batch 5.x**
- **PostgreSQL**
- **Docker**
---
## 배치 작업 구조
### 1. Parent Job: `exportGeoJsonJob`
Parent Job은 진행 중인 모든 분석 회차를 조회하여 각 회차별로 Child Job을 실행합니다.
```
exportGeoJsonJob (Parent Job)
└─ Step: launchChildJobsStep
└─ Tasklet: LaunchChildJobsTasklet
├─ AnalCntInfo 리스트 조회
└─ 각 AnalCntInfo마다 Child Job 실행
```
**실행 조건:**
- `tb_map_sheet_anal_inference` 테이블의 `anal_state = 'ING'` (진행 중)
- 검수 완료(`COMPLETE`) 건수가 1개 이상 존재
- `all_cnt != file_cnt` (아직 파일 생성이 완료되지 않은 경우)
---
### 2. Child Job: `processAnalCntInfoJob`
각 AnalCntInfo(분석 회차)마다 독립적으로 실행되는 서브 작업입니다.
```
processAnalCntInfoJob (Child Job)
├─ Step 1: makeGeoJsonStep
│ └─ Tasklet: MakeGeoJsonTasklet
│ └─ 검수 완료된 라벨링 데이터를 GeoJSON 파일로 생성
│ → /dataset/request/{resultUid}/*.geojson
├─ Step 2: dockerRunStep
│ └─ Tasklet: DockerRunTasklet
│ └─ Docker 컨테이너 실행 (학습 데이터 생성 파이프라인)
│ → /dataset/response/{resultUid}/*
└─ Step 3: zipResponseStep
└─ Tasklet: ZipResponseTasklet
└─ 생성된 결과물을 ZIP으로 압축
→ /dataset/response/{resultUid}.zip
```
**JobParameters:**
- `analUid` (Long): 분석 회차 UID
- `resultUid` (String): 결과물 고유 ID (UUID)
- `timestamp` (Long): 고유성 보장을 위한 타임스탬프
---
## 데이터베이스 스키마
### 1. `batch_history` 테이블
전체 배치 작업(Parent Job) 실행 이력을 기록합니다.
```sql
CREATE TABLE public.batch_history (
uuid UUID PRIMARY KEY, -- 배치 실행 고유 ID
job VARCHAR(255) NOT NULL, -- 배치 작업 이름 (exportGeoJsonJob)
id VARCHAR(255) NOT NULL, -- 비즈니스 ID
created_dttm TIMESTAMP NOT NULL, -- 생성 일시
updated_dttm TIMESTAMP NOT NULL, -- 수정 일시
status VARCHAR(50) NOT NULL, -- 상태 (STARTED/COMPLETED/FAILED)
completed_dttm TIMESTAMP -- 완료 일시
);
```
**인덱스:**
- `idx_batch_history_job` (job)
- `idx_batch_history_status` (status)
- `idx_batch_history_created` (created_dttm DESC)
---
### 2. `batch_step_history` 테이블
각 AnalCntInfo의 Step별 실행 이력을 기록합니다.
```sql
CREATE TABLE public.batch_step_history (
id BIGSERIAL PRIMARY KEY, -- Step 이력 고유 ID
anal_uid BIGINT NOT NULL, -- 분석 UID
result_uid VARCHAR(255) NOT NULL, -- 결과 UID
step_name VARCHAR(100) NOT NULL, -- Step 이름
status VARCHAR(50) NOT NULL, -- 상태 (STARTED/SUCCESS/FAILED)
error_message TEXT, -- 에러 메시지 (최대 1000자)
started_dttm TIMESTAMP NOT NULL, -- Step 시작 일시
completed_dttm TIMESTAMP, -- Step 완료 일시
created_dttm TIMESTAMP NOT NULL, -- 생성 일시
updated_dttm TIMESTAMP NOT NULL -- 수정 일시
);
```
**Step 이름:**
- `makeGeoJsonStep`: GeoJSON 파일 생성
- `dockerRunStep`: Docker 컨테이너 실행
- `zipResponseStep`: 결과물 ZIP 압축
**인덱스:**
- `idx_batch_step_history_anal_uid` (anal_uid)
- `idx_batch_step_history_result_uid` (result_uid)
- `idx_batch_step_history_status` (status)
- `idx_batch_step_history_step_name` (step_name)
---
## 실행 흐름
### 전체 프로세스
```
1. Parent Job 시작
2. 진행 중인 AnalCntInfo 리스트 조회
3. 각 AnalCntInfo마다 반복:
├─ 3.1. Child Job 실행 (processAnalCntInfoJob)
│ ↓
│ ├─ Step 1: makeGeoJsonStep
│ │ - beforeStep: DB에 STARTED 기록
│ │ - Tasklet 실행: GeoJSON 파일 생성
│ │ - afterStep: DB에 SUCCESS/FAILED 기록
│ │
│ ├─ Step 2: dockerRunStep
│ │ - beforeStep: DB에 STARTED 기록
│ │ - Tasklet 실행: Docker 컨테이너 실행
│ │ - afterStep: DB에 SUCCESS/FAILED 기록
│ │
│ └─ Step 3: zipResponseStep
│ - beforeStep: DB에 STARTED 기록
│ - Tasklet 실행: 결과물 ZIP 압축
│ - afterStep: DB에 SUCCESS/FAILED 기록
└─ 3.2. 다음 AnalCntInfo 처리
4. Parent Job 종료 (부분 성공 허용)
```
---
### Step 1: makeGeoJsonStep
**목적:** 검수 완료된 라벨링 데이터를 GeoJSON 파일로 변환
**처리 과정:**
1. `findCompletedAnalMapSheetList()`: 검수 완료된 도엽 목록 조회
2. 각 도엽별로:
- `findCompletedYesterdayLabelingList()`: 어제까지 검수 완료된 데이터 조회
- GeoJSON Feature 생성
- `/dataset/request/{resultUid}/{filename}.geojson` 저장
- `updateLearnDataGeomFileCreateYn()`: DB에 파일 생성 완료 플래그 업데이트
**출력 파일 형식:**
```
{resultUid_8자}_{compareYyyy}_{targetYyyy}_{mapSheetNum}_D15.geojson
```
**예시:**
```
ED80D700_2022_2023_3724036_D15.geojson
```
---
### Step 2: dockerRunStep
**목적:** Docker 컨테이너를 통해 학습 데이터 생성 파이프라인 실행
**Docker 명령어:**
```bash
docker run --rm \
--user {dockerUser} \
-v {datasetVolume} \
-v {imagesVolume} \
--entrypoint python \
{dockerImage} \
code/kamco_full_pipeline.py \
--labelling-folder request/{resultUid} \
--output-folder response/{resultUid} \
--input_root {inputRoot} \
--output_root {outputRoot} \
--patch_size {patchSize} \
--overlap_pct {overlapPct} \
--train_val_test_ratio {train} {val} {test} \
--keep_empty_ratio {keepEmptyRatio}
```
**에러 처리:**
- Docker 프로세스의 `exitCode != 0``RuntimeException` 발생
- Step 실패로 처리되어 DB에 `FAILED` 상태 기록
- 에러 메시지와 exit code가 `error_message` 컬럼에 저장됨
---
### Step 3: zipResponseStep
**목적:** 생성된 학습 데이터를 ZIP 파일로 압축
**처리 과정:**
1. `/dataset/response/{resultUid}/` 디렉토리 검증
2. 디렉토리 내 모든 파일과 서브디렉토리를 재귀적으로 압축
3. `/dataset/response/{resultUid}.zip` 파일 생성
**압축 설정:**
- Hidden 파일 제외
- 디렉토리 구조 유지
- 버퍼 크기: 1024 bytes
---
## 설정 방법
### application.yml 설정
```yaml
# 학습 데이터 디렉토리 경로
training-data:
geojson-dir: /kamco-nfs/dataset
# Docker 설정
docker:
user: "1000:1000"
image: "kamco/dataset-generator:latest"
dataset-volume: "/kamco-nfs/dataset:/dataset"
images-volume: "/kamco-nfs/images:/images"
input-root: "/dataset"
output-root: "/dataset"
patch-size: 512
overlap-pct: 0.2
train-val-test-ratio:
- "0.7"
- "0.2"
- "0.1"
keep-empty-ratio: 0.5
```
---
### 환경 변수
| 환경 변수 | 설명 | 기본값 |
|----------|------|--------|
| `TRAINING_DATA_GEOJSON_DIR` | GeoJSON 파일 저장 경로 | `/kamco-nfs/dataset` |
| `DOCKER_USER` | Docker 컨테이너 실행 유저 | `1000:1000` |
| `DOCKER_IMAGE` | Docker 이미지 이름 | `kamco/dataset-generator:latest` |
---
## 모니터링 및 로그
### 로그 레벨
```yaml
logging:
level:
com.kamco.cd.geojsonscheduler: INFO
com.kamco.cd.geojsonscheduler.batch: DEBUG
org.springframework.batch: INFO
```
---
### 주요 로그 포인트
#### Parent Job 로그
```log
[INFO] Parent Job 시작: AnalCntInfo 리스트 조회 및 Child Job 실행
[INFO] 진행중인 회차 목록 조회 중...
[INFO] 진행중인 회차 수: 3
[INFO] 회차 검토: AnalUid=100, ResultUid=ED80D700...
[INFO] Child Job 실행 중... (AnalUid=100, ResultUid=ED80D700...)
[INFO] Child Job 실행 완료 (AnalUid=100, ResultUid=ED80D700...)
[INFO] Parent Job 완료 - 성공: 2, 건너뜀: 1, 실패: 0
```
#### Step 로그
```log
[INFO] ========================================
[INFO] GeoJSON 생성 시작 (AnalUid=100, ResultUid=ED80D700...)
[INFO] 검수 완료된 도엽 수: 5
[INFO] 도엽 처리 중: MapSheetNum=3724036
[INFO] 완료된 라벨링 데이터 수: 150
[INFO] GeoJSON 파일 저장 완료: /dataset/request/ED80D700.../ED80D700_2022_2023_3724036_D15.geojson
[INFO] GeoJSON 생성 완료 (ResultUid=ED80D700...) - 처리된 도엽 수: 5, 생성된 파일 수: 5
[INFO] ========================================
```
#### Docker 실행 로그
```log
[INFO] ========================================
[INFO] Docker 컨테이너 실행 시작 (ResultUid=ED80D700...)
[INFO] Running docker command: docker run --rm --user 1000:1000...
[INFO] [docker] Loading configuration...
[INFO] [docker] Processing pipeline...
[INFO] [docker] Pipeline completed successfully
[INFO] Docker process completed successfully for resultUid: ED80D700...
[INFO] ========================================
```
---
### DB 조회 쿼리
#### 1. 특정 회차의 모든 Step 실행 이력
```sql
SELECT
step_name,
status,
started_dttm,
completed_dttm,
EXTRACT(EPOCH FROM (completed_dttm - started_dttm)) AS duration_seconds,
error_message
FROM batch_step_history
WHERE anal_uid = 100
AND result_uid = 'ED80D700A0F5482BB0EC11A366DEA8DE'
ORDER BY started_dttm;
```
#### 2. 최근 실패한 Step 조회
```sql
SELECT
anal_uid,
result_uid,
step_name,
error_message,
started_dttm,
completed_dttm
FROM batch_step_history
WHERE status = 'FAILED'
ORDER BY started_dttm DESC
LIMIT 10;
```
#### 3. Step별 성공률 통계
```sql
SELECT
step_name,
COUNT(*) AS total_executions,
SUM(CASE WHEN status = 'SUCCESS' THEN 1 ELSE 0 END) AS success_count,
SUM(CASE WHEN status = 'FAILED' THEN 1 ELSE 0 END) AS failed_count,
ROUND(
SUM(CASE WHEN status = 'SUCCESS' THEN 1 ELSE 0 END)::NUMERIC / COUNT(*) * 100,
2
) AS success_rate_pct
FROM batch_step_history
GROUP BY step_name;
```
#### 4. 평균 실행 시간 (Step별)
```sql
SELECT
step_name,
COUNT(*) AS total_executions,
AVG(EXTRACT(EPOCH FROM (completed_dttm - started_dttm))) AS avg_duration_seconds,
MIN(EXTRACT(EPOCH FROM (completed_dttm - started_dttm))) AS min_duration_seconds,
MAX(EXTRACT(EPOCH FROM (completed_dttm - started_dttm))) AS max_duration_seconds
FROM batch_step_history
WHERE status = 'SUCCESS'
AND completed_dttm IS NOT NULL
GROUP BY step_name;
```
#### 5. 특정 기간 동안 처리된 회차 수
```sql
SELECT
DATE(started_dttm) AS execution_date,
COUNT(DISTINCT result_uid) AS processed_count
FROM batch_step_history
WHERE step_name = 'makeGeoJsonStep'
AND started_dttm >= CURRENT_DATE - INTERVAL '7 days'
GROUP BY DATE(started_dttm)
ORDER BY execution_date DESC;
```
---
## 트러블슈팅
### 1. Docker 컨테이너 실행 실패
**증상:**
```log
[ERROR] Docker process exited with code 1 for resultUid: ED80D700...
FileNotFoundError: Missing training pairs root at /dataset/response/.../tifs/train
```
**원인:**
- GeoJSON 파일이 생성되지 않았거나 잘못된 형식
- Docker 볼륨 마운트 경로 불일치
- 파이프라인 실행 중 필수 파일 누락
**해결 방법:**
1. `batch_step_history` 테이블에서 `makeGeoJsonStep` 상태 확인
```sql
SELECT * FROM batch_step_history
WHERE result_uid = 'ED80D700...'
AND step_name = 'makeGeoJsonStep';
```
2. GeoJSON 파일 존재 여부 확인
```bash
ls -la /kamco-nfs/dataset/request/ED80D700.../
```
3. Docker 볼륨 설정 확인
```yaml
docker:
dataset-volume: "/kamco-nfs/dataset:/dataset" # 호스트:컨테이너
```
---
### 2. GeoJSON 파일이 생성되지 않음
**증상:**
```log
[WARN] 검수 완료된 도엽이 없음. 작업 건너뜀.
```
**원인:**
- 검수 완료(`COMPLETE`) 상태의 데이터가 없음
- `inspect_stat_dttm`이 오늘 이후 (어제까지만 조회)
**해결 방법:**
1. 검수 완료 데이터 확인
```sql
SELECT COUNT(*)
FROM tb_labeling_assignment
WHERE anal_uid = 100
AND inspect_state = 'COMPLETE';
```
2. 검수 완료 시간 확인
```sql
SELECT MAX(inspect_stat_dttm)
FROM tb_labeling_assignment
WHERE anal_uid = 100
AND inspect_state = 'COMPLETE';
```
---
### 3. ZIP 파일 생성 실패
**증상:**
```log
[ERROR] Response 디렉토리가 존재하지 않음: /dataset/response/ED80D700...
```
**원인:**
- Docker Step에서 결과물이 생성되지 않음
- Docker 컨테이너가 중간에 실패했지만 감지되지 않음
**해결 방법:**
1. Docker Step 상태 확인
```sql
SELECT * FROM batch_step_history
WHERE result_uid = 'ED80D700...'
AND step_name = 'dockerRunStep';
```
2. Response 디렉토리 확인
```bash
ls -la /kamco-nfs/dataset/response/ED80D700.../
```
3. Docker 컨테이너 로그 확인 (DB의 `error_message` 컬럼)
---
### 4. Child Job이 실행되지 않음
**증상:**
```log
[INFO] 모든 파일이 이미 처리됨. 건너뜀.
```
**원인:**
- `all_cnt == file_cnt` (이미 모든 파일이 생성됨)
- 재실행이 필요한 경우 플래그를 초기화하지 않음
**해결 방법:**
1. 파일 생성 플래그 초기화
```sql
UPDATE tb_map_sheet_learn_data_geom
SET file_create_yn = false,
updated_dttm = NOW()
WHERE geo_uid IN (
SELECT inference_geom_uid
FROM tb_labeling_assignment
WHERE anal_uid = 100
);
```
2. 배치 재실행
---
### 5. 배치 작업 전체가 실패함
**증상:**
```log
[ERROR] Child Job 실행 실패 (AnalUid=100, ResultUid=ED80D700...): ...
[WARN] 3 개의 Child Job 실행이 실패했습니다.
```
**원인:**
- 여러 회차에서 동시에 실패 발생
- 현재는 부분 실패를 허용하도록 설정됨
**해결 방법:**
1. 실패한 회차 확인
```sql
SELECT DISTINCT anal_uid, result_uid
FROM batch_step_history
WHERE status = 'FAILED'
AND started_dttm >= CURRENT_DATE;
```
2. 각 회차별로 실패 원인 분석
```sql
SELECT step_name, error_message
FROM batch_step_history
WHERE anal_uid = 100
AND status = 'FAILED';
```
3. 실패 정책 변경 (필요 시)
- `LaunchChildJobsTasklet.java:87-89` 주석 해제하여 하나라도 실패 시 Parent Job 실패 처리
---
## 개발자 가이드
### 새로운 Step 추가하기
1. **Tasklet 생성**
```java
@Component
@RequiredArgsConstructor
public class NewStepTasklet implements Tasklet {
@Value("#{jobParameters['analUid']}")
private Long analUid;
@Value("#{jobParameters['resultUid']}")
private String resultUid;
@Override
public RepeatStatus execute(StepContribution contribution, ChunkContext chunkContext) {
// 로직 구현
return RepeatStatus.FINISHED;
}
}
```
2. **JobConfig에 Step 등록**
```java
@Bean
public Step newStep() {
return new StepBuilder("newStep", jobRepository)
.tasklet(newStepTasklet, transactionManager)
.listener(stepHistoryListener) // 이력 기록
.build();
}
```
3. **Job Flow에 추가**
```java
@Bean
public Job processAnalCntInfoJob() {
return new JobBuilder("processAnalCntInfoJob", jobRepository)
.start(makeGeoJsonStep())
.next(dockerRunStep())
.next(zipResponseStep())
.next(newStep()) // 새로운 Step 추가
.build();
}
```
---
### 로그 커스터마이징
**로그 레벨 변경:**
```yaml
logging:
level:
com.kamco.cd.geojsonscheduler.batch.DockerRunTasklet: DEBUG
```
**특정 Step만 로그 출력:**
```java
@Slf4j
public class CustomTasklet implements Tasklet {
@Override
public RepeatStatus execute(...) {
if (log.isDebugEnabled()) {
log.debug("상세 디버그 정보: {}", details);
}
return RepeatStatus.FINISHED;
}
}
```
---
## 배포 및 운영
### 배포 절차
1. **빌드**
```bash
./gradlew clean build
```
2. **Docker 이미지 빌드**
```bash
docker build -t kamco-batch:latest .
```
3. **실행**
```bash
java -jar build/libs/kamco-geojson-scheduler-1.0.0.jar
```
---
### 스케줄링 설정
Spring Scheduler를 사용한 정기 실행:
```java
@Scheduled(cron = "0 0 2 * * *") // 매일 새벽 2시
public void runBatch() {
JobParameters jobParameters = new JobParametersBuilder()
.addLong("timestamp", System.currentTimeMillis())
.toJobParameters();
jobLauncher.run(exportGeoJsonJob, jobParameters);
}
```
---
## 라이선스
Copyright (c) 2024 KAMCO. All rights reserved.
---
## 문의
기술 지원: tech-support@kamco.co.kr

View File

@@ -1,13 +1,12 @@
package com.kamco.cd.geojsonscheduler; package com.kamco.cd.geojsonscheduler;
import com.kamco.cd.geojsonscheduler.config.DockerProperties; import com.kamco.cd.geojsonscheduler.config.DockerProperties;
import com.kamco.cd.geojsonscheduler.config.TrainDockerProperties;
import org.springframework.boot.SpringApplication; import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.context.properties.EnableConfigurationProperties; import org.springframework.boot.context.properties.EnableConfigurationProperties;
@SpringBootApplication @SpringBootApplication
@EnableConfigurationProperties({DockerProperties.class, TrainDockerProperties.class}) @EnableConfigurationProperties(DockerProperties.class)
public class GeoJsonSchedulerApplication { public class GeoJsonSchedulerApplication {
public static void main(String[] args) { public static void main(String[] args) {

View File

@@ -1,110 +0,0 @@
package com.kamco.cd.geojsonscheduler.batch;
import com.kamco.cd.geojsonscheduler.service.DockerRunnerService;
import lombok.RequiredArgsConstructor;
import lombok.extern.log4j.Log4j2;
import org.springframework.batch.core.StepContribution;
import org.springframework.batch.core.configuration.annotation.StepScope;
import org.springframework.batch.core.scope.context.ChunkContext;
import org.springframework.batch.core.step.tasklet.Tasklet;
import org.springframework.batch.repeat.RepeatStatus;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
/**
* Docker 컨테이너 실행 Tasklet
*
* <p>학습 데이터 생성 파이프라인이 담긴 Docker 컨테이너를 실행합니다. 이전 Step(makeGeoJsonStep)에서 생성된 GeoJSON
* 파일들을 입력으로 받아 학습 데이터셋을 생성합니다.
*
* <p><b>주요 기능:</b>
*
* <ul>
* <li>Docker 컨테이너 실행 (kamco_full_pipeline.py)
* <li>입력: /dataset/request/{resultUid}/*.geojson
* <li>출력: /dataset/response/{resultUid}/*
* <li>Docker 실행 실패 시 RuntimeException 발생 (Step 실패 처리)
* </ul>
*
* <p><b>실행 조건:</b>
*
* <ul>
* <li>makeGeoJsonStep이 성공적으로 완료되어야 함
* <li>request/{resultUid}/ 디렉토리에 GeoJSON 파일이 존재해야 함
* </ul>
*
* <p><b>Docker Exit Code 처리:</b>
*
* <ul>
* <li>Exit Code = 0: 정상 종료 (Step 성공)
* <li>Exit Code != 0: 비정상 종료 (RuntimeException 발생 → Step 실패)
* </ul>
*
* @author KAMCO Development Team
* @since 1.0.0
* @see DockerRunnerService
*/
@Log4j2
@Component
@StepScope
@RequiredArgsConstructor
public class DockerRunTasklet implements Tasklet {
/** Docker 컨테이너 실행을 담당하는 서비스 */
private final DockerRunnerService dockerRunnerService;
/** Job Parameter로 전달받은 결과물 고유 ID (UUID) */
@Value("#{jobParameters['resultUid']}")
private String resultUid;
/**
* Docker 컨테이너 실행 작업 수행
*
* <p>DockerRunnerService를 통해 학습 데이터 생성 파이프라인을 실행합니다. Docker 프로세스가 비정상 종료될 경우
* RuntimeException이 발생하여 Step이 실패 처리됩니다.
*
* @param contribution Step 실행 정보를 담는 객체
* @param chunkContext Chunk 실행 컨텍스트
* @return RepeatStatus.FINISHED - 작업 완료
* @throws RuntimeException Docker 프로세스가 비정상 종료(exitCode != 0)된 경우
*/
@Override
public RepeatStatus execute(StepContribution contribution, ChunkContext chunkContext) {
log.info("========================================");
log.info("Docker 컨테이너 실행 시작 (ResultUid={})", resultUid);
log.info("========================================");
// 실행 전 사전 정보 로깅
log.info("[사전 정보]");
log.info(" - 입력 디렉토리: /dataset/request/{}/", resultUid);
log.info(" - 출력 디렉토리: /dataset/response/{}/", resultUid);
log.info(" - 파이프라인: kamco_full_pipeline.py");
try {
// DockerRunnerService를 통해 Docker 컨테이너 실행
// exitCode != 0 시 RuntimeException 발생
log.info("[Docker 실행] 컨테이너 시작...");
dockerRunnerService.run(resultUid);
log.info("[Docker 실행] ✓ 컨테이너 정상 종료 (exitCode=0)");
} catch (RuntimeException e) {
// Docker 실행 실패 시 (DockerRunnerService에서 던진 예외)
log.error("[Docker 실행] ✗ 컨테이너 실행 실패!", e);
log.error(" - ResultUid: {}", resultUid);
log.error(" - 에러 메시지: {}", e.getMessage());
log.error(" - 확인사항:");
log.error(" 1. request/{}/ 디렉토리에 GeoJSON 파일이 있는지 확인", resultUid);
log.error(" 2. Docker 이미지가 정상적으로 존재하는지 확인");
log.error(" 3. Docker 볼륨 마운트 경로 확인");
log.error(" 4. kamco_full_pipeline.py 스크립트 로그 확인");
throw e; // 예외 재발생 → Step 실패 처리
}
log.info("========================================");
log.info("Docker 컨테이너 실행 완료 (ResultUid={})", resultUid);
log.info(" - 결과물 저장 위치: /dataset/response/{}/", resultUid);
log.info("========================================");
return RepeatStatus.FINISHED;
}
}

View File

@@ -4,6 +4,7 @@ import com.kamco.cd.geojsonscheduler.listener.BatchHistoryListener;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import org.springframework.batch.core.Job; import org.springframework.batch.core.Job;
import org.springframework.batch.core.Step; import org.springframework.batch.core.Step;
import org.springframework.batch.core.StepExecutionListener;
import org.springframework.batch.core.job.builder.JobBuilder; import org.springframework.batch.core.job.builder.JobBuilder;
import org.springframework.batch.core.repository.JobRepository; import org.springframework.batch.core.repository.JobRepository;
import org.springframework.batch.core.step.builder.StepBuilder; import org.springframework.batch.core.step.builder.StepBuilder;
@@ -17,20 +18,20 @@ public class ExportGeoJsonJobConfig {
private final JobRepository jobRepository; private final JobRepository jobRepository;
private final PlatformTransactionManager transactionManager; private final PlatformTransactionManager transactionManager;
private final LaunchChildJobsTasklet launchChildJobsTasklet; private final ExportGeoJsonTasklet exportGeoJsonTasklet;
@Bean @Bean
public Job exportGeoJsonJob(BatchHistoryListener historyListener) { public Job exportGeoJsonJob(BatchHistoryListener historyListener) { // 1. 리스너 주입 받기
return new JobBuilder("exportGeoJsonJob", jobRepository) return new JobBuilder("exportGeoJsonJob", jobRepository)
.listener(historyListener) .listener(historyListener) // 2. 리스너 등록
.start(launchChildJobsStep()) .start(exportGeoJsonStep())
.build(); .build();
} }
@Bean @Bean
public Step launchChildJobsStep() { public Step exportGeoJsonStep() {
return new StepBuilder("launchChildJobsStep", jobRepository) return new StepBuilder("exportGeoJsonStep", jobRepository)
.tasklet(launchChildJobsTasklet, transactionManager) .tasklet(exportGeoJsonTasklet, transactionManager)
.build(); .build();
} }
} }

View File

@@ -1,276 +0,0 @@
package com.kamco.cd.geojsonscheduler.batch;
import com.kamco.cd.geojsonscheduler.dto.TrainingDataReviewJobDto.AnalCntInfo;
import com.kamco.cd.geojsonscheduler.repository.TrainingDataReviewJobRepository;
import java.util.List;
import java.util.Objects;
import lombok.RequiredArgsConstructor;
import lombok.extern.log4j.Log4j2;
import org.springframework.batch.core.BatchStatus;
import org.springframework.batch.core.Job;
import org.springframework.batch.core.JobExecution;
import org.springframework.batch.core.JobParameters;
import org.springframework.batch.core.JobParametersBuilder;
import org.springframework.batch.core.StepContribution;
import org.springframework.batch.core.launch.JobLauncher;
import org.springframework.batch.core.scope.context.ChunkContext;
import org.springframework.batch.core.step.tasklet.Tasklet;
import org.springframework.batch.repeat.RepeatStatus;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.stereotype.Component;
import org.springframework.transaction.PlatformTransactionManager;
import org.springframework.transaction.TransactionDefinition;
import org.springframework.transaction.support.DefaultTransactionDefinition;
import org.springframework.transaction.support.TransactionTemplate;
/**
* Child Job 실행 Tasklet (Parent Job용)
*
* <p>
* 진행 중인 모든 분석 회차(AnalCntInfo)를 조회하여 각 회차마다 독립적인 Child Job을 실행합니다. 각 Child Job은
* 3개의
* Step(makeGeoJson → dockerRun → zipResponse)을 순차적으로 실행합니다.
*
* <p>
* <b>주요 기능:</b>
*
* <ul>
* <li>진행 중인 분석 회차 목록 조회 (tb_map_sheet_anal_inference, anal_state='ING')
* <li>각 회차별 처리 필요 여부 판단 (all_cnt != file_cnt)
* <li>회차마다 Child Job(processAnalCntInfoJob) 실행
* <li>부분 실패 허용 (한 회차 실패해도 다른 회차 계속 처리)
* </ul>
*
* <p>
* <b>실행 조건:</b>
*
* <ul>
* <li>tb_map_sheet_anal_inference.anal_state = 'ING' (진행 중)
* <li>검수 완료 건수(complete_cnt) > 0
* <li>all_cnt != file_cnt (아직 파일 생성이 완료되지 않음)
* </ul>
*
* <p>
* <b>실패 정책:</b>
*
* <ul>
* <li>현재: 부분 실패 허용 (일부 Child Job 실패해도 Parent Job 성공)
* <li>변경 가능: 87-89라인 주석 해제 시 하나라도 실패하면 Parent Job 실패
* </ul>
*
* @author KAMCO Development Team
* @since 1.0.0
* @see com.kamco.cd.geojsonscheduler.batch.ProcessAnalCntInfoJobConfig
*/
@Log4j2
@Component
@RequiredArgsConstructor
public class LaunchChildJobsTasklet implements Tasklet {
/** 분석 회차 정보 조회를 위한 Repository */
private final TrainingDataReviewJobRepository repository;
/** Child Job을 실행하기 위한 비동기 JobLauncher (트랜잭션 충돌 방지) */
@Qualifier("asyncJobLauncher")
private final JobLauncher asyncJobLauncher;
/** 실행할 Child Job (processAnalCntInfoJob) */
@Qualifier("processAnalCntInfoJob")
private final Job processAnalCntInfoJob;
/** 트랜잭션 매니저 (트랜잭션 제어용) */
private final PlatformTransactionManager transactionManager;
/**
* Parent Job의 메인 로직 실행
*
* <p>
* 진행 중인 모든 분석 회차를 조회하여 각 회차마다 Child Job을 실행합니다. 한 회차가 실패해도 다른 회차는 계속 처리되며,
* 최종적으로 통계를
* 로깅합니다.
*
* @param contribution Step 실행 정보를 담는 객체
* @param chunkContext Chunk 실행 컨텍스트
* @return RepeatStatus.FINISHED - 작업 완료
*/
@Override
public RepeatStatus execute(StepContribution contribution, ChunkContext chunkContext) {
log.info("========================================");
log.info("Parent Job 시작: AnalCntInfo 리스트 조회 및 Child Job 실행");
log.info("========================================");
// Step 1: 진행 중인 분석 회차 목록 조회
log.info("[Step 1/3] 진행 중인 분석 회차 목록 조회 중...");
log.info(" - 조회 조건: anal_state='ING' AND complete_cnt > 0");
List<AnalCntInfo> analList = repository.findAnalCntInfoList();
log.info("[Step 1/3] 조회 완료");
log.info(" - 진행 중인 회차 수: {} 개", analList.size());
if (analList.isEmpty()) {
log.warn("[경고] 처리할 분석 회차가 없습니다.");
log.warn(" - 확인사항:");
log.warn(" 1. tb_map_sheet_anal_inference 테이블의 anal_state='ING' 데이터 확인");
log.warn(" 2. 검수 완료(COMPLETE) 건수가 있는지 확인");
return RepeatStatus.FINISHED;
}
// Step 2: 각 회차별 Child Job 실행
log.info("[Step 2/3] 회차별 Child Job 실행 시작");
// 실행 통계를 위한 카운터
int processedCount = 0; // 성공적으로 처리된 회차 수
int skippedCount = 0; // 건너뛴 회차 수 (이미 처리 완료)
int failedCount = 0; // 실패한 회차 수
// 각 분석 회차별로 Child Job 실행
for (int i = 0; i < analList.size(); i++) {
AnalCntInfo info = analList.get(i);
log.info("========================================");
log.info("[회차 {}/{}] AnalCntInfo 처리 시작", i + 1, analList.size());
log.info(" - AnalUid: {}", info.getAnalUid());
log.info(" - ResultUid: {}", info.getResultUid());
log.info(" - 전체 건수(all_cnt): {}", info.getAllCnt());
log.info(" - 검수 완료 건수(complete_cnt): {}", info.getCompleteCnt());
log.info(" - 파일 생성 완료 건수(file_cnt): {}", info.getFileCnt());
// 처리 필요 여부 판단: all_cnt == file_cnt면 이미 모든 파일이 생성됨
if (Objects.equals(info.getAllCnt(), info.getFileCnt())) {
log.info("[건너뜀] 모든 파일이 이미 처리 완료됨 (all_cnt={}, file_cnt={})",
info.getAllCnt(), info.getFileCnt());
log.info(" - 재처리가 필요한 경우 file_create_yn 플래그를 초기화하세요.");
skippedCount++;
continue;
}
try {
// Child Job Parameters 생성
JobParameters jobParameters = new JobParametersBuilder()
.addLong("analUid", info.getAnalUid())
.addString("resultUid", info.getResultUid())
.addLong("timestamp", System.currentTimeMillis()) // JobInstance 고유성 보장
.toJobParameters();
log.info("[Child Job 실행] processAnalCntInfoJob 시작...");
log.info(" - JobParameters: analUid={}, resultUid={}", info.getAnalUid(),
info.getResultUid());
// Child Job 실행 (비동기 방식 - 트랜잭션 충돌 방지)
// asyncJobLauncher를 사용하여 별도 쓰레드에서 실행
// 내부적으로 makeGeoJsonStep → dockerRunStep → zipResponseStep 순차 실행
long startTime = System.currentTimeMillis();
// 트랜잭션 일시 정지 후 실행 (Existing transaction detected 에러 방지)
JobExecution jobExecution = new TransactionTemplate(transactionManager,
new DefaultTransactionDefinition(TransactionDefinition.PROPAGATION_NOT_SUPPORTED))
.execute(status -> {
try {
return asyncJobLauncher.run(processAnalCntInfoJob, jobParameters);
} catch (Exception e) {
throw new RuntimeException(e);
}
});
if (jobExecution == null) {
throw new RuntimeException("JobExecution is null after launching child job");
}
// Child Job 완료 대기 (비동기 실행이므로 완료를 폴링)
log.info("[Child Job 대기] 실행 완료 대기 중... (JobExecutionId={})",
jobExecution.getId());
while (jobExecution.isRunning()) {
try {
Thread.sleep(1000); // 1초마다 상태 확인
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
throw new RuntimeException("Child Job 대기 중 인터럽트 발생", e);
}
}
long duration = System.currentTimeMillis() - startTime;
BatchStatus status = jobExecution.getStatus();
if (status == BatchStatus.COMPLETED) {
log.info("[Child Job 완료] ✓ 정상 종료");
log.info(" - AnalUid: {}", info.getAnalUid());
log.info(" - ResultUid: {}", info.getResultUid());
log.info(" - 실행 시간: {} ms ({} 초)", duration, duration / 1000);
log.info(" - 최종 상태: {}", status);
processedCount++;
} else {
// Child Job 실패
log.error("[Child Job 실패] ✗ 비정상 종료");
log.error(" - AnalUid: {}", info.getAnalUid());
log.error(" - ResultUid: {}", info.getResultUid());
log.error(" - 실행 시간: {} ms ({} 초)", duration, duration / 1000);
log.error(" - 최종 상태: {}", status);
log.error(" - Exit 상태: {}", jobExecution.getExitStatus());
// 실패 예외 정보 로깅
if (!jobExecution.getAllFailureExceptions().isEmpty()) {
log.error(" - 실패 예외:");
for (Throwable t : jobExecution.getAllFailureExceptions()) {
log.error(" * {}: {}", t.getClass().getSimpleName(), t.getMessage());
}
}
failedCount++;
// 실패해도 다음 회차 계속 처리
log.info("[계속 진행] 다음 회차 처리를 계속합니다.");
continue; // 다음 for 루프로
}
} catch (Exception e) {
// Child Job 실행 실패 시 (Step 실패 또는 예외 발생)
log.error("[Child Job 실패] ✗ 실행 중 오류 발생", e);
log.error(" - AnalUid: {}", info.getAnalUid());
log.error(" - ResultUid: {}", info.getResultUid());
log.error(" - 에러 메시지: {}", e.getMessage());
log.error(" - 확인사항:");
log.error(" 1. batch_step_history 테이블에서 실패한 Step 확인");
log.error(" 2. error_message 컬럼에서 상세 에러 내용 확인");
failedCount++;
// 한 회차 실패해도 다음 회차 계속 처리 (부분 실패 허용)
log.info("[계속 진행] 다음 회차 처리를 계속합니다.");
}
}
// Step 3: 최종 통계 및 결과 로깅
log.info("========================================");
log.info("[Step 3/3] Parent Job 실행 결과 요약");
log.info(" - 총 회차 수: {} 개", analList.size());
log.info(" - 성공: {} 개", processedCount);
log.info(" - 건너뜀: {} 개", skippedCount);
log.info(" - 실패: {} 개", failedCount);
// 성공률 계산
if (analList.size() > 0) {
double successRate = (double) processedCount / (analList.size() - skippedCount) * 100;
log.info(" - 성공률: {}% (건너뛴 회차 제외)", String.format("%.2f", successRate));
}
log.info("========================================");
// 실패 정책 처리
if (failedCount > 0) {
log.warn("[경고] {} 개의 Child Job 실행이 실패했습니다.", failedCount);
log.warn(" - 실패 상세 내용은 batch_step_history 테이블을 확인하세요.");
log.warn(" - SQL: SELECT * FROM batch_step_history WHERE status='FAILED' ORDER BY"
+ " started_dttm DESC;");
// 실패가 있어도 Parent Job은 성공으로 처리 (부분 성공 정책)
// 만약 하나라도 실패하면 Parent Job도 실패로 처리하려면 아래 주석 해제
// throw new RuntimeException(
// String.format("%d 개의 Child Job 실행이 실패했습니다. (성공: %d, 실패: %d)",
// failedCount, processedCount, failedCount));
} else {
log.info("[완료] 모든 Child Job이 정상적으로 완료되었습니다.");
}
return RepeatStatus.FINISHED;
}
}

View File

@@ -1,207 +0,0 @@
package com.kamco.cd.geojsonscheduler.batch;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.SerializationFeature;
import com.kamco.cd.geojsonscheduler.dto.TrainingDataReviewJobDto.AnalMapSheetList;
import com.kamco.cd.geojsonscheduler.dto.TrainingDataReviewJobDto.CompleteLabelData;
import com.kamco.cd.geojsonscheduler.dto.TrainingDataReviewJobDto.CompleteLabelData.GeoJsonFeature;
import com.kamco.cd.geojsonscheduler.dto.TrainingDataReviewJobDto.FeatureCollection;
import com.kamco.cd.geojsonscheduler.repository.TrainingDataReviewJobRepository;
import java.io.File;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.List;
import lombok.RequiredArgsConstructor;
import lombok.extern.log4j.Log4j2;
import org.springframework.batch.core.StepContribution;
import org.springframework.batch.core.configuration.annotation.StepScope;
import org.springframework.batch.core.scope.context.ChunkContext;
import org.springframework.batch.core.step.tasklet.Tasklet;
import org.springframework.batch.repeat.RepeatStatus;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
/**
* GeoJSON 파일 생성 Tasklet
*
* <p>검수 완료된 라벨링 데이터를 GeoJSON 형식으로 변환하여 파일로 저장합니다. 각 도엽(Map Sheet)별로 별도의 GeoJSON 파일을
* 생성하며, 파일 생성 완료 후 DB에 플래그를 업데이트합니다.
*
* <p><b>주요 기능:</b>
*
* <ul>
* <li>검수 완료된 도엽 목록 조회
* <li>도엽별 라벨링 데이터 조회 및 GeoJSON Feature 변환
* <li>GeoJSON 파일 생성 (/dataset/request/{resultUid}/*.geojson)
* <li>DB에 파일 생성 완료 플래그 업데이트
* </ul>
*
* <p><b>파일 명명 규칙:</b> {resultUid_8자}_{compareYyyy}_{targetYyyy}_{mapSheetNum}_D15.geojson
*
* <p><b>예시:</b> ED80D700_2022_2023_3724036_D15.geojson
*
* @author KAMCO Development Team
* @since 1.0.0
*/
@Log4j2
@Component
@StepScope
@RequiredArgsConstructor
public class MakeGeoJsonTasklet implements Tasklet {
/** 라벨링 데이터 조회를 위한 Repository */
private final TrainingDataReviewJobRepository repository;
/** GeoJSON 파일이 저장될 베이스 디렉토리 경로 (예: /kamco-nfs/dataset) */
@Value("${training-data.geojson-dir}")
private String trainingDataDir;
/** Job Parameter로 전달받은 분석 회차 UID */
@Value("#{jobParameters['analUid']}")
private Long analUid;
/** Job Parameter로 전달받은 결과물 고유 ID (UUID) */
@Value("#{jobParameters['resultUid']}")
private String resultUid;
/**
* GeoJSON 파일 생성 작업 실행
*
* <p>검수 완료된 라벨링 데이터를 조회하여 도엽별로 GeoJSON 파일을 생성합니다.
*
* @param contribution Step 실행 정보를 담는 객체
* @param chunkContext Chunk 실행 컨텍스트
* @return RepeatStatus.FINISHED - 작업 완료
* @throws RuntimeException 생성된 GeoJSON 파일이 없는 경우 또는 파일 생성 실패 시
*/
@Override
public RepeatStatus execute(StepContribution contribution, ChunkContext chunkContext) {
log.info("========================================");
log.info("GeoJSON 생성 시작 (AnalUid={}, ResultUid={})", analUid, resultUid);
log.info("========================================");
// Step 1: 검수 완료된 도엽 목록 조회
log.info("[Step 1/4] 검수 완료된 도엽 목록 조회 중... (AnalUid={})", analUid);
List<AnalMapSheetList> analMapList = repository.findCompletedAnalMapSheetList(analUid);
log.info("[Step 1/4] 검수 완료된 도엽 수: {}", analMapList.size());
// 검수 완료된 도엽이 없으면 작업 종료
if (analMapList.isEmpty()) {
log.warn("[경고] 검수 완료된 도엽이 없음. 작업을 건너뜁니다.");
log.warn(" - 확인사항: tb_labeling_assignment 테이블의 inspect_state='COMPLETE' 데이터가 있는지 확인");
return RepeatStatus.FINISHED;
}
// GeoJSON 파일 생성 통계를 위한 카운터
int processedMapSheetCount = 0; // 처리된 도엽 수
int totalGeoJsonFiles = 0; // 생성된 GeoJSON 파일 수
// Step 2: 각 도엽별로 GeoJSON 파일 생성
log.info("[Step 2/4] 도엽별 GeoJSON 파일 생성 시작 (총 {} 개 도엽)", analMapList.size());
for (AnalMapSheetList mapSheet : analMapList) {
log.info("----------------------------------------");
log.info(" [도엽 처리] MapSheetNum={}", mapSheet.getMapSheetNum());
// Step 2-1: 도엽별 검수 완료된 라벨링 데이터 조회
log.info(" [2-1] 라벨링 데이터 조회 중...");
List<CompleteLabelData> completeList =
repository.findCompletedYesterdayLabelingList(analUid, mapSheet.getMapSheetNum());
log.info(" [2-1] 조회 완료: {} 건", completeList.size());
// 라벨링 데이터가 없으면 다음 도엽으로
if (completeList.isEmpty()) {
log.info(" [건너뜀] 라벨링 데이터가 없어 GeoJSON 파일 생성을 건너뜁니다.");
continue;
}
// Step 2-2: GeoUID 목록 추출 (DB 업데이트용)
log.info(" [2-2] GeoUID 목록 추출 중...");
List<Long> geoUids = completeList.stream().map(CompleteLabelData::getGeoUid).toList();
log.info(" [2-2] GeoUID 목록 생성 완료: {} 건", geoUids.size());
// Step 2-3: GeoJSON Feature 객체로 변환
log.info(" [2-3] GeoJSON Feature 변환 중...");
List<GeoJsonFeature> features = completeList.stream().map(GeoJsonFeature::from).toList();
log.info(" [2-3] GeoJSON Feature 변환 완료: {} 개", features.size());
// Step 2-4: FeatureCollection 생성 및 파일명 결정
log.info(" [2-4] FeatureCollection 생성 중...");
FeatureCollection collection = new FeatureCollection(features);
String filename = mapSheet.buildFilename(resultUid);
log.info(" [2-4] GeoJSON 파일명: {}", filename);
// Step 2-5: 파일 저장 경로 생성
// 형식: /kamco-nfs/dataset/request/{resultUid}/{filename}.geojson
Path outputPath =
Paths.get(
trainingDataDir + File.separator + "request" + File.separator + resultUid, filename);
log.info(" [2-5] 출력 경로: {}", outputPath);
try {
// Step 2-6: 디렉토리 생성 (존재하지 않으면)
log.info(" [2-6] 디렉토리 생성 중... {}", outputPath.getParent());
Files.createDirectories(outputPath.getParent());
log.info(" [2-6] 디렉토리 생성 완료: {}", outputPath.getParent());
// Step 2-7: GeoJSON 파일 저장 (Pretty Print 포맷)
log.info(" [2-7] GeoJSON 파일 저장 중...");
ObjectMapper objectMapper = new ObjectMapper();
objectMapper.enable(SerializationFeature.INDENT_OUTPUT); // JSON 들여쓰기 적용
objectMapper.writeValue(outputPath.toFile(), collection);
log.info(" [2-7] ✓ GeoJSON 파일 저장 완료: {}", outputPath);
// Step 2-8: DB에 파일 생성 완료 플래그 업데이트
log.info(" [2-8] DB 파일 생성 플래그 업데이트 중...");
repository.updateLearnDataGeomFileCreateYn(geoUids);
log.info(" [2-8] ✓ DB 업데이트 완료: {} 건", geoUids.size());
// 통계 카운터 증가
processedMapSheetCount++;
totalGeoJsonFiles++;
log.info(" [완료] 도엽 '{}' 처리 성공", mapSheet.getMapSheetNum());
} catch (IOException e) {
// 파일 생성 실패 시 예외 발생 (Step 실패 처리)
log.error(" [실패] GeoJSON 파일 생성 실패", e);
log.error(" - 파일명: {}", filename);
log.error(" - 경로: {}", outputPath);
log.error(" - 에러 메시지: {}", e.getMessage());
throw new RuntimeException("GeoJSON 파일 생성 실패: " + filename, e);
}
}
// Step 3: 처리 결과 요약 및 검증
log.info("========================================");
log.info("[Step 3/4] GeoJSON 생성 작업 완료");
log.info(" - ResultUid: {}", resultUid);
log.info(" - AnalUid: {}", analUid);
log.info(" - 처리 대상 도엽 수: {}", analMapList.size());
log.info(" - 처리 완료 도엽 수: {}", processedMapSheetCount);
log.info(" - 생성된 GeoJSON 파일 수: {}", totalGeoJsonFiles);
log.info("========================================");
// Step 4: 필수 검증 - 최소 1개 이상의 파일이 생성되어야 함
log.info("[Step 4/4] 필수 검증 수행 중...");
if (totalGeoJsonFiles == 0) {
log.error("[실패] 생성된 GeoJSON 파일이 없습니다!");
log.error(" - AnalUid: {}", analUid);
log.error(" - ResultUid: {}", resultUid);
log.error(" - 조회된 도엽 수: {}", analMapList.size());
log.error(" - 확인사항:");
log.error(" 1. 각 도엽에 라벨링 데이터가 있는지 확인");
log.error(" 2. findCompletedYesterdayLabelingList() 쿼리 결과 확인");
throw new RuntimeException(
String.format(
"생성된 GeoJSON 파일이 없습니다. (AnalUid=%d, ResultUid=%s)", analUid, resultUid));
}
log.info("[Step 4/4] ✓ 검증 완료: {} 개의 GeoJSON 파일이 정상적으로 생성되었습니다.", totalGeoJsonFiles);
log.info("GeoJSON 파일 저장 위치: {}/request/{}/", trainingDataDir, resultUid);
return RepeatStatus.FINISHED;
}
}

View File

@@ -1,57 +0,0 @@
package com.kamco.cd.geojsonscheduler.batch;
import com.kamco.cd.geojsonscheduler.listener.StepHistoryListener;
import lombok.RequiredArgsConstructor;
import org.springframework.batch.core.Job;
import org.springframework.batch.core.Step;
import org.springframework.batch.core.job.builder.JobBuilder;
import org.springframework.batch.core.repository.JobRepository;
import org.springframework.batch.core.step.builder.StepBuilder;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.transaction.PlatformTransactionManager;
@Configuration
@RequiredArgsConstructor
public class ProcessAnalCntInfoJobConfig {
private final JobRepository jobRepository;
private final PlatformTransactionManager transactionManager;
private final MakeGeoJsonTasklet makeGeoJsonTasklet;
private final DockerRunTasklet dockerRunTasklet;
private final ZipResponseTasklet zipResponseTasklet;
private final StepHistoryListener stepHistoryListener;
@Bean
public Job processAnalCntInfoJob() {
return new JobBuilder("processAnalCntInfoJob", jobRepository)
.start(makeGeoJsonStep())
.next(dockerRunStep())
.next(zipResponseStep())
.build();
}
@Bean
public Step makeGeoJsonStep() {
return new StepBuilder("makeGeoJsonStep", jobRepository)
.tasklet(makeGeoJsonTasklet, transactionManager)
.listener(stepHistoryListener)
.build();
}
@Bean
public Step dockerRunStep() {
return new StepBuilder("dockerRunStep", jobRepository)
.tasklet(dockerRunTasklet, transactionManager)
.listener(stepHistoryListener)
.build();
}
@Bean
public Step zipResponseStep() {
return new StepBuilder("zipResponseStep", jobRepository)
.tasklet(zipResponseTasklet, transactionManager)
.listener(stepHistoryListener)
.build();
}
}

View File

@@ -1,36 +0,0 @@
package com.kamco.cd.geojsonscheduler.batch;
import com.kamco.cd.geojsonscheduler.listener.BatchHistoryListener;
import lombok.RequiredArgsConstructor;
import org.springframework.batch.core.Job;
import org.springframework.batch.core.Step;
import org.springframework.batch.core.job.builder.JobBuilder;
import org.springframework.batch.core.repository.JobRepository;
import org.springframework.batch.core.step.builder.StepBuilder;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.transaction.PlatformTransactionManager;
@Configuration
@RequiredArgsConstructor
public class TrainModelJobConfig {
private final JobRepository jobRepository;
private final PlatformTransactionManager transactionManager;
private final TrainModelTasklet trainModelTasklet;
@Bean
public Job trainModelJob(BatchHistoryListener historyListener) {
return new JobBuilder("trainModelJob", jobRepository)
.listener(historyListener)
.start(trainModelStep())
.build();
}
@Bean
public Step trainModelStep() {
return new StepBuilder("trainModelStep", jobRepository)
.tasklet(trainModelTasklet, transactionManager)
.build();
}
}

View File

@@ -1,60 +0,0 @@
package com.kamco.cd.geojsonscheduler.batch;
import com.kamco.cd.geojsonscheduler.service.TrainDockerRunnerService;
import lombok.RequiredArgsConstructor;
import lombok.extern.log4j.Log4j2;
import org.springframework.batch.core.StepContribution;
import org.springframework.batch.core.scope.context.ChunkContext;
import org.springframework.batch.core.step.tasklet.Tasklet;
import org.springframework.batch.repeat.RepeatStatus;
import org.springframework.stereotype.Component;
@Log4j2
@Component
@RequiredArgsConstructor
public class TrainModelTasklet implements Tasklet {
private final TrainDockerRunnerService trainDockerRunnerService;
@Override
public RepeatStatus execute(StepContribution contribution, ChunkContext chunkContext) {
log.info("========================================");
log.info("학습 배치 작업 시작");
log.info("========================================");
String jobName = chunkContext.getStepContext().getJobName();
log.info("Job Name: {}", jobName);
// Job 파라미터에서 dataset-folder와 output-folder 가져오기
String datasetFolder = (String) chunkContext.getStepContext()
.getJobParameters()
.get("dataset-folder");
String outputFolder = (String) chunkContext.getStepContext()
.getJobParameters()
.get("output-folder");
log.info("Dataset Folder Parameter: {}", datasetFolder);
log.info("Output Folder Parameter: {}", outputFolder);
if (datasetFolder == null || datasetFolder.isBlank()) {
log.error("dataset-folder 파라미터가 없습니다!");
throw new IllegalArgumentException("dataset-folder parameter is required");
}
if (outputFolder == null || outputFolder.isBlank()) {
log.error("output-folder 파라미터가 없습니다!");
throw new IllegalArgumentException("output-folder parameter is required");
}
// Train Docker 실행
log.info("Train Docker 실행 중...");
trainDockerRunnerService.runTraining(datasetFolder, outputFolder);
log.info("Train Docker 실행 완료");
log.info("========================================");
log.info("학습 배치 작업 완료");
log.info("========================================");
return RepeatStatus.FINISHED;
}
}

View File

@@ -1,266 +0,0 @@
package com.kamco.cd.geojsonscheduler.batch;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.zip.ZipEntry;
import java.util.zip.ZipOutputStream;
import lombok.RequiredArgsConstructor;
import lombok.extern.log4j.Log4j2;
import org.springframework.batch.core.StepContribution;
import org.springframework.batch.core.configuration.annotation.StepScope;
import org.springframework.batch.core.scope.context.ChunkContext;
import org.springframework.batch.core.step.tasklet.Tasklet;
import org.springframework.batch.repeat.RepeatStatus;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
/**
* 결과물 ZIP 압축 Tasklet
*
* <p>Docker 컨테이너 실행으로 생성된 학습 데이터 결과물을 ZIP 파일로 압축합니다. 압축된 파일은 다운로드 또는 배포를 위해
* 사용됩니다.
*
* <p><b>주요 기능:</b>
*
* <ul>
* <li>response/{resultUid}/ 디렉토리 검증
* <li>디렉토리 내 모든 파일과 서브디렉토리 재귀적 압축
* <li>압축 파일 생성: response/{resultUid}.zip
* </ul>
*
* <p><b>압축 설정:</b>
*
* <ul>
* <li>Hidden 파일 제외
* <li>디렉토리 구조 유지
* <li>버퍼 크기: 1024 bytes
* </ul>
*
* <p><b>실행 조건:</b>
*
* <ul>
* <li>dockerRunStep이 성공적으로 완료되어야 함
* <li>response/{resultUid}/ 디렉토리가 존재해야 함
* <li>디렉토리 내에 최소 1개 이상의 파일이 존재해야 함
* </ul>
*
* @author KAMCO Development Team
* @since 1.0.0
*/
@Log4j2
@Component
@StepScope
@RequiredArgsConstructor
public class ZipResponseTasklet implements Tasklet {
/** 학습 데이터 저장 베이스 디렉토리 경로 (예: /kamco-nfs/dataset) */
@Value("${training-data.geojson-dir}")
private String trainingDataDir;
/** Job Parameter로 전달받은 결과물 고유 ID (UUID) */
@Value("#{jobParameters['resultUid']}")
private String resultUid;
/**
* 결과물 압축 작업 실행
*
* <p>response/{resultUid}/ 디렉토리를 검증하고 ZIP 파일로 압축합니다.
*
* @param contribution Step 실행 정보를 담는 객체
* @param chunkContext Chunk 실행 컨텍스트
* @return RepeatStatus.FINISHED - 작업 완료
* @throws RuntimeException response 디렉토리가 존재하지 않거나 압축 실패 시
*/
@Override
public RepeatStatus execute(StepContribution contribution, ChunkContext chunkContext) {
log.info("========================================");
log.info("결과물 압축 시작 (ResultUid={})", resultUid);
log.info("========================================");
// Step 1: 압축 대상 디렉토리 및 출력 파일 경로 설정
log.info("[Step 1/5] 경로 설정 중...");
Path responseDir =
Paths.get(trainingDataDir + File.separator + "response" + File.separator + resultUid);
Path zipFile =
Paths.get(
trainingDataDir + File.separator + "response" + File.separator + resultUid + ".zip");
log.info("[Step 1/5] 경로 설정 완료");
log.info(" - 압축 대상 디렉토리: {}", responseDir);
log.info(" - 압축 파일 저장 경로: {}", zipFile);
// Step 2: response 디렉토리 존재 여부 검증
log.info("[Step 2/5] Response 디렉토리 검증 중...");
if (!Files.exists(responseDir)) {
log.error("[실패] Response 디렉토리가 존재하지 않습니다!");
log.error(" - 경로: {}", responseDir);
log.error(" - ResultUid: {}", resultUid);
log.error(" - 확인사항:");
log.error(" 1. dockerRunStep이 정상적으로 완료되었는지 확인");
log.error(" 2. Docker 컨테이너가 결과물을 생성했는지 확인");
log.error(" 3. Docker 볼륨 마운트 경로가 올바른지 확인");
throw new RuntimeException("Response 디렉토리가 존재하지 않습니다: " + responseDir);
}
log.info("[Step 2/5] ✓ Response 디렉토리 존재 확인");
// Step 3: 디렉토리 내용 확인 (파일 수 카운트)
log.info("[Step 3/5] 디렉토리 내용 분석 중...");
File responseDirFile = responseDir.toFile();
long fileCount = countFilesRecursively(responseDirFile);
log.info("[Step 3/5] 디렉토리 분석 완료");
log.info(" - 총 파일 수: {} 개", fileCount);
if (fileCount == 0) {
log.warn("[경고] 압축할 파일이 없습니다. (디렉토리가 비어있음)");
log.warn(" - 디렉토리: {}", responseDir);
}
// Step 4: ZIP 압축 실행
log.info("[Step 4/5] ZIP 압축 시작...");
try {
long startTime = System.currentTimeMillis();
zipDirectory(responseDirFile, zipFile.toFile());
long endTime = System.currentTimeMillis();
long duration = endTime - startTime;
long zipSize = Files.size(zipFile);
double zipSizeMB = zipSize / (1024.0 * 1024.0);
log.info("[Step 4/5] ✓ ZIP 압축 완료");
log.info(" - 압축 파일: {}", zipFile);
log.info(" - 압축 파일 크기: {} bytes ({} MB)", zipSize, String.format("%.2f", zipSizeMB));
log.info(" - 압축 소요 시간: {} ms", duration);
} catch (IOException e) {
log.error("[실패] ZIP 압축 중 오류 발생!", e);
log.error(" - 소스 디렉토리: {}", responseDir);
log.error(" - 대상 ZIP 파일: {}", zipFile);
log.error(" - 에러 메시지: {}", e.getMessage());
throw new RuntimeException("Response 디렉토리 압축 실패: " + responseDir, e);
}
// Step 5: 최종 검증 및 완료
log.info("[Step 5/5] 최종 검증 중...");
if (Files.exists(zipFile)) {
log.info("[Step 5/5] ✓ ZIP 파일 생성 확인 완료");
} else {
log.error("[실패] ZIP 파일이 생성되지 않았습니다!");
throw new RuntimeException("ZIP 파일 생성 실패: " + zipFile);
}
log.info("========================================");
log.info("결과물 압축 완료 (ResultUid={})", resultUid);
log.info(" - ZIP 파일 위치: {}", zipFile);
log.info("========================================");
return RepeatStatus.FINISHED;
}
/**
* 디렉토리를 ZIP 파일로 압축
*
* @param sourceDir 압축할 소스 디렉토리
* @param zipFile 생성될 ZIP 파일
* @throws IOException 압축 중 IO 오류 발생 시
*/
private void zipDirectory(File sourceDir, File zipFile) throws IOException {
log.debug("ZIP 압축 시작: {} -> {}", sourceDir.getAbsolutePath(), zipFile.getAbsolutePath());
try (FileOutputStream fos = new FileOutputStream(zipFile);
ZipOutputStream zos = new ZipOutputStream(fos)) {
zipDirectoryRecursive(sourceDir, sourceDir.getName(), zos);
}
log.debug("ZIP 압축 완료: {}", zipFile.getAbsolutePath());
}
/**
* 디렉토리를 재귀적으로 ZIP으로 압축
*
* <p>서브디렉토리를 포함한 모든 파일과 디렉토리를 ZIP에 추가합니다. Hidden 파일은 제외됩니다.
*
* @param fileToZip 압축할 파일 또는 디렉토리
* @param fileName ZIP 엔트리 이름
* @param zos ZipOutputStream
* @throws IOException 압축 중 IO 오류 발생 시
*/
private void zipDirectoryRecursive(File fileToZip, String fileName, ZipOutputStream zos)
throws IOException {
// Hidden 파일은 제외
if (fileToZip.isHidden()) {
log.debug("Hidden 파일 제외: {}", fileToZip.getName());
return;
}
// 디렉토리인 경우
if (fileToZip.isDirectory()) {
log.debug("디렉토리 추가: {}", fileName);
// 디렉토리 엔트리 생성
if (fileName.endsWith("/")) {
zos.putNextEntry(new ZipEntry(fileName));
zos.closeEntry();
} else {
zos.putNextEntry(new ZipEntry(fileName + "/"));
zos.closeEntry();
}
// 하위 파일 및 디렉토리 재귀 처리
File[] children = fileToZip.listFiles();
if (children != null) {
for (File childFile : children) {
zipDirectoryRecursive(childFile, fileName + "/" + childFile.getName(), zos);
}
}
return;
}
// 파일인 경우: 파일 내용을 ZIP에 추가
log.debug("파일 추가: {}", fileName);
try (FileInputStream fis = new FileInputStream(fileToZip)) {
ZipEntry zipEntry = new ZipEntry(fileName);
zos.putNextEntry(zipEntry);
// 파일 내용을 버퍼를 통해 복사
byte[] buffer = new byte[1024];
int length;
while ((length = fis.read(buffer)) >= 0) {
zos.write(buffer, 0, length);
}
}
}
/**
* 디렉토리 내 파일 수를 재귀적으로 카운트
*
* @param directory 카운트할 디렉토리
* @return 디렉토리 내 총 파일 수 (서브디렉토리 포함)
*/
private long countFilesRecursively(File directory) {
if (!directory.isDirectory()) {
return directory.isFile() ? 1 : 0;
}
File[] children = directory.listFiles();
if (children == null) {
return 0;
}
long count = 0;
for (File child : children) {
if (child.isFile()) {
count++;
} else if (child.isDirectory()) {
count += countFilesRecursively(child);
}
}
return count;
}
}

View File

@@ -1,44 +0,0 @@
package com.kamco.cd.geojsonscheduler.config;
import org.springframework.batch.core.launch.JobLauncher;
import org.springframework.batch.core.launch.support.TaskExecutorJobLauncher;
import org.springframework.batch.core.repository.JobRepository;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.task.SimpleAsyncTaskExecutor;
/**
* 비동기 JobLauncher 설정
*
* <p>Parent Job 내에서 Child Job을 실행할 때 트랜잭션 충돌을 방지하기 위해 비동기 JobLauncher를 생성합니다.
*
* <p><b>문제:</b> Parent Job의 Step이 트랜잭션 내에서 실행되는데, 그 안에서 동기 JobLauncher로 Child Job을
* 실행하면 "Existing transaction detected in JobRepository" 에러 발생
*
* <p><b>해결:</b> 비동기 TaskExecutor를 사용하는 별도의 JobLauncher를 생성하여 트랜잭션 분리
*
* @author KAMCO Development Team
* @since 1.0.0
*/
@Configuration
public class AsyncJobLauncherConfig {
/**
* 비동기 JobLauncher 생성
*
* <p>SimpleAsyncTaskExecutor를 사용하여 Child Job을 별도 쓰레드에서 실행합니다. 이렇게 하면 Parent Job의
* 트랜잭션과 분리되어 트랜잭션 충돌이 발생하지 않습니다.
*
* @param jobRepository JobRepository
* @return 비동기 JobLauncher
* @throws Exception JobLauncher 초기화 실패 시
*/
@Bean(name = "asyncJobLauncher")
public JobLauncher asyncJobLauncher(JobRepository jobRepository) throws Exception {
TaskExecutorJobLauncher jobLauncher = new TaskExecutorJobLauncher();
jobLauncher.setJobRepository(jobRepository);
jobLauncher.setTaskExecutor(new SimpleAsyncTaskExecutor()); // 비동기 실행
jobLauncher.afterPropertiesSet();
return jobLauncher;
}
}

View File

@@ -1,25 +0,0 @@
package com.kamco.cd.geojsonscheduler.config;
import lombok.Getter;
import lombok.Setter;
import org.springframework.boot.context.properties.ConfigurationProperties;
@Getter
@Setter
@ConfigurationProperties(prefix = "train-data.docker")
public class TrainDockerProperties {
private String image;
private String dataVolume;
private String checkpointsVolume;
private String datasetFolder;
private String outputFolder;
private String inputSize;
private String cropSize;
private int batchSize;
private String gpuIds;
private int gpus;
private String lr;
private String backbone;
private int epochs;
}

View File

@@ -1,148 +0,0 @@
package com.kamco.cd.geojsonscheduler.listener;
import com.kamco.cd.geojsonscheduler.repository.BatchStepHistoryRepository;
import lombok.RequiredArgsConstructor;
import lombok.extern.log4j.Log4j2;
import org.springframework.batch.core.ExitStatus;
import org.springframework.batch.core.StepExecution;
import org.springframework.batch.core.StepExecutionListener;
import org.springframework.stereotype.Component;
/**
* Step 실행 이력 Listener
*
* <p>각 Step의 시작과 종료 시점에 실행되어 batch_step_history 테이블에 실행 이력을 기록합니다. Step 시작 시 STARTED
* 상태로 INSERT하고, Step 종료 시 SUCCESS 또는 FAILED 상태로 UPDATE합니다.
*
* <p><b>주요 기능:</b>
*
* <ul>
* <li>Step 시작 시 DB에 STARTED 상태 기록
* <li>Step 종료 시 DB에 SUCCESS 또는 FAILED 상태 업데이트
* <li>실패 시 에러 메시지 자동 기록
* <li>리스너 자체 오류가 Step 실행에 영향을 주지 않도록 예외 처리
* </ul>
*
* <p><b>적용 대상:</b>
*
* <ul>
* <li>makeGeoJsonStep
* <li>dockerRunStep
* <li>zipResponseStep
* </ul>
*
* <p><b>필수 JobParameters:</b>
*
* <ul>
* <li>analUid (Long): 분석 회차 UID
* <li>resultUid (String): 결과물 고유 ID
* </ul>
*
* @author KAMCO Development Team
* @since 1.0.0
* @see BatchStepHistoryRepository
*/
@Log4j2
@Component
@RequiredArgsConstructor
public class StepHistoryListener implements StepExecutionListener {
/** Step 이력 DB 저장을 위한 Repository */
private final BatchStepHistoryRepository batchStepHistoryRepository;
@Override
public void beforeStep(StepExecution stepExecution) {
log.info("=========================================================");
log.info("Step 시작 - StepHistoryListener");
log.info("=========================================================");
String stepName = stepExecution.getStepName();
log.info("Step Name: {}", stepName);
try {
Long analUid = stepExecution.getJobParameters().getLong("analUid");
String resultUid = stepExecution.getJobParameters().getString("resultUid");
if (analUid == null || resultUid == null) {
log.warn(
"JobParameters에 analUid 또는 resultUid가 없어 Step 이력을 기록할 수 없습니다.");
return;
}
log.info("AnalUid: {}, ResultUid: {}", analUid, resultUid);
// Step 시작 기록
batchStepHistoryRepository.startStep(analUid, resultUid, stepName);
log.info("Step 시작 기록 저장 완료");
} catch (Exception e) {
log.error("Step 시작 기록 저장 실패: {}", e.getMessage(), e);
// 리스너 오류가 Step 실행을 방해하지 않도록 예외를 던지지 않음
}
}
@Override
public ExitStatus afterStep(StepExecution stepExecution) {
log.info("=========================================================");
log.info("Step 종료 - StepHistoryListener");
log.info("=========================================================");
String stepName = stepExecution.getStepName();
log.info("Step Name: {}", stepName);
log.info("Step Exit Status: {}", stepExecution.getExitStatus());
try {
Long analUid = stepExecution.getJobParameters().getLong("analUid");
String resultUid = stepExecution.getJobParameters().getString("resultUid");
if (analUid == null || resultUid == null) {
log.warn(
"JobParameters에 analUid 또는 resultUid가 없어 Step 이력을 기록할 수 없습니다.");
return stepExecution.getExitStatus();
}
log.info("AnalUid: {}, ResultUid: {}", analUid, resultUid);
// Step 성공 여부 판단
boolean isSuccess = ExitStatus.COMPLETED.equals(stepExecution.getExitStatus());
log.info("Step 성공 여부: {}", isSuccess ? "성공" : "실패");
if (isSuccess) {
// Step 성공 기록
batchStepHistoryRepository.finishStepSuccess(analUid, resultUid, stepName);
log.info("Step 성공 기록 저장 완료");
} else {
// Step 실패 기록
String errorMessage = buildErrorMessage(stepExecution);
batchStepHistoryRepository.finishStepFailed(analUid, resultUid, stepName, errorMessage);
log.info("Step 실패 기록 저장 완료");
}
} catch (Exception e) {
log.error("Step 종료 기록 저장 실패: {}", e.getMessage(), e);
// 리스너 오류가 Step 실행을 방해하지 않도록 예외를 던지지 않음
}
log.info("=========================================================");
return stepExecution.getExitStatus();
}
/**
* Step 실행 실패 시 에러 메시지 생성
*/
private String buildErrorMessage(StepExecution stepExecution) {
StringBuilder sb = new StringBuilder();
sb.append("ExitStatus: ").append(stepExecution.getExitStatus()).append("\n");
if (!stepExecution.getFailureExceptions().isEmpty()) {
sb.append("Failure Exceptions:\n");
for (Throwable t : stepExecution.getFailureExceptions()) {
sb.append("- ").append(t.getClass().getSimpleName()).append(": ").append(t.getMessage())
.append("\n");
}
}
return sb.toString();
}
}

View File

@@ -1,169 +0,0 @@
package com.kamco.cd.geojsonscheduler.repository;
import java.sql.Timestamp;
import java.time.LocalDateTime;
import lombok.RequiredArgsConstructor;
import lombok.extern.log4j.Log4j2;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.stereotype.Repository;
import org.springframework.transaction.annotation.Transactional;
/**
* Batch Step 실행 이력 Repository
*
* <p>각 AnalCntInfo의 Step별 실행 이력을 batch_step_history 테이블에 저장하고 조회합니다. Step 시작 시 STARTED
* 상태로 INSERT하고, Step 종료 시 SUCCESS 또는 FAILED 상태로 UPDATE합니다.
*
* <p><b>주요 기능:</b>
*
* <ul>
* <li>Step 시작 기록 (status=STARTED)
* <li>Step 성공 기록 (status=SUCCESS)
* <li>Step 실패 기록 (status=FAILED, error_message 포함)
* </ul>
*
* <p><b>테이블 구조:</b>
*
* <ul>
* <li>id: Step 이력 고유 ID (BIGSERIAL)
* <li>anal_uid: 분석 UID
* <li>result_uid: 결과 UID
* <li>step_name: Step 이름 (makeGeoJsonStep/dockerRunStep/zipResponseStep)
* <li>status: 상태 (STARTED/SUCCESS/FAILED)
* <li>error_message: 에러 메시지 (실패 시)
* <li>started_dttm: Step 시작 일시
* <li>completed_dttm: Step 완료 일시
* </ul>
*
* @author KAMCO Development Team
* @since 1.0.0
*/
@Log4j2
@Repository
@RequiredArgsConstructor
public class BatchStepHistoryRepository {
/** JDBC 쿼리 실행을 위한 Template */
private final JdbcTemplate jdbcTemplate;
/**
* Step 시작 기록
*/
@Transactional
public void startStep(Long analUid, String resultUid, String stepName) {
log.info("[BatchStepHistoryRepository] Step 시작 기록 저장");
log.info(" AnalUid: {}, ResultUid: {}, StepName: {}", analUid, resultUid, stepName);
String sql =
"""
INSERT INTO public.batch_step_history
(anal_uid, result_uid, step_name, status, started_dttm, created_dttm, updated_dttm)
VALUES (?, ?, ?, ?, ?, ?, ?)
""";
Timestamp now = Timestamp.valueOf(LocalDateTime.now());
int rowsAffected =
jdbcTemplate.update(
sql,
analUid,
resultUid,
stepName,
"STARTED",
now, // started_dttm
now, // created_dttm
now // updated_dttm
);
log.info(
"[BatchStepHistoryRepository] Step 시작 기록 저장 완료 ({} rows affected)", rowsAffected);
}
/**
* Step 성공 기록
*/
@Transactional
public void finishStepSuccess(Long analUid, String resultUid, String stepName) {
log.info("[BatchStepHistoryRepository] Step 성공 기록 업데이트");
log.info(" AnalUid: {}, ResultUid: {}, StepName: {}", analUid, resultUid, stepName);
String sql =
"""
UPDATE public.batch_step_history
SET status = ?,
completed_dttm = ?,
updated_dttm = ?
WHERE anal_uid = ?
AND result_uid = ?
AND step_name = ?
AND status = 'STARTED'
""";
Timestamp now = Timestamp.valueOf(LocalDateTime.now());
int rowsAffected =
jdbcTemplate.update(sql, "SUCCESS", now, now, analUid, resultUid, stepName);
if (rowsAffected > 0) {
log.info(
"[BatchStepHistoryRepository] Step 성공 기록 업데이트 완료 ({} rows affected)",
rowsAffected);
} else {
log.warn(
"[BatchStepHistoryRepository] 업데이트된 row가 없습니다. AnalUid: {}, ResultUid: {},"
+ " StepName: {}",
analUid,
resultUid,
stepName);
}
}
/**
* Step 실패 기록
*/
@Transactional
public void finishStepFailed(
Long analUid, String resultUid, String stepName, String errorMessage) {
log.info("[BatchStepHistoryRepository] Step 실패 기록 업데이트");
log.info(" AnalUid: {}, ResultUid: {}, StepName: {}", analUid, resultUid, stepName);
log.info(" ErrorMessage: {}", errorMessage);
String sql =
"""
UPDATE public.batch_step_history
SET status = ?,
error_message = ?,
completed_dttm = ?,
updated_dttm = ?
WHERE anal_uid = ?
AND result_uid = ?
AND step_name = ?
AND status = 'STARTED'
""";
Timestamp now = Timestamp.valueOf(LocalDateTime.now());
// error_message는 최대 1000자로 제한
String truncatedError =
errorMessage != null && errorMessage.length() > 1000
? errorMessage.substring(0, 1000)
: errorMessage;
int rowsAffected =
jdbcTemplate.update(
sql, "FAILED", truncatedError, now, now, analUid, resultUid, stepName);
if (rowsAffected > 0) {
log.info(
"[BatchStepHistoryRepository] Step 실패 기록 업데이트 완료 ({} rows affected)",
rowsAffected);
} else {
log.warn(
"[BatchStepHistoryRepository] 업데이트된 row가 없습니다. AnalUid: {}, ResultUid: {},"
+ " StepName: {}",
analUid,
resultUid,
stepName);
}
}
}

View File

@@ -84,7 +84,7 @@ public class TrainingDataReviewJobRepository {
SELECT SELECT
mslg.geo_uid, mslg.geo_uid,
'Feature' AS type, 'Feature' AS type,
ST_AsGeoJSON(ST_Transform(mslg.geom, 4326)) AS geom_str, ST_AsGeoJSON(mslg.geom) AS geom_str,
CASE CASE
WHEN mslg.class_after_cd IN ('building', 'container') THEN 'M1' WHEN mslg.class_after_cd IN ('building', 'container') THEN 'M1'
WHEN mslg.class_after_cd = 'waste' THEN 'M2' WHEN mslg.class_after_cd = 'waste' THEN 'M2'

View File

@@ -10,206 +10,75 @@ import lombok.RequiredArgsConstructor;
import lombok.extern.log4j.Log4j2; import lombok.extern.log4j.Log4j2;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
/**
* Docker 컨테이너 실행 서비스
*
* <p>학습 데이터 생성 파이프라인이 포함된 Docker 컨테이너를 실행하고 결과를 모니터링합니다. Docker 프로세스의 표준 출력을 실시간으로
* 로깅하며, 비정상 종료 시 RuntimeException을 발생시켜 Batch Step을 실패 처리합니다.
*
* <p><b>주요 기능:</b>
*
* <ul>
* <li>Docker run 명령어 생성 (볼륨 마운트, 환경 변수, 파라미터 설정)
* <li>Docker 프로세스 실행 및 실시간 로그 출력
* <li>Exit Code 검증 (0이 아닐 시 예외 발생)
* <li>프로세스 인터럽트 처리
* </ul>
*
* <p><b>Docker 명령어 구조:</b>
*
* <pre>
* docker run --rm
* --user {dockerUser}
* -v {datasetVolume}
* -v {imagesVolume}
* --entrypoint python
* {dockerImage}
* code/kamco_full_pipeline.py
* --labelling-folder request/{resultUid}
* --output-folder response/{resultUid}
* --input_root {inputRoot}
* --output_root {outputRoot}
* --patch_size {patchSize}
* --overlap_pct {overlapPct}
* --train_val_test_ratio {train} {val} {test}
* --keep_empty_ratio {keepEmptyRatio}
* </pre>
*
* @author KAMCO Development Team
* @since 1.0.0
* @see DockerProperties
*/
@Log4j2 @Log4j2
@Service @Service
@RequiredArgsConstructor @RequiredArgsConstructor
public class DockerRunnerService { public class DockerRunnerService {
/** Docker 실행 관련 설정 정보 (application.yml) */
private final DockerProperties dockerProperties; private final DockerProperties dockerProperties;
/**
* Docker 컨테이너 실행 및 모니터링
*
* <p>학습 데이터 생성 파이프라인을 Docker 컨테이너로 실행합니다. 컨테이너의 표준 출력을 실시간으로 로깅하며, 비정상 종료 시
* RuntimeException을 발생시켜 Step 실패로 처리합니다.
*
* @param resultUid 결과물 고유 ID (UUID)
* @throws RuntimeException Docker 프로세스 실패 (exitCode != 0), IO 오류, 또는 인터럽트 발생 시
*/
public void run(String resultUid) { public void run(String resultUid) {
// Step 1: Docker 명령어 생성
log.info("[Step 1/4] Docker 명령어 생성 중...");
List<String> command = buildCommand(resultUid); List<String> command = buildCommand(resultUid);
log.info("[Step 1/4] Docker 명령어 생성 완료"); log.info("Running docker command: {}", String.join(" ", command));
log.debug(" - 명령어: {}", String.join(" ", command));
try { try {
// Step 2: Docker 프로세스 시작
log.info("[Step 2/4] Docker 프로세스 시작 중...");
ProcessBuilder pb = new ProcessBuilder(command); ProcessBuilder pb = new ProcessBuilder(command);
pb.redirectErrorStream(true); // stderr를 stdout으로 리다이렉트 pb.redirectErrorStream(true);
Process process = pb.start(); Process process = pb.start();
log.info("[Step 2/4] Docker 프로세스 시작 완료 (PID={})", process.pid());
// Step 3: Docker 프로세스 출력 실시간 로깅
log.info("[Step 3/4] Docker 프로세스 출력 모니터링 중...");
int lineCount = 0;
try (BufferedReader reader = try (BufferedReader reader =
new BufferedReader(new InputStreamReader(process.getInputStream()))) { new BufferedReader(new InputStreamReader(process.getInputStream()))) {
String line; String line;
while ((line = reader.readLine()) != null) { while ((line = reader.readLine()) != null) {
log.info("[docker] {}", line); log.info("[docker] {}", line);
lineCount++;
} }
} }
log.info("[Step 3/4] Docker 프로세스 출력 완료 (총 {} 라인)", lineCount);
// Step 4: Exit Code 검증
log.info("[Step 4/4] Docker 프로세스 종료 대기 중...");
int exitCode = process.waitFor(); int exitCode = process.waitFor();
log.info("[Step 4/4] Docker 프로세스 종료 (exitCode={})", exitCode);
if (exitCode != 0) { if (exitCode != 0) {
// Docker 프로세스 비정상 종료 (Step 실패 처리) log.error("Docker process exited with code {} for resultUid: {}", exitCode, resultUid);
log.error("[실패] Docker 프로세스가 비정상 종료되었습니다!");
log.error(" - Exit Code: {}", exitCode);
log.error(" - ResultUid: {}", resultUid);
log.error(" - 확인사항:");
log.error(" 1. Docker 컨테이너 로그 확인 (위 [docker] 로그 참조)");
log.error(" 2. request/{}/ 디렉토리에 GeoJSON 파일 확인", resultUid);
log.error(" 3. Docker 이미지 및 볼륨 마운트 경로 확인");
throw new RuntimeException(
String.format(
"Docker process failed with exit code %d for resultUid: %s",
exitCode, resultUid));
} else { } else {
// Docker 프로세스 정상 종료 log.info("Docker process completed successfully for resultUid: {}", resultUid);
log.info("[성공] Docker 프로세스가 정상 종료되었습니다.");
log.info(" - ResultUid: {}", resultUid);
log.info(" - 결과물 위치: /dataset/response/{}/", resultUid);
} }
} catch (IOException e) { } catch (IOException e) {
// Docker 명령어 실행 실패 (파일 시스템 오류 등) log.error("Failed to run docker command for resultUid {}: {}", resultUid, e.getMessage());
log.error("[실패] Docker 명령어 실행 중 IO 오류 발생!", e);
log.error(" - ResultUid: {}", resultUid);
log.error(" - 에러 메시지: {}", e.getMessage());
throw new RuntimeException("Failed to run docker command for resultUid: " + resultUid, e);
} catch (InterruptedException e) { } catch (InterruptedException e) {
// Docker 프로세스 인터럽트 (사용자 취소 또는 시스템 종료) log.error("Docker process interrupted for resultUid {}: {}", resultUid, e.getMessage());
log.error("[인터럽트] Docker 프로세스가 중단되었습니다!", e); Thread.currentThread().interrupt();
log.error(" - ResultUid: {}", resultUid);
log.error(" - 에러 메시지: {}", e.getMessage());
Thread.currentThread().interrupt(); // 인터럽트 상태 복원
throw new RuntimeException("Docker process interrupted for resultUid: " + resultUid, e);
} }
} }
/**
* Docker 명령어 생성
*
* <p>DockerProperties 설정 정보와 resultUid를 기반으로 Docker run 명령어를 생성합니다.
*
* @param resultUid 결과물 고유 ID (입력/출력 폴더 경로에 사용)
* @return Docker 명령어 문자열 리스트 (ProcessBuilder 실행용)
*/
private List<String> buildCommand(String resultUid) { private List<String> buildCommand(String resultUid) {
log.debug("Docker 명령어 파라미터 구성 중...");
List<String> cmd = new ArrayList<>(); List<String> cmd = new ArrayList<>();
// Docker 기본 명령어
cmd.add("docker"); cmd.add("docker");
cmd.add("run"); cmd.add("run");
cmd.add("--rm"); // 컨테이너 종료 시 자동 삭제 cmd.add("--rm");
// 사용자 및 권한 설정
cmd.add("--user"); cmd.add("--user");
cmd.add(dockerProperties.getUser()); // 예: "1000:1000" cmd.add(dockerProperties.getUser());
log.debug(" - User: {}", dockerProperties.getUser());
// 볼륨 마운트 (호스트:컨테이너)
cmd.add("-v"); cmd.add("-v");
cmd.add(dockerProperties.getDatasetVolume()); // 예: "/kamco-nfs/dataset:/dataset" cmd.add(dockerProperties.getDatasetVolume());
log.debug(" - Dataset Volume: {}", dockerProperties.getDatasetVolume());
cmd.add("-v"); cmd.add("-v");
cmd.add(dockerProperties.getImagesVolume()); // 예: "/kamco-nfs/images:/images" cmd.add(dockerProperties.getImagesVolume());
log.debug(" - Images Volume: {}", dockerProperties.getImagesVolume());
// Entrypoint 및 이미지
cmd.add("--entrypoint"); cmd.add("--entrypoint");
cmd.add("python"); // Python으로 스크립트 실행 cmd.add("python");
cmd.add(dockerProperties.getImage()); // 예: "kamco/dataset-generator:latest" cmd.add(dockerProperties.getImage());
log.debug(" - Image: {}", dockerProperties.getImage());
// Python 스크립트 및 파라미터
cmd.add("code/kamco_full_pipeline.py"); cmd.add("code/kamco_full_pipeline.py");
// 입출력 폴더 설정
cmd.add("--labelling-folder"); cmd.add("--labelling-folder");
cmd.add("request/" + resultUid); cmd.add("request/" + resultUid);
log.debug(" - Labelling Folder: request/{}", resultUid);
cmd.add("--output-folder"); cmd.add("--output-folder");
cmd.add("response/" + resultUid); cmd.add("response/" + resultUid);
log.debug(" - Output Folder: response/{}", resultUid);
// 파이프라인 파라미터
cmd.add("--input_root"); cmd.add("--input_root");
cmd.add(dockerProperties.getInputRoot()); cmd.add(dockerProperties.getInputRoot());
cmd.add("--output_root"); cmd.add("--output_root");
cmd.add(dockerProperties.getOutputRoot()); cmd.add(dockerProperties.getOutputRoot());
cmd.add("--patch_size"); cmd.add("--patch_size");
cmd.add(String.valueOf(dockerProperties.getPatchSize())); cmd.add(String.valueOf(dockerProperties.getPatchSize()));
log.debug(" - Patch Size: {}", dockerProperties.getPatchSize());
cmd.add("--overlap_pct"); cmd.add("--overlap_pct");
cmd.add(String.valueOf(dockerProperties.getOverlapPct())); cmd.add(String.valueOf(dockerProperties.getOverlapPct()));
log.debug(" - Overlap Percent: {}", dockerProperties.getOverlapPct());
cmd.add("--train_val_test_ratio"); cmd.add("--train_val_test_ratio");
cmd.addAll(dockerProperties.getTrainValTestRatio()); // 예: ["0.7", "0.2", "0.1"] cmd.addAll(dockerProperties.getTrainValTestRatio());
log.debug(" - Train/Val/Test Ratio: {}", dockerProperties.getTrainValTestRatio());
cmd.add("--keep_empty_ratio"); cmd.add("--keep_empty_ratio");
cmd.add(String.valueOf(dockerProperties.getKeepEmptyRatio())); cmd.add(String.valueOf(dockerProperties.getKeepEmptyRatio()));
log.debug(" - Keep Empty Ratio: {}", dockerProperties.getKeepEmptyRatio());
log.debug("Docker 명령어 파라미터 구성 완료");
return cmd; return cmd;
} }
} }

View File

@@ -1,121 +0,0 @@
package com.kamco.cd.geojsonscheduler.service;
import com.kamco.cd.geojsonscheduler.config.TrainDockerProperties;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.util.ArrayList;
import java.util.List;
import lombok.RequiredArgsConstructor;
import lombok.extern.log4j.Log4j2;
import org.springframework.stereotype.Service;
@Log4j2
@Service
@RequiredArgsConstructor
public class TrainDockerRunnerService {
private final TrainDockerProperties trainDockerProperties;
public void runTraining(String datasetFolder, String outputFolder) {
log.info("========================================");
log.info("Train Docker 실행 시작");
log.info("Dataset Folder: {}", datasetFolder);
log.info("Output Folder: {}", outputFolder);
log.info("========================================");
List<String> command = buildTrainCommand(datasetFolder, outputFolder);
log.info("Docker 명령어: {}", String.join(" ", command));
try {
ProcessBuilder pb = new ProcessBuilder(command);
pb.redirectErrorStream(true);
Process process = pb.start();
log.info("Docker 프로세스 시작됨 (Detached mode)");
try (BufferedReader reader =
new BufferedReader(new InputStreamReader(process.getInputStream()))) {
String line;
while ((line = reader.readLine()) != null) {
log.info("[train-docker] {}", line);
}
}
int exitCode = process.waitFor();
if (exitCode != 0) {
log.error("Train Docker 프로세스 실패: exitCode={}, datasetFolder={}, outputFolder={}",
exitCode, datasetFolder, outputFolder);
} else {
log.info("Train Docker 프로세스 시작 완료: datasetFolder={}, outputFolder={}",
datasetFolder, outputFolder);
}
} catch (IOException e) {
log.error("Train Docker 실행 실패: datasetFolder={}, outputFolder={}, error={}",
datasetFolder, outputFolder, e.getMessage(), e);
} catch (InterruptedException e) {
log.error("Train Docker 프로세스 중단: datasetFolder={}, outputFolder={}, error={}",
datasetFolder, outputFolder, e.getMessage(), e);
Thread.currentThread().interrupt();
}
log.info("========================================");
log.info("Train Docker 실행 완료");
log.info("========================================");
}
private List<String> buildTrainCommand(String datasetFolder, String outputFolder) {
List<String> cmd = new ArrayList<>();
cmd.add("docker");
cmd.add("run");
cmd.add("-d"); // detached mode
cmd.add("--name");
cmd.add("train-cd");
cmd.add("--rm");
cmd.add("--gpus");
cmd.add("all");
cmd.add("--ipc=host");
cmd.add("--shm-size=16g");
cmd.add("--ulimit");
cmd.add("memlock=-1");
cmd.add("--ulimit");
cmd.add("stack=67108864");
cmd.add("-e");
cmd.add("NCCL_DEBUG=INFO");
cmd.add("-e");
cmd.add("NCCL_IB_DISABLE=1");
cmd.add("-e");
cmd.add("NCCL_P2P_DISABLE=0");
cmd.add("-e");
cmd.add("NCCL_SOCKET_IFNAME=eth0");
cmd.add("-v");
cmd.add(trainDockerProperties.getDataVolume());
cmd.add("-v");
cmd.add(trainDockerProperties.getCheckpointsVolume());
cmd.add("-it");
cmd.add(trainDockerProperties.getImage());
cmd.add("python");
cmd.add("/workspace/change-detection-code/train_wrapper.py");
cmd.add("--dataset-folder");
cmd.add(datasetFolder);
cmd.add("--output-folder");
cmd.add(outputFolder);
cmd.add("--input-size");
cmd.add(trainDockerProperties.getInputSize());
cmd.add("--crop-size");
cmd.add(trainDockerProperties.getCropSize());
cmd.add("--batch-size");
cmd.add(String.valueOf(trainDockerProperties.getBatchSize()));
cmd.add("--gpu-ids");
cmd.add(trainDockerProperties.getGpuIds());
cmd.add("--gpus");
cmd.add(String.valueOf(trainDockerProperties.getGpus()));
cmd.add("--lr");
cmd.add(trainDockerProperties.getLr());
cmd.add("--backbone");
cmd.add(trainDockerProperties.getBackbone());
cmd.add("--epochs");
cmd.add(String.valueOf(trainDockerProperties.getEpochs()));
return cmd;
}
}

View File

@@ -6,9 +6,6 @@ spring:
hikari: hikari:
minimum-idle: 2 minimum-idle: 2
maximum-pool-size: 5 maximum-pool-size: 5
batch:
job:
name: exportGeoJsonJob # 기본 실행 Job 지정
training-data: training-data:
geojson-dir: /kamco-nfs/dataset geojson-dir: /kamco-nfs/dataset

View File

@@ -3,9 +3,6 @@ spring:
url: jdbc:postgresql://localhost:5432/kamco_cds url: jdbc:postgresql://localhost:5432/kamco_cds
username: kamco_cds username: kamco_cds
password: kamco_cds password: kamco_cds
batch:
job:
name: exportGeoJsonJob # 기본 실행 Job 지정
training-data: training-data:
geojson-dir: /tmp/geojson geojson-dir: /tmp/geojson

View File

@@ -6,26 +6,6 @@ spring:
hikari: hikari:
minimum-idle: 2 minimum-idle: 2
maximum-pool-size: 5 maximum-pool-size: 5
batch:
job:
name: exportGeoJsonJob # 기본 실행 Job 지정
training-data: training-data:
geojson-dir: /kamco-nfs/dataset geojson-dir: /kamco-nfs/dataset
# Train Model Docker Configuration
train-data:
docker:
image: kamco-cd-train:latest
data-volume: /kamco-nfs/dataset:/data
checkpoints-volume: /kamco-nfs/checkpoints:/checkpoints
dataset-folder: /data/dataset
output-folder: /data/output
input-size: "512"
crop-size: "256"
batch-size: 8
gpu-ids: "0,1,2,3"
gpus: 4
lr: "0.001"
backbone: resnet50
epochs: 100

View File

@@ -21,7 +21,6 @@ spring:
batch: batch:
job: job:
enabled: true enabled: true
name: exportGeoJsonJob # 기본 실행 Job 지정
jdbc: jdbc:
initialize-schema: always initialize-schema: always

View File

@@ -23,36 +23,3 @@ COMMENT ON COLUMN public.batch_history.created_dttm IS '생성 일시';
COMMENT ON COLUMN public.batch_history.updated_dttm IS '수정 일시'; COMMENT ON COLUMN public.batch_history.updated_dttm IS '수정 일시';
COMMENT ON COLUMN public.batch_history.status IS '상태 (STARTED/COMPLETED/FAILED)'; COMMENT ON COLUMN public.batch_history.status IS '상태 (STARTED/COMPLETED/FAILED)';
COMMENT ON COLUMN public.batch_history.completed_dttm IS '완료 일시'; COMMENT ON COLUMN public.batch_history.completed_dttm IS '완료 일시';
-- batch_step_history 테이블 생성
CREATE TABLE IF NOT EXISTS public.batch_step_history (
id BIGSERIAL PRIMARY KEY,
anal_uid BIGINT NOT NULL,
result_uid VARCHAR(255) NOT NULL,
step_name VARCHAR(100) NOT NULL,
status VARCHAR(50) NOT NULL,
error_message TEXT,
started_dttm TIMESTAMP NOT NULL,
completed_dttm TIMESTAMP,
created_dttm TIMESTAMP NOT NULL,
updated_dttm TIMESTAMP NOT NULL
);
-- 인덱스 생성
CREATE INDEX IF NOT EXISTS idx_batch_step_history_anal_uid ON public.batch_step_history(anal_uid);
CREATE INDEX IF NOT EXISTS idx_batch_step_history_result_uid ON public.batch_step_history(result_uid);
CREATE INDEX IF NOT EXISTS idx_batch_step_history_status ON public.batch_step_history(status);
CREATE INDEX IF NOT EXISTS idx_batch_step_history_step_name ON public.batch_step_history(step_name);
-- 코멘트
COMMENT ON TABLE public.batch_step_history IS '배치 Step 실행 이력';
COMMENT ON COLUMN public.batch_step_history.id IS 'Step 이력 고유 ID';
COMMENT ON COLUMN public.batch_step_history.anal_uid IS '분석 UID';
COMMENT ON COLUMN public.batch_step_history.result_uid IS '결과 UID';
COMMENT ON COLUMN public.batch_step_history.step_name IS 'Step 이름 (makeGeoJsonStep/dockerRunStep/zipResponseStep)';
COMMENT ON COLUMN public.batch_step_history.status IS '상태 (STARTED/SUCCESS/FAILED)';
COMMENT ON COLUMN public.batch_step_history.error_message IS '에러 메시지';
COMMENT ON COLUMN public.batch_step_history.started_dttm IS 'Step 시작 일시';
COMMENT ON COLUMN public.batch_step_history.completed_dttm IS 'Step 완료 일시';
COMMENT ON COLUMN public.batch_step_history.created_dttm IS '생성 일시';
COMMENT ON COLUMN public.batch_step_history.updated_dttm IS '수정 일시';

View File

@@ -1,7 +0,0 @@
{
"permissions": {
"allow": [
"WebSearch"
]
}
}

View File

@@ -1,15 +0,0 @@
root = true
[*]
charset = utf-8
end_of_line = lf
insert_final_newline = true
trim_trailing_whitespace = true
[*.java]
indent_style = space
indent_size = 2
[*.{gradle,yml,yaml}]
indent_style = space
indent_size = 2

View File

@@ -1,2 +0,0 @@
#Wed Jan 14 15:14:03 KST 2026
gradle.version=8.14.3

8
shp-exporter/.idea/.gitignore generated vendored
View File

@@ -1,8 +0,0 @@
# Default ignored files
/shelf/
/workspace.xml
# Editor-based HTTP Client requests
/httpRequests/
# Datasource local storage ignored files
/dataSources/
/dataSources.local.xml

View File

@@ -1,9 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="CompilerConfiguration">
<bytecodeTargetLevel target="17" />
</component>
<component name="JavacSettings">
<option name="ADDITIONAL_OPTIONS_STRING" value="-parameters" />
</component>
</project>

View File

@@ -1,19 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="GradleMigrationSettings" migrationVersion="1" />
<component name="GradleSettings">
<option name="linkedExternalProjectsSettings">
<GradleProjectSettings>
<option name="delegatedBuild" value="false" />
<option name="testRunner" value="PLATFORM" />
<option name="externalProjectPath" value="$PROJECT_DIR$" />
<option name="modules">
<set>
<option value="$PROJECT_DIR$" />
</set>
</option>
<option name="resolveExternalAnnotations" value="true" />
</GradleProjectSettings>
</option>
</component>
</project>

View File

@@ -1,35 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="RemoteRepositoriesConfiguration">
<remote-repository>
<option name="id" value="central" />
<option name="name" value="Maven Central repository" />
<option name="url" value="https://repo1.maven.org/maven2" />
</remote-repository>
<remote-repository>
<option name="id" value="jboss.community" />
<option name="name" value="JBoss Community repository" />
<option name="url" value="https://repository.jboss.org/nexus/content/repositories/public/" />
</remote-repository>
<remote-repository>
<option name="id" value="MavenRepo" />
<option name="name" value="MavenRepo" />
<option name="url" value="https://repo.maven.apache.org/maven2/" />
</remote-repository>
<remote-repository>
<option name="id" value="maven" />
<option name="name" value="maven" />
<option name="url" value="https://repo.osgeo.org/repository/release/" />
</remote-repository>
<remote-repository>
<option name="id" value="maven2" />
<option name="name" value="maven2" />
<option name="url" value="https://repo.osgeo.org/repository/geotools-releases/" />
</remote-repository>
<remote-repository>
<option name="id" value="maven3" />
<option name="name" value="maven3" />
<option name="url" value="https://repo.osgeo.org/repository/snapshot/" />
</remote-repository>
</component>
</project>

View File

@@ -1,9 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<module type="JAVA_MODULE" version="4">
<component name="NewModuleRootManager" inherit-compiler-output="true">
<exclude-output />
<content url="file://$MODULE_DIR$" />
<orderEntry type="inheritedJdk" />
<orderEntry type="sourceFolder" forTests="false" />
</component>
</module>

View File

@@ -1,10 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ExternalStorageConfigurationManager" enabled="true" />
<component name="FrameworkDetectionExcludesConfiguration">
<file type="web" url="file://$PROJECT_DIR$" />
</component>
<component name="ProjectRootManager" version="2" languageLevel="JDK_17" default="true" project-jdk-name="17" project-jdk-type="JavaSDK">
<output url="file://$PROJECT_DIR$/out" />
</component>
</project>

View File

@@ -1,122 +0,0 @@
# CLAUDE.md
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
## Project Overview
Spring Boot CLI application that queries PostgreSQL PostGIS spatial data and converts it to ESRI shapefiles. The application processes inference results from the KAMCO database and generates geographic shapefiles for visualization in GIS applications.
## Build and Run Commands
### Build
```bash
./gradlew build
```
### Run Application
```bash
./gradlew bootRun
```
Or run the built JAR:
```bash
java -jar build/libs/makesample-1.0.0.jar
```
### Code Formatting
Apply Google Java Format (2-space indentation) before committing:
```bash
./gradlew spotlessApply
```
Check formatting without applying:
```bash
./gradlew spotlessCheck
```
## Architecture
### Processing Pipeline
The application follows a layered architecture with a linear data flow:
1. **CLI Entry** (`ConverterCommandLineRunner`) → Reads configuration and initiates batch processing
2. **Service Orchestration** (`ShapefileConverterService`) → Coordinates the conversion workflow for each map_id
3. **Data Access** (`InferenceResultRepository`) → Queries PostGIS database and converts WKT to JTS geometries
4. **Geometry Conversion** (`GeometryConverter`) → Converts PostGIS WKT format to JTS Geometry objects
5. **Shapefile Writing** (`ShapefileWriter`) → Uses GeoTools to generate shapefile artifacts (.shp, .shx, .dbf, .prj)
### Key Design Points
**Geometry Handling**: The application uses a two-step geometry conversion process:
- PostGIS returns geometries as WKT (Well-Known Text) via `ST_AsText(geometry)`
- `GeometryConverter` parses WKT to JTS `Geometry` objects
- `ShapefileWriter` uses JTS geometries with GeoTools to write shapefiles
**Batch Processing**: Configuration in `application.yml` drives batch execution:
- Multiple `map-ids` processed sequentially (if specified)
- If `map-ids` is null/empty, creates a merged shapefile for all batch-ids
- Each map_id filtered by `batch-ids` array
- Output directory structure: `{output-base-dir}/{inference-id}/{map-id}/` or `{output-base-dir}/{inference-id}/merge/` for merged mode
- Separate output directory created for each map_id
**Shapefile Constraints**: The application validates that all geometries for a single shapefile are homogeneous (same type) because shapefiles cannot contain mixed geometry types. This validation happens in `ShapefileConverterService.validateGeometries()`.
**Feature Schema**: GeoTools requires explicit geometry field setup:
- Default geometry field named `the_geom` (not `geometry`)
- Field names truncated to 10 characters for DBF format compatibility
- Geometry type determined from first valid geometry in result set
## Configuration
Primary configuration in `src/main/resources/application.yml`:
```yaml
converter:
inference-id: 'D5E46F60FC40B1A8BE0CD1F3547AA6' # Inference ID (used for output folder structure)
map-ids: ['35813030'] # List of map_ids to process (text type), omit for merged shapefile
batch-ids: [252, 253, 257] # Batch ID array filter
output-base-dir: '/kamco-nfs/dataset/export/' # Base directory for shapefile output
crs: 'EPSG:5186' # Korean 2000 / Central Belt CRS
```
Database connection configured via standard Spring Boot datasource properties.
## Database Integration
### Query Pattern
The repository uses `PreparedStatementCreator` to handle PostgreSQL array parameters:
```sql
WHERE batch_id = ANY(?) AND map_id = ?
```
The `ANY(?)` clause requires creating a PostgreSQL array using `Connection.createArrayOf("bigint", ...)`.
### Field Mapping
Database columns are mapped to shapefile fields with Korean naming:
| Database Column | Shapefile Field |
|-----------------|-----------------|
| uid | uid |
| map_id | map_id |
| probability | chn_dtct_prob |
| before_year | cprs_yr |
| after_year | crtr_yr |
| before_c | bf_cls_cd |
| before_p | bf_cls_prob |
| after_c | af_cls_cd |
| after_p | af_cls_prob |
| geometry | the_geom |
### Coordinate Reference System
All geometries use **EPSG:5186** (Korean 2000 / Central Belt). The PostGIS geometry column is defined as `geometry(Polygon, 5186)`, and this CRS is preserved in the output shapefile's `.prj` file via GeoTools CRS encoding.
## Dependencies
Key libraries and their roles:
- **GeoTools 30.0**: Shapefile generation (`gt-shapefile`, `gt-referencing`, `gt-epsg-hsql`)
- **JTS 1.19.0**: Java Topology Suite for geometry representation
- **PostGIS JDBC 2.5.1**: PostgreSQL spatial extension support
- **Spring Boot 3.5.7**: Framework for DI, JDBC, and configuration
Note: `javax.media:jai_core` is excluded in `build.gradle` to avoid conflicts.

View File

@@ -1,388 +0,0 @@
# PostgreSQL to Shapefile Converter
Spring Boot CLI application that queries PostgreSQL spatial data and generates shapefiles.
## Features
- Batch processing for multiple map_ids
- PostGIS geometry to Shapefile conversion
- **GeoServer REST API integration for automatic layer registration**
- Configurable via `application.yml`
- Generates all required shapefile files (.shp, .shx, .dbf, .prj)
- Supports EPSG:5186 (Korean 2000 / Central Belt) coordinate reference system
- GeoJSON export support
## Prerequisites
- Java 17 or higher
- PostgreSQL database with PostGIS extension
- Access to the KAMCO database at 192.168.2.127:15432
## Configuration
Edit `src/main/resources/application.yml` to configure:
```yaml
converter:
spring.profiles.active: 'dev' #profiles.active
inference-id: 'D5E46F60FC40B1A8BE0CD1F3547AA6' # Inference ID (used for output folder structure)
map-ids:
- '35813030' # Add your map_ids here (text type)
batch-ids:
- 252
- 253
- 257
output-base-dir: '/kamco-nfs/dataset/export/'
crs: 'EPSG:5186' # Korean 2000 / Central Belt
geoserver:
base-url: 'https://kamco.geo-dev.gs.dabeeo.com'
workspace: 'cd'
datastore: 'inference_result'
overwrite-existing: true # Delete existing layers before re-registering
connection-timeout: 30000 # 30 seconds
read-timeout: 60000 # 60 seconds
```
```md
## Converter Mode (`converter.mode`)
`converter.mode`는 대량 `map_ids` 처리 시
OS 커맨드라인 길이 제한(`Argument list too long`) 문제를 방지하기 위해 추가 하였습니다.
### Supported Modes
#### MERGED
- `batch-ids`에 해당하는 **모든 데이터를 하나의 Shapefile로 병합 생성**
- `map-ids`가 설정되어 있어도 **무시됨**
- 단일 결과 파일이 필요한 경우 적합
#### MAP_IDS
- 명시적으로 전달한 `map-ids`만 대상으로 Shapefile 생성
- `converter.map-ids` **필수**
- `map-ids` 개수가 많을 경우 OS 커맨드라인 길이 제한에 걸릴 수 있음
#### RESOLVE
- `batch-ids` 기준으로 **JAR 내부에서 map_ids를 조회**한 뒤 Shapefile 생성
- `map-ids`를 커맨드라인 인자로 전달하지 않음
- 대량 데이터 처리 시 가장 안전한 방식
### Default Behavior (mode 미지정 시)
- `converter.map-ids`가 비어 있으면 → **MERGED**
- `converter.map-ids`가 있으면 → **MAP_IDS**
### Command Line Parameters
You can override configuration values using command line arguments:
**Using Gradle (recommended - no quoting issues):**
```bash
./gradlew bootRun --args="--converter.inference-id=D5E46F60FC40B1A8BE0CD1F3547AA6 --converter.map-ids[0]=35813030 --converter.batch-ids[0]=252 --converter.batch-ids[1]=253 --converter.batch-ids[2]=257 --converter.mode=MERGED"
```
**Using JAR (zsh shell - quote arguments with brackets):**
```bash
java -jar build/libs/makesample-1.0.0.jar \
'--converter.inference-id=D5E46F60FC40B1A8BE0CD1F3547AA6' \
'--converter.map-ids[0]=35813030' \
'--converter.batch-ids[0]=252' \
'--converter.batch-ids[1]=253' \
'--converter.batch-ids[2]=257' \
'--converter.mode=MERGED'
```
**Using JAR (bash shell - no quotes needed):**
```bash
java -jar build/libs/makesample-1.0.0.jar \
--converter.inference-id=D5E46F60FC40B1A8BE0CD1F3547AA6 \
--converter.map-ids[0]=35813030 \
--converter.batch-ids[0]=252 \
--converter.batch-ids[1]=253 \
--converter.batch-ids[2]=257 \
--converter.mode=MERGED
```
**Note for zsh users:** zsh interprets square brackets `[]` as glob patterns. Always quote arguments containing brackets when using zsh.
## Building
```bash
./gradlew build
```
## Running
### Generate Shapefiles
```bash
./gradlew bootRun
```
Or run the JAR directly:
```bash
java -jar build/libs/makesample-1.0.0.jar
```
### Register Shapefile to GeoServer
First, set GeoServer credentials as environment variables:
```bash
export GEOSERVER_USERNAME=admin
export GEOSERVER_PASSWORD=geoserver
```
Then register a shapefile:
```bash
./gradlew bootRun --args="--upload-shp /kamco-nfs/dataset/export/D5E46F60FC40B1A8BE0CD1F3547AA6/35813030/35813030.shp --layer inference_35813030"
```
Or using the JAR:
```bash
java -jar build/libs/makesample-1.0.0.jar \
--upload-shp /path/to/shapefile.shp \
--layer layer_name
```
**GeoServer Registration Process:**
1. Verifies workspace 'cd' exists (must be pre-created in GeoServer)
2. Creates datastore 'inference_result' if it doesn't exist
3. Deletes existing layer if `overwrite-existing: true`
4. Publishes shapefile via REST API
5. Verifies successful registration
6. Automatically enables WMS, WFS, WMTS services
**Important Notes:**
- Workspace 'cd' must exist in GeoServer before registration
- Environment variables `GEOSERVER_USERNAME` and `GEOSERVER_PASSWORD` must be set
- Shapefile path must be absolute
- GeoServer must have file system access to the shapefile location
## Output
Shapefiles will be created in directories structured as `output-base-dir/inference-id/map-id/`:
```
/kamco-nfs/dataset/export/D5E46F60FC40B1A8BE0CD1F3547AA6/35813030/
├── 35813030.shp # Shapefile geometry
├── 35813030.shx # Shape index
├── 35813030.dbf # Attribute data
└── 35813030.prj # Projection information
```
## Database Query
The application executes the following query for each map_id:
```sql
SELECT uid, map_id, probability, before_year, after_year,
before_c, before_p, after_c, after_p, ST_AsText(geometry) as geometry_wkt
FROM inference_results_testing
WHERE batch_id = ANY(?) AND map_id = ?
```
### Database Schema
- **geometry**: `geometry(Polygon, 5186)` - EPSG:5186 좌표계
- **map_id**: `text` - 문자열 타입
- **before_year, after_year**: `bigint` - Long 타입
- **batch_id**: `bigint` - Long 타입
- **uid**: `uuid` - UUID 타입
## Field Mapping
Shapefile field names are limited to 10 characters:
| Database Column | DB Type | Shapefile Field | Shapefile Type |
|-----------------|----------|-----------------|----------------|
| uid | uuid | chnDtctId | String |
| map_id | text | mpqd_no | String |
| probability | float8 | chn_dtct_p | Double |
| before_year | bigint | cprs_yr | Long |
| after_year | bigint | crtr_yr | Long |
| before_c | text | bf_cls_cd | String |
| before_p | float8 | bf_cls_pro | Double |
| after_c | text | af_cls_cd | String |
| after_p | float8 | af_cls_pro | Double |
| geometry | geom | the_geom | Polygon |
## Error Handling
### Shapefile Generation
- **No results**: Logs warning and continues to next map_id
- **Mixed geometry types**: Throws exception (shapefiles require homogeneous geometry)
- **Database connection failure**: Application exits with error
- **Invalid geometry**: Logs warning and continues processing
### GeoServer Registration
- **Workspace not found**: Logs error with remediation steps (must be pre-created)
- **Authentication failure**: Logs error prompting to verify environment variables
- **Network timeout**: Logs connection error with timeout details
- **Layer already exists**: Automatically deletes and re-registers if `overwrite-existing: true`
- **Registration failure**: Logs error but does not stop application (non-blocking)
## Validating Output
### Shapefile Validation
Open the generated shapefiles in QGIS or ArcGIS to verify:
1. Geometry displays correctly
2. Attribute table contains all expected fields
3. CRS is EPSG:5186 (Korean 2000 / Central Belt)
### GeoServer Layer Validation
After registering to GeoServer, verify the layer:
1. **GeoServer Admin Console**: https://kamco.geo-dev.gs.dabeeo.com/geoserver/web
2. Navigate to **Layers** → Find your layer (e.g., `cd:inference_35813030`)
3. Preview the layer using **Layer Preview**
4. Verify services are enabled:
- WMS: `https://kamco.geo-dev.gs.dabeeo.com/geoserver/cd/wms`
- WFS: `https://kamco.geo-dev.gs.dabeeo.com/geoserver/cd/wfs`
- WMTS: `https://kamco.geo-dev.gs.dabeeo.com/geoserver/cd/wmts`
**Example WMS GetMap Request:**
```
https://kamco.geo-dev.gs.dabeeo.com/geoserver/cd/wms?
service=WMS&
version=1.1.0&
request=GetMap&
layers=cd:inference_35813030&
bbox=<bounds>&
width=768&
height=768&
srs=EPSG:5186&
format=image/png
```
## Development
### Code Formatting
The project uses Google Java Format with 2-space indentation:
```bash
./gradlew spotlessApply
```
### Project Structure
```
src/main/java/com/kamco/makesample/
├── MakeSampleApplication.java # Main application class
├── cli/
│ └── ConverterCommandLineRunner.java # CLI entry point
├── config/
│ ├── ConverterProperties.java # Shapefile converter configuration
│ ├── GeoServerProperties.java # GeoServer configuration
│ ├── GeoServerCredentials.java # GeoServer authentication
│ └── RestTemplateConfig.java # HTTP client configuration
├── exception/
│ ├── ShapefileConversionException.java
│ ├── GeometryConversionException.java
│ ├── MixedGeometryException.java
│ └── GeoServerRegistrationException.java # GeoServer registration errors
├── model/
│ └── InferenceResult.java # Domain model
├── repository/
│ └── InferenceResultRepository.java # Data access layer
├── service/
│ ├── GeometryConverter.java # PostGIS to JTS conversion
│ ├── ShapefileConverterService.java # Orchestration service
│ └── GeoServerRegistrationService.java # GeoServer REST API integration
└── writer/
├── ShapefileWriter.java # GeoTools shapefile writer
└── GeoJsonWriter.java # GeoJSON export writer
```
## Dependencies
- Spring Boot 3.5.7
- spring-boot-starter
- spring-boot-starter-jdbc
- spring-boot-starter-web (for RestTemplate)
- spring-boot-starter-validation (for @NotBlank annotations)
- GeoTools 30.0
- gt-shapefile
- gt-referencing
- gt-epsg-hsql
- gt-geojson
- PostgreSQL JDBC Driver
- PostGIS JDBC 2.5.1
- JTS (Java Topology Suite) 1.19.0
## Troubleshooting
### GeoServer Registration Issues
**Problem: "Workspace not found: cd"**
```
Solution: Create workspace 'cd' in GeoServer admin console before registration
Steps:
1. Login to GeoServer admin: https://kamco.geo-dev.gs.dabeeo.com/geoserver/web
2. Go to Workspaces → Add new workspace
3. Name: cd, Namespace URI: http://cd
4. Click Save
```
**Problem: "GeoServer credentials not configured"**
```
Solution: Set environment variables before running
export GEOSERVER_USERNAME=admin
export GEOSERVER_PASSWORD=geoserver
```
**Problem: "Layer already exists and overwrite is disabled"**
```
Solution: Enable overwrite in application.yml
geoserver:
overwrite-existing: true
```
**Problem: Connection timeout to GeoServer**
```
Solution: Increase timeout values in application.yml
geoserver:
connection-timeout: 60000 # 60 seconds
read-timeout: 120000 # 120 seconds
```
**Problem: "Registration failed. Layer not found after publication"**
```
Possible causes:
1. GeoServer cannot access shapefile path (check file system permissions)
2. Shapefile is corrupted or invalid
3. Network issue interrupted registration
Solution:
1. Verify GeoServer has read access to shapefile directory
2. Validate shapefile using QGIS or ogr2ogr
3. Check GeoServer logs for detailed error messages
```
### Database Connection Issues
**Problem: "Connection refused to 192.168.2.127:15432"**
```
Solution: Verify PostgreSQL is running and accessible
psql -h 192.168.2.127 -p 15432 -U kamco_cds -d kamco_cds
```
**Problem: "No results found for map_id"**
```
Solution: Verify data exists in database
SELECT COUNT(*) FROM inference_results_testing
WHERE batch_id IN (252, 253, 257) AND map_id = '35813030';
```
## License
KAMCO Internal Use Only

View File

@@ -1,88 +0,0 @@
plugins {
id 'java'
id 'org.springframework.boot' version '3.5.7'
id 'io.spring.dependency-management' version '1.1.7'
id 'com.diffplug.spotless' version '6.25.0'
}
group = 'com.kamco'
version = '1.0.0'
java {
toolchain {
languageVersion = JavaLanguageVersion.of(17)
}
}
repositories {
mavenCentral()
maven {
url 'https://repo.osgeo.org/repository/release/'
}
maven {
url 'https://repo.osgeo.org/repository/geotools-releases/'
}
maven {
url 'https://repo.osgeo.org/repository/snapshot/'
}
}
ext {
geoToolsVersion = '30.0'
}
configurations.all {
exclude group: 'javax.media', module: 'jai_core'
}
bootJar {
archiveFileName = "shp-exporter.jar"
}
jar {
enabled = false // plain.jar 안 만들기(혼동 방지)
}
dependencies {
// Spring Boot
implementation 'org.springframework.boot:spring-boot-starter'
implementation 'org.springframework.boot:spring-boot-starter-jdbc'
implementation 'org.springframework.boot:spring-boot-starter-web'
implementation 'org.springframework.boot:spring-boot-starter-validation'
// Database
implementation 'org.postgresql:postgresql'
implementation 'com.zaxxer:HikariCP'
// PostGIS
implementation 'net.postgis:postgis-jdbc:2.5.1'
// JTS Geometry
implementation 'org.locationtech.jts:jts-core:1.19.0'
// GeoTools
implementation "org.geotools:gt-shapefile:${geoToolsVersion}"
implementation "org.geotools:gt-referencing:${geoToolsVersion}"
implementation "org.geotools:gt-epsg-hsql:${geoToolsVersion}"
implementation "org.geotools:gt-geojson:${geoToolsVersion}"
// Logging
implementation 'org.slf4j:slf4j-api'
// Testing
testImplementation 'org.springframework.boot:spring-boot-starter-test'
testRuntimeOnly 'org.junit.platform:junit-platform-launcher'
}
spotless {
java {
googleJavaFormat('1.19.2')
indentWithSpaces(2)
trimTrailingWhitespace()
endWithNewline()
}
}
tasks.named('test') {
useJUnitPlatform()
}

File diff suppressed because one or more lines are too long

View File

@@ -1 +0,0 @@
com.kamco.makesample.MakeSampleApplication

View File

@@ -1,3 +0,0 @@
spring:
application:
name: make-shapefile-service

View File

@@ -1,12 +0,0 @@
Manifest-Version: 1.0
Main-Class: org.springframework.boot.loader.launch.JarLauncher
Start-Class: com.kamco.makesample.MakeSampleApplication
Spring-Boot-Version: 3.5.7
Spring-Boot-Classes: BOOT-INF/classes/
Spring-Boot-Lib: BOOT-INF/lib/
Spring-Boot-Classpath-Index: BOOT-INF/classpath.idx
Spring-Boot-Layers-Index: BOOT-INF/layers.idx
Build-Jdk-Spec: 17
Implementation-Title: shp-exporter
Implementation-Version: 1.0.0

Binary file not shown.

View File

@@ -1,7 +0,0 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-8.14.3-bin.zip
networkTimeout=10000
validateDistributionUrl=true
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists

251
shp-exporter/gradlew vendored
View File

@@ -1,251 +0,0 @@
#!/bin/sh
#
# Copyright © 2015-2021 the original authors.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# https://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
# SPDX-License-Identifier: Apache-2.0
#
##############################################################################
#
# Gradle start up script for POSIX generated by Gradle.
#
# Important for running:
#
# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is
# noncompliant, but you have some other compliant shell such as ksh or
# bash, then to run this script, type that shell name before the whole
# command line, like:
#
# ksh Gradle
#
# Busybox and similar reduced shells will NOT work, because this script
# requires all of these POSIX shell features:
# * functions;
# * expansions «$var», «${var}», «${var:-default}», «${var+SET}»,
# «${var#prefix}», «${var%suffix}», and «$( cmd )»;
# * compound commands having a testable exit status, especially «case»;
# * various built-in commands including «command», «set», and «ulimit».
#
# Important for patching:
#
# (2) This script targets any POSIX shell, so it avoids extensions provided
# by Bash, Ksh, etc; in particular arrays are avoided.
#
# The "traditional" practice of packing multiple parameters into a
# space-separated string is a well documented source of bugs and security
# problems, so this is (mostly) avoided, by progressively accumulating
# options in "$@", and eventually passing that to Java.
#
# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS,
# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly;
# see the in-line comments for details.
#
# There are tweaks for specific operating systems such as AIX, CygWin,
# Darwin, MinGW, and NonStop.
#
# (3) This script is generated from the Groovy template
# https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
# within the Gradle project.
#
# You can find Gradle at https://github.com/gradle/gradle/.
#
##############################################################################
# Attempt to set APP_HOME
# Resolve links: $0 may be a link
app_path=$0
# Need this for daisy-chained symlinks.
while
APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path
[ -h "$app_path" ]
do
ls=$( ls -ld "$app_path" )
link=${ls#*' -> '}
case $link in #(
/*) app_path=$link ;; #(
*) app_path=$APP_HOME$link ;;
esac
done
# This is normally unused
# shellcheck disable=SC2034
APP_BASE_NAME=${0##*/}
# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036)
APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s\n' "$PWD" ) || exit
# Use the maximum available, or set MAX_FD != -1 to use that value.
MAX_FD=maximum
warn () {
echo "$*"
} >&2
die () {
echo
echo "$*"
echo
exit 1
} >&2
# OS specific support (must be 'true' or 'false').
cygwin=false
msys=false
darwin=false
nonstop=false
case "$( uname )" in #(
CYGWIN* ) cygwin=true ;; #(
Darwin* ) darwin=true ;; #(
MSYS* | MINGW* ) msys=true ;; #(
NONSTOP* ) nonstop=true ;;
esac
CLASSPATH="\\\"\\\""
# Determine the Java command to use to start the JVM.
if [ -n "$JAVA_HOME" ] ; then
if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
# IBM's JDK on AIX uses strange locations for the executables
JAVACMD=$JAVA_HOME/jre/sh/java
else
JAVACMD=$JAVA_HOME/bin/java
fi
if [ ! -x "$JAVACMD" ] ; then
die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
fi
else
JAVACMD=java
if ! command -v java >/dev/null 2>&1
then
die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
fi
fi
# Increase the maximum file descriptors if we can.
if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
case $MAX_FD in #(
max*)
# In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked.
# shellcheck disable=SC2039,SC3045
MAX_FD=$( ulimit -H -n ) ||
warn "Could not query maximum file descriptor limit"
esac
case $MAX_FD in #(
'' | soft) :;; #(
*)
# In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked.
# shellcheck disable=SC2039,SC3045
ulimit -n "$MAX_FD" ||
warn "Could not set maximum file descriptor limit to $MAX_FD"
esac
fi
# Collect all arguments for the java command, stacking in reverse order:
# * args from the command line
# * the main class name
# * -classpath
# * -D...appname settings
# * --module-path (only if needed)
# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables.
# For Cygwin or MSYS, switch paths to Windows format before running java
if "$cygwin" || "$msys" ; then
APP_HOME=$( cygpath --path --mixed "$APP_HOME" )
CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" )
JAVACMD=$( cygpath --unix "$JAVACMD" )
# Now convert the arguments - kludge to limit ourselves to /bin/sh
for arg do
if
case $arg in #(
-*) false ;; # don't mess with options #(
/?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath
[ -e "$t" ] ;; #(
*) false ;;
esac
then
arg=$( cygpath --path --ignore --mixed "$arg" )
fi
# Roll the args list around exactly as many times as the number of
# args, so each arg winds up back in the position where it started, but
# possibly modified.
#
# NB: a `for` loop captures its iteration list before it begins, so
# changing the positional parameters here affects neither the number of
# iterations, nor the values presented in `arg`.
shift # remove old arg
set -- "$@" "$arg" # push replacement arg
done
fi
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
# Collect all arguments for the java command:
# * DEFAULT_JVM_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments,
# and any embedded shellness will be escaped.
# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be
# treated as '${Hostname}' itself on the command line.
set -- \
"-Dorg.gradle.appname=$APP_BASE_NAME" \
-classpath "$CLASSPATH" \
-jar "$APP_HOME/gradle/wrapper/gradle-wrapper.jar" \
"$@"
# Stop when "xargs" is not available.
if ! command -v xargs >/dev/null 2>&1
then
die "xargs is not available"
fi
# Use "xargs" to parse quoted args.
#
# With -n1 it outputs one arg per line, with the quotes and backslashes removed.
#
# In Bash we could simply go:
#
# readarray ARGS < <( xargs -n1 <<<"$var" ) &&
# set -- "${ARGS[@]}" "$@"
#
# but POSIX shell has neither arrays nor command substitution, so instead we
# post-process each arg (as a line of input to sed) to backslash-escape any
# character that might be a shell metacharacter, then use eval to reverse
# that process (while maintaining the separation between arguments), and wrap
# the whole thing up as a single "set" statement.
#
# This will of course break if any of these variables contains a newline or
# an unmatched quote.
#
eval "set -- $(
printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" |
xargs -n1 |
sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' |
tr '\n' ' '
)" '"$@"'
exec "$JAVACMD" "$@"

View File

@@ -1,94 +0,0 @@
@rem
@rem Copyright 2015 the original author or authors.
@rem
@rem Licensed under the Apache License, Version 2.0 (the "License");
@rem you may not use this file except in compliance with the License.
@rem You may obtain a copy of the License at
@rem
@rem https://www.apache.org/licenses/LICENSE-2.0
@rem
@rem Unless required by applicable law or agreed to in writing, software
@rem distributed under the License is distributed on an "AS IS" BASIS,
@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
@rem See the License for the specific language governing permissions and
@rem limitations under the License.
@rem
@rem SPDX-License-Identifier: Apache-2.0
@rem
@if "%DEBUG%"=="" @echo off
@rem ##########################################################################
@rem
@rem Gradle startup script for Windows
@rem
@rem ##########################################################################
@rem Set local scope for the variables with windows NT shell
if "%OS%"=="Windows_NT" setlocal
set DIRNAME=%~dp0
if "%DIRNAME%"=="" set DIRNAME=.
@rem This is normally unused
set APP_BASE_NAME=%~n0
set APP_HOME=%DIRNAME%
@rem Resolve any "." and ".." in APP_HOME to make it shorter.
for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
@rem Find java.exe
if defined JAVA_HOME goto findJavaFromJavaHome
set JAVA_EXE=java.exe
%JAVA_EXE% -version >NUL 2>&1
if %ERRORLEVEL% equ 0 goto execute
echo. 1>&2
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2
echo. 1>&2
echo Please set the JAVA_HOME variable in your environment to match the 1>&2
echo location of your Java installation. 1>&2
goto fail
:findJavaFromJavaHome
set JAVA_HOME=%JAVA_HOME:"=%
set JAVA_EXE=%JAVA_HOME%/bin/java.exe
if exist "%JAVA_EXE%" goto execute
echo. 1>&2
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2
echo. 1>&2
echo Please set the JAVA_HOME variable in your environment to match the 1>&2
echo location of your Java installation. 1>&2
goto fail
:execute
@rem Setup the command line
set CLASSPATH=
@rem Execute Gradle
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %*
:end
@rem End local scope for the variables with windows NT shell
if %ERRORLEVEL% equ 0 goto mainEnd
:fail
rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
rem the _cmd.exe /c_ return code!
set EXIT_CODE=%ERRORLEVEL%
if %EXIT_CODE% equ 0 set EXIT_CODE=1
if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE%
exit /b %EXIT_CODE%
:mainEnd
if "%OS%"=="Windows_NT" endlocal
:omega

File diff suppressed because one or more lines are too long

View File

@@ -1 +0,0 @@
PROJCS["Korea 2000 / Central Belt 2010", GEOGCS["Korea 2000", DATUM["Geocentric datum of Korea", SPHEROID["GRS 1980", 6378137.0, 298.257222101, AUTHORITY["EPSG","7019"]], TOWGS84[0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0], AUTHORITY["EPSG","6737"]], PRIMEM["Greenwich", 0.0, AUTHORITY["EPSG","8901"]], UNIT["degree", 0.017453292519943295], AXIS["Geodetic latitude", NORTH], AXIS["Geodetic longitude", EAST], AUTHORITY["EPSG","4737"]], PROJECTION["Transverse_Mercator", AUTHORITY["EPSG","9807"]], PARAMETER["central_meridian", 127.0], PARAMETER["latitude_of_origin", 38.0], PARAMETER["scale_factor", 1.0], PARAMETER["false_easting", 200000.0], PARAMETER["false_northing", 600000.0], UNIT["m", 1.0], AXIS["Northing", NORTH], AXIS["Easting", EAST], AUTHORITY["EPSG","5186"]]

Some files were not shown because too many files have changed in this diff Show More