Rviewer add

This commit is contained in:
DanielLee
2026-01-14 09:42:08 +09:00
parent 56e7866d4f
commit b918ad14c4
16 changed files with 13017 additions and 8 deletions

View File

@@ -182,4 +182,163 @@ public class TrainingDataLabelApiController {
return ApiResponseDto.ok(
trainingDataLabelService.getDefaultPagingNumber(userId, size, assignmentUid));
}
@Operation(
summary = "새로운 polygon(들) 추가 저장",
description = "탐지결과 외 새로운 polygon을 추가로 저장합니다. 단일 또는 여러 개를 저장할 수 있습니다.")
@ApiResponses(
value = {
@ApiResponse(
responseCode = "200",
description = "저장 성공",
content =
@Content(
mediaType = "application/json",
schema = @Schema(implementation = ResponseObj.class))),
@ApiResponse(responseCode = "400", description = "잘못된 요청", content = @Content),
@ApiResponse(responseCode = "500", description = "서버 오류", content = @Content)
})
@PostMapping("/new-polygon")
public ApiResponseDto<ResponseObj> saveNewPolygon(
@io.swagger.v3.oas.annotations.parameters.RequestBody(
description = "새로운 polygon 저장 요청",
required = true,
content =
@Content(
mediaType = "application/json",
schema =
@Schema(implementation = TrainingDataLabelDto.NewPolygonRequest.class),
examples = {
@io.swagger.v3.oas.annotations.media.ExampleObject(
name = "1개 polygon 저장",
value =
"""
{
"assignmentUid": "4f9ebc8b-6635-4177-b42f-7efc9c7b4c02",
"analUid": 53,
"mapSheetNum": "35905086",
"compareYyyy": 2023,
"targetYyyy": 2024,
"features": [
{
"type": "Feature",
"geometry": {
"type": "Polygon",
"coordinates": [
[
[126.663, 34.588],
[126.662, 34.587],
[126.664, 34.589],
[126.663, 34.588]
]
]
},
"properties": {
"beforeClass": "WASTE",
"afterClass": "LAND"
}
}
]
}
"""),
@io.swagger.v3.oas.annotations.media.ExampleObject(
name = "3개 polygon 저장",
value =
"""
{
"assignmentUid": "4f9ebc8b-6635-4177-b42f-7efc9c7b4c02",
"analUid": 53,
"mapSheetNum": "35905086",
"compareYyyy": 2023,
"targetYyyy": 2024,
"features": [
{
"type": "Feature",
"geometry": {
"type": "Polygon",
"coordinates": [
[
[126.663, 34.588],
[126.662, 34.587],
[126.664, 34.589],
[126.663, 34.588]
]
]
},
"properties": {
"beforeClass": "WASTE",
"afterClass": "LAND"
}
},
{
"type": "Feature",
"geometry": {
"type": "Polygon",
"coordinates": [
[
[126.665, 34.590],
[126.664, 34.589],
[126.666, 34.591],
[126.665, 34.590]
]
]
},
"properties": {
"beforeClass": "FOREST",
"afterClass": "BUILDING"
}
},
{
"type": "Feature",
"geometry": {
"type": "Polygon",
"coordinates": [
[
[126.667, 34.592],
[126.666, 34.591],
[126.668, 34.593],
[126.667, 34.592]
]
]
},
"properties": {
"beforeClass": "FARMLAND",
"afterClass": "SOLAR_PANEL"
}
}
]
}
""")
}))
@RequestBody
TrainingDataLabelDto.NewPolygonRequest request) {
return ApiResponseDto.okObject(trainingDataLabelService.saveNewPolygon(request));
}
@Operation(summary = "COG 이미지 URL 조회", description = "변화 전/후 COG 이미지 URL을 함께 조회합니다")
@ApiResponses(
value = {
@ApiResponse(
responseCode = "200",
description = "조회 성공",
content =
@Content(
mediaType = "application/json",
schema =
@Schema(implementation = TrainingDataLabelDto.CogImageResponse.class))),
@ApiResponse(responseCode = "404", description = "이미지를 찾을 수 없음", content = @Content),
@ApiResponse(responseCode = "500", description = "서버 오류", content = @Content)
})
@GetMapping("/cog-image")
public ApiResponseDto<TrainingDataLabelDto.CogImageResponse> getCogImageUrl(
@Parameter(description = "도엽번호", required = true, example = "35905086") @RequestParam
String mapSheetNum,
@Parameter(description = "변화 전 년도", required = true, example = "2023") @RequestParam
Integer beforeYear,
@Parameter(description = "변화 후 년도", required = true, example = "2024") @RequestParam
Integer afterYear) {
return ApiResponseDto.ok(
trainingDataLabelService.getCogImageUrl(mapSheetNum, beforeYear, afterYear));
}
}

View File

@@ -0,0 +1,343 @@
package com.kamco.cd.kamcoback.trainingdata;
import com.kamco.cd.kamcoback.code.dto.CommonCodeDto;
import com.kamco.cd.kamcoback.config.api.ApiResponseDto;
import com.kamco.cd.kamcoback.config.api.ApiResponseDto.ResponseObj;
import com.kamco.cd.kamcoback.trainingdata.dto.TrainingDataReviewDto;
import com.kamco.cd.kamcoback.trainingdata.dto.TrainingDataReviewDto.ReviewGeometryInfo;
import com.kamco.cd.kamcoback.trainingdata.dto.TrainingDataReviewDto.ReviewListDto;
import com.kamco.cd.kamcoback.trainingdata.service.TrainingDataReviewService;
import io.swagger.v3.oas.annotations.Hidden;
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 lombok.RequiredArgsConstructor;
import org.springframework.data.domain.Page;
import org.springframework.web.bind.annotation.GetMapping;
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.RequestParam;
import org.springframework.web.bind.annotation.RestController;
@Tag(name = "라벨링 툴 > 검수자", description = "라벨링 툴 > 검수자 API")
@RestController
@RequiredArgsConstructor
@RequestMapping("/api/training-data/review")
public class TrainingDataReviewApiController {
private final TrainingDataReviewService trainingDataReviewService;
@Operation(summary = "목록 조회", description = "검수 할당 목록 조회")
@ApiResponses(
value = {
@ApiResponse(
responseCode = "200",
description = "조회 성공",
content =
@Content(
mediaType = "application/json",
schema = @Schema(implementation = CommonCodeDto.Basic.class))),
@ApiResponse(responseCode = "404", description = "코드를 찾을 수 없음", content = @Content),
@ApiResponse(responseCode = "500", description = "서버 오류", content = @Content)
})
@GetMapping
public ApiResponseDto<Page<ReviewListDto>> findReviewAssignedList(
@RequestParam(defaultValue = "0") int page,
@RequestParam(defaultValue = "20") int size,
@RequestParam(defaultValue = "01022223333") String userId) {
TrainingDataReviewDto.searchReq searchReq = new TrainingDataReviewDto.searchReq(page, size, "");
return ApiResponseDto.ok(trainingDataReviewService.findReviewAssignedList(searchReq, userId));
}
@Hidden
@Operation(summary = "상세 Geometry 조회", description = "검수 할당 상세 Geometry 조회")
@ApiResponses(
value = {
@ApiResponse(
responseCode = "200",
description = "조회 성공",
content =
@Content(
mediaType = "application/json",
schema = @Schema(implementation = CommonCodeDto.Basic.class))),
@ApiResponse(responseCode = "404", description = "코드를 찾을 수 없음", content = @Content),
@ApiResponse(responseCode = "500", description = "서버 오류", content = @Content)
})
@GetMapping("/geom-info")
public ApiResponseDto<ReviewGeometryInfo> findReviewAssignedGeom(
@RequestParam(defaultValue = "4f9ebc8b-6635-4177-b42f-7efc9c7b4c02") String operatorUid) {
return ApiResponseDto.ok(trainingDataReviewService.findReviewAssignedGeom(operatorUid));
}
@Operation(summary = "검수 결과 저장", description = "검수 결과 저장")
@ApiResponses(
value = {
@ApiResponse(
responseCode = "200",
description = "조회 성공",
content =
@Content(
mediaType = "application/json",
schema = @Schema(implementation = CommonCodeDto.Basic.class))),
@ApiResponse(responseCode = "404", description = "코드를 찾을 수 없음", content = @Content),
@ApiResponse(responseCode = "500", description = "서버 오류", content = @Content)
})
@PostMapping
public ApiResponseDto<ResponseObj> saveReviewFeature(
@RequestBody TrainingDataReviewDto.GeoFeatureRequest request) {
return ApiResponseDto.okObject(trainingDataReviewService.saveReviewFeature(request));
}
@Operation(summary = "작업 통계 조회", description = "검수자의 작업 현황 통계를 조회합니다. (전체/미작업/Today 건수)")
@ApiResponses(
value = {
@ApiResponse(
responseCode = "200",
description = "조회 성공",
content =
@Content(
mediaType = "application/json",
schema = @Schema(implementation = TrainingDataReviewDto.SummaryRes.class))),
@ApiResponse(responseCode = "400", description = "잘못된 요청", content = @Content),
@ApiResponse(responseCode = "500", description = "서버 오류", content = @Content)
})
@GetMapping("/summary")
public ApiResponseDto<TrainingDataReviewDto.SummaryRes> getSummary(
@io.swagger.v3.oas.annotations.Parameter(
description = "검수자 사번",
required = true,
example = "01022223333")
@RequestParam
String userId) {
try {
System.out.println("[Controller] getSummary called with userId: " + userId);
TrainingDataReviewDto.SummaryRes result = trainingDataReviewService.getSummary(userId);
System.out.println("[Controller] getSummary result: " + result);
return ApiResponseDto.ok(result);
} catch (Exception e) {
System.err.println("[Controller] getSummary ERROR: " + e.getMessage());
e.printStackTrace();
// 예외 발생 시에도 빈 통계 반환
return ApiResponseDto.ok(
TrainingDataReviewDto.SummaryRes.builder()
.totalCnt(0L)
.undoneCnt(0L)
.todayCnt(0L)
.build());
}
}
@Operation(summary = "변화탐지정보 및 실태조사결과 조회", description = "선택한 작업의 변화탐지정보 및 실태조사결과를 조회합니다.")
@ApiResponses(
value = {
@ApiResponse(
responseCode = "200",
description = "조회 성공",
content =
@Content(
mediaType = "application/json",
schema = @Schema(implementation = TrainingDataReviewDto.DetailRes.class))),
@ApiResponse(responseCode = "400", description = "잘못된 요청", content = @Content),
@ApiResponse(responseCode = "404", description = "데이터를 찾을 수 없음", content = @Content),
@ApiResponse(responseCode = "500", description = "서버 오류", content = @Content)
})
@GetMapping("/detail")
public ApiResponseDto<TrainingDataReviewDto.DetailRes> getDetail(
@io.swagger.v3.oas.annotations.Parameter(
description = "검수 작업 ID (UUID)",
required = true,
example = "93c56be8-0246-4b22-b976-2476549733cc")
@RequestParam
java.util.UUID operatorUid) {
return ApiResponseDto.ok(trainingDataReviewService.getDetail(operatorUid));
}
@Operation(summary = "검수자 기본 page number 제공", description = "검수자 기본 page number 제공")
@ApiResponses(
value = {
@ApiResponse(
responseCode = "200",
description = "조회 성공",
content =
@Content(
mediaType = "application/json",
schema = @Schema(implementation = TrainingDataReviewDto.DetailRes.class))),
@ApiResponse(responseCode = "400", description = "잘못된 요청", content = @Content),
@ApiResponse(responseCode = "404", description = "데이터를 찾을 수 없음", content = @Content),
@ApiResponse(responseCode = "500", description = "서버 오류", content = @Content)
})
@GetMapping("/default-page")
public ApiResponseDto<TrainingDataReviewDto.DefaultPaging> getDefaultPagingNumber(
@Parameter(description = "사번", example = "01022223333") @RequestParam String userId,
@Parameter(description = "페이징 사이즈", example = "20") @RequestParam(defaultValue = "20")
Integer size,
@Parameter(description = "개별 UUID", example = "79bcdbbe-6ed4-4caa-b4a4-22f3cf2f9d25")
@RequestParam(required = false)
String operatorUid) {
return ApiResponseDto.ok(
trainingDataReviewService.getDefaultPagingNumber(userId, size, operatorUid));
}
@Operation(
summary = "새로운 polygon(들) 추가 저장",
description = "탐지결과 외 새로운 polygon을 추가로 저장합니다. 단일 또는 여러 개를 저장할 수 있습니다.")
@ApiResponses(
value = {
@ApiResponse(
responseCode = "200",
description = "저장 성공",
content =
@Content(
mediaType = "application/json",
schema = @Schema(implementation = ResponseObj.class))),
@ApiResponse(responseCode = "400", description = "잘못된 요청", content = @Content),
@ApiResponse(responseCode = "500", description = "서버 오류", content = @Content)
})
@PostMapping("/new-polygon")
public ApiResponseDto<ResponseObj> saveNewPolygon(
@io.swagger.v3.oas.annotations.parameters.RequestBody(
description = "새로운 polygon 저장 요청",
required = true,
content =
@Content(
mediaType = "application/json",
schema =
@Schema(implementation = TrainingDataReviewDto.NewPolygonRequest.class),
examples = {
@io.swagger.v3.oas.annotations.media.ExampleObject(
name = "1개 polygon 저장",
value =
"""
{
"operatorUid": "93c56be8-0246-4b22-b976-2476549733cc",
"analUid": 53,
"mapSheetNum": "35905086",
"compareYyyy": 2023,
"targetYyyy": 2024,
"features": [
{
"type": "Feature",
"geometry": {
"type": "Polygon",
"coordinates": [
[
[126.663, 34.588],
[126.662, 34.587],
[126.664, 34.589],
[126.663, 34.588]
]
]
},
"properties": {
"beforeClass": "WASTE",
"afterClass": "LAND"
}
}
]
}
"""),
@io.swagger.v3.oas.annotations.media.ExampleObject(
name = "3개 polygon 저장",
value =
"""
{
"operatorUid": "93c56be8-0246-4b22-b976-2476549733cc",
"analUid": 53,
"mapSheetNum": "35905086",
"compareYyyy": 2023,
"targetYyyy": 2024,
"features": [
{
"type": "Feature",
"geometry": {
"type": "Polygon",
"coordinates": [
[
[126.663, 34.588],
[126.662, 34.587],
[126.664, 34.589],
[126.663, 34.588]
]
]
},
"properties": {
"beforeClass": "WASTE",
"afterClass": "LAND"
}
},
{
"type": "Feature",
"geometry": {
"type": "Polygon",
"coordinates": [
[
[126.665, 34.590],
[126.664, 34.589],
[126.666, 34.591],
[126.665, 34.590]
]
]
},
"properties": {
"beforeClass": "FOREST",
"afterClass": "BUILDING"
}
},
{
"type": "Feature",
"geometry": {
"type": "Polygon",
"coordinates": [
[
[126.667, 34.592],
[126.666, 34.591],
[126.668, 34.593],
[126.667, 34.592]
]
]
},
"properties": {
"beforeClass": "FARMLAND",
"afterClass": "SOLAR_PANEL"
}
}
]
}
""")
}))
@RequestBody
TrainingDataReviewDto.NewPolygonRequest request) {
return ApiResponseDto.okObject(trainingDataReviewService.saveNewPolygon(request));
}
@Operation(summary = "COG 이미지 URL 조회", description = "변화 전/후 COG 이미지 URL을 함께 조회합니다")
@ApiResponses(
value = {
@ApiResponse(
responseCode = "200",
description = "조회 성공",
content =
@Content(
mediaType = "application/json",
schema =
@Schema(implementation = TrainingDataReviewDto.CogImageResponse.class))),
@ApiResponse(responseCode = "404", description = "이미지를 찾을 수 없음", content = @Content),
@ApiResponse(responseCode = "500", description = "서버 오류", content = @Content)
})
@GetMapping("/cog-image")
public ApiResponseDto<TrainingDataReviewDto.CogImageResponse> getCogImageUrl(
@Parameter(description = "도엽번호", required = true, example = "35905086") @RequestParam
String mapSheetNum,
@Parameter(description = "변화 전 년도", required = true, example = "2023") @RequestParam
Integer beforeYear,
@Parameter(description = "변화 후 년도", required = true, example = "2024") @RequestParam
Integer afterYear) {
return ApiResponseDto.ok(
trainingDataReviewService.getCogImageUrl(mapSheetNum, beforeYear, afterYear));
}
}

View File

@@ -8,6 +8,7 @@ import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
import com.fasterxml.jackson.databind.node.ObjectNode;
import com.kamco.cd.kamcoback.common.utils.geometry.GeometryDeserializer;
import io.swagger.v3.oas.annotations.media.Schema;
import java.util.List;
import java.util.UUID;
import lombok.AllArgsConstructor;
import lombok.Builder;
@@ -298,8 +299,8 @@ public class TrainingDataLabelDto {
@Schema(description = "도엽 bbox")
private JsonNode mapBox;
@Schema(description = "라벨링 툴에서 그린 폴리곤")
private LearnDataGeometry learnGeometry;
@Schema(description = "라벨링 툴에서 그린 폴리곤들 (여러 개 가능)")
private List<LearnDataGeometry> learnGeometries;
}
@Schema(name = "ChangeDetectionInfo", description = "변화탐지정보")
@@ -394,4 +395,133 @@ public class TrainingDataLabelDto {
private int page;
private UUID assignmentUid;
}
@Schema(name = "NewPolygonRequest", description = "새로운 polygon(들) 추가 저장")
@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
public static class NewPolygonRequest {
@Schema(description = "assignmentUid", example = "4f9ebc8b-6635-4177-b42f-7efc9c7b4c02")
private String assignmentUid;
@Schema(description = "anal_uid", example = "1")
private Long analUid;
@Schema(description = "map_sheet_num (도엽번호)", example = "NI52-3-13-1")
private String mapSheetNum;
@Schema(description = "compare_yyyy (변화 전 년도)", example = "2022")
private Integer compareYyyy;
@Schema(description = "target_yyyy (변화 후 년도)", example = "2023")
private Integer targetYyyy;
@Schema(description = "새로 그린 polygon 리스트")
private List<PolygonFeature> features;
@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
public static class PolygonFeature {
@Schema(description = "type", example = "Feature")
private String type;
@JsonDeserialize(using = GeometryDeserializer.class)
@Schema(
description = "polygon geometry",
example =
"""
{
"type": "Polygon",
"coordinates": [
[
[
126.66292461969202,
34.58785236216609
],
[
126.66263801099049,
34.58740117447532
],
[
126.66293668521236,
34.5873904146878
],
[
126.66312820122245,
34.587841464427825
],
[
126.66289124481979,
34.58786048381633
],
[
126.66292461969202,
34.58785236216609
]
]
]
}
""")
private Geometry geometry;
@Schema(description = "polygon properties")
private PolygonProperties properties;
@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
public static class PolygonProperties {
@Schema(description = "beforeClass", example = "WASTE")
private String beforeClass;
@Schema(description = "afterClass", example = "LAND")
private String afterClass;
}
}
}
@Schema(name = "CogImageRequest", description = "COG 이미지 조회 요청")
@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
public static class CogImageRequest {
@Schema(description = "map_sheet_num (도엽번호)", example = "NI52-3-13-1", required = true)
private String mapSheetNum;
@Schema(description = "year (년도)", example = "2022", required = true)
private Integer year;
}
@Schema(name = "CogImageResponse", description = "COG 이미지 URL 응답")
@Getter
@Setter
@Builder
@NoArgsConstructor
@AllArgsConstructor
public static class CogImageResponse {
@Schema(description = "변화 전 COG 이미지 URL")
private String beforeCogUrl;
@Schema(description = "변화 후 COG 이미지 URL")
private String afterCogUrl;
@Schema(description = "변화 전 년도")
private Integer beforeYear;
@Schema(description = "변화 후 년도")
private Integer afterYear;
@Schema(description = "도엽번호")
private String mapSheetNum;
}
}

View File

@@ -0,0 +1,533 @@
package com.kamco.cd.kamcoback.trainingdata.dto;
import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
import com.fasterxml.jackson.databind.node.ObjectNode;
import com.kamco.cd.kamcoback.common.utils.geometry.GeometryDeserializer;
import io.swagger.v3.oas.annotations.media.Schema;
import java.util.List;
import java.util.UUID;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import org.locationtech.jts.geom.Geometry;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Sort;
public class TrainingDataReviewDto {
@Schema(name = "ReviewListDto", description = "ReviewListDto")
@Getter
@Setter
@NoArgsConstructor
public static class ReviewListDto {
private UUID operatorUid;
private Long inferenceGeomUid;
private String inspectorUid;
private String inspectState;
private String mapSheetNum;
private String mapIdNm;
private Long pnu;
public ReviewListDto(
UUID operatorUid,
Long inferenceGeomUid,
String inspectorUid,
String inspectState,
String mapSheetNum,
String mapIdNm,
Long pnu) {
this.operatorUid = operatorUid;
this.inferenceGeomUid = inferenceGeomUid;
this.inspectorUid = inspectorUid;
this.inspectState = inspectState;
this.mapSheetNum = mapSheetNum;
this.mapIdNm = mapIdNm;
this.pnu = pnu;
}
}
@Schema(name = "ReviewGeometryInfo", description = "ReviewGeometryInfo")
@Getter
@Setter
@NoArgsConstructor
public static class ReviewGeometryInfo {
private UUID operatorUid;
private Long inferenceGeomUid;
@JsonIgnore private String geomData; // json string
private JsonNode geom;
private String beforeCogUrl;
private String afterCogUrl;
@JsonIgnore private String mapBboxString; // json string
private JsonNode mapBbox;
public ReviewGeometryInfo(
UUID operatorUid,
Long inferenceGeomUid,
String geomData,
String beforeCogUrl,
String afterCogUrl,
String mapBboxString) {
this.operatorUid = operatorUid;
this.inferenceGeomUid = inferenceGeomUid;
this.beforeCogUrl = beforeCogUrl;
this.afterCogUrl = afterCogUrl;
ObjectMapper mapper = new ObjectMapper();
JsonNode geomJson;
JsonNode mapBboxJson;
try {
geomJson = mapper.readTree(geomData);
mapBboxJson = mapper.readTree(mapBboxString);
} catch (JsonProcessingException e) {
throw new RuntimeException(e);
}
this.geom = geomJson;
this.mapBbox = mapBboxJson;
}
}
@Schema(name = "GeoFeatureRequest", description = "검수 결과 저장")
@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
public static class GeoFeatureRequest {
@Schema(description = "operatorUid", example = "4f9ebc8b-6635-4177-b42f-7efc9c7b4c02")
private String operatorUid;
@Schema(description = "type", example = "Feature")
private String type;
@JsonDeserialize(using = GeometryDeserializer.class)
@Schema(
description = "검수 결과 polygon",
example =
"""
{
"type": "Polygon",
"coordinates": [
[
[
126.66292461969202,
34.58785236216609
],
[
126.66263801099049,
34.58740117447532
],
[
126.66293668521236,
34.5873904146878
],
[
126.66312820122245,
34.587841464427825
],
[
126.66289124481979,
34.58786048381633
],
[
126.66292461969202,
34.58785236216609
]
]
]
}
""")
private Geometry geometry;
private Properties properties;
@Getter
public static class Properties {
@Schema(description = "beforeClass", example = "WASTE")
private String beforeClass;
@Schema(description = "afterClass", example = "LAND")
private String afterClass;
@Schema(description = "inspectState", example = "COMPLETE")
private String inspectState;
@Schema(description = "inspectMemo", example = "검수 완료")
private String inspectMemo;
}
}
@Schema(name = "InferenceDataGeometry", description = "InferenceDataGeometry")
@Getter
@Setter
@NoArgsConstructor
public static class InferenceDataGeometry {
private String type;
@JsonIgnore private String learnGeomString;
private JsonNode geometry;
private InferenceProperties properties;
public InferenceDataGeometry(
String type, String learnGeomString, InferenceProperties properties) {
this.type = type;
this.properties = properties;
ObjectMapper mapper = new ObjectMapper();
JsonNode inferenceJson;
try {
inferenceJson = mapper.readTree(learnGeomString);
} catch (JsonProcessingException e) {
throw new RuntimeException(e);
}
this.geometry = inferenceJson;
if (inferenceJson.isObject()) {
((ObjectNode) inferenceJson).remove("crs");
}
}
@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
public static class InferenceProperties {
@Schema(description = "beforeClass", example = "WASTE")
private String beforeClass;
@Schema(description = "afterClass", example = "LAND")
private String afterClass;
}
}
@Schema(name = "LearnDataGeometry", description = "LearnDataGeometry")
@Getter
@Setter
@NoArgsConstructor
public static class LearnDataGeometry {
private String type;
@JsonIgnore private String learnGeomString;
private JsonNode geometry;
private LearnProperties properties;
public LearnDataGeometry(String type, String learnGeomString, LearnProperties properties) {
this.type = type;
this.properties = properties;
ObjectMapper mapper = new ObjectMapper();
JsonNode learnJson;
try {
learnJson = mapper.readTree(learnGeomString);
} catch (JsonProcessingException e) {
throw new RuntimeException(e);
}
this.geometry = learnJson;
if (learnJson.isObject()) {
((ObjectNode) learnJson).remove("crs");
}
}
@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
public static class LearnProperties {
@Schema(description = "beforeClass", example = "WASTE")
private String beforeClass;
@Schema(description = "afterClass", example = "LAND")
private String afterClass;
}
}
@Schema(name = "searchReq", description = "검색 요청")
@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
public static class searchReq {
// 페이징 파라미터
private int page = 0;
private int size = 20;
private String sort;
public Pageable toPageable() {
if (sort != null && !sort.isEmpty()) {
String[] sortParams = sort.split(",");
String property = sortParams[0];
Sort.Direction direction =
sortParams.length > 1 ? Sort.Direction.fromString(sortParams[1]) : Sort.Direction.ASC;
return PageRequest.of(page, size, Sort.by(direction, property));
}
return PageRequest.of(page, size);
}
}
@Schema(name = "DetailRes", description = "객체 상세 정보 응답")
@Getter
@Setter
@Builder
@NoArgsConstructor
@AllArgsConstructor
public static class DetailRes {
@Schema(description = "검수 작업 ID")
private UUID operatorUid;
@Schema(description = "변화탐지정보")
private ChangeDetectionInfo changeDetectionInfo;
@Schema(description = "실태조사결과정보")
private InspectionResultInfo inspectionResultInfo;
@Schema(description = "Geometry (GeoJSON)")
private InferenceDataGeometry geom;
@Schema(description = "변화 전 COG 이미지 URL")
private String beforeCogUrl;
@Schema(description = "변화 후 COG 이미지 URL")
private String afterCogUrl;
@Schema(description = "도엽 bbox")
private JsonNode mapBox;
@Schema(description = "검수 시 추가/수정한 폴리곤들 (여러 개 가능)")
private List<LearnDataGeometry> learnGeometries;
}
@Schema(name = "ChangeDetectionInfo", description = "변화탐지정보")
@Getter
@Setter
@Builder
@NoArgsConstructor
@AllArgsConstructor
public static class ChangeDetectionInfo {
@Schema(description = "도엽번호정보", example = "남해")
private String mapSheetInfo;
@Schema(description = "변화탐지연도", example = "2022-2023")
private String detectionYear;
@Schema(description = "변화 전 분류 정보")
private ClassificationInfo beforeClass;
@Schema(description = "변화 후 분류 정보")
private ClassificationInfo afterClass;
@Schema(description = "면적 (㎡)", example = "179.52")
private Double area;
@Schema(description = "탐지정확도 (%)", example = "84.8")
private Double detectionAccuracy;
@Schema(description = "PNU (필지고유번호)", example = "36221202306020")
private Long pnu;
}
@Schema(name = "ClassificationInfo", description = "분류정보")
@Getter
@Setter
@Builder
@NoArgsConstructor
@AllArgsConstructor
public static class ClassificationInfo {
@Schema(description = "분류", example = "일반토지")
private String classification;
@Schema(description = "확률", example = "80.0")
private Double probability;
}
@Schema(name = "InspectionResultInfo", description = "실태조사결과정보")
@Getter
@Setter
@Builder
@NoArgsConstructor
@AllArgsConstructor
public static class InspectionResultInfo {
@Schema(description = "검증결과 (미확인/제외/완료)", example = "미확인")
private String verificationResult;
@Schema(description = "부적합사유")
private String inappropriateReason;
@Schema(description = "메모")
private String memo;
}
@Schema(name = "SummaryRes", description = "작업 통계 응답")
@Getter
@Setter
@Builder
@NoArgsConstructor
@AllArgsConstructor
public static class SummaryRes {
@Schema(description = "전체 배정 건수", example = "8901")
private Long totalCnt;
@Schema(description = "미작업 건수 (UNCONFIRM 상태)", example = "7211")
private Long undoneCnt;
@Schema(description = "오늘 완료 건수", example = "0")
private Long todayCnt;
}
@Schema(name = "DefaultPaging", description = "페이징 기본 number, uuid 전달")
@Getter
@Setter
@Builder
@NoArgsConstructor
@AllArgsConstructor
public static class DefaultPaging {
private int page;
private UUID operatorUid;
}
@Schema(name = "NewPolygonRequest", description = "새로운 polygon(들) 추가 저장")
@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
public static class NewPolygonRequest {
@Schema(description = "operatorUid", example = "4f9ebc8b-6635-4177-b42f-7efc9c7b4c02")
private String operatorUid;
@Schema(description = "anal_uid", example = "53")
private Long analUid;
@Schema(description = "map_sheet_num (도엽번호)", example = "35905086")
private String mapSheetNum;
@Schema(description = "compare_yyyy (변화 전 년도)", example = "2023")
private Integer compareYyyy;
@Schema(description = "target_yyyy (변화 후 년도)", example = "2024")
private Integer targetYyyy;
@Schema(description = "새로 그린 polygon 리스트")
private List<PolygonFeature> features;
@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
public static class PolygonFeature {
@Schema(description = "type", example = "Feature")
private String type;
@JsonDeserialize(using = GeometryDeserializer.class)
@Schema(
description = "polygon geometry",
example =
"""
{
"type": "Polygon",
"coordinates": [
[
[
126.66292461969202,
34.58785236216609
],
[
126.66263801099049,
34.58740117447532
],
[
126.66293668521236,
34.5873904146878
],
[
126.66312820122245,
34.587841464427825
],
[
126.66289124481979,
34.58786048381633
],
[
126.66292461969202,
34.58785236216609
]
]
]
}
""")
private Geometry geometry;
@Schema(description = "polygon properties")
private PolygonProperties properties;
@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
public static class PolygonProperties {
@Schema(description = "beforeClass", example = "WASTE")
private String beforeClass;
@Schema(description = "afterClass", example = "LAND")
private String afterClass;
}
}
}
@Schema(name = "CogImageRequest", description = "COG 이미지 조회 요청")
@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
public static class CogImageRequest {
@Schema(description = "map_sheet_num (도엽번호)", example = "NI52-3-13-1", required = true)
private String mapSheetNum;
@Schema(description = "year (년도)", example = "2022", required = true)
private Integer year;
}
@Schema(name = "CogImageResponse", description = "COG 이미지 URL 응답")
@Getter
@Setter
@Builder
@NoArgsConstructor
@AllArgsConstructor
public static class CogImageResponse {
@Schema(description = "변화 전 COG 이미지 URL")
private String beforeCogUrl;
@Schema(description = "변화 후 COG 이미지 URL")
private String afterCogUrl;
@Schema(description = "변화 전 년도")
private Integer beforeYear;
@Schema(description = "변화 후 년도")
private Integer afterYear;
@Schema(description = "도엽번호")
private String mapSheetNum;
}
}

View File

@@ -3,6 +3,7 @@ package com.kamco.cd.kamcoback.trainingdata.service;
import com.kamco.cd.kamcoback.config.api.ApiResponseDto.ApiResponseCode;
import com.kamco.cd.kamcoback.config.api.ApiResponseDto.ResponseObj;
import com.kamco.cd.kamcoback.postgres.core.TrainingDataLabelCoreService;
import com.kamco.cd.kamcoback.trainingdata.dto.TrainingDataLabelDto;
import com.kamco.cd.kamcoback.trainingdata.dto.TrainingDataLabelDto.DefaultPaging;
import com.kamco.cd.kamcoback.trainingdata.dto.TrainingDataLabelDto.DetailRes;
import com.kamco.cd.kamcoback.trainingdata.dto.TrainingDataLabelDto.GeoFeatureRequest;
@@ -87,4 +88,30 @@ public class TrainingDataLabelService {
public DefaultPaging getDefaultPagingNumber(String userId, Integer size, String assignmentUid) {
return trainingDataLabelCoreService.getDefaultPagingNumber(userId, size, assignmentUid);
}
/**
* 새로운 polygon(들) 추가 저장
*
* @param request 새 polygon 정보 (여러 개 가능)
* @return 응답 메시지
*/
@Transactional
public ResponseObj saveNewPolygon(TrainingDataLabelDto.NewPolygonRequest request) {
trainingDataLabelCoreService.saveNewPolygon(request);
int count = request.getFeatures() != null ? request.getFeatures().size() : 0;
return new ResponseObj(ApiResponseCode.OK, count + "개의 polygon이 저장되었습니다.");
}
/**
* COG 이미지 URL 조회 (변화 전/후)
*
* @param mapSheetNum 도엽번호
* @param beforeYear 변화 전 년도
* @param afterYear 변화 후 년도
* @return 변화 전/후 COG 이미지 URL
*/
public TrainingDataLabelDto.CogImageResponse getCogImageUrl(
String mapSheetNum, Integer beforeYear, Integer afterYear) {
return trainingDataLabelCoreService.getCogImageUrl(mapSheetNum, beforeYear, afterYear);
}
}

View File

@@ -0,0 +1,119 @@
package com.kamco.cd.kamcoback.trainingdata.service;
import com.kamco.cd.kamcoback.config.api.ApiResponseDto.ApiResponseCode;
import com.kamco.cd.kamcoback.config.api.ApiResponseDto.ResponseObj;
import com.kamco.cd.kamcoback.postgres.core.TrainingDataReviewCoreService;
import com.kamco.cd.kamcoback.trainingdata.dto.TrainingDataReviewDto;
import com.kamco.cd.kamcoback.trainingdata.dto.TrainingDataReviewDto.DefaultPaging;
import com.kamco.cd.kamcoback.trainingdata.dto.TrainingDataReviewDto.DetailRes;
import com.kamco.cd.kamcoback.trainingdata.dto.TrainingDataReviewDto.GeoFeatureRequest;
import com.kamco.cd.kamcoback.trainingdata.dto.TrainingDataReviewDto.ReviewGeometryInfo;
import com.kamco.cd.kamcoback.trainingdata.dto.TrainingDataReviewDto.ReviewListDto;
import com.kamco.cd.kamcoback.trainingdata.dto.TrainingDataReviewDto.SummaryRes;
import com.kamco.cd.kamcoback.trainingdata.dto.TrainingDataReviewDto.searchReq;
import jakarta.transaction.Transactional;
import java.util.UUID;
import lombok.extern.slf4j.Slf4j;
import org.springframework.data.domain.Page;
import org.springframework.stereotype.Service;
@Slf4j
@Service
public class TrainingDataReviewService {
private final TrainingDataReviewCoreService trainingDataReviewCoreService;
public TrainingDataReviewService(TrainingDataReviewCoreService trainingDataReviewCoreService) {
this.trainingDataReviewCoreService = trainingDataReviewCoreService;
}
public Page<ReviewListDto> findReviewAssignedList(searchReq searchReq, String userId) {
return trainingDataReviewCoreService.findReviewAssignedList(searchReq, userId);
}
public ReviewGeometryInfo findReviewAssignedGeom(String operatorUid) {
return trainingDataReviewCoreService.findReviewAssignedGeom(operatorUid);
}
@Transactional
public ResponseObj saveReviewFeature(GeoFeatureRequest request) {
String status = "";
String operatorUid = request.getOperatorUid();
Long inferenceGeomUid = trainingDataReviewCoreService.findReviewOperatorGeoUid(operatorUid);
if (request.getGeometry() == null || request.getGeometry().isEmpty()) {
// EXCEPT 상태만 업데이트
status = "EXCEPT";
trainingDataReviewCoreService.updateReviewStateOperator(
operatorUid, status, request.getProperties().getInspectMemo());
trainingDataReviewCoreService.updateReviewExceptState(inferenceGeomUid, status);
} else {
status = "COMPLETE";
trainingDataReviewCoreService.updateReviewStateOperator(
operatorUid, status, request.getProperties().getInspectMemo());
trainingDataReviewCoreService.updateReviewPolygonClass(
inferenceGeomUid, request.getGeometry(), request.getProperties(), status);
}
return new ResponseObj(ApiResponseCode.OK, "저장되었습니다.");
}
/**
* 검수자별 작업 통계 조회
*
* @param userId 검수자 사번
* @return 전체/미작업/Today 건수
*/
public SummaryRes getSummary(String userId) {
try {
System.out.println("[Service] getSummary called with userId: " + userId);
SummaryRes result = trainingDataReviewCoreService.getSummary(userId);
System.out.println("[Service] getSummary result: " + result);
return result;
} catch (Exception e) {
System.err.println("[Service] getSummary ERROR: " + e.getMessage());
e.printStackTrace();
// 예외 발생 시에도 빈 통계 반환
return SummaryRes.builder().totalCnt(0L).undoneCnt(0L).todayCnt(0L).build();
}
}
/**
* 검수 작업 상세 정보 조회
*
* @param operatorUid 검수 작업 ID
* @return 변화탐지정보 + 실태조사결과정보
*/
public DetailRes getDetail(UUID operatorUid) {
return trainingDataReviewCoreService.getDetail(operatorUid);
}
public DefaultPaging getDefaultPagingNumber(String userId, Integer size, String operatorUid) {
return trainingDataReviewCoreService.getDefaultPagingNumber(userId, size, operatorUid);
}
/**
* 새로운 polygon(들) 추가 저장
*
* @param request 새 polygon 정보 (여러 개 가능)
* @return 응답 메시지
*/
@Transactional
public ResponseObj saveNewPolygon(TrainingDataReviewDto.NewPolygonRequest request) {
trainingDataReviewCoreService.saveNewPolygon(request);
int count = request.getFeatures() != null ? request.getFeatures().size() : 0;
return new ResponseObj(ApiResponseCode.OK, count + "개의 polygon이 저장되었습니다.");
}
/**
* COG 이미지 URL 조회 (변화 전/후)
*
* @param mapSheetNum 도엽번호
* @param beforeYear 변화 전 년도
* @param afterYear 변화 후 년도
* @return 변화 전/후 COG 이미지 URL
*/
public TrainingDataReviewDto.CogImageResponse getCogImageUrl(
String mapSheetNum, Integer beforeYear, Integer afterYear) {
return trainingDataReviewCoreService.getCogImageUrl(mapSheetNum, beforeYear, afterYear);
}
}