학습결과 파일 베스트 에폭 제외 삭제

This commit is contained in:
2026-03-25 17:56:36 +09:00
parent 888c0e314b
commit 04eddfce54
6 changed files with 200 additions and 3 deletions

View File

@@ -286,8 +286,7 @@ public class DatasetApiController {
@ApiResponse(responseCode = "500", description = "서버 오류", content = @Content)
})
@PostMapping("/deliveries")
public ApiResponseDto<ResponseObj> insertDeliveriesDataset(@RequestBody AddDeliveriesReq req)
throws IOException {
public ApiResponseDto<ResponseObj> insertDeliveriesDataset(@RequestBody AddDeliveriesReq req) {
return ApiResponseDto.createOK(datasetService.insertDeliveriesDataset(req));
}
}

View File

@@ -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;
}
}

View File

@@ -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<CleanupResult> cleanup(
@Parameter(description = "모델 uuid") @PathVariable UUID uuid) {
return ApiResponseDto.ok(modelTrainDetailService.cleanup(uuid));
}
}

View File

@@ -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<String> failedFiles;
// 유지된 파일명 (best epoch 기준)
private String keptFile;
}
}

View File

@@ -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<ModelProgressStepDto> 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<Path> stream = Files.list(dir)) {
List<Path> 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<String> 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;
}
}

View File

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