2 Commits

5 changed files with 476 additions and 0 deletions

View File

@@ -11,10 +11,12 @@ import io.swagger.v3.oas.annotations.media.Schema;
import io.swagger.v3.oas.annotations.responses.ApiResponse;
import io.swagger.v3.oas.annotations.responses.ApiResponses;
import io.swagger.v3.oas.annotations.tags.Tag;
import java.util.UUID;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
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.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
@@ -122,4 +124,113 @@ public class FileManagerApiController {
FileManagerDto.DeleteFileRes response = fileManagerService.deleteFiles(request);
return ApiResponseDto.ok(response);
}
@Operation(
summary = "모델 파일 경로 조회",
description =
"특정 모델 UUID로 파일 위치 경로와 하위 파일 목록을 조회합니다. "
+ "request_path(심볼릭 링크 디렉토리)와 response_path(모델 결과)를 동시에 반환합니다.")
@ApiResponses(
value = {
@ApiResponse(
responseCode = "200",
description = "조회 성공",
content =
@Content(
mediaType = "application/json",
schema = @Schema(implementation = FileManagerDto.ModelFilePathRes.class))),
@ApiResponse(responseCode = "404", description = "모델을 찾을 수 없음", content = @Content),
@ApiResponse(responseCode = "500", description = "서버 오류", content = @Content)
})
@GetMapping("/model-path/{modelUuid}")
public ApiResponseDto<FileManagerDto.ModelFilePathRes> getModelFilePath(
@Parameter(description = "모델 UUID", example = "123e4567-e89b-12d3-a456-426614174000")
@PathVariable
UUID modelUuid) {
FileManagerDto.ModelFilePathRes response = fileManagerService.getModelFilePath(modelUuid);
return ApiResponseDto.ok(response);
}
@Operation(
summary = "데이터셋 파일 경로 조회",
description =
"특정 데이터셋 UUID로 파일 위치 경로와 하위 파일 목록을 조회합니다. " + "dataset_path 컬럼의 request_dir 경로를 반환합니다.")
@ApiResponses(
value = {
@ApiResponse(
responseCode = "200",
description = "조회 성공",
content =
@Content(
mediaType = "application/json",
schema = @Schema(implementation = FileManagerDto.DatasetFilePathRes.class))),
@ApiResponse(responseCode = "404", description = "데이터셋을 찾을 수 없음", content = @Content),
@ApiResponse(responseCode = "500", description = "서버 오류", content = @Content)
})
@GetMapping("/dataset-path/{datasetUuid}")
public ApiResponseDto<FileManagerDto.DatasetFilePathRes> getDatasetFilePath(
@Parameter(description = "데이터셋 UUID", example = "123e4567-e89b-12d3-a456-426614174001")
@PathVariable
UUID datasetUuid) {
FileManagerDto.DatasetFilePathRes response = fileManagerService.getDatasetFilePath(datasetUuid);
return ApiResponseDto.ok(response);
}
@Operation(
summary = "디렉토리 용량 체크",
description = "특정 디렉토리의 총 용량, 파일 개수, 디렉토리 개수를 조회합니다. " + "basepath 하위 폴더의 용량을 재귀적으로 계산합니다.")
@ApiResponses(
value = {
@ApiResponse(
responseCode = "200",
description = "조회 성공",
content =
@Content(
mediaType = "application/json",
schema = @Schema(implementation = FileManagerDto.DirectoryCapacityRes.class))),
@ApiResponse(responseCode = "400", description = "잘못된 경로", content = @Content),
@ApiResponse(responseCode = "404", description = "디렉토리를 찾을 수 없음", content = @Content),
@ApiResponse(responseCode = "500", description = "서버 오류", content = @Content)
})
@GetMapping("/directory-capacity")
public ApiResponseDto<FileManagerDto.DirectoryCapacityRes> checkDirectoryCapacity(
@Parameter(
description = "디렉토리 경로",
example = "/home/kcomu/data/request/123e4567-e89b-12d3-a456-426614174001")
@RequestParam
String directoryPath) {
FileManagerDto.DirectoryCapacityRes response =
fileManagerService.checkDirectoryCapacity(directoryPath);
return ApiResponseDto.ok(response);
}
@Operation(
summary = "모델별 학습 실행 상태 조회",
description =
"G1~G4 모델의 현재 학습 실행 상태를 조회합니다. "
+ "step1_state, step2_state를 체크하여 어떤 모델이 학습 중인지 확인합니다. "
+ "step1과 step2는 동시 진행되지 않습니다.")
@ApiResponses(
value = {
@ApiResponse(
responseCode = "200",
description = "조회 성공",
content =
@Content(
mediaType = "application/json",
schema =
@Schema(
implementation = FileManagerDto.AllModelsExecutionStatusRes.class))),
@ApiResponse(responseCode = "500", description = "서버 오류", content = @Content)
})
@GetMapping("/models-execution-status")
public ApiResponseDto<FileManagerDto.AllModelsExecutionStatusRes> getModelsExecutionStatus() {
FileManagerDto.AllModelsExecutionStatusRes response =
fileManagerService.getModelsExecutionStatus();
return ApiResponseDto.ok(response);
}
}

View File

@@ -131,4 +131,105 @@ public class FileManagerDto {
@Schema(description = "사용률 (%)", example = "50.0")
private Double usagePercentage;
}
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
@Schema(description = "모델 파일 경로 정보 응답")
public static class ModelFilePathRes {
@Schema(description = "모델 UUID", example = "123e4567-e89b-12d3-a456-426614174000")
private String modelUuid;
@Schema(
description = "요청 경로 (심볼릭 링크 디렉토리)",
example = "/home/kcomu/data/tmp/123e4567-e89b-12d3-a456-426614174000")
private String requestPath;
@Schema(
description = "응답 경로 (모델 결과 저장)",
example = "/home/kcomu/data/response/123e4567-e89b-12d3-a456-426614174000")
private String responsePath;
@Schema(description = "요청 경로 파일 목록")
private List<FileInfo> requestFiles;
@Schema(description = "응답 경로 파일 목록")
private List<FileInfo> responseFiles;
}
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
@Schema(description = "데이터셋 파일 경로 정보 응답")
public static class DatasetFilePathRes {
@Schema(description = "데이터셋 UUID", example = "123e4567-e89b-12d3-a456-426614174001")
private String datasetUuid;
@Schema(
description = "데이터셋 경로",
example = "/home/kcomu/data/request/123e4567-e89b-12d3-a456-426614174001")
private String datasetPath;
@Schema(description = "데이터셋 파일 목록")
private List<FileInfo> files;
}
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
@Schema(description = "디렉토리 용량 체크 응답")
public static class DirectoryCapacityRes {
@Schema(description = "디렉토리 경로", example = "/home/kcomu/data/request/dataset-uuid")
private String directoryPath;
@Schema(description = "파일 개수", example = "1234")
private Integer fileCount;
@Schema(description = "디렉토리 개수", example = "56")
private Integer directoryCount;
@Schema(description = "총 용량 (bytes)", example = "10485760000")
private Long totalSize;
@Schema(description = "총 용량 (읽기 쉬운 형식)", example = "9.77 GB")
private String totalSizeFormatted;
}
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
@Schema(description = "모델별 학습 실행 상태 응답")
public static class ModelExecutionStatusRes {
@Schema(description = "모델 번호", example = "G1")
private String modelNo;
@Schema(description = "실행 상태 메시지", example = "G1 모델에 테스트를 진행중입니다.")
private String statusMessage;
@Schema(description = "1단계 상태", example = "COMPLETED")
private String step1State;
@Schema(description = "2단계 상태", example = "IN_PROGRESS")
private String step2State;
@Schema(description = "현재 실행 중인 단계", example = "2")
private Integer currentStep;
}
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
@Schema(description = "전체 모델 학습 실행 상태 목록 응답")
public static class AllModelsExecutionStatusRes {
@Schema(description = "모델별 실행 상태 목록")
private List<ModelExecutionStatusRes> modelStatuses;
@Schema(description = "현재 실행 중인 모델 개수", example = "2")
private Integer runningCount;
}
}

View File

@@ -1,6 +1,10 @@
package com.kamco.cd.training.filemanager.service;
import com.kamco.cd.training.filemanager.dto.FileManagerDto;
import com.kamco.cd.training.postgres.entity.DatasetEntity;
import com.kamco.cd.training.postgres.entity.ModelMasterEntity;
import com.kamco.cd.training.postgres.repository.dataset.DatasetRepository;
import com.kamco.cd.training.postgres.repository.model.ModelMngRepository;
import java.io.IOException;
import java.nio.file.FileVisitResult;
import java.nio.file.Files;
@@ -12,10 +16,13 @@ import java.time.Instant;
import java.time.LocalDateTime;
import java.time.ZoneId;
import java.util.ArrayList;
import java.util.Arrays;
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.stereotype.Service;
@Slf4j
@@ -26,6 +33,18 @@ public class FileManagerService {
private static final String BASE_DATA_PATH = "/data";
private static final long MAX_PATH_LENGTH = 500;
@Value("${train.docker.request_dir}")
private String requestDir;
@Value("${train.docker.response_dir}")
private String responseDir;
@Value("${train.docker.symbolic_link_dir}")
private String symbolicLinkDir;
private final ModelMngRepository modelMngRepository;
private final DatasetRepository datasetRepository;
/**
* 디렉토리 내 파일 목록 조회
*
@@ -231,4 +250,228 @@ public class FileManagerService {
throw new IllegalArgumentException("상대 경로(..)는 사용할 수 없습니다");
}
}
/**
* 모델 파일 경로 조회
*
* @param modelUuid 모델 UUID
* @return 모델 파일 경로 및 파일 목록
*/
public FileManagerDto.ModelFilePathRes getModelFilePath(UUID modelUuid) {
// tb_model_master 테이블에서 모델 존재 여부 확인
modelMngRepository
.findByUuid(modelUuid)
.orElseThrow(() -> new IllegalArgumentException("모델을 찾을 수 없습니다: " + modelUuid));
// request_path: symbolic_link_dir + model_uuid
String requestPath = symbolicLinkDir + "/" + modelUuid;
// response_path: response_dir + model_uuid
String responsePath = responseDir + "/" + modelUuid;
// 파일 목록 조회
List<FileManagerDto.FileInfo> requestFiles = getFilesInDirectory(requestPath);
List<FileManagerDto.FileInfo> responseFiles = getFilesInDirectory(responsePath);
return FileManagerDto.ModelFilePathRes.builder()
.modelUuid(modelUuid.toString())
.requestPath(requestPath)
.responsePath(responsePath)
.requestFiles(requestFiles)
.responseFiles(responseFiles)
.build();
}
/**
* 데이터셋 파일 경로 조회
*
* @param datasetUuid 데이터셋 UUID
* @return 데이터셋 파일 경로 및 파일 목록
*/
public FileManagerDto.DatasetFilePathRes getDatasetFilePath(UUID datasetUuid) {
// tb_dataset 테이블에서 데이터셋 조회
DatasetEntity dataset =
datasetRepository
.findByUuid(datasetUuid)
.orElseThrow(() -> new IllegalArgumentException("데이터셋을 찾을 수 없습니다: " + datasetUuid));
// dataset_path 컬럼이 있으면 사용, 없으면 request_dir + uuid 사용
String datasetPath = dataset.getDatasetPath();
if (datasetPath == null || datasetPath.isEmpty()) {
datasetPath = requestDir + "/" + datasetUuid;
}
// 파일 목록 조회
List<FileManagerDto.FileInfo> files = getFilesInDirectory(datasetPath);
return FileManagerDto.DatasetFilePathRes.builder()
.datasetUuid(datasetUuid.toString())
.datasetPath(datasetPath)
.files(files)
.build();
}
/**
* 디렉토리 용량 체크
*
* @param directoryPath 디렉토리 경로
* @return 디렉토리 용량 정보
*/
public FileManagerDto.DirectoryCapacityRes checkDirectoryCapacity(String directoryPath) {
validatePath(directoryPath);
Path directory = Paths.get(directoryPath);
if (!Files.exists(directory)) {
throw new IllegalArgumentException("디렉토리가 존재하지 않습니다: " + directoryPath);
}
if (!Files.isDirectory(directory)) {
throw new IllegalArgumentException("디렉토리 경로가 아닙니다: " + directoryPath);
}
final long[] totalSize = {0};
final int[] fileCount = {0};
final int[] directoryCount = {0};
try {
Files.walkFileTree(
directory,
new SimpleFileVisitor<>() {
@Override
public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) {
totalSize[0] += attrs.size();
fileCount[0]++;
return FileVisitResult.CONTINUE;
}
@Override
public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs) {
if (!dir.equals(directory)) {
directoryCount[0]++;
}
return FileVisitResult.CONTINUE;
}
});
} catch (IOException e) {
log.error("디렉토리 용량 체크 중 오류 발생: {}", directoryPath, e);
throw new RuntimeException("디렉토리 용량 체크에 실패했습니다: " + e.getMessage());
}
return FileManagerDto.DirectoryCapacityRes.builder()
.directoryPath(directoryPath)
.fileCount(fileCount[0])
.directoryCount(directoryCount[0])
.totalSize(totalSize[0])
.totalSizeFormatted(formatFileSize(totalSize[0]))
.build();
}
/**
* 모델별 학습 실행 상태 조회
*
* @return 모델별 학습 실행 상태 목록
*/
public FileManagerDto.AllModelsExecutionStatusRes getModelsExecutionStatus() {
// G1 ~ G4 모델 조회
List<String> modelNumbers = Arrays.asList("G1", "G2", "G3", "G4");
List<FileManagerDto.ModelExecutionStatusRes> modelStatuses = new ArrayList<>();
int runningCount = 0;
for (String modelNo : modelNumbers) {
// model_no로 가장 최근 모델 조회 (del_yn = false)
List<ModelMasterEntity> models =
modelMngRepository.findByModelNoAndDelYnOrderByCreatedDttmDesc(modelNo, false);
if (models.isEmpty()) {
// 모델이 없으면 대기 상태
modelStatuses.add(
FileManagerDto.ModelExecutionStatusRes.builder()
.modelNo(modelNo)
.statusMessage(modelNo + " 모델은 대기 중입니다.")
.step1State(null)
.step2State(null)
.currentStep(null)
.build());
continue;
}
ModelMasterEntity model = models.getFirst();
String step1State = model.getStep1State();
String step2State = model.getStep2State();
String statusMessage;
Integer currentStep = null;
// step1, step2 상태 확인
boolean step1Running = "IN_PROGRESS".equals(step1State);
boolean step2Running = "IN_PROGRESS".equals(step2State);
if (step1Running) {
statusMessage = modelNo + " 모델에 학습(1단계)을 진행중입니다.";
currentStep = 1;
runningCount++;
} else if (step2Running) {
statusMessage = modelNo + " 모델에 테스트(2단계)를 진행중입니다.";
currentStep = 2;
runningCount++;
} else if ("COMPLETED".equals(step1State) && "COMPLETED".equals(step2State)) {
statusMessage = modelNo + " 모델은 학습이 완료되었습니다.";
} else if ("COMPLETED".equals(step1State)) {
statusMessage = modelNo + " 모델은 1단계 학습이 완료되었습니다.";
} else {
statusMessage = modelNo + " 모델은 대기 중입니다.";
}
modelStatuses.add(
FileManagerDto.ModelExecutionStatusRes.builder()
.modelNo(modelNo)
.statusMessage(statusMessage)
.step1State(step1State)
.step2State(step2State)
.currentStep(currentStep)
.build());
}
return FileManagerDto.AllModelsExecutionStatusRes.builder()
.modelStatuses(modelStatuses)
.runningCount(runningCount)
.build();
}
/** 디렉토리 내 파일 목록 조회 (내부 사용) */
private List<FileManagerDto.FileInfo> getFilesInDirectory(String directoryPath) {
List<FileManagerDto.FileInfo> files = new ArrayList<>();
Path directory = Paths.get(directoryPath);
if (!Files.exists(directory)) {
log.warn("디렉토리가 존재하지 않습니다: {}", directoryPath);
return files;
}
if (!Files.isDirectory(directory)) {
log.warn("디렉토리 경로가 아닙니다: {}", directoryPath);
return files;
}
try (Stream<Path> stream = Files.list(directory)) {
stream.forEach(path -> files.add(createFileInfo(path)));
} catch (IOException e) {
log.error("파일 목록 조회 중 오류 발생: {}", directoryPath, e);
}
return files;
}
/** 파일 크기 포맷팅 (읽기 쉬운 형식) */
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));
}
}
}

View File

@@ -4,6 +4,7 @@ import com.kamco.cd.training.model.dto.ModelTrainMngDto;
import com.kamco.cd.training.model.dto.ModelTrainMngDto.ListDto;
import com.kamco.cd.training.postgres.entity.ModelMasterEntity;
import com.kamco.cd.training.train.dto.TrainRunRequest;
import java.util.List;
import java.util.Optional;
import java.util.UUID;
import org.springframework.data.domain.Page;
@@ -28,4 +29,14 @@ public interface ModelMngRepositoryCustom {
TrainRunRequest findTrainRunRequest(Long modelId);
Long findModelStep1InProgressCnt();
/**
* 모델 번호와 삭제 여부로 모델 조회 (최신순)
*
* @param modelNo 모델 번호 (G1, G2, G3, G4)
* @param delYn 삭제 여부
* @return 모델 목록
*/
List<ModelMasterEntity> findByModelNoAndDelYnOrderByCreatedDttmDesc(
String modelNo, Boolean delYn);
}

View File

@@ -211,4 +211,14 @@ public class ModelMngRepositoryImpl implements ModelMngRepositoryCustom {
.or(modelMasterEntity.step2State.eq(TrainStatusType.IN_PROGRESS.getId())))
.fetchOne();
}
@Override
public List<ModelMasterEntity> findByModelNoAndDelYnOrderByCreatedDttmDesc(
String modelNo, Boolean delYn) {
return queryFactory
.selectFrom(modelMasterEntity)
.where(modelMasterEntity.modelNo.eq(modelNo), modelMasterEntity.delYn.eq(delYn))
.orderBy(modelMasterEntity.createdDttm.desc())
.fetch();
}
}