This commit is contained in:
2026-02-02 12:29:00 +09:00
parent f8f5cef6e1
commit 495ef7d86c
175 changed files with 45128 additions and 0 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,38 @@
package com.kamco.cd.training.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,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<Basic> getAllCommonCodes() {
try {
return commonCodeService.getFindAll();
} catch (Exception e) {
log.error("공통코드 전체 조회 중 오류 발생", e);
return List.of();
}
}
/**
* 특정 코드로 공통코드 조회
*
* @param code 코드값
* @return 해당 코드의 공통코드 목록
*/
public List<Basic> 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<Basic> 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<String> 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<Basic> 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;
}
}
}

View File

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

View File

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

View File

@@ -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<Double> 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<String> 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<Folder> getFolderAll(String dirPath, String sortType, int maxDepth) {
Path startPath = Paths.get(dirPath);
List<Folder> folderList = List.of();
try (Stream<Path> 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<Folder> getFolderAll(String dirPath) {
return getFolderAll(dirPath, "name", 1);
}
public static List<Folder> 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<Basic> 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<String> targetExtensions = createExtensionSet(extension);
List<Basic> fileList = new ArrayList<>();
SimpleDateFormat dttmFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
Predicate<Path> isTargetName =
p -> {
if (targetFileNm == null
|| targetFileNm.trim().isEmpty()
|| targetFileNm.trim().equals("*")) {
return true; // 전체 파일 허용
}
return p.getFileName().toString().contains(targetFileNm);
};
try (Stream<Path> 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<Basic> 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<FIleChecker.Basic> basicList =
FIleChecker.getFilesFromAllDepth(dir, targetFileNm, extension);
return (int)
basicList.stream().filter(dto -> dto.getExtension().toString().equals(extension)).count();
}
public static Long getFileTotSize(List<FIleChecker.Basic> 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<String> 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<Path> getFileComparator(String sortType) {
// 파일 이름 비교 기본 Comparator (대소문자 무시)
Comparator<Path> nameComparator =
Comparator.comparing(path -> path.getFileName().toString(), CASE_INSENSITIVE_ORDER);
Comparator<Path> 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;
}
}
}

View File

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,8 @@
package com.kamco.cd.training.common.utils.enums;
public interface EnumType {
String getId();
String getText();
}

View File

@@ -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<EnumValid, String> {
private Set<String> 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);
}
}

View File

@@ -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<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)
.map(e -> new CodeDto(e.getId(), e.getText()))
.toList();
}
/** 특정 타입(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

@@ -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<T extends Geometry> extends StdDeserializer<T> {
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();
return (T) reader.read(json);
} catch (Exception e) {
throw new IllegalArgumentException("Failed to deserialize GeoJSON into Geometry", e);
}
}
}

View File

@@ -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<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,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<Object> {
@Override
public Object deserialize(JsonParser jsonParser, DeserializationContext deserializationContext)
throws IOException, JacksonException {
String value = jsonParser.getValueAsString();
return value == null ? null : HtmlUtils.htmlEscape(value);
}
}

View File

@@ -0,0 +1,20 @@
package com.kamco.cd.training.common.utils.html;
import com.fasterxml.jackson.core.JsonGenerator;
import com.fasterxml.jackson.databind.JsonSerializer;
import com.fasterxml.jackson.databind.SerializerProvider;
import java.io.IOException;
import org.springframework.web.util.HtmlUtils;
public class HtmlUnescapeSerializer extends JsonSerializer<String> {
@Override
public void serialize(
String value, JsonGenerator jsonGenerator, SerializerProvider serializerProvider)
throws IOException {
if (value == null) {
jsonGenerator.writeNull();
} else {
jsonGenerator.writeString(HtmlUtils.htmlUnescape(value));
}
}
}

View File

@@ -0,0 +1,23 @@
package com.kamco.cd.training.common.utils.interfaces;
import com.kamco.cd.training.common.utils.enums.EnumValidator;
import jakarta.validation.Constraint;
import jakarta.validation.Payload;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
@Constraint(validatedBy = EnumValidator.class)
public @interface EnumValid {
String message() default "올바르지 않은 값입니다.";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
Class<? extends Enum<?>> enumClass();
}

View File

@@ -0,0 +1,15 @@
package com.kamco.cd.training.common.utils.interfaces;
import com.fasterxml.jackson.annotation.JacksonAnnotationsInside;
import com.fasterxml.jackson.annotation.JsonFormat;
import java.lang.annotation.*;
@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 {}