diff --git a/src/main/java/com/kamco/cd/kamcoback/Innopam/InnopamApiController.java b/src/main/java/com/kamco/cd/kamcoback/Innopam/InnopamApiController.java new file mode 100644 index 00000000..e266552e --- /dev/null +++ b/src/main/java/com/kamco/cd/kamcoback/Innopam/InnopamApiController.java @@ -0,0 +1,190 @@ +package com.kamco.cd.kamcoback.Innopam; + +import com.kamco.cd.kamcoback.Innopam.dto.DetectMastDto; +import com.kamco.cd.kamcoback.Innopam.dto.DetectMastDto.Basic; +import com.kamco.cd.kamcoback.Innopam.dto.DetectMastDto.DetectMastReq; +import com.kamco.cd.kamcoback.Innopam.dto.DetectMastDto.DetectMastSearch; +import com.kamco.cd.kamcoback.Innopam.dto.DetectMastDto.FeaturePnuDto; +import com.kamco.cd.kamcoback.Innopam.service.DetectMastService; +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.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 jakarta.validation.Valid; +import java.util.List; +import java.util.UUID; +import lombok.RequiredArgsConstructor; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.PutMapping; +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; + +@Tag(name = "이노펨 mockup API", description = "이노펨 mockup API") +@RestController +@RequiredArgsConstructor +@RequestMapping("/api/kcd/cdi/detect") +public class InnopamApiController { + + private final DetectMastService detectMastService; + + /** 탐지결과 등록 */ + @Operation(summary = "탐지결과 등록", description = "탐지결과 등록") + @ApiResponses( + value = { + @ApiResponse( + responseCode = "201", + description = "등록 성공", + content = + @Content( + mediaType = "application/json", + schema = @Schema(implementation = DetectMastReq.class))), + @ApiResponse(responseCode = "404", description = "코드를 찾을 수 없음", content = @Content), + @ApiResponse(responseCode = "500", description = "서버 오류", content = @Content) + }) + @PostMapping("/mast/regist") + public DetectMastReq setChangeDetection( + @RequestBody @Valid DetectMastDto.DetectMastReq detectMast) { + detectMastService.saveDetectMast(detectMast); + return detectMast; + } + + @Operation(summary = "탐지결과 삭제", description = "탐지결과 삭제") + @ApiResponses( + value = { + @ApiResponse( + responseCode = "201", + description = "등록 성공", + content = + @Content( + mediaType = "application/json", + schema = @Schema(implementation = DetectMastReq.class))), + @ApiResponse(responseCode = "404", description = "코드를 찾을 수 없음", content = @Content), + @ApiResponse(responseCode = "500", description = "서버 오류", content = @Content) + }) + @PostMapping("/mast/remove") + public String deleteChangeDetection(@RequestBody DetectMastReq detectMast) { + return "OK"; + } + + @Operation(summary = "탐지결과 등록목록 조회", description = "탐지결과 등록목록 조회") + @GetMapping("/mast/list") + @ApiResponses( + value = { + @ApiResponse( + responseCode = "200", + description = "목록 성공", + content = + @Content( + mediaType = "application/json", + schema = @Schema(implementation = Basic.class))), + @ApiResponse(responseCode = "404", description = "코드를 찾을 수 없음", content = @Content), + @ApiResponse(responseCode = "500", description = "서버 오류", content = @Content) + }) + public List selectChangeDetectionList( + @RequestParam(required = false) String cprsBfYr, + @RequestParam(required = false) String cprsAdYr, + @RequestParam(required = false) Integer dtctSno) { + DetectMastSearch detectMastSearch = new DetectMastSearch(); + detectMastSearch.setCprsAdYr(cprsAdYr); + detectMastSearch.setCprsBfYr(cprsBfYr); + detectMastSearch.setDtctSno(dtctSno); + return detectMastService.selectDetectMast(detectMastSearch); + } + + @Operation(summary = "탐지결과 등록목록 상세 조회", description = "탐지결과 등록목록 상세 조회") + @ApiResponses( + value = { + @ApiResponse( + responseCode = "200", + description = "목록 성공", + content = + @Content( + mediaType = "application/json", + schema = @Schema(implementation = Basic.class))), + @ApiResponse(responseCode = "404", description = "코드를 찾을 수 없음", content = @Content), + @ApiResponse(responseCode = "500", description = "서버 오류", content = @Content) + }) + @GetMapping("/mast/list/{dtctMstId}") + public Basic selectChangeDetectionDetail(@PathVariable Long dtctMstId) { + return detectMastService.selectDetectMast(dtctMstId); + } + + @Operation(summary = "탐지객체 랜덤 PNU 리스트 조회", description = "탐지객체 PNU 랜덤값을 생성해서 보여준다") + @ApiResponses( + value = { + @ApiResponse( + responseCode = "200", + description = "목록 성공", + content = + @Content( + mediaType = "application/json", + schema = @Schema(implementation = FeaturePnuDto.class))), + @ApiResponse(responseCode = "404", description = "코드를 찾을 수 없음", content = @Content), + @ApiResponse(responseCode = "500", description = "서버 오류", content = @Content) + }) + @GetMapping("/pnu/{cprsBfYr}/{cprsAfYr}/{dtctSno}") + public List selectPnuList( + @PathVariable String cprsBfYr, @PathVariable String cprsAfYr, @PathVariable Integer dtctSno) { + DetectMastSearch detectMastSearch = new DetectMastSearch(); + detectMastSearch.setCprsAdYr(cprsAfYr); + detectMastSearch.setCprsBfYr(cprsBfYr); + detectMastSearch.setDtctSno(dtctSno); + return detectMastService.findPnuData(detectMastSearch); + } + + @Operation(summary = "탐지객체 랜덤 PNU 상세 조회", description = "탐지객체 PNU 랜덤값을 생성해서 보여준다") + @ApiResponses( + value = { + @ApiResponse( + responseCode = "200", + description = "목록 성공", + content = + @Content( + mediaType = "application/json", + schema = @Schema(implementation = FeaturePnuDto.class))), + @ApiResponse(responseCode = "404", description = "코드를 찾을 수 없음", content = @Content), + @ApiResponse(responseCode = "500", description = "서버 오류", content = @Content) + }) + @GetMapping("/pnu/{cprsBfYr}/{cprsAfYr}/{dtctSno}/{featureId}") + public FeaturePnuDto selectPnuDetail( + @Parameter(description = "이전년도", example = "2022") @PathVariable String cprsBfYr, + @Parameter(description = "기준년도", example = "2024") @PathVariable String cprsAfYr, + @Parameter(description = "회차", example = "4") @PathVariable Integer dtctSno, + @Parameter(description = "featureId", example = "000e161b-1955-4c89-ad87-0b3b4a91d00f") + @PathVariable + UUID featureId) { + return detectMastService.selectPnuDetail(featureId); + } + + @Operation( + summary = "탐지객체 랜덤 PNU GEOM 업데이트(이노펨에 없는 API)", + description = "탐지객체 랜덤 PNU GEOM 업데이트(이노펨에 없는 API)") + @ApiResponses( + value = { + @ApiResponse( + responseCode = "201", + description = "pnu 업데이트 성공", + content = + @Content( + mediaType = "application/json", + schema = @Schema(implementation = Integer.class))), + @ApiResponse(responseCode = "404", description = "코드를 찾을 수 없음", content = @Content), + @ApiResponse(responseCode = "500", description = "서버 오류", content = @Content) + }) + @PutMapping("/pnu/{cprsBfYr}/{cprsAfYr}/{dtctSno}") + public Integer updatePnuList( + @PathVariable String cprsBfYr, @PathVariable String cprsAfYr, @PathVariable Integer dtctSno) { + DetectMastSearch detectMastSearch = new DetectMastSearch(); + detectMastSearch.setCprsAdYr(cprsAfYr); + detectMastSearch.setCprsBfYr(cprsBfYr); + detectMastSearch.setDtctSno(dtctSno); + return detectMastService.updatePnuData(detectMastSearch); + } +} diff --git a/src/main/java/com/kamco/cd/kamcoback/Innopam/dto/DetectMastDto.java b/src/main/java/com/kamco/cd/kamcoback/Innopam/dto/DetectMastDto.java new file mode 100644 index 00000000..3a4a7e7e --- /dev/null +++ b/src/main/java/com/kamco/cd/kamcoback/Innopam/dto/DetectMastDto.java @@ -0,0 +1,80 @@ +package com.kamco.cd.kamcoback.Innopam.dto; + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; + +public class DetectMastDto { + + @Getter + @Setter + @NoArgsConstructor + @AllArgsConstructor + public static class Basic { + + private Long dtctMstId; + private String cprsBfYr; + private String cprsAdYr; + private Integer dtctSno; + private String pathNm; + private String crtEpno; + private String crtIp; + } + + @Getter + @Setter + @NoArgsConstructor + @AllArgsConstructor + public static class DetectMastReq { + + @NotBlank + @Schema(description = "before 연도", example = "2023") + private String cprsBfYr; + + @NotBlank + @Schema(description = "after 연도", example = "2024") + private String cprsAdYr; + + @NotNull + @Schema(description = "차수(회차)", example = "4") + private Integer dtctSno; + + @NotBlank + @Schema(description = "파일경로", example = "/app/detect/result/2023_2024/4") + private String pathNm; + + @NotBlank + @Schema(description = "사원번호", example = "1234567") + private String crtEpno; + + @NotBlank + @Schema(description = "아이피", example = "0.0.0.0") + private String crtIp; + } + + @Getter + @Setter + @NoArgsConstructor + @AllArgsConstructor + public static class DetectMastSearch { + + private String cprsBfYr; + private String cprsAdYr; + private Integer dtctSno; + private String featureId; + } + + @Getter + @Setter + @NoArgsConstructor + @AllArgsConstructor + public static class FeaturePnuDto { + + private String featureId; // polygon_id + private String pnu; // 랜덤 생성 + } +} diff --git a/src/main/java/com/kamco/cd/kamcoback/Innopam/postgres/core/DetectMastCoreService.java b/src/main/java/com/kamco/cd/kamcoback/Innopam/postgres/core/DetectMastCoreService.java new file mode 100644 index 00000000..6a77bcee --- /dev/null +++ b/src/main/java/com/kamco/cd/kamcoback/Innopam/postgres/core/DetectMastCoreService.java @@ -0,0 +1,67 @@ +package com.kamco.cd.kamcoback.Innopam.postgres.core; + +import com.kamco.cd.kamcoback.Innopam.dto.DetectMastDto; +import com.kamco.cd.kamcoback.Innopam.dto.DetectMastDto.Basic; +import com.kamco.cd.kamcoback.Innopam.dto.DetectMastDto.DetectMastReq; +import com.kamco.cd.kamcoback.Innopam.dto.DetectMastDto.DetectMastSearch; +import com.kamco.cd.kamcoback.Innopam.dto.DetectMastDto.FeaturePnuDto; +import com.kamco.cd.kamcoback.Innopam.postgres.entity.DetectMastEntity; +import com.kamco.cd.kamcoback.Innopam.postgres.repository.DetectMastRepository; +import java.util.List; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; + +@Service +@RequiredArgsConstructor +public class DetectMastCoreService { + + private final DetectMastRepository detectMastRepository; + + public void saveDetectMast(DetectMastReq detectMast) { + DetectMastEntity detectMastEntity = new DetectMastEntity(); + detectMastEntity.setCprsBfYr(detectMast.getCprsBfYr()); + detectMastEntity.setCprsAdYr(detectMast.getCprsAdYr()); + detectMastEntity.setDtctSno(detectMast.getDtctSno()); + detectMastEntity.setPathNm(detectMast.getPathNm()); + detectMastEntity.setCrtEpno(detectMast.getCrtEpno()); + detectMastEntity.setCrtIp(detectMast.getCrtIp()); + detectMastRepository.save(detectMastEntity); + } + + public List selectDetectMast(DetectMastSearch detectMast) { + return detectMastRepository.findDetectMastList(detectMast).stream() + .map( + e -> + new DetectMastDto.Basic( + e.getId(), + e.getCprsBfYr(), + e.getCprsAdYr(), + e.getDtctSno(), + e.getPathNm(), + e.getCrtEpno(), + e.getCrtIp())) + .toList(); + } + + public Basic selectDetectMast(Long id) { + DetectMastEntity e = + detectMastRepository.findById(id).orElseThrow(() -> new RuntimeException("등록 데이터가 없습니다.")); + return new DetectMastDto.Basic( + e.getId(), + e.getCprsBfYr(), + e.getCprsAdYr(), + e.getDtctSno(), + e.getPathNm(), + e.getCrtEpno(), + e.getCrtIp()); + } + + public String findPnuData(DetectMastSearch detectMast) { + DetectMastEntity detectMastEntity = detectMastRepository.findPnuData(detectMast); + return detectMastEntity.getPathNm(); + } + + public Integer updatePnu(List list) { + return detectMastRepository.updateGeomPnu(list); + } +} diff --git a/src/main/java/com/kamco/cd/kamcoback/Innopam/postgres/entity/DetectMastEntity.java b/src/main/java/com/kamco/cd/kamcoback/Innopam/postgres/entity/DetectMastEntity.java new file mode 100644 index 00000000..5d326b83 --- /dev/null +++ b/src/main/java/com/kamco/cd/kamcoback/Innopam/postgres/entity/DetectMastEntity.java @@ -0,0 +1,85 @@ +package com.kamco.cd.kamcoback.Innopam.postgres.entity; + +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.SequenceGenerator; +import jakarta.persistence.Table; +import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.Size; +import java.time.ZonedDateTime; +import java.util.UUID; +import lombok.Getter; +import lombok.Setter; +import org.hibernate.annotations.ColumnDefault; + +@Getter +@Setter +@Entity +@Table(name = "detect_mast") +public class DetectMastEntity { + + @Id + @GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "detect_mast_id_gen") + @SequenceGenerator( + name = "detect_mast_id_gen", + sequenceName = "seq_detect_mast_id", + allocationSize = 1) + @Column(name = "dtct_mst_id", nullable = false) + private Long id; + + @NotNull + @ColumnDefault("gen_random_uuid()") + @Column(name = "dtct_mst_uuid", nullable = false) + private UUID dtctMstUuid = UUID.randomUUID(); + + @Size(max = 4) + @NotNull + @Column(name = "cprs_bf_yr", nullable = false, length = 4) + private String cprsBfYr; + + @Size(max = 4) + @NotNull + @Column(name = "cprs_ad_yr", nullable = false, length = 4) + private String cprsAdYr; + + @NotNull + @Column(name = "dtct_sno", nullable = false) + private Integer dtctSno; + + @NotNull + @Column(name = "path_nm", nullable = false, length = Integer.MAX_VALUE) + private String pathNm; + + @Size(max = 50) + @Column(name = "feature_id", length = 50) + private String featureId; + + @Size(max = 30) + @NotNull + @Column(name = "crt_epno", nullable = false, length = 30) + private String crtEpno; + + @Size(max = 45) + @NotNull + @Column(name = "crt_ip", nullable = false, length = 45) + private String crtIp; + + @NotNull + @ColumnDefault("now()") + @Column(name = "crt_dttm", nullable = false) + private ZonedDateTime crtDttm = ZonedDateTime.now(); + + @Size(max = 30) + @Column(name = "chg_epno", length = 30) + private String chgEpno; + + @Size(max = 45) + @Column(name = "chg_ip", length = 45) + private String chgIp; + + @Column(name = "chg_dttm") + private ZonedDateTime chgDttm; +} diff --git a/src/main/java/com/kamco/cd/kamcoback/Innopam/postgres/entity/DetectMastPnuEntity.java b/src/main/java/com/kamco/cd/kamcoback/Innopam/postgres/entity/DetectMastPnuEntity.java new file mode 100644 index 00000000..aef47a97 --- /dev/null +++ b/src/main/java/com/kamco/cd/kamcoback/Innopam/postgres/entity/DetectMastPnuEntity.java @@ -0,0 +1,48 @@ +package com.kamco.cd.kamcoback.Innopam.postgres.entity; + +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.SequenceGenerator; +import jakarta.persistence.Table; +import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.Size; +import java.util.UUID; +import lombok.Getter; +import lombok.Setter; +import org.hibernate.annotations.ColumnDefault; + +@Getter +@Setter +@Entity +@Table(name = "detect_mast_pnu") +public class DetectMastPnuEntity { + + @Id + @GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "detect_mast_pnu_id_gen") + @SequenceGenerator( + name = "detect_mast_pnu_id_gen", + sequenceName = "seq_detect_mast_pnu_id", + allocationSize = 1) + @Column(name = "dtct_mst_pnu_id", nullable = false) + private Long id; + + @NotNull + @ColumnDefault("gen_random_uuid()") + @Column(name = "detect_mast_pnu_uuid", nullable = false) + private UUID detectMastPnuUuid; + + @NotNull + @Column(name = "dtct_mst_id", nullable = false) + private Long dtctMstId; + + @Size(max = 4) + @NotNull + @Column(name = "pnu", nullable = false, length = 4) + private String pnu; + + @Column(name = "polygon", length = Integer.MAX_VALUE) + private String polygon; +} diff --git a/src/main/java/com/kamco/cd/kamcoback/Innopam/postgres/repository/DetectMastPnuRepository.java b/src/main/java/com/kamco/cd/kamcoback/Innopam/postgres/repository/DetectMastPnuRepository.java new file mode 100644 index 00000000..b44f3677 --- /dev/null +++ b/src/main/java/com/kamco/cd/kamcoback/Innopam/postgres/repository/DetectMastPnuRepository.java @@ -0,0 +1,7 @@ +package com.kamco.cd.kamcoback.Innopam.postgres.repository; + +import com.kamco.cd.kamcoback.Innopam.postgres.entity.DetectMastPnuEntity; +import org.springframework.data.jpa.repository.JpaRepository; + +public interface DetectMastPnuRepository + extends JpaRepository, DetectMastPnuRepositoryCustom {} diff --git a/src/main/java/com/kamco/cd/kamcoback/Innopam/postgres/repository/DetectMastPnuRepositoryCustom.java b/src/main/java/com/kamco/cd/kamcoback/Innopam/postgres/repository/DetectMastPnuRepositoryCustom.java new file mode 100644 index 00000000..3029c658 --- /dev/null +++ b/src/main/java/com/kamco/cd/kamcoback/Innopam/postgres/repository/DetectMastPnuRepositoryCustom.java @@ -0,0 +1,3 @@ +package com.kamco.cd.kamcoback.Innopam.postgres.repository; + +public interface DetectMastPnuRepositoryCustom {} diff --git a/src/main/java/com/kamco/cd/kamcoback/Innopam/postgres/repository/DetectMastRepository.java b/src/main/java/com/kamco/cd/kamcoback/Innopam/postgres/repository/DetectMastRepository.java new file mode 100644 index 00000000..c6aa24f3 --- /dev/null +++ b/src/main/java/com/kamco/cd/kamcoback/Innopam/postgres/repository/DetectMastRepository.java @@ -0,0 +1,7 @@ +package com.kamco.cd.kamcoback.Innopam.postgres.repository; + +import com.kamco.cd.kamcoback.Innopam.postgres.entity.DetectMastEntity; +import org.springframework.data.jpa.repository.JpaRepository; + +public interface DetectMastRepository + extends JpaRepository, DetectMastRepositoryCustom {} diff --git a/src/main/java/com/kamco/cd/kamcoback/Innopam/postgres/repository/DetectMastRepositoryCustom.java b/src/main/java/com/kamco/cd/kamcoback/Innopam/postgres/repository/DetectMastRepositoryCustom.java new file mode 100644 index 00000000..47d1ea99 --- /dev/null +++ b/src/main/java/com/kamco/cd/kamcoback/Innopam/postgres/repository/DetectMastRepositoryCustom.java @@ -0,0 +1,15 @@ +package com.kamco.cd.kamcoback.Innopam.postgres.repository; + +import com.kamco.cd.kamcoback.Innopam.dto.DetectMastDto.DetectMastSearch; +import com.kamco.cd.kamcoback.Innopam.dto.DetectMastDto.FeaturePnuDto; +import com.kamco.cd.kamcoback.Innopam.postgres.entity.DetectMastEntity; +import java.util.List; + +public interface DetectMastRepositoryCustom { + + public List findDetectMastList(DetectMastSearch detectMast); + + public DetectMastEntity findPnuData(DetectMastSearch detectMast); + + Integer updateGeomPnu(List list); +} diff --git a/src/main/java/com/kamco/cd/kamcoback/Innopam/postgres/repository/DetectMastRepositoryImpl.java b/src/main/java/com/kamco/cd/kamcoback/Innopam/postgres/repository/DetectMastRepositoryImpl.java new file mode 100644 index 00000000..78888af4 --- /dev/null +++ b/src/main/java/com/kamco/cd/kamcoback/Innopam/postgres/repository/DetectMastRepositoryImpl.java @@ -0,0 +1,94 @@ +package com.kamco.cd.kamcoback.Innopam.postgres.repository; + +import static com.kamco.cd.kamcoback.Innopam.postgres.entity.QDetectMastEntity.detectMastEntity; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.kamco.cd.kamcoback.Innopam.dto.DetectMastDto.DetectMastSearch; +import com.kamco.cd.kamcoback.Innopam.dto.DetectMastDto.FeaturePnuDto; +import com.kamco.cd.kamcoback.Innopam.postgres.entity.DetectMastEntity; +import com.querydsl.core.BooleanBuilder; +import com.querydsl.jpa.impl.JPAQueryFactory; +import jakarta.persistence.EntityManager; +import java.util.List; +import lombok.RequiredArgsConstructor; +import org.apache.commons.lang3.StringUtils; +import org.springframework.stereotype.Repository; + +@Repository +@RequiredArgsConstructor +public class DetectMastRepositoryImpl implements DetectMastRepositoryCustom { + + private final EntityManager em; + private final JPAQueryFactory queryFactory; + private final ObjectMapper objectMapper; + + @Override + public List findDetectMastList(DetectMastSearch detectMast) { + + BooleanBuilder whereBuilder = new BooleanBuilder(); + + if (StringUtils.isNotBlank(detectMast.getCprsAdYr())) { + whereBuilder.and(detectMastEntity.cprsAdYr.eq(detectMast.getCprsAdYr())); + } + + if (StringUtils.isNotBlank(detectMast.getCprsBfYr())) { + whereBuilder.and(detectMastEntity.cprsBfYr.eq(detectMast.getCprsBfYr())); + } + + if (detectMast.getDtctSno() != null) { + whereBuilder.and(detectMastEntity.dtctSno.eq(detectMast.getDtctSno())); + } + + return queryFactory.select(detectMastEntity).from(detectMastEntity).where(whereBuilder).fetch(); + } + + @Override + public DetectMastEntity findPnuData(DetectMastSearch detectMast) { + + BooleanBuilder whereBuilder = new BooleanBuilder(); + + whereBuilder.and(detectMastEntity.cprsAdYr.eq(detectMast.getCprsAdYr())); + whereBuilder.and(detectMastEntity.cprsBfYr.eq(detectMast.getCprsBfYr())); + whereBuilder.and(detectMastEntity.dtctSno.eq(detectMast.getDtctSno())); + + if (detectMast.getFeatureId() != null) { + whereBuilder.and(detectMastEntity.featureId.eq(detectMast.getFeatureId())); + } + + return queryFactory + .select(detectMastEntity) + .from(detectMastEntity) + .where(whereBuilder) + .fetchOne(); + } + + @Override + public Integer updateGeomPnu(List list) { + if (list == null || list.isEmpty()) { + return 0; + } + + String sql = + """ + UPDATE tb_map_sheet_anal_data_inference_geom g + SET pnu = j.pnu + FROM ( + SELECT + (elem->>'featureId')::uuid AS feature_uuid, + (elem->>'pnu')::bigint AS pnu + FROM jsonb_array_elements(CAST(:json AS jsonb)) AS elem + ) j + WHERE g.uuid = j.feature_uuid; + """; + + String json = ""; + try { + json = objectMapper.writeValueAsString(list); + } catch (JsonProcessingException e) { + throw new RuntimeException("PNU 업데이트 실패", e); + } + + return em.createNativeQuery(sql).setParameter("json", json).executeUpdate(); + } +} diff --git a/src/main/java/com/kamco/cd/kamcoback/Innopam/service/DetectMastService.java b/src/main/java/com/kamco/cd/kamcoback/Innopam/service/DetectMastService.java new file mode 100644 index 00000000..f4485d20 --- /dev/null +++ b/src/main/java/com/kamco/cd/kamcoback/Innopam/service/DetectMastService.java @@ -0,0 +1,153 @@ +package com.kamco.cd.kamcoback.Innopam.service; + +import com.fasterxml.jackson.core.JsonFactory; +import com.fasterxml.jackson.core.JsonParser; +import com.fasterxml.jackson.core.JsonToken; +import com.kamco.cd.kamcoback.Innopam.dto.DetectMastDto.Basic; +import com.kamco.cd.kamcoback.Innopam.dto.DetectMastDto.DetectMastReq; +import com.kamco.cd.kamcoback.Innopam.dto.DetectMastDto.DetectMastSearch; +import com.kamco.cd.kamcoback.Innopam.dto.DetectMastDto.FeaturePnuDto; +import com.kamco.cd.kamcoback.Innopam.postgres.core.DetectMastCoreService; +import java.io.InputStream; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.ArrayList; +import java.util.List; +import java.util.UUID; +import java.util.concurrent.ThreadLocalRandom; +import java.util.stream.Stream; +import lombok.RequiredArgsConstructor; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +@Service +@Transactional(readOnly = true) +@RequiredArgsConstructor +public class DetectMastService { + + @Value("${spring.profiles.active:local}") + private String profile; + + private final DetectMastCoreService detectMastCoreService; + private final JsonFactory jsonFactory = new JsonFactory(); + + @Transactional + public void saveDetectMast(DetectMastReq detectMast) { + detectMastCoreService.saveDetectMast(detectMast); + // + // String dirPath = + // "local".equals(profile) + // ? "/Users/bokmin/detect/result/2023_2024/4" + // : detectMast.getPathNm(); + // + // List list = this.extractFeaturePnusRandom(dirPath); + } + + public List selectDetectMast(DetectMastSearch detectMast) { + return detectMastCoreService.selectDetectMast(detectMast); + } + + public Basic selectDetectMast(Long id) { + return detectMastCoreService.selectDetectMast(id); + } + + /** GeoJSON → polygon_id + 랜덤 PNU */ + public List findPnuData(DetectMastSearch detectMast) { + + String dirPath = + "local".equals(profile) + ? "/Users/bokmin/detect/result/" + + detectMast.getCprsBfYr() + + "_" + + detectMast.getCprsAdYr() + + "/" + + detectMast.getDtctSno() + : detectMastCoreService.findPnuData(detectMast); + + return extractFeaturePnusRandom(dirPath); + } + + public FeaturePnuDto selectPnuDetail(UUID uuid) { + FeaturePnuDto dto = new FeaturePnuDto(); + dto.setPnu(randomPnu()); + dto.setFeatureId(uuid.toString()); + return dto; + } + + @Transactional + public Integer updatePnuData(DetectMastSearch detectMast) { + + String dirPath = + "local".equals(profile) + ? "/Users/bokmin/detect/result/" + + detectMast.getCprsBfYr() + + "_" + + detectMast.getCprsAdYr() + + "/" + + detectMast.getDtctSno() + : detectMastCoreService.findPnuData(detectMast); + + List list = extractFeaturePnusRandom(dirPath); + return detectMastCoreService.updatePnu(list); + } + + /** 하위 폴더까지 .geojson 파일들에서 polygon_id만 뽑음 병렬처리(parallel) 제거: IO + parallel은 거의 항상 느려짐 */ + private List extractFeaturePnusRandom(String dirPath) { + + Path basePath = Paths.get(dirPath); + if (!Files.isDirectory(basePath)) { + System.err.println("유효하지 않은 디렉터리: " + dirPath); + return List.of(); + } + + List out = new ArrayList<>(4096); + + try (Stream stream = Files.walk(basePath)) { + stream + .filter(Files::isRegularFile) + .filter(p -> p.toString().toLowerCase().endsWith(".geojson")) + .forEach( + p -> { + try (InputStream in = Files.newInputStream(p); + JsonParser parser = jsonFactory.createParser(in)) { + + while (parser.nextToken() != null) { + if (parser.currentToken() == JsonToken.FIELD_NAME + && "polygon_id".equals(parser.getCurrentName())) { + + JsonToken next = parser.nextToken(); // 값으로 이동 + if (next == JsonToken.VALUE_STRING) { + String polygonId = parser.getValueAsString(); + out.add(new FeaturePnuDto(polygonId, randomPnu())); + } + } + } + + } catch (Exception e) { + // 파일 단위 실패는 최소 로그 + System.err.println("GeoJSON 파싱 실패: " + p.getFileName() + " / " + e.getMessage()); + } + }); + + } catch (Exception e) { + System.err.println("디렉터리 탐색 실패: " + e.getMessage()); + return List.of(); + } + + return out; + } + + /** 랜덤 PNU 생성 (임시) - 법정동코드(5) + 산구분(1) + 본번(4) + 부번(4) = 14자리 */ + private String randomPnu() { + ThreadLocalRandom r = ThreadLocalRandom.current(); + + String dongCode = String.format("%05d", r.nextInt(10000, 99999)); + String san = r.nextBoolean() ? "1" : "2"; + String bon = String.format("%04d", r.nextInt(1, 10000)); + String bu = String.format("%04d", r.nextInt(0, 10000)); + + return dongCode + san + bon + bu; + } +} diff --git a/src/main/java/com/kamco/cd/kamcoback/Innopam/utils/GeoJsonGeometryConverter.java b/src/main/java/com/kamco/cd/kamcoback/Innopam/utils/GeoJsonGeometryConverter.java new file mode 100644 index 00000000..c1d8842e --- /dev/null +++ b/src/main/java/com/kamco/cd/kamcoback/Innopam/utils/GeoJsonGeometryConverter.java @@ -0,0 +1,48 @@ +package com.kamco.cd.kamcoback.Innopam.utils; + +import com.fasterxml.jackson.databind.JsonNode; +import org.locationtech.jts.geom.Coordinate; +import org.locationtech.jts.geom.Geometry; +import org.locationtech.jts.geom.GeometryFactory; +import org.locationtech.jts.geom.LinearRing; +import org.locationtech.jts.geom.MultiPolygon; +import org.locationtech.jts.geom.Polygon; + +public class GeoJsonGeometryConverter { + + private static final GeometryFactory GF = new GeometryFactory(); + + public static Geometry toGeometry(JsonNode geomNode) { + String type = geomNode.path("type").asText(); + + if ("Polygon".equals(type)) { + return toPolygon(geomNode.path("coordinates")); + } + if ("MultiPolygon".equals(type)) { + return toMultiPolygon(geomNode.path("coordinates")); + } + return null; + } + + private static Polygon toPolygon(JsonNode coords) { + LinearRing shell = GF.createLinearRing(toCoords(coords.get(0))); + return GF.createPolygon(shell); + } + + private static MultiPolygon toMultiPolygon(JsonNode coords) { + Polygon[] polys = new Polygon[coords.size()]; + for (int i = 0; i < coords.size(); i++) { + polys[i] = toPolygon(coords.get(i)); + } + return GF.createMultiPolygon(polys); + } + + private static Coordinate[] toCoords(JsonNode ring) { + Coordinate[] c = new Coordinate[ring.size() + 1]; + for (int i = 0; i < ring.size(); i++) { + c[i] = new Coordinate(ring.get(i).get(0).asDouble(), ring.get(i).get(1).asDouble()); + } + c[c.length - 1] = c[0]; + return c; + } +} diff --git a/src/main/java/com/kamco/cd/kamcoback/Innopam/utils/GeoJsonLoader.java b/src/main/java/com/kamco/cd/kamcoback/Innopam/utils/GeoJsonLoader.java new file mode 100644 index 00000000..879cdea4 --- /dev/null +++ b/src/main/java/com/kamco/cd/kamcoback/Innopam/utils/GeoJsonLoader.java @@ -0,0 +1,44 @@ +package com.kamco.cd.kamcoback.Innopam.utils; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import java.io.File; +import java.util.ArrayList; +import java.util.List; + +public class GeoJsonLoader { + + private final ObjectMapper om = new ObjectMapper(); + + public GeoJsonFile load(File geoJsonFile) throws Exception { + + JsonNode root = om.readTree(geoJsonFile); + + long mapId = root.path("properties").path("map_id").asLong(-1); + if (mapId <= 0) { + throw new IllegalStateException( + "GeoJSON top-level properties.map_id 없음: " + geoJsonFile.getName()); + } + + List features = new ArrayList<>(); + root.path("features").forEach(features::add); + + return new GeoJsonFile(mapId, features); + } + + /** ✅ feature에서 polygon_id 추출 */ + public static String polygonId(JsonNode feature) { + return feature.path("properties").path("polygon_id").asText(null); + } + + public static class GeoJsonFile { + + public final long mapId; + public final List features; + + public GeoJsonFile(long mapId, List features) { + this.mapId = mapId; + this.features = features; + } + } +} diff --git a/src/main/java/com/kamco/cd/kamcoback/Innopam/utils/MapIdUtils.java b/src/main/java/com/kamco/cd/kamcoback/Innopam/utils/MapIdUtils.java new file mode 100644 index 00000000..2ab92670 --- /dev/null +++ b/src/main/java/com/kamco/cd/kamcoback/Innopam/utils/MapIdUtils.java @@ -0,0 +1,17 @@ +package com.kamco.cd.kamcoback.Innopam.utils; + +public class MapIdUtils { + + private MapIdUtils() { + // util class + } + + /** map_id → 시도코드 예: 34602060 → "34" */ + public static String sidoCodeFromMapId(long mapId) { + String s = String.valueOf(mapId); + if (s.length() < 2) { + throw new IllegalArgumentException("잘못된 map_id: " + mapId); + } + return s.substring(0, 2); + } +} diff --git a/src/main/java/com/kamco/cd/kamcoback/Innopam/utils/ShpIndexManager.java b/src/main/java/com/kamco/cd/kamcoback/Innopam/utils/ShpIndexManager.java new file mode 100644 index 00000000..60100995 --- /dev/null +++ b/src/main/java/com/kamco/cd/kamcoback/Innopam/utils/ShpIndexManager.java @@ -0,0 +1,76 @@ +package com.kamco.cd.kamcoback.Innopam.utils; + +import java.io.File; +import java.util.Map; +import java.util.Objects; +import java.util.concurrent.ConcurrentHashMap; +import org.geotools.api.data.DataStore; +import org.geotools.api.data.DataStoreFinder; +import org.geotools.api.data.SimpleFeatureSource; +import org.geotools.api.feature.simple.SimpleFeature; +import org.geotools.data.simple.SimpleFeatureCollection; +import org.geotools.data.simple.SimpleFeatureIterator; +import org.locationtech.jts.geom.Geometry; +import org.locationtech.jts.index.strtree.STRtree; + +public class ShpIndexManager { + + private static final String SHP_ROOT = "/shp"; + private static final String SHP_YYYYMM = "202512"; + private static final String PNU_FIELD = "PNU"; + + private final Map cache = new ConcurrentHashMap<>(); + + public STRtree getIndex(String sidoCode) { + return cache.computeIfAbsent(sidoCode, this::loadIndex); + } + + private STRtree loadIndex(String sidoCode) { + try { + String path = SHP_ROOT + "/LSMD_CONT_LDREG_" + sidoCode + "_" + SHP_YYYYMM + ".shp"; + + File shp = new File(path); + if (!shp.exists()) { + return null; + } + + STRtree index = new STRtree(10); + + DataStore store = DataStoreFinder.getDataStore(Map.of("url", shp.toURI().toURL())); + + String typeName = store.getTypeNames()[0]; + SimpleFeatureSource source = store.getFeatureSource(typeName); + SimpleFeatureCollection col = source.getFeatures(); + + try (SimpleFeatureIterator it = col.features()) { + while (it.hasNext()) { + SimpleFeature f = it.next(); + Geometry geom = (Geometry) f.getDefaultGeometry(); + String pnu = Objects.toString(f.getAttribute(PNU_FIELD), null); + if (geom != null && pnu != null) { + index.insert(geom.getEnvelopeInternal(), new ShpRow(geom, pnu)); + } + } + } + + index.build(); + store.dispose(); + return index; + + } catch (Exception e) { + return null; + } + } + + /** SHP 한 row */ + public static class ShpRow { + + public final Geometry geom; + public final String pnu; + + public ShpRow(Geometry geom, String pnu) { + this.geom = geom; + this.pnu = pnu; + } + } +} diff --git a/src/main/java/com/kamco/cd/kamcoback/Innopam/utils/ShpPnuMatcher.java b/src/main/java/com/kamco/cd/kamcoback/Innopam/utils/ShpPnuMatcher.java new file mode 100644 index 00000000..f61d3852 --- /dev/null +++ b/src/main/java/com/kamco/cd/kamcoback/Innopam/utils/ShpPnuMatcher.java @@ -0,0 +1,42 @@ +package com.kamco.cd.kamcoback.Innopam.utils; + +import java.util.List; +import org.locationtech.jts.geom.Envelope; +import org.locationtech.jts.geom.Geometry; +import org.locationtech.jts.geom.prep.PreparedGeometry; +import org.locationtech.jts.geom.prep.PreparedGeometryFactory; +import org.locationtech.jts.index.strtree.STRtree; + +public class ShpPnuMatcher { + + public static String pickByIntersectionMax(STRtree index, Geometry target) { + + Envelope env = target.getEnvelopeInternal(); + + @SuppressWarnings("unchecked") + List rows = index.query(env); + + double best = 0; + String bestPnu = null; + + for (ShpIndexManager.ShpRow row : rows) { + + PreparedGeometry prep = PreparedGeometryFactory.prepare(row.geom); + + if (prep.contains(target) || prep.covers(target)) { + return row.pnu; + } + + if (!prep.intersects(target)) { + continue; + } + + double area = row.geom.intersection(target).getArea(); + if (area > best) { + best = area; + bestPnu = row.pnu; + } + } + return bestPnu; + } +} diff --git a/src/main/java/com/kamco/cd/kamcoback/common/enums/CommonUseStatus.java b/src/main/java/com/kamco/cd/kamcoback/common/enums/CommonUseStatus.java index c099d74f..75596c54 100644 --- a/src/main/java/com/kamco/cd/kamcoback/common/enums/CommonUseStatus.java +++ b/src/main/java/com/kamco/cd/kamcoback/common/enums/CommonUseStatus.java @@ -1,5 +1,7 @@ package com.kamco.cd.kamcoback.common.enums; +import com.kamco.cd.kamcoback.common.enums.ApiConfigEnum.EnumDto; +import com.kamco.cd.kamcoback.common.utils.enums.CodeExpose; import com.kamco.cd.kamcoback.common.utils.enums.EnumType; import java.util.Arrays; import lombok.AllArgsConstructor; @@ -11,18 +13,19 @@ import lombok.Getter; *

This enum represents whether a resource is active, excluded from processing, or inactive. It * is commonly used for filtering, business rules, and status management. */ +@CodeExpose @Getter @AllArgsConstructor public enum CommonUseStatus implements EnumType { // @formatter:off - USE("USE", "Active", 100) + USE("USE", "사용중", 100) /** Actively used and available */ , - EXCEPT("EXCEPT", "Excluded", 200) + EXCEPT("EXCEPT", "영구 추론제외", 200) /** Explicitly excluded from use or processing */ , - NOT_USE("NOT_USE", "Inactive", 999) + NOT_USE("NOT_USE", "사용안", 999) /** Not used or disabled */ ; // @formatter:on @@ -37,4 +40,8 @@ public enum CommonUseStatus implements EnumType { .findFirst() .orElse(CommonUseStatus.NOT_USE); } + + public EnumDto getEnumDto() { + return new EnumDto<>(this, this.id, this.text); + } } diff --git a/src/main/java/com/kamco/cd/kamcoback/common/enums/SyncCheckStateType.java b/src/main/java/com/kamco/cd/kamcoback/common/enums/SyncCheckStateType.java new file mode 100644 index 00000000..2b4f1e81 --- /dev/null +++ b/src/main/java/com/kamco/cd/kamcoback/common/enums/SyncCheckStateType.java @@ -0,0 +1,26 @@ +package com.kamco.cd.kamcoback.common.enums; + +import com.kamco.cd.kamcoback.common.utils.enums.CodeExpose; +import com.kamco.cd.kamcoback.common.utils.enums.EnumType; +import lombok.AllArgsConstructor; +import lombok.Getter; + +@CodeExpose +@Getter +@AllArgsConstructor +public enum SyncCheckStateType implements EnumType { + NOTYET("미처리"), + DONE("완료"); + + private final String desc; + + @Override + public String getId() { + return name(); + } + + @Override + public String getText() { + return desc; + } +} 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/config/api/ApiLogFunction.java b/src/main/java/com/kamco/cd/kamcoback/config/api/ApiLogFunction.java index 43a78ac7..446c7317 100644 --- a/src/main/java/com/kamco/cd/kamcoback/config/api/ApiLogFunction.java +++ b/src/main/java/com/kamco/cd/kamcoback/config/api/ApiLogFunction.java @@ -129,4 +129,12 @@ public class ApiLogFunction { return m != null ? m.getMenuUid() : "SYSTEM"; } + + public static String cutRequestBody(String value) { + int MAX_LEN = 255; + if (value == null) { + return null; + } + return value.length() <= MAX_LEN ? value : value.substring(0, MAX_LEN); + } } diff --git a/src/main/java/com/kamco/cd/kamcoback/config/api/ApiResponseAdvice.java b/src/main/java/com/kamco/cd/kamcoback/config/api/ApiResponseAdvice.java index 034eccf3..72cb50bc 100644 --- a/src/main/java/com/kamco/cd/kamcoback/config/api/ApiResponseAdvice.java +++ b/src/main/java/com/kamco/cd/kamcoback/config/api/ApiResponseAdvice.java @@ -9,6 +9,7 @@ import com.kamco.cd.kamcoback.postgres.repository.log.AuditLogRepository; import jakarta.servlet.http.HttpServletRequest; import java.util.LinkedHashMap; import java.util.List; +import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.core.MethodParameter; import org.springframework.http.MediaType; @@ -26,6 +27,7 @@ import org.springframework.web.util.ContentCachingRequestWrapper; * *

createOK() → 201 CREATED ok() → 200 OK deleteOk() → 204 NO_CONTENT */ +@Slf4j @RestControllerAdvice public class ApiResponseAdvice implements ResponseBodyAdvice { @@ -110,7 +112,7 @@ public class ApiResponseAdvice implements ResponseBodyAdvice { ApiLogFunction.getUriMenuInfo(result, servletRequest.getRequestURI()), ip, servletRequest.getRequestURI(), - requestBody, + ApiLogFunction.cutRequestBody(requestBody), apiResponse.getErrorLogUid()); auditLogRepository.save(log); } diff --git a/src/main/java/com/kamco/cd/kamcoback/label/LabelAllocateApiController.java b/src/main/java/com/kamco/cd/kamcoback/label/LabelAllocateApiController.java new file mode 100644 index 00000000..b3855573 --- /dev/null +++ b/src/main/java/com/kamco/cd/kamcoback/label/LabelAllocateApiController.java @@ -0,0 +1,229 @@ +package com.kamco.cd.kamcoback.label; + +import com.kamco.cd.kamcoback.common.enums.RoleType; +import com.kamco.cd.kamcoback.config.api.ApiResponseDto; +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.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.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.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 jakarta.validation.Valid; +import java.util.List; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.data.domain.Page; +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 +@Tag(name = "라벨링 작업 관리", description = "라벨링 작업 배정 및 통계 조회 API") +@RequestMapping({"/api/label"}) +@RequiredArgsConstructor +@RestController +public class LabelAllocateApiController { + + private final LabelAllocateService labelAllocateService; + + @Operation(summary = "배정 가능한 사용자 목록 조회", description = "라벨링 작업 배정을 위한 활성 상태의 사용자 목록을 조회합니다.") + @ApiResponses( + value = { + @ApiResponse(responseCode = "200", description = "조회 성공"), + @ApiResponse(responseCode = "404", description = "코드를 찾을 수 없음"), + @ApiResponse(responseCode = "500", description = "서버 오류") + }) + @GetMapping("/avail-user") + public ApiResponseDto> availUserList( + @Parameter( + description = "사용자 역할", + example = "LABELER", + schema = @Schema(allowableValues = {"LABELER", "REVIEWER"})) + @RequestParam + String role) { + return ApiResponseDto.ok(labelAllocateService.availUserList(role)); + } + + @Operation(summary = "작업현황 관리 (라벨링, 검수 진행률 요약정보)", description = "작업현황 관리 (라벨링, 검수 진행률 요약정보)") + @ApiResponses( + value = { + @ApiResponse(responseCode = "200", description = "조회 성공"), + @ApiResponse(responseCode = "404", description = "데이터를 찾을 수 없음"), + @ApiResponse(responseCode = "500", description = "서버 오류") + }) + @GetMapping("/admin/workers") + public ApiResponseDto getWorkerStatistics( + // @Parameter(description = "분석 ID (선택)", example = "3") @RequestParam(required = false) + // Long analUid, + @Parameter( + description = "작업자 유형 (선택) - 미입력 시 LABELER로 조회", + example = "LABELER", + schema = + @Schema( + allowableValues = {"LABELER", "REVIEWER"}, + defaultValue = "LABELER")) + @RequestParam(required = false) + String type, + @Parameter(description = "검색어 (작업자 이름 또는 사번으로 검색, 부분 일치)", example = "김라벨") + @RequestParam(required = false) + String search, + @Parameter( + description = "정렬 조건 (선택) - 미입력 시 이름 오름차순", + example = "REMAINING_DESC", + schema = + @Schema( + allowableValues = { + "REMAINING_DESC", + "REMAINING_ASC", + "COMPLETED_DESC", + "COMPLETED_ASC", + "NAME_ASC", + "NAME_DESC" + }, + defaultValue = "NAME_ASC")) + @RequestParam(required = false) + String sort) { + + // type이 null이면 기본값으로 LABELER 설정 + String workerType = (type == null || type.isEmpty()) ? RoleType.LABELER.name() : type; + + return ApiResponseDto.ok( + labelAllocateService.getWorkerStatistics(null, workerType, search, sort)); + } + + @Operation(summary = "라벨링작업 관리 > 작업 배정", description = "라벨링작업 관리 > 작업 배정") + @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 @Valid LabelAllocateDto.AllocateDto dto) { + + return ApiResponseDto.okObject( + labelAllocateService.allocateAsc( + dto.getStage(), + dto.getLabelers(), + dto.getInspectors(), + dto.getCompareYyyy(), + dto.getTargetYyyy())); + } + + @Operation(summary = "작업현황 관리 > 변화탐지 회차 정보", description = "작업현황 관리 > 변화탐지 회차 정보") + @ApiResponses( + value = { + @ApiResponse(responseCode = "200", description = "조회 성공"), + @ApiResponse(responseCode = "404", description = "데이터를 찾을 수 없음"), + @ApiResponse(responseCode = "500", description = "서버 오류") + }) + @GetMapping("/stage-detail") + public ApiResponseDto findInferenceDetail( + @Parameter( + description = "회차 마스터 key", + required = true, + example = "8584e8d4-53b3-4582-bde2-28a81495a626") + @RequestParam + String uuid) { + return ApiResponseDto.ok(labelAllocateService.findInferenceDetail(uuid)); + } + + @Operation( + summary = "작업현황 관리 > 라벨러/검수자 상세 정보, 작업이관 팝업 내 라벨러 상세 정보 동일", + description = "작업현황 관리 > 라벨러/검수자 상세 정보, 작업이관 팝업 내 라벨러 상세 정보 동일") + @GetMapping("/user-detail") + public ApiResponseDto findUserDetail( + @RequestParam(defaultValue = "01022223333", required = true) String userId, + @Parameter( + description = "회차 마스터 key", + required = true, + example = "8584e8d4-53b3-4582-bde2-28a81495a626") + @RequestParam + String uuid, + @Schema( + allowableValues = {"LABELER", "REVIEWER"}, + defaultValue = "LABELER") + @Parameter(description = "라벨러/검수자(LABELER/REVIEWER)", required = true) + @RequestParam + String type) { + return ApiResponseDto.ok(labelAllocateService.findUserDetail(userId, uuid, type)); + } + + @Operation(summary = "작업현황 관리 > 상세 > 작업 이관", description = "작업현황 관리 > 상세 > 작업 이관") + @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-move") + public ApiResponseDto labelAllocateMove( + @io.swagger.v3.oas.annotations.parameters.RequestBody( + description = "라벨링 이관", + required = true, + content = + @Content( + mediaType = "application/json", + schema = @Schema(implementation = LabelAllocateDto.AllocateMoveDto.class))) + @RequestBody + LabelAllocateDto.AllocateMoveDto dto) { + + return ApiResponseDto.okObject( + labelAllocateService.allocateMove( + dto.getStage(), + dto.getLabelers(), + dto.getCompareYyyy(), + dto.getTargetYyyy(), + dto.getUserId())); + } + + @Operation( + summary = "라벨링작업 관리 > 상세 > 라벨러/검수자 일별 작업량 목록", + description = "라벨링작업 관리 > 상세 > 라벨러/검수자 일별 작업량 목록") + @GetMapping("/daily-list") + public ApiResponseDto> findDaliyList( + @RequestParam(defaultValue = "0", required = true) int page, + @RequestParam(defaultValue = "20", required = true) int size, + @Parameter( + description = "회차 마스터 key", + required = true, + example = "8584e8d4-53b3-4582-bde2-28a81495a626") + @RequestParam + String uuid, + @Parameter(description = "사번", required = true, example = "123456") @RequestParam + String userId, + @Schema( + allowableValues = {"LABELER", "REVIEWER"}, + defaultValue = "LABELER") + @Parameter(description = "라벨러/검수자(LABELER/REVIEWER)", required = true) + @RequestParam + String type) { + LabelAllocateDto.searchReq searchReq = new LabelAllocateDto.searchReq(page, size, ""); + return ApiResponseDto.ok(labelAllocateService.findDaliyList(searchReq, uuid, userId, type)); + } +} diff --git a/src/main/java/com/kamco/cd/kamcoback/label/LabelWorkerApiController.java b/src/main/java/com/kamco/cd/kamcoback/label/LabelWorkerApiController.java new file mode 100644 index 00000000..3d85d264 --- /dev/null +++ b/src/main/java/com/kamco/cd/kamcoback/label/LabelWorkerApiController.java @@ -0,0 +1,139 @@ +package com.kamco.cd.kamcoback.label; + +import com.kamco.cd.kamcoback.config.api.ApiResponseDto; +import com.kamco.cd.kamcoback.label.dto.LabelWorkDto; +import com.kamco.cd.kamcoback.label.dto.LabelWorkDto.ChangeDetectYear; +import com.kamco.cd.kamcoback.label.dto.LabelWorkDto.LabelWorkMng; +import com.kamco.cd.kamcoback.label.dto.LabelWorkDto.LabelWorkMngDetail; +import com.kamco.cd.kamcoback.label.dto.LabelWorkDto.LabelWorkMngSearchReq; +import com.kamco.cd.kamcoback.label.dto.LabelWorkDto.WorkerState; +import com.kamco.cd.kamcoback.label.dto.LabelWorkDto.WorkerStateSearchReq; +import com.kamco.cd.kamcoback.label.service.LabelWorkService; +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.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 java.util.UUID; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.data.domain.Page; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +@Slf4j +@Tag(name = "라벨링 작업 관리", description = "라벨링 작업 관리") +@RequestMapping({"/api/label"}) +@RequiredArgsConstructor +@RestController +public class LabelWorkerApiController { + + private final LabelWorkService labelWorkService; + + @Operation(summary = "변화탐지 년도 셀렉트박스 조회", description = "라벨링작업 관리 > 목록 조회 변화탐지 년도 셀렉트박스 조회") + @ApiResponses( + value = { + @ApiResponse( + responseCode = "200", + description = "조회 성공", + content = + @Content( + mediaType = "application/json", + schema = @Schema(implementation = ChangeDetectYear.class))), + @ApiResponse(responseCode = "404", description = "코드를 찾을 수 없음", content = @Content), + @ApiResponse(responseCode = "500", description = "서버 오류", content = @Content) + }) + @GetMapping("/change-detect-year") + public ApiResponseDto> getChangeDetectYear() { + return ApiResponseDto.ok(labelWorkService.getChangeDetectYear()); + } + + @Operation(summary = "라벨링작업 관리 > 목록 조회", description = "라벨링작업 관리 > 목록 조회") + @ApiResponses( + value = { + @ApiResponse( + responseCode = "200", + description = "조회 성공", + content = + @Content( + mediaType = "application/json", + schema = @Schema(implementation = Page.class))), + @ApiResponse(responseCode = "404", description = "코드를 찾을 수 없음", content = @Content), + @ApiResponse(responseCode = "500", description = "서버 오류", content = @Content) + }) + @GetMapping("/label-work-mng-list") + public ApiResponseDto> labelWorkMngList( + @Parameter(description = "변화탐지년도", example = "2022-2024") @RequestParam(required = false) + String detectYear, + @Parameter(description = "시작일", example = "20220101") @RequestParam String strtDttm, + @Parameter(description = "종료일", example = "20261201") @RequestParam String endDttm, + @Parameter(description = "페이지 번호 (0부터 시작)", example = "0") @RequestParam(defaultValue = "0") + int page, + @Parameter(description = "페이지 크기", example = "20") @RequestParam(defaultValue = "20") + int size) { + LabelWorkDto.LabelWorkMngSearchReq searchReq = new LabelWorkMngSearchReq(); + searchReq.setDetectYear(detectYear); + searchReq.setStrtDttm(strtDttm); + searchReq.setEndDttm(endDttm); + searchReq.setPage(page); + searchReq.setSize(size); + return ApiResponseDto.ok(labelWorkService.labelWorkMngList(searchReq)); + } + + @Operation(summary = "라벨링작업 관리 > 작업 배정 정보조회", description = "작업 배정 정보조회") + @ApiResponses( + value = { + @ApiResponse( + responseCode = "200", + description = "조회 성공", + content = + @Content( + mediaType = "application/json", + schema = @Schema(implementation = LabelWorkMngDetail.class))), + @ApiResponse(responseCode = "404", description = "코드를 찾을 수 없음", content = @Content), + @ApiResponse(responseCode = "500", description = "서버 오류", content = @Content) + }) + @GetMapping("/label-work-mng-detail/{uuid}") + public ApiResponseDto labelWorkMngDetail( + @Parameter(description = "uuid") @PathVariable UUID uuid) { + return ApiResponseDto.ok(labelWorkService.findLabelWorkMngDetail(uuid)); + } + + @Operation(summary = "작업현황 관리 > 현황 목록 조회", description = "작업현황 관리 > 현황 목록 조회") + @ApiResponses( + value = { + @ApiResponse( + responseCode = "200", + description = "조회 성공", + content = + @Content( + mediaType = "application/json", + schema = @Schema(implementation = Page.class))), + @ApiResponse(responseCode = "404", description = "코드를 찾을 수 없음", content = @Content), + @ApiResponse(responseCode = "500", description = "서버 오류", content = @Content) + }) + @GetMapping("/work-state-list") + public ApiResponseDto> findWorkStateList( + @Parameter(description = "유형", example = "LABELER") @RequestParam(required = false) + String userRole, + @Parameter(description = "검색어", example = "20261201") @RequestParam(required = false) + String searchVal, + @Parameter(description = "페이지 번호 (0부터 시작)", example = "0") @RequestParam(defaultValue = "0") + int page, + @Parameter(description = "페이지 크기", example = "20") @RequestParam(defaultValue = "20") + int size) { + + LabelWorkDto.WorkerStateSearchReq searchReq = new WorkerStateSearchReq(); + searchReq.setUserRole(userRole); + searchReq.setSearchVal(searchVal); + searchReq.setPage(page); + searchReq.setSize(size); + return ApiResponseDto.ok(labelWorkService.findlabelWorkStateList(searchReq)); + } +} 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 new file mode 100644 index 00000000..12c95bb7 --- /dev/null +++ b/src/main/java/com/kamco/cd/kamcoback/label/dto/LabelAllocateDto.java @@ -0,0 +1,304 @@ +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; +import lombok.NoArgsConstructor; +import lombok.Setter; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Pageable; +import org.springframework.data.domain.Sort; + +public class LabelAllocateDto { + + @CodeExpose + @Getter + @AllArgsConstructor + public enum LabelMngState implements EnumType { + PENDING("작업대기"), + ASSIGNED("작업할당"), + STOP("중단"), + LABEL_ING("라벨진행중"), + LABEL_COMPLETE("라벨완료"), + INSPECT_REQ("검수요청"), + INSPECT_ING("검수진행중"), + INSPECT_COMPLETE("검수완료"); + + private String desc; + + @Override + public String getId() { + return name(); + } + + @Override + public String getText() { + return desc; + } + } + + @CodeExpose + @Getter + @AllArgsConstructor + public enum LabelState implements EnumType { + WAIT("대기"), + ASSIGNED("배정"), + SKIP("스킵"), + DONE("완료"), + 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; + } + } + + @Getter + @Setter + @AllArgsConstructor + public static class AllocateDto { + + @Schema(description = "비교년도", example = "2022", required = true) + private Integer compareYyyy; + + @Schema(description = "기준년도", example = "2024", required = true) + private Integer targetYyyy; + + @Schema(description = "회차", example = "4", required = true) + private Integer stage; + + @Schema( + description = "라벨러 할당 목록", + example = + """ + [ + { + "userId": "123456", + "demand": 1000 + }, + { + "userId": "010222297501", + "demand": 400 + }, + { + "userId": "01022223333", + "demand": 440 + } + ] + """) + private List labelers; + + @Schema( + description = "검수자 할당 목록", + example = + """ + ["K20251216001", + "01022225555", + "K20251212001" + ] + """) + 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) { + this.userId = userId; + this.demand = demand; + } + } + + @Getter + @AllArgsConstructor + public static class TargetInspector { + + @Schema(description = "검수자 사번", example = "K20251212001") + private final String inspectorUid; + + @Schema(description = "할당 명수", example = "3") + 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; + private String inspectState; + private ZonedDateTime workStatDttm; + private ZonedDateTime inspectStatDttm; + } + + @Getter + @Setter + @AllArgsConstructor + public static class UserList { + + private String userRole; + 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; + } + + @Getter + @Setter + @AllArgsConstructor + public static class LabelerDetail { + + private String roleType; + private String name; + private String userId; // 사번 + private Long count; + private Long completeCnt; + private Long skipCnt; + private Double percent; + private Integer ranking; + private ZonedDateTime createdDttm; + private String ownerName; + } + + @Getter + @Setter + @AllArgsConstructor + public static class AllocateMoveDto { + + @Schema(description = "회차", example = "4") + private Integer stage; + + @Schema( + description = "이관할 라벨러 할당량", + example = + """ + [ + { + "userId": "123456", + "demand": 10 + }, + { + "userId": "010222297501", + "demand": 5 + } + ] + """) + private List labelers; + + @Schema(description = "비교년도", example = "2022") + private Integer compareYyyy; + + @Schema(description = "기준년도", example = "2024") + private Integer targetYyyy; + + @Schema(description = "대상 사번", example = "01022223333") + private String userId; + } + + @Getter + @Setter + @AllArgsConstructor + public static class AllocateInfoDto { + + private Long geoUid; + private Long mapSheetNum; + } + + @Getter + @Setter + @AllArgsConstructor + public static class LabelingStatDto { + + private String workDate; + private Long dailyTotalCnt; + private Long totalCnt; + private Long assignedCnt; + private Long skipCnt; + private Long completeCnt; + private Long remainCnt; + } + + @Schema(name = "searchReq", description = "일자별 작업 목록 요청") + @Getter + @Setter + @NoArgsConstructor + @AllArgsConstructor + public static class searchReq { + + // 페이징 파라미터 + private int page = 0; + private int size = 20; + private String sort; + + public Pageable toPageable() { + if (sort != null && !sort.isEmpty()) { + String[] sortParams = sort.split(","); + String property = sortParams[0]; + Sort.Direction direction = + 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); + } + } +} diff --git a/src/main/java/com/kamco/cd/kamcoback/label/dto/LabelInspectorDto.java b/src/main/java/com/kamco/cd/kamcoback/label/dto/LabelInspectorDto.java new file mode 100644 index 00000000..89fd8ef7 --- /dev/null +++ b/src/main/java/com/kamco/cd/kamcoback/label/dto/LabelInspectorDto.java @@ -0,0 +1,22 @@ +package com.kamco.cd.kamcoback.label.dto; + +import java.time.ZonedDateTime; +import java.util.UUID; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.Setter; + +public class LabelInspectorDto { + + @Getter + @Setter + @AllArgsConstructor + public static class Basic { + + private UUID operatorUid; + private Long analUid; + private String inspectorUid; + private ZonedDateTime createdDttm; + private ZonedDateTime updatedDttm; + } +} diff --git a/src/main/java/com/kamco/cd/kamcoback/label/dto/LabelWorkDto.java b/src/main/java/com/kamco/cd/kamcoback/label/dto/LabelWorkDto.java new file mode 100644 index 00000000..cdd0f1c0 --- /dev/null +++ b/src/main/java/com/kamco/cd/kamcoback/label/dto/LabelWorkDto.java @@ -0,0 +1,217 @@ +package com.kamco.cd.kamcoback.label.dto; + +import com.fasterxml.jackson.annotation.JsonProperty; +import com.kamco.cd.kamcoback.common.utils.enums.Enums; +import com.kamco.cd.kamcoback.common.utils.interfaces.JsonFormatDttm; +import com.kamco.cd.kamcoback.label.dto.LabelAllocateDto.LabelMngState; +import io.swagger.v3.oas.annotations.media.Schema; +import java.time.ZonedDateTime; +import java.util.UUID; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Pageable; + +public class LabelWorkDto { + + @Getter + @Setter + @AllArgsConstructor + public static class ChangeDetectYear { + + private String code; + private String name; + } + + @Schema(name = "LabelWorkMng", description = "라벨작업관리") + @Getter + @Setter + @NoArgsConstructor + @AllArgsConstructor + public static class LabelWorkMng { + + private UUID uuid; + private Integer compareYyyy; + private Integer targetYyyy; + private int stage; + @JsonFormatDttm private ZonedDateTime createdDttm; + private Long detectionTotCnt; + private Long labelTotCnt; + private Long labelAssignCnt; + private Long labelStopTotCnt; + private Long labelIngTotCnt; + private Long labelCompleteTotCnt; + @JsonFormatDttm private ZonedDateTime labelStartDttm; + + @JsonProperty("detectYear") + public String getDetectYear() { + if (compareYyyy == null || targetYyyy == null) { + return null; + } + return compareYyyy + "-" + targetYyyy; + } + + public String getLabelState() { + + String mngState = "PENDING"; + + if (this.labelTotCnt == 0) { + mngState = "PENDING"; + } else if (this.labelTotCnt > 0 && this.labelAssignCnt > 0 && this.labelIngTotCnt == 0) { + mngState = "ASSIGNED"; + } else if (this.labelIngTotCnt > 0) { + mngState = "LABEL_ING"; + } else if (this.labelTotCnt <= labelCompleteTotCnt) { + mngState = "LABEL_COMPLETE"; + } + + return mngState; + } + + public String getLabelStateName() { + String enumId = this.getLabelState(); + if (enumId == null || enumId.isEmpty()) { + enumId = "PENDING"; + } + + LabelMngState type = Enums.fromId(LabelMngState.class, enumId); + return type.getText(); + } + + public double getLabelRate() { + if (this.labelTotCnt == null || this.labelCompleteTotCnt == 0) { + return 0.0; + } + return (double) this.labelTotCnt / this.labelCompleteTotCnt * 100.0; + } + } + + @Getter + @Setter + @AllArgsConstructor + @NoArgsConstructor + public static class LabelWorkMngDetail { + + private String detectionYear; + private Integer stage; + @JsonFormatDttm private ZonedDateTime createdDttm; + private Long labelTotCnt; + private Long labeler; + private Long reviewer; + } + + @Schema(name = "LabelWorkMngSearchReq", description = "라벨작업관리 검색 요청") + @Getter + @Setter + @NoArgsConstructor + @AllArgsConstructor + public static class LabelWorkMngSearchReq { + + // 페이징 파라미터 + @Schema(description = "페이지 번호 (0부터 시작) ", example = "0") + private int page = 0; + + @Schema(description = "페이지 크기", example = "20") + private int size = 20; + + @Schema(description = "변화탐지년도", example = "2024") + private String detectYear; + + @Schema(description = "시작일", example = "20260101") + private String strtDttm; + + @Schema(description = "종료일", example = "20261201") + private String endDttm; + + public Pageable toPageable() { + + return PageRequest.of(page, size); + } + } + + @Getter + @Setter + @Builder + @NoArgsConstructor + @AllArgsConstructor + @Schema(description = "작업자 통계 응답") + public static class WorkerState { + + @Schema(description = "작업자 유형 (LABELER/INSPECTOR)") + private String userRole; + + @Schema(description = "작업자 ID (사번)") + private String name; + + @Schema(description = "작업자 이름") + private String userId; + + @Schema(description = "배정개수") + private Long assignedCnt; + + @Schema(description = "완료개수") + private Long doneCnt; + + @Schema(description = "Skip개수") + private Long skipCnt; + + @Schema(description = "Skip개수") + private Long day3AgoDoneCnt; + + @Schema(description = "Skip개수") + private Long day2AgoDoneCnt; + + @Schema(description = "Skip개수") + private Long day1AgoDoneCnt; + + public Long getremindCnt() { + return this.assignedCnt - this.doneCnt; + } + + public double getDoneRate() { + if (this.doneCnt == null || this.assignedCnt == 0) { + return 0.0; + } + return (double) this.doneCnt / this.assignedCnt * 100.0; + } + + public String getUserRoleName() { + if (this.userRole.equals("LABELER")) { + return "라벨러"; + } + return "검수자"; + } + } + + @Schema(name = "WorkerStateSearchReq", description = "라벨작업관리 검색 요청") + @Getter + @Setter + @NoArgsConstructor + @AllArgsConstructor + public static class WorkerStateSearchReq { + + // 페이징 파라미터 + @Schema(description = "페이지 번호 (0부터 시작) ", example = "0") + private int page = 0; + + @Schema(description = "페이지 크기", example = "20") + private int size = 20; + + @Schema(description = "유형", example = "LABELER") + private String userRole; + + @Schema(description = "종료일", example = "20261201") + private String searchVal; + + @Schema(description = "종료일", example = "20261201") + private String uuid; + + public Pageable toPageable() { + + return PageRequest.of(page, size); + } + } +} 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..e7a3e035 --- /dev/null +++ b/src/main/java/com/kamco/cd/kamcoback/label/dto/WorkerStatsDto.java @@ -0,0 +1,204 @@ +package com.kamco.cd.kamcoback.label.dto; + +import com.kamco.cd.kamcoback.common.utils.interfaces.JsonFormatDttm; +import io.swagger.v3.oas.annotations.media.Schema; +import java.time.ZonedDateTime; +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 ProjectInfo { + + @Schema(description = "변화탐지년도 (예: 2026-2025)") + private String detectionYear; + + @Schema(description = "회차 (예: 8)") + private String stage; + + @Schema(description = "국유인 반영일 (예: 2026-03-31)") + @JsonFormatDttm + private ZonedDateTime gukyuinApplyDttm; + + @Schema(description = "작업 시작일 (예: 2026-04-06)") + @JsonFormatDttm + private ZonedDateTime startDttm; + + @Schema(description = "프로젝트 UUID") + private String uuid; + } + + @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; + + // 레거시 필드 (기존 호환성 유지) + @Deprecated private Long doneCnt; // completed로 대체 + + @Deprecated private Long skipCnt; // skipped로 대체 + + @Deprecated private Long remainingCnt; // remaining으로 대체 + + @Deprecated private Long day3AgoDoneCnt; // history.day3Ago로 대체 + + @Deprecated private Long day2AgoDoneCnt; // history.day2Ago로 대체 + + @Deprecated private Long day1AgoDoneCnt; // history.day1Ago로 대체 + } + + @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 labelingStatus; + + @Schema(description = "라벨링 전체 배정 건수") + private Long labelingTotalCount; + + @Schema(description = "라벨링 완료 건수 (LABEL_FIN + TEST_ING + DONE)") + private Long labelingCompletedCount; + + @Schema(description = "라벨링 스킵 건수 (SKIP)") + private Long labelingSkipCount; + + @Schema(description = "라벨링 남은 작업 건수") + private Long labelingRemainingCount; + + @Schema(description = "투입된 라벨러 수") + private Long labelerCount; + + // === 검수(Inspection) 관련 (신규 추가) === + @Schema(description = "검수 진행률 (완료건/대상건)") + private Double inspectionProgressRate; + + @Schema(description = "검수 작업 상태 (진행중/완료)") + private String inspectionStatus; + + @Schema(description = "검수 전체 대상 건수") + private Long inspectionTotalCount; + + @Schema(description = "검수 완료 건수 (DONE)") + private Long inspectionCompletedCount; + + @Schema(description = "검수 제외 건수 (라벨링 스킵과 동일)") + private Long inspectionSkipCount; + + @Schema(description = "검수 남은 작업 건수") + private Long inspectionRemainingCount; + + @Schema(description = "투입된 검수자 수") + private Long inspectorCount; + + // === 레거시 호환 필드 (Deprecated) === + @Deprecated + @Schema(description = "[Deprecated] labelingProgressRate 사용 권장") + private Double progressRate; + + @Deprecated + @Schema(description = "[Deprecated] labelingTotalCount 사용 권장") + private Long totalAssignedCount; + + @Deprecated + @Schema(description = "[Deprecated] labelingCompletedCount 사용 권장") + private Long completedCount; + + @Deprecated + @Schema(description = "[Deprecated] labelingRemainingCount 사용 권장") + private Long remainingLabelCount; + + @Deprecated + @Schema(description = "[Deprecated] inspectionRemainingCount 사용 권장") + private Long remainingInspectCount; + + @Deprecated + @Schema(description = "[Deprecated] labelingStatus/inspectionStatus 사용 권장") + private String workStatus; + } + + @Getter + @Setter + @Builder + @NoArgsConstructor + @AllArgsConstructor + @Schema(description = "작업자 목록 응답 (작업 정보 포함)") + public static class WorkerListResponse { + + @Schema(description = "프로젝트 기본 정보 (상단 표시용)") + private ProjectInfo projectInfo; + + @Schema(description = "작업 진행 현황 정보") + private WorkProgressInfo progressInfo; + + // workers 필드는 제거되었습니다 (프로젝트 정보와 진행현황만 반환) + } +} 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 new file mode 100644 index 00000000..8b6b82b9 --- /dev/null +++ b/src/main/java/com/kamco/cd/kamcoback/label/service/LabelAllocateService.java @@ -0,0 +1,182 @@ +package com.kamco.cd.kamcoback.label.service; + +import com.kamco.cd.kamcoback.config.api.ApiResponseDto; +import com.kamco.cd.kamcoback.config.api.ApiResponseDto.ApiResponseCode; +import com.kamco.cd.kamcoback.label.dto.LabelAllocateDto; +import com.kamco.cd.kamcoback.label.dto.LabelAllocateDto.AllocateInfoDto; +import com.kamco.cd.kamcoback.label.dto.LabelAllocateDto.InferenceDetail; +import com.kamco.cd.kamcoback.label.dto.LabelAllocateDto.LabelerDetail; +import com.kamco.cd.kamcoback.label.dto.LabelAllocateDto.LabelingStatDto; +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.WorkerListResponse; +import com.kamco.cd.kamcoback.postgres.core.LabelAllocateCoreService; +import java.util.List; +import java.util.Objects; +import lombok.extern.slf4j.Slf4j; +import org.springframework.data.domain.Page; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +@Slf4j +@Service +@Transactional +public class LabelAllocateService { + + private final LabelAllocateCoreService labelAllocateCoreService; + + public LabelAllocateService(LabelAllocateCoreService labelAllocateCoreService) { + this.labelAllocateCoreService = labelAllocateCoreService; + } + + /** + * 도엽 기준 asc sorting 해서 할당 수만큼 배정하는 로직 + * + * @param stage 회차 + * @param targetUsers 라벨러 목록 + * @param targetInspectors 검수자 목록 + */ + @Transactional + public ApiResponseDto.ResponseObj allocateAsc( + Integer stage, + List targetUsers, + List targetInspectors, + Integer compareYyyy, + Integer targetYyyy) { + Long lastId = null; + + // geom 잔여건수 조회 + Long chargeCnt = + labelAllocateCoreService.findLabelUnAssignedCnt(stage, compareYyyy, targetYyyy); + if (chargeCnt <= 0) { + return new ApiResponseDto.ResponseObj(ApiResponseCode.DUPLICATE_DATA, "이미 배정완료된 회차 입니다."); + } + + Long totalDemand = targetUsers.stream().mapToLong(TargetUser::getDemand).sum(); + if (!Objects.equals(chargeCnt, totalDemand)) { + return new ApiResponseDto.ResponseObj( + ApiResponseCode.BAD_REQUEST, "총 잔여건수와 요청 값의 합계가 맞지 않습니다."); + } + + List allIds = + labelAllocateCoreService.fetchNextIds(lastId, chargeCnt, compareYyyy, targetYyyy, stage); + + // MapSheetAnalInferenceEntity analUid 가져오기 + Long analUid = + labelAllocateCoreService.findMapSheetAnalInferenceUid(compareYyyy, targetYyyy, stage); + + int index = 0; + for (TargetUser target : targetUsers) { + int end = index + target.getDemand(); + List sub = allIds.subList(index, end); + + labelAllocateCoreService.assignOwner(sub, target.getUserId(), analUid); + index = end; + } + + // 검수자에게 userCount명 만큼 할당 + List list = labelAllocateCoreService.findAssignedLabelerList(analUid); + + for (String inspector : targetInspectors) { + labelAllocateCoreService.insertInspector(analUid, inspector); + } + + // int from = 0; + // for (TargetInspector inspector : targetInspectors) { + // int to = Math.min(from + inspector.getUserCount(), list.size()); + // + // if (from >= to) { + // break; + // } + // + // List assignmentUids = + // list.subList(from, + // to).stream().map(LabelAllocateDto.Basic::getAssignmentUid).toList(); + // + // labelAllocateCoreService.assignInspectorBulk(assignmentUids, + // inspector.getInspectorUid()); + // + // from = to; + // } + + return new ApiResponseDto.ResponseObj(ApiResponseCode.OK, "배정이 완료되었습니다."); + } + + public List availUserList(String role) { + return labelAllocateCoreService.availUserList(role); + } + + /** + * 작업자 통계 조회 + * + * @param analUid 분석 ID + * @param workerType 작업자 유형 (LABELER/INSPECTOR) + * @param search 검색어 (이름 또는 사번) + * @param sortType 정렬 조건 + * @return 작업자 목록 및 통계 + */ + public WorkerListResponse getWorkerStatistics( + Long analUid, String workerType, String search, String sortType) { + + // 프로젝트 정보 조회 (analUid가 없으면 최신 프로젝트 정보 조회) + var projectInfo = + analUid != null + ? labelAllocateCoreService.findProjectInfo(analUid) + : labelAllocateCoreService.findLatestProjectInfo(); + + // 작업 진행 현황 조회 + var progressInfo = labelAllocateCoreService.findWorkProgressInfo(analUid); + + return WorkerListResponse.builder().projectInfo(projectInfo).progressInfo(progressInfo).build(); + } + + public InferenceDetail findInferenceDetail(String uuid) { + return labelAllocateCoreService.findInferenceDetail(uuid); + } + + public ApiResponseDto.ResponseObj allocateMove( + Integer stage, + List targetUsers, + Integer compareYyyy, + Integer targetYyyy, + String userId) { + Long lastId = null; + + Long chargeCnt = targetUsers.stream().mapToLong(TargetUser::getDemand).sum(); + + if (chargeCnt <= 0) { + return new ApiResponseDto.ResponseObj(ApiResponseCode.BAD_REQUEST, "이관할 데이터를 입력해주세요."); + } + + List allIds = + labelAllocateCoreService.fetchNextMoveIds( + lastId, chargeCnt, compareYyyy, targetYyyy, stage, userId); + int index = 0; + for (TargetUser target : targetUsers) { + int end = index + target.getDemand(); + List sub = allIds.subList(index, end); + + labelAllocateCoreService.assignOwnerMove(sub, target.getUserId()); + index = end; + } + + return new ApiResponseDto.ResponseObj(ApiResponseCode.OK, "이관을 완료하였습니다."); + } + + public LabelerDetail findUserDetail(String userId, String uuid, String type) { + if (type.equals("LABELER")) { + return labelAllocateCoreService.findLabelerDetail(userId, uuid); + } else { + return labelAllocateCoreService.findInspectorDetail(userId, uuid); + } + } + + public Page findDaliyList( + LabelAllocateDto.searchReq searchReq, String uuid, String userId, String type) { + if (type.equals("LABELER")) { + return labelAllocateCoreService.findLabelerDailyStat(searchReq, uuid, userId); + } else { + return labelAllocateCoreService.findInspectorDailyStat(searchReq, uuid, userId); + } + } +} diff --git a/src/main/java/com/kamco/cd/kamcoback/label/service/LabelWorkService.java b/src/main/java/com/kamco/cd/kamcoback/label/service/LabelWorkService.java new file mode 100644 index 00000000..f6405984 --- /dev/null +++ b/src/main/java/com/kamco/cd/kamcoback/label/service/LabelWorkService.java @@ -0,0 +1,58 @@ +package com.kamco.cd.kamcoback.label.service; + +import com.kamco.cd.kamcoback.label.dto.LabelWorkDto; +import com.kamco.cd.kamcoback.label.dto.LabelWorkDto.ChangeDetectYear; +import com.kamco.cd.kamcoback.label.dto.LabelWorkDto.LabelWorkMng; +import com.kamco.cd.kamcoback.label.dto.LabelWorkDto.LabelWorkMngDetail; +import com.kamco.cd.kamcoback.label.dto.LabelWorkDto.WorkerState; +import com.kamco.cd.kamcoback.postgres.core.LabelWorkCoreService; +import java.util.List; +import java.util.UUID; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.data.domain.Page; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +@Slf4j +@Service +@RequiredArgsConstructor +@Transactional(rollbackFor = Exception.class) +public class LabelWorkService { + + private final LabelWorkCoreService labelWorkCoreService; + + /** + * 라벨링작업 관리 목록조회 + * + * @param searchReq + * @return + */ + public Page labelWorkMngList(LabelWorkDto.LabelWorkMngSearchReq searchReq) { + return labelWorkCoreService.labelWorkMngList(searchReq); + } + + /** + * 작업배정 정보 조회 + * + * @param uuid + * @return + */ + public LabelWorkMngDetail findLabelWorkMngDetail(UUID uuid) { + return labelWorkCoreService.findLabelWorkMngDetail(uuid); + } + + /** + * 변화탐지 셀렉트박스 조회 + * + * @return + */ + public List getChangeDetectYear() { + return labelWorkCoreService.getChangeDetectYear(); + } + + public Page findlabelWorkStateList(LabelWorkDto.WorkerStateSearchReq searchReq) { + + return labelWorkCoreService.findlabelWorkStateList(searchReq); + } +} 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 new file mode 100644 index 00000000..ca31c812 --- /dev/null +++ b/src/main/java/com/kamco/cd/kamcoback/postgres/core/LabelAllocateCoreService.java @@ -0,0 +1,125 @@ +package com.kamco.cd.kamcoback.postgres.core; + +import com.kamco.cd.kamcoback.label.dto.LabelAllocateDto; +import com.kamco.cd.kamcoback.label.dto.LabelAllocateDto.AllocateInfoDto; +import com.kamco.cd.kamcoback.label.dto.LabelAllocateDto.InferenceDetail; +import com.kamco.cd.kamcoback.label.dto.LabelAllocateDto.LabelerDetail; +import com.kamco.cd.kamcoback.label.dto.LabelAllocateDto.LabelingStatDto; +import com.kamco.cd.kamcoback.label.dto.LabelAllocateDto.UserList; +import com.kamco.cd.kamcoback.label.dto.LabelAllocateDto.searchReq; +import com.kamco.cd.kamcoback.label.dto.WorkerStatsDto.ProjectInfo; +import com.kamco.cd.kamcoback.label.dto.WorkerStatsDto.WorkProgressInfo; +import com.kamco.cd.kamcoback.label.dto.WorkerStatsDto.WorkerStatistics; +import com.kamco.cd.kamcoback.postgres.entity.LabelingAssignmentEntity; +import com.kamco.cd.kamcoback.postgres.repository.label.LabelAllocateRepository; +import java.time.LocalDate; +import java.util.List; +import java.util.UUID; +import lombok.RequiredArgsConstructor; +import org.springframework.data.domain.Page; +import org.springframework.stereotype.Service; + +@Service +@RequiredArgsConstructor +public class LabelAllocateCoreService { + + private final LabelAllocateRepository labelAllocateRepository; + + public List fetchNextIds( + Long lastId, Long batchSize, Integer compareYyyy, Integer targetYyyy, Integer stage) { + return labelAllocateRepository.fetchNextIds(lastId, batchSize, compareYyyy, targetYyyy, stage); + } + + public void assignOwner(List ids, String userId, Long analUid) { + labelAllocateRepository.assignOwner(ids, userId, analUid); + } + + public List findAssignedLabelerList(Long analUid) { + return labelAllocateRepository.findAssignedLabelerList(analUid).stream() + .map(LabelingAssignmentEntity::toDto) + .toList(); + } + + public Long findLabelUnAssignedCnt(Integer stage, Integer compareYyyy, Integer targetYyyy) { + return labelAllocateRepository.findLabelUnAssignedCnt(stage, compareYyyy, targetYyyy); + } + + public void assignInspector(UUID assignmentUid, String inspectorUid) { + labelAllocateRepository.assignInspector(assignmentUid, inspectorUid); + } + + public List availUserList(String role) { + return labelAllocateRepository.availUserList(role); + } + + public ProjectInfo findProjectInfo(Long analUid) { + return labelAllocateRepository.findProjectInfo(analUid); + } + + public ProjectInfo findLatestProjectInfo() { + return labelAllocateRepository.findLatestProjectInfo(); + } + + public List findWorkerStatistics( + Long analUid, String workerType, String search, String sortType) { + return labelAllocateRepository.findWorkerStatistics(analUid, workerType, search, 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); + } + + public void assignInspectorBulk(List assignmentUids, String inspectorUid) { + labelAllocateRepository.assignInspectorBulk(assignmentUids, inspectorUid); + } + + public InferenceDetail findInferenceDetail(String uuid) { + return labelAllocateRepository.findInferenceDetail(uuid); + } + + public List fetchNextMoveIds( + Long lastId, + Long batchSize, + Integer compareYyyy, + Integer targetYyyy, + Integer stage, + String userId) { + return labelAllocateRepository.fetchNextMoveIds( + lastId, batchSize, compareYyyy, targetYyyy, stage, userId); + } + + public void assignOwnerMove(List sub, String userId) { + labelAllocateRepository.assignOwnerMove(sub, userId); + } + + public LabelerDetail findLabelerDetail(String userId, String uuid) { + return labelAllocateRepository.findLabelerDetail(userId, uuid); + } + + public Long findMapSheetAnalInferenceUid(Integer compareYyyy, Integer targetYyyy, Integer stage) { + return labelAllocateRepository.findMapSheetAnalInferenceUid(compareYyyy, targetYyyy, stage); + } + + public void insertInspector(Long analUid, String inspector) { + labelAllocateRepository.insertInspector(analUid, inspector); + } + + public Page findLabelerDailyStat( + searchReq searchReq, String uuid, String userId) { + return labelAllocateRepository.findLabelerDailyStat(searchReq, uuid, userId); + } + + public Page findInspectorDailyStat( + searchReq searchReq, String uuid, String userId) { + return labelAllocateRepository.findInspectorDailyStat(searchReq, uuid, userId); + } + + public LabelerDetail findInspectorDetail(String userId, String uuid) { + return labelAllocateRepository.findInspectorDetail(userId, uuid); + } +} diff --git a/src/main/java/com/kamco/cd/kamcoback/postgres/core/LabelWorkCoreService.java b/src/main/java/com/kamco/cd/kamcoback/postgres/core/LabelWorkCoreService.java new file mode 100644 index 00000000..4b236c13 --- /dev/null +++ b/src/main/java/com/kamco/cd/kamcoback/postgres/core/LabelWorkCoreService.java @@ -0,0 +1,60 @@ +package com.kamco.cd.kamcoback.postgres.core; + +import com.kamco.cd.kamcoback.label.dto.LabelWorkDto; +import com.kamco.cd.kamcoback.label.dto.LabelWorkDto.ChangeDetectYear; +import com.kamco.cd.kamcoback.label.dto.LabelWorkDto.LabelWorkMng; +import com.kamco.cd.kamcoback.label.dto.LabelWorkDto.LabelWorkMngDetail; +import com.kamco.cd.kamcoback.label.dto.LabelWorkDto.WorkerState; +import com.kamco.cd.kamcoback.postgres.repository.label.LabelWorkRepository; +import java.util.List; +import java.util.UUID; +import lombok.RequiredArgsConstructor; +import org.springframework.data.domain.Page; +import org.springframework.stereotype.Service; + +@Service +@RequiredArgsConstructor +public class LabelWorkCoreService { + + private final LabelWorkRepository labelWorkRepository; + + /** + * 변화탐지 년도 셀렉트박스 조회 + * + * @return + */ + public List getChangeDetectYear() { + return labelWorkRepository.findChangeDetectYearList().stream() + .map( + e -> + new ChangeDetectYear( + e.getCompareYyyy() + "-" + e.getTargetYyyy(), + e.getCompareYyyy() + "-" + e.getTargetYyyy())) + .toList(); + } + + /** + * 라벨링작업 관리 목록 조회 + * + * @param searchReq + * @return + */ + public Page labelWorkMngList(LabelWorkDto.LabelWorkMngSearchReq searchReq) { + return labelWorkRepository.labelWorkMngList(searchReq); + } + + public Page findlabelWorkStateList(LabelWorkDto.WorkerStateSearchReq searchReq) { + return labelWorkRepository.findlabelWorkStateList(searchReq); + } + ; + + /** + * 작업배정 정보 조회 + * + * @param uuid + * @return + */ + public LabelWorkMngDetail findLabelWorkMngDetail(UUID uuid) { + return labelWorkRepository.findLabelWorkMngDetail(uuid); + } +} diff --git a/src/main/java/com/kamco/cd/kamcoback/postgres/core/MapInkxMngCoreService.java b/src/main/java/com/kamco/cd/kamcoback/postgres/core/MapInkxMngCoreService.java index 67e2dfa3..78aecaa1 100644 --- a/src/main/java/com/kamco/cd/kamcoback/postgres/core/MapInkxMngCoreService.java +++ b/src/main/java/com/kamco/cd/kamcoback/postgres/core/MapInkxMngCoreService.java @@ -9,6 +9,7 @@ import com.kamco.cd.kamcoback.postgres.entity.MapInkx5kEntity; import com.kamco.cd.kamcoback.postgres.repository.scene.MapInkx50kRepository; import com.kamco.cd.kamcoback.postgres.repository.scene.MapInkx5kRepository; import com.kamco.cd.kamcoback.scene.dto.MapInkxMngDto; +import com.kamco.cd.kamcoback.scene.dto.MapInkxMngDto.MapListEntity; import com.kamco.cd.kamcoback.scene.dto.MapInkxMngDto.UseInferReq; import jakarta.persistence.EntityNotFoundException; import jakarta.validation.Valid; @@ -101,4 +102,11 @@ public class MapInkxMngCoreService { return getScene5k.toEntity(); } + + public Page getSceneListByPage( + CommonUseStatus useInference, String searchVal, MapInkxMngDto.searchReq searchReq) { + return mapInkx5kRepository + .getSceneListByPage(useInference, searchVal, searchReq) + .map(MapInkx5kEntity::toDto); + } } 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..ea8e6893 --- /dev/null +++ b/src/main/java/com/kamco/cd/kamcoback/postgres/entity/LabelingAssignmentEntity.java @@ -0,0 +1,70 @@ +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.time.ZonedDateTime; +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; + + @Column(name = "inspect_state") + private String inspectState; + + @Column(name = "work_stat_dttm") + private ZonedDateTime workStatDttm; + + @Column(name = "inspect_stat_dttm") + private ZonedDateTime inspectStatDttm; + + 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(), + this.inspectState, + this.workStatDttm, + this.inspectStatDttm); + } +} diff --git a/src/main/java/com/kamco/cd/kamcoback/postgres/entity/LabelingInspectorEntity.java b/src/main/java/com/kamco/cd/kamcoback/postgres/entity/LabelingInspectorEntity.java new file mode 100644 index 00000000..5da0a685 --- /dev/null +++ b/src/main/java/com/kamco/cd/kamcoback/postgres/entity/LabelingInspectorEntity.java @@ -0,0 +1,33 @@ +package com.kamco.cd.kamcoback.postgres.entity; + +import com.kamco.cd.kamcoback.label.dto.LabelInspectorDto; +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_inspector") +public class LabelingInspectorEntity extends CommonDateEntity { + + @Id + @Column(name = "operator_uid") + private UUID operatorUid; + + @Column(name = "anal_uid") + private Long analUid; + + @Column(name = "inspector_uid") + private String inspectorUid; + + public LabelInspectorDto.Basic toDto() { + return new LabelInspectorDto.Basic( + this.operatorUid, + this.analUid, + this.inspectorUid, + super.getCreatedDate(), + super.getModifiedDate()); + } +} diff --git a/src/main/java/com/kamco/cd/kamcoback/postgres/entity/MapInkx50kEntity.java b/src/main/java/com/kamco/cd/kamcoback/postgres/entity/MapInkx50kEntity.java index 95649a18..df11dee0 100644 --- a/src/main/java/com/kamco/cd/kamcoback/postgres/entity/MapInkx50kEntity.java +++ b/src/main/java/com/kamco/cd/kamcoback/postgres/entity/MapInkx50kEntity.java @@ -1,5 +1,6 @@ package com.kamco.cd.kamcoback.postgres.entity; +import com.kamco.cd.kamcoback.inference.dto.InferenceResultDto.MapSheet; import com.kamco.cd.kamcoback.postgres.CommonDateEntity; import jakarta.persistence.Column; import jakarta.persistence.Entity; @@ -46,4 +47,8 @@ public class MapInkx50kEntity extends CommonDateEntity { this.mapidNo = mapidNo; this.geom = geom; } + + public MapSheet toEntity() { + return new MapSheet(mapidcdNo, mapidNm); + } } diff --git a/src/main/java/com/kamco/cd/kamcoback/postgres/entity/MapInkx5kEntity.java b/src/main/java/com/kamco/cd/kamcoback/postgres/entity/MapInkx5kEntity.java index 80247365..b4d26049 100644 --- a/src/main/java/com/kamco/cd/kamcoback/postgres/entity/MapInkx5kEntity.java +++ b/src/main/java/com/kamco/cd/kamcoback/postgres/entity/MapInkx5kEntity.java @@ -4,6 +4,7 @@ import com.kamco.cd.kamcoback.common.enums.CommonUseStatus; import com.kamco.cd.kamcoback.inference.dto.InferenceResultDto; import com.kamco.cd.kamcoback.inference.dto.InferenceResultDto.MapSheet; import com.kamco.cd.kamcoback.postgres.CommonDateEntity; +import com.kamco.cd.kamcoback.scene.dto.MapInkxMngDto.MapListEntity; import jakarta.persistence.Column; import jakarta.persistence.Entity; import jakarta.persistence.EnumType; @@ -73,4 +74,14 @@ public class MapInkx5kEntity extends CommonDateEntity { public InferenceResultDto.MapSheet toEntity() { return new MapSheet(mapidcdNo, mapidNm); } + + public MapListEntity toDto() { + return MapListEntity.builder() + .scene5k(this.toEntity()) + .scene50k(this.mapInkx50k.toEntity()) + .useInference(useInference) + .createdDttm(super.getCreatedDate()) + .updatedDttm(super.getModifiedDate()) + .build(); + } } 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/entity/MapSheetAnalInferenceEntity.java b/src/main/java/com/kamco/cd/kamcoback/postgres/entity/MapSheetAnalInferenceEntity.java new file mode 100644 index 00000000..195721e1 --- /dev/null +++ b/src/main/java/com/kamco/cd/kamcoback/postgres/entity/MapSheetAnalInferenceEntity.java @@ -0,0 +1,152 @@ +package com.kamco.cd.kamcoback.postgres.entity; + +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.SequenceGenerator; +import jakarta.persistence.Table; +import jakarta.validation.constraints.Size; +import java.time.ZonedDateTime; +import java.util.List; +import java.util.Map; +import java.util.UUID; +import lombok.Getter; +import lombok.Setter; +import org.hibernate.annotations.ColumnDefault; +import org.hibernate.annotations.JdbcTypeCode; +import org.hibernate.type.SqlTypes; + +@Getter +@Setter +@Entity +@Table(name = "tb_map_sheet_anal_inference") +public class MapSheetAnalInferenceEntity { + + @Id + @GeneratedValue( + strategy = GenerationType.SEQUENCE, + generator = "tb_map_sheet_anal_inference_id_gen") + @SequenceGenerator( + name = "tb_map_sheet_anal_inference_id_gen", + sequenceName = "tb_map_sheet_anal_inference_uid", + allocationSize = 1) + @Column(name = "anal_uid", nullable = false) + private Long id; + + @Column(name = "compare_yyyy") + private Integer compareYyyy; + + @Column(name = "target_yyyy") + private Integer targetYyyy; + + @Column(name = "model_uid") + private Long modelUid; + + @Size(max = 100) + @Column(name = "server_ids", length = 100) + private String serverIds; + + @Column(name = "anal_strt_dttm") + private ZonedDateTime analStrtDttm; + + @Column(name = "anal_end_dttm") + private ZonedDateTime analEndDttm; + + @Column(name = "anal_sec") + private Long analSec; + + @Size(max = 20) + @Column(name = "anal_state", length = 20) + private String analState; + + @Size(max = 20) + @Column(name = "gukyuin_used", length = 20) + private String gukyuinUsed; + + @Column(name = "accuracy") + private Double accuracy; + + @Size(max = 255) + @Column(name = "result_url") + private String resultUrl; + + @ColumnDefault("now()") + @Column(name = "created_dttm") + private ZonedDateTime createdDttm; + + @Column(name = "created_uid") + private Long createdUid; + + @ColumnDefault("now()") + @Column(name = "updated_dttm") + private ZonedDateTime updatedDttm; + + @Column(name = "updated_uid") + private Long updatedUid; + + @Size(max = 255) + @Column(name = "anal_title") + private String analTitle; + + @Column(name = "detecting_cnt") + private Long detectingCnt; + + @Column(name = "anal_pred_sec") + private Long analPredSec; + + @Column(name = "model_ver_uid") + private Long modelVerUid; + + @Column(name = "hyper_params") + @JdbcTypeCode(SqlTypes.JSON) + private Map hyperParams; + + @Column(name = "tranning_rate") + private List tranningRate; + + @Column(name = "validation_rate") + private List validationRate; + + @Column(name = "test_rate", length = Integer.MAX_VALUE) + private String testRate; + + @Size(max = 128) + @Column(name = "detecting_description", length = 128) + private String detectingDescription; + + @Size(max = 12) + @Column(name = "base_map_sheet_num", length = 12) + private String baseMapSheetNum; + + @ColumnDefault("gen_random_uuid()") + @Column(name = "uuid") + private UUID uuid; + + @Size(max = 50) + @Column(name = "model_m1_ver", length = 50) + private String modelM1Ver; + + @Size(max = 50) + @Column(name = "model_m2_ver", length = 50) + private String modelM2Ver; + + @Size(max = 50) + @Column(name = "model_m3_ver", length = 50) + private String modelM3Ver; + + @Size(max = 20) + @Column(name = "anal_target_type", length = 20) + private String analTargetType; + + @Column(name = "gukyuin_apply_dttm") + private ZonedDateTime gukyuinApplyDttm; + + @Size(max = 20) + @Column(name = "detection_data_option", length = 20) + private String detectionDataOption; + + @Column(name = "stage") + private Integer stage; +} diff --git a/src/main/java/com/kamco/cd/kamcoback/postgres/entity/MapSheetMngHstEntity.java b/src/main/java/com/kamco/cd/kamcoback/postgres/entity/MapSheetMngHstEntity.java index 2cf1f359..c5482e28 100644 --- a/src/main/java/com/kamco/cd/kamcoback/postgres/entity/MapSheetMngHstEntity.java +++ b/src/main/java/com/kamco/cd/kamcoback/postgres/entity/MapSheetMngHstEntity.java @@ -3,12 +3,9 @@ package com.kamco.cd.kamcoback.postgres.entity; import com.kamco.cd.kamcoback.postgres.CommonDateEntity; import jakarta.persistence.Column; import jakarta.persistence.Entity; -import jakarta.persistence.FetchType; import jakarta.persistence.GeneratedValue; import jakarta.persistence.GenerationType; import jakarta.persistence.Id; -import jakarta.persistence.JoinColumn; -import jakarta.persistence.ManyToOne; import jakarta.persistence.Table; import jakarta.validation.constraints.Size; import java.time.ZonedDateTime; @@ -69,10 +66,11 @@ public class MapSheetMngHstEntity extends CommonDateEntity { private Integer mngYyyy; // 년도 // JPA 연관관계: MapInkx5k 참조 (PK 기반) 소속도엽번호 1:5k + /* @ManyToOne(fetch = FetchType.LAZY) @JoinColumn(name = "map_sheet_code", referencedColumnName = "fid") private MapInkx5kEntity mapInkx5kByCode; - + */ // TODO 1:5k 관련 정보 추후 제거 필요 @Column(name = "map_sheet_num") private String mapSheetNum; // 도엽번호 diff --git a/src/main/java/com/kamco/cd/kamcoback/postgres/repository/Inference/InferenceResultRepositoryImpl.java b/src/main/java/com/kamco/cd/kamcoback/postgres/repository/Inference/InferenceResultRepositoryImpl.java index a1d7afb6..2ea1a66a 100644 --- a/src/main/java/com/kamco/cd/kamcoback/postgres/repository/Inference/InferenceResultRepositoryImpl.java +++ b/src/main/java/com/kamco/cd/kamcoback/postgres/repository/Inference/InferenceResultRepositoryImpl.java @@ -5,7 +5,6 @@ import com.kamco.cd.kamcoback.postgres.entity.QMapSheetAnalDataInferenceEntity; import com.kamco.cd.kamcoback.postgres.entity.QMapSheetAnalDataInferenceGeomEntity; import com.querydsl.jpa.impl.JPAQueryFactory; import jakarta.persistence.EntityManager; -import jakarta.persistence.PersistenceContext; import java.time.ZonedDateTime; import java.util.List; import lombok.RequiredArgsConstructor; @@ -16,8 +15,7 @@ import org.springframework.stereotype.Repository; public class InferenceResultRepositoryImpl implements InferenceResultRepositoryCustom { private final JPAQueryFactory queryFactory; - - @PersistenceContext private final EntityManager em; + private final EntityManager em; /** tb_map_sheet_anal_data_inference */ private final QMapSheetAnalDataInferenceEntity inferenceEntity = @@ -36,22 +34,28 @@ public class InferenceResultRepositoryImpl implements InferenceResultRepositoryC String sql = """ INSERT INTO tb_map_sheet_anal_inference ( + stage, compare_yyyy, target_yyyy, - anal_map_sheet, - stage, - anal_title + anal_title, + detecting_cnt, + created_dttm, + updated_dttm ) SELECT - r.input1 AS compare_yyyy, - r.input2 AS target_yyyy, - r.map_id AS anal_map_sheet, - r.stage, - CONCAT(r.stage ,'_', r.input1 ,'_', r.input2 ,'_', r.map_id) as anal_title - FROM inference_results r - GROUP BY r.stage, r.input1, r.input2, r.map_id - ON CONFLICT (compare_yyyy, target_yyyy, anal_map_sheet, stage) + r.stage, + r.input1 AS compare_yyyy, + r.input2 AS target_yyyy, + CONCAT(r.stage, '_', r.input1, '_', r.input2) AS anal_title, + COUNT(*) AS detecting_cnt, + now(), + now() + FROM inference_results r + GROUP BY r.stage, r.input1, r.input2 + ON CONFLICT (stage, compare_yyyy, target_yyyy) DO UPDATE SET + detecting_cnt = EXCLUDED.detecting_cnt, + anal_title = EXCLUDED.anal_title, updated_dttm = now() """; @@ -72,30 +76,42 @@ public class InferenceResultRepositoryImpl implements InferenceResultRepositoryC String sql = """ INSERT INTO tb_map_sheet_anal_data_inference ( + anal_uid, stage, compare_yyyy, target_yyyy, map_sheet_num, - created_dttm, - updated_dttm, + detecting_cnt, file_created_yn, - detecting_cnt + created_dttm, + updated_dttm ) SELECT + ai.id AS anal_uid, r.stage, r.input1 AS compare_yyyy, r.input2 AS target_yyyy, r.map_id AS map_sheet_num, - now() AS created_dttm, - now() AS updated_dttm, - false AS file_created_yn, - count(*) AS detecting_cnt + COUNT(*) AS detecting_cnt, + false AS file_created_yn, + now(), + now() FROM inference_results r - GROUP BY r.stage, r.input1, r.input2, r.map_id + JOIN tb_map_sheet_anal_inference ai + ON ai.stage = r.stage + AND ai.compare_yyyy = r.input1 + AND ai.target_yyyy = r.input2 + GROUP BY + ai.id, + r.stage, + r.input1, + r.input2, + r.map_id ON CONFLICT (stage, compare_yyyy, target_yyyy, map_sheet_num) DO UPDATE SET - updated_dttm = now(), - detecting_cnt = EXCLUDED.detecting_cnt + anal_uid = EXCLUDED.anal_uid, + detecting_cnt = EXCLUDED.detecting_cnt, + updated_dttm = now() """; return em.createNativeQuery(sql).executeUpdate(); @@ -114,46 +130,70 @@ public class InferenceResultRepositoryImpl implements InferenceResultRepositoryC String sql = """ - INSERT INTO tb_map_sheet_anal_data_inference_geom ( - uuid, stage, cd_prob, compare_yyyy, target_yyyy, map_sheet_num, - class_before_cd, class_before_prob, class_after_cd, class_after_prob, - geom, area, data_uid, created_dttm, updated_dttm, - file_created_yn + INSERT INTO tb_map_sheet_anal_data_inference_geom ( + uuid, + stage, + cd_prob, + compare_yyyy, + target_yyyy, + map_sheet_num, + class_before_cd, + class_before_prob, + class_after_cd, + class_after_prob, + geom, + area, + data_uid, + file_created_yn, + created_dttm, + updated_dttm ) SELECT - x.uuid, x.stage, x.cd_prob, x.compare_yyyy, x.target_yyyy, x.map_sheet_num, - x.class_before_cd, x.class_before_prob, x.class_after_cd, x.class_after_prob, - x.geom, x.area, x.data_uid, x.created_dttm, x.updated_dttm, - false AS file_created_yn + x.uuid, + x.stage, + x.cd_prob, + x.compare_yyyy, + x.target_yyyy, + x.map_sheet_num, + x.class_before_cd, + x.class_before_prob, + x.class_after_cd, + x.class_after_prob, + x.geom, + x.area, + x.data_uid, + false, + x.created_dttm, + x.updated_dttm FROM ( - SELECT DISTINCT ON (r.uuid) - r.uuid, - r.stage, - r.cd_prob, - r.input1 AS compare_yyyy, - r.input2 AS target_yyyy, - r.map_id AS map_sheet_num, - r.before_class AS class_before_cd, - r.before_probability AS class_before_prob, - r.after_class AS class_after_cd, - r.after_probability AS class_after_prob, - CASE - WHEN r.geometry IS NULL THEN NULL - WHEN left(r.geometry, 2) = '01' - THEN ST_SetSRID(ST_GeomFromWKB(decode(r.geometry, 'hex')), 5186) - ELSE ST_GeomFromText(r.geometry, 5186) - END AS geom, - r.area, - di.data_uid, - r.created_dttm, - r.updated_dttm - FROM inference_results r - JOIN tb_map_sheet_anal_data_inference di - ON di.stage = r.stage - AND di.compare_yyyy = r.input1 - AND di.target_yyyy = r.input2 - AND di.map_sheet_num = r.map_id - ORDER BY r.uuid, r.updated_dttm DESC NULLS LAST, r.uid DESC + SELECT DISTINCT ON (r.uuid) + r.uuid, + r.stage, + r.cd_prob, + r.input1 AS compare_yyyy, + r.input2 AS target_yyyy, + r.map_id AS map_sheet_num, + r.before_class AS class_before_cd, + r.before_probability AS class_before_prob, + r.after_class AS class_after_cd, + r.after_probability AS class_after_prob, + CASE + WHEN r.geometry IS NULL THEN NULL + WHEN LEFT(r.geometry, 2) = '01' + THEN ST_SetSRID(ST_GeomFromWKB(decode(r.geometry, 'hex')), 5186) + ELSE ST_GeomFromText(r.geometry, 5186) + END AS geom, + r.area, + di.data_uid, + r.created_dttm, + r.updated_dttm + FROM inference_results r + JOIN tb_map_sheet_anal_data_inference di + ON di.stage = r.stage + AND di.compare_yyyy = r.input1 + AND di.target_yyyy = r.input2 + AND di.map_sheet_num = r.map_id + ORDER BY r.uuid, r.updated_dttm DESC NULLS LAST, r.uid DESC ) x ON CONFLICT (uuid) DO UPDATE SET diff --git a/src/main/java/com/kamco/cd/kamcoback/postgres/repository/label/LabelAllocateRepository.java b/src/main/java/com/kamco/cd/kamcoback/postgres/repository/label/LabelAllocateRepository.java new file mode 100644 index 00000000..976a083e --- /dev/null +++ b/src/main/java/com/kamco/cd/kamcoback/postgres/repository/label/LabelAllocateRepository.java @@ -0,0 +1,8 @@ +package com.kamco.cd.kamcoback.postgres.repository.label; + +import com.kamco.cd.kamcoback.postgres.entity.MapSheetAnalDataInferenceGeomEntity; +import org.springframework.data.jpa.repository.JpaRepository; + +public interface LabelAllocateRepository + extends JpaRepository, + LabelAllocateRepositoryCustom {} 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 new file mode 100644 index 00000000..187ded4d --- /dev/null +++ b/src/main/java/com/kamco/cd/kamcoback/postgres/repository/label/LabelAllocateRepositoryCustom.java @@ -0,0 +1,76 @@ +package com.kamco.cd.kamcoback.postgres.repository.label; + +import com.kamco.cd.kamcoback.label.dto.LabelAllocateDto; +import com.kamco.cd.kamcoback.label.dto.LabelAllocateDto.AllocateInfoDto; +import com.kamco.cd.kamcoback.label.dto.LabelAllocateDto.InferenceDetail; +import com.kamco.cd.kamcoback.label.dto.LabelAllocateDto.LabelerDetail; +import com.kamco.cd.kamcoback.label.dto.LabelAllocateDto.LabelingStatDto; +import com.kamco.cd.kamcoback.label.dto.LabelAllocateDto.UserList; +import com.kamco.cd.kamcoback.label.dto.WorkerStatsDto.ProjectInfo; +import com.kamco.cd.kamcoback.label.dto.WorkerStatsDto.WorkProgressInfo; +import com.kamco.cd.kamcoback.label.dto.WorkerStatsDto.WorkerStatistics; +import com.kamco.cd.kamcoback.postgres.entity.LabelingAssignmentEntity; +import java.time.LocalDate; +import java.util.List; +import java.util.UUID; +import org.springframework.data.domain.Page; + +public interface LabelAllocateRepositoryCustom { + + List fetchNextIds( + Long lastId, Long batchSize, Integer compareYyyy, Integer targetYyyy, Integer stage); + + void assignOwner(List ids, String userId, Long analUid); + + List findAssignedLabelerList(Long analUid); + + Long findLabelUnAssignedCnt(Integer stage, Integer compareYyyy, Integer targetYyyy); + + void assignInspector(UUID assignmentUid, String userId); + + List availUserList(String role); + + // 프로젝트 정보 조회 + ProjectInfo findProjectInfo(Long analUid); + + // 최신 프로젝트 정보 조회 (analUid 없이) + ProjectInfo findLatestProjectInfo(); + + // 작업자 통계 조회 + List findWorkerStatistics( + Long analUid, String workerType, String search, String sortType); + + // 작업 진행 현황 조회 + WorkProgressInfo findWorkProgressInfo(Long analUid); + + // 작업자별 일일 처리량 조회 + Long findDailyProcessedCount(String workerId, String workerType, LocalDate date, Long analUid); + + void assignInspectorBulk(List assignmentUids, String inspectorUid); + + InferenceDetail findInferenceDetail(String uuid); + + List fetchNextMoveIds( + Long lastId, + Long batchSize, + Integer compareYyyy, + Integer targetYyyy, + Integer stage, + String userId); + + void assignOwnerMove(List sub, String userId); + + LabelerDetail findLabelerDetail(String userId, String uuid); + + Long findMapSheetAnalInferenceUid(Integer compareYyyy, Integer targetYyyy, Integer stage); + + void insertInspector(Long analUid, String inspector); + + Page findLabelerDailyStat( + LabelAllocateDto.searchReq searchReq, String uuid, String userId); + + Page findInspectorDailyStat( + LabelAllocateDto.searchReq searchReq, String uuid, String userId); + + LabelerDetail findInspectorDetail(String userId, String uuid); +} 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 new file mode 100644 index 00000000..fa94a7b5 --- /dev/null +++ b/src/main/java/com/kamco/cd/kamcoback/postgres/repository/label/LabelAllocateRepositoryImpl.java @@ -0,0 +1,973 @@ +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.QLabelingInspectorEntity.labelingInspectorEntity; +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.QMapSheetAnalInferenceEntity.mapSheetAnalInferenceEntity; +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.AllocateInfoDto; +import com.kamco.cd.kamcoback.label.dto.LabelAllocateDto.InferenceDetail; +import com.kamco.cd.kamcoback.label.dto.LabelAllocateDto.InspectState; +import com.kamco.cd.kamcoback.label.dto.LabelAllocateDto.LabelState; +import com.kamco.cd.kamcoback.label.dto.LabelAllocateDto.LabelerDetail; +import com.kamco.cd.kamcoback.label.dto.LabelAllocateDto.LabelingStatDto; +import com.kamco.cd.kamcoback.label.dto.LabelAllocateDto.UserList; +import com.kamco.cd.kamcoback.label.dto.WorkerStatsDto.ProjectInfo; +import com.kamco.cd.kamcoback.label.dto.WorkerStatsDto.WorkProgressInfo; +import com.kamco.cd.kamcoback.label.dto.WorkerStatsDto.WorkerStatistics; +import com.kamco.cd.kamcoback.postgres.entity.LabelingAssignmentEntity; +import com.kamco.cd.kamcoback.postgres.entity.MapSheetAnalInferenceEntity; +import com.kamco.cd.kamcoback.postgres.entity.QMemberEntity; +import com.querydsl.core.types.Expression; +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; +import jakarta.persistence.EntityManager; +import jakarta.persistence.EntityNotFoundException; +import jakarta.persistence.PersistenceContext; +import jakarta.transaction.Transactional; +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.domain.Page; +import org.springframework.data.domain.PageImpl; +import org.springframework.data.domain.Pageable; +import org.springframework.stereotype.Repository; + +@Slf4j +@Repository +@RequiredArgsConstructor +public class LabelAllocateRepositoryImpl implements LabelAllocateRepositoryCustom { + + private final JPAQueryFactory queryFactory; + + @PersistenceContext private EntityManager em; + + @Override + public List fetchNextIds( + Long lastId, Long batchSize, Integer compareYyyy, Integer targetYyyy, Integer stage) { + + return queryFactory + .select( + Projections.constructor( + AllocateInfoDto.class, + mapSheetAnalDataInferenceGeomEntity.geoUid, + mapSheetAnalDataInferenceGeomEntity.mapSheetNum)) + .from(mapSheetAnalDataInferenceGeomEntity) + .where( + lastId == null ? null : mapSheetAnalDataInferenceGeomEntity.geoUid.gt(lastId), + mapSheetAnalDataInferenceGeomEntity.pnu.isNotNull(), + mapSheetAnalDataInferenceGeomEntity.compareYyyy.eq(compareYyyy), + mapSheetAnalDataInferenceGeomEntity.targetYyyy.eq(targetYyyy), + mapSheetAnalDataInferenceGeomEntity.stage.eq(stage), + mapSheetAnalDataInferenceGeomEntity.labelState.isNull()) + .orderBy(mapSheetAnalDataInferenceGeomEntity.mapSheetNum.asc()) + .limit(batchSize) + .fetch(); + } + + @Override + public void assignOwner(List ids, String userId, Long analUid) { + + // analUid로 분석 정보 조회 + MapSheetAnalInferenceEntity analEntity = + queryFactory + .selectFrom(mapSheetAnalInferenceEntity) + .where(mapSheetAnalInferenceEntity.id.eq(analUid)) + .fetchOne(); + + if (Objects.isNull(analEntity)) { + throw new EntityNotFoundException("MapSheetAnalInferenceEntity not found for analUid: "); + } + + // data_geom 테이블에 label state 를 ASSIGNED 로 update + List geoUidList = + ids.stream().map(AllocateInfoDto::getGeoUid).filter(Objects::nonNull).toList(); + + queryFactory + .update(mapSheetAnalDataInferenceGeomEntity) + .set(mapSheetAnalDataInferenceGeomEntity.labelState, LabelState.ASSIGNED.getId()) + .set(mapSheetAnalDataInferenceGeomEntity.labelStateDttm, ZonedDateTime.now()) + .where(mapSheetAnalDataInferenceGeomEntity.geoUid.in(geoUidList)) + .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 (AllocateInfoDto info : ids) { + em.createNativeQuery(sql) + .setParameter(1, UUID.randomUUID()) + .setParameter(2, info.getGeoUid()) + .setParameter(3, userId) + .setParameter(4, LabelState.ASSIGNED.getId()) + .setParameter(5, info.getMapSheetNum()) + .setParameter(6, analEntity.getId()) + .executeUpdate(); + } + + em.flush(); + em.clear(); + } + + @Override + public List findAssignedLabelerList(Long analUid) { + // analUid로 분석 정보 조회 + MapSheetAnalInferenceEntity analEntity = + queryFactory + .selectFrom(mapSheetAnalInferenceEntity) + .where(mapSheetAnalInferenceEntity.id.eq(analUid)) + .fetchOne(); + + if (Objects.isNull(analEntity)) { + throw new EntityNotFoundException("mapSheetAnalInferenceEntity not found for analUid: "); + } + + return queryFactory + .selectFrom(labelingAssignmentEntity) + .where( + labelingAssignmentEntity.analUid.eq(analEntity.getId()), + labelingAssignmentEntity.workState.eq(LabelState.ASSIGNED.getId()), + labelingAssignmentEntity.inspectorUid.isNull()) + .orderBy(labelingAssignmentEntity.workerUid.asc()) + .fetch(); + } + + @Override + public Long findLabelUnAssignedCnt(Integer stage, Integer compareYyyy, Integer targetYyyy) { + + return queryFactory + .select(mapSheetAnalDataInferenceGeomEntity.geoUid.count()) + .from(mapSheetAnalDataInferenceGeomEntity) + .where( + mapSheetAnalDataInferenceGeomEntity.pnu.isNotNull(), + mapSheetAnalDataInferenceGeomEntity.compareYyyy.eq(compareYyyy), + mapSheetAnalDataInferenceGeomEntity.targetYyyy.eq(targetYyyy), + mapSheetAnalDataInferenceGeomEntity.stage.eq(stage), + 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(com.kamco.cd.kamcoback.common.enums.StatusType.ACTIVE.getId())) + .orderBy(memberEntity.name.asc()) + .fetch(); + } + + @Override + public List findWorkerStatistics( + Long analUid, String workerType, String search, String sortType) { + + // 작업자 유형에 따른 필드 선택 + StringExpression workerIdField = + "REVIEWER".equals(workerType) + ? labelingAssignmentEntity.inspectorUid + : labelingAssignmentEntity.workerUid; + + BooleanExpression workerCondition = + "REVIEWER".equals(workerType) + ? labelingAssignmentEntity.inspectorUid.isNotNull() + : labelingAssignmentEntity.workerUid.isNotNull(); + + // 검색 조건 (이름 또는 사번으로 검색) + BooleanExpression searchCondition = null; + if (search != null && !search.isEmpty()) { + searchCondition = + memberEntity.name.contains(search).or(memberEntity.employeeNo.contains(search)); + } + + // 완료, 스킵, 남은 작업 계산 + 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(); + + // 기본 통계 조회 쿼리 + BooleanExpression analUidCondition = + analUid != null ? labelingAssignmentEntity.analUid.eq(analUid) : null; + + var baseQuery = + queryFactory + .select( + workerIdField, + memberEntity.name, + workerIdField.count(), + completedSum, + skippedSum, + remainingSum, + labelingAssignmentEntity.stagnationYn.max()) + .from(labelingAssignmentEntity) + .leftJoin(memberEntity) + .on( + "REVIEWER".equals(workerType) + ? memberEntity.employeeNo.eq(labelingAssignmentEntity.inspectorUid) + : memberEntity.employeeNo.eq(labelingAssignmentEntity.workerUid)) + .where(analUidCondition, 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 "COMPLETED_DESC": + baseQuery.orderBy(completedSum.desc()); + break; + case "COMPLETED_ASC": + baseQuery.orderBy(completedSum.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) { + BooleanExpression analUidCondition = + analUid != null ? labelingAssignmentEntity.analUid.eq(analUid) : null; + + // 전체 배정 건수 + Long totalAssigned = + queryFactory + .select(labelingAssignmentEntity.count()) + .from(labelingAssignmentEntity) + .where(analUidCondition) + .fetchOne(); + + // === 라벨링 통계 === + // 라벨링 완료: LABEL_FIN, TEST_ING, DONE (검수 포함) + Long labelingCompleted = + queryFactory + .select(labelingAssignmentEntity.count()) + .from(labelingAssignmentEntity) + .where( + analUidCondition, + labelingAssignmentEntity.workState.in("LABEL_FIN", "TEST_ING", "DONE")) + .fetchOne(); + + // 스킵 건수 + Long skipCount = + queryFactory + .select(labelingAssignmentEntity.count()) + .from(labelingAssignmentEntity) + .where(analUidCondition, labelingAssignmentEntity.workState.eq("SKIP")) + .fetchOne(); + + // 투입된 라벨러 수 + Long labelerCount = + queryFactory + .select(labelingAssignmentEntity.workerUid.countDistinct()) + .from(labelingAssignmentEntity) + .where(analUidCondition, labelingAssignmentEntity.workerUid.isNotNull()) + .fetchOne(); + + // === 검수 통계 === + // 검수 완료: DONE만 + Long inspectionCompleted = + queryFactory + .select(labelingAssignmentEntity.count()) + .from(labelingAssignmentEntity) + .where(analUidCondition, labelingAssignmentEntity.workState.eq("DONE")) + .fetchOne(); + + // 투입된 검수자 수 + Long inspectorCount = + queryFactory + .select(labelingAssignmentEntity.inspectorUid.countDistinct()) + .from(labelingAssignmentEntity) + .where(analUidCondition, labelingAssignmentEntity.inspectorUid.isNotNull()) + .fetchOne(); + + // 남은 작업 건수 계산 + long total = totalAssigned != null ? totalAssigned : 0L; + long labelCompleted = labelingCompleted != null ? labelingCompleted : 0L; + long inspectCompleted = inspectionCompleted != null ? inspectionCompleted : 0L; + long skipped = skipCount != null ? skipCount : 0L; + + long labelingRemaining = total - labelCompleted - skipped; + long inspectionRemaining = total - inspectCompleted - skipped; + + // 진행률 계산 + double labelingRate = total > 0 ? (double) labelCompleted / total * 100 : 0.0; + double inspectionRate = total > 0 ? (double) inspectCompleted / total * 100 : 0.0; + + // 상태 판단 + String labelingStatus = labelingRemaining > 0 ? "진행중" : "완료"; + String inspectionStatus = inspectionRemaining > 0 ? "진행중" : "완료"; + + return WorkProgressInfo.builder() + // 라벨링 + .labelingProgressRate(labelingRate) + .labelingStatus(labelingStatus) + .labelingTotalCount(total) + .labelingCompletedCount(labelCompleted) + .labelingSkipCount(skipped) + .labelingRemainingCount(labelingRemaining) + .labelerCount(labelerCount != null ? labelerCount : 0L) + // 검수 + .inspectionProgressRate(inspectionRate) + .inspectionStatus(inspectionStatus) + .inspectionTotalCount(total) + .inspectionCompletedCount(inspectCompleted) + .inspectionSkipCount(skipped) + .inspectionRemainingCount(inspectionRemaining) + .inspectorCount(inspectorCount != null ? inspectorCount : 0L) + // 레거시 호환 필드 (Deprecated) + .progressRate(labelingRate) + .totalAssignedCount(total) + .completedCount(labelCompleted) + .remainingLabelCount(labelingRemaining) + .remainingInspectCount(inspectionRemaining) + .workStatus(labelingStatus) + .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 = + "REVIEWER".equals(workerType) + ? labelingAssignmentEntity.inspectorUid.eq(workerId) + : labelingAssignmentEntity.workerUid.eq(workerId); + + BooleanExpression analUidCondition = + analUid != null ? labelingAssignmentEntity.analUid.eq(analUid) : null; + + Long count = + queryFactory + .select(labelingAssignmentEntity.count()) + .from(labelingAssignmentEntity) + .where( + analUidCondition, + workerCondition, + labelingAssignmentEntity.workState.in( + LabelState.DONE.getId(), LabelState.SKIP.getId()), + labelingAssignmentEntity.modifiedDate.between(startOfDay, endOfDay)) + .fetchOne(); + + 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(String uuid) { + // analUid로 분석 정보 조회 + MapSheetAnalInferenceEntity analEntity = + queryFactory + .selectFrom(mapSheetAnalInferenceEntity) + .where(mapSheetAnalInferenceEntity.uuid.eq(UUID.fromString(uuid))) + .fetchOne(); + + if (Objects.isNull(analEntity)) { + throw new EntityNotFoundException("MapSheetAnalInferenceEntity not found for 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(analEntity.getId())) + .groupBy( + mapSheetAnalEntity.analTitle, + mapSheetAnalEntity.gukyuinApplyDttm, + mapSheetAnalEntity.detectingCnt) + .fetchOne(); + } + + @Override + public List fetchNextMoveIds( + Long lastId, + Long batchSize, + Integer compareYyyy, + Integer targetYyyy, + Integer stage, + String userId) { + + MapSheetAnalInferenceEntity analEntity = + queryFactory + .selectFrom(mapSheetAnalInferenceEntity) + .where( + mapSheetAnalInferenceEntity.compareYyyy.eq(compareYyyy), + mapSheetAnalInferenceEntity.targetYyyy.eq(targetYyyy), + mapSheetAnalInferenceEntity.stage.eq(stage)) + .fetchOne(); + + if (Objects.isNull(analEntity)) { + throw new EntityNotFoundException("MapSheetAnalInferenceEntity not found for analUid: "); + } + + return queryFactory + .select(labelingAssignmentEntity.inferenceGeomUid) + .from(labelingAssignmentEntity) + .where( + labelingAssignmentEntity.workerUid.eq(userId), + labelingAssignmentEntity.workState.eq(LabelState.ASSIGNED.getId()), + labelingAssignmentEntity.analUid.eq(analEntity.getId()), + lastId == null ? null : labelingAssignmentEntity.inferenceGeomUid.gt(lastId)) + .orderBy(labelingAssignmentEntity.inferenceGeomUid.asc()) + .limit(batchSize) + .fetch(); + } + + @Transactional + @Override + public void assignOwnerMove(List sub, String userId) { + queryFactory + .update(labelingAssignmentEntity) + .set(labelingAssignmentEntity.workerUid, userId) + .where(labelingAssignmentEntity.inferenceGeomUid.in(sub)) + .execute(); + + em.clear(); + } + + @Override + public LabelerDetail findLabelerDetail(String userId, String uuid) { + NumberExpression assignedCnt = + new CaseBuilder() + .when(labelingAssignmentEntity.workState.eq(LabelState.ASSIGNED.getId())) + .then(1L) + .otherwise((Long) null) + .count(); + + NumberExpression skipCnt = + new CaseBuilder() + .when(labelingAssignmentEntity.workState.eq(LabelState.SKIP.getId())) + .then(1L) + .otherwise((Long) null) + .count(); + + NumberExpression completeCnt = + new CaseBuilder() + .when(labelingAssignmentEntity.workState.eq(LabelState.COMPLETE.getId())) + .then(1L) + .otherwise((Long) null) + .count(); + + NumberExpression percent = + new CaseBuilder() + .when(completeCnt.eq(0L)) + .then(0.0) + .otherwise( + Expressions.numberTemplate( + Double.class, + "round({0} / {1}, 2)", + labelingAssignmentEntity.count(), + completeCnt)); + + // analUid로 분석 정보 조회 + MapSheetAnalInferenceEntity analEntity = + queryFactory + .selectFrom(mapSheetAnalInferenceEntity) + .where(mapSheetAnalInferenceEntity.uuid.eq(UUID.fromString(uuid))) + .fetchOne(); + + if (Objects.isNull(analEntity)) { + throw new EntityNotFoundException("MapSheetAnalInferenceEntity not found for analUid: "); + } + + QMemberEntity worker = QMemberEntity.memberEntity; + QMemberEntity inspector = new QMemberEntity("inspector"); + + return queryFactory + .select( + Projections.constructor( + LabelerDetail.class, + worker.userRole, + worker.name, + worker.employeeNo, + assignedCnt, + skipCnt, + completeCnt, + percent, + Expressions.constant(0), // TODO: 순위, 꼭 해야할지? + labelingAssignmentEntity.workStatDttm.min(), + inspector.name.min())) + .from(worker) + .innerJoin(labelingAssignmentEntity) + .on( + worker.employeeNo.eq(labelingAssignmentEntity.workerUid), + labelingAssignmentEntity.analUid.eq(analEntity.getId())) + .leftJoin(inspector) + .on(labelingAssignmentEntity.inspectorUid.eq(inspector.employeeNo)) + .where(worker.employeeNo.eq(userId)) + .groupBy(worker.userRole, worker.name, worker.employeeNo) + .fetchOne(); + } + + @Override + public Long findMapSheetAnalInferenceUid(Integer compareYyyy, Integer targetYyyy, Integer stage) { + return queryFactory + .select(mapSheetAnalInferenceEntity.id) + .from(mapSheetAnalInferenceEntity) + .where( + mapSheetAnalInferenceEntity.compareYyyy.eq(compareYyyy), + mapSheetAnalInferenceEntity.targetYyyy.eq(targetYyyy), + mapSheetAnalInferenceEntity.stage.eq(stage)) + .fetchOne(); + } + + @Override + public void insertInspector(Long analUid, String inspector) { + queryFactory + .insert(labelingInspectorEntity) + .columns( + labelingInspectorEntity.operatorUid, + labelingInspectorEntity.analUid, + labelingInspectorEntity.inspectorUid) + .values(UUID.randomUUID(), analUid, inspector) + .execute(); + + em.flush(); + em.clear(); + } + + @Override + public ProjectInfo findProjectInfo(Long analUid) { + if (analUid == null) { + return null; + } + + var result = + queryFactory + .select( + mapSheetAnalInferenceEntity.compareYyyy, + mapSheetAnalInferenceEntity.targetYyyy, + mapSheetAnalInferenceEntity.stage, + mapSheetAnalInferenceEntity.gukyuinApplyDttm, + mapSheetAnalInferenceEntity.createdDttm, + mapSheetAnalInferenceEntity.uuid) + .from(mapSheetAnalInferenceEntity) + .where(mapSheetAnalInferenceEntity.id.eq(analUid)) + .fetchOne(); + + if (result == null) { + return null; + } + + Integer compareYyyy = result.get(mapSheetAnalInferenceEntity.compareYyyy); + Integer targetYyyy = result.get(mapSheetAnalInferenceEntity.targetYyyy); + Integer stage = result.get(mapSheetAnalInferenceEntity.stage); + ZonedDateTime gukyuinApplyDttm = result.get(mapSheetAnalInferenceEntity.gukyuinApplyDttm); + ZonedDateTime createdDttm = result.get(mapSheetAnalInferenceEntity.createdDttm); + UUID uuid = result.get(mapSheetAnalInferenceEntity.uuid); + + // 변화탐지년도 생성 + String detectionYear = + (compareYyyy != null && targetYyyy != null) ? compareYyyy + "-" + targetYyyy : null; + + // 회차를 stage 컬럼에서 가져옴 + String round = stage != null ? String.valueOf(stage) : null; + + return ProjectInfo.builder() + .detectionYear(detectionYear) + .stage(round) + .gukyuinApplyDttm(gukyuinApplyDttm) + .startDttm(createdDttm) + .uuid(uuid != null ? uuid.toString() : null) + .build(); + } + + @Override + public ProjectInfo findLatestProjectInfo() { + // 최신 target_yyyy를 기준으로 프로젝트 정보 조회 + var result = + queryFactory + .select( + mapSheetAnalInferenceEntity.compareYyyy, + mapSheetAnalInferenceEntity.targetYyyy, + mapSheetAnalInferenceEntity.stage, + mapSheetAnalInferenceEntity.gukyuinApplyDttm, + mapSheetAnalInferenceEntity.createdDttm, + mapSheetAnalInferenceEntity.uuid) + .from(mapSheetAnalInferenceEntity) + .orderBy( + mapSheetAnalInferenceEntity.targetYyyy.desc(), + mapSheetAnalInferenceEntity.compareYyyy.desc(), + mapSheetAnalInferenceEntity.createdDttm.desc()) + .limit(1) + .fetchOne(); + + if (result == null) { + return null; + } + + Integer compareYyyy = result.get(mapSheetAnalInferenceEntity.compareYyyy); + Integer targetYyyy = result.get(mapSheetAnalInferenceEntity.targetYyyy); + Integer stage = result.get(mapSheetAnalInferenceEntity.stage); + ZonedDateTime gukyuinApplyDttm = result.get(mapSheetAnalInferenceEntity.gukyuinApplyDttm); + ZonedDateTime createdDttm = result.get(mapSheetAnalInferenceEntity.createdDttm); + UUID uuid = result.get(mapSheetAnalInferenceEntity.uuid); + + // 변화탐지년도 생성 + String detectionYear = + (compareYyyy != null && targetYyyy != null) ? compareYyyy + "-" + targetYyyy : null; + + // 회차를 stage 컬럼에서 가져옴 + String round = stage != null ? String.valueOf(stage) : null; + + return ProjectInfo.builder() + .detectionYear(detectionYear) + .stage(round) + .gukyuinApplyDttm(gukyuinApplyDttm) + .startDttm(createdDttm) + .uuid(uuid != null ? uuid.toString() : null) + .build(); + } + + @Override + public Page findLabelerDailyStat( + LabelAllocateDto.searchReq searchReq, String uuid, String userId) { + // 날짜 포맷 + Expression workDate = + Expressions.stringTemplate( + "TO_CHAR({0}, 'YYYY-MM-DD')", labelingAssignmentEntity.workStatDttm); + + // 날짜별 전체 건수 + Expression dailyTotalCnt = Expressions.numberTemplate(Long.class, "COUNT(*)"); + + // ⭐ 전체 기간 총 건수 (윈도우 함수) + Expression totalCnt = Expressions.numberTemplate(Long.class, "SUM(COUNT(*)) OVER ()"); + + // 상태별 카운트 (Postgres FILTER 사용) + Expression assignedCnt = + Expressions.numberTemplate( + Long.class, + "COUNT(*) FILTER (WHERE {0} = 'ASSIGNED')", + labelingAssignmentEntity.workState); + + Expression skipCnt = + Expressions.numberTemplate( + Long.class, "COUNT(*) FILTER (WHERE {0} = 'SKIP')", labelingAssignmentEntity.workState); + + Expression completeCnt = + Expressions.numberTemplate( + Long.class, + "COUNT(*) FILTER (WHERE {0} = 'COMPLETE')", + labelingAssignmentEntity.workState); + + Expression remainCnt = + Expressions.numberTemplate(Long.class, "({0} - {1} - {2})", totalCnt, skipCnt, completeCnt); + + // analUid로 분석 정보 조회 + MapSheetAnalInferenceEntity analEntity = + queryFactory + .selectFrom(mapSheetAnalInferenceEntity) + .where(mapSheetAnalInferenceEntity.uuid.eq(UUID.fromString(uuid))) + .fetchOne(); + + if (Objects.isNull(analEntity)) { + throw new EntityNotFoundException("MapSheetAnalInferenceEntity not found for analUid: "); + } + + Pageable pageable = searchReq.toPageable(); + List foundContent = + queryFactory + .select( + Projections.constructor( + LabelingStatDto.class, + workDate, + dailyTotalCnt, + totalCnt, // ⭐ 전체 일자 배정 건수 + assignedCnt, + skipCnt, + completeCnt, + remainCnt)) + .from(labelingAssignmentEntity) + .where( + labelingAssignmentEntity.workerUid.eq(userId), + labelingAssignmentEntity.analUid.eq(analEntity.getId())) + .groupBy(workDate) + .orderBy(labelingAssignmentEntity.workStatDttm.min().asc()) + .offset(pageable.getOffset()) + .limit(pageable.getPageSize()) + .fetch(); + + Long countQuery = + queryFactory + .select(workDate) + .from(labelingAssignmentEntity) + .where( + labelingAssignmentEntity.workerUid.eq(userId), + labelingAssignmentEntity.analUid.eq(analEntity.getId())) + .distinct() + .fetch() + .stream() + .count(); + + return new PageImpl<>(foundContent, pageable, countQuery); + } + + @Override + public Page findInspectorDailyStat( + LabelAllocateDto.searchReq searchReq, String uuid, String userId) { + // 날짜 포맷 + Expression workDate = + Expressions.stringTemplate( + "TO_CHAR({0}, 'YYYY-MM-DD')", labelingAssignmentEntity.inspectStatDttm); + + // 날짜별 전체 건수 + Expression dailyTotalCnt = Expressions.numberTemplate(Long.class, "COUNT(*)"); + + // ⭐ 전체 기간 총 건수 (윈도우 함수) + Expression totalCnt = Expressions.numberTemplate(Long.class, "SUM(COUNT(*)) OVER ()"); + + // 상태별 카운트 (Postgres FILTER 사용) + Expression assignedCnt = + Expressions.numberTemplate( + Long.class, + "COUNT(*) FILTER (WHERE {0} = 'UNCONFIRM')", + labelingAssignmentEntity.inspectState); + + Expression skipCnt = + Expressions.numberTemplate( + Long.class, + "COUNT(*) FILTER (WHERE {0} = 'EXCEPT')", + labelingAssignmentEntity.inspectState); + + Expression completeCnt = + Expressions.numberTemplate( + Long.class, + "COUNT(*) FILTER (WHERE {0} = 'COMPLETE')", + labelingAssignmentEntity.inspectState); + + Expression remainCnt = + Expressions.numberTemplate(Long.class, "({0} - {1} - {2})", totalCnt, skipCnt, completeCnt); + + // analUid로 분석 정보 조회 + MapSheetAnalInferenceEntity analEntity = + queryFactory + .selectFrom(mapSheetAnalInferenceEntity) + .where(mapSheetAnalInferenceEntity.uuid.eq(UUID.fromString(uuid))) + .fetchOne(); + + if (Objects.isNull(analEntity)) { + throw new EntityNotFoundException("MapSheetAnalInferenceEntity not found for analUid: "); + } + + Pageable pageable = searchReq.toPageable(); + List foundContent = + queryFactory + .select( + Projections.constructor( + LabelingStatDto.class, + workDate, + dailyTotalCnt, + totalCnt, // ⭐ 전체 일자 배정 건수 + assignedCnt, + skipCnt, + completeCnt, + remainCnt)) + .from(labelingAssignmentEntity) + .where( + labelingAssignmentEntity.inspectorUid.eq(userId), + labelingAssignmentEntity.analUid.eq(analEntity.getId())) + .groupBy(workDate) + .orderBy(labelingAssignmentEntity.inspectStatDttm.min().asc()) + .offset(pageable.getOffset()) + .limit(pageable.getPageSize()) + .fetch(); + + Long countQuery = + queryFactory + .select(workDate) + .from(labelingAssignmentEntity) + .where( + labelingAssignmentEntity.inspectorUid.eq(userId), + labelingAssignmentEntity.analUid.eq(analEntity.getId())) + .distinct() + .fetch() + .stream() + .count(); + + return new PageImpl<>(foundContent, pageable, countQuery); + } + + @Override + public LabelerDetail findInspectorDetail(String userId, String uuid) { + NumberExpression assignedCnt = + new CaseBuilder() + .when(labelingAssignmentEntity.inspectState.eq(InspectState.UNCONFIRM.getId())) + .then(1L) + .otherwise((Long) null) + .count(); + + NumberExpression skipCnt = + new CaseBuilder() + .when(labelingAssignmentEntity.inspectState.eq(InspectState.EXCEPT.getId())) + .then(1L) + .otherwise((Long) null) + .count(); + + NumberExpression completeCnt = + new CaseBuilder() + .when(labelingAssignmentEntity.inspectState.eq(InspectState.COMPLETE.getId())) + .then(1L) + .otherwise((Long) null) + .count(); + + NumberExpression percent = + new CaseBuilder() + .when(completeCnt.eq(0L)) + .then(0.0) + .otherwise( + Expressions.numberTemplate( + Double.class, + "round({0} / {1}, 2)", + labelingAssignmentEntity.count(), + completeCnt)); + + // analUid로 분석 정보 조회 + MapSheetAnalInferenceEntity analEntity = + queryFactory + .selectFrom(mapSheetAnalInferenceEntity) + .where(mapSheetAnalInferenceEntity.uuid.eq(UUID.fromString(uuid))) + .fetchOne(); + + if (Objects.isNull(analEntity)) { + throw new EntityNotFoundException("MapSheetAnalInferenceEntity not found for analUid: "); + } + + QMemberEntity inspector = QMemberEntity.memberEntity; + QMemberEntity worker = new QMemberEntity("worker"); + + return queryFactory + .select( + Projections.constructor( + LabelerDetail.class, + inspector.userRole, + inspector.name, + inspector.employeeNo, + assignedCnt, + skipCnt, + completeCnt, + percent, + Expressions.constant(0), // TODO: 순위, 꼭 해야할지? + labelingAssignmentEntity.inspectStatDttm.min(), + worker.name.min())) + .from(inspector) + .innerJoin(labelingAssignmentEntity) + .on( + inspector.employeeNo.eq(labelingAssignmentEntity.inspectorUid), + labelingAssignmentEntity.analUid.eq(analEntity.getId())) + .leftJoin(worker) + .on(labelingAssignmentEntity.workerUid.eq(worker.employeeNo)) + .where(inspector.employeeNo.eq(userId)) + .groupBy(inspector.userRole, inspector.name, inspector.employeeNo) + .fetchOne(); + } +} diff --git a/src/main/java/com/kamco/cd/kamcoback/postgres/repository/label/LabelWorkRepository.java b/src/main/java/com/kamco/cd/kamcoback/postgres/repository/label/LabelWorkRepository.java new file mode 100644 index 00000000..9cbad5b0 --- /dev/null +++ b/src/main/java/com/kamco/cd/kamcoback/postgres/repository/label/LabelWorkRepository.java @@ -0,0 +1,7 @@ +package com.kamco.cd.kamcoback.postgres.repository.label; + +import com.kamco.cd.kamcoback.postgres.entity.MapSheetAnalDataInferenceGeomEntity; +import org.springframework.data.jpa.repository.JpaRepository; + +public interface LabelWorkRepository + extends JpaRepository, LabelWorkRepositoryCustom {} diff --git a/src/main/java/com/kamco/cd/kamcoback/postgres/repository/label/LabelWorkRepositoryCustom.java b/src/main/java/com/kamco/cd/kamcoback/postgres/repository/label/LabelWorkRepositoryCustom.java new file mode 100644 index 00000000..baf773e7 --- /dev/null +++ b/src/main/java/com/kamco/cd/kamcoback/postgres/repository/label/LabelWorkRepositoryCustom.java @@ -0,0 +1,21 @@ +package com.kamco.cd.kamcoback.postgres.repository.label; + +import com.kamco.cd.kamcoback.label.dto.LabelWorkDto; +import com.kamco.cd.kamcoback.label.dto.LabelWorkDto.LabelWorkMng; +import com.kamco.cd.kamcoback.label.dto.LabelWorkDto.LabelWorkMngDetail; +import com.kamco.cd.kamcoback.label.dto.LabelWorkDto.WorkerState; +import com.kamco.cd.kamcoback.postgres.entity.MapSheetAnalInferenceEntity; +import java.util.List; +import java.util.UUID; +import org.springframework.data.domain.Page; + +public interface LabelWorkRepositoryCustom { + + List findChangeDetectYearList(); + + public Page labelWorkMngList(LabelWorkDto.LabelWorkMngSearchReq searchReq); + + LabelWorkMngDetail findLabelWorkMngDetail(UUID uuid); + + Page findlabelWorkStateList(LabelWorkDto.WorkerStateSearchReq searchReq); +} diff --git a/src/main/java/com/kamco/cd/kamcoback/postgres/repository/label/LabelWorkRepositoryImpl.java b/src/main/java/com/kamco/cd/kamcoback/postgres/repository/label/LabelWorkRepositoryImpl.java new file mode 100644 index 00000000..c4010415 --- /dev/null +++ b/src/main/java/com/kamco/cd/kamcoback/postgres/repository/label/LabelWorkRepositoryImpl.java @@ -0,0 +1,370 @@ +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.QMapSheetAnalDataInferenceEntity.mapSheetAnalDataInferenceEntity; +import static com.kamco.cd.kamcoback.postgres.entity.QMapSheetAnalDataInferenceGeomEntity.mapSheetAnalDataInferenceGeomEntity; +import static com.kamco.cd.kamcoback.postgres.entity.QMapSheetAnalInferenceEntity.mapSheetAnalInferenceEntity; +import static com.kamco.cd.kamcoback.postgres.entity.QMemberEntity.memberEntity; + +import com.kamco.cd.kamcoback.label.dto.LabelWorkDto; +import com.kamco.cd.kamcoback.label.dto.LabelWorkDto.LabelWorkMng; +import com.kamco.cd.kamcoback.label.dto.LabelWorkDto.LabelWorkMngDetail; +import com.kamco.cd.kamcoback.label.dto.LabelWorkDto.WorkerState; +import com.kamco.cd.kamcoback.postgres.entity.MapSheetAnalDataGeomEntity; +import com.kamco.cd.kamcoback.postgres.entity.MapSheetAnalInferenceEntity; +import com.querydsl.core.BooleanBuilder; +import com.querydsl.core.types.Projections; +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.JPAExpressions; +import com.querydsl.jpa.impl.JPAQueryFactory; +import jakarta.persistence.EntityManager; +import jakarta.persistence.PersistenceContext; +import java.time.LocalDate; +import java.time.format.DateTimeFormatter; +import java.util.List; +import java.util.UUID; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.StringUtils; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageImpl; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Pageable; +import org.springframework.data.jpa.repository.support.QuerydslRepositorySupport; +import org.springframework.stereotype.Repository; + +@Slf4j +@Repository +public class LabelWorkRepositoryImpl extends QuerydslRepositorySupport + implements LabelWorkRepositoryCustom { + + private final JPAQueryFactory queryFactory; + private final StringExpression NULL_STRING = Expressions.stringTemplate("cast(null as text)"); + + @PersistenceContext private EntityManager em; + + public LabelWorkRepositoryImpl(JPAQueryFactory queryFactory) { + super(MapSheetAnalDataGeomEntity.class); + this.queryFactory = queryFactory; + } + + /** + * 변화탐지 년도 셀렉트박스 조회 + * + * @return + */ + @Override + public List findChangeDetectYearList() { + return queryFactory + .selectFrom(mapSheetAnalInferenceEntity) + .where( + mapSheetAnalInferenceEntity.id.in( + JPAExpressions.select(mapSheetAnalInferenceEntity.id.min()) + .from(mapSheetAnalInferenceEntity) + .groupBy( + mapSheetAnalInferenceEntity.compareYyyy, + mapSheetAnalInferenceEntity.targetYyyy))) + .orderBy( + mapSheetAnalInferenceEntity.compareYyyy.asc(), + mapSheetAnalInferenceEntity.targetYyyy.asc()) + .fetch(); + } + + /** + * 라벨링 작업관리 목록 조회 + * + * @param searchReq + * @return + */ + @Override + public Page labelWorkMngList(LabelWorkDto.LabelWorkMngSearchReq searchReq) { + + Pageable pageable = PageRequest.of(searchReq.getPage(), searchReq.getSize()); + BooleanBuilder whereBuilder = new BooleanBuilder(); + BooleanBuilder whereSubDataBuilder = new BooleanBuilder(); + BooleanBuilder whereSubBuilder = new BooleanBuilder(); + + if (StringUtils.isNotBlank(searchReq.getDetectYear())) { + String[] years = searchReq.getDetectYear().split("-"); + + if (years.length == 2) { + Integer compareYear = Integer.valueOf(years[0]); + Integer targetYear = Integer.valueOf(years[1]); + + whereBuilder.and( + mapSheetAnalDataInferenceEntity + .compareYyyy + .eq(compareYear) + .and(mapSheetAnalDataInferenceEntity.targetYyyy.eq(targetYear))); + } + } + + whereSubDataBuilder.and( + mapSheetAnalInferenceEntity.id.eq(mapSheetAnalDataInferenceEntity.analUid)); + + whereSubBuilder.and( + mapSheetAnalDataInferenceGeomEntity.dataUid.eq(mapSheetAnalDataInferenceEntity.id)); + + if (searchReq.getStrtDttm() != null + && !searchReq.getStrtDttm().isEmpty() + && searchReq.getEndDttm() != null + && !searchReq.getEndDttm().isEmpty()) { + whereSubBuilder.and( + Expressions.stringTemplate( + "to_char({0}, 'YYYYMMDD')", mapSheetAnalDataInferenceGeomEntity.labelStateDttm) + .between(searchReq.getStrtDttm(), searchReq.getEndDttm())); + } + + List foundContent = + queryFactory + .select( + Projections.constructor( + LabelWorkMng.class, + mapSheetAnalInferenceEntity.uuid, + mapSheetAnalInferenceEntity.compareYyyy, + mapSheetAnalInferenceEntity.targetYyyy, + mapSheetAnalInferenceEntity.stage, + mapSheetAnalDataInferenceEntity.createdDttm.min(), + mapSheetAnalDataInferenceGeomEntity.dataUid.count(), + new CaseBuilder() + .when( + mapSheetAnalDataInferenceGeomEntity + .pnu + .isNotNull() + .and(mapSheetAnalDataInferenceGeomEntity.pnu.ne(0L))) + .then(1L) + .otherwise(0L) + .sum(), + new CaseBuilder() + .when(mapSheetAnalDataInferenceGeomEntity.labelState.eq("ASSIGNED")) + .then(1L) + .otherwise(0L) + .sum(), + new CaseBuilder() + .when(mapSheetAnalDataInferenceGeomEntity.labelState.eq("STOP")) + .then(1L) + .otherwise(0L) + .sum(), + new CaseBuilder() + .when(mapSheetAnalDataInferenceGeomEntity.labelState.eq("LABEL_ING")) + .then(1L) + .otherwise(0L) + .sum(), + new CaseBuilder() + .when(mapSheetAnalDataInferenceGeomEntity.labelState.eq("LABEL_COMPLETE")) + .then(1L) + .otherwise(0L) + .sum(), + mapSheetAnalDataInferenceGeomEntity.labelStateDttm.min())) + .from(mapSheetAnalInferenceEntity) + .innerJoin(mapSheetAnalDataInferenceEntity) + .on(whereSubDataBuilder) + .innerJoin(mapSheetAnalDataInferenceGeomEntity) + .on(whereSubBuilder) + .where(whereBuilder) + .groupBy( + mapSheetAnalInferenceEntity.uuid, + mapSheetAnalInferenceEntity.compareYyyy, + mapSheetAnalInferenceEntity.targetYyyy, + mapSheetAnalInferenceEntity.stage) + .orderBy( + mapSheetAnalInferenceEntity.targetYyyy.desc(), + mapSheetAnalInferenceEntity.compareYyyy.desc(), + mapSheetAnalInferenceEntity.stage.desc()) + .offset(pageable.getOffset()) + .limit(pageable.getPageSize()) + .fetch(); + + /* + Long countQuery = + queryFactory + .select(mapSheetAnalDataInferenceEntity.count()) + .from(mapSheetAnalDataInferenceEntity) + .leftJoin(mapSheetAnalDataInferenceGeomEntity) + .on(whereSubBuilder) + .where(whereBuilder) + .groupBy( + mapSheetAnalDataInferenceEntity.compareYyyy, + mapSheetAnalDataInferenceEntity.targetYyyy, + mapSheetAnalDataInferenceEntity.stage + ) + .fetchOne(); + + */ + + Long total = + queryFactory + .select(mapSheetAnalInferenceEntity.uuid.countDistinct()) + .from(mapSheetAnalInferenceEntity) + .innerJoin(mapSheetAnalDataInferenceEntity) + .on(whereSubDataBuilder) + .innerJoin(mapSheetAnalDataInferenceGeomEntity) + .on(whereSubBuilder) + .where(whereBuilder) + .fetchOne(); + + return new PageImpl<>(foundContent, pageable, total); + } + + @Override + public Page findlabelWorkStateList(LabelWorkDto.WorkerStateSearchReq searchReq) { + Pageable pageable = PageRequest.of(searchReq.getPage(), searchReq.getSize()); + BooleanBuilder whereBuilder = new BooleanBuilder(); + BooleanBuilder whereSubBuilder = new BooleanBuilder(); + + LocalDate threeDaysAgo = LocalDate.now().minusDays(3); + String s3 = threeDaysAgo.format(DateTimeFormatter.ofPattern("YYYY-MM-DD")); + + LocalDate twoDaysAgo = LocalDate.now().minusDays(2); + String s2 = twoDaysAgo.format(DateTimeFormatter.ofPattern("YYYY-MM-DD")); + + LocalDate oneDaysAgo = LocalDate.now().minusDays(1); + String s1 = oneDaysAgo.format(DateTimeFormatter.ofPattern("YYYY-MM-DD")); + + if (searchReq.getUserRole() != null && !searchReq.getUserRole().isEmpty()) { + whereSubBuilder.and(memberEntity.userRole.eq(searchReq.getUserRole())); + } + + if (searchReq.getSearchVal() != null && !searchReq.getSearchVal().isEmpty()) { + whereSubBuilder.and( + Expressions.stringTemplate("{0}", memberEntity.userId) + .likeIgnoreCase("%" + searchReq.getSearchVal() + "%") + .or( + Expressions.stringTemplate("{0}", memberEntity.name) + .likeIgnoreCase("%" + searchReq.getSearchVal() + "%"))); + } + + whereSubBuilder.and(labelingAssignmentEntity.workerUid.eq(memberEntity.userId)); + + List foundContent = + queryFactory + .select( + Projections.constructor( + WorkerState.class, + memberEntity.userRole, + memberEntity.name, + memberEntity.userId, + labelingAssignmentEntity.workerUid.count().as("assignedCnt"), + new CaseBuilder() + .when(labelingAssignmentEntity.workState.eq("DONE")) + .then(1L) + .otherwise(0L) + .sum() + .as("doneCnt"), + new CaseBuilder() + .when(labelingAssignmentEntity.workState.eq("SKIP")) + .then(1L) + .otherwise(0L) + .sum() + .as("skipCnt"), + new CaseBuilder() + .when( + labelingAssignmentEntity + .workState + .eq("DONE") + .and( + Expressions.stringTemplate( + "to_char({0}, 'YYYY-MM-DD')", + labelingAssignmentEntity.modifiedDate) + .eq(s3))) + .then(1L) + .otherwise(0L) + .sum() + .as("day3AgoDoneCnt"), + new CaseBuilder() + .when( + labelingAssignmentEntity + .workState + .eq("DONE") + .and( + Expressions.stringTemplate( + "to_char({0}, 'YYYY-MM-DD')", + labelingAssignmentEntity.modifiedDate) + .eq(s2))) + .then(1L) + .otherwise(0L) + .sum() + .as("day2AgoDoneCnt"), + new CaseBuilder() + .when( + labelingAssignmentEntity + .workState + .eq("DONE") + .and( + Expressions.stringTemplate( + "to_char({0}, 'YYYY-MM-DD')", + labelingAssignmentEntity.modifiedDate) + .eq(s1))) + .then(1L) + .otherwise(0L) + .sum() + .as("day1AgoDoneCnt"))) + .from(labelingAssignmentEntity) + .innerJoin(memberEntity) + .on(whereSubBuilder) + .where(whereBuilder) + .groupBy(memberEntity.userRole, memberEntity.name, memberEntity.userId) + .offset(pageable.getOffset()) + .limit(pageable.getPageSize()) + .fetch(); + + Long countQuery = + queryFactory + .select(labelingAssignmentEntity.workerUid.countDistinct()) + .from(labelingAssignmentEntity) + .innerJoin(memberEntity) + .on(whereSubBuilder) + .where(whereBuilder) + // .groupBy(memberEntity.userRole, memberEntity.name, memberEntity.userId) + .fetchOne(); + + return new PageImpl<>(foundContent, pageable, countQuery); + } + + /** + * 작업배정 상세조회 + * + * @param uuid + * @return + */ + @Override + public LabelWorkMngDetail findLabelWorkMngDetail(UUID uuid) { + + NumberExpression labelTotCnt = mapSheetAnalDataInferenceGeomEntity.geoUid.count(); + NumberExpression labelerCnt = labelingAssignmentEntity.workerUid.count(); + NumberExpression reviewerCnt = labelingAssignmentEntity.inspectorUid.count(); + + return queryFactory + .select( + Projections.constructor( + LabelWorkMngDetail.class, + mapSheetAnalInferenceEntity + .compareYyyy + .stringValue() + .concat("-") + .concat(mapSheetAnalInferenceEntity.targetYyyy.stringValue()), + mapSheetAnalInferenceEntity.stage, + mapSheetAnalInferenceEntity.createdDttm, + labelTotCnt, + labelerCnt, + reviewerCnt)) + .from(mapSheetAnalInferenceEntity) + .leftJoin(mapSheetAnalDataInferenceEntity) + .on(mapSheetAnalInferenceEntity.id.eq(mapSheetAnalDataInferenceEntity.analUid)) + .leftJoin(mapSheetAnalDataInferenceGeomEntity) + .on(mapSheetAnalDataInferenceEntity.id.eq(mapSheetAnalDataInferenceGeomEntity.dataUid)) + .leftJoin(labelingAssignmentEntity) + .on( + mapSheetAnalDataInferenceGeomEntity.geoUid.eq( + labelingAssignmentEntity.inferenceGeomUid)) + .where(mapSheetAnalInferenceEntity.uuid.eq(uuid)) + .groupBy( + mapSheetAnalInferenceEntity.compareYyyy, + mapSheetAnalInferenceEntity.targetYyyy, + mapSheetAnalInferenceEntity.stage, + mapSheetAnalInferenceEntity.createdDttm) + .fetchOne(); + } +} diff --git a/src/main/java/com/kamco/cd/kamcoback/postgres/repository/mapsheet/MapSheetMngFileCheckerRepositoryImpl.java b/src/main/java/com/kamco/cd/kamcoback/postgres/repository/mapsheet/MapSheetMngFileCheckerRepositoryImpl.java index b23d9f47..28a7f9dc 100644 --- a/src/main/java/com/kamco/cd/kamcoback/postgres/repository/mapsheet/MapSheetMngFileCheckerRepositoryImpl.java +++ b/src/main/java/com/kamco/cd/kamcoback/postgres/repository/mapsheet/MapSheetMngFileCheckerRepositoryImpl.java @@ -8,7 +8,6 @@ import com.kamco.cd.kamcoback.postgres.entity.MapSheetMngHstEntity; import com.querydsl.core.BooleanBuilder; import com.querydsl.core.types.Projections; 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; import jakarta.validation.Valid; @@ -76,8 +75,11 @@ public class MapSheetMngFileCheckerRepositoryImpl extends QuerydslRepositorySupp return new PageImpl<>(foundContent, pageable, countQuery); } + /* private NumberExpression rowNum() { return Expressions.numberTemplate( Integer.class, "row_number() over(order by {0} desc)", mapSheetMngHstEntity.createdDate); } + + */ } diff --git a/src/main/java/com/kamco/cd/kamcoback/postgres/repository/mapsheet/MapSheetMngRepositoryImpl.java b/src/main/java/com/kamco/cd/kamcoback/postgres/repository/mapsheet/MapSheetMngRepositoryImpl.java index fad395a7..6b4bc67f 100644 --- a/src/main/java/com/kamco/cd/kamcoback/postgres/repository/mapsheet/MapSheetMngRepositoryImpl.java +++ b/src/main/java/com/kamco/cd/kamcoback/postgres/repository/mapsheet/MapSheetMngRepositoryImpl.java @@ -1,6 +1,5 @@ package com.kamco.cd.kamcoback.postgres.repository.mapsheet; -import static com.kamco.cd.kamcoback.postgres.entity.QMapInkx50kEntity.mapInkx50kEntity; import static com.kamco.cd.kamcoback.postgres.entity.QMapInkx5kEntity.mapInkx5kEntity; import static com.kamco.cd.kamcoback.postgres.entity.QMapSheetMngEntity.mapSheetMngEntity; import static com.kamco.cd.kamcoback.postgres.entity.QMapSheetMngFileEntity.mapSheetMngFileEntity; @@ -394,9 +393,8 @@ public class MapSheetMngRepositoryImpl extends QuerydslRepositorySupport queryFactory .select(mapSheetMngHstEntity.hstUid.count()) .from(mapSheetMngHstEntity) - .innerJoin(mapInkx5kEntity, mapSheetMngHstEntity.mapInkx5kByCode) - .fetchJoin() - .leftJoin(mapInkx5kEntity.mapInkx50k, mapInkx50kEntity) + .innerJoin(mapInkx5kEntity) + .on(mapSheetMngHstEntity.mapSheetNum.eq(mapInkx5kEntity.mapidcdNo)) .where(whereBuilder) .fetchOne(); diff --git a/src/main/java/com/kamco/cd/kamcoback/postgres/repository/scene/MapInkx5kRepositoryCustom.java b/src/main/java/com/kamco/cd/kamcoback/postgres/repository/scene/MapInkx5kRepositoryCustom.java index 50bdf94f..ee971c5c 100644 --- a/src/main/java/com/kamco/cd/kamcoback/postgres/repository/scene/MapInkx5kRepositoryCustom.java +++ b/src/main/java/com/kamco/cd/kamcoback/postgres/repository/scene/MapInkx5kRepositoryCustom.java @@ -4,6 +4,7 @@ import com.kamco.cd.kamcoback.common.enums.CommonUseStatus; import com.kamco.cd.kamcoback.postgres.entity.MapInkx5kEntity; import com.kamco.cd.kamcoback.scene.dto.MapInkxMngDto; import com.kamco.cd.kamcoback.scene.dto.MapInkxMngDto.MapList; +import com.kamco.cd.kamcoback.scene.dto.MapInkxMngDto.searchReq; import java.util.List; import java.util.Optional; import org.springframework.data.domain.Page; @@ -18,4 +19,7 @@ public interface MapInkx5kRepositoryCustom { Long findByMapidCdNoExists(String mapidcdNo); Optional findByMapidCdNoInfo(String mapidcdNo); + + Page getSceneListByPage( + CommonUseStatus useInference, String searchVal, searchReq searchReq); } diff --git a/src/main/java/com/kamco/cd/kamcoback/postgres/repository/scene/MapInkx5kRepositoryImpl.java b/src/main/java/com/kamco/cd/kamcoback/postgres/repository/scene/MapInkx5kRepositoryImpl.java index 6485e9c1..6e3789d3 100644 --- a/src/main/java/com/kamco/cd/kamcoback/postgres/repository/scene/MapInkx5kRepositoryImpl.java +++ b/src/main/java/com/kamco/cd/kamcoback/postgres/repository/scene/MapInkx5kRepositoryImpl.java @@ -102,6 +102,31 @@ public class MapInkx5kRepositoryImpl extends QuerydslRepositorySupport .fetchOne()); } + @Override + public Page getSceneListByPage( + CommonUseStatus useInference, String searchVal, searchReq searchReq) { + Pageable pageable = searchReq.toPageable(); + List content = + queryFactory + .selectFrom(mapInkx5kEntity) + .innerJoin(mapInkx5kEntity.mapInkx50k, mapInkx50kEntity) + .fetchJoin() + .where(searchUseInference(useInference), searchValueMapCdNm(searchVal)) + .offset(pageable.getOffset()) + .limit(pageable.getPageSize()) + .orderBy(mapInkx5kEntity.mapidcdNo.asc()) + .fetch(); + Long count = + queryFactory + .select(mapInkx5kEntity.count()) + .from(mapInkx5kEntity) + .innerJoin(mapInkx5kEntity.mapInkx50k, mapInkx50kEntity) + .where(searchUseInference(useInference), searchValueMapCdNm(searchVal)) + .fetchOne(); + + return new PageImpl<>(content, pageable, count); + } + private BooleanExpression searchUseInference(CommonUseStatus useInference) { if (Objects.isNull(useInference)) { return null; diff --git a/src/main/java/com/kamco/cd/kamcoback/scene/MapInkxMngApiV2Controller.java b/src/main/java/com/kamco/cd/kamcoback/scene/MapInkxMngApiV2Controller.java new file mode 100644 index 00000000..39a1dae7 --- /dev/null +++ b/src/main/java/com/kamco/cd/kamcoback/scene/MapInkxMngApiV2Controller.java @@ -0,0 +1,52 @@ +package com.kamco.cd.kamcoback.scene; + +import com.kamco.cd.kamcoback.code.dto.CommonCodeDto; +import com.kamco.cd.kamcoback.common.enums.CommonUseStatus; +import com.kamco.cd.kamcoback.config.api.ApiResponseDto; +import com.kamco.cd.kamcoback.scene.dto.MapInkxMngDto; +import com.kamco.cd.kamcoback.scene.service.MapInkxMngService; +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 lombok.RequiredArgsConstructor; +import org.springframework.data.domain.Page; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +@Tag(name = "도엽 관리", description = "도엽 관리 API") +@RestController +@RequiredArgsConstructor +@RequestMapping("/api/v2/scene") +public class MapInkxMngApiV2Controller { + + private final MapInkxMngService mapInkxMngService; + + @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 + public ApiResponseDto> findMapInkxMngList( + @RequestParam(defaultValue = "0") int page, + @RequestParam(defaultValue = "20") int size, + @RequestParam(required = false) CommonUseStatus useInference, + @RequestParam(required = false) String searchVal) { + MapInkxMngDto.searchReq searchReq = new MapInkxMngDto.searchReq(page, size, ""); + return ApiResponseDto.ok( + mapInkxMngService.findMapInkxMngLists(useInference, searchVal, searchReq)); + } +} diff --git a/src/main/java/com/kamco/cd/kamcoback/scene/dto/MapInkxMngDto.java b/src/main/java/com/kamco/cd/kamcoback/scene/dto/MapInkxMngDto.java index caa995a6..32464f83 100644 --- a/src/main/java/com/kamco/cd/kamcoback/scene/dto/MapInkxMngDto.java +++ b/src/main/java/com/kamco/cd/kamcoback/scene/dto/MapInkxMngDto.java @@ -1,15 +1,17 @@ package com.kamco.cd.kamcoback.scene.dto; +import com.fasterxml.jackson.annotation.JsonFormat; import com.fasterxml.jackson.databind.JsonNode; +import com.kamco.cd.kamcoback.common.enums.ApiConfigEnum.EnumDto; import com.kamco.cd.kamcoback.common.enums.CommonUseStatus; -import com.kamco.cd.kamcoback.common.utils.enums.CodeExpose; -import com.kamco.cd.kamcoback.common.utils.enums.EnumType; +import com.kamco.cd.kamcoback.inference.dto.InferenceResultDto; import io.swagger.v3.oas.annotations.media.Schema; import jakarta.persistence.EntityNotFoundException; import java.time.ZoneId; import java.time.ZonedDateTime; import java.time.format.DateTimeFormatter; import lombok.AllArgsConstructor; +import lombok.Builder; import lombok.Getter; import lombok.NoArgsConstructor; import lombok.Setter; @@ -19,25 +21,26 @@ import org.springframework.data.domain.Sort; public class MapInkxMngDto { - @CodeExpose - @Getter - @AllArgsConstructor - public enum UseInferenceType implements EnumType { - USE("사용중"), - EXCEPT("영구 추론제외"); - - private final String desc; - - @Override - public String getId() { - return name(); - } - - @Override - public String getText() { - return desc; - } - } + // CommonUseStatus class로 통합 20251230 + // @CodeExpose + // @Getter + // @AllArgsConstructor + // public enum UseInferenceType implements EnumType { + // USE("사용중"), + // EXCEPT("영구 추론제외"); + // + // private final String desc; + // + // @Override + // public String getId() { + // return name(); + // } + // + // @Override + // public String getText() { + // return desc; + // } + // } @Schema(name = "Basic", description = "Basic") @Getter @@ -55,6 +58,46 @@ public class MapInkxMngDto { private ZonedDateTime updatedDttm; } + @Getter + @Schema(name = "MapListEntity", description = "목록 항목") + public static class MapListEntity { + + private InferenceResultDto.MapSheet scene50k; + private InferenceResultDto.MapSheet scene5k; + private CommonUseStatus useInference; + + @JsonFormat( + shape = JsonFormat.Shape.STRING, + pattern = "yyyy-MM-dd'T'HH:mm:ssXXX", + timezone = "Asia/Seoul") + private ZonedDateTime createdDttm; + + @JsonFormat( + shape = JsonFormat.Shape.STRING, + pattern = "yyyy-MM-dd'T'HH:mm:ssXXX", + timezone = "Asia/Seoul") + private ZonedDateTime updatedDttm; + + public EnumDto getUseInference() { + EnumDto enumDto = useInference.getEnumDto(); + return enumDto; + } + + @Builder + public MapListEntity( + InferenceResultDto.MapSheet scene50k, + InferenceResultDto.MapSheet scene5k, + CommonUseStatus useInference, + ZonedDateTime createdDttm, + ZonedDateTime updatedDttm) { + this.scene50k = scene50k; + this.scene5k = scene5k; + this.useInference = useInference; + this.createdDttm = createdDttm; + this.updatedDttm = updatedDttm; + } + } + @Schema(name = "MapList", description = "목록 항목") @Getter @Setter diff --git a/src/main/java/com/kamco/cd/kamcoback/scene/service/MapInkxMngService.java b/src/main/java/com/kamco/cd/kamcoback/scene/service/MapInkxMngService.java index 5cd06c7c..bab97bbf 100644 --- a/src/main/java/com/kamco/cd/kamcoback/scene/service/MapInkxMngService.java +++ b/src/main/java/com/kamco/cd/kamcoback/scene/service/MapInkxMngService.java @@ -25,11 +25,18 @@ public class MapInkxMngService { private final MapInkxMngCoreService mapInkxMngCoreService; + // 도엽의 리스트 조회 public Page findMapInkxMngList( MapInkxMngDto.searchReq searchReq, CommonUseStatus useInference, String searchVal) { return mapInkxMngCoreService.findMapInkxMngList(searchReq, useInference, searchVal); } + // 도엽의 리스트 조회 + public Page findMapInkxMngLists( + CommonUseStatus useInference, String searchVal, MapInkxMngDto.searchReq searchReq) { + return mapInkxMngCoreService.getSceneListByPage(useInference, searchVal, searchReq); + } + public ResponseObj saveMapInkx5k(@Valid MapInkxMngDto.AddMapReq req) { String[] coordinates = req.getCoordinates().split("\\r?\\n"); diff --git a/src/main/resources/application-dev.yml b/src/main/resources/application-dev.yml index 4a00ed71..579b7a42 100644 --- a/src/main/resources/application-dev.yml +++ b/src/main/resources/application-dev.yml @@ -74,5 +74,5 @@ mapsheet: upload: skipGdalValidation: true shp: - baseurl: /app/detect/result + baseurl: /app/tmp/detect/result