From 01a1211e55bc7b77eb30048657961a24c38f4878 Mon Sep 17 00:00:00 2001 From: teddy Date: Tue, 7 Apr 2026 14:55:27 +0900 Subject: [PATCH] =?UTF-8?q?=EB=AA=A8=EB=8D=B8=20=ED=8C=8C=EC=9D=BC=20?= =?UTF-8?q?=EA=B2=BD=EB=A1=9C=20=EC=A1=B0=ED=9A=8C,=20=EB=8D=B0=EC=9D=B4?= =?UTF-8?q?=ED=84=B0=EC=85=8B=20=ED=8C=8C=EC=9D=BC=20=EA=B2=BD=EB=A1=9C=20?= =?UTF-8?q?=EC=A1=B0=ED=9A=8C,=20=EB=94=94=EB=A0=89=ED=86=A0=EB=A6=AC=20?= =?UTF-8?q?=EC=9A=A9=EB=9F=89=20=EC=B2=B4=ED=81=AC,=20=EB=AA=A8=EB=8D=B8?= =?UTF-8?q?=EB=B3=84=20=ED=95=99=EC=8A=B5=20=EC=8B=A4=ED=96=89=20=EC=83=81?= =?UTF-8?q?=ED=83=9C=20=EC=A1=B0=ED=9A=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../filemanager/FileManagerApiController.java | 111 ++++++++ .../filemanager/dto/FileManagerDto.java | 101 ++++++++ .../service/FileManagerService.java | 243 ++++++++++++++++++ .../model/ModelMngRepositoryCustom.java | 11 + .../model/ModelMngRepositoryImpl.java | 10 + 5 files changed, 476 insertions(+) diff --git a/src/main/java/com/kamco/cd/training/filemanager/FileManagerApiController.java b/src/main/java/com/kamco/cd/training/filemanager/FileManagerApiController.java index 20a8d9b..221066f 100644 --- a/src/main/java/com/kamco/cd/training/filemanager/FileManagerApiController.java +++ b/src/main/java/com/kamco/cd/training/filemanager/FileManagerApiController.java @@ -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 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 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 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 getModelsExecutionStatus() { + + FileManagerDto.AllModelsExecutionStatusRes response = + fileManagerService.getModelsExecutionStatus(); + return ApiResponseDto.ok(response); + } } diff --git a/src/main/java/com/kamco/cd/training/filemanager/dto/FileManagerDto.java b/src/main/java/com/kamco/cd/training/filemanager/dto/FileManagerDto.java index 87ec29f..ee21b9d 100644 --- a/src/main/java/com/kamco/cd/training/filemanager/dto/FileManagerDto.java +++ b/src/main/java/com/kamco/cd/training/filemanager/dto/FileManagerDto.java @@ -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 requestFiles; + + @Schema(description = "응답 경로 파일 목록") + private List 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 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 modelStatuses; + + @Schema(description = "현재 실행 중인 모델 개수", example = "2") + private Integer runningCount; + } } diff --git a/src/main/java/com/kamco/cd/training/filemanager/service/FileManagerService.java b/src/main/java/com/kamco/cd/training/filemanager/service/FileManagerService.java index 1dd2a53..d799174 100644 --- a/src/main/java/com/kamco/cd/training/filemanager/service/FileManagerService.java +++ b/src/main/java/com/kamco/cd/training/filemanager/service/FileManagerService.java @@ -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 requestFiles = getFilesInDirectory(requestPath); + List 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 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 modelNumbers = Arrays.asList("G1", "G2", "G3", "G4"); + List modelStatuses = new ArrayList<>(); + int runningCount = 0; + + for (String modelNo : modelNumbers) { + // model_no로 가장 최근 모델 조회 (del_yn = false) + List 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 getFilesInDirectory(String directoryPath) { + List 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 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)); + } + } } diff --git a/src/main/java/com/kamco/cd/training/postgres/repository/model/ModelMngRepositoryCustom.java b/src/main/java/com/kamco/cd/training/postgres/repository/model/ModelMngRepositoryCustom.java index 9adfe9a..2cd976b 100644 --- a/src/main/java/com/kamco/cd/training/postgres/repository/model/ModelMngRepositoryCustom.java +++ b/src/main/java/com/kamco/cd/training/postgres/repository/model/ModelMngRepositoryCustom.java @@ -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 findByModelNoAndDelYnOrderByCreatedDttmDesc( + String modelNo, Boolean delYn); } diff --git a/src/main/java/com/kamco/cd/training/postgres/repository/model/ModelMngRepositoryImpl.java b/src/main/java/com/kamco/cd/training/postgres/repository/model/ModelMngRepositoryImpl.java index b3abf0f..1f31b74 100644 --- a/src/main/java/com/kamco/cd/training/postgres/repository/model/ModelMngRepositoryImpl.java +++ b/src/main/java/com/kamco/cd/training/postgres/repository/model/ModelMngRepositoryImpl.java @@ -211,4 +211,14 @@ public class ModelMngRepositoryImpl implements ModelMngRepositoryCustom { .or(modelMasterEntity.step2State.eq(TrainStatusType.IN_PROGRESS.getId()))) .fetchOne(); } + + @Override + public List findByModelNoAndDelYnOrderByCreatedDttmDesc( + String modelNo, Boolean delYn) { + return queryFactory + .selectFrom(modelMasterEntity) + .where(modelMasterEntity.modelNo.eq(modelNo), modelMasterEntity.delYn.eq(delYn)) + .orderBy(modelMasterEntity.createdDttm.desc()) + .fetch(); + } } -- 2.49.1