diff --git a/src/main/java/com/kamco/cd/kamcoback/common/utils/geometry/GeometryDeserializer.java b/src/main/java/com/kamco/cd/kamcoback/common/utils/geometry/GeometryDeserializer.java index ee14b8d5..ea9366a3 100644 --- a/src/main/java/com/kamco/cd/kamcoback/common/utils/geometry/GeometryDeserializer.java +++ b/src/main/java/com/kamco/cd/kamcoback/common/utils/geometry/GeometryDeserializer.java @@ -11,6 +11,10 @@ import org.springframework.util.StringUtils; public class GeometryDeserializer extends StdDeserializer { + public GeometryDeserializer() { + super(Geometry.class); + } + public GeometryDeserializer(Class targetType) { super(targetType); } diff --git a/src/main/java/com/kamco/cd/kamcoback/postgres/core/TrainingDataLabelCoreService.java b/src/main/java/com/kamco/cd/kamcoback/postgres/core/TrainingDataLabelCoreService.java index c57ebc8d..46288e50 100644 --- a/src/main/java/com/kamco/cd/kamcoback/postgres/core/TrainingDataLabelCoreService.java +++ b/src/main/java/com/kamco/cd/kamcoback/postgres/core/TrainingDataLabelCoreService.java @@ -1,10 +1,15 @@ package com.kamco.cd.kamcoback.postgres.core; import com.kamco.cd.kamcoback.postgres.repository.trainingdata.TrainingDataLabelRepository; +import com.kamco.cd.kamcoback.trainingdata.dto.TrainingDataLabelDto.DetailRes; +import com.kamco.cd.kamcoback.trainingdata.dto.TrainingDataLabelDto.GeoFeatureRequest.Properties; import com.kamco.cd.kamcoback.trainingdata.dto.TrainingDataLabelDto.LabelingGeometryInfo; import com.kamco.cd.kamcoback.trainingdata.dto.TrainingDataLabelDto.LabelingListDto; +import com.kamco.cd.kamcoback.trainingdata.dto.TrainingDataLabelDto.SummaryRes; import com.kamco.cd.kamcoback.trainingdata.dto.TrainingDataLabelDto.searchReq; +import java.util.UUID; import lombok.RequiredArgsConstructor; +import org.locationtech.jts.geom.Geometry; import org.springframework.data.domain.Page; import org.springframework.stereotype.Service; @@ -22,4 +27,52 @@ public class TrainingDataLabelCoreService { public LabelingGeometryInfo findLabelingAssignedGeom(String assignmentUid) { return trainingDataLabelRepository.findLabelingAssignedGeom(assignmentUid); } + + public Long findLabelingAssignmentGeoUid(String assignmentUid) { + return trainingDataLabelRepository.findLabelingAssignmentGeoUid(assignmentUid); + } + + public void updateLabelingStateAssignment(String assignmentUid, String status) { + trainingDataLabelRepository.updateLabelingStateAssignment(assignmentUid, status); + } + + public void updateLabelingSkipState(Long inferenceGeomUid, String status) { + trainingDataLabelRepository.updateLabelingSkipState(inferenceGeomUid, status); + } + + public void updateLabelingPolygonClass( + Long inferenceGeomUid, Geometry geometry, Properties properties, String status) { + trainingDataLabelRepository.updateLabelingPolygonClass( + inferenceGeomUid, geometry, properties, status); + } + + /** + * 라벨러별 작업 통계 조회 + * + * @param userId 라벨러 사번 + * @return 전체/미작업/Today 건수 + */ + public SummaryRes getSummary(String userId) { + try { + System.out.println("[CoreService] getSummary called with userId: " + userId); + SummaryRes result = trainingDataLabelRepository.getSummary(userId); + System.out.println("[CoreService] getSummary result: " + result); + return result; + } catch (Exception e) { + System.err.println("[CoreService] getSummary ERROR: " + e.getMessage()); + e.printStackTrace(); + // 예외 발생 시에도 빈 통계 반환 + return SummaryRes.builder().totalCnt(0L).undoneCnt(0L).todayCnt(0L).build(); + } + } + + /** + * 작업 배정 상세 정보 조회 + * + * @param assignmentUid 작업 배정 ID + * @return 변화탐지정보 + 실태조사결과정보 + */ + public DetailRes getDetail(UUID assignmentUid) { + return trainingDataLabelRepository.getDetail(assignmentUid); + } } diff --git a/src/main/java/com/kamco/cd/kamcoback/postgres/repository/trainingdata/TrainingDataLabelRepositoryCustom.java b/src/main/java/com/kamco/cd/kamcoback/postgres/repository/trainingdata/TrainingDataLabelRepositoryCustom.java index 62f9cbf6..7e0dd67f 100644 --- a/src/main/java/com/kamco/cd/kamcoback/postgres/repository/trainingdata/TrainingDataLabelRepositoryCustom.java +++ b/src/main/java/com/kamco/cd/kamcoback/postgres/repository/trainingdata/TrainingDataLabelRepositoryCustom.java @@ -1,8 +1,13 @@ package com.kamco.cd.kamcoback.postgres.repository.trainingdata; +import com.kamco.cd.kamcoback.trainingdata.dto.TrainingDataLabelDto.DetailRes; +import com.kamco.cd.kamcoback.trainingdata.dto.TrainingDataLabelDto.GeoFeatureRequest.Properties; import com.kamco.cd.kamcoback.trainingdata.dto.TrainingDataLabelDto.LabelingGeometryInfo; import com.kamco.cd.kamcoback.trainingdata.dto.TrainingDataLabelDto.LabelingListDto; +import com.kamco.cd.kamcoback.trainingdata.dto.TrainingDataLabelDto.SummaryRes; import com.kamco.cd.kamcoback.trainingdata.dto.TrainingDataLabelDto.searchReq; +import java.util.UUID; +import org.locationtech.jts.geom.Geometry; import org.springframework.data.domain.Page; public interface TrainingDataLabelRepositoryCustom { @@ -10,4 +15,17 @@ public interface TrainingDataLabelRepositoryCustom { Page findLabelingAssignedList(searchReq searchReq, String userId, String status); LabelingGeometryInfo findLabelingAssignedGeom(String assignmentUid); + + Long findLabelingAssignmentGeoUid(String assignmentUid); + + void updateLabelingStateAssignment(String assignmentUid, String status); + + void updateLabelingSkipState(Long inferenceGeomUid, String status); + + void updateLabelingPolygonClass( + Long inferenceGeomUid, Geometry geometry, Properties properties, String status); + + SummaryRes getSummary(String userId); + + DetailRes getDetail(UUID assignmentUid); } diff --git a/src/main/java/com/kamco/cd/kamcoback/postgres/repository/trainingdata/TrainingDataLabelRepositoryImpl.java b/src/main/java/com/kamco/cd/kamcoback/postgres/repository/trainingdata/TrainingDataLabelRepositoryImpl.java index 7c7a1a95..e1a34d56 100644 --- a/src/main/java/com/kamco/cd/kamcoback/postgres/repository/trainingdata/TrainingDataLabelRepositoryImpl.java +++ b/src/main/java/com/kamco/cd/kamcoback/postgres/repository/trainingdata/TrainingDataLabelRepositoryImpl.java @@ -5,9 +5,13 @@ import static com.kamco.cd.kamcoback.postgres.entity.QLabelingAssignmentEntity.l import static com.kamco.cd.kamcoback.postgres.entity.QMapInkx5kEntity.mapInkx5kEntity; import static com.kamco.cd.kamcoback.postgres.entity.QMapSheetAnalDataInferenceGeomEntity.mapSheetAnalDataInferenceGeomEntity; +import com.fasterxml.jackson.databind.JsonNode; import com.kamco.cd.kamcoback.postgres.entity.LabelingAssignmentEntity; +import com.kamco.cd.kamcoback.trainingdata.dto.TrainingDataLabelDto.DetailRes; +import com.kamco.cd.kamcoback.trainingdata.dto.TrainingDataLabelDto.GeoFeatureRequest.Properties; import com.kamco.cd.kamcoback.trainingdata.dto.TrainingDataLabelDto.LabelingGeometryInfo; import com.kamco.cd.kamcoback.trainingdata.dto.TrainingDataLabelDto.LabelingListDto; +import com.kamco.cd.kamcoback.trainingdata.dto.TrainingDataLabelDto.SummaryRes; import com.kamco.cd.kamcoback.trainingdata.dto.TrainingDataLabelDto.searchReq; import com.querydsl.core.types.Projections; import com.querydsl.core.types.dsl.BooleanExpression; @@ -16,10 +20,13 @@ import com.querydsl.core.types.dsl.Expressions; import com.querydsl.core.types.dsl.NumberPath; import com.querydsl.core.types.dsl.StringExpression; import com.querydsl.jpa.impl.JPAQueryFactory; +import java.time.LocalDate; +import java.time.ZonedDateTime; import java.util.List; import java.util.Objects; import java.util.Optional; import java.util.UUID; +import org.locationtech.jts.geom.Geometry; import org.springframework.data.domain.Page; import org.springframework.data.domain.PageImpl; import org.springframework.data.domain.PageRequest; @@ -132,6 +139,356 @@ public class TrainingDataLabelRepositoryImpl extends QuerydslRepositorySupport .fetchOne(); } + @Override + public Long findLabelingAssignmentGeoUid(String assignmentUid) { + return queryFactory + .select(labelingAssignmentEntity.inferenceGeomUid) + .from(labelingAssignmentEntity) + .where(labelingAssignmentEntity.assignmentUid.eq(UUID.fromString(assignmentUid))) + .fetchOne(); + } + + @Override + public void updateLabelingStateAssignment(String assignmentUid, String status) { + queryFactory + .update(labelingAssignmentEntity) + .set(labelingAssignmentEntity.workState, status) + .set(labelingAssignmentEntity.workStatDttm, ZonedDateTime.now()) + .where(labelingAssignmentEntity.assignmentUid.eq(UUID.fromString(assignmentUid))) + .execute(); + } + + @Override + public void updateLabelingSkipState(Long inferenceGeomUid, String status) { + queryFactory + .update(mapSheetAnalDataInferenceGeomEntity) + .set(mapSheetAnalDataInferenceGeomEntity.labelStateDttm, ZonedDateTime.now()) + .set(mapSheetAnalDataInferenceGeomEntity.labelState, status) + .where(mapSheetAnalDataInferenceGeomEntity.geoUid.eq(inferenceGeomUid)) + .execute(); + } + + @Override + public void updateLabelingPolygonClass( + Long mapSheetAnalDataInferenceGeomEntityUid, + Geometry geometry, + Properties properties, + String status) { + queryFactory + .update(mapSheetAnalDataInferenceGeomEntity) + .set(mapSheetAnalDataInferenceGeomEntity.geom, geometry) + .set( + mapSheetAnalDataInferenceGeomEntity.classBeforeCd, + properties.getBeforeClass().toLowerCase()) + .set( + mapSheetAnalDataInferenceGeomEntity.classAfterCd, + properties.getAfterClass().toLowerCase()) + .set(mapSheetAnalDataInferenceGeomEntity.labelStateDttm, ZonedDateTime.now()) + .set(mapSheetAnalDataInferenceGeomEntity.labelState, status) + .set(mapSheetAnalDataInferenceGeomEntity.geomCenter, geometry.getCentroid()) + .where( + mapSheetAnalDataInferenceGeomEntity.geoUid.eq(mapSheetAnalDataInferenceGeomEntityUid)) + .execute(); + } + + @Override + public SummaryRes getSummary(String userId) { + // 기본값 설정 + Long totalCnt = 0L; + Long undoneCnt = 0L; + Long todayCnt = 0L; + + try { + System.out.println("=== getSummary START ==="); + System.out.println("userId: " + userId); + + // 1. 전체 배정 건수 + try { + Long result = + queryFactory + .select(labelingAssignmentEntity.count()) + .from(labelingAssignmentEntity) + .where(labelingAssignmentEntity.workerUid.eq(userId)) + .fetchOne(); + + totalCnt = (result != null) ? result : 0L; + System.out.println("totalCnt: " + totalCnt); + } catch (Exception e) { + System.err.println( + "totalCnt query error: " + e.getClass().getName() + " - " + e.getMessage()); + if (e.getCause() != null) { + System.err.println("Caused by: " + e.getCause().getMessage()); + } + totalCnt = 0L; + } + + // 2. 미작업 건수 (ASSIGNED 상태) + try { + Long result = + queryFactory + .select(labelingAssignmentEntity.count()) + .from(labelingAssignmentEntity) + .where( + labelingAssignmentEntity.workerUid.eq(userId), + labelingAssignmentEntity.workState.eq("ASSIGNED")) + .fetchOne(); + + undoneCnt = (result != null) ? result : 0L; + System.out.println("undoneCnt: " + undoneCnt); + } catch (Exception e) { + System.err.println( + "undoneCnt query error: " + e.getClass().getName() + " - " + e.getMessage()); + if (e.getCause() != null) { + System.err.println("Caused by: " + e.getCause().getMessage()); + } + undoneCnt = 0L; + } + + // 3. 오늘 완료 건수 + try { + // 오늘 날짜의 시작과 끝 시간 계산 + ZonedDateTime startOfToday = LocalDate.now().atStartOfDay(java.time.ZoneId.systemDefault()); + ZonedDateTime endOfToday = startOfToday.plusDays(1); + System.out.println("startOfToday: " + startOfToday); + System.out.println("endOfToday: " + endOfToday); + + Long result = + queryFactory + .select(labelingAssignmentEntity.count()) + .from(labelingAssignmentEntity) + .where( + labelingAssignmentEntity.workerUid.eq(userId), + labelingAssignmentEntity.workState.eq("DONE"), + labelingAssignmentEntity.modifiedDate.isNotNull(), + labelingAssignmentEntity.modifiedDate.goe(startOfToday), + labelingAssignmentEntity.modifiedDate.lt(endOfToday)) + .fetchOne(); + + todayCnt = (result != null) ? result : 0L; + System.out.println("todayCnt: " + todayCnt); + } catch (Exception e) { + System.err.println( + "todayCnt query error: " + e.getClass().getName() + " - " + e.getMessage()); + if (e.getCause() != null) { + System.err.println("Caused by: " + e.getCause().getMessage()); + } + todayCnt = 0L; + } + + System.out.println("=== getSummary END ==="); + System.out.println( + "Final result - totalCnt: " + + totalCnt + + ", undoneCnt: " + + undoneCnt + + ", todayCnt: " + + todayCnt); + + } catch (Exception e) { + // 최상위 예외 처리 + System.err.println("=== getSummary OUTER ERROR ==="); + System.err.println("Error: " + e.getClass().getName() + " - " + e.getMessage()); + if (e.getCause() != null) { + System.err.println("Caused by: " + e.getCause().getMessage()); + } + e.printStackTrace(); + } + + // 항상 정상 응답 반환 (예외를 throw하지 않음) + return SummaryRes.builder().totalCnt(totalCnt).undoneCnt(undoneCnt).todayCnt(todayCnt).build(); + } + + @Override + public DetailRes getDetail(UUID assignmentUid) { + try { + // 1. 작업 배정 정보 조회 + var assignment = + queryFactory + .selectFrom(labelingAssignmentEntity) + .where(labelingAssignmentEntity.assignmentUid.eq(assignmentUid)) + .fetchOne(); + + if (assignment == null) { + throw new RuntimeException("Assignment not found: " + assignmentUid); + } + + // 2. 추론 결과 정보 조회 + var mapSheetAnalDataInferenceGeomEntityEntity = + queryFactory + .selectFrom(mapSheetAnalDataInferenceGeomEntity) + .where( + mapSheetAnalDataInferenceGeomEntity.geoUid.eq( + assignment.toDto().getInferenceGeomUid())) + .fetchOne(); + + if (mapSheetAnalDataInferenceGeomEntityEntity == null) { + throw new RuntimeException( + "Inference geometry not found: " + assignment.toDto().getInferenceGeomUid()); + } + + // 3. 도엽 정보 조회 + var mapSheetEntity = + queryFactory + .selectFrom(mapInkx5kEntity) + .where(mapInkx5kEntity.mapidcdNo.eq(assignment.toDto().getAssignGroupId())) + .fetchOne(); + + // 4. COG URL 조회 - imagery만 사용 + String beforeCogUrl = ""; + String afterCogUrl = ""; + + try { + var beforeImagery = + queryFactory + .select( + Expressions.stringTemplate( + "{0} || {1}", imageryEntity.cogMiddlePath, imageryEntity.cogFilename)) + .from(imageryEntity) + .where( + imageryEntity.scene5k.eq(assignment.toDto().getAssignGroupId()), + imageryEntity.year.eq( + mapSheetAnalDataInferenceGeomEntityEntity.getCompareYyyy())) + .fetchFirst(); + beforeCogUrl = beforeImagery != null ? beforeImagery : ""; + + var afterImagery = + queryFactory + .select( + Expressions.stringTemplate( + "{0} || {1}", imageryEntity.cogMiddlePath, imageryEntity.cogFilename)) + .from(imageryEntity) + .where( + imageryEntity.scene5k.eq(assignment.toDto().getAssignGroupId()), + imageryEntity.year.eq( + mapSheetAnalDataInferenceGeomEntityEntity.getTargetYyyy())) + .fetchFirst(); + afterCogUrl = afterImagery != null ? afterImagery : ""; + } catch (Exception e) { + System.err.println("COG URL retrieval error: " + e.getMessage()); + // COG URL 조회 실패 시 빈 문자열 유지 + } + + // 5. DTO 생성 + var changeDetectionInfo = + com.kamco.cd.kamcoback.trainingdata.dto.TrainingDataLabelDto.ChangeDetectionInfo.builder() + .mapSheetInfo(mapSheetEntity != null ? mapSheetEntity.getMapidNm() : "") + .detectionYear( + (mapSheetAnalDataInferenceGeomEntityEntity.getCompareYyyy() != null + ? mapSheetAnalDataInferenceGeomEntityEntity.getCompareYyyy() + : 0) + + "-" + + (mapSheetAnalDataInferenceGeomEntityEntity.getTargetYyyy() != null + ? mapSheetAnalDataInferenceGeomEntityEntity.getTargetYyyy() + : 0)) + .beforeClass( + com.kamco.cd.kamcoback.trainingdata.dto.TrainingDataLabelDto.ClassificationInfo + .builder() + .classification( + mapSheetAnalDataInferenceGeomEntityEntity.getClassBeforeCd() != null + ? mapSheetAnalDataInferenceGeomEntityEntity.getClassBeforeCd() + : "") + .probability( + mapSheetAnalDataInferenceGeomEntityEntity.getClassBeforeProb() != null + ? mapSheetAnalDataInferenceGeomEntityEntity.getClassBeforeProb() + : 0.0) + .build()) + .afterClass( + com.kamco.cd.kamcoback.trainingdata.dto.TrainingDataLabelDto.ClassificationInfo + .builder() + .classification( + mapSheetAnalDataInferenceGeomEntityEntity.getClassAfterCd() != null + ? mapSheetAnalDataInferenceGeomEntityEntity.getClassAfterCd() + : "") + .probability( + mapSheetAnalDataInferenceGeomEntityEntity.getClassAfterProb() != null + ? mapSheetAnalDataInferenceGeomEntityEntity.getClassAfterProb() + : 0.0) + .build()) + .area( + mapSheetAnalDataInferenceGeomEntityEntity.getArea() != null + ? mapSheetAnalDataInferenceGeomEntityEntity.getArea() + : 0.0) + .detectionAccuracy( + mapSheetAnalDataInferenceGeomEntityEntity.getCdProb() != null + ? mapSheetAnalDataInferenceGeomEntityEntity.getCdProb() + : 0.0) + .pnu( + mapSheetAnalDataInferenceGeomEntityEntity.getPnu() != null + ? mapSheetAnalDataInferenceGeomEntityEntity.getPnu() + : 0L) + .build(); + + var inspectionResultInfo = + com.kamco.cd.kamcoback.trainingdata.dto.TrainingDataLabelDto.InspectionResultInfo + .builder() + .verificationResult(convertInspectState(assignment.toDto().getInspectState())) + .inappropriateReason("") + // .memo(assignment.toDto().getInspectMemo() != null ? + // assignment.toDto().getInspectMemo() : "") + .build(); + + // 6. Geometry를 GeoJSON으로 변환 + com.fasterxml.jackson.databind.JsonNode geomJson = null; + if (mapSheetAnalDataInferenceGeomEntityEntity.getGeom() != null) { + try { + String geomString = + queryFactory + .select( + Expressions.stringTemplate( + "ST_AsGeoJSON({0})", mapSheetAnalDataInferenceGeomEntity.geom)) + .from(mapSheetAnalDataInferenceGeomEntity) + .where( + mapSheetAnalDataInferenceGeomEntity.geoUid.eq( + mapSheetAnalDataInferenceGeomEntityEntity.getGeoUid())) + .fetchOne(); + + if (geomString != null) { + com.fasterxml.jackson.databind.ObjectMapper mapper = + new com.fasterxml.jackson.databind.ObjectMapper(); + geomJson = mapper.readTree(geomString); + } + } catch (Exception e) { + System.err.println("GeoJSON parsing error: " + e.getMessage()); + // JSON 파싱 실패 시 null 유지 + } + } + + // 도엽 bbox json으로 가져오기 + JsonNode mapBbox = null; + if (mapSheetEntity.getGeom() != null) { + try { + String bboxString = + queryFactory + .select(Expressions.stringTemplate("ST_AsGeoJSON({0})", mapInkx5kEntity.geom)) + .from(mapInkx5kEntity) + .where(mapInkx5kEntity.mapidcdNo.eq(assignment.toDto().getAssignGroupId())) + .fetchOne(); + if (bboxString != null) { + com.fasterxml.jackson.databind.ObjectMapper mapper = + new com.fasterxml.jackson.databind.ObjectMapper(); + mapBbox = mapper.readTree(bboxString); + } + } catch (Exception e) { + throw new RuntimeException("GeoJSON parsing error: " + e.getMessage()); + } + } + + return DetailRes.builder() + .assignmentUid(assignmentUid) + .changeDetectionInfo(changeDetectionInfo) + .inspectionResultInfo(inspectionResultInfo) + .geom(geomJson) + .beforeCogUrl(beforeCogUrl) + .afterCogUrl(afterCogUrl) + .mapBox(mapBbox) + .build(); + } catch (Exception e) { + System.err.println("getDetail Error: " + e.getMessage()); + e.printStackTrace(); + throw new RuntimeException("Failed to get detail for assignmentUid: " + assignmentUid, e); + } + } + private StringExpression makeCogUrl(NumberPath year) { return new CaseBuilder() .when(imageryEntity.year.eq(year)) @@ -149,4 +506,20 @@ public class TrainingDataLabelRepositoryImpl extends QuerydslRepositorySupport String[] arrStatus = status.split(","); return labelingAssignmentEntity.workState.in(arrStatus); } + + private String convertInspectState(String inspectState) { + if (inspectState == null) { + return "미확인"; + } + switch (inspectState) { + case "UNCONFIRM": + return "미확인"; + case "EXCEPT": + return "제외"; + case "COMPLETE": + return "완료"; + default: + return "미확인"; + } + } } diff --git a/src/main/java/com/kamco/cd/kamcoback/trainingdata/TrainingDataLabelApiController.java b/src/main/java/com/kamco/cd/kamcoback/trainingdata/TrainingDataLabelApiController.java index 483cd2b9..e47db299 100644 --- a/src/main/java/com/kamco/cd/kamcoback/trainingdata/TrainingDataLabelApiController.java +++ b/src/main/java/com/kamco/cd/kamcoback/trainingdata/TrainingDataLabelApiController.java @@ -2,10 +2,12 @@ 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.TrainingDataLabelDto; import com.kamco.cd.kamcoback.trainingdata.dto.TrainingDataLabelDto.LabelingGeometryInfo; import com.kamco.cd.kamcoback.trainingdata.dto.TrainingDataLabelDto.LabelingListDto; import com.kamco.cd.kamcoback.trainingdata.service.TrainingDataLabelService; +import io.swagger.v3.oas.annotations.Hidden; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.media.Content; import io.swagger.v3.oas.annotations.media.Schema; @@ -15,6 +17,8 @@ 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; @@ -51,6 +55,7 @@ public class TrainingDataLabelApiController { trainingDataLabelService.findLabelingAssignedList(searchReq, userId, status)); } + @Hidden @Operation(summary = "상세 Geometry 조회", description = "라벨 할당 상세 Geometry 조회") @ApiResponses( value = { @@ -69,4 +74,87 @@ public class TrainingDataLabelApiController { @RequestParam(defaultValue = "4f9ebc8b-6635-4177-b42f-7efc9c7b4c02") String assignmentUid) { return ApiResponseDto.ok(trainingDataLabelService.findLabelingAssignedGeom(assignmentUid)); } + + @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) + }) + @PostMapping + public ApiResponseDto saveLabelingFeature( + @RequestBody TrainingDataLabelDto.GeoFeatureRequest request) { + return ApiResponseDto.okObject(trainingDataLabelService.saveLabelingFeature(request)); + } + + @Operation(summary = "작업 통계 조회", description = "라벨러의 작업 현황 통계를 조회합니다. (전체/미작업/Today 건수)") + @ApiResponses( + value = { + @ApiResponse( + responseCode = "200", + description = "조회 성공", + content = + @Content( + mediaType = "application/json", + schema = @Schema(implementation = TrainingDataLabelDto.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 = "01022223333") + @RequestParam + String userId) { + try { + System.out.println("[Controller] getSummary called with userId: " + userId); + TrainingDataLabelDto.SummaryRes result = trainingDataLabelService.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( + TrainingDataLabelDto.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 = TrainingDataLabelDto.DetailRes.class))), + @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 = "93c56be8-0246-4b22-b976-2476549733cc") + @RequestParam + java.util.UUID assignmentUid) { + return ApiResponseDto.ok(trainingDataLabelService.getDetail(assignmentUid)); + } } diff --git a/src/main/java/com/kamco/cd/kamcoback/trainingdata/dto/TrainingDataLabelDto.java b/src/main/java/com/kamco/cd/kamcoback/trainingdata/dto/TrainingDataLabelDto.java index 8094e78d..88f26ff7 100644 --- a/src/main/java/com/kamco/cd/kamcoback/trainingdata/dto/TrainingDataLabelDto.java +++ b/src/main/java/com/kamco/cd/kamcoback/trainingdata/dto/TrainingDataLabelDto.java @@ -4,12 +4,16 @@ 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.kamco.cd.kamcoback.common.utils.geometry.GeometryDeserializer; import io.swagger.v3.oas.annotations.media.Schema; 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; @@ -30,15 +34,6 @@ public class TrainingDataLabelDto { private String mapIdNm; private Long pnu; - // @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 LabelingListDto( UUID assignmentUid, Long inferenceGeomUid, @@ -99,6 +94,71 @@ public class TrainingDataLabelDto { } } + @Schema(name = "GeoFeatureRequest", description = "polygon 저장") + @Getter + @Setter + @NoArgsConstructor + @AllArgsConstructor + public static class GeoFeatureRequest { + + @Schema(description = "assignmentUid", example = "4f9ebc8b-6635-4177-b42f-7efc9c7b4c02") + private String assignmentUid; + + @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(name = "searchReq", description = "검색 요청") @Getter @Setter @@ -122,4 +182,115 @@ public class TrainingDataLabelDto { return PageRequest.of(page, size); } } + + @Schema(name = "DetailRes", description = "객체 상세 정보 응답") + @Getter + @Setter + @Builder + @NoArgsConstructor + @AllArgsConstructor + public static class DetailRes { + + @Schema(description = "작업 배정 ID") + private UUID assignmentUid; + + @Schema(description = "변화탐지정보") + private ChangeDetectionInfo changeDetectionInfo; + + @Schema(description = "실태조사결과정보") + private InspectionResultInfo inspectionResultInfo; + + @Schema(description = "Geometry (GeoJSON)") + private JsonNode geom; + + @Schema(description = "변화 전 COG 이미지 URL") + private String beforeCogUrl; + + @Schema(description = "변화 후 COG 이미지 URL") + private String afterCogUrl; + + @Schema(description = "도엽 bbox") + private JsonNode mapBox; + } + + @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 = "미작업 건수 (ASSIGNED 상태)", example = "7211") + private Long undoneCnt; + + @Schema(description = "오늘 완료 건수", example = "0") + private Long todayCnt; + } } diff --git a/src/main/java/com/kamco/cd/kamcoback/trainingdata/service/TrainingDataLabelService.java b/src/main/java/com/kamco/cd/kamcoback/trainingdata/service/TrainingDataLabelService.java index c400e87c..ff7f1468 100644 --- a/src/main/java/com/kamco/cd/kamcoback/trainingdata/service/TrainingDataLabelService.java +++ b/src/main/java/com/kamco/cd/kamcoback/trainingdata/service/TrainingDataLabelService.java @@ -1,12 +1,21 @@ 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.DetailRes; +import com.kamco.cd.kamcoback.trainingdata.dto.TrainingDataLabelDto.GeoFeatureRequest; import com.kamco.cd.kamcoback.trainingdata.dto.TrainingDataLabelDto.LabelingGeometryInfo; import com.kamco.cd.kamcoback.trainingdata.dto.TrainingDataLabelDto.LabelingListDto; +import com.kamco.cd.kamcoback.trainingdata.dto.TrainingDataLabelDto.SummaryRes; import com.kamco.cd.kamcoback.trainingdata.dto.TrainingDataLabelDto.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 TrainingDataLabelService { @@ -24,4 +33,54 @@ public class TrainingDataLabelService { public LabelingGeometryInfo findLabelingAssignedGeom(String assignmentUid) { return trainingDataLabelCoreService.findLabelingAssignedGeom(assignmentUid); } + + @Transactional + public ResponseObj saveLabelingFeature(GeoFeatureRequest request) { + String status = ""; + String assignmentUid = request.getAssignmentUid(); + Long inferenceGeomUid = + trainingDataLabelCoreService.findLabelingAssignmentGeoUid(assignmentUid); + if (request.getGeometry() == null || request.getGeometry().isEmpty()) { + // SKIP 상태만 업데이트 + status = "SKIP"; + trainingDataLabelCoreService.updateLabelingStateAssignment(assignmentUid, status); + trainingDataLabelCoreService.updateLabelingSkipState(inferenceGeomUid, status); + } else { + status = "DONE"; + trainingDataLabelCoreService.updateLabelingStateAssignment(assignmentUid, status); + trainingDataLabelCoreService.updateLabelingPolygonClass( + 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 = trainingDataLabelCoreService.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 assignmentUid 작업 배정 ID + * @return 변화탐지정보 + 실태조사결과정보 + */ + public DetailRes getDetail(UUID assignmentUid) { + return trainingDataLabelCoreService.getDetail(assignmentUid); + } }