feat/training_260324 #173
@@ -286,8 +286,7 @@ public class DatasetApiController {
|
|||||||
@ApiResponse(responseCode = "500", description = "서버 오류", content = @Content)
|
@ApiResponse(responseCode = "500", description = "서버 오류", content = @Content)
|
||||||
})
|
})
|
||||||
@PostMapping("/deliveries")
|
@PostMapping("/deliveries")
|
||||||
public ApiResponseDto<ResponseObj> insertDeliveriesDataset(@RequestBody AddDeliveriesReq req)
|
public ApiResponseDto<ResponseObj> insertDeliveriesDataset(@RequestBody AddDeliveriesReq req) {
|
||||||
throws IOException {
|
|
||||||
return ApiResponseDto.createOK(datasetService.insertDeliveriesDataset(req));
|
return ApiResponseDto.createOK(datasetService.insertDeliveriesDataset(req));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -541,10 +541,19 @@ public class DatasetDto {
|
|||||||
@Schema(description = "경로", example = "/")
|
@Schema(description = "경로", example = "/")
|
||||||
private String filePath;
|
private String filePath;
|
||||||
|
|
||||||
|
@Schema(description = "제목", example = "")
|
||||||
private String title;
|
private String title;
|
||||||
|
|
||||||
|
@Schema(description = "메모", example = "")
|
||||||
private String memo;
|
private String memo;
|
||||||
|
|
||||||
|
@Schema(description = "비교년도", example = "")
|
||||||
private Integer compareYyyy;
|
private Integer compareYyyy;
|
||||||
|
|
||||||
|
@Schema(description = "기준년도", example = "")
|
||||||
private Integer targetYyyy;
|
private Integer targetYyyy;
|
||||||
|
|
||||||
|
@Schema(description = "회차", example = "")
|
||||||
private Long roundNo;
|
private Long roundNo;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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.ModelValidationMetrics;
|
||||||
import com.kamco.cd.training.model.dto.ModelTrainDetailDto.TransferDetailDto;
|
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.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.dto.ModelTrainMngDto.ModelProgressStepDto;
|
||||||
import com.kamco.cd.training.model.service.ModelTrainDetailService;
|
import com.kamco.cd.training.model.service.ModelTrainDetailService;
|
||||||
import com.kamco.cd.training.model.service.ModelTrainMngService;
|
import com.kamco.cd.training.model.service.ModelTrainMngService;
|
||||||
@@ -34,6 +35,7 @@ import lombok.RequiredArgsConstructor;
|
|||||||
import org.apache.coyote.BadRequestException;
|
import org.apache.coyote.BadRequestException;
|
||||||
import org.springframework.beans.factory.annotation.Value;
|
import org.springframework.beans.factory.annotation.Value;
|
||||||
import org.springframework.http.ResponseEntity;
|
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.GetMapping;
|
||||||
import org.springframework.web.bind.annotation.PathVariable;
|
import org.springframework.web.bind.annotation.PathVariable;
|
||||||
import org.springframework.web.bind.annotation.RequestMapping;
|
import org.springframework.web.bind.annotation.RequestMapping;
|
||||||
@@ -326,4 +328,25 @@ public class ModelTrainDetailApiController {
|
|||||||
UUID uuid) {
|
UUID uuid) {
|
||||||
return ApiResponseDto.ok(modelTrainDetailService.findModelTrainProgressInfo(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));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -48,6 +48,7 @@ public class ModelTrainMngDto {
|
|||||||
private ZonedDateTime packingEndDttm;
|
private ZonedDateTime packingEndDttm;
|
||||||
|
|
||||||
private Long beforeModelId;
|
private Long beforeModelId;
|
||||||
|
private Integer bestEpoch;
|
||||||
|
|
||||||
public String getStatusName() {
|
public String getStatusName() {
|
||||||
if (this.statusCd == null || this.statusCd.isBlank()) return null;
|
if (this.statusCd == null || this.statusCd.isBlank()) return null;
|
||||||
@@ -327,4 +328,23 @@ public class ModelTrainMngDto {
|
|||||||
@JsonFormatDttm private ZonedDateTime endTime;
|
@JsonFormatDttm private ZonedDateTime endTime;
|
||||||
private boolean isError;
|
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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
package com.kamco.cd.training.model.service;
|
package com.kamco.cd.training.model.service;
|
||||||
|
|
||||||
import com.kamco.cd.training.common.enums.ModelType;
|
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.DatasetReq;
|
||||||
import com.kamco.cd.training.dataset.dto.DatasetDto.SelectTransferDataSet;
|
import com.kamco.cd.training.dataset.dto.DatasetDto.SelectTransferDataSet;
|
||||||
import com.kamco.cd.training.model.dto.ModelConfigDto;
|
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.ModelValidationMetrics;
|
||||||
import com.kamco.cd.training.model.dto.ModelTrainDetailDto.TransferDetailDto;
|
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.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.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.dto.ModelTrainMngDto.ModelProgressStepDto;
|
||||||
import com.kamco.cd.training.postgres.core.ModelTrainDetailCoreService;
|
import com.kamco.cd.training.postgres.core.ModelTrainDetailCoreService;
|
||||||
import com.kamco.cd.training.postgres.core.ModelTrainMngCoreService;
|
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.ArrayList;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.UUID;
|
import java.util.UUID;
|
||||||
|
import java.util.stream.Stream;
|
||||||
import lombok.RequiredArgsConstructor;
|
import lombok.RequiredArgsConstructor;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
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.stereotype.Service;
|
||||||
import org.springframework.transaction.annotation.Transactional;
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
|
|
||||||
@@ -35,6 +45,9 @@ public class ModelTrainDetailService {
|
|||||||
private final ModelTrainDetailCoreService modelTrainDetailCoreService;
|
private final ModelTrainDetailCoreService modelTrainDetailCoreService;
|
||||||
private final ModelTrainMngCoreService mngCoreService;
|
private final ModelTrainMngCoreService mngCoreService;
|
||||||
|
|
||||||
|
@Value("${train.docker.responseDir}")
|
||||||
|
private String responseDir;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 모델 상세정보 요약
|
* 모델 상세정보 요약
|
||||||
*
|
*
|
||||||
@@ -152,4 +165,136 @@ public class ModelTrainDetailService {
|
|||||||
public List<ModelProgressStepDto> findModelTrainProgressInfo(UUID uuid) {
|
public List<ModelProgressStepDto> findModelTrainProgressInfo(UUID uuid) {
|
||||||
return modelTrainDetailCoreService.findModelTrainProgressInfo(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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -141,6 +141,7 @@ public class ModelMasterEntity {
|
|||||||
this.packingState,
|
this.packingState,
|
||||||
this.packingStrtDttm,
|
this.packingStrtDttm,
|
||||||
this.packingEndDttm,
|
this.packingEndDttm,
|
||||||
this.beforeModelId);
|
this.beforeModelId,
|
||||||
|
this.bestEpoch);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user