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 5014147e..0a52baf2 100644 --- a/src/main/java/com/kamco/cd/kamcoback/label/LabelAllocateApiController.java +++ b/src/main/java/com/kamco/cd/kamcoback/label/LabelAllocateApiController.java @@ -1,16 +1,25 @@ 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.service.LabelAllocateService; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.media.Content; +import io.swagger.v3.oas.annotations.media.Schema; +import io.swagger.v3.oas.annotations.responses.ApiResponse; +import io.swagger.v3.oas.annotations.responses.ApiResponses; import io.swagger.v3.oas.annotations.tags.Tag; import java.util.List; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; +import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; @Slf4j @@ -22,39 +31,40 @@ public class LabelAllocateApiController { private final LabelAllocateService labelAllocateService; + @Operation(summary = "배정 가능한 사용자 목록 조회", description = "배정 가능한 사용자 목록 조회") + @ApiResponses( + value = { + @ApiResponse( + responseCode = "200", + description = "조회 성공", + content = + @Content( + mediaType = "application/json", + schema = @Schema(implementation = CommonCodeDto.Basic.class))), + @ApiResponse(responseCode = "404", description = "코드를 찾을 수 없음", content = @Content), + @ApiResponse(responseCode = "500", description = "서버 오류", content = @Content) + }) + @GetMapping("/avail-user") + public ApiResponseDto> availUserList( + @RequestParam @Schema() String role) { + return ApiResponseDto.ok(labelAllocateService.availUserList(role)); + } + // 라벨링 수량 할당하는 로직 테스트 @PostMapping("/allocate") public ApiResponseDto labelAllocate(@RequestBody LabelAllocateDto dto) { - // 도엽별 카운트 쿼리 - // List sheets = - // List.of( - // new Sheet("1", 261), - // new Sheet("2", 500), - // new Sheet("3", 350), - // new Sheet("4", 250), - // new Sheet("5", 380), - // new Sheet("6", 459)); - - // 사용자별 할당 입력한 값 - // List targets = - // List.of(new TargetUser("A", 1000), new TargetUser("B", 500), new TargetUser("C", 700)); - // LabelAllocateService.allocateSheetCount(new ArrayList<>(sheets), new - // ArrayList<>(targets)); - - // targets.forEach( - // t -> { - // log.info("[" + t.getUserId() + "]"); - // t.getAssigned() - // .forEach( - // u -> { - // log.info(" - 도엽: " + u.getSheetId() + " (" + u.getCount() + ")"); - // }); - // }); - List targets = - List.of(new TargetUser("1", 1000), new TargetUser("2", 400), new TargetUser("3", 440)); - labelAllocateService.allocateAsc(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); return ApiResponseDto.ok(null); } 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 a6802547..d1705e92 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 @@ -1,22 +1,80 @@ package com.kamco.cd.kamcoback.label.dto; -import java.util.ArrayList; -import java.util.List; +import com.kamco.cd.kamcoback.common.utils.enums.CodeExpose; +import com.kamco.cd.kamcoback.common.utils.enums.EnumType; +import java.time.ZonedDateTime; +import java.util.UUID; import lombok.AllArgsConstructor; import lombok.Getter; import lombok.Setter; public class LabelAllocateDto { + @CodeExpose @Getter @AllArgsConstructor - public static class Sheet { + public enum LabelMngState implements EnumType { + PENDING("작업대기"), + ASSIGNED("작업할당"), + STOP("중단"), + LABEL_ING("라벨진행중"), + LABEL_COMPLETE("라벨완료"), + INSPECT_REQ("검수요청"), + INSPECT_ING("검수진행중"), + INSPECT_COMPLETE("검수완료"); - private final String sheetId; - private int count; + private String desc; - public void decrease(int amount) { - this.count -= amount; + @Override + public String getId() { + return name(); + } + + @Override + public String getText() { + return desc; + } + } + + @CodeExpose + @Getter + @AllArgsConstructor + public enum LabelState implements EnumType { + ASSIGNED("배정"), + SKIP("스킵"), + COMPLETE("완료"); + + private String desc; + + @Override + public String getId() { + return name(); + } + + @Override + public String getText() { + return desc; + } + } + + @CodeExpose + @Getter + @AllArgsConstructor + public enum InspectState implements EnumType { + UNCONFIRM("미확인"), + EXCEPT("제외"), + COMPLETE("완료"); + + private String desc; + + @Override + public String getId() { + return name(); + } + + @Override + public String getText() { + return desc; } } @@ -25,22 +83,46 @@ public class LabelAllocateDto { private final String userId; private final int demand; - private final List assigned = new ArrayList<>(); - private int allocated = 0; - @Setter private int shortage = 0; public TargetUser(String userId, int demand) { this.userId = userId; this.demand = demand; } + } - public int getRemainDemand() { - return demand - allocated; - } + @Getter + @AllArgsConstructor + public static class TargetInspector { - public void assign(String sheetId, int count) { - assigned.add(new Sheet(sheetId, count)); - allocated += count; - } + private final String inspectorUid; + private int userCount; + } + + @Getter + @Setter + @AllArgsConstructor + public static class Basic { + + private UUID assignmentUid; + private Long inferenceGeomUid; + private String workerUid; + private String inspectorUid; + private String workState; + private Character stagnationYn; + private String assignGroupId; + private Long learnGeomUid; + private Long analUid; + private ZonedDateTime createdDttm; + private ZonedDateTime updatedDttm; + } + + @Getter + @Setter + @AllArgsConstructor + public static class UserList { + + private String userRole; + private String employeeNo; + private String name; } } 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 59cdbd9a..6d273a95 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,12 +1,13 @@ package com.kamco.cd.kamcoback.label.service; -import com.kamco.cd.kamcoback.label.dto.LabelAllocateDto.Sheet; +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.postgres.core.LabelAllocateCoreService; import jakarta.transaction.Transactional; -import java.util.Comparator; -import java.util.Iterator; import java.util.List; +import java.util.Objects; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Service; @@ -20,103 +21,63 @@ public class LabelAllocateService { this.labelAllocateCoreService = labelAllocateCoreService; } - /** - * 도엽 count 수와 할당된 count 수를 비교해서 많은 수부터 먼저 배정하고 나머지를 분배 배정하는 로직 - * - * @param sheets - * @param targetUsers - */ - public static void allocateSheetCount(List sheets, List targetUsers) { - - // 1️⃣ 실제 도엽 기준 할당 - allocateSheets(sheets, targetUsers); - - // 2️⃣ 전체 부족분 비율 계산 - distributeShortage(targetUsers); // 항상 마지막에 한 번만 호출해야함 - } - - public static void allocateSheets(List sheets, List targets) { - // 도엽 큰 것부터 (선택 사항) - sheets.sort(Comparator.comparingInt(Sheet::getCount).reversed()); - - for (TargetUser target : targets) { - Iterator it = sheets.iterator(); - - while (it.hasNext() && target.getRemainDemand() > 0) { - Sheet sheet = it.next(); - - int assignable = Math.min(sheet.getCount(), target.getRemainDemand()); - - if (assignable > 0) { - target.assign(sheet.getSheetId(), assignable); - sheet.decrease(assignable); - } - - if (sheet.getCount() == 0) { - it.remove(); - } - } - } - } - - public static void distributeShortage(List targets) { - - int totalDemand = targets.stream().mapToInt(TargetUser::getDemand).sum(); - - int totalAllocated = targets.stream().mapToInt(t -> t.getAllocated()).sum(); - - int shortage = totalDemand - totalAllocated; - - if (shortage <= 0) { - return; - } - - int distributed = 0; - - for (int i = 0; i < targets.size(); i++) { - TargetUser t = targets.get(i); - - // 마지막 타겟이 나머지 가져가게 (반올림 오차 방지) - int share; - if (i == targets.size() - 1) { - share = shortage - distributed; - } else { - double ratio = (double) t.getDemand() / totalDemand; - share = (int) Math.round(shortage * ratio); - distributed += share; - } - - t.setShortage(share); - } - } - /** * 도엽 기준 asc sorting 해서 할당 수만큼 배정하는 로직 * * @param targetUsers */ @Transactional - public void allocateAsc(List targetUsers) { + public void allocateAsc(List targetUsers, List targetInspectors) { 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"); + return; + } + + // 라벨러에게 건수만큼 할당 for (TargetUser target : targetUsers) { int remaining = target.getDemand(); while (remaining > 0) { - int batchSize = Math.min(remaining, 500); + int batchSize = Math.min(remaining, 100); List ids = labelAllocateCoreService.fetchNextIds(lastId, batchSize); if (ids.isEmpty()) { return; // 더이상 할당할 데이터가 없으면 return } - labelAllocateCoreService.assignOwner( - ids, Long.valueOf(target.getUserId())); // TODO : userId를 숫자값을 가져올지 사번을 가져올지 고민 + labelAllocateCoreService.assignOwner(ids, target.getUserId()); remaining -= ids.size(); lastId = ids.get(ids.size() - 1); } } + + // 검수자에게 userCount명 만큼 할당 + List list = labelAllocateCoreService.findAssignedLabelerList(3L); + int reviewerIndex = 0; + int count = 0; + log.info("list : " + list.size()); + + for (LabelAllocateDto.Basic labeler : list) { + TargetInspector inspector = targetInspectors.get(reviewerIndex); + labelAllocateCoreService.assignInspector( + labeler.getAssignmentUid(), inspector.getInspectorUid()); + count++; + + if (count == inspector.getUserCount()) { + reviewerIndex++; + count = 0; + } + } + } + + public List availUserList(String role) { + return labelAllocateCoreService.availUserList(role); } } 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 d4f3da10..1d8fa075 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,7 +1,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.postgres.entity.LabelingAssignmentEntity; import com.kamco.cd.kamcoback.postgres.repository.label.LabelAllocateRepository; import java.util.List; +import java.util.UUID; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; @@ -15,7 +19,25 @@ public class LabelAllocateCoreService { return labelAllocateRepository.fetchNextIds(lastId, batchSize); } - public Long assignOwner(List ids, Long userId) { - return labelAllocateRepository.assignOwner(ids, userId); + public void assignOwner(List ids, String userId) { + labelAllocateRepository.assignOwner(ids, userId); + } + + public List findAssignedLabelerList(Long analUid) { + return labelAllocateRepository.findAssignedLabelerList(analUid).stream() + .map(LabelingAssignmentEntity::toDto) + .toList(); + } + + public Long findLabelUnAssignedCnt(Long analUid) { + return labelAllocateRepository.findLabelUnAssignedCnt(analUid); + } + + public void assignInspector(UUID assignmentUid, String inspectorUid) { + labelAllocateRepository.assignInspector(assignmentUid, inspectorUid); + } + + public List availUserList(String role) { + return labelAllocateRepository.availUserList(role); } } diff --git a/src/main/java/com/kamco/cd/kamcoback/postgres/entity/LabelingAssignmentEntity.java b/src/main/java/com/kamco/cd/kamcoback/postgres/entity/LabelingAssignmentEntity.java new file mode 100644 index 00000000..22afd4aa --- /dev/null +++ b/src/main/java/com/kamco/cd/kamcoback/postgres/entity/LabelingAssignmentEntity.java @@ -0,0 +1,57 @@ +package com.kamco.cd.kamcoback.postgres.entity; + +import com.kamco.cd.kamcoback.label.dto.LabelAllocateDto; +import com.kamco.cd.kamcoback.postgres.CommonDateEntity; +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.Id; +import jakarta.persistence.Table; +import java.util.UUID; + +@Entity +@Table(name = "tb_labeling_assignment") +public class LabelingAssignmentEntity extends CommonDateEntity { + + @Id + @Column(name = "assignment_uid") + private UUID assignmentUid; + + @Column(name = "inference_geom_uid") + private Long inferenceGeomUid; + + @Column(name = "worker_uid") + private String workerUid; + + @Column(name = "inspector_uid") + private String inspectorUid; + + @Column(name = "work_state") + private String workState; + + @Column(name = "stagnation_yn") + private Character stagnationYn; + + @Column(name = "assign_group_id") + private String assignGroupId; + + @Column(name = "learn_geom_uid") + private Long learnGeomUid; + + @Column(name = "anal_uid") + private Long analUid; + + public LabelAllocateDto.Basic toDto() { + return new LabelAllocateDto.Basic( + this.assignmentUid, + this.inferenceGeomUid, + this.workerUid, + this.inspectorUid, + this.workState, + this.stagnationYn, + this.assignGroupId, + this.learnGeomUid, + this.analUid, + super.getCreatedDate(), + super.getModifiedDate()); + } +} 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 fa4ae5e5..659f5c7b 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,10 +1,21 @@ package com.kamco.cd.kamcoback.postgres.repository.label; +import com.kamco.cd.kamcoback.label.dto.LabelAllocateDto.UserList; +import com.kamco.cd.kamcoback.postgres.entity.LabelingAssignmentEntity; import java.util.List; +import java.util.UUID; public interface LabelAllocateRepositoryCustom { List fetchNextIds(Long lastId, int batchSize); - Long assignOwner(List ids, Long userId); + void assignOwner(List ids, String userId); + + List findAssignedLabelerList(Long analUid); + + Long findLabelUnAssignedCnt(Long analUid); + + void assignInspector(UUID assignmentUid, String userId); + + List availUserList(String role); } 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 dfdec9ae..f9e419d2 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 @@ -1,12 +1,26 @@ package com.kamco.cd.kamcoback.postgres.repository.label; +import static com.kamco.cd.kamcoback.postgres.entity.QLabelingAssignmentEntity.labelingAssignmentEntity; import static com.kamco.cd.kamcoback.postgres.entity.QMapSheetAnalDataInferenceGeomEntity.mapSheetAnalDataInferenceGeomEntity; +import static com.kamco.cd.kamcoback.postgres.entity.QMapSheetAnalEntity.mapSheetAnalEntity; +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.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.StringExpression; import com.querydsl.jpa.impl.JPAQueryFactory; +import jakarta.persistence.EntityManager; +import jakarta.persistence.EntityNotFoundException; +import jakarta.persistence.PersistenceContext; import java.util.List; +import java.util.Objects; +import java.util.UUID; import lombok.extern.slf4j.Slf4j; import org.springframework.data.jpa.repository.support.QuerydslRepositorySupport; import org.springframework.stereotype.Repository; @@ -19,6 +33,8 @@ public class LabelAllocateRepositoryImpl extends QuerydslRepositorySupport 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; @@ -26,6 +42,7 @@ public class LabelAllocateRepositoryImpl extends QuerydslRepositorySupport @Override public List fetchNextIds(Long lastId, int batchSize) { + return queryFactory .select(mapSheetAnalDataInferenceGeomEntity.geoUid) .from(mapSheetAnalDataInferenceGeomEntity) @@ -34,18 +51,103 @@ public class LabelAllocateRepositoryImpl extends QuerydslRepositorySupport lastId == null ? null : mapSheetAnalDataInferenceGeomEntity.geoUid.gt(lastId), mapSheetAnalDataInferenceGeomEntity.compareYyyy.eq(2022), mapSheetAnalDataInferenceGeomEntity.targetYyyy.eq(2024), - mapSheetAnalDataInferenceGeomEntity.labelerUid.isNull()) + mapSheetAnalDataInferenceGeomEntity.labelState.isNull()) .orderBy(mapSheetAnalDataInferenceGeomEntity.geoUid.asc()) .limit(batchSize) .fetch(); } @Override - public Long assignOwner(List ids, Long userId) { - return queryFactory + public void assignOwner(List ids, String userId) { + + // data_geom 테이블에 label state 를 ASSIGNED 로 update + queryFactory .update(mapSheetAnalDataInferenceGeomEntity) - .set(mapSheetAnalDataInferenceGeomEntity.labelerUid, userId) + .set(mapSheetAnalDataInferenceGeomEntity.labelState, LabelState.ASSIGNED.getId()) .where(mapSheetAnalDataInferenceGeomEntity.geoUid.in(ids)) .execute(); + + // 라벨러 할당 테이블에 insert + 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.flush(); + em.clear(); + } + + @Override + public List findAssignedLabelerList(Long analUid) { + return queryFactory + .selectFrom(labelingAssignmentEntity) + .where( + labelingAssignmentEntity.analUid.eq(analUid), + labelingAssignmentEntity.workState.eq(LabelState.ASSIGNED.getId()), + labelingAssignmentEntity.inspectorUid.isNull()) + .orderBy(labelingAssignmentEntity.workerUid.asc()) + .fetch(); + } + + @Override + public Long findLabelUnAssignedCnt(Long analUid) { + MapSheetAnalEntity entity = + queryFactory + .selectFrom(mapSheetAnalEntity) + .where(mapSheetAnalEntity.id.eq(analUid)) + .fetchOne(); + + if (Objects.isNull(entity)) { + throw new EntityNotFoundException(); + } + + return queryFactory + .select(mapSheetAnalDataInferenceGeomEntity.geoUid.count()) + .from(mapSheetAnalDataInferenceGeomEntity) + .where( + mapSheetAnalDataInferenceGeomEntity.compareYyyy.eq(entity.getCompareYyyy()), + mapSheetAnalDataInferenceGeomEntity.targetYyyy.eq(entity.getTargetYyyy()), + mapSheetAnalDataInferenceGeomEntity.stage.eq(4), // TODO: 회차 컬럼을 가져와야 할 듯? + mapSheetAnalDataInferenceGeomEntity.labelState.isNull()) + .fetchOne(); + } + + @Override + public void assignInspector(UUID assignmentUid, String inspectorUid) { + queryFactory + .update(labelingAssignmentEntity) + .set(labelingAssignmentEntity.inspectorUid, inspectorUid) + .where(labelingAssignmentEntity.assignmentUid.eq(assignmentUid)) + .execute(); + } + + @Override + public List availUserList(String role) { + return queryFactory + .select( + Projections.constructor( + LabelAllocateDto.UserList.class, + memberEntity.userRole, + memberEntity.employeeNo, + memberEntity.name)) + .from(memberEntity) + .where(memberEntity.userRole.eq(role), memberEntity.status.eq("ACTIVE")) + .orderBy(memberEntity.name.asc()) + .fetch(); } }