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.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 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": [ [ [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 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)); } }