Merge pull request '개발서버 shp생성 경로 수정, 라벨 자동할당 로직 임시 커밋' (#127) from feat/dev_251201 into develop

Reviewed-on: https://kamco.gitea.gs.dabeeo.com/dabeeo/kamco-dabeeo-backoffice/pulls/127
This commit is contained in:
2025-12-31 09:26:02 +09:00
12 changed files with 712 additions and 1 deletions

View File

@@ -0,0 +1,145 @@
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.service.DetectMastService;
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 jakarta.validation.Valid;
import java.util.List;
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.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<Basic> 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 = Basic.class))),
@ApiResponse(responseCode = "404", description = "코드를 찾을 수 없음", content = @Content),
@ApiResponse(responseCode = "500", description = "서버 오류", content = @Content)
})
@GetMapping("/pnu/{cprsBfYr}/{cprsAfYr}/{dtctSno}")
public void selectPnuList(
@PathVariable String cprsBfYr, @PathVariable String cprsAfYr, @PathVariable String dtctSno) {
DetectMastSearch detectMastSearch = new DetectMastSearch();
detectMastSearch.setCprsAdYr(cprsAfYr);
detectMastSearch.setCprsBfYr(cprsBfYr);
detectMastSearch.setDtctSno(Integer.parseInt(dtctSno));
detectMastService.findPnuData(detectMastSearch);
}
/**
* 탐지객체 PNU 단건 조회
*
* @param detectMast
*/
@GetMapping("/pnu/{cprsBfYr}/{cprsAfYr}/{dtctSno}/{featureId}")
public void selectPnuDetail(@RequestBody DetectMastDto detectMast) {}
}

View File

@@ -0,0 +1,91 @@
package com.kamco.cd.kamcoback.Innopam.dto;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotBlank;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
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;
@NotBlank
@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;
}
/** before 연도 */
private String cprsBfYr;
/** after 연도 */
private String cprsAdYr;
/** 차수 */
private Integer dtctSno;
/** shp 파일경로 */
private String pathNm;
/** 등록한 사람 사번 */
private String crtEpno;
/** 등록한 사람 ip */
private String crtIp;
}

View File

@@ -0,0 +1,62 @@
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.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<Basic> 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();
}
}

View File

@@ -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;
}

View File

@@ -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<DetectMastEntity, Long>, DetectMastRepositoryCustom {}

View File

@@ -0,0 +1,12 @@
package com.kamco.cd.kamcoback.Innopam.postgres.repository;
import com.kamco.cd.kamcoback.Innopam.dto.DetectMastDto.DetectMastSearch;
import com.kamco.cd.kamcoback.Innopam.postgres.entity.DetectMastEntity;
import java.util.List;
public interface DetectMastRepositoryCustom {
public List<DetectMastEntity> findDetectMastList(DetectMastSearch detectMast);
public DetectMastEntity findPnuData(DetectMastSearch detectMast);
}

View File

@@ -0,0 +1,59 @@
package com.kamco.cd.kamcoback.Innopam.postgres.repository;
import static com.kamco.cd.kamcoback.Innopam.postgres.entity.QDetectMastEntity.detectMastEntity;
import com.kamco.cd.kamcoback.Innopam.dto.DetectMastDto.DetectMastSearch;
import com.kamco.cd.kamcoback.Innopam.postgres.entity.DetectMastEntity;
import com.querydsl.core.BooleanBuilder;
import com.querydsl.jpa.impl.JPAQueryFactory;
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 JPAQueryFactory queryFactory;
@Override
public List<DetectMastEntity> 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();
}
}

View File

@@ -0,0 +1,59 @@
package com.kamco.cd.kamcoback.Innopam.service;
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.postgres.core.DetectMastCoreService;
import java.io.File;
import java.util.List;
import java.util.concurrent.ThreadLocalRandom;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@Service
@Transactional(readOnly = true)
@RequiredArgsConstructor
public class DetectMastService {
private final DetectMastCoreService detectMastCoreService;
@Transactional
public void saveDetectMast(DetectMastReq detectMast) {
detectMastCoreService.saveDetectMast(detectMast);
}
public List<Basic> selectDetectMast(DetectMastSearch detectMast) {
return detectMastCoreService.selectDetectMast(detectMast);
}
public Basic selectDetectMast(Long id) {
return detectMastCoreService.selectDetectMast(id);
}
public void findPnuData(DetectMastSearch detectMast) {
String pathNm = detectMastCoreService.findPnuData(detectMast);
File dir = new File(pathNm);
if (dir.exists() && dir.isDirectory()) {
File[] files = dir.listFiles();
if (files != null) {
for (File file : files) {
System.out.println(file.getName());
}
}
}
}
public static String randomPnu() {
ThreadLocalRandom r = ThreadLocalRandom.current();
String lawCode = String.valueOf(r.nextLong(1000000000L, 9999999999L)); // 10자리
String sanFlag = r.nextBoolean() ? "1" : "2"; // 산/대지
String bon = String.format("%04d", r.nextInt(1, 10000)); // 본번
String bu = String.format("%04d", r.nextInt(0, 10000)); // 부번
return lawCode + sanFlag + bon + bu;
}
}

View File

@@ -0,0 +1,67 @@
package com.kamco.cd.kamcoback.label;
import com.kamco.cd.kamcoback.config.api.ApiResponseDto;
import com.kamco.cd.kamcoback.label.dto.LabelAllocateDto;
import com.kamco.cd.kamcoback.label.dto.LabelAllocateDto.Sheet;
import com.kamco.cd.kamcoback.label.dto.LabelAllocateDto.TargetUser;
import com.kamco.cd.kamcoback.label.service.LabelAllocateService;
import io.swagger.v3.oas.annotations.tags.Tag;
import java.util.ArrayList;
import java.util.List;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
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.RestController;
@Slf4j
@Tag(name = "라벨링 작업 관리", description = "라벨링 작업 관리")
@RequestMapping({"/api/label/mng"})
@RequiredArgsConstructor
@RestController
public class LabelAllocateApiController {
// 라벨링 수량 할당하는 로직 테스트
@PostMapping("/allocate")
public ApiResponseDto<Void> labelAllocate(@RequestBody LabelAllocateDto dto) {
// 도엽별 카운트 쿼리
List<Sheet> sheets =
List.of(
new Sheet("1", 261),
new Sheet("2", 500),
new Sheet("3", 350),
new Sheet("4", 250),
new Sheet("5", 380),
new Sheet("6", 459));
// 사용자별 할당 입력한 값
List<TargetUser> targets =
List.of(new TargetUser("A", 1000), new TargetUser("B", 500), new TargetUser("C", 700));
LabelAllocateService.allocate(new ArrayList<>(sheets), new ArrayList<>(targets));
targets.forEach(
t -> {
log.info("[" + t.getUserId() + "]");
t.getAssigned()
.forEach(
u -> {
log.info(" - 도엽: " + u.getSheetId() + " (" + u.getCount() + ")");
});
});
/**
* [A] 입력한 수 : 1000 - 도엽: 2 (500) - 도엽: 6 (459) - 도엽: 5 (41)
*
* <p>[B] 입력한 수 : 500 - 도엽: 5 (339) - 도엽: 3 (161)
*
* <p>[C] 입력한 수 : 700 - 도엽: 3 (189) - 도엽: 1 (261) - 도엽: 4 (250)
*/
// A 에게 도엽 2 asc 해서 500건 할당 -> 도엽 6 asc 해서 459 할당 -> 도엽 5 asc 해서 41건 할당 -> insert
// B 에게 도엽 5 위에 41건 할당한 것 빼고 asc 해서 339건 할당 -> 도엽 3 asc 해서 161건 할당 -> insert
// .... for문에서 할당한 것 빼고 asc 해서 건수만큼 할당 insert 하고 다음 으로 넘어가기
return ApiResponseDto.ok(null);
}
}

View File

@@ -0,0 +1,46 @@
package com.kamco.cd.kamcoback.label.dto;
import java.util.ArrayList;
import java.util.List;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.Setter;
public class LabelAllocateDto {
@Getter
@AllArgsConstructor
public static class Sheet {
private final String sheetId;
private int count;
public void decrease(int amount) {
this.count -= amount;
}
}
@Getter
public static class TargetUser {
private final String userId;
private final int demand;
private final List<Sheet> assigned = new ArrayList<>();
private int allocated = 0;
@Setter private int shortage = 0;
public TargetUser(String userId, int demand) {
this.userId = userId;
this.demand = demand;
}
public int getRemainDemand() {
return demand - allocated;
}
public void assign(String sheetId, int count) {
assigned.add(new Sheet(sheetId, count));
allocated += count;
}
}
}

View File

@@ -0,0 +1,78 @@
package com.kamco.cd.kamcoback.label.service;
import com.kamco.cd.kamcoback.label.dto.LabelAllocateDto.Sheet;
import com.kamco.cd.kamcoback.label.dto.LabelAllocateDto.TargetUser;
import java.util.Comparator;
import java.util.Iterator;
import java.util.List;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
@Slf4j
@Service
public class LabelAllocateService {
public static void allocate(List<Sheet> sheets, List<TargetUser> targetUsers) {
// 1⃣ 실제 도엽 기준 할당
allocateSheets(sheets, targetUsers);
// 2⃣ 전체 부족분 비율 계산
distributeShortage(targetUsers); // 항상 마지막에 한 번만 호출해야함
}
public static void allocateSheets(List<Sheet> sheets, List<TargetUser> targets) {
// 도엽 큰 것부터 (선택 사항)
sheets.sort(Comparator.comparingInt(Sheet::getCount).reversed());
for (TargetUser target : targets) {
Iterator<Sheet> it = sheets.iterator();
while (it.hasNext() && target.getRemainDemand() > 0) {
Sheet sheet = it.next();
int assignable = Math.min(sheet.getCount(), target.getRemainDemand());
if (assignable > 0) {
target.assign(sheet.getSheetId(), assignable);
sheet.decrease(assignable);
}
if (sheet.getCount() == 0) {
it.remove();
}
}
}
}
public static void distributeShortage(List<TargetUser> targets) {
int totalDemand = targets.stream().mapToInt(TargetUser::getDemand).sum();
int totalAllocated = targets.stream().mapToInt(t -> t.getAllocated()).sum();
int shortage = totalDemand - totalAllocated;
if (shortage <= 0) {
return;
}
int distributed = 0;
for (int i = 0; i < targets.size(); i++) {
TargetUser t = targets.get(i);
// 마지막 타겟이 나머지 가져가게 (반올림 오차 방지)
int share;
if (i == targets.size() - 1) {
share = shortage - distributed;
} else {
double ratio = (double) t.getDemand() / totalDemand;
share = (int) Math.round(shortage * ratio);
distributed += share;
}
t.setShortage(share);
}
}
}

View File

@@ -74,5 +74,5 @@ mapsheet:
upload:
skipGdalValidation: true
shp:
baseurl: /app/detect/result
baseurl: /app/tmp/detect/result