From 26e58b01d5444165d47eee672beb57d33723f456 Mon Sep 17 00:00:00 2001 From: DanielLee <198891672+sanghyeonhd@users.noreply.github.com> Date: Fri, 9 Jan 2026 10:10:28 +0900 Subject: [PATCH 1/4] =?UTF-8?q?=EB=9D=BC=EB=B2=A8=EB=A7=81=20=EC=A2=85?= =?UTF-8?q?=EB=A3=8C=EC=97=AC=EB=B6=80=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../label/LabelAllocateApiController.java | 63 ++++++++ .../kamcoback/label/dto/WorkerStatsDto.java | 45 +++++- .../label/service/LabelAllocateService.java | 23 +++ .../core/LabelAllocateCoreService.java | 5 + .../entity/MapSheetAnalInferenceEntity.java | 10 ++ .../label/LabelAllocateRepositoryCustom.java | 4 + .../label/LabelAllocateRepositoryImpl.java | 136 +++++++++++++++--- .../label/LabelWorkRepositoryImpl.java | 25 ++-- 8 files changed, 271 insertions(+), 40 deletions(-) diff --git a/src/main/java/com/kamco/cd/kamcoback/label/LabelAllocateApiController.java b/src/main/java/com/kamco/cd/kamcoback/label/LabelAllocateApiController.java index 8bd140e9..663ab9e0 100644 --- a/src/main/java/com/kamco/cd/kamcoback/label/LabelAllocateApiController.java +++ b/src/main/java/com/kamco/cd/kamcoback/label/LabelAllocateApiController.java @@ -6,6 +6,7 @@ import com.kamco.cd.kamcoback.label.dto.LabelAllocateDto; import com.kamco.cd.kamcoback.label.dto.LabelAllocateDto.InferenceDetail; import com.kamco.cd.kamcoback.label.dto.LabelAllocateDto.LabelerDetail; import com.kamco.cd.kamcoback.label.dto.LabelAllocateDto.LabelingStatDto; +import com.kamco.cd.kamcoback.label.dto.WorkerStatsDto.UpdateClosedRequest; import com.kamco.cd.kamcoback.label.dto.WorkerStatsDto.WorkerListResponse; import com.kamco.cd.kamcoback.label.service.LabelAllocateService; import io.swagger.v3.oas.annotations.Operation; @@ -236,4 +237,66 @@ public class LabelAllocateApiController { String uuid) { return ApiResponseDto.ok(labelAllocateService.moveAvailUserList(userId, uuid)); } + + @Operation( + summary = "작업현황 관리 > 라벨링/검수 종료 여부 업데이트", + description = "라벨링/검수 종료 여부를 업데이트합니다. uuid 생략 시 최신 프로젝트 대상") + @ApiResponses( + value = { + @ApiResponse(responseCode = "200", description = "업데이트 성공"), + @ApiResponse(responseCode = "400", description = "잘못된 요청 데이터", content = @Content), + @ApiResponse(responseCode = "500", description = "서버 오류", content = @Content) + }) + @PostMapping("/projectinfo/closed") + public ApiResponseDto updateClosedYn( + @io.swagger.v3.oas.annotations.parameters.RequestBody( + description = "종료 여부 업데이트 요청", + required = true, + content = + @Content( + mediaType = "application/json", + schema = @Schema(implementation = UpdateClosedRequest.class), + examples = { + @io.swagger.v3.oas.annotations.media.ExampleObject( + name = "라벨링 종료", + value = """ + {"closedType": "LABELING", "closedYn": "Y"} + """), + @io.swagger.v3.oas.annotations.media.ExampleObject( + name = "검수 종료", + value = """ + {"closedType": "INSPECTION", "closedYn": "Y"} + """), + @io.swagger.v3.oas.annotations.media.ExampleObject( + name = "라벨링 재개", + value = """ + {"closedType": "LABELING", "closedYn": "N"} + """), + @io.swagger.v3.oas.annotations.media.ExampleObject( + name = "검수 재개", + value = """ + {"closedType": "INSPECTION", "closedYn": "N"} + """), + @io.swagger.v3.oas.annotations.media.ExampleObject( + name = "특정 프로젝트 라벨링 종료", + value = """ + {"uuid": "f97dc186-e6d3-4645-9737-3173dde8dc64", "closedType": "LABELING", "closedYn": "Y"} + """) + })) + @RequestBody + @Valid + UpdateClosedRequest request) { + + labelAllocateService.updateClosedYn( + request.getUuid(), request.getClosedType(), request.getClosedYn()); + + String typeLabel = "LABELING".equals(request.getClosedType()) ? "라벨링" : "검수"; + String statusMessage = + "Y".equals(request.getClosedYn()) + ? typeLabel + "이(가) 종료되었습니다." + : typeLabel + "이(가) 재개되었습니다."; + + return ApiResponseDto.okObject( + new ApiResponseDto.ResponseObj(ApiResponseDto.ApiResponseCode.OK, statusMessage)); + } } diff --git a/src/main/java/com/kamco/cd/kamcoback/label/dto/WorkerStatsDto.java b/src/main/java/com/kamco/cd/kamcoback/label/dto/WorkerStatsDto.java index e7a3e035..75f09508 100644 --- a/src/main/java/com/kamco/cd/kamcoback/label/dto/WorkerStatsDto.java +++ b/src/main/java/com/kamco/cd/kamcoback/label/dto/WorkerStatsDto.java @@ -2,6 +2,8 @@ package com.kamco.cd.kamcoback.label.dto; import com.kamco.cd.kamcoback.common.utils.interfaces.JsonFormatDttm; import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.Pattern; import java.time.ZonedDateTime; import lombok.AllArgsConstructor; import lombok.Builder; @@ -35,6 +37,43 @@ public class WorkerStatsDto { @Schema(description = "프로젝트 UUID") private String uuid; + + @Schema(description = "라벨링 종료 여부 (Y: 종료, N: 진행중)") + private String labelingClosedYn; + + @Schema(description = "검수 종료 여부 (Y: 종료, N: 진행중)") + private String inspectionClosedYn; + } + + @Getter + @Setter + @NoArgsConstructor + @AllArgsConstructor + @Schema(description = "프로젝트 종료 여부 업데이트 요청") + public static class UpdateClosedRequest { + + @Schema( + description = "프로젝트 UUID (선택) - 미입력 시 현재 진행중인 최신 프로젝트가 대상", + example = "f97dc186-e6d3-4645-9737-3173dde8dc64") + private String uuid; + + @NotBlank(message = "종료 유형은 필수입니다.") + @Pattern(regexp = "^(LABELING|INSPECTION)$", message = "종료 유형은 LABELING 또는 INSPECTION이어야 합니다.") + @Schema( + description = "종료 유형 (LABELING: 라벨링, INSPECTION: 검수)", + example = "LABELING", + allowableValues = {"LABELING", "INSPECTION"}, + requiredMode = Schema.RequiredMode.REQUIRED) + private String closedType; + + @NotBlank(message = "종료 여부는 필수입니다.") + @Pattern(regexp = "^[YN]$", message = "종료 여부는 Y 또는 N이어야 합니다.") + @Schema( + description = "종료 여부 (Y: 종료, N: 진행중)", + example = "Y", + allowableValues = {"Y", "N"}, + requiredMode = Schema.RequiredMode.REQUIRED) + private String closedYn; } @Getter @@ -144,7 +183,7 @@ public class WorkerStatsDto { @Schema(description = "검수 작업 상태 (진행중/완료)") private String inspectionStatus; - @Schema(description = "검수 전체 대상 건수") + @Schema(description = "검수 대상 건수 (라벨링 대상과 동일)") private Long inspectionTotalCount; @Schema(description = "검수 완료 건수 (DONE)") @@ -179,10 +218,6 @@ public class WorkerStatsDto { @Deprecated @Schema(description = "[Deprecated] inspectionRemainingCount 사용 권장") private Long remainingInspectCount; - - @Deprecated - @Schema(description = "[Deprecated] labelingStatus/inspectionStatus 사용 권장") - private String workStatus; } @Getter diff --git a/src/main/java/com/kamco/cd/kamcoback/label/service/LabelAllocateService.java b/src/main/java/com/kamco/cd/kamcoback/label/service/LabelAllocateService.java index ac9bd2f3..caa84cfb 100644 --- a/src/main/java/com/kamco/cd/kamcoback/label/service/LabelAllocateService.java +++ b/src/main/java/com/kamco/cd/kamcoback/label/service/LabelAllocateService.java @@ -195,4 +195,27 @@ public class LabelAllocateService { public MoveInfo moveAvailUserList(String userId, String uuid) { return labelAllocateCoreService.moveAvailUserList(userId, uuid); } + + /** + * 프로젝트 종료 여부 업데이트 + * + * @param uuid 프로젝트 UUID (선택, 미입력 시 최신 프로젝트 대상) + * @param closedType 종료 유형 (LABELING/INSPECTION) + * @param closedYn 종료 여부 (Y/N) + */ + @Transactional + public void updateClosedYn(String uuid, String closedType, String closedYn) { + String targetUuid = uuid; + + // uuid가 없으면 최신 프로젝트 uuid 조회 + if (targetUuid == null || targetUuid.isBlank()) { + var latestProjectInfo = labelAllocateCoreService.findLatestProjectInfo(); + if (latestProjectInfo == null || latestProjectInfo.getUuid() == null) { + throw new IllegalArgumentException("진행중인 프로젝트가 없습니다."); + } + targetUuid = latestProjectInfo.getUuid(); + } + + labelAllocateCoreService.updateClosedYnByUuid(targetUuid, closedType, closedYn); + } } diff --git a/src/main/java/com/kamco/cd/kamcoback/postgres/core/LabelAllocateCoreService.java b/src/main/java/com/kamco/cd/kamcoback/postgres/core/LabelAllocateCoreService.java index ff7c00ba..2cf1422d 100644 --- a/src/main/java/com/kamco/cd/kamcoback/postgres/core/LabelAllocateCoreService.java +++ b/src/main/java/com/kamco/cd/kamcoback/postgres/core/LabelAllocateCoreService.java @@ -133,4 +133,9 @@ public class LabelAllocateCoreService { String uuid, String userId, String paramUserId, Long assignCount) { labelAllocateRepository.assignOwnerReAllocate(uuid, userId, paramUserId, assignCount); } + + + public void updateClosedYnByUuid(String uuid, String closedType, String closedYn) { + labelAllocateRepository.updateClosedYnByUuid(uuid, closedType, closedYn); + } } diff --git a/src/main/java/com/kamco/cd/kamcoback/postgres/entity/MapSheetAnalInferenceEntity.java b/src/main/java/com/kamco/cd/kamcoback/postgres/entity/MapSheetAnalInferenceEntity.java index 195721e1..73e61299 100644 --- a/src/main/java/com/kamco/cd/kamcoback/postgres/entity/MapSheetAnalInferenceEntity.java +++ b/src/main/java/com/kamco/cd/kamcoback/postgres/entity/MapSheetAnalInferenceEntity.java @@ -149,4 +149,14 @@ public class MapSheetAnalInferenceEntity { @Column(name = "stage") private Integer stage; + + @Size(max = 1) + @ColumnDefault("'N'") + @Column(name = "labeling_closed_yn", length = 1) + private String labelingClosedYn = "N"; + + @Size(max = 1) + @ColumnDefault("'N'") + @Column(name = "inspection_closed_yn", length = 1) + private String inspectionClosedYn = "N"; } diff --git a/src/main/java/com/kamco/cd/kamcoback/postgres/repository/label/LabelAllocateRepositoryCustom.java b/src/main/java/com/kamco/cd/kamcoback/postgres/repository/label/LabelAllocateRepositoryCustom.java index 350b30c2..319a118b 100644 --- a/src/main/java/com/kamco/cd/kamcoback/postgres/repository/label/LabelAllocateRepositoryCustom.java +++ b/src/main/java/com/kamco/cd/kamcoback/postgres/repository/label/LabelAllocateRepositoryCustom.java @@ -77,4 +77,8 @@ public interface LabelAllocateRepositoryCustom { void insertLabelerUser(Long analUid, String userId, int demand); void assignOwnerReAllocate(String uuid, String userId, String paramUserId, Long assignCount); + + + // 프로젝트 종료 여부 업데이트 (uuid 기반) + void updateClosedYnByUuid(String uuid, String closedType, String closedYn); } diff --git a/src/main/java/com/kamco/cd/kamcoback/postgres/repository/label/LabelAllocateRepositoryImpl.java b/src/main/java/com/kamco/cd/kamcoback/postgres/repository/label/LabelAllocateRepositoryImpl.java index 6d64776a..28b2f625 100644 --- a/src/main/java/com/kamco/cd/kamcoback/postgres/repository/label/LabelAllocateRepositoryImpl.java +++ b/src/main/java/com/kamco/cd/kamcoback/postgres/repository/label/LabelAllocateRepositoryImpl.java @@ -335,10 +335,50 @@ public class LabelAllocateRepositoryImpl implements LabelAllocateRepositoryCusto @Override public WorkProgressInfo findWorkProgressInfo(Long analUid) { - BooleanExpression analUidCondition = - analUid != null ? labelingAssignmentEntity.analUid.eq(analUid) : null; + // analUid가 null이면 최신 프로젝트의 analUid 조회 + Long effectiveAnalUid = analUid; + if (effectiveAnalUid == null) { + UUID latestUuid = findLastLabelWorkState(); + if (latestUuid != null) { + effectiveAnalUid = + queryFactory + .select(mapSheetAnalInferenceEntity.id) + .from(mapSheetAnalInferenceEntity) + .where(mapSheetAnalInferenceEntity.uuid.eq(latestUuid)) + .fetchOne(); + } + } - // 전체 배정 건수 + BooleanExpression analUidCondition = + effectiveAnalUid != null ? labelingAssignmentEntity.analUid.eq(effectiveAnalUid) : null; + + // analUid로 분석 정보(compareYyyy, targetYyyy, stage) 조회 + MapSheetAnalInferenceEntity analEntity = null; + if (effectiveAnalUid != null) { + analEntity = + queryFactory + .selectFrom(mapSheetAnalInferenceEntity) + .where(mapSheetAnalInferenceEntity.id.eq(effectiveAnalUid)) + .fetchOne(); + } + + // 라벨링 대상 건수: tb_map_sheet_anal_data_inference_geom에서 pnu > 0 AND pass_yn = false(부적합)인 건수 + Long labelingTargetCount = 0L; + if (analEntity != null) { + labelingTargetCount = + queryFactory + .select(mapSheetAnalDataInferenceGeomEntity.geoUid.count()) + .from(mapSheetAnalDataInferenceGeomEntity) + .where( + mapSheetAnalDataInferenceGeomEntity.compareYyyy.eq(analEntity.getCompareYyyy()), + mapSheetAnalDataInferenceGeomEntity.targetYyyy.eq(analEntity.getTargetYyyy()), + mapSheetAnalDataInferenceGeomEntity.stage.eq(analEntity.getStage()), + mapSheetAnalDataInferenceGeomEntity.pnu.gt(0L), + mapSheetAnalDataInferenceGeomEntity.passYn.isFalse()) + .fetchOne(); + } + + // 전체 배정 건수 (tb_labeling_assignment 기준) Long totalAssigned = queryFactory .select(labelingAssignmentEntity.count()) @@ -390,47 +430,69 @@ public class LabelAllocateRepositoryImpl implements LabelAllocateRepositoryCusto .where(analUidCondition, labelingAssignmentEntity.inspectorUid.isNotNull()) .fetchOne(); - // 남은 작업 건수 계산 - long total = totalAssigned != null ? totalAssigned : 0L; + // 라벨링 대상 건수 (pass_yn = false인 부적합 데이터 기준) + long labelingTotal = labelingTargetCount != null ? labelingTargetCount : 0L; + long assignedTotal = totalAssigned != null ? totalAssigned : 0L; long labelCompleted = labelingCompleted != null ? labelingCompleted : 0L; long inspectCompleted = inspectionCompleted != null ? inspectionCompleted : 0L; long skipped = skipCount != null ? skipCount : 0L; - long labelingRemaining = total - labelCompleted - skipped; - long inspectionRemaining = total - inspectCompleted - skipped; + // 라벨링 남은 건수: 라벨링 대상 건수 - 완료 - 스킵 + long labelingRemaining = labelingTotal - labelCompleted - skipped; + if (labelingRemaining < 0) labelingRemaining = 0; - // 진행률 계산 - double labelingRate = total > 0 ? (double) labelCompleted / total * 100 : 0.0; - double inspectionRate = total > 0 ? (double) inspectCompleted / total * 100 : 0.0; + // 검수 대상 건수: 라벨링 대상 건수와 동일 (기획서 기준) + long inspectionTotal = labelingTotal; + // 검수 남은 건수: 검수 대상 건수 - 검수완료(DONE) - 스킵 + long inspectionRemaining = inspectionTotal - inspectCompleted - skipped; + if (inspectionRemaining < 0) inspectionRemaining = 0; - // 상태 판단 - String labelingStatus = labelingRemaining > 0 ? "진행중" : "완료"; - String inspectionStatus = inspectionRemaining > 0 ? "진행중" : "완료"; + // 진행률 계산 (라벨링 대상 건수 기준) + double labelingRate = labelingTotal > 0 ? (double) labelCompleted / labelingTotal * 100 : 0.0; + double inspectionRate = + inspectionTotal > 0 ? (double) inspectCompleted / inspectionTotal * 100 : 0.0; + + // 상태 판단 (각각의 closedYn이 "Y"이면 "종료", 아니면 진행중/완료) + String labelingStatus; + String inspectionStatus; + + // 라벨링 상태 판단 + if (analEntity != null && "Y".equals(analEntity.getLabelingClosedYn())) { + labelingStatus = "종료"; + } else { + labelingStatus = labelingRemaining > 0 ? "진행중" : "완료"; + } + + // 검수 상태 판단 + if (analEntity != null && "Y".equals(analEntity.getInspectionClosedYn())) { + inspectionStatus = "종료"; + } else { + inspectionStatus = inspectionRemaining > 0 ? "진행중" : "완료"; + } return WorkProgressInfo.builder() - // 라벨링 + // 라벨링 (pass_yn = false인 부적합 데이터 기준) .labelingProgressRate(labelingRate) .labelingStatus(labelingStatus) - .labelingTotalCount(total) + .labelingTotalCount(labelingTotal) .labelingCompletedCount(labelCompleted) .labelingSkipCount(skipped) .labelingRemainingCount(labelingRemaining) .labelerCount(labelerCount != null ? labelerCount : 0L) - // 검수 + // 검수 (라벨링 완료 건수 기준) .inspectionProgressRate(inspectionRate) .inspectionStatus(inspectionStatus) - .inspectionTotalCount(total) + .inspectionTotalCount(inspectionTotal) .inspectionCompletedCount(inspectCompleted) .inspectionSkipCount(skipped) .inspectionRemainingCount(inspectionRemaining) .inspectorCount(inspectorCount != null ? inspectorCount : 0L) // 레거시 호환 필드 (Deprecated) .progressRate(labelingRate) - .totalAssignedCount(total) + .totalAssignedCount(labelingTotal) .completedCount(labelCompleted) .remainingLabelCount(labelingRemaining) .remainingInspectCount(inspectionRemaining) - .workStatus(labelingStatus) .build(); } @@ -677,7 +739,9 @@ public class LabelAllocateRepositoryImpl implements LabelAllocateRepositoryCusto mapSheetAnalInferenceEntity.stage, mapSheetAnalInferenceEntity.gukyuinApplyDttm, mapSheetAnalInferenceEntity.createdDttm, - mapSheetAnalInferenceEntity.uuid) + mapSheetAnalInferenceEntity.uuid, + mapSheetAnalInferenceEntity.labelingClosedYn, + mapSheetAnalInferenceEntity.inspectionClosedYn) .from(mapSheetAnalInferenceEntity) .where(mapSheetAnalInferenceEntity.id.eq(analUid)) .fetchOne(); @@ -692,6 +756,8 @@ public class LabelAllocateRepositoryImpl implements LabelAllocateRepositoryCusto ZonedDateTime gukyuinApplyDttm = result.get(mapSheetAnalInferenceEntity.gukyuinApplyDttm); ZonedDateTime createdDttm = result.get(mapSheetAnalInferenceEntity.createdDttm); UUID uuid = result.get(mapSheetAnalInferenceEntity.uuid); + String labelingClosedYn = result.get(mapSheetAnalInferenceEntity.labelingClosedYn); + String inspectionClosedYn = result.get(mapSheetAnalInferenceEntity.inspectionClosedYn); // 변화탐지년도 생성 String detectionYear = @@ -706,6 +772,8 @@ public class LabelAllocateRepositoryImpl implements LabelAllocateRepositoryCusto .gukyuinApplyDttm(gukyuinApplyDttm) .startDttm(createdDttm) .uuid(uuid != null ? uuid.toString() : null) + .labelingClosedYn(labelingClosedYn) + .inspectionClosedYn(inspectionClosedYn) .build(); } @@ -727,7 +795,9 @@ public class LabelAllocateRepositoryImpl implements LabelAllocateRepositoryCusto mapSheetAnalInferenceEntity.stage, mapSheetAnalInferenceEntity.gukyuinApplyDttm, mapSheetAnalInferenceEntity.createdDttm, - mapSheetAnalInferenceEntity.uuid) + mapSheetAnalInferenceEntity.uuid, + mapSheetAnalInferenceEntity.labelingClosedYn, + mapSheetAnalInferenceEntity.inspectionClosedYn) .from(mapSheetAnalInferenceEntity) .where(mapSheetAnalInferenceEntity.uuid.eq(uuid)) .fetchOne(); @@ -741,6 +811,8 @@ public class LabelAllocateRepositoryImpl implements LabelAllocateRepositoryCusto Integer stage = result.get(mapSheetAnalInferenceEntity.stage); ZonedDateTime gukyuinApplyDttm = result.get(mapSheetAnalInferenceEntity.gukyuinApplyDttm); ZonedDateTime createdDttm = result.get(mapSheetAnalInferenceEntity.createdDttm); + String labelingClosedYn = result.get(mapSheetAnalInferenceEntity.labelingClosedYn); + String inspectionClosedYn = result.get(mapSheetAnalInferenceEntity.inspectionClosedYn); // 변화탐지년도 생성 String detectionYear = @@ -755,6 +827,8 @@ public class LabelAllocateRepositoryImpl implements LabelAllocateRepositoryCusto .gukyuinApplyDttm(gukyuinApplyDttm) .startDttm(createdDttm) .uuid(uuid.toString()) + .labelingClosedYn(labelingClosedYn) + .inspectionClosedYn(inspectionClosedYn) .build(); } @@ -1160,4 +1234,24 @@ public class LabelAllocateRepositoryImpl implements LabelAllocateRepositoryCusto labelingLabelerEntity.workerUid.eq(paramUserId)) .execute(); } + + + @Override + public void updateClosedYnByUuid(String uuid, String closedType, String closedYn) { + var updateQuery = queryFactory.update(mapSheetAnalInferenceEntity); + + if ("LABELING".equals(closedType)) { + updateQuery.set(mapSheetAnalInferenceEntity.labelingClosedYn, closedYn); + } else if ("INSPECTION".equals(closedType)) { + updateQuery.set(mapSheetAnalInferenceEntity.inspectionClosedYn, closedYn); + } + + updateQuery + .set(mapSheetAnalInferenceEntity.updatedDttm, ZonedDateTime.now()) + .where(mapSheetAnalInferenceEntity.uuid.eq(UUID.fromString(uuid))) + .execute(); + + em.flush(); + em.clear(); + } } diff --git a/src/main/java/com/kamco/cd/kamcoback/postgres/repository/label/LabelWorkRepositoryImpl.java b/src/main/java/com/kamco/cd/kamcoback/postgres/repository/label/LabelWorkRepositoryImpl.java index 10308aed..57f28ff5 100644 --- a/src/main/java/com/kamco/cd/kamcoback/postgres/repository/label/LabelWorkRepositoryImpl.java +++ b/src/main/java/com/kamco/cd/kamcoback/postgres/repository/label/LabelWorkRepositoryImpl.java @@ -267,32 +267,29 @@ public class LabelWorkRepositoryImpl implements LabelWorkRepositoryCustom { whereSubBuilder.and(labelingAssignmentEntity.workerUid.eq(memberEntity.userId)); + // 공통 조건 추출 + BooleanExpression doneStateCondition = + labelingAssignmentEntity.workState.eq(LabelState.DONE.name()); + NumberExpression assignedCnt = labelingAssignmentEntity.workerUid.count(); - NumberExpression doneCnt = - this.caseSumExpression(labelingAssignmentEntity.workState.eq(LabelState.DONE.name())); + NumberExpression doneCnt = this.caseSumExpression(doneStateCondition); NumberExpression skipCnt = this.caseSumExpression(labelingAssignmentEntity.workState.eq(LabelState.SKIP.name())); NumberExpression day3AgoDoneCnt = this.caseSumExpression( - labelingAssignmentEntity - .workState - .eq(LabelState.DONE.name()) - .and(this.fromDateEqExpression(labelingAssignmentEntity.modifiedDate, -3))); + doneStateCondition.and( + this.fromDateEqExpression(labelingAssignmentEntity.modifiedDate, -3))); NumberExpression day2AgoDoneCnt = this.caseSumExpression( - labelingAssignmentEntity - .workState - .eq(LabelState.DONE.name()) - .and(this.fromDateEqExpression(labelingAssignmentEntity.modifiedDate, -2))); + doneStateCondition.and( + this.fromDateEqExpression(labelingAssignmentEntity.modifiedDate, -2))); NumberExpression day1AgoDoneCnt = this.caseSumExpression( - labelingAssignmentEntity - .workState - .eq(LabelState.DONE.name()) - .and(this.fromDateEqExpression(labelingAssignmentEntity.modifiedDate, -1))); + doneStateCondition.and( + this.fromDateEqExpression(labelingAssignmentEntity.modifiedDate, -1))); NumberExpression remainingCnt = assignedCnt.subtract(doneCnt); From 40b91b7deab8f1d8a564f696a91c3ab171165075 Mon Sep 17 00:00:00 2001 From: DanielLee <198891672+sanghyeonhd@users.noreply.github.com> Date: Fri, 9 Jan 2026 11:01:46 +0900 Subject: [PATCH 2/4] =?UTF-8?q?projectinfo=20uuid=EC=A1=B0=ED=9A=8C=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- login.json | 0 .../label/LabelAllocateApiController.java | 13 +- .../label/service/LabelAllocateService.java | 32 +++ .../core/LabelAllocateCoreService.java | 8 + .../label/LabelAllocateRepositoryCustom.java | 6 + .../label/LabelAllocateRepositoryImpl.java | 198 ++++++++++++++++++ 6 files changed, 253 insertions(+), 4 deletions(-) delete mode 100644 login.json diff --git a/login.json b/login.json deleted file mode 100644 index e69de29b..00000000 diff --git a/src/main/java/com/kamco/cd/kamcoback/label/LabelAllocateApiController.java b/src/main/java/com/kamco/cd/kamcoback/label/LabelAllocateApiController.java index 663ab9e0..cd70855f 100644 --- a/src/main/java/com/kamco/cd/kamcoback/label/LabelAllocateApiController.java +++ b/src/main/java/com/kamco/cd/kamcoback/label/LabelAllocateApiController.java @@ -55,7 +55,9 @@ public class LabelAllocateApiController { return ApiResponseDto.ok(labelAllocateService.availUserList(role)); } - @Operation(summary = "작업현황 관리 > 프로젝트 및 진행 상황 정보", description = "작업현황 관리 > 프로젝트 및 진행 상황 정보") + @Operation( + summary = "작업현황 관리 > 프로젝트 및 진행 상황 정보", + description = "작업현황 관리 > 프로젝트 및 진행 상황 정보. UUID를 입력하면 해당 프로젝트 정보를 조회하고, 미입력 시 최신 프로젝트를 조회합니다.") @ApiResponses( value = { @ApiResponse(responseCode = "200", description = "조회 성공"), @@ -64,8 +66,11 @@ public class LabelAllocateApiController { }) @GetMapping("/projectinfo") public ApiResponseDto getWorkerStatistics( - // @Parameter(description = "분석 ID (선택)", example = "3") @RequestParam(required = false) - // Long analUid, + @Parameter( + description = "프로젝트 UUID (선택) - 미입력 시 최신 프로젝트 조회", + example = "f97dc186-e6d3-4645-9737-3173dde8dc64") + @RequestParam(required = false) + String uuid, @Parameter( description = "작업자 유형 (선택) - 미입력 시 LABELER로 조회", example = "LABELER", @@ -99,7 +104,7 @@ public class LabelAllocateApiController { String workerType = (type == null || type.isEmpty()) ? RoleType.LABELER.name() : type; return ApiResponseDto.ok( - labelAllocateService.getWorkerStatistics(null, workerType, search, sort)); + labelAllocateService.getWorkerStatisticsByUuid(uuid, workerType, search, sort)); } @Operation(summary = "라벨링작업 관리 > 작업 배정", description = "라벨링작업 관리 > 작업 배정") diff --git a/src/main/java/com/kamco/cd/kamcoback/label/service/LabelAllocateService.java b/src/main/java/com/kamco/cd/kamcoback/label/service/LabelAllocateService.java index caa84cfb..e4fa63df 100644 --- a/src/main/java/com/kamco/cd/kamcoback/label/service/LabelAllocateService.java +++ b/src/main/java/com/kamco/cd/kamcoback/label/service/LabelAllocateService.java @@ -10,6 +10,8 @@ import com.kamco.cd.kamcoback.label.dto.LabelAllocateDto.LabelingStatDto; import com.kamco.cd.kamcoback.label.dto.LabelAllocateDto.MoveInfo; import com.kamco.cd.kamcoback.label.dto.LabelAllocateDto.TargetUser; import com.kamco.cd.kamcoback.label.dto.LabelAllocateDto.UserList; +import com.kamco.cd.kamcoback.label.dto.WorkerStatsDto.ProjectInfo; +import com.kamco.cd.kamcoback.label.dto.WorkerStatsDto.WorkProgressInfo; import com.kamco.cd.kamcoback.label.dto.WorkerStatsDto.WorkerListResponse; import com.kamco.cd.kamcoback.postgres.core.LabelAllocateCoreService; import java.util.List; @@ -114,6 +116,36 @@ public class LabelAllocateService { return WorkerListResponse.builder().projectInfo(projectInfo).progressInfo(progressInfo).build(); } + /** + * 작업자 통계 조회 (UUID 기반) + * + * @param uuid 프로젝트 UUID (선택, 미입력 시 최신 프로젝트 조회) + * @param workerType 작업자 유형 (LABELER/INSPECTOR) + * @param search 검색어 (이름 또는 사번) + * @param sortType 정렬 조건 + * @return 작업자 목록 및 통계 + */ + public WorkerListResponse getWorkerStatisticsByUuid( + String uuid, String workerType, String search, String sortType) { + + ProjectInfo projectInfo; + WorkProgressInfo progressInfo; + + if (uuid != null && !uuid.isBlank()) { + // UUID로 프로젝트 정보 조회 + projectInfo = labelAllocateCoreService.findProjectInfoByUuid(uuid); + // UUID로 작업 진행 현황 조회 + progressInfo = labelAllocateCoreService.findWorkProgressInfoByUuid(uuid); + } else { + // 최신 프로젝트 정보 조회 + projectInfo = labelAllocateCoreService.findLatestProjectInfo(); + // 최신 프로젝트 작업 진행 현황 조회 + progressInfo = labelAllocateCoreService.findWorkProgressInfo(null); + } + + return WorkerListResponse.builder().projectInfo(projectInfo).progressInfo(progressInfo).build(); + } + public InferenceDetail findInferenceDetail(String uuid) { return labelAllocateCoreService.findInferenceDetail(uuid); } diff --git a/src/main/java/com/kamco/cd/kamcoback/postgres/core/LabelAllocateCoreService.java b/src/main/java/com/kamco/cd/kamcoback/postgres/core/LabelAllocateCoreService.java index 2cf1422d..65b2c78c 100644 --- a/src/main/java/com/kamco/cd/kamcoback/postgres/core/LabelAllocateCoreService.java +++ b/src/main/java/com/kamco/cd/kamcoback/postgres/core/LabelAllocateCoreService.java @@ -61,6 +61,10 @@ public class LabelAllocateCoreService { return labelAllocateRepository.findLatestProjectInfo(); } + public ProjectInfo findProjectInfoByUuid(String uuid) { + return labelAllocateRepository.findProjectInfoByUuid(uuid); + } + public UUID findLastLabelWorkState() { return labelAllocateRepository.findLastLabelWorkState(); } @@ -74,6 +78,10 @@ public class LabelAllocateCoreService { return labelAllocateRepository.findWorkProgressInfo(analUid); } + public WorkProgressInfo findWorkProgressInfoByUuid(String uuid) { + return labelAllocateRepository.findWorkProgressInfoByUuid(uuid); + } + public Long findDailyProcessedCount( String workerId, String workerType, LocalDate date, Long analUid) { return labelAllocateRepository.findDailyProcessedCount(workerId, workerType, date, analUid); diff --git a/src/main/java/com/kamco/cd/kamcoback/postgres/repository/label/LabelAllocateRepositoryCustom.java b/src/main/java/com/kamco/cd/kamcoback/postgres/repository/label/LabelAllocateRepositoryCustom.java index 319a118b..9a7bcc8c 100644 --- a/src/main/java/com/kamco/cd/kamcoback/postgres/repository/label/LabelAllocateRepositoryCustom.java +++ b/src/main/java/com/kamco/cd/kamcoback/postgres/repository/label/LabelAllocateRepositoryCustom.java @@ -37,6 +37,9 @@ public interface LabelAllocateRepositoryCustom { // 최신 프로젝트 정보 조회 (analUid 없이) ProjectInfo findLatestProjectInfo(); + // UUID로 프로젝트 정보 조회 + ProjectInfo findProjectInfoByUuid(String uuid); + // 최신 작업 상태의 UUID 조회 UUID findLastLabelWorkState(); @@ -47,6 +50,9 @@ public interface LabelAllocateRepositoryCustom { // 작업 진행 현황 조회 WorkProgressInfo findWorkProgressInfo(Long analUid); + // UUID로 작업 진행 현황 조회 + WorkProgressInfo findWorkProgressInfoByUuid(String uuid); + // 작업자별 일일 처리량 조회 Long findDailyProcessedCount(String workerId, String workerType, LocalDate date, Long analUid); diff --git a/src/main/java/com/kamco/cd/kamcoback/postgres/repository/label/LabelAllocateRepositoryImpl.java b/src/main/java/com/kamco/cd/kamcoback/postgres/repository/label/LabelAllocateRepositoryImpl.java index 28b2f625..12c5450b 100644 --- a/src/main/java/com/kamco/cd/kamcoback/postgres/repository/label/LabelAllocateRepositoryImpl.java +++ b/src/main/java/com/kamco/cd/kamcoback/postgres/repository/label/LabelAllocateRepositoryImpl.java @@ -496,6 +496,154 @@ public class LabelAllocateRepositoryImpl implements LabelAllocateRepositoryCusto .build(); } + @Override + public WorkProgressInfo findWorkProgressInfoByUuid(String uuid) { + if (uuid == null || uuid.isBlank()) { + return findWorkProgressInfo(null); + } + + UUID targetUuid = UUID.fromString(uuid); + + // UUID로 analUid 조회 + Long effectiveAnalUid = + queryFactory + .select(mapSheetAnalInferenceEntity.id) + .from(mapSheetAnalInferenceEntity) + .where(mapSheetAnalInferenceEntity.uuid.eq(targetUuid)) + .fetchOne(); + + if (effectiveAnalUid == null) { + return null; + } + + BooleanExpression analUidCondition = labelingAssignmentEntity.analUid.eq(effectiveAnalUid); + + // analUid로 분석 정보(compareYyyy, targetYyyy, stage) 조회 + MapSheetAnalInferenceEntity analEntity = + queryFactory + .selectFrom(mapSheetAnalInferenceEntity) + .where(mapSheetAnalInferenceEntity.id.eq(effectiveAnalUid)) + .fetchOne(); + + // 라벨링 대상 건수: tb_map_sheet_anal_data_inference_geom에서 pnu > 0 AND pass_yn = false(부적합)인 건수 + Long labelingTargetCount = 0L; + if (analEntity != null) { + labelingTargetCount = + queryFactory + .select(mapSheetAnalDataInferenceGeomEntity.geoUid.count()) + .from(mapSheetAnalDataInferenceGeomEntity) + .where( + mapSheetAnalDataInferenceGeomEntity.compareYyyy.eq(analEntity.getCompareYyyy()), + mapSheetAnalDataInferenceGeomEntity.targetYyyy.eq(analEntity.getTargetYyyy()), + mapSheetAnalDataInferenceGeomEntity.stage.eq(analEntity.getStage()), + mapSheetAnalDataInferenceGeomEntity.pnu.gt(0L), + mapSheetAnalDataInferenceGeomEntity.passYn.isFalse()) + .fetchOne(); + } + + // 전체 배정 건수 (tb_labeling_assignment 기준) + Long totalAssigned = + queryFactory + .select(labelingAssignmentEntity.count()) + .from(labelingAssignmentEntity) + .where(analUidCondition) + .fetchOne(); + + // === 라벨링 통계 === + Long labelingCompleted = + queryFactory + .select(labelingAssignmentEntity.count()) + .from(labelingAssignmentEntity) + .where( + analUidCondition, + labelingAssignmentEntity.workState.in("LABEL_FIN", "TEST_ING", "DONE")) + .fetchOne(); + + Long skipCount = + queryFactory + .select(labelingAssignmentEntity.count()) + .from(labelingAssignmentEntity) + .where(analUidCondition, labelingAssignmentEntity.workState.eq("SKIP")) + .fetchOne(); + + Long labelerCount = + queryFactory + .select(labelingAssignmentEntity.workerUid.countDistinct()) + .from(labelingAssignmentEntity) + .where(analUidCondition, labelingAssignmentEntity.workerUid.isNotNull()) + .fetchOne(); + + // === 검수 통계 === + Long inspectionCompleted = + queryFactory + .select(labelingAssignmentEntity.count()) + .from(labelingAssignmentEntity) + .where(analUidCondition, labelingAssignmentEntity.workState.eq("DONE")) + .fetchOne(); + + Long inspectorCount = + queryFactory + .select(labelingAssignmentEntity.inspectorUid.countDistinct()) + .from(labelingAssignmentEntity) + .where(analUidCondition, labelingAssignmentEntity.inspectorUid.isNotNull()) + .fetchOne(); + + // 계산 + long labelingTotal = labelingTargetCount != null ? labelingTargetCount : 0L; + long labelCompleted = labelingCompleted != null ? labelingCompleted : 0L; + long inspectCompleted = inspectionCompleted != null ? inspectionCompleted : 0L; + long skipped = skipCount != null ? skipCount : 0L; + + long labelingRemaining = labelingTotal - labelCompleted - skipped; + if (labelingRemaining < 0) labelingRemaining = 0; + + long inspectionTotal = labelingTotal; + long inspectionRemaining = inspectionTotal - inspectCompleted - skipped; + if (inspectionRemaining < 0) inspectionRemaining = 0; + + double labelingRate = labelingTotal > 0 ? (double) labelCompleted / labelingTotal * 100 : 0.0; + double inspectionRate = + inspectionTotal > 0 ? (double) inspectCompleted / inspectionTotal * 100 : 0.0; + + // 상태 판단 + String labelingStatus; + String inspectionStatus; + + if (analEntity != null && "Y".equals(analEntity.getLabelingClosedYn())) { + labelingStatus = "종료"; + } else { + labelingStatus = labelingRemaining > 0 ? "진행중" : "완료"; + } + + if (analEntity != null && "Y".equals(analEntity.getInspectionClosedYn())) { + inspectionStatus = "종료"; + } else { + inspectionStatus = inspectionRemaining > 0 ? "진행중" : "완료"; + } + + return WorkProgressInfo.builder() + .labelingProgressRate(labelingRate) + .labelingStatus(labelingStatus) + .labelingTotalCount(labelingTotal) + .labelingCompletedCount(labelCompleted) + .labelingSkipCount(skipped) + .labelingRemainingCount(labelingRemaining) + .labelerCount(labelerCount != null ? labelerCount : 0L) + .inspectionProgressRate(inspectionRate) + .inspectionStatus(inspectionStatus) + .inspectionTotalCount(inspectionTotal) + .inspectionCompletedCount(inspectCompleted) + .inspectionSkipCount(skipped) + .inspectionRemainingCount(inspectionRemaining) + .inspectorCount(inspectorCount != null ? inspectorCount : 0L) + .progressRate(labelingRate) + .totalAssignedCount(labelingTotal) + .completedCount(labelCompleted) + .remainingLabelCount(labelingRemaining) + .remainingInspectCount(inspectionRemaining) + .build(); + } + @Override public Long findDailyProcessedCount( String workerId, String workerType, LocalDate date, Long analUid) { @@ -832,6 +980,56 @@ public class LabelAllocateRepositoryImpl implements LabelAllocateRepositoryCusto .build(); } + @Override + public ProjectInfo findProjectInfoByUuid(String uuid) { + if (uuid == null || uuid.isBlank()) { + return null; + } + + UUID targetUuid = UUID.fromString(uuid); + + var result = + queryFactory + .select( + mapSheetAnalInferenceEntity.compareYyyy, + mapSheetAnalInferenceEntity.targetYyyy, + mapSheetAnalInferenceEntity.stage, + mapSheetAnalInferenceEntity.gukyuinApplyDttm, + mapSheetAnalInferenceEntity.createdDttm, + mapSheetAnalInferenceEntity.uuid, + mapSheetAnalInferenceEntity.labelingClosedYn, + mapSheetAnalInferenceEntity.inspectionClosedYn) + .from(mapSheetAnalInferenceEntity) + .where(mapSheetAnalInferenceEntity.uuid.eq(targetUuid)) + .fetchOne(); + + if (result == null) { + return null; + } + + Integer compareYyyy = result.get(mapSheetAnalInferenceEntity.compareYyyy); + Integer targetYyyy = result.get(mapSheetAnalInferenceEntity.targetYyyy); + Integer stage = result.get(mapSheetAnalInferenceEntity.stage); + ZonedDateTime gukyuinApplyDttm = result.get(mapSheetAnalInferenceEntity.gukyuinApplyDttm); + ZonedDateTime createdDttm = result.get(mapSheetAnalInferenceEntity.createdDttm); + String labelingClosedYn = result.get(mapSheetAnalInferenceEntity.labelingClosedYn); + String inspectionClosedYn = result.get(mapSheetAnalInferenceEntity.inspectionClosedYn); + + String detectionYear = + (compareYyyy != null && targetYyyy != null) ? compareYyyy + "-" + targetYyyy : null; + String round = stage != null ? String.valueOf(stage) : null; + + return ProjectInfo.builder() + .detectionYear(detectionYear) + .stage(round) + .gukyuinApplyDttm(gukyuinApplyDttm) + .startDttm(createdDttm) + .uuid(uuid) + .labelingClosedYn(labelingClosedYn) + .inspectionClosedYn(inspectionClosedYn) + .build(); + } + @Override public UUID findLastLabelWorkState() { BooleanBuilder whereBuilder = new BooleanBuilder(); From 6235cda5970895498842c243770b8387dc420c23 Mon Sep 17 00:00:00 2001 From: DanielLee <198891672+sanghyeonhd@users.noreply.github.com> Date: Fri, 9 Jan 2026 11:10:49 +0900 Subject: [PATCH 3/4] projectinfo explain fix --- .../label/LabelAllocateApiController.java | 17 ++++++----------- 1 file changed, 6 insertions(+), 11 deletions(-) diff --git a/src/main/java/com/kamco/cd/kamcoback/label/LabelAllocateApiController.java b/src/main/java/com/kamco/cd/kamcoback/label/LabelAllocateApiController.java index cd70855f..f6da8a1d 100644 --- a/src/main/java/com/kamco/cd/kamcoback/label/LabelAllocateApiController.java +++ b/src/main/java/com/kamco/cd/kamcoback/label/LabelAllocateApiController.java @@ -1,6 +1,5 @@ package com.kamco.cd.kamcoback.label; -import com.kamco.cd.kamcoback.common.enums.RoleType; import com.kamco.cd.kamcoback.config.api.ApiResponseDto; import com.kamco.cd.kamcoback.label.dto.LabelAllocateDto; import com.kamco.cd.kamcoback.label.dto.LabelAllocateDto.InferenceDetail; @@ -72,15 +71,14 @@ public class LabelAllocateApiController { @RequestParam(required = false) String uuid, @Parameter( - description = "작업자 유형 (선택) - 미입력 시 LABELER로 조회", + description = "작업자 유형 (선택) - 미입력 시 전체 조회", example = "LABELER", - schema = - @Schema( - allowableValues = {"LABELER", "REVIEWER"}, - defaultValue = "LABELER")) + schema = @Schema(allowableValues = {"LABELER", "REVIEWER"})) @RequestParam(required = false) String type, - @Parameter(description = "검색어 (작업자 이름 또는 사번으로 검색, 부분 일치)", example = "김라벨") + @Parameter( + description = "검색어 (작업자 이름 또는 사번으로 검색, 부분 일치) - 미입력 시 전체 조회", + example = "김라벨") @RequestParam(required = false) String search, @Parameter( @@ -100,11 +98,8 @@ public class LabelAllocateApiController { @RequestParam(required = false) String sort) { - // type이 null이면 기본값으로 LABELER 설정 - String workerType = (type == null || type.isEmpty()) ? RoleType.LABELER.name() : type; - return ApiResponseDto.ok( - labelAllocateService.getWorkerStatisticsByUuid(uuid, workerType, search, sort)); + labelAllocateService.getWorkerStatisticsByUuid(uuid, type, search, sort)); } @Operation(summary = "라벨링작업 관리 > 작업 배정", description = "라벨링작업 관리 > 작업 배정") From ac5423ef3e33b8baed978338e4dc2cafc700a1c8 Mon Sep 17 00:00:00 2001 From: teddy Date: Fri, 9 Jan 2026 11:21:43 +0900 Subject: [PATCH 4/4] =?UTF-8?q?[KC-99]=20=EC=B6=94=EB=A1=A0=EA=B4=80?= =?UTF-8?q?=EB=A6=AC=20=EB=93=B1=EB=A1=9D=20dto=20=EC=88=98=EC=A0=95,=20sp?= =?UTF-8?q?oless=20=EC=A0=81=EC=9A=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../inference/dto/InferenceResultDto.java | 19 +++---- .../label/LabelAllocateApiController.java | 19 ++++--- .../core/InferenceResultCoreService.java | 47 +++++++++++++++-- .../core/LabelAllocateCoreService.java | 1 - .../entity/MapSheetLearn5kEntity.java | 51 +++++++++++++++++++ .../postgres/entity/MapSheetLearnEntity.java | 4 +- .../Inference/MapSheetLearn5kRepository.java | 7 +++ .../MapSheetLearn5kRepositoryCustom.java | 3 ++ .../MapSheetLearn5kRepositoryImpl.java | 6 +++ .../label/LabelAllocateRepositoryCustom.java | 1 - .../label/LabelAllocateRepositoryImpl.java | 1 - src/main/resources/application-dev.yml | 3 ++ src/main/resources/application-prod.yml | 6 ++- 13 files changed, 140 insertions(+), 28 deletions(-) create mode 100644 src/main/java/com/kamco/cd/kamcoback/postgres/entity/MapSheetLearn5kEntity.java create mode 100644 src/main/java/com/kamco/cd/kamcoback/postgres/repository/Inference/MapSheetLearn5kRepository.java create mode 100644 src/main/java/com/kamco/cd/kamcoback/postgres/repository/Inference/MapSheetLearn5kRepositoryCustom.java create mode 100644 src/main/java/com/kamco/cd/kamcoback/postgres/repository/Inference/MapSheetLearn5kRepositoryImpl.java diff --git a/src/main/java/com/kamco/cd/kamcoback/inference/dto/InferenceResultDto.java b/src/main/java/com/kamco/cd/kamcoback/inference/dto/InferenceResultDto.java index 26efc23d..c9d5cabf 100644 --- a/src/main/java/com/kamco/cd/kamcoback/inference/dto/InferenceResultDto.java +++ b/src/main/java/com/kamco/cd/kamcoback/inference/dto/InferenceResultDto.java @@ -64,9 +64,10 @@ public class InferenceResultDto { @Getter @AllArgsConstructor public enum MapSheetScope implements EnumType { - EXCL("추론제외"), - PREV("이전 년도 도엽 사용"), + ALL("전체"), + PART("부분"), ; + private final String desc; @Override @@ -84,8 +85,8 @@ public class InferenceResultDto { @Getter @AllArgsConstructor public enum DetectOption implements EnumType { - ALL("전체"), - PART("부분"), + EXCL("추론제외"), + PREV("이전 년도 도엽 사용"), ; private final String desc; @@ -131,16 +132,16 @@ public class InferenceResultDto { @NotNull private Integer targetYyyy; + @Schema(description = "분석대상 도엽 - 전체(ALL), 부분(PART)", example = "PART") + @NotBlank + @EnumValid(enumClass = DetectOption.class, message = "분석대상 도엽 옵션은 '전체', '부분' 만 사용 가능합니다.") + private String mapSheetScope; + @Schema(description = "탐지 데이터 옵션 - 추론제외(PREV), 이전 년도 도엽 사용(PREV)", example = "EXCL") @NotBlank @EnumValid( enumClass = MapSheetScope.class, message = "탐지 데이터 옵션은 '추론제외', '이전 년도 도엽 사용' 만 사용 가능합니다.") - private String mapSheetScope; - - @Schema(description = "분석대상 도엽 - 전체(ALL), 부분(PART)", example = "PART") - @NotBlank - @EnumValid(enumClass = DetectOption.class, message = "분석대상 도엽 옵션은 '전체', '부분' 만 사용 가능합니다.") private String detectOption; @Schema(description = "5k 도협 번호 목록", example = "[34607067,34607067]") diff --git a/src/main/java/com/kamco/cd/kamcoback/label/LabelAllocateApiController.java b/src/main/java/com/kamco/cd/kamcoback/label/LabelAllocateApiController.java index f6da8a1d..31bd064e 100644 --- a/src/main/java/com/kamco/cd/kamcoback/label/LabelAllocateApiController.java +++ b/src/main/java/com/kamco/cd/kamcoback/label/LabelAllocateApiController.java @@ -76,9 +76,7 @@ public class LabelAllocateApiController { schema = @Schema(allowableValues = {"LABELER", "REVIEWER"})) @RequestParam(required = false) String type, - @Parameter( - description = "검색어 (작업자 이름 또는 사번으로 검색, 부분 일치) - 미입력 시 전체 조회", - example = "김라벨") + @Parameter(description = "검색어 (작업자 이름 또는 사번으로 검색, 부분 일치) - 미입력 시 전체 조회", example = "김라벨") @RequestParam(required = false) String search, @Parameter( @@ -259,27 +257,32 @@ public class LabelAllocateApiController { examples = { @io.swagger.v3.oas.annotations.media.ExampleObject( name = "라벨링 종료", - value = """ + value = + """ {"closedType": "LABELING", "closedYn": "Y"} """), @io.swagger.v3.oas.annotations.media.ExampleObject( name = "검수 종료", - value = """ + value = + """ {"closedType": "INSPECTION", "closedYn": "Y"} """), @io.swagger.v3.oas.annotations.media.ExampleObject( name = "라벨링 재개", - value = """ + value = + """ {"closedType": "LABELING", "closedYn": "N"} """), @io.swagger.v3.oas.annotations.media.ExampleObject( name = "검수 재개", - value = """ + value = + """ {"closedType": "INSPECTION", "closedYn": "N"} """), @io.swagger.v3.oas.annotations.media.ExampleObject( name = "특정 프로젝트 라벨링 종료", - value = """ + value = + """ {"uuid": "f97dc186-e6d3-4645-9737-3173dde8dc64", "closedType": "LABELING", "closedYn": "Y"} """) })) diff --git a/src/main/java/com/kamco/cd/kamcoback/postgres/core/InferenceResultCoreService.java b/src/main/java/com/kamco/cd/kamcoback/postgres/core/InferenceResultCoreService.java index 34fe6e51..b3f32167 100644 --- a/src/main/java/com/kamco/cd/kamcoback/postgres/core/InferenceResultCoreService.java +++ b/src/main/java/com/kamco/cd/kamcoback/postgres/core/InferenceResultCoreService.java @@ -1,6 +1,6 @@ package com.kamco.cd.kamcoback.postgres.core; -import com.fasterxml.jackson.databind.ObjectMapper; +import com.kamco.cd.kamcoback.common.utils.UserUtil; import com.kamco.cd.kamcoback.inference.dto.InferenceDetailDto; import com.kamco.cd.kamcoback.inference.dto.InferenceDetailDto.Dashboard; import com.kamco.cd.kamcoback.inference.dto.InferenceDetailDto.MapSheet; @@ -9,15 +9,18 @@ import com.kamco.cd.kamcoback.inference.dto.InferenceResultDto.ResultList; import com.kamco.cd.kamcoback.postgres.entity.MapInkx5kEntity; import com.kamco.cd.kamcoback.postgres.entity.MapSheetAnalDataInferenceEntity; import com.kamco.cd.kamcoback.postgres.entity.MapSheetAnalDataInferenceGeomEntity; +import com.kamco.cd.kamcoback.postgres.entity.MapSheetLearn5kEntity; import com.kamco.cd.kamcoback.postgres.entity.MapSheetLearnEntity; import com.kamco.cd.kamcoback.postgres.repository.Inference.MapSheetAnalDataInferenceRepository; +import com.kamco.cd.kamcoback.postgres.repository.Inference.MapSheetLearn5kRepository; import com.kamco.cd.kamcoback.postgres.repository.Inference.MapSheetLearnRepository; import com.kamco.cd.kamcoback.postgres.repository.scene.MapInkx5kRepository; +import jakarta.persistence.EntityManager; import jakarta.persistence.EntityNotFoundException; import jakarta.validation.constraints.NotNull; +import java.util.ArrayList; import java.util.List; import lombok.RequiredArgsConstructor; -import org.locationtech.jts.io.geojson.GeoJsonWriter; import org.springframework.data.domain.Page; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @@ -29,9 +32,10 @@ public class InferenceResultCoreService { private final MapSheetAnalDataInferenceRepository mapSheetAnalDataRepository; private final MapSheetLearnRepository mapSheetLearnRepository; private final MapInkx5kRepository mapInkx5kRepository; + private final MapSheetLearn5kRepository mapSheetLearn5kRepository; - private final ObjectMapper objectMapper = new ObjectMapper(); - private final GeoJsonWriter geoJsonWriter = new GeoJsonWriter(); + private final EntityManager entityManager; + private final UserUtil userUtil; /** * 추론관리 목록 @@ -55,7 +59,40 @@ public class InferenceResultCoreService { mapSheetLearnEntity.setM1ModelUid(req.getModel1Uid()); mapSheetLearnEntity.setM2ModelUid(req.getModel2Uid()); mapSheetLearnEntity.setM3ModelUid(req.getModel3Uid()); - // mapSheetLearnRepository.save() + mapSheetLearnEntity.setCompareYyyy(req.getCompareYyyy()); + mapSheetLearnEntity.setTargetYyyy(req.getTargetYyyy()); + mapSheetLearnEntity.setMapSheetScope(req.getMapSheetScope()); + mapSheetLearnEntity.setDetectOption(req.getDetectOption()); + mapSheetLearnEntity.setCreatedUid(userUtil.getId()); + + // learn 테이블 저장 + MapSheetLearnEntity savedLearn = mapSheetLearnRepository.save(mapSheetLearnEntity); + + final int CHUNK = 1000; + List buffer = new ArrayList<>(CHUNK); + List mapSheetNumList = req.getMapSheetNum(); + + // learn 도엽 저장 + for (String mapSheetNum : mapSheetNumList) { + MapSheetLearn5kEntity e = new MapSheetLearn5kEntity(); + e.setLearn(savedLearn); + e.setMapSheetNum(Long.parseLong(mapSheetNum)); + e.setCreatedUid(userUtil.getId()); + buffer.add(e); + + if (buffer.size() == CHUNK) { + mapSheetLearn5kRepository.saveAll(buffer); + mapSheetLearn5kRepository.flush(); + entityManager.clear(); + buffer.clear(); + } + } + + if (!buffer.isEmpty()) { + mapSheetLearn5kRepository.saveAll(buffer); + mapSheetLearn5kRepository.flush(); + entityManager.clear(); + } } /****/ diff --git a/src/main/java/com/kamco/cd/kamcoback/postgres/core/LabelAllocateCoreService.java b/src/main/java/com/kamco/cd/kamcoback/postgres/core/LabelAllocateCoreService.java index 65b2c78c..86c165e9 100644 --- a/src/main/java/com/kamco/cd/kamcoback/postgres/core/LabelAllocateCoreService.java +++ b/src/main/java/com/kamco/cd/kamcoback/postgres/core/LabelAllocateCoreService.java @@ -142,7 +142,6 @@ public class LabelAllocateCoreService { labelAllocateRepository.assignOwnerReAllocate(uuid, userId, paramUserId, assignCount); } - public void updateClosedYnByUuid(String uuid, String closedType, String closedYn) { labelAllocateRepository.updateClosedYnByUuid(uuid, closedType, closedYn); } diff --git a/src/main/java/com/kamco/cd/kamcoback/postgres/entity/MapSheetLearn5kEntity.java b/src/main/java/com/kamco/cd/kamcoback/postgres/entity/MapSheetLearn5kEntity.java new file mode 100644 index 00000000..8c44ef8a --- /dev/null +++ b/src/main/java/com/kamco/cd/kamcoback/postgres/entity/MapSheetLearn5kEntity.java @@ -0,0 +1,51 @@ +package com.kamco.cd.kamcoback.postgres.entity; + +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.FetchType; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.ManyToOne; +import jakarta.persistence.SequenceGenerator; +import jakarta.persistence.Table; +import jakarta.validation.constraints.NotNull; +import java.time.ZonedDateTime; +import lombok.Getter; +import lombok.Setter; +import org.hibernate.annotations.OnDelete; +import org.hibernate.annotations.OnDeleteAction; + +@Getter +@Setter +@Entity +@Table(name = "tb_map_sheet_learn_5k") +public class MapSheetLearn5kEntity { + + @Id + @GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "tb_map_sheet_learn_5k_id_gen") + @SequenceGenerator( + name = "tb_map_sheet_learn_5k_id_gen", + sequenceName = "tb_map_sheet_learn_5k_seq", + allocationSize = 1) + @Column(name = "id", nullable = false) + private Long id; + + @NotNull + @ManyToOne(fetch = FetchType.LAZY, optional = false) + @OnDelete(action = OnDeleteAction.CASCADE) + @JoinColumn(name = "learn_id", nullable = false, referencedColumnName = "id") + private MapSheetLearnEntity learn; + + @NotNull + @Column(name = "map_sheet_num", nullable = false) + private Long mapSheetNum; + + @org.hibernate.annotations.CreationTimestamp + @Column(name = "created_dttm") + private ZonedDateTime createdDttm; + + @Column(name = "created_uid") + private Long createdUid; +} diff --git a/src/main/java/com/kamco/cd/kamcoback/postgres/entity/MapSheetLearnEntity.java b/src/main/java/com/kamco/cd/kamcoback/postgres/entity/MapSheetLearnEntity.java index 062c7d87..5399a611 100644 --- a/src/main/java/com/kamco/cd/kamcoback/postgres/entity/MapSheetLearnEntity.java +++ b/src/main/java/com/kamco/cd/kamcoback/postgres/entity/MapSheetLearnEntity.java @@ -33,7 +33,7 @@ public class MapSheetLearnEntity { @ColumnDefault("gen_random_uuid()") @Column(name = "uuid") - private UUID uuid; + private UUID uuid = UUID.randomUUID(); @Size(max = 200) @NotNull @@ -89,7 +89,7 @@ public class MapSheetLearnEntity { @Column(name = "apply_dttm") private ZonedDateTime applyDttm; - @ColumnDefault("now()") + @org.hibernate.annotations.CreationTimestamp @Column(name = "created_dttm") private ZonedDateTime createdDttm; diff --git a/src/main/java/com/kamco/cd/kamcoback/postgres/repository/Inference/MapSheetLearn5kRepository.java b/src/main/java/com/kamco/cd/kamcoback/postgres/repository/Inference/MapSheetLearn5kRepository.java new file mode 100644 index 00000000..a5969a4a --- /dev/null +++ b/src/main/java/com/kamco/cd/kamcoback/postgres/repository/Inference/MapSheetLearn5kRepository.java @@ -0,0 +1,7 @@ +package com.kamco.cd.kamcoback.postgres.repository.Inference; + +import com.kamco.cd.kamcoback.postgres.entity.MapSheetLearn5kEntity; +import org.springframework.data.jpa.repository.JpaRepository; + +public interface MapSheetLearn5kRepository + extends JpaRepository, MapSheetLearn5kRepositoryCustom {} diff --git a/src/main/java/com/kamco/cd/kamcoback/postgres/repository/Inference/MapSheetLearn5kRepositoryCustom.java b/src/main/java/com/kamco/cd/kamcoback/postgres/repository/Inference/MapSheetLearn5kRepositoryCustom.java new file mode 100644 index 00000000..a81faeae --- /dev/null +++ b/src/main/java/com/kamco/cd/kamcoback/postgres/repository/Inference/MapSheetLearn5kRepositoryCustom.java @@ -0,0 +1,3 @@ +package com.kamco.cd.kamcoback.postgres.repository.Inference; + +public interface MapSheetLearn5kRepositoryCustom {} diff --git a/src/main/java/com/kamco/cd/kamcoback/postgres/repository/Inference/MapSheetLearn5kRepositoryImpl.java b/src/main/java/com/kamco/cd/kamcoback/postgres/repository/Inference/MapSheetLearn5kRepositoryImpl.java new file mode 100644 index 00000000..68c7593c --- /dev/null +++ b/src/main/java/com/kamco/cd/kamcoback/postgres/repository/Inference/MapSheetLearn5kRepositoryImpl.java @@ -0,0 +1,6 @@ +package com.kamco.cd.kamcoback.postgres.repository.Inference; + +import org.springframework.stereotype.Repository; + +@Repository +public class MapSheetLearn5kRepositoryImpl implements MapSheetLearn5kRepositoryCustom {} diff --git a/src/main/java/com/kamco/cd/kamcoback/postgres/repository/label/LabelAllocateRepositoryCustom.java b/src/main/java/com/kamco/cd/kamcoback/postgres/repository/label/LabelAllocateRepositoryCustom.java index 9a7bcc8c..5980da85 100644 --- a/src/main/java/com/kamco/cd/kamcoback/postgres/repository/label/LabelAllocateRepositoryCustom.java +++ b/src/main/java/com/kamco/cd/kamcoback/postgres/repository/label/LabelAllocateRepositoryCustom.java @@ -84,7 +84,6 @@ public interface LabelAllocateRepositoryCustom { void assignOwnerReAllocate(String uuid, String userId, String paramUserId, Long assignCount); - // 프로젝트 종료 여부 업데이트 (uuid 기반) void updateClosedYnByUuid(String uuid, String closedType, String closedYn); } diff --git a/src/main/java/com/kamco/cd/kamcoback/postgres/repository/label/LabelAllocateRepositoryImpl.java b/src/main/java/com/kamco/cd/kamcoback/postgres/repository/label/LabelAllocateRepositoryImpl.java index 12c5450b..95a5105a 100644 --- a/src/main/java/com/kamco/cd/kamcoback/postgres/repository/label/LabelAllocateRepositoryImpl.java +++ b/src/main/java/com/kamco/cd/kamcoback/postgres/repository/label/LabelAllocateRepositoryImpl.java @@ -1433,7 +1433,6 @@ public class LabelAllocateRepositoryImpl implements LabelAllocateRepositoryCusto .execute(); } - @Override public void updateClosedYnByUuid(String uuid, String closedType, String closedYn) { var updateQuery = queryFactory.update(mapSheetAnalInferenceEntity); diff --git a/src/main/resources/application-dev.yml b/src/main/resources/application-dev.yml index 579b7a42..790a1c95 100644 --- a/src/main/resources/application-dev.yml +++ b/src/main/resources/application-dev.yml @@ -11,8 +11,11 @@ spring: hibernate: default_batch_fetch_size: 100 # ✅ 성능 - N+1 쿼리 방지 order_updates: true # ✅ 성능 - 업데이트 순서 정렬로 데드락 방지 + order_inserts: true use_sql_comments: true # ⚠️ 선택 - SQL에 주석 추가 (디버깅용) format_sql: true # ⚠️ 선택 - SQL 포맷팅 (가독성) + jdbc: + batch_size: 1000 # ✅ 추가 (JDBC batch) datasource: url: jdbc:postgresql://192.168.2.127:15432/kamco_cds diff --git a/src/main/resources/application-prod.yml b/src/main/resources/application-prod.yml index 7c633ecd..5a23ca0e 100644 --- a/src/main/resources/application-prod.yml +++ b/src/main/resources/application-prod.yml @@ -4,14 +4,18 @@ spring: on-profile: prod jpa: - show-sql: false + show-sql: true hibernate: ddl-auto: validate properties: hibernate: default_batch_fetch_size: 100 # ✅ 성능 - N+1 쿼리 방지 order_updates: true # ✅ 성능 - 업데이트 순서 정렬로 데드락 방지 + order_inserts: true use_sql_comments: true # ⚠️ 선택 - SQL에 주석 추가 (디버깅용) + format_sql: true # ⚠️ 선택 - SQL 포맷팅 (가독성) + jdbc: + batch_size: 1000 # ✅ 추가 (JDBC batch) datasource: url: jdbc:postgresql://10.100.0.10:25432/temp