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..243965a2 100644 --- a/src/main/java/com/kamco/cd/kamcoback/label/LabelAllocateApiController.java +++ b/src/main/java/com/kamco/cd/kamcoback/label/LabelAllocateApiController.java @@ -2,6 +2,7 @@ package com.kamco.cd.kamcoback.label; 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.tags.Tag; @@ -26,35 +27,10 @@ public class LabelAllocateApiController { @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..b3509a38 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,36 @@ 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; } } 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..28dfa8ad 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,12 @@ 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.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,91 +20,30 @@ 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()) { @@ -112,11 +51,29 @@ public class LabelAllocateService { } labelAllocateCoreService.assignOwner( - ids, Long.valueOf(target.getUserId())); // TODO : userId를 숫자값을 가져올지 사번을 가져올지 고민 + 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; + } + } + } } 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..3d7ce013 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,10 @@ package com.kamco.cd.kamcoback.postgres.core; +import com.kamco.cd.kamcoback.label.dto.LabelAllocateDto; +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 +18,19 @@ 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); } } 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..513b513e --- /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..0fed2538 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,18 @@ package com.kamco.cd.kamcoback.postgres.repository.label; +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); } 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..6b33ad4e 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,22 @@ 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 com.kamco.cd.kamcoback.label.dto.LabelAllocateDto.LabelState; +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.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; @@ -14,11 +24,14 @@ import org.springframework.stereotype.Repository; @Slf4j @Repository public class LabelAllocateRepositoryImpl extends QuerydslRepositorySupport - implements LabelAllocateRepositoryCustom { + 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; @@ -26,26 +39,100 @@ public class LabelAllocateRepositoryImpl extends QuerydslRepositorySupport @Override public List fetchNextIds(Long lastId, int batchSize) { + return queryFactory - .select(mapSheetAnalDataInferenceGeomEntity.geoUid) - .from(mapSheetAnalDataInferenceGeomEntity) - .where( - // mapSheetAnalDataGeomEntity.pnu.isNotNull(), //TODO: Mockup 진행 이후 확인하기 - lastId == null ? null : mapSheetAnalDataInferenceGeomEntity.geoUid.gt(lastId), - mapSheetAnalDataInferenceGeomEntity.compareYyyy.eq(2022), - mapSheetAnalDataInferenceGeomEntity.targetYyyy.eq(2024), - mapSheetAnalDataInferenceGeomEntity.labelerUid.isNull()) - .orderBy(mapSheetAnalDataInferenceGeomEntity.geoUid.asc()) - .limit(batchSize) - .fetch(); + .select(mapSheetAnalDataInferenceGeomEntity.geoUid) + .from(mapSheetAnalDataInferenceGeomEntity) + .where( + // mapSheetAnalDataGeomEntity.pnu.isNotNull(), //TODO: Mockup 진행 이후 확인하기 + lastId == null ? null : mapSheetAnalDataInferenceGeomEntity.geoUid.gt(lastId), + mapSheetAnalDataInferenceGeomEntity.compareYyyy.eq(2022), + mapSheetAnalDataInferenceGeomEntity.targetYyyy.eq(2024), + mapSheetAnalDataInferenceGeomEntity.labelState.isNull()) + .orderBy(mapSheetAnalDataInferenceGeomEntity.geoUid.asc()) + .limit(batchSize) + .fetch(); } @Override - public Long assignOwner(List ids, Long userId) { - return queryFactory - .update(mapSheetAnalDataInferenceGeomEntity) - .set(mapSheetAnalDataInferenceGeomEntity.labelerUid, userId) - .where(mapSheetAnalDataInferenceGeomEntity.geoUid.in(ids)) + public void assignOwner(List ids, String userId) { + + //data_geom 테이블에 label state 를 ASSIGNED 로 update + queryFactory + .update(mapSheetAnalDataInferenceGeomEntity) + .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(); } }