From 62c4b9e732c0fea8ccdc6ad017601b131baad224 Mon Sep 17 00:00:00 2001 From: DanielLee <198891672+sanghyeonhd@users.noreply.github.com> Date: Fri, 2 Jan 2026 18:59:16 +0900 Subject: [PATCH 1/3] =?UTF-8?q?=EB=9D=BC=EB=B2=A8=EB=9F=AC=20=EB=AA=A9?= =?UTF-8?q?=EB=A1=9D=EC=A1=B0=ED=9A=8C=20=EC=9E=84=EC=8B=9C=EC=BB=A4?= =?UTF-8?q?=EB=B0=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../label/LabelAllocateApiController.java | 217 ++++++++++++++- .../label/service/LabelAllocateService.java | 69 ++++- .../core/LabelAllocateCoreService.java | 22 ++ .../label/LabelAllocateRepositoryCustom.java | 13 + .../label/LabelAllocateRepositoryImpl.java | 251 +++++++++++++++++- 5 files changed, 555 insertions(+), 17 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 0a52baf2..8f53ccbf 100644 --- a/src/main/java/com/kamco/cd/kamcoback/label/LabelAllocateApiController.java +++ b/src/main/java/com/kamco/cd/kamcoback/label/LabelAllocateApiController.java @@ -5,9 +5,12 @@ import com.kamco.cd.kamcoback.config.api.ApiResponseDto; import com.kamco.cd.kamcoback.label.dto.LabelAllocateDto; import com.kamco.cd.kamcoback.label.dto.LabelAllocateDto.TargetInspector; import com.kamco.cd.kamcoback.label.dto.LabelAllocateDto.TargetUser; +import com.kamco.cd.kamcoback.label.dto.WorkerStatsDto.WorkerListResponse; import com.kamco.cd.kamcoback.label.service.LabelAllocateService; import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; import io.swagger.v3.oas.annotations.media.Content; +import io.swagger.v3.oas.annotations.media.ExampleObject; import io.swagger.v3.oas.annotations.media.Schema; import io.swagger.v3.oas.annotations.responses.ApiResponse; import io.swagger.v3.oas.annotations.responses.ApiResponses; @@ -23,7 +26,7 @@ import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; @Slf4j -@Tag(name = "라벨링 작업 관리", description = "라벨링 작업 관리") +@Tag(name = "라벨링 작업 관리", description = "라벨링 작업 배정 및 통계 조회 API") @RequestMapping({"/api/label"}) @RequiredArgsConstructor @RestController @@ -31,7 +34,7 @@ public class LabelAllocateApiController { private final LabelAllocateService labelAllocateService; - @Operation(summary = "배정 가능한 사용자 목록 조회", description = "배정 가능한 사용자 목록 조회") + @Operation(summary = "배정 가능한 사용자 목록 조회", description = "라벨링 작업 배정을 위한 활성 상태의 사용자 목록을 조회합니다.") @ApiResponses( value = { @ApiResponse( @@ -40,16 +43,222 @@ public class LabelAllocateApiController { content = @Content( mediaType = "application/json", - schema = @Schema(implementation = CommonCodeDto.Basic.class))), + schema = @Schema(implementation = CommonCodeDto.Basic.class), + examples = + @ExampleObject( + name = "사용자 목록 응답", + value = + """ + { + "data": [ + { + "userRole": "LABELER", + "employeeNo": "1234567", + "name": "김라벨" + }, + { + "userRole": "LABELER", + "employeeNo": "2345678", + "name": "이작업" + } + ] + } + """))), @ApiResponse(responseCode = "404", description = "코드를 찾을 수 없음", content = @Content), @ApiResponse(responseCode = "500", description = "서버 오류", content = @Content) }) @GetMapping("/avail-user") public ApiResponseDto> availUserList( - @RequestParam @Schema() String role) { + @Parameter(description = "사용자 역할 (LABELER: 라벨러, INSPECTOR: 검수자)", example = "LABELER") + @RequestParam + @Schema() + String role) { return ApiResponseDto.ok(labelAllocateService.availUserList(role)); } + @Operation( + summary = "작업자 목록 및 3일치 통계 조회", + description = + """ + 학습데이터 제작 현황 조회 API입니다. + """) + @ApiResponses( + value = { + @ApiResponse( + responseCode = "200", + description = "조회 성공", + content = + @Content( + mediaType = "application/json", + schema = @Schema(implementation = WorkerListResponse.class), + examples = { + @ExampleObject( + name = "라벨러 조회 예시", + description = "라벨러 작업자들의 통계 정보", + value = + """ + { + "data": { + "progressInfo": { + "labelingProgressRate": 79.34, + "workStatus": "진행중", + "completedCount": 6554, + "totalAssignedCount": 8258, + "labelerCount": 5, + "remainingLabelCount": 1704, + "inspectorCount": 3, + "remainingInspectCount": 890 + }, + "workers": [ + { + "workerId": "1234567", + "workerName": "김라벨", + "workerType": "LABELER", + "totalAssigned": 1500, + "completed": 1100, + "skipped": 50, + "remaining": 350, + "history": { + "day1Ago": 281, + "day2Ago": 302, + "day3Ago": 294, + "average": 292 + }, + "isStagnated": false + }, + { + "workerId": "2345678", + "workerName": "이작업", + "workerType": "LABELER", + "totalAssigned": 2000, + "completed": 1850, + "skipped": 100, + "remaining": 50, + "history": { + "day1Ago": 5, + "day2Ago": 3, + "day3Ago": 8, + "average": 5 + }, + "isStagnated": true + } + ] + } + } + """), + @ExampleObject( + name = "검수자 조회 예시", + description = "검수자 작업자들의 통계 정보", + value = + """ + { + "data": { + "progressInfo": { + "labelingProgressRate": 79.34, + "workStatus": "진행중", + "completedCount": 6554, + "totalAssignedCount": 8258, + "labelerCount": 5, + "remainingLabelCount": 1704, + "inspectorCount": 3, + "remainingInspectCount": 890 + }, + "workers": [ + { + "workerId": "9876543", + "workerName": "박검수", + "workerType": "INSPECTOR", + "totalAssigned": 1200, + "completed": 980, + "skipped": 20, + "remaining": 200, + "history": { + "day1Ago": 150, + "day2Ago": 145, + "day3Ago": 155, + "average": 150 + }, + "isStagnated": false + } + ] + } + } + """) + })), + @ApiResponse( + responseCode = "404", + description = "데이터를 찾을 수 없음", + content = + @Content( + examples = + @ExampleObject( + value = + """ + { + "error": { + "code": "NOT_FOUND", + "message": "해당 분석 ID의 데이터를 찾을 수 없습니다." + } + } + """))), + @ApiResponse( + responseCode = "500", + description = "서버 오류", + content = + @Content( + examples = + @ExampleObject( + value = + """ + { + "error": { + "code": "INTERNAL_SERVER_ERROR", + "message": "서버에 문제가 발생 하였습니다." + } + } + """))) + }) + @GetMapping("/admin/workers") + public ApiResponseDto getWorkerStatistics( + @Parameter(description = "분석 ID (필수)", required = true, example = "3") @RequestParam + Long analUid, + @Parameter( + description = "작업자 유형 (선택) - 미입력 시 LABELER로 조회", + example = "LABELER", + schema = + @Schema( + allowableValues = {"LABELER", "INSPECTOR"}, + defaultValue = "LABELER")) + @RequestParam(required = false) + String type, + @Parameter(description = "작업자 이름 검색 (부분 일치)", example = "김라벨") @RequestParam(required = false) + String searchName, + @Parameter(description = "작업자 사번 검색 (부분 일치)", example = "1234567") + @RequestParam(required = false) + String searchEmployeeNo, + @Parameter( + description = "정렬 조건 (선택) - 미입력 시 이름 오름차순", + example = "REMAINING_DESC", + schema = + @Schema( + allowableValues = { + "REMAINING_DESC", + "REMAINING_ASC", + "NAME_ASC", + "NAME_DESC" + }, + defaultValue = "NAME_ASC")) + @RequestParam(required = false) + String sort) { + + // type이 null이면 전체 조회 (일단 LABELER로 기본 설정) + String workerType = (type == null || type.isEmpty()) ? "LABELER" : type; + + return ApiResponseDto.ok( + labelAllocateService.getWorkerStatistics( + analUid, workerType, searchName, searchEmployeeNo, sort)); + } + // 라벨링 수량 할당하는 로직 테스트 @PostMapping("/allocate") public ApiResponseDto labelAllocate(@RequestBody LabelAllocateDto dto) { 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 6d273a95..98fd7a70 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 @@ -4,15 +4,20 @@ import com.kamco.cd.kamcoback.label.dto.LabelAllocateDto; import com.kamco.cd.kamcoback.label.dto.LabelAllocateDto.TargetInspector; 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.DailyHistory; +import com.kamco.cd.kamcoback.label.dto.WorkerStatsDto.WorkerListResponse; +import com.kamco.cd.kamcoback.label.dto.WorkerStatsDto.WorkerStatistics; import com.kamco.cd.kamcoback.postgres.core.LabelAllocateCoreService; -import jakarta.transaction.Transactional; +import java.time.LocalDate; import java.util.List; import java.util.Objects; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; @Slf4j @Service +@Transactional(readOnly = true) public class LabelAllocateService { private final LabelAllocateCoreService labelAllocateCoreService; @@ -24,7 +29,8 @@ public class LabelAllocateService { /** * 도엽 기준 asc sorting 해서 할당 수만큼 배정하는 로직 * - * @param targetUsers + * @param targetUsers 라벨러 타겟 목록 + * @param targetInspectors 검수자 타겟 목록 */ @Transactional public void allocateAsc(List targetUsers, List targetInspectors) { @@ -80,4 +86,63 @@ public class LabelAllocateService { public List availUserList(String role) { return labelAllocateCoreService.availUserList(role); } + + /** + * 작업자 목록 및 3일치 통계 조회 + * + * @param analUid 분석 ID + * @param workerType 작업자 유형 (LABELER/INSPECTOR) + * @param searchName 이름 검색 + * @param searchEmployeeNo 사번 검색 + * @param sortType 정렬 조건 + * @return 작업자 목록 및 통계 + */ + public WorkerListResponse getWorkerStatistics( + Long analUid, + String workerType, + String searchName, + String searchEmployeeNo, + String sortType) { + + // 작업 진행 현황 조회 + var progressInfo = labelAllocateCoreService.findWorkProgressInfo(analUid); + + // 작업자 통계 조회 + List workers = + labelAllocateCoreService.findWorkerStatistics( + analUid, workerType, searchName, searchEmployeeNo, sortType); + + // 각 작업자별 3일치 처리량 조회 + LocalDate today = LocalDate.now(); + for (WorkerStatistics worker : workers) { + Long day1Count = + labelAllocateCoreService.findDailyProcessedCount( + worker.getWorkerId(), workerType, today.minusDays(1), analUid); + Long day2Count = + labelAllocateCoreService.findDailyProcessedCount( + worker.getWorkerId(), workerType, today.minusDays(2), analUid); + Long day3Count = + labelAllocateCoreService.findDailyProcessedCount( + worker.getWorkerId(), workerType, today.minusDays(3), analUid); + + long average = (day1Count + day2Count + day3Count) / 3; + + DailyHistory history = + DailyHistory.builder() + .day1Ago(day1Count) + .day2Ago(day2Count) + .day3Ago(day3Count) + .average(average) + .build(); + + worker.setHistory(history); + + // 정체 여부 판단 (3일 평균이 특정 기준 미만일 때 - 예: 10건 미만) + if (average < 10) { + worker.setIsStagnated(true); + } + } + + return WorkerListResponse.builder().progressInfo(progressInfo).workers(workers).build(); + } } 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 1d8fa075..c1719913 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 @@ -2,8 +2,11 @@ package com.kamco.cd.kamcoback.postgres.core; import com.kamco.cd.kamcoback.label.dto.LabelAllocateDto; import com.kamco.cd.kamcoback.label.dto.LabelAllocateDto.UserList; +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; import com.kamco.cd.kamcoback.postgres.repository.label.LabelAllocateRepository; +import java.time.LocalDate; import java.util.List; import java.util.UUID; import lombok.RequiredArgsConstructor; @@ -40,4 +43,23 @@ public class LabelAllocateCoreService { public List availUserList(String role) { return labelAllocateRepository.availUserList(role); } + + public List findWorkerStatistics( + Long analUid, + String workerType, + String searchName, + String searchEmployeeNo, + String sortType) { + return labelAllocateRepository.findWorkerStatistics( + analUid, workerType, searchName, searchEmployeeNo, sortType); + } + + public WorkProgressInfo findWorkProgressInfo(Long analUid) { + return labelAllocateRepository.findWorkProgressInfo(analUid); + } + + 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 659f5c7b..5392d88f 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 @@ -1,7 +1,10 @@ package com.kamco.cd.kamcoback.postgres.repository.label; import com.kamco.cd.kamcoback.label.dto.LabelAllocateDto.UserList; +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; +import java.time.LocalDate; import java.util.List; import java.util.UUID; @@ -18,4 +21,14 @@ public interface LabelAllocateRepositoryCustom { void assignInspector(UUID assignmentUid, String userId); List availUserList(String role); + + // 작업자 통계 조회 + List findWorkerStatistics( + Long analUid, String workerType, String searchName, String searchEmployeeNo, String sortType); + + // 작업 진행 현황 조회 + WorkProgressInfo findWorkProgressInfo(Long analUid); + + // 작업자별 일일 처리량 조회 + 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 f9e419d2..7ea657dc 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 @@ -8,38 +8,39 @@ import static com.kamco.cd.kamcoback.postgres.entity.QMemberEntity.memberEntity; import com.kamco.cd.kamcoback.label.dto.LabelAllocateDto; import com.kamco.cd.kamcoback.label.dto.LabelAllocateDto.LabelState; import com.kamco.cd.kamcoback.label.dto.LabelAllocateDto.UserList; +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; -import com.kamco.cd.kamcoback.postgres.entity.MapSheetAnalDataGeomEntity; import com.kamco.cd.kamcoback.postgres.entity.MapSheetAnalEntity; import com.querydsl.core.types.Projections; -import com.querydsl.core.types.dsl.Expressions; +import com.querydsl.core.types.dsl.BooleanExpression; +import com.querydsl.core.types.dsl.CaseBuilder; +import com.querydsl.core.types.dsl.NumberExpression; import com.querydsl.core.types.dsl.StringExpression; import com.querydsl.jpa.impl.JPAQueryFactory; import jakarta.persistence.EntityManager; import jakarta.persistence.EntityNotFoundException; import jakarta.persistence.PersistenceContext; +import java.time.LocalDate; +import java.time.LocalTime; +import java.time.ZoneId; +import java.time.ZonedDateTime; import java.util.List; import java.util.Objects; import java.util.UUID; +import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; -import org.springframework.data.jpa.repository.support.QuerydslRepositorySupport; import org.springframework.stereotype.Repository; @Slf4j @Repository -public class LabelAllocateRepositoryImpl extends QuerydslRepositorySupport - implements LabelAllocateRepositoryCustom { +@RequiredArgsConstructor +public class LabelAllocateRepositoryImpl implements LabelAllocateRepositoryCustom { private final JPAQueryFactory queryFactory; - private final StringExpression NULL_STRING = Expressions.stringTemplate("cast(null as text)"); @PersistenceContext private EntityManager em; - public LabelAllocateRepositoryImpl(JPAQueryFactory queryFactory) { - super(MapSheetAnalDataGeomEntity.class); - this.queryFactory = queryFactory; - } - @Override public List fetchNextIds(Long lastId, int batchSize) { @@ -150,4 +151,232 @@ public class LabelAllocateRepositoryImpl extends QuerydslRepositorySupport .orderBy(memberEntity.name.asc()) .fetch(); } + + @Override + public List findWorkerStatistics( + Long analUid, + String workerType, + String searchName, + String searchEmployeeNo, + String sortType) { + + // 작업자 유형에 따른 필드 선택 + StringExpression workerIdField = + "INSPECTOR".equals(workerType) + ? labelingAssignmentEntity.inspectorUid + : labelingAssignmentEntity.workerUid; + + BooleanExpression workerCondition = + "INSPECTOR".equals(workerType) + ? labelingAssignmentEntity.inspectorUid.isNotNull() + : labelingAssignmentEntity.workerUid.isNotNull(); + + // 검색 조건 + BooleanExpression searchCondition = null; + if (searchName != null && !searchName.isEmpty()) { + searchCondition = memberEntity.name.contains(searchName); + } + if (searchEmployeeNo != null && !searchEmployeeNo.isEmpty()) { + BooleanExpression empCondition = memberEntity.employeeNo.contains(searchEmployeeNo); + searchCondition = searchCondition == null ? empCondition : searchCondition.and(empCondition); + } + + // 완료, 스킵, 남은 작업 계산 + NumberExpression completedSum = + new CaseBuilder() + .when(labelingAssignmentEntity.workState.eq("DONE")) + .then(1L) + .otherwise(0L) + .sum(); + + NumberExpression skippedSum = + new CaseBuilder() + .when(labelingAssignmentEntity.workState.eq("SKIP")) + .then(1L) + .otherwise(0L) + .sum(); + + NumberExpression remainingSum = + new CaseBuilder() + .when( + labelingAssignmentEntity + .workState + .notIn("DONE", "SKIP") + .and(labelingAssignmentEntity.workState.isNotNull())) + .then(1L) + .otherwise(0L) + .sum(); + + // 기본 통계 조회 쿼리 + var baseQuery = + queryFactory + .select( + workerIdField, + memberEntity.name, + workerIdField.count(), + completedSum, + skippedSum, + remainingSum, + labelingAssignmentEntity.stagnationYn.max()) + .from(labelingAssignmentEntity) + .leftJoin(memberEntity) + .on( + "INSPECTOR".equals(workerType) + ? memberEntity.employeeNo.eq(labelingAssignmentEntity.inspectorUid) + : memberEntity.employeeNo.eq(labelingAssignmentEntity.workerUid)) + .where(labelingAssignmentEntity.analUid.eq(analUid), workerCondition, searchCondition) + .groupBy(workerIdField, memberEntity.name); + + // 정렬 조건 적용 + if (sortType != null) { + switch (sortType) { + case "REMAINING_DESC": + baseQuery.orderBy(remainingSum.desc()); + break; + case "REMAINING_ASC": + baseQuery.orderBy(remainingSum.asc()); + break; + case "NAME_ASC": + baseQuery.orderBy(memberEntity.name.asc()); + break; + case "NAME_DESC": + baseQuery.orderBy(memberEntity.name.desc()); + break; + default: + baseQuery.orderBy(memberEntity.name.asc()); + } + } else { + baseQuery.orderBy(memberEntity.name.asc()); + } + + // 결과를 DTO로 변환 + return baseQuery.fetch().stream() + .map( + tuple -> { + Character maxStagnationYn = tuple.get(labelingAssignmentEntity.stagnationYn.max()); + return WorkerStatistics.builder() + .workerId(tuple.get(workerIdField)) + .workerName(tuple.get(memberEntity.name)) + .workerType(workerType) + .totalAssigned(tuple.get(workerIdField.count())) + .completed(tuple.get(completedSum)) + .skipped(tuple.get(skippedSum)) + .remaining(tuple.get(remainingSum)) + .history(null) // 3일 이력은 Service에서 채움 + .isStagnated(maxStagnationYn != null && maxStagnationYn == 'Y') + .build(); + }) + .toList(); + } + + @Override + public WorkProgressInfo findWorkProgressInfo(Long analUid) { + // 전체 배정 건수 + Long totalAssigned = + queryFactory + .select(labelingAssignmentEntity.count()) + .from(labelingAssignmentEntity) + .where(labelingAssignmentEntity.analUid.eq(analUid)) + .fetchOne(); + + // 완료 + 스킵 건수 + Long completedCount = + queryFactory + .select(labelingAssignmentEntity.count()) + .from(labelingAssignmentEntity) + .where( + labelingAssignmentEntity.analUid.eq(analUid), + labelingAssignmentEntity.workState.in("DONE", "SKIP")) + .fetchOne(); + + // 투입된 라벨러 수 (고유한 worker_uid 수) + Long labelerCount = + queryFactory + .select(labelingAssignmentEntity.workerUid.countDistinct()) + .from(labelingAssignmentEntity) + .where( + labelingAssignmentEntity.analUid.eq(analUid), + labelingAssignmentEntity.workerUid.isNotNull()) + .fetchOne(); + + // 남은 라벨링 작업 데이터 수 + Long remainingLabelCount = + queryFactory + .select(labelingAssignmentEntity.count()) + .from(labelingAssignmentEntity) + .where( + labelingAssignmentEntity.analUid.eq(analUid), + labelingAssignmentEntity.workerUid.isNotNull(), + labelingAssignmentEntity.workState.notIn("DONE", "SKIP")) + .fetchOne(); + + // 투입된 검수자 수 (고유한 inspector_uid 수) + Long inspectorCount = + queryFactory + .select(labelingAssignmentEntity.inspectorUid.countDistinct()) + .from(labelingAssignmentEntity) + .where( + labelingAssignmentEntity.analUid.eq(analUid), + labelingAssignmentEntity.inspectorUid.isNotNull()) + .fetchOne(); + + // 남은 검수 작업 데이터 수 + Long remainingInspectCount = + queryFactory + .select(labelingAssignmentEntity.count()) + .from(labelingAssignmentEntity) + .where( + labelingAssignmentEntity.analUid.eq(analUid), + labelingAssignmentEntity.inspectorUid.isNotNull(), + labelingAssignmentEntity.workState.notIn("DONE")) + .fetchOne(); + + // 진행률 계산 + double progressRate = 0.0; + if (totalAssigned != null && totalAssigned > 0) { + progressRate = + (completedCount != null ? completedCount.doubleValue() : 0.0) / totalAssigned * 100; + } + + // 작업 상태 판단 (간단하게 진행률 100%면 종료, 아니면 진행중) + String workStatus = (progressRate >= 100.0) ? "종료" : "진행중"; + + return WorkProgressInfo.builder() + .labelingProgressRate(progressRate) + .workStatus(workStatus) + .completedCount(completedCount != null ? completedCount : 0L) + .totalAssignedCount(totalAssigned != null ? totalAssigned : 0L) + .labelerCount(labelerCount != null ? labelerCount : 0L) + .remainingLabelCount(remainingLabelCount != null ? remainingLabelCount : 0L) + .inspectorCount(inspectorCount != null ? inspectorCount : 0L) + .remainingInspectCount(remainingInspectCount != null ? remainingInspectCount : 0L) + .build(); + } + + @Override + public Long findDailyProcessedCount( + String workerId, String workerType, LocalDate date, Long analUid) { + + // 해당 날짜의 시작과 끝 시간 + ZonedDateTime startOfDay = date.atStartOfDay(ZoneId.systemDefault()); + ZonedDateTime endOfDay = date.atTime(LocalTime.MAX).atZone(ZoneId.systemDefault()); + + BooleanExpression workerCondition = + "INSPECTOR".equals(workerType) + ? labelingAssignmentEntity.inspectorUid.eq(workerId) + : labelingAssignmentEntity.workerUid.eq(workerId); + + Long count = + queryFactory + .select(labelingAssignmentEntity.count()) + .from(labelingAssignmentEntity) + .where( + labelingAssignmentEntity.analUid.eq(analUid), + workerCondition, + labelingAssignmentEntity.workState.in("DONE", "SKIP"), + labelingAssignmentEntity.modifiedDate.between(startOfDay, endOfDay)) + .fetchOne(); + + return count != null ? count : 0L; + } } From 513a2d3ebedf70fa985f8466cd1c06963757bd4f Mon Sep 17 00:00:00 2001 From: DanielLee <198891672+sanghyeonhd@users.noreply.github.com> Date: Fri, 2 Jan 2026 19:16:00 +0900 Subject: [PATCH 2/3] =?UTF-8?q?=EB=9D=BC=EB=B2=A8=EB=9F=AC=20=EB=AA=A9?= =?UTF-8?q?=EB=A1=9D=EC=A1=B0=ED=9A=8C=20=EC=9E=84=EC=8B=9C=EC=BB=A4?= =?UTF-8?q?=EB=B0=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../kamcoback/label/dto/WorkerStatsDto.java | 117 ++++++++++++++++++ 1 file changed, 117 insertions(+) create mode 100644 src/main/java/com/kamco/cd/kamcoback/label/dto/WorkerStatsDto.java 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 new file mode 100644 index 00000000..efca81b2 --- /dev/null +++ b/src/main/java/com/kamco/cd/kamcoback/label/dto/WorkerStatsDto.java @@ -0,0 +1,117 @@ +package com.kamco.cd.kamcoback.label.dto; + +import io.swagger.v3.oas.annotations.media.Schema; +import java.util.List; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; + +public class WorkerStatsDto { + + @Getter + @Setter + @Builder + @NoArgsConstructor + @AllArgsConstructor + @Schema(description = "작업자 통계 응답") + public static class WorkerStatistics { + + @Schema(description = "작업자 ID (사번)") + private String workerId; + + @Schema(description = "작업자 이름") + private String workerName; + + @Schema(description = "작업자 유형 (LABELER/INSPECTOR)") + private String workerType; + + @Schema(description = "전체 배정 건수") + private Long totalAssigned; + + @Schema(description = "완료 건수") + private Long completed; + + @Schema(description = "스킵 건수") + private Long skipped; + + @Schema(description = "남은 작업 건수") + private Long remaining; + + @Schema(description = "최근 3일간 처리 이력") + private DailyHistory history; + + @Schema(description = "작업 정체 여부 (3일간 실적이 저조하면 true)") + private Boolean isStagnated; + } + + @Getter + @Setter + @Builder + @NoArgsConstructor + @AllArgsConstructor + @Schema(description = "최근 3일간 일일 처리 이력") + public static class DailyHistory { + + @Schema(description = "1일 전 (어제) 처리량") + private Long day1Ago; + + @Schema(description = "2일 전 처리량") + private Long day2Ago; + + @Schema(description = "3일 전 처리량") + private Long day3Ago; + + @Schema(description = "3일 평균 처리량") + private Long average; + } + + @Getter + @Setter + @Builder + @NoArgsConstructor + @AllArgsConstructor + @Schema(description = "작업 진행 현황 정보") + public static class WorkProgressInfo { + + @Schema(description = "라벨링 진행률 (완료건+스킵건)/배정건") + private Double labelingProgressRate; + + @Schema(description = "작업 상태 (진행중/종료)") + private String workStatus; + + @Schema(description = "진행률 수치 (완료+스킵)") + private Long completedCount; + + @Schema(description = "전체 배정 건수") + private Long totalAssignedCount; + + @Schema(description = "투입된 라벨러 수") + private Long labelerCount; + + @Schema(description = "남은 라벨링 작업 데이터 수") + private Long remainingLabelCount; + + @Schema(description = "투입된 검수자 수") + private Long inspectorCount; + + @Schema(description = "남은 검수 작업 데이터 수") + private Long remainingInspectCount; + } + + @Getter + @Setter + @Builder + @NoArgsConstructor + @AllArgsConstructor + @Schema(description = "작업자 목록 응답 (작업 정보 포함)") + public static class WorkerListResponse { + + @Schema(description = "작업 진행 현황 정보") + private WorkProgressInfo progressInfo; + + @Schema(description = "작업자 목록") + private List workers; + } +} From 89b1ea755fc6986ee6e72ecdd71586a323604f55 Mon Sep 17 00:00:00 2001 From: "gayoun.park" Date: Fri, 2 Jan 2026 20:19:54 +0900 Subject: [PATCH 3/3] =?UTF-8?q?=EB=9D=BC=EB=B2=A8=ED=95=A0=EB=8B=B9=20?= =?UTF-8?q?=EC=88=98=EC=A0=95,=20=EC=83=81=EC=84=B8=20=EC=A1=B0=ED=9A=8C?= =?UTF-8?q?=20API?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../config/GlobalExceptionHandler.java | 3 +- .../label/LabelAllocateApiController.java | 214 ++++++++++-------- .../kamcoback/label/dto/LabelAllocateDto.java | 39 ++++ .../label/service/LabelAllocateService.java | 72 +++--- .../core/LabelAllocateCoreService.java | 11 +- .../postgres/entity/MapSheetAnalEntity.java | 3 + .../label/LabelAllocateRepositoryCustom.java | 7 +- .../label/LabelAllocateRepositoryImpl.java | 74 ++++-- 8 files changed, 268 insertions(+), 155 deletions(-) diff --git a/src/main/java/com/kamco/cd/kamcoback/config/GlobalExceptionHandler.java b/src/main/java/com/kamco/cd/kamcoback/config/GlobalExceptionHandler.java index 161b1c11..bff7b299 100644 --- a/src/main/java/com/kamco/cd/kamcoback/config/GlobalExceptionHandler.java +++ b/src/main/java/com/kamco/cd/kamcoback/config/GlobalExceptionHandler.java @@ -463,7 +463,8 @@ public class GlobalExceptionHandler { String stackTraceStr = Arrays.stream(stackTrace) .map(StackTraceElement::toString) - .collect(Collectors.joining("\n")); + .collect(Collectors.joining("\n")) + .substring(0, 255); ErrorLogEntity errorLogEntity = new ErrorLogEntity( 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 8f53ccbf..9e359a82 100644 --- a/src/main/java/com/kamco/cd/kamcoback/label/LabelAllocateApiController.java +++ b/src/main/java/com/kamco/cd/kamcoback/label/LabelAllocateApiController.java @@ -3,8 +3,7 @@ package com.kamco.cd.kamcoback.label; import com.kamco.cd.kamcoback.code.dto.CommonCodeDto; import com.kamco.cd.kamcoback.config.api.ApiResponseDto; import com.kamco.cd.kamcoback.label.dto.LabelAllocateDto; -import com.kamco.cd.kamcoback.label.dto.LabelAllocateDto.TargetInspector; -import com.kamco.cd.kamcoback.label.dto.LabelAllocateDto.TargetUser; +import com.kamco.cd.kamcoback.label.dto.LabelAllocateDto.InferenceDetail; import com.kamco.cd.kamcoback.label.dto.WorkerStatsDto.WorkerListResponse; import com.kamco.cd.kamcoback.label.service.LabelAllocateService; import io.swagger.v3.oas.annotations.Operation; @@ -69,7 +68,7 @@ public class LabelAllocateApiController { }) @GetMapping("/avail-user") public ApiResponseDto> availUserList( - @Parameter(description = "사용자 역할 (LABELER: 라벨러, INSPECTOR: 검수자)", example = "LABELER") + @Parameter(description = "사용자 역할 (LABELER: 라벨러, REVIEWER: 검수자)", example = "LABELER") @RequestParam @Schema() String role) { @@ -78,10 +77,9 @@ public class LabelAllocateApiController { @Operation( summary = "작업자 목록 및 3일치 통계 조회", - description = - """ - 학습데이터 제작 현황 조회 API입니다. - """) + description = """ + 학습데이터 제작 현황 조회 API입니다. + """) @ApiResponses( value = { @ApiResponse( @@ -97,93 +95,93 @@ public class LabelAllocateApiController { description = "라벨러 작업자들의 통계 정보", value = """ - { - "data": { - "progressInfo": { - "labelingProgressRate": 79.34, - "workStatus": "진행중", - "completedCount": 6554, - "totalAssignedCount": 8258, - "labelerCount": 5, - "remainingLabelCount": 1704, - "inspectorCount": 3, - "remainingInspectCount": 890 - }, - "workers": [ - { - "workerId": "1234567", - "workerName": "김라벨", - "workerType": "LABELER", - "totalAssigned": 1500, - "completed": 1100, - "skipped": 50, - "remaining": 350, - "history": { - "day1Ago": 281, - "day2Ago": 302, - "day3Ago": 294, - "average": 292 - }, - "isStagnated": false + { + "data": { + "progressInfo": { + "labelingProgressRate": 79.34, + "workStatus": "진행중", + "completedCount": 6554, + "totalAssignedCount": 8258, + "labelerCount": 5, + "remainingLabelCount": 1704, + "inspectorCount": 3, + "remainingInspectCount": 890 }, - { - "workerId": "2345678", - "workerName": "이작업", - "workerType": "LABELER", - "totalAssigned": 2000, - "completed": 1850, - "skipped": 100, - "remaining": 50, - "history": { - "day1Ago": 5, - "day2Ago": 3, - "day3Ago": 8, - "average": 5 + "workers": [ + { + "workerId": "1234567", + "workerName": "김라벨", + "workerType": "LABELER", + "totalAssigned": 1500, + "completed": 1100, + "skipped": 50, + "remaining": 350, + "history": { + "day1Ago": 281, + "day2Ago": 302, + "day3Ago": 294, + "average": 292 + }, + "isStagnated": false }, - "isStagnated": true - } - ] + { + "workerId": "2345678", + "workerName": "이작업", + "workerType": "LABELER", + "totalAssigned": 2000, + "completed": 1850, + "skipped": 100, + "remaining": 50, + "history": { + "day1Ago": 5, + "day2Ago": 3, + "day3Ago": 8, + "average": 5 + }, + "isStagnated": true + } + ] + } } - } - """), + """), @ExampleObject( name = "검수자 조회 예시", description = "검수자 작업자들의 통계 정보", value = """ - { - "data": { - "progressInfo": { - "labelingProgressRate": 79.34, - "workStatus": "진행중", - "completedCount": 6554, - "totalAssignedCount": 8258, - "labelerCount": 5, - "remainingLabelCount": 1704, - "inspectorCount": 3, - "remainingInspectCount": 890 - }, - "workers": [ - { - "workerId": "9876543", - "workerName": "박검수", - "workerType": "INSPECTOR", - "totalAssigned": 1200, - "completed": 980, - "skipped": 20, - "remaining": 200, - "history": { - "day1Ago": 150, - "day2Ago": 145, - "day3Ago": 155, - "average": 150 - }, - "isStagnated": false - } - ] + { + "data": { + "progressInfo": { + "labelingProgressRate": 79.34, + "workStatus": "진행중", + "completedCount": 6554, + "totalAssignedCount": 8258, + "labelerCount": 5, + "remainingLabelCount": 1704, + "inspectorCount": 3, + "remainingInspectCount": 890 + }, + "workers": [ + { + "workerId": "9876543", + "workerName": "박검수", + "workerType": "REVIEWER", + "totalAssigned": 1200, + "completed": 980, + "skipped": 20, + "remaining": 200, + "history": { + "day1Ago": 150, + "day2Ago": 145, + "day3Ago": 155, + "average": 150 + }, + "isStagnated": false + } + ] + } } - } - """) + """) })), @ApiResponse( responseCode = "404", @@ -227,7 +225,7 @@ public class LabelAllocateApiController { example = "LABELER", schema = @Schema( - allowableValues = {"LABELER", "INSPECTOR"}, + allowableValues = {"LABELER", "REVIEWER"}, defaultValue = "LABELER")) @RequestParam(required = false) String type, @@ -259,22 +257,40 @@ public class LabelAllocateApiController { analUid, workerType, searchName, searchEmployeeNo, sort)); } - // 라벨링 수량 할당하는 로직 테스트 + @ApiResponses( + value = { + @ApiResponse( + responseCode = "201", + description = "등록 성공", + content = + @Content( + mediaType = "application/json", + schema = @Schema(implementation = Long.class))), + @ApiResponse(responseCode = "400", description = "잘못된 요청 데이터", content = @Content), + @ApiResponse(responseCode = "404", description = "코드를 찾을 수 없음", content = @Content), + @ApiResponse(responseCode = "500", description = "서버 오류", content = @Content) + }) @PostMapping("/allocate") - public ApiResponseDto labelAllocate(@RequestBody LabelAllocateDto dto) { + public ApiResponseDto labelAllocate( + @io.swagger.v3.oas.annotations.parameters.RequestBody( + description = "라벨링 수량 할당", + required = true, + content = + @Content( + mediaType = "application/json", + schema = @Schema(implementation = LabelAllocateDto.AllocateDto.class))) + @RequestBody + LabelAllocateDto.AllocateDto dto) + throws Exception { - List targets = - List.of( - new TargetUser("1234567", 1000), - new TargetUser("2345678", 400), - new TargetUser("3456789", 440)); - List inspectors = - List.of( - new TargetInspector("9876543", 1000), - new TargetInspector("8765432", 340), - new TargetInspector("98765432", 500)); - labelAllocateService.allocateAsc(targets, inspectors); + labelAllocateService.allocateAsc( + dto.getAutoType(), dto.getStage(), dto.getLabelers(), dto.getInspectors()); return ApiResponseDto.ok(null); } + + @GetMapping + public ApiResponseDto findInferenceDetail(@RequestParam Long analUid) { + return ApiResponseDto.ok(labelAllocateService.findInferenceDetail(analUid)); + } } diff --git a/src/main/java/com/kamco/cd/kamcoback/label/dto/LabelAllocateDto.java b/src/main/java/com/kamco/cd/kamcoback/label/dto/LabelAllocateDto.java index d1705e92..5b508a07 100644 --- a/src/main/java/com/kamco/cd/kamcoback/label/dto/LabelAllocateDto.java +++ b/src/main/java/com/kamco/cd/kamcoback/label/dto/LabelAllocateDto.java @@ -2,7 +2,9 @@ package com.kamco.cd.kamcoback.label.dto; import com.kamco.cd.kamcoback.common.utils.enums.CodeExpose; import com.kamco.cd.kamcoback.common.utils.enums.EnumType; +import io.swagger.v3.oas.annotations.media.Schema; import java.time.ZonedDateTime; +import java.util.List; import java.util.UUID; import lombok.AllArgsConstructor; import lombok.Getter; @@ -78,10 +80,31 @@ public class LabelAllocateDto { } } + @Getter + @Setter + @AllArgsConstructor + public static class AllocateDto { + + @Schema(description = "자동/수동여부(AUTO/MANUAL)", example = "AUTO") + private String autoType; + + @Schema(description = "회차", example = "4") + private Integer stage; + + @Schema(description = "라벨러 할당 목록") + private List labelers; + + @Schema(description = "검수자 할당 목록") + private List inspectors; + } + @Getter public static class TargetUser { + @Schema(description = "라벨러 사번", example = "labeler44") private final String userId; + + @Schema(description = "할당 건수", example = "200") private final int demand; public TargetUser(String userId, int demand) { @@ -94,7 +117,10 @@ public class LabelAllocateDto { @AllArgsConstructor public static class TargetInspector { + @Schema(description = "검수자 사번", example = "K20251212001") private final String inspectorUid; + + @Schema(description = "할당 명수", example = "3") private int userCount; } @@ -125,4 +151,17 @@ public class LabelAllocateDto { private String employeeNo; private String name; } + + @Getter + @Setter + @AllArgsConstructor + public static class InferenceDetail { + + private String analTitle; + private Integer stage; + private ZonedDateTime gukyuinDttm; + private Long count; + private Long labelCnt; + private Long inspectorCnt; + } } 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 98fd7a70..a51a2ae7 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 @@ -1,6 +1,7 @@ package com.kamco.cd.kamcoback.label.service; 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.TargetInspector; import com.kamco.cd.kamcoback.label.dto.LabelAllocateDto.TargetUser; import com.kamco.cd.kamcoback.label.dto.LabelAllocateDto.UserList; @@ -10,7 +11,7 @@ import com.kamco.cd.kamcoback.label.dto.WorkerStatsDto.WorkerStatistics; import com.kamco.cd.kamcoback.postgres.core.LabelAllocateCoreService; import java.time.LocalDate; import java.util.List; -import java.util.Objects; +import java.util.UUID; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @@ -29,57 +30,56 @@ public class LabelAllocateService { /** * 도엽 기준 asc sorting 해서 할당 수만큼 배정하는 로직 * - * @param targetUsers 라벨러 타겟 목록 - * @param targetInspectors 검수자 타겟 목록 + * @param targetUsers */ @Transactional - public void allocateAsc(List targetUsers, List targetInspectors) { + public void allocateAsc( + String autoType, + Integer stage, + List targetUsers, + List targetInspectors) + throws Exception { Long lastId = null; // geom 잔여건수 != 프론트에서 넘어 온 총 건수 -> return Long chargeCnt = labelAllocateCoreService.findLabelUnAssignedCnt(3L); // TODO - Long totalDemand = targetUsers.stream().mapToLong(TargetUser::getDemand).sum(); - if (!Objects.equals(chargeCnt, totalDemand)) { - log.info("chargeCnt != totalDemand"); + // Long totalDemand = targetUsers.stream().mapToLong(TargetUser::getDemand).sum(); + // if (!Objects.equals(chargeCnt, totalDemand)) { + // log.info("chargeCnt != totalDemand"); + // return; + // } + + if (chargeCnt <= 0) { return; } - // 라벨러에게 건수만큼 할당 + List allIds = labelAllocateCoreService.fetchNextIds(lastId, chargeCnt); + int index = 0; for (TargetUser target : targetUsers) { - int remaining = target.getDemand(); + int end = index + target.getDemand(); + List sub = allIds.subList(index, end); - while (remaining > 0) { - - int batchSize = Math.min(remaining, 100); - List ids = labelAllocateCoreService.fetchNextIds(lastId, batchSize); - - if (ids.isEmpty()) { - return; // 더이상 할당할 데이터가 없으면 return - } - - labelAllocateCoreService.assignOwner(ids, target.getUserId()); - - remaining -= ids.size(); - lastId = ids.get(ids.size() - 1); - } + labelAllocateCoreService.assignOwner(sub, target.getUserId()); + index = end; } // 검수자에게 userCount명 만큼 할당 List list = labelAllocateCoreService.findAssignedLabelerList(3L); - int reviewerIndex = 0; - int count = 0; - log.info("list : " + list.size()); + int from = 0; - for (LabelAllocateDto.Basic labeler : list) { - TargetInspector inspector = targetInspectors.get(reviewerIndex); - labelAllocateCoreService.assignInspector( - labeler.getAssignmentUid(), inspector.getInspectorUid()); - count++; + for (TargetInspector inspector : targetInspectors) { + int to = Math.min(from + inspector.getUserCount(), list.size()); - if (count == inspector.getUserCount()) { - reviewerIndex++; - count = 0; + if (from >= to) { + break; } + + List assignmentUids = + list.subList(from, to).stream().map(LabelAllocateDto.Basic::getAssignmentUid).toList(); + + labelAllocateCoreService.assignInspectorBulk(assignmentUids, inspector.getInspectorUid()); + + from = to; } } @@ -145,4 +145,8 @@ public class LabelAllocateService { return WorkerListResponse.builder().progressInfo(progressInfo).workers(workers).build(); } + + public InferenceDetail findInferenceDetail(Long analUid) { + return labelAllocateCoreService.findInferenceDetail(analUid); + } } 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 c1719913..e7b53b73 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 @@ -1,6 +1,7 @@ package com.kamco.cd.kamcoback.postgres.core; 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.UserList; import com.kamco.cd.kamcoback.label.dto.WorkerStatsDto.WorkProgressInfo; import com.kamco.cd.kamcoback.label.dto.WorkerStatsDto.WorkerStatistics; @@ -18,7 +19,7 @@ public class LabelAllocateCoreService { private final LabelAllocateRepository labelAllocateRepository; - public List fetchNextIds(Long lastId, int batchSize) { + public List fetchNextIds(Long lastId, Long batchSize) { return labelAllocateRepository.fetchNextIds(lastId, batchSize); } @@ -62,4 +63,12 @@ public class LabelAllocateCoreService { String workerId, String workerType, LocalDate date, Long analUid) { return labelAllocateRepository.findDailyProcessedCount(workerId, workerType, date, analUid); } + + public void assignInspectorBulk(List assignmentUids, String inspectorUid) { + labelAllocateRepository.assignInspectorBulk(assignmentUids, inspectorUid); + } + + public InferenceDetail findInferenceDetail(Long analUid) { + return labelAllocateRepository.findInferenceDetail(analUid); + } } diff --git a/src/main/java/com/kamco/cd/kamcoback/postgres/entity/MapSheetAnalEntity.java b/src/main/java/com/kamco/cd/kamcoback/postgres/entity/MapSheetAnalEntity.java index b2aeaf6d..dc9b1340 100644 --- a/src/main/java/com/kamco/cd/kamcoback/postgres/entity/MapSheetAnalEntity.java +++ b/src/main/java/com/kamco/cd/kamcoback/postgres/entity/MapSheetAnalEntity.java @@ -103,4 +103,7 @@ public class MapSheetAnalEntity { @ColumnDefault("now()") @Column(name = "updated_dttm") private ZonedDateTime updatedDttm; + + @Column(name = "gukyuin_apply_dttm") + private ZonedDateTime gukyuinApplyDttm; } 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 5392d88f..df73428b 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 @@ -1,5 +1,6 @@ package com.kamco.cd.kamcoback.postgres.repository.label; +import com.kamco.cd.kamcoback.label.dto.LabelAllocateDto.InferenceDetail; import com.kamco.cd.kamcoback.label.dto.LabelAllocateDto.UserList; import com.kamco.cd.kamcoback.label.dto.WorkerStatsDto.WorkProgressInfo; import com.kamco.cd.kamcoback.label.dto.WorkerStatsDto.WorkerStatistics; @@ -10,7 +11,7 @@ import java.util.UUID; public interface LabelAllocateRepositoryCustom { - List fetchNextIds(Long lastId, int batchSize); + List fetchNextIds(Long lastId, Long batchSize); void assignOwner(List ids, String userId); @@ -31,4 +32,8 @@ public interface LabelAllocateRepositoryCustom { // 작업자별 일일 처리량 조회 Long findDailyProcessedCount(String workerId, String workerType, LocalDate date, Long analUid); + + void assignInspectorBulk(List assignmentUids, String inspectorUid); + + InferenceDetail findInferenceDetail(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 7ea657dc..88d07305 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 @@ -6,6 +6,7 @@ import static com.kamco.cd.kamcoback.postgres.entity.QMapSheetAnalEntity.mapShee import static com.kamco.cd.kamcoback.postgres.entity.QMemberEntity.memberEntity; 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.LabelState; import com.kamco.cd.kamcoback.label.dto.LabelAllocateDto.UserList; import com.kamco.cd.kamcoback.label.dto.WorkerStatsDto.WorkProgressInfo; @@ -15,6 +16,7 @@ import com.kamco.cd.kamcoback.postgres.entity.MapSheetAnalEntity; import com.querydsl.core.types.Projections; import com.querydsl.core.types.dsl.BooleanExpression; import com.querydsl.core.types.dsl.CaseBuilder; +import com.querydsl.core.types.dsl.Expressions; import com.querydsl.core.types.dsl.NumberExpression; import com.querydsl.core.types.dsl.StringExpression; import com.querydsl.jpa.impl.JPAQueryFactory; @@ -42,7 +44,7 @@ public class LabelAllocateRepositoryImpl implements LabelAllocateRepositoryCusto @PersistenceContext private EntityManager em; @Override - public List fetchNextIds(Long lastId, int batchSize) { + public List fetchNextIds(Long lastId, Long batchSize) { return queryFactory .select(mapSheetAnalDataInferenceGeomEntity.geoUid) @@ -53,7 +55,7 @@ public class LabelAllocateRepositoryImpl implements LabelAllocateRepositoryCusto mapSheetAnalDataInferenceGeomEntity.compareYyyy.eq(2022), mapSheetAnalDataInferenceGeomEntity.targetYyyy.eq(2024), mapSheetAnalDataInferenceGeomEntity.labelState.isNull()) - .orderBy(mapSheetAnalDataInferenceGeomEntity.geoUid.asc()) + .orderBy(mapSheetAnalDataInferenceGeomEntity.mapSheetNum.asc()) .limit(batchSize) .fetch(); } @@ -65,28 +67,28 @@ public class LabelAllocateRepositoryImpl implements LabelAllocateRepositoryCusto queryFactory .update(mapSheetAnalDataInferenceGeomEntity) .set(mapSheetAnalDataInferenceGeomEntity.labelState, LabelState.ASSIGNED.getId()) + .set(mapSheetAnalDataInferenceGeomEntity.labelStateDttm, ZonedDateTime.now()) .where(mapSheetAnalDataInferenceGeomEntity.geoUid.in(ids)) .execute(); // 라벨러 할당 테이블에 insert + String sql = + """ + insert into tb_labeling_assignment + (assignment_uid, inference_geom_uid, worker_uid, + work_state, assign_group_id, anal_uid) + values (?, ?, ?, ?, ?, ?) + """; + for (Long geoUid : ids) { - queryFactory - .insert(labelingAssignmentEntity) - .columns( - labelingAssignmentEntity.assignmentUid, - labelingAssignmentEntity.inferenceGeomUid, - labelingAssignmentEntity.workerUid, - labelingAssignmentEntity.workState, - labelingAssignmentEntity.assignGroupId, - labelingAssignmentEntity.analUid) - .values( - UUID.randomUUID(), - geoUid, - userId, - LabelState.ASSIGNED.getId(), - "", // TODO: 도엽번호 - 3) - .execute(); + em.createNativeQuery(sql) + .setParameter(1, UUID.randomUUID()) + .setParameter(2, geoUid) + .setParameter(3, userId) + .setParameter(4, LabelState.ASSIGNED.getId()) + .setParameter(5, "") + .setParameter(6, 3) + .executeUpdate(); } em.flush(); @@ -379,4 +381,38 @@ public class LabelAllocateRepositoryImpl implements LabelAllocateRepositoryCusto return count != null ? count : 0L; } + + @Override + public void assignInspectorBulk(List assignmentUids, String inspectorUid) { + queryFactory + .update(labelingAssignmentEntity) + .set(labelingAssignmentEntity.inspectorUid, inspectorUid) + .where(labelingAssignmentEntity.assignmentUid.in(assignmentUids)) + .execute(); + + em.clear(); + } + + @Override + public InferenceDetail findInferenceDetail(Long analUid) { + return queryFactory + .select( + Projections.constructor( + InferenceDetail.class, + mapSheetAnalEntity.analTitle, + Expressions.numberTemplate(Integer.class, "{0}", 4), + mapSheetAnalEntity.gukyuinApplyDttm, + mapSheetAnalEntity.detectingCnt, + labelingAssignmentEntity.workerUid.countDistinct(), + labelingAssignmentEntity.inspectorUid.countDistinct())) + .from(mapSheetAnalEntity) + .innerJoin(labelingAssignmentEntity) + .on(mapSheetAnalEntity.id.eq(labelingAssignmentEntity.analUid)) + .where(mapSheetAnalEntity.id.eq(analUid)) + .groupBy( + mapSheetAnalEntity.analTitle, + mapSheetAnalEntity.gukyuinApplyDttm, + mapSheetAnalEntity.detectingCnt) + .fetchOne(); + } }