60 Commits

Author SHA1 Message Date
650b1695f0 Merge pull request '미사용 목록 주석 추가, 학습데이터 삭제 테스트' (#178) from feat/training_260324 into develop
Reviewed-on: #178
2026-03-27 18:02:34 +09:00
960e4215e0 미사용 목록 주석 추가, 학습데이터 삭제 테스트 2026-03-27 18:02:13 +09:00
08a220db4d Merge pull request '미사용 목록 주석 추가, 납품데이터 폴더 벨리데이션 위치 옮김' (#177) from feat/training_260324 into develop
Reviewed-on: #177
2026-03-27 17:04:25 +09:00
a9b49faa6c 미사용 목록 주석 추가, 납품데이터 폴더 벨리데이션 위치 옮김 2026-03-27 17:03:52 +09:00
b760e9874c Merge pull request '납품데이터셋 업로드 geojson 파일 수정' (#176) from feat/training_260324 into develop
Reviewed-on: #176
2026-03-27 10:56:57 +09:00
8698bf61d1 납품데이터셋 업로드 geojson 파일 수정 2026-03-27 10:56:38 +09:00
680e137284 Merge pull request '납품데이터셋 업로드 geojson 파일 수정' (#175) from feat/training_260324 into develop
Reviewed-on: #175
2026-03-27 10:18:15 +09:00
f4a81a34d6 납품데이터셋 업로드 geojson 파일 수정 2026-03-27 10:17:58 +09:00
479ad710e0 Merge pull request '납품데이터셋 업로드 geojson 파일 수정' (#174) from feat/training_260324 into develop
Reviewed-on: #174
2026-03-27 10:15:32 +09:00
3cb9840248 납품데이터셋 업로드 geojson 파일 수정 2026-03-27 10:14:44 +09:00
fc9543f195 Merge pull request 'feat/training_260324' (#173) from feat/training_260324 into develop
Reviewed-on: #173
2026-03-27 10:05:24 +09:00
73d0e03b08 납품데이터셋 업로드 geojson 파일 수정 2026-03-27 10:04:56 +09:00
50c965cb79 학습결과 파일 베스트 에폭 제외 삭제 추가, 납품데이터 등록 비동기 수정 2026-03-27 09:32:49 +09:00
dean
abca9467d8 docker 2026-03-26 20:07:37 +09:00
dean
4ed03f6e94 docker 2026-03-26 11:26:46 +09:00
04eddfce54 학습결과 파일 베스트 에폭 제외 삭제 2026-03-25 17:56:36 +09:00
f49b7cc850 Merge pull request '납품데이터 등록 수정' (#172) from feat/training_260324 into develop
Reviewed-on: #172
2026-03-25 12:52:25 +09:00
888c0e314b 납품데이터 등록 수정 2026-03-25 12:50:28 +09:00
6c043b0031 Merge pull request '납품데이터 등록 수정' (#171) from feat/training_260324 into develop
Reviewed-on: #171
2026-03-25 12:38:56 +09:00
531da09c5f 납품데이터 등록 수정 2026-03-25 12:37:27 +09:00
4da2a1f0d7 Merge pull request 'spotlessApply 적용' (#170) from feat/training_260324 into develop
Reviewed-on: #170
2026-03-25 12:33:04 +09:00
50b3f1ba62 spotlessApply 적용 2026-03-25 12:32:50 +09:00
f1f88c83e1 Merge pull request '납품 데이터 등록 api 추가' (#169) from feat/training_260324 into develop
Reviewed-on: #169
2026-03-25 12:30:51 +09:00
ff478452a6 납품 데이터 등록 api 추가 2026-03-25 12:30:30 +09:00
bf77725ef8 Merge pull request '납품 폴더 조회 api 추가' (#168) from feat/training_260324 into develop
Reviewed-on: #168
2026-03-24 17:30:18 +09:00
dec0f26999 납품 폴더 조회 api 추가 2026-03-24 17:30:04 +09:00
dfd4a42379 Merge pull request '납품 폴더 조회 api 추가' (#167) from feat/training_260324 into develop
Reviewed-on: #167
2026-03-24 17:19:39 +09:00
79272137ab 납품 폴더 조회 api 추가 2026-03-24 17:19:06 +09:00
73ea6176b4 Merge pull request '시스템 사용율 모니터링 기능 로그 수정' (#166) from feat/training_260303 into develop
Reviewed-on: #166
2026-03-19 17:38:45 +09:00
3d2a4049d3 시스템 사용율 모니터링 기능 로그 수정 2026-03-19 17:38:30 +09:00
26caf505b9 Merge pull request '시스템 사용율 모니터링 기능 추가' (#165) from feat/training_260303 into develop
Reviewed-on: #165
2026-03-19 17:09:08 +09:00
0cbaf53e86 시스템 사용율 모니터링 기능 추가 2026-03-19 17:08:37 +09:00
bd54854bc6 Merge pull request '시스템 사용율 모니터링 기능 테스트' (#164) from feat/training_260303 into develop
Reviewed-on: #164
2026-03-19 16:52:44 +09:00
80fd2bda3e 시스템 사용율 모니터링 기능 테스트 2026-03-19 16:52:26 +09:00
ca3d115d0e Merge pull request '시스템 사용율 모니터링 기능 테스트' (#163) from feat/training_260303 into develop
Reviewed-on: #163
2026-03-19 16:36:56 +09:00
fb647e5991 시스템 사용율 모니터링 기능 테스트 2026-03-19 16:36:39 +09:00
831ba3e616 Merge pull request '시스템 사용율 모니터링 기능 테스트' (#162) from feat/training_260303 into develop
Reviewed-on: #162
2026-03-19 16:27:19 +09:00
87575a62f7 시스템 사용율 모니터링 기능 테스트 2026-03-19 16:27:05 +09:00
a4b5e20db2 Merge pull request '시스템 사용율 모니터링 기능 테스트' (#161) from feat/training_260303 into develop
Reviewed-on: #161
2026-03-19 16:12:45 +09:00
246c11f8b0 시스템 사용율 모니터링 기능 테스트 2026-03-19 16:12:27 +09:00
da260f35ea Merge pull request '시스템 사용율 모니터링 기능 테스트' (#160) from feat/training_260303 into develop
Reviewed-on: #160
2026-03-19 15:46:40 +09:00
2c1f9bdf5c 시스템 사용율 모니터링 기능 테스트 2026-03-19 15:46:23 +09:00
6cf81bf60f Merge pull request '시스템 사용율 모니터링 기능 테스트' (#159) from feat/training_260303 into develop
Reviewed-on: #159
2026-03-19 15:41:34 +09:00
5799f7dfb2 시스템 사용율 모니터링 기능 테스트 2026-03-19 15:41:18 +09:00
ed95829a34 Merge pull request '시스템 사용율 모니터링 기능 테스트' (#158) from feat/training_260303 into develop
Reviewed-on: #158
2026-03-19 15:37:47 +09:00
9f428e9572 시스템 사용율 모니터링 기능 테스트 2026-03-19 15:37:26 +09:00
52ffe53815 Merge pull request 'feat/training_260303' (#157) from feat/training_260303 into develop
Reviewed-on: #157
2026-03-19 15:25:37 +09:00
904968a1be 시스템 사용율 모니터링 기능 테스트 2026-03-19 15:25:20 +09:00
4b44be6a29 시스템 사용율 모니터링 기능 테스트 2026-03-19 15:25:11 +09:00
5887a954ea Merge pull request '시스템 사용율 모니터링 기능 테스트' (#156) from feat/training_260303 into develop
Reviewed-on: #156
2026-03-19 15:24:38 +09:00
f9f0662f8e 시스템 사용율 모니터링 기능 테스트 2026-03-19 15:24:03 +09:00
dean
72bc2fd47b test 2026-03-17 21:02:21 +09:00
7416327cc3 ai 학습실행 run command 수정 2026-03-11 10:18:33 +09:00
da31bd9d99 ai 학습실행 run command 수정 2026-03-10 23:09:39 +09:00
f3e5347335 ai 학습실행 run command 수정 2026-03-10 22:48:10 +09:00
7d5581f60c 데이터셋 삭제 플래그 추가 2026-03-10 19:09:00 +09:00
b4428217ea hello 2026-03-10 18:01:03 +09:00
8a63fdacdd confict 2026-03-10 17:27:14 +09:00
cb2e42143a confict 2026-03-10 17:23:27 +09:00
997e85c0cc 운영환경처리 2026-03-10 17:22:27 +09:00
64 changed files with 2016 additions and 102 deletions

View File

@@ -5,6 +5,13 @@ services:
dockerfile: Dockerfile-dev dockerfile: Dockerfile-dev
image: kamco-cd-training-api:${IMAGE_TAG:-latest} image: kamco-cd-training-api:${IMAGE_TAG:-latest}
container_name: kamco-cd-training-api container_name: kamco-cd-training-api
deploy:
resources:
reservations:
devices:
- driver: nvidia
count: all
capabilities: [gpu]
ports: ports:
- "7200:8080" - "7200:8080"
environment: environment:

View File

@@ -5,6 +5,13 @@ services:
dockerfile: Dockerfile dockerfile: Dockerfile
image: kamco-train-api:${IMAGE_TAG:-latest} image: kamco-train-api:${IMAGE_TAG:-latest}
container_name: kamco-train-api container_name: kamco-train-api
deploy:
resources:
reservations:
devices:
- driver: nvidia
count: all
capabilities: [gpu]
expose: expose:
- "8080" - "8080"
environment: environment:

View File

@@ -125,6 +125,7 @@ public class CommonCodeService {
return commonCodeCoreService.getCode(parentCodeCd, childCodeCd); return commonCodeCoreService.getCode(parentCodeCd, childCodeCd);
} }
// TODO 미사용시작
/** /**
* 공통코드 이름 조회 * 공통코드 이름 조회
* *
@@ -136,6 +137,8 @@ public class CommonCodeService {
return commonCodeCoreService.getCode(parentCodeCd, childCodeCd); return commonCodeCoreService.getCode(parentCodeCd, childCodeCd);
} }
// TODO 미사용 끝
public List<CodeDto> getTypeCode(String type) { public List<CodeDto> getTypeCode(String type) {
return Enums.getCodes(type); return Enums.getCodes(type);
} }

View File

@@ -1,3 +1,4 @@
// TODO 미사용시작
package com.kamco.cd.training.common.download; package com.kamco.cd.training.common.download;
import com.kamco.cd.training.common.download.dto.DownloadSpec; import com.kamco.cd.training.common.download.dto.DownloadSpec;
@@ -46,3 +47,4 @@ public class DownloadExecutor {
.body(body); .body(body);
} }
} }
// TODO 미사용 끝

View File

@@ -1,3 +1,4 @@
// TODO 미사용시작
package com.kamco.cd.training.common.download; package com.kamco.cd.training.common.download;
import org.springframework.util.AntPathMatcher; import org.springframework.util.AntPathMatcher;
@@ -17,3 +18,4 @@ public final class DownloadPaths {
return false; return false;
} }
} }
// TODO 미사용 끝

View File

@@ -0,0 +1,8 @@
package com.kamco.cd.training.common.dto;
public class MonitorDto {
public int cpu; // CPU 사용률 (%)
public long[] memory; // "사용/전체"
public int gpu; // 🔥 전체 GPU 평균 (%)
}

View File

@@ -1,3 +1,4 @@
// TODO 미사용시작
package com.kamco.cd.training.common.enums; package com.kamco.cd.training.common.enums;
import com.kamco.cd.training.common.utils.enums.CodeExpose; import com.kamco.cd.training.common.utils.enums.CodeExpose;
@@ -17,3 +18,4 @@ public enum DeployTargetType implements EnumType {
private final String id; private final String id;
private final String text; private final String text;
} }
// TODO 미사용 끝

View File

@@ -1,3 +1,4 @@
// TODO 미사용시작
package com.kamco.cd.training.common.enums; package com.kamco.cd.training.common.enums;
import com.kamco.cd.training.common.utils.enums.CodeExpose; import com.kamco.cd.training.common.utils.enums.CodeExpose;
@@ -25,3 +26,4 @@ public enum ModelMngStatusType implements EnumType {
return desc; return desc;
} }
} }
// TODO 미사용 끝

View File

@@ -1,3 +1,4 @@
// TODO 미사용시작
package com.kamco.cd.training.common.enums; package com.kamco.cd.training.common.enums;
import com.kamco.cd.training.common.utils.enums.CodeExpose; import com.kamco.cd.training.common.utils.enums.CodeExpose;
@@ -18,3 +19,4 @@ public enum ProcessStepType implements EnumType {
private final String id; private final String id;
private final String text; private final String text;
} }
// TODO 미사용 끝

View File

@@ -0,0 +1,142 @@
package com.kamco.cd.training.common.service;
import jakarta.annotation.PostConstruct;
import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import lombok.extern.log4j.Log4j2;
import org.springframework.stereotype.Component;
@Component
@Log4j2
public class GpuDmonReader {
// =========================
// GPU 사용률 저장소
// key: GPU index (0,1,2...)
// value: 현재 GPU 사용률 (%)
// ConcurrentHashMap → 멀티스레드 안전
// =========================
private final Map<Integer, Integer> gpuUtilMap = new ConcurrentHashMap<>();
// =========================
// 외부 조회용
// SystemMonitorService에서 호출
// =========================
public Map<Integer, Integer> getGpuUtilMap() {
return gpuUtilMap;
}
// =========================
// Bean 초기화 시 실행
// - 별도 스레드에서 GPU 모니터링 시작
// - 메인 스레드 block 방지
// =========================
@PostConstruct
public void start() {
// nvidia-smi 없는 환경이면 GPU 모니터링 비활성화
if (!isNvidiaAvailable()) {
log.warn("nvidia-smi not found. GPU monitoring disabled.");
return;
}
// 데몬 스레드로 실행 (서버 종료 시 자동 종료)
Thread t = new Thread(this::runLoop, "gpu-dmon-thread");
t.setDaemon(true);
t.start();
}
// =========================
// 무한 루프
// - dmon 실행
// - 죽으면 자동 재시작
// =========================
private void runLoop() {
while (true) {
try {
runDmon(); // GPU 사용률 수집 시작
} catch (Exception e) {
// dmon 프로세스 종료되면 여기로 들어옴
log.warn("dmon restart: {}", e.getMessage());
}
// 5초 대기 후 재시작
sleep(5000);
}
}
// =========================
// nvidia-smi dmon 실행
// - GPU 사용률 스트리밍으로 계속 수신
// =========================
private void runDmon() throws Exception {
// -s u → GPU utilization만 출력
ProcessBuilder pb = new ProcessBuilder("nvidia-smi", "dmon", "-s", "u");
// 프로세스 실행 후 stdout 읽기
try (BufferedReader br =
new BufferedReader(new InputStreamReader(pb.start().getInputStream()))) {
String line;
// dmon은 계속 출력됨 (스트리밍)
while ((line = br.readLine()) != null) {
// 헤더 제거 (#로 시작)
if (line.startsWith("#")) continue;
line = line.trim();
if (line.isEmpty()) continue;
// 공백 기준 분리
String[] parts = line.split("\\s+");
// 첫 번째 값이 GPU index인지 확인
if (!parts[0].matches("\\d+")) continue;
int index = Integer.parseInt(parts[0]);
try {
// 두 번째 값이 GPU 사용률 (sm)
int util = Integer.parseInt(parts[1]);
// 최신 값 갱신
gpuUtilMap.put(index, util);
} catch (Exception ignored) {
// 파싱 실패 시 무시
}
}
}
// 여기까지 왔다는 건 dmon 프로세스 종료됨
// → runLoop에서 재시작하도록 예외 발생
throw new IllegalStateException("dmon stopped");
}
// =========================
// nvidia-smi 존재 여부 확인
// =========================
private boolean isNvidiaAvailable() {
try {
Process p = new ProcessBuilder("which", "nvidia-smi").start();
return p.waitFor() == 0;
} catch (Exception e) {
return false;
}
}
// =========================
// sleep 유틸
// =========================
private void sleep(long ms) {
try {
Thread.sleep(ms);
} catch (InterruptedException ignored) {
}
}
}

View File

@@ -0,0 +1,224 @@
package com.kamco.cd.training.common.service;
import com.kamco.cd.training.common.dto.MonitorDto;
import java.io.BufferedReader;
import java.io.FileReader;
import java.util.ArrayDeque;
import java.util.Deque;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import lombok.RequiredArgsConstructor;
import lombok.extern.log4j.Log4j2;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Service;
@Service
@RequiredArgsConstructor
@Log4j2
public class SystemMonitorService {
// =========================
// CPU 이전값 (delta 계산용)
// - /proc/stat은 누적값이기 때문에
// - 이전 값과 비교해서 사용률 계산
// =========================
private long prevIdle = 0;
private long prevTotal = 0;
// =========================
// 최근 30초 히스토리
// - CPU: 30개 (1초 * 30)
// - GPU: GPU별 30개
// =========================
private final Deque<Double> cpuHistory = new ArrayDeque<>();
// key: GPU index
// value: 최근 30개 사용률
private final Map<Integer, Deque<Integer>> gpuHistory = new ConcurrentHashMap<>();
// =========================
// GPU 데이터 제공 (dmon reader)
// =========================
private final GpuDmonReader gpuReader;
// =========================
// 캐시 (API 응답용)
// - 매 요청마다 계산하지 않기 위해 사용
// - volatile → 멀티스레드 안전하게 최신값 유지
// =========================
private volatile MonitorDto cached = new MonitorDto();
// =========================
// 1초마다 수집
// =========================
@Scheduled(fixedRate = 1000)
public void collect() {
try {
// =====================
// 1. CPU 수집
// =====================
double cpu = readCpu();
cpuHistory.add(cpu);
// 30개 유지 (rolling window)
if (cpuHistory.size() > 30) cpuHistory.poll();
// =====================
// 2. GPU 수집
// =====================
Map<Integer, Integer> gpuMap = gpuReader.getGpuUtilMap();
for (Map.Entry<Integer, Integer> entry : gpuMap.entrySet()) {
int index = entry.getKey();
int util = entry.getValue();
// GPU별 히스토리 생성 및 추가
gpuHistory.computeIfAbsent(index, k -> new ArrayDeque<>()).add(util);
// 30개 유지
Deque<Integer> q = gpuHistory.get(index);
if (q.size() > 30) q.poll();
}
// =====================
// 3. 캐시 업데이트
// =====================
updateCache();
} catch (Exception e) {
log.error("collect error", e);
}
}
// =========================
// CPU 사용률 계산
// - /proc/stat 사용
// - 이전값과의 차이로 계산 (delta 방식)
// =========================
private double readCpu() throws Exception {
if (!isLinux()) return 0;
try (BufferedReader br = new BufferedReader(new FileReader("/proc/stat"))) {
String[] p = br.readLine().split("\\s+");
long user = Long.parseLong(p[1]);
long nice = Long.parseLong(p[2]);
long system = Long.parseLong(p[3]);
long idle = Long.parseLong(p[4]);
long iowait = Long.parseLong(p[5]);
long irq = Long.parseLong(p[6]);
long softirq = Long.parseLong(p[7]);
long total = user + nice + system + idle + iowait + irq + softirq;
long idleAll = idle + iowait;
// 최초 실행 시 기준값만 저장
if (prevTotal == 0) {
prevTotal = total;
prevIdle = idleAll;
return 0;
}
long totalDiff = total - prevTotal;
long idleDiff = idleAll - prevIdle;
prevTotal = total;
prevIdle = idleAll;
if (totalDiff == 0) return 0;
// CPU 사용률 (%)
return (1.0 - (double) idleDiff / totalDiff) * 100;
}
}
// =========================
// Linux 환경 체크
// =========================
private boolean isLinux() {
return System.getProperty("os.name").toLowerCase().contains("linux");
}
// =========================
// Memory 조회 (/proc/meminfo)
// - OS 값 그대로 사용 (kB)
// - [사용량, 전체]
// =========================
private long[] readMemory() throws Exception {
if (!isLinux()) return new long[] {0, 0};
try (BufferedReader br = new BufferedReader(new FileReader("/proc/meminfo"))) {
long total = 0;
long available = 0;
String line;
while ((line = br.readLine()) != null) {
if (line.startsWith("MemTotal")) {
total = Long.parseLong(line.replaceAll("\\D+", ""));
} else if (line.startsWith("MemAvailable")) {
available = Long.parseLong(line.replaceAll("\\D+", ""));
}
}
long used = total - available;
return new long[] {used, total};
}
}
// =========================
// 캐시 업데이트
// - CPU: 30초 평균
// - GPU: 전체 샘플 평균
// - Memory: 현재값
// =========================
private void updateCache() throws Exception {
MonitorDto dto = new MonitorDto();
// =====================
// CPU 평균 (30초)
// =====================
dto.cpu = (int) cpuHistory.stream().mapToDouble(Double::doubleValue).average().orElse(0);
// =====================
// Memory (kB 그대로)
// =====================
dto.memory = readMemory();
// =====================
// GPU 평균 (🔥 전체 샘플 기준)
// =====================
int sum = 0;
int count = 0;
for (Deque<Integer> q : gpuHistory.values()) {
for (int v : q) {
sum += v;
count++;
}
}
dto.gpu = (count == 0) ? 0 : sum / count;
// =====================
// 캐시 교체 (atomic)
// =====================
this.cached = dto;
}
// =========================
// 외부 조회
// - Controller에서 호출
// =========================
public MonitorDto get() {
return cached;
}
}

View File

@@ -20,4 +20,18 @@ public class AsyncConfig {
executor.initialize(); executor.initialize();
return executor; return executor;
} }
@Bean("datasetExecutor")
public Executor datasetExecutor() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
executor.setCorePoolSize(5);
executor.setMaxPoolSize(10);
executor.setQueueCapacity(100);
executor.setThreadNamePrefix("dataset-");
executor.initialize();
return executor;
}
} }

View File

@@ -24,7 +24,7 @@ public class OpenApiConfig {
@Value("${swagger.dev-url:https://kamco.training-dev-api.gs.dabeeo.com}") @Value("${swagger.dev-url:https://kamco.training-dev-api.gs.dabeeo.com}")
private String devUrl; private String devUrl;
@Value("${swagger.prod-url:https://api.training-kamco.com}") @Value("${swagger.prod-url:https://api.train-kamco.com}")
private String prodUrl; private String prodUrl;
@Bean @Bean
@@ -53,10 +53,10 @@ public class OpenApiConfig {
} else if ("prod".equals(profile)) { } else if ("prod".equals(profile)) {
// servers.add(new Server().url(prodUrl).description("운영 서버")); // servers.add(new Server().url(prodUrl).description("운영 서버"));
servers.add(new Server().url("http://localhost:" + localPort).description("로컬 서버")); servers.add(new Server().url("http://localhost:" + localPort).description("로컬 서버"));
servers.add(new Server().url(devUrl).description("개발 서버")); servers.add(new Server().url(prodUrl).description("개발 서버"));
} else { } else {
servers.add(new Server().url("http://localhost:" + localPort).description("로컬 서버")); servers.add(new Server().url("http://localhost:" + localPort).description("로컬 서버"));
servers.add(new Server().url(devUrl).description("개발 서버")); servers.add(new Server().url(devUrl).description("운영 서버"));
// servers.add(new Server().url(prodUrl).description("운영 서버")); // servers.add(new Server().url(prodUrl).description("운영 서버"));
} }

View File

@@ -57,7 +57,7 @@ public class StartupLogger {
""" """
╔════════════════════════════════════════════════════════════════════════════════╗ ╔════════════════════════════════════════════════════════════════════════════════╗
║ 🚀 APPLICATION STARTUP INFORMATION ║ 🚀 APPLICATION STARTUP INFORMATION 2
╠════════════════════════════════════════════════════════════════════════════════╣ ╠════════════════════════════════════════════════════════════════════════════════╣
║ PROFILE CONFIGURATION ║ ║ PROFILE CONFIGURATION ║
╠────────────────────────────────────────────────────────────────────────────────╣ ╠────────────────────────────────────────────────────────────────────────────────╣

View File

@@ -16,6 +16,12 @@ public class ApiLogFilter extends OncePerRequestFilter {
protected void doFilterInternal( protected void doFilterInternal(
HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
throws ServletException, IOException { throws ServletException, IOException {
String uri = request.getRequestURI();
if (uri.contains("/download/")) {
filterChain.doFilter(request, response);
return;
}
ContentCachingRequestWrapper wrappedRequest = new ContentCachingRequestWrapper(request); ContentCachingRequestWrapper wrappedRequest = new ContentCachingRequestWrapper(request);
ContentCachingResponseWrapper wrappedResponse = new ContentCachingResponseWrapper(response); ContentCachingResponseWrapper wrappedResponse = new ContentCachingResponseWrapper(response);

View File

@@ -2,10 +2,14 @@ package com.kamco.cd.training.dataset;
import com.kamco.cd.training.config.api.ApiResponseDto; import com.kamco.cd.training.config.api.ApiResponseDto;
import com.kamco.cd.training.dataset.dto.DatasetDto; import com.kamco.cd.training.dataset.dto.DatasetDto;
import com.kamco.cd.training.dataset.dto.DatasetDto.AddDeliveriesReq;
import com.kamco.cd.training.dataset.dto.DatasetObjDto; import com.kamco.cd.training.dataset.dto.DatasetObjDto;
import com.kamco.cd.training.dataset.dto.DatasetObjDto.DatasetClass; import com.kamco.cd.training.dataset.dto.DatasetObjDto.DatasetClass;
import com.kamco.cd.training.dataset.dto.DatasetObjDto.DatasetStorage; import com.kamco.cd.training.dataset.dto.DatasetObjDto.DatasetStorage;
import com.kamco.cd.training.dataset.service.DatasetAsyncService;
import com.kamco.cd.training.dataset.service.DatasetService; import com.kamco.cd.training.dataset.service.DatasetService;
import com.kamco.cd.training.model.dto.FileDto.FoldersDto;
import com.kamco.cd.training.model.dto.FileDto.SrchFoldersDto;
import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter; import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.media.Content; import io.swagger.v3.oas.annotations.media.Content;
@@ -33,6 +37,7 @@ import org.springframework.web.bind.annotation.*;
public class DatasetApiController { public class DatasetApiController {
private final DatasetService datasetService; private final DatasetService datasetService;
private final DatasetAsyncService datasetAsyncService;
@Operation(summary = "학습데이터 관리 목록 조회", description = "학습데이터 목록을 조회합니다.") @Operation(summary = "학습데이터 관리 목록 조회", description = "학습데이터 목록을 조회합니다.")
@ApiResponses( @ApiResponses(
@@ -248,4 +253,48 @@ public class DatasetApiController {
String path = datasetService.getFilePathByUUIDPathType(uuid, pathType); String path = datasetService.getFilePathByUUIDPathType(uuid, pathType);
return datasetService.getFilePathByFile(path); return datasetService.getFilePathByFile(path);
} }
@Operation(summary = "납품 폴더 조회", description = "납품 폴더 조회 API")
@ApiResponses(
value = {
@ApiResponse(
responseCode = "200",
description = "조회 성공",
content =
@Content(
mediaType = "application/json",
schema = @Schema(implementation = FoldersDto.class))),
@ApiResponse(responseCode = "404", description = "조회 오류", content = @Content),
@ApiResponse(responseCode = "500", description = "서버 오류", content = @Content)
})
@PostMapping("/folder-list")
public ApiResponseDto<FoldersDto> getDir(@RequestBody SrchFoldersDto srchDto) throws IOException {
return ApiResponseDto.createOK(datasetService.getFolderAll(srchDto));
}
@Operation(summary = "납품 학습데이터셋 등록", description = "납품 학습데이터셋 등록 API")
@ApiResponses(
value = {
@ApiResponse(
responseCode = "200",
description = "등록 성공",
content =
@Content(
mediaType = "application/json",
schema = @Schema(implementation = String.class))),
@ApiResponse(responseCode = "404", description = "조회 오류", content = @Content),
@ApiResponse(responseCode = "500", description = "서버 오류", content = @Content)
})
@PostMapping("/deliveries")
public ApiResponseDto<String> insertDeliveriesDataset(@RequestBody AddDeliveriesReq req) {
// 폴더 구조 검증
DatasetService.validateTrainValTestDirs(req.getFilePath());
// 파일 개수 검증
DatasetService.validateDirFileCount(req.getFilePath());
datasetAsyncService.insertDeliveriesDatasetAsync(req);
return ApiResponseDto.createOK("ok");
}
} }

View File

@@ -145,6 +145,7 @@ public class DatasetDto {
} }
} }
// TODO 미사용시작
@Schema(name = "DatasetDetailReq", description = "데이터셋 상세 조회 요청") @Schema(name = "DatasetDetailReq", description = "데이터셋 상세 조회 요청")
@Getter @Getter
@Setter @Setter
@@ -157,6 +158,8 @@ public class DatasetDto {
private Long datasetId; private Long datasetId;
} }
// TODO 미사용 끝
@Schema(name = "DatasetRegisterReq", description = "데이터셋 등록 요청") @Schema(name = "DatasetRegisterReq", description = "데이터셋 등록 요청")
@Getter @Getter
@Setter @Setter
@@ -532,4 +535,28 @@ public class DatasetDto {
private Long totalObjectCount; private Long totalObjectCount;
private String datasetPath; private String datasetPath;
} }
@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
public static class AddDeliveriesReq {
@Schema(description = "경로", example = "/")
private String filePath;
@Schema(description = "제목", example = "")
private String title;
@Schema(description = "메모", example = "")
private String memo;
@Schema(description = "비교년도", example = "")
private Integer compareYyyy;
@Schema(description = "기준년도", example = "")
private Integer targetYyyy;
@Schema(description = "회차", example = "")
private Long roundNo;
}
} }

View File

@@ -1,3 +1,4 @@
// TODO 미사용시작
package com.kamco.cd.training.dataset.dto; package com.kamco.cd.training.dataset.dto;
import com.kamco.cd.training.common.utils.interfaces.JsonFormatDttm; import com.kamco.cd.training.common.utils.interfaces.JsonFormatDttm;
@@ -72,6 +73,7 @@ public class MapSheetDto {
private List<Long> itemIds; private List<Long> itemIds;
} }
// TODO 미사용시작
@Schema(name = "MapSheetCheckReq", description = "도엽 번호 유효성 검증 요청") @Schema(name = "MapSheetCheckReq", description = "도엽 번호 유효성 검증 요청")
@Getter @Getter
@Setter @Setter
@@ -101,3 +103,4 @@ public class MapSheetDto {
private boolean duplicate; private boolean duplicate;
} }
} }
// TODO 미사용 끝

View File

@@ -0,0 +1,124 @@
package com.kamco.cd.training.dataset.service;
import com.kamco.cd.training.common.enums.LearnDataRegister;
import com.kamco.cd.training.dataset.dto.DatasetDto.AddDeliveriesReq;
import com.kamco.cd.training.dataset.dto.DatasetDto.DatasetMngRegDto;
import com.kamco.cd.training.postgres.core.DatasetCoreService;
import java.util.UUID;
import lombok.RequiredArgsConstructor;
import lombok.extern.log4j.Log4j2;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Service;
@Service
@Log4j2
@RequiredArgsConstructor
public class DatasetAsyncService {
private final DatasetService datasetService;
private final DatasetCoreService datasetCoreService;
private static final String LOG_PREFIX = "[납품 데이터셋]";
/**
* 납품 데이터셋 등록 비동기 업로드 처리 1) 데이터셋 구조/파일 검증 2) UID 생성 및 마스터 데이터 저장 3) 상태를 UPLOADING으로 변경 4) 실제
* 데이터(train/val/test) 등록 5) 완료 시 COMPLETED 상태로 변경 6) 실패 시 상태를 UPLOAD_FAILED로 변경 후 데이터 정리(삭제)
*
* @param req
*/
@Async("datasetExecutor")
public void insertDeliveriesDatasetAsync(AddDeliveriesReq req) {
long startTime = System.currentTimeMillis();
log.info("{} 업로드 시작 ==========", LOG_PREFIX);
log.info(
"{} filePath={}, targetYyyy={}, compareYyyy={}, roundNo={}",
LOG_PREFIX,
req.getFilePath(),
req.getTargetYyyy(),
req.getCompareYyyy(),
req.getRoundNo());
Long datasetUid = null;
try {
// ===== 1. UID 생성 =====
String uid = UUID.randomUUID().toString().replace("-", "").toUpperCase();
log.info("{} 생성된 UID: {}", LOG_PREFIX, uid);
// ===== 2. 마스터 데이터 생성 =====
String title = req.getTitle();
if (title == null || title.isBlank()) {
Integer compareYyyy = req.getCompareYyyy();
Integer targetYyyy = req.getTargetYyyy();
if (compareYyyy != null && targetYyyy != null) {
title = compareYyyy + "-" + targetYyyy;
} else {
title = null;
}
}
DatasetMngRegDto datasetMngRegDto = new DatasetMngRegDto();
datasetMngRegDto.setUid(uid);
datasetMngRegDto.setDataType("DELIVER");
datasetMngRegDto.setCompareYyyy(req.getCompareYyyy() == null ? 0 : req.getCompareYyyy());
datasetMngRegDto.setTargetYyyy(req.getTargetYyyy() == null ? 0 : req.getTargetYyyy());
datasetMngRegDto.setRoundNo(req.getRoundNo());
datasetMngRegDto.setTitle(title);
datasetMngRegDto.setMemo(req.getMemo());
datasetMngRegDto.setDatasetPath(req.getFilePath());
// 마스터 저장
datasetUid = datasetCoreService.insertDatasetMngData(datasetMngRegDto);
log.info("{} 마스터 저장 완료. datasetUid={}", LOG_PREFIX, datasetUid);
// ===== 3. 상태 변경 (업로드중) =====
datasetCoreService.updateDatasetUploadStatus(datasetUid, LearnDataRegister.UPLOADING);
log.info("{} 상태 변경 → UPLOADING. datasetUid={}", LOG_PREFIX, datasetUid);
// ===== 4. 데이터 등록 =====
long insertStart = System.currentTimeMillis();
// 납품 데이터 obj 등록
datasetService.insertDeliveriesDataset(req, datasetUid);
log.info(
"{} 데이터 등록 완료. datasetUid={}, 소요시간={} ms",
LOG_PREFIX,
datasetUid,
System.currentTimeMillis() - insertStart);
// ===== 5. 상태 변경 (완료) =====
datasetCoreService.updateDatasetUploadStatus(datasetUid, LearnDataRegister.COMPLETED);
log.info("{} 상태 변경 → COMPLETED. datasetUid={}", LOG_PREFIX, datasetUid);
log.info(
"{} 업로드 완료. 총 소요시간={} ms ==========", LOG_PREFIX, System.currentTimeMillis() - startTime);
} catch (Exception e) {
log.error(
"{} 업로드 실패. datasetUid={}, filePath={}", LOG_PREFIX, datasetUid, req.getFilePath(), e);
if (datasetUid != null) {
try {
// ===== 실패 처리 =====
datasetCoreService.updateDatasetUploadStatus(datasetUid, LearnDataRegister.UPLOAD_FAILED);
log.error("{} 상태 변경 → 업로드 실패. datasetUid={}", LOG_PREFIX, datasetUid);
// 실패 시 데이터 정리
datasetCoreService.deleteAllDatasetObj(datasetUid);
log.error("{} 데이터 정리 완료. datasetUid={}", LOG_PREFIX, datasetUid);
} catch (Exception ex) {
log.error("{} 실패 후 정리 작업 중 오류. datasetUid={}", LOG_PREFIX, datasetUid, ex);
}
}
}
}
}

View File

@@ -0,0 +1,199 @@
package com.kamco.cd.training.dataset.service;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.node.ArrayNode;
import com.fasterxml.jackson.databind.node.ObjectNode;
import com.kamco.cd.training.dataset.dto.DatasetObjDto.DatasetObjRegDto;
import com.kamco.cd.training.postgres.core.DatasetCoreService;
import java.nio.file.Paths;
import java.util.List;
import java.util.Map;
import lombok.RequiredArgsConstructor;
import lombok.extern.log4j.Log4j2;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@Service
@Log4j2
@RequiredArgsConstructor
public class DatasetBatchService {
private final DatasetCoreService datasetCoreService;
private final ObjectMapper mapper;
/**
* 배치 단위 데이터 저장
*
* <p>- 전달받은 데이터 목록을 순회하며 개별 insert 처리 - batch 단위로 트랜잭션 관리
*/
@Transactional
public void saveBatch(List<Map<String, Object>> batch, Long datasetUid, String type) {
for (Map<String, Object> map : batch) {
try {
insertTrainTestData(map, datasetUid, type);
} catch (Exception e) {
log.error("파일 단위 실패. skip. file={}", batch, e);
continue;
}
}
}
/**
* 단일 데이터 처리 및 insert DTO 생성
*
* <p>처리 흐름: 1) 경로/JSON 데이터 추출 2) 파일명에서 연도 및 도엽번호 파싱 3) label JSON → feature 단위 분리 4) feature별 DTO
* 생성 후 DB insert
*/
private void insertTrainTestData(Map<String, Object> map, Long datasetUid, String subDir) {
String comparePath = (String) map.get("input1");
String targetPath = (String) map.get("input2");
String labelPath = (String) map.get("label");
String geojsonPath = (String) map.get("geojson_path");
Object labelJson = map.get("label-json");
// JSON 파싱
JsonNode json;
try {
json = parseJson(labelJson);
if (json == null) {
log.warn("json null. skip. file={}", labelJson);
return;
}
} catch (Exception e) {
// 실패하면 skip, 다음 진행
log.error("GeoJSON 파싱 실패. skip. file={}", geojsonPath, e);
return;
}
// 파일명 파싱
String fileName = Paths.get(comparePath).getFileName().toString();
String[] fileNameStr = fileName.split("_");
if (fileNameStr.length < 4) {
log.error("파일명 파싱 실패: {}", fileName);
return;
// throw new IllegalArgumentException("잘못된 파일명 형식: " + fileName);
}
int compareYyyy = 0;
int targetYyyy = 0;
try {
compareYyyy = parseInt(fileNameStr[1], "compareYyyy", fileName);
targetYyyy = parseInt(fileNameStr[2], "targetYyyy", fileName);
} catch (Exception e) {
log.error("기준년도 파싱 실패: {}", fileName);
return;
}
String mapSheetNum = fileNameStr[3];
// JSON 유효성 체크
JsonNode featuresNode = json.path("features");
// 2. 비어있는지 확인
if (featuresNode.isEmpty()) {
log.warn("features empty. skip. file={}", geojsonPath);
return;
}
if (!featuresNode.isArray()) {
log.warn("features array 아님. skip. file={}", geojsonPath);
return; // skip
}
if (featuresNode.isMissingNode() || !featuresNode.isArray() || featuresNode.isEmpty()) {
return; // skip
}
ObjectNode base = mapper.createObjectNode();
base.put("type", "FeatureCollection");
for (JsonNode feature : featuresNode) {
try {
JsonNode prop = feature.path("properties");
String compareClassCd = prop.path("before").asText(null);
String targetClassCd = prop.path("after").asText(null);
// null 방어
if (compareClassCd == null || targetClassCd == null) {
log.warn("class 값 없음. skip. file={}", fileName);
continue;
}
ArrayNode arr = mapper.createArrayNode();
arr.add(feature);
ObjectNode root = base.deepCopy();
root.set("features", arr);
DatasetObjRegDto objRegDto =
DatasetObjRegDto.builder()
.datasetUid(datasetUid)
.compareYyyy(compareYyyy)
.compareClassCd(compareClassCd)
.targetYyyy(targetYyyy)
.targetClassCd(targetClassCd)
.comparePath(comparePath)
.targetPath(targetPath)
.labelPath(labelPath)
.mapSheetNum(mapSheetNum)
.geojson(root)
.geojsonPath(geojsonPath)
.fileName(fileName)
.build();
// 데이터 타입별 insert
insertByType(subDir, objRegDto);
} catch (Exception e) {
// 개별 feature skip
log.error("feature 처리 실패. skip. file={}", fileName, e);
}
}
}
/** 데이터 타입별 insert 처리 - type 값에 따라 대상 테이블 분기 - 잘못된 타입 입력 시 예외 발생 */
private void insertByType(String type, DatasetObjRegDto dto) {
switch (type) {
case "train" -> datasetCoreService.insertDatasetObj(dto);
case "val" -> datasetCoreService.insertDatasetValObj(dto);
case "test" -> datasetCoreService.insertDatasetTestObj(dto);
default -> throw new IllegalArgumentException("잘못된 타입: " + type);
}
}
/**
* label_json → JsonNode 변환
*
* <p>- JsonNode면 그대로 사용 - 문자열이면 파싱 수행 - 실패 시 로그 후 예외 발생
*/
private JsonNode parseJson(Object labelJson) {
try {
if (labelJson instanceof JsonNode jn) {
return jn;
}
return mapper.readTree(labelJson.toString());
} catch (Exception e) {
log.error("label_json parse error: {}", labelJson, e);
return null;
}
}
/**
* 문자열 → 정수 변환
*
* <p>- 파싱 실패 시 어떤 필드/파일에서 발생했는지 로그 기록 - 잘못된 데이터는 즉시 예외 처리
*/
private int parseInt(String value, String field, String fileName) {
try {
return Integer.parseInt(value);
} catch (NumberFormatException e) {
log.error("{} 파싱 실패. fileName={}, value={}", field, fileName, value);
throw new IllegalArgumentException(field + " 파싱 실패: " + fileName);
}
}
}

View File

@@ -4,6 +4,7 @@ import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.node.ArrayNode; import com.fasterxml.jackson.databind.node.ArrayNode;
import com.fasterxml.jackson.databind.node.ObjectNode; import com.fasterxml.jackson.databind.node.ObjectNode;
import com.kamco.cd.training.common.enums.LearnDataRegister;
import com.kamco.cd.training.common.enums.LearnDataType; import com.kamco.cd.training.common.enums.LearnDataType;
import com.kamco.cd.training.common.exception.CustomApiException; import com.kamco.cd.training.common.exception.CustomApiException;
import com.kamco.cd.training.common.service.FormatStorage; import com.kamco.cd.training.common.service.FormatStorage;
@@ -11,6 +12,7 @@ import com.kamco.cd.training.common.utils.FIleChecker;
import com.kamco.cd.training.config.api.ApiResponseDto.ApiResponseCode; import com.kamco.cd.training.config.api.ApiResponseDto.ApiResponseCode;
import com.kamco.cd.training.config.api.ApiResponseDto.ResponseObj; import com.kamco.cd.training.config.api.ApiResponseDto.ResponseObj;
import com.kamco.cd.training.dataset.dto.DatasetDto; import com.kamco.cd.training.dataset.dto.DatasetDto;
import com.kamco.cd.training.dataset.dto.DatasetDto.AddDeliveriesReq;
import com.kamco.cd.training.dataset.dto.DatasetDto.AddReq; import com.kamco.cd.training.dataset.dto.DatasetDto.AddReq;
import com.kamco.cd.training.dataset.dto.DatasetDto.DatasetMngRegDto; import com.kamco.cd.training.dataset.dto.DatasetDto.DatasetMngRegDto;
import com.kamco.cd.training.dataset.dto.DatasetObjDto; import com.kamco.cd.training.dataset.dto.DatasetObjDto;
@@ -18,8 +20,11 @@ import com.kamco.cd.training.dataset.dto.DatasetObjDto.DatasetClass;
import com.kamco.cd.training.dataset.dto.DatasetObjDto.DatasetObjRegDto; import com.kamco.cd.training.dataset.dto.DatasetObjDto.DatasetObjRegDto;
import com.kamco.cd.training.dataset.dto.DatasetObjDto.DatasetStorage; import com.kamco.cd.training.dataset.dto.DatasetObjDto.DatasetStorage;
import com.kamco.cd.training.dataset.dto.DatasetObjDto.SearchReq; import com.kamco.cd.training.dataset.dto.DatasetObjDto.SearchReq;
import com.kamco.cd.training.model.dto.FileDto.FoldersDto;
import com.kamco.cd.training.model.dto.FileDto.SrchFoldersDto;
import com.kamco.cd.training.postgres.core.DatasetCoreService; import com.kamco.cd.training.postgres.core.DatasetCoreService;
import jakarta.validation.Valid; import jakarta.validation.Valid;
import java.io.File;
import java.io.IOException; import java.io.IOException;
import java.io.InputStream; import java.io.InputStream;
import java.nio.file.Files; import java.nio.file.Files;
@@ -27,6 +32,8 @@ import java.nio.file.Path;
import java.nio.file.Paths; import java.nio.file.Paths;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Arrays; import java.util.Arrays;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap; import java.util.HashMap;
import java.util.HashSet; import java.util.HashSet;
import java.util.List; import java.util.List;
@@ -37,7 +44,6 @@ import java.util.stream.Collectors;
import java.util.stream.Stream; import java.util.stream.Stream;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.core.io.InputStreamResource; import org.springframework.core.io.InputStreamResource;
import org.springframework.core.io.Resource; import org.springframework.core.io.Resource;
import org.springframework.data.domain.Page; import org.springframework.data.domain.Page;
@@ -51,13 +57,10 @@ import org.springframework.transaction.annotation.Transactional;
@Slf4j @Slf4j
@Service @Service
@RequiredArgsConstructor @RequiredArgsConstructor
@Transactional
public class DatasetService { public class DatasetService {
private final DatasetCoreService datasetCoreService; private final DatasetCoreService datasetCoreService;
private final DatasetBatchService datasetBatchService;
@Value("${file.dataset-dir}")
private String datasetDir;
private static final List<String> LABEL_DIRS = List.of("label-json", "label", "input1", "input2"); private static final List<String> LABEL_DIRS = List.of("label-json", "label", "input1", "input2");
private static final List<String> REQUIRED_DIRS = Arrays.asList("train", "val", "test"); private static final List<String> REQUIRED_DIRS = Arrays.asList("train", "val", "test");
@@ -84,6 +87,7 @@ public class DatasetService {
return datasetCoreService.getOneByUuid(id); return datasetCoreService.getOneByUuid(id);
} }
// TODO 미사용시작
/** /**
* 데이터셋 등록 * 데이터셋 등록
* *
@@ -98,6 +102,7 @@ public class DatasetService {
return saved.getId(); return saved.getId();
} }
// TODO 미사용 끝
/** /**
* 데이터셋 수정 * 데이터셋 수정
* *
@@ -231,7 +236,7 @@ public class DatasetService {
return new ResponseObj(ApiResponseCode.INTERNAL_SERVER_ERROR, e.getMessage()); return new ResponseObj(ApiResponseCode.INTERNAL_SERVER_ERROR, e.getMessage());
} }
datasetCoreService.updateDatasetUploadStatus(datasetUid); datasetCoreService.updateDatasetUploadStatus(datasetUid, LearnDataRegister.COMPLETED);
return new ResponseObj(ApiResponseCode.OK, "업로드 성공하였습니다."); return new ResponseObj(ApiResponseCode.OK, "업로드 성공하였습니다.");
} }
@@ -365,7 +370,12 @@ public class DatasetService {
// 폴더별 처리 // 폴더별 처리
if ("label-json".equals(dirName)) { if ("label-json".equals(dirName)) {
// json 파일이면 파싱 // json 파일이면 파싱
try {
data.put("label-json", readJson(path)); data.put("label-json", readJson(path));
} catch (Exception e) {
log.error("파일 JSON 읽기 실패. skip. file={}", path, e);
return; // skip
}
data.put("geojson_path", path.toAbsolutePath().toString()); data.put("geojson_path", path.toAbsolutePath().toString());
} else { } else {
data.put(dirName, path.toAbsolutePath().toString()); data.put(dirName, path.toAbsolutePath().toString());
@@ -506,4 +516,164 @@ public class DatasetService {
} }
} }
} }
/**
* 폴더 조회
*
* @param srchDto 폴더 경로
* @return 폴더 리스트
* @throws IOException
*/
public FoldersDto getFolderAll(SrchFoldersDto srchDto) throws IOException {
File dir = new File(srchDto.getDirPath() == null ? "/" : srchDto.getDirPath());
// 존재 + 디렉토리 체크
if (!dir.exists() || !dir.isDirectory()) {
throw new CustomApiException("BAD_REQUEST", HttpStatus.BAD_REQUEST, "잘못된 경로입니다.");
}
// 권한 없을때
if (!dir.canRead()) {
throw new CustomApiException(
ApiResponseCode.FORBIDDEN.getId(), HttpStatus.FORBIDDEN, "디렉토리에 접근할 권한이 없습니다.");
}
String canonicalPath = dir.getCanonicalPath();
File[] files = dir.listFiles();
if (files == null) {
return new FoldersDto(canonicalPath, 0, 0, Collections.emptyList());
}
List<FIleChecker.Folder> folders = new ArrayList<>();
int folderTotCnt = 0;
int folderErrTotCnt = 0;
for (File f : files) {
// 숨김 제외
if (f.isHidden()) continue;
if (f.isDirectory()) {
// 폴더 개수 증가
folderTotCnt++;
// 폴더 유효성 여부 (기본 true, 이후 검증 로직으로 변경 가능)
boolean isValid = true;
// 유효하지 않은 폴더 카운트 증가
if (!isValid) folderErrTotCnt++;
// 현재 폴더 이름 (ex: train, images 등)
String folderNm = f.getName();
// 부모 경로 (ex: /data/datasets)
String parentPath = f.getParent();
// 부모 폴더 이름 (ex: datasets)
String parentFolderNm = new File(parentPath).getName();
// 전체 절대 경로 (ex: /data/datasets/train)
String fullPath = f.getAbsolutePath();
// 폴더 깊이 (경로 기준 depth)
// ex: /a/b/c → depth = 3
int depth = f.toPath().getNameCount();
// 하위 폴더 개수
long childCnt = FIleChecker.getChildFolderCount(f);
// 마지막 수정 시간 (문자열 포맷)
String lastModified = FIleChecker.getLastModified(f);
// Folder DTO 생성 및 리스트에 추가
folders.add(
new FIleChecker.Folder(
folderNm, // 폴더명
parentFolderNm, // 부모 폴더명
parentPath, // 부모 경로
fullPath, // 전체 경로
depth, // 깊이
childCnt, // 하위 폴더 개수
lastModified, // 수정일시
isValid // 유효성 여부
));
}
}
// 폴더 정렬
folders.sort(
Comparator.comparing(FIleChecker.Folder::getFolderNm, String.CASE_INSENSITIVE_ORDER));
return new FoldersDto(canonicalPath, folderTotCnt, folderErrTotCnt, folders);
}
/**
* 납품 데이터 등록
*
* @param req 폴더경로, 메모
* @return 성공/실패 여부0
*/
public void insertDeliveriesDataset(AddDeliveriesReq req, Long datasetUid) {
long startTime = System.currentTimeMillis();
// 처리
processType(req.getFilePath(), datasetUid, "train");
processType(req.getFilePath(), datasetUid, "val");
processType(req.getFilePath(), datasetUid, "test");
log.info("========== 전체 완료. 총 소요시간: {} ms ==========", System.currentTimeMillis() - startTime);
}
/**
* 납품 데이터 등록 처리
*
* @param path
* @param datasetUid
* @param type
*/
private void processType(String path, Long datasetUid, String type) {
long start = System.currentTimeMillis();
log.info("[납품 데이터 등록 처리][{}] 시작", type.toUpperCase());
List<Map<String, Object>> list = getUnzipDatasetFiles(path, type);
int batchSize = 1000;
int total = list.size();
int processed = 0;
for (int i = 0; i < total; i += batchSize) {
List<Map<String, Object>> batch = list.subList(i, Math.min(i + batchSize, total));
try {
log.info("[납품 데이터 등록 처리][{}] batch 시작: {} ~ {}", type, i, i + batch.size());
datasetBatchService.saveBatch(batch, datasetUid, type);
processed += batch.size();
} catch (Exception e) {
log.error("batch 실패 row 데이터: {}", batch);
log.error(
"[납품 데이터 등록 처리][{}] batch 실패. range: {} ~ {}, datasetUid={}",
type,
i,
i + batch.size(),
datasetUid,
e);
}
}
log.info(
"[납품 데이터 등록 처리][{}] 완료. 총 {}건, 소요시간: {} ms",
type,
total,
System.currentTimeMillis() - start);
}
} }

View File

@@ -1,3 +1,4 @@
// TODO 미사용시작
package com.kamco.cd.training.dataset.service; package com.kamco.cd.training.dataset.service;
import com.kamco.cd.training.dataset.dto.MapSheetDto; import com.kamco.cd.training.dataset.dto.MapSheetDto;
@@ -39,3 +40,4 @@ public class MapSheetService {
log.info("도엽 삭제 완료 - 개수: {}", deleteReq.getItemIds().size()); log.info("도엽 삭제 완료 - 개수: {}", deleteReq.getItemIds().size());
} }
} }
// TODO 미사용 끝

View File

@@ -11,9 +11,9 @@ import com.kamco.cd.training.model.dto.ModelTrainDetailDto.ModelTrainMetrics;
import com.kamco.cd.training.model.dto.ModelTrainDetailDto.ModelValidationMetrics; import com.kamco.cd.training.model.dto.ModelTrainDetailDto.ModelValidationMetrics;
import com.kamco.cd.training.model.dto.ModelTrainDetailDto.TransferDetailDto; import com.kamco.cd.training.model.dto.ModelTrainDetailDto.TransferDetailDto;
import com.kamco.cd.training.model.dto.ModelTrainMngDto.Basic; import com.kamco.cd.training.model.dto.ModelTrainMngDto.Basic;
import com.kamco.cd.training.model.dto.ModelTrainMngDto.CleanupResult;
import com.kamco.cd.training.model.dto.ModelTrainMngDto.ModelProgressStepDto; import com.kamco.cd.training.model.dto.ModelTrainMngDto.ModelProgressStepDto;
import com.kamco.cd.training.model.service.ModelTrainDetailService; import com.kamco.cd.training.model.service.ModelTrainDetailService;
import com.kamco.cd.training.model.service.ModelTrainMngService;
import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter; import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.enums.ParameterIn; import io.swagger.v3.oas.annotations.enums.ParameterIn;
@@ -34,6 +34,7 @@ import lombok.RequiredArgsConstructor;
import org.apache.coyote.BadRequestException; import org.apache.coyote.BadRequestException;
import org.springframework.beans.factory.annotation.Value; import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.ResponseEntity; import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMapping;
@@ -45,7 +46,6 @@ import org.springframework.web.bind.annotation.RestController;
@RequestMapping("/api/models") @RequestMapping("/api/models")
public class ModelTrainDetailApiController { public class ModelTrainDetailApiController {
private final ModelTrainDetailService modelTrainDetailService; private final ModelTrainDetailService modelTrainDetailService;
private final ModelTrainMngService modelTrainMngService;
private final RangeDownloadResponder rangeDownloadResponder; private final RangeDownloadResponder rangeDownloadResponder;
@Value("${train.docker.responseDir}") @Value("${train.docker.responseDir}")
@@ -326,4 +326,44 @@ public class ModelTrainDetailApiController {
UUID uuid) { UUID uuid) {
return ApiResponseDto.ok(modelTrainDetailService.findModelTrainProgressInfo(uuid)); return ApiResponseDto.ok(modelTrainDetailService.findModelTrainProgressInfo(uuid));
} }
@Operation(
summary = "모델관리 > 모델 상세 > best epoch 제외 삭제 될 파일 미리보기",
description = "best epoch 제외 삭제 될 파일 미리보기 API")
@ApiResponses(
value = {
@ApiResponse(
responseCode = "200",
description = "조회 성공",
content =
@Content(
mediaType = "application/json",
schema = @Schema(implementation = CleanupResult.class))),
@ApiResponse(responseCode = "404", description = "데이터셋을 찾을 수 없음", content = @Content),
@ApiResponse(responseCode = "500", description = "서버 오류", content = @Content)
})
@GetMapping("/{uuid}/cleanup/preview")
public ApiResponseDto<CleanupResult> previewCleanup(
@Parameter(description = "모델 uuid") @PathVariable UUID uuid) {
return ApiResponseDto.ok(modelTrainDetailService.previewCleanup(uuid));
}
@Operation(summary = "모델관리 > 모델 상세 > best epoch 제외 삭제", description = "best epoch 제외 파일 삭제 API")
@ApiResponses(
value = {
@ApiResponse(
responseCode = "200",
description = "삭제 성공",
content =
@Content(
mediaType = "application/json",
schema = @Schema(implementation = CleanupResult.class))),
@ApiResponse(responseCode = "404", description = "데이터셋을 찾을 수 없음", content = @Content),
@ApiResponse(responseCode = "500", description = "서버 오류", content = @Content)
})
@DeleteMapping("/{uuid}/cleanup")
public ApiResponseDto<CleanupResult> cleanup(
@Parameter(description = "모델 uuid") @PathVariable UUID uuid) {
return ApiResponseDto.ok(modelTrainDetailService.cleanup(uuid));
}
} }

View File

@@ -1,5 +1,7 @@
package com.kamco.cd.training.model; package com.kamco.cd.training.model;
import com.kamco.cd.training.common.dto.MonitorDto;
import com.kamco.cd.training.common.service.SystemMonitorService;
import com.kamco.cd.training.config.api.ApiResponseDto; import com.kamco.cd.training.config.api.ApiResponseDto;
import com.kamco.cd.training.dataset.dto.DatasetDto; import com.kamco.cd.training.dataset.dto.DatasetDto;
import com.kamco.cd.training.dataset.dto.DatasetDto.DatasetReq; import com.kamco.cd.training.dataset.dto.DatasetDto.DatasetReq;
@@ -40,6 +42,7 @@ public class ModelTrainMngApiController {
private final ModelTrainMngService modelTrainMngService; private final ModelTrainMngService modelTrainMngService;
private final ModelTrainMetricsJobService modelTrainMetricsJobService; private final ModelTrainMetricsJobService modelTrainMetricsJobService;
private final ModelTestMetricsJobService modelTestMetricsJobService; private final ModelTestMetricsJobService modelTestMetricsJobService;
private final SystemMonitorService systemMonitorService;
@Operation(summary = "모델학습 목록 조회", description = "모델학습 목록 조회 API") @Operation(summary = "모델학습 목록 조회", description = "모델학습 목록 조회 API")
@ApiResponses( @ApiResponses(
@@ -214,4 +217,22 @@ public class ModelTrainMngApiController {
modelTestMetricsJobService.findTestValidMetricCsvFiles(); modelTestMetricsJobService.findTestValidMetricCsvFiles();
return ApiResponseDto.ok(null); return ApiResponseDto.ok(null);
} }
@Operation(summary = "학습서버 시스템 사용율 조회", description = "cpu, gpu, memory 사용율 조회")
@ApiResponses(
value = {
@ApiResponse(
responseCode = "200",
description = "검색 성공",
content =
@Content(
mediaType = "application/json",
schema = @Schema(implementation = Long.class))),
@ApiResponse(responseCode = "400", description = "잘못된 검색 조건", content = @Content),
@ApiResponse(responseCode = "500", description = "서버 오류", content = @Content)
})
@GetMapping("/monitor")
public ApiResponseDto<MonitorDto> getSystem() throws IOException {
return ApiResponseDto.ok(systemMonitorService.get());
}
} }

View File

@@ -0,0 +1,160 @@
package com.kamco.cd.training.model.dto;
import com.kamco.cd.training.common.utils.FIleChecker;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotNull;
import java.util.List;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
public class FileDto {
@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
public static class SrchFoldersDto {
@Schema(description = "디렉토리경로(ROOT:/)", example = "")
@NotNull
private String dirPath = "/";
}
@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
public static class SrchFilesDto {
@Schema(description = "디렉토리경로", example = "D:\\kamco\\2022\\캠코_2021_2022_34602060_D1")
@NotNull
private String dirPath;
@Schema(description = "전체(*), cpg,dbf,geojson등", example = "*")
@NotNull
private String extension;
@Schema(description = "파일명(name), 최종수정일(date)", example = "name")
@NotNull
private String sortType;
@Schema(description = "파일시작위치", example = "1")
@NotNull
private Integer startPos;
@Schema(description = "파일종료위치", example = "100")
@NotNull
private Integer endPos;
}
// TODO 미사용시작
@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
public static class SrchFilesDepthDto extends SrchFilesDto {
@Schema(description = "최대폴더Depth", example = "5")
@NotNull
private Integer maxDepth;
}
@Schema(name = "FolderDto", description = "폴더 정보")
@Getter
public static class FolderDto {
private final String folderNm;
private final String parentFolderNm;
private final String parentPath;
private final String fullPath;
private final int depth;
private final long childCnt;
private final String lastModified;
private final Boolean isValid;
public FolderDto(
String folderNm,
String parentFolderNm,
String parentPath,
String fullPath,
int depth,
long childCnt,
String lastModified,
Boolean isValid) {
this.folderNm = folderNm;
this.parentFolderNm = parentFolderNm;
this.parentPath = parentPath;
this.fullPath = fullPath;
this.depth = depth;
this.childCnt = childCnt;
this.lastModified = lastModified;
this.isValid = isValid;
}
}
// TODO 미사용 끝
@Schema(name = "FoldersDto", description = "폴더목록 정보")
@Getter
public static class FoldersDto {
private final String dirPath;
private final int folderTotCnt;
private final int folderErrTotCnt;
private final List<FIleChecker.Folder> folders;
public FoldersDto(
String dirPath, int folderTotCnt, int folderErrTotCnt, List<FIleChecker.Folder> folders) {
this.dirPath = dirPath;
this.folderTotCnt = folderTotCnt;
this.folderErrTotCnt = folderErrTotCnt;
this.folders = folders;
}
}
@Schema(name = "File Basic", description = "파일 기본 정보")
@Getter
public static class Basic {
private final String fileNm;
private final String parentFolderNm;
private final String parentPath;
private final String fullPath;
private final String extension;
private final long fileSize;
private final String lastModified;
public Basic(
String fileNm,
String parentFolderNm,
String parentPath,
String fullPath,
String extension,
long fileSize,
String lastModified) {
this.fileNm = fileNm;
this.parentFolderNm = parentFolderNm;
this.parentPath = parentPath;
this.fullPath = fullPath;
this.extension = extension;
this.fileSize = fileSize;
this.lastModified = lastModified;
}
}
@Schema(name = "FilesDto", description = "파일 목록 정보")
@Getter
public static class FilesDto {
private final String dirPath;
private final int fileTotCnt;
private final long fileTotSize;
private final List<FIleChecker.Basic> files;
public FilesDto(
String dirPath, int fileTotCnt, long fileTotSize, List<FIleChecker.Basic> files) {
this.dirPath = dirPath;
this.fileTotCnt = fileTotCnt;
this.fileTotSize = fileTotSize;
this.files = files;
}
}
}

View File

@@ -48,6 +48,7 @@ public class ModelTrainMngDto {
private ZonedDateTime packingEndDttm; private ZonedDateTime packingEndDttm;
private Long beforeModelId; private Long beforeModelId;
private Integer bestEpoch;
public String getStatusName() { public String getStatusName() {
if (this.statusCd == null || this.statusCd.isBlank()) return null; if (this.statusCd == null || this.statusCd.isBlank()) return null;
@@ -327,4 +328,26 @@ public class ModelTrainMngDto {
@JsonFormatDttm private ZonedDateTime endTime; @JsonFormatDttm private ZonedDateTime endTime;
private boolean isError; private boolean isError;
} }
@Getter
@Setter
public static class CleanupResult {
// cleanup 대상 전체 파일 수 (삭제 대상 + 유지 파일 포함)
private int totalCount;
// 실제로 삭제된 파일 개수
private int deletedCount;
// 삭제 실패한 파일 개수
private int failedCount;
// 삭제 실패한 파일명 목록
private List<String> failedFiles;
// 유지된 파일명 (best epoch 기준)
private String keptFile;
// 삭제 될 파일
private List<String> deleteTargets;
}
} }

View File

@@ -1,6 +1,8 @@
package com.kamco.cd.training.model.service; package com.kamco.cd.training.model.service;
import com.kamco.cd.training.common.enums.ModelType; import com.kamco.cd.training.common.enums.ModelType;
import com.kamco.cd.training.common.enums.TrainStatusType;
import com.kamco.cd.training.common.exception.CustomApiException;
import com.kamco.cd.training.dataset.dto.DatasetDto.DatasetReq; import com.kamco.cd.training.dataset.dto.DatasetDto.DatasetReq;
import com.kamco.cd.training.dataset.dto.DatasetDto.SelectTransferDataSet; import com.kamco.cd.training.dataset.dto.DatasetDto.SelectTransferDataSet;
import com.kamco.cd.training.model.dto.ModelConfigDto; import com.kamco.cd.training.model.dto.ModelConfigDto;
@@ -14,15 +16,31 @@ import com.kamco.cd.training.model.dto.ModelTrainDetailDto.ModelTrainMetrics;
import com.kamco.cd.training.model.dto.ModelTrainDetailDto.ModelValidationMetrics; import com.kamco.cd.training.model.dto.ModelTrainDetailDto.ModelValidationMetrics;
import com.kamco.cd.training.model.dto.ModelTrainDetailDto.TransferDetailDto; import com.kamco.cd.training.model.dto.ModelTrainDetailDto.TransferDetailDto;
import com.kamco.cd.training.model.dto.ModelTrainDetailDto.TransferHyperSummary; import com.kamco.cd.training.model.dto.ModelTrainDetailDto.TransferHyperSummary;
import com.kamco.cd.training.model.dto.ModelTrainMngDto;
import com.kamco.cd.training.model.dto.ModelTrainMngDto.Basic; import com.kamco.cd.training.model.dto.ModelTrainMngDto.Basic;
import com.kamco.cd.training.model.dto.ModelTrainMngDto.CleanupResult;
import com.kamco.cd.training.model.dto.ModelTrainMngDto.ModelProgressStepDto; import com.kamco.cd.training.model.dto.ModelTrainMngDto.ModelProgressStepDto;
import com.kamco.cd.training.postgres.core.ModelTrainDetailCoreService; import com.kamco.cd.training.postgres.core.ModelTrainDetailCoreService;
import com.kamco.cd.training.postgres.core.ModelTrainMngCoreService; import com.kamco.cd.training.postgres.core.ModelTrainMngCoreService;
import java.io.IOException;
import java.nio.file.AccessDeniedException;
import java.nio.file.FileVisitOption;
import java.nio.file.FileVisitResult;
import java.nio.file.Files;
import java.nio.file.LinkOption;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.SimpleFileVisitor;
import java.nio.file.attribute.BasicFileAttributes;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.EnumSet;
import java.util.List; import java.util.List;
import java.util.UUID; import java.util.UUID;
import java.util.stream.Stream;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.HttpStatus;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional; import org.springframework.transaction.annotation.Transactional;
@@ -35,6 +53,9 @@ public class ModelTrainDetailService {
private final ModelTrainDetailCoreService modelTrainDetailCoreService; private final ModelTrainDetailCoreService modelTrainDetailCoreService;
private final ModelTrainMngCoreService mngCoreService; private final ModelTrainMngCoreService mngCoreService;
@Value("${train.docker.responseDir}")
private String responseDir;
/** /**
* 모델 상세정보 요약 * 모델 상세정보 요약
* *
@@ -63,6 +84,7 @@ public class ModelTrainDetailService {
return modelTrainDetailCoreService.findByModelByUUID(uuid); return modelTrainDetailCoreService.findByModelByUUID(uuid);
} }
// TODO 미사용시작
/** /**
* 전이학습 모델선택 정보 * 전이학습 모델선택 정보
* *
@@ -129,6 +151,8 @@ public class ModelTrainDetailService {
return transferDetailDto; return transferDetailDto;
} }
// TODO 미사용 끝
public List<ModelTrainMetrics> getModelTrainMetricResult(UUID uuid) { public List<ModelTrainMetrics> getModelTrainMetricResult(UUID uuid) {
return modelTrainDetailCoreService.getModelTrainMetricResult(uuid); return modelTrainDetailCoreService.getModelTrainMetricResult(uuid);
} }
@@ -152,4 +176,262 @@ public class ModelTrainDetailService {
public List<ModelProgressStepDto> findModelTrainProgressInfo(UUID uuid) { public List<ModelProgressStepDto> findModelTrainProgressInfo(UUID uuid) {
return modelTrainDetailCoreService.findModelTrainProgressInfo(uuid); return modelTrainDetailCoreService.findModelTrainProgressInfo(uuid);
} }
/**
* 삭제될 파일목록 및 유지될 파일 목록
*
* @param uuid
* @return
*/
public CleanupResult previewCleanup(UUID uuid) {
CleanupResult result = new CleanupResult();
// ===== 모델 조회 =====
ModelTrainMngDto.Basic model = modelTrainDetailCoreService.findByModelByUUID(uuid);
if (model == null) {
throw new CustomApiException(
"NOT_FOUND_DATA", HttpStatus.NOT_FOUND, "모델을 찾을 수 없습니다. UUID: " + uuid);
}
Path dir = Paths.get(responseDir, model.getUuid().toString()).toAbsolutePath().normalize();
if (!Files.exists(dir) || !Files.isDirectory(dir)) {
throw new CustomApiException("NOT_FOUND_DATA", HttpStatus.NOT_FOUND, "디렉토리가 없습니다.");
}
if (!Files.isReadable(dir)) {
throw new CustomApiException("FORBIDDEN", HttpStatus.FORBIDDEN, "디렉토리 읽기 권한이 없습니다.");
}
try (Stream<Path> stream = Files.list(dir)) {
List<Path> files = stream.toList();
if (files.isEmpty()) {
throw new CustomApiException("NOT_FOUND_DATA", HttpStatus.NOT_FOUND, "파일이 없습니다.");
}
// ===== keep 파일 찾기 =====
Path keep =
files.stream()
.filter(
p -> {
String name = p.getFileName().toString();
return name.endsWith(".zip") && name.contains(model.getUuid().toString());
})
.findFirst()
.orElseThrow(
() ->
new CustomApiException(
"NOT_FOUND_DATA", HttpStatus.NOT_FOUND, "zip 파일이 없습니다."));
log.info("유지 파일: {}", keep.getFileName());
// ===== 결과 세팅 =====
result.setTotalCount(files.size());
result.setKeptFile(keep.getFileName().toString());
// ===== 삭제 대상 =====
List<String> deleteTargets =
files.stream()
.filter(
p -> !p.toAbsolutePath().normalize().equals(keep.toAbsolutePath().normalize()))
.map(p -> p.getFileName().toString())
.toList();
result.setDeleteTargets(deleteTargets);
log.info(
"previewCleanup 완료. total={}, deleteTargets={}",
result.getTotalCount(),
deleteTargets.size());
return result;
} catch (IOException e) {
log.error("파일 목록 조회 실패: {}", dir, e);
throw new CustomApiException(
"INTERNAL_SERVER_ERROR", HttpStatus.INTERNAL_SERVER_ERROR, "파일 목록 조회 실패");
}
}
public CleanupResult cleanup(UUID uuid) {
// ===== 모델 조회 =====
ModelTrainMngDto.Basic model = modelTrainDetailCoreService.findByModelByUUID(uuid);
if (model == null) {
throw new CustomApiException(
"NOT_FOUND_DATA", HttpStatus.NOT_FOUND, "모델을 찾을 수 없습니다. UUID: " + uuid);
}
if (!TrainStatusType.COMPLETED.getId().equals(model.getStep2Status())) {
throw new CustomApiException("CONFLICT", HttpStatus.CONFLICT, "테스트가 완료되지 않았습니다.");
}
// ===== 경로 =====
Path dir = Paths.get(responseDir, model.getUuid().toString()).toAbsolutePath().normalize();
return executeCleanup(model, dir);
}
/**
* 베스트 에폭 제외 파일 삭제, 베스트 에폭 zip 파일만 남김
*
* @param model model 정보
* @param dir response 폴더 경로
* @return 삭제 정보
*/
public CleanupResult executeCleanup(ModelTrainMngDto.Basic model, Path dir) {
CleanupResult result = new CleanupResult();
if (!Files.exists(dir) || !Files.isDirectory(dir)) {
throw new CustomApiException("NOT_FOUND_DATA", HttpStatus.NOT_FOUND, "디렉토리가 없습니다.");
}
if (!Files.isReadable(dir)) {
throw new CustomApiException("FORBIDDEN", HttpStatus.FORBIDDEN, "디렉토리 읽기 권한이 없습니다.");
}
if (!Files.isWritable(dir)) {
throw new CustomApiException("FORBIDDEN", HttpStatus.FORBIDDEN, "디렉토리 삭제 권한이 없습니다.");
}
int bestEpoch = model.getBestEpoch();
if (bestEpoch <= 0) {
throw new CustomApiException(
"BAD_REQUEST", HttpStatus.BAD_REQUEST, "잘못된 bestEpoch 값 입니다. : " + bestEpoch);
}
log.info("cleanup 시작. dir={}, bestEpoch={}", dir, bestEpoch);
try (Stream<Path> stream = Files.list(dir)) {
List<Path> files = stream.toList();
if (files.isEmpty()) {
throw new CustomApiException("NOT_FOUND_DATA", HttpStatus.NOT_FOUND, "파일이 없습니다.");
}
// ===== keep 파일 찾기 =====
Path keep = null;
for (Path p : files) {
String name = p.getFileName().toString();
if (name.endsWith(".zip") && name.contains(model.getUuid().toString())) {
keep = p;
break;
}
}
if (keep == null) {
throw new CustomApiException("NOT_FOUND_DATA", HttpStatus.NOT_FOUND, "zip 파일이 없습니다.");
}
log.info("유지 파일: {}", keep.getFileName());
result.setTotalCount(files.size());
result.setKeptFile(keep.getFileName().toString());
int deletedCount = 0;
List<String> failed = new ArrayList<>();
// ===== 삭제 =====
for (Path p : files) {
if (p.equals(keep)) {
continue;
}
try {
// 심볼릭 링크 → 링크만 삭제
if (Files.isSymbolicLink(p)) {
Files.deleteIfExists(p);
log.info("심볼릭 링크 삭제: {}", p.getFileName());
}
// 디렉토리 → 재귀 삭제
else if (Files.isDirectory(p, LinkOption.NOFOLLOW_LINKS)) {
log.info("디렉토리 재귀 삭제: {}", p.getFileName());
deleteDirectory(p);
}
// 일반 파일
else {
Files.deleteIfExists(p);
log.info("파일 삭제: {}", p.getFileName());
}
deletedCount++;
} catch (AccessDeniedException e) {
failed.add(p.getFileName().toString());
log.error("권한 없음: {}", p.getFileName(), e);
} catch (IOException e) {
failed.add(p.getFileName().toString());
log.error("삭제 실패: {}", p.getFileName(), e);
}
}
result.setDeletedCount(deletedCount);
result.setFailedCount(failed.size());
result.setFailedFiles(failed);
} catch (IOException e) {
log.error("파일 목록 조회 실패: {}", dir, e);
throw new CustomApiException(
"INTERNAL_SERVER_ERROR", HttpStatus.INTERNAL_SERVER_ERROR, "파일 목록 조회 실패");
}
log.info(
"cleanup 완료. total={}, deleted={}, failed={}",
result.getTotalCount(),
result.getDeletedCount(),
result.getFailedCount());
return result;
}
// 디렉토리 재귀 삭제
private void deleteDirectory(Path dir) throws IOException {
if (!Files.exists(dir, LinkOption.NOFOLLOW_LINKS)) {
return;
}
// dir 자체가 심볼릭 링크면 링크만 삭제
if (Files.isSymbolicLink(dir)) {
Files.delete(dir);
return;
}
Files.walkFileTree(
dir,
EnumSet.noneOf(FileVisitOption.class), // NOFOLLOW_LINKS
Integer.MAX_VALUE,
new SimpleFileVisitor<>() {
@Override
public FileVisitResult visitFile(Path file, BasicFileAttributes attrs)
throws IOException {
Files.delete(file); // 링크면 링크만 삭제됨
return FileVisitResult.CONTINUE;
}
@Override
public FileVisitResult postVisitDirectory(Path directory, IOException exc)
throws IOException {
if (exc != null) throw exc;
Files.delete(directory);
return FileVisitResult.CONTINUE;
}
});
}
} }

View File

@@ -3,6 +3,7 @@ package com.kamco.cd.training.model.service;
import com.kamco.cd.training.common.dto.HyperParam; import com.kamco.cd.training.common.dto.HyperParam;
import com.kamco.cd.training.common.enums.HyperParamSelectType; import com.kamco.cd.training.common.enums.HyperParamSelectType;
import com.kamco.cd.training.common.enums.ModelType; import com.kamco.cd.training.common.enums.ModelType;
import com.kamco.cd.training.common.enums.TrainStatusType;
import com.kamco.cd.training.common.enums.TrainType; import com.kamco.cd.training.common.enums.TrainType;
import com.kamco.cd.training.common.exception.CustomApiException; import com.kamco.cd.training.common.exception.CustomApiException;
import com.kamco.cd.training.dataset.dto.DatasetDto.DatasetReq; import com.kamco.cd.training.dataset.dto.DatasetDto.DatasetReq;
@@ -14,10 +15,21 @@ import com.kamco.cd.training.model.dto.ModelTrainMngDto.SearchReq;
import com.kamco.cd.training.postgres.core.HyperParamCoreService; import com.kamco.cd.training.postgres.core.HyperParamCoreService;
import com.kamco.cd.training.postgres.core.ModelTrainMngCoreService; import com.kamco.cd.training.postgres.core.ModelTrainMngCoreService;
import com.kamco.cd.training.train.service.TrainJobService; import com.kamco.cd.training.train.service.TrainJobService;
import java.io.IOException;
import java.nio.file.FileVisitOption;
import java.nio.file.FileVisitResult;
import java.nio.file.Files;
import java.nio.file.LinkOption;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.SimpleFileVisitor;
import java.nio.file.attribute.BasicFileAttributes;
import java.util.EnumSet;
import java.util.List; import java.util.List;
import java.util.UUID; import java.util.UUID;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.data.domain.Page; import org.springframework.data.domain.Page;
import org.springframework.http.HttpStatus; import org.springframework.http.HttpStatus;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
@@ -32,6 +44,13 @@ public class ModelTrainMngService {
private final ModelTrainMngCoreService modelTrainMngCoreService; private final ModelTrainMngCoreService modelTrainMngCoreService;
private final HyperParamCoreService hyperParamCoreService; private final HyperParamCoreService hyperParamCoreService;
private final TrainJobService trainJobService; private final TrainJobService trainJobService;
private final ModelTrainDetailService modelTrainDetailService;
@Value("${train.docker.basePath}")
private String basePath;
@Value("${train.docker.responseDir}")
private String responseDir;
/** /**
* 모델학습 조회 * 모델학습 조회
@@ -46,11 +65,195 @@ public class ModelTrainMngService {
/** /**
* 모델학습 삭제 * 모델학습 삭제
* *
* @param uuid * <p>순서: 1. tmp 구조 검증 (예외 발생 가능) 2. DB 삭제 (트랜잭션) 3. 파일 삭제 (실패해도 로그만)
*/ */
@Transactional @Transactional
public void deleteModelTrain(UUID uuid) { public void deleteModelTrain(UUID uuid) {
log.info("deleteModelTrain 시작. uuid={}", uuid);
// ===== 1. 모델 조회 =====
ModelTrainMngDto.Basic model = modelTrainMngCoreService.findModelByUuid(uuid);
if (model == null) {
throw new CustomApiException("NOT_FOUND", HttpStatus.NOT_FOUND, "모델 없음");
}
// ===== 2. 경로 생성 =====
Path tmpBase = Path.of(basePath, "tmp").toAbsolutePath().normalize();
Path tmp = tmpBase.resolve(model.getRequestPath()).normalize();
Path responseBase = Paths.get(responseDir).toAbsolutePath().normalize();
Path response = responseBase.resolve(model.getUuid().toString()).normalize();
// ===== 3. 경로 탈출 방지 =====
if (!tmp.startsWith(tmpBase)) {
throw new CustomApiException("INVALID_PATH", HttpStatus.BAD_REQUEST, "잘못된 tmp 경로");
}
if (!response.startsWith(responseBase)) {
throw new CustomApiException("INVALID_PATH", HttpStatus.BAD_REQUEST, "잘못된 response 경로");
}
// ===== 4. 상태 로그 =====
log.info(
"tmp 상태: exists={}, isDir={}, isSymlink={}",
Files.exists(tmp, LinkOption.NOFOLLOW_LINKS),
Files.isDirectory(tmp, LinkOption.NOFOLLOW_LINKS),
Files.isSymbolicLink(tmp));
log.info(
"response 상태: exists={}, isDir={}, isSymlink={}",
Files.exists(response, LinkOption.NOFOLLOW_LINKS),
Files.isDirectory(response, LinkOption.NOFOLLOW_LINKS),
Files.isSymbolicLink(response));
// ===== 5. tmp 구조 검증 =====
validateTmpStructure(tmp);
// ===== 6. DB 삭제 =====
modelTrainMngCoreService.deleteModel(uuid); modelTrainMngCoreService.deleteModel(uuid);
log.info("DB 삭제 완료. uuid={}", uuid);
// ===== 7. tmp 삭제 =====
log.info("tmp 삭제 시작: {}", tmp);
try {
deleteTmpDirectory(tmp);
log.info("tmp 삭제 완료: {}", tmp);
} catch (Exception e) {
log.error("tmp 삭제 실패 (DB는 이미 삭제됨): {}", tmp, e);
}
// ===== 8. response 삭제 =====
log.info("response 삭제 시작: {}", response);
try {
// 테스트 완료되었으면 베스트 에폭은 삭제안함
if (TrainStatusType.COMPLETED.getId().equals(model.getStep2Status())) {
modelTrainDetailService.executeCleanup(model, response);
} else {
deleteResponseDirectory(response);
}
log.info("response 삭제 완료: {}", response);
} catch (Exception e) {
log.error("response 삭제 실패 (DB는 이미 삭제됨): {}", response, e);
}
log.info("deleteModelTrain 완료. uuid={}", uuid);
}
/** tmp 디렉토리 삭제 */
private void deleteTmpDirectory(Path dir) throws IOException {
if (!Files.exists(dir, LinkOption.NOFOLLOW_LINKS)) {
log.warn("삭제 대상 없음: {}", dir);
return;
}
Files.walkFileTree(
dir,
EnumSet.noneOf(FileVisitOption.class),
Integer.MAX_VALUE,
new SimpleFileVisitor<>() {
@Override
public FileVisitResult visitFile(Path file, BasicFileAttributes attrs)
throws IOException {
Files.delete(file);
return FileVisitResult.CONTINUE;
}
@Override
public FileVisitResult postVisitDirectory(Path directory, IOException exc)
throws IOException {
if (exc != null) {
throw exc;
}
Files.delete(directory);
return FileVisitResult.CONTINUE;
}
});
}
/** response 디렉토리 삭제 */
private void deleteResponseDirectory(Path dir) throws IOException {
if (!Files.exists(dir, LinkOption.NOFOLLOW_LINKS)) {
log.warn("삭제 대상 없음: {}", dir);
return;
}
Files.walkFileTree(
dir,
EnumSet.noneOf(FileVisitOption.class),
Integer.MAX_VALUE,
new SimpleFileVisitor<>() {
@Override
public FileVisitResult visitFile(Path file, BasicFileAttributes attrs)
throws IOException {
Files.delete(file);
return FileVisitResult.CONTINUE;
}
@Override
public FileVisitResult postVisitDirectory(Path directory, IOException exc)
throws IOException {
if (exc != null) {
throw exc;
}
Files.delete(directory);
return FileVisitResult.CONTINUE;
}
});
}
/** tmp 내부 구조 검증 - 내부는 반드시 symlink만 허용 */
private void validateTmpStructure(Path dir) {
if (!Files.exists(dir, LinkOption.NOFOLLOW_LINKS)) {
return;
}
try {
Files.walkFileTree(
dir,
EnumSet.noneOf(FileVisitOption.class),
Integer.MAX_VALUE,
new SimpleFileVisitor<>() {
@Override
public FileVisitResult visitFile(Path file, BasicFileAttributes attrs)
throws IOException {
// 파일은 전부 허용 (일반 + symlink)
return FileVisitResult.CONTINUE;
}
@Override
public FileVisitResult preVisitDirectory(Path directory, BasicFileAttributes attrs)
throws IOException {
// 루트 제외 + symlink 디렉토리 금지
if (!directory.equals(dir) && Files.isSymbolicLink(directory)) {
log.error("tmp 내부에 symlink 디렉토리 존재: {}", directory);
throw new CustomApiException(
"BAD_REQUEST", HttpStatus.BAD_REQUEST, "tmp 내부에 symlink 디렉토리는 허용되지 않습니다.");
}
return FileVisitResult.CONTINUE;
}
});
} catch (IOException e) {
throw new CustomApiException(
"INTERNAL_SERVER_ERROR", HttpStatus.INTERNAL_SERVER_ERROR, "tmp 구조 검증 실패");
}
} }
/** /**

View File

@@ -1,6 +1,5 @@
package com.kamco.cd.training.postgres.core; package com.kamco.cd.training.postgres.core;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.kamco.cd.training.common.enums.LearnDataRegister; import com.kamco.cd.training.common.enums.LearnDataRegister;
import com.kamco.cd.training.common.enums.LearnDataType; import com.kamco.cd.training.common.enums.LearnDataType;
import com.kamco.cd.training.common.exception.NotFoundException; import com.kamco.cd.training.common.exception.NotFoundException;
@@ -30,9 +29,9 @@ import org.springframework.stereotype.Service;
@Slf4j @Slf4j
public class DatasetCoreService public class DatasetCoreService
implements BaseCoreService<DatasetDto.Basic, Long, DatasetDto.SearchReq> { implements BaseCoreService<DatasetDto.Basic, Long, DatasetDto.SearchReq> {
private final DatasetRepository datasetRepository; private final DatasetRepository datasetRepository;
private final DatasetObjRepository datasetObjRepository; private final DatasetObjRepository datasetObjRepository;
private final ObjectMapper objectMapper;
/** /**
* 학습 데이터 삭제 * 학습 데이터 삭제
@@ -96,6 +95,7 @@ public class DatasetCoreService
return search(searchReq); return search(searchReq);
} }
// TODO 미사용시작
/** /**
* 학습데이터 등록 * 학습데이터 등록
* *
@@ -130,6 +130,7 @@ public class DatasetCoreService
return savedEntity.toDto(); return savedEntity.toDto();
} }
// TODO 미사용 끝
/** /**
* 학습 데이터 수정 * 학습 데이터 수정
* *
@@ -221,7 +222,6 @@ public class DatasetCoreService
return datasetRepository.insertDatasetMngData(mngRegDto); return datasetRepository.insertDatasetMngData(mngRegDto);
} }
@Transactional
public void insertDatasetObj(DatasetObjRegDto objRegDto) { public void insertDatasetObj(DatasetObjRegDto objRegDto) {
datasetObjRepository.insertDatasetObj(objRegDto); datasetObjRepository.insertDatasetObj(objRegDto);
} }
@@ -234,13 +234,20 @@ public class DatasetCoreService
datasetObjRepository.insertDatasetTestObj(objRegDto); datasetObjRepository.insertDatasetTestObj(objRegDto);
} }
public void updateDatasetUploadStatus(Long datasetUid) { /**
* 학습데이터셋 마스터 상태 변경
*
* @param datasetUid 학습데이터셋 마스터 id
* @param register 상태
*/
@Transactional
public void updateDatasetUploadStatus(Long datasetUid, LearnDataRegister register) {
DatasetEntity entity = DatasetEntity entity =
datasetRepository datasetRepository
.findById(datasetUid) .findById(datasetUid)
.orElseThrow(() -> new NotFoundException("데이터셋을 찾을 수 없습니다. ID: " + datasetUid)); .orElseThrow(() -> new NotFoundException("데이터셋을 찾을 수 없습니다. ID: " + datasetUid));
entity.setStatus(LearnDataRegister.COMPLETED.getId()); entity.setStatus(register.getId());
} }
public void insertDatasetValObj(DatasetObjRegDto objRegDto) { public void insertDatasetValObj(DatasetObjRegDto objRegDto) {
@@ -250,4 +257,15 @@ public class DatasetCoreService
public Long findDatasetByUidExistsCnt(String uid) { public Long findDatasetByUidExistsCnt(String uid) {
return datasetRepository.findDatasetByUidExistsCnt(uid); return datasetRepository.findDatasetByUidExistsCnt(uid);
} }
/**
* 데이터셋 등록 실패시 Obj 데이터 정리
*
* @param datasetUid 모델 마스터 id
*/
@Transactional
public void deleteAllDatasetObj(Long datasetUid) {
int cnt = datasetObjRepository.deleteAllDatasetObj(datasetUid);
log.info("datasetUid={} 데이터셋 실패 - 전체 삭제 완료. 총 {}건", datasetUid, cnt);
}
} }

View File

@@ -1,3 +1,4 @@
// TODO 미사용시작
package com.kamco.cd.training.postgres.core; package com.kamco.cd.training.postgres.core;
import com.kamco.cd.training.common.exception.NotFoundException; import com.kamco.cd.training.common.exception.NotFoundException;
@@ -70,3 +71,4 @@ public class MapSheetCoreService
} }
} }
} }
// TODO 미사용 끝

View File

@@ -67,10 +67,13 @@ public class ModelTrainDetailCoreService {
return modelDetailRepository.getByModelHyperParamSummary(uuid); return modelDetailRepository.getByModelHyperParamSummary(uuid);
} }
// TODO 미사용시작
public TransferHyperSummary getTransferHyperSummary(UUID uuid) { public TransferHyperSummary getTransferHyperSummary(UUID uuid) {
return modelDetailRepository.getByModelTransferHyperParamSummary(uuid); return modelDetailRepository.getByModelTransferHyperParamSummary(uuid);
} }
// TODO 미사용 끝
public List<MappingDataset> getByModelMappingDataset(UUID uuid) { public List<MappingDataset> getByModelMappingDataset(UUID uuid) {
return modelDetailRepository.getByModelMappingDataset(uuid); return modelDetailRepository.getByModelMappingDataset(uuid);
} }
@@ -80,6 +83,7 @@ public class ModelTrainDetailCoreService {
return entity.toDto(); return entity.toDto();
} }
// TODO 미사용시작
/** /**
* 모델 학습별 config 정보 조회 * 모델 학습별 config 정보 조회
* *
@@ -90,6 +94,7 @@ public class ModelTrainDetailCoreService {
return modelConfigRepository.findModelConfigByModelId(modelId).orElse(null); return modelConfigRepository.findModelConfigByModelId(modelId).orElse(null);
} }
// TODO 미사용 끝
public List<ModelTrainMetrics> getModelTrainMetricResult(UUID uuid) { public List<ModelTrainMetrics> getModelTrainMetricResult(UUID uuid) {
return modelDetailRepository.getModelTrainMetricResult(uuid); return modelDetailRepository.getModelTrainMetricResult(uuid);
} }

View File

@@ -219,6 +219,7 @@ public class ModelTrainMngCoreService {
modelConfigRepository.save(entity); modelConfigRepository.save(entity);
} }
// TODO 미사용시작
/** /**
* 데이터셋 매핑 생성 * 데이터셋 매핑 생성
* *
@@ -235,6 +236,8 @@ public class ModelTrainMngCoreService {
} }
} }
// TODO 미사용 끝
/** /**
* UUID로 모델 조회 * UUID로 모델 조회
* *
@@ -278,6 +281,7 @@ public class ModelTrainMngCoreService {
.orElseThrow(() -> new CustomApiException("NOT_FOUND_DATA", HttpStatus.NOT_FOUND)); .orElseThrow(() -> new CustomApiException("NOT_FOUND_DATA", HttpStatus.NOT_FOUND));
} }
// TODO 미사용시작
public ModelConfigDto.TransferBasic findModelTransferConfigByModelId(UUID uuid) { public ModelConfigDto.TransferBasic findModelTransferConfigByModelId(UUID uuid) {
ModelMasterEntity modelEntity = findByUuid(uuid); ModelMasterEntity modelEntity = findByUuid(uuid);
return modelConfigRepository return modelConfigRepository
@@ -285,6 +289,8 @@ public class ModelTrainMngCoreService {
.orElseThrow(() -> new CustomApiException("NOT_FOUND_DATA", HttpStatus.NOT_FOUND)); .orElseThrow(() -> new CustomApiException("NOT_FOUND_DATA", HttpStatus.NOT_FOUND));
} }
// TODO 미사용 끝
/** /**
* 데이터셋 G1 목록 * 데이터셋 G1 목록
* *
@@ -295,6 +301,7 @@ public class ModelTrainMngCoreService {
return datasetRepository.getDatasetSelectG1List(req); return datasetRepository.getDatasetSelectG1List(req);
} }
// TODO 미사용시작
/** /**
* 전이학습 데이터셋 G1 목록 * 전이학습 데이터셋 G1 목록
* *
@@ -305,6 +312,8 @@ public class ModelTrainMngCoreService {
return datasetRepository.getDatasetTransferSelectG1List(modelId); return datasetRepository.getDatasetTransferSelectG1List(modelId);
} }
// TODO 미사용 끝
/** /**
* 데이터셋 G2, G3 목록 * 데이터셋 G2, G3 목록
* *
@@ -315,6 +324,7 @@ public class ModelTrainMngCoreService {
return datasetRepository.getDatasetSelectG2G3List(req); return datasetRepository.getDatasetSelectG2G3List(req);
} }
// TODO 미사용시작
/** /**
* 전이학습 데이터셋 G2, G3 목록 * 전이학습 데이터셋 G2, G3 목록
* *
@@ -327,6 +337,8 @@ public class ModelTrainMngCoreService {
return datasetRepository.getDatasetTransferSelectG2G3List(modelId, modelNo); return datasetRepository.getDatasetTransferSelectG2G3List(modelId, modelNo);
} }
// TODO 미사용 끝
/** /**
* 모델관리 조회 * 모델관리 조회
* *
@@ -341,6 +353,20 @@ public class ModelTrainMngCoreService {
return entity.toDto(); return entity.toDto();
} }
/**
* 모델관리 조회
*
* @param uuid
* @return
*/
public ModelTrainMngDto.Basic findModelByUuid(UUID uuid) {
ModelMasterEntity entity =
modelMngRepository
.findByUuid(uuid)
.orElseThrow(() -> new IllegalArgumentException("Model not found: " + uuid));
return entity.toDto();
}
/** 마스터를 IN_PROGRESS로 전환하고, 현재 실행 jobId를 연결 - UI/중단/상태조회 모두 currentAttemptId를 기준으로 동작 */ /** 마스터를 IN_PROGRESS로 전환하고, 현재 실행 jobId를 연결 - UI/중단/상태조회 모두 currentAttemptId를 기준으로 동작 */
@Transactional @Transactional
public void markInProgress(Long modelId, Long jobId) { public void markInProgress(Long modelId, Long jobId) {

View File

@@ -1,3 +1,4 @@
// TODO 미사용시작
package com.kamco.cd.training.postgres.core; package com.kamco.cd.training.postgres.core;
import com.kamco.cd.training.postgres.entity.SystemMetricsEntity; import com.kamco.cd.training.postgres.entity.SystemMetricsEntity;
@@ -64,3 +65,4 @@ public class SystemMetricsCoreService {
return isAvailable; return isAvailable;
} }
} }
// TODO 미사용 끝

View File

@@ -117,10 +117,12 @@ public class DatasetEntity {
@Column(name = "dataset_path", length = 1000) @Column(name = "dataset_path", length = 1000)
private String datasetPath; private String datasetPath;
// TODO 미사용시작
@Column(name = "class_counts") @Column(name = "class_counts")
@JdbcTypeCode(SqlTypes.JSON) @JdbcTypeCode(SqlTypes.JSON)
private Map<String, Integer> classCounts; private Map<String, Integer> classCounts;
// TODO 미사용 끝
@Size(max = 32) @Size(max = 32)
@Column(name = "uid") @Column(name = "uid")
private String uid; private String uid;

View File

@@ -1,3 +1,4 @@
// TODO 미사용시작
package com.kamco.cd.training.postgres.entity; package com.kamco.cd.training.postgres.entity;
import com.kamco.cd.training.dataset.dto.MapSheetDto; import com.kamco.cd.training.dataset.dto.MapSheetDto;
@@ -103,3 +104,4 @@ public class MapSheetEntity {
return dto; return dto;
} }
} }
// TODO 미사용 끝

View File

@@ -32,10 +32,13 @@ public class ModelDatasetMappEntity {
@Column(name = "dataset_uid", nullable = false) @Column(name = "dataset_uid", nullable = false)
private Long datasetUid; private Long datasetUid;
// TODO 미사용시작
@Size(max = 20) @Size(max = 20)
@Column(name = "dataset_type", length = 20) @Column(name = "dataset_type", length = 20)
private String datasetType; private String datasetType;
// TODO 미사용 끝
@Getter @Getter
@Setter @Setter
@NoArgsConstructor @NoArgsConstructor

View File

@@ -141,6 +141,7 @@ public class ModelMasterEntity {
this.packingState, this.packingState,
this.packingStrtDttm, this.packingStrtDttm,
this.packingEndDttm, this.packingEndDttm,
this.beforeModelId); this.beforeModelId,
this.bestEpoch);
} }
} }

View File

@@ -1,3 +1,4 @@
// TODO 미사용시작
package com.kamco.cd.training.postgres.entity; package com.kamco.cd.training.postgres.entity;
import jakarta.persistence.Column; import jakarta.persistence.Column;
@@ -92,3 +93,4 @@ public class ModelMngEntity {
return this.uuid != null ? this.uuid.toString() : null; return this.uuid != null ? this.uuid.toString() : null;
} }
} }
// TODO 미사용 끝

View File

@@ -1,3 +1,4 @@
// TODO 미사용시작
package com.kamco.cd.training.postgres.entity; package com.kamco.cd.training.postgres.entity;
import jakarta.persistence.Column; import jakarta.persistence.Column;
@@ -53,3 +54,4 @@ public class SystemMetricsEntity {
@Column(name = "memused") @Column(name = "memused")
private Float memused; private Float memused;
} }
// TODO 미사용 끝

View File

@@ -1,3 +1,4 @@
// TODO 미사용시작
package com.kamco.cd.training.postgres.repository; package com.kamco.cd.training.postgres.repository;
import com.kamco.cd.training.postgres.entity.SystemMetricsEntity; import com.kamco.cd.training.postgres.entity.SystemMetricsEntity;
@@ -17,3 +18,4 @@ public interface SystemMetricsRepository extends JpaRepository<SystemMetricsEnti
@Query("SELECT s FROM SystemMetricsEntity s ORDER BY s.timestamp DESC LIMIT 1") @Query("SELECT s FROM SystemMetricsEntity s ORDER BY s.timestamp DESC LIMIT 1")
Optional<SystemMetricsEntity> findLatestMetrics(); Optional<SystemMetricsEntity> findLatestMetrics();
} }
// TODO 미사용 끝

View File

@@ -24,4 +24,12 @@ public interface DatasetObjRepositoryCustom {
void insertDatasetTestObj(DatasetObjRegDto objRegDto); void insertDatasetTestObj(DatasetObjRegDto objRegDto);
void insertDatasetValObj(DatasetObjRegDto objRegDto); void insertDatasetValObj(DatasetObjRegDto objRegDto);
/**
* 데이터셋 등록 실패시 Obj 데이터 정리
*
* @param datasetUid
* @return
*/
int deleteAllDatasetObj(Long datasetUid);
} }

View File

@@ -40,6 +40,7 @@ public class DatasetObjRepositoryImpl implements DatasetObjRepositoryCustom {
private final JPAQueryFactory queryFactory; private final JPAQueryFactory queryFactory;
private final QDatasetEntity dataset = datasetEntity; private final QDatasetEntity dataset = datasetEntity;
private final ObjectMapper objectMapper = new ObjectMapper();
@PersistenceContext EntityManager em; @PersistenceContext EntityManager em;
@@ -55,7 +56,6 @@ public class DatasetObjRepositoryImpl implements DatasetObjRepositoryCustom {
@Override @Override
public void insertDatasetTestObj(DatasetObjRegDto objRegDto) { public void insertDatasetTestObj(DatasetObjRegDto objRegDto) {
ObjectMapper objectMapper = new ObjectMapper();
String json; String json;
Geometry geometry; Geometry geometry;
String geometryJson; String geometryJson;
@@ -99,7 +99,6 @@ public class DatasetObjRepositoryImpl implements DatasetObjRepositoryCustom {
@Override @Override
public void insertDatasetValObj(DatasetObjRegDto objRegDto) { public void insertDatasetValObj(DatasetObjRegDto objRegDto) {
ObjectMapper objectMapper = new ObjectMapper();
String json; String json;
String geometryJson; String geometryJson;
try { try {
@@ -219,7 +218,6 @@ public class DatasetObjRepositoryImpl implements DatasetObjRepositoryCustom {
@Override @Override
public void insertDatasetObj(DatasetObjRegDto objRegDto) { public void insertDatasetObj(DatasetObjRegDto objRegDto) {
ObjectMapper objectMapper = new ObjectMapper();
String json; String json;
String geometryJson; String geometryJson;
try { try {
@@ -276,4 +274,38 @@ public class DatasetObjRepositoryImpl implements DatasetObjRepositoryCustom {
.where(datasetObjEntity.uuid.eq(uuid)) .where(datasetObjEntity.uuid.eq(uuid))
.fetchOne(); .fetchOne();
} }
@Override
public int deleteAllDatasetObj(Long datasetUid) {
int cnt = 0;
cnt =
em.createNativeQuery(
"""
delete from tb_dataset_obj
where dataset_uid = ?
""")
.setParameter(1, datasetUid)
.executeUpdate();
cnt +=
em.createNativeQuery(
"""
delete from tb_dataset_val_obj
where dataset_uid = ?
""")
.setParameter(1, datasetUid)
.executeUpdate();
cnt +=
em.createNativeQuery(
"""
delete from tb_dataset_test_obj
where dataset_uid = ?
""")
.setParameter(1, datasetUid)
.executeUpdate();
em.clear();
return cnt;
}
} }

View File

@@ -6,6 +6,7 @@ import org.springframework.data.jpa.repository.JpaRepository;
public interface DatasetRepository public interface DatasetRepository
extends JpaRepository<DatasetEntity, Long>, DatasetRepositoryCustom { extends JpaRepository<DatasetEntity, Long>, DatasetRepositoryCustom {
// TODO 미사용시작
List<DatasetEntity> findByDeletedOrderByCreatedDttmDesc(Boolean deleted); List<DatasetEntity> findByDeletedOrderByCreatedDttmDesc(Boolean deleted);
// TODO 미사용 끝
} }

View File

@@ -18,10 +18,13 @@ public interface DatasetRepositoryCustom {
List<SelectDataSet> getDatasetSelectG1List(DatasetReq req); List<SelectDataSet> getDatasetSelectG1List(DatasetReq req);
// TODO 미사용시작
public List<SelectTransferDataSet> getDatasetTransferSelectG1List(Long modelId); public List<SelectTransferDataSet> getDatasetTransferSelectG1List(Long modelId);
public List<SelectTransferDataSet> getDatasetTransferSelectG2G3List(Long modelId, String modelNo); public List<SelectTransferDataSet> getDatasetTransferSelectG2G3List(Long modelId, String modelNo);
// TODO 미사용 끝
List<SelectDataSet> getDatasetSelectG2G3List(DatasetReq req); List<SelectDataSet> getDatasetSelectG2G3List(DatasetReq req);
Long getDatasetMaxStage(int compareYyyy, int targetYyyy); Long getDatasetMaxStage(int compareYyyy, int targetYyyy);

View File

@@ -98,6 +98,8 @@ public class DatasetRepositoryImpl implements DatasetRepositoryCustom {
BooleanBuilder builder = new BooleanBuilder(); BooleanBuilder builder = new BooleanBuilder();
builder.and(dataset.deleted.isFalse());
if (StringUtils.isNotBlank(req.getDataType()) && !"CURRENT".equals(req.getDataType())) { if (StringUtils.isNotBlank(req.getDataType()) && !"CURRENT".equals(req.getDataType())) {
builder.and(dataset.dataType.eq(req.getDataType())); builder.and(dataset.dataType.eq(req.getDataType()));
} }
@@ -148,6 +150,7 @@ public class DatasetRepositoryImpl implements DatasetRepositoryCustom {
.fetch(); .fetch();
} }
// TODO 미사용시작
@Override @Override
public List<SelectTransferDataSet> getDatasetTransferSelectG1List(Long modelId) { public List<SelectTransferDataSet> getDatasetTransferSelectG1List(Long modelId) {
@@ -245,10 +248,12 @@ public class DatasetRepositoryImpl implements DatasetRepositoryCustom {
.fetch(); .fetch();
} }
// TODO 미사용 끝
@Override @Override
public List<SelectDataSet> getDatasetSelectG2G3List(DatasetReq req) { public List<SelectDataSet> getDatasetSelectG2G3List(DatasetReq req) {
BooleanBuilder builder = new BooleanBuilder(); BooleanBuilder builder = new BooleanBuilder();
builder.and(dataset.deleted.isFalse());
NumberExpression<Long> selectedCnt = null; NumberExpression<Long> selectedCnt = null;
NumberExpression<Long> wasteCnt = NumberExpression<Long> wasteCnt =
@@ -308,6 +313,7 @@ public class DatasetRepositoryImpl implements DatasetRepositoryCustom {
.fetch(); .fetch();
} }
// TODO 미사용시작
@Override @Override
public List<SelectTransferDataSet> getDatasetTransferSelectG2G3List( public List<SelectTransferDataSet> getDatasetTransferSelectG2G3List(
Long modelId, String modelNo) { Long modelId, String modelNo) {
@@ -418,6 +424,8 @@ public class DatasetRepositoryImpl implements DatasetRepositoryCustom {
.fetch(); .fetch();
} }
// TODO 미사용 끝
@Override @Override
public Long getDatasetMaxStage(int compareYyyy, int targetYyyy) { public Long getDatasetMaxStage(int compareYyyy, int targetYyyy) {
return queryFactory return queryFactory

View File

@@ -1,3 +1,4 @@
// TODO 미사용시작
package com.kamco.cd.training.postgres.repository.dataset; package com.kamco.cd.training.postgres.repository.dataset;
import com.kamco.cd.training.postgres.entity.MapSheetEntity; import com.kamco.cd.training.postgres.entity.MapSheetEntity;
@@ -11,3 +12,4 @@ public interface MapSheetRepository
long countByDatasetIdAndDeletedFalse(Long datasetId); long countByDatasetIdAndDeletedFalse(Long datasetId);
} }
// TODO 미사용 끝

View File

@@ -1,3 +1,4 @@
// TODO 미사용시작
package com.kamco.cd.training.postgres.repository.dataset; package com.kamco.cd.training.postgres.repository.dataset;
import com.kamco.cd.training.dataset.dto.MapSheetDto; import com.kamco.cd.training.dataset.dto.MapSheetDto;
@@ -7,3 +8,4 @@ import org.springframework.data.domain.Page;
public interface MapSheetRepositoryCustom { public interface MapSheetRepositoryCustom {
Page<MapSheetEntity> findMapSheetList(MapSheetDto.SearchReq searchReq); Page<MapSheetEntity> findMapSheetList(MapSheetDto.SearchReq searchReq);
} }
// TODO 미사용 끝

View File

@@ -1,3 +1,4 @@
// TODO 미사용시작
package com.kamco.cd.training.postgres.repository.dataset; package com.kamco.cd.training.postgres.repository.dataset;
import com.kamco.cd.training.dataset.dto.MapSheetDto; import com.kamco.cd.training.dataset.dto.MapSheetDto;
@@ -52,3 +53,4 @@ public class MapSheetRepositoryImpl implements MapSheetRepositoryCustom {
return new PageImpl<>(content, pageable, total); return new PageImpl<>(content, pageable, total);
} }
} }
// TODO 미사용 끝

View File

@@ -8,5 +8,7 @@ import org.springframework.stereotype.Repository;
@Repository @Repository
public interface HyperParamRepository public interface HyperParamRepository
extends JpaRepository<ModelHyperParamEntity, Long>, HyperParamRepositoryCustom { extends JpaRepository<ModelHyperParamEntity, Long>, HyperParamRepositoryCustom {
// TODO 미사용시작
Optional<ModelHyperParamEntity> findByHyperVer(String hyperVer); Optional<ModelHyperParamEntity> findByHyperVer(String hyperVer);
// TODO 미사용 끝
} }

View File

@@ -11,6 +11,7 @@ import org.springframework.data.domain.Page;
public interface HyperParamRepositoryCustom { public interface HyperParamRepositoryCustom {
// TODO 미사용시작
/** /**
* 마지막 버전 조회 * 마지막 버전 조회
* *
@@ -19,6 +20,8 @@ public interface HyperParamRepositoryCustom {
@Deprecated @Deprecated
Optional<ModelHyperParamEntity> findHyperParamVer(); Optional<ModelHyperParamEntity> findHyperParamVer();
// TODO 미사용 끝
/** /**
* 모델 타입별 마지막 버전 조회 * 모델 타입별 마지막 버전 조회
* *
@@ -27,8 +30,11 @@ public interface HyperParamRepositoryCustom {
*/ */
Optional<ModelHyperParamEntity> findHyperParamVerByModelType(ModelType modelType); Optional<ModelHyperParamEntity> findHyperParamVerByModelType(ModelType modelType);
// TODO 미사용시작
Optional<ModelHyperParamEntity> findHyperParamByHyperVer(String hyperVer); Optional<ModelHyperParamEntity> findHyperParamByHyperVer(String hyperVer);
// TODO 미사용 끝
/** /**
* 하이퍼 파라미터 상세조회 * 하이퍼 파라미터 상세조회
* *

View File

@@ -29,6 +29,7 @@ public class HyperParamRepositoryImpl implements HyperParamRepositoryCustom {
private final JPAQueryFactory queryFactory; private final JPAQueryFactory queryFactory;
// TODO 미사용시작
@Override @Override
public Optional<ModelHyperParamEntity> findHyperParamVer() { public Optional<ModelHyperParamEntity> findHyperParamVer() {
@@ -42,6 +43,8 @@ public class HyperParamRepositoryImpl implements HyperParamRepositoryCustom {
.fetchOne()); .fetchOne());
} }
// TODO 미사용 끝
@Override @Override
public Optional<ModelHyperParamEntity> findHyperParamVerByModelType(ModelType modelType) { public Optional<ModelHyperParamEntity> findHyperParamVerByModelType(ModelType modelType) {
@@ -59,6 +62,7 @@ public class HyperParamRepositoryImpl implements HyperParamRepositoryCustom {
.fetchOne()); .fetchOne());
} }
// TODO 미사용시작
@Override @Override
public Optional<ModelHyperParamEntity> findHyperParamByHyperVer(String hyperVer) { public Optional<ModelHyperParamEntity> findHyperParamByHyperVer(String hyperVer) {
@@ -75,6 +79,8 @@ public class HyperParamRepositoryImpl implements HyperParamRepositoryCustom {
.fetchOne()); .fetchOne());
} }
// TODO 미사용 끝
@Override @Override
public Optional<ModelHyperParamEntity> findHyperParamByUuid(UUID uuid) { public Optional<ModelHyperParamEntity> findHyperParamByUuid(UUID uuid) {
return Optional.ofNullable( return Optional.ofNullable(

View File

@@ -7,14 +7,18 @@ import java.util.UUID;
import org.springframework.data.domain.Page; import org.springframework.data.domain.Page;
public interface MembersRepositoryCustom { public interface MembersRepositoryCustom {
// TODO 미사용시작
boolean existsByUserId(String userId); boolean existsByUserId(String userId);
// TODO 미사용 끝
boolean existsByEmployeeNo(String employeeNo); boolean existsByEmployeeNo(String employeeNo);
Optional<MemberEntity> findByEmployeeNo(String employeeNo); Optional<MemberEntity> findByEmployeeNo(String employeeNo);
// TODO 미사용시작
Optional<MemberEntity> findByUserId(String userId); Optional<MemberEntity> findByUserId(String userId);
// TODO 미사용 끝
Optional<MemberEntity> findByUUID(UUID uuid); Optional<MemberEntity> findByUUID(UUID uuid);
Page<MemberEntity> findByMembers(MembersDto.SearchReq searchReq); Page<MemberEntity> findByMembers(MembersDto.SearchReq searchReq);

View File

@@ -27,6 +27,7 @@ public class MembersRepositoryImpl extends QuerydslRepositorySupport
this.queryFactory = queryFactory; this.queryFactory = queryFactory;
} }
// TODO 미사용시작
/** /**
* 사용자 ID 조회 * 사용자 ID 조회
* *
@@ -43,6 +44,8 @@ public class MembersRepositoryImpl extends QuerydslRepositorySupport
!= null; != null;
} }
// TODO 미사용 끝
/** /**
* 사용자 사번 조회 * 사용자 사번 조회
* *
@@ -59,6 +62,7 @@ public class MembersRepositoryImpl extends QuerydslRepositorySupport
!= null; != null;
} }
// TODO 미사용시작
/** /**
* 사용자 조회 user id * 사용자 조회 user id
* *
@@ -71,6 +75,8 @@ public class MembersRepositoryImpl extends QuerydslRepositorySupport
queryFactory.selectFrom(memberEntity).where(memberEntity.userId.eq(userId)).fetchOne()); queryFactory.selectFrom(memberEntity).where(memberEntity.userId.eq(userId)).fetchOne());
} }
// TODO 미사용 끝
/** /**
* 사용자 조회 employeed no * 사용자 조회 employeed no
* *

View File

@@ -6,5 +6,7 @@ import java.util.Optional;
public interface ModelConfigRepositoryCustom { public interface ModelConfigRepositoryCustom {
Optional<ModelConfigDto.Basic> findModelConfigByModelId(Long modelId); Optional<ModelConfigDto.Basic> findModelConfigByModelId(Long modelId);
// TODO 미사용시작
Optional<ModelConfigDto.TransferBasic> findModelTransferConfigByModelId(Long modelId); Optional<ModelConfigDto.TransferBasic> findModelTransferConfigByModelId(Long modelId);
// TODO 미사용 끝
} }

View File

@@ -39,6 +39,7 @@ public class ModelConfigRepositoryImpl implements ModelConfigRepositoryCustom {
.fetchOne()); .fetchOne());
} }
// TODO 미사용시작
@Override @Override
public Optional<TransferBasic> findModelTransferConfigByModelId(Long modelId) { public Optional<TransferBasic> findModelTransferConfigByModelId(Long modelId) {
QModelConfigEntity beforeConfig = new QModelConfigEntity("beforeConfig"); QModelConfigEntity beforeConfig = new QModelConfigEntity("beforeConfig");
@@ -78,4 +79,5 @@ public class ModelConfigRepositoryImpl implements ModelConfigRepositoryCustom {
.where(modelMasterEntity.id.eq(modelId)) .where(modelMasterEntity.id.eq(modelId))
.fetchOne()); .fetchOne());
} }
// TODO 미사용 끝
} }

View File

@@ -23,8 +23,11 @@ public interface ModelDetailRepositoryCustom {
HyperSummary getByModelHyperParamSummary(UUID uuid); HyperSummary getByModelHyperParamSummary(UUID uuid);
// TODO 미사용시작
TransferHyperSummary getByModelTransferHyperParamSummary(UUID uuid); TransferHyperSummary getByModelTransferHyperParamSummary(UUID uuid);
// TODO 미사용 끝
List<MappingDataset> getByModelMappingDataset(UUID uuid); List<MappingDataset> getByModelMappingDataset(UUID uuid);
ModelMasterEntity findByModelByUUID(UUID uuid); ModelMasterEntity findByModelByUUID(UUID uuid);

View File

@@ -20,8 +20,11 @@ public interface ModelMngRepositoryCustom {
Optional<ModelMasterEntity> findByUuid(UUID uuid); Optional<ModelMasterEntity> findByUuid(UUID uuid);
// TODO 미사용시작
Optional<ModelMasterEntity> findFirstByStatusCdAndDelYn(String statusCd, Boolean delYn); Optional<ModelMasterEntity> findFirstByStatusCdAndDelYn(String statusCd, Boolean delYn);
// TODO 미사용 끝
TrainRunRequest findTrainRunRequest(Long modelId); TrainRunRequest findTrainRunRequest(Long modelId);
Long findModelStep1InProgressCnt(); Long findModelStep1InProgressCnt();

View File

@@ -133,11 +133,14 @@ public class ModelMngRepositoryImpl implements ModelMngRepositoryCustom {
.fetchOne()); .fetchOne());
} }
// TODO 미사용시작
@Override @Override
public Optional<ModelMasterEntity> findFirstByStatusCdAndDelYn(String statusCd, Boolean delYn) { public Optional<ModelMasterEntity> findFirstByStatusCdAndDelYn(String statusCd, Boolean delYn) {
return Optional.empty(); return Optional.empty();
} }
// TODO 미사용 끝
@Override @Override
public TrainRunRequest findTrainRunRequest(Long modelId) { public TrainRunRequest findTrainRunRequest(Long modelId) {
return queryFactory return queryFactory

View File

@@ -46,10 +46,17 @@ public class DockerTrainService {
@Value("${train.docker.shmSize:16g}") @Value("${train.docker.shmSize:16g}")
private String shmSize; private String shmSize;
// data 경로 request,response 상위 폴더
@Value("${train.docker.basePath}")
private String basePath;
// IPC host 사용 여부 // IPC host 사용 여부
@Value("${train.docker.ipcHost:true}") @Value("${train.docker.ipcHost:true}")
private boolean ipcHost; private boolean ipcHost;
@Value("${spring.profiles.active}")
private String profile;
private final ModelTrainJobCoreService modelTrainJobCoreService; private final ModelTrainJobCoreService modelTrainJobCoreService;
/** /**
@@ -254,7 +261,9 @@ public class DockerTrainService {
// 요청/결과 디렉토리 볼륨 마운트 // 요청/결과 디렉토리 볼륨 마운트
c.add("-v"); c.add("-v");
c.add("/home/kcomu/data" + "/tmp:/data"); c.add(basePath + ":" + basePath); // 심볼릭 링크와 연결되는 실제 파일 경로도 마운트를 해줘야 함
c.add("-v");
c.add(basePath + "/tmp:/data");
c.add("-v"); c.add("-v");
c.add(responseDir + ":/checkpoints"); c.add(responseDir + ":/checkpoints");
@@ -273,8 +282,15 @@ public class DockerTrainService {
addArg(c, "--output-folder", req.getOutputFolder()); addArg(c, "--output-folder", req.getOutputFolder());
addArg(c, "--input-size", req.getInputSize()); addArg(c, "--input-size", req.getInputSize());
addArg(c, "--crop-size", req.getCropSize()); addArg(c, "--crop-size", req.getCropSize());
addArg(c, "--batch-size", req.getBatchSize()); // addArg(c, "--batch-size", req.getBatchSize());
addArg(c, "--gpu-ids", req.getGpuIds()); // null // addArg(c, "--gpu-ids", req.getGpuIds()); // null
if ("prod".equals(profile)) {
addArg(c, "--batch-size", 2); // 학습서버 GPU 1개인 곳은 batch-size:2 까지만 가능
addArg(c, "--gpus", "1"); // 학습서버 GPU 1개인 곳은 1이어야 함
addArg(c, "--gpu-ids", "0"); // 학습서버 GPU 1개인 곳은 0이어야 함
} else {
addArg(c, "--batch-size", req.getBatchSize()); // 학습서버 GPU 1개인 곳은 batch-size:2 까지만 가능
}
addArg(c, "--lr", req.getLearningRate()); addArg(c, "--lr", req.getLearningRate());
addArg(c, "--backbone", req.getBackbone()); addArg(c, "--backbone", req.getBackbone());
addArg(c, "--epochs", req.getEpochs()); addArg(c, "--epochs", req.getEpochs());
@@ -440,11 +456,14 @@ public class DockerTrainService {
c.add("--rm"); c.add("--rm");
c.add("--gpus"); c.add("--gpus");
c.add("all"); c.add("all");
c.add("--ipc=host"); c.add("--ipc=host");
c.add("--shm-size=" + shmSize); c.add("--shm-size=" + shmSize);
c.add("-v"); c.add("-v");
c.add("/home/kcomu/data" + "/tmp:/data"); c.add(basePath + ":" + basePath); // 심볼릭 링크와 연결되는 실제 파일 경로도 마운트를 해줘야 함
c.add("-v");
c.add(basePath + "/tmp:/data");
c.add("-v"); c.add("-v");
c.add(responseDir + ":/checkpoints"); c.add(responseDir + ":/checkpoints");

View File

@@ -84,18 +84,35 @@ public class ModelTrainMetricsJobService {
for (CSVRecord record : parser) { for (CSVRecord record : parser) {
int epoch = Integer.parseInt(record.get("Epoch")); int epoch = Integer.parseInt(record.get("Epoch"));
float aAcc = Float.parseFloat(record.get("aAcc"));
float mFscore = Float.parseFloat(record.get("mFscore")); float aAcc = parseFloatSafe(record.get("aAcc"));
float mPrecision = Float.parseFloat(record.get("mPrecision")); float mFscore = parseFloatSafe(record.get("mFscore"));
float mRecall = Float.parseFloat(record.get("mRecall")); float mPrecision = parseFloatSafe(record.get("mPrecision"));
float mIoU = Float.parseFloat(record.get("mIoU")); float mRecall = parseFloatSafe(record.get("mRecall"));
float mAcc = Float.parseFloat(record.get("mAcc")); float mIoU = parseFloatSafe(record.get("mIoU"));
float changed_fscore = Float.parseFloat(record.get("changed_fscore")); float mAcc = parseFloatSafe(record.get("mAcc"));
float changed_precision = Float.parseFloat(record.get("changed_precision"));
float changed_recall = Float.parseFloat(record.get("changed_recall")); float changed_fscore = parseFloatSafe(record.get("changed_fscore"));
float unchanged_fscore = Float.parseFloat(record.get("unchanged_fscore")); float changed_precision = parseFloatSafe(record.get("changed_precision"));
float unchanged_precision = Float.parseFloat(record.get("unchanged_precision")); float changed_recall = parseFloatSafe(record.get("changed_recall"));
float unchanged_recall = Float.parseFloat(record.get("unchanged_recall"));
float unchanged_fscore = parseFloatSafe(record.get("unchanged_fscore"));
float unchanged_precision = parseFloatSafe(record.get("unchanged_precision"));
float unchanged_recall = parseFloatSafe(record.get("unchanged_recall"));
// int epoch = Integer.parseInt(record.get("Epoch"));
// float aAcc = Float.parseFloat(record.get("aAcc"));
// float mFscore = Float.parseFloat(record.get("mFscore"));
// float mPrecision = Float.parseFloat(record.get("mPrecision"));
// float mRecall = Float.parseFloat(record.get("mRecall"));
// float mIoU = Float.parseFloat(record.get("mIoU"));
// float mAcc = Float.parseFloat(record.get("mAcc"));
// float changed_fscore = Float.parseFloat(record.get("changed_fscore"));
// float changed_precision = Float.parseFloat(record.get("changed_precision"));
// float changed_recall = Float.parseFloat(record.get("changed_recall"));
// float unchanged_fscore = Float.parseFloat(record.get("unchanged_fscore"));
// float unchanged_precision =
// Float.parseFloat(record.get("unchanged_precision"));
// float unchanged_recall = Float.parseFloat(record.get("unchanged_recall"));
batchArgs.add( batchArgs.add(
new Object[] { new Object[] {
@@ -153,4 +170,23 @@ public class ModelTrainMetricsJobService {
modelInfo.getModelId(), "step1"); modelInfo.getModelId(), "step1");
} }
} }
private Float parseFloatSafe(String value) {
try {
if (value == null) return null;
value = value.trim();
if (value.isEmpty()) return null;
if (value.equalsIgnoreCase("nan")) return null;
float f = Float.parseFloat(value);
return Float.isNaN(f) ? null : f;
} catch (Exception e) {
return null;
}
}
} }

View File

@@ -38,7 +38,7 @@ public class TmpDatasetService {
Path tmp = Path.of(trainBaseDir, "tmp", uid); Path tmp = Path.of(trainBaseDir, "tmp", uid);
long hardlinksMade = 0; long linksMade = 0;
for (ModelTrainLinkDto dto : links) { for (ModelTrainLinkDto dto : links) {
@@ -54,23 +54,23 @@ public class TmpDatasetService {
Files.createDirectories(tmp.resolve(type).resolve("label-json")); Files.createDirectories(tmp.resolve(type).resolve("label-json"));
// comparePath → input1 // comparePath → input1
hardlinksMade += link(tmp, type, "input1", dto.getComparePath()); linksMade += link(tmp, type, "input1", dto.getComparePath());
// targetPath → input2 // targetPath → input2
hardlinksMade += link(tmp, type, "input2", dto.getTargetPath()); linksMade += link(tmp, type, "input2", dto.getTargetPath());
// labelPath → label // labelPath → label
hardlinksMade += link(tmp, type, "label", dto.getLabelPath()); linksMade += link(tmp, type, "label", dto.getLabelPath());
// geoJsonPath -> label-json // geoJsonPath -> label-json
hardlinksMade += link(tmp, type, "label-json", dto.getGeoJsonPath()); linksMade += link(tmp, type, "label-json", dto.getGeoJsonPath());
} }
if (hardlinksMade == 0) { if (linksMade == 0) {
throw new IOException("No hardlinks created."); throw new IOException("No symlinks created.");
} }
log.info("tmp dataset created: {}, hardlinksMade={}", tmp, hardlinksMade); log.info("tmp dataset created: {}, linksMade={}", tmp, linksMade);
} }
private long link(Path tmp, String type, String part, String fullPath) throws IOException { private long link(Path tmp, String type, String part, String fullPath) throws IOException {
@@ -88,34 +88,17 @@ public class TmpDatasetService {
Files.createDirectories(dst.getParent()); Files.createDirectories(dst.getParent());
if (Files.exists(dst)) { if (Files.exists(dst) || Files.isSymbolicLink(dst)) {
Files.delete(dst); Files.delete(dst);
} }
try {
// Files.createLink(dst, src);
Files.createSymbolicLink(dst, src); Files.createSymbolicLink(dst, src);
log.info("symbolic created: {} -> {}", dst, src); log.info("symlink created: {} -> {}", dst, src);
} catch (FileSystemException e) {
log.error(e.getMessage());
// if (e.getMessage() != null && e.getMessage().contains("Invalid cross-device link")) {
// log.warn(
// "Hardlink failed due to cross-device link. Fallback to symlink. src={}, dst={}",
// src,
// dst);
// Files.createSymbolicLink(dst, src);
// } else {
// throw e;
// }
}
return 1; return 1;
} }
private String safe(String s) { // TODO 미사용시작
return (s == null || s.isBlank()) ? null : s.trim();
}
/** /**
* request 전체 폴더 link * request 전체 폴더 link
* *
@@ -126,7 +109,7 @@ public class TmpDatasetService {
*/ */
public String buildTmpDatasetSymlink(String uid, List<String> datasetUids) throws IOException { public String buildTmpDatasetSymlink(String uid, List<String> datasetUids) throws IOException {
log.info("========== buildTmpDatasetHardlink START =========="); log.info("========== buildTmpDatasetSymlink START ==========");
log.info("uid={}", uid); log.info("uid={}", uid);
log.info("datasetUids={}", datasetUids); log.info("datasetUids={}", datasetUids);
log.info("requestDir(raw)={}", requestDir); log.info("requestDir(raw)={}", requestDir);
@@ -138,7 +121,7 @@ public class TmpDatasetService {
log.info("BASE exists? {}", Files.isDirectory(BASE)); log.info("BASE exists? {}", Files.isDirectory(BASE));
log.info("tmp={}", tmp); log.info("tmp={}", tmp);
long noDir = 0, scannedDirs = 0, regularFiles = 0, hardlinksMade = 0; long noDir = 0, scannedDirs = 0, regularFiles = 0, symlinksMade = 0;
// tmp 디렉토리 준비 // tmp 디렉토리 준비
for (String type : List.of("train", "val", "test")) { for (String type : List.of("train", "val", "test")) {
@@ -149,26 +132,7 @@ public class TmpDatasetService {
} }
} }
// 하드링크는 "같은 파일시스템"에서만 가능하므로 BASE/tmp가 같은 FS인지 미리 확인(권장) // 심볼릭 링크는 파일시스템이 달라도 작동하므로 FileStore 체크 불필요
try {
var baseStore = Files.getFileStore(BASE);
var tmpStore = Files.getFileStore(tmp.getParent()); // BASE/tmp
if (!baseStore.name().equals(tmpStore.name()) || !baseStore.type().equals(tmpStore.type())) {
throw new IOException(
"Hardlink requires same filesystem. baseStore="
+ baseStore.name()
+ "("
+ baseStore.type()
+ "), tmpStore="
+ tmpStore.name()
+ "("
+ tmpStore.type()
+ ")");
}
} catch (Exception e) {
// FileStore 비교가 환경마다 애매할 수 있어서, 여기서는 경고만 주고 실제 createLink에서 최종 판단하게 둘 수도 있음.
log.warn("FileStore check skipped/failed (will rely on createLink): {}", e.toString());
}
for (String id : datasetUids) { for (String id : datasetUids) {
Path srcRoot = BASE.resolve(id); Path srcRoot = BASE.resolve(id);
@@ -206,13 +170,12 @@ public class TmpDatasetService {
} }
try { try {
// 하드링크 생성 (dst가 새 파일로 생기지만 inode는 f와 동일) // 심볼릭 링크 생성 (파일시스템이 달라도 작동)
Files.createLink(dst, f); Files.createSymbolicLink(dst, f);
hardlinksMade++; symlinksMade++;
log.debug("created hardlink: {} => {}", dst, f); log.debug("created symlink: {} => {}", dst, f);
} catch (IOException e) { } catch (IOException e) {
// 여기서 바로 실패시키면 “tmp는 만들었는데 내용은 0개” 같은 상태를 방지할 수 있음 log.error("FAILED create symlink: {} => {}", dst, f, e);
log.error("FAILED create hardlink: {} => {}", dst, f, e);
throw e; throw e;
} }
} }
@@ -221,9 +184,9 @@ public class TmpDatasetService {
} }
} }
if (hardlinksMade == 0) { if (symlinksMade == 0) {
throw new IOException( throw new IOException(
"No hardlinks created. regularFiles=" "No symlinks created. regularFiles="
+ regularFiles + regularFiles
+ ", scannedDirs=" + ", scannedDirs="
+ scannedDirs + scannedDirs
@@ -233,11 +196,11 @@ public class TmpDatasetService {
log.info("tmp dataset created: {}", tmp); log.info("tmp dataset created: {}", tmp);
log.info( log.info(
"summary: scannedDirs={}, noDir={}, regularFiles={}, hardlinksMade={}", "summary: scannedDirs={}, noDir={}, regularFiles={}, symlinksMade={}",
scannedDirs, scannedDirs,
noDir, noDir,
regularFiles, regularFiles,
hardlinksMade); symlinksMade);
return uid; return uid;
} }
@@ -248,4 +211,5 @@ public class TmpDatasetService {
} }
return Paths.get(p).toAbsolutePath().normalize(); return Paths.get(p).toAbsolutePath().normalize();
} }
// TODO 미사용 끝
} }

View File

@@ -83,6 +83,7 @@ public class UploadDto {
private UUID uuid; private UUID uuid;
} }
// TODO 미사용시작
@Schema(name = "UploadCompleteReq", description = "업로드 완료 요청") @Schema(name = "UploadCompleteReq", description = "업로드 완료 요청")
@Getter @Getter
@Setter @Setter
@@ -126,12 +127,15 @@ public class UploadDto {
@Schema(description = "상태", example = "UPLOADING") @Schema(description = "상태", example = "UPLOADING")
private String status; private String status;
// TODO 미사용시작
@Schema(description = "총 청크 수", example = "100") @Schema(description = "총 청크 수", example = "100")
private Integer totalChunks; private Integer totalChunks;
@Schema(description = "업로드된 청크 수", example = "50") @Schema(description = "업로드된 청크 수", example = "50")
private Integer uploadedChunks; private Integer uploadedChunks;
// TODO 미사용 끝
@Schema(description = "진행률 (%)", example = "50.0") @Schema(description = "진행률 (%)", example = "50.0")
private Double progress; private Double progress;
@@ -139,6 +143,8 @@ public class UploadDto {
private String errorMessage; private String errorMessage;
} }
// TODO 미사용 끝
@Schema(name = "UploadAddReq", description = "업로드 요청") @Schema(name = "UploadAddReq", description = "업로드 요청")
@Getter @Getter
@Setter @Setter

View File

@@ -41,12 +41,15 @@ public class UploadService {
@Value("${file.dataset-tmp-dir}") @Value("${file.dataset-tmp-dir}")
private String datasetTmpDir; private String datasetTmpDir;
// TODO 미사용시작
@Transactional @Transactional
public DmlReturn initUpload(UploadDto.InitReq initReq) { public DmlReturn initUpload(UploadDto.InitReq initReq) {
return new DmlReturn("success", "UPLOAD CHUNK INIT"); return new DmlReturn("success", "UPLOAD CHUNK INIT");
} }
// TODO 미사용 끝
@Transactional @Transactional
public UploadDto.UploadRes uploadChunk(UploadDto.UploadAddReq upAddReqDto, MultipartFile file) { public UploadDto.UploadRes uploadChunk(UploadDto.UploadAddReq upAddReqDto, MultipartFile file) {

View File

@@ -47,7 +47,7 @@ member:
init_password: kamco1234! init_password: kamco1234!
swagger: swagger:
local-port: 9080 local-port: 8080
file: file:
sync-root-dir: /app/original-images/ sync-root-dir: /app/original-images/