This commit is contained in:
2026-01-05 13:36:15 +09:00
parent d86e70e27e
commit 8c149605e5
8 changed files with 295 additions and 371 deletions

View File

@@ -5,6 +5,7 @@ import com.kamco.cd.kamcoback.config.api.ApiResponseDto;
import com.kamco.cd.kamcoback.label.dto.LabelAllocateDto; 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.InferenceDetail;
import com.kamco.cd.kamcoback.label.dto.LabelAllocateDto.LabelerDetail; 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.WorkerListResponse; import com.kamco.cd.kamcoback.label.dto.WorkerStatsDto.WorkerListResponse;
import com.kamco.cd.kamcoback.label.service.LabelAllocateService; import com.kamco.cd.kamcoback.label.service.LabelAllocateService;
import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Operation;
@@ -18,6 +19,7 @@ import jakarta.validation.Valid;
import java.util.List; import java.util.List;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.springframework.data.domain.Page;
import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestBody;
@@ -93,19 +95,16 @@ public class LabelAllocateApiController {
defaultValue = "NAME_ASC")) defaultValue = "NAME_ASC"))
@RequestParam(required = false) @RequestParam(required = false)
String sort, String sort,
@Parameter(description = "페이지 번호 (0부터 시작)", example = "0") @Parameter(description = "페이지 번호 (0부터 시작)", example = "0") @RequestParam(defaultValue = "0")
@RequestParam(defaultValue = "0")
Integer page, Integer page,
@Parameter(description = "페이지 크기", example = "20") @Parameter(description = "페이지 크기", example = "20") @RequestParam(defaultValue = "20")
@RequestParam(defaultValue = "20")
Integer size) { Integer size) {
// type이 null이면 기본값으로 LABELER 설정 // type이 null이면 기본값으로 LABELER 설정
String workerType = (type == null || type.isEmpty()) ? RoleType.LABELER.name() : type; String workerType = (type == null || type.isEmpty()) ? RoleType.LABELER.name() : type;
return ApiResponseDto.ok( return ApiResponseDto.ok(
labelAllocateService.getWorkerStatistics( labelAllocateService.getWorkerStatistics(null, workerType, search, sort, page, size));
null, workerType, search, sort, page, size));
} }
@Operation(summary = "라벨링작업 관리 > 작업 배정", description = "라벨링작업 관리 > 작업 배정") @Operation(summary = "라벨링작업 관리 > 작업 배정", description = "라벨링작업 관리 > 작업 배정")
@@ -154,24 +153,23 @@ public class LabelAllocateApiController {
} }
@Operation( @Operation(
summary = "작업현황 관리 > 라벨러/검수자 상세 정보, 작업이관 팝업 내 라벨러 상세 정보 동일", summary = "작업현황 관리 > 라벨러/검수자 상세 정보, 작업이관 팝업 내 라벨러 상세 정보 동일",
description = "작업현황 관리 > 라벨러/검수자 상세 정보, 작업이관 팝업 내 라벨러 상세 정보 동일") description = "작업현황 관리 > 라벨러/검수자 상세 정보, 작업이관 팝업 내 라벨러 상세 정보 동일")
@GetMapping("/user-detail") @GetMapping("/user-detail")
public ApiResponseDto<LabelerDetail> findUserDetail( public ApiResponseDto<LabelerDetail> findUserDetail(
@RequestParam(defaultValue = "01022223333", required = true) String userId, @RequestParam(defaultValue = "01022223333", required = true) String userId,
@Parameter( @Parameter(
description = "회차 마스터 key", description = "회차 마스터 key",
required = true, required = true,
example = "8584e8d4-53b3-4582-bde2-28a81495a626") example = "8584e8d4-53b3-4582-bde2-28a81495a626")
@RequestParam @RequestParam
String uuid, String uuid,
@Schema( @Schema(
allowableValues = {"LABELER", "REVIEWER"}, allowableValues = {"LABELER", "REVIEWER"},
defaultValue = "LABELER") defaultValue = "LABELER")
@Parameter( @Parameter(description = "라벨러/검수자(LABELER/REVIEWER)", required = true)
description = "라벨러/검수자(LABELER/REVIEWER)", @RequestParam
required = true) @RequestParam String type String type) {
) {
return ApiResponseDto.ok(labelAllocateService.findUserDetail(userId, uuid, type)); return ApiResponseDto.ok(labelAllocateService.findUserDetail(userId, uuid, type));
} }
@@ -211,30 +209,26 @@ public class LabelAllocateApiController {
} }
@Operation( @Operation(
summary = "라벨링작업 관리 > 상세 > 라벨러/검수자 일별 작업량 목록", summary = "라벨링작업 관리 > 상세 > 라벨러/검수자 일별 작업량 목록",
description = "라벨링작업 관리 > 상세 > 라벨러/검수자 일별 작업량 목록") description = "라벨링작업 관리 > 상세 > 라벨러/검수자 일별 작업량 목록")
@GetMapping("/daily-list") @GetMapping("/daily-list")
public ApiResponseDto<Page<LabelingStatDto>> findDaliyList( public ApiResponseDto<Page<LabelingStatDto>> findDaliyList(
@RequestParam(defaultValue = "0", required = true) int page, @RequestParam(defaultValue = "0", required = true) int page,
@RequestParam(defaultValue = "20", required = true) int size, @RequestParam(defaultValue = "20", required = true) int size,
@Parameter( @Parameter(
description = "회차 마스터 key", description = "회차 마스터 key",
required = true, required = true,
example = "8584e8d4-53b3-4582-bde2-28a81495a626") example = "8584e8d4-53b3-4582-bde2-28a81495a626")
@RequestParam @RequestParam
String uuid, String uuid,
@Parameter( @Parameter(description = "사번", required = true, example = "123456") @RequestParam
description = "사번", String userId,
required = true, @Schema(
example = "123456") allowableValues = {"LABELER", "REVIEWER"},
@RequestParam String userId, defaultValue = "LABELER")
@Schema( @Parameter(description = "라벨러/검수자(LABELER/REVIEWER)", required = true)
allowableValues = {"LABELER", "REVIEWER"}, @RequestParam
defaultValue = "LABELER") String type) {
@Parameter(
description = "라벨러/검수자(LABELER/REVIEWER)",
required = true) @RequestParam String type
) {
LabelAllocateDto.searchReq searchReq = new LabelAllocateDto.searchReq(page, size, ""); LabelAllocateDto.searchReq searchReq = new LabelAllocateDto.searchReq(page, size, "");
return ApiResponseDto.ok(labelAllocateService.findDaliyList(searchReq, uuid, userId, type)); return ApiResponseDto.ok(labelAllocateService.findDaliyList(searchReq, uuid, userId, type));
} }

View File

@@ -101,9 +101,9 @@ public class LabelAllocateDto {
private Integer stage; private Integer stage;
@Schema( @Schema(
description = "라벨러 할당 목록", description = "라벨러 할당 목록",
example = example =
""" """
[ [
{ {
"userId": "123456", "userId": "123456",
@@ -122,9 +122,9 @@ public class LabelAllocateDto {
private List<TargetUser> labelers; private List<TargetUser> labelers;
@Schema( @Schema(
description = "검수자 할당 목록", description = "검수자 할당 목록",
example = example =
""" """
["K20251216001", ["K20251216001",
"01022225555", "01022225555",
"K20251212001" "K20251212001"
@@ -232,9 +232,9 @@ public class LabelAllocateDto {
private Integer stage; private Integer stage;
@Schema( @Schema(
description = "라벨러 할당 목록", description = "라벨러 할당 목록",
example = example =
""" """
[ [
{ {
"userId": "123456", "userId": "123456",
@@ -295,11 +295,10 @@ public class LabelAllocateDto {
String[] sortParams = sort.split(","); String[] sortParams = sort.split(",");
String property = sortParams[0]; String property = sortParams[0];
Sort.Direction direction = Sort.Direction direction =
sortParams.length > 1 ? Sort.Direction.fromString(sortParams[1]) : Sort.Direction.ASC; sortParams.length > 1 ? Sort.Direction.fromString(sortParams[1]) : Sort.Direction.ASC;
return PageRequest.of(page, size, Sort.by(direction, property)); return PageRequest.of(page, size, Sort.by(direction, property));
} }
return PageRequest.of(page, size); return PageRequest.of(page, size);
} }
} }
} }

View File

@@ -67,23 +67,17 @@ public class WorkerStatsDto {
private Boolean isStagnated; private Boolean isStagnated;
// 레거시 필드 (기존 호환성 유지) // 레거시 필드 (기존 호환성 유지)
@Deprecated @Deprecated private Long doneCnt; // completed로 대체
private Long doneCnt; // completed로 대체
@Deprecated @Deprecated private Long skipCnt; // skipped로 대체
private Long skipCnt; // skipped로 대체
@Deprecated @Deprecated private Long remainingCnt; // remaining으로 대체
private Long remainingCnt; // remaining으로 대체
@Deprecated @Deprecated private Long day3AgoDoneCnt; // history.day3Ago로 대체
private Long day3AgoDoneCnt; // history.day3Ago로 대체
@Deprecated @Deprecated private Long day2AgoDoneCnt; // history.day2Ago로 대체
private Long day2AgoDoneCnt; // history.day2Ago로 대체
@Deprecated @Deprecated private Long day1AgoDoneCnt; // history.day1Ago로 대체
private Long day1AgoDoneCnt; // history.day1Ago로 대체
} }
@Getter @Getter
@@ -107,7 +101,6 @@ public class WorkerStatsDto {
private Long average; private Long average;
} }
@Getter @Getter
@Setter @Setter
@Builder @Builder

View File

@@ -124,12 +124,7 @@ public class LabelAllocateService {
* @return 작업자 목록 및 통계 * @return 작업자 목록 및 통계
*/ */
public WorkerListResponse getWorkerStatistics( public WorkerListResponse getWorkerStatistics(
Long analUid, Long analUid, String workerType, String search, String sortType, Integer page, Integer size) {
String workerType,
String search,
String sortType,
Integer page,
Integer size) {
// 프로젝트 정보 조회 (analUid가 있을 때만) // 프로젝트 정보 조회 (analUid가 있을 때만)
var projectInfo = labelAllocateCoreService.findProjectInfo(analUid); var projectInfo = labelAllocateCoreService.findProjectInfo(analUid);
@@ -139,8 +134,7 @@ public class LabelAllocateService {
// 작업자 통계 조회 // 작업자 통계 조회
List<WorkerStatistics> workers = List<WorkerStatistics> workers =
labelAllocateCoreService.findWorkerStatistics( labelAllocateCoreService.findWorkerStatistics(analUid, workerType, search, sortType);
analUid, workerType, search, sortType);
// 각 작업자별 3일치 처리량 조회 // 각 작업자별 3일치 처리량 조회
LocalDate today = LocalDate.now(); LocalDate today = LocalDate.now();
@@ -234,7 +228,8 @@ public class LabelAllocateService {
} }
} }
public Page<LabelingStatDto> findDaliyList(LabelAllocateDto.searchReq searchReq, String uuid, String userId, String type) { public Page<LabelingStatDto> findDaliyList(
LabelAllocateDto.searchReq searchReq, String uuid, String userId, String type) {
if (type.equals("LABELER")) { if (type.equals("LABELER")) {
return labelAllocateCoreService.findLabelerDailyStat(searchReq, uuid, userId); return labelAllocateCoreService.findLabelerDailyStat(searchReq, uuid, userId);
} else { } else {

View File

@@ -57,12 +57,8 @@ public class LabelAllocateCoreService {
} }
public List<WorkerStatistics> findWorkerStatistics( public List<WorkerStatistics> findWorkerStatistics(
Long analUid, Long analUid, String workerType, String search, String sortType) {
String workerType, return labelAllocateRepository.findWorkerStatistics(analUid, workerType, search, sortType);
String search,
String sortType) {
return labelAllocateRepository.findWorkerStatistics(
analUid, workerType, search, sortType);
} }
public WorkProgressInfo findWorkProgressInfo(Long analUid) { public WorkProgressInfo findWorkProgressInfo(Long analUid) {
@@ -104,11 +100,13 @@ public class LabelAllocateCoreService {
labelAllocateRepository.insertInspector(analUid, inspector); labelAllocateRepository.insertInspector(analUid, inspector);
} }
public Page<LabelingStatDto> findLabelerDailyStat(searchReq searchReq, String uuid, String userId) { public Page<LabelingStatDto> findLabelerDailyStat(
searchReq searchReq, String uuid, String userId) {
return labelAllocateRepository.findLabelerDailyStat(searchReq, uuid, userId); return labelAllocateRepository.findLabelerDailyStat(searchReq, uuid, userId);
} }
public Page<LabelingStatDto> findInspectorDailyStat(searchReq searchReq, String uuid, String userId) { public Page<LabelingStatDto> findInspectorDailyStat(
searchReq searchReq, String uuid, String userId) {
return labelAllocateRepository.findInspectorDailyStat(searchReq, uuid, userId); return labelAllocateRepository.findInspectorDailyStat(searchReq, uuid, userId);
} }

View File

@@ -52,20 +52,19 @@ public class LabelingAssignmentEntity extends CommonDateEntity {
public LabelAllocateDto.Basic toDto() { public LabelAllocateDto.Basic toDto() {
return new LabelAllocateDto.Basic( return new LabelAllocateDto.Basic(
this.assignmentUid, this.assignmentUid,
this.inferenceGeomUid, this.inferenceGeomUid,
this.workerUid, this.workerUid,
this.inspectorUid, this.inspectorUid,
this.workState, this.workState,
this.stagnationYn, this.stagnationYn,
this.assignGroupId, this.assignGroupId,
this.learnGeomUid, this.learnGeomUid,
this.analUid, this.analUid,
super.getCreatedDate(), super.getCreatedDate(),
super.getModifiedDate(), super.getModifiedDate(),
this.inspectState, this.inspectState,
this.workStatDttm, this.workStatDttm,
this.inspectStatDttm this.inspectStatDttm);
);
} }
} }

View File

@@ -18,7 +18,7 @@ import org.springframework.data.domain.Page;
public interface LabelAllocateRepositoryCustom { public interface LabelAllocateRepositoryCustom {
List<AllocateInfoDto> fetchNextIds( List<AllocateInfoDto> fetchNextIds(
Long lastId, Long batchSize, Integer compareYyyy, Integer targetYyyy, Integer stage); Long lastId, Long batchSize, Integer compareYyyy, Integer targetYyyy, Integer stage);
void assignOwner(List<AllocateInfoDto> ids, String userId, Long analUid); void assignOwner(List<AllocateInfoDto> ids, String userId, Long analUid);
@@ -35,7 +35,7 @@ public interface LabelAllocateRepositoryCustom {
// 작업자 통계 조회 // 작업자 통계 조회
List<WorkerStatistics> findWorkerStatistics( List<WorkerStatistics> findWorkerStatistics(
Long analUid, String workerType, String search, String sortType); Long analUid, String workerType, String search, String sortType);
// 작업 진행 현황 조회 // 작업 진행 현황 조회
WorkProgressInfo findWorkProgressInfo(Long analUid); WorkProgressInfo findWorkProgressInfo(Long analUid);
@@ -48,7 +48,7 @@ public interface LabelAllocateRepositoryCustom {
InferenceDetail findInferenceDetail(String uuid); InferenceDetail findInferenceDetail(String uuid);
List<Long> fetchNextMoveIds( List<Long> fetchNextMoveIds(
Long lastId, Long batchSize, Integer compareYyyy, Integer targetYyyy, Integer stage); Long lastId, Long batchSize, Integer compareYyyy, Integer targetYyyy, Integer stage);
void assignOwnerMove(List<Long> sub, String userId); void assignOwnerMove(List<Long> sub, String userId);
@@ -58,9 +58,11 @@ public interface LabelAllocateRepositoryCustom {
void insertInspector(Long analUid, String inspector); void insertInspector(Long analUid, String inspector);
Page<LabelingStatDto> findLabelerDailyStat(LabelAllocateDto.searchReq searchReq, String uuid, String userId); Page<LabelingStatDto> findLabelerDailyStat(
LabelAllocateDto.searchReq searchReq, String uuid, String userId);
Page<LabelingStatDto> findInspectorDailyStat(LabelAllocateDto.searchReq searchReq, String uuid, String userId); Page<LabelingStatDto> findInspectorDailyStat(
LabelAllocateDto.searchReq searchReq, String uuid, String userId);
LabelerDetail findInspectorDetail(String userId, String uuid); LabelerDetail findInspectorDetail(String userId, String uuid);
} }

View File

@@ -197,10 +197,7 @@ public class LabelAllocateRepositoryImpl implements LabelAllocateRepositoryCusto
@Override @Override
public List<WorkerStatistics> findWorkerStatistics( public List<WorkerStatistics> findWorkerStatistics(
Long analUid, Long analUid, String workerType, String search, String sortType) {
String workerType,
String search,
String sortType) {
// 작업자 유형에 따른 필드 선택 // 작업자 유형에 따른 필드 선택
StringExpression workerIdField = StringExpression workerIdField =
@@ -216,8 +213,8 @@ public class LabelAllocateRepositoryImpl implements LabelAllocateRepositoryCusto
// 검색 조건 (이름 또는 사번으로 검색) // 검색 조건 (이름 또는 사번으로 검색)
BooleanExpression searchCondition = null; BooleanExpression searchCondition = null;
if (search != null && !search.isEmpty()) { if (search != null && !search.isEmpty()) {
searchCondition = memberEntity.name.contains(search) searchCondition =
.or(memberEntity.employeeNo.contains(search)); memberEntity.name.contains(search).or(memberEntity.employeeNo.contains(search));
} }
// 완료, 스킵, 남은 작업 계산 // 완료, 스킵, 남은 작업 계산
@@ -247,7 +244,8 @@ public class LabelAllocateRepositoryImpl implements LabelAllocateRepositoryCusto
.sum(); .sum();
// 기본 통계 조회 쿼리 // 기본 통계 조회 쿼리
BooleanExpression analUidCondition = analUid != null ? labelingAssignmentEntity.analUid.eq(analUid) : null; BooleanExpression analUidCondition =
analUid != null ? labelingAssignmentEntity.analUid.eq(analUid) : null;
var baseQuery = var baseQuery =
queryFactory queryFactory
@@ -318,7 +316,8 @@ public class LabelAllocateRepositoryImpl implements LabelAllocateRepositoryCusto
@Override @Override
public WorkProgressInfo findWorkProgressInfo(Long analUid) { public WorkProgressInfo findWorkProgressInfo(Long analUid) {
BooleanExpression analUidCondition = analUid != null ? labelingAssignmentEntity.analUid.eq(analUid) : null; BooleanExpression analUidCondition =
analUid != null ? labelingAssignmentEntity.analUid.eq(analUid) : null;
// 전체 배정 건수 // 전체 배정 건수
Long totalAssigned = Long totalAssigned =
@@ -344,9 +343,7 @@ public class LabelAllocateRepositoryImpl implements LabelAllocateRepositoryCusto
queryFactory queryFactory
.select(labelingAssignmentEntity.count()) .select(labelingAssignmentEntity.count())
.from(labelingAssignmentEntity) .from(labelingAssignmentEntity)
.where( .where(analUidCondition, labelingAssignmentEntity.workState.eq("SKIP"))
analUidCondition,
labelingAssignmentEntity.workState.eq("SKIP"))
.fetchOne(); .fetchOne();
// 투입된 라벨러 수 // 투입된 라벨러 수
@@ -354,9 +351,7 @@ public class LabelAllocateRepositoryImpl implements LabelAllocateRepositoryCusto
queryFactory queryFactory
.select(labelingAssignmentEntity.workerUid.countDistinct()) .select(labelingAssignmentEntity.workerUid.countDistinct())
.from(labelingAssignmentEntity) .from(labelingAssignmentEntity)
.where( .where(analUidCondition, labelingAssignmentEntity.workerUid.isNotNull())
analUidCondition,
labelingAssignmentEntity.workerUid.isNotNull())
.fetchOne(); .fetchOne();
// === 검수 통계 === // === 검수 통계 ===
@@ -365,9 +360,7 @@ public class LabelAllocateRepositoryImpl implements LabelAllocateRepositoryCusto
queryFactory queryFactory
.select(labelingAssignmentEntity.count()) .select(labelingAssignmentEntity.count())
.from(labelingAssignmentEntity) .from(labelingAssignmentEntity)
.where( .where(analUidCondition, labelingAssignmentEntity.workState.eq("DONE"))
analUidCondition,
labelingAssignmentEntity.workState.eq("DONE"))
.fetchOne(); .fetchOne();
// 투입된 검수자 수 // 투입된 검수자 수
@@ -375,9 +368,7 @@ public class LabelAllocateRepositoryImpl implements LabelAllocateRepositoryCusto
queryFactory queryFactory
.select(labelingAssignmentEntity.inspectorUid.countDistinct()) .select(labelingAssignmentEntity.inspectorUid.countDistinct())
.from(labelingAssignmentEntity) .from(labelingAssignmentEntity)
.where( .where(analUidCondition, labelingAssignmentEntity.inspectorUid.isNotNull())
analUidCondition,
labelingAssignmentEntity.inspectorUid.isNotNull())
.fetchOne(); .fetchOne();
// 남은 작업 건수 계산 // 남은 작업 건수 계산
@@ -437,7 +428,8 @@ public class LabelAllocateRepositoryImpl implements LabelAllocateRepositoryCusto
? labelingAssignmentEntity.inspectorUid.eq(workerId) ? labelingAssignmentEntity.inspectorUid.eq(workerId)
: labelingAssignmentEntity.workerUid.eq(workerId); : labelingAssignmentEntity.workerUid.eq(workerId);
BooleanExpression analUidCondition = analUid != null ? labelingAssignmentEntity.analUid.eq(analUid) : null; BooleanExpression analUidCondition =
analUid != null ? labelingAssignmentEntity.analUid.eq(analUid) : null;
Long count = Long count =
queryFactory queryFactory
@@ -580,29 +572,29 @@ public class LabelAllocateRepositoryImpl implements LabelAllocateRepositoryCusto
QMemberEntity inspector = new QMemberEntity("inspector"); QMemberEntity inspector = new QMemberEntity("inspector");
return queryFactory return queryFactory
.select( .select(
Projections.constructor( Projections.constructor(
LabelerDetail.class, LabelerDetail.class,
worker.userRole, worker.userRole,
worker.name, worker.name,
worker.employeeNo, worker.employeeNo,
assignedCnt, assignedCnt,
skipCnt, skipCnt,
completeCnt, completeCnt,
percent, percent,
Expressions.constant(0), // TODO: 순위, 꼭 해야할지? Expressions.constant(0), // TODO: 순위, 꼭 해야할지?
labelingAssignmentEntity.workStatDttm.min(), labelingAssignmentEntity.workStatDttm.min(),
inspector.name.min())) inspector.name.min()))
.from(worker) .from(worker)
.innerJoin(labelingAssignmentEntity) .innerJoin(labelingAssignmentEntity)
.on( .on(
worker.employeeNo.eq(labelingAssignmentEntity.workerUid), worker.employeeNo.eq(labelingAssignmentEntity.workerUid),
labelingAssignmentEntity.analUid.eq(analEntity.getId())) labelingAssignmentEntity.analUid.eq(analEntity.getId()))
.leftJoin(inspector) .leftJoin(inspector)
.on(labelingAssignmentEntity.inspectorUid.eq(inspector.employeeNo)) .on(labelingAssignmentEntity.inspectorUid.eq(inspector.employeeNo))
.where(worker.employeeNo.eq(userId)) .where(worker.employeeNo.eq(userId))
.groupBy(worker.userRole, worker.name, worker.employeeNo) .groupBy(worker.userRole, worker.name, worker.employeeNo)
.fetchOne(); .fetchOne();
} }
@Override @Override
@@ -638,17 +630,17 @@ public class LabelAllocateRepositoryImpl implements LabelAllocateRepositoryCusto
return null; return null;
} }
var result = queryFactory var result =
.select( queryFactory
mapSheetAnalEntity.compareYyyy, .select(
mapSheetAnalEntity.targetYyyy, mapSheetAnalEntity.compareYyyy,
mapSheetAnalEntity.analTitle, mapSheetAnalEntity.targetYyyy,
mapSheetAnalEntity.gukyuinApplyDttm, mapSheetAnalEntity.analTitle,
mapSheetAnalEntity.analStrtDttm mapSheetAnalEntity.gukyuinApplyDttm,
) mapSheetAnalEntity.analStrtDttm)
.from(mapSheetAnalEntity) .from(mapSheetAnalEntity)
.where(mapSheetAnalEntity.id.eq(analUid)) .where(mapSheetAnalEntity.id.eq(analUid))
.fetchOne(); .fetchOne();
if (result == null) { if (result == null) {
return null; return null;
@@ -661,9 +653,8 @@ public class LabelAllocateRepositoryImpl implements LabelAllocateRepositoryCusto
ZonedDateTime analStrtDttm = result.get(mapSheetAnalEntity.analStrtDttm); ZonedDateTime analStrtDttm = result.get(mapSheetAnalEntity.analStrtDttm);
// 변화탐지년도 생성 // 변화탐지년도 생성
String detectionYear = (compareYyyy != null && targetYyyy != null) String detectionYear =
? compareYyyy + "-" + targetYyyy (compareYyyy != null && targetYyyy != null) ? compareYyyy + "-" + targetYyyy : null;
: null;
// 회차 추출 (예: "8회차" → "8") // 회차 추출 (예: "8회차" → "8")
String round = extractRoundFromTitle(analTitle); String round = extractRoundFromTitle(analTitle);
@@ -676,10 +667,7 @@ public class LabelAllocateRepositoryImpl implements LabelAllocateRepositoryCusto
.build(); .build();
} }
/** /** 제목에서 회차 숫자 추출 예: "8회차", "제8회차" → "8" */
* 제목에서 회차 숫자 추출
* 예: "8회차", "제8회차" → "8"
*/
private String extractRoundFromTitle(String title) { private String extractRoundFromTitle(String title) {
if (title == null || title.isEmpty()) { if (title == null || title.isEmpty()) {
return null; return null;
@@ -691,9 +679,7 @@ public class LabelAllocateRepositoryImpl implements LabelAllocateRepositoryCusto
return matcher.find() ? matcher.group(1) : null; return matcher.find() ? matcher.group(1) : null;
} }
/** /** ZonedDateTime을 "yyyy-MM-dd" 형식으로 변환 */
* ZonedDateTime을 "yyyy-MM-dd" 형식으로 변환
*/
private String formatDate(ZonedDateTime dateTime) { private String formatDate(ZonedDateTime dateTime) {
if (dateTime == null) { if (dateTime == null) {
return null; return null;
@@ -702,211 +688,169 @@ public class LabelAllocateRepositoryImpl implements LabelAllocateRepositoryCusto
} }
@Override @Override
public Page<LabelingStatDto> findLabelerDailyStat(LabelAllocateDto.searchReq searchReq, String uuid, String userId) { public Page<LabelingStatDto> findLabelerDailyStat(
LabelAllocateDto.searchReq searchReq, String uuid, String userId) {
// 날짜 포맷 // 날짜 포맷
Expression<String> workDate = Expression<String> workDate =
Expressions.stringTemplate( Expressions.stringTemplate(
"TO_CHAR({0}, 'YYYY-MM-DD')", "TO_CHAR({0}, 'YYYY-MM-DD')", labelingAssignmentEntity.workStatDttm);
labelingAssignmentEntity.workStatDttm
);
// 날짜별 전체 건수 // 날짜별 전체 건수
Expression<Long> dailyTotalCnt = Expression<Long> dailyTotalCnt = Expressions.numberTemplate(Long.class, "COUNT(*)");
Expressions.numberTemplate(
Long.class,
"COUNT(*)"
);
// ⭐ 전체 기간 총 건수 (윈도우 함수) // ⭐ 전체 기간 총 건수 (윈도우 함수)
Expression<Long> totalCnt = Expression<Long> totalCnt = Expressions.numberTemplate(Long.class, "SUM(COUNT(*)) OVER ()");
Expressions.numberTemplate(
Long.class,
"SUM(COUNT(*)) OVER ()"
);
// 상태별 카운트 (Postgres FILTER 사용) // 상태별 카운트 (Postgres FILTER 사용)
Expression<Long> assignedCnt = Expression<Long> assignedCnt =
Expressions.numberTemplate( Expressions.numberTemplate(
Long.class, Long.class,
"COUNT(*) FILTER (WHERE {0} = 'ASSIGNED')", "COUNT(*) FILTER (WHERE {0} = 'ASSIGNED')",
labelingAssignmentEntity.workState labelingAssignmentEntity.workState);
);
Expression<Long> skipCnt = Expression<Long> skipCnt =
Expressions.numberTemplate( Expressions.numberTemplate(
Long.class, Long.class, "COUNT(*) FILTER (WHERE {0} = 'SKIP')", labelingAssignmentEntity.workState);
"COUNT(*) FILTER (WHERE {0} = 'SKIP')",
labelingAssignmentEntity.workState
);
Expression<Long> completeCnt = Expression<Long> completeCnt =
Expressions.numberTemplate( Expressions.numberTemplate(
Long.class, Long.class,
"COUNT(*) FILTER (WHERE {0} = 'COMPLETE')", "COUNT(*) FILTER (WHERE {0} = 'COMPLETE')",
labelingAssignmentEntity.workState labelingAssignmentEntity.workState);
);
Expression<Long> remainCnt = Expression<Long> remainCnt =
Expressions.numberTemplate( Expressions.numberTemplate(Long.class, "({0} - {1} - {2})", totalCnt, skipCnt, completeCnt);
Long.class,
"({0} - {1} - {2})",
totalCnt,
skipCnt,
completeCnt
);
// analUid로 분석 정보 조회 // analUid로 분석 정보 조회
MapSheetAnalInferenceEntity analEntity = MapSheetAnalInferenceEntity analEntity =
queryFactory queryFactory
.selectFrom(mapSheetAnalInferenceEntity) .selectFrom(mapSheetAnalInferenceEntity)
.where(mapSheetAnalInferenceEntity.uuid.eq(UUID.fromString(uuid))) .where(mapSheetAnalInferenceEntity.uuid.eq(UUID.fromString(uuid)))
.fetchOne(); .fetchOne();
if (Objects.isNull(analEntity)) { if (Objects.isNull(analEntity)) {
throw new EntityNotFoundException("MapSheetAnalInferenceEntity not found for analUid: "); throw new EntityNotFoundException("MapSheetAnalInferenceEntity not found for analUid: ");
} }
Pageable pageable = searchReq.toPageable(); Pageable pageable = searchReq.toPageable();
List<LabelingStatDto> foundContent = queryFactory List<LabelingStatDto> foundContent =
.select( queryFactory
Projections.constructor( .select(
LabelingStatDto.class, Projections.constructor(
workDate, LabelingStatDto.class,
dailyTotalCnt, workDate,
totalCnt, // ⭐ 전체 일자 배정 건수 dailyTotalCnt,
assignedCnt, totalCnt, // ⭐ 전체 일자 배정 건수
skipCnt, assignedCnt,
completeCnt, skipCnt,
remainCnt completeCnt,
) remainCnt))
) .from(labelingAssignmentEntity)
.from(labelingAssignmentEntity) .where(
.where( labelingAssignmentEntity.workerUid.eq(userId),
labelingAssignmentEntity.workerUid.eq(userId), labelingAssignmentEntity.analUid.eq(analEntity.getId()))
labelingAssignmentEntity.analUid.eq(analEntity.getId()) .groupBy(workDate)
) .orderBy(labelingAssignmentEntity.workStatDttm.min().asc())
.groupBy(workDate) .offset(pageable.getOffset())
.orderBy(labelingAssignmentEntity.workStatDttm.min().asc()) .limit(pageable.getPageSize())
.offset(pageable.getOffset()) .fetch();
.limit(pageable.getPageSize())
.fetch();
Long countQuery = queryFactory Long countQuery =
.select(workDate) queryFactory
.from(labelingAssignmentEntity) .select(workDate)
.where( .from(labelingAssignmentEntity)
labelingAssignmentEntity.workerUid.eq(userId), .where(
labelingAssignmentEntity.analUid.eq(analEntity.getId()) labelingAssignmentEntity.workerUid.eq(userId),
) labelingAssignmentEntity.analUid.eq(analEntity.getId()))
.distinct() .distinct()
.fetch() .fetch()
.stream() .stream()
.count(); .count();
return new PageImpl<>(foundContent, pageable, countQuery); return new PageImpl<>(foundContent, pageable, countQuery);
} }
@Override @Override
public Page<LabelingStatDto> findInspectorDailyStat(searchReq searchReq, String uuid, String userId) { public Page<LabelingStatDto> findInspectorDailyStat(
searchReq searchReq, String uuid, String userId) {
// 날짜 포맷 // 날짜 포맷
Expression<String> workDate = Expression<String> workDate =
Expressions.stringTemplate( Expressions.stringTemplate(
"TO_CHAR({0}, 'YYYY-MM-DD')", "TO_CHAR({0}, 'YYYY-MM-DD')", labelingAssignmentEntity.inspectStatDttm);
labelingAssignmentEntity.inspectStatDttm
);
// 날짜별 전체 건수 // 날짜별 전체 건수
Expression<Long> dailyTotalCnt = Expression<Long> dailyTotalCnt = Expressions.numberTemplate(Long.class, "COUNT(*)");
Expressions.numberTemplate(
Long.class,
"COUNT(*)"
);
// ⭐ 전체 기간 총 건수 (윈도우 함수) // ⭐ 전체 기간 총 건수 (윈도우 함수)
Expression<Long> totalCnt = Expression<Long> totalCnt = Expressions.numberTemplate(Long.class, "SUM(COUNT(*)) OVER ()");
Expressions.numberTemplate(
Long.class,
"SUM(COUNT(*)) OVER ()"
);
// 상태별 카운트 (Postgres FILTER 사용) // 상태별 카운트 (Postgres FILTER 사용)
Expression<Long> assignedCnt = Expression<Long> assignedCnt =
Expressions.numberTemplate( Expressions.numberTemplate(
Long.class, Long.class,
"COUNT(*) FILTER (WHERE {0} = 'UNCONFIRM')", "COUNT(*) FILTER (WHERE {0} = 'UNCONFIRM')",
labelingAssignmentEntity.inspectState labelingAssignmentEntity.inspectState);
);
Expression<Long> skipCnt = Expression<Long> skipCnt =
Expressions.numberTemplate( Expressions.numberTemplate(
Long.class, Long.class,
"COUNT(*) FILTER (WHERE {0} = 'EXCEPT')", "COUNT(*) FILTER (WHERE {0} = 'EXCEPT')",
labelingAssignmentEntity.inspectState labelingAssignmentEntity.inspectState);
);
Expression<Long> completeCnt = Expression<Long> completeCnt =
Expressions.numberTemplate( Expressions.numberTemplate(
Long.class, Long.class,
"COUNT(*) FILTER (WHERE {0} = 'COMPLETE')", "COUNT(*) FILTER (WHERE {0} = 'COMPLETE')",
labelingAssignmentEntity.inspectState labelingAssignmentEntity.inspectState);
);
Expression<Long> remainCnt = Expression<Long> remainCnt =
Expressions.numberTemplate( Expressions.numberTemplate(Long.class, "({0} - {1} - {2})", totalCnt, skipCnt, completeCnt);
Long.class,
"({0} - {1} - {2})",
totalCnt,
skipCnt,
completeCnt
);
// analUid로 분석 정보 조회 // analUid로 분석 정보 조회
MapSheetAnalInferenceEntity analEntity = MapSheetAnalInferenceEntity analEntity =
queryFactory queryFactory
.selectFrom(mapSheetAnalInferenceEntity) .selectFrom(mapSheetAnalInferenceEntity)
.where(mapSheetAnalInferenceEntity.uuid.eq(UUID.fromString(uuid))) .where(mapSheetAnalInferenceEntity.uuid.eq(UUID.fromString(uuid)))
.fetchOne(); .fetchOne();
if (Objects.isNull(analEntity)) { if (Objects.isNull(analEntity)) {
throw new EntityNotFoundException("MapSheetAnalInferenceEntity not found for analUid: "); throw new EntityNotFoundException("MapSheetAnalInferenceEntity not found for analUid: ");
} }
Pageable pageable = searchReq.toPageable(); Pageable pageable = searchReq.toPageable();
List<LabelingStatDto> foundContent = queryFactory List<LabelingStatDto> foundContent =
.select( queryFactory
Projections.constructor( .select(
LabelingStatDto.class, Projections.constructor(
workDate, LabelingStatDto.class,
dailyTotalCnt, workDate,
totalCnt, // ⭐ 전체 일자 배정 건수 dailyTotalCnt,
assignedCnt, totalCnt, // ⭐ 전체 일자 배정 건수
skipCnt, assignedCnt,
completeCnt, skipCnt,
remainCnt completeCnt,
) remainCnt))
) .from(labelingAssignmentEntity)
.from(labelingAssignmentEntity) .where(
.where( labelingAssignmentEntity.inspectorUid.eq(userId),
labelingAssignmentEntity.inspectorUid.eq(userId), labelingAssignmentEntity.analUid.eq(analEntity.getId()))
labelingAssignmentEntity.analUid.eq(analEntity.getId()) .groupBy(workDate)
) .orderBy(labelingAssignmentEntity.inspectStatDttm.min().asc())
.groupBy(workDate) .offset(pageable.getOffset())
.orderBy(labelingAssignmentEntity.inspectStatDttm.min().asc()) .limit(pageable.getPageSize())
.offset(pageable.getOffset()) .fetch();
.limit(pageable.getPageSize())
.fetch();
Long countQuery = queryFactory Long countQuery =
.select(workDate) queryFactory
.from(labelingAssignmentEntity) .select(workDate)
.where( .from(labelingAssignmentEntity)
labelingAssignmentEntity.inspectorUid.eq(userId), .where(
labelingAssignmentEntity.analUid.eq(analEntity.getId()) labelingAssignmentEntity.inspectorUid.eq(userId),
) labelingAssignmentEntity.analUid.eq(analEntity.getId()))
.distinct() .distinct()
.fetch() .fetch()
.stream() .stream()
.count(); .count();
return new PageImpl<>(foundContent, pageable, countQuery); return new PageImpl<>(foundContent, pageable, countQuery);
} }
@@ -914,43 +858,43 @@ public class LabelAllocateRepositoryImpl implements LabelAllocateRepositoryCusto
@Override @Override
public LabelerDetail findInspectorDetail(String userId, String uuid) { public LabelerDetail findInspectorDetail(String userId, String uuid) {
NumberExpression<Long> assignedCnt = NumberExpression<Long> assignedCnt =
new CaseBuilder() new CaseBuilder()
.when(labelingAssignmentEntity.inspectState.eq(InspectState.UNCONFIRM.getId())) .when(labelingAssignmentEntity.inspectState.eq(InspectState.UNCONFIRM.getId()))
.then(1L) .then(1L)
.otherwise((Long) null) .otherwise((Long) null)
.count(); .count();
NumberExpression<Long> skipCnt = NumberExpression<Long> skipCnt =
new CaseBuilder() new CaseBuilder()
.when(labelingAssignmentEntity.inspectState.eq(InspectState.EXCEPT.getId())) .when(labelingAssignmentEntity.inspectState.eq(InspectState.EXCEPT.getId()))
.then(1L) .then(1L)
.otherwise((Long) null) .otherwise((Long) null)
.count(); .count();
NumberExpression<Long> completeCnt = NumberExpression<Long> completeCnt =
new CaseBuilder() new CaseBuilder()
.when(labelingAssignmentEntity.inspectState.eq(InspectState.COMPLETE.getId())) .when(labelingAssignmentEntity.inspectState.eq(InspectState.COMPLETE.getId()))
.then(1L) .then(1L)
.otherwise((Long) null) .otherwise((Long) null)
.count(); .count();
NumberExpression<Double> percent = NumberExpression<Double> percent =
new CaseBuilder() new CaseBuilder()
.when(completeCnt.eq(0L)) .when(completeCnt.eq(0L))
.then(0.0) .then(0.0)
.otherwise( .otherwise(
Expressions.numberTemplate( Expressions.numberTemplate(
Double.class, Double.class,
"round({0} / {1}, 2)", "round({0} / {1}, 2)",
labelingAssignmentEntity.count(), labelingAssignmentEntity.count(),
completeCnt)); completeCnt));
// analUid로 분석 정보 조회 // analUid로 분석 정보 조회
MapSheetAnalInferenceEntity analEntity = MapSheetAnalInferenceEntity analEntity =
queryFactory queryFactory
.selectFrom(mapSheetAnalInferenceEntity) .selectFrom(mapSheetAnalInferenceEntity)
.where(mapSheetAnalInferenceEntity.uuid.eq(UUID.fromString(uuid))) .where(mapSheetAnalInferenceEntity.uuid.eq(UUID.fromString(uuid)))
.fetchOne(); .fetchOne();
if (Objects.isNull(analEntity)) { if (Objects.isNull(analEntity)) {
throw new EntityNotFoundException("MapSheetAnalInferenceEntity not found for analUid: "); throw new EntityNotFoundException("MapSheetAnalInferenceEntity not found for analUid: ");
@@ -960,28 +904,28 @@ public class LabelAllocateRepositoryImpl implements LabelAllocateRepositoryCusto
QMemberEntity worker = new QMemberEntity("worker"); QMemberEntity worker = new QMemberEntity("worker");
return queryFactory return queryFactory
.select( .select(
Projections.constructor( Projections.constructor(
LabelerDetail.class, LabelerDetail.class,
inspector.userRole, inspector.userRole,
inspector.name, inspector.name,
inspector.employeeNo, inspector.employeeNo,
assignedCnt, assignedCnt,
skipCnt, skipCnt,
completeCnt, completeCnt,
percent, percent,
Expressions.constant(0), // TODO: 순위, 꼭 해야할지? Expressions.constant(0), // TODO: 순위, 꼭 해야할지?
labelingAssignmentEntity.inspectStatDttm.min(), labelingAssignmentEntity.inspectStatDttm.min(),
worker.name.min())) worker.name.min()))
.from(inspector) .from(inspector)
.innerJoin(labelingAssignmentEntity) .innerJoin(labelingAssignmentEntity)
.on( .on(
inspector.employeeNo.eq(labelingAssignmentEntity.inspectorUid), inspector.employeeNo.eq(labelingAssignmentEntity.inspectorUid),
labelingAssignmentEntity.analUid.eq(analEntity.getId())) labelingAssignmentEntity.analUid.eq(analEntity.getId()))
.leftJoin(worker) .leftJoin(worker)
.on(labelingAssignmentEntity.workerUid.eq(worker.employeeNo)) .on(labelingAssignmentEntity.workerUid.eq(worker.employeeNo))
.where(inspector.employeeNo.eq(userId)) .where(inspector.employeeNo.eq(userId))
.groupBy(inspector.userRole, inspector.name, inspector.employeeNo) .groupBy(inspector.userRole, inspector.name, inspector.employeeNo)
.fetchOne(); .fetchOne();
} }
} }