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 7da6156c..ae0ada1d 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 @@ -129,6 +129,9 @@ public class LabelAllocateService { Integer page, Integer size) { + // 프로젝트 정보 조회 (analUid가 있을 때만) + var projectInfo = labelAllocateCoreService.findProjectInfo(analUid); + // 작업 진행 현황 조회 var progressInfo = labelAllocateCoreService.findWorkProgressInfo(analUid); @@ -178,6 +181,7 @@ public class LabelAllocateService { (fromIndex < workers.size()) ? workers.subList(fromIndex, toIndex) : List.of(); return WorkerListResponse.builder() + .projectInfo(projectInfo) .progressInfo(progressInfo) .workers(pagedWorkers) .currentPage(page) 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 d71471c0..a16252fa 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 @@ -5,6 +5,7 @@ import com.kamco.cd.kamcoback.label.dto.LabelAllocateDto.AllocateInfoDto; 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.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.WorkerStatistics; import com.kamco.cd.kamcoback.postgres.entity.LabelingAssignmentEntity; @@ -48,6 +49,10 @@ public class LabelAllocateCoreService { return labelAllocateRepository.availUserList(role); } + public ProjectInfo findProjectInfo(Long analUid) { + return labelAllocateRepository.findProjectInfo(analUid); + } + public List findWorkerStatistics( Long analUid, String workerType, 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 a127e142..739c30c2 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 @@ -4,6 +4,7 @@ import com.kamco.cd.kamcoback.label.dto.LabelAllocateDto.AllocateInfoDto; 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.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.WorkerStatistics; import com.kamco.cd.kamcoback.postgres.entity.LabelingAssignmentEntity; @@ -26,6 +27,9 @@ public interface LabelAllocateRepositoryCustom { List availUserList(String role); + // 프로젝트 정보 조회 + ProjectInfo findProjectInfo(Long analUid); + // 작업자 통계 조회 List findWorkerStatistics( Long analUid, String workerType, String search, String sortType); 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 d6ceeb0f..141bace8 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 @@ -14,6 +14,7 @@ import com.kamco.cd.kamcoback.label.dto.LabelAllocateDto.InspectState; import com.kamco.cd.kamcoback.label.dto.LabelAllocateDto.LabelState; import com.kamco.cd.kamcoback.label.dto.LabelAllocateDto.LabelerDetail; 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.WorkerStatistics; import com.kamco.cd.kamcoback.postgres.entity.LabelingAssignmentEntity; @@ -34,9 +35,12 @@ import java.time.LocalDate; import java.time.LocalTime; import java.time.ZoneId; import java.time.ZonedDateTime; +import java.time.format.DateTimeFormatter; import java.util.List; import java.util.Objects; import java.util.UUID; +import java.util.regex.Matcher; +import java.util.regex.Pattern; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Repository; @@ -319,17 +323,28 @@ public class LabelAllocateRepositoryImpl implements LabelAllocateRepositoryCusto .where(analUidCondition) .fetchOne(); - // 완료 + 스킵 건수 - Long completedCount = + // === 라벨링 통계 === + // 라벨링 완료: LABEL_FIN, TEST_ING, DONE (검수 포함) + Long labelingCompleted = queryFactory .select(labelingAssignmentEntity.count()) .from(labelingAssignmentEntity) .where( analUidCondition, - labelingAssignmentEntity.workState.in("DONE", "SKIP")) + labelingAssignmentEntity.workState.in("LABEL_FIN", "TEST_ING", "DONE")) .fetchOne(); - // 투입된 라벨러 수 (고유한 worker_uid 수) + // 스킵 건수 + Long skipCount = + queryFactory + .select(labelingAssignmentEntity.count()) + .from(labelingAssignmentEntity) + .where( + analUidCondition, + labelingAssignmentEntity.workState.eq("SKIP")) + .fetchOne(); + + // 투입된 라벨러 수 Long labelerCount = queryFactory .select(labelingAssignmentEntity.workerUid.countDistinct()) @@ -339,18 +354,18 @@ public class LabelAllocateRepositoryImpl implements LabelAllocateRepositoryCusto labelingAssignmentEntity.workerUid.isNotNull()) .fetchOne(); - // 남은 라벨링 작업 데이터 수 - Long remainingLabelCount = + // === 검수 통계 === + // 검수 완료: DONE만 + Long inspectionCompleted = queryFactory .select(labelingAssignmentEntity.count()) .from(labelingAssignmentEntity) .where( analUidCondition, - labelingAssignmentEntity.workerUid.isNotNull(), - labelingAssignmentEntity.workState.notIn("DONE", "SKIP")) + labelingAssignmentEntity.workState.eq("DONE")) .fetchOne(); - // 투입된 검수자 수 (고유한 inspector_uid 수) + // 투입된 검수자 수 Long inspectorCount = queryFactory .select(labelingAssignmentEntity.inspectorUid.countDistinct()) @@ -360,36 +375,47 @@ public class LabelAllocateRepositoryImpl implements LabelAllocateRepositoryCusto labelingAssignmentEntity.inspectorUid.isNotNull()) .fetchOne(); - // 남은 검수 작업 데이터 수 - Long remainingInspectCount = - queryFactory - .select(labelingAssignmentEntity.count()) - .from(labelingAssignmentEntity) - .where( - analUidCondition, - labelingAssignmentEntity.inspectorUid.isNotNull(), - labelingAssignmentEntity.workState.notIn("DONE")) - .fetchOne(); + // 남은 작업 건수 계산 + long total = 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; // 진행률 계산 - double progressRate = 0.0; - if (totalAssigned != null && totalAssigned > 0) { - progressRate = - (completedCount != null ? completedCount.doubleValue() : 0.0) / totalAssigned * 100; - } + double labelingRate = total > 0 ? (double) labelCompleted / total * 100 : 0.0; + double inspectionRate = total > 0 ? (double) inspectCompleted / total * 100 : 0.0; - // 작업 상태 판단 (간단하게 진행률 100%면 종료, 아니면 진행중) - String workStatus = (progressRate >= 100.0) ? "종료" : "진행중"; + // 상태 판단 + String labelingStatus = labelingRemaining > 0 ? "진행중" : "완료"; + String inspectionStatus = inspectionRemaining > 0 ? "진행중" : "완료"; return WorkProgressInfo.builder() - .labelingProgressRate(progressRate) - .workStatus(workStatus) - .completedCount(completedCount != null ? completedCount : 0L) - .totalAssignedCount(totalAssigned != null ? totalAssigned : 0L) + // 라벨링 + .labelingProgressRate(labelingRate) + .labelingStatus(labelingStatus) + .labelingTotalCount(total) + .labelingCompletedCount(labelCompleted) + .labelingSkipCount(skipped) + .labelingRemainingCount(labelingRemaining) .labelerCount(labelerCount != null ? labelerCount : 0L) - .remainingLabelCount(remainingLabelCount != null ? remainingLabelCount : 0L) + // 검수 + .inspectionProgressRate(inspectionRate) + .inspectionStatus(inspectionStatus) + .inspectionTotalCount(total) + .inspectionCompletedCount(inspectCompleted) + .inspectionSkipCount(skipped) + .inspectionRemainingCount(inspectionRemaining) .inspectorCount(inspectorCount != null ? inspectorCount : 0L) - .remainingInspectCount(remainingInspectCount != null ? remainingInspectCount : 0L) + // 레거시 호환 필드 (Deprecated) + .progressRate(labelingRate) + .totalAssignedCount(total) + .completedCount(labelCompleted) + .remainingLabelCount(labelingRemaining) + .remainingInspectCount(inspectionRemaining) + .workStatus(labelingStatus) .build(); } @@ -600,4 +626,73 @@ public class LabelAllocateRepositoryImpl implements LabelAllocateRepositoryCusto em.flush(); em.clear(); } + + @Override + public ProjectInfo findProjectInfo(Long analUid) { + if (analUid == null) { + return null; + } + + var result = queryFactory + .select( + mapSheetAnalEntity.compareYyyy, + mapSheetAnalEntity.targetYyyy, + mapSheetAnalEntity.analTitle, + mapSheetAnalEntity.gukyuinApplyDttm, + mapSheetAnalEntity.analStrtDttm + ) + .from(mapSheetAnalEntity) + .where(mapSheetAnalEntity.id.eq(analUid)) + .fetchOne(); + + if (result == null) { + return null; + } + + Integer compareYyyy = result.get(mapSheetAnalEntity.compareYyyy); + Integer targetYyyy = result.get(mapSheetAnalEntity.targetYyyy); + String analTitle = result.get(mapSheetAnalEntity.analTitle); + ZonedDateTime gukyuinApplyDttm = result.get(mapSheetAnalEntity.gukyuinApplyDttm); + ZonedDateTime analStrtDttm = result.get(mapSheetAnalEntity.analStrtDttm); + + // 변화탐지년도 생성 + String detectionYear = (compareYyyy != null && targetYyyy != null) + ? compareYyyy + "-" + targetYyyy + : null; + + // 회차 추출 (예: "8회차" → "8") + String round = extractRoundFromTitle(analTitle); + + return ProjectInfo.builder() + .detectionYear(detectionYear) + .round(round) + .reflectionDate(formatDate(gukyuinApplyDttm)) + .startDate(formatDate(analStrtDttm)) + .build(); + } + + /** + * 제목에서 회차 숫자 추출 + * 예: "8회차", "제8회차" → "8" + */ + private String extractRoundFromTitle(String title) { + if (title == null || title.isEmpty()) { + return null; + } + + Pattern pattern = Pattern.compile("(\\d+)회차"); + Matcher matcher = pattern.matcher(title); + + return matcher.find() ? matcher.group(1) : null; + } + + /** + * ZonedDateTime을 "yyyy-MM-dd" 형식으로 변환 + */ + private String formatDate(ZonedDateTime dateTime) { + if (dateTime == null) { + return null; + } + return dateTime.format(DateTimeFormatter.ofPattern("yyyy-MM-dd")); + } }