16 Commits

Author SHA1 Message Date
dean
02954aa439 Merge branch 'develop' of https://kamco.git.gs.dabeeo.com/MVPTeam/kamco-train-api into develop 2026-04-09 09:34:47 +09:00
dean
e3cf7b8909 test 2026-04-09 09:34:26 +09:00
59d39a79c3 daniel 작업 내용 커밋 2026-04-09 09:16:42 +09:00
dean
501b4a6f51 test 2026-04-08 18:26:25 +09:00
dean
be3e32a87a welcome 2026-04-08 17:47:30 +09:00
61d28c4ce3 spotless 2026-04-08 15:23:48 +09:00
593db69245 daniel file manager 수정 반영 2026-04-08 15:19:46 +09:00
5651fd7819 Merge pull request '파일 메니저 api 반영' (#200) from feat/training_260408 into develop
Reviewed-on: #200
2026-04-08 09:39:00 +09:00
71a8b45097 파일 메니저 api 반영 2026-04-08 09:38:00 +09:00
219e17e8ba daniel file manager 수정 반영 2026-04-07 19:35:45 +09:00
a9260c17f4 Merge pull request '파일 메니저 api 반영' (#197) from feat/training_260324 into develop
Reviewed-on: #197
2026-04-07 17:23:52 +09:00
a66b25dda5 Merge pull request '학습 상세조회 데이터셋 수정' (#196) from feat/training_260324 into develop
Reviewed-on: #196
2026-04-07 17:19:48 +09:00
5dc817ecad Merge pull request '데이터셋 태양광 등록 변수명 수정' (#195) from feat/training_260324 into develop
Reviewed-on: #195
2026-04-07 16:47:06 +09:00
98e3c35d9a Merge pull request '데이터셋 태양광 조회 추가' (#194) from feat/training_260324 into develop
Reviewed-on: #194
2026-04-07 16:35:47 +09:00
3da4b73c59 Merge pull request '모델 파일 경로 조회, 데이터셋 파일 경로 조회, 디렉토리 용량 체크, 모델별 학습 실행 상태 조회' (#193) from feat/training_260324 into develop
Reviewed-on: #193
2026-04-07 14:55:58 +09:00
c0532f230c Merge pull request 'batch_size' (#192) from feat/training_260324 into develop
Reviewed-on: #192
2026-04-06 21:42:12 +09:00
10 changed files with 1224 additions and 14 deletions

View File

@@ -131,7 +131,9 @@ public class DatasetObjDto {
private String classCd;
public String getClassName() {
return DetectionClassification.valueOf(classCd.toUpperCase()).getDesc();
return DetectionClassification.fromString(classCd).getDesc();
// fromString 메서드를 사용하여 안전하게 변환 (미정의 값은 ETC로 처리)
// return DetectionClassification.valueOf(classCd.toUpperCase()).getDesc();
}
}

View File

@@ -33,6 +33,9 @@ public class FileManagerService {
private static final String BASE_DATA_PATH = "/data";
private static final long MAX_PATH_LENGTH = 500;
@Value("${train.docker.base_path}")
private String basePath;
@Value("${train.docker.request_dir}")
private String requestDir;
@@ -567,17 +570,19 @@ public class FileManagerService {
}
/**
* 저장공간 정보 조회 (남은 공간 포함) 고정 경로: /home/kcomu/data
* 저장공간 정보 조회 (남은 공간 포함) 고정 경로: train.docker.base_path 설정값 사용
*
* @return 저장공간 정보 (사용량, 남은 공간, 디스크 용량)
*/
public FileManagerDto.StorageSpaceRes getStorageSpaceInfo() {
// 고정 경로: /home/kcomu/data
String directoryPath = "/home/kcomu/data";
// 설정값에서 경로 가져오기 (train.docker.base_path)
log.info("basePath = {}", basePath);
String directoryPath = basePath;
Path directory = Paths.get(directoryPath);
if (!Files.exists(directory)) {
log.debug("디렉토리가 존재하지 않습니다: {}", directoryPath);
log.info("디렉토리가 존재하지 않습니다: {}", directoryPath);
return FileManagerDto.StorageSpaceRes.builder()
.directoryPath(directoryPath)
.fileCount(0)
@@ -595,7 +600,7 @@ public class FileManagerService {
}
if (!Files.isDirectory(directory)) {
log.debug("디렉토리 경로가 아닙니다: {}", directoryPath);
log.info("디렉토리 경로가 아닙니다: {}", directoryPath);
return FileManagerDto.StorageSpaceRes.builder()
.directoryPath(directoryPath)
.fileCount(0)
@@ -617,6 +622,7 @@ public class FileManagerService {
final int[] fileCount = {0};
final int[] directoryCount = {0};
log.info("[StorageSpace] walkFileTree 시작 - path={}", directoryPath);
try {
Files.walkFileTree(
directory,
@@ -636,8 +642,14 @@ public class FileManagerService {
return FileVisitResult.CONTINUE;
}
});
log.info(
"[StorageSpace] walkFileTree 완료 - fileCount={}, dirCount={}, usedSize={} bytes ({})",
fileCount[0],
directoryCount[0],
usedSize[0],
formatFileSize(usedSize[0]));
} catch (IOException e) {
log.debug("디렉토리 용량 계산 중 오류 발생: {}", directoryPath, e);
log.error("디렉토리 용량 계산 중 오류 발생: {}", directoryPath, e);
return FileManagerDto.StorageSpaceRes.builder()
.directoryPath(directoryPath)
.fileCount(0)
@@ -654,7 +666,7 @@ public class FileManagerService {
.build();
}
// 디스크 공간 정보 조회 (FileStore 사용)
// 디스크 공간 정보 조회 (FileStore 사용) — basePath가 속한 파티션/NFS 마운트 전체 기준
long totalDiskSpace = 0;
long freeSpace = 0;
long usableSpace = 0;
@@ -662,17 +674,28 @@ public class FileManagerService {
try {
java.nio.file.FileStore fileStore = Files.getFileStore(directory);
totalDiskSpace = fileStore.getTotalSpace(); // 전체 디스크 용량
freeSpace = fileStore.getUnallocatedSpace(); // 남은 저장공간 (할당되지 않은 공간)
usableSpace = fileStore.getUsableSpace(); // 사용 가능한 공간 (실제 사용 가능)
totalDiskSpace = fileStore.getTotalSpace();
freeSpace = fileStore.getUnallocatedSpace(); // OS 미할당 공간 (root 예약 블록 포함 가능)
usableSpace = fileStore.getUsableSpace(); // 실제 프로세스가 사용 가능한 공간
log.info(
"[StorageSpace] FileStore - name={}, type={}, total={} bytes ({}), unallocated={} bytes ({}), usable={} bytes ({})",
fileStore.name(),
fileStore.type(),
totalDiskSpace,
formatFileSize(totalDiskSpace),
freeSpace,
formatFileSize(freeSpace),
usableSpace,
formatFileSize(usableSpace));
// 디스크 사용률 계산
// 사용률은 usableSpace 기준으로 계산 (더 정확한 실사용 가능 공간 반영)
if (totalDiskSpace > 0) {
long usedDiskSpace = totalDiskSpace - freeSpace;
long usedDiskSpace = totalDiskSpace - usableSpace;
usagePercentage = (usedDiskSpace * 100.0) / totalDiskSpace;
}
log.info("[StorageSpace] usagePercentage={}%", Math.round(usagePercentage * 100.0) / 100.0);
} catch (IOException e) {
log.debug("디스크 공간 정보 조회 중 오류 발생: {}", directoryPath, e);
log.warn("[StorageSpace] 디스크 공간 정보 조회 실패: {}", directoryPath, e);
// 디스크 정보 조회 실패 시에도 디렉토리 용량은 반환
}

View File

@@ -37,6 +37,7 @@ 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.PathVariable;
import org.springframework.web.bind.annotation.RequestHeader;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@@ -366,4 +367,42 @@ public class ModelTrainDetailApiController {
@Parameter(description = "모델 uuid") @PathVariable UUID uuid) {
return ApiResponseDto.ok(modelTrainDetailService.cleanup(uuid));
}
@Operation(
summary = "학습 결과 ZIP 파일 전체 다운로드",
description =
"모델 UUID에 해당하는 모든 ZIP 파일 목록을 조회하고" + "생성된 모든 학습데이터의 ZIP 파일을 하나의 ZIP 파일로 압축하여 다운로드",
parameters = {
@Parameter(
name = "kamco-download-uuid",
in = ParameterIn.HEADER,
required = true,
description = "다운로드 요청 UUID",
schema =
@Schema(
type = "string",
format = "uuid",
example = "6d8d49dc-0c9d-4124-adc7-b9ca610cc394"))
})
@ApiResponses(
value = {
@ApiResponse(
responseCode = "200",
description = "학습데이터 zip파일 다운로드",
content =
@Content(
mediaType = "application/octet-stream",
schema = @Schema(type = "string", format = "binary"))),
@ApiResponse(responseCode = "404", description = "모델 또는 파일 없음", content = @Content),
@ApiResponse(responseCode = "500", description = "서버 오류", content = @Content)
})
@GetMapping("/downloadzip/{uuid}")
public ResponseEntity<?> downloadZip(
@Parameter(description = "모델 UUID") @PathVariable UUID uuid,
@Parameter(hidden = true) @RequestHeader("kamco-download-uuid") String downloadUuid,
HttpServletRequest request)
throws IOException {
return modelTrainDetailService.downloadZipFile(uuid, downloadUuid, request);
}
}

View File

@@ -266,4 +266,73 @@ public class ModelTrainDetailDto {
private Boolean fileExistsYn;
private String fileName;
}
@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
@Builder
@Schema(name = "ZipFileListResponse", description = "ZIP 파일 목록 조회 응답")
public static class ZipFileListResponse {
@Schema(description = "모델 UUID", example = "2c5cdd07-5b9b-44ad-a063-8dead628b45f")
private String modelUuid;
@Schema(description = "모델 번호", example = "G2")
private String modelNo;
@Schema(description = "현재 모델 버전", example = "G2_000001")
private String modelVer;
@Schema(
description = "기본 경로",
example = "/home/kcomu/data/response/2c5cdd07-5b9b-44ad-a063-8dead628b45f")
private String basePath;
@Schema(description = "ZIP 파일 목록")
private List<ZipFileInfo> zipFiles;
@Schema(description = "총 파일 개수", example = "3")
private Integer totalFiles;
@Schema(description = "전체 파일 크기 (bytes)", example = "4048640000")
private Long totalSize;
@Schema(description = "전체 파일 크기 (포맷)", example = "3.77 GB")
private String totalSizeFormatted;
}
@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
@Builder
@Schema(name = "ZipFileInfo", description = "ZIP 파일 상세 정보")
public static class ZipFileInfo {
@Schema(description = "파일명", example = "G2.G2_000001.{uuid}.zip")
private String fileName;
@Schema(description = "파일 전체 경로")
private String filePath;
@Schema(description = "모델 버전", example = "G2_000001")
private String version;
@Schema(description = "파일 크기 (bytes)", example = "1349516895")
private Long fileSize;
@Schema(description = "파일 크기 (포맷)", example = "1.26 GB")
private String fileSizeFormatted;
@Schema(description = "최종 수정 시간", example = "2026-03-10T23:19:24.347+09:00")
@JsonFormatDttm
private ZonedDateTime lastModified;
@Schema(description = "현재 모델 버전 여부", example = "true")
private Boolean isCurrent;
@Schema(description = "다운로드 URL")
private String downloadUrl;
}
}

View File

@@ -1,5 +1,6 @@
package com.kamco.cd.training.model.service;
import com.kamco.cd.training.common.download.RangeDownloadResponder;
import com.kamco.cd.training.common.enums.ModelType;
import com.kamco.cd.training.common.enums.TrainStatusType;
import com.kamco.cd.training.common.exception.CustomApiException;
@@ -16,12 +17,15 @@ 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.TransferDetailDto;
import com.kamco.cd.training.model.dto.ModelTrainDetailDto.TransferHyperSummary;
import com.kamco.cd.training.model.dto.ModelTrainDetailDto.ZipFileInfo;
import com.kamco.cd.training.model.dto.ModelTrainDetailDto.ZipFileListResponse;
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.CleanupResult;
import com.kamco.cd.training.model.dto.ModelTrainMngDto.ModelProgressStepDto;
import com.kamco.cd.training.postgres.core.ModelTrainDetailCoreService;
import com.kamco.cd.training.postgres.core.ModelTrainMngCoreService;
import jakarta.servlet.http.HttpServletRequest;
import java.io.IOException;
import java.nio.file.AccessDeniedException;
import java.nio.file.FileVisitOption;
@@ -32,7 +36,10 @@ import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.SimpleFileVisitor;
import java.nio.file.attribute.BasicFileAttributes;
import java.nio.file.attribute.FileTime;
import java.time.ZoneId;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.EnumSet;
import java.util.List;
import java.util.UUID;
@@ -41,6 +48,7 @@ import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@@ -52,6 +60,7 @@ public class ModelTrainDetailService {
private final ModelTrainDetailCoreService modelTrainDetailCoreService;
private final ModelTrainMngCoreService mngCoreService;
private final RangeDownloadResponder rangeDownloadResponder;
@Value("${train.docker.response_dir}")
private String responseDir;
@@ -434,4 +443,363 @@ public class ModelTrainDetailService {
}
});
}
/**
* 모델 UUID로 ZIP 파일 목록 조회 및 다운로드 링크 생성
*
* @param uuid 모델 UUID
* @param downloadUuid 다운로드 추적 UUID
* @return ZIP 파일 목록 및 다운로드 링크
*/
public ZipFileListResponse getZipFileList(UUID uuid, String downloadUuid) {
log.info("ZIP 파일 목록 조회 시작: modelUuid={}, downloadUuid={}", uuid, downloadUuid);
// 1. 모델 정보 조회
Basic modelInfo;
try {
modelInfo = findByModelByUUID(uuid);
if (modelInfo == null) {
throw new CustomApiException("NOT_FOUND", HttpStatus.NOT_FOUND, "모델을 찾을 수 없습니다: " + uuid);
}
} catch (NullPointerException e) {
log.error("모델 조회 실패: {}", uuid, e);
throw new CustomApiException("NOT_FOUND", HttpStatus.NOT_FOUND, "모델을 찾을 수 없습니다: " + uuid);
}
// 2. 실제 디렉토리 경로 찾기 (uuid 또는 uuid-out)
Path baseDir = findActualBasePath(uuid);
if (baseDir == null || !Files.exists(baseDir)) {
log.warn(
"디렉토리를 찾을 수 없음: modelUuid={}, 시도한 경로: {} 또는 {}-out",
uuid,
responseDir + "/" + uuid,
responseDir + "/" + uuid);
throw new CustomApiException("NOT_FOUND", HttpStatus.NOT_FOUND, "모델 결과 디렉토리가 존재하지 않습니다.");
}
log.info("디렉토리 발견: basePath={}", baseDir.toString());
// 3. ZIP 파일 목록 검색
List<ZipFileInfo> zipFiles = new ArrayList<>();
long totalSize = 0L;
try (Stream<Path> stream = Files.list(baseDir)) {
List<Path> files =
stream
.filter(Files::isRegularFile)
.filter(p -> p.getFileName().toString().endsWith(".zip"))
.filter(p -> p.getFileName().toString().contains(uuid.toString()))
.sorted(
Comparator.comparing(p -> p.getFileName().toString(), Comparator.reverseOrder()))
.toList();
for (Path file : files) {
String fileName = file.getFileName().toString();
long fileSize = Files.size(file);
totalSize += fileSize;
// 파일명에서 버전 추출
String version = extractVersionFromZipFileName(fileName);
boolean isCurrent = version.equals(modelInfo.getModelVer());
// 다운로드 URL 생성
String downloadUrl = String.format("/api/models/download/%s?file=%s", uuid, fileName);
zipFiles.add(
ZipFileInfo.builder()
.fileName(fileName)
.filePath(file.toString())
.version(version)
.fileSize(fileSize)
.fileSizeFormatted(formatFileSize(fileSize))
.lastModified(
Files.getLastModifiedTime(file).toInstant().atZone(ZoneId.systemDefault()))
.isCurrent(isCurrent)
.downloadUrl(downloadUrl)
.build());
}
log.info("ZIP 파일 {}개 발견", zipFiles.size());
} catch (IOException e) {
log.error("ZIP 파일 목록 조회 실패: {}", baseDir, e);
throw new CustomApiException(
"INTERNAL_SERVER_ERROR", HttpStatus.INTERNAL_SERVER_ERROR, "ZIP 파일 목록 조회 실패");
}
return ZipFileListResponse.builder()
.modelUuid(uuid.toString())
.modelNo(modelInfo.getModelNo())
.modelVer(modelInfo.getModelVer())
.basePath(baseDir.toString())
.zipFiles(zipFiles)
.totalFiles(zipFiles.size())
.totalSize(totalSize)
.totalSizeFormatted(formatFileSize(totalSize))
.build();
}
/**
* 실제 디렉토리 경로 찾기 (uuid 또는 uuid-out)
*
* @param uuid 모델 UUID
* @return 실제 경로 또는 null
*/
private Path findActualBasePath(UUID uuid) {
// 1순위: {uuid}-out
Path pathWithSuffix = Paths.get(responseDir, uuid + "-out");
if (Files.exists(pathWithSuffix) && Files.isDirectory(pathWithSuffix)) {
log.debug("경로 발견: {} (suffix -out 포함)", pathWithSuffix);
return pathWithSuffix;
}
// 2순위: {uuid}
Path pathWithoutSuffix = Paths.get(responseDir, uuid.toString());
if (Files.exists(pathWithoutSuffix) && Files.isDirectory(pathWithoutSuffix)) {
log.debug("경로 발견: {} (suffix 없음)", pathWithoutSuffix);
return pathWithoutSuffix;
}
return null;
}
/**
* ZIP 파일명에서 버전 추출 예: G2.G2_000001.{uuid}.zip → G2_000001
*
* @param fileName ZIP 파일명
* @return 모델 버전
*/
private String extractVersionFromZipFileName(String fileName) {
// 패턴: {modelNo}.{modelVer}.{uuid}.zip
// 예: G2.G2_000001.d7dd54e9-1f20-46a7-9c26-89287a7d61b0.zip
String[] parts = fileName.split("\\.");
// parts[0] = G2 (modelNo)
// parts[1] = G2_000001 (modelVer)
// parts[2-7] = uuid parts
// parts[8] = zip
if (parts.length >= 2) {
return parts[1]; // G2_000001
}
// fallback: 전체 파일명에서 .zip 제거
return fileName.replace(".zip", "");
}
/**
* 파일 크기를 읽기 쉬운 형식으로 변환
*
* @param size 파일 크기 (bytes)
* @return 포맷된 문자열 (예: "1.26 GB")
*/
private String formatFileSize(long size) {
if (size < 1024) {
return size + " B";
} else if (size < 1024 * 1024) {
return String.format("%.2f KB", size / 1024.0);
} else if (size < 1024 * 1024 * 1024) {
return String.format("%.2f MB", size / (1024.0 * 1024.0));
} else {
return String.format("%.2f GB", size / (1024.0 * 1024.0 * 1024.0));
}
}
/**
* 모델 UUID로 모든 ZIP 파일 다운로드 (여러 파일이 있으면 하나의 zip으로 묶어서)
*
* @param uuid 모델 UUID
* @param downloadUuid 다운로드 추적 UUID
* @param request HTTP 요청
* @return ZIP 파일 다운로드 응답
*/
public ResponseEntity<?> downloadZipFile(
UUID uuid, String downloadUuid, HttpServletRequest request) throws IOException {
log.info("ZIP 파일 다운로드 시작: modelUuid={}, downloadUuid={}", uuid, downloadUuid);
// 1. 모델 정보 조회
Basic modelInfo;
try {
modelInfo = findByModelByUUID(uuid);
if (modelInfo == null) {
throw new CustomApiException("NOT_FOUND", HttpStatus.NOT_FOUND, "모델을 찾을 수 없습니다: " + uuid);
}
} catch (NullPointerException e) {
log.warn("모델 조회 실패: {}", uuid);
throw new CustomApiException("NOT_FOUND", HttpStatus.NOT_FOUND, "모델을 찾을 수 없습니다: " + uuid);
}
// 2. 실제 디렉토리 경로 찾기 (uuid 또는 uuid-out)
Path baseDir = findActualBasePath(uuid);
if (baseDir == null || !Files.exists(baseDir)) {
log.warn(
"디렉토리를 찾을 수 없음: modelUuid={}, 시도한 경로: {} 또는 {}-out",
uuid,
responseDir + "/" + uuid,
responseDir + "/" + uuid);
throw new CustomApiException("NOT_FOUND", HttpStatus.NOT_FOUND, "모델 결과 디렉토리가 존재하지 않습니다.");
}
log.info("디렉토리 발견: basePath={}", baseDir);
// 3. 모든 ZIP 파일 찾기
List<Path> zipFiles = findAllZipFiles(baseDir, uuid);
if (zipFiles.isEmpty()) {
log.warn("ZIP 파일을 찾을 수 없음: modelUuid={}, basePath={}", uuid, baseDir);
throw new CustomApiException("NOT_FOUND", HttpStatus.NOT_FOUND, "ZIP 파일을 찾을 수 없습니다.");
}
log.info(
"ZIP 파일 {}개 발견: basePath={}, files={}",
zipFiles.size(),
baseDir,
zipFiles.stream().map(p -> p.getFileName().toString()).toList());
// 4. 파일이 1개면 바로 다운로드
if (zipFiles.size() == 1) {
Path zipPath = zipFiles.get(0);
log.info(
"ZIP 파일 1개 다운로드: fileName={}, fileSize={} bytes, basePath={}",
zipPath.getFileName(),
Files.size(zipPath),
baseDir);
String downloadFileName = zipPath.getFileName().toString();
return rangeDownloadResponder.buildZipResponse(zipPath, downloadFileName, request);
}
// 5. 파일이 여러 개면 하나의 zip으로 묶어서 다운로드
log.info("여러 ZIP 파일을 하나로 묶어서 다운로드: 총 {}개 파일", zipFiles.size());
String combinedZipName = modelInfo.getModelNo() + "." + modelInfo.getModelVer() + ".all.zip";
Path tempZipPath = createCombinedZipFile(zipFiles, combinedZipName, uuid, baseDir);
try {
long totalSize = Files.size(tempZipPath);
log.info(
"통합 ZIP 파일 생성 완료: fileName={}, fileSize={} bytes, basePath={}",
tempZipPath.getFileName(),
totalSize,
baseDir);
ResponseEntity<?> response =
rangeDownloadResponder.buildZipResponse(tempZipPath, combinedZipName, request);
// 다운로드 완료 후 임시 파일 삭제 (비동기)
deleteTempFileAsync(tempZipPath);
return response;
} catch (IOException e) {
log.warn("통합 ZIP 파일 처리 실패: {}", tempZipPath, e);
// 에러 발생 시 임시 파일 삭제
try {
Files.deleteIfExists(tempZipPath);
Path tempDir = tempZipPath.getParent();
if (tempDir != null && Files.exists(tempDir)) {
Files.deleteIfExists(tempDir);
}
} catch (IOException ex) {
log.warn("임시 파일 삭제 실패: {}", tempZipPath, ex);
}
throw new CustomApiException(
"INTERNAL_SERVER_ERROR", HttpStatus.INTERNAL_SERVER_ERROR, "ZIP 파일 생성 실패");
}
}
/**
* 모든 ZIP 파일 찾기
*
* @param baseDir 기본 디렉토리
* @param uuid 모델 UUID
* @return ZIP 파일 목록 (최신순 정렬)
*/
private List<Path> findAllZipFiles(Path baseDir, UUID uuid) {
try (Stream<Path> stream = Files.list(baseDir)) {
return stream
.filter(Files::isRegularFile)
.filter(p -> p.getFileName().toString().endsWith(".zip"))
.filter(p -> p.getFileName().toString().contains(uuid.toString()))
.sorted(
Comparator.comparing(
p -> {
try {
return Files.getLastModifiedTime(p);
} catch (IOException e) {
return FileTime.fromMillis(0);
}
},
Comparator.reverseOrder()))
.toList();
} catch (IOException e) {
log.warn("ZIP 파일 검색 실패: basePath={}", baseDir, e);
return new ArrayList<>();
}
}
/**
* 여러 ZIP 파일을 하나의 ZIP으로 묶기
*
* @param zipFiles 원본 ZIP 파일 목록
* @param combinedZipName 통합 ZIP 파일명
* @param uuid 모델 UUID
* @param baseDir 기본 디렉토리 (로그용)
* @return 통합 ZIP 파일 경로
*/
private Path createCombinedZipFile(
List<Path> zipFiles, String combinedZipName, UUID uuid, Path baseDir) throws IOException {
// 임시 디렉토리에 통합 ZIP 생성
Path tempDir = Files.createTempDirectory("kamco-download-" + uuid);
Path combinedZipPath = tempDir.resolve(combinedZipName);
log.debug("통합 ZIP 생성 시작: tempPath={}, 원본 파일 {}개", combinedZipPath, zipFiles.size());
try (java.util.zip.ZipOutputStream zos =
new java.util.zip.ZipOutputStream(Files.newOutputStream(combinedZipPath))) {
for (Path zipFile : zipFiles) {
String entryName = zipFile.getFileName().toString();
log.debug("ZIP에 파일 추가: {}", entryName);
java.util.zip.ZipEntry entry = new java.util.zip.ZipEntry(entryName);
zos.putNextEntry(entry);
Files.copy(zipFile, zos);
zos.closeEntry();
}
zos.finish();
}
log.debug("통합 ZIP 생성 완료: {}", combinedZipPath);
return combinedZipPath;
}
/**
* 임시 파일 비동기 삭제
*
* @param tempFile 삭제할 임시 파일
*/
private void deleteTempFileAsync(Path tempFile) {
new Thread(
() -> {
try {
// 다운로드 완료 후 대기
Thread.sleep(10000); // 10초 대기
Files.deleteIfExists(tempFile);
// 임시 디렉토리도 삭제
Path tempDir = tempFile.getParent();
if (tempDir != null && Files.exists(tempDir)) {
Files.deleteIfExists(tempDir);
}
log.debug("임시 파일 삭제 완료: {}", tempFile);
} catch (Exception e) {
log.warn("임시 파일 삭제 실패: {}", tempFile, e);
}
})
.start();
}
}

View File

@@ -39,4 +39,12 @@ public interface ModelMngRepositoryCustom {
*/
List<ModelMasterEntity> findByModelNoAndDelYnOrderByCreatedDttmDesc(
String modelNo, Boolean delYn);
/**
* 하이퍼파라미터 ID로 모델 조회
*
* @param hyperParamId 하이퍼파라미터 ID
* @return 모델 목록
*/
List<ModelMasterEntity> findByHyperParamId(Long hyperParamId);
}

View File

@@ -221,4 +221,13 @@ public class ModelMngRepositoryImpl implements ModelMngRepositoryCustom {
.orderBy(modelMasterEntity.createdDttm.desc())
.fetch();
}
@Override
public List<ModelMasterEntity> findByHyperParamId(Long hyperParamId) {
return queryFactory
.selectFrom(modelMasterEntity)
.where(modelMasterEntity.hyperParamId.eq(hyperParamId), modelMasterEntity.delYn.eq(false))
.orderBy(modelMasterEntity.createdDttm.desc())
.fetch();
}
}

View File

@@ -1,9 +1,11 @@
package com.kamco.cd.training.train;
import com.kamco.cd.training.config.api.ApiResponseDto;
import com.kamco.cd.training.train.dto.TrainingMetricsDto;
import com.kamco.cd.training.train.service.DataSetCountersService;
import com.kamco.cd.training.train.service.TestJobService;
import com.kamco.cd.training.train.service.TrainJobService;
import com.kamco.cd.training.train.service.TrainingMetricsService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.media.Content;
@@ -29,6 +31,7 @@ public class TrainApiController {
private final TrainJobService trainJobService;
private final TestJobService testJobService;
private final DataSetCountersService dataSetCountersService;
private final TrainingMetricsService trainingMetricsService;
@Operation(summary = "학습 실행", description = "학습 실행 API")
@ApiResponses(
@@ -236,4 +239,63 @@ public class TrainApiController {
trainJobService.status(uuid, modelId);
return ApiResponseDto.ok("ok");
}
@Operation(
summary = "하이퍼파라미터 기반 학습 메트릭 조회",
description =
"하이퍼파라미터 UUID로 해당 파라미터를 사용하는 모델의 학습 메트릭을 조회합니다. "
+ "val.csv와 train.csv를 우선 사용하며, 없을 경우 processing.log를 파싱합니다.")
@ApiResponses(
value = {
@ApiResponse(
responseCode = "200",
description = "조회 성공",
content =
@Content(
mediaType = "application/json",
schema = @Schema(implementation = TrainingMetricsDto.Response.class))),
@ApiResponse(
responseCode = "404",
description = "하이퍼파라미터 또는 모델을 찾을 수 없음",
content = @Content),
@ApiResponse(responseCode = "500", description = "서버 오류", content = @Content)
})
@GetMapping(
path = "/metrics/hyper-param/{hyperParamUuid}",
produces = MediaType.APPLICATION_JSON_VALUE)
public ApiResponseDto<TrainingMetricsDto.Response> getMetricsByHyperParam(
@Parameter(description = "하이퍼파라미터 UUID", example = "57fc9170-64c1-4128-aa7b-0657f08d6d10")
@PathVariable
UUID hyperParamUuid) {
TrainingMetricsDto.Response response =
trainingMetricsService.getTrainingMetricsByHyperParam(hyperParamUuid);
return ApiResponseDto.ok(response);
}
@Operation(
summary = "모델 기반 학습 메트릭 조회",
description =
"모델 UUID로 해당 모델의 학습 메트릭을 조회합니다. "
+ "val.csv와 train.csv를 우선 사용하며, 없을 경우 processing.log를 파싱합니다.")
@ApiResponses(
value = {
@ApiResponse(
responseCode = "200",
description = "조회 성공",
content =
@Content(
mediaType = "application/json",
schema = @Schema(implementation = TrainingMetricsDto.Response.class))),
@ApiResponse(responseCode = "404", description = "모델을 찾을 수 없음", content = @Content),
@ApiResponse(responseCode = "500", description = "서버 오류", content = @Content)
})
@GetMapping(path = "/metrics/model/{modelUuid}", produces = MediaType.APPLICATION_JSON_VALUE)
public ApiResponseDto<TrainingMetricsDto.Response> getMetricsByModel(
@Parameter(description = "모델 UUID", example = "b34a2d18-11e6-4b1b-a156-cd314bec45bb")
@PathVariable
UUID modelUuid) {
TrainingMetricsDto.Response response =
trainingMetricsService.getTrainingMetricsByModelUuid(modelUuid);
return ApiResponseDto.ok(response);
}
}

View File

@@ -0,0 +1,148 @@
package com.kamco.cd.training.train.dto;
import com.fasterxml.jackson.annotation.JsonProperty;
import io.swagger.v3.oas.annotations.media.Schema;
import java.util.List;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
public class TrainingMetricsDto {
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
@Schema(name = "TrainingMetricsResponse", description = "학습 메트릭 조회 응답")
public static class Response {
@Schema(description = "학습 작업 ID (모델 UUID)", example = "b34a2d18-11e6-4b1b-a156-cd314bec45bb")
private String jobId;
@Schema(
description = "기본 경로",
example = "/data/training/response/b34a2d18-11e6-4b1b-a156-cd314bec45bb-out")
private String basePath;
@Schema(description = "데이터 소스 (csv 또는 log)", example = "csv")
private String source;
@Schema(description = "응답 상태 (SUCCESS, EMPTY, ERROR)", example = "SUCCESS")
private String status;
@Schema(description = "에러 메시지 (오류 발생 시만 포함)")
private String errorMessage;
@Schema(description = "Epoch별 학습 메트릭 데이터")
private List<EpochMetrics> epochs;
}
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
@Schema(name = "EpochMetrics", description = "Epoch 단위 메트릭 데이터")
public static class EpochMetrics {
@Schema(description = "Epoch 번호", example = "1")
private Integer epoch;
@Schema(description = "학습 데이터 (train.csv에서 추출)")
private TrainMetrics train;
@Schema(description = "검증 데이터 요약 (val.csv에서 추출)")
private SummaryMetrics summary;
@Schema(description = "클래스별 메트릭 (changed, unchanged)")
private List<ClassMetrics> classes;
}
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
@Schema(name = "TrainMetrics", description = "학습 메트릭 (train.csv)")
public static class TrainMetrics {
@Schema(description = "Iteration", example = "37")
@JsonProperty("iteration")
private Integer iteration;
@Schema(description = "Loss", example = "0.61584342")
@JsonProperty("loss")
private Double loss;
@Schema(description = "Learning Rate", example = "8.4e-07")
@JsonProperty("lr")
private String lr;
@Schema(description = "처리 시간 (초)", example = "1.5034")
@JsonProperty("time")
private Double time;
}
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
@Schema(name = "SummaryMetrics", description = "검증 메트릭 요약 (val.csv)")
public static class SummaryMetrics {
@Schema(description = "Accuracy (All Accuracy)", example = "29.92")
@JsonProperty("aAcc")
private Double aAcc;
@Schema(description = "Mean F-Score", example = "24.49")
@JsonProperty("mFscore")
private Double mFscore;
@Schema(description = "Mean Precision", example = "51.07")
@JsonProperty("mPrecision")
private Double mPrecision;
@Schema(description = "Mean Recall", example = "64.22")
@JsonProperty("mRecall")
private Double mRecall;
@Schema(description = "Mean IoU (Intersection over Union)", example = "15.49")
@JsonProperty("mIoU")
private Double mIoU;
@Schema(description = "Mean Accuracy", example = "64.22")
@JsonProperty("mAcc")
private Double mAcc;
}
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
@Schema(name = "ClassMetrics", description = "클래스별 메트릭 (changed/unchanged)")
public static class ClassMetrics {
@Schema(description = "클래스명 (changed 또는 unchanged)", example = "unchanged")
private String className;
@Schema(description = "F-Score", example = "44.735149")
private Double fscore;
@Schema(description = "Precision", example = "99.779264")
private Double precision;
@Schema(description = "Recall", example = "28.813878")
private Double recall;
}
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
@Schema(name = "ProcessingLogMetrics", description = "processing.log 파싱 결과 (내부용)")
public static class ProcessingLogMetrics {
private Integer epoch;
private SummaryMetrics summary;
private List<ClassMetrics> classes;
}
}

View File

@@ -0,0 +1,482 @@
package com.kamco.cd.training.train.service;
import com.kamco.cd.training.postgres.entity.ModelHyperParamEntity;
import com.kamco.cd.training.postgres.entity.ModelMasterEntity;
import com.kamco.cd.training.postgres.repository.hyperparam.HyperParamRepository;
import com.kamco.cd.training.postgres.repository.model.ModelMngRepository;
import com.kamco.cd.training.train.dto.TrainingMetricsDto;
import java.io.BufferedReader;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.UUID;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
@Slf4j
@Service
@RequiredArgsConstructor
public class TrainingMetricsService {
@Value("${train.docker.response_dir}")
private String responseDir;
private final HyperParamRepository hyperParamRepository;
private final ModelMngRepository modelMngRepository;
/**
* 하이퍼파라미터 UUID로 학습 메트릭 조회
*
* @param hyperParamUuid 하이퍼파라미터 UUID
* @return 학습 메트릭 응답
*/
public TrainingMetricsDto.Response getTrainingMetricsByHyperParam(UUID hyperParamUuid) {
log.info("하이퍼파라미터 UUID로 학습 메트릭 조회 시작: {}", hyperParamUuid);
// 1. 하이퍼파라미터로 모델 찾기
ModelHyperParamEntity hyperParam =
hyperParamRepository
.findHyperParamByUuid(hyperParamUuid)
.orElseThrow(
() -> new IllegalArgumentException("하이퍼파라미터를 찾을 수 없습니다: " + hyperParamUuid));
// 2. 해당 하이퍼파라미터를 사용하는 모델 찾기
List<ModelMasterEntity> models = modelMngRepository.findByHyperParamId(hyperParam.getId());
if (models.isEmpty()) {
log.warn("하이퍼파라미터 ID {}를 사용하는 모델이 없습니다.", hyperParam.getId());
return TrainingMetricsDto.Response.builder()
.jobId(hyperParamUuid.toString())
.basePath(null)
.source("none")
.status("EMPTY")
.errorMessage("해당 하이퍼파라미터를 사용하는 모델이 없습니다.")
.epochs(new ArrayList<>())
.build();
}
// 3. 가장 최근 모델 선택 (step1이 완료된 것 우선)
ModelMasterEntity targetModel =
models.stream()
.filter(m -> "COMPLETED".equals(m.getStep1State()))
.findFirst()
.orElse(models.get(0));
log.info(
"선택된 모델 UUID: {}, ModelNo: {}, Step1State: {}",
targetModel.getUuid(),
targetModel.getModelNo(),
targetModel.getStep1State());
// 4. 모델 UUID로 메트릭 조회
return getTrainingMetricsByModelUuid(targetModel.getUuid());
}
/**
* 모델 UUID로 학습 메트릭 조회
*
* @param modelUuid 모델 UUID
* @return 학습 메트릭 응답
*/
public TrainingMetricsDto.Response getTrainingMetricsByModelUuid(UUID modelUuid) {
log.info("모델 UUID로 학습 메트릭 조회 시작: {}", modelUuid);
// 실제 존재하는 basePath 찾기 (uuid 또는 uuid-out)
PathInfo pathInfo = findActualBasePath(modelUuid);
if (pathInfo == null || !Files.exists(pathInfo.basePath)) {
log.warn(
"모델 결과 디렉토리가 존재하지 않습니다. 시도한 경로: {} 또는 {}-out",
responseDir + "/" + modelUuid,
responseDir + "/" + modelUuid);
return TrainingMetricsDto.Response.builder()
.jobId(modelUuid.toString())
.basePath(null)
.source("none")
.status("EMPTY")
.errorMessage("모델 결과 디렉토리가 존재하지 않습니다.")
.epochs(new ArrayList<>())
.build();
}
Path basePathObj = pathInfo.basePath;
String basePath = pathInfo.basePathString;
// 1. CSV 파일 우선 시도
Path metricsDir = basePathObj.resolve("metrics");
Path valCsvPath = metricsDir.resolve("val.csv");
Path trainCsvPath = metricsDir.resolve("train.csv");
if (Files.exists(valCsvPath) && Files.exists(trainCsvPath)) {
log.info("CSV 파일을 사용하여 메트릭 파싱: {}", metricsDir);
try {
List<TrainingMetricsDto.EpochMetrics> epochs = parseCsvFiles(trainCsvPath, valCsvPath);
return TrainingMetricsDto.Response.builder()
.jobId(modelUuid.toString())
.basePath(basePath)
.source("csv")
.status("SUCCESS")
.epochs(epochs)
.build();
} catch (Exception e) {
log.error("CSV 파싱 중 오류 발생", e);
// CSV 실패 시 log fallback
}
}
// 2. processing.log fallback
Path processingDir = basePathObj.resolve("processing");
Path logPath = processingDir.resolve("processing.log");
if (Files.exists(logPath)) {
log.info("processing.log 파일을 사용하여 메트릭 파싱: {}", logPath);
try {
List<TrainingMetricsDto.EpochMetrics> epochs = parseProcessingLog(logPath);
return TrainingMetricsDto.Response.builder()
.jobId(modelUuid.toString())
.basePath(basePath)
.source("log")
.status("SUCCESS")
.epochs(epochs)
.build();
} catch (Exception e) {
log.error("processing.log 파싱 중 오류 발생", e);
return TrainingMetricsDto.Response.builder()
.jobId(modelUuid.toString())
.basePath(basePath)
.source("log")
.status("ERROR")
.errorMessage("로그 파싱 중 오류 발생: " + e.getMessage())
.epochs(new ArrayList<>())
.build();
}
}
// 3. 둘 다 없으면 EMPTY
log.warn("메트릭 파일을 찾을 수 없습니다: {}", basePath);
return TrainingMetricsDto.Response.builder()
.jobId(modelUuid.toString())
.basePath(basePath)
.source("none")
.status("EMPTY")
.errorMessage("메트릭 파일(val.csv 또는 processing.log)을 찾을 수 없습니다.")
.epochs(new ArrayList<>())
.build();
}
/**
* train.csv + val.csv 파싱
*
* @param trainCsvPath train.csv 경로
* @param valCsvPath val.csv 경로
* @return Epoch별 메트릭 리스트
*/
private List<TrainingMetricsDto.EpochMetrics> parseCsvFiles(Path trainCsvPath, Path valCsvPath)
throws IOException {
log.debug("CSV 파일 파싱 시작: train={}, val={}", trainCsvPath, valCsvPath);
// train.csv 파싱
Map<Integer, TrainingMetricsDto.TrainMetrics> trainMetricsMap = parseTrainCsv(trainCsvPath);
// val.csv 파싱
Map<Integer, ValMetricsData> valMetricsMap = parseValCsv(valCsvPath);
// 합치기
List<TrainingMetricsDto.EpochMetrics> epochs = new ArrayList<>();
for (Integer epoch : trainMetricsMap.keySet()) {
TrainingMetricsDto.TrainMetrics trainMetrics = trainMetricsMap.get(epoch);
ValMetricsData valData = valMetricsMap.get(epoch);
if (valData != null) {
epochs.add(
TrainingMetricsDto.EpochMetrics.builder()
.epoch(epoch)
.train(trainMetrics)
.summary(valData.summary)
.classes(valData.classes)
.build());
}
}
log.debug("CSV 파싱 완료: {} epoch(s)", epochs.size());
return epochs;
}
/**
* train.csv 파싱
*
* @param trainCsvPath train.csv 경로
* @return Epoch별 TrainMetrics Map
*/
private Map<Integer, TrainingMetricsDto.TrainMetrics> parseTrainCsv(Path trainCsvPath)
throws IOException {
Map<Integer, TrainingMetricsDto.TrainMetrics> result = new HashMap<>();
try (BufferedReader reader = Files.newBufferedReader(trainCsvPath)) {
String headerLine = reader.readLine(); // 헤더 스킵
if (headerLine == null) {
return result;
}
String line;
while ((line = reader.readLine()) != null) {
String[] parts = line.split(",");
if (parts.length >= 5) {
try {
int epoch = Integer.parseInt(parts[0].trim());
int iteration = Integer.parseInt(parts[1].trim());
double loss = Double.parseDouble(parts[2].trim());
String lr = parts[3].trim();
double time = Double.parseDouble(parts[4].trim());
result.put(
epoch,
TrainingMetricsDto.TrainMetrics.builder()
.iteration(iteration)
.loss(loss)
.lr(lr)
.time(time)
.build());
} catch (NumberFormatException e) {
log.warn("train.csv 파싱 오류 (라인 스킵): {}", line);
}
}
}
}
return result;
}
/**
* val.csv 파싱
*
* @param valCsvPath val.csv 경로
* @return Epoch별 ValMetricsData Map
*/
private Map<Integer, ValMetricsData> parseValCsv(Path valCsvPath) throws IOException {
Map<Integer, ValMetricsData> result = new HashMap<>();
try (BufferedReader reader = Files.newBufferedReader(valCsvPath)) {
String headerLine = reader.readLine(); // 헤더 스킵
if (headerLine == null) {
return result;
}
String line;
while ((line = reader.readLine()) != null) {
String[] parts = line.split(",");
if (parts.length >= 13) {
try {
int epoch = Integer.parseInt(parts[0].trim());
double aAcc = Double.parseDouble(parts[1].trim());
double mFscore = Double.parseDouble(parts[2].trim());
double mPrecision = Double.parseDouble(parts[3].trim());
double mRecall = Double.parseDouble(parts[4].trim());
double mIoU = Double.parseDouble(parts[5].trim());
double mAcc = Double.parseDouble(parts[6].trim());
double changedFscore = Double.parseDouble(parts[7].trim());
double changedPrecision = Double.parseDouble(parts[8].trim());
double changedRecall = Double.parseDouble(parts[9].trim());
double unchangedFscore = Double.parseDouble(parts[10].trim());
double unchangedPrecision = Double.parseDouble(parts[11].trim());
double unchangedRecall = Double.parseDouble(parts[12].trim());
TrainingMetricsDto.SummaryMetrics summary =
TrainingMetricsDto.SummaryMetrics.builder()
.aAcc(aAcc)
.mFscore(mFscore)
.mPrecision(mPrecision)
.mRecall(mRecall)
.mIoU(mIoU)
.mAcc(mAcc)
.build();
List<TrainingMetricsDto.ClassMetrics> classes = new ArrayList<>();
classes.add(
TrainingMetricsDto.ClassMetrics.builder()
.className("unchanged")
.fscore(unchangedFscore)
.precision(unchangedPrecision)
.recall(unchangedRecall)
.build());
classes.add(
TrainingMetricsDto.ClassMetrics.builder()
.className("changed")
.fscore(changedFscore)
.precision(changedPrecision)
.recall(changedRecall)
.build());
result.put(epoch, new ValMetricsData(summary, classes));
} catch (NumberFormatException e) {
log.warn("val.csv 파싱 오류 (라인 스킵): {}", line);
}
}
}
}
return result;
}
/**
* processing.log 파싱 (fallback)
*
* @param logPath processing.log 경로
* @return Epoch별 메트릭 리스트
*/
private List<TrainingMetricsDto.EpochMetrics> parseProcessingLog(Path logPath)
throws IOException {
log.debug("processing.log 파싱 시작: {}", logPath);
List<TrainingMetricsDto.EpochMetrics> epochs = new ArrayList<>();
// 정규식 패턴
Pattern epochPattern = Pattern.compile("Epoch\\(val\\)\\s*\\[(\\d+)\\]");
Pattern summaryPattern =
Pattern.compile(
"aAcc:\\s*([\\d.]+)\\s+mFscore:\\s*([\\d.]+)\\s+mPrecision:\\s*([\\d.]+)\\s+mRecall:\\s*([\\d.]+)\\s+mIoU:\\s*([\\d.]+)\\s+mAcc:\\s*([\\d.]+)");
Pattern classPattern =
Pattern.compile(
"\\|\\s*(unchanged|changed)\\s*\\|\\s*([\\d.]+)\\s*\\|\\s*([\\d.]+)\\s*\\|\\s*([\\d.]+)\\s*\\|\\s*([\\d.]+)\\s*\\|\\s*([\\d.]+)");
try (BufferedReader reader = Files.newBufferedReader(logPath)) {
String line;
Integer currentEpoch = null;
TrainingMetricsDto.SummaryMetrics currentSummary = null;
List<TrainingMetricsDto.ClassMetrics> currentClasses = new ArrayList<>();
while ((line = reader.readLine()) != null) {
// Epoch 추출
Matcher epochMatcher = epochPattern.matcher(line);
if (epochMatcher.find()) {
// 이전 epoch 데이터 저장
if (currentEpoch != null && currentSummary != null) {
epochs.add(
TrainingMetricsDto.EpochMetrics.builder()
.epoch(currentEpoch)
.train(null) // log에는 train 정보 없음
.summary(currentSummary)
.classes(new ArrayList<>(currentClasses))
.build());
}
// 새 epoch 시작
currentEpoch = Integer.parseInt(epochMatcher.group(1));
currentSummary = null;
currentClasses.clear();
continue;
}
// Summary 추출
Matcher summaryMatcher = summaryPattern.matcher(line);
if (summaryMatcher.find()) {
currentSummary =
TrainingMetricsDto.SummaryMetrics.builder()
.aAcc(Double.parseDouble(summaryMatcher.group(1)))
.mFscore(Double.parseDouble(summaryMatcher.group(2)))
.mPrecision(Double.parseDouble(summaryMatcher.group(3)))
.mRecall(Double.parseDouble(summaryMatcher.group(4)))
.mIoU(Double.parseDouble(summaryMatcher.group(5)))
.mAcc(Double.parseDouble(summaryMatcher.group(6)))
.build();
continue;
}
// Class 추출
Matcher classMatcher = classPattern.matcher(line);
if (classMatcher.find()) {
currentClasses.add(
TrainingMetricsDto.ClassMetrics.builder()
.className(classMatcher.group(1))
.fscore(Double.parseDouble(classMatcher.group(2)))
.precision(Double.parseDouble(classMatcher.group(3)))
.recall(Double.parseDouble(classMatcher.group(4)))
.build());
}
}
// 마지막 epoch 데이터 저장
if (currentEpoch != null && currentSummary != null) {
epochs.add(
TrainingMetricsDto.EpochMetrics.builder()
.epoch(currentEpoch)
.train(null)
.summary(currentSummary)
.classes(new ArrayList<>(currentClasses))
.build());
}
}
log.debug("processing.log 파싱 완료: {} epoch(s)", epochs.size());
return epochs;
}
/**
* 실제 존재하는 basePath 찾기 (uuid 또는 uuid-out 둘 다 확인)
*
* @param modelUuid 모델 UUID
* @return PathInfo 또는 null
*/
private PathInfo findActualBasePath(UUID modelUuid) {
// 1순위: {uuid}-out 경로 확인
String basePathWithSuffix = responseDir + "/" + modelUuid + "-out";
Path pathWithSuffix = Paths.get(basePathWithSuffix);
if (Files.exists(pathWithSuffix) && Files.isDirectory(pathWithSuffix)) {
log.debug("경로 발견: {} (suffix -out 포함)", basePathWithSuffix);
return new PathInfo(pathWithSuffix, basePathWithSuffix);
}
// 2순위: {uuid} 경로 확인 (suffix 없음)
String basePathWithoutSuffix = responseDir + "/" + modelUuid;
Path pathWithoutSuffix = Paths.get(basePathWithoutSuffix);
if (Files.exists(pathWithoutSuffix) && Files.isDirectory(pathWithoutSuffix)) {
log.debug("경로 발견: {} (suffix 없음)", basePathWithoutSuffix);
return new PathInfo(pathWithoutSuffix, basePathWithoutSuffix);
}
// 둘 다 없으면 null 반환
log.warn("경로를 찾을 수 없음: {} 또는 {}", basePathWithSuffix, basePathWithoutSuffix);
return null;
}
/** 경로 정보를 담는 내부 클래스 */
private static class PathInfo {
Path basePath;
String basePathString;
PathInfo(Path basePath, String basePathString) {
this.basePath = basePath;
this.basePathString = basePathString;
}
}
/** val.csv 파싱 결과를 담는 내부 클래스 */
private static class ValMetricsData {
TrainingMetricsDto.SummaryMetrics summary;
List<TrainingMetricsDto.ClassMetrics> classes;
ValMetricsData(
TrainingMetricsDto.SummaryMetrics summary, List<TrainingMetricsDto.ClassMetrics> classes) {
this.summary = summary;
this.classes = classes;
}
}
}