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 index 53c97490..8fb0f69b 100644 --- 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 @@ -5,7 +5,11 @@ 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.HashSet; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Objects; +import java.util.Set; import java.util.stream.Collectors; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; @@ -13,8 +17,7 @@ import org.springframework.http.HttpStatus; import org.springframework.web.server.ResponseStatusException; /** - * GeoJSON 파일의 "features[].properties.scene_id" 값들이 "요청한 도엽번호 목록(requestedMapSheetNums)"과 정확히 일치하는지 - * 검증하는 유틸. + * GeoJSON 파일의 "features[].properties.scene_id" 값들이 "요청한 도엽번호 목록(requestedMapSheetNums)"과 정확히 일치하는지 검증하는 유틸. * *

핵심 목적: - 요청한 도엽번호를 기반으로 GeoJSON을 생성했는데, 실제 결과 파일에 누락/추가/중복/빈값(scene_id 없음) 등이 발생했는지 빠르게 잡아내기. * @@ -23,19 +26,22 @@ import org.springframework.web.server.ResponseStatusException; */ public class GeoJsonValidator { - /** GeoJSON 파싱용 ObjectMapper (정적 1개로 재사용) */ + /** + * GeoJSON 파싱용 ObjectMapper (정적 1개로 재사용) + */ private static final ObjectMapper om = new ObjectMapper(); - /** 로그 출력용 */ + /** + * 로그 출력용 + */ private static final Logger log = LogManager.getLogger(GeoJsonValidator.class); /** - * @param geojsonPath GeoJSON 파일 경로(문자열) + * @param geojsonPath GeoJSON 파일 경로(문자열) * @param requestedMapSheetNums "요청한 도엽번호" 리스트 (중복/공백/NULL 포함 가능) - *

동작 개요: 1) 파일 존재/크기 검증 2) 요청 도엽번호 목록 정리(Trim + 공백 제거 + 중복 제거) 3) GeoJSON 파싱 후 features 배열 - * 확보 4) features에서 scene_id 추출하여 유니크 set 구성 5) requested vs found 비교: - missing: requested - - * found - extra : found - requested - duplicates: GeoJSON 내부에서 scene_id 중복 등장 - nullIdCount: - * scene_id가 null/blank 인 feature 개수 6) 이상 있으면 422로 실패 처리 + *

동작 개요: 1) 파일 존재/크기 검증 2) 요청 도엽번호 목록 정리(Trim + 공백 제거 + 중복 제거) 3) GeoJSON 파싱 후 features 배열 + * 확보 4) features에서 scene_id 추출하여 유니크 set 구성 5) requested vs found 비교: - missing: requested - found - extra : found - requested - duplicates: GeoJSON + * 내부에서 scene_id 중복 등장 - nullIdCount: scene_id가 null/blank 인 feature 개수 6) 이상 있으면 422로 실패 처리 */ public static void validateWithRequested(String geojsonPath, List requestedMapSheetNums) { @@ -51,19 +57,19 @@ public class GeoJsonValidator { // 파일 존재 여부 체크 (없으면 404) if (!Files.exists(path)) { throw new ResponseStatusException( - HttpStatus.NOT_FOUND, "GeoJSON 파일이 존재하지 않습니다: " + geojsonPath); + HttpStatus.NOT_FOUND, "GeoJSON 파일이 존재하지 않습니다: " + geojsonPath); } // 파일 사이즈 체크 (0 byte면 400) if (Files.size(path) == 0) { throw new ResponseStatusException( - HttpStatus.BAD_REQUEST, "GeoJSON 파일이 비어있습니다: " + geojsonPath); + HttpStatus.BAD_REQUEST, "GeoJSON 파일이 비어있습니다: " + geojsonPath); } } catch (IOException e) { // 파일 사이즈/상태 확인 중 IO 오류면 서버오류로 처리 log.error("GeoJSON 파일 상태 확인 실패: path={}", path, e); throw new ResponseStatusException( - HttpStatus.INTERNAL_SERVER_ERROR, "GeoJSON 파일 상태 확인 실패: " + geojsonPath, e); + HttpStatus.INTERNAL_SERVER_ERROR, "GeoJSON 파일 상태 확인 실패: " + geojsonPath, e); } // ========================================================= @@ -82,16 +88,16 @@ public class GeoJsonValidator { // - LinkedHashSet 사용: "중복 제거 + 원래 입력 순서 유지" // ========================================================= Set requested = - requestedMapSheetNums.stream() - .filter(Objects::nonNull) // null 제거 - .map(String::trim) // 앞뒤 공백 제거 - .filter(s -> !s.isEmpty()) // "" 제거 - .collect(Collectors.toCollection(LinkedHashSet::new)); // 중복 제거 + 순서 유지 + requestedMapSheetNums.stream() + .filter(Objects::nonNull) // null 제거 + .map(String::trim) // 앞뒤 공백 제거 + .filter(s -> !s.isEmpty()) // "" 제거 + .collect(Collectors.toCollection(LinkedHashSet::new)); // 중복 제거 + 순서 유지 // 정리 결과가 비어있으면(전부 null/공백)이므로 400 if (requested.isEmpty()) { throw new ResponseStatusException( - HttpStatus.BAD_REQUEST, "requestedMapSheetNums 가 공백/NULL만 포함합니다."); + HttpStatus.BAD_REQUEST, "requestedMapSheetNums 가 공백/NULL만 포함합니다."); } // ========================================================= @@ -117,7 +123,7 @@ public class GeoJsonValidator { // features가 없거나 배열이 아니면 GeoJSON 구조가 이상한 것 if (features == null || !features.isArray()) { throw new ResponseStatusException( - HttpStatus.BAD_REQUEST, "유효하지 않은 GeoJSON: features가 없거나 배열이 아닙니다."); + HttpStatus.BAD_REQUEST, "유효하지 않은 GeoJSON: features가 없거나 배열이 아닙니다."); } } catch (ResponseStatusException e) { // 위에서 직접 던진 에러는 그대로 전달 @@ -126,12 +132,12 @@ public class GeoJsonValidator { // 읽기/파싱 과정에서 IO 문제가 터지면 서버오류 log.error("GeoJSON 파일 읽기/파싱 실패: path={}", path, e); throw new ResponseStatusException( - HttpStatus.INTERNAL_SERVER_ERROR, "GeoJSON 파일 읽기/파싱 실패: " + geojsonPath, e); + HttpStatus.INTERNAL_SERVER_ERROR, "GeoJSON 파일 읽기/파싱 실패: " + geojsonPath, e); } catch (Exception e) { // JSON 문법 오류/예상치 못한 파싱 오류는 클라이언트 입력/파일 자체 문제로 400 처리 log.error("GeoJSON 파싱 오류(비정상 JSON): path={}", path, e); throw new ResponseStatusException( - HttpStatus.BAD_REQUEST, "GeoJSON 파싱 오류(비정상 JSON): " + geojsonPath, e); + HttpStatus.BAD_REQUEST, "GeoJSON 파싱 오류(비정상 JSON): " + geojsonPath, e); } // ========================================================= @@ -170,7 +176,7 @@ public class GeoJsonValidator { // properties가 있고 scene_id가 null이 아니면 텍스트로 읽음 // 없으면 null 처리 String sceneId = - (props != null && props.hasNonNull("scene_id")) ? props.get("scene_id").asText() : null; + (props != null && props.hasNonNull("scene_id")) ? props.get("scene_id").asText().trim() : null; // scene_id가 없거나 빈값이면 "정상적으로 도엽번호가 들어오지 않은 feature"로 카운트 if (sceneId == null || sceneId.isBlank()) { @@ -178,6 +184,7 @@ public class GeoJsonValidator { continue; } + foundUnique.add(sceneId); // foundUnique.add(sceneId)가 false면 "이미 같은 값이 있었다"는 뜻 => 중복 // if (!foundUnique.add(sceneId)) { // duplicates.add(sceneId); @@ -208,34 +215,38 @@ public class GeoJsonValidator { // - sample 로그는 너무 길어질 수 있으므로 limit 걸어줌 // ========================================================= 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, // 중복 포함한 전체 feature 수 - requested.size(), // 요청 도엽 유니크 수 - foundUnique.size(), // GeoJSON에서 발견된 scene_id 유니크 수 - nullIdCount, // scene_id가 비어있는 feature 수 - 0, // 중복 scene_id 종류 수 - missing.size(), // 요청했지만 빠진 도엽 수 - extra.size()); // 요청하지 않았는데 들어온 도엽 수 + """ + ===== 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, // 중복 포함한 전체 feature 수 + requested.size(), // 요청 도엽 유니크 수 + foundUnique.size(), // GeoJSON에서 발견된 scene_id 유니크 수 + nullIdCount, // scene_id가 비어있는 feature 수 + 0, // 중복 scene_id 종류 수 + 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 (!missing.isEmpty()) { + log.warn("missing sample: {}", missing.stream().limit(50).toList()); + } - if (!extra.isEmpty()) log.warn("extra sample: {}", extra.stream().limit(50).toList()); + if (!extra.isEmpty()) { + log.warn("extra sample: {}", extra.stream().limit(50).toList()); + } // ========================================================= // 6) 실패 조건 판정 @@ -252,10 +263,10 @@ public class GeoJsonValidator { // ========================================================= if (!missing.isEmpty() || !extra.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(), 0, nullIdCount)); + HttpStatus.UNPROCESSABLE_ENTITY, + String.format( + "GeoJSON validation failed: missing=%d, extra=%d, duplicates=%d, nullId=%d", + missing.size(), extra.size(), 0, nullIdCount)); } // 모든 조건을 통과하면 정상