From a94e8e94a0172077ccbdd277ca3838fd24ae2cf0 Mon Sep 17 00:00:00 2001 From: "gayoun.park" Date: Mon, 20 Jul 2026 15:58:26 +0900 Subject: [PATCH] =?UTF-8?q?=EB=B6=84=EB=A5=98=EB=8D=B0=EC=9D=B4=ED=84=B0?= =?UTF-8?q?=EC=85=8B=20=EC=97=85=EB=A1=9C=EB=93=9C,=EC=83=81=EC=84=B8,?= =?UTF-8?q?=EB=AA=A9=EB=A1=9D=20API?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../dataset/DatasetClsApiController.java | 302 +++++++ .../dataset/dto/DatasetClsObjDto.java | 160 ++++ .../cd/training/dataset/dto/DatasetDto.java | 6 +- .../training/dataset/dto/DatasetObjDto.java | 15 + .../dataset/service/DatasetClsService.java | 821 ++++++++++++++++++ .../postgres/core/DatasetClsCoreService.java | 278 ++++++ .../postgres/entity/DatasetClsObjEntity.java | 77 ++ .../postgres/entity/DatasetEntity.java | 6 +- .../dataset/DatasetClsRepository.java | 7 + .../dataset/DatasetClsRepositoryCustom.java | 46 + .../dataset/DatasetClsRepositoryImpl.java | 659 ++++++++++++++ .../dataset/DatasetRepositoryImpl.java | 4 +- 12 files changed, 2377 insertions(+), 4 deletions(-) create mode 100644 src/main/java/com/kamco/cd/training/dataset/DatasetClsApiController.java create mode 100644 src/main/java/com/kamco/cd/training/dataset/dto/DatasetClsObjDto.java create mode 100644 src/main/java/com/kamco/cd/training/dataset/service/DatasetClsService.java create mode 100644 src/main/java/com/kamco/cd/training/postgres/core/DatasetClsCoreService.java create mode 100644 src/main/java/com/kamco/cd/training/postgres/entity/DatasetClsObjEntity.java create mode 100644 src/main/java/com/kamco/cd/training/postgres/repository/dataset/DatasetClsRepository.java create mode 100644 src/main/java/com/kamco/cd/training/postgres/repository/dataset/DatasetClsRepositoryCustom.java create mode 100644 src/main/java/com/kamco/cd/training/postgres/repository/dataset/DatasetClsRepositoryImpl.java diff --git a/src/main/java/com/kamco/cd/training/dataset/DatasetClsApiController.java b/src/main/java/com/kamco/cd/training/dataset/DatasetClsApiController.java new file mode 100644 index 0000000..3ab4816 --- /dev/null +++ b/src/main/java/com/kamco/cd/training/dataset/DatasetClsApiController.java @@ -0,0 +1,302 @@ +package com.kamco.cd.training.dataset; + +import com.kamco.cd.training.config.api.ApiResponseDto; +import com.kamco.cd.training.dataset.dto.DatasetClsObjDto; +import com.kamco.cd.training.dataset.dto.DatasetDto; +import com.kamco.cd.training.dataset.dto.DatasetDto.AddDeliveriesReq; +import com.kamco.cd.training.dataset.dto.DatasetObjDto.DatasetClass; +import com.kamco.cd.training.dataset.dto.DatasetObjDto.DatasetStorage; +import com.kamco.cd.training.dataset.service.DatasetAsyncService; +import com.kamco.cd.training.dataset.service.DatasetClsService; +import com.kamco.cd.training.dataset.service.DatasetService; +import com.kamco.cd.training.model.dto.FileDto.FoldersDto; +import com.kamco.cd.training.model.dto.FileDto.SrchFoldersDto; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.media.Content; +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 jakarta.validation.Valid; +import java.io.IOException; +import java.nio.file.FileStore; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.List; +import java.util.UUID; +import lombok.RequiredArgsConstructor; +import org.springframework.core.io.Resource; +import org.springframework.data.domain.Page; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; + +@Tag(name = "분류모델 학습데이터 관리", description = "어드민 홈 > 학습데이터관리 > 분류데이터셋") +@RestController +@RequestMapping("/api/datasets/cls") +@RequiredArgsConstructor +public class DatasetClsApiController { + + private final DatasetClsService datasetClsService; + private final DatasetAsyncService datasetAsyncService; + + @Operation(summary = "분류 학습데이터 관리 목록 조회", description = "분류 학습데이터 목록을 조회합니다.") + @ApiResponses( + value = { + @ApiResponse( + responseCode = "200", + description = "조회 성공", + content = + @Content( + mediaType = "application/json", + schema = @Schema(implementation = Page.class))), + @ApiResponse(responseCode = "400", description = "잘못된 검색 조건", content = @Content), + @ApiResponse(responseCode = "500", description = "서버 오류", content = @Content) + }) + @GetMapping + public ApiResponseDto> searchDatasets( + @Parameter( + description = "구분", + example = "", + schema = @Schema(allowableValues = {"DELIVER", "PRODUCTION"})) + @RequestParam(required = false) + String dataType, + @Parameter(description = "제목", example = "") @RequestParam(required = false) String title, + @Parameter(description = "페이지 번호 (0부터 시작)", example = "0") @RequestParam(defaultValue = "0") + int page, + @Parameter(description = "페이지 크기", example = "20") @RequestParam(defaultValue = "20") + int size) { + DatasetDto.SearchReq searchReq = new DatasetDto.SearchReq(); + searchReq.setTitle(title); + searchReq.setDataType(dataType); + searchReq.setPage(page); + searchReq.setSize(size); + return ApiResponseDto.ok(datasetClsService.searchDatasets(searchReq)); + } + + @Operation(summary = "학습데이터관리 상세 조회", description = "학습데이터관리 상세 정보를 조회합니다.") + @ApiResponses( + value = { + @ApiResponse( + responseCode = "200", + description = "조회 성공", + content = + @Content( + mediaType = "application/json", + schema = @Schema(implementation = DatasetDto.Basic.class))), + @ApiResponse(responseCode = "404", description = "데이터셋을 찾을 수 없음", content = @Content), + @ApiResponse(responseCode = "500", description = "서버 오류", content = @Content) + }) + @GetMapping("/{uuid}") + public ApiResponseDto getDatasetDetail(@PathVariable UUID uuid) { + return ApiResponseDto.ok(datasetClsService.getDatasetDetail(uuid)); + } + + @Operation(summary = "학습데이터 수정", description = "학습데이터 제목, 메모 수정") + @ApiResponses( + value = { + @ApiResponse( + responseCode = "200", + description = "수정 성공", + content = + @Content( + mediaType = "application/json", + schema = @Schema(implementation = Long.class))), + @ApiResponse(responseCode = "400", description = "잘못된 요청 데이터", content = @Content), + @ApiResponse(responseCode = "404", description = "데이터셋을 찾을 수 없음", content = @Content), + @ApiResponse(responseCode = "500", description = "서버 오류", content = @Content) + }) + @PutMapping("/{uuid}") + public ApiResponseDto updateDataset( + @PathVariable UUID uuid, @RequestBody DatasetDto.UpdateReq updateReq) { + datasetClsService.updateDataset(uuid, updateReq); + return ApiResponseDto.ok(uuid); + } + + @Operation(summary = "학습데이터 삭제", description = "학습데이터를 삭제합니다.(납품 데이터는 삭제 불가)") + @ApiResponses( + value = { + @ApiResponse(responseCode = "201", description = "삭제 성공", content = @Content), + @ApiResponse(responseCode = "400", description = "잘못된 요청", content = @Content), + @ApiResponse(responseCode = "404", description = "데이터셋을 찾을 수 없음", content = @Content), + @ApiResponse(responseCode = "500", description = "서버 오류", content = @Content) + }) + @DeleteMapping("/{uuid}") + public ApiResponseDto deleteDatasets(@PathVariable UUID uuid) { + + datasetClsService.deleteDatasets(uuid); + return ApiResponseDto.ok(uuid); + } + + @Operation(summary = "분류 학습데이터 관리 목록 조회", description = "분류 학습데이터 목록을 조회합니다.") + @ApiResponses( + value = { + @ApiResponse( + responseCode = "200", + description = "조회 성공", + content = + @Content( + mediaType = "application/json", + schema = @Schema(implementation = Page.class))), + @ApiResponse(responseCode = "400", description = "잘못된 검색 조건", content = @Content), + @ApiResponse(responseCode = "500", description = "서버 오류", content = @Content) + }) + @GetMapping("/obj-list") + public ApiResponseDto> searchDatasetObjectList( + @Parameter(description = "회차 uuid", example = "e9a6774b-4f81-4402-b080-51d27fac1f01") + @RequestParam(required = true) + UUID uuid, + @Parameter(description = "년도", example = "2023") @RequestParam(required = false) Integer yyyy, + @Parameter(description = "분류", example = "container") @RequestParam(required = false) + String classCd, + @Parameter(description = "도엽번호", example = "36713060") @RequestParam(required = false) + String mapSheetNum, + @RequestParam(defaultValue = "0") int page, + @RequestParam(defaultValue = "20") int size) { + DatasetClsObjDto.ClsSearchReq searchReq = new DatasetClsObjDto.ClsSearchReq(); + searchReq.setUuid(uuid); + searchReq.setYyyy(yyyy); + searchReq.setClassCd(classCd); + searchReq.setMapSheetNum(mapSheetNum); + searchReq.setPage(page); + searchReq.setSize(size); + return ApiResponseDto.ok(datasetClsService.searchDatasetClsObjectList(searchReq)); + } + + @Operation(summary = "학습데이터 관리 obj 삭제", description = "학습데이터 관리 obj 삭제 API") + @ApiResponses( + value = { + @ApiResponse( + responseCode = "200", + description = "삭제 성공", + content = + @Content( + mediaType = "application/json", + schema = @Schema(implementation = Page.class))), + @ApiResponse(responseCode = "400", description = "잘못된 검색 조건", content = @Content), + @ApiResponse(responseCode = "500", description = "서버 오류", content = @Content) + }) + @DeleteMapping("/obj/{uuid}") + public ApiResponseDto deleteDatasetObjByUuid(@PathVariable UUID uuid) { + return ApiResponseDto.ok(datasetClsService.deleteDatasetObjByUuid(uuid)); + } + + @Operation(summary = "학습데이터 결과 class 조회", description = "학습데이터 결과 class 조회 API") + @ApiResponses( + value = { + @ApiResponse( + responseCode = "200", + description = "조회 성공", + content = + @Content( + mediaType = "application/json", + schema = @Schema(implementation = Page.class))), + @ApiResponse(responseCode = "400", description = "잘못된 검색 조건", content = @Content), + @ApiResponse(responseCode = "500", description = "서버 오류", content = @Content) + }) + @GetMapping("/class/{uuid}") + public ApiResponseDto> getDatasetObjByUuid( + @Parameter(description = "dataset uuid", example = "e1416f32-769f-495c-a883-3ebfacef4bac") + @PathVariable + UUID uuid, + @Parameter(description = "compare, target", example = "compare") @RequestParam String type) { + return ApiResponseDto.ok(datasetClsService.getDatasetObjByUuid(uuid, type)); + } + + @Operation(summary = "남은 저장공간 조회", description = "남은 저장공간 조회 API") + @ApiResponses( + value = { + @ApiResponse( + responseCode = "200", + description = "조회 성공", + content = + @Content( + mediaType = "application/json", + schema = @Schema(implementation = Page.class))), + @ApiResponse(responseCode = "404", description = "저장 공간 조회 오류", content = @Content), + @ApiResponse(responseCode = "500", description = "서버 오류", content = @Content) + }) + @GetMapping("/usable-bytes") + public ApiResponseDto getUsableBytes() throws IOException { + FileStore store = Files.getFileStore(Path.of(".")); + + long usable = store.getUsableSpace(); + DatasetStorage storage = new DatasetStorage(); + storage.setUsableBytes(String.valueOf(usable)); + + // datasetClsService.getUsableBytes(); + return ApiResponseDto.ok(storage); + } + + @Operation(summary = "학습데이터 zip파일 등록", description = "학습데이터 zip파일 등록 합니다.") + @PostMapping + public ApiResponseDto insertDataset( + @RequestBody @Valid DatasetDto.AddReq addReq) { + + return ApiResponseDto.okObject(datasetClsService.insertDataset(addReq)); + } + + @Operation(summary = "객체별 파일 Path 조회", description = "파일 Path 조회") + @GetMapping("/files") + public ResponseEntity getFile(@RequestParam UUID uuid, @RequestParam String pathType) + throws Exception { + + String path = datasetClsService.getFilePathByUUIDPathType(uuid, pathType); + return datasetClsService.getFilePathByFile(path); + } + + @Operation(summary = "객체별 파일 Path 조회", description = "파일 Path 조회") + @GetMapping("/files-to86") + public ResponseEntity getFileTo86( + @RequestParam UUID uuid, @RequestParam String pathType) throws Exception { + + String path = datasetClsService.getFilePathByUUIDPathType(uuid, pathType); + return datasetClsService.getFilePathByFile(path); + } + + @Operation(summary = "납품 폴더 조회", description = "납품 폴더 조회 API") + @ApiResponses( + value = { + @ApiResponse( + responseCode = "200", + description = "조회 성공", + content = + @Content( + mediaType = "application/json", + schema = @Schema(implementation = FoldersDto.class))), + @ApiResponse(responseCode = "404", description = "조회 오류", content = @Content), + @ApiResponse(responseCode = "500", description = "서버 오류", content = @Content) + }) + @PostMapping("/folder-list") + public ApiResponseDto getDir(@RequestBody SrchFoldersDto srchDto) throws IOException { + + return ApiResponseDto.createOK(datasetClsService.getFolderAll(srchDto)); + } + + @Operation(summary = "납품 학습데이터셋 등록", description = "납품 학습데이터셋 등록 API") + @ApiResponses( + value = { + @ApiResponse( + responseCode = "200", + description = "등록 성공", + content = + @Content( + mediaType = "application/json", + schema = @Schema(implementation = String.class))), + @ApiResponse(responseCode = "404", description = "조회 오류", content = @Content), + @ApiResponse(responseCode = "500", description = "서버 오류", content = @Content) + }) + @PostMapping("/deliveries") + public ApiResponseDto insertDeliveriesDataset(@RequestBody AddDeliveriesReq req) { + + // 폴더 구조 검증 + DatasetService.validateTrainValTestDirs(req.getFilePath()); + // 파일 개수 검증 + DatasetService.validateDirFileCount(req.getFilePath()); + // 폴더명(uid)으로 등록한 건이 있는지 체크 + datasetClsService.validateExistsUidChk(req.getFilePath()); + + datasetAsyncService.insertDeliveriesDatasetAsync(req); + return ApiResponseDto.createOK("ok"); + } +} diff --git a/src/main/java/com/kamco/cd/training/dataset/dto/DatasetClsObjDto.java b/src/main/java/com/kamco/cd/training/dataset/dto/DatasetClsObjDto.java new file mode 100644 index 0000000..ff84710 --- /dev/null +++ b/src/main/java/com/kamco/cd/training/dataset/dto/DatasetClsObjDto.java @@ -0,0 +1,160 @@ +package com.kamco.cd.training.dataset.dto; + +import com.fasterxml.jackson.databind.JsonNode; +import com.kamco.cd.training.common.enums.DetectionClassification; +import com.kamco.cd.training.common.utils.HeaderUtil; +import com.kamco.cd.training.common.utils.enums.CodeExpose; +import com.kamco.cd.training.common.utils.enums.EnumType; +import com.kamco.cd.training.common.utils.interfaces.JsonFormatDttm; +import io.swagger.v3.oas.annotations.media.Schema; +import java.time.ZonedDateTime; +import java.util.UUID; +import lombok.*; +import lombok.extern.slf4j.Slf4j; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Pageable; +import org.springframework.data.domain.Sort; + +@Slf4j +public class DatasetClsObjDto { + + @Schema(name = "DatasetClsObj Basic", description = "분류 데이터셋 객체 Obj 기본 정보") + @Getter + @Setter + @NoArgsConstructor + public static class Basic { + + private Long objId; + private UUID uuid; + private Long datasetUid; + private String type; + private Integer yyyy; + private String classCd; + private String mapSheetNum; + private String tifImgPath; + private String fileName; + @JsonFormatDttm private ZonedDateTime createdDttm; + private Long createdUid; + private Boolean deleted; + + public Basic( + Long objId, + UUID uuid, + Long datasetUid, + String type, + Integer yyyy, + String classCd, + String mapSheetNum, + String tifImgPath, + String fileName, + ZonedDateTime createdDttm, + Long createdUid, + Boolean deleted) { + this.objId = objId; + this.uuid = uuid; + this.datasetUid = datasetUid; + this.type = type; + this.yyyy = yyyy; + this.classCd = classCd; + this.mapSheetNum = mapSheetNum; + this.tifImgPath = tifImgPath; + this.fileName = fileName; + this.createdDttm = createdDttm; + this.createdUid = createdUid; + this.deleted = deleted; + } + } + + @Schema(name = "ClsSearchReq", description = "분류 데이터셋 상세 도엽목록 조회 요청") + @Getter + @Setter + @NoArgsConstructor + @AllArgsConstructor + public static class ClsSearchReq { + @Schema(description = "회차 uuid", example = "e9a6774b-4f81-4402-b080-51d27fac1f01") + private UUID uuid; + + @Schema(description = "년도", example = "2021") + private Integer yyyy; + + @Schema(description = "분류", example = "waste") + private String classCd; + + @Schema(description = "도엽번호", example = "36713060") + private String mapSheetNum; + + @Schema(description = "페이지 번호 (0부터 시작)", example = "0") + private int page = 0; + + @Schema(description = "페이지 크기", example = "20") + private int size = 20; + + public Pageable toPageable() { + return PageRequest.of(page, size, Sort.by(Sort.Direction.DESC, "createdDttm")); + } + } + + @Getter + @Setter + @AllArgsConstructor + @NoArgsConstructor + public static class DatasetClass { + private String classCd; + + public String getClassName() { + + return HeaderUtil.isEnglishRequest() + ? DetectionClassification.fromString(classCd).getId() + : DetectionClassification.fromString(classCd).getDesc(); + // fromString 메서드를 사용하여 안전하게 변환 (미정의 값은 ETC로 처리) + // return DetectionClassification.valueOf(classCd.toUpperCase()).getDesc(); + } + } + + @Getter + @Setter + public static class DatasetStorage { + private String usableBytes; + } + + @Getter + @Setter + @NoArgsConstructor + @AllArgsConstructor + @Builder + public static class DatasetObjRegDto { + private Long datasetUid; + private Integer compareYyyy; + private String compareClassCd; + private Integer targetYyyy; + private String targetClassCd; + private String comparePath; + private String targetPath; + private String labelPath; + private String geojsonPath; + private String mapSheetNum; + private JsonNode geojson; + private String fileName; + } + + @CodeExpose + @Getter + @AllArgsConstructor + public enum FolderType implements EnumType { + TRAIN("train"), + VAL("val"), + TEST("test"); + + private String desc; + + @Override + public String getId() { + return name(); + } + + @Override + public String getText() { + return desc; + } + } +} 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 2301d36..2977102 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 @@ -47,6 +47,7 @@ public class DatasetDto { private String statusCd; private Boolean deleted; private String dataType; + private String cdClsType; public Basic( Long id, @@ -60,7 +61,8 @@ public class DatasetDto { ZonedDateTime createdDttm, String status, Boolean deleted, - String dataType) { + String dataType, + String cdClsType) { this.id = id; this.uuid = uuid; this.title = title; @@ -74,6 +76,7 @@ public class DatasetDto { this.statusCd = status; this.deleted = deleted; this.dataType = dataType; + this.cdClsType = cdClsType; } public String getTotalSize(Long totalSize) { @@ -542,6 +545,7 @@ public class DatasetDto { private Long totalSize; private Long totalObjectCount; private String datasetPath; + private String cdClsType; } @Getter diff --git a/src/main/java/com/kamco/cd/training/dataset/dto/DatasetObjDto.java b/src/main/java/com/kamco/cd/training/dataset/dto/DatasetObjDto.java index a18b88a..6dcb4c9 100644 --- a/src/main/java/com/kamco/cd/training/dataset/dto/DatasetObjDto.java +++ b/src/main/java/com/kamco/cd/training/dataset/dto/DatasetObjDto.java @@ -166,4 +166,19 @@ public class DatasetObjDto { private JsonNode geojson; private String fileName; } + + @Getter + @Setter + @NoArgsConstructor + @AllArgsConstructor + @Builder + public static class DatasetClsObjRegDto { + private Long datasetUid; + private String type; + private Integer yyyy; + private String classCd; + private String mapSheetNum; + private String tifImgPath; + private String fileName; + } } diff --git a/src/main/java/com/kamco/cd/training/dataset/service/DatasetClsService.java b/src/main/java/com/kamco/cd/training/dataset/service/DatasetClsService.java new file mode 100644 index 0000000..407563e --- /dev/null +++ b/src/main/java/com/kamco/cd/training/dataset/service/DatasetClsService.java @@ -0,0 +1,821 @@ +package com.kamco.cd.training.dataset.service; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.node.ArrayNode; +import com.fasterxml.jackson.databind.node.ObjectNode; +import com.kamco.cd.training.common.enums.LearnDataRegister; +import com.kamco.cd.training.common.enums.LearnDataType; +import com.kamco.cd.training.common.exception.CustomApiException; +import com.kamco.cd.training.common.service.FormatStorage; +import com.kamco.cd.training.common.utils.FIleChecker; +import com.kamco.cd.training.config.api.ApiResponseDto.ApiResponseCode; +import com.kamco.cd.training.config.api.ApiResponseDto.ResponseObj; +import com.kamco.cd.training.dataset.dto.DatasetClsObjDto; +import com.kamco.cd.training.dataset.dto.DatasetDto; +import com.kamco.cd.training.dataset.dto.DatasetDto.AddDeliveriesReq; +import com.kamco.cd.training.dataset.dto.DatasetDto.AddReq; +import com.kamco.cd.training.dataset.dto.DatasetDto.DatasetMngRegDto; +import com.kamco.cd.training.dataset.dto.DatasetObjDto; +import com.kamco.cd.training.dataset.dto.DatasetObjDto.DatasetClass; +import com.kamco.cd.training.dataset.dto.DatasetObjDto.DatasetObjRegDto; +import com.kamco.cd.training.dataset.dto.DatasetObjDto.DatasetStorage; +import com.kamco.cd.training.model.dto.FileDto.FoldersDto; +import com.kamco.cd.training.model.dto.FileDto.SrchFoldersDto; +import com.kamco.cd.training.postgres.core.DatasetClsCoreService; +import jakarta.validation.Valid; +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.*; +import java.util.stream.Collectors; +import java.util.stream.Stream; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.core.io.InputStreamResource; +import org.springframework.core.io.Resource; +import org.springframework.data.domain.Page; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; +import org.springframework.http.ResponseEntity; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +@Slf4j +@Service +@RequiredArgsConstructor +public class DatasetClsService { + + private final DatasetClsCoreService datasetClsCoreService; + private final DatasetBatchService datasetBatchService; + + private static final List LABEL_DIRS = List.of("label-json", "label", "input1", "input2"); + private static final List REQUIRED_DIRS = Arrays.asList("train", "val", "test"); + private static final List CHECK_DIRS = List.of("label", "input1", "input2"); + + /** + * 데이터셋 목록 조회 + * + * @param searchReq 검색 조건 + * @return 데이터셋 목록 + */ + public Page searchDatasets(DatasetDto.SearchReq searchReq) { + log.info("데이터셋 목록 조회 - 조건: {}", searchReq); + return datasetClsCoreService.findDatasetList(searchReq); + } + + /** + * 데이터셋 상세 조회 + * + * @param id 상세 조회할 목록 Id + * @return 데이터셋 상세 정보 + */ + public DatasetDto.Basic getDatasetDetail(UUID id) { + return datasetClsCoreService.getOneByUuid(id); + } + + // TODO 미사용시작 + /** + * 데이터셋 등록 + * + * @param registerReq 등록 요청 + * @return 등록된 데이터셋 ID + */ + @Transactional + public Long registerDataset(DatasetDto.RegisterReq registerReq) { + log.info("데이터셋 등록 - 요청: {}", registerReq); + DatasetDto.Basic saved = datasetClsCoreService.save(registerReq); + log.info("데이터셋 등록 완료 - ID: {}", saved.getId()); + return saved.getId(); + } + + // TODO 미사용 끝 + /** + * 데이터셋 수정 + * + * @param updateReq 수정 요청 + * @return 수정된 데이터셋 ID + */ + @Transactional + public void updateDataset(UUID uuid, DatasetDto.UpdateReq updateReq) { + datasetClsCoreService.update(uuid, updateReq); + } + + /** + * 데이터셋 삭제 (다건) + * + * @param uuid 삭제 요청 + */ + @Transactional + public void deleteDatasets(UUID uuid) { + datasetClsCoreService.deleteDatasets(uuid); + } + + /** + * 데이터셋 통계 요약 + * + * @param summaryReq 요약 요청 + * @return 통계 요약 + */ + public DatasetDto.Summary getDatasetSummary(DatasetDto.SummaryReq summaryReq) { + log.info("데이터셋 통계 요약 - 요청: {}", summaryReq); + return datasetClsCoreService.getDatasetSummary(summaryReq); + } + + public Page searchDatasetClsObjectList( + DatasetClsObjDto.ClsSearchReq searchReq) { + return datasetClsCoreService.searchDatasetClsObjectList(searchReq); + } + + public UUID deleteDatasetObjByUuid(UUID uuid) { + return datasetClsCoreService.deleteDatasetObjByUuid(uuid); + } + + /** + * 데이터셋 object class 조회 + * + * @param uuid dataset uuid + * @param type compare, target + */ + public List getDatasetObjByUuid(UUID uuid, String type) { + return datasetClsCoreService.findDatasetObjClassByUuid(uuid, type); + } + + /** + * 사용 가능 공간 조회 + * + * @return + */ + public DatasetStorage getUsableBytes() { + // 현재 실행 위치가 속한 디스크 기준 + try { + FormatStorage.DiskUsage usage = FormatStorage.getDiskUsage(Path.of(".")); + log.debug("경로 : {}", usage.path()); + log.debug("총 저장공간 : {}", usage.totalText()); + log.debug("남은 저장공간 : {}", usage.usableText()); + log.debug("사용률 : {}", usage.usedPercent()); + + DatasetStorage datasetStorage = new DatasetStorage(); + datasetStorage.setUsableBytes(usage.usableText()); + return datasetStorage; + } catch (Exception e) { + throw new CustomApiException("NOT_FOUND", HttpStatus.NOT_FOUND); + } + } + + /** + * 분류모델 업로드 + * + * @param addReq + * @return + */ + @Transactional + public ResponseObj insertDataset(@Valid AddReq addReq) { + + Long datasetUid = null; // master id 값, 등록하면서 가져올 예정 + + try { + // 압축 해제 + FIleChecker.unzip(addReq.getFileName(), addReq.getFilePath()); + + // 압축 해제한 폴더 하위에 train,val,test 폴더 모두 존재하는지 확인 + validateClsTrainValTestDirs(addReq.getFilePath() + addReq.getFileName().replace(".zip", "")); + + // 해제한 폴더 읽어서 데이터 저장 + datasetUid = + this.processAndInsertDataset( + addReq.getFilePath() + addReq.getFileName().replace(".zip", ""), addReq); + log.info("######testttest#### datasetUid: {}", datasetUid); + + } catch (IOException e) { + log.error(e.getMessage()); + return new ResponseObj(ApiResponseCode.INTERNAL_SERVER_ERROR, e.getMessage()); + } + + datasetClsCoreService.updateDatasetUploadStatus(datasetUid, LearnDataRegister.COMPLETED); + return new ResponseObj(ApiResponseCode.OK, "업로드 성공하였습니다."); + } + + private List> getUnzipDatasetFiles(String unzipRootPath, String subDir) { + + Path root = Paths.get(unzipRootPath).resolve(subDir); + Map> grouped = new HashMap<>(); + + for (String dirName : LABEL_DIRS) { + Path dir = root.resolve(dirName); + + if (!Files.isDirectory(dir)) { + throw new CustomApiException( + ApiResponseCode.NOT_FOUND_DATA.getId(), + HttpStatus.CONFLICT, + "폴더가 존재하지 않습니다. 업로드 된 파일을 확인하세요. : " + dir); + } + + try (Stream stream = Files.list(dir)) { + stream + .filter(Files::isRegularFile) + .forEach( + path -> { + String fileName = path.getFileName().toString(); + String baseName = getBaseName(fileName); + + // baseName 기준 Map 생성 + Map data = + grouped.computeIfAbsent(baseName, k -> new HashMap<>()); + + // 공통 메타 + data.put("baseName", baseName); + + // 폴더별 처리 + if ("label-json".equals(dirName)) { + // json 파일이면 파싱 + try { + data.put("label-json", readJson(path)); + } catch (Exception e) { + log.error("파일 JSON 읽기 실패. skip. file={}", path, e); + return; // skip + } + data.put("geojson_path", path.toAbsolutePath().toString()); + } else { + data.put(dirName, path.toAbsolutePath().toString()); + } + }); + + } catch (IOException e) { + throw new RuntimeException(e); + } + } + + return new ArrayList<>(grouped.values()); + } + + @Transactional + public Long insertTrainTestData( + Map map, AddReq addReq, int idx, Long datasetUid, String subDir) { + ObjectMapper mapper = new ObjectMapper(); + String comparePath = (String) map.get("input1"); + String targetPath = (String) map.get("input2"); + String labelPath = (String) map.get("label"); + String geojsonPath = (String) map.get("geojson_path"); + Object labelJson = map.get("label-json"); + JsonNode json; + + if (labelJson instanceof JsonNode jn) { + json = jn; + } else { + try { + json = mapper.readTree(labelJson.toString()); + } catch (IOException e) { + throw new RuntimeException("label_json parse error", e); + } + } + + String fileName = Paths.get(comparePath).getFileName().toString(); + String[] fileNameStr = fileName.split("_"); + String compareYyyy = fileNameStr[1]; + String targetYyyy = fileNameStr[2]; + String mapSheetNum = fileNameStr[3]; + + if (idx == 0 && subDir.equals("train")) { + String title = compareYyyy + "-" + targetYyyy; // 제목 : 비교년도-기준년도 + String dataType = LearnDataType.PRODUCTION.getId(); // 만들어 넣는 건 다 제작 + Long stage = + datasetClsCoreService.getDatasetMaxStage( + Integer.parseInt(compareYyyy), Integer.parseInt(targetYyyy)) + + 1; + String uid = addReq.getFileName().replace(".zip", ""); + + DatasetMngRegDto mngRegDto = + DatasetMngRegDto.builder() + .uid(uid) + .dataType(dataType) + .compareYyyy(Integer.parseInt(compareYyyy)) + .targetYyyy(Integer.parseInt(targetYyyy)) + .title(title) + .memo(addReq.getMemo()) + .roundNo(stage) + .totalSize(addReq.getFileSize()) + .datasetPath(addReq.getFilePath() + uid) + .build(); + + datasetUid = datasetClsCoreService.insertDatasetMngData(mngRegDto); // tb_dataset 에 insert + } + + // datasetUid 로 obj 도 등록하기 + // Json 갯수만큼 for문 돌려서 insert 해야 함, features에 빈값 + if (json != null && json.path("features") != null && !json.path("features").isEmpty()) { + + for (JsonNode feature : json.path("features")) { + JsonNode prop = feature.path("properties"); + String compareClassCd = prop.path("before").asText(null); + String targetClassCd = prop.path("after").asText(null); + + // 한 개씩 자른 geojson을 FeatureCollection 으로 만들어서 넣기 + ObjectNode root = mapper.createObjectNode(); + root.put("type", "FeatureCollection"); + ArrayNode features = mapper.createArrayNode(); + features.add(feature); + root.set("features", features); + + DatasetObjRegDto objRegDto = + DatasetObjRegDto.builder() + .datasetUid(datasetUid) + .compareYyyy(Integer.parseInt(compareYyyy)) + .compareClassCd(compareClassCd) + .targetYyyy(Integer.parseInt(targetYyyy)) + .targetClassCd(targetClassCd) + .comparePath(comparePath) + .targetPath(targetPath) + .labelPath(labelPath) + .mapSheetNum(mapSheetNum) + .geojson(root) + .geojsonPath(geojsonPath) + .fileName(fileName) + .build(); + + if (subDir.equals("train")) { + datasetClsCoreService.insertDatasetObj(objRegDto); + } else if (subDir.equals("val")) { + datasetClsCoreService.insertDatasetValObj(objRegDto); + } else { + datasetClsCoreService.insertDatasetTestObj(objRegDto); + } + } + } + + return datasetUid; + } + + /** + * CLS zip file DB화 + * + * @param unzipRootPath + * @param addReq + * @return + */ + @Transactional + public Long processAndInsertDataset(String unzipRootPath, AddReq addReq) { + Path datasetRoot = Paths.get(unzipRootPath).resolve("dataset"); + + log.info("datasetRoot: {}", datasetRoot); + Long datasetUid = null; + boolean isMasterInserted = false; // Master(Mng) 테이블에 최초 1회만 등록하기 위한 플래그 + + // 1. 순회할 상위 폴더 목록 + for (String subDir : REQUIRED_DIRS) { + Path subDirPath = datasetRoot.resolve(subDir); + if (!Files.exists(subDirPath) || !Files.isDirectory(subDirPath)) { + continue; // 해당 폴더가 없으면 다음으로 패스 + } + + try (Stream classDirs = Files.list(subDirPath)) { + // 2. 클래스명 폴더 목록 추출 (예: building, container, water 등) + List classDirList = classDirs.filter(Files::isDirectory).toList(); + + for (Path classDir : classDirList) { + // DB에 넣을 classCd (폴더명) 추출 + String classCd = classDir.getFileName().toString(); + + try (Stream files = Files.list(classDir)) { + // 3. 해당 클래스 폴더 안의 실제 파일(tif) 목록 추출 + List filePaths = + files + .filter(Files::isRegularFile) + .sorted(Comparator.comparing(Path::getFileName)) // 정렬 로직 추가 + .toList(); + + if (filePaths.isEmpty()) continue; // 폴더가 비어있으면 에러 방지를 위해 패스 + + // ★ 맨 앞의 첫 번째 파일명에서 compareYyyy 추출 및 targetYyyy 계산 + String firstFileName = filePaths.getFirst().getFileName().toString(); + String compareYyyy = firstFileName.split("_")[0]; // 예: "2021" + String targetYyyy = String.valueOf(Integer.parseInt(compareYyyy) + 1); // 예: "2022" + + for (Path file : filePaths) { + String fileName = file.getFileName().toString(); + String absolutePath = file.toAbsolutePath().toString(); + String yyyy = fileName.split("_")[0]; + + // 4. 파일명 파싱 (확장자 제거 후 Split) + // ※ 주의: 첨부하신 이미지의 파일명(2021_33607077_idx93)을 기준으로 인덱스를 재조정했습니다. + String nameWithoutExt = fileName.substring(0, fileName.lastIndexOf('.')); + String[] fileNameStr = nameWithoutExt.split("_"); + String mapSheetNum = fileNameStr.length > 1 ? fileNameStr[1] : ""; // 33607077 + + // 5. Master(Mng) 데이터 등록 (전체 순회 중 최초 1회만 실행) + if (!isMasterInserted) { + String title = compareYyyy + "-" + targetYyyy; + Long stage = + datasetClsCoreService.getDatasetMaxStage( + Integer.parseInt(compareYyyy), Integer.parseInt(targetYyyy)) + + 1; + String uid = addReq.getFileName().replace(".zip", ""); + + DatasetMngRegDto mngRegDto = + DatasetMngRegDto.builder() + .uid(uid) + .dataType(LearnDataType.PRODUCTION.getId()) + .compareYyyy(Integer.parseInt(compareYyyy)) + .targetYyyy(Integer.parseInt(targetYyyy)) + .title(title) + .memo(addReq.getMemo()) + .roundNo(stage) + .totalSize(addReq.getFileSize()) + .datasetPath(addReq.getFilePath() + uid) + .cdClsType("CLS") + .build(); + + datasetUid = datasetClsCoreService.insertDatasetMngData(mngRegDto); + isMasterInserted = true; + } + + // 6. Obj(상세) 데이터 등록 + DatasetObjDto.DatasetClsObjRegDto objRegDto = + DatasetObjDto.DatasetClsObjRegDto.builder() + .datasetUid(datasetUid) + .type(subDir) + .yyyy(Integer.parseInt(yyyy)) + .classCd(classCd) + .mapSheetNum(mapSheetNum) + .tifImgPath(absolutePath) // 실제 파일의 절대 경로 + .fileName(fileName) + .build(); + + // 7. 폴더 위치(train/val/test)에 맞는 테이블에 Insert + datasetClsCoreService.insertDatasetClsObj(objRegDto); + } + } + } + } catch (IOException e) { + throw new RuntimeException("디렉토리 읽기 중 오류가 발생했습니다: " + subDirPath, e); + } + } + + log.info("$$$$$$$$$$datasetUid: {}", datasetUid); + return datasetUid; + } + + /** + * Cls File 목록 + * + * @param unzipRootPath + * @param subDir + * @return + */ + private List getUnzipDatasetClsFiles(String unzipRootPath, String subDir) { + + Path root = Paths.get(unzipRootPath).resolve("dataset"); + Map> grouped = new HashMap<>(); + + Path dir = root.resolve(subDir); + + if (!Files.isDirectory(dir)) { + throw new CustomApiException( + ApiResponseCode.NOT_FOUND_DATA.getId(), + HttpStatus.CONFLICT, + "폴더가 존재하지 않습니다. 업로드 된 파일을 확인하세요. : " + dir); + } + + try (Stream stream = Files.list(dir)) { + return stream + .filter(Files::isRegularFile) // 파일만 필터링 (하위 폴더 제외) + .map(path -> path.toAbsolutePath().toString()) // 절대 경로 문자열로 변환 + .toList(); // List로 수집 (Java 16 미만인 경우 .collect(Collectors.toList()) 사용) + } catch (IOException e) { + throw new RuntimeException(e); + } + } + + private String getBaseName(String fileName) { + int idx = fileName.lastIndexOf('.'); + return (idx > 0) ? fileName.substring(0, idx) : fileName; + } + + private JsonNode readJson(Path jsonPath) { + try { + ObjectMapper mapper = new ObjectMapper(); + return mapper.readTree(jsonPath.toFile()); + } catch (IOException e) { + throw new RuntimeException("JSON 읽기 실패: " + jsonPath, e); + } + } + + public String getFilePathByUUIDPathType(UUID uuid, String pathType) { + return datasetClsCoreService.getFilePathByUUIDPathType(uuid, pathType); + } + + private String escape(String path) { + // 쉘 커맨드에서 안전하게 사용할 수 있도록 문자열을 작은따옴표로 감싸면서, 내부의 작은따옴표를 이스케이프 처리 + return "'" + path.replace("'", "'\"'\"'") + "'"; + } + + private static String normalizeLinuxPath(String path) { + return path.replace("\\", "/"); + } + + public ResponseEntity getFilePathByFile(String remoteFilePath) { + + try { + Path path = Paths.get(remoteFilePath); + InputStream inputStream = Files.newInputStream(path); + + InputStreamResource resource = + new InputStreamResource(inputStream) { + @Override + public long contentLength() { + return -1; // 알 수 없으면 -1 + } + }; + + String fileName = Paths.get(remoteFilePath.replace("\\", "/")).getFileName().toString(); + + return ResponseEntity.ok() + .header(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=\"" + fileName + "\"") + .contentType(MediaType.APPLICATION_OCTET_STREAM) + .body(resource); + + } catch (Exception e) { + throw new RuntimeException(e); + } + } + + /** unzipRootDir: 압축 해제된 폴더 경로 (ex: /data/xxx/myzipname) */ + public static void validateTrainValTestDirs(String unzipRootDir) { + Path root = Paths.get(unzipRootDir); + + // 루트 폴더 자체 존재 확인 + if (!Files.exists(root) || !Files.isDirectory(root)) { + throw new CustomApiException( + ApiResponseCode.NOT_FOUND_DATA.getId(), + HttpStatus.CONFLICT, + "압축 해제 폴더가 존재하지 않습니다: " + unzipRootDir); + } + + // 필요한 폴더들 존재/디렉토리 여부 확인 + List missing = + REQUIRED_DIRS.stream() + .filter(d -> !Files.isDirectory(root.resolve(d))) + .collect(Collectors.toList()); + + if (!missing.isEmpty()) { + throw new CustomApiException( + ApiResponseCode.NOT_FOUND_DATA.getId(), + HttpStatus.CONFLICT, + "데이터 폴더 구조가 올바르지 않습니다. 누락된 폴더: " + + String.join(", ", missing) + + " (필수: train, val, test)"); + } + } + + /** unzipRootDir: 압축 해제된 폴더 경로 (ex: /zip파일폴더/dataset/train,val,test) */ + public static void validateClsTrainValTestDirs(String unzipRootDir) { + Path root = Paths.get(unzipRootDir).resolve("dataset"); + + // 루트 폴더 자체 존재 확인 + if (!Files.exists(root) || !Files.isDirectory(root)) { + throw new CustomApiException( + ApiResponseCode.NOT_FOUND_DATA.getId(), + HttpStatus.CONFLICT, + "압축 해제 폴더가 존재하지 않습니다: " + root.toAbsolutePath().toString()); + } + + // 필요한 폴더들 존재/디렉토리 여부 확인 + List missing = + REQUIRED_DIRS.stream() + .filter(d -> !Files.isDirectory(root.resolve(d))) + .collect(Collectors.toList()); + + if (!missing.isEmpty()) { + throw new CustomApiException( + ApiResponseCode.NOT_FOUND_DATA.getId(), + HttpStatus.CONFLICT, + "데이터 폴더 구조가 올바르지 않습니다. 누락된 폴더: " + + String.join(", ", missing) + + " (필수: train, val, test)"); + } + } + + public static void validateDirFileCount(String unzipRootDir) { + Path root = Paths.get(unzipRootDir); + + for (String split : REQUIRED_DIRS) { + + Path splitPath = root.resolve(split); + + Map fileCountMap = new HashMap<>(); + + for (String subDir : CHECK_DIRS) { // input1, input2, label 폴더만 수행하기 + + Path subDirPath = splitPath.resolve(subDir); + + if (!Files.isDirectory(subDirPath)) { + throw new CustomApiException( + ApiResponseCode.NOT_FOUND_DATA.getId(), + HttpStatus.CONFLICT, + split + " 폴더 하위에 " + subDir + " 폴더가 존재하지 않습니다."); + } + + long count; + try (Stream files = Files.list(subDirPath)) { + count = files.filter(Files::isRegularFile).count(); + log.info("dir: " + subDirPath + ", count: " + count); + } catch (IOException e) { + throw new CustomApiException( + ApiResponseCode.NOT_FOUND_DATA.getId(), + HttpStatus.CONFLICT, + split + "/" + subDir + " 파일 개수 확인 중 오류 발생"); + } + + fileCountMap.put(subDir, count); + } + + // 모든 폴더 파일 개수가 동일한지 확인 + Set uniqueCounts = new HashSet<>(fileCountMap.values()); + + if (uniqueCounts.size() != 1) { + throw new CustomApiException( + ApiResponseCode.NOT_FOUND_DATA.getId(), + HttpStatus.CONFLICT, + split + " 데이터 파일 개수가 일치하지 않습니다. " + fileCountMap.toString()); + } + } + } + + /** + * 폴더 조회 + * + * @param srchDto 폴더 경로 + * @return 폴더 리스트 + * @throws IOException + */ + public FoldersDto getFolderAll(SrchFoldersDto srchDto) throws IOException { + + File dir = new File(srchDto.getDirPath() == null ? "/" : srchDto.getDirPath()); + + // 존재 + 디렉토리 체크 + if (!dir.exists() || !dir.isDirectory()) { + throw new CustomApiException("BAD_REQUEST", HttpStatus.BAD_REQUEST, "잘못된 경로입니다."); + } + + // 권한 없을때 + if (!dir.canRead()) { + throw new CustomApiException( + ApiResponseCode.FORBIDDEN.getId(), HttpStatus.FORBIDDEN, "디렉토리에 접근할 권한이 없습니다."); + } + + String canonicalPath = dir.getCanonicalPath(); + + File[] files = dir.listFiles(); + + if (files == null) { + return new FoldersDto(canonicalPath, 0, 0, Collections.emptyList()); + } + + List folders = new ArrayList<>(); + + int folderTotCnt = 0; + int folderErrTotCnt = 0; + + for (File f : files) { + + // 숨김 제외 + if (f.isHidden()) continue; + + if (f.isDirectory()) { + + // 폴더 개수 증가 + folderTotCnt++; + + // 폴더 유효성 여부 (기본 true, 이후 검증 로직으로 변경 가능) + boolean isValid = true; + + // 유효하지 않은 폴더 카운트 증가 + if (!isValid) folderErrTotCnt++; + + // 현재 폴더 이름 (ex: train, images 등) + String folderNm = f.getName(); + + // 부모 경로 (ex: /data/datasets) + String parentPath = f.getParent(); + + // 부모 폴더 이름 (ex: datasets) + String parentFolderNm = new File(parentPath).getName(); + + // 전체 절대 경로 (ex: /data/datasets/train) + String fullPath = f.getAbsolutePath(); + + // 폴더 깊이 (경로 기준 depth) + // ex: /a/b/c → depth = 3 + int depth = f.toPath().getNameCount(); + + // 하위 폴더 개수 + long childCnt = FIleChecker.getChildFolderCount(f); + + // 마지막 수정 시간 (문자열 포맷) + String lastModified = FIleChecker.getLastModified(f); + + // Folder DTO 생성 및 리스트에 추가 + folders.add( + new FIleChecker.Folder( + folderNm, // 폴더명 + parentFolderNm, // 부모 폴더명 + parentPath, // 부모 경로 + fullPath, // 전체 경로 + depth, // 깊이 + childCnt, // 하위 폴더 개수 + lastModified, // 수정일시 + isValid // 유효성 여부 + )); + } + } + + // 폴더 정렬 + folders.sort( + Comparator.comparing(FIleChecker.Folder::getFolderNm, String.CASE_INSENSITIVE_ORDER)); + + return new FoldersDto(canonicalPath, folderTotCnt, folderErrTotCnt, folders); + } + + /** + * 납품 데이터 등록 + * + * @param req 폴더경로, 메모 + * @return 성공/실패 여부0 + */ + public void insertDeliveriesDataset(AddDeliveriesReq req, Long datasetUid) { + + long startTime = System.currentTimeMillis(); + + // 처리 + processType(req.getFilePath(), datasetUid, "train"); + processType(req.getFilePath(), datasetUid, "val"); + processType(req.getFilePath(), datasetUid, "test"); + + log.info("========== 전체 완료. 총 소요시간: {} ms ==========", System.currentTimeMillis() - startTime); + } + + /** + * 납품 데이터 등록 처리 + * + * @param path + * @param datasetUid + * @param type + */ + private void processType(String path, Long datasetUid, String type) { + long start = System.currentTimeMillis(); + + log.info("[납품 데이터 등록 처리][{}] 시작", type.toUpperCase()); + + List> list = getUnzipDatasetFiles(path, type); + + int batchSize = 1000; + int total = list.size(); + int processed = 0; + + for (int i = 0; i < total; i += batchSize) { + + List> batch = list.subList(i, Math.min(i + batchSize, total)); + + try { + log.info("[납품 데이터 등록 처리][{}] batch 시작: {} ~ {}", type, i, i + batch.size()); + + datasetBatchService.saveBatch(batch, datasetUid, type); + + processed += batch.size(); + } catch (Exception e) { + log.error("batch 실패 row 데이터: {}", batch); + log.error( + "[납품 데이터 등록 처리][{}] batch 실패. range: {} ~ {}, datasetUid={}", + type, + i, + i + batch.size(), + datasetUid, + e); + } + } + + log.info( + "[납품 데이터 등록 처리][{}] 완료. 총 {}건, 소요시간: {} ms", + type, + total, + System.currentTimeMillis() - start); + } + + public void validateExistsUidChk(String filePath) { + Path selectedPath = Paths.get(filePath); + String uid = selectedPath.getFileName().toString(); + + // 같은 uid 로 등록한 파일이 있는지 확인 + Long existsCnt = datasetClsCoreService.findDatasetByUidExistsCnt(uid); + if (existsCnt > 0) { + throw new CustomApiException( + ApiResponseCode.DUPLICATE_DATA.getId(), + HttpStatus.CONFLICT, + "이미 등록된 회차 데이터 파일입니다. 확인 부탁드립니다."); + } + } +} diff --git a/src/main/java/com/kamco/cd/training/postgres/core/DatasetClsCoreService.java b/src/main/java/com/kamco/cd/training/postgres/core/DatasetClsCoreService.java new file mode 100644 index 0000000..c2d4c08 --- /dev/null +++ b/src/main/java/com/kamco/cd/training/postgres/core/DatasetClsCoreService.java @@ -0,0 +1,278 @@ +package com.kamco.cd.training.postgres.core; + +import com.kamco.cd.training.common.enums.LearnDataRegister; +import com.kamco.cd.training.common.enums.LearnDataType; +import com.kamco.cd.training.common.exception.NotFoundException; +import com.kamco.cd.training.common.service.BaseCoreService; +import com.kamco.cd.training.dataset.dto.DatasetClsObjDto; +import com.kamco.cd.training.dataset.dto.DatasetDto; +import com.kamco.cd.training.dataset.dto.DatasetDto.DatasetMngRegDto; +import com.kamco.cd.training.dataset.dto.DatasetObjDto; +import com.kamco.cd.training.dataset.dto.DatasetObjDto.DatasetClass; +import com.kamco.cd.training.dataset.dto.DatasetObjDto.DatasetObjRegDto; +import com.kamco.cd.training.postgres.entity.DatasetClsObjEntity; +import com.kamco.cd.training.postgres.entity.DatasetEntity; +import com.kamco.cd.training.postgres.entity.DatasetObjEntity; +import com.kamco.cd.training.postgres.repository.dataset.DatasetClsRepository; +import com.kamco.cd.training.postgres.repository.dataset.DatasetObjRepository; +import jakarta.transaction.Transactional; +import java.time.ZonedDateTime; +import java.util.List; +import java.util.UUID; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.StringUtils; +import org.springframework.data.domain.Page; +import org.springframework.stereotype.Service; + +@Service +@RequiredArgsConstructor +@Slf4j +public class DatasetClsCoreService + implements BaseCoreService { + + private final DatasetClsRepository datasetClsRepository; + private final DatasetObjRepository datasetObjRepository; + + /** + * 학습 데이터 삭제 + * + * @param id 데이터셋 ID + */ + @Override + public void remove(Long id) { + DatasetEntity entity = + datasetClsRepository + .findById(id) + .orElseThrow(() -> new NotFoundException("데이터셋을 찾을 수 없습니다. ID: " + id)); + entity.setDeleted(true); + datasetClsRepository.save(entity); + } + + public void remove(UUID id) { + DatasetEntity entity = + datasetClsRepository + .findByUuid(id) + .orElseThrow(() -> new NotFoundException("데이터셋을 찾을 수 없습니다. ID: " + id)); + entity.setDeleted(true); + datasetClsRepository.save(entity); + } + + @Override + public DatasetDto.Basic getOneById(Long id) { + DatasetEntity entity = + datasetClsRepository + .findById(id) + .orElseThrow(() -> new NotFoundException("데이터셋을 찾을 수 없습니다. ID: " + id)); + + if (entity.getDeleted()) { + throw new NotFoundException("삭제된 데이터셋입니다. ID: " + id); + } + + return entity.toDto(); + } + + public DatasetDto.Basic getOneByUuid(UUID id) { + DatasetEntity entity = + datasetClsRepository + .findByUuid(id) + .orElseThrow(() -> new NotFoundException("데이터셋을 찾을 수 없습니다. uuid: " + id)); + return entity.toDto(); + } + + @Override + public Page search(DatasetDto.SearchReq searchReq) { + Page entityPage = datasetClsRepository.findDatasetList(searchReq); + return entityPage.map(DatasetEntity::toDto); + } + + /** + * 학습데이터 조회 + * + * @param searchReq 검색 조건 + * @return 페이징 처리된 데이터셋 목록 + */ + public Page findDatasetList(DatasetDto.SearchReq searchReq) { + return search(searchReq); + } + + // TODO 미사용시작 + /** + * 학습데이터 등록 + * + * @param registerReq 등록 요청 데이터 + * @return 등록된 데이터셋 정보 + */ + public DatasetDto.Basic save(DatasetDto.RegisterReq registerReq) { + // 먼저 id1 필드를 임시값(0)으로 설정하여 저장 + DatasetEntity entity = new DatasetEntity(); + entity.setTitle(registerReq.getTitle()); + // entity.setYear(registerReq.getYear()); + entity.setDataType(LearnDataType.PRODUCTION.getId()); + entity.setCompareYyyy(registerReq.getCompareYear()); + entity.setTargetYyyy(registerReq.getTargetYyyy()); + entity.setRoundNo(registerReq.getRoundNo() != null ? registerReq.getRoundNo() : 1L); + entity.setMemo(registerReq.getMemo()); + entity.setStatus(LearnDataRegister.READY.getId()); + entity.setDataType("CREATE"); + entity.setTotalItems(0L); + entity.setTotalSize(0L); + entity.setItemCount(0L); + entity.setDeleted(false); + entity.setCreatedDttm(ZonedDateTime.now()); + // entity.setId1(0L); // 임시값 + + DatasetEntity savedEntity = datasetClsRepository.save(entity); + + // 저장 후 id1을 dataset_uid와 동일하게 업데이트 + // savedEntity.setId1(savedEntity.getId()); + // savedEntity = datasetClsRepository.save(savedEntity); + + return savedEntity.toDto(); + } + + // TODO 미사용 끝 + /** + * 학습 데이터 수정 + * + * @param updateReq 수정 요청 데이터 + * @return 수정된 데이터셋 정보 + */ + public void update(UUID uuid, DatasetDto.UpdateReq updateReq) { + DatasetEntity entity = + datasetClsRepository + .findByUuid(uuid) + .orElseThrow(() -> new NotFoundException("데이터셋을 찾을 수 없습니다. uuid: " + uuid)); + + if (StringUtils.isNotBlank(updateReq.getTitle())) { + entity.setTitle(updateReq.getTitle()); + } + if (StringUtils.isNotBlank(updateReq.getMemo())) { + entity.setMemo(updateReq.getMemo()); + } + + datasetClsRepository.save(entity); + } + + /** + * 학습데이터 삭제 + * + * @param uuid 삭제 요청 (데이터셋 ID 목록) + */ + public void deleteDatasets(UUID uuid) { + remove(uuid); + } + + public DatasetDto.Summary getDatasetSummary(DatasetDto.SummaryReq summaryReq) { + long totalMapSheets = 0; + long totalFileSize = 0; + + for (Long datasetId : summaryReq.getDatasetIds()) { + DatasetEntity entity = + datasetClsRepository + .findById(datasetId) + .orElseThrow(() -> new NotFoundException("데이터셋을 찾을 수 없습니다. ID: " + datasetId)); + + if (!entity.getDeleted()) { + totalMapSheets += entity.getTotalItems() != null ? entity.getTotalItems() : 0; + totalFileSize += entity.getTotalSize() != null ? entity.getTotalSize() : 0; + } + } + + double averageMapSheets = + !summaryReq.getDatasetIds().isEmpty() + ? (double) totalMapSheets / summaryReq.getDatasetIds().size() + : 0; + + return new DatasetDto.Summary( + summaryReq.getDatasetIds().size(), totalMapSheets, totalFileSize, averageMapSheets); + } + + public Page searchDatasetClsObjectList( + DatasetClsObjDto.ClsSearchReq searchReq) { + Page entityPage = + datasetClsRepository.searchDatasetClsObjectList(searchReq); + return entityPage.map(DatasetClsObjEntity::toDto); + } + + public UUID deleteDatasetObjByUuid(UUID uuid) { + DatasetObjEntity entity = + datasetObjRepository + .findByUuid(uuid) + .orElseThrow(() -> new NotFoundException("데이터셋 obj를 찾을 수 없습니다. ID: " + uuid)); + entity.setDeleted(true); + datasetObjRepository.save(entity); + return entity.getUuid(); + } + + /** + * 데이터셋 object class 조회 + * + * @param uuid dataset uuid + * @param type compare, target + * @return + */ + public List findDatasetObjClassByUuid(UUID uuid, String type) { + return datasetObjRepository.findDatasetObjClassByUuid(uuid, type); + } + + public Long getDatasetMaxStage(int compareYyyy, int targetYyyy) { + return datasetClsRepository.getDatasetMaxStage(compareYyyy, targetYyyy); + } + + @Transactional + public Long insertDatasetMngData(DatasetMngRegDto mngRegDto) { + return datasetClsRepository.insertDatasetMngData(mngRegDto); + } + + public void insertDatasetObj(DatasetObjRegDto objRegDto) { + datasetObjRepository.insertDatasetObj(objRegDto); + } + + public String getFilePathByUUIDPathType(UUID uuid, String pathType) { + return datasetObjRepository.getFilePathByUUIDPathType(uuid, pathType); + } + + public void insertDatasetTestObj(DatasetObjRegDto objRegDto) { + datasetObjRepository.insertDatasetTestObj(objRegDto); + } + + /** + * 학습데이터셋 마스터 상태 변경 + * + * @param datasetUid 학습데이터셋 마스터 id + * @param register 상태 + */ + @Transactional + public void updateDatasetUploadStatus(Long datasetUid, LearnDataRegister register) { + DatasetEntity entity = + datasetClsRepository + .findById(datasetUid) + .orElseThrow(() -> new NotFoundException("데이터셋을 찾을 수 없습니다. ID: " + datasetUid)); + + entity.setStatus(register.getId()); + } + + public void insertDatasetValObj(DatasetObjRegDto objRegDto) { + datasetObjRepository.insertDatasetValObj(objRegDto); + } + + public Long findDatasetByUidExistsCnt(String uid) { + return datasetClsRepository.findDatasetByUidExistsCnt(uid); + } + + /** + * 데이터셋 등록 실패시 Obj 데이터 정리 + * + * @param datasetUid 모델 마스터 id + */ + @Transactional + public void deleteAllDatasetObj(Long datasetUid) { + int cnt = datasetObjRepository.deleteAllDatasetObj(datasetUid); + log.info("datasetUid={} 데이터셋 실패 - 전체 삭제 완료. 총 {}건", datasetUid, cnt); + } + + public void insertDatasetClsObj(DatasetObjDto.DatasetClsObjRegDto objRegDto) { + datasetClsRepository.insertDatasetClsObj(objRegDto); + } +} diff --git a/src/main/java/com/kamco/cd/training/postgres/entity/DatasetClsObjEntity.java b/src/main/java/com/kamco/cd/training/postgres/entity/DatasetClsObjEntity.java new file mode 100644 index 0000000..24e80e7 --- /dev/null +++ b/src/main/java/com/kamco/cd/training/postgres/entity/DatasetClsObjEntity.java @@ -0,0 +1,77 @@ +package com.kamco.cd.training.postgres.entity; + +import com.kamco.cd.training.dataset.dto.DatasetClsObjDto.Basic; +import jakarta.persistence.*; +import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.Size; +import java.time.ZonedDateTime; +import java.util.UUID; +import lombok.Getter; +import lombok.Setter; +import org.hibernate.annotations.ColumnDefault; + +@Getter +@Setter +@Entity +@Table(name = "tb_dataset_cls_obj") +public class DatasetClsObjEntity { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(name = "obj_id", nullable = false) + private Long objId; + + @Column(name = "uuid") + private UUID uuid; + + @NotNull + @Column(name = "dataset_uid", nullable = false) + private Long datasetUid; + + @Column(name = "type") + private String type; + + @Column(name = "yyyy") + private Integer yyyy; + + @Size(max = 255) + @Column(name = "class_cd") + private String classCd; + + @Size(max = 255) + @Column(name = "map_sheet_num") + private String mapSheetNum; + + @Column(name = "tif_img_path") + private String tifImgPath; + + @Column(name = "file_name") + private String fileName; + + @ColumnDefault("now()") + @Column(name = "created_dttm") + private ZonedDateTime createdDttm; + + @Column(name = "created_uid") + private Long createdUid; + + @ColumnDefault("false") + @Column(name = "deleted") + private Boolean deleted; + + public Basic toDto() { + return new Basic( + this.objId, + this.uuid, + this.datasetUid, + this.type, + this.yyyy, + this.classCd, + this.mapSheetNum, + this.tifImgPath, + this.fileName, + this.createdDttm, + this.createdUid, + this.deleted); + } +} diff --git a/src/main/java/com/kamco/cd/training/postgres/entity/DatasetEntity.java b/src/main/java/com/kamco/cd/training/postgres/entity/DatasetEntity.java index 6876e39..1b7b6df 100644 --- a/src/main/java/com/kamco/cd/training/postgres/entity/DatasetEntity.java +++ b/src/main/java/com/kamco/cd/training/postgres/entity/DatasetEntity.java @@ -127,6 +127,9 @@ public class DatasetEntity { @Column(name = "uid") private String uid; + @Column(name = "cd_cls_type") + private String cdClsType; + public DatasetDto.Basic toDto() { return new DatasetDto.Basic( this.id, @@ -140,6 +143,7 @@ public class DatasetEntity { this.createdDttm, this.status, this.deleted, - this.dataType); + this.dataType, + this.cdClsType); } } diff --git a/src/main/java/com/kamco/cd/training/postgres/repository/dataset/DatasetClsRepository.java b/src/main/java/com/kamco/cd/training/postgres/repository/dataset/DatasetClsRepository.java new file mode 100644 index 0000000..9b1ab74 --- /dev/null +++ b/src/main/java/com/kamco/cd/training/postgres/repository/dataset/DatasetClsRepository.java @@ -0,0 +1,7 @@ +package com.kamco.cd.training.postgres.repository.dataset; + +import com.kamco.cd.training.postgres.entity.DatasetEntity; +import org.springframework.data.jpa.repository.JpaRepository; + +public interface DatasetClsRepository + extends JpaRepository, DatasetClsRepositoryCustom {} diff --git a/src/main/java/com/kamco/cd/training/postgres/repository/dataset/DatasetClsRepositoryCustom.java b/src/main/java/com/kamco/cd/training/postgres/repository/dataset/DatasetClsRepositoryCustom.java new file mode 100644 index 0000000..5deb299 --- /dev/null +++ b/src/main/java/com/kamco/cd/training/postgres/repository/dataset/DatasetClsRepositoryCustom.java @@ -0,0 +1,46 @@ +package com.kamco.cd.training.postgres.repository.dataset; + +import com.kamco.cd.training.dataset.dto.DatasetClsObjDto; +import com.kamco.cd.training.dataset.dto.DatasetDto; +import com.kamco.cd.training.dataset.dto.DatasetDto.DatasetMngRegDto; +import com.kamco.cd.training.dataset.dto.DatasetDto.DatasetReq; +import com.kamco.cd.training.dataset.dto.DatasetDto.SelectDataSet; +import com.kamco.cd.training.dataset.dto.DatasetDto.SelectTransferDataSet; +import com.kamco.cd.training.dataset.dto.DatasetObjDto; +import com.kamco.cd.training.postgres.entity.DatasetClsObjEntity; +import com.kamco.cd.training.postgres.entity.DatasetEntity; +import java.util.List; +import java.util.Optional; +import java.util.UUID; +import org.springframework.data.domain.Page; + +public interface DatasetClsRepositoryCustom { + Page findDatasetList(DatasetDto.SearchReq searchReq); + + Optional findByUuid(UUID id); + + List getDatasetSelectG1List(DatasetReq req); + + // TODO 미사용시작 + public List getDatasetTransferSelectG1List(Long modelId); + + public List getDatasetTransferSelectG2G3List(Long modelId, String modelNo); + + // TODO 미사용 끝 + + List getDatasetSelectG2G3List(DatasetReq req); + + List getDatasetSelectG4List(DatasetReq req); + + Long getDatasetMaxStage(int compareYyyy, int targetYyyy); + + Long insertDatasetMngData(DatasetMngRegDto mngRegDto); + + List findDatasetUid(List datasetIds); + + Long findDatasetByUidExistsCnt(String uid); + + Page searchDatasetClsObjectList(DatasetClsObjDto.ClsSearchReq searchReq); + + void insertDatasetClsObj(DatasetObjDto.DatasetClsObjRegDto objRegDto); +} diff --git a/src/main/java/com/kamco/cd/training/postgres/repository/dataset/DatasetClsRepositoryImpl.java b/src/main/java/com/kamco/cd/training/postgres/repository/dataset/DatasetClsRepositoryImpl.java new file mode 100644 index 0000000..7a5df7c --- /dev/null +++ b/src/main/java/com/kamco/cd/training/postgres/repository/dataset/DatasetClsRepositoryImpl.java @@ -0,0 +1,659 @@ +package com.kamco.cd.training.postgres.repository.dataset; + +import static com.kamco.cd.training.postgres.entity.QDatasetClsObjEntity.datasetClsObjEntity; +import static com.kamco.cd.training.postgres.entity.QDatasetEntity.datasetEntity; +import static com.kamco.cd.training.postgres.entity.QDatasetObjEntity.datasetObjEntity; +import static com.kamco.cd.training.postgres.entity.QModelDatasetMappEntity.modelDatasetMappEntity; +import static com.kamco.cd.training.postgres.entity.QModelMasterEntity.modelMasterEntity; + +import com.kamco.cd.training.common.enums.DetectionClassification; +import com.kamco.cd.training.common.enums.ModelType; +import com.kamco.cd.training.dataset.dto.DatasetClsObjDto; +import com.kamco.cd.training.dataset.dto.DatasetDto.*; +import com.kamco.cd.training.dataset.dto.DatasetObjDto; +import com.kamco.cd.training.postgres.entity.*; +import com.querydsl.core.BooleanBuilder; +import com.querydsl.core.types.Projections; +import com.querydsl.core.types.dsl.CaseBuilder; +import com.querydsl.core.types.dsl.Expressions; +import com.querydsl.core.types.dsl.NumberExpression; +import com.querydsl.jpa.impl.JPAQueryFactory; +import jakarta.persistence.EntityManager; +import jakarta.persistence.EntityNotFoundException; +import jakarta.persistence.PersistenceContext; +import java.util.List; +import java.util.Objects; +import java.util.Optional; +import java.util.UUID; +import lombok.RequiredArgsConstructor; +import org.apache.commons.lang3.StringUtils; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageImpl; +import org.springframework.data.domain.Pageable; +import org.springframework.stereotype.Repository; + +@Repository +@RequiredArgsConstructor +public class DatasetClsRepositoryImpl implements DatasetClsRepositoryCustom { + + private final JPAQueryFactory queryFactory; + private final QDatasetEntity dataset = QDatasetEntity.datasetEntity; + + @PersistenceContext private final EntityManager em; + + /** + * 데이터셋 목록 조회 + * + * @param searchReq 검색 조건 + * @return 페이징 처리된 데이터셋 Entity 목록 + */ + @Override + public Page findDatasetList(SearchReq searchReq) { + Pageable pageable = searchReq.toPageable(); + BooleanBuilder builder = new BooleanBuilder(); + + // 제목 + if (StringUtils.isNotBlank(searchReq.getTitle())) { + String contains = "%" + searchReq.getTitle() + "%"; + builder.and(dataset.title.likeIgnoreCase(contains)); + } + + // 구분 + if (StringUtils.isNotBlank(searchReq.getDataType())) { + builder.and(dataset.dataType.eq(searchReq.getDataType())); + } + + // Entity 직접 조회 (Projections 사용 지양) + List content = + queryFactory + .selectFrom(dataset) + .where(builder.and(dataset.deleted.isFalse()).and(dataset.cdClsType.eq("CLS"))) + .offset(pageable.getOffset()) + .limit(pageable.getPageSize()) + .orderBy(dataset.createdDttm.desc()) + .fetch(); + + // Count 쿼리 별도 실행 (null safe handling) + long total = + Optional.ofNullable( + queryFactory + .select(dataset.count()) + .from(dataset) + .where(builder.and(dataset.deleted.isFalse()).and(dataset.cdClsType.eq("CLS"))) + .fetchOne()) + .orElse(0L); + + return new PageImpl<>(content, pageable, total); + } + + @Override + public Optional findByUuid(UUID id) { + return Optional.ofNullable( + queryFactory + .select(dataset) + .from(dataset) + .where(dataset.uuid.eq(id), dataset.deleted.isFalse()) + .fetchOne()); + } + + @Override + public List getDatasetSelectG1List(DatasetReq req) { + + BooleanBuilder builder = new BooleanBuilder(); + + builder.and(dataset.deleted.isFalse()); + + if (StringUtils.isNotBlank(req.getDataType()) && !"CURRENT".equals(req.getDataType())) { + builder.and(dataset.dataType.eq(req.getDataType())); + } + + if (req.getIds() != null) { + builder.and(dataset.id.in(req.getIds())); + } + + return queryFactory + .select( + Projections.constructor( + SelectDataSet.class, + Expressions.constant(req.getModelNo()), + dataset.id, + dataset.uuid, + dataset.dataType, + dataset.title, + dataset.roundNo, + dataset.compareYyyy, + dataset.targetYyyy, + dataset.memo, + new CaseBuilder() + .when( + datasetObjEntity.targetClassCd.eq(DetectionClassification.BUILDING.getId())) + .then(1L) + .otherwise(0L) + .sum(), + new CaseBuilder() + .when( + datasetObjEntity.targetClassCd.eq( + DetectionClassification.CONTAINER.getId())) + .then(1L) + .otherwise(0L) + .sum())) + .from(dataset) + .leftJoin(datasetObjEntity) + .on(dataset.id.eq(datasetObjEntity.datasetUid)) + .where(builder) // datasetObjEntity.targetClassCd.in("building", "container").and( + .groupBy( + dataset.id, + dataset.uuid, + dataset.dataType, + dataset.title, + dataset.roundNo, + dataset.memo) + .orderBy(dataset.createdDttm.desc()) + .fetch(); + } + + // TODO 미사용시작 + @Override + public List getDatasetTransferSelectG1List(Long modelId) { + + QModelMasterEntity beforeMaster = new QModelMasterEntity("beforeMaster"); + QModelDatasetMappEntity beforeMapp = new QModelDatasetMappEntity("beforeMapp"); + QDatasetEntity beforeDataset = new QDatasetEntity("beforeDataset"); + QDatasetObjEntity beforeObj = new QDatasetObjEntity("beforeObj"); + + return queryFactory + .select( + Projections.constructor( + SelectTransferDataSet.class, + + // ===== 현재 ===== + modelMasterEntity.modelNo, + dataset.id, + dataset.uuid, + dataset.dataType, + dataset.title, + dataset.roundNo, + dataset.compareYyyy, + dataset.targetYyyy, + dataset.memo, + new CaseBuilder() + .when(datasetObjEntity.targetClassCd.eq("building")) + .then(1) + .otherwise(0) + .sum(), + new CaseBuilder() + .when(datasetObjEntity.targetClassCd.eq("container")) + .then(1) + .otherwise(0) + .sum(), + + // ===== before (join으로) ===== + beforeMaster.modelNo, + beforeDataset.id, + beforeDataset.uuid, + beforeDataset.dataType, + beforeDataset.title, + beforeDataset.roundNo, + beforeDataset.compareYyyy, + beforeDataset.targetYyyy, + beforeDataset.memo, + new CaseBuilder() + .when(beforeObj.targetClassCd.eq("building")) + .then(1) + .otherwise(0) + .sum(), + new CaseBuilder() + .when(beforeObj.targetClassCd.eq("container")) + .then(1) + .otherwise(0) + .sum())) + .from(modelMasterEntity) + + // ===== 현재 dataset join ===== + .leftJoin(modelDatasetMappEntity) + .on(modelDatasetMappEntity.modelUid.eq(modelMasterEntity.id)) + .leftJoin(dataset) + .on(modelDatasetMappEntity.datasetUid.eq(dataset.id)) + .leftJoin(datasetObjEntity) + .on(dataset.id.eq(datasetObjEntity.datasetUid)) + + // ===== before 모델 join ===== + .leftJoin(beforeMaster) + .on(beforeMaster.id.eq(modelMasterEntity.beforeModelId)) + .leftJoin(beforeMapp) + .on(beforeMapp.modelUid.eq(beforeMaster.id)) + .leftJoin(beforeDataset) + .on(beforeMapp.datasetUid.eq(beforeDataset.id)) + .leftJoin(beforeObj) + .on(beforeDataset.id.eq(beforeObj.datasetUid)) + .where(modelMasterEntity.id.eq(modelId)) + .groupBy( + modelMasterEntity.modelNo, + dataset.id, + dataset.uuid, + dataset.dataType, + dataset.title, + dataset.roundNo, + dataset.compareYyyy, + dataset.targetYyyy, + dataset.memo, + beforeMaster.modelNo, + beforeDataset.id, + beforeDataset.uuid, + beforeDataset.dataType, + beforeDataset.title, + beforeDataset.roundNo, + beforeDataset.compareYyyy, + beforeDataset.targetYyyy, + beforeDataset.memo) + .orderBy(dataset.createdDttm.desc()) + .fetch(); + } + + // TODO 미사용 끝 + + @Override + public List getDatasetSelectG2G3List(DatasetReq req) { + + String building = DetectionClassification.BUILDING.getId(); + String container = DetectionClassification.CONTAINER.getId(); + String waste = DetectionClassification.WASTE.getId(); + String solar = DetectionClassification.SOLAR.getId(); + + BooleanBuilder builder = new BooleanBuilder(); + builder.and(dataset.deleted.isFalse()); + + NumberExpression selectedCnt = null; + // G2 + NumberExpression wasteCnt = + datasetObjEntity + .targetClassCd + .when(DetectionClassification.WASTE.getId()) + .then(1L) + .otherwise(0L) + .sum(); + + // G3 (G1, G2, G4 제외) + NumberExpression elseCnt = + new CaseBuilder() + .when(datasetObjEntity.targetClassCd.notIn(building, container, waste, solar)) + .then(1L) + .otherwise(0L) + .sum(); + + if (req.getModelNo().equals(ModelType.G2.getId())) { + selectedCnt = wasteCnt; + } else { + selectedCnt = elseCnt; + } + + if (StringUtils.isNotBlank(req.getDataType())) { + if (!"CURRENT".equals(req.getDataType())) { + builder.and(dataset.dataType.eq(req.getDataType())); + } + } + + if (req.getIds() != null) { + builder.and(dataset.id.in(req.getIds())); + } + + return queryFactory + .select( + Projections.constructor( + SelectDataSet.class, + Expressions.constant(req.getModelNo()), + dataset.id, + dataset.uuid, + dataset.dataType, + dataset.title, + dataset.roundNo, + dataset.compareYyyy, + dataset.targetYyyy, + dataset.memo, + selectedCnt.as("cnt"))) + .from(dataset) + .leftJoin(datasetObjEntity) + .on(dataset.id.eq(datasetObjEntity.datasetUid)) + .where(builder) + .groupBy( + dataset.id, + dataset.uuid, + dataset.dataType, + dataset.title, + dataset.roundNo, + dataset.memo) + .orderBy(dataset.createdDttm.desc()) + .fetch(); + } + + // TODO 미사용시작 + @Override + public List getDatasetTransferSelectG2G3List( + Long modelId, String modelNo) { + + // before join용 + QModelMasterEntity beforeMaster = new QModelMasterEntity("beforeMaster"); + QModelDatasetMappEntity beforeMapp = new QModelDatasetMappEntity("beforeMapp"); + QDatasetEntity beforeDataset = new QDatasetEntity("beforeDataset"); + QDatasetObjEntity beforeObj = new QDatasetObjEntity("beforeObj"); + + BooleanBuilder builder = new BooleanBuilder(); + + NumberExpression wasteCnt = + datasetObjEntity.targetClassCd.when("waste").then(1L).otherwise(0L).sum(); + + NumberExpression elseCnt = + new CaseBuilder() + .when(datasetObjEntity.targetClassCd.notIn("building", "container", "waste")) + .then(1L) + .otherwise(0L) + .sum(); + + NumberExpression selectedCnt = ModelType.G2.getId().equals(modelNo) ? wasteCnt : elseCnt; + + // before도 동일 로직으로 cnt 계산 + NumberExpression beforeWasteCnt = + beforeObj.targetClassCd.when("waste").then(1L).otherwise(0L).sum(); + + NumberExpression beforeElseCnt = + new CaseBuilder() + .when(beforeObj.targetClassCd.notIn("building", "container", "waste")) + .then(1L) + .otherwise(0L) + .sum(); + + NumberExpression beforeSelectedCnt = + ModelType.G2.getId().equals(modelNo) ? beforeWasteCnt : beforeElseCnt; + + return queryFactory + .select( + Projections.constructor( + SelectTransferDataSet.class, + + // ===== 현재 ===== + modelMasterEntity.modelNo, // modelNo 파라미터 사용 (req.getModelNo() 제거) + dataset.id, + dataset.uuid, + dataset.dataType, + dataset.title, + dataset.roundNo, + dataset.compareYyyy, + dataset.targetYyyy, + dataset.memo, + selectedCnt, // classCount 자리에 들어가는 cnt (Long) + + // ===== before ===== + beforeMaster.modelNo, + beforeDataset.id, + beforeDataset.uuid, + beforeDataset.dataType, + beforeDataset.title, + beforeDataset.roundNo, + beforeDataset.compareYyyy, + beforeDataset.targetYyyy, + beforeDataset.memo, + beforeSelectedCnt)) + .from(modelMasterEntity) + + // ===== 현재 dataset ===== + .leftJoin(modelDatasetMappEntity) + .on(modelDatasetMappEntity.modelUid.eq(modelMasterEntity.id)) + .leftJoin(dataset) + .on(modelDatasetMappEntity.datasetUid.eq(dataset.id)) + .leftJoin(datasetObjEntity) + .on(dataset.id.eq(datasetObjEntity.datasetUid)) + + // ===== before dataset ===== + .leftJoin(beforeMaster) + .on(beforeMaster.id.eq(modelMasterEntity.beforeModelId)) + .leftJoin(beforeMapp) + .on(beforeMapp.modelUid.eq(beforeMaster.id)) + .leftJoin(beforeDataset) + .on(beforeMapp.datasetUid.eq(beforeDataset.id)) + .leftJoin(beforeObj) + .on(beforeDataset.id.eq(beforeObj.datasetUid)) + .where(modelMasterEntity.id.eq(modelId).and(builder)) + + // sum() 때문에 groupBy 필요 + .groupBy( + dataset.id, + dataset.uuid, + dataset.dataType, + dataset.title, + dataset.roundNo, + dataset.compareYyyy, + dataset.targetYyyy, + dataset.memo, + beforeMaster.modelNo, + beforeDataset.id, + beforeDataset.uuid, + beforeDataset.dataType, + beforeDataset.title, + beforeDataset.roundNo, + beforeDataset.compareYyyy, + beforeDataset.targetYyyy, + beforeDataset.memo) + .orderBy(dataset.createdDttm.desc()) + .fetch(); + } + + // TODO 미사용 끝 + + @Override + public Long getDatasetMaxStage(int compareYyyy, int targetYyyy) { + return queryFactory + .select(dataset.roundNo.max().coalesce(0L)) + .from(dataset) + .where(dataset.compareYyyy.eq(compareYyyy), dataset.targetYyyy.eq(targetYyyy)) + .fetchOne(); + } + + @Override + public Long insertDatasetMngData(DatasetMngRegDto mngRegDto) { + + UUID uuid = UUID.randomUUID(); + // 1. 기본 생성자로 빈 엔티티 객체 생성 + // DatasetEntity datasetEntity = new DatasetEntity(); + // + // // 2. Setter를 이용해 데이터 채우기 + // datasetEntity.setUid(mngRegDto.getUid()); + // datasetEntity.setDataType(mngRegDto.getDataType()); + // datasetEntity.setCompareYyyy(mngRegDto.getCompareYyyy()); + // datasetEntity.setTargetYyyy(mngRegDto.getTargetYyyy()); + // datasetEntity.setRoundNo(mngRegDto.getRoundNo()); + // datasetEntity.setTotalSize(mngRegDto.getTotalSize()); + // datasetEntity.setTitle(mngRegDto.getTitle()); + // datasetEntity.setMemo(mngRegDto.getMemo()); + // datasetEntity.setDatasetPath(mngRegDto.getDatasetPath()); + // datasetEntity.setCdClsType(mngRegDto.getCdClsType()); + // + // datasetEntity.setUuid(UUID.randomUUID()); + // + // + // // 2. JPA 영속성 컨텍스트에 저장 (이 시점에 DB에 들어가거나 ID가 생성됨) + // em.persist(datasetEntity); + + queryFactory + .insert(dataset) + .columns( + dataset.uid, + dataset.uuid, + dataset.dataType, + dataset.compareYyyy, + dataset.targetYyyy, + dataset.roundNo, + dataset.totalSize, + dataset.title, + dataset.memo, + dataset.datasetPath, + dataset.cdClsType) + .values( + mngRegDto.getUid(), + uuid, + mngRegDto.getDataType(), + mngRegDto.getCompareYyyy(), + mngRegDto.getTargetYyyy(), + mngRegDto.getRoundNo(), + mngRegDto.getTotalSize(), + mngRegDto.getTitle(), + mngRegDto.getMemo(), + mngRegDto.getDatasetPath(), + mngRegDto.getCdClsType()) + .execute(); + + // 3. uid 조회 필요 없이, 생성된 Key를 안전하게 바로 리턴 + // return datasetEntity.getId(); + return queryFactory.select(dataset.id).from(dataset).where(dataset.uuid.eq(uuid)).fetchOne(); + } + + @Override + public List findDatasetUid(List datasetIds) { + return queryFactory.select(dataset.uid).from(dataset).where(dataset.id.in(datasetIds)).fetch(); + } + + @Override + public Long findDatasetByUidExistsCnt(String uid) { + return queryFactory + .select(dataset.id.count()) + .from(dataset) + .where(dataset.uid.eq(uid), dataset.deleted.isFalse()) + .fetchOne(); + } + + @Override + public List getDatasetSelectG4List(DatasetReq req) { + BooleanBuilder builder = new BooleanBuilder(); + + builder.and(dataset.deleted.isFalse()); + + if (StringUtils.isNotBlank(req.getDataType()) && !"CURRENT".equals(req.getDataType())) { + builder.and(dataset.dataType.eq(req.getDataType())); + } + + if (req.getIds() != null) { + builder.and(dataset.id.in(req.getIds())); + } + + return queryFactory + .select( + Projections.constructor( + SelectDataSet.class, + Expressions.constant(req.getModelNo()), + dataset.id, + dataset.uuid, + dataset.dataType, + dataset.title, + dataset.roundNo, + dataset.compareYyyy, + dataset.targetYyyy, + dataset.memo, + new CaseBuilder() + .when(datasetObjEntity.targetClassCd.eq(DetectionClassification.SOLAR.getId())) + .then(1L) + .otherwise(0L) + .sum())) + .from(dataset) + .leftJoin(datasetObjEntity) + .on(dataset.id.eq(datasetObjEntity.datasetUid)) + .where(builder) + .groupBy( + dataset.id, + dataset.uuid, + dataset.dataType, + dataset.title, + dataset.roundNo, + dataset.memo) + .orderBy(dataset.createdDttm.desc()) + .fetch(); + } + + @Override + public Page searchDatasetClsObjectList( + DatasetClsObjDto.ClsSearchReq searchReq) { + { + Pageable pageable = searchReq.toPageable(); + BooleanBuilder builder = new BooleanBuilder(); + + if (StringUtils.isNotBlank(searchReq.getClassCd())) { + builder.and(datasetClsObjEntity.classCd.eq(searchReq.getClassCd())); + } + if (searchReq.getYyyy() != null) { + builder.and(datasetClsObjEntity.yyyy.eq(searchReq.getYyyy())); + } + if (StringUtils.isNotBlank(searchReq.getMapSheetNum())) { + builder.and(datasetClsObjEntity.mapSheetNum.eq(searchReq.getMapSheetNum())); + } + + DatasetEntity entity = + queryFactory + .selectFrom(datasetEntity) + .where(datasetEntity.uuid.eq(searchReq.getUuid())) + .fetchOne(); + + if (Objects.isNull(entity)) { + throw new EntityNotFoundException( + "DatasetEntity not found for uuid: " + searchReq.getUuid()); + } + + List content = + queryFactory + .selectFrom(datasetClsObjEntity) + .where( + datasetClsObjEntity + .deleted + .isFalse() + .and(datasetClsObjEntity.datasetUid.eq(entity.getId())) + .and(builder) + .and( + datasetClsObjEntity.type.eq(DatasetClsObjDto.FolderType.TRAIN.getText()))) + .offset(pageable.getOffset()) + .limit(pageable.getPageSize()) + .fetch(); + + // Count 쿼리 별도 실행 (null safe handling) + long total = + Optional.ofNullable( + queryFactory + .select(datasetClsObjEntity.count()) + .from(datasetClsObjEntity) + .where( + datasetClsObjEntity + .deleted + .isFalse() + .and(datasetClsObjEntity.datasetUid.eq(entity.getId())) + .and(builder) + .and( + datasetClsObjEntity.type.eq( + DatasetClsObjDto.FolderType.TRAIN.getText()))) + .fetchOne()) + .orElse(0L); + + return new PageImpl<>(content, pageable, total); + } + } + + @Override + public void insertDatasetClsObj(DatasetObjDto.DatasetClsObjRegDto objRegDto) { + try { + em.createNativeQuery( + """ + insert into tb_dataset_cls_obj + (uuid, dataset_uid, type, yyyy, class_cd, + map_sheet_num, tif_img_path, file_name) + values + (?, ?, ?, ?, ?, ?, ?, ?) + """) + .setParameter(1, UUID.randomUUID()) + .setParameter(2, objRegDto.getDatasetUid()) + .setParameter(3, objRegDto.getType()) + .setParameter(4, objRegDto.getYyyy()) + .setParameter(5, objRegDto.getClassCd()) + .setParameter(6, objRegDto.getMapSheetNum()) + .setParameter(7, objRegDto.getTifImgPath()) + .setParameter(8, objRegDto.getFileName()) + .executeUpdate(); + } catch (Exception e) { + throw new RuntimeException(e); + } + } +} diff --git a/src/main/java/com/kamco/cd/training/postgres/repository/dataset/DatasetRepositoryImpl.java b/src/main/java/com/kamco/cd/training/postgres/repository/dataset/DatasetRepositoryImpl.java index 54f2fc0..6256fb0 100644 --- a/src/main/java/com/kamco/cd/training/postgres/repository/dataset/DatasetRepositoryImpl.java +++ b/src/main/java/com/kamco/cd/training/postgres/repository/dataset/DatasetRepositoryImpl.java @@ -65,7 +65,7 @@ public class DatasetRepositoryImpl implements DatasetRepositoryCustom { List content = queryFactory .selectFrom(dataset) - .where(builder.and(dataset.deleted.isFalse())) + .where(builder.and(dataset.deleted.isFalse()).and(dataset.cdClsType.isNull())) .offset(pageable.getOffset()) .limit(pageable.getPageSize()) .orderBy(dataset.createdDttm.desc()) @@ -77,7 +77,7 @@ public class DatasetRepositoryImpl implements DatasetRepositoryCustom { queryFactory .select(dataset.count()) .from(dataset) - .where(builder.and(dataset.deleted.isFalse())) + .where(builder.and(dataset.deleted.isFalse()).and(dataset.cdClsType.isNull())) .fetchOne()) .orElse(0L);