diff --git a/src/main/java/com/kamco/cd/kamcoback/changedetection/dto/ChangeDetectionDto.java b/src/main/java/com/kamco/cd/kamcoback/changedetection/dto/ChangeDetectionDto.java index c053ffb0..e045186e 100644 --- a/src/main/java/com/kamco/cd/kamcoback/changedetection/dto/ChangeDetectionDto.java +++ b/src/main/java/com/kamco/cd/kamcoback/changedetection/dto/ChangeDetectionDto.java @@ -137,6 +137,7 @@ public class ChangeDetectionDto { private Integer afterYear; // 비교년도 private Double afterConfidence; // 비교 신뢰도(확률) private String afterClass; + private Double cdProb; // 탐지정확도 } @Schema(name = "PointFeature", description = "Geometry 리턴 객체") @@ -177,5 +178,6 @@ public class ChangeDetectionDto { private Integer afterYear; // 비교년도 private Double afterConfidence; // 비교 신뢰도(확률) private String afterClass; // 비교 분류 + private Double cdProb; // 탐지 정확도 } } diff --git a/src/main/java/com/kamco/cd/kamcoback/changedetection/service/ChangeDetectionService.java b/src/main/java/com/kamco/cd/kamcoback/changedetection/service/ChangeDetectionService.java index f037f038..ba3450bb 100644 --- a/src/main/java/com/kamco/cd/kamcoback/changedetection/service/ChangeDetectionService.java +++ b/src/main/java/com/kamco/cd/kamcoback/changedetection/service/ChangeDetectionService.java @@ -5,7 +5,6 @@ import com.kamco.cd.kamcoback.changedetection.dto.ChangeDetectionDto; import com.kamco.cd.kamcoback.postgres.core.ChangeDetectionCoreService; import java.util.List; import lombok.RequiredArgsConstructor; -import org.springframework.cache.annotation.Cacheable; import org.springframework.stereotype.Service; @Service @@ -35,7 +34,6 @@ public class ChangeDetectionService { return changeDetectionCoreService.getChangeDetectionYearList(); } - @Cacheable(value = "changeDetectionPolygon", key = "#analUid + '_' + #mapSheetNum") public ChangeDetectionDto.PolygonFeatureList getChangeDetectionPolygonList( Long analUid, String mapSheetNum) { diff --git a/src/main/java/com/kamco/cd/kamcoback/code/CommonCodeApiController.java b/src/main/java/com/kamco/cd/kamcoback/code/CommonCodeApiController.java index 2c0f7815..d6eb6903 100644 --- a/src/main/java/com/kamco/cd/kamcoback/code/CommonCodeApiController.java +++ b/src/main/java/com/kamco/cd/kamcoback/code/CommonCodeApiController.java @@ -2,7 +2,10 @@ package com.kamco.cd.kamcoback.code; import com.kamco.cd.kamcoback.code.dto.CommonCodeDto; import com.kamco.cd.kamcoback.code.service.CommonCodeService; +import com.kamco.cd.kamcoback.common.enums.DetectionClassification; import com.kamco.cd.kamcoback.config.api.ApiResponseDto; +import com.kamco.cd.kamcoback.inference.dto.InferenceResultDto; +import com.kamco.cd.kamcoback.inference.dto.InferenceResultDto.Clazzes; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.media.Content; import io.swagger.v3.oas.annotations.media.Schema; @@ -10,6 +13,8 @@ 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.util.Arrays; +import java.util.Comparator; import java.util.List; import lombok.RequiredArgsConstructor; import org.springframework.web.bind.annotation.DeleteMapping; @@ -206,4 +211,16 @@ public class CommonCodeApiController { String code) { return ApiResponseDto.ok(commonCodeService.findByCode(code)); } + + @GetMapping("/clazz") + public ApiResponseDto> getClasses() { + + List list = + Arrays.stream(DetectionClassification.values()) + .sorted(Comparator.comparingInt(DetectionClassification::getOrder)) + .map(Clazzes::new) + .toList(); + + return ApiResponseDto.ok(list); + } } diff --git a/src/main/java/com/kamco/cd/kamcoback/common/api/SceneDemoApiController.java b/src/main/java/com/kamco/cd/kamcoback/common/api/SceneDemoApiController.java new file mode 100644 index 00000000..b70289a2 --- /dev/null +++ b/src/main/java/com/kamco/cd/kamcoback/common/api/SceneDemoApiController.java @@ -0,0 +1,46 @@ +package com.kamco.cd.kamcoback.common.api; + +import com.kamco.cd.kamcoback.config.api.ApiResponseDto; +import com.kamco.cd.kamcoback.inference.dto.InferenceResultDto.MapSheet; +import com.kamco.cd.kamcoback.inference.dto.LearningModelResultDto.BatchProcessResponse; +import com.kamco.cd.kamcoback.inference.service.InferenceResultService; +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 java.util.List; +import lombok.RequiredArgsConstructor; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +@RequiredArgsConstructor +@RestController +@RequestMapping("/demo/api/scene") +public class SceneDemoApiController { + + private final InferenceResultService inferenceResultService; + + @Operation(summary = "추론된 도엽 목록", description = "추론된 도엽 목록 5000:1") + @ApiResponses( + value = { + @ApiResponse( + responseCode = "200", + description = "검색 성공", + content = + @Content( + mediaType = "application/json", + schema = @Schema(implementation = BatchProcessResponse.class))), + @ApiResponse(responseCode = "400", description = "잘못된 요청", content = @Content), + @ApiResponse(responseCode = "500", description = "서버 오류", content = @Content) + }) + @GetMapping("/sheets/train-set/{id}") + public ApiResponseDto> listGetScenes5k( + @Parameter(description = "분석결과 id", example = "1") @PathVariable Long id) { + List mapSheets = inferenceResultService.listGetScenes5k(id); + return ApiResponseDto.ok(mapSheets); + } +} diff --git a/src/main/java/com/kamco/cd/kamcoback/common/enums/DetectionClassification.java b/src/main/java/com/kamco/cd/kamcoback/common/enums/DetectionClassification.java index a3a2bd7c..42d231bd 100644 --- a/src/main/java/com/kamco/cd/kamcoback/common/enums/DetectionClassification.java +++ b/src/main/java/com/kamco/cd/kamcoback/common/enums/DetectionClassification.java @@ -6,24 +6,25 @@ import lombok.Getter; @Getter @AllArgsConstructor public enum DetectionClassification { - BUILDING("building", "빌딩"), - CONTAINER("container", "컨테이너(창고·적재함)"), - FIELD("field", "경작지 / 들판"), - FOREST("forest", "숲"), - GRASS("grass", "초지 / 잔디지역"), - GREENHOUSE("greenhouse", "비닐하우스"), - LAND("land", "나지"), - ORCHARD("orchard", "과수원"), - ROAD("road", "도로"), - STONE("stone", "암석 / 돌 지역"), - TANK("tank", "탱크(저유탱크 등 저장시설)"), - TUMULUS("tumulus", "고분(무덤)"), - WASTE("waste", "폐기물 적치장 / 황폐지"), - WATER("water", "수체(水域) / 물"), - ETC("ETC", "기타"); // For 'etc' (miscellaneous/other) + BUILDING("building", "건물", 10), + CONTAINER("container", "컨테이너", 20), + FIELD("field", "경작지", 30), + FOREST("forest", "숲", 40), + GRASS("grass", "초지", 50), + GREENHOUSE("greenhouse", "비닐하우스", 60), + LAND("land", "일반토지", 70), + ORCHARD("orchard", "과수원", 80), + ROAD("road", "도로", 90), + STONE("stone", "모래/자갈", 100), + TANK("tank", "물탱크", 110), + TUMULUS("tumulus", "토분(무덤)", 120), + WASTE("waste", "폐기물", 130), + WATER("water", "물", 140), + ETC("ETC", "기타", 200); // For 'etc' (miscellaneous/other) private final String id; private final String desc; + private final int order; /** * Optional: Helper method to get the enum from a String, case-insensitive, or return ETC if not diff --git a/src/main/java/com/kamco/cd/kamcoback/config/RedisConfig.java b/src/main/java/com/kamco/cd/kamcoback/config/RedisConfig.java index f1dde5be..4f03f569 100644 --- a/src/main/java/com/kamco/cd/kamcoback/config/RedisConfig.java +++ b/src/main/java/com/kamco/cd/kamcoback/config/RedisConfig.java @@ -1,5 +1,11 @@ package com.kamco.cd.kamcoback.config; +import com.fasterxml.jackson.databind.DeserializationFeature; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.SerializationFeature; +import com.fasterxml.jackson.databind.jsontype.BasicPolymorphicTypeValidator; +import com.fasterxml.jackson.databind.jsontype.PolymorphicTypeValidator; +import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; import java.time.Duration; import org.springframework.beans.factory.annotation.Value; import org.springframework.cache.CacheManager; @@ -38,6 +44,17 @@ public class RedisConfig { return new LettuceConnectionFactory(redisConfig); } + @Bean + public ObjectMapper redisObjectMapper() { + ObjectMapper objectMapper = new ObjectMapper(); + objectMapper.registerModule(new JavaTimeModule()); + objectMapper.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS); + objectMapper.disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES); + objectMapper.findAndRegisterModules(); + + return objectMapper; + } + @Bean public RedisTemplate redisTemplate(RedisConnectionFactory connectionFactory) { RedisTemplate template = new RedisTemplate<>(); @@ -47,9 +64,11 @@ public class RedisConfig { template.setKeySerializer(new StringRedisSerializer()); template.setHashKeySerializer(new StringRedisSerializer()); - // Value는 JSON으로 직렬화 - template.setValueSerializer(new GenericJackson2JsonRedisSerializer()); - template.setHashValueSerializer(new GenericJackson2JsonRedisSerializer()); + // Value는 JSON으로 직렬화 (JavaTimeModule 포함) + GenericJackson2JsonRedisSerializer serializer = + new GenericJackson2JsonRedisSerializer(redisObjectMapper()); + template.setValueSerializer(serializer); + template.setHashValueSerializer(serializer); template.afterPropertiesSet(); return template; @@ -58,6 +77,25 @@ public class RedisConfig { // 기본 레디스 캐시 세팅 @Bean public CacheManager cacheManager(RedisConnectionFactory connectionFactory) { + ObjectMapper cacheObjectMapper = new ObjectMapper(); + cacheObjectMapper.registerModule(new JavaTimeModule()); + cacheObjectMapper.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS); + cacheObjectMapper.disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES); + cacheObjectMapper.findAndRegisterModules(); + + // 타입 정보 포함 - JAVA_LANG_OBJECT로 제한적으로 적용 + PolymorphicTypeValidator ptv = + BasicPolymorphicTypeValidator.builder() + .allowIfSubType("com.kamco.cd.kamcoback") + .allowIfSubType("org.springframework.data.domain") + .allowIfSubType("java.util") + .allowIfSubType("java.time") + .build(); + cacheObjectMapper.activateDefaultTyping(ptv, ObjectMapper.DefaultTyping.JAVA_LANG_OBJECT); + + GenericJackson2JsonRedisSerializer serializer = + new GenericJackson2JsonRedisSerializer(cacheObjectMapper); + RedisCacheConfiguration config = RedisCacheConfiguration.defaultCacheConfig() .entryTtl(Duration.ofHours(1)) // 기본 TTL 1시간 @@ -65,8 +103,7 @@ public class RedisConfig { RedisSerializationContext.SerializationPair.fromSerializer( new StringRedisSerializer())) .serializeValuesWith( - RedisSerializationContext.SerializationPair.fromSerializer( - new GenericJackson2JsonRedisSerializer())); + RedisSerializationContext.SerializationPair.fromSerializer(serializer)); return RedisCacheManager.builder(connectionFactory).cacheDefaults(config).build(); } diff --git a/src/main/java/com/kamco/cd/kamcoback/inference/InferenceResultApiV2Controller.java b/src/main/java/com/kamco/cd/kamcoback/inference/InferenceResultApiV2Controller.java new file mode 100644 index 00000000..8b01288e --- /dev/null +++ b/src/main/java/com/kamco/cd/kamcoback/inference/InferenceResultApiV2Controller.java @@ -0,0 +1,69 @@ +package com.kamco.cd.kamcoback.inference; + +import com.kamco.cd.kamcoback.config.api.ApiResponseDto; +import com.kamco.cd.kamcoback.inference.dto.InferenceResultDto; +import com.kamco.cd.kamcoback.inference.service.InferenceResultService; +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 java.util.List; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.data.domain.Page; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +@Tag(name = "추론관리 분석결과", description = "추론관리 분석결과") +@RequiredArgsConstructor +@RestController +@RequestMapping({"/v2/demo/inf/res"}) +@Slf4j +public class InferenceResultApiV2Controller { + + private final InferenceResultService inferenceResultService; + + @Operation(summary = "추론관리 분석결과 상세 목록 V2", description = "추론관리 분석결과 상세 목록 geojson 데이터 조회") + @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("/geom/{id}") + public ApiResponseDto> listInferenceResultWithGeom( + @Parameter(description = "분석결과 id", example = "1") @PathVariable Long id, + @Parameter(description = "기준년도 분류", example = "land") @RequestParam(required = false) + String targetClass, + @Parameter(description = "비교년도 분류", example = "waste") @RequestParam(required = false) + String compareClass, + @Parameter(description = "5000:1 도협번호 37801011,37801012") @RequestParam(required = false) + List mapSheetNum, + @Parameter(description = "페이지 번호 (0부터 시작)", example = "0") @RequestParam(defaultValue = "0") + int page, + @Parameter(description = "페이지 크기", example = "20") @RequestParam(defaultValue = "20") + int size, + @Parameter(description = "정렬 조건 (형식: 필드명,방향)", example = "name,asc") + @RequestParam(required = false) + String sort) { + InferenceResultDto.SearchGeoReq searchGeoReq = + new InferenceResultDto.SearchGeoReq( + targetClass, compareClass, mapSheetNum, page, size, sort); + + Page geomList = + inferenceResultService.listInferenceResultWithGeom(id, searchGeoReq); + return ApiResponseDto.ok(geomList); + } +} diff --git a/src/main/java/com/kamco/cd/kamcoback/inference/dto/InferenceResultDto.java b/src/main/java/com/kamco/cd/kamcoback/inference/dto/InferenceResultDto.java index e093b8c3..d70abb60 100644 --- a/src/main/java/com/kamco/cd/kamcoback/inference/dto/InferenceResultDto.java +++ b/src/main/java/com/kamco/cd/kamcoback/inference/dto/InferenceResultDto.java @@ -1,10 +1,13 @@ package com.kamco.cd.kamcoback.inference.dto; +import com.fasterxml.jackson.annotation.JsonIgnore; +import com.fasterxml.jackson.annotation.JsonInclude; import com.kamco.cd.kamcoback.common.enums.DetectionClassification; import com.kamco.cd.kamcoback.common.utils.interfaces.JsonFormatDttm; import io.swagger.v3.oas.annotations.media.Schema; import java.time.ZonedDateTime; import java.util.List; +import java.util.UUID; import lombok.AllArgsConstructor; import lombok.Getter; import lombok.NoArgsConstructor; @@ -173,6 +176,122 @@ public class InferenceResultDto { } } + // 분석 상세 ROW + @Getter + @AllArgsConstructor + public static class DetailListEntity { + + private Uid code; + private Double detectionScore; + private Clazzes compare; + private Clazzes target; + private MapSheet mapSheet; + private Coordinate center; + @JsonFormatDttm private ZonedDateTime updatedDttm; + + public DetailListEntity( + UUID uuid, + Double detectionScore, + Clazzes compare, + Clazzes target, + MapSheet mapSheet, + Coordinate center, + ZonedDateTime updatedDttm) { + this.code = new Uid(uuid); + this.detectionScore = detectionScore; + this.compare = compare; + this.target = target; + this.mapSheet = mapSheet; + this.center = center; + this.updatedDttm = updatedDttm; + } + } + + @Getter + @AllArgsConstructor + public static class Uid { + + private String shortCode; + private String code; + + public Uid(UUID uuid) { + if (uuid != null) { + this.shortCode = uuid.toString().substring(0, 8).toUpperCase(); + this.code = uuid.toString(); + } + } + } + + // MAP NO + @Getter + @AllArgsConstructor + public static class MapSheet { + + private String number; + private String name; + } + + // classification info + @Getter + public static class Clazz { + + private String code; + private String name; + @JsonIgnore private Double score; + + public Clazz(String code, Double score) { + this.code = code; + this.score = score; + this.name = DetectionClassification.fromString(code).getDesc(); + } + + public Clazz(String code) { + this.code = code; + this.name = DetectionClassification.fromString(code).getDesc(); + } + } + + // classification info + @Getter + public static class Clazzes { + + private DetectionClassification code; + private String name; + + @JsonInclude(JsonInclude.Include.NON_NULL) + private Double score; + + private Integer order; + + public Clazzes(DetectionClassification classification, Double score) { + this.code = classification; + this.name = classification.getDesc(); + this.order = classification.getOrder(); + this.score = score; + } + + public Clazzes(DetectionClassification classification) { + this.code = classification; + this.name = classification.getDesc(); + this.order = classification.getOrder(); + } + } + + // 좌표 정보 point + @Getter + public static class Coordinate { + + private Double lon; // 경도(Longitude) + private Double lat; // 위도(Latitude) + private String srid; // Spatial Reference ID의 약자로, 데이터베이스에서 좌표계를 식별하는 고유 번호 추후enum으로 + + public Coordinate(Double lon, Double lat) { + this.lon = lon; + this.lat = lat; + this.srid = "EPSG:4326"; + } + } + @Getter public static class Geom { 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 76490500..45a9ea82 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 @@ -3,7 +3,9 @@ package com.kamco.cd.kamcoback.inference.service; import com.kamco.cd.kamcoback.inference.dto.InferenceResultDto; import com.kamco.cd.kamcoback.inference.dto.InferenceResultDto.Dashboard; import com.kamco.cd.kamcoback.inference.dto.InferenceResultDto.Detail; +import com.kamco.cd.kamcoback.inference.dto.InferenceResultDto.MapSheet; import com.kamco.cd.kamcoback.postgres.core.InferenceResultCoreService; +import jakarta.validation.constraints.NotNull; import java.util.List; import lombok.RequiredArgsConstructor; import org.springframework.data.domain.Page; @@ -59,6 +61,18 @@ public class InferenceResultService { return inferenceResultCoreService.getInferenceResultGeomList(id, searchGeoReq); } + /** + * 분석결과 상세 목록 + * + * @param searchReq + * @return + */ + public Page listInferenceResultWithGeom( + @NotNull Long id, InferenceResultDto.SearchGeoReq searchReq) { + + return inferenceResultCoreService.listInferenceResultWithGeom(id, searchReq); + } + /** * 분석결과 상제 정보 Summary, DashBoard * @@ -87,4 +101,8 @@ public class InferenceResultService { public List getSheets(Long id) { return inferenceResultCoreService.getSheets(id).stream().map(String::valueOf).toList(); } + + public List listGetScenes5k(Long id) { + return inferenceResultCoreService.listGetScenes5k(id); + } } diff --git a/src/main/java/com/kamco/cd/kamcoback/postgres/core/InferenceResultCoreService.java b/src/main/java/com/kamco/cd/kamcoback/postgres/core/InferenceResultCoreService.java index bbf1ea90..c66d5138 100644 --- a/src/main/java/com/kamco/cd/kamcoback/postgres/core/InferenceResultCoreService.java +++ b/src/main/java/com/kamco/cd/kamcoback/postgres/core/InferenceResultCoreService.java @@ -2,18 +2,26 @@ package com.kamco.cd.kamcoback.postgres.core; import com.kamco.cd.kamcoback.inference.dto.InferenceResultDto; import com.kamco.cd.kamcoback.inference.dto.InferenceResultDto.Dashboard; +import com.kamco.cd.kamcoback.inference.dto.InferenceResultDto.MapSheet; +import com.kamco.cd.kamcoback.postgres.entity.MapInkx5kEntity; +import com.kamco.cd.kamcoback.postgres.entity.MapSheetAnalDataEntity; +import com.kamco.cd.kamcoback.postgres.entity.MapSheetAnalDataGeomEntity; import com.kamco.cd.kamcoback.postgres.repository.Inference.InferenceResultRepository; +import com.kamco.cd.kamcoback.postgres.repository.scene.MapInkx5kRepository; import jakarta.persistence.EntityNotFoundException; +import jakarta.validation.constraints.NotNull; import java.util.List; import lombok.RequiredArgsConstructor; import org.springframework.data.domain.Page; import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; @Service @RequiredArgsConstructor public class InferenceResultCoreService { private final InferenceResultRepository inferenceResultRepository; + private final MapInkx5kRepository mapInkx5kRepository; /** * 추론관리 > 분석결과 목록 조회 @@ -61,6 +69,27 @@ public class InferenceResultCoreService { return inferenceResultRepository.getInferenceGeomList(id, searchGeoReq); } + /** + * 분석결과 상세 목록 + * + * @param searchReq + * @return + */ + @Transactional(readOnly = true) + public Page listInferenceResultWithGeom( + @NotNull Long analyId, InferenceResultDto.SearchGeoReq searchReq) { + // 분석 ID 에 해당하는 dataids를 가져온다. + List dataIds = + inferenceResultRepository.listAnalyGeom(analyId).stream() + .mapToLong(MapSheetAnalDataEntity::getId) + .boxed() + .toList(); + // 해당데이터의 폴리곤데이터를 가져온다 + Page mapSheetAnalDataGeomEntities = + inferenceResultRepository.listInferenceResultWithGeom(dataIds, searchReq); + return mapSheetAnalDataGeomEntities.map(MapSheetAnalDataGeomEntity::toEntity); + } + /** * 추론된 5000:1 도엽 목록 * @@ -70,4 +99,17 @@ public class InferenceResultCoreService { public List getSheets(Long id) { return inferenceResultRepository.getSheets(id); } + + @Transactional(readOnly = true) + public List listGetScenes5k(Long analyId) { + List sceneCodes = + inferenceResultRepository.listAnalyGeom(analyId).stream() + .mapToLong(MapSheetAnalDataEntity::getMapSheetNum) + .mapToObj(String::valueOf) + .toList(); + + return mapInkx5kRepository.listGetScenes5k(sceneCodes).stream() + .map(MapInkx5kEntity::toEntity) + .toList(); + } } diff --git a/src/main/java/com/kamco/cd/kamcoback/postgres/entity/MapInkx5kEntity.java b/src/main/java/com/kamco/cd/kamcoback/postgres/entity/MapInkx5kEntity.java index 4e16d734..fe95b6d1 100644 --- a/src/main/java/com/kamco/cd/kamcoback/postgres/entity/MapInkx5kEntity.java +++ b/src/main/java/com/kamco/cd/kamcoback/postgres/entity/MapInkx5kEntity.java @@ -1,6 +1,14 @@ package com.kamco.cd.kamcoback.postgres.entity; -import jakarta.persistence.*; +import com.kamco.cd.kamcoback.inference.dto.InferenceResultDto; +import com.kamco.cd.kamcoback.inference.dto.InferenceResultDto.MapSheet; +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.SequenceGenerator; +import jakarta.persistence.Table; import lombok.Getter; import lombok.Setter; import org.locationtech.jts.geom.Geometry; @@ -10,6 +18,7 @@ import org.locationtech.jts.geom.Geometry; @Table(name = "tb_map_inkx_5k") @Entity public class MapInkx5kEntity { + @Id @GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "tb_map_inkx_5k_fid_seq_gen") @SequenceGenerator( @@ -29,4 +38,8 @@ public class MapInkx5kEntity { @Column(name = "fid_k50") private Long fidK50; + + public InferenceResultDto.MapSheet toEntity() { + return new MapSheet(mapidcdNo, mapidNm); + } } diff --git a/src/main/java/com/kamco/cd/kamcoback/postgres/entity/MapSheetAnalDataGeomEntity.java b/src/main/java/com/kamco/cd/kamcoback/postgres/entity/MapSheetAnalDataGeomEntity.java index 2eb3fd76..ac082886 100644 --- a/src/main/java/com/kamco/cd/kamcoback/postgres/entity/MapSheetAnalDataGeomEntity.java +++ b/src/main/java/com/kamco/cd/kamcoback/postgres/entity/MapSheetAnalDataGeomEntity.java @@ -1,14 +1,21 @@ package com.kamco.cd.kamcoback.postgres.entity; +import com.kamco.cd.kamcoback.common.enums.DetectionClassification; +import com.kamco.cd.kamcoback.inference.dto.InferenceResultDto; +import com.kamco.cd.kamcoback.inference.dto.InferenceResultDto.Clazzes; import jakarta.persistence.Column; import jakarta.persistence.Entity; +import jakarta.persistence.FetchType; import jakarta.persistence.GeneratedValue; import jakarta.persistence.GenerationType; import jakarta.persistence.Id; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.ManyToOne; import jakarta.persistence.SequenceGenerator; import jakarta.persistence.Table; import jakarta.validation.constraints.Size; import java.time.ZonedDateTime; +import java.util.UUID; import lombok.Getter; import lombok.Setter; import org.hibernate.annotations.ColumnDefault; @@ -31,6 +38,9 @@ public class MapSheetAnalDataGeomEntity { @Column(name = "geo_uid", nullable = false) private Long id; + @Column(name = "uuid") + private UUID uuid; + @Column(name = "cd_prob") private Double cdProb; @@ -51,6 +61,10 @@ public class MapSheetAnalDataGeomEntity { @Column(name = "map_sheet_num") private Long mapSheetNum; + @ManyToOne(fetch = FetchType.EAGER) + @JoinColumn(name = "map_5k_id", referencedColumnName = "fid") + private MapInkx5kEntity map5k; + @Column(name = "compare_yyyy") private Integer compareYyyy; @@ -133,4 +147,21 @@ public class MapSheetAnalDataGeomEntity { @Column(name = "geom_center", columnDefinition = "geometry") private Geometry geomCenter; + + public InferenceResultDto.DetailListEntity toEntity() { + DetectionClassification classification = DetectionClassification.fromString(classBeforeCd); + Clazzes comparedClazz = new Clazzes(classification, classBeforeProb); + DetectionClassification classification1 = DetectionClassification.fromString(classAfterCd); + Clazzes targetClazz = new Clazzes(classification1, classAfterProb); + InferenceResultDto.MapSheet mapSheet = map5k != null ? map5k.toEntity() : null; + + InferenceResultDto.Coordinate coordinate = null; + if (geomCenter != null) { + org.locationtech.jts.geom.Point point = (org.locationtech.jts.geom.Point) geomCenter; + coordinate = new InferenceResultDto.Coordinate(point.getX(), point.getY()); + } + + return new InferenceResultDto.DetailListEntity( + uuid, cdProb, comparedClazz, targetClazz, mapSheet, coordinate, createdDttm); + } } diff --git a/src/main/java/com/kamco/cd/kamcoback/postgres/entity/MapSheetAnalEntity.java b/src/main/java/com/kamco/cd/kamcoback/postgres/entity/MapSheetAnalEntity.java index 440af411..b2aeaf6d 100644 --- a/src/main/java/com/kamco/cd/kamcoback/postgres/entity/MapSheetAnalEntity.java +++ b/src/main/java/com/kamco/cd/kamcoback/postgres/entity/MapSheetAnalEntity.java @@ -9,7 +9,9 @@ import jakarta.persistence.SequenceGenerator; import jakarta.persistence.Table; import jakarta.validation.constraints.Size; import java.time.ZonedDateTime; +import lombok.AccessLevel; import lombok.Getter; +import lombok.NoArgsConstructor; import lombok.Setter; import org.hibernate.annotations.ColumnDefault; @@ -17,6 +19,9 @@ import org.hibernate.annotations.ColumnDefault; @Setter @Entity @Table(name = "tb_map_sheet_anal") +@NoArgsConstructor(access = AccessLevel.PROTECTED) +// TODO SETTER 제거 +// TODO 주석 public class MapSheetAnalEntity { @Id @@ -28,21 +33,24 @@ public class MapSheetAnalEntity { @Column(name = "anal_uid", nullable = false) private Long id; + // TODO UUID + // UK 추가 + @Column(name = "compare_yyyy") - private Integer compareYyyy; + private Integer compareYyyy; // 비교년도 @Column(name = "target_yyyy") - private Integer targetYyyy; + private Integer targetYyyy; // 기분년도 @Column(name = "model_uid") - private Long modelUid; + private Long modelUid; // 모델식별키 ? @Size(max = 100) @Column(name = "server_ids", length = 100) - private String serverIds; + private String serverIds; // 서버ID? @Column(name = "anal_map_sheet", length = Integer.MAX_VALUE) - private String analMapSheet; + private String analMapSheet; // 분석도엽? @Column(name = "anal_strt_dttm") private ZonedDateTime analStrtDttm; @@ -54,15 +62,15 @@ public class MapSheetAnalEntity { private Long analSec; @Column(name = "anal_pred_sec") - private Long analPredSec; + private Long analPredSec; // 예상소요초? @Size(max = 20) @Column(name = "anal_state", length = 20) - private String analState; + private String analState; // enum 으로 관리 @Size(max = 20) @Column(name = "gukyuin_used", length = 20) - private String gukyuinUsed; + private String gukyuinUsed; // Boolean으로 관리 @Column(name = "accuracy") private Double accuracy; @@ -71,17 +79,9 @@ public class MapSheetAnalEntity { @Column(name = "result_url") private String resultUrl; - @ColumnDefault("now()") - @Column(name = "created_dttm") - private ZonedDateTime createdDttm; - @Column(name = "created_uid") private Long createdUid; - @ColumnDefault("now()") - @Column(name = "updated_dttm") - private ZonedDateTime updatedDttm; - @Column(name = "updated_uid") private Long updatedUid; @@ -94,4 +94,13 @@ public class MapSheetAnalEntity { @Column(name = "base_map_sheet_num") private String baseMapSheetNum; + + // TODO CommonDateEntity ? + @ColumnDefault("now()") + @Column(name = "created_dttm") + private ZonedDateTime createdDttm; + + @ColumnDefault("now()") + @Column(name = "updated_dttm") + private ZonedDateTime updatedDttm; } diff --git a/src/main/java/com/kamco/cd/kamcoback/postgres/repository/Inference/InferenceResultRepositoryCustom.java b/src/main/java/com/kamco/cd/kamcoback/postgres/repository/Inference/InferenceResultRepositoryCustom.java index 629d4ef9..7b9ac305 100644 --- a/src/main/java/com/kamco/cd/kamcoback/postgres/repository/Inference/InferenceResultRepositoryCustom.java +++ b/src/main/java/com/kamco/cd/kamcoback/postgres/repository/Inference/InferenceResultRepositoryCustom.java @@ -2,6 +2,10 @@ package com.kamco.cd.kamcoback.postgres.repository.Inference; import com.kamco.cd.kamcoback.inference.dto.InferenceResultDto; import com.kamco.cd.kamcoback.inference.dto.InferenceResultDto.Dashboard; +import com.kamco.cd.kamcoback.inference.dto.InferenceResultDto.SearchGeoReq; +import com.kamco.cd.kamcoback.postgres.entity.MapSheetAnalDataEntity; +import com.kamco.cd.kamcoback.postgres.entity.MapSheetAnalDataGeomEntity; +import jakarta.validation.constraints.NotNull; import java.util.List; import java.util.Optional; import org.springframework.data.domain.Page; @@ -16,7 +20,12 @@ public interface InferenceResultRepositoryCustom { Page getInferenceGeomList( Long id, InferenceResultDto.SearchGeoReq searchGeoReq); + Page listInferenceResultWithGeom( + List dataIds, SearchGeoReq searchReq); + List getSheets(Long id); List getDashboard(Long id); + + List listAnalyGeom(@NotNull Long id); } diff --git a/src/main/java/com/kamco/cd/kamcoback/postgres/repository/Inference/InferenceResultRepositoryImpl.java b/src/main/java/com/kamco/cd/kamcoback/postgres/repository/Inference/InferenceResultRepositoryImpl.java index 8582414b..dfcab8d6 100644 --- a/src/main/java/com/kamco/cd/kamcoback/postgres/repository/Inference/InferenceResultRepositoryImpl.java +++ b/src/main/java/com/kamco/cd/kamcoback/postgres/repository/Inference/InferenceResultRepositoryImpl.java @@ -3,6 +3,8 @@ package com.kamco.cd.kamcoback.postgres.repository.Inference; import com.kamco.cd.kamcoback.inference.dto.InferenceResultDto; import com.kamco.cd.kamcoback.inference.dto.InferenceResultDto.Dashboard; import com.kamco.cd.kamcoback.inference.dto.InferenceResultDto.SearchGeoReq; +import com.kamco.cd.kamcoback.postgres.entity.MapSheetAnalDataEntity; +import com.kamco.cd.kamcoback.postgres.entity.MapSheetAnalDataGeomEntity; import com.kamco.cd.kamcoback.postgres.entity.QMapSheetAnalDataEntity; import com.kamco.cd.kamcoback.postgres.entity.QMapSheetAnalDataGeomEntity; import com.kamco.cd.kamcoback.postgres.entity.QMapSheetAnalEntity; @@ -10,17 +12,23 @@ import com.kamco.cd.kamcoback.postgres.entity.QMapSheetAnalSttcEntity; import com.kamco.cd.kamcoback.postgres.entity.QModelMngEntity; import com.kamco.cd.kamcoback.postgres.entity.QModelVerEntity; import com.querydsl.core.BooleanBuilder; +import com.querydsl.core.types.Order; +import com.querydsl.core.types.OrderSpecifier; import com.querydsl.core.types.Projections; +import com.querydsl.core.types.dsl.BooleanExpression; import com.querydsl.core.types.dsl.Expressions; import com.querydsl.jpa.JPAExpressions; import com.querydsl.jpa.JPQLQuery; +import com.querydsl.jpa.impl.JPAQuery; import com.querydsl.jpa.impl.JPAQueryFactory; +import java.util.ArrayList; import java.util.List; import java.util.Optional; import lombok.RequiredArgsConstructor; import org.springframework.data.domain.Page; import org.springframework.data.domain.PageImpl; import org.springframework.data.domain.Pageable; +import org.springframework.data.domain.Sort; import org.springframework.stereotype.Repository; @Repository @@ -80,7 +88,7 @@ public class InferenceResultRepositoryImpl implements InferenceResultRepositoryC .where(builder) .offset(pageable.getOffset()) .limit(pageable.getPageSize()) - .orderBy(mapSheetAnalEntity.createdDttm.desc()) + .orderBy(mapSheetAnalEntity.id.desc()) .fetch(); long total = @@ -159,6 +167,56 @@ public class InferenceResultRepositoryImpl implements InferenceResultRepositoryC .fetch(); } + @Override + public List listAnalyGeom(Long id) { + QMapSheetAnalDataEntity analy = QMapSheetAnalDataEntity.mapSheetAnalDataEntity; + return queryFactory.selectFrom(analy).where(analy.analUid.eq(id)).fetch(); + } + + /** + * 분석결과 상세 목록 + * + * @param searchReq + * @return + */ + @Override + public Page listInferenceResultWithGeom( + List ids, SearchGeoReq searchReq) { + + // 분석 차수 + QMapSheetAnalDataGeomEntity detectedEntity = + QMapSheetAnalDataGeomEntity.mapSheetAnalDataGeomEntity; + Pageable pageable = searchReq.toPageable(); + + // 검색조건 + JPAQuery query = + queryFactory + .selectFrom(detectedEntity) + .where( + detectedEntity.dataUid.in(ids), + eqTargetClass(detectedEntity, searchReq.getTargetClass()), + eqCompareClass(detectedEntity, searchReq.getCompareClass()), + containsMapSheetNum(detectedEntity, searchReq.getMapSheetNum())); + + // count + long total = query.fetchCount(); + + // Pageable에서 정렬 가져오기, 없으면 기본 정렬(createdDttm desc) 사용 + List> orders = getOrderSpecifiers(pageable.getSort()); + if (orders.isEmpty()) { + orders.add(detectedEntity.createdDttm.desc()); + } + + List content = + query + .offset(pageable.getOffset()) + .limit(pageable.getPageSize()) + .orderBy(orders.toArray(new OrderSpecifier[0])) + .fetch(); + + return new PageImpl<>(content, pageable, total); + } + /** * 분석결과 상세 목록 * @@ -252,4 +310,61 @@ public class InferenceResultRepositoryImpl implements InferenceResultRepositoryC .groupBy(mapSheetAnalDataEntity.mapSheetNum) .fetch(); } + + /** Pageable의 Sort를 QueryDSL OrderSpecifier로 변환 */ + @SuppressWarnings({"unchecked", "rawtypes"}) + private List> getOrderSpecifiers(Sort sort) { + List> orders = new ArrayList<>(); + + if (sort.isSorted()) { + QMapSheetAnalDataGeomEntity entity = QMapSheetAnalDataGeomEntity.mapSheetAnalDataGeomEntity; + + for (Sort.Order order : sort) { + Order direction = order.isAscending() ? Order.ASC : Order.DESC; + String property = order.getProperty(); + + // 유효한 필드만 처리 + switch (property) { + case "classBeforeCd" -> orders.add(new OrderSpecifier(direction, entity.classBeforeCd)); + case "classBeforeProb" -> + orders.add(new OrderSpecifier(direction, entity.classBeforeProb)); + case "classAfterCd" -> orders.add(new OrderSpecifier(direction, entity.classAfterCd)); + case "classAfterProb" -> orders.add(new OrderSpecifier(direction, entity.classAfterProb)); + case "mapSheetNum" -> orders.add(new OrderSpecifier(direction, entity.mapSheetNum)); + case "compareYyyy" -> orders.add(new OrderSpecifier(direction, entity.compareYyyy)); + case "targetYyyy" -> orders.add(new OrderSpecifier(direction, entity.targetYyyy)); + case "area" -> orders.add(new OrderSpecifier(direction, entity.area)); + case "createdDttm" -> orders.add(new OrderSpecifier(direction, entity.createdDttm)); + case "updatedDttm" -> orders.add(new OrderSpecifier(direction, entity.updatedDttm)); + // 유효하지 않은 필드는 무시 + default -> {} + } + } + } + + return orders; + } + + private BooleanExpression eqTargetClass( + QMapSheetAnalDataGeomEntity detectedEntity, String targetClass) { + return targetClass != null && !targetClass.isEmpty() + ? detectedEntity.classAfterCd.toLowerCase().eq(targetClass.toLowerCase()) + : null; + } + + private BooleanExpression eqCompareClass( + QMapSheetAnalDataGeomEntity detectedEntity, String compareClass) { + return compareClass != null && !compareClass.isEmpty() + ? detectedEntity.classBeforeCd.toLowerCase().eq(compareClass.toLowerCase()) + : null; + } + + private BooleanExpression containsMapSheetNum( + QMapSheetAnalDataGeomEntity detectedEntity, List mapSheet) { + if (mapSheet == null || mapSheet.isEmpty()) { + return null; + } + + return detectedEntity.mapSheetNum.in(mapSheet); + } } diff --git a/src/main/java/com/kamco/cd/kamcoback/postgres/repository/changedetection/ChangeDetectionRepositoryImpl.java b/src/main/java/com/kamco/cd/kamcoback/postgres/repository/changedetection/ChangeDetectionRepositoryImpl.java index 19c68005..cf700729 100644 --- a/src/main/java/com/kamco/cd/kamcoback/postgres/repository/changedetection/ChangeDetectionRepositoryImpl.java +++ b/src/main/java/com/kamco/cd/kamcoback/postgres/repository/changedetection/ChangeDetectionRepositoryImpl.java @@ -126,7 +126,8 @@ public class ChangeDetectionRepositoryImpl extends QuerydslRepositorySupport mapSheetAnalDataGeomEntity.classBeforeCd.toUpperCase(), mapSheetAnalDataGeomEntity.targetYyyy, mapSheetAnalDataGeomEntity.classAfterProb, - mapSheetAnalDataGeomEntity.classAfterCd.toUpperCase())) + mapSheetAnalDataGeomEntity.classAfterCd.toUpperCase(), + mapSheetAnalDataGeomEntity.cdProb)) .from(mapSheetAnalDataGeomEntity) .innerJoin(mapSheetAnalDataEntity) .on(mapSheetAnalDataGeomEntity.dataUid.eq(mapSheetAnalDataEntity.id)) @@ -159,7 +160,8 @@ public class ChangeDetectionRepositoryImpl extends QuerydslRepositorySupport data.getBeforeClass(), data.getAfterYear(), data.getAfterConfidence(), - data.getAfterClass()); + data.getAfterClass(), + data.getCdProb()); return new ChangeDetectionDto.PolygonFeature( data.getType(), jsonNode, properties); diff --git a/src/main/java/com/kamco/cd/kamcoback/postgres/repository/scene/MapInkx5kRepository.java b/src/main/java/com/kamco/cd/kamcoback/postgres/repository/scene/MapInkx5kRepository.java new file mode 100644 index 00000000..0e6e58ee --- /dev/null +++ b/src/main/java/com/kamco/cd/kamcoback/postgres/repository/scene/MapInkx5kRepository.java @@ -0,0 +1,7 @@ +package com.kamco.cd.kamcoback.postgres.repository.scene; + +import com.kamco.cd.kamcoback.postgres.entity.MapInkx5kEntity; +import org.springframework.data.jpa.repository.JpaRepository; + +public interface MapInkx5kRepository + extends JpaRepository, MapInkx5kRepositoryCustom {} diff --git a/src/main/java/com/kamco/cd/kamcoback/postgres/repository/scene/MapInkx5kRepositoryCustom.java b/src/main/java/com/kamco/cd/kamcoback/postgres/repository/scene/MapInkx5kRepositoryCustom.java new file mode 100644 index 00000000..e509aaeb --- /dev/null +++ b/src/main/java/com/kamco/cd/kamcoback/postgres/repository/scene/MapInkx5kRepositoryCustom.java @@ -0,0 +1,9 @@ +package com.kamco.cd.kamcoback.postgres.repository.scene; + +import com.kamco.cd.kamcoback.postgres.entity.MapInkx5kEntity; +import java.util.List; + +public interface MapInkx5kRepositoryCustom { + + List listGetScenes5k(List codes); +} diff --git a/src/main/java/com/kamco/cd/kamcoback/postgres/repository/scene/MapInkx5kRepositoryImpl.java b/src/main/java/com/kamco/cd/kamcoback/postgres/repository/scene/MapInkx5kRepositoryImpl.java new file mode 100644 index 00000000..21fe09ea --- /dev/null +++ b/src/main/java/com/kamco/cd/kamcoback/postgres/repository/scene/MapInkx5kRepositoryImpl.java @@ -0,0 +1,28 @@ +package com.kamco.cd.kamcoback.postgres.repository.scene; + +import com.kamco.cd.kamcoback.postgres.entity.MapInkx5kEntity; +import com.kamco.cd.kamcoback.postgres.entity.QMapInkx5kEntity; +import com.querydsl.jpa.impl.JPAQueryFactory; +import java.util.List; +import org.springframework.data.jpa.repository.support.QuerydslRepositorySupport; + +public class MapInkx5kRepositoryImpl extends QuerydslRepositorySupport + implements MapInkx5kRepositoryCustom { + + private final JPAQueryFactory queryFactory; + + public MapInkx5kRepositoryImpl(JPAQueryFactory queryFactory) { + super(MapInkx5kEntity.class); + this.queryFactory = queryFactory; + } + + public List listGetScenes5k(List codes) { + QMapInkx5kEntity map5k = QMapInkx5kEntity.mapInkx5kEntity; + + return queryFactory + .selectFrom(map5k) + .where(map5k.mapidcdNo.in(codes)) + .orderBy(map5k.mapidcdNo.asc()) + .fetch(); + } +}