> getTypeCodes() {
+ return Enums.getAllCodes();
+ }
+
+ /** 메모리 캐시 초기화 */
+ @CacheEvict(value = "trainCommonCodes", allEntries = true)
+ public void refresh() {}
+}
diff --git a/src/main/java/com/kamco/cd/training/common/enums/DeployTargetType.java b/src/main/java/com/kamco/cd/training/common/enums/DeployTargetType.java
new file mode 100644
index 0000000..6839385
--- /dev/null
+++ b/src/main/java/com/kamco/cd/training/common/enums/DeployTargetType.java
@@ -0,0 +1,19 @@
+package com.kamco.cd.training.common.enums;
+
+import com.kamco.cd.training.common.utils.enums.CodeExpose;
+import com.kamco.cd.training.common.utils.enums.EnumType;
+import lombok.AllArgsConstructor;
+import lombok.Getter;
+
+@CodeExpose
+@Getter
+@AllArgsConstructor
+public enum DeployTargetType implements EnumType {
+ // @formatter:off
+ GUKU("GUKU", "국토교통부"),
+ PROD("PROD", "운영계");
+ // @formatter:on
+
+ private final String id;
+ private final String text;
+}
diff --git a/src/main/java/com/kamco/cd/training/common/enums/DetectionClassification.java b/src/main/java/com/kamco/cd/training/common/enums/DetectionClassification.java
new file mode 100644
index 0000000..ca4ec49
--- /dev/null
+++ b/src/main/java/com/kamco/cd/training/common/enums/DetectionClassification.java
@@ -0,0 +1,55 @@
+package com.kamco.cd.training.common.enums;
+
+import lombok.AllArgsConstructor;
+import lombok.Getter;
+
+@Getter
+@AllArgsConstructor
+public enum DetectionClassification {
+ BUILDING("building", "건물", 10),
+ CONTAINER("container", "컨테이너", 20),
+ FIELD("field", "경작지", 30),
+ FOREST("forest", "숲", 40),
+ GRASS("grass", "초지", 50),
+ GREENHOUSE("greenhouse", "비닐하우스", 60),
+ LAND("land", "일반토지", 70),
+ ORCHARD("orchard", "과수원", 80),
+ ROAD("road", "도로", 90),
+ STONE("stone", "모래/자갈", 100),
+ TANK("tank", "물탱크", 110),
+ TUMULUS("tumulus", "토분(무덤)", 120),
+ WASTE("waste", "폐기물", 130),
+ WATER("water", "물", 140),
+ ETC("ETC", "기타", 200); // For 'etc' (miscellaneous/other)
+
+ private final String id;
+ private final String desc;
+ private final int order;
+
+ /**
+ * Optional: Helper method to get the enum from a String, case-insensitive, or return ETC if not
+ * found.
+ */
+ public static DetectionClassification fromString(String text) {
+ if (text == null || text.trim().isEmpty()) {
+ return ETC;
+ }
+
+ try {
+ return DetectionClassification.valueOf(text.toUpperCase());
+ } catch (IllegalArgumentException e) {
+ // If the string doesn't match any enum constant name, return ETC
+ return ETC;
+ }
+ }
+
+ /**
+ * Desc 한글명 get 하기
+ *
+ * @return
+ */
+ public static String fromStrDesc(String text) {
+ DetectionClassification dtf = fromString(text);
+ return dtf.getDesc();
+ }
+}
diff --git a/src/main/java/com/kamco/cd/training/common/enums/LearnDataRegister.java b/src/main/java/com/kamco/cd/training/common/enums/LearnDataRegister.java
new file mode 100644
index 0000000..ddfa72b
--- /dev/null
+++ b/src/main/java/com/kamco/cd/training/common/enums/LearnDataRegister.java
@@ -0,0 +1,26 @@
+package com.kamco.cd.training.common.enums;
+
+import com.kamco.cd.training.common.utils.enums.EnumType;
+import lombok.AllArgsConstructor;
+import lombok.Getter;
+
+@Getter
+@AllArgsConstructor
+public enum LearnDataRegister implements EnumType {
+ READY("준비"),
+ UPLOADING("업로드중"),
+ UPLOAD_FAILED("업로드 실패"),
+ COMPLETED("완료");
+
+ private final String desc;
+
+ @Override
+ public String getId() {
+ return name();
+ }
+
+ @Override
+ public String getText() {
+ return desc;
+ }
+}
diff --git a/src/main/java/com/kamco/cd/training/common/enums/LearnDataType.java b/src/main/java/com/kamco/cd/training/common/enums/LearnDataType.java
new file mode 100644
index 0000000..65dd1a2
--- /dev/null
+++ b/src/main/java/com/kamco/cd/training/common/enums/LearnDataType.java
@@ -0,0 +1,26 @@
+package com.kamco.cd.training.common.enums;
+
+import com.kamco.cd.training.common.utils.enums.CodeExpose;
+import com.kamco.cd.training.common.utils.enums.EnumType;
+import lombok.AllArgsConstructor;
+import lombok.Getter;
+
+@CodeExpose
+@Getter
+@AllArgsConstructor
+public enum LearnDataType implements EnumType {
+ DELIVER("납품"),
+ PRODUCTION("제작");
+
+ private final String desc;
+
+ @Override
+ public String getId() {
+ return name();
+ }
+
+ @Override
+ public String getText() {
+ return desc;
+ }
+}
diff --git a/src/main/java/com/kamco/cd/training/common/enums/ModelMngStatusType.java b/src/main/java/com/kamco/cd/training/common/enums/ModelMngStatusType.java
new file mode 100644
index 0000000..abd2139
--- /dev/null
+++ b/src/main/java/com/kamco/cd/training/common/enums/ModelMngStatusType.java
@@ -0,0 +1,27 @@
+package com.kamco.cd.training.common.enums;
+
+import com.kamco.cd.training.common.utils.enums.CodeExpose;
+import com.kamco.cd.training.common.utils.enums.EnumType;
+import lombok.AllArgsConstructor;
+import lombok.Getter;
+
+@CodeExpose
+@Getter
+@AllArgsConstructor
+public enum ModelMngStatusType implements EnumType {
+ READY("준비"),
+ IN_PROGRESS("진행중"),
+ COMPLETED("완료");
+
+ private String desc;
+
+ @Override
+ public String getId() {
+ return name();
+ }
+
+ @Override
+ public String getText() {
+ return desc;
+ }
+}
diff --git a/src/main/java/com/kamco/cd/training/common/enums/ProcessStepType.java b/src/main/java/com/kamco/cd/training/common/enums/ProcessStepType.java
new file mode 100644
index 0000000..17ecf51
--- /dev/null
+++ b/src/main/java/com/kamco/cd/training/common/enums/ProcessStepType.java
@@ -0,0 +1,20 @@
+package com.kamco.cd.training.common.enums;
+
+import com.kamco.cd.training.common.utils.enums.CodeExpose;
+import com.kamco.cd.training.common.utils.enums.EnumType;
+import lombok.AllArgsConstructor;
+import lombok.Getter;
+
+@CodeExpose
+@Getter
+@AllArgsConstructor
+public enum ProcessStepType implements EnumType {
+ // @formatter:off
+ STEP1("STEP1", "학습 중"),
+ STEP2("STEP2", "테스트 중"),
+ STEP3("STEP3", "완료");
+ // @formatter:on
+
+ private final String id;
+ private final String text;
+}
diff --git a/src/main/java/com/kamco/cd/training/common/enums/RoleType.java b/src/main/java/com/kamco/cd/training/common/enums/RoleType.java
new file mode 100644
index 0000000..bcc2f0c
--- /dev/null
+++ b/src/main/java/com/kamco/cd/training/common/enums/RoleType.java
@@ -0,0 +1,25 @@
+package com.kamco.cd.training.common.enums;
+
+import com.kamco.cd.training.common.utils.enums.EnumType;
+import lombok.AllArgsConstructor;
+import lombok.Getter;
+
+@Getter
+@AllArgsConstructor
+public enum RoleType implements EnumType {
+ ROLE_ADMIN("시스템 관리자"),
+ ROLE_LABELER("라벨러"),
+ ROLE_REVIEWER("검수자");
+
+ private final String desc;
+
+ @Override
+ public String getId() {
+ return name();
+ }
+
+ @Override
+ public String getText() {
+ return desc;
+ }
+}
diff --git a/src/main/java/com/kamco/cd/training/common/enums/StatusType.java b/src/main/java/com/kamco/cd/training/common/enums/StatusType.java
new file mode 100644
index 0000000..af86ac5
--- /dev/null
+++ b/src/main/java/com/kamco/cd/training/common/enums/StatusType.java
@@ -0,0 +1,25 @@
+package com.kamco.cd.training.common.enums;
+
+import com.kamco.cd.training.common.utils.enums.EnumType;
+import lombok.AllArgsConstructor;
+import lombok.Getter;
+
+@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;
+ }
+}
diff --git a/src/main/java/com/kamco/cd/training/common/enums/TrainStatusType.java b/src/main/java/com/kamco/cd/training/common/enums/TrainStatusType.java
new file mode 100644
index 0000000..e5da42f
--- /dev/null
+++ b/src/main/java/com/kamco/cd/training/common/enums/TrainStatusType.java
@@ -0,0 +1,22 @@
+package com.kamco.cd.training.common.enums;
+
+import com.kamco.cd.training.common.utils.enums.CodeExpose;
+import com.kamco.cd.training.common.utils.enums.EnumType;
+import lombok.AllArgsConstructor;
+import lombok.Getter;
+
+@CodeExpose
+@Getter
+@AllArgsConstructor
+public enum TrainStatusType implements EnumType {
+ // @formatter:off
+ READY("READY", "대기"),
+ ING("ING", "진행중"),
+ COMPLETED("COMPLETED", "완료"),
+ STOPPED("STOPPED", "중단됨"),
+ ERROR("ERROR", "오류");
+ // @formatter:on
+
+ private final String id;
+ private final String text;
+}
diff --git a/src/main/java/com/kamco/cd/training/common/enums/error/AuthErrorCode.java b/src/main/java/com/kamco/cd/training/common/enums/error/AuthErrorCode.java
new file mode 100644
index 0000000..8763298
--- /dev/null
+++ b/src/main/java/com/kamco/cd/training/common/enums/error/AuthErrorCode.java
@@ -0,0 +1,26 @@
+package com.kamco.cd.training.common.enums.error;
+
+import com.kamco.cd.training.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),
+
+ REFRESH_TOKEN_EXPIRED_OR_REVOKED("REFRESH_TOKEN_EXPIRED_OR_REVOKED", HttpStatus.UNAUTHORIZED),
+
+ REFRESH_TOKEN_MISMATCH("REFRESH_TOKEN_MISMATCH", HttpStatus.UNAUTHORIZED);
+
+ private final String code;
+ private final HttpStatus status;
+
+ AuthErrorCode(String code, HttpStatus status) {
+ this.code = code;
+ this.status = status;
+ }
+}
diff --git a/src/main/java/com/kamco/cd/training/common/exception/BadRequestException.java b/src/main/java/com/kamco/cd/training/common/exception/BadRequestException.java
new file mode 100644
index 0000000..a017fd3
--- /dev/null
+++ b/src/main/java/com/kamco/cd/training/common/exception/BadRequestException.java
@@ -0,0 +1,10 @@
+package com.kamco.cd.training.common.exception;
+
+import org.springframework.http.HttpStatus;
+
+public class BadRequestException extends CustomApiException {
+
+ public BadRequestException(String message) {
+ super("BAD_REQUEST", HttpStatus.BAD_REQUEST, message);
+ }
+}
diff --git a/src/main/java/com/kamco/cd/training/common/exception/CustomApiException.java b/src/main/java/com/kamco/cd/training/common/exception/CustomApiException.java
new file mode 100644
index 0000000..e2d8fec
--- /dev/null
+++ b/src/main/java/com/kamco/cd/training/common/exception/CustomApiException.java
@@ -0,0 +1,28 @@
+package com.kamco.cd.training.common.exception;
+
+import com.kamco.cd.training.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) {
+ this.codeName = codeName;
+ this.status = status;
+ }
+
+ public CustomApiException(ErrorCode errorCode) {
+ this.codeName = errorCode.getCode();
+ this.status = errorCode.getStatus();
+ }
+}
diff --git a/src/main/java/com/kamco/cd/training/common/exception/DuplicateFileException.java b/src/main/java/com/kamco/cd/training/common/exception/DuplicateFileException.java
new file mode 100644
index 0000000..881cfc1
--- /dev/null
+++ b/src/main/java/com/kamco/cd/training/common/exception/DuplicateFileException.java
@@ -0,0 +1,7 @@
+package com.kamco.cd.training.common.exception;
+
+public class DuplicateFileException extends RuntimeException {
+ public DuplicateFileException(String message) {
+ super(message);
+ }
+}
diff --git a/src/main/java/com/kamco/cd/training/common/exception/NotFoundException.java b/src/main/java/com/kamco/cd/training/common/exception/NotFoundException.java
new file mode 100644
index 0000000..0c6b94f
--- /dev/null
+++ b/src/main/java/com/kamco/cd/training/common/exception/NotFoundException.java
@@ -0,0 +1,10 @@
+package com.kamco.cd.training.common.exception;
+
+import org.springframework.http.HttpStatus;
+
+public class NotFoundException extends CustomApiException {
+
+ public NotFoundException(String message) {
+ super("NOT_FOUND", HttpStatus.NOT_FOUND, message);
+ }
+}
diff --git a/src/main/java/com/kamco/cd/training/common/exception/ValidationException.java b/src/main/java/com/kamco/cd/training/common/exception/ValidationException.java
new file mode 100644
index 0000000..bc37f97
--- /dev/null
+++ b/src/main/java/com/kamco/cd/training/common/exception/ValidationException.java
@@ -0,0 +1,7 @@
+package com.kamco.cd.training.common.exception;
+
+public class ValidationException extends RuntimeException {
+ public ValidationException(String message) {
+ super(message);
+ }
+}
diff --git a/src/main/java/com/kamco/cd/training/common/service/BaseCoreService.java b/src/main/java/com/kamco/cd/training/common/service/BaseCoreService.java
new file mode 100644
index 0000000..d3ad069
--- /dev/null
+++ b/src/main/java/com/kamco/cd/training/common/service/BaseCoreService.java
@@ -0,0 +1,38 @@
+package com.kamco.cd.training.common.service;
+
+import org.springframework.data.domain.Page;
+
+/**
+ * Base Core Service Interface
+ *
+ * CRUD operations를 정의하는 기본 서비스 인터페이스
+ *
+ * @param Entity 타입
+ * @param Entity의 ID 타입
+ * @param Search Request 타입
+ */
+public interface BaseCoreService {
+
+ /**
+ * ID로 엔티티를 삭제합니다.
+ *
+ * @param id 삭제할 엔티티의 ID
+ */
+ void remove(ID id);
+
+ /**
+ * ID로 단건 조회합니다.
+ *
+ * @param id 조회할 엔티티의 ID
+ * @return 조회된 엔티티
+ */
+ T getOneById(ID id);
+
+ /**
+ * 검색 조건과 페이징으로 조회합니다.
+ *
+ * @param searchReq 검색 조건
+ * @return 페이징 처리된 검색 결과
+ */
+ Page search(S searchReq);
+}
diff --git a/src/main/java/com/kamco/cd/training/common/utils/CommonCodeUtil.java b/src/main/java/com/kamco/cd/training/common/utils/CommonCodeUtil.java
new file mode 100644
index 0000000..25cfef9
--- /dev/null
+++ b/src/main/java/com/kamco/cd/training/common/utils/CommonCodeUtil.java
@@ -0,0 +1,152 @@
+package com.kamco.cd.training.common.utils;
+
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.kamco.cd.training.code.dto.CommonCodeDto.Basic;
+import com.kamco.cd.training.code.service.CommonCodeService;
+import com.kamco.cd.training.config.api.ApiResponseDto;
+import java.util.List;
+import java.util.Optional;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Component;
+
+/**
+ * 공통코드 조회 유틸리티 클래스 애플리케이션 전역에서 공통코드를 조회하기 위한 유틸리티입니다. training 서버는 Redis 사용하고 Spring 내장 메모리 캐시
+ * 사용합니다.
+ */
+//
+@Slf4j
+@Component
+public class CommonCodeUtil {
+
+ private final CommonCodeService commonCodeService;
+
+ @Autowired private ObjectMapper objectMapper;
+
+ public CommonCodeUtil(CommonCodeService commonCodeService) {
+ this.commonCodeService = commonCodeService;
+ }
+
+ /**
+ * 모든 공통코드 조회
+ *
+ * @return 캐시된 모든 공통코드 목록
+ */
+ public List getAllCommonCodes() {
+ try {
+ return commonCodeService.getFindAll();
+ } catch (Exception e) {
+ log.error("공통코드 전체 조회 중 오류 발생", e);
+ return List.of();
+ }
+ }
+
+ /**
+ * 특정 코드로 공통코드 조회
+ *
+ * @param code 코드값
+ * @return 해당 코드의 공통코드 목록
+ */
+ public List getCommonCodesByCode(String code) {
+ if (code == null || code.isEmpty()) {
+ log.warn("유효하지 않은 코드: {}", code);
+ return List.of();
+ }
+
+ try {
+ return commonCodeService.findByCode(code);
+ } catch (Exception e) {
+ log.error("코드 기반 공통코드 조회 중 오류 발생: {}", code, e);
+ return List.of();
+ }
+ }
+
+ /**
+ * 특정 ID로 공통코드 단건 조회
+ *
+ * @param id 공통코드 ID
+ * @return 조회된 공통코드
+ */
+ public Optional getCommonCodeById(Long id) {
+ if (id == null || id <= 0) {
+ log.warn("유효하지 않은 ID: {}", id);
+ return Optional.empty();
+ }
+
+ try {
+ return Optional.of(commonCodeService.getOneById(id));
+ } catch (Exception e) {
+ log.error("ID 기반 공통코드 조회 중 오류 발생: {}", id, e);
+ return Optional.empty();
+ }
+ }
+
+ /**
+ * 상위 코드와 하위 코드로 공통코드명 조회
+ *
+ * @param parentCode 상위 코드
+ * @param childCode 하위 코드
+ * @return 공통코드명
+ */
+ public Optional getCodeName(String parentCode, String childCode) {
+ if (parentCode == null || parentCode.isEmpty() || childCode == null || childCode.isEmpty()) {
+ log.warn("유효하지 않은 코드: parentCode={}, childCode={}", parentCode, childCode);
+ return Optional.empty();
+ }
+
+ try {
+ return commonCodeService.getCode(parentCode, childCode);
+ } catch (Exception e) {
+ log.error("코드명 조회 중 오류 발생: parentCode={}, childCode={}", parentCode, childCode, e);
+ return Optional.empty();
+ }
+ }
+
+ /**
+ * 상위 코드를 기반으로 하위 코드 조회
+ *
+ * @param parentCode 상위 코드
+ * @return 해당 상위 코드의 하위 공통코드 목록
+ */
+ public List getChildCodesByParentCode(String parentCode) {
+ if (parentCode == null || parentCode.isEmpty()) {
+ log.warn("유효하지 않은 상위 코드: {}", parentCode);
+ return List.of();
+ }
+
+ try {
+ return commonCodeService.getFindAll().stream()
+ .filter(code -> parentCode.equals(code.getCode()))
+ .findFirst()
+ .map(Basic::getChildren)
+ .orElse(List.of());
+ } catch (Exception e) {
+ log.error("상위 코드 기반 하위 코드 조회 중 오류 발생: {}", parentCode, e);
+ return List.of();
+ }
+ }
+
+ /**
+ * 코드 사용 가능 여부 확인
+ *
+ * @param parentId 상위 코드 ID -> 1depth 는 null 허용 (파라미터 미필수로 변경)
+ * @param code 확인할 코드값
+ * @return 사용 가능 여부 (true: 사용 가능, false: 중복 또는 오류)
+ */
+ public boolean isCodeAvailable(Long parentId, String code) {
+ if (parentId <= 0 || code == null || code.isEmpty()) {
+ log.warn("유효하지 않은 입력: parentId={}, code={}", parentId, code);
+ return false;
+ }
+
+ try {
+ ApiResponseDto.ResponseObj response = commonCodeService.getCodeCheckDuplicate(parentId, code);
+ // ResponseObj의 code 필드 : OK이면 성공, 아니면 실패
+ return response.getCode() != null
+ && response.getCode().equals(ApiResponseDto.ApiResponseCode.OK);
+ } catch (Exception e) {
+ log.error("코드 중복 확인 중 오류 발생: parentId={}, code={}", parentId, code, e);
+ return false;
+ }
+ }
+}
diff --git a/src/main/java/com/kamco/cd/training/common/utils/CommonStringUtils.java b/src/main/java/com/kamco/cd/training/common/utils/CommonStringUtils.java
new file mode 100644
index 0000000..adc7193
--- /dev/null
+++ b/src/main/java/com/kamco/cd/training/common/utils/CommonStringUtils.java
@@ -0,0 +1,32 @@
+package com.kamco.cd.training.common.utils;
+
+import com.kamco.cd.training.auth.BCryptSaltGenerator;
+import java.util.regex.Pattern;
+import org.mindrot.jbcrypt.BCrypt;
+
+public class CommonStringUtils {
+
+ /**
+ * 영문, 숫자, 특수문자를 모두 포함하여 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 암호화 필요한 패스워드
+ * @param employeeNo salt 생성에 필요한 사원번호
+ * @return
+ */
+ public static String hashPassword(String password, String employeeNo) {
+ String salt = BCryptSaltGenerator.generateSaltWithEmployeeNo(employeeNo.trim());
+ return BCrypt.hashpw(password.trim(), salt);
+ }
+}
diff --git a/src/main/java/com/kamco/cd/training/common/utils/ErrorCode.java b/src/main/java/com/kamco/cd/training/common/utils/ErrorCode.java
new file mode 100644
index 0000000..8e99391
--- /dev/null
+++ b/src/main/java/com/kamco/cd/training/common/utils/ErrorCode.java
@@ -0,0 +1,10 @@
+package com.kamco.cd.training.common.utils;
+
+import org.springframework.http.HttpStatus;
+
+public interface ErrorCode {
+
+ String getCode();
+
+ HttpStatus getStatus();
+}
diff --git a/src/main/java/com/kamco/cd/training/common/utils/FIleChecker.java b/src/main/java/com/kamco/cd/training/common/utils/FIleChecker.java
new file mode 100644
index 0000000..20f78b9
--- /dev/null
+++ b/src/main/java/com/kamco/cd/training/common/utils/FIleChecker.java
@@ -0,0 +1,685 @@
+package com.kamco.cd.training.common.utils;
+
+import static java.lang.String.CASE_INSENSITIVE_ORDER;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import java.io.BufferedReader;
+import java.io.File;
+import java.io.FileReader;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.nio.file.attribute.FileTime;
+import java.security.MessageDigest;
+import java.security.NoSuchAlgorithmException;
+import java.text.SimpleDateFormat;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Comparator;
+import java.util.Date;
+import java.util.List;
+import java.util.Set;
+import java.util.function.Predicate;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
+import lombok.Getter;
+import org.apache.commons.io.FilenameUtils;
+import org.geotools.coverage.grid.GridCoverage2D;
+import org.geotools.gce.geotiff.GeoTiffReader;
+import org.springframework.util.FileSystemUtils;
+import org.springframework.web.multipart.MultipartFile;
+
+public class FIleChecker {
+
+ static SimpleDateFormat dttmFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
+
+ public static boolean isValidFile(String pathStr) {
+
+ Path path = Paths.get(pathStr);
+
+ if (!Files.exists(path)) {
+ return false;
+ }
+
+ if (!Files.isRegularFile(path)) {
+ return false;
+ }
+
+ if (!Files.isReadable(path)) {
+ return false;
+ }
+
+ try {
+ if (Files.size(path) <= 0) {
+ return false;
+ }
+ } catch (IOException e) {
+ return false;
+ }
+
+ return true;
+ }
+
+ public static boolean verifyFileIntegrity(Path path, String expectedHash)
+ throws IOException, NoSuchAlgorithmException {
+
+ // 1. 알고리즘 선택 (SHA-256 권장, MD5는 보안상 비추천)
+ MessageDigest digest = MessageDigest.getInstance("SHA-256");
+
+ try (InputStream fis = Files.newInputStream(path)) {
+ byte[] buffer = new byte[8192]; // 8KB 버퍼
+ int bytesRead;
+ while ((bytesRead = fis.read(buffer)) != -1) {
+ digest.update(buffer, 0, bytesRead);
+ }
+ }
+
+ // 3. 계산된 바이트 배열을 16진수 문자열로 변환
+ StringBuilder sb = new StringBuilder();
+ for (byte b : digest.digest()) {
+ sb.append(String.format("%02x", b));
+ }
+ String actualHash = sb.toString();
+
+ return actualHash.equalsIgnoreCase(expectedHash);
+ }
+
+ public static boolean checkTfw(String filePath) {
+
+ File file = new File(filePath);
+
+ if (!file.exists()) {
+ return false;
+ }
+
+ // 1. 파일의 모든 라인을 읽어옴
+ List lines = new ArrayList<>();
+ try (BufferedReader br = new BufferedReader(new FileReader(file))) {
+ String line;
+ while ((line = br.readLine()) != null) {
+ if (!line.trim().isEmpty()) { // 빈 줄 제외
+ lines.add(Double.parseDouble(line.trim()));
+ }
+ }
+ } catch (IOException ignored) {
+ return false;
+ }
+
+ // 2. 6줄이 맞는지 확인
+ if (lines.size() < 6) {
+ // System.out.println("유효하지 않은 TFW 파일입니다. (데이터 부족)");
+ return false;
+ }
+
+ return true;
+ }
+
+ public static boolean checkGeoTiff(String filePath) {
+
+ File file = new File(filePath);
+
+ if (!file.exists()) {
+ return false;
+ }
+
+ GeoTiffReader reader = null;
+ try {
+ // 1. 파일 포맷 및 헤더 확인
+ reader = new GeoTiffReader(file);
+
+ // 2. 실제 데이터 로딩 (여기서 파일 깨짐 여부 확인됨)
+ // null을 넣으면 전체 영역을 읽지 않고 메타데이터 위주로 체크하여 빠름
+ GridCoverage2D coverage = reader.read(null);
+
+ if (coverage == null) return false;
+
+ // 3. GIS 필수 정보(좌표계)가 있는지 확인
+ // if (coverage.getCoordinateReferenceSystem() == null) {
+ // GeoTIFF가 아니라 일반 TIFF일 수도 있음(이미지는 정상이지만, 좌표계(CRS) 정보가 없습니다.)
+ // }
+
+ return true;
+
+ } catch (Exception e) {
+ System.err.println("손상된 TIF 파일입니다: " + e.getMessage());
+ return false;
+ } finally {
+ // 리소스 해제 (필수)
+ if (reader != null) reader.dispose();
+ }
+ }
+
+ public static Boolean cmmndGdalInfo(String filePath) {
+
+ File file = new File(filePath);
+
+ if (!file.exists()) {
+ System.err.println("파일이 존재하지 않습니다: " + filePath);
+ return false;
+ }
+
+ boolean hasDriver = false;
+
+ // 운영체제 감지
+ String osName = System.getProperty("os.name").toLowerCase();
+ boolean isWindows = osName.contains("win");
+ boolean isMac = osName.contains("mac");
+ boolean isUnix = osName.contains("nix") || osName.contains("nux") || osName.contains("aix");
+
+ // gdalinfo 경로 찾기 (일반적인 설치 경로 우선 확인)
+ String gdalinfoPath = findGdalinfoPath();
+ if (gdalinfoPath == null) {
+ System.err.println("gdalinfo 명령어를 찾을 수 없습니다. GDAL이 설치되어 있는지 확인하세요.");
+ System.err.println("macOS: brew install gdal");
+ System.err.println("Ubuntu/Debian: sudo apt-get install gdal-bin");
+ System.err.println("CentOS/RHEL: sudo yum install gdal");
+ return false;
+ }
+
+ List command = new ArrayList<>();
+
+ if (isWindows) {
+ // 윈도우용
+ command.add("cmd.exe"); // 윈도우 명령 프롬프트 실행
+ command.add("/c"); // 명령어를 수행하고 종료한다는 옵션
+ command.add("gdalinfo");
+ command.add(filePath);
+ command.add("|");
+ command.add("findstr");
+ command.add("/i");
+ command.add("Geo");
+ } else if (isMac || isUnix) {
+ // 리눅스, 맥용
+ command.add("sh");
+ command.add("-c");
+ command.add(gdalinfoPath + " \"" + filePath + "\" | grep -i Geo");
+ } else {
+ System.err.println("지원하지 않는 운영체제: " + osName);
+ return false;
+ }
+
+ ProcessBuilder processBuilder = new ProcessBuilder(command);
+ processBuilder.redirectErrorStream(true);
+
+ Process process = null;
+ BufferedReader reader = null;
+ try {
+ System.out.println("gdalinfo 명령어 실행 시작: " + filePath);
+ process = processBuilder.start();
+
+ reader = new BufferedReader(new InputStreamReader(process.getInputStream()));
+
+ String line;
+ while ((line = reader.readLine()) != null) {
+ // System.out.println("gdalinfo 출력: " + line);
+ if (line.contains("Driver: GTiff/GeoTIFF")) {
+ hasDriver = true;
+ break;
+ }
+ }
+
+ int exitCode = process.waitFor();
+ System.out.println("gdalinfo 종료 코드: " + exitCode);
+
+ // 프로세스가 정상 종료되지 않았고 Driver를 찾지 못한 경우
+ if (exitCode != 0 && !hasDriver) {
+ System.err.println("gdalinfo 명령 실행 실패. Exit code: " + exitCode);
+ }
+
+ } catch (IOException e) {
+ System.err.println("gdalinfo 실행 중 I/O 오류 발생: " + e.getMessage());
+ e.printStackTrace();
+ return false;
+ } catch (InterruptedException e) {
+ System.err.println("gdalinfo 실행 중 인터럽트 발생: " + e.getMessage());
+ Thread.currentThread().interrupt();
+ return false;
+ } catch (Exception e) {
+ System.err.println("gdalinfo 실행 중 예상치 못한 오류 발생: " + e.getMessage());
+ e.printStackTrace();
+ return false;
+ } finally {
+ // 리소스 정리
+ if (reader != null) {
+ try {
+ reader.close();
+ } catch (IOException e) {
+ System.err.println("BufferedReader 종료 중 오류: " + e.getMessage());
+ }
+ }
+ if (process != null) {
+ process.destroy();
+ }
+ }
+
+ return hasDriver;
+ }
+
+ public static boolean mkDir(String dirPath) {
+ Path uploadTargetPath = Paths.get(dirPath);
+ try {
+ Files.createDirectories(uploadTargetPath);
+ } catch (IOException e) {
+ return false;
+ }
+
+ return true;
+ }
+
+ public static List getFolderAll(String dirPath, String sortType, int maxDepth) {
+
+ Path startPath = Paths.get(dirPath);
+
+ List folderList = List.of();
+
+ try (Stream stream = Files.walk(startPath, maxDepth)) {
+
+ folderList =
+ stream
+ .filter(Files::isDirectory)
+ .filter(p -> !p.toString().equals(dirPath))
+ .map(
+ path -> {
+ int depth = path.getNameCount();
+
+ String folderNm = path.getFileName().toString();
+ String parentFolderNm = path.getParent().getFileName().toString();
+ String parentPath = path.getParent().toString();
+ String fullPath = path.toAbsolutePath().toString();
+
+ boolean isValid =
+ !NameValidator.containsKorean(folderNm)
+ && !NameValidator.containsWhitespaceRegex(folderNm);
+
+ File file = new File(fullPath);
+ int childCnt = getChildFolderCount(file);
+ String lastModified = getLastModified(file);
+
+ return new Folder(
+ folderNm,
+ parentFolderNm,
+ parentPath,
+ fullPath,
+ depth,
+ childCnt,
+ lastModified,
+ isValid);
+ })
+ .collect(Collectors.toList());
+
+ if (sortType.equals("name") || sortType.equals("name asc")) {
+ folderList.sort(
+ Comparator.comparing(
+ Folder::getFolderNm, CASE_INSENSITIVE_ORDER // 대소문자 구분 없이
+ ));
+ } else if (sortType.equals("name desc")) {
+ folderList.sort(
+ Comparator.comparing(
+ Folder::getFolderNm, CASE_INSENSITIVE_ORDER // 대소문자 구분 없이
+ )
+ .reversed());
+ } else if (sortType.equals("dttm desc")) {
+ folderList.sort(
+ Comparator.comparing(
+ Folder::getLastModified, CASE_INSENSITIVE_ORDER // 대소문자 구분 없이
+ )
+ .reversed());
+ } else {
+ folderList.sort(
+ Comparator.comparing(
+ Folder::getLastModified, CASE_INSENSITIVE_ORDER // 대소문자 구분 없이
+ ));
+ }
+
+ } catch (IOException e) {
+ throw new RuntimeException(e);
+ }
+
+ return folderList;
+ }
+
+ public static List getFolderAll(String dirPath) {
+ return getFolderAll(dirPath, "name", 1);
+ }
+
+ public static List getFolderAll(String dirPath, String sortType) {
+ return getFolderAll(dirPath, sortType, 1);
+ }
+
+ public static int getChildFolderCount(String dirPath) {
+ File directory = new File(dirPath);
+ File[] childFolders = directory.listFiles(File::isDirectory);
+
+ int childCnt = 0;
+ if (childFolders != null) {
+ childCnt = childFolders.length;
+ }
+
+ return childCnt;
+ }
+
+ public static int getChildFolderCount(File directory) {
+ File[] childFolders = directory.listFiles(File::isDirectory);
+
+ int childCnt = 0;
+ if (childFolders != null) {
+ childCnt = childFolders.length;
+ }
+
+ return childCnt;
+ }
+
+ public static String getLastModified(String dirPath) {
+ File file = new File(dirPath);
+ return dttmFormat.format(new Date(file.lastModified()));
+ }
+
+ public static String getLastModified(File file) {
+ return dttmFormat.format(new Date(file.lastModified()));
+ }
+
+ public static List getFilesFromAllDepth(
+ String dir,
+ String targetFileNm,
+ String extension,
+ int maxDepth,
+ String sortType,
+ int startPos,
+ int endPos) {
+
+ Path startPath = Paths.get(dir);
+ String dirPath = dir;
+
+ int limit = endPos - startPos + 1;
+
+ Set targetExtensions = createExtensionSet(extension);
+
+ List fileList = new ArrayList<>();
+ SimpleDateFormat dttmFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
+
+ Predicate isTargetName =
+ p -> {
+ if (targetFileNm == null
+ || targetFileNm.trim().isEmpty()
+ || targetFileNm.trim().equals("*")) {
+ return true; // 전체 파일 허용
+ }
+ return p.getFileName().toString().contains(targetFileNm);
+ };
+
+ try (Stream stream = Files.walk(startPath, maxDepth)) {
+
+ fileList =
+ stream
+ .filter(Files::isRegularFile)
+ .filter(
+ p ->
+ extension == null
+ || extension.equals("")
+ || extension.equals("*")
+ || targetExtensions.contains(extractExtension(p)))
+ .sorted(getFileComparator(sortType))
+ .filter(isTargetName)
+ .skip(startPos)
+ .limit(limit)
+ .map(
+ path -> {
+ // int depth = path.getNameCount();
+
+ String fileNm = path.getFileName().toString();
+ String ext = FilenameUtils.getExtension(fileNm);
+ String parentFolderNm = path.getParent().getFileName().toString();
+ String parentPath = path.getParent().toString();
+ String fullPath = path.toAbsolutePath().toString();
+
+ File file = new File(fullPath);
+ long fileSize = file.length();
+ String lastModified = dttmFormat.format(new Date(file.lastModified()));
+
+ return new Basic(
+ fileNm, parentFolderNm, parentPath, fullPath, ext, fileSize, lastModified);
+ })
+ .collect(Collectors.toList());
+
+ } catch (IOException e) {
+ System.err.println("파일 I/O 오류 발생: " + e.getMessage());
+ }
+
+ return fileList;
+ }
+
+ public static List getFilesFromAllDepth(
+ String dir, String targetFileNm, String extension) {
+
+ return FIleChecker.getFilesFromAllDepth(dir, targetFileNm, extension, 100, "name", 0, 100);
+ }
+
+ public static int getFileCountFromAllDepth(String dir, String targetFileNm, String extension) {
+
+ List basicList =
+ FIleChecker.getFilesFromAllDepth(dir, targetFileNm, extension);
+
+ return (int)
+ basicList.stream().filter(dto -> dto.getExtension().toString().equals(extension)).count();
+ }
+
+ public static Long getFileTotSize(List files) {
+
+ Long fileTotSize = 0L;
+ if (files != null || files.size() > 0) {
+ fileTotSize = files.stream().mapToLong(FIleChecker.Basic::getFileSize).sum();
+ }
+
+ return fileTotSize;
+ }
+
+ public static boolean multipartSaveTo(MultipartFile mfile, String targetPath) {
+ Path tmpSavePath = Paths.get(targetPath);
+
+ boolean fileUpload = true;
+ try {
+ mfile.transferTo(tmpSavePath);
+ } catch (IOException e) {
+ // throw new RuntimeException(e);
+ return false;
+ }
+
+ return true;
+ }
+
+
+ public static boolean multipartChunkSaveTo(MultipartFile mfile, String targetPath, int chunkIndex) {
+ File dest = new File(targetPath, String.valueOf(chunkIndex));
+
+ boolean fileUpload = true;
+ try {
+ mfile.transferTo(dest);
+ } catch (IOException e) {
+ return false;
+ }
+
+ return true;
+ }
+
+ public static boolean deleteFolder(String path) {
+ return FileSystemUtils.deleteRecursively(new File(path));
+ }
+
+
+
+ public static boolean validationMultipart(MultipartFile mfile) {
+ // 파일 유효성 검증
+ if (mfile == null || mfile.isEmpty() || mfile.getSize() == 0) {
+ return false;
+ }
+
+ return true;
+ }
+
+ public static boolean checkExtensions(String fileName, String ext) {
+ if (fileName == null) return false;
+
+ if (!fileName.substring(fileName.lastIndexOf('.') + 1).toLowerCase().equals(ext)) {
+ return false;
+ }
+
+ return true;
+ }
+
+ public static Set createExtensionSet(String extensionString) {
+ if (extensionString == null || extensionString.isBlank()) {
+ return Set.of();
+ }
+
+ // "java, class" -> ["java", " class"] -> [".java", ".class"]
+ return Arrays.stream(extensionString.split(","))
+ .map(ext -> ext.trim())
+ .filter(ext -> !ext.isEmpty())
+ .map(ext -> "." + ext.toLowerCase())
+ .collect(Collectors.toSet());
+ }
+
+ public static String extractExtension(Path path) {
+ String filename = path.getFileName().toString();
+ int lastDotIndex = filename.lastIndexOf('.');
+
+ // 확장자가 없거나 파일명이 .으로 끝나는 경우
+ if (lastDotIndex == -1 || lastDotIndex == filename.length() - 1) {
+ return ""; // 빈 문자열 반환
+ }
+
+ // 확장자 추출 및 소문자 변환
+ return filename.substring(lastDotIndex).toLowerCase();
+ }
+
+ public static Comparator getFileComparator(String sortType) {
+
+ // 파일 이름 비교 기본 Comparator (대소문자 무시)
+ Comparator nameComparator =
+ Comparator.comparing(path -> path.getFileName().toString(), CASE_INSENSITIVE_ORDER);
+
+ Comparator dateComparator =
+ Comparator.comparing(
+ path -> {
+ try {
+ return Files.getLastModifiedTime(path);
+ } catch (IOException e) {
+ return FileTime.fromMillis(0);
+ }
+ });
+
+ if ("name desc".equalsIgnoreCase(sortType)) {
+ return nameComparator.reversed();
+ } else if ("date".equalsIgnoreCase(sortType)) {
+ return dateComparator;
+ } else if ("date desc".equalsIgnoreCase(sortType)) {
+ return dateComparator.reversed();
+ } else {
+ return nameComparator;
+ }
+ }
+
+ private static String findGdalinfoPath() {
+ // 일반적인 설치 경로 확인
+ String[] possiblePaths = {
+ "/usr/local/bin/gdalinfo", // Homebrew (macOS)
+ "/opt/homebrew/bin/gdalinfo", // Homebrew (Apple Silicon macOS)
+ "/usr/bin/gdalinfo", // Linux
+ "gdalinfo" // PATH에 있는 경우
+ };
+
+ for (String path : possiblePaths) {
+ if (isCommandAvailable(path)) {
+ return path;
+ }
+ }
+
+ return null;
+ }
+
+ private static boolean isCommandAvailable(String command) {
+ try {
+ ProcessBuilder pb = new ProcessBuilder(command, "--version");
+ pb.redirectErrorStream(true);
+ Process process = pb.start();
+
+ // 프로세스 완료 대기 (최대 5초)
+ boolean finished = process.waitFor(5, java.util.concurrent.TimeUnit.SECONDS);
+
+ if (!finished) {
+ process.destroy();
+ return false;
+ }
+
+ // 종료 코드가 0이면 정상 (일부 명령어는 --version에서 다른 코드 반환할 수 있음)
+ return process.exitValue() == 0 || process.exitValue() == 1;
+ } catch (Exception e) {
+ return false;
+ }
+ }
+
+ @Schema(name = "Folder", description = "폴더 정보")
+ @Getter
+ public static class Folder {
+ private final String folderNm;
+ private final String parentFolderNm;
+ private final String parentPath;
+ private final String fullPath;
+ private final int depth;
+ private final long childCnt;
+ private final String lastModified;
+ private final Boolean isValid;
+
+ public Folder(
+ String folderNm,
+ String parentFolderNm,
+ String parentPath,
+ String fullPath,
+ int depth,
+ long childCnt,
+ String lastModified,
+ Boolean isValid) {
+ this.folderNm = folderNm;
+ this.parentFolderNm = parentFolderNm;
+ this.parentPath = parentPath;
+ this.fullPath = fullPath;
+ this.depth = depth;
+ this.childCnt = childCnt;
+ this.lastModified = lastModified;
+ this.isValid = isValid;
+ }
+ }
+
+ @Schema(name = "File Basic", description = "파일 기본 정보")
+ @Getter
+ public static class Basic {
+
+ private final String fileNm;
+ private final String parentFolderNm;
+ private final String parentPath;
+ private final String fullPath;
+ private final String extension;
+ private final long fileSize;
+ private final String lastModified;
+
+ public Basic(
+ String fileNm,
+ String parentFolderNm,
+ String parentPath,
+ String fullPath,
+ String extension,
+ long fileSize,
+ String lastModified) {
+ this.fileNm = fileNm;
+ this.parentFolderNm = parentFolderNm;
+ this.parentPath = parentPath;
+ this.fullPath = fullPath;
+ this.extension = extension;
+ this.fileSize = fileSize;
+ this.lastModified = lastModified;
+ }
+ }
+}
diff --git a/src/main/java/com/kamco/cd/training/common/utils/NameValidator.java b/src/main/java/com/kamco/cd/training/common/utils/NameValidator.java
new file mode 100644
index 0000000..fff58fa
--- /dev/null
+++ b/src/main/java/com/kamco/cd/training/common/utils/NameValidator.java
@@ -0,0 +1,43 @@
+package com.kamco.cd.training.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;
+ }
+}
diff --git a/src/main/java/com/kamco/cd/training/common/utils/UserUtil.java b/src/main/java/com/kamco/cd/training/common/utils/UserUtil.java
new file mode 100644
index 0000000..4d1dd57
--- /dev/null
+++ b/src/main/java/com/kamco/cd/training/common/utils/UserUtil.java
@@ -0,0 +1,41 @@
+package com.kamco.cd.training.common.utils;
+
+import com.kamco.cd.training.auth.CustomUserDetails;
+import com.kamco.cd.training.members.dto.MembersDto;
+import com.kamco.cd.training.postgres.entity.MemberEntity;
+import java.util.Optional;
+import lombok.RequiredArgsConstructor;
+import org.springframework.security.core.context.SecurityContextHolder;
+import org.springframework.stereotype.Component;
+
+@Component
+@RequiredArgsConstructor
+public class UserUtil {
+
+ public MembersDto.Member getCurrentUser() {
+ return Optional.ofNullable(SecurityContextHolder.getContext().getAuthentication())
+ .filter(auth -> auth.getPrincipal() instanceof CustomUserDetails)
+ .map(
+ auth -> {
+ CustomUserDetails user = (CustomUserDetails) auth.getPrincipal();
+ MemberEntity m = user.getMember();
+ return new MembersDto.Member(m.getId(), m.getName(), m.getEmployeeNo());
+ })
+ .orElse(null);
+ }
+
+ public Long getId() {
+ MembersDto.Member user = getCurrentUser();
+ return user != null ? user.getId() : null;
+ }
+
+ public String getName() {
+ MembersDto.Member user = getCurrentUser();
+ return user != null ? user.getName() : null;
+ }
+
+ public String getEmployeeNo() {
+ MembersDto.Member user = getCurrentUser();
+ return user != null ? user.getEmployeeNo() : null;
+ }
+}
diff --git a/src/main/java/com/kamco/cd/training/common/utils/enums/CodeDto.java b/src/main/java/com/kamco/cd/training/common/utils/enums/CodeDto.java
new file mode 100644
index 0000000..edad49b
--- /dev/null
+++ b/src/main/java/com/kamco/cd/training/common/utils/enums/CodeDto.java
@@ -0,0 +1,20 @@
+package com.kamco.cd.training.common.utils.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;
+ }
+}
diff --git a/src/main/java/com/kamco/cd/training/common/utils/enums/CodeExpose.java b/src/main/java/com/kamco/cd/training/common/utils/enums/CodeExpose.java
new file mode 100644
index 0000000..cd431a8
--- /dev/null
+++ b/src/main/java/com/kamco/cd/training/common/utils/enums/CodeExpose.java
@@ -0,0 +1,10 @@
+package com.kamco.cd.training.common.utils.enums;
+
+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 {}
diff --git a/src/main/java/com/kamco/cd/training/common/utils/enums/EnumType.java b/src/main/java/com/kamco/cd/training/common/utils/enums/EnumType.java
new file mode 100644
index 0000000..5d2ea0f
--- /dev/null
+++ b/src/main/java/com/kamco/cd/training/common/utils/enums/EnumType.java
@@ -0,0 +1,8 @@
+package com.kamco.cd.training.common.utils.enums;
+
+public interface EnumType {
+
+ String getId();
+
+ String getText();
+}
diff --git a/src/main/java/com/kamco/cd/training/common/utils/enums/EnumValidator.java b/src/main/java/com/kamco/cd/training/common/utils/enums/EnumValidator.java
new file mode 100644
index 0000000..7904316
--- /dev/null
+++ b/src/main/java/com/kamco/cd/training/common/utils/enums/EnumValidator.java
@@ -0,0 +1,26 @@
+package com.kamco.cd.training.common.utils.enums;
+
+import com.kamco.cd.training.common.utils.interfaces.EnumValid;
+import jakarta.validation.ConstraintValidator;
+import jakarta.validation.ConstraintValidatorContext;
+import java.util.Arrays;
+import java.util.Set;
+import java.util.stream.Collectors;
+
+public class EnumValidator implements ConstraintValidator {
+
+ private Set acceptedValues;
+
+ @Override
+ public void initialize(EnumValid constraintAnnotation) {
+ acceptedValues =
+ Arrays.stream(constraintAnnotation.enumClass().getEnumConstants())
+ .map(Enum::name)
+ .collect(Collectors.toSet());
+ }
+
+ @Override
+ public boolean isValid(String value, ConstraintValidatorContext context) {
+ return value != null && acceptedValues.contains(value);
+ }
+}
diff --git a/src/main/java/com/kamco/cd/training/common/utils/enums/Enums.java b/src/main/java/com/kamco/cd/training/common/utils/enums/Enums.java
new file mode 100644
index 0000000..12ecf89
--- /dev/null
+++ b/src/main/java/com/kamco/cd/training/common/utils/enums/Enums.java
@@ -0,0 +1,76 @@
+package com.kamco.cd.training.common.utils.enums;
+
+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.training";
+
+ /** 노출 가능한 enum만 모아둔 맵 key: enum simpleName (예: RoleType) value: enum Class */
+ private static final Map>> exposedEnumMap = scanExposedEnumMap();
+
+ // code로 enum 찾기
+ public static & EnumType> E fromId(Class 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 toList(Class extends Enum>> enumClass) {
+ Object[] enums = enumClass.getEnumConstants();
+
+ return Arrays.stream(enums)
+ .map(e -> (EnumType) e)
+ .map(e -> new CodeDto(e.getId(), e.getText()))
+ .toList();
+ }
+
+ /** 특정 타입(enum)만 조회 /codes/{type} -> type = RoleType 같은 값 */
+ public static List getCodes(String type) {
+ Class extends Enum>> enumClass = exposedEnumMap.get(type);
+ if (enumClass == null) {
+ throw new IllegalArgumentException("지원하지 않는 코드 타입: " + type);
+ }
+ return toList(enumClass);
+ }
+
+ /** 전체 enum 코드 조회 */
+ public static Map> getAllCodes() {
+ Map> result = new HashMap<>();
+ for (Map.Entry>> e : exposedEnumMap.entrySet()) {
+ result.put(e.getKey(), toList(e.getValue()));
+ }
+ return result;
+ }
+
+ /**
+ * @CodeExpose + EnumType 인 enum만 스캔해서 Map 구성
+ */
+ private static Map>> scanExposedEnumMap() {
+ Reflections reflections = new Reflections(BASE_PACKAGE);
+
+ Set> types = reflections.getTypesAnnotatedWith(CodeExpose.class);
+
+ Map>> result = new HashMap<>();
+
+ for (Class> clazz : types) {
+ if (clazz.isEnum() && EnumType.class.isAssignableFrom(clazz)) {
+ result.put(clazz.getSimpleName(), (Class extends Enum>>) clazz);
+ }
+ }
+ return result;
+ }
+}
diff --git a/src/main/java/com/kamco/cd/training/common/utils/geometry/GeometryDeserializer.java b/src/main/java/com/kamco/cd/training/common/utils/geometry/GeometryDeserializer.java
new file mode 100644
index 0000000..9ae6fcd
--- /dev/null
+++ b/src/main/java/com/kamco/cd/training/common/utils/geometry/GeometryDeserializer.java
@@ -0,0 +1,36 @@
+package com.kamco.cd.training.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 extends StdDeserializer {
+
+ public GeometryDeserializer(Class 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();
+ return (T) reader.read(json);
+ } catch (Exception e) {
+ throw new IllegalArgumentException("Failed to deserialize GeoJSON into Geometry", e);
+ }
+ }
+}
diff --git a/src/main/java/com/kamco/cd/training/common/utils/geometry/GeometrySerializer.java b/src/main/java/com/kamco/cd/training/common/utils/geometry/GeometrySerializer.java
new file mode 100644
index 0000000..4d5e066
--- /dev/null
+++ b/src/main/java/com/kamco/cd/training/common/utils/geometry/GeometrySerializer.java
@@ -0,0 +1,31 @@
+package com.kamco.cd.training.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 extends StdSerializer {
+
+ // TODO: test code
+ public GeometrySerializer(Class 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();
+ }
+ }
+}
diff --git a/src/main/java/com/kamco/cd/training/common/utils/html/HtmlEscapeDeserializer.java b/src/main/java/com/kamco/cd/training/common/utils/html/HtmlEscapeDeserializer.java
new file mode 100644
index 0000000..81487a4
--- /dev/null
+++ b/src/main/java/com/kamco/cd/training/common/utils/html/HtmlEscapeDeserializer.java
@@ -0,0 +1,18 @@
+package com.kamco.cd.training.common.utils.html;
+
+import com.fasterxml.jackson.core.JacksonException;
+import com.fasterxml.jackson.core.JsonParser;
+import com.fasterxml.jackson.databind.DeserializationContext;
+import com.fasterxml.jackson.databind.JsonDeserializer;
+import java.io.IOException;
+import org.springframework.web.util.HtmlUtils;
+
+public class HtmlEscapeDeserializer extends JsonDeserializer