From 8904de0e3d054e4e3c39923c0f8f19cc56ae246d Mon Sep 17 00:00:00 2001 From: teddy Date: Thu, 26 Feb 2026 23:18:31 +0900 Subject: [PATCH] =?UTF-8?q?=EC=B6=94=EB=A1=A0=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../inference/utils/GeoJsonValidator.java | 173 +++++++ .../kamcoback/config/api/ApiResponseDto.java | 5 +- .../InferenceResultShpApiController.java | 31 -- .../service/InferenceResultService.java | 380 ++++++++++------ .../service/InferenceResultShpService.java | 19 - .../postgres/core/MapSheetMngCoreService.java | 230 +++------- .../mapsheet/MapSheetMngRepositoryCustom.java | 48 +- .../mapsheet/MapSheetMngRepositoryImpl.java | 421 ++++++++++++++---- 8 files changed, 853 insertions(+), 454 deletions(-) create mode 100644 src/main/java/com/kamco/cd/kamcoback/common/inference/utils/GeoJsonValidator.java diff --git a/src/main/java/com/kamco/cd/kamcoback/common/inference/utils/GeoJsonValidator.java b/src/main/java/com/kamco/cd/kamcoback/common/inference/utils/GeoJsonValidator.java new file mode 100644 index 00000000..080acede --- /dev/null +++ b/src/main/java/com/kamco/cd/kamcoback/common/inference/utils/GeoJsonValidator.java @@ -0,0 +1,173 @@ +package com.kamco.cd.kamcoback.common.inference.utils; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.*; +import java.util.stream.Collectors; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.springframework.http.HttpStatus; +import org.springframework.web.server.ResponseStatusException; + +public class GeoJsonValidator { + + private static final ObjectMapper om = new ObjectMapper(); + private static final Logger log = LogManager.getLogger(GeoJsonValidator.class); + + public static void validateWithRequested(String geojsonPath, List requestedMapSheetNums) { + + Path path = Path.of(geojsonPath); + + // 1) 파일 기본 검증 + try { + if (!Files.exists(path)) { + throw new ResponseStatusException( + HttpStatus.NOT_FOUND, "GeoJSON 파일이 존재하지 않습니다: " + geojsonPath); + } + if (Files.size(path) == 0) { + throw new ResponseStatusException( + HttpStatus.BAD_REQUEST, "GeoJSON 파일이 비어있습니다: " + geojsonPath); + } + } catch (IOException e) { + log.error("GeoJSON 파일 상태 확인 실패: path={}", path, e); + throw new ResponseStatusException( + HttpStatus.INTERNAL_SERVER_ERROR, "GeoJSON 파일 상태 확인 실패: " + geojsonPath, e); + } + + if (requestedMapSheetNums == null || requestedMapSheetNums.isEmpty()) { + throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "requestedMapSheetNums 가 비어있습니다."); + } + + // 2) 요청 도엽 Set (중복/공백 제거) + Set requested = + requestedMapSheetNums.stream() + .filter(Objects::nonNull) + .map(String::trim) + .filter(s -> !s.isEmpty()) + .collect(Collectors.toCollection(LinkedHashSet::new)); + + if (requested.isEmpty()) { + throw new ResponseStatusException( + HttpStatus.BAD_REQUEST, "requestedMapSheetNums 가 공백/NULL만 포함합니다."); + } + + // 3) GeoJSON 파싱 + final JsonNode features; + try { + JsonNode root = om.readTree(path.toFile()); + features = root.get("features"); + + if (features == null || !features.isArray()) { + throw new ResponseStatusException( + HttpStatus.BAD_REQUEST, "유효하지 않은 GeoJSON: features가 없거나 배열이 아닙니다."); + } + } catch (ResponseStatusException e) { + throw e; + } catch (IOException e) { + log.error("GeoJSON 파일 읽기/파싱 실패: path={}", path, e); + throw new ResponseStatusException( + HttpStatus.INTERNAL_SERVER_ERROR, "GeoJSON 파일 읽기/파싱 실패: " + geojsonPath, e); + } catch (Exception e) { + log.error("GeoJSON 파싱 오류(비정상 JSON): path={}", path, e); + throw new ResponseStatusException( + HttpStatus.BAD_REQUEST, "GeoJSON 파싱 오류(비정상 JSON): " + geojsonPath, e); + } + + // 4) 검증 로직 + int featureCount = features.size(); + + Set foundUnique = new HashSet<>(); + Set duplicates = new LinkedHashSet<>(); + int nullIdCount = 0; + + for (JsonNode feature : features) { + JsonNode props = feature.get("properties"); + String sceneId = + (props != null && props.hasNonNull("scene_id")) ? props.get("scene_id").asText() : null; + + if (sceneId == null || sceneId.isBlank()) { + nullIdCount++; + continue; + } + if (!foundUnique.add(sceneId)) { + duplicates.add(sceneId); + } + } + + // foundUnique에 있는 것들을 missing에서 제거 + Set missing = new LinkedHashSet<>(requested); + missing.removeAll(foundUnique); + + // requested에 있는 것들을 extra에서 제거 + Set extra = new LinkedHashSet<>(foundUnique); + extra.removeAll(requested); + + // ================================================ + // GeoJSON Validation + // + // 요청한 도엽번호(requested)와 + // 실제 생성된 GeoJSON 파일의 scene_id를 비교하여 + // 정합성(데이터 일치 여부)을 검증한다. + // + // 검증 항목: + // 1. features(total) : GeoJSON 전체 feature 개수 (중복 포함) + // 2. requested(unique) : 요청한 도엽번호 개수 + // 3. found(unique scene_id) : GeoJSON에서 실제 발견된 유니크 도엽 개수 + // 4. scene_id null/blank : scene_id가 없는 feature 개수 (데이터 이상) + // 5. duplicates(scene_id) : 동일 도엽이 중복 생성된 개수 + // 6. missing(requested - found) : 요청했지만 파일에 없는 도엽 개수 + // 7. extra(found - requested) : 요청하지 않았는데 파일에 포함된 도엽 개수 + // + // 정상 기준: + // - missing = 0 + // - extra = 0 + // - duplicates = 0 + // - nullId = 0 + // - requested(unique) == found(unique scene_id) + // + // 위 조건을 만족하지 않으면 GeoJSON 생성 오류로 판단한다. + // ================================================ + + // 5) 로그 + log.info( + """ + ===== GeoJSON Validation ===== + file: {} + features(total): {} + requested(unique): {} + found(unique scene_id): {} + scene_id null/blank: {} + duplicates(scene_id): {} + missing(requested - found): {} + extra(found - requested): {} + ============================== + """, + geojsonPath, + featureCount, + requested.size(), + foundUnique.size(), + nullIdCount, + duplicates.size(), + missing.size(), + extra.size()); + + if (!duplicates.isEmpty()) + log.warn("duplicates sample: {}", duplicates.stream().limit(20).toList()); + if (!missing.isEmpty()) log.warn("missing sample: {}", missing.stream().limit(50).toList()); + if (!extra.isEmpty()) log.warn("extra sample: {}", extra.stream().limit(50).toList()); + + // 6) 실패면 422 + if (!missing.isEmpty() || !extra.isEmpty() || !duplicates.isEmpty() || nullIdCount > 0) { + throw new ResponseStatusException( + HttpStatus.UNPROCESSABLE_ENTITY, + String.format( + "GeoJSON validation failed: missing=%d, extra=%d, duplicates=%d, nullId=%d", + missing.size(), extra.size(), duplicates.size(), nullIdCount)); + } + + log.info("GeoJSON validation OK"); + } +} diff --git a/src/main/java/com/kamco/cd/kamcoback/config/api/ApiResponseDto.java b/src/main/java/com/kamco/cd/kamcoback/config/api/ApiResponseDto.java index c32c958e..8eb9e9ae 100644 --- a/src/main/java/com/kamco/cd/kamcoback/config/api/ApiResponseDto.java +++ b/src/main/java/com/kamco/cd/kamcoback/config/api/ApiResponseDto.java @@ -173,8 +173,9 @@ public class ApiResponseDto { + "To reset your password again, please submit a new request through \"Forgot" + " Password.\""), PAYLOAD_TOO_LARGE("업로드 용량 제한을 초과했습니다."), - NOT_FOUND_TARGET_YEAR("기준년도 도엽을 찾을 수 없습니다."), - NOT_FOUND_COMPARE_YEAR("비교년도 도엽을 찾을 수 없습니다."), + NOT_FOUND_TARGET_YEAR("기준연도 도엽을 찾을 수 없습니다."), + NOT_FOUND_COMPARE_YEAR("비교연도 도엽을 찾을 수 없습니다."), + NOT_FOUND_MAP_SHEET_NUM("추론 가능한 도엽이 없습니다."), FAIL_SAVE_MAP_SHEET("도엽 저장 중 오류가 발생했습니다."), FAIL_CREATE_MAP_SHEET_FILE("도엽 설정파일 생성 중 오류가 발생했습니다."), ; diff --git a/src/main/java/com/kamco/cd/kamcoback/inference/InferenceResultShpApiController.java b/src/main/java/com/kamco/cd/kamcoback/inference/InferenceResultShpApiController.java index 4821941c..ed310a8f 100644 --- a/src/main/java/com/kamco/cd/kamcoback/inference/InferenceResultShpApiController.java +++ b/src/main/java/com/kamco/cd/kamcoback/inference/InferenceResultShpApiController.java @@ -1,7 +1,5 @@ package com.kamco.cd.kamcoback.inference; -import com.kamco.cd.kamcoback.common.exception.CustomApiException; -import com.kamco.cd.kamcoback.common.geometry.GeoJsonFileWriter.Scene; import com.kamco.cd.kamcoback.config.api.ApiResponseDto; import com.kamco.cd.kamcoback.inference.dto.InferenceResultShpDto; import com.kamco.cd.kamcoback.inference.service.InferenceResultShpService; @@ -12,15 +10,11 @@ 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.List; -import java.util.Map; import java.util.UUID; import lombok.RequiredArgsConstructor; import lombok.extern.log4j.Log4j2; -import org.springframework.http.HttpStatus; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.PostMapping; -import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; @@ -62,29 +56,4 @@ public class InferenceResultShpApiController { inferenceResultShpService.createShp(uuid); return ApiResponseDto.createOK(null); } - - @Operation(summary = "추론실행에 필요한 geojson 파일 생성", description = "추론실행에 필요한 geojson 파일 생성") - @PostMapping("/geojson/{yyyy}/{mapSheetScope}/{detectOption}") - public ApiResponseDto createGeojson( - @Schema(description = "년도") @PathVariable String yyyy, - @Schema(description = "전체(ALL),부분(PART)", example = "PART") @PathVariable - String mapSheetScope, - @Schema(description = "추론제외(EXCL),이전 년도 도엽 사용(PREV)", example = "EXCL") @PathVariable - String detectOption, - @Schema(description = "5k도엽번호", example = MAP_ID) @RequestBody Map body) { - - Object raw = body.get("mapIds"); - - if (raw == null) { - throw new CustomApiException("BAD_REQUEST", HttpStatus.BAD_REQUEST); - } - - @SuppressWarnings("unchecked") - List mapIds = (List) raw; - - Scene scene = - inferenceResultShpService.createGeojson(yyyy, mapSheetScope, detectOption, mapIds); - - return ApiResponseDto.createOK(scene); - } } diff --git a/src/main/java/com/kamco/cd/kamcoback/inference/service/InferenceResultService.java b/src/main/java/com/kamco/cd/kamcoback/inference/service/InferenceResultService.java index b8c28547..4967771f 100644 --- a/src/main/java/com/kamco/cd/kamcoback/inference/service/InferenceResultService.java +++ b/src/main/java/com/kamco/cd/kamcoback/inference/service/InferenceResultService.java @@ -1,9 +1,11 @@ package com.kamco.cd.kamcoback.inference.service; import com.kamco.cd.kamcoback.common.exception.CustomApiException; +import com.kamco.cd.kamcoback.common.geometry.GeoJsonFileWriter; import com.kamco.cd.kamcoback.common.geometry.GeoJsonFileWriter.ImageFeature; import com.kamco.cd.kamcoback.common.geometry.GeoJsonFileWriter.Scene; import com.kamco.cd.kamcoback.common.inference.service.InferenceCommonService; +import com.kamco.cd.kamcoback.common.inference.utils.GeoJsonValidator; import com.kamco.cd.kamcoback.common.utils.UserUtil; import com.kamco.cd.kamcoback.config.resttemplate.ExternalHttpClient; import com.kamco.cd.kamcoback.config.resttemplate.ExternalHttpClient.ExternalCallResult; @@ -19,7 +21,7 @@ import com.kamco.cd.kamcoback.inference.dto.InferenceResultDto.DetectOption; import com.kamco.cd.kamcoback.inference.dto.InferenceResultDto.InferenceLearnDto; import com.kamco.cd.kamcoback.inference.dto.InferenceResultDto.InferenceServerStatusDto; import com.kamco.cd.kamcoback.inference.dto.InferenceResultDto.InferenceStatusDetailDto; -import com.kamco.cd.kamcoback.inference.dto.InferenceResultDto.MapSheetFallbackYearDto; +import com.kamco.cd.kamcoback.inference.dto.InferenceResultDto.MapSheetScope; import com.kamco.cd.kamcoback.inference.dto.InferenceResultDto.ResultList; import com.kamco.cd.kamcoback.inference.dto.InferenceResultDto.SaveInferenceAiDto; import com.kamco.cd.kamcoback.inference.dto.InferenceResultDto.Status; @@ -37,6 +39,8 @@ import com.kamco.cd.kamcoback.postgres.core.InferenceResultCoreService; import com.kamco.cd.kamcoback.postgres.core.MapSheetMngCoreService; import com.kamco.cd.kamcoback.postgres.core.ModelMngCoreService; import jakarta.validation.constraints.NotNull; +import java.io.IOException; +import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; import java.time.ZonedDateTime; @@ -87,6 +91,12 @@ public class InferenceResultService { @Value("${file.dataset-dir}") private String datasetDir; + @Value("${spring.profiles.active}") + private String activeEnv; + + @Value("${inference.geojson-dir}") + private String inferenceDir; + /** * 추론관리 목록 * @@ -119,11 +129,11 @@ public class InferenceResultService { @Transactional public UUID run(InferenceResultDto.RegReq req) { if (req.getDetectOption().equals(DetectOption.EXCL.getId())) { - // 추론 제외 일때 + // 추론 제외 일때 EXCL return runExcl(req); } - // 이전연도 도엽 사용 일때 + // 이전연도 도엽 사용 일때 PREV return runPrev(req); } @@ -134,46 +144,70 @@ public class InferenceResultService { * @return */ public UUID runExcl(InferenceResultDto.RegReq req) { - // target 도엽 조회 - List targetDtoList = mapSheetMngCoreService.getHstMapSheetList(req); + // 기준연도 실행가능 도엽 조회 + List mngList = + mapSheetMngCoreService.findExecutableSheets( + req.getCompareYyyy(), + req.getTargetYyyy(), + req.getMapSheetScope(), + req.getMapSheetNum()); - if (targetDtoList == null || targetDtoList.isEmpty()) { - throw new CustomApiException("NOT_FOUND", HttpStatus.NOT_FOUND, "기준년도 추론가능한 도엽이 없습니다."); + if (mngList == null || mngList.isEmpty()) { + throw new CustomApiException("NOT_FOUND_MAP_SHEET_NUM", HttpStatus.NOT_FOUND); } - // target 리스트 추출 (null 제거 + 중복 제거) - List targetList = - targetDtoList.stream() + // 비교 연도 도엽번호를 꺼내와서 최종 추론 대상 도엽번호를 담기 + List mapSheetNums = + mngList.stream() .map(MngListDto::getMapSheetNum) .filter(Objects::nonNull) .distinct() - .toList(); + .collect(Collectors.toList()); - // compare 도엽번호 리스트 조회 (null 제거 + 중복 제거), 기준연도와 비교하여 실행하므로 부분, 전체 조건 걸지 않음 - List compareList = - mapSheetMngCoreService.getMapSheetNumByHst(req.getCompareYyyy()).stream() - .filter(Objects::nonNull) - .distinct() - .toList(); + // 기준연도 추론 실행가능 도엽 count + int targetTotal = + mapSheetMngCoreService.countExecutableSheetsDistinct( + req.getTargetYyyy(), req.getMapSheetScope(), mapSheetNums); + // 비교연도 추론 실행가능 도엽 count + int compareTotal = + mapSheetMngCoreService.countExecutableSheetsDistinct( + req.getCompareYyyy(), req.getMapSheetScope(), mapSheetNums); + int intersection = mapSheetNums.size(); - // compare 기준 - Set compareSet = new HashSet<>(compareList); + // ===== MapSheet Year Comparison ===== + // target Total : 기준연도 실행가능 전체 도엽 수 + // compare Total : 비교연도 실행가능 전체 도엽 수 + // Intersection : 양 연도에 모두 존재하는 도엽 수 (최종 추론 대상) + // target Only (Excluded) : 기준연도에만 존재하고 비교연도에는 없는 도엽 수 + // compare Only : 비교연도에만 존재하고 기준연도에는 없는 도엽 수 + // ==================================== - // target 기준으로 compare에 존재하는 도엽만 필터링 - List filteredTargetList = targetList.stream().filter(compareSet::contains).toList(); + log.info( + """ + ===== MapSheet Year Comparison ===== + target Total: {} + compare Total: {} + Intersection: {} + target Only (Excluded): {} + compare Only: {} + ==================================== + """, + targetTotal, + compareTotal, + intersection, + targetTotal - intersection, + compareTotal - intersection); - // 도엽 비교 로그 출력 - logYearComparison(targetList, compareList, filteredTargetList); - - if (filteredTargetList == null || filteredTargetList.isEmpty()) { - throw new CustomApiException("NOT_FOUND", HttpStatus.NOT_FOUND, "추론 가능한 도엽을 확인해 주세요."); + if (mapSheetNums.isEmpty()) { + // 추론 가능한 도엽이 없습니다. + throw new CustomApiException("NOT_FOUND_MAP_SHEET_NUM", HttpStatus.NOT_FOUND); } // compare geojson 파일 생성 Scene compareScene = getSceneInference( req.getCompareYyyy().toString(), // 기준년도 - filteredTargetList, // 교집합 도엽 + mapSheetNums, // 최종 추론 대상 req.getMapSheetScope(), // ALL / 부분 req.getDetectOption()); // EXCL / PREV @@ -181,15 +215,20 @@ public class InferenceResultService { Scene targetScene = getSceneInference( req.getTargetYyyy().toString(), // 대상년도 - filteredTargetList, // 교집합 도엽 + mapSheetNums, // 최종 추론 대상 req.getMapSheetScope(), req.getDetectOption()); + log.info("비교년도 geojson 파일 validation ===== {}", compareScene.getFilePath()); + GeoJsonValidator.validateWithRequested(compareScene.getFilePath(), mapSheetNums); + log.info("기준년도 geojson 파일 validation ===== {}", targetScene.getFilePath()); + GeoJsonValidator.validateWithRequested(targetScene.getFilePath(), mapSheetNums); + // 추론 실행 return executeInference( req, - targetDtoList, // 전체 target 목록 - filteredTargetList, // 최종 추론 대상 + mngList, // 전체 target 목록 + mapSheetNums, // 최종 추론 대상 compareScene, // compare geojson targetScene // target geojson ); @@ -203,99 +242,100 @@ public class InferenceResultService { */ @Transactional public UUID runPrev(InferenceResultDto.RegReq req) { - // target 목록 조회 - List targetDtoList = mapSheetMngCoreService.getHstMapSheetList(req); - if (targetDtoList == null || targetDtoList.isEmpty()) { - throw new CustomApiException("NOT_FOUND", HttpStatus.NOT_FOUND, "기준년도 추론가능한 도엽이 없습니다."); + // 기준연도 실행가능 도엽 조회 + List targetMngList = + mapSheetMngCoreService.getMapSheetMngHst( + req.getTargetYyyy(), req.getMapSheetScope(), req.getMapSheetNum()); + + if (targetMngList == null || targetMngList.isEmpty()) { + throw new CustomApiException("NOT_FOUND_TARGET_YEAR", HttpStatus.NOT_FOUND); } - // target 도엽번호 리스트 추출 중복 제거 - List targetList = - targetDtoList.stream() + // 비교연도 실행가능 도엽 조회 + List compareMngList = + mapSheetMngCoreService.getMapSheetMngHst( + req.getCompareYyyy(), req.getMapSheetScope(), req.getMapSheetNum()); + + if (compareMngList == null || compareMngList.isEmpty()) { + throw new CustomApiException("NOT_FOUND_COMPARE_YEAR", HttpStatus.NOT_FOUND); + } + + // 로그용 원본 카운트 (fallback 추가 전) + int targetTotal = targetMngList.size(); + int compareTotalBeforeFallback = compareMngList.size(); + + // target - compare 구해서 이전년도로 compare 보완 + List compareNums0 = + compareMngList.stream().map(MngListDto::getMapSheetNum).filter(Objects::nonNull).toList(); + + // 기준연도에 없는 도엽을 비교년도 이전 도엽에서 찾아서 추가하기 + List targetOnlyMapSheetNums = + targetMngList.stream() .map(MngListDto::getMapSheetNum) .filter(Objects::nonNull) - .distinct() + .filter(num -> !compareNums0.contains(num)) .toList(); - // compare 목록 조회 - List compareDtoList = - new ArrayList<>(mapSheetMngCoreService.getMapSheetNumDtoByHst(req.getCompareYyyy())); - - // compare 도엽번호 Set 구성 - Set compareSet = - compareDtoList.stream() - .map(MapSheetFallbackYearDto::getMapSheetNum) - .filter(Objects::nonNull) - .collect(Collectors.toSet()); - - // target에는 있으나 compare에는 없는 도엽 추출 - List remainingTargetList = - targetList.stream().filter(s -> !compareSet.contains(s)).toList(); - - // compare에 없을때 이전 년도 사용 가능여부 조회 - List fallbackYearDtoList = + // 이전년도(fallback) 추가 + compareMngList.addAll( mapSheetMngCoreService.findFallbackCompareYearByMapSheets( - req.getTargetYyyy(), // 대상년도 기준 - remainingTargetList // compare에 없는 도엽들 - ); + req.getCompareYyyy(), targetOnlyMapSheetNums)); - // 기존 compare , 사용가능 이전년도 정보 합치기 - compareDtoList.addAll(fallbackYearDtoList); + // 이전연도 추가 후 compare 총 개수 + int compareTotalAfterFallback = compareMngList.size(); - // 중복제거하여 사용할 compare 도엽 목록 - Set availableCompareSheets = - compareDtoList.stream() - .map(MapSheetFallbackYearDto::getMapSheetNum) + // 교집합 도엽번호(mapSheetNums) 교집합 생성 + List compareNums1 = + compareMngList.stream().map(MngListDto::getMapSheetNum).filter(Objects::nonNull).toList(); + + // 기준연도 교집합 + List mapSheetNums = + targetMngList.stream() + .map(MngListDto::getMapSheetNum) .filter(Objects::nonNull) - .collect(Collectors.toSet()); - - // 최종 추론 대상 도엽 - // target 기준으로 compare 에 존재하는 도엽만 추출 - List filteredTargetList = - targetList.stream().filter(availableCompareSheets::contains).toList(); - - // compareDtoList도 최종 기준으로 필터 - Set filteredTargetSet = new HashSet<>(filteredTargetList); - - List filteredCompareDtoList = - compareDtoList.stream() - .filter(d -> d.getMapSheetNum() != null) - .filter(d -> filteredTargetSet.contains(d.getMapSheetNum())) + .filter(compareNums1::contains) .toList(); - // compare only 계산 (target에는 없는 compare 도엽 수) log 용 - long compareOnlyCount = - compareDtoList.stream() - .map(MapSheetFallbackYearDto::getMapSheetNum) - .filter(s -> s != null && !targetList.contains(s)) - .count(); + int intersection = mapSheetNums.size(); + + // 서로 같은 것만 남기기: compare 모두 교집합 + compareMngList = + compareMngList.stream() + .filter(c -> c.getMapSheetNum() != null) + .filter(c -> mapSheetNums.contains(c.getMapSheetNum())) + .toList(); + + // 로그 + int targetOnlyExcluded = targetTotal - intersection; + int compareOnly = compareTotalAfterFallback - intersection; - // 연도별 도엽 비교 로그 출력 log.info( """ - ===== MapSheet Year Comparison ===== - target Total: {} - compare Total: {} - Intersection: {} - target Only (Excluded): {} - compare Only: {} - ==================================== - """, - targetList.size(), // target count - compareDtoList.size(), // compare count - filteredTargetList.size(), // target 기준으로 compare 비교하여 최종 추론할 도엽 count - targetList.size() - filteredTargetList.size(), // compare에 존재하지 않는 target 도엽 수 - compareOnlyCount); // target 에 존재하지 않는 compare 도엽수 + ===== MapSheet Year Comparison ===== + target Total: {} + compare Total(before fallback): {} + compare Total(after fallback): {} + Intersection: {} + target Only (Excluded): {} + compare Only: {} + ==================================== + """, + targetTotal, + compareTotalBeforeFallback, + compareTotalAfterFallback, + intersection, + targetOnlyExcluded, + compareOnly); - if (filteredTargetList == null || filteredTargetList.isEmpty()) { - throw new CustomApiException("NOT_FOUND", HttpStatus.NOT_FOUND, "추론 가능한 도엽을 확인해 주세요."); + if (mapSheetNums.isEmpty()) { + throw new CustomApiException("NOT_FOUND_MAP_SHEET_NUM", HttpStatus.NOT_FOUND); } - // compare 기준 geojson 생성 (년도 fallback 반영) + // compare 기준 geojson 생성 Scene compareScene = getSceneInference( - filteredCompareDtoList, + compareMngList, req.getCompareYyyy().toString(), req.getMapSheetScope(), req.getDetectOption()); @@ -304,12 +344,18 @@ public class InferenceResultService { Scene targetScene = getSceneInference( req.getTargetYyyy().toString(), - filteredTargetList, + mapSheetNums, req.getMapSheetScope(), req.getDetectOption()); - // AI 추론 실행 - return executeInference(req, targetDtoList, filteredTargetList, compareScene, targetScene); + log.info("비교년도 geojson 파일 validation ===== {}", compareScene.getFilePath()); + GeoJsonValidator.validateWithRequested(compareScene.getFilePath(), mapSheetNums); + + log.info("기준년도 geojson 파일 validation ===== {}", targetScene.getFilePath()); + GeoJsonValidator.validateWithRequested(targetScene.getFilePath(), mapSheetNums); + + // 추론 실행 + return executeInference(req, targetMngList, mapSheetNums, compareScene, targetScene); } /** @@ -371,36 +417,6 @@ public class InferenceResultService { return uuid; } - /** - * EXCL 로그 - * - * @param targetList - * @param compareList - * @param filteredTargetList - */ - private void logYearComparison( - List targetList, List compareList, List filteredTargetList) { - Set targetSet = new HashSet<>(targetList); - - long compareOnlyCount = compareList.stream().filter(s -> !targetSet.contains(s)).count(); - - log.info( - """ - ===== MapSheet Year Comparison ===== - target Total: {} - compare Total: {} - Intersection: {} - target Only (Excluded): {} - compare Only: {} - ==================================== - """, - targetList.size(), // target count - compareList.size(), // compare count - filteredTargetList.size(), // target 기준으로 compare 비교하여 최종 추론할 도엽 count - targetList.size() - filteredTargetList.size(), // compare에 존재하지 않는 target 도엽 수 - compareOnlyCount); // target 에 존재하지 않는 compare 도엽수 - } - /** * 변화탐지 실행 정보 생성 TODO 미사용, 새로운 추론실행 로직 테스트후 삭제 해야합니다. * @@ -410,7 +426,7 @@ public class InferenceResultService { public UUID saveInferenceInfo(InferenceResultDto.RegReq req) { // 변화탐지 실행 가능 기준 년도 조회 - List targetList = mapSheetMngCoreService.getHstMapSheetList(req); + List targetList = null; // mapSheetMngCoreService.getHstMapSheetList(req); if (targetList.isEmpty()) { throw new CustomApiException("NOT_FOUND_TARGET_YEAR", HttpStatus.NOT_FOUND); @@ -607,25 +623,36 @@ public class InferenceResultService { */ private Scene getSceneInference( String yyyy, List mapSheetNums, String mapSheetScope, String detectOption) { - return mapSheetMngCoreService.getSceneInference( - yyyy, mapSheetNums, mapSheetScope, detectOption); + + // geojson 생성시 필요한 영상파일 정보 조회 + List features = + mapSheetMngCoreService.loadSceneInferenceBySheets(yyyy, mapSheetNums); + + if (features == null || features.isEmpty()) { + log.warn( + "NOT_FOUND_MAP_SHEET_NUM : yyyy={}, scenesSize={}", + yyyy, + mapSheetNums == null ? 0 : mapSheetNums.size()); + throw new CustomApiException("NOT_FOUND_MAP_SHEET_NUM", HttpStatus.NOT_FOUND); + } + return writeSceneGeoJson(yyyy, mapSheetScope, detectOption, features); } /** * 년도 별로 조회하여 geojson 파일 생성 * * @param yearDtos - * @param year + * @param yyyy * @param mapSheetScope * @param detectOption * @return */ private Scene getSceneInference( - List yearDtos, - String year, - String mapSheetScope, - String detectOption) { - return mapSheetMngCoreService.getSceneInference(yearDtos, year, mapSheetScope, detectOption); + List yearDtos, String yyyy, String mapSheetScope, String detectOption) { + + List features = + mapSheetMngCoreService.loadSceneInferenceByFallbackYears(yearDtos); + return writeSceneGeoJson(yyyy, mapSheetScope, detectOption, features); } /** @@ -865,4 +892,69 @@ public class InferenceResultService { public List getInferenceRunMapId(UUID uuid) { return inferenceResultCoreService.getInferenceRunMapId(uuid); } + + /** + * 파일 경로/이름 , 파일 생성 , 도엽번호 반환 + * + * @param yyyy + * @param mapSheetScope + * @param detectOption + * @param sceneInference + * @return Scene + */ + private Scene writeSceneGeoJson( + String yyyy, String mapSheetScope, String detectOption, List sceneInference) { + + boolean isAll = MapSheetScope.ALL.getId().equals(mapSheetScope); + String optionSuffix = buildOptionSuffix(detectOption); + + String targetDir = + "local".equals(activeEnv) ? System.getProperty("user.home") + "/geojson" : inferenceDir; + + // 파일명 생성 + String filename = + isAll + ? String.format("%s_%s_ALL%s.geojson", yyyy, activeEnv, optionSuffix) + : String.format("%s_%s%s.geojson", yyyy, activeEnv, optionSuffix); + + Path outputPath = Paths.get(targetDir, filename); + + try { + log.info("create Directories outputPath: {}", outputPath); + log.info( + "activeEnv={}, inferenceDir={}, targetDir={}, filename={}", + activeEnv, + inferenceDir, + targetDir, + filename); + log.info("outputPath={}, parent={}", outputPath.toAbsolutePath(), outputPath.getParent()); + + Files.createDirectories(outputPath.getParent()); + + new GeoJsonFileWriter() + .exportToFile(sceneInference, "scene_inference_" + yyyy, 5186, outputPath.toString()); + + Scene scene = new Scene(); + scene.setFeatures(sceneInference); + scene.setFilePath(outputPath.toString()); + return scene; + + } catch (IOException e) { + log.error( + "FAIL_CREATE_MAP_SHEET_FILE: yyyy={}, isAll={}, path={}", yyyy, isAll, outputPath, e); + throw new CustomApiException("INTERNAL_SERVER_ERROR", HttpStatus.INTERNAL_SERVER_ERROR, e); + } + } + + /** + * geojson 파일명 Suffix + * + * @param detectOption + * @return + */ + private String buildOptionSuffix(String detectOption) { + if (DetectOption.EXCL.getId().equals(detectOption)) return "_EXCL"; + if (DetectOption.PREV.getId().equals(detectOption)) return "_PREV"; + return ""; + } } diff --git a/src/main/java/com/kamco/cd/kamcoback/inference/service/InferenceResultShpService.java b/src/main/java/com/kamco/cd/kamcoback/inference/service/InferenceResultShpService.java index fc41a727..9d906092 100644 --- a/src/main/java/com/kamco/cd/kamcoback/inference/service/InferenceResultShpService.java +++ b/src/main/java/com/kamco/cd/kamcoback/inference/service/InferenceResultShpService.java @@ -1,13 +1,11 @@ package com.kamco.cd.kamcoback.inference.service; -import com.kamco.cd.kamcoback.common.geometry.GeoJsonFileWriter.Scene; import com.kamco.cd.kamcoback.inference.dto.InferenceResultDto.InferenceLearnDto; import com.kamco.cd.kamcoback.inference.dto.InferenceResultShpDto; import com.kamco.cd.kamcoback.postgres.core.InferenceResultCoreService; import com.kamco.cd.kamcoback.postgres.core.InferenceResultShpCoreService; import com.kamco.cd.kamcoback.postgres.core.MapSheetMngCoreService; import com.kamco.cd.kamcoback.scheduler.service.ShpPipelineService; -import java.util.List; import java.util.Objects; import java.util.UUID; import java.util.stream.Collectors; @@ -66,21 +64,4 @@ public class InferenceResultShpService { // shp 파일 비동기 생성 shpPipelineService.runPipeline(jarPath, datasetDir, batchId, dto.getUid()); } - - /** - * 추론 실행전 geojson 파일 생성 - * - * @param yyyy - * @param mapSheetScope - * @param detectOption - * @param mapIds - * @return - */ - public Scene createGeojson( - String yyyy, String mapSheetScope, String detectOption, List mapIds) { - Scene getSceneInference = - mapSheetMngCoreService.getSceneInference(yyyy, mapIds, mapSheetScope, detectOption); - log.info("getSceneInference: {}", getSceneInference); - return getSceneInference; - } } diff --git a/src/main/java/com/kamco/cd/kamcoback/postgres/core/MapSheetMngCoreService.java b/src/main/java/com/kamco/cd/kamcoback/postgres/core/MapSheetMngCoreService.java index c289ac78..7012420e 100644 --- a/src/main/java/com/kamco/cd/kamcoback/postgres/core/MapSheetMngCoreService.java +++ b/src/main/java/com/kamco/cd/kamcoback/postgres/core/MapSheetMngCoreService.java @@ -1,14 +1,8 @@ package com.kamco.cd.kamcoback.postgres.core; import com.kamco.cd.kamcoback.common.enums.MngStateType; -import com.kamco.cd.kamcoback.common.exception.CustomApiException; import com.kamco.cd.kamcoback.common.geometry.GeoJsonFileWriter; import com.kamco.cd.kamcoback.common.geometry.GeoJsonFileWriter.ImageFeature; -import com.kamco.cd.kamcoback.common.geometry.GeoJsonFileWriter.Scene; -import com.kamco.cd.kamcoback.inference.dto.InferenceResultDto; -import com.kamco.cd.kamcoback.inference.dto.InferenceResultDto.DetectOption; -import com.kamco.cd.kamcoback.inference.dto.InferenceResultDto.MapSheetFallbackYearDto; -import com.kamco.cd.kamcoback.inference.dto.InferenceResultDto.MapSheetScope; import com.kamco.cd.kamcoback.mapsheet.dto.MapSheetMngDto; import com.kamco.cd.kamcoback.mapsheet.dto.MapSheetMngDto.MngListCompareDto; import com.kamco.cd.kamcoback.mapsheet.dto.MapSheetMngDto.MngListDto; @@ -22,11 +16,11 @@ import jakarta.persistence.EntityNotFoundException; import jakarta.validation.Valid; import java.io.IOException; import java.nio.file.Files; -import java.nio.file.Path; import java.nio.file.Paths; import java.time.ZonedDateTime; import java.util.ArrayList; import java.util.Arrays; +import java.util.Collections; import java.util.List; import java.util.Map; import java.util.Objects; @@ -37,7 +31,6 @@ import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Value; import org.springframework.data.domain.Page; -import org.springframework.http.HttpStatus; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @@ -55,9 +48,6 @@ public class MapSheetMngCoreService { @Value("${file.sync-root-dir}") private String syncRootDir; - @Value("${inference.geojson-dir}") - private String inferenceDir; - public List findMapSheetMngList() { return mapSheetMngRepository.findMapSheetMngList(); } @@ -235,124 +225,29 @@ public class MapSheetMngCoreService { } /** - * geojson 생성 + * geojson 생성시 필요한 영상파일 정보 조회 * * @param yyyy - * @param scenes - * @param mapSheetScope - * @param detectOption - * @return - */ - public Scene getSceneInference( - String yyyy, List scenes, String mapSheetScope, String detectOption) { - List features = loadSceneInferenceBySheets(yyyy, scenes); - return writeSceneGeoJson(yyyy, mapSheetScope, detectOption, features); - } - - /** - * geojson 생성 - * - * @param yearDtos - * @param yyyy - * @param mapSheetScope - * @param detectOption - * @return - */ - public Scene getSceneInference( - List yearDtos, - String yyyy, - String mapSheetScope, - String detectOption) { - List features = loadSceneInferenceByFallbackYears(yearDtos); - return writeSceneGeoJson(yyyy, mapSheetScope, detectOption, features); - } - - /** - * 파일 경로/이름 , 파일 생성 , 도엽번호 반환 - * - * @param yyyy - * @param mapSheetScope - * @param detectOption - * @param sceneInference - * @return Scene - */ - private Scene writeSceneGeoJson( - String yyyy, String mapSheetScope, String detectOption, List sceneInference) { - boolean isAll = MapSheetScope.ALL.getId().equals(mapSheetScope); - String optionSuffix = buildOptionSuffix(detectOption); - - String targetDir = - "local".equals(activeEnv) ? System.getProperty("user.home") + "/geojson" : inferenceDir; - - String filename = - isAll - ? String.format("%s_%s_ALL%s.geojson", yyyy, activeEnv, optionSuffix) - : String.format("%s_%s%s.geojson", yyyy, activeEnv, optionSuffix); - - Path outputPath = Paths.get(targetDir, filename); - - if (sceneInference == null || sceneInference.isEmpty()) { - log.warn("NOT_FOUND_TARGET_YEAR: yyyy={}, isAll={}, featuresSize={}", yyyy, isAll, 0); - throw new CustomApiException("NOT_FOUND_TARGET_YEAR", HttpStatus.NOT_FOUND); - } - - try { - log.info("create Directories outputPath: {}", outputPath); - log.info( - "activeEnv={}, inferenceDir={}, targetDir={}, filename={}", - activeEnv, - inferenceDir, - targetDir, - filename); - log.info("outputPath={}, parent={}", outputPath.toAbsolutePath(), outputPath.getParent()); - - Files.createDirectories(outputPath.getParent()); - - new GeoJsonFileWriter() - .exportToFile(sceneInference, "scene_inference_" + yyyy, 5186, outputPath.toString()); - - Scene scene = new Scene(); - scene.setFeatures(sceneInference); - scene.setFilePath(outputPath.toString()); - return scene; - - } catch (IOException e) { - log.error( - "FAIL_CREATE_MAP_SHEET_FILE: yyyy={}, isAll={}, path={}", yyyy, isAll, outputPath, e); - throw new CustomApiException("INTERNAL_SERVER_ERROR", HttpStatus.INTERNAL_SERVER_ERROR, e); - } - } - - /** - * geojson 파일명 Suffix - * - * @param detectOption - * @return - */ - private String buildOptionSuffix(String detectOption) { - if (DetectOption.EXCL.getId().equals(detectOption)) return "_EXCL"; - if (DetectOption.PREV.getId().equals(detectOption)) return "_PREV"; - return ""; - } - - /** - * 년도, 도엽번호로 조회 - * - * @param yyyy - * @param scenes + * @param mapSheetNums * @return ImageFeature */ - private List loadSceneInferenceBySheets(String yyyy, List scenes) { - List sceneInference = mapSheetMngRepository.getSceneInference(yyyy, scenes); + public List loadSceneInferenceBySheets(String yyyy, List mapSheetNums) { - if (sceneInference == null || sceneInference.isEmpty()) { - log.warn( - "NOT_FOUND_TARGET_YEAR: yyyy={}, scenesSize={}", - yyyy, - scenes == null ? 0 : scenes.size()); - throw new CustomApiException("NOT_FOUND_TARGET_YEAR", HttpStatus.NOT_FOUND); + if (mapSheetNums == null || mapSheetNums.isEmpty()) { + return List.of(); } - return sceneInference; + + // CHUNK_SIZE 단위로 나누어 여러 번 조회한다. + final int CHUNK_SIZE = 1000; + List features = new ArrayList<>(); + + // i부터 CHUNK_SIZE만큼 잘라서 조회 + // 마지막 구간은 남은 개수만큼만 처리하기 위해 Math.min 사용 + for (int i = 0; i < mapSheetNums.size(); i += CHUNK_SIZE) { + List chunk = mapSheetNums.subList(i, Math.min(i + CHUNK_SIZE, mapSheetNums.size())); + features.addAll(mapSheetMngRepository.getSceneInference(yyyy, chunk)); + } + return features; } /** @@ -361,33 +256,32 @@ public class MapSheetMngCoreService { * @param yearDtos * @return ImageFeature */ - private List loadSceneInferenceByFallbackYears( - List yearDtos) { + public List loadSceneInferenceByFallbackYears(List yearDtos) { if (yearDtos == null || yearDtos.isEmpty()) { return List.of(); } // 년도 별로 루프를 돌리기위해 년도별 정리 - Map> groupedByYear = + Map> groupedByYear = yearDtos.stream() - .filter(d -> d.getMngYyyy() != null && d.getMapSheetNum() != null) - .collect(Collectors.groupingBy(MapSheetFallbackYearDto::getMngYyyy)); + .filter(d -> d.getMngYyyy() != 0 && d.getMapSheetNum() != null) + .collect(Collectors.groupingBy(MngListDto::getMngYyyy)); List sceneInference = new ArrayList<>(); - for (Map.Entry> entry : groupedByYear.entrySet()) { + for (Map.Entry> entry : groupedByYear.entrySet()) { Integer year = entry.getKey(); // 년도별 mapSheetNum 만들기 List sheetNums = entry.getValue().stream() - .map(MapSheetFallbackYearDto::getMapSheetNum) + .map(MngListDto::getMapSheetNum) .filter(Objects::nonNull) .distinct() .toList(); // tif파일 정보 조회 - List temp = mapSheetMngRepository.getSceneInference(year.toString(), sheetNums); + List temp = loadSceneInferenceBySheets(year.toString(), sheetNums); if (temp != null && !temp.isEmpty()) { sceneInference.addAll(temp); @@ -400,15 +294,35 @@ public class MapSheetMngCoreService { /** * 변화탐지 실행 가능 기준 년도 조회 * - * @param req - * @return + * @param compareYear 비교 연도 + * @param targetYear 기준 연도 + * @param mapSheetScope 부분실행, 전체실행 + * @param mapSheetNums 도엽번호 목록 + * @return 실행가능한 도엽번호 목록 */ - public List getHstMapSheetList(InferenceResultDto.RegReq req) { - return mapSheetMngRepository.findByHstMapSheetTargetList(req); + public List findExecutableSheets( + Integer compareYear, Integer targetYear, String mapSheetScope, List mapSheetNums) { + return mapSheetMngRepository.findExecutableSheets( + compareYear, targetYear, mapSheetScope, mapSheetNums); } - public List getHstMapSheetList(int mngYyyy, List mapIds) { - return mapSheetMngRepository.findByHstMapSheetTargetList(mngYyyy, mapIds); + public List fetchBaseWithCompare( + Integer compareYear, Integer targetYear, String mapSheetScope, List mapSheetNums) { + return mapSheetMngRepository.fetchBaseWithCompare( + compareYear, targetYear, mapSheetScope, mapSheetNums); + } + + /** + * 실행가능 도엽 count + * + * @param year 연도 + * @param mapSheetScope 부분, 전체 구분 + * @param mapSheetNums 도엽 목록 + * @return count + */ + public int countExecutableSheetsDistinct( + Integer year, String mapSheetScope, List mapSheetNums) { + return mapSheetMngRepository.countExecutableSheetsDistinct(year, mapSheetScope, mapSheetNums); } public void updateMapSheetMngHstUploadId(Long hstUid, UUID uuid, String uploadId) { @@ -431,36 +345,32 @@ public class MapSheetMngCoreService { return mapSheetMngYearRepository.findByHstMapSheetCompareList(mngYyyy, mapId); } - /** - * 연도 조건으로 실행 가능 도엽번호 조회 - * - * @param year 연도 - * @return 추론 가능한 도엽 정보 - */ - public List getMapSheetNumByHst(Integer year) { - List entity = mapSheetMngRepository.getMapSheetMngHst(year); - return entity.stream().map(MapSheetMngHstEntity::getMapSheetNum).toList(); + public List getMapSheetMngHst( + Integer year, String mapSheetScope, List mapSheetNum) { + return mapSheetMngRepository.getMapSheetMngHst(year, mapSheetScope, mapSheetNum); } /** - * 특정 연도의 도엽 이력 데이터를 조회 + * 이전 년도 도엽 조회 * * @param year + * @param mapIds * @return */ - public List getMapSheetNumDtoByHst(Integer year) { - List entity = mapSheetMngRepository.getMapSheetMngHst(year); - return entity.stream() - .map( - e -> - new MapSheetFallbackYearDto( - e.getMapSheetNum(), e.getMngYyyy() // 조회 기준 연도 - )) - .toList(); - } + public List findFallbackCompareYearByMapSheets(Integer year, List mapIds) { + if (mapIds == null || mapIds.isEmpty()) { + return Collections.emptyList(); + } - public List findFallbackCompareYearByMapSheets( - Integer year, List mapIds) { - return mapSheetMngRepository.findFallbackCompareYearByMapSheets(year, mapIds); + int chunkSize = 1000; + List result = new ArrayList<>(); + + for (int i = 0; i < mapIds.size(); i += chunkSize) { + List chunk = mapIds.subList(i, Math.min(i + chunkSize, mapIds.size())); + + result.addAll(mapSheetMngRepository.findFallbackCompareYearByMapSheets(year, chunk)); + } + + return result; } } diff --git a/src/main/java/com/kamco/cd/kamcoback/postgres/repository/mapsheet/MapSheetMngRepositoryCustom.java b/src/main/java/com/kamco/cd/kamcoback/postgres/repository/mapsheet/MapSheetMngRepositoryCustom.java index 763618f8..d27653a9 100644 --- a/src/main/java/com/kamco/cd/kamcoback/postgres/repository/mapsheet/MapSheetMngRepositoryCustom.java +++ b/src/main/java/com/kamco/cd/kamcoback/postgres/repository/mapsheet/MapSheetMngRepositoryCustom.java @@ -1,8 +1,6 @@ package com.kamco.cd.kamcoback.postgres.repository.mapsheet; import com.kamco.cd.kamcoback.common.geometry.GeoJsonFileWriter.ImageFeature; -import com.kamco.cd.kamcoback.inference.dto.InferenceResultDto; -import com.kamco.cd.kamcoback.inference.dto.InferenceResultDto.MapSheetFallbackYearDto; import com.kamco.cd.kamcoback.mapsheet.dto.MapSheetMngDto; import com.kamco.cd.kamcoback.mapsheet.dto.MapSheetMngDto.AddReq; import com.kamco.cd.kamcoback.mapsheet.dto.MapSheetMngDto.MngListDto; @@ -65,14 +63,38 @@ public interface MapSheetMngRepositoryCustom { List findByHstUidMapSheetFileList(Long hstUid); /** - * 기준년도 추론 실행 가능 도엽 조회 + * 추론 실행 가능 도엽 조회 (추론 제외) * - * @param req 조회 연도, 도엽번호 목록, - * @return 실행 가능한 도엽번호 + * @param compareYear 비교 연도 + * @param targetYear 기준 연도 + * @param mapSheetScope 부분실행, 전체실행 + * @param mapSheetNums 도엽번호 목록 + * @return 실행가능한 도엽번호 목록 */ - List findByHstMapSheetTargetList(InferenceResultDto.RegReq req); + List findExecutableSheets( + Integer compareYear, Integer targetYear, String mapSheetScope, List mapSheetNums); - List findByHstMapSheetTargetList(int mngYyyy, List mapIds); + /** + * 추론 실행 가능 도엽 조회 (이전년도 사용가능) + * + * @param targetYear + * @param compareYear + * @param mapSheetScope + * @param mapSheetNums + * @return + */ + List fetchBaseWithCompare( + Integer targetYear, Integer compareYear, String mapSheetScope, List mapSheetNums); + + /** + * 실행가능 도엽 count + * + * @param year 연도 + * @param mapSheetScope 부분, 전체 구분 + * @param mapSheetNums 도엽 목록 + * @return count + */ + int countExecutableSheetsDistinct(Integer year, String mapSheetScope, List mapSheetNums); MapSheetMngDto.MngFilesDto findByFileUidMapSheetFile(Long fileUid); @@ -82,6 +104,13 @@ public interface MapSheetMngRepositoryCustom { Page getYears(YearSearchReq req); + /** + * 도엽 영상파일 정보 조회 + * + * @param yyyy 연도 + * @param mapSheetNums 도엽목록 + * @return 도엽 영상파일 정보 + */ List getSceneInference(String yyyy, List mapSheetNums); void updateMapSheetMngHstUploadId(Long hstUid, UUID uuid, String uploadId); @@ -94,8 +123,7 @@ public interface MapSheetMngRepositoryCustom { * @param year 연도 * @return 추론 가능한 도엽 정보 */ - List getMapSheetMngHst(Integer year); + List getMapSheetMngHst(Integer year, String mapSheetScope, List mapSheetNum); - List findFallbackCompareYearByMapSheets( - Integer year, List mapIds); + List findFallbackCompareYearByMapSheets(Integer year, List mapIds); } diff --git a/src/main/java/com/kamco/cd/kamcoback/postgres/repository/mapsheet/MapSheetMngRepositoryImpl.java b/src/main/java/com/kamco/cd/kamcoback/postgres/repository/mapsheet/MapSheetMngRepositoryImpl.java index 9fe11340..142b616c 100644 --- a/src/main/java/com/kamco/cd/kamcoback/postgres/repository/mapsheet/MapSheetMngRepositoryImpl.java +++ b/src/main/java/com/kamco/cd/kamcoback/postgres/repository/mapsheet/MapSheetMngRepositoryImpl.java @@ -5,36 +5,41 @@ import static com.kamco.cd.kamcoback.postgres.entity.QMapSheetMngEntity.mapSheet import static com.kamco.cd.kamcoback.postgres.entity.QMapSheetMngFileEntity.mapSheetMngFileEntity; import static com.kamco.cd.kamcoback.postgres.entity.QMapSheetMngHstEntity.mapSheetMngHstEntity; import static com.kamco.cd.kamcoback.postgres.entity.QMapSheetMngTileEntity.mapSheetMngTileEntity; -import static com.kamco.cd.kamcoback.postgres.entity.QMapSheetMngYearYnEntity.mapSheetMngYearYnEntity; import static com.kamco.cd.kamcoback.postgres.entity.QYearEntity.yearEntity; import static com.querydsl.core.types.dsl.Expressions.nullExpression; import com.kamco.cd.kamcoback.common.enums.CommonUseStatus; import com.kamco.cd.kamcoback.common.geometry.GeoJsonFileWriter.ImageFeature; -import com.kamco.cd.kamcoback.inference.dto.InferenceResultDto; -import com.kamco.cd.kamcoback.inference.dto.InferenceResultDto.MapSheetFallbackYearDto; import com.kamco.cd.kamcoback.inference.dto.InferenceResultDto.MapSheetScope; import com.kamco.cd.kamcoback.mapsheet.dto.MapSheetMngDto; import com.kamco.cd.kamcoback.mapsheet.dto.MapSheetMngDto.AddReq; import com.kamco.cd.kamcoback.mapsheet.dto.MapSheetMngDto.MngListDto; import com.kamco.cd.kamcoback.mapsheet.dto.MapSheetMngDto.YearSearchReq; import com.kamco.cd.kamcoback.postgres.entity.MapSheetMngHstEntity; +import com.kamco.cd.kamcoback.postgres.entity.QMapInkx5kEntity; +import com.kamco.cd.kamcoback.postgres.entity.QMapSheetMngFileEntity; +import com.kamco.cd.kamcoback.postgres.entity.QMapSheetMngHstEntity; import com.kamco.cd.kamcoback.postgres.entity.QYearEntity; import com.kamco.cd.kamcoback.postgres.entity.YearEntity; import com.querydsl.core.BooleanBuilder; +import com.querydsl.core.Tuple; import com.querydsl.core.types.Projections; import com.querydsl.core.types.dsl.BooleanExpression; import com.querydsl.core.types.dsl.CaseBuilder; import com.querydsl.core.types.dsl.Expressions; import com.querydsl.core.types.dsl.NumberExpression; import com.querydsl.core.types.dsl.StringExpression; +import com.querydsl.jpa.JPAExpressions; import com.querydsl.jpa.impl.JPAQueryFactory; import jakarta.persistence.EntityManager; import jakarta.persistence.PersistenceContext; import jakarta.validation.Valid; import java.time.ZonedDateTime; +import java.util.ArrayList; import java.util.Comparator; +import java.util.HashMap; import java.util.List; +import java.util.Map; import java.util.Objects; import java.util.Optional; import java.util.UUID; @@ -573,80 +578,201 @@ public class MapSheetMngRepositoryImpl extends QuerydslRepositorySupport } @Override - public List findByHstMapSheetTargetList(InferenceResultDto.RegReq req) { - BooleanBuilder whereBuilder = new BooleanBuilder(); + public List findExecutableSheets( + Integer compareYear, Integer targetYear, String mapSheetScope, List mapSheetNums) { - whereBuilder.and(mapSheetMngHstEntity.dataState.eq("DONE")); - whereBuilder.and( - mapSheetMngHstEntity - .syncState - .eq("DONE") - .or(mapSheetMngHstEntity.syncCheckState.eq("DONE"))); - whereBuilder.and(mapSheetMngHstEntity.useInference.eq("USE")); + QMapSheetMngHstEntity t = new QMapSheetMngHstEntity("t"); + QMapSheetMngHstEntity c = new QMapSheetMngHstEntity("c"); - whereBuilder.and(mapSheetMngHstEntity.mngYyyy.eq(req.getTargetYyyy())); + QMapInkx5kEntity ti = new QMapInkx5kEntity("ti"); + QMapInkx5kEntity ci = new QMapInkx5kEntity("ci"); + QMapSheetMngFileEntity tf = new QMapSheetMngFileEntity("tf"); + QMapSheetMngFileEntity cf = new QMapSheetMngFileEntity("cf"); + + BooleanBuilder whereBuilderere = new BooleanBuilder(); + whereBuilderere.and(t.dataState.eq("DONE")); + whereBuilderere.and(t.syncState.eq("DONE").or(t.syncCheckState.eq("DONE"))); + whereBuilderere.and(t.useInference.eq("USE")); + whereBuilderere.and(t.mngYyyy.eq(targetYear)); + + // target: tif + DONE 파일 존재 + whereBuilderere.and( + JPAExpressions.selectOne() + .from(tf) + .where(tf.hstUid.eq(t.hstUid).and(tf.fileExt.eq("tif")).and(tf.fileState.eq("DONE"))) + .exists()); + + // PART scope면 prefix like 조건 추가 BooleanBuilder likeBuilder = new BooleanBuilder(); - // 부분 선택 실행이면 도엽번호 검색조건 추가 - if (MapSheetScope.PART.getId().equals(req.getMapSheetScope())) { - List list = req.getMapSheetNum(); - if (list == null || list.isEmpty()) { + if (MapSheetScope.PART.getId().equals(mapSheetScope)) { + if (mapSheetNums == null || mapSheetNums.isEmpty()) { return List.of(); } - - for (String prefix : list) { - if (prefix == null || prefix.isBlank()) { - continue; - } - // 50k 도엽번호로 Like 하여 5k 도엽번호 검색 - likeBuilder.or(mapSheetMngHstEntity.mapSheetNum.like(prefix.trim() + "%")); + for (String prefix : mapSheetNums) { + if (prefix == null || prefix.isBlank()) continue; + likeBuilder.or(t.mapSheetNum.like(prefix.trim() + "%")); + } + if (likeBuilder.hasValue()) { + whereBuilderere.and(likeBuilder); } } - if (likeBuilder.hasValue()) { - whereBuilder.and(likeBuilder); - } + // compare 조건은 EXISTS 서브쿼리 안에서 체크 + BooleanExpression compareExists = + JPAExpressions.selectOne() + .from(c) + .join(ci) + .on(ci.mapidcdNo.eq(c.mapSheetNum).and(ci.useInference.eq(CommonUseStatus.USE))) + .where( + c.mapSheetNum + .eq(t.mapSheetNum) // 핵심: 같은 도엽번호 + .and(c.dataState.eq("DONE")) + .and(c.syncState.eq("DONE").or(c.syncCheckState.eq("DONE"))) + .and(c.useInference.eq("USE")) + .and(c.mngYyyy.eq(compareYear)) + .and( + JPAExpressions.selectOne() + .from(cf) + .where( + cf.hstUid + .eq(c.hstUid) + .and(cf.fileExt.eq("tif")) + .and(cf.fileState.eq("DONE"))) + .exists())) + .exists(); return queryFactory .select( Projections.constructor( MngListDto.class, - mapSheetMngHstEntity.mngYyyy, - mapSheetMngHstEntity.mapSheetNum, - mapSheetMngHstEntity.mapSheetName, - nullExpression(Integer.class), - nullExpression(Boolean.class))) - .from(mapSheetMngHstEntity) - .innerJoin(mapInkx5kEntity) - .on( - mapInkx5kEntity - .mapidcdNo - .eq(mapSheetMngHstEntity.mapSheetNum) - .and(mapInkx5kEntity.useInference.eq(CommonUseStatus.USE))) - .where(whereBuilder) + t.mngYyyy, // int mngYyyy + t.mapSheetNum, // String mapSheetNum + t.mapSheetName, // String mapSheetName + Expressions.nullExpression(Integer.class), + Expressions.nullExpression(Boolean.class))) + .from(t) + .join(ti) + .on(ti.mapidcdNo.eq(t.mapSheetNum).and(ti.useInference.eq(CommonUseStatus.USE))) + .where(whereBuilderere.and(compareExists)) + .distinct() .fetch(); } @Override - public List findByHstMapSheetTargetList(int mngYyyy, List mapIds) { - BooleanBuilder whereBuilder = new BooleanBuilder(); - whereBuilder.and(mapSheetMngHstEntity.mngYyyy.eq(mngYyyy)); - whereBuilder.and(mapSheetMngHstEntity.mapSheetNum.in(mapIds)); - whereBuilder.and(mapSheetMngHstEntity.dataState.eq("DONE")); - whereBuilder.and(mapSheetMngHstEntity.syncState.eq("DONE")); // TODO 싱크체크 or조건 추가 - whereBuilder.and(mapSheetMngHstEntity.useInference.eq("USE")); + public List fetchBaseWithCompare( + Integer compareYear, // 예: 2024 (비교 상한) + Integer targetYear, // 예: 2026 (기준 고정) + String mapSheetScope, + List mapSheetNums) { - return queryFactory - .select( - Projections.constructor( - MngListDto.class, - mapSheetMngHstEntity.mngYyyy, - mapSheetMngHstEntity.mapSheetNum, - mapSheetMngHstEntity.mapSheetName)) - .from(mapSheetMngHstEntity) - .where(whereBuilder) - .fetch(); + QMapSheetMngHstEntity h = QMapSheetMngHstEntity.mapSheetMngHstEntity; // base + QMapSheetMngHstEntity h2 = new QMapSheetMngHstEntity("h2"); // cmp + QMapInkx5kEntity i = QMapInkx5kEntity.mapInkx5kEntity; + QMapSheetMngFileEntity f = QMapSheetMngFileEntity.mapSheetMngFileEntity; + QMapSheetMngFileEntity f2 = new QMapSheetMngFileEntity("f2"); + + // ----------------------- + // 공통: tif DONE exists + // ----------------------- + BooleanExpression baseTifExists = + JPAExpressions.selectOne() + .from(f) + .where(f.hstUid.eq(h.hstUid), f.fileExt.eq("tif"), f.fileState.eq("DONE")) + .exists(); + + BooleanExpression cmpTifExists = + JPAExpressions.selectOne() + .from(f2) + .where(f2.hstUid.eq(h2.hstUid), f2.fileExt.eq("tif"), f2.fileState.eq("DONE")) + .exists(); + + // ----------------------- + // 1) base 조회 (CTE base) + // ----------------------- + BooleanBuilder baseWhere = + new BooleanBuilder() + .and(h.mngYyyy.eq(targetYear)) + .and(h.dataState.eq("DONE")) + .and(h.useInference.eq("USE")) + .and(h.syncState.eq("DONE").or(h.syncCheckState.eq("DONE"))) + .and(baseTifExists); + + // PART 조건 + if (MapSheetScope.PART.getId().equals(mapSheetScope)) { + if (mapSheetNums == null || mapSheetNums.isEmpty()) return List.of(); + + BooleanBuilder like = new BooleanBuilder(); + for (String prefix : mapSheetNums) { + if (prefix == null || prefix.isBlank()) continue; + like.or(h.mapSheetNum.like(prefix.trim() + "%")); + } + if (!like.hasValue()) return List.of(); + baseWhere.and(like); + } + + // base: distinct map_sheet_num, mng_yyyy, map_sheet_name + List baseTuples = + queryFactory + .select(h.mapSheetNum, h.mngYyyy, h.mapSheetName) + .distinct() + .from(h) + .join(i) + .on(i.mapidcdNo.eq(h.mapSheetNum), i.useInference.eq(CommonUseStatus.USE)) + .where(baseWhere) + .fetch(); + + if (baseTuples.isEmpty()) return List.of(); + + List baseNums = baseTuples.stream().map(t -> t.get(h.mapSheetNum)).toList(); + + // ----------------------- + // 2) cmp 집계 (CTE cmp) + // map_sheet_num별 max(mng_yyyy <= 2024) + // ----------------------- + List cmpTuples = + queryFactory + .select(h2.mapSheetNum, h2.mngYyyy.max()) + .from(h2) + .where( + h2.mapSheetNum.in(baseNums), + h2.mngYyyy.loe(compareYear), + h2.dataState.eq("DONE"), + h2.useInference.eq("USE"), + h2.syncState.eq("DONE").or(h2.syncCheckState.eq("DONE")), + cmpTifExists) + .groupBy(h2.mapSheetNum) + .fetch(); + + Map beforeYearMap = new HashMap<>(cmpTuples.size()); + for (Tuple t : cmpTuples) { + String num = t.get(h2.mapSheetNum); + Integer beforeYear = t.get(h2.mngYyyy.max()); + beforeYearMap.put(num, beforeYear); + } + + // ----------------------- + // 3) base JOIN cmp (메모리) + // - SQL에서 join cmp c on c.map_sheet_num = b.map_sheet_num 동일 + // ----------------------- + List result = new ArrayList<>(); + for (Tuple bt : baseTuples) { + String num = bt.get(h.mapSheetNum); + Integer beforeYear = beforeYearMap.get(num); + if (beforeYear == null) continue; // SQL의 inner join cmp 효과 + + result.add( + new MngListDto( + bt.get(h.mngYyyy), // 2026 + num, + bt.get(h.mapSheetName), + beforeYear, // <=2024 max + null // isSuccess + )); + } + + return result; } @Override @@ -1099,42 +1225,161 @@ public class MapSheetMngRepositoryImpl extends QuerydslRepositorySupport } @Override - public List getMapSheetMngHst(Integer year) { - return queryFactory - .select(mapSheetMngHstEntity) - .from(mapSheetMngHstEntity) - .innerJoin(mapSheetMngFileEntity) - .on(mapSheetMngFileEntity.hstUid.eq(mapSheetMngHstEntity.hstUid)) - .where( - mapSheetMngHstEntity - .mngYyyy - .eq(year) - .and( - mapSheetMngHstEntity - .syncState - .eq("DONE") - .or(mapSheetMngHstEntity.syncCheckState.eq("DONE"))) - .and(mapSheetMngFileEntity.fileExt.eq("tif"))) - .fetch(); - } + public List getMapSheetMngHst( + Integer year, String mapSheetScope, List mapSheetNum) { + BooleanBuilder whereBuilder = new BooleanBuilder(); - @Override - public List findFallbackCompareYearByMapSheets( - Integer year, List mapIds) { - BooleanBuilder builder = new BooleanBuilder(); - builder.and(mapSheetMngYearYnEntity.id.mapSheetNum.in(mapIds)); - builder.and(mapSheetMngYearYnEntity.id.mngYyyy.lt(year)); - builder.and(mapSheetMngYearYnEntity.yn.eq("Y")); + whereBuilder.and(mapSheetMngHstEntity.dataState.eq("DONE")); + whereBuilder.and( + mapSheetMngHstEntity + .syncState + .eq("DONE") + .or(mapSheetMngHstEntity.syncCheckState.eq("DONE"))); + whereBuilder.and(mapSheetMngHstEntity.useInference.eq("USE")); + + whereBuilder.and(mapSheetMngHstEntity.mngYyyy.eq(year)); + + BooleanBuilder likeBuilder = new BooleanBuilder(); + + if (MapSheetScope.PART.getId().equals(mapSheetScope)) { + List list = mapSheetNum; + if (list == null || list.isEmpty()) { + return List.of(); + } + + for (String prefix : list) { + if (prefix == null || prefix.isBlank()) { + continue; + } + likeBuilder.or(mapSheetMngHstEntity.mapSheetNum.like(prefix.trim() + "%")); + } + } + + if (likeBuilder.hasValue()) { + whereBuilder.and(likeBuilder); + } return queryFactory .select( Projections.constructor( - MapSheetFallbackYearDto.class, - mapSheetMngYearYnEntity.id.mapSheetNum, - mapSheetMngYearYnEntity.id.mngYyyy.max())) - .from(mapSheetMngYearYnEntity) - .where(builder) - .groupBy(mapSheetMngYearYnEntity.id.mapSheetNum) + MngListDto.class, + mapSheetMngHstEntity.mngYyyy, + mapSheetMngHstEntity.mapSheetNum, + mapSheetMngHstEntity.mapSheetName, + nullExpression(Integer.class), + nullExpression(Boolean.class))) + .from(mapSheetMngHstEntity) + .innerJoin(mapInkx5kEntity) + .on( + mapInkx5kEntity + .mapidcdNo + .eq(mapSheetMngHstEntity.mapSheetNum) + .and(mapInkx5kEntity.useInference.eq(CommonUseStatus.USE))) + .where(whereBuilder) .fetch(); } + + @Override + public List findFallbackCompareYearByMapSheets(Integer year, List mapIds) { + QMapSheetMngHstEntity h = QMapSheetMngHstEntity.mapSheetMngHstEntity; + QMapSheetMngHstEntity h2 = new QMapSheetMngHstEntity("h2"); + QMapSheetMngFileEntity f = QMapSheetMngFileEntity.mapSheetMngFileEntity; + QMapSheetMngFileEntity f2 = new QMapSheetMngFileEntity("f2"); + QMapInkx5kEntity inkk = QMapInkx5kEntity.mapInkx5kEntity; + + return queryFactory + .select( + Projections.constructor( + MngListDto.class, + h.mngYyyy, + h.mapSheetNum, + h.mapSheetName, + nullExpression(Integer.class), + nullExpression(Boolean.class))) + .from(h) + .innerJoin(inkk) + .on(inkk.mapidcdNo.eq(h.mapSheetNum).and(inkk.useInference.eq(CommonUseStatus.USE))) + .where( + h.mapSheetNum.in(mapIds), + h.mngYyyy.lt(year), + h.dataState.eq("DONE"), + h.useInference.eq("USE"), + h.syncState.eq("DONE").or(h.syncCheckState.eq("DONE")), + JPAExpressions.selectOne() + .from(f) + .where(f.hstUid.eq(h.hstUid), f.fileExt.eq("tif"), f.fileState.eq("DONE")) + .exists(), + + // mapSheetNum별 최대 mngYyyy인 행만 남김 + h.mngYyyy.eq( + JPAExpressions.select(h2.mngYyyy.max()) + .from(h2) + .where( + h2.mapSheetNum.eq(h.mapSheetNum), + h2.mngYyyy.lt(year), + h2.dataState.eq("DONE"), + h2.useInference.eq("USE"), + h2.syncState.eq("DONE").or(h2.syncCheckState.eq("DONE")), + JPAExpressions.selectOne() + .from(f2) + .where( + f2.hstUid.eq(h2.hstUid), + f2.fileExt.eq("tif"), + f2.fileState.eq("DONE")) + .exists()))) + .fetch(); + } + + @Override + public int countExecutableSheetsDistinct( + Integer year, String mapSheetScope, List mapSheetNum) { + + QMapSheetMngHstEntity h = QMapSheetMngHstEntity.mapSheetMngHstEntity; + QMapInkx5kEntity i = QMapInkx5kEntity.mapInkx5kEntity; + QMapSheetMngFileEntity f = QMapSheetMngFileEntity.mapSheetMngFileEntity; + + BooleanBuilder where = new BooleanBuilder(); + + // 실행가능 조건 + where.and(h.dataState.eq("DONE")); + where.and(h.syncState.eq("DONE").or(h.syncCheckState.eq("DONE"))); + where.and(h.useInference.eq("USE")); + where.and(h.mngYyyy.eq(year)); + + // tif + DONE 파일 존재 조건 AND EXISTS + where.and( + JPAExpressions.selectOne() + .from(f) + .where(f.hstUid.eq(h.hstUid).and(f.fileExt.eq("tif")).and(f.fileState.eq("DONE"))) + .exists()); + + // PART scope prefix 조건 + if (MapSheetScope.PART.getId().equals(mapSheetScope)) { + if (mapSheetNum == null || mapSheetNum.isEmpty()) { + return 0; + } + + BooleanBuilder likeBuilder = new BooleanBuilder(); + for (String prefix : mapSheetNum) { + if (prefix == null || prefix.isBlank()) continue; + likeBuilder.or(h.mapSheetNum.like(prefix.trim() + "%")); + } + + if (likeBuilder.hasValue()) { + where.and(likeBuilder); + } + } + + // DISTINCT mapSheetNum 기준 카운트 + Long count = + queryFactory + .select(h.mapSheetNum.countDistinct()) + .from(h) + .join(i) + .on(i.mapidcdNo.eq(h.mapSheetNum).and(i.useInference.eq(CommonUseStatus.USE))) + .where(where) + .fetchOne(); + + return count != null ? count.intValue() : 0; + } }