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> findReviewAssignedList( @RequestParam(defaultValue = "0") int page, @RequestParam(defaultValue = "20") int size, @RequestParam(defaultValue = "K20251212001") 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 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 saveReviewFeature( @RequestBody TrainingDataReviewDto.GeoFeatureRequest request) { return ApiResponseDto.ok(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 getSummary( @io.swagger.v3.oas.annotations.Parameter( description = "검수자 사번", required = true, example = "K20251212001") @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 = "선택한 작업의 변화탐지정보 및 실태조사결과를 조회합니다. 저장된 여러 개의 polygon을 조회할 수 있습니다.") @ApiResponses( value = { @ApiResponse( responseCode = "200", description = "조회 성공", content = @Content( mediaType = "application/json", schema = @Schema(implementation = TrainingDataReviewDto.DetailRes.class), examples = { @io.swagger.v3.oas.annotations.media.ExampleObject( name = "단일 polygon 조회", description = "1개의 polygon이 저장된 경우 응답 예시", value = """ { "code": "OK", "message": null, "data": { "operatorUid": "4f9ebc8b-6635-4177-b42f-7efc9c7b4c02", "changeDetectionInfo": { "mapSheetInfo": "NI52-3-13-1", "detectionYear": "2023-2024", "beforeClass": { "classification": "waste", "probability": 0.95 }, "afterClass": { "classification": "land", "probability": 0.98 }, "area": 1250.5, "detectionAccuracy": 0.96, "pnu": 1234567890 }, "inspectionResultInfo": { "verificationResult": "완료", "inappropriateReason": "" }, "geom": { "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" } }, "beforeCogUrl": "https://storage.example.com/cog/2023/NI52-3-13-1.tif", "afterCogUrl": "https://storage.example.com/cog/2024/NI52-3-13-1.tif", "mapBox": { "type": "Polygon", "coordinates": [[[126.65, 34.58], [126.67, 34.58], [126.67, 34.60], [126.65, 34.60], [126.65, 34.58]]] }, "learnGeometries": [ { "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 = "여러 polygon 조회", description = "3개의 polygon이 저장된 경우 응답 예시", value = """ { "code": "OK", "message": null, "data": { "operatorUid": "4f9ebc8b-6635-4177-b42f-7efc9c7b4c02", "changeDetectionInfo": { "mapSheetInfo": "NI52-3-13-1", "detectionYear": "2023-2024", "beforeClass": { "classification": "waste", "probability": 0.95 }, "afterClass": { "classification": "land", "probability": 0.98 }, "area": 1250.5, "detectionAccuracy": 0.96, "pnu": 1234567890 }, "inspectionResultInfo": { "verificationResult": "완료", "inappropriateReason": "" }, "geom": { "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" } }, "beforeCogUrl": "https://storage.example.com/cog/2023/NI52-3-13-1.tif", "afterCogUrl": "https://storage.example.com/cog/2024/NI52-3-13-1.tif", "mapBox": { "type": "Polygon", "coordinates": [[[126.65, 34.58], [126.67, 34.58], [126.67, 34.60], [126.65, 34.60], [126.65, 34.58]]] }, "learnGeometries": [ { "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.585], [126.664, 34.584], [126.666, 34.586], [126.665, 34.585]]] }, "properties": { "beforeClass": "forest", "afterClass": "building" } }, { "type": "Feature", "geometry": { "type": "Polygon", "coordinates": [[[126.660, 34.590], [126.659, 34.589], [126.661, 34.591], [126.660, 34.590]]] }, "properties": { "beforeClass": "grassland", "afterClass": "concrete" } } ] } } """) })), @ApiResponse(responseCode = "400", description = "잘못된 요청", content = @Content), @ApiResponse(responseCode = "404", description = "데이터를 찾을 수 없음", content = @Content), @ApiResponse(responseCode = "500", description = "서버 오류", content = @Content) }) @GetMapping("/detail") public ApiResponseDto getDetail( @io.swagger.v3.oas.annotations.Parameter( description = "검수 작업 ID (UUID)", required = true, example = "da9d026c-a67a-4d2d-a05a-6cc924372795") @RequestParam(defaultValue = "da9d026c-a67a-4d2d-a05a-6cc924372795") java.util.UUID operatorUid) { return ApiResponseDto.ok(trainingDataReviewService.getDetail(operatorUid)); } @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("/default-page") public ApiResponseDto getDefaultPagingNumber( @Parameter(description = "사번", example = "K20251212001") @RequestParam String userId, @Parameter(description = "페이징 사이즈", example = "20") @RequestParam(defaultValue = "20") Integer size, @Parameter(description = "개별 UUID", example = "da9d026c-a67a-4d2d-a05a-6cc924372795") @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 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": [ [ [255968.87499999875, 522096.0392135622], [255969.3749999955, 522097.7892135601], [255973.8750000003, 522103.53921356186], [255976.62499999997, 522108.53921356145], [255978.9607864372, 522110.87500000006], [255980.7107864374, 522111.624999999], [255982.53921356448, 522111.6250000003], [255988.37499999587, 522109.87499999726], [255988.37499999927, 522108.28921356244], [255990.12499999776, 522106.5392135628], [255990.87499999846, 522105.039213567], [255990.87500000012, 522102.4607864364], [255990.12499999718, 522100.4607864384], [255988.37499999863, 522097.46078643657], [255988.37499999968, 522096.46078643645], [255983.28921356171, 522092.8749999984], [255979.5392135627, 522088.37499999907], [255978.53921355822, 522087.8750000016], [255974.46078643817, 522087.874999998], [255971.21078643805, 522088.87499999977], [255969.87500000134, 522090.21078643604], [255968.87499999907, 522092.21078643575], [255968.87499999875, 522096.0392135622] ] ] }, "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": [ [ [168526.71078643727, 544547.3749999998], [168527.71078643794, 544548.3749999991], [168530.28921356675, 544548.6249999998], [168538.53921356046, 544547.1250000003], [168547.28921356154, 544547.374999999], [168549.53921357248, 544545.1250000008], [168526.9607864362, 544544.6249999998], [168525.87500000178, 544545.710786436], [168525.87499999133, 544547.3749999998], [168526.71078643727, 544547.3749999998] ] ] }, "properties": { "beforeClass": "WASTE", "afterClass": "LAND" } }, { "type": "Feature", "geometry": { "type": "Polygon", "coordinates": [ [ [321550.124999999, 399476.9607864386], [321550.12500000146, 399480.53921356204], [321551.96078643796, 399482.37499999895], [321553.46078643616, 399483.12499999907], [321558.21078643785, 399484.62500000035], [321560.96078643884, 399486.1250000005], [321563.78921356366, 399486.1249999994], [321565.62500000204, 399484.28921355924], [321565.87499999726, 399479.7107864349], [321565.37500000506, 399478.71078644204], [321562.0392135627, 399476.12499999604], [321559.0392135677, 399474.62499999924], [321556.0392135648, 399473.6249999991], [321552.4607864374, 399473.6249999991], [321550.8750000004, 399474.96078643366], [321550.124999999, 399476.9607864386] ] ] }, "properties": { "beforeClass": "FOREST", "afterClass": "BUILDING" } }, { "type": "Feature", "geometry": { "type": "Polygon", "coordinates": [ [ [386684.62499999895, 310943.28921356204], [386684.87499999773, 310944.7892135619], [386686.87500000314, 310949.03921356524], [386689.71078643756, 310952.37499999907], [386692.5392135586, 310952.6249999989], [386694.0392135624, 310951.87500000035], [386697.28921356064, 310951.87500000035], [386700.53921356215, 310948.62499999854], [386709.78921356826, 310946.37499999977], [386715.0392135615, 310942.87500000285], [386717.8750000029, 310939.2892135617], [386718.3750000007, 310937.28921356343], [386718.124999998, 310933.71078643954], [386716.87500000326, 310931.4607864389], [386712.7892135609, 310927.12499999825], [386708.4607864385, 310927.124999999], [386699.2107864381, 310931.3749999989], [386694.9607864365, 310932.6249999988], [386692.21078644064, 310932.8750000003], [386686.4607864376, 310935.12499999994], [386685.3750000027, 310936.2107864344], [386684.3750000007, 310938.71078643826], [386684.62499999895, 310943.28921356204] ] ] }, "properties": { "beforeClass": "FARMLAND", "afterClass": "SOLAR_PANEL" } } ] } """) })) @RequestBody TrainingDataReviewDto.NewPolygonRequest request) { return ApiResponseDto.okObject(trainingDataReviewService.saveNewPolygon(request)); } @Operation( summary = "COG 이미지 URL 조회", description = "변화 전/후 COG 이미지 URL을 조회합니다. beforeYear와 afterYear 중 최소 하나는 필수입니다.") @ApiResponses( value = { @ApiResponse( responseCode = "200", description = "조회 성공", content = @Content( mediaType = "application/json", schema = @Schema(implementation = TrainingDataReviewDto.CogImageResponse.class))), @ApiResponse( responseCode = "400", description = "년도 파라미터가 하나도 제공되지 않음", content = @Content), @ApiResponse(responseCode = "404", description = "이미지를 찾을 수 없음", content = @Content), @ApiResponse(responseCode = "500", description = "서버 오류", content = @Content) }) @GetMapping("/cog-image") public ApiResponseDto getCogImageUrl( @Parameter(description = "도엽번호", required = true, example = "35905086") @RequestParam String mapSheetNum, @Parameter(description = "변화 전 년도", required = false, example = "2023") @RequestParam(required = false) Integer beforeYear, @Parameter(description = "변화 후 년도", required = false, example = "2024") @RequestParam(required = false) Integer afterYear) { return ApiResponseDto.ok( trainingDataReviewService.getCogImageUrl(mapSheetNum, beforeYear, afterYear)); } }