20 Commits

Author SHA1 Message Date
10319ce0a6 커맨드 수정 2026-07-07 17:01:11 +09:00
fcc5d7a437 test 실행에 user 권한 추가 2026-07-07 15:39:03 +09:00
0ab0fdc43e 전이학습 등록 시 uuid 추가 2026-06-23 16:13:53 +09:00
ad750f0f06 학습 상세 API에 beforeModelUuid 추가 2026-06-23 15:25:03 +09:00
5fc93b64b6 전이학습 API 호출 파라미터 적용 2026-06-23 14:25:16 +09:00
cd14e6e3aa percent 수정 2026-06-04 13:43:10 +09:00
0447dd80ed 수정 2026-06-04 12:47:12 +09:00
5f4640ea60 학습 진행 현황 - 진행율, 진행중인 모델 API 추가 2026-06-04 12:38:23 +09:00
b85f920f40 학습 소요시간 로직 수정 2026-05-19 14:55:49 +09:00
e4851b1153 test 실행 도커 명령어 수정 2026-05-19 14:33:36 +09:00
b73aef5cf8 class 명칭 가져오는 부분 영어버전 추가 2026-05-15 14:59:39 +09:00
72c8f6a047 제작 업로드 시 dataset_path 안 맞는 현상 수정 2026-05-15 10:45:44 +09:00
c7a49ea4ea headerUtil 확인 2026-05-15 10:36:05 +09:00
fbef92af55 모델관리 API 영문버전 로직 수정 2026-05-12 14:51:18 +09:00
154db0ac27 영어버전도 나오게 추가 2026-05-12 14:27:09 +09:00
9835170cd7 납품데이터 등록 시 totalSize 추가 2026-05-08 10:40:06 +09:00
fd51f21ba6 납품 데이터 저장 시 uid 수정, 파일 업로드 경로 수정 2026-05-08 09:58:52 +09:00
776622e0a2 트랜잭션 5분 걸려있던 내용 주석처리 2026-05-07 15:29:24 +09:00
dabeeo
4d2d7a9ad1 check 2026-04-20 17:45:33 +09:00
dabeeo
532fbdbee4 hello 2026-04-20 17:35:26 +09:00
24 changed files with 390 additions and 53 deletions

View File

@@ -1,7 +1,10 @@
services: services:
kamco-changedetection-api: kamco-changedetection-api:
image: kamco-train-app:latest build:
container_name: kamco-train-api context: .
dockerfile: Dockerfile-dev
image: kamco-cd-training-api:${IMAGE_TAG:-latest}
container_name: kamco-cd-training-api
deploy: deploy:
resources: resources:
reservations: reservations:
@@ -9,13 +12,13 @@ services:
- driver: nvidia - driver: nvidia
count: all count: all
capabilities: [gpu] capabilities: [gpu]
ports:
- "7200:8080"
environment: environment:
- SPRING_PROFILES_ACTIVE=dev - SPRING_PROFILES_ACTIVE=dev
- TZ=Asia/Seoul - TZ=Asia/Seoul
- cors.allowed-origins=*
- cors.allowed-origins[0]=*
volumes: volumes:
- /data/training:/data/training - /backup/data/training:/backup/data/training
- /var/run/docker.sock:/var/run/docker.sock - /var/run/docker.sock:/var/run/docker.sock
networks: networks:
- kamco-cds - kamco-cds

View File

@@ -37,7 +37,8 @@ public class GpuDmonReader {
int avg = int avg =
deque.isEmpty() deque.isEmpty()
? 0 ? 0
: (int) Math.round(deque.stream().mapToInt(Integer::intValue).average().orElse(0)); : (int)
Math.round(deque.stream().mapToInt(Integer::intValue).average().orElse(0));
result.put(gpu, avg); result.put(gpu, avg);
} }
}); });
@@ -98,8 +99,7 @@ public class GpuDmonReader {
Process process = pb.start(); Process process = pb.start();
// 프로세스 실행 후 stdout 읽기 // 프로세스 실행 후 stdout 읽기
try (BufferedReader br = try (BufferedReader br = new BufferedReader(new InputStreamReader(process.getInputStream()))) {
new BufferedReader(new InputStreamReader(process.getInputStream()))) {
String line; String line;

View File

@@ -1,7 +1,12 @@
package com.kamco.cd.training.common.utils; package com.kamco.cd.training.common.utils;
import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletRequest;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.context.request.RequestAttributes;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
@Slf4j
public final class HeaderUtil { public final class HeaderUtil {
private HeaderUtil() {} private HeaderUtil() {}
@@ -20,4 +25,20 @@ public final class HeaderUtil {
public static String getRequired(HttpServletRequest request, String headerName) { public static String getRequired(HttpServletRequest request, String headerName) {
return get(request, headerName); return get(request, headerName);
} }
public static boolean isEnglishRequest() {
RequestAttributes attrs = RequestContextHolder.getRequestAttributes();
if (!(attrs instanceof ServletRequestAttributes servletAttrs)) {
return false;
}
String acceptLanguage = servletAttrs.getRequest().getHeader("Accept-Language");
if (acceptLanguage == null || acceptLanguage.isBlank()) {
return false;
}
return acceptLanguage.toLowerCase().startsWith("en");
}
} }

View File

@@ -1,5 +1,6 @@
package com.kamco.cd.training.common.utils.enums; package com.kamco.cd.training.common.utils.enums;
import com.kamco.cd.training.common.utils.HeaderUtil;
import java.util.Arrays; import java.util.Arrays;
import java.util.HashMap; import java.util.HashMap;
import java.util.List; import java.util.List;
@@ -31,10 +32,11 @@ public class Enums {
// enum -> CodeDto list // enum -> CodeDto list
public static List<CodeDto> toList(Class<? extends Enum<?>> enumClass) { public static List<CodeDto> toList(Class<? extends Enum<?>> enumClass) {
Object[] enums = enumClass.getEnumConstants(); Object[] enums = enumClass.getEnumConstants();
boolean english = HeaderUtil.isEnglishRequest();
return Arrays.stream(enums) return Arrays.stream(enums)
.map(e -> (EnumType) e) .map(e -> (EnumType) e)
.map(e -> new CodeDto(e.getId(), e.getText())) .map(e -> new CodeDto(e.getId(), english ? e.getId() : e.getText()))
.toList(); .toList();
} }

View File

@@ -293,6 +293,8 @@ public class DatasetApiController {
DatasetService.validateTrainValTestDirs(req.getFilePath()); DatasetService.validateTrainValTestDirs(req.getFilePath());
// 파일 개수 검증 // 파일 개수 검증
DatasetService.validateDirFileCount(req.getFilePath()); DatasetService.validateDirFileCount(req.getFilePath());
// 폴더명(uid)으로 등록한 건이 있는지 체크
datasetService.validateExistsUidChk(req.getFilePath());
datasetAsyncService.insertDeliveriesDatasetAsync(req); datasetAsyncService.insertDeliveriesDatasetAsync(req);
return ApiResponseDto.createOK("ok"); return ApiResponseDto.createOK("ok");

View File

@@ -4,6 +4,7 @@ import com.fasterxml.jackson.annotation.JsonIgnore;
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.enums.ModelType; import com.kamco.cd.training.common.enums.ModelType;
import com.kamco.cd.training.common.utils.HeaderUtil;
import com.kamco.cd.training.common.utils.enums.Enums; import com.kamco.cd.training.common.utils.enums.Enums;
import com.kamco.cd.training.common.utils.interfaces.JsonFormatDttm; import com.kamco.cd.training.common.utils.interfaces.JsonFormatDttm;
import io.swagger.v3.oas.annotations.media.Schema; import io.swagger.v3.oas.annotations.media.Schema;
@@ -90,7 +91,8 @@ public class DatasetDto {
public String getStatus(String status) { public String getStatus(String status) {
LearnDataRegister type = Enums.fromId(LearnDataRegister.class, status); LearnDataRegister type = Enums.fromId(LearnDataRegister.class, status);
return type == null ? null : type.getText(); boolean english = HeaderUtil.isEnglishRequest();
return type == null ? null : (english ? type.getId() : type.getText());
} }
public String getYear() { public String getYear() {
@@ -99,7 +101,8 @@ public class DatasetDto {
public String getDataTypeName() { public String getDataTypeName() {
LearnDataType type = Enums.fromId(LearnDataType.class, this.dataType); LearnDataType type = Enums.fromId(LearnDataType.class, this.dataType);
return type == null ? null : type.getText(); boolean english = HeaderUtil.isEnglishRequest();
return type == null ? null : (english ? type.getId() : type.getText());
} }
} }
@@ -315,7 +318,7 @@ public class DatasetDto {
public String getDataTypeName(String groupTitleCd) { public String getDataTypeName(String groupTitleCd) {
LearnDataType type = Enums.fromId(LearnDataType.class, groupTitleCd); LearnDataType type = Enums.fromId(LearnDataType.class, groupTitleCd);
return type == null ? null : type.getText(); return type == null ? null : (HeaderUtil.isEnglishRequest() ? type.getId() : type.getText());
} }
public String getYear() { public String getYear() {

View File

@@ -5,6 +5,7 @@ import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.ObjectMapper;
import com.kamco.cd.training.common.enums.DetectionClassification; import com.kamco.cd.training.common.enums.DetectionClassification;
import com.kamco.cd.training.common.utils.HeaderUtil;
import com.kamco.cd.training.common.utils.interfaces.JsonFormatDttm; import com.kamco.cd.training.common.utils.interfaces.JsonFormatDttm;
import io.swagger.v3.oas.annotations.media.Schema; import io.swagger.v3.oas.annotations.media.Schema;
import java.time.ZonedDateTime; import java.time.ZonedDateTime;
@@ -131,7 +132,10 @@ public class DatasetObjDto {
private String classCd; private String classCd;
public String getClassName() { public String getClassName() {
return DetectionClassification.fromString(classCd).getDesc();
return HeaderUtil.isEnglishRequest()
? DetectionClassification.fromString(classCd).getId()
: DetectionClassification.fromString(classCd).getDesc();
// fromString 메서드를 사용하여 안전하게 변환 (미정의 값은 ETC로 처리) // fromString 메서드를 사용하여 안전하게 변환 (미정의 값은 ETC로 처리)
// return DetectionClassification.valueOf(classCd.toUpperCase()).getDesc(); // return DetectionClassification.valueOf(classCd.toUpperCase()).getDesc();
} }

View File

@@ -4,7 +4,11 @@ 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.AddDeliveriesReq;
import com.kamco.cd.training.dataset.dto.DatasetDto.DatasetMngRegDto; import com.kamco.cd.training.dataset.dto.DatasetDto.DatasetMngRegDto;
import com.kamco.cd.training.postgres.core.DatasetCoreService; import com.kamco.cd.training.postgres.core.DatasetCoreService;
import java.util.UUID; import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.stream.Stream;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import lombok.extern.log4j.Log4j2; import lombok.extern.log4j.Log4j2;
import org.springframework.scheduling.annotation.Async; import org.springframework.scheduling.annotation.Async;
@@ -45,7 +49,11 @@ public class DatasetAsyncService {
try { try {
// ===== 1. UID 생성 ===== // ===== 1. UID 생성 =====
String uid = UUID.randomUUID().toString().replace("-", "").toUpperCase(); // String uid = UUID.randomUUID().toString().replace("-", "").toUpperCase();
// 26-05-08: UID를 생성하지 않고 folder name 을 uid 로 저장하기 -> req.getFilePath()
Path selectedPath = Paths.get(req.getFilePath());
String uid = selectedPath.getFileName().toString();
log.info("{} 생성된 UID: {}", LOG_PREFIX, uid); log.info("{} 생성된 UID: {}", LOG_PREFIX, uid);
// ===== 2. 마스터 데이터 생성 ===== // ===== 2. 마스터 데이터 생성 =====
@@ -71,6 +79,7 @@ public class DatasetAsyncService {
datasetMngRegDto.setTitle(title); datasetMngRegDto.setTitle(title);
datasetMngRegDto.setMemo(req.getMemo()); datasetMngRegDto.setMemo(req.getMemo());
datasetMngRegDto.setDatasetPath(req.getFilePath()); datasetMngRegDto.setDatasetPath(req.getFilePath());
datasetMngRegDto.setTotalSize(getDirectorySize(req.getFilePath())); // 선택한 폴더 안의 모든 파일 용량 합계
// 마스터 저장 // 마스터 저장
datasetUid = datasetCoreService.insertDatasetMngData(datasetMngRegDto); datasetUid = datasetCoreService.insertDatasetMngData(datasetMngRegDto);
@@ -121,4 +130,24 @@ public class DatasetAsyncService {
} }
} }
} }
private Long getDirectorySize(String filePath) {
Path selectedPath = Paths.get(filePath);
try (Stream<Path> paths = Files.walk(selectedPath)) {
return paths
.filter(Files::isRegularFile)
.mapToLong(
path -> {
try {
return Files.size(path);
} catch (IOException e) {
return 0L;
}
})
.sum();
} catch (IOException e) {
return 0L;
}
}
} }

View File

@@ -286,7 +286,7 @@ public class DatasetService {
.memo(addReq.getMemo()) .memo(addReq.getMemo())
.roundNo(stage) .roundNo(stage)
.totalSize(addReq.getFileSize()) .totalSize(addReq.getFileSize())
.datasetPath(addReq.getFilePath()) .datasetPath(addReq.getFilePath() + uid)
.build(); .build();
datasetUid = datasetCoreService.insertDatasetMngData(mngRegDto); // tb_dataset 에 insert datasetUid = datasetCoreService.insertDatasetMngData(mngRegDto); // tb_dataset 에 insert
@@ -676,4 +676,18 @@ public class DatasetService {
total, total,
System.currentTimeMillis() - start); System.currentTimeMillis() - start);
} }
public void validateExistsUidChk(String filePath) {
Path selectedPath = Paths.get(filePath);
String uid = selectedPath.getFileName().toString();
// 같은 uid 로 등록한 파일이 있는지 확인
Long existsCnt = datasetCoreService.findDatasetByUidExistsCnt(uid);
if (existsCnt > 0) {
throw new CustomApiException(
ApiResponseCode.DUPLICATE_DATA.getId(),
HttpStatus.CONFLICT,
"이미 등록된 회차 데이터 파일입니다. 확인 부탁드립니다.");
}
}
} }

View File

@@ -277,9 +277,7 @@ public class FileManagerDto {
@Schema(description = "디스크 사용률 (%)", example = "50.5") @Schema(description = "디스크 사용률 (%)", example = "50.5")
private Double usagePercentage; private Double usagePercentage;
@JsonFormatDttm private ZonedDateTime lastModifiedDate;
@JsonFormatDttm
private ZonedDateTime lastModifiedDate;
public ZonedDateTime getLastModifiedDate() { public ZonedDateTime getLastModifiedDate() {
return ZonedDateTime.now(); return ZonedDateTime.now();

View File

@@ -627,8 +627,7 @@ public class FileManagerService {
duPb.redirectErrorStream(true); duPb.redirectErrorStream(true);
Process duProcess = duPb.start(); Process duProcess = duPb.start();
try (java.io.BufferedReader br = try (java.io.BufferedReader br =
new java.io.BufferedReader( new java.io.BufferedReader(new java.io.InputStreamReader(duProcess.getInputStream()))) {
new java.io.InputStreamReader(duProcess.getInputStream()))) {
String line = br.readLine(); String line = br.readLine();
if (line != null) { if (line != null) {
// du -sb 출력 형식: "12345678\t/path/to/dir" // du -sb 출력 형식: "12345678\t/path/to/dir"

View File

@@ -236,4 +236,41 @@ public class ModelTrainMngApiController {
public ApiResponseDto<MonitorDto> getSystem() throws IOException { public ApiResponseDto<MonitorDto> getSystem() throws IOException {
return ApiResponseDto.ok(systemMonitorService.get()); return ApiResponseDto.ok(systemMonitorService.get());
} }
@Operation(summary = "모델학습 1단계/2단계 실행중인 것 id 정보", description = "모델학습 1단계/2단계 실행중인 것 id 정보")
@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("/ing-model")
public ApiResponseDto<ModelTrainMngDto.InProgressModel> findInprogressModel() {
return ApiResponseDto.ok(modelTrainMngService.findInprogressModel());
}
@Operation(summary = "모델학습 진행율 퍼센트", description = "모델학습 진행율 퍼센트")
@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("/progress-percent/{uuid}")
public ApiResponseDto<ModelTrainMngDto.ProgressPercent> findTrainProgressPercent(
@PathVariable UUID uuid) {
return ApiResponseDto.ok(modelTrainMngService.findTrainProgressPercent(uuid));
}
} }

View File

@@ -4,6 +4,7 @@ import com.fasterxml.jackson.annotation.JsonInclude;
import com.kamco.cd.training.common.enums.LearnDataType; import com.kamco.cd.training.common.enums.LearnDataType;
import com.kamco.cd.training.common.enums.TrainStatusType; 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.utils.HeaderUtil;
import com.kamco.cd.training.common.utils.enums.Enums; import com.kamco.cd.training.common.utils.enums.Enums;
import com.kamco.cd.training.common.utils.interfaces.JsonFormatDttm; import com.kamco.cd.training.common.utils.interfaces.JsonFormatDttm;
import com.kamco.cd.training.dataset.dto.DatasetDto.SelectTransferDataSet; import com.kamco.cd.training.dataset.dto.DatasetDto.SelectTransferDataSet;
@@ -32,6 +33,8 @@ public class ModelTrainDetailDto {
private String modelNo; private String modelNo;
private String modelVer; private String modelVer;
@JsonFormatDttm private ZonedDateTime step1StrtDttm; @JsonFormatDttm private ZonedDateTime step1StrtDttm;
@JsonFormatDttm private ZonedDateTime step1EndDttm;
@JsonFormatDttm private ZonedDateTime step2StrtDttm;
@JsonFormatDttm private ZonedDateTime step2EndDttm; @JsonFormatDttm private ZonedDateTime step2EndDttm;
private String statusCd; private String statusCd;
private String trainType; private String trainType;
@@ -40,7 +43,9 @@ public class ModelTrainDetailDto {
public String getStatusName() { public String getStatusName() {
if (this.statusCd == null || this.statusCd.isBlank()) return null; if (this.statusCd == null || this.statusCd.isBlank()) return null;
try { try {
return TrainStatusType.valueOf(this.statusCd).getText(); // 또는 getName() return HeaderUtil.isEnglishRequest()
? TrainStatusType.valueOf(this.statusCd).getId()
: TrainStatusType.valueOf(this.statusCd).getText(); // 또는 getName()
} catch (IllegalArgumentException e) { } catch (IllegalArgumentException e) {
return this.statusCd; // 매핑 못하면 코드 그대로 반환(원하면 null 처리) return this.statusCd; // 매핑 못하면 코드 그대로 반환(원하면 null 처리)
} }
@@ -49,18 +54,16 @@ public class ModelTrainDetailDto {
public String getTrainTypeName() { public String getTrainTypeName() {
if (this.trainType == null || this.trainType.isBlank()) return null; if (this.trainType == null || this.trainType.isBlank()) return null;
try { try {
return TrainType.valueOf(this.trainType).getText(); // 또는 getName() return HeaderUtil.isEnglishRequest()
? TrainType.valueOf(this.trainType).getId()
: TrainType.valueOf(this.trainType).getText(); // 또는 getName()
} catch (IllegalArgumentException e) { } catch (IllegalArgumentException e) {
return this.trainType; // 매핑 못하면 코드 그대로 반환(원하면 null 처리) return this.trainType; // 매핑 못하면 코드 그대로 반환(원하면 null 처리)
} }
} }
private String formatDuration(ZonedDateTime start, ZonedDateTime end) { private String formatDuration(ZonedDateTime start, ZonedDateTime end) {
if (end == null) { if (start == null || end == null) {
end = ZonedDateTime.now();
}
if (start == null) {
return null; return null;
} }
@@ -70,11 +73,42 @@ public class ModelTrainDetailDto {
long minutes = (totalSeconds % 3600) / 60; long minutes = (totalSeconds % 3600) / 60;
long seconds = totalSeconds % 60; long seconds = totalSeconds % 60;
return String.format("%d시간 %d분 %d초", hours, minutes, seconds); if (HeaderUtil.isEnglishRequest()) {
return String.format("%dh %dm %ds", hours, minutes, seconds);
} else {
return String.format("%d시간 %d분 %d초", hours, minutes, seconds);
}
} }
public String getStepAllDuration() { public String getStepAllDuration() {
return formatDuration(this.step1StrtDttm, this.step2EndDttm); if (this.step2EndDttm != null) {
// step1 + step2 실제 소요시간 합산
long step1Seconds = 0;
long step2Seconds = 0;
if (this.step1StrtDttm != null && this.step1EndDttm != null) {
step1Seconds =
Math.abs(Duration.between(this.step1StrtDttm, this.step1EndDttm).getSeconds());
}
if (this.step2StrtDttm != null && this.step2EndDttm != null) {
step2Seconds =
Math.abs(Duration.between(this.step2StrtDttm, this.step2EndDttm).getSeconds());
}
long totalSeconds = step1Seconds + step2Seconds;
long hours = totalSeconds / 3600;
long minutes = (totalSeconds % 3600) / 60;
long seconds = totalSeconds % 60;
if (HeaderUtil.isEnglishRequest()) {
return String.format("%dh %dm %ds", hours, minutes, seconds);
} else {
return String.format("%d시간 %d분 %d초", hours, minutes, seconds);
}
} else {
// step2 없으면 step1만
return formatDuration(this.step1StrtDttm, this.step1EndDttm);
}
} }
} }
@@ -174,7 +208,7 @@ public class ModelTrainDetailDto {
public String getDataTypeName(String groupTitleCd) { public String getDataTypeName(String groupTitleCd) {
LearnDataType type = Enums.fromId(LearnDataType.class, groupTitleCd); LearnDataType type = Enums.fromId(LearnDataType.class, groupTitleCd);
return type == null ? null : type.getText(); return type == null ? null : (HeaderUtil.isEnglishRequest() ? type.getId() : type.getText());
} }
} }

View File

@@ -3,6 +3,7 @@ package com.kamco.cd.training.model.dto;
import com.kamco.cd.training.common.dto.HyperParam; import com.kamco.cd.training.common.dto.HyperParam;
import com.kamco.cd.training.common.enums.TrainStatusType; 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.utils.HeaderUtil;
import com.kamco.cd.training.common.utils.interfaces.JsonFormatDttm; import com.kamco.cd.training.common.utils.interfaces.JsonFormatDttm;
import io.swagger.v3.oas.annotations.media.Schema; import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotNull; import jakarta.validation.constraints.NotNull;
@@ -44,16 +45,20 @@ public class ModelTrainMngDto {
private String requestPath; private String requestPath;
private String packingState; private String packingState;
private ZonedDateTime packingStrtDttm; @JsonFormatDttm private ZonedDateTime packingStrtDttm;
private ZonedDateTime packingEndDttm; @JsonFormatDttm private ZonedDateTime packingEndDttm;
private Long beforeModelId; private Long beforeModelId;
private Integer bestEpoch; private Integer bestEpoch;
private UUID beforeModelUuid;
public String getStatusName() { public String getStatusName() {
if (this.statusCd == null || this.statusCd.isBlank()) return null; if (this.statusCd == null || this.statusCd.isBlank()) return null;
try { try {
return TrainStatusType.valueOf(this.statusCd).getText(); // 또는 getName() return (HeaderUtil.isEnglishRequest()
? TrainStatusType.valueOf(this.statusCd).getId()
: TrainStatusType.valueOf(this.statusCd).getText()); // 또는 getName()
} catch (IllegalArgumentException e) { } catch (IllegalArgumentException e) {
return this.statusCd; // 매핑 못하면 코드 그대로 반환(원하면 null 처리) return this.statusCd; // 매핑 못하면 코드 그대로 반환(원하면 null 처리)
} }
@@ -62,7 +67,9 @@ public class ModelTrainMngDto {
public String getStep1StatusName() { public String getStep1StatusName() {
if (this.step1Status == null || this.step1Status.isBlank()) return null; if (this.step1Status == null || this.step1Status.isBlank()) return null;
try { try {
return TrainStatusType.valueOf(this.step1Status).getText(); // 또는 getName() return (HeaderUtil.isEnglishRequest()
? TrainStatusType.valueOf(this.step1Status).getId()
: TrainStatusType.valueOf(this.step1Status).getText()); // 또는 getName()
} catch (IllegalArgumentException e) { } catch (IllegalArgumentException e) {
return this.step1Status; // 매핑 못하면 코드 그대로 반환(원하면 null 처리) return this.step1Status; // 매핑 못하면 코드 그대로 반환(원하면 null 처리)
} }
@@ -71,7 +78,9 @@ public class ModelTrainMngDto {
public String getStep2StatusName() { public String getStep2StatusName() {
if (this.step2Status == null || this.step2Status.isBlank()) return null; if (this.step2Status == null || this.step2Status.isBlank()) return null;
try { try {
return TrainStatusType.valueOf(this.step2Status).getText(); // 또는 getName() return (HeaderUtil.isEnglishRequest()
? TrainStatusType.valueOf(this.step2Status).getId()
: TrainStatusType.valueOf(this.step2Status).getText()); // 또는 getName()
} catch (IllegalArgumentException e) { } catch (IllegalArgumentException e) {
return this.step2Status; // 매핑 못하면 코드 그대로 반환(원하면 null 처리) return this.step2Status; // 매핑 못하면 코드 그대로 반환(원하면 null 처리)
} }
@@ -80,7 +89,9 @@ public class ModelTrainMngDto {
public String getTrainTypeName() { public String getTrainTypeName() {
if (this.trainType == null || this.trainType.isBlank()) return null; if (this.trainType == null || this.trainType.isBlank()) return null;
try { try {
return TrainType.valueOf(this.trainType).getText(); // 또는 getName() return (HeaderUtil.isEnglishRequest()
? TrainType.valueOf(this.trainType).getId()
: TrainType.valueOf(this.trainType).getText()); // 또는 getName()
} catch (IllegalArgumentException e) { } catch (IllegalArgumentException e) {
return this.trainType; // 매핑 못하면 코드 그대로 반환(원하면 null 처리) return this.trainType; // 매핑 못하면 코드 그대로 반환(원하면 null 처리)
} }
@@ -97,7 +108,11 @@ public class ModelTrainMngDto {
long minutes = (totalSeconds % 3600) / 60; long minutes = (totalSeconds % 3600) / 60;
long seconds = totalSeconds % 60; long seconds = totalSeconds % 60;
return String.format("%d시간 %d분 %d초", hours, minutes, seconds); if (HeaderUtil.isEnglishRequest()) {
return String.format("%dh %dm %ds", hours, minutes, seconds);
} else {
return String.format("%d시간 %d분 %d초", hours, minutes, seconds);
}
} }
public String getStep1Duration() { public String getStep1Duration() {
@@ -260,7 +275,9 @@ public class ModelTrainMngDto {
public String getStatusName() { public String getStatusName() {
if (this.statusCd == null || this.statusCd.isBlank()) return null; if (this.statusCd == null || this.statusCd.isBlank()) return null;
try { try {
return TrainStatusType.valueOf(this.statusCd).getText(); // 또는 getName() return HeaderUtil.isEnglishRequest()
? TrainStatusType.valueOf(this.statusCd).getId()
: TrainStatusType.valueOf(this.statusCd).getText(); // 또는 getName()
} catch (IllegalArgumentException e) { } catch (IllegalArgumentException e) {
return this.statusCd; // 매핑 못하면 코드 그대로 반환(원하면 null 처리) return this.statusCd; // 매핑 못하면 코드 그대로 반환(원하면 null 처리)
} }
@@ -269,7 +286,9 @@ public class ModelTrainMngDto {
public String getStep1StatusName() { public String getStep1StatusName() {
if (this.step1Status == null || this.step1Status.isBlank()) return null; if (this.step1Status == null || this.step1Status.isBlank()) return null;
try { try {
return TrainStatusType.valueOf(this.step1Status).getText(); // 또는 getName() return HeaderUtil.isEnglishRequest()
? TrainStatusType.valueOf(this.step1Status).getId()
: TrainStatusType.valueOf(this.step1Status).getText(); // 또는 getName()
} catch (IllegalArgumentException e) { } catch (IllegalArgumentException e) {
return this.step1Status; // 매핑 못하면 코드 그대로 반환(원하면 null 처리) return this.step1Status; // 매핑 못하면 코드 그대로 반환(원하면 null 처리)
} }
@@ -278,7 +297,9 @@ public class ModelTrainMngDto {
public String getStep2StatusName() { public String getStep2StatusName() {
if (this.step2Status == null || this.step2Status.isBlank()) return null; if (this.step2Status == null || this.step2Status.isBlank()) return null;
try { try {
return TrainStatusType.valueOf(this.step2Status).getText(); // 또는 getName() return HeaderUtil.isEnglishRequest()
? TrainStatusType.valueOf(this.step2Status).getId()
: TrainStatusType.valueOf(this.step2Status).getText(); // 또는 getName()
} catch (IllegalArgumentException e) { } catch (IllegalArgumentException e) {
return this.step2Status; // 매핑 못하면 코드 그대로 반환(원하면 null 처리) return this.step2Status; // 매핑 못하면 코드 그대로 반환(원하면 null 처리)
} }
@@ -287,7 +308,9 @@ public class ModelTrainMngDto {
public String getTrainTypeName() { public String getTrainTypeName() {
if (this.trainType == null || this.trainType.isBlank()) return null; if (this.trainType == null || this.trainType.isBlank()) return null;
try { try {
return TrainType.valueOf(this.trainType).getText(); // 또는 getName() return HeaderUtil.isEnglishRequest()
? TrainType.valueOf(this.trainType).getId()
: TrainType.valueOf(this.trainType).getText(); // 또는 getName()
} catch (IllegalArgumentException e) { } catch (IllegalArgumentException e) {
return this.trainType; // 매핑 못하면 코드 그대로 반환(원하면 null 처리) return this.trainType; // 매핑 못하면 코드 그대로 반환(원하면 null 처리)
} }
@@ -304,7 +327,11 @@ public class ModelTrainMngDto {
long minutes = (totalSeconds % 3600) / 60; long minutes = (totalSeconds % 3600) / 60;
long seconds = totalSeconds % 60; long seconds = totalSeconds % 60;
return String.format("%d시간 %d분 %d초", hours, minutes, seconds); if (HeaderUtil.isEnglishRequest()) {
return String.format("%dh %dm %ds", hours, minutes, seconds);
} else {
return String.format("%d시간 %d분 %d초", hours, minutes, seconds);
}
} }
public String getStep1Duration() { public String getStep1Duration() {
@@ -353,4 +380,26 @@ public class ModelTrainMngDto {
// 삭제 될 파일 // 삭제 될 파일
private List<String> deleteTargets; private List<String> deleteTargets;
} }
@Getter
@Builder
@AllArgsConstructor
public static class InProgressModel {
private String modelNo;
private UUID uuid;
}
@Getter
@Builder
@AllArgsConstructor
public static class ProgressPercent {
private Long modelId;
private String jobType;
private String statusCd;
private Integer totalEpoch;
private Integer currentEpoch;
private Double percent;
}
} }

View File

@@ -350,4 +350,12 @@ public class ModelTrainMngService {
public Long findModelStep1InProgressCnt() { public Long findModelStep1InProgressCnt() {
return modelTrainMngCoreService.findModelStep1InProgressCnt(); return modelTrainMngCoreService.findModelStep1InProgressCnt();
} }
public ModelTrainMngDto.InProgressModel findInprogressModel() {
return modelTrainMngCoreService.findInprogressModel();
}
public ModelTrainMngDto.ProgressPercent findTrainProgressPercent(UUID uuid) {
return modelTrainMngCoreService.findTrainProgressPercent(uuid);
}
} }

View File

@@ -120,6 +120,15 @@ public class ModelTrainMngCoreService {
entity.setTrainType(addReq.getTrainType()); // 일반, 전이 entity.setTrainType(addReq.getTrainType()); // 일반, 전이
entity.setBeforeModelId(addReq.getBeforeModelId()); entity.setBeforeModelId(addReq.getBeforeModelId());
// 전이학습일 때 beforeModelUuid 추가하기
if (addReq.getBeforeModelId() != null) {
ModelMasterEntity beforeModel =
modelMngRepository
.findById(addReq.getBeforeModelId())
.orElseThrow(() -> new CustomApiException("NOT_FOUND_DATA", HttpStatus.NOT_FOUND));
entity.setBeforeModelUuid(beforeModel.getUuid());
}
entity.setStatusCd(TrainStatusType.READY.getId()); entity.setStatusCd(TrainStatusType.READY.getId());
entity.setStep1State(TrainStatusType.READY.getId()); entity.setStep1State(TrainStatusType.READY.getId());
@@ -713,4 +722,17 @@ public class ModelTrainMngCoreService {
entity.setTmpFileStatus("FAIL"); entity.setTmpFileStatus("FAIL");
entity.setTmpFileErrMessage(message); entity.setTmpFileErrMessage(message);
} }
public ModelTrainMngDto.InProgressModel findInprogressModel() {
return modelMngRepository.findInprogressModel();
}
public ModelTrainMngDto.ProgressPercent findTrainProgressPercent(UUID uuid) {
ModelMasterEntity entity =
modelMngRepository
.findByUuid(uuid)
.orElseThrow(() -> new CustomApiException("NOT_FOUND_DATA", HttpStatus.NOT_FOUND));
return modelMngRepository.findTrainProgressPercent(entity.getId());
}
} }

View File

@@ -136,6 +136,9 @@ public class ModelMasterEntity {
@Column(name = "tmp_file_err_message", columnDefinition = "TEXT") @Column(name = "tmp_file_err_message", columnDefinition = "TEXT")
private String tmpFileErrMessage; private String tmpFileErrMessage;
@Column(name = "before_model_uuid")
private UUID beforeModelUuid;
public ModelTrainMngDto.Basic toDto() { public ModelTrainMngDto.Basic toDto() {
return new ModelTrainMngDto.Basic( return new ModelTrainMngDto.Basic(
this.id, this.id,
@@ -157,6 +160,7 @@ public class ModelMasterEntity {
this.packingStrtDttm, this.packingStrtDttm,
this.packingEndDttm, this.packingEndDttm,
this.beforeModelId, this.beforeModelId,
this.bestEpoch); this.bestEpoch,
this.beforeModelUuid);
} }
} }

View File

@@ -80,6 +80,8 @@ public class ModelDetailRepositoryImpl implements ModelDetailRepositoryCustom {
modelMasterEntity.modelNo, modelMasterEntity.modelNo,
modelMasterEntity.modelVer, modelMasterEntity.modelVer,
modelMasterEntity.step1StrtDttm, modelMasterEntity.step1StrtDttm,
modelMasterEntity.step1EndDttm,
modelMasterEntity.step2StrtDttm,
modelMasterEntity.step2EndDttm, modelMasterEntity.step2EndDttm,
modelMasterEntity.statusCd, modelMasterEntity.statusCd,
modelMasterEntity.trainType, modelMasterEntity.trainType,

View File

@@ -47,4 +47,13 @@ public interface ModelMngRepositoryCustom {
* @return 모델 목록 * @return 모델 목록
*/ */
List<ModelMasterEntity> findByHyperParamId(Long hyperParamId); List<ModelMasterEntity> findByHyperParamId(Long hyperParamId);
/**
* 학습 진행중인 모델 type, uuid 조회
*
* @return
*/
ModelTrainMngDto.InProgressModel findInprogressModel();
ModelTrainMngDto.ProgressPercent findTrainProgressPercent(Long id);
} }

View File

@@ -4,6 +4,7 @@ import static com.kamco.cd.training.postgres.entity.QMemberEntity.memberEntity;
import static com.kamco.cd.training.postgres.entity.QModelConfigEntity.modelConfigEntity; import static com.kamco.cd.training.postgres.entity.QModelConfigEntity.modelConfigEntity;
import static com.kamco.cd.training.postgres.entity.QModelHyperParamEntity.modelHyperParamEntity; import static com.kamco.cd.training.postgres.entity.QModelHyperParamEntity.modelHyperParamEntity;
import static com.kamco.cd.training.postgres.entity.QModelMasterEntity.modelMasterEntity; import static com.kamco.cd.training.postgres.entity.QModelMasterEntity.modelMasterEntity;
import static com.kamco.cd.training.postgres.entity.QModelTrainJobEntity.modelTrainJobEntity;
import com.kamco.cd.training.common.enums.TrainStatusType; import com.kamco.cd.training.common.enums.TrainStatusType;
import com.kamco.cd.training.model.dto.ModelTrainMngDto; import com.kamco.cd.training.model.dto.ModelTrainMngDto;
@@ -14,7 +15,9 @@ import com.kamco.cd.training.train.dto.TrainRunRequest;
import com.querydsl.core.BooleanBuilder; import com.querydsl.core.BooleanBuilder;
import com.querydsl.core.types.Expression; import com.querydsl.core.types.Expression;
import com.querydsl.core.types.Projections; import com.querydsl.core.types.Projections;
import com.querydsl.core.types.dsl.CaseBuilder;
import com.querydsl.core.types.dsl.Expressions; import com.querydsl.core.types.dsl.Expressions;
import com.querydsl.core.types.dsl.NumberExpression;
import com.querydsl.jpa.impl.JPAQueryFactory; import com.querydsl.jpa.impl.JPAQueryFactory;
import java.util.List; import java.util.List;
import java.util.Optional; import java.util.Optional;
@@ -143,6 +146,8 @@ public class ModelMngRepositoryImpl implements ModelMngRepositoryCustom {
@Override @Override
public TrainRunRequest findTrainRunRequest(Long modelId) { public TrainRunRequest findTrainRunRequest(Long modelId) {
QModelMasterEntity beforeModel = new QModelMasterEntity("beforeModel");
return queryFactory return queryFactory
.select( .select(
Projections.constructor( Projections.constructor(
@@ -190,12 +195,17 @@ public class ModelMngRepositoryImpl implements ModelMngRepositoryCustom {
Expressions.nullExpression(Integer.class), Expressions.nullExpression(Integer.class),
Expressions.nullExpression(String.class), Expressions.nullExpression(String.class),
modelHyperParamEntity.uuid, modelHyperParamEntity.uuid,
modelMasterEntity.reqTmpYn)) modelMasterEntity.reqTmpYn,
modelMasterEntity.trainType,
beforeModel.uuid,
beforeModel.bestEpoch))
.from(modelMasterEntity) .from(modelMasterEntity)
.leftJoin(modelHyperParamEntity) .leftJoin(modelHyperParamEntity)
.on(modelHyperParamEntity.id.eq(modelMasterEntity.hyperParamId)) .on(modelHyperParamEntity.id.eq(modelMasterEntity.hyperParamId))
.leftJoin(modelConfigEntity) .leftJoin(modelConfigEntity)
.on(modelConfigEntity.model.id.eq(modelMasterEntity.id)) .on(modelConfigEntity.model.id.eq(modelMasterEntity.id))
.leftJoin(beforeModel)
.on(modelMasterEntity.beforeModelId.eq(beforeModel.id))
.where(modelMasterEntity.id.eq(modelId)) .where(modelMasterEntity.id.eq(modelId))
.fetchOne(); .fetchOne();
} }
@@ -231,4 +241,58 @@ public class ModelMngRepositoryImpl implements ModelMngRepositoryCustom {
.orderBy(modelMasterEntity.createdDttm.desc()) .orderBy(modelMasterEntity.createdDttm.desc())
.fetch(); .fetch();
} }
@Override
public ModelTrainMngDto.InProgressModel findInprogressModel() {
return queryFactory
.select(
Projections.constructor(
ModelTrainMngDto.InProgressModel.class,
modelMasterEntity.modelNo,
modelMasterEntity.uuid))
.from(modelMasterEntity)
.where(
modelMasterEntity
.step1State
.eq(TrainStatusType.IN_PROGRESS.getId())
.or(modelMasterEntity.step2State.eq(TrainStatusType.IN_PROGRESS.getId())))
.fetchOne();
}
@Override
public ModelTrainMngDto.ProgressPercent findTrainProgressPercent(Long id) {
NumberExpression<Integer> currentEpoch =
new CaseBuilder()
.when(
modelTrainJobEntity
.jobType
.eq("TEST")
.and(modelTrainJobEntity.statusCd.eq("SUCCESS")))
.then(1)
.otherwise(modelTrainJobEntity.currentEpoch.coalesce(0));
NumberExpression<Integer> totalEpoch = modelTrainJobEntity.totalEpoch.coalesce(1);
// per 계산
NumberExpression<Double> per =
currentEpoch
.castToNum(Double.class)
.divide(totalEpoch.castToNum(Double.class))
.multiply(100);
return queryFactory
.select(
Projections.constructor(
ModelTrainMngDto.ProgressPercent.class,
modelTrainJobEntity.modelId,
modelTrainJobEntity.jobType,
modelTrainJobEntity.statusCd,
totalEpoch.as("totalEpoch"),
currentEpoch.as("currentEpoch"),
per.as("per")))
.from(modelTrainJobEntity)
.where(modelTrainJobEntity.modelId.eq(id))
.orderBy(modelTrainJobEntity.attemptNo.desc())
.fetchFirst();
}
} }

View File

@@ -86,6 +86,12 @@ public class TrainRunRequest {
private Boolean reqTmpYn; // tmp 심볼릭 링크를 쓰는지 아닌지 여부 private Boolean reqTmpYn; // tmp 심볼릭 링크를 쓰는지 아닌지 여부
private String trainType; // 일반 : GENERAL, 전이 : TRANSFER
private UUID beforeModelUUID;
private Integer
beforeBestEpoch; // 전이학습인 경우, before 모델 베스트 pth 경로를 보내야 해서 before 모델의 bestEpoch 을 조회하기
public String getOutputFolder() { public String getOutputFolder() {
return String.valueOf(this.outputFolder); return String.valueOf(this.outputFolder);
} }

View File

@@ -379,9 +379,30 @@ public class DockerTrainService {
addArg(c, "--saturation-range", req.getSaturationRange()); addArg(c, "--saturation-range", req.getSaturationRange());
addArg(c, "--hue-delta", req.getHueDelta()); addArg(c, "--hue-delta", req.getHueDelta());
// 재시작을 위한 파라미터
if (req.getResumeFrom() != null && !req.getResumeFrom().isBlank()) { if (req.getResumeFrom() != null && !req.getResumeFrom().isBlank()) {
c.add("--resume"); c.add("--resume");
addArg(c, "--load-from", req.getResumeFrom()); addArg(c, "--load-from", req.getResumeFrom());
} else if ("TRANSFER".equals(req.getTrainType())
&& req.getBeforeModelUUID() != null
&& req.getBeforeBestEpoch() != null) {
// 재시작이 아닌데 전이학습인 경우, 전이학습을 위한 파라미터
String finalLoadFromPath;
String bestModelFileName = "best_changed_fscore_epoch_" + req.getBeforeBestEpoch() + ".pth";
String epochFileName = "epoch_" + req.getBeforeBestEpoch() + ".pth";
// 이전 모델 response 경로
Path beforeModelDir = Path.of(responseDir).resolve(req.getBeforeModelUUID().toString());
// 이전 모델 response 의 베스트 fscore pth 모델 파일 경로
Path bestFilePath = beforeModelDir.resolve(bestModelFileName);
// 베스트 fscore 파일이 있으면 그 경로를 사용, 없으면 epoch_00.pth 파일 경로 사용
if (Files.exists(bestFilePath)) {
finalLoadFromPath = bestFilePath.toString();
} else {
finalLoadFromPath = beforeModelDir.resolve(epochFileName).toString();
}
addArg(c, "--load-from", finalLoadFromPath);
} }
addArg(c, "--save-interval", 1); addArg(c, "--save-interval", 1);
@@ -498,6 +519,10 @@ public class DockerTrainService {
c.add("docker"); c.add("docker");
c.add("run"); c.add("run");
c.add("--rm"); c.add("--rm");
c.add("--user"); // root 권한 문제를 해결하기 위해 임시 추가
c.add("0:0");
c.add("--gpus"); c.add("--gpus");
c.add("all"); c.add("all");
@@ -515,7 +540,8 @@ public class DockerTrainService {
} }
c.add("-v"); c.add("-v");
c.add(responseDir + ":/checkpoints"); // c.add(responseDir + ":/checkpoints");
c.add(responseDir + "/" + req.getOutputFolder() + ":/checkpoints");
c.add("kamco-cd-train:latest"); c.add("kamco-cd-train:latest");
@@ -523,7 +549,7 @@ public class DockerTrainService {
c.add("/workspace/change-detection-code/run_evaluation_pipeline.py"); c.add("/workspace/change-detection-code/run_evaluation_pipeline.py");
addArg(c, "--dataset-folder", req.getDatasetFolder()); addArg(c, "--dataset-folder", req.getDatasetFolder());
addArg(c, "--output-folder", req.getOutputFolder()); // addArg(c, "--output-folder", req.getOutputFolder());
c.add("--epoch"); c.add("--epoch");
c.add(modelFile); c.add(modelFile);

View File

@@ -50,16 +50,17 @@ swagger:
file: file:
dataset-dir: /data/training/request/ base_path: /backup/data/training
dataset-dir: ${file.base_path}/request/
dataset-tmp-dir: ${file.dataset-dir}tmp/ dataset-tmp-dir: ${file.dataset-dir}tmp/
pt-path: /data/training/response/v6-cls-checkpoints/ pt-path: ${file.base_path}/response/v6-cls-checkpoints/
pt-FileName: yolov8_6th-6m.pt pt-FileName: yolov8_6th-6m.pt
train: train:
docker: docker:
image: kamco-cd-train:latest image: kamco-cd-train:latest
base_path: /data/training base_path: /backup/data/training
request_dir: ${train.docker.base_path}/request request_dir: ${train.docker.base_path}/request
response_dir: ${train.docker.base_path}/response response_dir: ${train.docker.base_path}/response
symbolic_link_dir: ${train.docker.base_path}/tmp symbolic_link_dir: ${train.docker.base_path}/tmp
@@ -71,4 +72,4 @@ hyper:
parameter: parameter:
gpus: 1 gpus: 1
gpu-ids: 0 gpu-ids: 0
batch-size: 10 batch-size: 10

View File

@@ -37,8 +37,8 @@ spring:
max-file-size: 10GB max-file-size: 10GB
max-request-size: 10GB max-request-size: 10GB
transaction: #transaction:
default-timeout: 300 # 5분 트랜잭션 타임아웃 # default-timeout: 300 # 5분 트랜잭션 타임아웃
logging: logging:
level: level: