From 04eddfce543e5f417affa233517a24d4db11aba2 Mon Sep 17 00:00:00 2001 From: teddy Date: Wed, 25 Mar 2026 17:56:36 +0900 Subject: [PATCH] =?UTF-8?q?=ED=95=99=EC=8A=B5=EA=B2=B0=EA=B3=BC=20?= =?UTF-8?q?=ED=8C=8C=EC=9D=BC=20=EB=B2=A0=EC=8A=A4=ED=8A=B8=20=EC=97=90?= =?UTF-8?q?=ED=8F=AD=20=EC=A0=9C=EC=99=B8=20=EC=82=AD=EC=A0=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../dataset/DatasetApiController.java | 3 +- .../cd/training/dataset/dto/DatasetDto.java | 9 ++ .../model/ModelTrainDetailApiController.java | 23 +++ .../training/model/dto/ModelTrainMngDto.java | 20 +++ .../service/ModelTrainDetailService.java | 145 ++++++++++++++++++ .../postgres/entity/ModelMasterEntity.java | 3 +- 6 files changed, 200 insertions(+), 3 deletions(-) diff --git a/src/main/java/com/kamco/cd/training/dataset/DatasetApiController.java b/src/main/java/com/kamco/cd/training/dataset/DatasetApiController.java index 3c909b3..c0db7b3 100644 --- a/src/main/java/com/kamco/cd/training/dataset/DatasetApiController.java +++ b/src/main/java/com/kamco/cd/training/dataset/DatasetApiController.java @@ -286,8 +286,7 @@ public class DatasetApiController { @ApiResponse(responseCode = "500", description = "서버 오류", content = @Content) }) @PostMapping("/deliveries") - public ApiResponseDto insertDeliveriesDataset(@RequestBody AddDeliveriesReq req) - throws IOException { + public ApiResponseDto insertDeliveriesDataset(@RequestBody AddDeliveriesReq req) { return ApiResponseDto.createOK(datasetService.insertDeliveriesDataset(req)); } } diff --git a/src/main/java/com/kamco/cd/training/dataset/dto/DatasetDto.java b/src/main/java/com/kamco/cd/training/dataset/dto/DatasetDto.java index 8bf3e36..694524d 100644 --- a/src/main/java/com/kamco/cd/training/dataset/dto/DatasetDto.java +++ b/src/main/java/com/kamco/cd/training/dataset/dto/DatasetDto.java @@ -541,10 +541,19 @@ public class DatasetDto { @Schema(description = "경로", example = "/") private String filePath; + @Schema(description = "제목", example = "") private String title; + + @Schema(description = "메모", example = "") private String memo; + + @Schema(description = "비교년도", example = "") private Integer compareYyyy; + + @Schema(description = "기준년도", example = "") private Integer targetYyyy; + + @Schema(description = "회차", example = "") private Long roundNo; } } diff --git a/src/main/java/com/kamco/cd/training/model/ModelTrainDetailApiController.java b/src/main/java/com/kamco/cd/training/model/ModelTrainDetailApiController.java index 5412dd7..c01a0d4 100644 --- a/src/main/java/com/kamco/cd/training/model/ModelTrainDetailApiController.java +++ b/src/main/java/com/kamco/cd/training/model/ModelTrainDetailApiController.java @@ -11,6 +11,7 @@ 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.ModelTrainMngDto.Basic; +import com.kamco.cd.training.model.dto.ModelTrainMngDto.CleanupResult; import com.kamco.cd.training.model.dto.ModelTrainMngDto.ModelProgressStepDto; import com.kamco.cd.training.model.service.ModelTrainDetailService; import com.kamco.cd.training.model.service.ModelTrainMngService; @@ -34,6 +35,7 @@ import lombok.RequiredArgsConstructor; import org.apache.coyote.BadRequestException; import org.springframework.beans.factory.annotation.Value; 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.RequestMapping; @@ -326,4 +328,25 @@ public class ModelTrainDetailApiController { UUID uuid) { return ApiResponseDto.ok(modelTrainDetailService.findModelTrainProgressInfo(uuid)); } + + @Operation( + summary = "모델관리 > 모델 상세 > best epoch 제외 삭제", + description = "best epoch 제외 pth 파일 삭제 API") + @ApiResponses( + value = { + @ApiResponse( + responseCode = "200", + description = "삭제 성공", + content = + @Content( + mediaType = "application/json", + schema = @Schema(implementation = CleanupResult.class))), + @ApiResponse(responseCode = "404", description = "데이터셋을 찾을 수 없음", content = @Content), + @ApiResponse(responseCode = "500", description = "서버 오류", content = @Content) + }) + @DeleteMapping("/{uuid}/cleanup") + public ApiResponseDto cleanup( + @Parameter(description = "모델 uuid") @PathVariable UUID uuid) { + return ApiResponseDto.ok(modelTrainDetailService.cleanup(uuid)); + } } diff --git a/src/main/java/com/kamco/cd/training/model/dto/ModelTrainMngDto.java b/src/main/java/com/kamco/cd/training/model/dto/ModelTrainMngDto.java index 049e2fe..a344332 100644 --- a/src/main/java/com/kamco/cd/training/model/dto/ModelTrainMngDto.java +++ b/src/main/java/com/kamco/cd/training/model/dto/ModelTrainMngDto.java @@ -48,6 +48,7 @@ public class ModelTrainMngDto { private ZonedDateTime packingEndDttm; private Long beforeModelId; + private Integer bestEpoch; public String getStatusName() { if (this.statusCd == null || this.statusCd.isBlank()) return null; @@ -327,4 +328,23 @@ public class ModelTrainMngDto { @JsonFormatDttm private ZonedDateTime endTime; private boolean isError; } + + @Getter + @Setter + public static class CleanupResult { + // cleanup 대상 전체 파일 수 (삭제 대상 + 유지 파일 포함) + private int totalCount; + + // 실제로 삭제된 파일 개수 + private int deletedCount; + + // 삭제 실패한 파일 개수 + private int failedCount; + + // 삭제 실패한 파일명 목록 + private List failedFiles; + + // 유지된 파일명 (best epoch 기준) + private String keptFile; + } } 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 b33c524..ed4dbe0 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 @@ -1,6 +1,7 @@ package com.kamco.cd.training.model.service; import com.kamco.cd.training.common.enums.ModelType; +import com.kamco.cd.training.common.exception.CustomApiException; import com.kamco.cd.training.dataset.dto.DatasetDto.DatasetReq; import com.kamco.cd.training.dataset.dto.DatasetDto.SelectTransferDataSet; import com.kamco.cd.training.model.dto.ModelConfigDto; @@ -14,15 +15,24 @@ 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.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 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.List; import java.util.UUID; +import java.util.stream.Stream; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.http.HttpStatus; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @@ -35,6 +45,9 @@ public class ModelTrainDetailService { private final ModelTrainDetailCoreService modelTrainDetailCoreService; private final ModelTrainMngCoreService mngCoreService; + @Value("${train.docker.responseDir}") + private String responseDir; + /** * 모델 상세정보 요약 * @@ -152,4 +165,136 @@ public class ModelTrainDetailService { public List findModelTrainProgressInfo(UUID uuid) { return modelTrainDetailCoreService.findModelTrainProgressInfo(uuid); } + + /** + * 베스트 에폭 제외 *.pth 파일 삭제 + * + * @param uuid 학습 모델 uuid + */ + public CleanupResult cleanup(UUID uuid) { + + CleanupResult result = new CleanupResult(); + + // 학습 정보 조회 + ModelTrainMngDto.Basic model = modelTrainDetailCoreService.findByModelByUUID(uuid); + + if (model == null) { + throw new CustomApiException( + "NOT_FOUND_DATA", HttpStatus.NOT_FOUND, "모델을 찾을 수 없습니다. UUID: " + uuid); + } + + // 학습 결과 폴더 경로 + Path dir = Paths.get(responseDir, model.getUuid().toString()); + + if (!Files.exists(dir) || !Files.isDirectory(dir)) { + log.info("디렉토리가 없습니다.: {}", dir); + throw new CustomApiException("NOT_FOUND_DATA", HttpStatus.NOT_FOUND, "디렉토리가 없습니다."); + } + + if (!Files.isReadable(dir)) { + throw new CustomApiException("FORBIDDEN", HttpStatus.FORBIDDEN, "디렉토리 읽기 권한이 없습니다."); + } + + if (!Files.isWritable(dir)) { + throw new CustomApiException("FORBIDDEN", HttpStatus.FORBIDDEN, "디렉토리 삭제 권한이 없습니다."); + } + + // 저장된 best epoch + int bestEpoch = model.getBestEpoch(); + + if (bestEpoch <= 0) { + throw new CustomApiException( + "BAD_REQUEST", HttpStatus.BAD_REQUEST, "잘못된 bestEpoch 값 입니다. : " + bestEpoch); + } + + log.info("cleanup 시작. dir={}, bestEpoch={}", dir, bestEpoch); + + try (Stream stream = Files.list(dir)) { + + List pthFiles = + stream.filter(p -> p.getFileName().toString().endsWith(".pth")).toList(); + + if (pthFiles.isEmpty()) { + log.info("pth 파일이 없습니다."); + throw new CustomApiException("NOT_FOUND_DATA", HttpStatus.NOT_FOUND, "pth 파일이 없습니다."); + } + + // ===== keep 파일 찾기 ===== + Path keep = null; + + // 우선순위 1: best_*_epoch_{bestEpoch}.pth + for (Path p : pthFiles) { + String name = p.getFileName().toString(); + if (name.startsWith("best_") && name.contains("epoch_" + bestEpoch + ".pth")) { + keep = p; + break; + } + } + + // 우선순위 2: epoch_{bestEpoch}.pth + if (keep == null) { + for (Path p : pthFiles) { + if (p.getFileName().toString().equals("epoch_" + bestEpoch + ".pth")) { + keep = p; + break; + } + } + } + + if (keep == null) { + log.info("bestEpoch에 해당하는 파일이 없습니다. epoch={}", bestEpoch); + throw new CustomApiException( + "NOT_FOUND_DATA", HttpStatus.NOT_FOUND, "bestEpoch에 해당하는 파일이 없습니다. : " + bestEpoch); + } + + log.info("유지할 파일: {}", keep.getFileName()); + + // ===== 결과 세팅 ===== + result.setTotalCount(pthFiles.size()); + result.setKeptFile(keep.getFileName().toString()); + + int deletedCount = 0; + List failed = new ArrayList<>(); + + // ===== 삭제 처리 ===== + for (Path p : pthFiles) { + + if (!p.toAbsolutePath().normalize().equals(keep.toAbsolutePath().normalize())) { + + try { + boolean deleted = Files.deleteIfExists(p); + + if (deleted) { + deletedCount++; + log.info("삭제됨: {}", p.getFileName()); + } else { + log.info("이미 없음 (skip): {}", p.getFileName()); + } + + } catch (IOException e) { + failed.add(p.getFileName().toString()); + log.error("삭제 실패: {}", p.getFileName(), e); + } + } + } + + // ===== 결과 저장 ===== + result.setDeletedCount(deletedCount); + result.setFailedCount(failed.size()); + result.setFailedFiles(failed); + + } catch (IOException e) { + log.error("파일 목록 조회 실패: {}", dir, e); + throw new CustomApiException( + "INTERNAL_SERVER_ERROR", HttpStatus.INTERNAL_SERVER_ERROR, "파일 목록 조회 실패"); + } + + log.info( + "cleanup 완료. total={}, deleted={}, failed={}", + result.getTotalCount(), + result.getDeletedCount(), + result.getFailedCount()); + + return result; + } } diff --git a/src/main/java/com/kamco/cd/training/postgres/entity/ModelMasterEntity.java b/src/main/java/com/kamco/cd/training/postgres/entity/ModelMasterEntity.java index 32303e3..2609f3b 100644 --- a/src/main/java/com/kamco/cd/training/postgres/entity/ModelMasterEntity.java +++ b/src/main/java/com/kamco/cd/training/postgres/entity/ModelMasterEntity.java @@ -141,6 +141,7 @@ public class ModelMasterEntity { this.packingState, this.packingStrtDttm, this.packingEndDttm, - this.beforeModelId); + this.beforeModelId, + this.bestEpoch); } }