label-to-review 커밋

This commit is contained in:
2026-02-25 15:26:09 +09:00
parent 25f6deebb1
commit 7ac03692e9
214 changed files with 4771 additions and 738 deletions

View File

@@ -87,7 +87,7 @@ public class CommonCodeDto {
private Integer order;
private Boolean used;
private Boolean deleted;
private List<CommonCodeDto.Basic> children;
private List<Basic> children;
@JsonFormatDttm private ZonedDateTime createdDttm;
@@ -112,7 +112,7 @@ public class CommonCodeDto {
Integer order,
Boolean used,
Boolean deleted,
List<CommonCodeDto.Basic> children,
List<Basic> children,
ZonedDateTime createdDttm,
ZonedDateTime updatedDttm,
String props1,

View File

@@ -0,0 +1,13 @@
### TODO
## 환경분리
- local
- dev
- prod
## codestyle 세팅 - google 고쳐주세요. tab 간격 2자리
## objectmepper 빈으로 세팅
## 라이브러리 체크
## querydsl 세팅 custom, impl
## 공통코드 관리
## 나중 레디스랑 캐시

View File

@@ -1,4 +1,4 @@
package com.kamco.cd.kamcoback.enums;
package com.kamco.cd.kamcoback.common.enums;
import lombok.EqualsAndHashCode;
import lombok.Getter;

View File

@@ -1,7 +1,8 @@
package com.kamco.cd.kamcoback.enums;
package com.kamco.cd.kamcoback.common.enums;
import com.kamco.cd.kamcoback.enums.ApiConfigEnum.EnumDto;
import com.kamco.cd.kamcoback.inferface.EnumType;
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;
import lombok.Getter;
@@ -12,6 +13,7 @@ import lombok.Getter;
* <p>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 {

View File

@@ -0,0 +1,27 @@
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 CrsType implements EnumType {
EPSG_3857("Web Mercator, 웹지도 미터(EPSG:900913 동일)"),
EPSG_4326("WGS84 위경도, GeoJSON/OSM 기본"),
EPSG_5186("Korea 2000 중부 TM, 한국 SHP");
private final String desc;
@Override
public String getId() {
return name();
}
@Override
public String getText() {
return desc;
}
}

View File

@@ -1,4 +1,4 @@
package com.kamco.cd.kamcoback.inference.dto;
package com.kamco.cd.kamcoback.common.enums;
import lombok.AllArgsConstructor;
import lombok.Getter;

View File

@@ -0,0 +1,29 @@
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 FileUploadStatus implements EnumType {
INIT("초기화"),
UPLOADING("업로드중"),
DONE("업로드완료"),
MERGED("병합완료"),
MERGE_FAIL("병합 실패");
private final String desc;
@Override
public String getId() {
return name();
}
@Override
public String getText() {
return desc;
}
}

View File

@@ -1,5 +1,7 @@
package com.kamco.cd.kamcoback.common.utils.enums;
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 java.util.Arrays;
import lombok.AllArgsConstructor;
import lombok.Getter;

View File

@@ -0,0 +1,37 @@
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 java.util.Optional;
import lombok.AllArgsConstructor;
import lombok.Getter;
@CodeExpose
@Getter
@AllArgsConstructor
public enum LayerType implements EnumType {
TILE("배경지도"),
GEOJSON("객체데이터"),
WMTS("타일레이어"),
WMS("지적도");
private final String desc;
@Override
public String getId() {
return name();
}
@Override
public String getText() {
return desc;
}
public static Optional<LayerType> from(String type) {
try {
return Optional.of(LayerType.valueOf(type));
} catch (Exception e) {
return Optional.empty();
}
}
}

View File

@@ -1,9 +1,11 @@
package com.kamco.cd.kamcoback.enums;
package com.kamco.cd.kamcoback.common.enums;
import com.kamco.cd.kamcoback.inferface.EnumType;
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 MngStateType implements EnumType {

View File

@@ -0,0 +1,27 @@
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 RoleType implements EnumType {
ADMIN("관리자"),
LABELER("라벨러"),
REVIEWER("검수자");
private final String desc;
@Override
public String getId() {
return name();
}
@Override
public String getText() {
return desc;
}
}

View File

@@ -0,0 +1,27 @@
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 StatusType implements EnumType {
ACTIVE("사용"),
INACTIVE("사용중지"),
PENDING("계정등록");
private final String desc;
@Override
public String getId() {
return name();
}
@Override
public String getText() {
return desc;
}
}

View File

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

View File

@@ -1,8 +1,8 @@
package com.kamco.cd.kamcoback.enums;
package com.kamco.cd.kamcoback.common.enums;
import com.kamco.cd.kamcoback.inferface.CodeExpose;
import com.kamco.cd.kamcoback.inferface.CodeHidden;
import com.kamco.cd.kamcoback.inferface.EnumType;
import com.kamco.cd.kamcoback.common.utils.enums.CodeExpose;
import com.kamco.cd.kamcoback.common.utils.enums.CodeHidden;
import com.kamco.cd.kamcoback.common.utils.enums.EnumType;
import lombok.AllArgsConstructor;
import lombok.Getter;

View File

@@ -0,0 +1,24 @@
package com.kamco.cd.kamcoback.common.enums.error;
import com.kamco.cd.kamcoback.common.utils.ErrorCode;
import lombok.Getter;
import org.springframework.http.HttpStatus;
@Getter
public enum AuthErrorCode implements ErrorCode {
LOGIN_ID_NOT_FOUND("LOGIN_ID_NOT_FOUND", HttpStatus.UNAUTHORIZED),
LOGIN_PASSWORD_MISMATCH("LOGIN_PASSWORD_MISMATCH", HttpStatus.UNAUTHORIZED),
LOGIN_PASSWORD_EXCEEDED("LOGIN_PASSWORD_EXCEEDED", HttpStatus.UNAUTHORIZED),
INACTIVE_ID("INACTIVE_ID", HttpStatus.UNAUTHORIZED);
private final String code;
private final HttpStatus status;
AuthErrorCode(String code, HttpStatus status) {
this.code = code;
this.status = status;
}
}

View File

@@ -0,0 +1,36 @@
package com.kamco.cd.kamcoback.common.exception;
import com.kamco.cd.kamcoback.common.utils.ErrorCode;
import lombok.Getter;
import org.springframework.http.HttpStatus;
@Getter
public class CustomApiException extends RuntimeException {
private final String codeName; // ApiResponseCode enum name과 맞추는 용도 (예: "UNPROCESSABLE_ENTITY")
private final HttpStatus status; // 응답으로 내려줄 HttpStatus
public CustomApiException(String codeName, HttpStatus status, String message) {
super(message);
this.codeName = codeName;
this.status = status;
}
public CustomApiException(String codeName, HttpStatus status) {
super(codeName);
this.codeName = codeName;
this.status = status;
}
public CustomApiException(ErrorCode errorCode) {
super(errorCode.getCode()); // 또는 errorCode.getMessage()
this.codeName = errorCode.getCode();
this.status = errorCode.getStatus();
}
public CustomApiException(String codeName, HttpStatus status, Throwable cause) {
super(codeName, cause);
this.codeName = codeName;
this.status = status;
}
}

View File

@@ -0,0 +1,7 @@
package com.kamco.cd.kamcoback.common.exception;
public class DuplicateFileException extends RuntimeException {
public DuplicateFileException(String message) {
super(message);
}
}

View File

@@ -0,0 +1,7 @@
package com.kamco.cd.kamcoback.common.exception;
public class ValidationException extends RuntimeException {
public ValidationException(String message) {
super(message);
}
}

View File

@@ -0,0 +1,171 @@
package com.kamco.cd.kamcoback.common.geometry;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.node.ArrayNode;
import com.fasterxml.jackson.databind.node.ObjectNode;
import java.io.FileWriter;
import java.io.IOException;
import java.util.List;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import org.locationtech.jts.geom.Geometry;
/** GeoJSON 파일 생성 유틸리티 */
public class GeoJsonFileWriter {
private final ObjectMapper objectMapper;
public GeoJsonFileWriter() {
this.objectMapper = new ObjectMapper();
}
public GeoJsonFileWriter(ObjectMapper objectMapper) {
this.objectMapper = objectMapper;
}
/**
* GeoJSON 문자열 생성
*
* @param features Feature 목록
* @param name GeoJSON name 속성
* @param srid CRS EPSG 코드
* @return GeoJSON 문자열
*/
public String buildGeoJson(List<ImageFeature> features, String name, int srid) {
try {
ObjectNode root = objectMapper.createObjectNode();
root.put("type", "FeatureCollection");
root.put("name", name);
// CRS 정보
ObjectNode crs = objectMapper.createObjectNode();
crs.put("type", "name");
ObjectNode crsProps = objectMapper.createObjectNode();
crsProps.put("name", "urn:ogc:def:crs:EPSG::" + srid);
crs.set("properties", crsProps);
root.set("crs", crs);
// Features 배열
ArrayNode featuresArray = objectMapper.createArrayNode();
for (ImageFeature f : features) {
featuresArray.add(buildFeature(f));
}
root.set("features", featuresArray);
return objectMapper.writerWithDefaultPrettyPrinter().writeValueAsString(root);
} catch (Exception e) {
throw new RuntimeException("GeoJSON 생성 실패", e);
}
}
/** 단일 Feature 객체 생성 */
private ObjectNode buildFeature(ImageFeature f) throws Exception {
ObjectNode feature = objectMapper.createObjectNode();
feature.put("type", "Feature");
// Properties
ObjectNode properties = objectMapper.createObjectNode();
properties.put("scene_id", f.getSceneId());
properties.put("abs_path", f.getAbsPath());
properties.put("basename", f.getFileName());
properties.put("georef_source", "internal");
properties.put("crs_source", "transformed");
feature.set("properties", properties);
// Geometry (CRS 제거)
ObjectNode geometry = (ObjectNode) objectMapper.readTree(f.getGeomJson());
geometry.remove("crs");
feature.set("geometry", geometry);
return feature;
}
/**
* 파일로 저장
*
* @param geojson GeoJSON 문자열
* @param filePath 저장 경로
*/
public void writeToFile(String geojson, String filePath) throws IOException {
try (FileWriter writer = new FileWriter(filePath)) {
writer.write(geojson);
}
}
/** Feature 목록을 바로 파일로 저장 */
public void exportToFile(List<ImageFeature> features, String name, int srid, String filePath)
throws IOException {
String geojson = buildGeoJson(features, name, srid);
writeToFile(geojson, filePath);
}
/** Feature 데이터 클래스 */
public static class ImageFeature {
private String sceneId;
private String filePath;
private String fileName;
private String geomJson;
public ImageFeature() {}
public ImageFeature(String sceneId, String filePath, String fileName, Geometry geomJson) {
this.sceneId = sceneId;
this.filePath = filePath;
this.fileName = fileName;
if (geomJson != null) {
this.geomJson = GeometryUtils.toGeoJson(geomJson);
}
}
public String getSceneId() {
return sceneId;
}
public void setSceneId(String sceneId) {
this.sceneId = sceneId;
}
public String getFilePath() {
return filePath;
}
public void setFilePath(String filePath) {
this.filePath = filePath;
}
public String getFileName() {
return fileName;
}
public void setFileName(String fileName) {
this.fileName = fileName;
}
public String getGeomJson() {
return geomJson;
}
public void setGeomJson(String geomJson) {
this.geomJson = geomJson;
}
public String getAbsPath() {
return filePath + "/" + fileName;
}
}
@Getter
@Setter
@AllArgsConstructor
@NoArgsConstructor
public static class Scene {
List<ImageFeature> features;
String filePath;
}
}

View File

@@ -0,0 +1,17 @@
package com.kamco.cd.kamcoback.common.geometry;
import org.locationtech.jts.geom.Geometry;
import org.locationtech.jts.io.geojson.GeoJsonWriter;
public class GeometryUtils {
private static final GeoJsonWriter GEOJSON_WRITER = new GeoJsonWriter(8);
/** JTS Geometry를 GeoJSON 문자열로 변환 */
public static String toGeoJson(Geometry geometry) {
if (geometry == null) {
return null;
}
return GEOJSON_WRITER.write(geometry);
}
}

View File

@@ -0,0 +1,38 @@
package com.kamco.cd.kamcoback.common.service;
import org.springframework.data.domain.Page;
/**
* Base Core Service Interface
*
* <p>CRUD operations를 정의하는 기본 서비스 인터페이스
*
* @param <T> Entity 타입
* @param <ID> Entity의 ID 타입
* @param <S> Search Request 타입
*/
public interface BaseCoreService<T, ID, S> {
/**
* ID로 엔티티를 삭제합니다.
*
* @param id 삭제할 엔티티의 ID
*/
void remove(ID id);
/**
* ID로 단건 조회합니다.
*
* @param id 조회할 엔티티의 ID
* @return 조회된 엔티티
*/
T getOneById(ID id);
/**
* 검색 조건과 페이징으로 조회합니다.
*
* @param searchReq 검색 조건
* @return 페이징 처리된 검색 결과
*/
Page<T> search(S searchReq);
}

View File

@@ -0,0 +1,135 @@
package com.kamco.cd.kamcoback.common.service;
import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.TimeUnit;
import lombok.extern.log4j.Log4j2;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
@Log4j2
@Component
public class ExternalJarRunner {
@Value("${spring.profiles.active}")
private String profile;
private static final long TIMEOUT_MINUTES = TimeUnit.DAYS.toMinutes(3);
/**
* shp 파일 생성
*
* @param jarPath jar 경로
* @param batchIds 배치 아이디
* @param inferenceId uid
* @param mapIds 추론 실행한 도엽 ids
* @param mode
* <p>MERGED - batch-ids 에 해당하는 **모든 데이터를 하나의 Shapefile로 병합 생성,
* <p>MAP_IDS - 명시적으로 전달한 map-ids만 대상으로 Shapefile 생성,
* <p>RESOLVE - batch-ids 기준으로 **JAR 내부에서 map_ids를 조회**한 뒤 Shapefile 생성
*/
public void run(String jarPath, String batchIds, String inferenceId, String mapIds, String mode) {
List<String> args = new ArrayList<>();
addArg(args, "converter.inference-id", inferenceId);
addArg(args, "converter.batch-ids", batchIds);
if (mapIds != null && !mapIds.isEmpty()) {
addArg(args, "converter.map-ids", mapIds);
}
if (mode != null && !mode.isEmpty()) {
addArg(args, "converter.mode", mode);
}
addArg(args, "spring.profiles.active", profile);
execJar(jarPath, args);
}
/**
* geoserver 등록
*
* @param jarPath jar 파일경로
* @param register shp 경로
* @param layer geoserver에 등록될 레이어 이름
*/
public void run(String jarPath, String register, String layer) {
List<String> args = new ArrayList<>();
addArg(args, "upload-shp", register);
// addArg(args, "layer", layer);
addArg(args, "spring.profiles.active", profile);
execJar(jarPath, args);
}
private void execJar(String jarPath, List<String> args) {
StringBuilder out = new StringBuilder();
try {
List<String> cmd = new ArrayList<>();
cmd.add("java");
cmd.add("-jar");
cmd.add(jarPath);
cmd.addAll(args);
ProcessBuilder pb = new ProcessBuilder(cmd);
pb.redirectErrorStream(true);
Process p = pb.start();
try (BufferedReader br =
new BufferedReader(new InputStreamReader(p.getInputStream(), StandardCharsets.UTF_8))) {
String line;
while ((line = br.readLine()) != null) {
out.append(line).append('\n');
log.info("[jar] {}", line);
}
}
boolean finished = p.waitFor(TIMEOUT_MINUTES, TimeUnit.MINUTES);
if (!finished) {
p.destroyForcibly();
throw new RuntimeException("jar timeout\n" + out);
}
int exit = p.exitValue();
if (exit != 0) {
throw new RuntimeException("jar failed. exitCode=" + exit + "\n" + out);
}
log.info("jar finished successfully");
} catch (Exception e) {
log.error("jar execution error. output=\n{}", out, e);
}
}
private void addArg(List<String> args, String key, String value) {
value = normalizeCliValue(value);
if (value != null && !value.isBlank()) {
log.info("addArg key={}, normalizedValue=[{}], length={}", key, value, value.length());
args.add("--" + key + "=" + value);
}
}
private String normalizeCliValue(String v) {
if (v == null) {
return null;
}
v = v.trim();
// 양끝 따옴표 제거
if (v.length() >= 2 && v.startsWith("\"") && v.endsWith("\"")) {
v = v.substring(1, v.length() - 1);
}
// 남아있는 따옴표 제거
v = v.replace("\"", "");
return v;
}
}

View File

@@ -0,0 +1,20 @@
package com.kamco.cd.kamcoback.common.utils;
import java.time.LocalDate;
import java.time.ZoneId;
import java.time.ZonedDateTime;
public class DateRange {
private static final ZoneId KST = ZoneId.of("Asia/Seoul");
private DateRange() {}
public static ZonedDateTime start(LocalDate date) {
return date == null ? null : date.atStartOfDay(KST);
}
public static ZonedDateTime end(LocalDate date) {
return date == null ? null : date.plusDays(1).atStartOfDay(KST);
}
}

View File

@@ -0,0 +1,10 @@
package com.kamco.cd.kamcoback.common.utils;
import org.springframework.http.HttpStatus;
public interface ErrorCode {
String getCode();
HttpStatus getStatus();
}

View File

@@ -0,0 +1,23 @@
package com.kamco.cd.kamcoback.common.utils;
import jakarta.servlet.http.HttpServletRequest;
public final class HeaderUtil {
private HeaderUtil() {}
/** 특정 Header 값 조회 */
public static String get(HttpServletRequest request, String headerName) {
if (request == null || headerName == null) {
return null;
}
String value = request.getHeader(headerName);
return (value != null && !value.isBlank()) ? value : null;
}
/** 필수 Header 조회 (없으면 null) */
public static String getRequired(HttpServletRequest request, String headerName) {
return get(request, headerName);
}
}

View File

@@ -0,0 +1,43 @@
package com.kamco.cd.kamcoback.common.utils;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
public class NameValidator {
private static final String HANGUL_REGEX = ".*\\p{IsHangul}.*";
private static final Pattern HANGUL_PATTERN = Pattern.compile(HANGUL_REGEX);
private static final String WHITESPACE_REGEX = ".*\\s.*";
private static final Pattern WHITESPACE_PATTERN = Pattern.compile(WHITESPACE_REGEX);
public static boolean containsKorean(String str) {
if (str == null || str.isEmpty()) {
return false;
}
Matcher matcher = HANGUL_PATTERN.matcher(str);
return matcher.matches();
}
public static boolean containsWhitespaceRegex(String str) {
if (str == null || str.isEmpty()) {
return false;
}
Matcher matcher = WHITESPACE_PATTERN.matcher(str);
// find()를 사용하여 문자열 내에서 패턴이 일치하는 부분이 있는지 확인
return matcher.find();
}
public static boolean isNullOrEmpty(String str) {
if (str == null) {
return true;
}
if (str.isEmpty()) {
return true;
}
return false;
}
}

View File

@@ -0,0 +1,61 @@
package com.kamco.cd.kamcoback.common.utils;
import com.fasterxml.jackson.databind.ObjectMapper;
import java.net.InetAddress;
import java.net.URLEncoder;
import java.net.UnknownHostException;
import java.nio.charset.StandardCharsets;
import java.util.Map;
import java.util.stream.Collectors;
import org.springframework.http.HttpHeaders;
public class NetUtils {
public String getLocalIP() {
String ip;
{
try {
InetAddress local = InetAddress.getLocalHost();
ip = local.getHostAddress();
} catch (UnknownHostException e) {
throw new RuntimeException(e);
}
}
return ip;
}
public String dtoToQueryString(Object dto, String queryString) {
ObjectMapper objectMapper = new ObjectMapper();
Map<String, Object> map = objectMapper.convertValue(dto, Map.class);
String qStr =
map.entrySet().stream()
.filter(entry -> entry.getValue() != null) // null 제외
.map(
entry ->
String.format(
"%s=%s",
entry.getKey(),
URLEncoder.encode(entry.getValue().toString(), StandardCharsets.UTF_8)))
.collect(Collectors.joining("&"));
if (queryString == null || queryString.isEmpty()) {
queryString = "?" + qStr;
} else {
queryString = queryString + "&" + qStr;
}
// 2. Map을 쿼리 스트링 문자열로 변환
return queryString;
}
public HttpHeaders jsonHeaders() {
HttpHeaders headers = new HttpHeaders();
headers.set(HttpHeaders.ACCEPT, "application/json;charset=UTF-8");
headers.set(HttpHeaders.CONTENT_TYPE, "application/json;charset=UTF-8");
return headers;
}
}

View File

@@ -0,0 +1,34 @@
package com.kamco.cd.kamcoback.common.utils;
import java.util.regex.Pattern;
import org.mindrot.jbcrypt.BCrypt;
public class StringUtils {
private static final int BCRYPT_COST = 10;
/**
* 영문, 숫자, 특수문자를 모두 포함하여 8~20자 이내의 비밀번호
*
* @param password 벨리데이션 필요한 패스워드
* @return
*/
public static boolean isValidPassword(String password) {
String passwordPattern =
"^(?=.*[A-Za-z])(?=.*\\d)(?=.*[!@#$%^&*()_+\\-\\[\\]{};':\"\\\\|,.<>/?=]).{8,20}$";
return Pattern.matches(passwordPattern, password);
}
/**
* 패스워드 암호화
*
* @param password 암호화 필요한 패스워드
* @return
*/
public static String hashPassword(String password) {
if (password == null) {
throw new IllegalArgumentException("password must not be null");
}
return BCrypt.hashpw(password.trim(), BCrypt.gensalt(BCRYPT_COST));
}
}

View File

@@ -0,0 +1,42 @@
package com.kamco.cd.kamcoback.common.utils.geometry;
import com.fasterxml.jackson.core.JacksonException;
import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.databind.DeserializationContext;
import com.fasterxml.jackson.databind.deser.std.StdDeserializer;
import java.io.IOException;
import org.locationtech.jts.geom.Geometry;
import org.locationtech.jts.io.geojson.GeoJsonReader;
import org.springframework.util.StringUtils;
public class GeometryDeserializer<T extends Geometry> extends StdDeserializer<T> {
public GeometryDeserializer() {
super(Geometry.class);
}
public GeometryDeserializer(Class<T> targetType) {
super(targetType);
}
// TODO: test code
@SuppressWarnings("unchecked")
@Override
public T deserialize(JsonParser jsonParser, DeserializationContext deserializationContext)
throws IOException, JacksonException {
String json = jsonParser.readValueAsTree().toString();
if (!StringUtils.hasText(json)) {
return null;
}
try {
GeoJsonReader reader = new GeoJsonReader();
Geometry geometry = reader.read(json);
geometry.setSRID(5186);
return (T) geometry;
} catch (Exception e) {
throw new IllegalArgumentException("Failed to deserialize GeoJSON into Geometry", e);
}
}
}

View File

@@ -0,0 +1,31 @@
package com.kamco.cd.kamcoback.common.utils.geometry;
import com.fasterxml.jackson.core.JsonGenerator;
import com.fasterxml.jackson.databind.SerializerProvider;
import com.fasterxml.jackson.databind.ser.std.StdSerializer;
import java.io.IOException;
import java.util.Objects;
import org.locationtech.jts.geom.Geometry;
import org.locationtech.jts.io.geojson.GeoJsonWriter;
public class GeometrySerializer<T extends Geometry> extends StdSerializer<T> {
// TODO: test code
public GeometrySerializer(Class<T> targetType) {
super(targetType);
}
@Override
public void serialize(
T geometry, JsonGenerator jsonGenerator, SerializerProvider serializerProvider)
throws IOException {
if (Objects.nonNull(geometry)) {
// default: 8자리 강제로 반올림시킴. 16자리로 늘려줌
GeoJsonWriter writer = new GeoJsonWriter(16);
String json = writer.write(geometry);
jsonGenerator.writeRawValue(json);
} else {
jsonGenerator.writeNull();
}
}
}

View File

@@ -0,0 +1,33 @@
package com.kamco.cd.kamcoback.common.utils.zip;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
@Slf4j
@Component
public class CsvFileProcessor implements ZipEntryProcessor {
@Override
public boolean supports(String fileName) {
return fileName.toLowerCase().endsWith(".csv");
}
@Override
public void process(String fileName, InputStream is) throws IOException {
try (BufferedReader br = new BufferedReader(new InputStreamReader(is))) {
br.lines()
.forEach(
line -> {
String[] cols = line.split(",");
// CSV 처리
for (String col : cols) {
log.info(col); // TODO : 추후에 csv 파일 읽어서 작업 필요할 때 정의하기
}
});
}
}
}

View File

@@ -0,0 +1,73 @@
package com.kamco.cd.kamcoback.common.utils.zip;
import com.fasterxml.jackson.core.JsonFactory;
import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.core.JsonToken;
import com.fasterxml.jackson.databind.ObjectMapper;
import java.io.IOException;
import java.io.InputStream;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
@Slf4j
@Component
public class JsonStreamingFileProcessor implements ZipEntryProcessor {
private final JsonFactory jsonFactory;
public JsonStreamingFileProcessor(ObjectMapper objectMapper) {
// ZipInputStream 보호용 설정
objectMapper.configure(JsonParser.Feature.AUTO_CLOSE_SOURCE, false);
this.jsonFactory = objectMapper.getFactory();
}
@Override
public boolean supports(String fileName) {
return fileName.toLowerCase().endsWith(".json");
}
@Override
public void process(String fileName, InputStream is) throws IOException {
log.info("JSON process start: {}", fileName);
JsonParser parser = jsonFactory.createParser(is);
// JSON 구조에 상관없이 token 단위로 순회
while (parser.nextToken() != null) {
handleToken(parser);
}
log.info("JSON process end: {}", fileName);
}
private void handleToken(JsonParser parser) throws IOException {
JsonToken token = parser.currentToken();
if (token == JsonToken.FIELD_NAME) {
String fieldName = parser.getCurrentName();
// TODO: json 파일 읽어야 할 내용 정의되면 항목 확정하기
switch (fieldName) {
case "type" -> {
parser.nextToken();
String type = parser.getValueAsString();
log.info("type: {}", type);
}
case "name" -> {
parser.nextToken();
String name = parser.getValueAsString();
log.info("Name: {}", name);
}
case "features" -> {
parser.nextToken();
String features = parser.readValueAsTree().toString();
log.info("features: {}", features);
}
default -> {
parser.nextToken();
parser.skipChildren();
}
}
}
}
}

View File

@@ -0,0 +1,27 @@
package com.kamco.cd.kamcoback.common.utils.zip;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
@Slf4j
@Component
public class TextFileProcessor implements ZipEntryProcessor {
@Override
public boolean supports(String fileName) {
return fileName.toLowerCase().endsWith(".txt");
}
@Override
public void process(String fileName, InputStream is) throws IOException {
BufferedReader br = new BufferedReader(new InputStreamReader(is));
String line;
while ((line = br.readLine()) != null) {
log.info(line); // TODO : 추후 txt 파일 읽어서 작업할 때 정의하기
}
}
}

View File

@@ -0,0 +1,11 @@
package com.kamco.cd.kamcoback.common.utils.zip;
import java.io.IOException;
import java.io.InputStream;
public interface ZipEntryProcessor {
boolean supports(String fileName);
void process(String fileName, InputStream is) throws IOException;
}

View File

@@ -0,0 +1,49 @@
package com.kamco.cd.kamcoback.common.utils.zip;
import java.io.IOException;
import java.io.InputStream;
import java.io.UncheckedIOException;
import java.util.List;
import java.util.zip.ZipEntry;
import java.util.zip.ZipInputStream;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
@Slf4j
@Component
public class ZipUtils {
private final List<ZipEntryProcessor> processors;
public ZipUtils(List<ZipEntryProcessor> processors) {
this.processors = processors;
}
public void processZip(InputStream zipStream) throws IOException {
try (ZipInputStream zis = new ZipInputStream(zipStream)) {
ZipEntry entry;
while ((entry = zis.getNextEntry()) != null) {
if (entry.isDirectory()) {
continue;
}
String fileName = entry.getName();
processors.stream()
.filter(p -> p.supports(fileName))
.findFirst()
.ifPresent(
processor -> {
try {
processor.process(fileName, zis);
} catch (IOException ioe) {
throw new UncheckedIOException(ioe);
}
});
zis.closeEntry();
}
}
}
}

View File

@@ -0,0 +1,77 @@
package com.kamco.cd.kamcoback.config;
import io.swagger.v3.oas.models.Components;
import io.swagger.v3.oas.models.OpenAPI;
import io.swagger.v3.oas.models.info.Info;
import io.swagger.v3.oas.models.security.SecurityRequirement;
import io.swagger.v3.oas.models.security.SecurityScheme;
import io.swagger.v3.oas.models.servers.Server;
import java.util.ArrayList;
import java.util.List;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class OpenApiConfig {
@Value("${server.port}")
private String serverPort;
@Value("${spring.profiles.active:local}")
private String profile;
@Value("${swagger.dev-url:https://kamco.dev-api.gs.dabeeo.com}")
private String devUrl;
@Value("${swagger.prod-url:https://api.kamco.com}")
private String prodUrl;
@Bean
public OpenAPI kamcoOpenAPI() {
// 1) SecurityScheme 정의 (Bearer JWT)
SecurityScheme bearerAuth =
new SecurityScheme()
.type(SecurityScheme.Type.HTTP)
.scheme("bearer")
.bearerFormat("JWT")
.in(SecurityScheme.In.HEADER)
.name("Authorization");
// 2) SecurityRequirement (기본으로 BearerAuth 사용)
SecurityRequirement securityRequirement = new SecurityRequirement().addList("BearerAuth");
// 3) Components 에 SecurityScheme 등록
Components components = new Components().addSecuritySchemes("BearerAuth", bearerAuth);
// profile 별 server url 분기
List<Server> servers = new ArrayList<>();
if ("dev".equals(profile)) {
servers.add(new Server().url(devUrl).description("개발 서버"));
servers.add(new Server().url("http://localhost:" + serverPort).description("로컬 서버"));
// servers.add(new Server().url(prodUrl).description("운영 서버"));
} else if ("prod".equals(profile)) {
// servers.add(new Server().url(prodUrl).description("운영 서버"));
servers.add(new Server().url("http://localhost:" + serverPort).description("로컬 서버"));
servers.add(new Server().url(devUrl).description("개발 서버"));
} else {
servers.add(new Server().url("http://localhost:" + serverPort).description("로컬 서버"));
servers.add(new Server().url(devUrl).description("개발 서버"));
// servers.add(new Server().url(prodUrl).description("운영 서버"));
}
return new OpenAPI()
.info(
new Info()
.title("KAMCO Change Detection API")
.description(
"KAMCO 변화 탐지 시스템 API 문서\n\n"
+ "이 API는 지리공간 데이터를 활용한 변화 탐지 시스템을 제공합니다.\n"
+ "GeoJSON 형식의 공간 데이터를 처리하며, PostgreSQL/PostGIS 기반으로 동작합니다.")
.version("v1.0.0"))
.servers(servers)
// 만들어둔 components를 넣어야 함
.components(components)
.addSecurityItem(securityRequirement);
}
}

View File

@@ -0,0 +1,96 @@
package com.kamco.cd.kamcoback.config;
import com.zaxxer.hikari.HikariDataSource;
import javax.sql.DataSource;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.context.event.ApplicationReadyEvent;
import org.springframework.context.event.EventListener;
import org.springframework.core.env.Environment;
import org.springframework.stereotype.Component;
@Slf4j
@Component
@RequiredArgsConstructor
public class StartupLogger {
private final Environment environment;
private final DataSource dataSource;
@EventListener(ApplicationReadyEvent.class)
public void logStartupInfo() {
String[] activeProfiles = environment.getActiveProfiles();
String profileInfo = activeProfiles.length > 0 ? String.join(", ", activeProfiles) : "default";
// Database connection information
String dbUrl = environment.getProperty("spring.datasource.url");
String dbUsername = environment.getProperty("spring.datasource.username");
String dbDriver = environment.getProperty("spring.datasource.driver-class-name");
// HikariCP pool settings
String poolInfo = "";
if (dataSource instanceof HikariDataSource hikariDs) {
poolInfo =
String.format(
"""
│ Pool Size : min=%d, max=%d
│ Connection Timeout: %dms
│ Idle Timeout : %dms
│ Max Lifetime : %dms""",
hikariDs.getMinimumIdle(),
hikariDs.getMaximumPoolSize(),
hikariDs.getConnectionTimeout(),
hikariDs.getIdleTimeout(),
hikariDs.getMaxLifetime());
}
// JPA/Hibernate settings
String showSql = environment.getProperty("spring.jpa.show-sql", "false");
String ddlAuto = environment.getProperty("spring.jpa.hibernate.ddl-auto", "none");
String batchSize =
environment.getProperty("spring.jpa.properties.hibernate.jdbc.batch_size", "N/A");
String batchFetchSize =
environment.getProperty("spring.jpa.properties.hibernate.default_batch_fetch_size", "N/A");
String startupMessage =
String.format(
"""
╔════════════════════════════════════════════════════════════════════════════════╗
║ 🚀 APPLICATION STARTUP INFORMATION ║
╠════════════════════════════════════════════════════════════════════════════════╣
║ PROFILE CONFIGURATION ║
╠────────────────────────────────────────────────────────────────────────────────╣
│ Active Profile(s): %s
╠════════════════════════════════════════════════════════════════════════════════╣
║ DATABASE CONFIGURATION ║
╠────────────────────────────────────────────────────────────────────────────────╣
│ Database URL : %s
│ Username : %s
│ Driver : %s
╠════════════════════════════════════════════════════════════════════════════════╣
║ HIKARICP CONNECTION POOL ║
╠────────────────────────────────────────────────────────────────────────────────╣
%s
╠════════════════════════════════════════════════════════════════════════════════╣
║ JPA/HIBERNATE CONFIGURATION ║
╠────────────────────────────────────────────────────────────────────────────────╣
│ Show SQL : %s
│ DDL Auto : %s
│ JDBC Batch Size : %s
│ Fetch Batch Size : %s
╚════════════════════════════════════════════════════════════════════════════════╝
""",
profileInfo,
dbUrl != null ? dbUrl : "N/A",
dbUsername != null ? dbUsername : "N/A",
dbDriver != null ? dbDriver : "PostgreSQL JDBC Driver (auto-detected)",
poolInfo,
showSql,
ddlAuto,
batchSize,
batchFetchSize);
log.info(startupMessage);
}
}

View File

@@ -0,0 +1,32 @@
package com.kamco.cd.kamcoback.config;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.module.SimpleModule;
import com.kamco.cd.kamcoback.common.utils.geometry.GeometryDeserializer;
import com.kamco.cd.kamcoback.common.utils.geometry.GeometrySerializer;
import org.locationtech.jts.geom.Geometry;
import org.locationtech.jts.geom.Point;
import org.locationtech.jts.geom.Polygon;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.converter.json.Jackson2ObjectMapperBuilder;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
@Configuration
public class WebConfig implements WebMvcConfigurer {
@Bean
public ObjectMapper objectMapper() {
SimpleModule module = new SimpleModule();
module.addSerializer(Geometry.class, new GeometrySerializer<>(Geometry.class));
module.addDeserializer(Geometry.class, new GeometryDeserializer<>(Geometry.class));
module.addSerializer(Polygon.class, new GeometrySerializer<>(Polygon.class));
module.addDeserializer(Polygon.class, new GeometryDeserializer<>(Polygon.class));
module.addSerializer(Point.class, new GeometrySerializer<>(Point.class));
module.addDeserializer(Point.class, new GeometryDeserializer<>(Point.class));
return Jackson2ObjectMapperBuilder.json().modulesToInstall(module).build();
}
}

View File

@@ -1,8 +1,8 @@
package com.kamco.cd.kamcoback.dto;
package com.kamco.cd.kamcoback.config.api;
import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.annotation.JsonInclude;
import com.kamco.cd.kamcoback.inferface.EnumType;
import com.kamco.cd.kamcoback.common.utils.enums.EnumType;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
import lombok.ToString;

View File

@@ -0,0 +1,120 @@
package com.kamco.cd.kamcoback.config.resttemplate;
import com.fasterxml.jackson.databind.ObjectMapper;
import java.nio.charset.StandardCharsets;
import java.util.List;
import lombok.RequiredArgsConstructor;
import lombok.extern.log4j.Log4j2;
import org.springframework.http.HttpEntity;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpMethod;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Component;
import org.springframework.web.client.HttpStatusCodeException;
import org.springframework.web.client.RestTemplate;
@RequiredArgsConstructor
@Component
@Log4j2
public class ExternalHttpClient {
private final RestTemplate restTemplate;
private final ObjectMapper objectMapper;
public <T> ExternalCallResult<T> call(
String url, HttpMethod method, Object body, HttpHeaders headers, Class<T> responseType) {
// responseType 기반으로 Accept 동적 세팅
HttpHeaders resolvedHeaders = resolveHeaders(headers, responseType);
logRequestBody(body);
HttpEntity<Object> entity = new HttpEntity<>(body, resolvedHeaders);
try {
// String: raw bytes -> UTF-8 string
if (responseType == String.class) {
ResponseEntity<byte[]> res = restTemplate.exchange(url, method, entity, byte[].class);
String raw =
(res.getBody() == null) ? null : new String(res.getBody(), StandardCharsets.UTF_8);
@SuppressWarnings("unchecked")
T casted = (T) raw;
return new ExternalCallResult<>(res.getStatusCodeValue(), true, casted, null);
}
// byte[]: raw bytes로 받고, JSON이면 에러로 처리
if (responseType == byte[].class) {
ResponseEntity<byte[]> res = restTemplate.exchange(url, method, entity, byte[].class);
MediaType ct = res.getHeaders().getContentType();
byte[] bytes = res.getBody();
if (isJsonLike(ct)) {
String err = (bytes == null) ? null : new String(bytes, StandardCharsets.UTF_8);
return new ExternalCallResult<>(res.getStatusCodeValue(), false, null, err);
}
@SuppressWarnings("unchecked")
T casted = (T) bytes;
return new ExternalCallResult<>(res.getStatusCodeValue(), true, casted, null);
}
// DTO 등: 일반 역직렬화
ResponseEntity<T> res = restTemplate.exchange(url, method, entity, responseType);
return new ExternalCallResult<>(res.getStatusCodeValue(), true, res.getBody(), null);
} catch (HttpStatusCodeException e) {
return new ExternalCallResult<>(
e.getStatusCode().value(), false, null, e.getResponseBodyAsString());
}
}
// 기존 resolveJsonHeaders를 "동적"으로 교체
private HttpHeaders resolveHeaders(HttpHeaders headers, Class<?> responseType) {
// 원본 headers를 그대로 쓰면 외부에서 재사용할 때 사이드이펙트 날 수 있어서 복사 권장
HttpHeaders h = (headers == null) ? new HttpHeaders() : new HttpHeaders(headers);
// 요청 바디 기본은 JSON이라고 가정 (필요하면 호출부에서 덮어쓰기)
if (h.getContentType() == null) {
h.setContentType(MediaType.APPLICATION_JSON);
}
// 호출부에서 Accept를 명시했으면 존중
if (h.getAccept() != null && !h.getAccept().isEmpty()) {
return h;
}
// responseType 기반 Accept 자동 지정
if (responseType == byte[].class) {
h.setAccept(
List.of(
MediaType.APPLICATION_OCTET_STREAM,
MediaType.valueOf("application/zip"),
MediaType.APPLICATION_JSON // 실패(JSON 에러 바디) 대비
));
} else {
h.setAccept(List.of(MediaType.APPLICATION_JSON));
}
return h;
}
private boolean isJsonLike(MediaType ct) {
if (ct == null) return false;
return ct.includes(MediaType.APPLICATION_JSON)
|| "application/problem+json".equalsIgnoreCase(ct.toString());
}
private void logRequestBody(Object body) {
try {
if (body != null) {
log.info("[HTTP-REQ-BODY-JSON] {}", objectMapper.writeValueAsString(body));
}
} catch (Exception e) {
log.warn("[HTTP-REQ-BODY-JSON] serialize failed: {}", e.getMessage());
}
}
public record ExternalCallResult<T>(int statusCode, boolean success, T body, String errBody) {}
}

View File

@@ -0,0 +1,33 @@
package com.kamco.cd.kamcoback.config.resttemplate;
import lombok.extern.log4j.Log4j2;
import org.springframework.boot.web.client.RestTemplateBuilder;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.client.BufferingClientHttpRequestFactory;
import org.springframework.http.client.SimpleClientHttpRequestFactory;
import org.springframework.web.client.RestTemplate;
@Log4j2
@Configuration
public class RestTemplateConfig {
@Bean
public RestTemplate restTemplate(RestTemplateBuilder builder) {
SimpleClientHttpRequestFactory baseFactory = new SimpleClientHttpRequestFactory();
baseFactory.setConnectTimeout(2000);
baseFactory.setReadTimeout(3000);
RestTemplate rt =
builder
.requestFactory(() -> new BufferingClientHttpRequestFactory(baseFactory))
.additionalInterceptors(new RetryInterceptor())
.build();
// byte[] 응답은 무조건 raw로 읽게 강제 (Jackson이 끼어들 여지 제거)
rt.getMessageConverters()
.add(0, new org.springframework.http.converter.ByteArrayHttpMessageConverter());
return rt;
}
}

View File

@@ -0,0 +1,56 @@
package com.kamco.cd.kamcoback.config.resttemplate;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.util.concurrent.TimeUnit;
import lombok.extern.log4j.Log4j2;
import org.springframework.http.HttpRequest;
import org.springframework.http.client.ClientHttpRequestExecution;
import org.springframework.http.client.ClientHttpRequestInterceptor;
import org.springframework.http.client.ClientHttpResponse;
@Log4j2
public class RetryInterceptor implements ClientHttpRequestInterceptor {
private static final int MAX_RETRY = 3;
private static final long WAIT_MILLIS = 3000;
@Override
public ClientHttpResponse intercept(
HttpRequest request, byte[] body, ClientHttpRequestExecution execution) throws IOException {
IOException lastException = null;
for (int attempt = 1; attempt <= MAX_RETRY; attempt++) {
try {
log.info("[WIRE-REQ] {} {}", request.getMethod(), request.getURI());
log.info("[WIRE-REQ-HEADERS] {}", request.getHeaders());
log.info("[WIRE-REQ-BODY] {}", new String(body, StandardCharsets.UTF_8));
ClientHttpResponse response = execution.execute(request, body);
log.info("[WIRE-RES-STATUS] {}", response.getStatusCode());
return response;
} catch (IOException e) {
lastException = e;
log.error("[WIRE-IO-ERR] attempt={} msg={}", attempt, e.getMessage(), e);
}
if (attempt < MAX_RETRY) {
sleep();
}
}
throw lastException;
}
private void sleep() throws IOException {
try {
TimeUnit.MILLISECONDS.sleep(WAIT_MILLIS);
} catch (InterruptedException ie) {
Thread.currentThread().interrupt();
throw new IOException("Retry interrupted", ie);
}
}
}

View File

@@ -0,0 +1,13 @@
package com.kamco.cd.kamcoback.config.swagger;
import io.swagger.v3.oas.annotations.enums.SecuritySchemeType;
import io.swagger.v3.oas.annotations.security.SecurityScheme;
import org.springframework.context.annotation.Configuration;
@Configuration
@SecurityScheme(
name = "BearerAuth",
type = SecuritySchemeType.HTTP,
scheme = "bearer",
bearerFormat = "JWT")
public class SwaggerConfig {}

View File

@@ -0,0 +1,97 @@
package com.kamco.cd.kamcoback.config.swagger;
import jakarta.servlet.http.HttpServletRequest;
import java.nio.charset.StandardCharsets;
import org.springdoc.core.properties.SwaggerUiConfigProperties;
import org.springdoc.core.properties.SwaggerUiOAuthProperties;
import org.springdoc.core.providers.ObjectMapperProvider;
import org.springdoc.webmvc.ui.SwaggerIndexPageTransformer;
import org.springdoc.webmvc.ui.SwaggerIndexTransformer;
import org.springdoc.webmvc.ui.SwaggerWelcomeCommon;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
import org.springframework.context.annotation.Profile;
import org.springframework.core.io.Resource;
import org.springframework.web.servlet.resource.ResourceTransformerChain;
import org.springframework.web.servlet.resource.TransformedResource;
@Profile({"local", "dev"})
@Configuration
public class SwaggerUiAutoAuthConfig {
@Bean
@Primary
public SwaggerIndexTransformer swaggerIndexTransformer(
SwaggerUiConfigProperties swaggerUiConfigProperties,
SwaggerUiOAuthProperties swaggerUiOAuthProperties,
SwaggerWelcomeCommon swaggerWelcomeCommon,
ObjectMapperProvider objectMapperProvider) {
SwaggerIndexPageTransformer delegate =
new SwaggerIndexPageTransformer(
swaggerUiConfigProperties,
swaggerUiOAuthProperties,
swaggerWelcomeCommon,
objectMapperProvider);
return new SwaggerIndexTransformer() {
private static final String TOKEN_KEY = "SWAGGER_ACCESS_TOKEN";
@Override
public Resource transform(
HttpServletRequest request, Resource resource, ResourceTransformerChain chain) {
try {
// 1) springdoc 기본 변환 먼저 적용
Resource transformed = delegate.transform(request, resource, chain);
String html =
new String(transformed.getInputStream().readAllBytes(), StandardCharsets.UTF_8);
String loginPathContains = "/api/auth/signin";
String inject =
"""
tagsSorter: (a, b) => {
const TOP = '인증(Auth)';
if (a === TOP && b !== TOP) return -1;
if (b === TOP && a !== TOP) return 1;
return a.localeCompare(b);
},
requestInterceptor: (req) => {
const token = localStorage.getItem('%s');
if (token) {
req.headers = req.headers || {};
req.headers['Authorization'] = 'Bearer ' + token;
}
return req;
},
responseInterceptor: async (res) => {
try {
const isLogin = (res?.url?.includes('%s') && res?.status === 200);
if (isLogin) {
const text = (typeof res.data === 'string') ? res.data : JSON.stringify(res.data);
const json = JSON.parse(text);
const token = json?.data?.accessToken;
if (token) {
localStorage.setItem('%s', token);
}
}
} catch (e) {}
return res;
},
"""
.formatted(TOKEN_KEY, loginPathContains, TOKEN_KEY);
html = html.replace("SwaggerUIBundle({", "SwaggerUIBundle({\n" + inject);
return new TransformedResource(transformed, html.getBytes(StandardCharsets.UTF_8));
} catch (Exception e) {
// 실패 시 원본 반환(문서 깨짐 방지)
return resource;
}
}
};
}
}

View File

@@ -1,20 +0,0 @@
package com.kamco.cd.kamcoback.enums;
public class CodeDto {
private String code;
private String name;
public CodeDto(String code, String name) {
this.code = code;
this.name = name;
}
public String getCode() {
return code;
}
public String getName() {
return name;
}
}

View File

@@ -1,86 +0,0 @@
package com.kamco.cd.kamcoback.enums;
import com.kamco.cd.kamcoback.inferface.CodeExpose;
import com.kamco.cd.kamcoback.inferface.CodeHidden;
import com.kamco.cd.kamcoback.inferface.EnumType;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import org.reflections.Reflections;
public class Enums {
private static final String BASE_PACKAGE = "com.kamco.cd.kamcoback";
/** 노출 가능한 enum만 모아둔 맵 key: enum simpleName (예: RoleType) value: enum Class */
private static final Map<String, Class<? extends Enum<?>>> exposedEnumMap = scanExposedEnumMap();
// code로 enum 찾기
public static <E extends Enum<E> & EnumType> E fromId(Class<E> enumClass, String id) {
if (id == null) {
return null;
}
for (E e : enumClass.getEnumConstants()) {
if (id.equalsIgnoreCase(e.getId())) {
return e;
}
}
return null;
}
// enum -> CodeDto list
public static List<CodeDto> toList(Class<? extends Enum<?>> enumClass) {
Object[] enums = enumClass.getEnumConstants();
return Arrays.stream(enums)
.map(e -> (EnumType) e)
.filter(e -> !isHidden(enumClass, (Enum<?>) e))
.map(e -> new CodeDto(e.getId(), e.getText()))
.toList();
}
private static boolean isHidden(Class<? extends Enum<?>> enumClass, Enum<?> e) {
try {
return enumClass.getField(e.name()).isAnnotationPresent(CodeHidden.class);
} catch (NoSuchFieldException ex) {
return false;
}
}
/** 특정 타입(enum)만 조회 /codes/{type} -> type = RoleType 같은 값 */
public static List<CodeDto> getCodes(String type) {
Class<? extends Enum<?>> enumClass = exposedEnumMap.get(type);
if (enumClass == null) {
throw new IllegalArgumentException("지원하지 않는 코드 타입: " + type);
}
return toList(enumClass);
}
/** 전체 enum 코드 조회 */
public static Map<String, List<CodeDto>> getAllCodes() {
Map<String, List<CodeDto>> result = new HashMap<>();
for (Map.Entry<String, Class<? extends Enum<?>>> e : exposedEnumMap.entrySet()) {
result.put(e.getKey(), toList(e.getValue()));
}
return result;
}
/** CodeExpose + EnumType 인 enum만 스캔해서 Map 구성 */
private static Map<String, Class<? extends Enum<?>>> scanExposedEnumMap() {
Reflections reflections = new Reflections(BASE_PACKAGE);
Set<Class<?>> types = reflections.getTypesAnnotatedWith(CodeExpose.class);
Map<String, Class<? extends Enum<?>>> result = new HashMap<>();
for (Class<?> clazz : types) {
if (clazz.isEnum() && EnumType.class.isAssignableFrom(clazz)) {
result.put(clazz.getSimpleName(), (Class<? extends Enum<?>>) clazz);
}
}
return result;
}
}

View File

@@ -6,7 +6,8 @@ import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.kamco.cd.kamcoback.common.utils.enums.ImageryFitStatus;
import com.kamco.cd.kamcoback.common.enums.DetectionClassification;
import com.kamco.cd.kamcoback.common.enums.ImageryFitStatus;
import com.kamco.cd.kamcoback.common.utils.interfaces.JsonFormatDttm;
import com.kamco.cd.kamcoback.inference.dto.InferenceResultDto.DetectOption;
import com.kamco.cd.kamcoback.inference.dto.InferenceResultDto.MapSheetScope;

View File

@@ -0,0 +1,58 @@
package com.kamco.cd.kamcoback.inference.dto;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
@Getter
@Setter
@NoArgsConstructor
public class InferenceProgressDto {
private pred_requests_areas pred_requests_areas;
private String modelVersion;
private String cdModelPath;
private String cdModelFileName;
private String cdModelConfigPath;
private String cdModelConfigFileName;
private String cdModelClsPath;
private String cdModelClsFileName;
private String clsModelVersion;
private Double priority;
public InferenceProgressDto(
pred_requests_areas pred_requests_areas,
String modelVersion,
String cdModelPath,
String cdModelFileName,
String cdModelConfigPath,
String cdModelConfigFileName,
String cdModelClsPath,
String cdModelClsFileName,
String clsModelVersion,
Double priority) {
this.pred_requests_areas = pred_requests_areas;
this.modelVersion = modelVersion;
this.cdModelPath = cdModelPath;
this.cdModelFileName = cdModelFileName;
this.cdModelConfigPath = cdModelConfigPath;
this.cdModelConfigFileName = cdModelConfigFileName;
this.cdModelClsPath = cdModelClsPath;
this.cdModelClsFileName = cdModelClsFileName;
this.clsModelVersion = clsModelVersion;
this.priority = priority;
}
@Getter
@Setter
@AllArgsConstructor
@NoArgsConstructor
public static class pred_requests_areas {
private Integer input1_year;
private Integer input2_year;
private String input1_scene_path;
private String input2_scene_path;
}
}

View File

@@ -246,15 +246,15 @@ public class InferenceResultDto {
@NotBlank
private String title;
@Schema(description = "M1", example = "b40e0f68-c1d8-49fc-93f9-a36270093861")
@Schema(description = "G1", example = "b40e0f68-c1d8-49fc-93f9-a36270093861")
@NotNull
private UUID model1Uuid;
@Schema(description = "M2", example = "ec92b7d2-b5a3-4915-9bdf-35fb3ca8ad27")
@Schema(description = "G2", example = "ec92b7d2-b5a3-4915-9bdf-35fb3ca8ad27")
@NotNull
private UUID model2Uuid;
@Schema(description = "M3", example = "37f45782-8ccf-4cf6-911c-a055a1510d39")
@Schema(description = "G3", example = "37f45782-8ccf-4cf6-911c-a055a1510d39")
@NotNull
private UUID model3Uuid;
@@ -297,6 +297,30 @@ public class InferenceResultDto {
@Schema(name = "InferenceStatusDetailDto", description = "추론(변화탐지) 진행상태")
public static class InferenceStatusDetailDto {
@Schema(description = "모델1 사용시간 시작일시")
@JsonFormatDttm
ZonedDateTime m1ModelStartDttm;
@Schema(description = "모델2 사용시간 시작일시")
@JsonFormatDttm
ZonedDateTime m2ModelStartDttm;
@Schema(description = "모델3 사용시간 시작일시")
@JsonFormatDttm
ZonedDateTime m3ModelStartDttm;
@Schema(description = "모델1 사용시간 종료일시")
@JsonFormatDttm
ZonedDateTime m1ModelEndDttm;
@Schema(description = "모델2 사용시간 종료일시")
@JsonFormatDttm
ZonedDateTime m2ModelEndDttm;
@Schema(description = "모델3 사용시간 종료일시")
@JsonFormatDttm
ZonedDateTime m3ModelEndDttm;
@Schema(description = "탐지대상 도엽수")
private Long detectingCnt;
@@ -336,30 +360,6 @@ public class InferenceResultDto {
@Schema(description = "모델3 분석 실패")
private Integer m3FailedJobs;
@Schema(description = "모델1 사용시간 시작일시")
@JsonFormatDttm
ZonedDateTime m1ModelStartDttm;
@Schema(description = "모델2 사용시간 시작일시")
@JsonFormatDttm
ZonedDateTime m2ModelStartDttm;
@Schema(description = "모델3 사용시간 시작일시")
@JsonFormatDttm
ZonedDateTime m3ModelStartDttm;
@Schema(description = "모델1 사용시간 종료일시")
@JsonFormatDttm
ZonedDateTime m1ModelEndDttm;
@Schema(description = "모델2 사용시간 종료일시")
@JsonFormatDttm
ZonedDateTime m2ModelEndDttm;
@Schema(description = "모델3 사용시간 종료일시")
@JsonFormatDttm
ZonedDateTime m3ModelEndDttm;
@Schema(description = "변화탐지 제목")
private String title;
@@ -496,19 +496,19 @@ public class InferenceResultDto {
return MapSheetScope.getDescByCode(this.mapSheetScope);
}
@Schema(description = "M1 사용시간")
@Schema(description = "G1 사용시간")
@JsonProperty("m1ElapsedTim")
public String getM1ElapsedTime() {
return formatElapsedTime(this.m1ModelStartDttm, this.m1ModelEndDttm);
}
@Schema(description = "M2 사용시간")
@Schema(description = "G2 사용시간")
@JsonProperty("m2ElapsedTim")
public String getM2ElapsedTime() {
return formatElapsedTime(this.m2ModelStartDttm, this.m2ModelEndDttm);
}
@Schema(description = "M3 사용시간")
@Schema(description = "G3 사용시간")
@JsonProperty("m3ElapsedTim")
public String getM3ElapsedTime() {
return formatElapsedTime(this.m3ModelStartDttm, this.m3ModelEndDttm);

View File

@@ -0,0 +1,113 @@
package com.kamco.cd.kamcoback.inference.dto;
import com.kamco.cd.kamcoback.postgres.entity.MapSheetAnalDataInferenceGeomEntity;
import io.swagger.v3.oas.annotations.media.Schema;
import java.util.UUID;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import org.locationtech.jts.geom.Geometry;
public class InferenceResultShpDto {
@Getter
@Setter
public static class Basic {
// ===== 식별 =====
private Long geoUid;
private UUID uuid;
// ===== 그룹 키 =====
private Integer stage;
private Long mapId;
private Integer input1; // compare_yyyy
private Integer input2; // target_yyyy
// ===== 추론 결과 =====
private Double cdProb;
private String beforeClass;
private Double beforeProbability;
private String afterClass;
private Double afterProbability;
// ===== 공간 정보 =====
private Geometry geometry;
private Double area;
/** Entity → DTO 변환 */
public static Basic from(MapSheetAnalDataInferenceGeomEntity e) {
Basic d = new Basic();
d.geoUid = e.getGeoUid();
d.uuid = e.getUuid();
d.stage = e.getStage();
d.mapId = e.getMapSheetNum();
d.input1 = e.getCompareYyyy();
d.input2 = e.getTargetYyyy();
d.cdProb = e.getCdProb();
d.beforeClass = e.getClassBeforeCd();
d.beforeProbability = e.getClassBeforeProb();
d.afterClass = e.getClassAfterCd();
d.afterProbability = e.getClassAfterProb();
d.geometry = e.getGeom();
d.area = e.getArea();
return d;
}
}
@Setter
@Getter
@AllArgsConstructor
@NoArgsConstructor
public static class InferenceCntDto {
@Schema(description = "추론 결과(inference_results)를 기준으로 신규 목록 저장 터이터 건수", example = "120")
int sheetAnalDataCnt;
@Schema(description = "추론 결과(inference_results)를 기준으로 신규 저장 데이터 건수", example = "120")
int inferenceCnt;
@Schema(description = "추론 결과(inference_results)를 기준으로 신규 저장 Geom 데이터 건수", example = "120")
int inferenceGeomCnt;
}
@Setter
@Getter
@AllArgsConstructor
@NoArgsConstructor
public static class FileCntDto {
@Schema(description = "shp 파일 생성 수 (덮어쓰기 포함)", example = "120")
private int shp;
@Schema(description = "shx 파일 생성 수 (덮어쓰기 포함)", example = "120")
private int shx;
@Schema(description = "dbf 파일 생성 수 (덮어쓰기 포함)", example = "120")
private int dbf;
@Schema(description = "prj 파일 생성 수 (덮어쓰기 포함)", example = "120")
private int prj;
@Schema(description = "geojson 파일 생성 수 (덮어쓰기 포함)", example = "120")
private int geojson;
}
@Getter
public static class CreateShpRequest {
private Long m1BatchId;
private Long m2BatchId;
private Long m3BatchId;
}
}

View File

@@ -0,0 +1,25 @@
package com.kamco.cd.kamcoback.inference.dto;
import com.kamco.cd.kamcoback.postgres.entity.InferenceResultsTestingEntity;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
public class InferenceResultsTestingDto {
@Getter
@Setter
@AllArgsConstructor
@NoArgsConstructor
public static class ShpDto {
private Long batchId;
private String uid;
private String mapId;
public static ShpDto fromEntity(InferenceResultsTestingEntity e) {
return new ShpDto(e.getBatchId(), e.getUid(), e.getMapId());
}
}
}

View File

@@ -0,0 +1,38 @@
package com.kamco.cd.kamcoback.inference.dto;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import lombok.ToString;
/** AI API 추론 실행 DTO */
@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
@ToString
public class InferenceSendDto {
private pred_requests_areas pred_requests_areas;
private String model_version;
private String cd_model_path;
private String cd_model_config;
private String cls_model_path;
private String cls_model_version;
private String cd_model_type;
private Double priority;
@Getter
@Setter
@AllArgsConstructor
@NoArgsConstructor
@ToString
public static class pred_requests_areas {
private Integer input1_year;
private Integer input2_year;
private String input1_scene_path;
private String input2_scene_path;
}
}

View File

@@ -0,0 +1,180 @@
package com.kamco.cd.kamcoback.inference.dto;
import io.swagger.v3.oas.annotations.media.Schema;
import java.util.List;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
/** DTO classes for learning model result processing */
public class LearningModelResultDto {
@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
@Schema(description = "학습모델 결과 처리 요청")
public static class ProcessRequest {
@Schema(
description = "GeoJSON 파일 경로",
example =
"src/main/resources/db/migration/sample-results_updated/캠코_2021_2022_35813023.geojson")
private String filePath;
}
@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
@Schema(description = "학습모델 결과 처리 응답")
public static class ProcessResponse {
@Schema(description = "처리 성공 여부")
private boolean success;
@Schema(description = "처리 결과 메시지")
private String message;
@Schema(description = "처리된 feature 개수")
private int processedFeatures;
@Schema(description = "처리된 파일 경로")
private String filePath;
}
@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
@Schema(description = "학습모델 결과 일괄 처리 요청")
public static class BatchProcessRequest {
@Schema(description = "GeoJSON 파일 경로 목록")
private List<String> filePaths;
}
@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
@Schema(description = "학습모델 결과 일괄 처리 응답")
public static class BatchProcessResponse {
@Schema(description = "처리 성공 여부")
private boolean success;
@Schema(description = "처리 결과 메시지")
private String message;
@Schema(description = "전체 처리된 feature 개수")
private int totalProcessedFeatures;
@Schema(description = "처리된 파일 개수")
private int processedFileCount;
@Schema(description = "처리된 파일 경로 목록")
private List<String> filePaths;
}
@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
@Schema(description = "학습모델 처리 상태")
public static class ProcessingStatus {
@Schema(description = "처리 ID")
private String processingId;
@Schema(description = "처리 상태 (PENDING, PROCESSING, COMPLETED, FAILED)")
private String status;
@Schema(description = "진행률 (0-100)")
private int progressPercentage;
@Schema(description = "현재 처리 중인 파일")
private String currentFile;
@Schema(description = "전체 파일 개수")
private int totalFiles;
@Schema(description = "처리 완료된 파일 개수")
private int completedFiles;
@Schema(description = "시작 시간")
private String startTime;
@Schema(description = "예상 완료 시간")
private String estimatedEndTime;
}
@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
@Schema(description = "학습모델 데이터 요약")
public static class DataSummary {
@Schema(description = "전체 데이터 개수")
private long totalRecords;
@Schema(description = "연도별 데이터 개수")
private List<YearDataCount> yearDataCounts;
@Schema(description = "분류별 데이터 개수")
private List<ClassDataCount> classDataCounts;
@Schema(description = "지도 영역별 데이터 개수")
private List<MapSheetDataCount> mapSheetDataCounts;
}
@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
@Schema(description = "연도별 데이터 개수")
public static class YearDataCount {
@Schema(description = "비교 연도 (예: 2021_2022)")
private String compareYear;
@Schema(description = "데이터 개수")
private long count;
}
@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
@Schema(description = "분류별 데이터 개수")
public static class ClassDataCount {
@Schema(description = "분류명")
private String className;
@Schema(description = "변화 전 개수")
private long beforeCount;
@Schema(description = "변화 후 개수")
private long afterCount;
}
@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
@Schema(description = "지도 영역별 데이터 개수")
public static class MapSheetDataCount {
@Schema(description = "지도 영역 번호")
private String mapSheetNum;
@Schema(description = "데이터 개수")
private long count;
@Schema(description = "평균 변화 탐지 확률")
private double avgChangeDetectionProb;
}
}

View File

@@ -0,0 +1,17 @@
package com.kamco.cd.kamcoback.inference.dto;
public record WriteCnt(int shp, int shx, int dbf, int prj, int geojson) {
public static WriteCnt zero() {
return new WriteCnt(0, 0, 0, 0, 0);
}
public WriteCnt plus(WriteCnt o) {
return new WriteCnt(
this.shp + o.shp,
this.shx + o.shx,
this.dbf + o.dbf,
this.prj + o.prj,
this.geojson + o.geojson);
}
}

View File

@@ -1,10 +0,0 @@
package com.kamco.cd.kamcoback.inferface;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface CodeExpose {}

View File

@@ -1,10 +0,0 @@
package com.kamco.cd.kamcoback.inferface;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.FIELD)
public @interface CodeHidden {}

View File

@@ -1,8 +0,0 @@
package com.kamco.cd.kamcoback.inferface;
public interface EnumType {
String getId();
String getText();
}

View File

@@ -1,19 +0,0 @@
package com.kamco.cd.kamcoback.inferface;
import com.fasterxml.jackson.annotation.JacksonAnnotationsInside;
import com.fasterxml.jackson.annotation.JsonFormat;
import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Target({ElementType.FIELD, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@JacksonAnnotationsInside
@JsonFormat(
shape = JsonFormat.Shape.STRING,
pattern = "yyyy-MM-dd'T'HH:mm:ssXXX",
timezone = "Asia/Seoul")
public @interface JsonFormatDttm {}

View File

@@ -1,8 +1,9 @@
package com.kamco.cd.kamcoback.dto;
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.LocalDate;
import java.time.ZonedDateTime;
import java.util.List;
import java.util.UUID;
@@ -359,4 +360,41 @@ public class LabelAllocateDto {
@Schema(description = "작업기간 종료일")
private ZonedDateTime projectCloseDttm;
}
@Getter
@Setter
@AllArgsConstructor
@NoArgsConstructor
public static class InferenceLearnDto {
private UUID analUuid;
private String learnUid;
private String analState;
private Long analId;
}
@Getter
@Setter
@AllArgsConstructor
public static class AllocateAddStbltDto {
@Schema(description = "총 잔여 건수", example = "179")
private Integer totalCnt;
@Schema(
description = "추가할당할 라벨러",
example =
"""
[
"123454", "654321", "222233", "777222"
]
""")
private List<String> labelers;
@Schema(description = "회차 마스터 key", example = "c0e77cc7-8c28-46ba-9ca4-11e90246ab44")
private UUID uuid;
@Schema(description = "기준일자", example = "2026-02-20")
private LocalDate baseDate;
}
}

View File

@@ -1,4 +1,4 @@
package com.kamco.cd.kamcoback.dto;
package com.kamco.cd.kamcoback.label.dto;
import java.time.ZonedDateTime;
import java.util.UUID;

View File

@@ -1,4 +1,4 @@
package com.kamco.cd.kamcoback.dto;
package com.kamco.cd.kamcoback.label.dto;
import java.time.ZonedDateTime;
import java.util.UUID;

View File

@@ -0,0 +1,263 @@
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.LocalDate;
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 Integer stage;
@JsonFormatDttm private ZonedDateTime gukyuinApplyDttm;
private Long detectionTotCnt;
private Long labelTotCnt;
private Long labelAssignCnt;
private Long labelSkipTotCnt;
private Long labelCompleteTotCnt;
@JsonFormatDttm private ZonedDateTime labelStartDttm;
// tb_map_sheet_anal_inference.anal_state 컬럼 값
private String analState;
// tb_labeling_assignment 테이블에서 stagnation_yn = 'N'인 정상 진행 건수
private Long normalProgressCnt;
// tb_labeling_assignment 테이블에서 총 배정 건수
private Long totalAssignmentCnt;
private String labelingClosedYn;
private String inspectionClosedYn;
private Long inspectorCompleteTotCnt;
private Long inspectorRemainCnt;
private ZonedDateTime projectCloseDttm;
private String resultUid;
private String subUid;
private UUID learnUuid;
@JsonProperty("detectYear")
public String getDetectYear() {
if (compareYyyy == null || targetYyyy == null) {
return null;
}
return compareYyyy + "-" + targetYyyy;
}
/** 라벨링 상태 반환 (tb_map_sheet_anal_inference.anal_state 기준) */
public String getLabelState() {
// anal_state 값이 있으면 해당 값 사용 -> 우선은 미사용
if (this.analState != null && !this.analState.isEmpty()) {
return this.analState;
}
// anal_state 값이 없으면 기존 로직으로 폴백
String mngState = LabelMngState.PENDING.getId();
if (this.labelTotCnt == 0) {
mngState = LabelMngState.PENDING.getId();
} else if (this.labelTotCnt > 0 && this.labelAssignCnt > 0 && this.labelCompleteTotCnt == 0) {
mngState = LabelMngState.ASSIGNED.getId();
} else if (this.labelingClosedYn.equals("Y") && this.inspectionClosedYn.equals("Y")) {
mngState = LabelMngState.FINISH.getId();
} else if (this.labelCompleteTotCnt > 0) {
mngState = LabelMngState.ING.getId();
}
return mngState;
}
public String getLabelStateName() {
String enumId = this.getLabelState();
if (enumId == null || enumId.isEmpty()) {
enumId = "PENDING";
}
LabelMngState type = Enums.fromId(LabelMngState.class, enumId);
// type이 null인 경우 (enum에 정의되지 않은 상태값) 상태값 자체를 반환
if (type == null) {
return enumId;
}
return type.getText();
}
/**
* 작업 진행률 반환 (tb_labeling_assignment.stagnation_yn = 'N'인 정상 진행율 기준) 계산식: (정상 진행 건수 / 총 배정 건수) *
* 100
*/
public double getLabelRate() {
if (this.totalAssignmentCnt == null || this.totalAssignmentCnt == 0) {
return 0.0;
}
if (this.labelCompleteTotCnt == null) {
return 0.0;
}
return Math.round(((double) this.labelCompleteTotCnt / this.totalAssignmentCnt * 100.0) * 100)
/ 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 = "2026-01-01")
private LocalDate strtDttm;
@Schema(description = "종료일", example = "2026-12-01")
private LocalDate 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 = "3일전처리개수")
private Long day3AgoDoneCnt;
@Schema(description = "2일전처리개수")
private Long day2AgoDoneCnt;
@Schema(description = "1일전처리개수")
private Long day1AgoDoneCnt;
@Schema(description = "계정 상태")
private String memberStatus;
public Long getRemainCnt() {
return this.assignedCnt - this.doneCnt;
}
public double getDoneRate() {
long dayDoneCnt = this.day3AgoDoneCnt + this.day2AgoDoneCnt + this.day1AgoDoneCnt;
if (dayDoneCnt == 0) {
return 0.0;
}
return (double) dayDoneCnt / 3;
}
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;
@Schema(description = "정렬(remindCnt desc, doneCnt desc)", example = "remindCnt desc")
private String sort;
public Pageable toPageable() {
return PageRequest.of(page, size);
}
}
}

View File

@@ -0,0 +1,240 @@
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 jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.Pattern;
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;
@Schema(description = "라벨링 종료 여부 (Y: 종료, N: 진행중)")
private String labelingClosedYn;
@Schema(description = "검수 종료 여부 (Y: 종료, N: 진행중)")
private String inspectionClosedYn;
}
@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
@Schema(description = "프로젝트 종료 여부 업데이트 요청")
public static class UpdateClosedRequest {
@Schema(
description = "프로젝트 UUID (선택) - 미입력 시 현재 진행중인 최신 프로젝트가 대상",
example = "f97dc186-e6d3-4645-9737-3173dde8dc64")
private String uuid;
@Pattern(
regexp = "^(LABELING|INSPECTION|BOTH)$",
message = "종료 유형은 LABELING, INSPECTION 또는 BOTH 이어야 합니다.")
@Schema(
description = "종료 유형 (LABELING: 라벨링만, INSPECTION: 검수만, BOTH: 라벨링+검수 동시)",
example = "LABELING",
allowableValues = {"LABELING", "INSPECTION", "BOTH"},
requiredMode = Schema.RequiredMode.NOT_REQUIRED)
private String closedType;
@NotBlank(message = "종료 여부는 필수입니다.")
@Pattern(regexp = "^[YN]$", message = "종료 여부는 Y 또는 N이어야 합니다.")
@Schema(
description = "종료 여부 (Y: 종료, N: 진행중)",
example = "Y",
allowableValues = {"Y", "N"},
requiredMode = Schema.RequiredMode.REQUIRED)
private String closedYn;
}
@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;
}
@Getter
@Setter
@Builder
@NoArgsConstructor
@AllArgsConstructor
@Schema(description = "작업자 목록 응답 (작업 정보 포함)")
public static class WorkerListResponse {
@Schema(description = "프로젝트 기본 정보 (상단 표시용)")
private ProjectInfo projectInfo;
@Schema(description = "작업 진행 현황 정보")
private WorkProgressInfo progressInfo;
// workers 필드는 제거되었습니다 (프로젝트 정보와 진행현황만 반환)
}
}

View File

@@ -0,0 +1,265 @@
package com.kamco.cd.kamcoback.members.dto;
import com.fasterxml.jackson.annotation.JsonIgnore;
import com.kamco.cd.kamcoback.common.enums.RoleType;
import com.kamco.cd.kamcoback.common.enums.StatusType;
import com.kamco.cd.kamcoback.common.utils.enums.Enums;
import com.kamco.cd.kamcoback.common.utils.interfaces.EnumValid;
import com.kamco.cd.kamcoback.common.utils.interfaces.JsonFormatDttm;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.Size;
import java.time.ZonedDateTime;
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;
public class MembersDto {
@Getter
@Setter
public static class Basic {
private Long id;
private UUID uuid;
private String userRole;
private String userRoleName;
private String name;
private String employeeNo;
private String status;
private String statusName;
@JsonFormatDttm private ZonedDateTime createdDttm;
@JsonFormatDttm private ZonedDateTime firstLoginDttm;
@JsonFormatDttm private ZonedDateTime lastLoginDttm;
@JsonFormatDttm private ZonedDateTime statusChgDttm;
public Basic(
Long id,
UUID uuid,
String userRole,
String name,
String employeeNo,
String status,
ZonedDateTime createdDttm,
ZonedDateTime firstLoginDttm,
ZonedDateTime lastLoginDttm,
ZonedDateTime statusChgDttm,
Boolean pwdResetYn) {
this.id = id;
this.uuid = uuid;
this.userRole = userRole;
this.userRoleName = getUserRoleName(userRole);
this.name = name;
this.employeeNo = employeeNo;
this.status = status;
this.statusName = getStatusName(status, pwdResetYn);
this.createdDttm = createdDttm;
this.firstLoginDttm = firstLoginDttm;
this.lastLoginDttm = lastLoginDttm;
this.statusChgDttm = statusChgDttm;
}
private String getUserRoleName(String roleId) {
RoleType type = Enums.fromId(RoleType.class, roleId);
return type.getText();
}
private String getStatusName(String status, Boolean pwdResetYn) {
StatusType type = Enums.fromId(StatusType.class, status);
pwdResetYn = pwdResetYn != null && pwdResetYn;
if (type.equals(StatusType.PENDING) && pwdResetYn) {
type = StatusType.ACTIVE;
}
return type.getText();
}
}
@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
public static class SearchReq {
@Schema(description = "전체, 관리자(ADMIN), 라벨러(LABELER), 검수자(REVIEWER)", example = "")
private String userRole;
@Schema(description = "키워드", example = "홍길동")
private String keyword;
// 페이징 파라미터
@Schema(description = "페이지 번호 (0부터 시작) ", example = "0")
private int page = 0;
@Schema(description = "페이지 크기", example = "20")
private int size = 20;
public Pageable toPageable() {
return PageRequest.of(page, size);
}
}
@Getter
@Setter
public static class AddReq {
@Schema(description = "관리자 유형", example = "ADMIN")
@NotBlank
@EnumValid(enumClass = RoleType.class, message = "userRole은 ADMIN, LABELER, REVIEWER 만 가능합니다.")
private String userRole;
@Schema(description = "사번", example = "123456")
@Size(max = 6)
private String employeeNo;
@Schema(description = "이름", example = "홍길동")
@NotBlank
@Size(min = 2, max = 100)
private String name;
@NotBlank
@Schema(description = "패스워드", example = "")
@Size(max = 255)
private String password;
public AddReq(String userRole, String employeeNo, String name, String password) {
this.userRole = userRole;
this.employeeNo = employeeNo;
this.name = name;
this.password = password;
}
}
@Getter
@Setter
public static class UpdateReq {
@Schema(description = "이름", example = "홍길동")
@Size(min = 2, max = 100)
private String name;
@Schema(description = "상태", example = "ACTIVE")
@EnumValid(enumClass = StatusType.class, message = "status는 ACTIVE, INACTIVE, DELETED 만 가능합니다.")
private String status;
@Schema(description = "패스워드", example = "")
@Size(max = 255)
private String password;
public UpdateReq(String name, String status, String password) {
this.name = name;
this.status = status;
this.password = password;
}
}
@Getter
@Setter
public static class InitReq {
@Schema(description = "기존 패스워드", example = "")
@Size(max = 255)
@NotBlank
private String oldPassword;
@Schema(description = "신규 패스워드", example = "")
@Size(max = 255)
@NotBlank
private String newPassword;
}
@Getter
@Setter
@AllArgsConstructor
@NoArgsConstructor
public static class Member {
private Long id;
private String name;
private String employeeNo;
private String role;
}
@Getter
public static class RoleEntity {
private final RoleType type;
private final String name;
public RoleEntity(RoleType status) {
this.type = status;
this.name = status.getText();
}
}
@Getter
public static class StatusEntity {
private final StatusType type;
private final String name;
public StatusEntity(StatusType status) {
this.type = status;
this.name = status.getText();
}
}
@Getter
public static class EntityData {
@JsonIgnore private Long id;
private UUID uuid;
private String name;
private String employeeNo;
private RoleEntity role;
private StatusEntity status;
@JsonFormatDttm private ZonedDateTime createdDttm;
@JsonFormatDttm private ZonedDateTime firstLoginDttm;
@JsonFormatDttm private ZonedDateTime lastLoginDttm;
@JsonFormatDttm private ZonedDateTime statusChgDttm;
private Boolean isReset;
public EntityData(
Long id,
UUID uuid,
RoleType role,
String name,
String employeeNo,
StatusType status,
ZonedDateTime createdDttm,
ZonedDateTime firstLoginDttm,
ZonedDateTime lastLoginDttm,
ZonedDateTime statusChgDttm,
Boolean isReset) {
this.id = id;
this.uuid = uuid;
this.name = name;
this.employeeNo = employeeNo;
this.status = new StatusEntity(status);
this.createdDttm = createdDttm;
this.firstLoginDttm = firstLoginDttm;
this.lastLoginDttm = lastLoginDttm;
this.statusChgDttm = statusChgDttm;
this.isReset = isReset;
this.role = new RoleEntity(role);
}
private String getUserRoleName(String roleId) {
RoleType type = Enums.fromId(RoleType.class, roleId);
return type.getText();
}
private String getStatusName(String status, Boolean pwdResetYn) {
StatusType type = Enums.fromId(StatusType.class, status);
pwdResetYn = pwdResetYn != null && pwdResetYn;
if (type.equals(StatusType.PENDING) && pwdResetYn) {
type = StatusType.ACTIVE;
}
return type.getText();
}
}
}

View File

@@ -0,0 +1,50 @@
package com.kamco.cd.kamcoback.members.exception;
import lombok.Getter;
@Getter
public class MemberException {
// *** Duplicate Member Exception ***
@Getter
public static class DuplicateMemberException extends RuntimeException {
public enum Field {
USER_ID,
EMPLOYEE_NO,
DEFAULT
}
private final Field field;
private final String value;
public DuplicateMemberException(Field field, String value) {
super(field.name() + " duplicate: " + value);
this.field = field;
this.value = value;
}
}
// *** Member Not Found Exception ***
public static class MemberNotFoundException extends RuntimeException {
public MemberNotFoundException() {
super("Member not found");
}
public MemberNotFoundException(String message) {
super(message);
}
}
public static class PasswordNotFoundException extends RuntimeException {
public PasswordNotFoundException() {
super("Password not found");
}
public PasswordNotFoundException(String message) {
super(message);
}
}
}

View File

@@ -0,0 +1,22 @@
package com.kamco.cd.kamcoback.postgres;
import jakarta.persistence.Column;
import jakarta.persistence.MappedSuperclass;
import jakarta.persistence.PrePersist;
import java.time.ZonedDateTime;
import lombok.Getter;
import org.springframework.data.annotation.CreatedDate;
@Getter
@MappedSuperclass
public class CommonCreateEntity {
@CreatedDate
@Column(name = "created_dttm", updatable = false, nullable = false)
private ZonedDateTime createdDate;
@PrePersist
protected void onPersist() {
this.createdDate = ZonedDateTime.now();
}
}

View File

@@ -1,4 +1,4 @@
package com.kamco.cd.kamcoback.postgres.entity;
package com.kamco.cd.kamcoback.postgres;
import jakarta.persistence.Column;
import jakarta.persistence.MappedSuperclass;

View File

@@ -1,15 +1,16 @@
package com.kamco.cd.kamcoback.config;
package com.kamco.cd.kamcoback.postgres;
import com.querydsl.jpa.impl.JPAQueryFactory;
import jakarta.persistence.EntityManager;
import jakarta.persistence.PersistenceContext;
import lombok.RequiredArgsConstructor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@RequiredArgsConstructor
@Configuration
public class QuerydslConfig {
public class QueryDslConfig {
@PersistenceContext private EntityManager entityManager;
private final EntityManager entityManager;
@Bean
public JPAQueryFactory jpaQueryFactory() {

View File

@@ -0,0 +1,30 @@
package com.kamco.cd.kamcoback.postgres;
import com.querydsl.core.types.Order;
import com.querydsl.core.types.OrderSpecifier;
import com.querydsl.core.types.dsl.PathBuilder;
import org.springframework.data.domain.Pageable;
public class QuerydslOrderUtil {
/**
* Pageable의 Sort 정보를 QueryDSL OrderSpecifier 배열로 변환
*
* @param pageable Spring Pageable
* @param entityClass 엔티티 클래스 (예: User.class)
* @param alias Q 엔티티 alias (예: "user")
*/
public static <T> OrderSpecifier<?>[] getOrderSpecifiers(
Pageable pageable, Class<T> entityClass, String alias) {
PathBuilder<T> entityPath = new PathBuilder<>(entityClass, alias);
return pageable.getSort().stream()
.map(
sort -> {
Order order = sort.isAscending() ? Order.ASC : Order.DESC;
// PathBuilder.get()는 컬럼명(String)을 동적 Path로 반환
return new OrderSpecifier<>(
order, entityPath.get(sort.getProperty(), Comparable.class));
})
.toArray(OrderSpecifier[]::new);
}
}

View File

@@ -1,8 +1,9 @@
package com.kamco.cd.kamcoback.postgres.core;
import com.kamco.cd.kamcoback.dto.TrainingDataReviewJobDto.InspectorPendingDto;
import com.kamco.cd.kamcoback.dto.TrainingDataReviewJobDto.Tasks;
import com.kamco.cd.kamcoback.postgres.repository.scheduler.TrainingDataLabelJobRepository;
import com.kamco.cd.kamcoback.scheduler.dto.TrainingDataReviewJobDto.InspectorPendingDto;
import com.kamco.cd.kamcoback.scheduler.dto.TrainingDataReviewJobDto.Tasks;
import java.time.LocalDate;
import java.util.List;
import java.util.UUID;
import lombok.RequiredArgsConstructor;
@@ -14,8 +15,8 @@ public class TrainingDataLabelJobCoreService {
private final TrainingDataLabelJobRepository trainingDataLabelJobRepository;
public List<Tasks> findCompletedYesterdayUnassigned() {
return trainingDataLabelJobRepository.findCompletedYesterdayUnassigned();
public List<Tasks> findCompletedYesterdayUnassigned(LocalDate baseDate) {
return trainingDataLabelJobRepository.findCompletedYesterdayUnassigned(baseDate);
}
public void assignReviewerBatch(List<UUID> assignmentUids, String reviewerId) {

View File

@@ -0,0 +1,87 @@
package com.kamco.cd.kamcoback.postgres.entity;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.Id;
import jakarta.persistence.Table;
import jakarta.validation.constraints.NotNull;
import jakarta.validation.constraints.Size;
import java.time.ZonedDateTime;
import lombok.Getter;
import lombok.Setter;
import org.hibernate.annotations.ColumnDefault;
import org.locationtech.jts.geom.Geometry;
@Getter
@Setter
@Entity
@Table(name = "inference_results_testing")
public class InferenceResultsTestingEntity {
@Column(name = "probability")
private Double probability;
@Column(name = "before_year")
private Long beforeYear;
@Column(name = "after_year")
private Long afterYear;
@Column(name = "map_id", length = Integer.MAX_VALUE)
private String mapId;
@Column(name = "model_version", length = Integer.MAX_VALUE)
private String modelVersion;
@Column(name = "cls_model_path", length = Integer.MAX_VALUE)
private String clsModelPath;
@Column(name = "cls_model_version", length = Integer.MAX_VALUE)
private String clsModelVersion;
@Column(name = "cd_model_type", length = Integer.MAX_VALUE)
private String cdModelType;
@Column(name = "id")
private Long id;
@Column(name = "model_name", length = Integer.MAX_VALUE)
private String modelName;
@Column(name = "batch_id")
private Long batchId;
@Column(name = "area")
private Double area;
@Column(name = "before_c", length = Integer.MAX_VALUE)
private String beforeC;
@Column(name = "before_p")
private Double beforeP;
@Column(name = "after_c", length = Integer.MAX_VALUE)
private String afterC;
@Column(name = "after_p")
private Double afterP;
@Id
@NotNull
@ColumnDefault("nextval('inference_results_testing_seq_seq')")
@Column(name = "seq", nullable = false)
private Long seq;
@ColumnDefault("now()")
@Column(name = "created_date")
private ZonedDateTime createdDate;
@Size(max = 32)
@NotNull
@ColumnDefault("upper(replace((uuid_generate_v4()), '-', ''))")
@Column(name = "uid", nullable = false, length = 32)
private String uid;
@Column(name = "geometry", columnDefinition = "geometry")
private Geometry geometry;
}

View File

@@ -1,6 +1,7 @@
package com.kamco.cd.kamcoback.postgres.entity;
import com.kamco.cd.kamcoback.dto.LabelAllocateDto;
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;

View File

@@ -1,6 +1,7 @@
package com.kamco.cd.kamcoback.postgres.entity;
import com.kamco.cd.kamcoback.dto.LabelInspectorDto;
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;

View File

@@ -1,5 +1,7 @@
package com.kamco.cd.kamcoback.postgres.entity;
import com.kamco.cd.kamcoback.inference.dto.InferenceDetailDto.MapSheet;
import com.kamco.cd.kamcoback.postgres.CommonDateEntity;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
@@ -45,4 +47,8 @@ public class MapInkx50kEntity extends CommonDateEntity {
this.mapidNo = mapidNo;
this.geom = geom;
}
public MapSheet toEntity() {
return new MapSheet(mapidcdNo, mapidNm);
}
}

View File

@@ -1,8 +1,10 @@
package com.kamco.cd.kamcoback.postgres.entity;
import com.kamco.cd.kamcoback.enums.CommonUseStatus;
import com.kamco.cd.kamcoback.common.enums.CommonUseStatus;
import com.kamco.cd.kamcoback.inference.dto.InferenceDetailDto;
import com.kamco.cd.kamcoback.inference.dto.InferenceDetailDto.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;
@@ -53,10 +55,6 @@ public class MapInkx5kEntity extends CommonDateEntity {
@Enumerated(EnumType.STRING)
private CommonUseStatus useInference;
public InferenceDetailDto.MapSheet toEntity() {
return new MapSheet(mapidcdNo, mapidNm);
}
// Constructor
public MapInkx5kEntity(
String mapidcdNo, String mapidNm, Geometry geom, MapInkx50kEntity mapInkx50k) {
@@ -72,4 +70,18 @@ public class MapInkx5kEntity extends CommonDateEntity {
public void updateUseInference(CommonUseStatus useInference) {
this.useInference = useInference;
}
public InferenceDetailDto.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();
}
}

View File

@@ -0,0 +1,167 @@
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.Table;
import jakarta.validation.constraints.Size;
import java.time.ZonedDateTime;
import lombok.Getter;
import lombok.Setter;
import org.hibernate.annotations.ColumnDefault;
@Getter
@Setter
@Entity
@Table(name = "tb_map_sheet_anal_data_inference")
public class MapSheetAnalDataInferenceEntity {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "data_uid", nullable = false)
private Long id;
// @Size(max = 128)
// @Column(name = "data_name", length = 128)
// private String dataName;
//
// @Size(max = 255)
// @Column(name = "data_path")
// private String dataPath;
//
// @Size(max = 128)
// @Column(name = "data_type", length = 128)
// private String dataType;
//
// @Size(max = 128)
// @Column(name = "data_crs_type", length = 128)
// private String dataCrsType;
//
// @Size(max = 255)
// @Column(name = "data_crs_type_name")
// private String dataCrsTypeName;
@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;
@Column(name = "compare_yyyy")
private Integer compareYyyy;
@Column(name = "target_yyyy")
private Integer targetYyyy;
// @Column(name = "data_json", length = Integer.MAX_VALUE)
// private String dataJson;
//
// @Size(max = 20)
// @ColumnDefault("'0'")
// @Column(name = "data_state", length = 20)
// private String dataState;
// @ColumnDefault("now()")
// @Column(name = "data_state_dttm")
// private ZonedDateTime dataStateDttm;
//
// @Column(name = "anal_strt_dttm")
// private ZonedDateTime analStrtDttm;
//
// @Column(name = "anal_end_dttm")
// private ZonedDateTime analEndDttm;
//
// @ColumnDefault("0")
// @Column(name = "anal_sec")
// private Long analSec;
@Size(max = 20)
@Column(name = "anal_state", length = 20)
private String analState;
@Column(name = "anal_uid")
private Long analUid;
@Column(name = "map_sheet_num")
private Long mapSheetNum;
// @ColumnDefault("0")
// @Column(name = "detecting_cnt")
// private Long detectingCnt;
// @ColumnDefault("0")
// @Column(name = "pnu")
// private Long pnu;
// @Size(max = 20)
// @Column(name = "down_state", length = 20)
// private String downState;
//
// @Column(name = "down_state_dttm")
// private ZonedDateTime downStateDttm;
@Size(max = 20)
@Column(name = "fit_state", length = 20)
private String fitState;
@Column(name = "fit_state_dttm")
private ZonedDateTime fitStateDttm;
@Column(name = "labeler_uid")
private Long labelerUid;
@Size(max = 20)
@ColumnDefault("NULL")
@Column(name = "label_state", length = 20)
private String labelState;
@Column(name = "label_state_dttm")
private ZonedDateTime labelStateDttm;
@Column(name = "tester_uid")
private Long testerUid;
@Size(max = 20)
@Column(name = "test_state", length = 20)
private String testState;
@Column(name = "test_state_dttm")
private ZonedDateTime testStateDttm;
@Column(name = "fit_state_cmmnt", length = Integer.MAX_VALUE)
private String fitStateCmmnt;
@Column(name = "ref_map_sheet_num")
private Long refMapSheetNum;
@Column(name = "stage")
private Integer stage;
@Column(name = "file_created_yn")
private Boolean fileCreatedYn;
@Column(name = "file_created_dttm")
private ZonedDateTime fileCreatedDttm;
// @Size(max = 100)
// @Column(name = "m1", length = 100)
// private String m1;
//
// @Size(max = 100)
// @Column(name = "m2", length = 100)
// private String m2;
//
// @Size(max = 100)
// @Column(name = "m3", length = 100)
// private String m3;
}

View File

@@ -1,6 +1,6 @@
package com.kamco.cd.kamcoback.postgres.entity;
import com.kamco.cd.kamcoback.inference.dto.DetectionClassification;
import com.kamco.cd.kamcoback.common.enums.DetectionClassification;
import com.kamco.cd.kamcoback.inference.dto.InferenceDetailDto;
import com.kamco.cd.kamcoback.inference.dto.InferenceDetailDto.Clazzes;
import jakarta.persistence.Column;

View File

@@ -0,0 +1,165 @@
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<String, Object> hyperParams;
@Column(name = "tranning_rate")
private List<Double> tranningRate;
@Column(name = "validation_rate")
private List<Double> 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;
@Size(max = 1)
@ColumnDefault("'N'")
@Column(name = "labeling_closed_yn", length = 1)
private String labelingClosedYn = "N";
@Size(max = 1)
@ColumnDefault("'N'")
@Column(name = "inspection_closed_yn", length = 1)
private String inspectionClosedYn = "N";
@Column(name = "learn_id")
private Long learnId;
}

View File

@@ -1,13 +1,14 @@
package com.kamco.cd.kamcoback.postgres.repository.scheduler;
import com.kamco.cd.kamcoback.dto.TrainingDataReviewJobDto.InspectorPendingDto;
import com.kamco.cd.kamcoback.dto.TrainingDataReviewJobDto.Tasks;
import com.kamco.cd.kamcoback.scheduler.dto.TrainingDataReviewJobDto.InspectorPendingDto;
import com.kamco.cd.kamcoback.scheduler.dto.TrainingDataReviewJobDto.Tasks;
import java.time.LocalDate;
import java.util.List;
import java.util.UUID;
public interface TrainingDataLabelJobRepositoryCustom {
List<Tasks> findCompletedYesterdayUnassigned();
List<Tasks> findCompletedYesterdayUnassigned(LocalDate baseDate);
List<InspectorPendingDto> findInspectorPendingByRound(Long analUid);

View File

@@ -4,16 +4,17 @@ import static com.kamco.cd.kamcoback.postgres.entity.QLabelingAssignmentEntity.l
import static com.kamco.cd.kamcoback.postgres.entity.QLabelingInspectorEntity.labelingInspectorEntity;
import static com.kamco.cd.kamcoback.postgres.entity.QMapSheetAnalDataInferenceGeomEntity.mapSheetAnalDataInferenceGeomEntity;
import com.kamco.cd.kamcoback.dto.LabelAllocateDto.InspectState;
import com.kamco.cd.kamcoback.dto.LabelAllocateDto.LabelState;
import com.kamco.cd.kamcoback.dto.TrainingDataReviewJobDto.InspectorPendingDto;
import com.kamco.cd.kamcoback.dto.TrainingDataReviewJobDto.Tasks;
import com.kamco.cd.kamcoback.label.dto.LabelAllocateDto.InspectState;
import com.kamco.cd.kamcoback.label.dto.LabelAllocateDto.LabelState;
import com.kamco.cd.kamcoback.postgres.entity.LabelingAssignmentEntity;
import com.kamco.cd.kamcoback.scheduler.dto.TrainingDataReviewJobDto.InspectorPendingDto;
import com.kamco.cd.kamcoback.scheduler.dto.TrainingDataReviewJobDto.Tasks;
import com.querydsl.core.types.Projections;
import com.querydsl.core.types.dsl.BooleanExpression;
import com.querydsl.core.types.dsl.Expressions;
import com.querydsl.core.types.dsl.StringExpression;
import com.querydsl.jpa.impl.JPAQueryFactory;
import java.time.LocalDate;
import java.time.ZoneId;
import java.time.ZonedDateTime;
import java.util.List;
@@ -34,16 +35,23 @@ public class TrainingDataLabelJobRepositoryImpl extends QuerydslRepositorySuppor
}
@Override
public List<Tasks> findCompletedYesterdayUnassigned() {
public List<Tasks> findCompletedYesterdayUnassigned(LocalDate baseDate) {
ZoneId zone = ZoneId.of("Asia/Seoul");
ZonedDateTime todayStart = ZonedDateTime.now(zone).toLocalDate().atStartOfDay(zone);
ZonedDateTime yesterdayStart = todayStart.minusDays(1);
// baseDate가 null이면 "어제"를 타겟으로, 아니면 baseDate
LocalDate targetDate = (baseDate != null) ? baseDate : LocalDate.now(zone).minusDays(1);
// end: targetDate + 1일 00:00
ZonedDateTime baseStart = targetDate.plusDays(1).atStartOfDay(zone);
// start: targetDate 00:00
ZonedDateTime yesterdayStart = baseStart.minusDays(1);
BooleanExpression isYesterday =
labelingAssignmentEntity
.workStatDttm
.goe(yesterdayStart)
.and(labelingAssignmentEntity.workStatDttm.lt(todayStart));
.and(labelingAssignmentEntity.workStatDttm.lt(baseStart));
return queryFactory
.select(

View File

@@ -0,0 +1,220 @@
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.inference.dto.InferenceDetailDto;
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;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Sort;
public class MapInkxMngDto {
// 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
@Setter
@NoArgsConstructor
@AllArgsConstructor
public static class Basic {
private Integer fid;
private String mapidcdNo;
private String mapidNm;
private JsonNode geom;
private String useInference;
private ZonedDateTime createdDttm;
private ZonedDateTime updatedDttm;
}
@Getter
@Schema(name = "MapListEntity", description = "목록 항목")
public static class MapListEntity {
private InferenceDetailDto.MapSheet scene50k;
private InferenceDetailDto.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<CommonUseStatus> getUseInference() {
EnumDto<CommonUseStatus> enumDto = useInference.getEnumDto();
return enumDto;
}
@Builder
public MapListEntity(
InferenceDetailDto.MapSheet scene50k,
InferenceDetailDto.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
@NoArgsConstructor
public static class MapList {
private Integer rowNum;
private String mapidcdNo5k;
private String mapidcdNo50k;
private String mapidNm;
private String createdDttm;
private String updatedDttm;
private String useInference;
private ZonedDateTime createdDttmTime;
private ZonedDateTime updatedDttmTime;
// 목록 Querydsl 에서 리턴 받는 건 생성자 기준임 -> 쿼리 컬럼 그대로 받고 여기서 Java 형변환 해서 return 하기
public MapList(
Integer rowNum,
String mapidcdNo5k,
String mapidcdNo50k,
String mapidNm,
ZonedDateTime createdDttmTime,
ZonedDateTime updatedDttmTime,
CommonUseStatus useInference) {
this.rowNum = rowNum;
this.mapidcdNo5k = mapidcdNo5k;
this.mapidcdNo50k = mapidcdNo50k;
this.mapidNm = mapidNm;
DateTimeFormatter fmt =
DateTimeFormatter.ofPattern("yyyy-MM-dd").withZone(ZoneId.of("Asia/Seoul"));
this.createdDttm = fmt.format(createdDttmTime);
this.updatedDttm = fmt.format(updatedDttmTime);
this.createdDttmTime = createdDttmTime;
this.updatedDttmTime = updatedDttmTime;
this.useInference = useInference.getId();
}
}
@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);
}
}
@Schema(name = "AddMapReq", description = "등록 요청")
@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
public static class AddMapReq {
@Schema(description = "도엽번호", example = "31540687")
private String mapidcdNo;
@Schema(description = "도엽명", example = "공덕")
private String mapidNm;
@Schema(
description = "좌표 목록 (한 줄에 한 점, '경도 위도' 형식)",
example =
"127.17500001632317 36.17499998262991\n"
+ "127.14999995475043 36.17500002877932\n"
+ "127.15000004313612 36.199999984012415\n"
+ "127.1750000466954 36.20000001863179")
private String coordinates;
}
@Schema(name = "UseInferReq", description = "추론제외 업데이트 요청")
@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
public static class UseInferReq {
private String mapidcdNo;
private CommonUseStatus useInference; // 변경하고자하는 상태
public void valid() {
if (mapidcdNo == null || mapidcdNo.isEmpty()) {
throw new IllegalArgumentException("도엽번호는 필수 입력값입니다.");
}
// 공백제거
mapidcdNo = mapidcdNo.trim();
if (!mapidcdNo.matches("^\\d{8}$")) {
throw new EntityNotFoundException("도엽번호는 8자리 숫자로 구성되어야 합니다.");
}
}
}
@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
public static class Search5kReq {
private String mapidcdNo;
private String useInference;
}
}

View File

@@ -0,0 +1,31 @@
package com.kamco.cd.kamcoback.scheduler;
import com.kamco.cd.kamcoback.config.api.ApiResponseDto;
import com.kamco.cd.kamcoback.scheduler.service.TrainingDataLabelJobService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import java.time.LocalDate;
import lombok.RequiredArgsConstructor;
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/schedule")
public class SchedulerApiController {
private final TrainingDataLabelJobService trainingDataLabelJobService;
@Operation(
summary = "라벨완료 -> 검수할당 스케줄링",
description = "스케줄링이 실패한 경우 수동 호출하는 API, 어제 라벨링 완료된 것을 해당 검수자들에게 할당함")
@GetMapping("/label-to-review")
public ApiResponseDto<Void> runTrainingReviewSchedule(
@RequestParam(required = false) LocalDate baseDate) {
trainingDataLabelJobService.assignReviewerYesterdayLabelComplete(baseDate);
return ApiResponseDto.ok(null);
}
}

View File

@@ -1,4 +1,4 @@
package com.kamco.cd.kamcoback.dto;
package com.kamco.cd.kamcoback.scheduler.dto;
import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.annotation.JsonPropertyOrder;
@@ -6,7 +6,7 @@ import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.node.ObjectNode;
import com.kamco.cd.kamcoback.dto.TrainingDataReviewJobDto.CompleteLabelData.GeoJsonFeature;
import com.kamco.cd.kamcoback.scheduler.dto.TrainingDataReviewJobDto.CompleteLabelData.GeoJsonFeature;
import java.util.List;
import java.util.UUID;
import lombok.AllArgsConstructor;

View File

@@ -1,9 +1,10 @@
package com.kamco.cd.kamcoback.scheduler.service;
import com.kamco.cd.kamcoback.dto.TrainingDataReviewJobDto.InspectorPendingDto;
import com.kamco.cd.kamcoback.dto.TrainingDataReviewJobDto.Tasks;
import com.kamco.cd.kamcoback.postgres.core.TrainingDataLabelJobCoreService;
import com.kamco.cd.kamcoback.scheduler.dto.TrainingDataReviewJobDto.InspectorPendingDto;
import com.kamco.cd.kamcoback.scheduler.dto.TrainingDataReviewJobDto.Tasks;
import jakarta.transaction.Transactional;
import java.time.LocalDate;
import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;
@@ -13,6 +14,7 @@ import java.util.stream.Collectors;
import lombok.RequiredArgsConstructor;
import lombok.extern.log4j.Log4j2;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.ApplicationContext;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Service;
@@ -22,6 +24,7 @@ import org.springframework.stereotype.Service;
public class TrainingDataLabelJobService {
private final TrainingDataLabelJobCoreService trainingDataLabelJobCoreService;
private final ApplicationContext applicationContext;
@Value("${spring.profiles.active}")
private String profile;
@@ -30,16 +33,24 @@ public class TrainingDataLabelJobService {
return "local".equalsIgnoreCase(profile);
}
@Transactional
@Scheduled(cron = "0 0 0 * * *")
public void assignReviewerYesterdayLabelComplete() {
@Scheduled(cron = "0 * * * * *")
public void runTask() {
// 프록시를 통해 호출해야 @Transactional이 적용됨
applicationContext
.getBean(TrainingDataLabelJobService.class)
.assignReviewerYesterdayLabelComplete(null);
}
if (isLocalProfile()) {
return;
}
@Transactional
public void assignReviewerYesterdayLabelComplete(LocalDate baseDate) {
// if (isLocalProfile()) {
// return;
// }
try {
List<Tasks> tasks = trainingDataLabelJobCoreService.findCompletedYesterdayUnassigned();
List<Tasks> tasks =
trainingDataLabelJobCoreService.findCompletedYesterdayUnassigned(baseDate);
if (tasks.isEmpty()) {
return;
@@ -88,6 +99,7 @@ public class TrainingDataLabelJobService {
}
} catch (Exception e) {
log.error("배치 처리 중 예외", e);
throw e;
}
}

View File

@@ -0,0 +1,138 @@
spring:
config:
activate:
on-profile: dev
jpa:
show-sql: false
hibernate:
ddl-auto: validate
properties:
hibernate:
default_batch_fetch_size: 100 # ✅ 성능 - N+1 쿼리 방지
order_updates: true # ✅ 성능 - 업데이트 순서 정렬로 데드락 방지
order_inserts: true
use_sql_comments: true # ⚠️ 선택 - SQL에 주석 추가 (디버깅용)
format_sql: true # ⚠️ 선택 - SQL 포맷팅 (가독성)
jdbc:
batch_size: 1000 # ✅ 추가 (JDBC batch)
open-in-view: false
mvc:
async:
request-timeout: 300s # 5분 (예: 30s, 120s, 10m 등도 가능)
datasource:
url: jdbc:postgresql://192.168.2.127:15432/kamco_cds
#url: jdbc:postgresql://localhost:15432/kamco_cds
username: kamco_cds
password: kamco_cds_Q!W@E#R$
hikari:
minimum-idle: 10
maximum-pool-size: 20
connection-timeout: 60000 # 60초 연결 타임아웃
idle-timeout: 300000 # 5분 유휴 타임아웃
max-lifetime: 1800000 # 30분 최대 수명
leak-detection-threshold: 60000 # 연결 누수 감지
transaction:
default-timeout: 300 # 5분 트랜잭션 타임아웃
data:
redis:
host: 192.168.2.109
port: 6379
password: kamco
servlet:
multipart:
enabled: true
max-file-size: 4GB
max-request-size: 4GB
file-size-threshold: 10MB
server:
tomcat:
max-swallow-size: 4GB
max-http-form-post-size: 4GB
jwt:
secret: "kamco_token_9b71e778-19a3-4c1d-97bf-2d687de17d5b"
access-token-validity-in-ms: 86400000 # 1일
refresh-token-validity-in-ms: 604800000 # 7일
#access-token-validity-in-ms: 60000 # 1분
#refresh-token-validity-in-ms: 300000 # 5분
token:
refresh-cookie-name: kamco-dev # 개발용 쿠키 이름
refresh-cookie-secure: false # 로컬 http 테스트면 false
springdoc:
swagger-ui:
persist-authorization: true # 스웨거 새로고침해도 토큰 유지, 로컬스토리지에 저장
logging:
level:
root: INFO
org.springframework.web: DEBUG
org.springframework.security: DEBUG
# 헬스체크 노이즈 핵심만 다운
org.springframework.security.web.FilterChainProxy: INFO
org.springframework.security.web.authentication.AnonymousAuthenticationFilter: INFO
org.springframework.security.web.authentication.Http403ForbiddenEntryPoint: INFO
org.springframework.web.servlet.DispatcherServlet: INFO
mapsheet:
upload:
skipGdalValidation: true
shp:
baseurl: /app/tmp/detect/result #현재사용안함
file:
#sync-root-dir: D:/kamco-nfs/images/
sync-root-dir: /kamco-nfs/images/
sync-tmp-dir: /kamco-nfs/requests/temp # image upload temp dir
#sync-tmp-dir: ${file.sync-root-dir}/tmp
sync-file-extention: tfw,tif
sync-auto-exception-start-year: 2024
sync-auto-exception-before-year-cnt: 3
#dataset-dir: D:/kamco-nfs/model_output/
dataset-dir: /kamco-nfs/model_output/export/ # 마운트경로 AI 추론결과
dataset-tmp-dir: ${file.dataset-dir}tmp/
#model-dir: D:/kamco-nfs/ckpt/model/
model-dir: /kamco-nfs/ckpt/model/ # 학습서버에서 트레이닝한 모델업로드경로
model-tmp-dir: ${file.model-dir}tmp/
model-file-extention: pth,json,py
pt-path: /kamco-nfs/ckpt/model/v6-cls-checkpoints/
pt-FileName: yolov8_6th-6m.pt
dataset-response: /kamco-nfs/dataset/response/
inference:
url: http://192.168.2.183:8000/jobs
batch-url: http://192.168.2.183:8000/batches
geojson-dir: /kamco-nfs/requests/ # 추론실행을 위한 파일생성경로
jar-path: /kamco-nfs/repo/jar/shp-exporter.jar
inference-server-name: server1,server2,server3,server4
gukyuin:
#url: http://localhost:8080
url: http://192.168.2.129:5301
cdi: ${gukyuin.url}/api/kcd/cdi
training-data:
geojson-dir: /kamco-nfs/dataset/request/
layer:
geoserver-url: https://kamco.geo-dev.gs.dabeeo.com
wms-path: geoserver/cd
wmts-path: geoserver/cd/gwc/service
workspace: cd

View File

@@ -0,0 +1,122 @@
spring:
config:
activate:
on-profile: prod
jpa:
show-sql: true
hibernate:
ddl-auto: validate
properties:
hibernate:
default_batch_fetch_size: 100 # ✅ 성능 - N+1 쿼리 방지
order_updates: true # ✅ 성능 - 업데이트 순서 정렬로 데드락 방지
order_inserts: true
use_sql_comments: true # ⚠️ 선택 - SQL에 주석 추가 (디버깅용)
format_sql: true # ⚠️ 선택 - SQL 포맷팅 (가독성)
jdbc:
batch_size: 1000 # ✅ 추가 (JDBC batch)
open-in-view: false
mvc:
async:
request-timeout: 300s # 5분 (예: 30s, 120s, 10m 등도 가능)
datasource:
url: jdbc:postgresql://kamco-cd-postgis:5432/kamco_cds
#url: jdbc:postgresql://localhost:15432/kamco_cds
username: kamco_cds
password: kamco_cds_Q!W@E#R$
hikari:
minimum-idle: 10
maximum-pool-size: 20
connection-timeout: 60000 # 60초 연결 타임아웃
idle-timeout: 300000 # 5분 유휴 타임아웃
max-lifetime: 1800000 # 30분 최대 수명
leak-detection-threshold: 60000 # 연결 누수 감지
transaction:
default-timeout: 300 # 5분 트랜잭션 타임아웃
data:
redis:
host: 127.0.0.1
port: 16379
password: kamco
servlet:
multipart:
enabled: true
max-file-size: 4GB
max-request-size: 4GB
file-size-threshold: 10MB
server:
tomcat:
max-swallow-size: 4GB
max-http-form-post-size: 4GB
jwt:
secret: "kamco_token_9b71e778-19a3-4c1d-97bf-2d687de17d5b"
access-token-validity-in-ms: 86400000 # 1일
refresh-token-validity-in-ms: 604800000 # 7일
token:
refresh-cookie-name: kamco # 개발용 쿠키 이름
refresh-cookie-secure: true # 로컬 http 테스트면 false
logging:
level:
root: INFO
org.springframework.web: DEBUG
org.springframework.security: DEBUG
# 헬스체크 노이즈 핵심만 다운
org.springframework.security.web.FilterChainProxy: INFO
org.springframework.security.web.authentication.AnonymousAuthenticationFilter: INFO
org.springframework.security.web.authentication.Http403ForbiddenEntryPoint: INFO
org.springframework.web.servlet.DispatcherServlet: INFO
mapsheet:
upload:
skipGdalValidation: true
shp:
baseurl: /app/detect/result #현재사용안함
file:
sync-root-dir: /kamco-nfs/images/
sync-tmp-dir: /kamco-nfs/repo/tmp # image upload temp dir
sync-file-extention: tfw,tif
#dataset-dir: D:/kamco-nfs/model_output/ #변경 model_output
dataset-dir: /kamco-nfs/model_output/export/ # 마운트경로 AI 추론결과
dataset-tmp-dir: ${file.dataset-dir}tmp/
#model-dir: D:/kamco-nfs/ckpt/model/
model-dir: /kamco-nfs/ckpt/model/ # 학습서버에서 트레이닝한 모델업로드경로
model-tmp-dir: ${file.model-dir}tmp/
model-file-extention: pth,json,py
pt-path: /kamco-nfs/ckpt/v6-cls-checkpoints/
pt-FileName: yolov8_6th-6m.pt
dataset-response: /kamco-nfs/dataset/response/
inference:
url: http://127.0.0.1:8000/jobs
batch-url: http://127.0.0.1:8000/batches
geojson-dir: /kamco-nfs/requests/ # 학습서버에서 트레이닝한 모델업로드경로
jar-path: /kamco-nfs/repo/jar/shp-exporter.jar # 추론실행을 위한 파일생성경로
inference-server-name: server1,server2,server3,server4
gukyuin:
url: http://127.0.0.1:5301
cdi: ${gukyuin.url}/api/kcd/cdi
training-data:
geojson-dir: /kamco-nfs/dataset/request/
layer:
geoserver-url: https://kamco.geo-dev.gs.dabeeo.com
wms-path: geoserver/cd
wmts-path: geoserver/cd/gwc/service
workspace: cd

View File

@@ -1,4 +1,69 @@
server:
port: 9080
port: 8080
spring:
application:
name: kamco-change-detection-api
profiles:
active: prod # 사용할 프로파일 지정 (ex. dev, prod, test)
datasource:
driver-class-name: org.postgresql.Driver
hikari:
jdbc:
time_zone: UTC
batch_size: 50
# 권장 설정
minimum-idle: 2
maximum-pool-size: 2
connection-timeout: 20000
idle-timeout: 300000
max-lifetime: 1800000
leak-detection-threshold: 60000
data:
redis:
host: localhost
port: 6379
password:
jpa:
hibernate:
ddl-auto: update # 테이블이 없으면 생성, 있으면 업데이트
properties:
hibernate:
jdbc:
batch_size: 50
default_batch_fetch_size: 100
logging:
level:
root: INFO
org.springframework.web: DEBUG
org.springframework.security: DEBUG
# 헬스체크 노이즈 핵심만 다운
org.springframework.security.web.FilterChainProxy: INFO
org.springframework.security.web.authentication.AnonymousAuthenticationFilter: INFO
org.springframework.security.web.authentication.Http403ForbiddenEntryPoint: INFO
org.springframework.web.servlet.DispatcherServlet: INFO
# actuator
management:
health:
readinessstate:
enabled: true
livenessstate:
enabled: true
endpoint:
health:
probes:
enabled: true
show-details: always
endpoints:
jmx:
exposure:
exclude: "*"
web:
base-path: /monitor
exposure:
include:
- "health"

View File

@@ -1,67 +0,0 @@
server:
port: 9080
spring:
application:
name: label-to-review
profiles:
active: dev # 사용할 프로파일 지정 (ex. dev, prod, test)
datasource:
url: jdbc:postgresql://192.168.2.127:15432/kamco_cds
#url: jdbc:postgresql://localhost:5432/kamco_cds
username: kamco_cds
password: kamco_cds_Q!W@E#R$
hikari:
minimum-idle: 1
maximum-pool-size: 5
jpa:
hibernate:
ddl-auto: update # 테이블이 없으면 생성, 있으면 업데이트
properties:
hibernate:
jdbc:
batch_size: 50
default_batch_fetch_size: 100
logging:
level:
root: INFO
org.springframework.web: DEBUG
org.springframework.security: DEBUG
# 헬스체크 노이즈 핵심만 다운
org.springframework.security.web.FilterChainProxy: INFO
org.springframework.security.web.authentication.AnonymousAuthenticationFilter: INFO
org.springframework.security.web.authentication.Http403ForbiddenEntryPoint: INFO
org.springframework.web.servlet.DispatcherServlet: INFO
# actuator
management:
health:
readinessstate:
enabled: true
livenessstate:
enabled: true
endpoint:
health:
probes:
enabled: true
show-details: always
endpoints:
jmx:
exposure:
exclude: "*"
web:
base-path: /monitor
exposure:
include:
- "health"
file:
#sync-root-dir: D:/kamco-nfs/images/
sync-root-dir: /kamco-nfs/images/
sync-tmp-dir: ${file.sync-root-dir}/tmp
sync-file-extention: tfw,tif
sync-auto-exception-start-year: 2025
sync-auto-exception-before-year-cnt: 3

View File

@@ -1,67 +0,0 @@
server:
port: 9080
spring:
application:
name: imagery-make-dataset
profiles:
active: local # 사용할 프로파일 지정 (ex. dev, prod, test)
datasource:
url: jdbc:postgresql://192.168.2.127:15432/kamco_cds
#url: jdbc:postgresql://localhost:5432/kamco_cds
username: kamco_cds
password: kamco_cds_Q!W@E#R$
hikari:
minimum-idle: 1
maximum-pool-size: 5
jpa:
hibernate:
ddl-auto: update # 테이블이 없으면 생성, 있으면 업데이트
properties:
hibernate:
jdbc:
batch_size: 50
default_batch_fetch_size: 100
logging:
level:
root: INFO
org.springframework.web: DEBUG
org.springframework.security: DEBUG
# 헬스체크 노이즈 핵심만 다운
org.springframework.security.web.FilterChainProxy: INFO
org.springframework.security.web.authentication.AnonymousAuthenticationFilter: INFO
org.springframework.security.web.authentication.Http403ForbiddenEntryPoint: INFO
org.springframework.web.servlet.DispatcherServlet: INFO
# actuator
management:
health:
readinessstate:
enabled: true
livenessstate:
enabled: true
endpoint:
health:
probes:
enabled: true
show-details: always
endpoints:
jmx:
exposure:
exclude: "*"
web:
base-path: /monitor
exposure:
include:
- "health"
file:
#sync-root-dir: D:/kamco-nfs/images/
sync-root-dir: /kamco-nfs/images/
sync-tmp-dir: ${file.sync-root-dir}/tmp
sync-file-extention: tfw,tif
sync-auto-exception-start-year: 2025
sync-auto-exception-before-year-cnt: 3

View File

@@ -1,67 +0,0 @@
server:
port: 9080
spring:
application:
name: imagery-make-dataset
profiles:
active: prod # 사용할 프로파일 지정 (ex. dev, prod, test)
datasource:
url: jdbc:postgresql://192.168.2.127:15432/kamco_cds
#url: jdbc:postgresql://localhost:5432/kamco_cds
username: kamco_cds
password: kamco_cds_Q!W@E#R$
hikari:
minimum-idle: 1
maximum-pool-size: 5
jpa:
hibernate:
ddl-auto: update # 테이블이 없으면 생성, 있으면 업데이트
properties:
hibernate:
jdbc:
batch_size: 50
default_batch_fetch_size: 100
logging:
level:
root: INFO
org.springframework.web: DEBUG
org.springframework.security: DEBUG
# 헬스체크 노이즈 핵심만 다운
org.springframework.security.web.FilterChainProxy: INFO
org.springframework.security.web.authentication.AnonymousAuthenticationFilter: INFO
org.springframework.security.web.authentication.Http403ForbiddenEntryPoint: INFO
org.springframework.web.servlet.DispatcherServlet: INFO
# actuator
management:
health:
readinessstate:
enabled: true
livenessstate:
enabled: true
endpoint:
health:
probes:
enabled: true
show-details: always
endpoints:
jmx:
exposure:
exclude: "*"
web:
base-path: /monitor
exposure:
include:
- "health"
file:
#sync-root-dir: D:/kamco-nfs/images/
sync-root-dir: /kamco-nfs/images/
sync-tmp-dir: ${file.sync-root-dir}/tmp
sync-file-extention: tfw,tif
sync-auto-exception-start-year: 2025
sync-auto-exception-before-year-cnt: 3

View File

@@ -0,0 +1,83 @@
<!doctype html>
<html lang="ko">
<head>
<meta charset="utf-8" />
<title>라벨 ZIP 다운로드</title>
</head>
<body>
<h3>라벨 ZIP 다운로드</h3>
UUID:
<input id="uuid" value="6d8d49dc-0c9d-4124-adc7-b9ca610cc394" />
<br><br>
JWT Token:
<input id="token" style="width:600px;" placeholder="Bearer 토큰 붙여넣기" />
<br><br>
<button onclick="download()">다운로드</button>
<br><br>
<progress id="bar" value="0" max="100" style="width:400px;"></progress>
<div id="status"></div>
<script>
async function download() {
const uuid = document.getElementById("uuid").value.trim();
const token = document.getElementById("token").value.trim();
if (!uuid) {
alert("UUID 입력하세요");
return;
}
if (!token) {
alert("토큰 입력하세요");
return;
}
const url = `/api/training-data/stage/download/${uuid}`;
const res = await fetch(url, {
headers: {
"Authorization": token.startsWith("Bearer ")
? token
: `Bearer ${token}`,
"kamco-download-uuid": uuid
}
});
if (!res.ok) {
document.getElementById("status").innerText =
"실패: " + res.status;
return;
}
const total = parseInt(res.headers.get("Content-Length") || "0", 10);
const reader = res.body.getReader();
const chunks = [];
let received = 0;
while (true) {
const { done, value } = await reader.read();
if (done) break;
chunks.push(value);
received += value.length;
if (total) {
document.getElementById("bar").value =
(received / total) * 100;
}
}
const blob = new Blob(chunks);
const a = document.createElement("a");
a.href = URL.createObjectURL(blob);
a.download = uuid + ".zip";
a.click();
document.getElementById("status").innerText = "완료 ✅";
}
</script>
</body>
</html>