diff --git a/src/main/java/com/kamco/cd/training/model/service/ModelTrainDetailService.java b/src/main/java/com/kamco/cd/training/model/service/ModelTrainDetailService.java index 2675493..ba5a603 100644 --- a/src/main/java/com/kamco/cd/training/model/service/ModelTrainDetailService.java +++ b/src/main/java/com/kamco/cd/training/model/service/ModelTrainDetailService.java @@ -454,34 +454,54 @@ public class ModelTrainDetailService { */ public ZipFileListResponse getZipFileListWithFullUrl( UUID uuid, String downloadUuid, HttpServletRequest request) { - log.info("ZIP 파일 목록 조회 시작 (전체 URL): modelUuid={}, downloadUuid={}", uuid, downloadUuid); + log.info("=== ZIP 파일 목록 조회 시작: modelUuid={}, downloadUuid={} ===", uuid, downloadUuid); // 1. 모델 정보 조회 Basic modelInfo; try { modelInfo = findByModelByUUID(uuid); if (modelInfo == null) { + log.warn("모델을 찾을 수 없음: modelUuid={}", uuid); throw new CustomApiException("NOT_FOUND", HttpStatus.NOT_FOUND, "모델을 찾을 수 없습니다: " + uuid); } + log.debug( + "모델 정보 조회 성공: modelNo={}, modelVer={}", modelInfo.getModelNo(), modelInfo.getModelVer()); } catch (NullPointerException e) { - log.error("모델 조회 실패: {}", uuid, e); + log.error("모델 조회 중 NullPointerException 발생: modelUuid={}", uuid, e); throw new CustomApiException("NOT_FOUND", HttpStatus.NOT_FOUND, "모델을 찾을 수 없습니다: " + uuid); + } catch (CustomApiException e) { + // CustomApiException은 그대로 재throw + throw e; + } catch (Exception e) { + log.error("모델 조회 중 예상치 못한 오류 발생: modelUuid={}", uuid, e); + throw new CustomApiException( + "INTERNAL_SERVER_ERROR", HttpStatus.INTERNAL_SERVER_ERROR, "모델 조회 중 오류가 발생했습니다."); } // 2. 실제 디렉토리 경로 찾기 (uuid 또는 uuid-out) - Path baseDir = findActualBasePath(uuid); + Path baseDir; + try { + 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, "모델 결과 디렉토리가 존재하지 않습니다."); + if (baseDir == null || !Files.exists(baseDir)) { + log.warn( + "모델 결과 디렉토리를 찾을 수 없음: modelUuid={}, 시도한 경로: {} 또는 {}-out", + uuid, + responseDir + "/" + uuid, + responseDir + "/" + uuid); + throw new CustomApiException("NOT_FOUND", HttpStatus.NOT_FOUND, "모델 결과 디렉토리가 존재하지 않습니다."); + } + + log.debug("디렉토리 발견: basePath={}", baseDir.toString()); + } catch (CustomApiException e) { + // CustomApiException은 그대로 재throw + throw e; + } catch (Exception e) { + log.error("디렉토리 경로 확인 중 오류 발생: modelUuid={}", uuid, e); + throw new CustomApiException( + "INTERNAL_SERVER_ERROR", HttpStatus.INTERNAL_SERVER_ERROR, "디렉토리 경로 확인 중 오류가 발생했습니다."); } - log.info("디렉토리 발견: basePath={}", baseDir.toString()); - // 요청에서 도메인 정보 추출 String scheme = request.getScheme(); // http 또는 https String serverName = request.getServerName(); // localhost 또는 도메인 @@ -497,6 +517,8 @@ public class ModelTrainDetailService { baseUrl = scheme + "://" + serverName + ":" + serverPort + contextPath; } + log.debug("다운로드 URL 베이스: {}", baseUrl); + // 3. ZIP 파일 목록 검색 List zipFiles = new ArrayList<>(); long totalSize = 0L; @@ -511,38 +533,62 @@ public class ModelTrainDetailService { Comparator.comparing(p -> p.getFileName().toString(), Comparator.reverseOrder())) .toList(); + log.debug("ZIP 파일 필터링 완료: 발견된 파일 개수={}", files.size()); + for (Path file : files) { - String fileName = file.getFileName().toString(); - long fileSize = Files.size(file); - totalSize += fileSize; + try { + String fileName = file.getFileName().toString(); + long fileSize = Files.size(file); + totalSize += fileSize; - // 파일명에서 버전 추출 - String version = extractVersionFromZipFileName(fileName); - boolean isCurrent = version.equals(modelInfo.getModelVer()); + // 파일명에서 버전 추출 + String version = extractVersionFromZipFileName(fileName); + boolean isCurrent = version.equals(modelInfo.getModelVer()); - // 전체 도메인 URL 포함한 다운로드 링크 생성 - String downloadUrl = baseUrl + "/api/models/download/" + uuid + "?file=" + fileName; + // 전체 도메인 URL 포함한 다운로드 링크 생성 + String downloadUrl = baseUrl + "/api/models/download/" + uuid + "?file=" + 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()); + 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.debug( + "ZIP 파일 정보 추가: fileName={}, size={}, version={}, isCurrent={}", + fileName, + formatFileSize(fileSize), + version, + isCurrent); + + } catch (IOException e) { + log.warn("ZIP 파일 정보 조회 실패 (건너뜀): file={}", file.getFileName(), e); + // 개별 파일 실패 시 전체 프로세스를 중단하지 않고 계속 진행 + } catch (Exception e) { + log.error("ZIP 파일 정보 처리 중 예상치 못한 오류 (건너뜀): file={}", file.getFileName(), e); + // 예상치 못한 오류도 전체 프로세스를 중단하지 않음 + } } - log.info("ZIP 파일 {}개 발견", zipFiles.size()); + log.info(" ZIP 파일 목록 조회 완료: 총 {}개 파일, 전체 크기={}", zipFiles.size(), formatFileSize(totalSize)); } catch (IOException e) { - log.error("ZIP 파일 목록 조회 실패: {}", baseDir, e); + log.error("ZIP 파일 목록 조회 중 IO 오류 발생: basePath={}", baseDir, e); throw new CustomApiException( - "INTERNAL_SERVER_ERROR", HttpStatus.INTERNAL_SERVER_ERROR, "ZIP 파일 목록 조회 실패"); + "INTERNAL_SERVER_ERROR", + HttpStatus.INTERNAL_SERVER_ERROR, + "ZIP 파일 목록 조회 중 IO 오류가 발생했습니다."); + } catch (Exception e) { + log.error("ZIP 파일 목록 조회 중 예상치 못한 오류 발생: basePath={}", baseDir, e); + throw new CustomApiException( + "INTERNAL_SERVER_ERROR", HttpStatus.INTERNAL_SERVER_ERROR, "ZIP 파일 목록 조회 중 오류가 발생했습니다."); } return ZipFileListResponse.builder() diff --git a/src/main/java/com/kamco/cd/training/train/service/ModelTestMetricsJobService.java b/src/main/java/com/kamco/cd/training/train/service/ModelTestMetricsJobService.java index 2341f15..18c570b 100644 --- a/src/main/java/com/kamco/cd/training/train/service/ModelTestMetricsJobService.java +++ b/src/main/java/com/kamco/cd/training/train/service/ModelTestMetricsJobService.java @@ -4,6 +4,7 @@ import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.SerializationFeature; import com.kamco.cd.training.common.enums.TrainStatusType; import com.kamco.cd.training.postgres.core.ModelTestMetricsJobCoreService; +import com.kamco.cd.training.train.dto.ModelTrainMetricsDto.BestPthInfo; import com.kamco.cd.training.train.dto.ModelTrainMetricsDto.ModelMetricJsonDto; import com.kamco.cd.training.train.dto.ModelTrainMetricsDto.ModelTestFileName; import com.kamco.cd.training.train.dto.ModelTrainMetricsDto.ResponsePathDto; @@ -19,6 +20,8 @@ import java.time.ZonedDateTime; import java.util.ArrayList; import java.util.List; import java.util.Set; +import java.util.regex.Matcher; +import java.util.regex.Pattern; import java.util.stream.Collectors; import java.util.stream.Stream; import java.util.zip.ZipEntry; @@ -81,13 +84,13 @@ public class ModelTestMetricsJobService { /** * 베스트 에폭 zip파일 생성, 테스트결과 db등록 * - * @param modelInfo + * @param modelInfo 모델 정보 (modelId, responsePath, uuid) */ private void createFile(ResponsePathDto modelInfo) { String testPath = responseDir + "/" + modelInfo.getUuid() + "/metrics/test.csv"; try (BufferedReader reader = - Files.newBufferedReader(Paths.get(testPath), StandardCharsets.UTF_8); ) { + Files.newBufferedReader(Paths.get(testPath), StandardCharsets.UTF_8)) { CSVParser parser = CSVFormat.DEFAULT.withFirstRecordAsHeader().parse(reader); @@ -161,52 +164,60 @@ public class ModelTestMetricsJobService { fileInfo.getBestEpochFileName() + ".pth", fileInfo.getModelVersion() + ".json"); - List files = new ArrayList<>(); - try (Stream s = Files.list(responsePath)) { - files.addAll( - s.filter(Files::isRegularFile) - .filter(p -> targetNames.contains(p.getFileName().toString())) - .collect(Collectors.toList())); - } catch (IOException e) { - throw new RuntimeException(e); - } - - try (Stream s = Files.list(Path.of(ptPathDir))) { - files.addAll( - s.filter(Files::isRegularFile) - .limit(1) // yolov8_6th-6m.pt 파일 1개만 - .collect(Collectors.toList())); - } catch (IOException e) { - throw new RuntimeException(e); - } - try { - zipFiles(files, zipPath); + List files = new ArrayList<>(); + try (Stream s = Files.list(responsePath)) { + files.addAll( + s.filter(Files::isRegularFile) + .filter(p -> targetNames.contains(p.getFileName().toString())) + .collect(Collectors.toList())); + } + // PT 파일 정확하게 조회 + Path ptFile = Paths.get(ptPathDir, ptFileName); + if (Files.exists(ptFile)) { + files.add(ptFile); + } else { + log.warn("PT 파일을 찾을 수 없습니다: {}", ptFile); + throw new IOException("PT 파일 누락: " + ptFile); + } + + // 기본 ZIP 생성 + zipFiles(files, zipPath); + log.info(" 기본 ZIP 생성 완료: {}", zipPath.getFileName()); + + // 개별 best*.pth ZIP 생성 + int individualZipCount = createIndividualBestPthZips(modelInfo, responsePath); + + // 모든 ZIP 생성 성공 시 COMPLETED modelTestMetricsJobCoreService.updatePackingEnd( modelInfo.getModelId(), ZonedDateTime.now(), TrainStatusType.COMPLETED.getId()); + + log.info(" 전체 ZIP 생성 완료: 기본 1개 + 개별 {}개", individualZipCount); + } catch (IOException e) { modelTestMetricsJobCoreService.updatePackingEnd( modelInfo.getModelId(), ZonedDateTime.now(), TrainStatusType.ERROR.getId()); + log.error("ZIP 생성 중 오류 발생: modelId={}", modelInfo.getModelId(), e); throw new RuntimeException(e); } - - // ===== 추가: 각 best*.pth 파일별 개별 ZIP 생성 ===== - createIndividualBestPthZips(modelInfo, responsePath, jsonDto); } /** * Response 폴더의 모든 best*.pth 파일을 각각 개별 ZIP 파일로 생성 * + *

각 PTH 파일의 Epoch과 메트릭 타입을 파싱하여 해당 Epoch의 메트릭 정보를 조회한 후, 개별 JSON 파일을 생성하고 ZIP으로 패키징합니다. + * * @param modelInfo 모델 정보 * @param responsePath Response 디렉토리 경로 - * @param jsonDto JSON 메타데이터 + * @return 생성된 개별 ZIP 파일 개수 */ - private void createIndividualBestPthZips( - ResponsePathDto modelInfo, Path responsePath, ModelMetricJsonDto jsonDto) { + private int createIndividualBestPthZips(ResponsePathDto modelInfo, Path responsePath) { log.info("=== 개별 best*.pth ZIP 파일 생성 시작: modelId={} ===", modelInfo.getModelId()); + int successCount = 0; + try { // 1. Response 폴더에서 모든 best*.pth 파일 찾기 List bestPthFiles; @@ -221,51 +232,88 @@ public class ModelTestMetricsJobService { if (bestPthFiles.isEmpty()) { log.warn("best*.pth 파일을 찾을 수 없습니다: path={}", responsePath); - return; + return 0; } log.info("발견된 best*.pth 파일 개수: {}", bestPthFiles.size()); - // 2. PT 파일 경로 (모든 ZIP에 공통으로 포함) + // 2. PT 파일 경로 확인 (모든 ZIP에 공통으로 포함) Path ptFile = Paths.get(ptPathDir, ptFileName); if (!Files.exists(ptFile)) { log.warn("PT 파일을 찾을 수 없습니다: {}", ptFile); - return; + return 0; } + // model_config.py 경로 (선택적) + Path modelConfigPath = responsePath.resolve("model_config.py"); + // 3. 각 best*.pth 파일별로 개별 ZIP 생성 for (Path bestPthFile : bestPthFiles) { String pthFileName = bestPthFile.getFileName().toString(); log.info("처리 중인 best PTH 파일: {}", pthFileName); - try { - // 3-1. 개별 JSON 파일 생성 - String individualJsonName = pthFileName.replace(".pth", ".json"); - Path individualJsonPath = responsePath.resolve(individualJsonName); - writeJsonFile(jsonDto, individualJsonPath); - log.info("개별 JSON 생성: {}", individualJsonName); + Path individualJsonPath = null; - // 3-2. 개별 ZIP 파일명 생성 + try { + // 3-1. PTH 파일명에서 Epoch과 메트릭 타입 추출 + BestPthInfo pthInfo = parsePthFileName(pthFileName); + if (pthInfo == null) { + log.warn("PTH 파일명 파싱 실패, 기본 JSON 사용: {}", pthFileName); + // 파싱 실패 시 기본 메트릭 정보 사용 + ModelMetricJsonDto defaultJsonDto = + modelTestMetricsJobCoreService.getTestMetricPackingInfo(modelInfo.getModelId()); + individualJsonPath = createIndividualJson(responsePath, pthFileName, defaultJsonDto); + } else { + // 3-2. 해당 Epoch의 메트릭 정보 조회 + log.debug( + "PTH 파일 파싱 결과: epoch={}, metricType={}", + pthInfo.getEpoch(), + pthInfo.getMetricType()); + + ModelMetricJsonDto individualJsonDto = + modelTestMetricsJobCoreService.getMetricsByEpoch( + modelInfo.getModelId(), pthInfo.getEpoch(), pthInfo.getMetricType()); + + if (individualJsonDto == null) { + log.warn("Epoch={} 메트릭 정보 없음, 기본 JSON 사용: {}", pthInfo.getEpoch(), pthFileName); + ModelMetricJsonDto defaultJsonDto = + modelTestMetricsJobCoreService.getTestMetricPackingInfo(modelInfo.getModelId()); + individualJsonPath = createIndividualJson(responsePath, pthFileName, defaultJsonDto); + } else { + // 3-3. 개별 JSON 파일 생성 + individualJsonPath = + createIndividualJson(responsePath, pthFileName, individualJsonDto); + log.info( + " Epoch별 JSON 생성: epoch={}, file={}", + pthInfo.getEpoch(), + individualJsonPath.getFileName()); + } + } + + // 3-4. 개별 ZIP 파일명 생성 // 형식: {modelVersion}.{pthFileNameWithoutExt}.zip - // 예: G1_000001.best_epoch_3.zip + // 예: G1_000001.best_fscore_5.zip, G1_000001.best_precision_7.zip String pthFileNameWithoutExt = pthFileName.replace(".pth", ""); + + // modelVersion 조회 (JSON에서 추출) + ModelTestFileName fileInfo = + modelTestMetricsJobCoreService.findModelTestFileNames(modelInfo.getModelId()); String individualZipName = - jsonDto.getModelVersion() + "." + pthFileNameWithoutExt + ".zip"; + fileInfo.getModelVersion() + "." + pthFileNameWithoutExt + ".zip"; Path individualZipPath = responsePath.resolve(individualZipName); - // 3-3. ZIP에 포함될 파일 목록 구성 + // 3-5. ZIP에 포함될 파일 목록 구성 List zipFileList = new ArrayList<>(); zipFileList.add(bestPthFile); // best*.pth 파일 zipFileList.add(individualJsonPath); // 개별 JSON 파일 zipFileList.add(ptFile); // PT 파일 // model_config.py 파일이 있으면 추가 - Path modelConfigPath = responsePath.resolve("model_config.py"); if (Files.exists(modelConfigPath)) { zipFileList.add(modelConfigPath); } - // 3-4. 개별 ZIP 생성 + // 3-6. 개별 ZIP 생성 zipFiles(zipFileList, individualZipPath); log.info( @@ -274,18 +322,112 @@ public class ModelTestMetricsJobService { pthFileName, Files.size(individualZipPath)); - } catch (IOException e) { + // 3-7. 임시 JSON 파일 정리 + try { + Files.deleteIfExists(individualJsonPath); + log.debug("임시 JSON 파일 삭제: {}", individualJsonPath.getFileName()); + } catch (IOException deleteEx) { + log.warn("임시 JSON 파일 삭제 실패: {}", individualJsonPath, deleteEx); + } + + successCount++; + + } catch (Exception e) { log.error("개별 ZIP 생성 실패: pthFile={}", pthFileName, e); + + // 실패한 임시 JSON 파일도 정리 시도 + if (individualJsonPath != null) { + try { + Files.deleteIfExists(individualJsonPath); + } catch (IOException deleteEx) { + log.warn("실패한 임시 JSON 파일 삭제 실패: {}", individualJsonPath, deleteEx); + } + } + // 개별 ZIP 실패는 전체 프로세스를 중단하지 않음 } } - log.info("=== 개별 best*.pth ZIP 파일 생성 완료: 총 {}개 ===", bestPthFiles.size()); + log.info("=== 개별 best*.pth ZIP 파일 생성 완료: 성공 {}/{}개 ===", successCount, bestPthFiles.size()); } catch (IOException e) { log.error("개별 ZIP 생성 중 오류 발생", e); // 에러 발생해도 기존 ZIP은 이미 생성되었으므로 예외를 던지지 않음 } + + return successCount; + } + + /** + * PTH 파일명에서 Epoch과 메트릭 타입을 추출 + * + *

지원되는 파일명 패턴: + * + *

    + *
  • best_{metricType}_{epoch}.pth (예: best_fscore_5.pth) + *
  • best_epoch_{epoch}.pth (예: best_epoch_10.pth) + *
  • best_changed_{metricType}_{epoch}.pth (예: best_changed_fscore_5.pth) + *
+ * + * @param fileName PTH 파일명 + * @return 파싱된 PTH 정보, 실패 시 null + */ + private BestPthInfo parsePthFileName(String fileName) { + try { + // 패턴 1: best_changed_{metricType}_{epoch}.pth + Pattern pattern1 = Pattern.compile("best_changed_([a-z_]+)_(\\d+)\\.pth"); + Matcher matcher1 = pattern1.matcher(fileName); + if (matcher1.matches()) { + String metricType = matcher1.group(1); // "fscore", "precision", "recall" + Integer epoch = Integer.parseInt(matcher1.group(2)); + Path filePath = Paths.get(fileName); + return new BestPthInfo(fileName, metricType, epoch, filePath); + } + + // 패턴 2: best_{metricType}_{epoch}.pth + Pattern pattern2 = Pattern.compile("best_([a-z_]+)_(\\d+)\\.pth"); + Matcher matcher2 = pattern2.matcher(fileName); + if (matcher2.matches()) { + String metricType = matcher2.group(1); // "fscore", "precision", "recall" + Integer epoch = Integer.parseInt(matcher2.group(2)); + Path filePath = Paths.get(fileName); + return new BestPthInfo(fileName, metricType, epoch, filePath); + } + + // 패턴 3: best_epoch_{epoch}.pth (메트릭 타입 없음) + Pattern pattern3 = Pattern.compile("best_epoch_(\\d+)\\.pth"); + Matcher matcher3 = pattern3.matcher(fileName); + if (matcher3.matches()) { + Integer epoch = Integer.parseInt(matcher3.group(1)); + Path filePath = Paths.get(fileName); + // 메트릭 타입을 "epoch"로 설정 (기본값) + return new BestPthInfo(fileName, "epoch", epoch, filePath); + } + + log.warn("알 수 없는 PTH 파일명 패턴: {}", fileName); + return null; + + } catch (Exception e) { + log.error("PTH 파일명 파싱 중 오류: {}", fileName, e); + return null; + } + } + + /** + * 개별 JSON 파일 생성 + * + * @param responsePath Response 디렉토리 경로 + * @param pthFileName PTH 파일명 + * @param jsonDto JSON 메타데이터 + * @return 생성된 JSON 파일 경로 + * @throws IOException JSON 쓰기 실패 시 + */ + private Path createIndividualJson( + Path responsePath, String pthFileName, ModelMetricJsonDto jsonDto) throws IOException { + String individualJsonName = pthFileName.replace(".pth", ".json"); + Path individualJsonPath = responsePath.resolve(individualJsonName); + writeJsonFile(jsonDto, individualJsonPath); + return individualJsonPath; } private void writeJsonFile(Object data, Path outputPath) throws IOException {