148 Commits

Author SHA1 Message Date
0c34ea7dcb hyperparam_with_modeltype 2026-02-12 18:48:14 +09:00
3547c28361 Merge pull request 'feat/training_260202' (#55) from feat/training_260202 into develop
Reviewed-on: #55
2026-02-12 16:56:23 +09:00
6c70bfed18 Merge remote-tracking branch 'origin/feat/training_260202' into feat/training_260202
# Conflicts:
#	src/main/java/com/kamco/cd/training/postgres/core/ModelTrainMngCoreService.java
2026-02-12 16:55:52 +09:00
95a75e63f4 임시폴더생성 api 추가 2026-02-12 16:55:10 +09:00
2a1dbee290 Merge pull request '모델학습 1단계 실행중인 것이 있는지 count API' (#54) from feat/training_260202 into develop
Reviewed-on: #54
2026-02-12 16:51:09 +09:00
384a321bf3 모델학습 1단계 실행중인 것이 있는지 count API 2026-02-12 16:50:40 +09:00
f4e97d389b Merge pull request 'file 확인 API 수정' (#53) from feat/training_260202 into develop
Reviewed-on: #53
2026-02-12 16:42:20 +09:00
590810ff0a file 확인 API 수정 2026-02-12 16:41:40 +09:00
a01c872982 Merge pull request 'feat/training_260202' (#52) from feat/training_260202 into develop
Reviewed-on: #52
2026-02-12 16:15:11 +09:00
905a245070 Merge branch 'feat/training_260202' of https://kamco.git.gs.dabeeo.com/MVPTeam/kamco-train-api into feat/training_260202 2026-02-12 16:14:45 +09:00
860ce35a8f docker mount 경로 추가 2026-02-12 16:14:19 +09:00
7f3f5dca40 Merge pull request 'feat/training_260202' (#51) from feat/training_260202 into develop
Reviewed-on: #51
2026-02-12 16:13:19 +09:00
4a0a4e35ed 학습 실행 수정 2026-02-12 16:12:58 +09:00
ae055dca1e 모델등록 수정 2026-02-12 16:01:14 +09:00
26e8e1492f Merge pull request 'feat/training_260202' (#50) from feat/training_260202 into develop
Reviewed-on: #50
2026-02-12 15:52:09 +09:00
8fa722011c 모델등록 수정 2026-02-12 15:51:54 +09:00
17d47d6200 Merge branch 'feat/training_260202' of https://kamco.git.gs.dabeeo.com/MVPTeam/kamco-train-api into feat/training_260202 2026-02-12 15:47:10 +09:00
e178f58fe2 chunk save log 추가 2026-02-12 15:47:06 +09:00
cd0cf5726d Merge pull request 'feat/training_260202' (#49) from feat/training_260202 into develop
Reviewed-on: #49
2026-02-12 15:44:11 +09:00
8e4bea53da 모델등록 수정 2026-02-12 15:43:52 +09:00
7a22d8ba73 containerName 생성 변경 2026-02-12 15:39:12 +09:00
2df4a7a80b csv 파일 읽는 경로 읽어서 수정, train은 epoch + 1 해서 저장 2026-02-12 15:24:30 +09:00
b451f697bc 모델 마스터 테이블 request,response 경로 추가 2026-02-12 14:59:35 +09:00
7e9c867f34 Merge pull request '모델 등록할 때 step1State를 READY로 업데이트' (#48) from feat/training_260202 into develop
Reviewed-on: #48
2026-02-12 14:35:52 +09:00
130e85f8a1 모델 등록할 때 step1State를 READY로 업데이트 2026-02-12 14:35:17 +09:00
9e713cb49d Merge pull request '업로드 로직 재수정' (#47) from feat/training_260202 into develop
Reviewed-on: #47
2026-02-12 14:21:57 +09:00
51dfa97900 업로드 로직 재수정 2026-02-12 14:21:08 +09:00
87c6b599b4 Merge pull request 'feat/training_260202' (#46) from feat/training_260202 into develop
Reviewed-on: #46
2026-02-12 12:10:04 +09:00
f50855a822 Merge branch 'feat/training_260202' of https://kamco.git.gs.dabeeo.com/MVPTeam/kamco-train-api into feat/training_260202 2026-02-12 12:08:04 +09:00
8d416317a8 베스트 에폭 API, 2단계 실행 시 best epoch 업데이트 2026-02-12 12:07:44 +09:00
22aa071476 Merge pull request 'feat/training_260202' (#45) from feat/training_260202 into develop
Reviewed-on: #45
2026-02-12 12:06:04 +09:00
a83bd09f8f containerName 생성 변경 2026-02-12 12:05:30 +09:00
96035f864a containerName 생성 변경 2026-02-12 11:42:38 +09:00
fd7dfd7e7f containerName 생성 변경 2026-02-12 11:10:28 +09:00
190b93bee8 실행 오류 수정 2026-02-12 10:58:51 +09:00
c5f19cc961 실행 오류 수정 2026-02-12 10:58:32 +09:00
c56c0ca605 실행 오류 수정 2026-02-12 10:58:26 +09:00
c6e721aa37 실행 오류 수정 2026-02-12 10:58:12 +09:00
6572e17f00 실행 오류 수정 2026-02-12 10:51:15 +09:00
be6365807c Merge pull request '실행 오류 수정' (#43) from feat/training_260202 into develop
Reviewed-on: #43
2026-02-12 10:20:05 +09:00
d2fff7dfde 실행 오류 수정 2026-02-12 10:19:44 +09:00
f66bc22c95 Merge pull request '실행 오류 수정' (#42) from feat/training_260202 into develop
Reviewed-on: #42
2026-02-12 10:14:54 +09:00
3367d0e7be 실행 오류 수정 2026-02-12 10:14:32 +09:00
352ec6ccb0 Merge pull request 'feat/training_260202' (#41) from feat/training_260202 into develop
Reviewed-on: #41
2026-02-12 09:53:02 +09:00
6a989255a3 모델별 데이터셋 목록 - G2,G3 dataTypeName 추가 2026-02-12 09:52:24 +09:00
878b21573f 테스트 실행 추가 2026-02-11 22:00:35 +09:00
0602db1436 Merge pull request '테스트 실행 추가' (#40) from feat/training_260202 into develop
Reviewed-on: #40
2026-02-11 21:58:58 +09:00
2f8bd1f98c 테스트 실행 추가 2026-02-11 21:58:25 +09:00
75231ccbba Merge pull request '추론 실행 추가' (#39) from feat/training_260202 into develop
Reviewed-on: #39
2026-02-11 20:22:01 +09:00
1249a80da5 추론 실행 추가 2026-02-11 20:21:25 +09:00
00c78eb42f Merge pull request '성능정보 그래프 데이터 API 추가' (#38) from feat/training_260202 into develop
Reviewed-on: #38
2026-02-11 19:52:23 +09:00
35767adba1 성능정보 그래프 데이터 API 추가 2026-02-11 19:52:00 +09:00
47a2a159ef Merge pull request 'test metrics 스케줄 추가' (#37) from feat/training_260202 into develop
Reviewed-on: #37
2026-02-11 19:10:37 +09:00
95548223cd test metrics 스케줄 추가 2026-02-11 19:09:58 +09:00
2debdc5312 Merge pull request 'feat/training_260202' (#36) from feat/training_260202 into develop
Reviewed-on: #36
2026-02-11 18:51:01 +09:00
207cc47f1b 스케줄 주석 2026-02-11 18:50:43 +09:00
b6338bce8e 테이블 구조 변경 2026-02-11 18:49:59 +09:00
2cfa2adcf5 tb_model_master 컬럼 추가 2026-02-11 17:21:48 +09:00
d7e19abfc9 uploadRate 로직 수정 2026-02-11 17:06:02 +09:00
c843703ee7 Merge pull request 'file 가져오기 86 호출하는 거로 추가' (#35) from feat/training_260202 into develop
Reviewed-on: #35
2026-02-11 16:53:25 +09:00
133ea6b1ba file 가져오기 86 호출하는 거로 추가 2026-02-11 16:49:48 +09:00
0df977ae81 Merge pull request '업로드 로직 86으로 수행하기 수정' (#34) from feat/training_260202 into develop
Reviewed-on: #34
2026-02-11 16:33:03 +09:00
3e39006822 업로드 로직 86으로 수행하기 수정 2026-02-11 16:32:40 +09:00
3ec1a71406 Merge pull request '업로드 로직 수정' (#33) from feat/training_260202 into develop
Reviewed-on: #33
2026-02-11 15:53:21 +09:00
16009f1623 업로드 로직 수정 2026-02-11 15:52:57 +09:00
41911014c9 Merge pull request '업로드 로직 수정' (#32) from feat/training_260202 into develop
Reviewed-on: #32
2026-02-11 15:44:54 +09:00
8ea32ce675 업로드 로직 수정 2026-02-11 15:44:18 +09:00
a4ac80c787 Merge pull request '업로드 경로 수정' (#31) from feat/training_260202 into develop
Reviewed-on: #31
2026-02-11 15:11:02 +09:00
3a5d136d34 업로드 경로 수정 2026-02-11 15:10:37 +09:00
2f63b9ddcd Merge pull request 'feat/training_260202' (#30) from feat/training_260202 into develop
Reviewed-on: #30
2026-02-11 14:08:58 +09:00
92de48b55e 전이학습 상세 로직 수정 2026-02-11 14:08:21 +09:00
224ddae68b 전이학습 상세 수정 2026-02-11 14:05:15 +09:00
885b72a0c6 Merge pull request '모델별 데이터셋 목록 조회 수정' (#29) from feat/training_260202 into develop
Reviewed-on: #29
2026-02-11 12:29:08 +09:00
9ac00d37c5 모델별 데이터셋 목록 조회 수정 2026-02-11 12:28:38 +09:00
fbb5a34867 Merge pull request '업로드 경로 원복' (#28) from feat/training_260202 into develop
Reviewed-on: #28
2026-02-11 12:12:43 +09:00
e25fc01b25 업로드 경로 원복 2026-02-11 12:12:08 +09:00
6b3f22dd66 Merge pull request '업로드 파일 max 수정' (#27) from feat/training_260202 into develop
Reviewed-on: #27
2026-02-11 11:53:04 +09:00
abc2c8e806 업로드 파일 max 수정 2026-02-11 11:52:41 +09:00
29e1d0ec7e Merge pull request '업로드 경로 수정' (#26) from feat/training_260202 into develop
Reviewed-on: #26
2026-02-11 11:41:49 +09:00
8d379d064c 업로드 경로 수정 2026-02-11 11:38:55 +09:00
a2072e0148 Merge pull request 'feat/training_260202' (#25) from feat/training_260202 into develop
Reviewed-on: #25
2026-02-11 10:28:38 +09:00
6352f74b08 업로드 성공 후 COMPLETED 해주기 2026-02-10 18:28:42 +09:00
025b573859 학습데이터 obj-list API geojson 로직 수정 2026-02-10 16:36:07 +09:00
0e9fa80092 학습데이터 등록 로직 커밋 2026-02-10 16:15:21 +09:00
2d5de88a6b Merge pull request '학습데이터 업로드, unzip 로직 진행중' (#24) from feat/training_260202 into develop
Reviewed-on: #24
2026-02-10 10:44:28 +09:00
89744d2aa1 학습데이터 업로드, unzip 로직 진행중 2026-02-10 10:43:40 +09:00
eda1d19942 Merge pull request '스웨거 로그인 설정 수정' (#23) from feat/training_260202 into develop
Reviewed-on: #23
2026-02-06 14:54:56 +09:00
b4a4486560 스웨거 로그인 설정 수정 2026-02-06 14:51:45 +09:00
653717a074 Merge pull request 'feat/training_260202' (#22) from feat/training_260202 into develop
Reviewed-on: #22
2026-02-06 11:09:36 +09:00
7cc3392856 사용 가능 공간 조회 API 추가 2026-02-06 11:09:13 +09:00
def84d2b1c 학습데이터 obj 삭제 스웨거 문구 수정 2026-02-06 10:33:12 +09:00
679795d14d Merge pull request '전이학습 추가' (#21) from feat/training_260202 into develop
Reviewed-on: #21
2026-02-05 18:23:30 +09:00
0a7f01a2f5 전이학습 추가 2026-02-05 18:23:07 +09:00
9655c62d35 Merge pull request '하이퍼 파라미터 최적값 조회 수정' (#20) from feat/training_260202 into develop
Reviewed-on: #20
2026-02-05 15:22:56 +09:00
db6844f0e7 하이퍼 파라미터 최적값 조회 수정 2026-02-05 15:22:34 +09:00
af16933378 Merge pull request 'feat/training_260202' (#19) from feat/training_260202 into develop
Reviewed-on: #19
2026-02-05 15:08:41 +09:00
29b653a4e9 데이터셋 상세조회 class 조회 추가 2026-02-05 15:08:22 +09:00
693e3ef3ab 모델학습 데이터셋 선택 목록 수정 2026-02-05 14:22:37 +09:00
03135a972a Merge pull request '모델학습 데이터셋 선택 목록 수정' (#18) from feat/training_260202 into develop
Reviewed-on: #18
2026-02-05 13:59:00 +09:00
381b7d7e0b 모델학습 데이터셋 선택 목록 수정 2026-02-05 13:58:31 +09:00
947cba2742 Merge pull request 'feat/training_260202' (#17) from feat/training_260202 into develop
Reviewed-on: #17
2026-02-04 19:54:23 +09:00
f038fdd1db 모델 상세 추가 API 커밋 2026-02-04 19:54:00 +09:00
474a3c119e 모델학습 config 정보 조회 추가 2026-02-04 19:51:43 +09:00
b2be43a76e 모델 상세 API 커밋 2026-02-04 19:46:57 +09:00
ce69bacb01 모델학습 데이터 등록 수정 2026-02-04 19:15:10 +09:00
200b384e19 모델 종류 이름 변경 2026-02-04 18:41:34 +09:00
350d622e5a 미사용 소스 정리 2026-02-04 18:26:02 +09:00
b25fc6fe68 Merge pull request 'feat/training_260202' (#16) from feat/training_260202 into develop
Reviewed-on: #16
2026-02-04 18:01:15 +09:00
6cdf4efda6 데이터셋 등록 추가 2026-02-04 18:00:56 +09:00
7d866e5869 데이터셋 테이블 수정 2026-02-04 15:13:13 +09:00
5bc59c0e0b Merge pull request '데이터셋 조회 수정' (#15) from feat/training_260202 into develop
Reviewed-on: #15
2026-02-04 14:11:00 +09:00
3c0a12da4e 데이터셋 조회 수정 2026-02-04 14:10:46 +09:00
cdac9d6148 Merge pull request '모델학습 설정 dto 수정' (#14) from feat/training_260202 into develop
Reviewed-on: #14
2026-02-04 14:03:41 +09:00
abe4272227 모델학습 설정 dto 수정 2026-02-04 14:03:25 +09:00
d238debcc8 Merge pull request '모델학습 설정 dto 수정' (#13) from feat/training_260202 into develop
Reviewed-on: #13
2026-02-04 13:57:00 +09:00
fdfda049f8 모델학습 설정 dto 수정 2026-02-04 13:56:32 +09:00
3e780ef007 Merge pull request 'feat/training_260202' (#12) from feat/training_260202 into develop
Reviewed-on: #12
2026-02-04 12:32:36 +09:00
2c825b14ee 모델학습 설정 dto 수정 2026-02-04 12:32:22 +09:00
f9d081970d 모델학습 설정 dto 수정 2026-02-04 12:29:09 +09:00
2110f395b7 Merge pull request 'dataset 테이블 수정, 모델학습 설정 dto 추가' (#11) from feat/training_260202 into develop
Reviewed-on: #11
2026-02-04 12:25:19 +09:00
60d45ee2ce dataset 테이블 수정, 모델학습 설정 dto 추가 2026-02-04 12:24:55 +09:00
50464c1aa8 Merge pull request 'feat/training_260202' (#10) from feat/training_260202 into develop
Reviewed-on: #10
2026-02-03 18:52:24 +09:00
f1ad59d0b1 데이터셋 API 커밋 2026-02-03 18:51:56 +09:00
19644e5c9f dataset 테이블 수정 2026-02-03 18:51:35 +09:00
4c80017fc5 dataset 테이블 수정 2026-02-03 18:46:00 +09:00
5dfcd3d181 Merge pull request '하이퍼파라미터 , 모델관리 수정' (#9) from feat/training_260202 into develop
Reviewed-on: #9
2026-02-03 18:25:13 +09:00
6e99c209d6 하이퍼파라미터 , 모델관리 수정 2026-02-03 18:24:49 +09:00
38b037da31 Merge pull request 'feat/training_260202' (#8) from feat/training_260202 into develop
Reviewed-on: #8
2026-02-03 16:32:02 +09:00
d66711e4f4 하이퍼파라미터 기능 추가 2026-02-03 16:31:43 +09:00
44878e9c37 하이퍼파라미터 기능 추가 2026-02-03 15:15:25 +09:00
53c8e1dd50 Merge pull request 'feat/training_260202' (#7) from feat/training_260202 into develop
Reviewed-on: #7
2026-02-03 15:09:29 +09:00
15aa2fa041 하이퍼파라미터 기능 추가 2026-02-03 15:09:03 +09:00
335e9d33d6 하이퍼파라미터 기능 추가 2026-02-03 15:05:39 +09:00
d6f16544e2 Merge pull request '하이퍼파라미터 기능 추가' (#6) from feat/training_260202 into develop
Reviewed-on: #6
2026-02-03 14:39:27 +09:00
3bb52bb344 하이퍼파라미터 기능 추가 2026-02-03 14:39:10 +09:00
77912b7081 Merge pull request '하이퍼파라미터 기능 추가' (#5) from feat/training_260202 into develop
Reviewed-on: #5
2026-02-03 14:32:31 +09:00
3a8d6e3ef0 하이퍼파라미터 기능 추가 2026-02-03 14:31:53 +09:00
25d4cbf672 Merge pull request 'feat/training_260202' (#4) from feat/training_260202 into develop
Reviewed-on: #4
2026-02-02 20:20:37 +09:00
e2757d3ca0 모델관리 학습 조회 수정 2026-02-02 20:19:30 +09:00
9956ca7d63 모델관리 학습 조회 수정 2026-02-02 19:42:48 +09:00
210306151b Merge pull request 'feat/training_260202' (#3) from feat/training_260202 into develop
Reviewed-on: #3
2026-02-02 19:32:22 +09:00
d168e9712e 모델관리 학습 조회 수정 2026-02-02 19:32:00 +09:00
999e413305 모델관리 학습 조회 수정 2026-02-02 19:13:18 +09:00
8619ded142 학습데이터관리 api 문구 수정 2026-02-02 18:00:58 +09:00
4b64b03e53 Merge pull request 'upload 수정' (#2) from feat/training_260202 into develop
Reviewed-on: #2
2026-02-02 17:53:29 +09:00
c7d7be9d06 upload 수정 2026-02-02 17:53:03 +09:00
37f8d728fa Merge pull request 'init spotless 적용' (#1) from feat/training_260202 into develop
Reviewed-on: #1
2026-02-02 15:49:14 +09:00
a1ffad1c4e init spotless 적용 2026-02-02 15:48:23 +09:00
232 changed files with 18802 additions and 12952 deletions

View File

@@ -3,6 +3,7 @@ plugins {
id 'org.springframework.boot' version '3.5.7'
id 'io.spring.dependency-management' version '1.1.7'
id 'com.diffplug.spotless' version '6.25.0'
id 'idea'
}
group = 'com.kamco.cd'
@@ -21,11 +22,23 @@ configurations {
}
}
// QueryDSL 생성된 소스 디렉토리 정의
def generatedSourcesDir = file("$buildDir/generated/sources/annotationProcessor/java/main")
repositories {
mavenCentral()
maven { url "https://repo.osgeo.org/repository/release/" }
}
// Gradle이 생성된 소스를 컴파일 경로에 포함하도록 설정
sourceSets {
main {
java {
srcDirs += generatedSourcesDir
}
}
}
dependencies {
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
implementation 'org.springframework.boot:spring-boot-starter-web'
@@ -83,6 +96,23 @@ dependencies {
implementation 'io.hypersistence:hypersistence-utils-hibernate-63:3.7.0'
implementation 'org.reflections:reflections:0.10.2'
implementation 'com.jcraft:jsch:0.1.55'
implementation 'org.apache.commons:commons-csv:1.10.0'
}
// IntelliJ가 생성된 소스를 인식하도록 설정
idea {
module {
// 소스 디렉토리로 인식
sourceDirs += generatedSourcesDir
// Generated Sources Root로 마킹 (IntelliJ에서 특별 처리)
generatedSourceDirs += generatedSourcesDir
// 소스 및 Javadoc 다운로드
downloadJavadoc = true
downloadSources = true
}
}
configurations.configureEach {
@@ -93,6 +123,21 @@ tasks.named('test') {
useJUnitPlatform()
}
// 컴파일 전 생성된 소스 디렉토리 생성 보장
tasks.named('compileJava') {
doFirst {
generatedSourcesDir.mkdirs()
}
}
// 생성된 소스 정리 태스크
tasks.register('cleanGeneratedSources', Delete) {
delete generatedSourcesDir
}
tasks.named('clean') {
dependsOn 'cleanGeneratedSources'
}
bootJar {
archiveFileName = 'ROOT.jar'

View File

@@ -14,6 +14,7 @@ services:
- /mnt/nfs_share/images:/app/original-images
- /mnt/nfs_share/model_output:/app/model-outputs
- /mnt/nfs_share/train_dataset:/app/train-dataset
- /home/kcomu/data:/home/kcomu/data
networks:
- kamco-cds
restart: unless-stopped

0
gradlew vendored Normal file → Executable file
View File

View File

@@ -1,14 +1,16 @@
package com.kamco.cd.training;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.scheduling.annotation.EnableScheduling;
@SpringBootApplication
@EnableScheduling
public class KamcoTrainingApplication {
public static void main(String[] args) {
SpringApplication.run(KamcoTrainingApplication.class, args);
}
}
package com.kamco.cd.training;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.scheduling.annotation.EnableAsync;
import org.springframework.scheduling.annotation.EnableScheduling;
@EnableAsync
@SpringBootApplication
@EnableScheduling
public class KamcoTrainingApplication {
public static void main(String[] args) {
SpringApplication.run(KamcoTrainingApplication.class, args);
}
}

View File

@@ -1,22 +1,22 @@
package com.kamco.cd.training.auth;
import java.security.SecureRandom;
import java.util.Base64;
public class BCryptSaltGenerator {
public static String generateSaltWithEmployeeNo(String employeeNo) {
// bcrypt salt는 16바이트(128비트) 필요
byte[] randomBytes = new byte[16];
new SecureRandom().nextBytes(randomBytes);
String base64 = Base64.getEncoder().encodeToString(randomBytes);
// 사번을 포함 (22자 제한 → 잘라내기)
String mixedSalt = (employeeNo + base64).substring(0, 22);
// bcrypt 포맷에 맞게 구성
return "$2a$10$" + mixedSalt;
}
}
package com.kamco.cd.training.auth;
import java.security.SecureRandom;
import java.util.Base64;
public class BCryptSaltGenerator {
public static String generateSaltWithEmployeeNo(String employeeNo) {
// bcrypt salt는 16바이트(128비트) 필요
byte[] randomBytes = new byte[16];
new SecureRandom().nextBytes(randomBytes);
String base64 = Base64.getEncoder().encodeToString(randomBytes);
// 사번을 포함 (22자 제한 → 잘라내기)
String mixedSalt = (employeeNo + base64).substring(0, 22);
// bcrypt 포맷에 맞게 구성
return "$2a$10$" + mixedSalt;
}
}

View File

@@ -1,62 +1,62 @@
package com.kamco.cd.training.auth;
import com.kamco.cd.training.common.enums.StatusType;
import com.kamco.cd.training.common.enums.error.AuthErrorCode;
import com.kamco.cd.training.common.exception.CustomApiException;
import com.kamco.cd.training.postgres.entity.MemberEntity;
import com.kamco.cd.training.postgres.repository.members.MembersRepository;
import lombok.RequiredArgsConstructor;
import org.mindrot.jbcrypt.BCrypt;
import org.springframework.security.authentication.AuthenticationProvider;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.stereotype.Component;
@Component
@RequiredArgsConstructor
public class CustomAuthenticationProvider implements AuthenticationProvider {
private final MembersRepository membersRepository;
@Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
String username = authentication.getName();
String rawPassword = authentication.getCredentials().toString();
// 유저 조회
MemberEntity member =
membersRepository
.findByEmployeeNo(username)
.orElseThrow(() -> new CustomApiException(AuthErrorCode.LOGIN_ID_NOT_FOUND));
// 미사용 상태
if (member.getStatus().equals(StatusType.INACTIVE.getId())) {
throw new CustomApiException(AuthErrorCode.LOGIN_ID_NOT_FOUND);
}
// jBCrypt + 커스텀 salt 로 저장된 패스워드 비교
if (!BCrypt.checkpw(rawPassword, member.getPassword())) {
// 실패 카운트 저장
int cnt = member.getLoginFailCount() + 1;
member.setLoginFailCount(cnt);
membersRepository.save(member);
throw new CustomApiException(AuthErrorCode.LOGIN_PASSWORD_MISMATCH);
}
// 로그인 실패 체크
if (member.getLoginFailCount() >= 5) {
throw new CustomApiException(AuthErrorCode.LOGIN_PASSWORD_EXCEEDED);
}
// 인증 성공 → UserDetails 생성
CustomUserDetails userDetails = new CustomUserDetails(member);
return new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities());
}
@Override
public boolean supports(Class<?> authentication) {
return UsernamePasswordAuthenticationToken.class.isAssignableFrom(authentication);
}
}
package com.kamco.cd.training.auth;
import com.kamco.cd.training.common.enums.StatusType;
import com.kamco.cd.training.common.enums.error.AuthErrorCode;
import com.kamco.cd.training.common.exception.CustomApiException;
import com.kamco.cd.training.postgres.entity.MemberEntity;
import com.kamco.cd.training.postgres.repository.members.MembersRepository;
import lombok.RequiredArgsConstructor;
import org.mindrot.jbcrypt.BCrypt;
import org.springframework.security.authentication.AuthenticationProvider;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.stereotype.Component;
@Component
@RequiredArgsConstructor
public class CustomAuthenticationProvider implements AuthenticationProvider {
private final MembersRepository membersRepository;
@Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
String username = authentication.getName();
String rawPassword = authentication.getCredentials().toString();
// 유저 조회
MemberEntity member =
membersRepository
.findByEmployeeNo(username)
.orElseThrow(() -> new CustomApiException(AuthErrorCode.LOGIN_ID_NOT_FOUND));
// 미사용 상태
if (member.getStatus().equals(StatusType.INACTIVE.getId())) {
throw new CustomApiException(AuthErrorCode.LOGIN_ID_NOT_FOUND);
}
// jBCrypt + 커스텀 salt 로 저장된 패스워드 비교
if (!BCrypt.checkpw(rawPassword, member.getPassword())) {
// 실패 카운트 저장
int cnt = member.getLoginFailCount() + 1;
member.setLoginFailCount(cnt);
membersRepository.save(member);
throw new CustomApiException(AuthErrorCode.LOGIN_PASSWORD_MISMATCH);
}
// 로그인 실패 체크
if (member.getLoginFailCount() >= 5) {
throw new CustomApiException(AuthErrorCode.LOGIN_PASSWORD_EXCEEDED);
}
// 인증 성공 → UserDetails 생성
CustomUserDetails userDetails = new CustomUserDetails(member);
return new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities());
}
@Override
public boolean supports(Class<?> authentication) {
return UsernamePasswordAuthenticationToken.class.isAssignableFrom(authentication);
}
}

View File

@@ -1,56 +1,56 @@
package com.kamco.cd.training.auth;
import com.kamco.cd.training.postgres.entity.MemberEntity;
import java.util.Collection;
import java.util.List;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
public class CustomUserDetails implements UserDetails {
private final MemberEntity member;
public CustomUserDetails(MemberEntity member) {
this.member = member;
}
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
return List.of(new SimpleGrantedAuthority("ROLE_" + member.getUserRole()));
}
@Override
public String getPassword() {
return member.getPassword();
}
@Override
public String getUsername() {
return String.valueOf(member.getUuid());
}
@Override
public boolean isAccountNonExpired() {
return true; // 추후 상태 필드에 따라 수정 가능
}
@Override
public boolean isAccountNonLocked() {
return true;
}
@Override
public boolean isCredentialsNonExpired() {
return true;
}
@Override
public boolean isEnabled() {
return "ACTIVE".equals(member.getStatus());
}
public MemberEntity getMember() {
return member;
}
}
package com.kamco.cd.training.auth;
import com.kamco.cd.training.postgres.entity.MemberEntity;
import java.util.Collection;
import java.util.List;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
public class CustomUserDetails implements UserDetails {
private final MemberEntity member;
public CustomUserDetails(MemberEntity member) {
this.member = member;
}
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
return List.of(new SimpleGrantedAuthority("ROLE_" + member.getUserRole()));
}
@Override
public String getPassword() {
return member.getPassword();
}
@Override
public String getUsername() {
return String.valueOf(member.getUuid());
}
@Override
public boolean isAccountNonExpired() {
return true; // 추후 상태 필드에 따라 수정 가능
}
@Override
public boolean isAccountNonLocked() {
return true;
}
@Override
public boolean isCredentialsNonExpired() {
return true;
}
@Override
public boolean isEnabled() {
return "ACTIVE".equals(member.getStatus());
}
public MemberEntity getMember() {
return member;
}
}

View File

@@ -1,70 +1,70 @@
package com.kamco.cd.training.auth;
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import java.io.IOException;
import lombok.RequiredArgsConstructor;
import org.springframework.lang.NonNull;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.stereotype.Component;
import org.springframework.util.AntPathMatcher;
import org.springframework.web.filter.OncePerRequestFilter;
@Component
@RequiredArgsConstructor
public class JwtAuthenticationFilter extends OncePerRequestFilter {
private final JwtTokenProvider jwtTokenProvider;
private final UserDetailsService userDetailsService;
private static final AntPathMatcher PATH_MATCHER = new AntPathMatcher();
private static final String[] EXCLUDE_PATHS = {
"/api/auth/signin", "/api/auth/refresh", "/api/auth/logout", "/api/members/*/password"
};
@Override
protected void doFilterInternal(
@NonNull HttpServletRequest request,
@NonNull HttpServletResponse response,
@NonNull FilterChain filterChain)
throws ServletException, IOException {
String token = resolveToken(request);
if (token != null && jwtTokenProvider.isValidToken(token)) {
String username = jwtTokenProvider.getSubject(token);
UserDetails userDetails = userDetailsService.loadUserByUsername(username);
UsernamePasswordAuthenticationToken authentication =
new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities());
SecurityContextHolder.getContext().setAuthentication(authentication);
}
filterChain.doFilter(request, response);
}
@Override
protected boolean shouldNotFilter(HttpServletRequest request) {
String path = request.getServletPath();
// JWT 필터를 타지 않게 할 URL 패턴들
for (String pattern : EXCLUDE_PATHS) {
if (PATH_MATCHER.match(pattern, path)) {
return true;
}
}
return false;
}
private String resolveToken(HttpServletRequest request) {
String bearer = request.getHeader("Authorization");
if (bearer != null && bearer.startsWith("Bearer ")) {
return bearer.substring(7);
}
return null;
}
}
package com.kamco.cd.training.auth;
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import java.io.IOException;
import lombok.RequiredArgsConstructor;
import org.springframework.lang.NonNull;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.stereotype.Component;
import org.springframework.util.AntPathMatcher;
import org.springframework.web.filter.OncePerRequestFilter;
@Component
@RequiredArgsConstructor
public class JwtAuthenticationFilter extends OncePerRequestFilter {
private final JwtTokenProvider jwtTokenProvider;
private final UserDetailsService userDetailsService;
private static final AntPathMatcher PATH_MATCHER = new AntPathMatcher();
private static final String[] EXCLUDE_PATHS = {
"/api/auth/signin", "/api/auth/refresh", "/api/auth/logout", "/api/members/*/password"
};
@Override
protected void doFilterInternal(
@NonNull HttpServletRequest request,
@NonNull HttpServletResponse response,
@NonNull FilterChain filterChain)
throws ServletException, IOException {
String token = resolveToken(request);
if (token != null && jwtTokenProvider.isValidToken(token)) {
String username = jwtTokenProvider.getSubject(token);
UserDetails userDetails = userDetailsService.loadUserByUsername(username);
UsernamePasswordAuthenticationToken authentication =
new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities());
SecurityContextHolder.getContext().setAuthentication(authentication);
}
filterChain.doFilter(request, response);
}
@Override
protected boolean shouldNotFilter(HttpServletRequest request) {
String path = request.getServletPath();
// JWT 필터를 타지 않게 할 URL 패턴들
for (String pattern : EXCLUDE_PATHS) {
if (PATH_MATCHER.match(pattern, path)) {
return true;
}
}
return false;
}
private String resolveToken(HttpServletRequest request) {
String bearer = request.getHeader("Authorization");
if (bearer != null && bearer.startsWith("Bearer ")) {
return bearer.substring(7);
}
return null;
}
}

View File

@@ -1,72 +1,72 @@
package com.kamco.cd.training.auth;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jws;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.security.Keys;
import jakarta.annotation.PostConstruct;
import java.nio.charset.StandardCharsets;
import java.util.Date;
import javax.crypto.SecretKey;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
@Component
public class JwtTokenProvider {
@Value("${jwt.secret}")
private String secret;
@Value("${jwt.access-token-validity-in-ms}")
private long accessTokenValidityInMs;
@Value("${jwt.refresh-token-validity-in-ms}")
private long refreshTokenValidityInMs;
private SecretKey key;
@PostConstruct
public void init() {
// HS256용 SecretKey
this.key = Keys.hmacShaKeyFor(secret.getBytes(StandardCharsets.UTF_8));
}
public String createAccessToken(String subject) {
return createToken(subject, accessTokenValidityInMs);
}
public String createRefreshToken(String subject) {
return createToken(subject, refreshTokenValidityInMs);
}
private String createToken(String subject, long validityInMs) {
Date now = new Date();
Date expiry = new Date(now.getTime() + validityInMs);
return Jwts.builder().subject(subject).issuedAt(now).expiration(expiry).signWith(key).compact();
}
public String getSubject(String token) {
var claims = parseClaims(token).getPayload();
return claims.getSubject();
}
public boolean isValidToken(String token) {
try {
Jws<Claims> claims = parseClaims(token);
return !claims.getPayload().getExpiration().before(new Date());
} catch (Exception e) {
return false;
}
}
private Jws<Claims> parseClaims(String token) {
return Jwts.parser()
.verifyWith(key) // SecretKey 타입
.build()
.parseSignedClaims(token);
}
public long getRefreshTokenValidityInMs() {
return refreshTokenValidityInMs;
}
}
package com.kamco.cd.training.auth;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jws;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.security.Keys;
import jakarta.annotation.PostConstruct;
import java.nio.charset.StandardCharsets;
import java.util.Date;
import javax.crypto.SecretKey;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
@Component
public class JwtTokenProvider {
@Value("${jwt.secret}")
private String secret;
@Value("${jwt.access-token-validity-in-ms}")
private long accessTokenValidityInMs;
@Value("${jwt.refresh-token-validity-in-ms}")
private long refreshTokenValidityInMs;
private SecretKey key;
@PostConstruct
public void init() {
// HS256용 SecretKey
this.key = Keys.hmacShaKeyFor(secret.getBytes(StandardCharsets.UTF_8));
}
public String createAccessToken(String subject) {
return createToken(subject, accessTokenValidityInMs);
}
public String createRefreshToken(String subject) {
return createToken(subject, refreshTokenValidityInMs);
}
private String createToken(String subject, long validityInMs) {
Date now = new Date();
Date expiry = new Date(now.getTime() + validityInMs);
return Jwts.builder().subject(subject).issuedAt(now).expiration(expiry).signWith(key).compact();
}
public String getSubject(String token) {
var claims = parseClaims(token).getPayload();
return claims.getSubject();
}
public boolean isValidToken(String token) {
try {
Jws<Claims> claims = parseClaims(token);
return !claims.getPayload().getExpiration().before(new Date());
} catch (Exception e) {
return false;
}
}
private Jws<Claims> parseClaims(String token) {
return Jwts.parser()
.verifyWith(key) // SecretKey 타입
.build()
.parseSignedClaims(token);
}
public long getRefreshTokenValidityInMs() {
return refreshTokenValidityInMs;
}
}

View File

@@ -1,299 +1,299 @@
package com.kamco.cd.training.code;
import com.kamco.cd.training.code.dto.CommonCodeDto;
import com.kamco.cd.training.code.service.CommonCodeService;
import com.kamco.cd.training.common.utils.CommonCodeUtil;
import com.kamco.cd.training.common.utils.enums.CodeDto;
import com.kamco.cd.training.config.api.ApiResponseDto;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.media.Content;
import io.swagger.v3.oas.annotations.media.Schema;
import io.swagger.v3.oas.annotations.responses.ApiResponse;
import io.swagger.v3.oas.annotations.responses.ApiResponses;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.validation.Valid;
import java.util.List;
import java.util.Map;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.*;
@Tag(name = "공통코드 관리", description = "공통코드 관리 API")
@RestController
@RequiredArgsConstructor
@RequestMapping("/api/code")
public class CommonCodeApiController {
private final CommonCodeService commonCodeService;
private final CommonCodeUtil commonCodeUtil;
@Operation(summary = "목록 조회", description = "모든 공통코드 조회")
@ApiResponses(
value = {
@ApiResponse(
responseCode = "200",
description = "조회 성공",
content =
@Content(
mediaType = "application/json",
schema = @Schema(implementation = CommonCodeDto.Basic.class))),
@ApiResponse(responseCode = "404", description = "코드를 찾을 수 없음", content = @Content),
@ApiResponse(responseCode = "500", description = "서버 오류", content = @Content)
})
@GetMapping
public ApiResponseDto<List<CommonCodeDto.Basic>> getFindAll() {
return ApiResponseDto.createOK(commonCodeService.getFindAll());
}
@Operation(summary = "단건 조회", description = "단건 조회")
@ApiResponses(
value = {
@ApiResponse(
responseCode = "200",
description = "조회 성공",
content =
@Content(
mediaType = "application/json",
schema = @Schema(implementation = CommonCodeDto.Basic.class))),
@ApiResponse(responseCode = "404", description = "코드를 찾을 수 없음", content = @Content),
@ApiResponse(responseCode = "500", description = "서버 오류", content = @Content)
})
@GetMapping("/{id}")
public ApiResponseDto<CommonCodeDto.Basic> getOneById(
@io.swagger.v3.oas.annotations.parameters.RequestBody(description = "단건 조회", required = true)
@PathVariable
Long id) {
return ApiResponseDto.ok(commonCodeService.getOneById(id));
}
@Operation(summary = "저장", description = "공통코드를 저장 합니다.")
@ApiResponses(
value = {
@ApiResponse(
responseCode = "201",
description = "공통코드 저장 성공",
content =
@Content(
mediaType = "application/json",
schema = @Schema(implementation = Long.class))),
@ApiResponse(responseCode = "400", description = "잘못된 요청 데이터", content = @Content),
@ApiResponse(responseCode = "404", description = "코드를 찾을 수 없음", content = @Content),
@ApiResponse(responseCode = "500", description = "서버 오류", content = @Content)
})
@PostMapping
public ApiResponseDto<ApiResponseDto.ResponseObj> save(
@io.swagger.v3.oas.annotations.parameters.RequestBody(
description = "공통코드 생성 요청 정보",
required = true,
content =
@Content(
mediaType = "application/json",
schema = @Schema(implementation = CommonCodeDto.AddReq.class)))
@RequestBody
@Valid
CommonCodeDto.AddReq req) {
return ApiResponseDto.okObject(commonCodeService.save(req));
}
@Operation(summary = "수정", description = "공통코드를 수정 합니다.")
@ApiResponses(
value = {
@ApiResponse(
responseCode = "204",
description = "공통코드 수정 성공",
content =
@Content(
mediaType = "application/json",
schema = @Schema(implementation = Long.class))),
@ApiResponse(responseCode = "400", description = "잘못된 요청 데이터", content = @Content),
@ApiResponse(responseCode = "404", description = "코드를 찾을 수 없음", content = @Content),
@ApiResponse(responseCode = "500", description = "서버 오류", content = @Content)
})
@PutMapping("/{id}")
public ApiResponseDto<ApiResponseDto.ResponseObj> update(
@io.swagger.v3.oas.annotations.parameters.RequestBody(
description = "공통코드 수정 요청 정보",
required = true,
content =
@Content(
mediaType = "application/json",
schema = @Schema(implementation = CommonCodeDto.ModifyReq.class)))
@PathVariable
Long id,
@RequestBody @Valid CommonCodeDto.ModifyReq req) {
return ApiResponseDto.okObject(commonCodeService.update(id, req));
}
@Operation(summary = "삭제", description = "공통코드를 삭제 합니다.")
@ApiResponses(
value = {
@ApiResponse(
responseCode = "204",
description = "공통코드 삭제 성공",
content =
@Content(
mediaType = "application/json",
schema = @Schema(implementation = Long.class))),
@ApiResponse(responseCode = "400", description = "잘못된 요청 데이터", content = @Content),
@ApiResponse(responseCode = "404", description = "코드를 찾을 수 없음", content = @Content),
@ApiResponse(responseCode = "500", description = "서버 오류", content = @Content)
})
@DeleteMapping("/{id}")
public ApiResponseDto<ApiResponseDto.ResponseObj> remove(
@io.swagger.v3.oas.annotations.parameters.RequestBody(
description = "공통코드 삭제 요청 정보",
required = true)
@PathVariable
Long id) {
return ApiResponseDto.okObject(commonCodeService.removeCode(id));
}
@Operation(summary = "순서 변경", description = "공통코드 순서를 변경 합니다.")
@ApiResponses(
value = {
@ApiResponse(
responseCode = "204",
description = "공통코드 순서 변경 성공",
content =
@Content(
mediaType = "application/json",
schema = @Schema(implementation = Long.class))),
@ApiResponse(responseCode = "400", description = "잘못된 요청 데이터", content = @Content),
@ApiResponse(responseCode = "404", description = "코드를 찾을 수 없음", content = @Content),
@ApiResponse(responseCode = "500", description = "서버 오류", content = @Content)
})
@PutMapping("/order")
public ApiResponseDto<ApiResponseDto.ResponseObj> updateOrder(
@io.swagger.v3.oas.annotations.parameters.RequestBody(
description = "공통코드 순서변경 요청 정보",
required = true,
content =
@Content(
mediaType = "application/json",
schema = @Schema(implementation = CommonCodeDto.OrderReq.class)))
@RequestBody
@Valid
CommonCodeDto.OrderReq req) {
return ApiResponseDto.okObject(commonCodeService.updateOrder(req));
}
@Operation(summary = "code 기반 조회", description = "code 기반 조회")
@ApiResponses(
value = {
@ApiResponse(
responseCode = "200",
description = "code 기반 조회 성공",
content =
@Content(
mediaType = "application/json",
schema = @Schema(implementation = Long.class))),
@ApiResponse(responseCode = "400", description = "잘못된 요청 데이터", content = @Content),
@ApiResponse(responseCode = "404", description = "코드를 찾을 수 없음", content = @Content),
@ApiResponse(responseCode = "500", description = "서버 오류", content = @Content)
})
@GetMapping("/used")
public ApiResponseDto<List<CommonCodeDto.Basic>> getByCode(
@io.swagger.v3.oas.annotations.parameters.RequestBody(
description = "공통코드 순서변경 요청 정보",
required = true)
@RequestParam
String code) {
return ApiResponseDto.ok(commonCodeService.findByCode(code));
}
@Operation(summary = "변화탐지 분류 코드 목록", description = "변화탐지 분류 코드 목록(공통코드 기반)")
@GetMapping("/clazz")
public ApiResponseDto<List<CommonCodeDto.Clazzes>> getClasses() {
// List<Clazzes> list =
// Arrays.stream(DetectionClassification.values())
// .sorted(Comparator.comparingInt(DetectionClassification::getOrder))
// .map(Clazzes::new)
// .toList();
// 변화탐지 clazz API : enum -> 공통코드로 변경
List<CommonCodeDto.Clazzes> list =
commonCodeUtil.getChildCodesByParentCode("0000").stream()
.map(
child ->
new CommonCodeDto.Clazzes(
child.getCode(), child.getName(), child.getOrder(), child.getProps2()))
.toList();
return ApiResponseDto.ok(list);
}
@Operation(summary = "공통코드 중복여부 체크", description = "공통코드 중복여부 체크")
@ApiResponses(
value = {
@ApiResponse(
responseCode = "200",
description = "조회 성공",
content =
@Content(
mediaType = "application/json",
schema = @Schema(implementation = CommonCodeDto.Basic.class))),
@ApiResponse(responseCode = "404", description = "코드를 찾을 수 없음", content = @Content),
@ApiResponse(responseCode = "500", description = "서버 오류", content = @Content)
})
@GetMapping("/check-duplicate")
public ApiResponseDto<ApiResponseDto.ResponseObj> getCodeCheckDuplicate(
@io.swagger.v3.oas.annotations.parameters.RequestBody(description = "단건 조회", required = true)
@RequestParam(required = false)
Long parentId,
@RequestParam String code) {
return ApiResponseDto.okObject(commonCodeService.getCodeCheckDuplicate(parentId, code));
}
@Operation(summary = "코드 조회", description = "코드 리스트 조회")
@ApiResponses(
value = {
@ApiResponse(
responseCode = "200",
description = "코드 조회 성공",
content =
@Content(
mediaType = "application/json",
schema = @Schema(implementation = CodeDto.class))),
@ApiResponse(responseCode = "500", description = "서버 오류", content = @Content)
})
@GetMapping("/type/codes")
public ApiResponseDto<Map<String, List<CodeDto>>> getTypeCodes() {
return ApiResponseDto.ok(commonCodeService.getTypeCodes());
}
@Operation(summary = "코드 단건 조회", description = "코드 조회")
@ApiResponses(
value = {
@ApiResponse(
responseCode = "200",
description = "코드 조회 성공",
content =
@Content(
mediaType = "application/json",
schema = @Schema(implementation = CodeDto.class))),
@ApiResponse(responseCode = "500", description = "서버 오류", content = @Content)
})
@GetMapping("/type/{type}")
public ApiResponseDto<List<CodeDto>> getTypeCode(@PathVariable String type) {
return ApiResponseDto.ok(commonCodeService.getTypeCode(type));
}
@Operation(summary = "캐시 초기화", description = "공통코드 캐시를 초기화합니다.")
@ApiResponses(
value = {
@ApiResponse(
responseCode = "200",
description = "캐시 초기화 성공",
content =
@Content(
mediaType = "application/json",
schema = @Schema(implementation = String.class))),
@ApiResponse(responseCode = "500", description = "서버 오류", content = @Content)
})
@PostMapping("/cache/refresh")
public ApiResponseDto<String> refreshCommonCodeCache() {
commonCodeService.refresh();
return ApiResponseDto.ok("공통코드 캐시가 초기화 되었습니다.");
}
}
package com.kamco.cd.training.code;
import com.kamco.cd.training.code.dto.CommonCodeDto;
import com.kamco.cd.training.code.service.CommonCodeService;
import com.kamco.cd.training.common.utils.CommonCodeUtil;
import com.kamco.cd.training.common.utils.enums.CodeDto;
import com.kamco.cd.training.config.api.ApiResponseDto;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.media.Content;
import io.swagger.v3.oas.annotations.media.Schema;
import io.swagger.v3.oas.annotations.responses.ApiResponse;
import io.swagger.v3.oas.annotations.responses.ApiResponses;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.validation.Valid;
import java.util.List;
import java.util.Map;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.*;
@Tag(name = "공통코드 관리", description = "공통코드 관리 API")
@RestController
@RequiredArgsConstructor
@RequestMapping("/api/code")
public class CommonCodeApiController {
private final CommonCodeService commonCodeService;
private final CommonCodeUtil commonCodeUtil;
@Operation(summary = "목록 조회", description = "모든 공통코드 조회")
@ApiResponses(
value = {
@ApiResponse(
responseCode = "200",
description = "조회 성공",
content =
@Content(
mediaType = "application/json",
schema = @Schema(implementation = CommonCodeDto.Basic.class))),
@ApiResponse(responseCode = "404", description = "코드를 찾을 수 없음", content = @Content),
@ApiResponse(responseCode = "500", description = "서버 오류", content = @Content)
})
@GetMapping
public ApiResponseDto<List<CommonCodeDto.Basic>> getFindAll() {
return ApiResponseDto.createOK(commonCodeService.getFindAll());
}
@Operation(summary = "단건 조회", description = "단건 조회")
@ApiResponses(
value = {
@ApiResponse(
responseCode = "200",
description = "조회 성공",
content =
@Content(
mediaType = "application/json",
schema = @Schema(implementation = CommonCodeDto.Basic.class))),
@ApiResponse(responseCode = "404", description = "코드를 찾을 수 없음", content = @Content),
@ApiResponse(responseCode = "500", description = "서버 오류", content = @Content)
})
@GetMapping("/{id}")
public ApiResponseDto<CommonCodeDto.Basic> getOneById(
@io.swagger.v3.oas.annotations.parameters.RequestBody(description = "단건 조회", required = true)
@PathVariable
Long id) {
return ApiResponseDto.ok(commonCodeService.getOneById(id));
}
@Operation(summary = "저장", description = "공통코드를 저장 합니다.")
@ApiResponses(
value = {
@ApiResponse(
responseCode = "201",
description = "공통코드 저장 성공",
content =
@Content(
mediaType = "application/json",
schema = @Schema(implementation = Long.class))),
@ApiResponse(responseCode = "400", description = "잘못된 요청 데이터", content = @Content),
@ApiResponse(responseCode = "404", description = "코드를 찾을 수 없음", content = @Content),
@ApiResponse(responseCode = "500", description = "서버 오류", content = @Content)
})
@PostMapping
public ApiResponseDto<ApiResponseDto.ResponseObj> save(
@io.swagger.v3.oas.annotations.parameters.RequestBody(
description = "공통코드 생성 요청 정보",
required = true,
content =
@Content(
mediaType = "application/json",
schema = @Schema(implementation = CommonCodeDto.AddReq.class)))
@RequestBody
@Valid
CommonCodeDto.AddReq req) {
return ApiResponseDto.okObject(commonCodeService.save(req));
}
@Operation(summary = "수정", description = "공통코드를 수정 합니다.")
@ApiResponses(
value = {
@ApiResponse(
responseCode = "204",
description = "공통코드 수정 성공",
content =
@Content(
mediaType = "application/json",
schema = @Schema(implementation = Long.class))),
@ApiResponse(responseCode = "400", description = "잘못된 요청 데이터", content = @Content),
@ApiResponse(responseCode = "404", description = "코드를 찾을 수 없음", content = @Content),
@ApiResponse(responseCode = "500", description = "서버 오류", content = @Content)
})
@PutMapping("/{id}")
public ApiResponseDto<ApiResponseDto.ResponseObj> update(
@io.swagger.v3.oas.annotations.parameters.RequestBody(
description = "공통코드 수정 요청 정보",
required = true,
content =
@Content(
mediaType = "application/json",
schema = @Schema(implementation = CommonCodeDto.ModifyReq.class)))
@PathVariable
Long id,
@RequestBody @Valid CommonCodeDto.ModifyReq req) {
return ApiResponseDto.okObject(commonCodeService.update(id, req));
}
@Operation(summary = "삭제", description = "공통코드를 삭제 합니다.")
@ApiResponses(
value = {
@ApiResponse(
responseCode = "204",
description = "공통코드 삭제 성공",
content =
@Content(
mediaType = "application/json",
schema = @Schema(implementation = Long.class))),
@ApiResponse(responseCode = "400", description = "잘못된 요청 데이터", content = @Content),
@ApiResponse(responseCode = "404", description = "코드를 찾을 수 없음", content = @Content),
@ApiResponse(responseCode = "500", description = "서버 오류", content = @Content)
})
@DeleteMapping("/{id}")
public ApiResponseDto<ApiResponseDto.ResponseObj> remove(
@io.swagger.v3.oas.annotations.parameters.RequestBody(
description = "공통코드 삭제 요청 정보",
required = true)
@PathVariable
Long id) {
return ApiResponseDto.okObject(commonCodeService.removeCode(id));
}
@Operation(summary = "순서 변경", description = "공통코드 순서를 변경 합니다.")
@ApiResponses(
value = {
@ApiResponse(
responseCode = "204",
description = "공통코드 순서 변경 성공",
content =
@Content(
mediaType = "application/json",
schema = @Schema(implementation = Long.class))),
@ApiResponse(responseCode = "400", description = "잘못된 요청 데이터", content = @Content),
@ApiResponse(responseCode = "404", description = "코드를 찾을 수 없음", content = @Content),
@ApiResponse(responseCode = "500", description = "서버 오류", content = @Content)
})
@PutMapping("/order")
public ApiResponseDto<ApiResponseDto.ResponseObj> updateOrder(
@io.swagger.v3.oas.annotations.parameters.RequestBody(
description = "공통코드 순서변경 요청 정보",
required = true,
content =
@Content(
mediaType = "application/json",
schema = @Schema(implementation = CommonCodeDto.OrderReq.class)))
@RequestBody
@Valid
CommonCodeDto.OrderReq req) {
return ApiResponseDto.okObject(commonCodeService.updateOrder(req));
}
@Operation(summary = "code 기반 조회", description = "code 기반 조회")
@ApiResponses(
value = {
@ApiResponse(
responseCode = "200",
description = "code 기반 조회 성공",
content =
@Content(
mediaType = "application/json",
schema = @Schema(implementation = Long.class))),
@ApiResponse(responseCode = "400", description = "잘못된 요청 데이터", content = @Content),
@ApiResponse(responseCode = "404", description = "코드를 찾을 수 없음", content = @Content),
@ApiResponse(responseCode = "500", description = "서버 오류", content = @Content)
})
@GetMapping("/used")
public ApiResponseDto<List<CommonCodeDto.Basic>> getByCode(
@io.swagger.v3.oas.annotations.parameters.RequestBody(
description = "공통코드 순서변경 요청 정보",
required = true)
@RequestParam
String code) {
return ApiResponseDto.ok(commonCodeService.findByCode(code));
}
@Operation(summary = "변화탐지 분류 코드 목록", description = "변화탐지 분류 코드 목록(공통코드 기반)")
@GetMapping("/clazz")
public ApiResponseDto<List<CommonCodeDto.Clazzes>> getClasses() {
// List<Clazzes> list =
// Arrays.stream(DetectionClassification.values())
// .sorted(Comparator.comparingInt(DetectionClassification::getOrder))
// .map(Clazzes::new)
// .toList();
// 변화탐지 clazz API : enum -> 공통코드로 변경
List<CommonCodeDto.Clazzes> list =
commonCodeUtil.getChildCodesByParentCode("0000").stream()
.map(
child ->
new CommonCodeDto.Clazzes(
child.getCode(), child.getName(), child.getOrder(), child.getProps2()))
.toList();
return ApiResponseDto.ok(list);
}
@Operation(summary = "공통코드 중복여부 체크", description = "공통코드 중복여부 체크")
@ApiResponses(
value = {
@ApiResponse(
responseCode = "200",
description = "조회 성공",
content =
@Content(
mediaType = "application/json",
schema = @Schema(implementation = CommonCodeDto.Basic.class))),
@ApiResponse(responseCode = "404", description = "코드를 찾을 수 없음", content = @Content),
@ApiResponse(responseCode = "500", description = "서버 오류", content = @Content)
})
@GetMapping("/check-duplicate")
public ApiResponseDto<ApiResponseDto.ResponseObj> getCodeCheckDuplicate(
@io.swagger.v3.oas.annotations.parameters.RequestBody(description = "단건 조회", required = true)
@RequestParam(required = false)
Long parentId,
@RequestParam String code) {
return ApiResponseDto.okObject(commonCodeService.getCodeCheckDuplicate(parentId, code));
}
@Operation(summary = "코드 조회", description = "코드 리스트 조회")
@ApiResponses(
value = {
@ApiResponse(
responseCode = "200",
description = "코드 조회 성공",
content =
@Content(
mediaType = "application/json",
schema = @Schema(implementation = CodeDto.class))),
@ApiResponse(responseCode = "500", description = "서버 오류", content = @Content)
})
@GetMapping("/type/codes")
public ApiResponseDto<Map<String, List<CodeDto>>> getTypeCodes() {
return ApiResponseDto.ok(commonCodeService.getTypeCodes());
}
@Operation(summary = "코드 단건 조회", description = "코드 조회")
@ApiResponses(
value = {
@ApiResponse(
responseCode = "200",
description = "코드 조회 성공",
content =
@Content(
mediaType = "application/json",
schema = @Schema(implementation = CodeDto.class))),
@ApiResponse(responseCode = "500", description = "서버 오류", content = @Content)
})
@GetMapping("/type/{type}")
public ApiResponseDto<List<CodeDto>> getTypeCode(@PathVariable String type) {
return ApiResponseDto.ok(commonCodeService.getTypeCode(type));
}
@Operation(summary = "캐시 초기화", description = "공통코드 캐시를 초기화합니다.")
@ApiResponses(
value = {
@ApiResponse(
responseCode = "200",
description = "캐시 초기화 성공",
content =
@Content(
mediaType = "application/json",
schema = @Schema(implementation = String.class))),
@ApiResponse(responseCode = "500", description = "서버 오류", content = @Content)
})
@PostMapping("/cache/refresh")
public ApiResponseDto<String> refreshCommonCodeCache() {
commonCodeService.refresh();
return ApiResponseDto.ok("공통코드 캐시가 초기화 되었습니다.");
}
}

View File

@@ -1,179 +1,179 @@
package com.kamco.cd.training.code.dto;
import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
import com.kamco.cd.training.common.utils.html.HtmlEscapeDeserializer;
import com.kamco.cd.training.common.utils.html.HtmlUnescapeSerializer;
import com.kamco.cd.training.common.utils.interfaces.JsonFormatDttm;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotEmpty;
import jakarta.validation.constraints.NotNull;
import java.time.ZonedDateTime;
import java.util.List;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Sort;
public class CommonCodeDto {
@Schema(name = "CodeAddReq", description = "공통코드 저장 정보")
@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
public static class AddReq {
@NotEmpty private String code;
@NotEmpty private String name;
private String description;
private int order;
private boolean used;
private Long parentId;
@JsonDeserialize(using = HtmlEscapeDeserializer.class)
private String props1;
@JsonDeserialize(using = HtmlEscapeDeserializer.class)
private String props2;
@JsonDeserialize(using = HtmlEscapeDeserializer.class)
private String props3;
}
@Schema(name = "CodeModifyReq", description = "공통코드 수정 정보")
@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
public static class ModifyReq {
@NotEmpty private String name;
private String description;
private boolean used;
@JsonDeserialize(using = HtmlEscapeDeserializer.class)
private String props1;
@JsonDeserialize(using = HtmlEscapeDeserializer.class)
private String props2;
@JsonDeserialize(using = HtmlEscapeDeserializer.class)
private String props3;
}
@Schema(name = "CodeOrderReq", description = "공통코드 순서 변경 정보")
@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
public static class OrderReq {
@NotNull private Long id;
@NotNull private Integer order;
}
@Schema(name = "CommonCode Basic", description = "공통코드 기본 정보")
@Getter
public static class Basic {
private Long id;
private String code;
private String description;
private String name;
private Integer order;
private Boolean used;
private Boolean deleted;
private List<CommonCodeDto.Basic> children;
@JsonFormatDttm private ZonedDateTime createdDttm;
@JsonFormatDttm private ZonedDateTime updatedDttm;
@JsonSerialize(using = HtmlUnescapeSerializer.class)
private String props1;
@JsonSerialize(using = HtmlUnescapeSerializer.class)
private String props2;
@JsonSerialize(using = HtmlUnescapeSerializer.class)
private String props3;
@JsonFormatDttm private ZonedDateTime deletedDttm;
public Basic(
Long id,
String code,
String description,
String name,
Integer order,
Boolean used,
Boolean deleted,
List<CommonCodeDto.Basic> children,
ZonedDateTime createdDttm,
ZonedDateTime updatedDttm,
String props1,
String props2,
String props3,
ZonedDateTime deletedDttm) {
this.id = id;
this.code = code;
this.description = description;
this.name = name;
this.order = order;
this.used = used;
this.deleted = deleted;
this.children = children;
this.createdDttm = createdDttm;
this.updatedDttm = updatedDttm;
this.props1 = props1;
this.props2 = props2;
this.props3 = props3;
this.deletedDttm = deletedDttm;
}
}
@Schema(name = "SearchReq", description = "검색 요청")
@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
public static class SearchReq {
// 검색 조건
private String name;
// 페이징 파라미터
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);
}
}
@Getter
public static class Clazzes {
private String code;
private String name;
private Integer order;
private String color;
public Clazzes(String code, String name, Integer order, String color) {
this.code = code;
this.name = name;
this.order = order;
this.color = color;
}
}
}
package com.kamco.cd.training.code.dto;
import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
import com.kamco.cd.training.common.utils.html.HtmlEscapeDeserializer;
import com.kamco.cd.training.common.utils.html.HtmlUnescapeSerializer;
import com.kamco.cd.training.common.utils.interfaces.JsonFormatDttm;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotEmpty;
import jakarta.validation.constraints.NotNull;
import java.time.ZonedDateTime;
import java.util.List;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Sort;
public class CommonCodeDto {
@Schema(name = "CodeAddReq", description = "공통코드 저장 정보")
@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
public static class AddReq {
@NotEmpty private String code;
@NotEmpty private String name;
private String description;
private int order;
private boolean used;
private Long parentId;
@JsonDeserialize(using = HtmlEscapeDeserializer.class)
private String props1;
@JsonDeserialize(using = HtmlEscapeDeserializer.class)
private String props2;
@JsonDeserialize(using = HtmlEscapeDeserializer.class)
private String props3;
}
@Schema(name = "CodeModifyReq", description = "공통코드 수정 정보")
@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
public static class ModifyReq {
@NotEmpty private String name;
private String description;
private boolean used;
@JsonDeserialize(using = HtmlEscapeDeserializer.class)
private String props1;
@JsonDeserialize(using = HtmlEscapeDeserializer.class)
private String props2;
@JsonDeserialize(using = HtmlEscapeDeserializer.class)
private String props3;
}
@Schema(name = "CodeOrderReq", description = "공통코드 순서 변경 정보")
@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
public static class OrderReq {
@NotNull private Long id;
@NotNull private Integer order;
}
@Schema(name = "CommonCode Basic", description = "공통코드 기본 정보")
@Getter
public static class Basic {
private Long id;
private String code;
private String description;
private String name;
private Integer order;
private Boolean used;
private Boolean deleted;
private List<CommonCodeDto.Basic> children;
@JsonFormatDttm private ZonedDateTime createdDttm;
@JsonFormatDttm private ZonedDateTime updatedDttm;
@JsonSerialize(using = HtmlUnescapeSerializer.class)
private String props1;
@JsonSerialize(using = HtmlUnescapeSerializer.class)
private String props2;
@JsonSerialize(using = HtmlUnescapeSerializer.class)
private String props3;
@JsonFormatDttm private ZonedDateTime deletedDttm;
public Basic(
Long id,
String code,
String description,
String name,
Integer order,
Boolean used,
Boolean deleted,
List<CommonCodeDto.Basic> children,
ZonedDateTime createdDttm,
ZonedDateTime updatedDttm,
String props1,
String props2,
String props3,
ZonedDateTime deletedDttm) {
this.id = id;
this.code = code;
this.description = description;
this.name = name;
this.order = order;
this.used = used;
this.deleted = deleted;
this.children = children;
this.createdDttm = createdDttm;
this.updatedDttm = updatedDttm;
this.props1 = props1;
this.props2 = props2;
this.props3 = props3;
this.deletedDttm = deletedDttm;
}
}
@Schema(name = "SearchReq", description = "검색 요청")
@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
public static class SearchReq {
// 검색 조건
private String name;
// 페이징 파라미터
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);
}
}
@Getter
public static class Clazzes {
private String code;
private String name;
private Integer order;
private String color;
public Clazzes(String code, String name, Integer order, String color) {
this.code = code;
this.name = name;
this.order = order;
this.color = color;
}
}
}

View File

@@ -1,155 +1,155 @@
package com.kamco.cd.training.code.service;
import com.kamco.cd.training.code.dto.CommonCodeDto.AddReq;
import com.kamco.cd.training.code.dto.CommonCodeDto.Basic;
import com.kamco.cd.training.code.dto.CommonCodeDto.ModifyReq;
import com.kamco.cd.training.code.dto.CommonCodeDto.OrderReq;
import com.kamco.cd.training.common.utils.enums.CodeDto;
import com.kamco.cd.training.common.utils.enums.Enums;
import com.kamco.cd.training.config.api.ApiResponseDto;
import com.kamco.cd.training.postgres.core.CommonCodeCoreService;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import lombok.RequiredArgsConstructor;
import org.springframework.cache.annotation.CacheEvict;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
// training 서버는 Redis 사용하지 않고 Spring Boot 메모리 캐시를 사용함
// => org.springframework.cache.annotation.Cacheable
@Service
@RequiredArgsConstructor
@Transactional(readOnly = true)
public class CommonCodeService {
private final CommonCodeCoreService commonCodeCoreService;
/**
* 공통코드 목록 조회
*
* @return 모튼 코드 정보
*/
@Cacheable("trainCommonCodes")
public List<Basic> getFindAll() {
return commonCodeCoreService.findAll();
}
/**
* 공통코드 단건 조회
*
* @param id
* @return 코드 아이디로 조회한 코드 정보
*/
public Basic getOneById(Long id) {
return commonCodeCoreService.getOneById(id);
}
/**
* 공통코드 생성 요청
*
* @param req 생성요청 정보
* @return 생성된 코드 id
*/
@Transactional
@CacheEvict(value = "trainCommonCodes", allEntries = true)
public ApiResponseDto.ResponseObj save(AddReq req) {
return commonCodeCoreService.save(req);
}
/**
* 공통코드 수정 요청
*
* @param id 코드 아이디
* @param req 수정요청 정보
*/
@Transactional
@CacheEvict(value = "trainCommonCodes", allEntries = true)
public ApiResponseDto.ResponseObj update(Long id, ModifyReq req) {
return commonCodeCoreService.update(id, req);
}
/**
* 공통코드 삭제 처리
*
* @param id 코드 아이디
*/
@Transactional
@CacheEvict(value = "trainCommonCodes", allEntries = true)
public ApiResponseDto.ResponseObj removeCode(Long id) {
return commonCodeCoreService.removeCode(id);
}
/**
* 공통코드 순서 변경
*
* @param req id, order 정보를 가진 List
*/
@Transactional
@CacheEvict(value = "trainCommonCodes", allEntries = true)
public ApiResponseDto.ResponseObj updateOrder(OrderReq req) {
return commonCodeCoreService.updateOrder(req);
}
/**
* 코드기반 조회
*
* @param code 코드
* @return 코드로 조회한 공통코드 정보
*/
public List<Basic> findByCode(String code) {
return commonCodeCoreService.findByCode(code);
}
/**
* 중복 체크
*
* @param parentId
* @param code
* @return
*/
public ApiResponseDto.ResponseObj getCodeCheckDuplicate(Long parentId, String code) {
return commonCodeCoreService.getCodeCheckDuplicate(parentId, code);
}
/**
* 공통코드 이름 조회
*
* @param parentCodeCd 상위 코드
* @param childCodeCd 하위 코드
* @return 공통코드명
*/
public Optional<String> getCode(String parentCodeCd, String childCodeCd) {
return commonCodeCoreService.getCode(parentCodeCd, childCodeCd);
}
/**
* 공통코드 이름 조회
*
* @param parentCodeCd 상위 코드
* @param childCodeCd 하위 코드
* @return 공통코드명
*/
public Optional<String> getTypeCode(String parentCodeCd, String childCodeCd) {
return commonCodeCoreService.getCode(parentCodeCd, childCodeCd);
}
public List<CodeDto> getTypeCode(String type) {
return Enums.getCodes(type);
}
/**
* 공통코드 리스트 조회
*
* @return
*/
public Map<String, List<CodeDto>> getTypeCodes() {
return Enums.getAllCodes();
}
/** 메모리 캐시 초기화 */
@CacheEvict(value = "trainCommonCodes", allEntries = true)
public void refresh() {}
}
package com.kamco.cd.training.code.service;
import com.kamco.cd.training.code.dto.CommonCodeDto.AddReq;
import com.kamco.cd.training.code.dto.CommonCodeDto.Basic;
import com.kamco.cd.training.code.dto.CommonCodeDto.ModifyReq;
import com.kamco.cd.training.code.dto.CommonCodeDto.OrderReq;
import com.kamco.cd.training.common.utils.enums.CodeDto;
import com.kamco.cd.training.common.utils.enums.Enums;
import com.kamco.cd.training.config.api.ApiResponseDto;
import com.kamco.cd.training.postgres.core.CommonCodeCoreService;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import lombok.RequiredArgsConstructor;
import org.springframework.cache.annotation.CacheEvict;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
// training 서버는 Redis 사용하지 않고 Spring Boot 메모리 캐시를 사용함
// => org.springframework.cache.annotation.Cacheable
@Service
@RequiredArgsConstructor
@Transactional(readOnly = true)
public class CommonCodeService {
private final CommonCodeCoreService commonCodeCoreService;
/**
* 공통코드 목록 조회
*
* @return 모튼 코드 정보
*/
@Cacheable("trainCommonCodes")
public List<Basic> getFindAll() {
return commonCodeCoreService.findAll();
}
/**
* 공통코드 단건 조회
*
* @param id
* @return 코드 아이디로 조회한 코드 정보
*/
public Basic getOneById(Long id) {
return commonCodeCoreService.getOneById(id);
}
/**
* 공통코드 생성 요청
*
* @param req 생성요청 정보
* @return 생성된 코드 id
*/
@Transactional
@CacheEvict(value = "trainCommonCodes", allEntries = true)
public ApiResponseDto.ResponseObj save(AddReq req) {
return commonCodeCoreService.save(req);
}
/**
* 공통코드 수정 요청
*
* @param id 코드 아이디
* @param req 수정요청 정보
*/
@Transactional
@CacheEvict(value = "trainCommonCodes", allEntries = true)
public ApiResponseDto.ResponseObj update(Long id, ModifyReq req) {
return commonCodeCoreService.update(id, req);
}
/**
* 공통코드 삭제 처리
*
* @param id 코드 아이디
*/
@Transactional
@CacheEvict(value = "trainCommonCodes", allEntries = true)
public ApiResponseDto.ResponseObj removeCode(Long id) {
return commonCodeCoreService.removeCode(id);
}
/**
* 공통코드 순서 변경
*
* @param req id, order 정보를 가진 List
*/
@Transactional
@CacheEvict(value = "trainCommonCodes", allEntries = true)
public ApiResponseDto.ResponseObj updateOrder(OrderReq req) {
return commonCodeCoreService.updateOrder(req);
}
/**
* 코드기반 조회
*
* @param code 코드
* @return 코드로 조회한 공통코드 정보
*/
public List<Basic> findByCode(String code) {
return commonCodeCoreService.findByCode(code);
}
/**
* 중복 체크
*
* @param parentId
* @param code
* @return
*/
public ApiResponseDto.ResponseObj getCodeCheckDuplicate(Long parentId, String code) {
return commonCodeCoreService.getCodeCheckDuplicate(parentId, code);
}
/**
* 공통코드 이름 조회
*
* @param parentCodeCd 상위 코드
* @param childCodeCd 하위 코드
* @return 공통코드명
*/
public Optional<String> getCode(String parentCodeCd, String childCodeCd) {
return commonCodeCoreService.getCode(parentCodeCd, childCodeCd);
}
/**
* 공통코드 이름 조회
*
* @param parentCodeCd 상위 코드
* @param childCodeCd 하위 코드
* @return 공통코드명
*/
public Optional<String> getTypeCode(String parentCodeCd, String childCodeCd) {
return commonCodeCoreService.getCode(parentCodeCd, childCodeCd);
}
public List<CodeDto> getTypeCode(String type) {
return Enums.getCodes(type);
}
/**
* 공통코드 리스트 조회
*
* @return
*/
public Map<String, List<CodeDto>> getTypeCodes() {
return Enums.getAllCodes();
}
/** 메모리 캐시 초기화 */
@CacheEvict(value = "trainCommonCodes", allEntries = true)
public void refresh() {}
}

View File

@@ -0,0 +1,159 @@
package com.kamco.cd.training.common.dto;
import com.kamco.cd.training.common.enums.ModelType;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
@Getter
@Setter
@AllArgsConstructor
@NoArgsConstructor
public class HyperParam {
// -------------------------
// Important
// -------------------------
@Schema(description = "모델", example = "large")
private ModelType model; // backbone
@Schema(description = "백본 네트워크", example = "large")
private String backbone; // backbone
@Schema(description = "입력 이미지 크기(H,W)", example = "512,512")
private String inputSize; // input_size
@Schema(description = "크롭 크기(H,W 또는 단일값)", example = "256,256")
private String cropSize; // crop_size
@Schema(description = "배치 크기(Per GPU)", example = "16")
private Integer batchSize; // batch_size
// -------------------------
// Data
// -------------------------
@Schema(description = "Train dataloader workers", example = "16")
private Integer trainNumWorkers; // train_num_workers
@Schema(description = "Val dataloader workers", example = "8")
private Integer valNumWorkers; // val_num_workers
@Schema(description = "Test dataloader workers", example = "8")
private Integer testNumWorkers; // test_num_workers
@Schema(description = "Train shuffle 여부", example = "true")
private Boolean trainShuffle; // train_shuffle
@Schema(description = "Train persistent workers 여부", example = "true")
private Boolean trainPersistent; // train_persistent
@Schema(description = "Val persistent workers 여부", example = "true")
private Boolean valPersistent; // val_persistent
// -------------------------
// Model Architecture
// -------------------------
@Schema(description = "Drop Path 비율", example = "0.3")
private Double dropPathRate; // drop_path_rate
@Schema(description = "Freeze 단계(-1:None)", example = "-1")
private Integer frozenStages; // frozen_stages
@Schema(description = "Neck 결합 정책", example = "abs_diff")
private String neckPolicy; // neck_policy
@Schema(description = "디코더 채널 구성", example = "512,256,128,64")
private String decoderChannels; // decoder_channels
@Schema(description = "클래스별 가중치", example = "1,10")
private String classWeight; // class_weight
// -------------------------
// Loss & Optimization
// -------------------------
@Schema(description = "학습률", example = "0.00006")
private Double learningRate; // learning_rate
@Schema(description = "Weight Decay", example = "0.05")
private Double weightDecay; // weight_decay
@Schema(description = "Layer Decay Rate", example = "0.9")
private Double layerDecayRate; // layer_decay_rate
@Schema(description = "DDP unused params 탐색 여부", example = "true")
private Boolean ddpFindUnusedParams; // ddp_find_unused_params
@Schema(description = "Loss 계산 제외 인덱스", example = "255")
private Integer ignoreIndex; // ignore_index
@Schema(description = "레이어 깊이", example = "24")
private Integer numLayers; // num_layers
// -------------------------
// Evaluation
// -------------------------
@Schema(description = "평가 지표 목록", example = "mFscore,mIoU")
private String metrics; // metrics
@Schema(description = "Best 모델 선정 기준 지표", example = "changed_fscore")
private String saveBest; // save_best
@Schema(description = "Best 모델 선정 규칙", example = "less")
private String saveBestRule; // save_best_rule
@Schema(description = "검증 수행 주기(Epoch)", example = "10")
private Integer valInterval; // val_interval
@Schema(description = "로그 기록 주기(Iteration)", example = "400")
private Integer logInterval; // log_interval
@Schema(description = "시각화 저장 주기(Epoch)", example = "1")
private Integer visInterval; // vis_interval
// -------------------------
// Augmentation
// -------------------------
@Schema(description = "회전 적용 확률", example = "0.5")
private Double rotProb; // rot_prob
@Schema(description = "회전 각도 범위(Min,Max)", example = "-20,20")
private String rotDegree; // rot_degree
@Schema(description = "반전 적용 확률", example = "0.5")
private Double flipProb; // flip_prob
@Schema(description = "채널 교환 확률", example = "0.5")
private Double exchangeProb; // exchange_prob
@Schema(description = "밝기 변화량", example = "10")
private Integer brightnessDelta; // brightness_delta
@Schema(description = "대비 범위(Min,Max)", example = "0.8,1.2")
private String contrastRange; // contrast_range
@Schema(description = "채도 범위(Min,Max)", example = "0.8,1.2")
private String saturationRange; // saturation_range
@Schema(description = "색조 변화량", example = "10")
private Integer hueDelta; // hue_delta
// -------------------------
// Hardware
// -------------------------
@Schema(description = "사용 GPU 개수", example = "4")
private Integer gpuCnt; // gpu_cnt
@Schema(description = "사용 GPU ID 목록", example = "0,1,2,3")
private String gpuIds; // gpu_ids
@Schema(description = "분산학습 마스터 포트", example = "1122")
private Integer masterPort; // master_port
// -------------------------
// Memo
// -------------------------
@Schema(description = "메모", example = "하이퍼파라미터 신규등록")
private String memo; // memo
}

View File

@@ -1,19 +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;
}
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

@@ -1,55 +1,56 @@
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();
}
}
package com.kamco.cd.training.common.enums;
import lombok.AllArgsConstructor;
import lombok.Getter;
@Getter
@AllArgsConstructor
public enum DetectionClassification {
ROAD("road", "도로", 10),
BUILDING("building", "건물", 20),
GREENHOUSE("greenhouse", "비닐하우스", 30),
FIELD("field", "논/밭", 40),
ORCHARD("orchard", "과수원", 50),
GRASS("grass", "초지", 60),
FOREST("forest", "", 70),
WATER("water", "", 80),
STONE("stone", "모래/자갈", 90),
WASTE("waste", "적치물", 100),
CONTAINER("container", "컨테이너", 110),
LAND("land", "일반토지", 120),
SOLAR("solar", "태양광", 130),
TANK("tank", "탱크", 140),
NDC("NDC", "미분류", 150),
ETC("ETC", "기타", 160);
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,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 HyperParamSelectType implements EnumType {
OPTIMIZED("최적화 파라미터"),
EXISTING("기존 파라미터"),
NEW("신규 파라미터");
private final String desc;
@Override
public String getId() {
return name();
}
@Override
public String getText() {
return desc;
}
}

View File

@@ -1,26 +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;
}
}
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

@@ -1,26 +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;
}
}
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

@@ -1,27 +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;
}
}
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,32 @@
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 java.util.Arrays;
import lombok.AllArgsConstructor;
import lombok.Getter;
@CodeExpose
@Getter
@AllArgsConstructor
public enum ModelType implements EnumType {
G1("G1"),
G2("G2"),
G3("G3");
private String desc;
public static ModelType getValueData(String modelNo) {
return Arrays.stream(ModelType.values()).filter(m -> m.getId().equals(modelNo)).findFirst().orElse(G1);
}
@Override
public String getId() {
return name();
}
@Override
public String getText() {
return desc;
}
}

View File

@@ -1,20 +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;
}
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

@@ -1,25 +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;
}
}
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

@@ -1,25 +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;
}
}
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

@@ -1,22 +1,30 @@
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;
}
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("대기"),
IN_PROGRESS("진행중"),
COMPLETED("완료"),
STOPPED("중단됨"),
ERROR("오류");
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 TrainType implements EnumType {
GENERAL("일반"),
TRANSFER("전이");
private final String desc;
@Override
public String getId() {
return name();
}
@Override
public String getText() {
return desc;
}
}

View File

@@ -1,26 +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;
}
}
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

@@ -1,10 +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);
}
}
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

@@ -1,28 +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();
}
}
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

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

View File

@@ -1,10 +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);
}
}
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

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

View File

@@ -1,38 +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);
}
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,50 @@
package com.kamco.cd.training.common.service;
import java.nio.file.FileStore;
import java.nio.file.Files;
import java.nio.file.Path;
public class FormatStorage {
private FormatStorage() {}
/** 디스크 사용량 조회 */
public static DiskUsage getDiskUsage(Path path) throws Exception {
FileStore store = Files.getFileStore(path);
long total = store.getTotalSpace();
long usable = store.getUsableSpace();
return new DiskUsage(path.toString(), total, usable);
}
/** 디스크 사용량 DTO */
public record DiskUsage(String path, long totalBytes, long usableBytes) {
public long usedBytes() {
return totalBytes - usableBytes;
}
public double usedPercent() {
return totalBytes == 0 ? 0.0 : (usedBytes() * 100.0) / totalBytes;
}
public String totalText() {
return formatStorageSize(totalBytes);
}
public String usableText() {
return formatStorageSize(usableBytes);
}
/** 저장공간을 사람이 읽기 좋은 단위로 변환 (GB / MB) */
private static String formatStorageSize(long bytes) {
double gb = bytes / 1024.0 / 1024 / 1024;
if (gb >= 1) {
return String.format("%.1f GB", gb);
}
double mb = bytes / 1024.0 / 1024;
return String.format("%.0f MB", mb);
}
}
}

View File

@@ -1,152 +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;
}
}
}
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

@@ -1,32 +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);
}
}
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

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

View File

@@ -1,43 +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;
}
}
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

@@ -1,41 +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;
}
}
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

@@ -1,20 +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;
}
}
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

@@ -1,10 +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 {}
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

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

View File

@@ -1,26 +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);
}
}
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

@@ -1,76 +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;
}
}
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

@@ -1,36 +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);
}
}
}
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

@@ -1,31 +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();
}
}
}
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

@@ -1,18 +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);
}
}
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

@@ -1,20 +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));
}
}
}
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

@@ -1,23 +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();
}
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

@@ -1,15 +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 {}
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 {}

View File

@@ -1,11 +1,11 @@
package com.kamco.cd.training.config;
import org.springframework.cache.annotation.EnableCaching;
import org.springframework.context.annotation.Configuration;
@EnableCaching
@Configuration
public class CacheConfig {
// training 서버는 Redis 사용하지 않고 Spring Boot 메모리 캐시를 사용함
// => org.springframework.cache.annotation.Cacheable
}
package com.kamco.cd.training.config;
import org.springframework.cache.annotation.EnableCaching;
import org.springframework.context.annotation.Configuration;
@EnableCaching
@Configuration
public class CacheConfig {
// training 서버는 Redis 사용하지 않고 Spring Boot 메모리 캐시를 사용함
// => org.springframework.cache.annotation.Cacheable
}

View File

@@ -1,27 +1,27 @@
package com.kamco.cd.training.config;
import lombok.Getter;
import lombok.Setter;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;
/** GeoJSON 파일 모니터링 설정 */
@Component
@ConfigurationProperties(prefix = "file.config")
@Getter
@Setter
public class FileConfig {
// private String rootDir = "D:\\app/";
private String rootDir = "/app/";
private String rootSyncDir = rootDir + "original-images/";
private String tmpSyncDir = rootSyncDir + "tmp/";
private String dataSetDir = rootDir + "dataset/";
private String tmpDataSetDir = dataSetDir + "tmp/";
// private String rootSyncDir = "/app/original-images/";
// private String tmpSyncDir = rootSyncDir + "tmp/";
private String syncFileExt = "tfw,tif";
}
package com.kamco.cd.training.config;
import lombok.Getter;
import lombok.Setter;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;
/** GeoJSON 파일 모니터링 설정 */
@Component
@ConfigurationProperties(prefix = "file.config")
@Getter
@Setter
public class FileConfig {
// private String rootDir = "D:\\app/";
private String rootDir = "/app/";
private String rootSyncDir = rootDir + "original-images/";
private String tmpSyncDir = rootSyncDir + "tmp/";
private String dataSetDir = rootDir + "dataset/";
private String tmpDataSetDir = dataSetDir + "tmp/";
// private String rootSyncDir = "/app/original-images/";
// private String tmpSyncDir = rootSyncDir + "tmp/";
private String syncFileExt = "tfw,tif";
}

View File

@@ -1,77 +1,77 @@
package com.kamco.cd.training.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("${swagger.local-port}")
private String localPort;
@Value("${spring.profiles.active:local}")
private String profile;
@Value("${swagger.dev-url:https://kamco.training-dev-api.gs.dabeeo.com}")
private String devUrl;
@Value("${swagger.prod-url:https://api.training-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:" + localPort).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:" + localPort).description("로컬 서버"));
servers.add(new Server().url(devUrl).description("개발 서버"));
} else {
servers.add(new Server().url("http://localhost:" + localPort).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);
}
}
package com.kamco.cd.training.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("${swagger.local-port}")
private String localPort;
@Value("${spring.profiles.active:local}")
private String profile;
@Value("${swagger.dev-url:https://kamco.training-dev-api.gs.dabeeo.com}")
private String devUrl;
@Value("${swagger.prod-url:https://api.training-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:" + localPort).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:" + localPort).description("로컬 서버"));
servers.add(new Server().url(devUrl).description("개발 서버"));
} else {
servers.add(new Server().url("http://localhost:" + localPort).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

@@ -1,136 +1,136 @@
package com.kamco.cd.training.config;
import com.kamco.cd.training.auth.CustomAuthenticationProvider;
import com.kamco.cd.training.auth.JwtAuthenticationFilter;
import java.util.List;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpMethod;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityCustomizer;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import org.springframework.security.web.firewall.HttpFirewall;
import org.springframework.security.web.firewall.StrictHttpFirewall;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.CorsConfigurationSource;
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
@Configuration
@EnableWebSecurity
public class SecurityConfig {
@Bean
public SecurityFilterChain securityFilterChain(
org.springframework.security.config.annotation.web.builders.HttpSecurity http,
JwtAuthenticationFilter jwtAuthenticationFilter,
CustomAuthenticationProvider customAuthenticationProvider)
throws Exception {
http.cors(cors -> cors.configurationSource(corsConfigurationSource()))
.csrf(csrf -> csrf.disable())
.sessionManagement(sm -> sm.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
.formLogin(form -> form.disable())
// /monitor 에서 Basic 인증을 쓰려면 disable 하면 안됨
.httpBasic(basic -> {})
.logout(logout -> logout.disable())
.authenticationProvider(customAuthenticationProvider)
.authorizeHttpRequests(
auth ->
auth
// monitor
.requestMatchers("/monitor/health", "/monitor/health/**")
.permitAll()
.requestMatchers("/monitor/**")
.authenticated() // Basic으로 인증되게끔
// mapsheet
.requestMatchers("/api/mapsheet/**")
.permitAll()
.requestMatchers(HttpMethod.POST, "/api/mapsheet/upload")
.permitAll()
// test role
.requestMatchers("/api/test/admin")
.hasRole("ADMIN")
.requestMatchers("/api/test/label")
.hasAnyRole("ADMIN", "LABELER")
.requestMatchers("/api/test/review")
.hasAnyRole("ADMIN", "REVIEWER")
// common permit
.requestMatchers("/error")
.permitAll()
.requestMatchers(HttpMethod.OPTIONS, "/**")
.permitAll()
.requestMatchers(
"/api/auth/signin",
"/api/auth/refresh",
"/api/auth/logout",
"/swagger-ui/**",
"/v3/api-docs/**",
"/api/members/*/password",
"/api/upload/chunk-upload-dataset",
"/api/upload/chunk-upload-complete")
.permitAll()
// default
.anyRequest()
.authenticated())
// JWT 필터는 앞단에
.addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class);
return http.build();
}
@Bean
public AuthenticationManager authenticationManager(AuthenticationConfiguration configuration)
throws Exception {
return configuration.getAuthenticationManager();
}
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
/** CORS 설정 */
@Bean
public CorsConfigurationSource corsConfigurationSource() {
CorsConfiguration config = new CorsConfiguration(); // CORS 객체 생성
config.setAllowedOriginPatterns(List.of("*")); // 도메인 허용
config.setAllowedMethods(List.of("GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS"));
config.setAllowedHeaders(List.of("*")); // 헤더요청 Authorization, Content-Type, X-Custom-Header
config.setAllowCredentials(true); // 쿠키, Authorization 헤더, Bearer Token 등 자격증명 포함 요청을 허용할지 설정
config.setExposedHeaders(List.of("Content-Disposition"));
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
/** "/**" → 모든 API 경로에 대해 이 CORS 규칙을 적용 /api/** 같이 특정 경로만 지정 가능. */
source.registerCorsConfiguration("/**", config); // CORS 정책을 등록
return source;
}
@Bean
public HttpFirewall httpFirewall() {
StrictHttpFirewall firewall = new StrictHttpFirewall();
firewall.setAllowUrlEncodedSlash(true);
firewall.setAllowUrlEncodedDoubleSlash(true);
firewall.setAllowUrlEncodedPercent(true);
firewall.setAllowSemicolon(true);
return firewall;
}
/** 완전 제외(필터 자체를 안 탐) */
@Bean
public WebSecurityCustomizer webSecurityCustomizer() {
return (web) -> web.ignoring().requestMatchers("/api/mapsheet/**");
}
}
package com.kamco.cd.training.config;
import com.kamco.cd.training.auth.CustomAuthenticationProvider;
import com.kamco.cd.training.auth.JwtAuthenticationFilter;
import java.util.List;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpMethod;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityCustomizer;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import org.springframework.security.web.firewall.HttpFirewall;
import org.springframework.security.web.firewall.StrictHttpFirewall;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.CorsConfigurationSource;
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
@Configuration
@EnableWebSecurity
public class SecurityConfig {
@Bean
public SecurityFilterChain securityFilterChain(
org.springframework.security.config.annotation.web.builders.HttpSecurity http,
JwtAuthenticationFilter jwtAuthenticationFilter,
CustomAuthenticationProvider customAuthenticationProvider)
throws Exception {
http.cors(cors -> cors.configurationSource(corsConfigurationSource()))
.csrf(csrf -> csrf.disable())
.sessionManagement(sm -> sm.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
.formLogin(form -> form.disable())
// /monitor 에서 Basic 인증을 쓰려면 disable 하면 안됨
.httpBasic(basic -> {})
.logout(logout -> logout.disable())
.authenticationProvider(customAuthenticationProvider)
.authorizeHttpRequests(
auth ->
auth
// monitor
.requestMatchers("/monitor/health", "/monitor/health/**")
.permitAll()
.requestMatchers("/monitor/**")
.authenticated() // Basic으로 인증되게끔
// mapsheet
.requestMatchers("/api/mapsheet/**")
.permitAll()
.requestMatchers(HttpMethod.POST, "/api/mapsheet/upload")
.permitAll()
// test role
.requestMatchers("/api/test/admin")
.hasRole("ADMIN")
.requestMatchers("/api/test/label")
.hasAnyRole("ADMIN", "LABELER")
.requestMatchers("/api/test/review")
.hasAnyRole("ADMIN", "REVIEWER")
// common permit
.requestMatchers("/error")
.permitAll()
.requestMatchers(HttpMethod.OPTIONS, "/**")
.permitAll()
.requestMatchers(
"/api/auth/signin",
"/api/auth/refresh",
"/api/auth/logout",
"/swagger-ui/**",
"/v3/api-docs/**",
"/api/members/*/password",
"/api/upload/chunk-upload-dataset",
"/api/upload/chunk-upload-complete")
.permitAll()
// default
.anyRequest()
.authenticated())
// JWT 필터는 앞단에
.addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class);
return http.build();
}
@Bean
public AuthenticationManager authenticationManager(AuthenticationConfiguration configuration)
throws Exception {
return configuration.getAuthenticationManager();
}
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
/** CORS 설정 */
@Bean
public CorsConfigurationSource corsConfigurationSource() {
CorsConfiguration config = new CorsConfiguration(); // CORS 객체 생성
config.setAllowedOriginPatterns(List.of("*")); // 도메인 허용
config.setAllowedMethods(List.of("GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS"));
config.setAllowedHeaders(List.of("*")); // 헤더요청 Authorization, Content-Type, X-Custom-Header
config.setAllowCredentials(true); // 쿠키, Authorization 헤더, Bearer Token 등 자격증명 포함 요청을 허용할지 설정
config.setExposedHeaders(List.of("Content-Disposition"));
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
/** "/**" → 모든 API 경로에 대해 이 CORS 규칙을 적용 /api/** 같이 특정 경로만 지정 가능. */
source.registerCorsConfiguration("/**", config); // CORS 정책을 등록
return source;
}
@Bean
public HttpFirewall httpFirewall() {
StrictHttpFirewall firewall = new StrictHttpFirewall();
firewall.setAllowUrlEncodedSlash(true);
firewall.setAllowUrlEncodedDoubleSlash(true);
firewall.setAllowUrlEncodedPercent(true);
firewall.setAllowSemicolon(true);
return firewall;
}
/** 완전 제외(필터 자체를 안 탐) */
@Bean
public WebSecurityCustomizer webSecurityCustomizer() {
return (web) -> web.ignoring().requestMatchers("/api/mapsheet/**");
}
}

View File

@@ -1,96 +1,96 @@
package com.kamco.cd.training.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);
}
}
package com.kamco.cd.training.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

@@ -1,32 +1,32 @@
package com.kamco.cd.training.config;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.module.SimpleModule;
import com.kamco.cd.training.common.utils.geometry.GeometryDeserializer;
import com.kamco.cd.training.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();
}
}
package com.kamco.cd.training.config;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.module.SimpleModule;
import com.kamco.cd.training.common.utils.geometry.GeometryDeserializer;
import com.kamco.cd.training.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,28 +1,28 @@
package com.kamco.cd.training.config.api;
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import java.io.IOException;
import org.springframework.stereotype.Component;
import org.springframework.web.filter.OncePerRequestFilter;
import org.springframework.web.util.ContentCachingRequestWrapper;
import org.springframework.web.util.ContentCachingResponseWrapper;
@Component
public class ApiLogFilter extends OncePerRequestFilter {
@Override
protected void doFilterInternal(
HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
throws ServletException, IOException {
ContentCachingRequestWrapper wrappedRequest = new ContentCachingRequestWrapper(request);
ContentCachingResponseWrapper wrappedResponse = new ContentCachingResponseWrapper(response);
filterChain.doFilter(wrappedRequest, wrappedResponse);
// 반드시 response body copy
wrappedResponse.copyBodyToResponse();
}
}
package com.kamco.cd.training.config.api;
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import java.io.IOException;
import org.springframework.stereotype.Component;
import org.springframework.web.filter.OncePerRequestFilter;
import org.springframework.web.util.ContentCachingRequestWrapper;
import org.springframework.web.util.ContentCachingResponseWrapper;
@Component
public class ApiLogFilter extends OncePerRequestFilter {
@Override
protected void doFilterInternal(
HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
throws ServletException, IOException {
ContentCachingRequestWrapper wrappedRequest = new ContentCachingRequestWrapper(request);
ContentCachingResponseWrapper wrappedResponse = new ContentCachingResponseWrapper(response);
filterChain.doFilter(wrappedRequest, wrappedResponse);
// 반드시 response body copy
wrappedResponse.copyBodyToResponse();
}
}

View File

@@ -1,132 +1,132 @@
package com.kamco.cd.training.config.api;
import com.kamco.cd.training.log.dto.EventStatus;
import com.kamco.cd.training.log.dto.EventType;
import com.kamco.cd.training.menu.dto.MenuDto;
import jakarta.servlet.http.HttpServletRequest;
import java.io.UnsupportedEncodingException;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
import org.springframework.web.util.ContentCachingRequestWrapper;
public class ApiLogFunction {
// 클라이언트 IP 추출
public static String getClientIp(HttpServletRequest request) {
String[] headers = {
"X-Forwarded-For",
"Proxy-Client-IP",
"WL-Proxy-Client-IP",
"HTTP_CLIENT_IP",
"HTTP_X_FORWARDED_FOR"
};
for (String header : headers) {
String ip = request.getHeader(header);
if (ip != null && ip.length() != 0 && !"unknown".equalsIgnoreCase(ip)) {
return ip.split(",")[0];
}
}
String ip = request.getRemoteAddr();
if ("0:0:0:0:0:0:0:1".equals(ip)) { // local 일 때
ip = "127.0.0.1";
}
return ip;
}
// 사용자 ID 추출 예시 (Spring Security 기준)
public static String getUserId(HttpServletRequest request) {
try {
return request.getUserPrincipal() != null ? request.getUserPrincipal().getName() : null;
} catch (Exception e) {
return null;
}
}
public static EventType getEventType(HttpServletRequest request) {
String method = request.getMethod().toUpperCase();
String uri = request.getRequestURI().toLowerCase();
// URL 기반 DOWNLOAD/PRINT 분류
if (uri.contains("/download") || uri.contains("/export")) {
return EventType.DOWNLOAD;
}
if (uri.contains("/print")) {
return EventType.PRINT;
}
// 일반 CRUD
return switch (method) {
case "POST" -> EventType.CREATE;
case "GET" -> EventType.READ;
case "DELETE" -> EventType.DELETE;
case "PUT", "PATCH" -> EventType.UPDATE;
default -> EventType.OTHER;
};
}
public static String getRequestBody(
HttpServletRequest servletRequest, ContentCachingRequestWrapper contentWrapper) {
StringBuilder resultBody = new StringBuilder();
// GET, form-urlencoded POST 파라미터
Map<String, String[]> paramMap = servletRequest.getParameterMap();
String queryParams =
paramMap.entrySet().stream()
.map(e -> e.getKey() + "=" + String.join(",", e.getValue()))
.collect(Collectors.joining("&"));
resultBody.append(queryParams.isEmpty() ? "" : queryParams);
// JSON Body
if ("POST".equalsIgnoreCase(servletRequest.getMethod())
&& servletRequest.getContentType() != null
&& servletRequest.getContentType().contains("application/json")) {
try {
// json인 경우는 Wrapper를 통해 가져오기
resultBody.append(getBodyData(contentWrapper));
} catch (Exception e) {
resultBody.append("cannot read JSON body ").append(e.toString());
}
}
// Multipart form-data
if ("POST".equalsIgnoreCase(servletRequest.getMethod())
&& servletRequest.getContentType() != null
&& servletRequest.getContentType().startsWith("multipart/form-data")) {
resultBody.append("multipart/form-data request");
}
return resultBody.toString();
}
// JSON Body 읽기
public static String getBodyData(ContentCachingRequestWrapper request) {
byte[] buf = request.getContentAsByteArray();
if (buf.length == 0) {
return null;
}
try {
return new String(buf, request.getCharacterEncoding());
} catch (UnsupportedEncodingException e) {
return new String(buf);
}
}
// ApiResponse 의 Status가 2xx 범위이면 SUCCESS, 아니면 FAILED
public static EventStatus isSuccessFail(ApiResponseDto<?> apiResponse) {
return apiResponse.getHttpStatus().is2xxSuccessful() ? EventStatus.SUCCESS : EventStatus.FAILED;
}
public static String getUriMenuInfo(List<MenuDto.Basic> menuList, String uri) {
MenuDto.Basic m =
menuList.stream()
.filter(menu -> menu.getMenuApiUrl() != null && uri.contains(menu.getMenuApiUrl()))
.findFirst()
.orElse(null);
return m != null ? m.getMenuUid() : "SYSTEM";
}
}
package com.kamco.cd.training.config.api;
import com.kamco.cd.training.log.dto.EventStatus;
import com.kamco.cd.training.log.dto.EventType;
import com.kamco.cd.training.menu.dto.MenuDto;
import jakarta.servlet.http.HttpServletRequest;
import java.io.UnsupportedEncodingException;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
import org.springframework.web.util.ContentCachingRequestWrapper;
public class ApiLogFunction {
// 클라이언트 IP 추출
public static String getClientIp(HttpServletRequest request) {
String[] headers = {
"X-Forwarded-For",
"Proxy-Client-IP",
"WL-Proxy-Client-IP",
"HTTP_CLIENT_IP",
"HTTP_X_FORWARDED_FOR"
};
for (String header : headers) {
String ip = request.getHeader(header);
if (ip != null && ip.length() != 0 && !"unknown".equalsIgnoreCase(ip)) {
return ip.split(",")[0];
}
}
String ip = request.getRemoteAddr();
if ("0:0:0:0:0:0:0:1".equals(ip)) { // local 일 때
ip = "127.0.0.1";
}
return ip;
}
// 사용자 ID 추출 예시 (Spring Security 기준)
public static String getUserId(HttpServletRequest request) {
try {
return request.getUserPrincipal() != null ? request.getUserPrincipal().getName() : null;
} catch (Exception e) {
return null;
}
}
public static EventType getEventType(HttpServletRequest request) {
String method = request.getMethod().toUpperCase();
String uri = request.getRequestURI().toLowerCase();
// URL 기반 DOWNLOAD/PRINT 분류
if (uri.contains("/download") || uri.contains("/export")) {
return EventType.DOWNLOAD;
}
if (uri.contains("/print")) {
return EventType.PRINT;
}
// 일반 CRUD
return switch (method) {
case "POST" -> EventType.CREATE;
case "GET" -> EventType.READ;
case "DELETE" -> EventType.DELETE;
case "PUT", "PATCH" -> EventType.UPDATE;
default -> EventType.OTHER;
};
}
public static String getRequestBody(
HttpServletRequest servletRequest, ContentCachingRequestWrapper contentWrapper) {
StringBuilder resultBody = new StringBuilder();
// GET, form-urlencoded POST 파라미터
Map<String, String[]> paramMap = servletRequest.getParameterMap();
String queryParams =
paramMap.entrySet().stream()
.map(e -> e.getKey() + "=" + String.join(",", e.getValue()))
.collect(Collectors.joining("&"));
resultBody.append(queryParams.isEmpty() ? "" : queryParams);
// JSON Body
if ("POST".equalsIgnoreCase(servletRequest.getMethod())
&& servletRequest.getContentType() != null
&& servletRequest.getContentType().contains("application/json")) {
try {
// json인 경우는 Wrapper를 통해 가져오기
resultBody.append(getBodyData(contentWrapper));
} catch (Exception e) {
resultBody.append("cannot read JSON body ").append(e.toString());
}
}
// Multipart form-data
if ("POST".equalsIgnoreCase(servletRequest.getMethod())
&& servletRequest.getContentType() != null
&& servletRequest.getContentType().startsWith("multipart/form-data")) {
resultBody.append("multipart/form-data request");
}
return resultBody.toString();
}
// JSON Body 읽기
public static String getBodyData(ContentCachingRequestWrapper request) {
byte[] buf = request.getContentAsByteArray();
if (buf.length == 0) {
return null;
}
try {
return new String(buf, request.getCharacterEncoding());
} catch (UnsupportedEncodingException e) {
return new String(buf);
}
}
// ApiResponse 의 Status가 2xx 범위이면 SUCCESS, 아니면 FAILED
public static EventStatus isSuccessFail(ApiResponseDto<?> apiResponse) {
return apiResponse.getHttpStatus().is2xxSuccessful() ? EventStatus.SUCCESS : EventStatus.FAILED;
}
public static String getUriMenuInfo(List<MenuDto.Basic> menuList, String uri) {
MenuDto.Basic m =
menuList.stream()
.filter(menu -> menu.getMenuApiUrl() != null && uri.contains(menu.getMenuApiUrl()))
.findFirst()
.orElse(null);
return m != null ? m.getMenuUid() : "SYSTEM";
}
}

View File

@@ -1,124 +1,124 @@
package com.kamco.cd.training.config.api;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.kamco.cd.training.auth.CustomUserDetails;
import com.kamco.cd.training.menu.service.MenuService;
import com.kamco.cd.training.postgres.entity.AuditLogEntity;
import com.kamco.cd.training.postgres.repository.log.AuditLogRepository;
import jakarta.servlet.http.HttpServletRequest;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.MethodParameter;
import org.springframework.http.MediaType;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.http.server.ServerHttpRequest;
import org.springframework.http.server.ServerHttpResponse;
import org.springframework.http.server.ServletServerHttpRequest;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import org.springframework.web.servlet.mvc.method.annotation.ResponseBodyAdvice;
import org.springframework.web.util.ContentCachingRequestWrapper;
/**
* ApiResponseDto의 내장된 HTTP 상태 코드를 실제 HTTP 응답에 적용하는 Advice
*
* <p>createOK() → 201 CREATED ok() → 200 OK deleteOk() → 204 NO_CONTENT
*/
@RestControllerAdvice
public class ApiResponseAdvice implements ResponseBodyAdvice<Object> {
private final AuditLogRepository auditLogRepository;
private final MenuService menuService;
@Autowired private ObjectMapper objectMapper;
public ApiResponseAdvice(AuditLogRepository auditLogRepository, MenuService menuService) {
this.auditLogRepository = auditLogRepository;
this.menuService = menuService;
}
@Override
public boolean supports(
MethodParameter returnType, Class<? extends HttpMessageConverter<?>> converterType) {
// ApiResponseDto를 반환하는 경우에만 적용
return returnType.getParameterType().equals(ApiResponseDto.class);
}
@Override
public Object beforeBodyWrite(
Object body,
MethodParameter returnType,
MediaType selectedContentType,
Class<? extends HttpMessageConverter<?>> selectedConverterType,
ServerHttpRequest request,
ServerHttpResponse response) {
HttpServletRequest servletRequest = ((ServletServerHttpRequest) request).getServletRequest();
ContentCachingRequestWrapper contentWrapper = null;
if (servletRequest instanceof ContentCachingRequestWrapper wrapper) {
contentWrapper = wrapper;
}
if (body instanceof ApiResponseDto<?> apiResponse) {
response.setStatusCode(apiResponse.getHttpStatus());
String ip = ApiLogFunction.getClientIp(servletRequest);
Long userid = null;
if (servletRequest.getUserPrincipal() instanceof UsernamePasswordAuthenticationToken auth
&& auth.getPrincipal() instanceof CustomUserDetails customUserDetails) {
userid = customUserDetails.getMember().getId();
}
String requestBody;
// 멀티파트 요청은 바디 로깅을 생략 (파일 바이너리로 인한 문제 예방)
MediaType reqContentType = null;
try {
String ct = servletRequest.getContentType();
reqContentType = ct != null ? MediaType.valueOf(ct) : null;
} catch (Exception ignored) {
}
if (reqContentType != null && MediaType.MULTIPART_FORM_DATA.includes(reqContentType)) {
requestBody = "(multipart omitted)";
} else {
requestBody = ApiLogFunction.getRequestBody(servletRequest, contentWrapper);
requestBody = maskSensitiveFields(requestBody);
}
AuditLogEntity log =
new AuditLogEntity(
userid,
ApiLogFunction.getEventType(servletRequest),
ApiLogFunction.isSuccessFail(apiResponse),
ApiLogFunction.getUriMenuInfo(
menuService.getFindAll(), servletRequest.getRequestURI()),
ip,
servletRequest.getRequestURI(),
requestBody,
apiResponse.getErrorLogUid());
auditLogRepository.save(log);
}
return body;
}
/**
* 마스킹
*
* @param json
* @return
*/
private String maskSensitiveFields(String json) {
if (json == null) {
return null;
}
// password 마스킹
json = json.replaceAll("\"password\"\\s*:\\s*\"[^\"]*\"", "\"password\":\"****\"");
// 토큰 마스킹
json = json.replaceAll("\"accessToken\"\\s*:\\s*\"[^\"]*\"", "\"accessToken\":\"****\"");
json = json.replaceAll("\"refreshToken\"\\s*:\\s*\"[^\"]*\"", "\"refreshToken\":\"****\"");
return json;
}
}
package com.kamco.cd.training.config.api;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.kamco.cd.training.auth.CustomUserDetails;
import com.kamco.cd.training.menu.service.MenuService;
import com.kamco.cd.training.postgres.entity.AuditLogEntity;
import com.kamco.cd.training.postgres.repository.log.AuditLogRepository;
import jakarta.servlet.http.HttpServletRequest;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.MethodParameter;
import org.springframework.http.MediaType;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.http.server.ServerHttpRequest;
import org.springframework.http.server.ServerHttpResponse;
import org.springframework.http.server.ServletServerHttpRequest;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import org.springframework.web.servlet.mvc.method.annotation.ResponseBodyAdvice;
import org.springframework.web.util.ContentCachingRequestWrapper;
/**
* ApiResponseDto의 내장된 HTTP 상태 코드를 실제 HTTP 응답에 적용하는 Advice
*
* <p>createOK() → 201 CREATED ok() → 200 OK deleteOk() → 204 NO_CONTENT
*/
@RestControllerAdvice
public class ApiResponseAdvice implements ResponseBodyAdvice<Object> {
private final AuditLogRepository auditLogRepository;
private final MenuService menuService;
@Autowired private ObjectMapper objectMapper;
public ApiResponseAdvice(AuditLogRepository auditLogRepository, MenuService menuService) {
this.auditLogRepository = auditLogRepository;
this.menuService = menuService;
}
@Override
public boolean supports(
MethodParameter returnType, Class<? extends HttpMessageConverter<?>> converterType) {
// ApiResponseDto를 반환하는 경우에만 적용
return returnType.getParameterType().equals(ApiResponseDto.class);
}
@Override
public Object beforeBodyWrite(
Object body,
MethodParameter returnType,
MediaType selectedContentType,
Class<? extends HttpMessageConverter<?>> selectedConverterType,
ServerHttpRequest request,
ServerHttpResponse response) {
HttpServletRequest servletRequest = ((ServletServerHttpRequest) request).getServletRequest();
ContentCachingRequestWrapper contentWrapper = null;
if (servletRequest instanceof ContentCachingRequestWrapper wrapper) {
contentWrapper = wrapper;
}
if (body instanceof ApiResponseDto<?> apiResponse) {
response.setStatusCode(apiResponse.getHttpStatus());
String ip = ApiLogFunction.getClientIp(servletRequest);
Long userid = null;
if (servletRequest.getUserPrincipal() instanceof UsernamePasswordAuthenticationToken auth
&& auth.getPrincipal() instanceof CustomUserDetails customUserDetails) {
userid = customUserDetails.getMember().getId();
}
String requestBody;
// 멀티파트 요청은 바디 로깅을 생략 (파일 바이너리로 인한 문제 예방)
MediaType reqContentType = null;
try {
String ct = servletRequest.getContentType();
reqContentType = ct != null ? MediaType.valueOf(ct) : null;
} catch (Exception ignored) {
}
if (reqContentType != null && MediaType.MULTIPART_FORM_DATA.includes(reqContentType)) {
requestBody = "(multipart omitted)";
} else {
requestBody = ApiLogFunction.getRequestBody(servletRequest, contentWrapper);
requestBody = maskSensitiveFields(requestBody);
}
AuditLogEntity log =
new AuditLogEntity(
userid,
ApiLogFunction.getEventType(servletRequest),
ApiLogFunction.isSuccessFail(apiResponse),
ApiLogFunction.getUriMenuInfo(
menuService.getFindAll(), servletRequest.getRequestURI()),
ip,
servletRequest.getRequestURI(),
requestBody,
apiResponse.getErrorLogUid());
auditLogRepository.save(log);
}
return body;
}
/**
* 마스킹
*
* @param json
* @return
*/
private String maskSensitiveFields(String json) {
if (json == null) {
return null;
}
// password 마스킹
json = json.replaceAll("\"password\"\\s*:\\s*\"[^\"]*\"", "\"password\":\"****\"");
// 토큰 마스킹
json = json.replaceAll("\"accessToken\"\\s*:\\s*\"[^\"]*\"", "\"accessToken\":\"****\"");
json = json.replaceAll("\"refreshToken\"\\s*:\\s*\"[^\"]*\"", "\"refreshToken\":\"****\"");
return json;
}
}

View File

@@ -1,199 +1,205 @@
package com.kamco.cd.training.config.api;
import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.annotation.JsonInclude;
import com.kamco.cd.training.common.utils.enums.EnumType;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
import lombok.ToString;
import org.springframework.http.HttpStatus;
@Getter
@ToString
public class ApiResponseDto<T> {
private T data;
@JsonInclude(JsonInclude.Include.NON_NULL)
private Error error;
@JsonInclude(JsonInclude.Include.NON_NULL)
private T errorData;
@JsonIgnore private HttpStatus httpStatus;
@JsonIgnore private Long errorLogUid;
public ApiResponseDto(T data) {
this.data = data;
}
private ApiResponseDto(T data, HttpStatus httpStatus) {
this.data = data;
this.httpStatus = httpStatus;
}
public ApiResponseDto(ApiResponseCode code) {
this.error = new Error(code.getId(), code.getMessage());
}
public ApiResponseDto(ApiResponseCode code, String message) {
this.error = new Error(code.getId(), message);
}
public ApiResponseDto(ApiResponseCode code, String message, HttpStatus httpStatus) {
this.error = new Error(code.getId(), message);
this.httpStatus = httpStatus;
}
public ApiResponseDto(
ApiResponseCode code, String message, HttpStatus httpStatus, Long errorLogUid) {
this.error = new Error(code.getId(), message);
this.httpStatus = httpStatus;
this.errorLogUid = errorLogUid;
}
public ApiResponseDto(ApiResponseCode code, String message, T errorData) {
this.error = new Error(code.getId(), message);
this.errorData = errorData;
}
// HTTP 상태 코드가 내장된 ApiResponseDto 반환 메서드들
public static <T> ApiResponseDto<T> createOK(T data) {
return new ApiResponseDto<>(data, HttpStatus.CREATED);
}
public static <T> ApiResponseDto<T> ok(T data) {
return new ApiResponseDto<>(data, HttpStatus.OK);
}
public static <T> ApiResponseDto<ResponseObj> okObject(ResponseObj data) {
if (data.getCode().equals(ApiResponseCode.OK)) {
return new ApiResponseDto<>(data, HttpStatus.NO_CONTENT);
} else {
return new ApiResponseDto<>(data.getCode(), data.getMessage(), HttpStatus.CONFLICT);
}
}
public static <T> ApiResponseDto<T> deleteOk(T data) {
return new ApiResponseDto<>(data, HttpStatus.NO_CONTENT);
}
public static ApiResponseDto<String> createException(ApiResponseCode code) {
return new ApiResponseDto<>(code);
}
public static ApiResponseDto<String> createException(ApiResponseCode code, String message) {
return new ApiResponseDto<>(code, message);
}
public static ApiResponseDto<String> createException(
ApiResponseCode code, String message, HttpStatus httpStatus) {
return new ApiResponseDto<>(code, message, httpStatus);
}
public static ApiResponseDto<String> createException(
ApiResponseCode code, String message, HttpStatus httpStatus, Long errorLogUid) {
return new ApiResponseDto<>(code, message, httpStatus, errorLogUid);
}
public static <T> ApiResponseDto<T> createException(
ApiResponseCode code, String message, T data) {
return new ApiResponseDto<>(code, message, data);
}
@Getter
public static class Error {
private final String code;
private final String message;
public Error(String code, String message) {
this.code = code;
this.message = message;
}
}
/** Error가 아닌 Business상 성공이거나 실패인 경우, 메세지 함께 전달하기 위한 object */
@Getter
public static class ResponseObj {
private final ApiResponseCode code;
private final String message;
public ResponseObj(ApiResponseCode code, String message) {
this.code = code;
this.message = message;
}
}
@Getter
@RequiredArgsConstructor
public enum ApiResponseCode implements EnumType {
// @formatter:off
OK("요청이 성공하였습니다."),
BAD_REQUEST("요청 파라미터가 잘못되었습니다."),
BAD_GATEWAY("네트워크 상태가 불안정합니다."),
ALREADY_EXIST_MALL("이미 등록된 쇼핑센터입니다."),
NOT_FOUND_MAP("지도를 찾을 수 없습니다."),
UNAUTHORIZED("권한이 없습니다."),
CONFLICT("이미 등록된 컨텐츠입니다."),
NOT_FOUND("Resource를 찾을 수 없습니다."),
NOT_FOUND_DATA("데이터를 찾을 수 없습니다."),
NOT_FOUND_WEATHER_DATA("날씨 데이터를 찾을 수 없습니다."),
FAIL_SEND_MESSAGE("메시지를 전송하지 못했습니다."),
TOO_MANY_CONNECTED_MACHINES("연결된 기기가 너무 많습니다."),
UNAUTHENTICATED("인증에 실패하였습니다."),
INVALID_TOKEN("잘못된 토큰입니다."),
EXPIRED_TOKEN("만료된 토큰입니다."),
INTERNAL_SERVER_ERROR("서버에 문제가 발생 하였습니다."),
FORBIDDEN("권한을 확인해주세요."),
INVALID_PASSWORD("잘못된 비밀번호 입니다."),
NOT_FOUND_CAR_IN("입차정보가 없습니다."),
WRONG_STATUS("잘못된 상태입니다."),
FAIL_VERIFICATION("인증에 실패하였습니다."),
INVALID_EMAIL("잘못된 형식의 이메일입니다."),
REQUIRED_EMAIL("이메일은 필수 항목입니다."),
WRONG_PASSWORD("잘못된 패스워드입니다."),
DUPLICATE_EMAIL("이미 가입된 이메일입니다."),
DUPLICATE_DATA("이미 등록되어 있습니다."),
DATA_INTEGRITY_ERROR("데이터 무결성이 위반되어 요청을 처리할수 없습니다."),
FOREIGN_KEY_ERROR("참조 중인 데이터가 있어 삭제할 수 없습니다."),
DUPLICATE_EMPLOYEEID("이미 가입된 사번입니다."),
NOT_FOUND_USER_FOR_EMAIL("이메일로 유저를 찾을 수 없습니다."),
NOT_FOUND_USER("사용자를 찾을 수 없습니다."),
UNPROCESSABLE_ENTITY("이 데이터는 삭제할 수 없습니다."),
LOGIN_ID_NOT_FOUND("아이디를 잘못 입력하셨습니다."),
LOGIN_PASSWORD_MISMATCH("비밀번호를 잘못 입력하셨습니다."),
LOGIN_PASSWORD_EXCEEDED("비밀번호 오류 횟수를 초과하여 이용하실 수 없습니다.\n로그인 오류에 대해 관리자에게 문의하시기 바랍니다."),
REFRESH_TOKEN_EXPIRED_OR_REVOKED("토큰 정보가 만료 되었습니다."),
REFRESH_TOKEN_MISMATCH("토큰 정보가 일치하지 않습니다."),
INACTIVE_ID("사용할 수 없는 계정입니다."),
INVALID_EMAIL_TOKEN(
"You can only reset your password within 24 hours from when the email was sent.\n"
+ "To reset your password again, please submit a new request through \"Forgot"
+ " Password.\""),
;
// @formatter:on
private final String message;
@Override
public String getId() {
return name();
}
@Override
public String getText() {
return message;
}
public static ApiResponseCode getCode(String name) {
return ApiResponseCode.valueOf(name.toUpperCase());
}
public static String getMessage(String name) {
return ApiResponseCode.valueOf(name.toUpperCase()).getText();
}
}
}
package com.kamco.cd.training.config.api;
import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.annotation.JsonInclude;
import com.kamco.cd.training.common.utils.enums.EnumType;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
import lombok.ToString;
import org.springframework.http.HttpStatus;
@Getter
@ToString
public class ApiResponseDto<T> {
private T data;
@JsonInclude(JsonInclude.Include.NON_NULL)
private Error error;
@JsonInclude(JsonInclude.Include.NON_NULL)
private T errorData;
@JsonIgnore private HttpStatus httpStatus;
@JsonIgnore private Long errorLogUid;
public ApiResponseDto(T data) {
this.data = data;
}
private ApiResponseDto(T data, HttpStatus httpStatus) {
this.data = data;
this.httpStatus = httpStatus;
}
public ApiResponseDto(ApiResponseCode code) {
this.error = new Error(code.getId(), code.getMessage());
}
public ApiResponseDto(ApiResponseCode code, String message) {
this.error = new Error(code.getId(), message);
}
public ApiResponseDto(ApiResponseCode code, String message, HttpStatus httpStatus) {
this.error = new Error(code.getId(), message);
this.httpStatus = httpStatus;
}
public ApiResponseDto(
ApiResponseCode code, String message, HttpStatus httpStatus, Long errorLogUid) {
this.error = new Error(code.getId(), message);
this.httpStatus = httpStatus;
this.errorLogUid = errorLogUid;
}
public ApiResponseDto(ApiResponseCode code, String message, T errorData) {
this.error = new Error(code.getId(), message);
this.errorData = errorData;
}
// HTTP 상태 코드가 내장된 ApiResponseDto 반환 메서드들
public static <T> ApiResponseDto<T> createOK(T data) {
return new ApiResponseDto<>(data, HttpStatus.CREATED);
}
public static <T> ApiResponseDto<T> ok(T data) {
return new ApiResponseDto<>(data, HttpStatus.OK);
}
public static <T> ApiResponseDto<ResponseObj> okObject(ResponseObj data) {
if (data.getCode().equals(ApiResponseCode.OK)) {
return new ApiResponseDto<>(data, HttpStatus.NO_CONTENT);
} else {
return new ApiResponseDto<>(data.getCode(), data.getMessage(), HttpStatus.CONFLICT);
}
}
public static <T> ApiResponseDto<T> deleteOk(T data) {
return new ApiResponseDto<>(data, HttpStatus.NO_CONTENT);
}
public static ApiResponseDto<String> createException(ApiResponseCode code) {
return new ApiResponseDto<>(code);
}
public static ApiResponseDto<String> createException(ApiResponseCode code, String message) {
return new ApiResponseDto<>(code, message);
}
public static ApiResponseDto<String> createException(
ApiResponseCode code, String message, HttpStatus httpStatus) {
return new ApiResponseDto<>(code, message, httpStatus);
}
public static ApiResponseDto<String> createException(
ApiResponseCode code, String message, HttpStatus httpStatus, Long errorLogUid) {
return new ApiResponseDto<>(code, message, httpStatus, errorLogUid);
}
public static <T> ApiResponseDto<T> createException(
ApiResponseCode code, String message, T data) {
return new ApiResponseDto<>(code, message, data);
}
@Getter
public static class Error {
private final String code;
private final String message;
public Error(String code, String message) {
this.code = code;
this.message = message;
}
}
/** Error가 아닌 Business상 성공이거나 실패인 경우, 메세지 함께 전달하기 위한 object */
@Getter
public static class ResponseObj {
private final ApiResponseCode code;
private final String message;
public ResponseObj(ApiResponseCode code, String message) {
this.code = code;
this.message = message;
}
}
@Getter
@RequiredArgsConstructor
public enum ApiResponseCode implements EnumType {
// @formatter:off
OK("요청이 성공하였습니다."),
BAD_REQUEST("요청 파라미터가 잘못되었습니다."),
BAD_GATEWAY("네트워크 상태가 불안정합니다."),
ALREADY_EXIST_MALL("이미 등록된 쇼핑센터입니다."),
NOT_FOUND_MAP("지도를 찾을 수 없습니다."),
UNAUTHORIZED("권한이 없습니다."),
CONFLICT("이미 등록된 컨텐츠입니다."),
NOT_FOUND("Resource를 찾을 수 없습니다."),
NOT_FOUND_DATA("데이터를 찾을 수 없습니다."),
NOT_FOUND_WEATHER_DATA("날씨 데이터를 찾을 수 없습니다."),
FAIL_SEND_MESSAGE("메시지를 전송하지 못했습니다."),
TOO_MANY_CONNECTED_MACHINES("연결된 기기가 너무 많습니다."),
UNAUTHENTICATED("인증에 실패하였습니다."),
INVALID_TOKEN("잘못된 토큰입니다."),
EXPIRED_TOKEN("만료된 토큰입니다."),
INTERNAL_SERVER_ERROR("서버에 문제가 발생 하였습니다."),
FORBIDDEN("권한을 확인해주세요."),
INVALID_PASSWORD("잘못된 비밀번호 입니다."),
NOT_FOUND_CAR_IN("입차정보가 없습니다."),
WRONG_STATUS("잘못된 상태입니다."),
FAIL_VERIFICATION("인증에 실패하였습니다."),
INVALID_EMAIL("잘못된 형식의 이메일입니다."),
REQUIRED_EMAIL("이메일은 필수 항목입니다."),
WRONG_PASSWORD("잘못된 패스워드입니다."),
DUPLICATE_EMAIL("이미 가입된 이메일입니다."),
DUPLICATE_DATA("이미 등록되어 있습니다."),
DATA_INTEGRITY_ERROR("데이터 무결성이 위반되어 요청을 처리할수 없습니다."),
FOREIGN_KEY_ERROR("참조 중인 데이터가 있어 삭제할 수 없습니다."),
DUPLICATE_EMPLOYEEID("이미 가입된 사번입니다."),
NOT_FOUND_USER_FOR_EMAIL("이메일로 유저를 찾을 수 없습니다."),
NOT_FOUND_USER("사용자를 찾을 수 없습니다."),
UNPROCESSABLE_ENTITY("이 데이터는 삭제할 수 없습니다."),
UNPROCESSABLE_ENTITY_UPDATE("이 데이터는 수정할 수 없습니다."),
LOGIN_ID_NOT_FOUND("아이디를 잘못 입력하셨습니다."),
LOGIN_PASSWORD_MISMATCH("비밀번호를 잘못 입력하셨습니다."),
LOGIN_PASSWORD_EXCEEDED("비밀번호 오류 횟수를 초과하여 이용하실 수 없습니다.\n로그인 오류에 대해 관리자에게 문의하시기 바랍니다."),
REFRESH_TOKEN_EXPIRED_OR_REVOKED("토큰 정보가 만료 되었습니다."),
REFRESH_TOKEN_MISMATCH("토큰 정보가 일치하지 않습니다."),
INACTIVE_ID("사용할 수 없는 계정입니다."),
INVALID_EMAIL_TOKEN(
"You can only reset your password within 24 hours from when the email was sent.\n"
+ "To reset your password again, please submit a new request through \"Forgot"
+ " Password.\""),
;
// @formatter:on
private final String message;
@Override
public String getId() {
return name();
}
@Override
public String getText() {
return message;
}
public static ApiResponseCode getCode(String name) {
if (name == null || name.isBlank()) return null;
try {
return ApiResponseCode.valueOf(name.toUpperCase());
} catch (IllegalArgumentException ex) {
return null;
}
}
public static String getMessage(String name) {
return ApiResponseCode.valueOf(name.toUpperCase()).getText();
}
}
}

View File

@@ -1,13 +1,13 @@
package com.kamco.cd.training.config;
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 {}
package com.kamco.cd.training.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.training.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,154 +1,240 @@
package com.kamco.cd.training.dataset;
import com.kamco.cd.training.config.api.ApiResponseDto;
import com.kamco.cd.training.dataset.dto.DatasetDto;
import com.kamco.cd.training.dataset.service.DatasetService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.media.Content;
import io.swagger.v3.oas.annotations.media.Schema;
import io.swagger.v3.oas.annotations.responses.ApiResponse;
import io.swagger.v3.oas.annotations.responses.ApiResponses;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.validation.Valid;
import java.util.UUID;
import lombok.RequiredArgsConstructor;
import org.springframework.data.domain.Page;
import org.springframework.web.bind.annotation.*;
@Tag(name = "데이터셋 관리", description = "어드민 홈 > 학습데이터관리 > 전체데이터 API")
@RestController
@RequestMapping("/api/datasets")
@RequiredArgsConstructor
public class DatasetApiController {
private final DatasetService datasetService;
@Operation(summary = "데이터셋 목록 조회", description = "데이터셋(회차) 목록을 조회합니다.")
@ApiResponses(
value = {
@ApiResponse(
responseCode = "200",
description = "조회 성공",
content =
@Content(
mediaType = "application/json",
schema = @Schema(implementation = Page.class))),
@ApiResponse(responseCode = "400", description = "잘못된 검색 조건", content = @Content),
@ApiResponse(responseCode = "500", description = "서버 오류", content = @Content)
})
@GetMapping
public ApiResponseDto<Page<DatasetDto.Basic>> searchDatasets(
@Parameter(description = "구분", example = "DELIVER(납품), PRODUCTION(제작)")
@RequestParam(required = false)
String groupTitle,
@Parameter(description = "제목", example = "") @RequestParam(required = false) String title,
@Parameter(description = "페이지 번호 (0부터 시작)", example = "0") @RequestParam(defaultValue = "0")
int page,
@Parameter(description = "페이지 크기", example = "20") @RequestParam(defaultValue = "20")
int size) {
DatasetDto.SearchReq searchReq = new DatasetDto.SearchReq();
searchReq.setTitle(title);
searchReq.setGroupTitle(groupTitle);
searchReq.setPage(page);
searchReq.setSize(size);
return ApiResponseDto.ok(datasetService.searchDatasets(searchReq));
}
@Operation(summary = "데이터셋 상세 조회", description = "데이터셋 상세 정보를 조회합니다.")
@ApiResponses(
value = {
@ApiResponse(
responseCode = "200",
description = "조회 성공",
content =
@Content(
mediaType = "application/json",
schema = @Schema(implementation = DatasetDto.Basic.class))),
@ApiResponse(responseCode = "404", description = "데이터셋을 찾을 수 없음", content = @Content),
@ApiResponse(responseCode = "500", description = "서버 오류", content = @Content)
})
@GetMapping("/{uuid}")
public ApiResponseDto<DatasetDto.Basic> getDatasetDetail(@PathVariable UUID uuid) {
return ApiResponseDto.ok(datasetService.getDatasetDetail(uuid));
}
@Operation(summary = "데이터셋 등록", description = "신규 데이터셋(회차)을 생성합니다.")
@ApiResponses(
value = {
@ApiResponse(
responseCode = "201",
description = "등록 성공",
content =
@Content(
mediaType = "application/json",
schema = @Schema(implementation = Long.class))),
@ApiResponse(responseCode = "400", description = "잘못된 요청 데이터", content = @Content),
@ApiResponse(responseCode = "500", description = "서버 오류", content = @Content)
})
@PostMapping("/register")
public ApiResponseDto<Long> registerDataset(
@RequestBody @Valid DatasetDto.RegisterReq registerReq) {
Long id = datasetService.registerDataset(registerReq);
return ApiResponseDto.createOK(id);
}
@Operation(summary = "데이터셋 수정", description = "데이터셋 정보를 수정합니다.")
@ApiResponses(
value = {
@ApiResponse(
responseCode = "200",
description = "수정 성공",
content =
@Content(
mediaType = "application/json",
schema = @Schema(implementation = Long.class))),
@ApiResponse(responseCode = "400", description = "잘못된 요청 데이터", content = @Content),
@ApiResponse(responseCode = "404", description = "데이터셋을 찾을 수 없음", content = @Content),
@ApiResponse(responseCode = "500", description = "서버 오류", content = @Content)
})
@PutMapping("/{uuid}")
public ApiResponseDto<UUID> updateDataset(
@PathVariable UUID uuid, @RequestBody DatasetDto.UpdateReq updateReq) {
datasetService.updateDataset(uuid, updateReq);
return ApiResponseDto.ok(uuid);
}
@Operation(summary = "데이터셋 삭제", description = "데이터셋을 삭제합니다.")
@ApiResponses(
value = {
@ApiResponse(responseCode = "201", description = "삭제 성공", content = @Content),
@ApiResponse(responseCode = "400", description = "잘못된 요청", content = @Content),
@ApiResponse(responseCode = "404", description = "데이터셋을 찾을 수 없음", content = @Content),
@ApiResponse(responseCode = "500", description = "서버 오류", content = @Content)
})
@DeleteMapping("/{uuid}")
public ApiResponseDto<UUID> deleteDatasets(@PathVariable UUID uuid) {
datasetService.deleteDatasets(uuid);
return ApiResponseDto.ok(uuid);
}
/*
@Operation(summary = "데이터셋 통계 요약", description = "선택 데이터셋의 통계를 요약합니다.")
@ApiResponses(
value = {
@ApiResponse(
responseCode = "200",
description = "조회 성공",
content =
@Content(
mediaType = "application/json",
schema = @Schema(implementation = DatasetDto.Summary.class))),
@ApiResponse(responseCode = "400", description = "잘못된 요청", content = @Content),
@ApiResponse(responseCode = "500", description = "서버 오류", content = @Content)
})
@PostMapping("/summary")
public ApiResponseDto<DatasetDto.Summary> getDatasetSummary(
@RequestBody @Valid DatasetDto.SummaryReq summaryReq) {
return ApiResponseDto.ok(datasetService.getDatasetSummary(summaryReq));
}
*/
}
package com.kamco.cd.training.dataset;
import com.kamco.cd.training.config.api.ApiResponseDto;
import com.kamco.cd.training.dataset.dto.DatasetDto;
import com.kamco.cd.training.dataset.dto.DatasetObjDto;
import com.kamco.cd.training.dataset.dto.DatasetObjDto.DatasetClass;
import com.kamco.cd.training.dataset.dto.DatasetObjDto.DatasetStorage;
import com.kamco.cd.training.dataset.service.DatasetService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.media.Content;
import io.swagger.v3.oas.annotations.media.Schema;
import io.swagger.v3.oas.annotations.responses.ApiResponse;
import io.swagger.v3.oas.annotations.responses.ApiResponses;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.validation.Valid;
import java.util.List;
import java.util.UUID;
import lombok.RequiredArgsConstructor;
import org.springframework.core.io.Resource;
import org.springframework.data.domain.Page;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
@Tag(name = "학습데이터 관리", description = "어드민 홈 > 학습데이터관리 > 전체데이터 API")
@RestController
@RequestMapping("/api/datasets")
@RequiredArgsConstructor
public class DatasetApiController {
private final DatasetService datasetService;
@Operation(summary = "학습데이터 관리 목록 조회", description = "학습데이터 목록을 조회합니다.")
@ApiResponses(
value = {
@ApiResponse(
responseCode = "200",
description = "조회 성공",
content =
@Content(
mediaType = "application/json",
schema = @Schema(implementation = Page.class))),
@ApiResponse(responseCode = "400", description = "잘못된 검색 조건", content = @Content),
@ApiResponse(responseCode = "500", description = "서버 오류", content = @Content)
})
@GetMapping
public ApiResponseDto<Page<DatasetDto.Basic>> searchDatasets(
@Parameter(
description = "구분",
example = "",
schema = @Schema(allowableValues = {"DELIVER", "PRODUCTION"}))
@RequestParam(required = false)
String dataType,
@Parameter(description = "제목", example = "") @RequestParam(required = false) String title,
@Parameter(description = "페이지 번호 (0부터 시작)", example = "0") @RequestParam(defaultValue = "0")
int page,
@Parameter(description = "페이지 크기", example = "20") @RequestParam(defaultValue = "20")
int size) {
DatasetDto.SearchReq searchReq = new DatasetDto.SearchReq();
searchReq.setTitle(title);
searchReq.setDataType(dataType);
searchReq.setPage(page);
searchReq.setSize(size);
return ApiResponseDto.ok(datasetService.searchDatasets(searchReq));
}
@Operation(summary = "학습데이터관리 상세 조회", description = "학습데이터관리 상세 정보를 조회합니다.")
@ApiResponses(
value = {
@ApiResponse(
responseCode = "200",
description = "조회 성공",
content =
@Content(
mediaType = "application/json",
schema = @Schema(implementation = DatasetDto.Basic.class))),
@ApiResponse(responseCode = "404", description = "데이터셋을 찾을 수 없음", content = @Content),
@ApiResponse(responseCode = "500", description = "서버 오류", content = @Content)
})
@GetMapping("/{uuid}")
public ApiResponseDto<DatasetDto.Basic> getDatasetDetail(@PathVariable UUID uuid) {
return ApiResponseDto.ok(datasetService.getDatasetDetail(uuid));
}
@Operation(summary = "학습데이터 수정", description = "학습데이터 제목, 메모 수정")
@ApiResponses(
value = {
@ApiResponse(
responseCode = "200",
description = "수정 성공",
content =
@Content(
mediaType = "application/json",
schema = @Schema(implementation = Long.class))),
@ApiResponse(responseCode = "400", description = "잘못된 요청 데이터", content = @Content),
@ApiResponse(responseCode = "404", description = "데이터셋을 찾을 수 없음", content = @Content),
@ApiResponse(responseCode = "500", description = "서버 오류", content = @Content)
})
@PutMapping("/{uuid}")
public ApiResponseDto<UUID> updateDataset(
@PathVariable UUID uuid, @RequestBody DatasetDto.UpdateReq updateReq) {
datasetService.updateDataset(uuid, updateReq);
return ApiResponseDto.ok(uuid);
}
@Operation(summary = "학습데이터 삭제", description = "학습데이터를 삭제합니다.(납품 데이터는 삭제 불가)")
@ApiResponses(
value = {
@ApiResponse(responseCode = "201", description = "삭제 성공", content = @Content),
@ApiResponse(responseCode = "400", description = "잘못된 요청", content = @Content),
@ApiResponse(responseCode = "404", description = "데이터셋을 찾을 수 없음", content = @Content),
@ApiResponse(responseCode = "500", description = "서버 오류", content = @Content)
})
@DeleteMapping("/{uuid}")
public ApiResponseDto<UUID> deleteDatasets(@PathVariable UUID uuid) {
datasetService.deleteDatasets(uuid);
return ApiResponseDto.ok(uuid);
}
@Operation(summary = "학습데이터 관리 목록 조회", description = "학습데이터 목록을 조회합니다.")
@ApiResponses(
value = {
@ApiResponse(
responseCode = "200",
description = "조회 성공",
content =
@Content(
mediaType = "application/json",
schema = @Schema(implementation = Page.class))),
@ApiResponse(responseCode = "400", description = "잘못된 검색 조건", content = @Content),
@ApiResponse(responseCode = "500", description = "서버 오류", content = @Content)
})
@GetMapping("/obj-list")
public ApiResponseDto<Page<DatasetObjDto.Basic>> searchDatasetObjectList(
@Parameter(description = "회차 uuid", example = "e9a6774b-4f81-4402-b080-51d27fac1f01")
@RequestParam(required = true)
UUID uuid,
@Parameter(description = "비교년도분류", example = "container") @RequestParam(required = false)
String compareClassCd,
@Parameter(description = "기준년도분류", example = "waste") @RequestParam(required = false)
String targetClassCd,
@Parameter(description = "도엽번호", example = "36713060") @RequestParam(required = false)
String mapSheetNum,
@RequestParam(defaultValue = "0") int page,
@RequestParam(defaultValue = "20") int size) {
DatasetObjDto.SearchReq searchReq = new DatasetObjDto.SearchReq();
searchReq.setUuid(uuid);
searchReq.setCompareClassCd(compareClassCd);
searchReq.setTargetClassCd(targetClassCd);
searchReq.setMapSheetNum(mapSheetNum);
searchReq.setPage(page);
searchReq.setSize(size);
return ApiResponseDto.ok(datasetService.searchDatasetObjectList(searchReq));
}
@Operation(summary = "학습데이터 관리 obj 삭제", description = "학습데이터 관리 obj 삭제 API")
@ApiResponses(
value = {
@ApiResponse(
responseCode = "200",
description = "삭제 성공",
content =
@Content(
mediaType = "application/json",
schema = @Schema(implementation = Page.class))),
@ApiResponse(responseCode = "400", description = "잘못된 검색 조건", content = @Content),
@ApiResponse(responseCode = "500", description = "서버 오류", content = @Content)
})
@DeleteMapping("/obj/{uuid}")
public ApiResponseDto<UUID> deleteDatasetObjByUuid(@PathVariable UUID uuid) {
return ApiResponseDto.ok(datasetService.deleteDatasetObjByUuid(uuid));
}
@Operation(summary = "학습데이터 결과 class 조회", description = "학습데이터 결과 class 조회 API")
@ApiResponses(
value = {
@ApiResponse(
responseCode = "200",
description = "조회 성공",
content =
@Content(
mediaType = "application/json",
schema = @Schema(implementation = Page.class))),
@ApiResponse(responseCode = "400", description = "잘못된 검색 조건", content = @Content),
@ApiResponse(responseCode = "500", description = "서버 오류", content = @Content)
})
@GetMapping("/class/{uuid}")
public ApiResponseDto<List<DatasetClass>> getDatasetObjByUuid(
@Parameter(description = "dataset uuid", example = "e1416f32-769f-495c-a883-3ebfacef4bac")
@PathVariable
UUID uuid,
@Parameter(description = "compare, target", example = "compare") @RequestParam String type) {
return ApiResponseDto.ok(datasetService.getDatasetObjByUuid(uuid, type));
}
@Operation(summary = "남은 저장공간 조회", description = "남은 저장공간 조회 API")
@ApiResponses(
value = {
@ApiResponse(
responseCode = "200",
description = "조회 성공",
content =
@Content(
mediaType = "application/json",
schema = @Schema(implementation = Page.class))),
@ApiResponse(responseCode = "404", description = "저장 공간 조회 오류", content = @Content),
@ApiResponse(responseCode = "500", description = "서버 오류", content = @Content)
})
@GetMapping("/usable-bytes")
public ApiResponseDto<DatasetStorage> getUsableBytes() {
return ApiResponseDto.ok(datasetService.getUsableBytes());
}
@Operation(summary = "학습데이터 zip파일 등록", description = "학습데이터 zip파일 등록 합니다.")
@PostMapping
public ApiResponseDto<ApiResponseDto.ResponseObj> insertDataset(
@RequestBody @Valid DatasetDto.AddReq addReq) {
return ApiResponseDto.ok(datasetService.insertDataset(addReq));
}
@Operation(summary = "객체별 파일 Path 조회", description = "파일 Path 조회")
@GetMapping("/files")
public ResponseEntity<Resource> getFile(@RequestParam UUID uuid, @RequestParam String pathType)
throws Exception {
String path = datasetService.getFilePathByUUIDPathType(uuid, pathType);
return datasetService.getFilePathByFile(path);
}
@Operation(summary = "객체별 파일 Path 조회", description = "파일 Path 조회")
@GetMapping("/files-to86")
public ResponseEntity<Resource> getFileTo86(
@RequestParam UUID uuid, @RequestParam String pathType) throws Exception {
String path = datasetService.getFilePathByUUIDPathType(uuid, pathType);
return datasetService.getFilePathByFile(path);
}
}

View File

@@ -1,56 +0,0 @@
package com.kamco.cd.training.dataset;
import com.kamco.cd.training.config.api.ApiResponseDto;
import com.kamco.cd.training.dataset.dto.MapSheetDto;
import com.kamco.cd.training.dataset.service.MapSheetService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.media.Content;
import io.swagger.v3.oas.annotations.media.Schema;
import io.swagger.v3.oas.annotations.responses.ApiResponse;
import io.swagger.v3.oas.annotations.responses.ApiResponses;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
import org.springframework.data.domain.Page;
import org.springframework.web.bind.annotation.*;
@Tag(name = "도엽 관리", description = "도엽(MapSheet) 관리 API")
@RestController
@RequiredArgsConstructor
public class MapSheetApiController {
private final MapSheetService mapSheetService;
@Operation(summary = "도엽 목록 조회", description = "데이터셋의 도엽 목록을 조회합니다.")
@ApiResponses(
value = {
@ApiResponse(
responseCode = "200",
description = "조회 성공",
content =
@Content(
mediaType = "application/json",
schema = @Schema(implementation = Page.class))),
@ApiResponse(responseCode = "400", description = "잘못된 검색 조건", content = @Content),
@ApiResponse(responseCode = "500", description = "서버 오류", content = @Content)
})
@PostMapping("/api/datasets/items/search")
public ApiResponseDto<Page<MapSheetDto.Basic>> searchMapSheets(
@RequestBody @Valid MapSheetDto.SearchReq searchReq) {
return ApiResponseDto.ok(mapSheetService.searchMapSheets(searchReq));
}
@Operation(summary = "도엽 삭제", description = "도엽을 삭제합니다 (다건 지원).")
@ApiResponses(
value = {
@ApiResponse(responseCode = "200", description = "삭제 성공", content = @Content),
@ApiResponse(responseCode = "400", description = "잘못된 요청", content = @Content),
@ApiResponse(responseCode = "404", description = "도엽을 찾을 수 없음", content = @Content),
@ApiResponse(responseCode = "500", description = "서버 오류", content = @Content)
})
@PostMapping("/api/datasets/items/delete")
public ApiResponseDto<Void> deleteMapSheets(@RequestBody @Valid MapSheetDto.DeleteReq deleteReq) {
mapSheetService.deleteMapSheets(deleteReq);
return ApiResponseDto.ok(null);
}
}

View File

@@ -1,212 +1,353 @@
package com.kamco.cd.training.dataset.dto;
import com.kamco.cd.training.common.enums.LearnDataRegister;
import com.kamco.cd.training.common.enums.LearnDataType;
import com.kamco.cd.training.common.utils.enums.Enums;
import com.kamco.cd.training.common.utils.interfaces.JsonFormatDttm;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
import jakarta.validation.constraints.Size;
import java.time.ZonedDateTime;
import java.util.List;
import java.util.UUID;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Sort;
public class DatasetDto {
@Schema(name = "Dataset Basic", description = "데이터셋 기본 정보")
@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
public static class Basic {
private Long id;
private UUID uuid;
private String groupTitle;
private String groupTitleCd;
private String title;
private Long roundNo;
private String totalSize;
private String memo;
@JsonFormatDttm private ZonedDateTime createdDttm;
private String status;
private String statusCd;
private Boolean deleted;
public Basic(
Long id,
UUID uuid,
String groupTitle,
String title,
Long roundNo,
Long totalSize,
String memo,
ZonedDateTime createdDttm,
String status,
Boolean deleted) {
this.id = id;
this.uuid = uuid;
this.groupTitle = getGroupTitle(groupTitle);
this.groupTitleCd = groupTitle;
this.title = title;
this.roundNo = roundNo;
this.totalSize = getTotalSize(totalSize);
this.memo = memo;
this.createdDttm = createdDttm;
this.status = getStatus(status);
this.statusCd = status;
this.deleted = deleted;
}
public String getTotalSize(Long totalSize) {
if (totalSize == null) return "0G";
double giga = totalSize / (1024.0 * 1024 * 1024);
return String.format("%.2fG", giga);
}
public String getGroupTitle(String groupTitleCd) {
LearnDataType type = Enums.fromId(LearnDataType.class, groupTitleCd);
return type == null ? null : type.getText();
}
public String getStatus(String status) {
LearnDataRegister type = Enums.fromId(LearnDataRegister.class, status);
return type == null ? null : type.getText();
}
}
@Schema(name = "Dataset Detail", description = "데이터셋 상세 정보")
@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
public static class Detail {
private Long id;
private String groupTitle;
private String title;
private Long roundNo;
private String totalSize;
private String memo;
@JsonFormatDttm private ZonedDateTime createdDttm;
private String status;
private Boolean deleted;
}
@Schema(name = "DatasetSearchReq", description = "데이터셋 목록 조회 요청")
@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
public static class SearchReq {
@Schema(description = "구분", example = "DELIVER(납품), PRODUCTION(제작)")
private String groupTitle;
@Schema(description = "제목 (부분 검색)", example = "1차")
private String title;
@Schema(description = "페이지 번호 (1부터 시작)", example = "1")
private int page = 1;
@Schema(description = "페이지 크기", example = "20")
private int size = 20;
public Pageable toPageable() {
// API에서는 1부터 시작하지만 내부적으로는 0부터 시작
int pageIndex = Math.max(0, page - 1);
return PageRequest.of(pageIndex, size, Sort.by(Sort.Direction.DESC, "createdDttm"));
}
}
@Schema(name = "DatasetDetailReq", description = "데이터셋 상세 조회 요청")
@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
public static class DetailReq {
@NotNull(message = "데이터셋 ID는 필수입니다")
@Schema(description = "데이터셋 ID", example = "101")
private Long datasetId;
}
@Schema(name = "DatasetRegisterReq", description = "데이터셋 등록 요청")
@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
public static class RegisterReq {
@NotBlank(message = "제목은 필수입니다")
@Size(max = 200, message = "제목은 최대 200자까지 입력 가능합니다")
@Schema(description = "제목", example = "1차 제작")
private String title;
@NotBlank(message = "연도는 필수입니다")
@Size(max = 4, message = "연도는 4자리입니다")
@Schema(description = "연도 (YYYY)", example = "2024")
private String year;
@Schema(description = "회차", example = "1")
private Long roundNo;
@Schema(description = "메모", example = "데이터셋 설명")
private String memo;
}
@Schema(name = "DatasetUpdateReq", description = "데이터셋 수정 요청")
@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
public static class UpdateReq {
@Size(max = 200, message = "제목은 최대 200자까지 입력 가능합니다")
@Schema(description = "제목", example = "1차 제작")
private String title;
@Schema(description = "메모", example = "데이터셋 설명")
private String memo;
}
@Schema(name = "DatasetSummaryReq", description = "데이터셋 통계 요약 요청")
@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
public static class SummaryReq {
@NotNull(message = "데이터셋 ID 목록은 필수입니다")
@Schema(description = "데이터셋 ID 목록", example = "[101, 105]")
private List<Long> datasetIds;
}
@Schema(name = "DatasetSummary", description = "데이터셋 통계 요약")
@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
public static class Summary {
@Schema(description = "총 데이터셋 수", example = "2")
private int totalDatasets;
@Schema(description = "총 도엽 수", example = "1500")
private long totalMapSheets;
@Schema(description = "총 파일 크기 (bytes)", example = "10737418240")
private long totalFileSize;
@Schema(description = "평균 도엽 수", example = "750")
private double averageMapSheets;
}
}
package com.kamco.cd.training.dataset.dto;
import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.annotation.JsonInclude;
import com.kamco.cd.training.common.enums.LearnDataRegister;
import com.kamco.cd.training.common.enums.LearnDataType;
import com.kamco.cd.training.common.enums.ModelType;
import com.kamco.cd.training.common.utils.enums.Enums;
import com.kamco.cd.training.common.utils.interfaces.JsonFormatDttm;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
import jakarta.validation.constraints.Size;
import java.time.ZonedDateTime;
import java.util.List;
import java.util.UUID;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import lombok.extern.slf4j.Slf4j;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Sort;
@Slf4j
public class DatasetDto {
@Schema(name = "Dataset Basic", description = "데이터셋 기본 정보")
@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
public static class Basic {
private Long id;
private UUID uuid;
private String title;
private Long roundNo;
private Integer compareYyyy;
private Integer targetYyyy;
private String totalSize;
private String memo;
@JsonFormatDttm private ZonedDateTime createdDttm;
private String status;
private String statusCd;
private Boolean deleted;
private String dataType;
public Basic(
Long id,
UUID uuid,
String title,
Long roundNo,
Integer compareYyyy,
Integer targetYyyy,
Long totalSize,
String memo,
ZonedDateTime createdDttm,
String status,
Boolean deleted,
String dataType) {
this.id = id;
this.uuid = uuid;
this.title = title;
this.roundNo = roundNo;
this.compareYyyy = compareYyyy;
this.targetYyyy = targetYyyy;
this.totalSize = getTotalSize(totalSize);
this.memo = memo;
this.createdDttm = createdDttm;
this.status = getStatus(status);
this.statusCd = status;
this.deleted = deleted;
this.dataType = dataType;
}
public String getTotalSize(Long totalSize) {
if (totalSize == null) return "0G";
double giga = totalSize / (1024.0 * 1024 * 1024);
return String.format("%.2fG", giga);
}
public String getStatus(String status) {
LearnDataRegister type = Enums.fromId(LearnDataRegister.class, status);
return type == null ? null : type.getText();
}
public String getYear() {
return this.compareYyyy + "-" + this.targetYyyy;
}
public String getDataTypeName() {
LearnDataType type = Enums.fromId(LearnDataType.class, this.dataType);
return type == null ? null : type.getText();
}
}
@Schema(name = "Dataset Detail", description = "데이터셋 상세 정보")
@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
public static class Detail {
private Long id;
private String groupTitle;
private String title;
private Long roundNo;
private String totalSize;
private String memo;
@JsonFormatDttm private ZonedDateTime createdDttm;
private String status;
private Boolean deleted;
}
@Schema(name = "DatasetSearchReq", description = "데이터셋 목록 조회 요청")
@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
public static class SearchReq {
@Schema(description = "구분")
private String dataType;
@Schema(description = "제목 (부분 검색)", example = "1차")
private String title;
@Schema(description = "페이지 번호 (1부터 시작)", example = "1")
private int page = 1;
@Schema(description = "페이지 크기", example = "20")
private int size = 20;
public Pageable toPageable() {
return PageRequest.of(page, size, Sort.by(Sort.Direction.DESC, "createdDttm"));
}
}
@Schema(name = "DatasetDetailReq", description = "데이터셋 상세 조회 요청")
@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
public static class DetailReq {
@NotNull(message = "데이터셋 ID는 필수입니다")
@Schema(description = "데이터셋 ID", example = "101")
private Long datasetId;
}
@Schema(name = "DatasetRegisterReq", description = "데이터셋 등록 요청")
@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
public static class RegisterReq {
@NotBlank(message = "제목은 필수입니다")
@Size(max = 200, message = "제목은 최대 200자까지 입력 가능합니다")
@Schema(description = "제목", example = "1차 제작")
private String title;
@Schema(description = "비교연도 (YYYY)", example = "2023")
private Integer compareYear;
@Schema(description = "기준연도 (YYYY)", example = "2024")
private Integer targetYyyy;
@Schema(description = "회차", example = "1")
private Long roundNo;
@Schema(description = "메모", example = "데이터셋 설명")
private String memo;
}
@Schema(name = "DatasetUpdateReq", description = "데이터셋 수정 요청")
@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
public static class UpdateReq {
@Size(max = 200, message = "제목은 최대 200자까지 입력 가능합니다")
@Schema(description = "제목", example = "1차 제작")
private String title;
@Schema(description = "메모", example = "데이터셋 설명")
private String memo;
}
@Schema(name = "DatasetSummaryReq", description = "데이터셋 통계 요약 요청")
@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
public static class SummaryReq {
@NotNull(message = "데이터셋 ID 목록은 필수입니다")
@Schema(description = "데이터셋 ID 목록", example = "[101, 105]")
private List<Long> datasetIds;
}
@Schema(name = "DatasetSummary", description = "데이터셋 통계 요약")
@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
public static class Summary {
@Schema(description = "총 데이터셋 수", example = "2")
private int totalDatasets;
@Schema(description = "총 도엽 수", example = "1500")
private long totalMapSheets;
@Schema(description = "총 파일 크기 (bytes)", example = "10737418240")
private long totalFileSize;
@Schema(description = "평균 도엽 수", example = "750")
private double averageMapSheets;
}
@Schema(name = "SelectDataSet", description = "데이터셋 선택 리스트")
@Getter
@Setter
@NoArgsConstructor
@JsonInclude(JsonInclude.Include.NON_NULL)
public static class SelectDataSet {
private String modelNo; // G1, G2, G3 모델 타입
private Long datasetId;
private UUID uuid;
private String dataType;
private String title;
private Long roundNo;
private Integer compareYyyy;
private Integer targetYyyy;
private String memo;
@JsonIgnore private Long classCount;
private Integer buildingCnt;
private Integer containerCnt;
private String dataTypeName;
private Long wasteCnt;
private Long landCoverCnt;
public SelectDataSet(
String modelNo,
Long datasetId,
UUID uuid,
String dataType,
String title,
Long roundNo,
Integer compareYyyy,
Integer targetYyyy,
String memo,
Long classCount) {
this.datasetId = datasetId;
this.uuid = uuid;
this.dataType = dataType;
this.dataTypeName = getDataTypeName(dataType);
this.title = title;
this.roundNo = roundNo;
this.compareYyyy = compareYyyy;
this.targetYyyy = targetYyyy;
this.memo = memo;
this.classCount = classCount;
if (modelNo.equals(ModelType.G2.getId())) {
this.wasteCnt = classCount;
} else if (modelNo.equals(ModelType.G3.getId())) {
this.landCoverCnt = classCount;
}
}
public SelectDataSet(
String modelNo,
Long datasetId,
UUID uuid,
String dataType,
String title,
Long roundNo,
Integer compareYyyy,
Integer targetYyyy,
String memo,
Integer buildingCnt,
Integer containerCnt) {
this.datasetId = datasetId;
this.uuid = uuid;
this.dataType = dataType;
this.dataTypeName = getDataTypeName(dataType);
this.title = title;
this.roundNo = roundNo;
this.compareYyyy = compareYyyy;
this.targetYyyy = targetYyyy;
this.memo = memo;
this.buildingCnt = buildingCnt;
this.containerCnt = containerCnt;
}
public String getDataTypeName(String groupTitleCd) {
LearnDataType type = Enums.fromId(LearnDataType.class, groupTitleCd);
return type == null ? null : type.getText();
}
public String getYear() {
return this.compareYyyy + "-" + this.targetYyyy;
}
}
@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
public static class DatasetReq {
String modelNo;
String dataType;
UUID uuid;
Long id;
List<Long> ids;
}
@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
public static class AddReq {
private String fileName;
private String filePath;
private Long fileSize;
private String memo;
}
@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
@Builder
public static class DatasetMngRegDto {
private String uid;
private String dataType;
private Integer compareYyyy;
private Integer targetYyyy;
private Long roundNo;
private String title;
private String memo;
private Long totalSize;
private Long totalObjectCount;
private String datasetPath;
}
}

View File

@@ -0,0 +1,163 @@
package com.kamco.cd.training.dataset.dto;
import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.kamco.cd.training.common.enums.DetectionClassification;
import com.kamco.cd.training.common.utils.interfaces.JsonFormatDttm;
import io.swagger.v3.oas.annotations.media.Schema;
import java.time.ZonedDateTime;
import java.util.UUID;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import lombok.extern.slf4j.Slf4j;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Sort;
@Slf4j
public class DatasetObjDto {
@Schema(name = "DatasetObj Basic", description = "데이터셋 객체 Obj 기본 정보")
@Getter
@Setter
@NoArgsConstructor
public static class Basic {
private Long objId;
private Long datasetUid;
private Integer targetYyyy;
private String targetClassCd;
private Integer compareYyyy;
private String compareClassCd;
private String targetPath;
private String comparePath;
private String labelPath;
private String geojsonPath;
private String mapSheetNum;
@JsonFormatDttm private ZonedDateTime createdDttm;
private Long createdUid;
private Boolean deleted;
private UUID uuid;
@JsonIgnore private String geoJsonb;
private JsonNode geoJson;
public Basic(
Long objId,
Long datasetUid,
Integer targetYyyy,
String targetClassCd,
Integer compareYyyy,
String compareClassCd,
String targetPath,
String comparePath,
String labelPath,
String geojsonPath,
String mapSheetNum,
ZonedDateTime createdDttm,
Long createdUid,
Boolean deleted,
UUID uuid,
String geoJsonb) {
this.objId = objId;
this.datasetUid = datasetUid;
this.targetYyyy = targetYyyy;
this.targetClassCd = targetClassCd;
this.compareYyyy = compareYyyy;
this.compareClassCd = compareClassCd;
this.targetPath = targetPath;
this.comparePath = comparePath;
this.labelPath = labelPath;
this.geojsonPath = geojsonPath;
this.mapSheetNum = mapSheetNum;
this.createdDttm = createdDttm;
this.createdUid = createdUid;
this.deleted = deleted;
this.uuid = uuid;
this.geoJsonb = geoJsonb;
JsonNode geoJsonNode = null;
ObjectMapper mapper = new ObjectMapper();
if (geoJsonb != null) {
try {
geoJsonNode = mapper.readTree(geoJsonb);
} catch (JsonProcessingException e) {
throw new RuntimeException(e);
}
}
this.geoJson = geoJsonNode;
}
}
@Schema(name = "DatasetSearchReq", description = "데이터셋 상세 도엽목록 조회 요청")
@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
public static class SearchReq {
@Schema(description = "회차 uuid", example = "e9a6774b-4f81-4402-b080-51d27fac1f01")
private UUID uuid;
@Schema(description = "비교년도분류", example = "waste")
private String compareClassCd;
@Schema(description = "기준년도분류", example = "land")
private String targetClassCd;
@Schema(description = "도엽번호", example = "36713060")
private String mapSheetNum;
@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, Sort.by(Sort.Direction.DESC, "createdDttm"));
}
}
@Getter
@Setter
@AllArgsConstructor
@NoArgsConstructor
public static class DatasetClass {
private String classCd;
public String getClassName() {
return DetectionClassification.valueOf(classCd.toUpperCase()).getDesc();
}
}
@Getter
@Setter
public static class DatasetStorage {
private String usableBytes;
}
@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
@Builder
public static class DatasetObjRegDto {
private Long datasetUid;
private Integer compareYyyy;
private String compareClassCd;
private Integer targetYyyy;
private String targetClassCd;
private String comparePath;
private String targetPath;
private String labelPath;
private String geojsonPath;
private String mapSheetNum;
private JsonNode geojson;
private String fileName;
}
}

View File

@@ -1,103 +1,103 @@
package com.kamco.cd.training.dataset.dto;
import com.kamco.cd.training.common.utils.interfaces.JsonFormatDttm;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotNull;
import java.time.ZonedDateTime;
import java.util.List;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Sort;
public class MapSheetDto {
@Schema(name = "MapSheet Basic", description = "도엽 기본 정보")
@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
public static class Basic {
private Long id;
private Long datasetId;
private String sheetNum;
private String fileName;
private Long fileSize;
private String filePath;
private String status;
private String memo;
private Boolean deleted;
@JsonFormatDttm private ZonedDateTime createdDttm;
@JsonFormatDttm private ZonedDateTime updatedDttm;
}
@Schema(name = "MapSheetSearchReq", description = "도엽 목록 조회 요청")
@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
public static class SearchReq {
@NotNull(message = "데이터셋 ID는 필수입니다")
@Schema(description = "데이터셋 ID", example = "101")
private Long datasetId;
@Schema(description = "페이지 번호 (1부터 시작)", example = "1")
private int page = 1;
@Schema(description = "페이지 크기", example = "20")
private int size = 20;
public Pageable toPageable() {
int pageIndex = Math.max(0, page - 1);
return PageRequest.of(pageIndex, size, Sort.by(Sort.Direction.DESC, "createdDttm"));
}
}
@Schema(name = "MapSheetDeleteReq", description = "도엽 삭제 요청 (다건)")
@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
public static class DeleteReq {
@NotNull(message = "삭제할 도엽 ID 목록은 필수입니다")
@Schema(description = "삭제할 도엽 ID 목록", example = "[9991, 9992]")
private List<Long> itemIds;
}
@Schema(name = "MapSheetCheckReq", description = "도엽 번호 유효성 검증 요청")
@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
public static class CheckReq {
@NotNull(message = "도엽 번호는 필수입니다")
@Schema(description = "도엽 번호", example = "377055")
private String sheetNum;
}
@Schema(name = "MapSheetCheckRes", description = "도엽 번호 유효성 검증 응답")
@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
public static class CheckRes {
@Schema(description = "유효 여부", example = "true")
private boolean valid;
@Schema(description = "메시지", example = "유효한 도엽 번호입니다")
private String message;
@Schema(description = "중복 여부", example = "false")
private boolean duplicate;
}
}
package com.kamco.cd.training.dataset.dto;
import com.kamco.cd.training.common.utils.interfaces.JsonFormatDttm;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotNull;
import java.time.ZonedDateTime;
import java.util.List;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Sort;
public class MapSheetDto {
@Schema(name = "MapSheet Basic", description = "도엽 기본 정보")
@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
public static class Basic {
private Long id;
private Long datasetId;
private String sheetNum;
private String fileName;
private Long fileSize;
private String filePath;
private String status;
private String memo;
private Boolean deleted;
@JsonFormatDttm private ZonedDateTime createdDttm;
@JsonFormatDttm private ZonedDateTime updatedDttm;
}
@Schema(name = "MapSheetSearchReq", description = "도엽 목록 조회 요청")
@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
public static class SearchReq {
@NotNull(message = "데이터셋 ID는 필수입니다")
@Schema(description = "데이터셋 ID", example = "101")
private Long datasetId;
@Schema(description = "페이지 번호 (1부터 시작)", example = "1")
private int page = 1;
@Schema(description = "페이지 크기", example = "20")
private int size = 20;
public Pageable toPageable() {
int pageIndex = Math.max(0, page - 1);
return PageRequest.of(pageIndex, size, Sort.by(Sort.Direction.DESC, "createdDttm"));
}
}
@Schema(name = "MapSheetDeleteReq", description = "도엽 삭제 요청 (다건)")
@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
public static class DeleteReq {
@NotNull(message = "삭제할 도엽 ID 목록은 필수입니다")
@Schema(description = "삭제할 도엽 ID 목록", example = "[9991, 9992]")
private List<Long> itemIds;
}
@Schema(name = "MapSheetCheckReq", description = "도엽 번호 유효성 검증 요청")
@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
public static class CheckReq {
@NotNull(message = "도엽 번호는 필수입니다")
@Schema(description = "도엽 번호", example = "377055")
private String sheetNum;
}
@Schema(name = "MapSheetCheckRes", description = "도엽 번호 유효성 검증 응답")
@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
public static class CheckRes {
@Schema(description = "유효 여부", example = "true")
private boolean valid;
@Schema(description = "메시지", example = "유효한 도엽 번호입니다")
private String message;
@Schema(description = "중복 여부", example = "false")
private boolean duplicate;
}
}

View File

@@ -1,86 +1,524 @@
package com.kamco.cd.training.dataset.service;
import com.kamco.cd.training.dataset.dto.DatasetDto;
import com.kamco.cd.training.postgres.core.DatasetCoreService;
import java.util.UUID;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.data.domain.Page;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@Slf4j
@Service
@RequiredArgsConstructor
@Transactional(readOnly = true)
public class DatasetService {
private final DatasetCoreService datasetCoreService;
/**
* 데이터셋 목록 조회
*
* @param searchReq 검색 조건
* @return 데이터셋 목록
*/
public Page<DatasetDto.Basic> searchDatasets(DatasetDto.SearchReq searchReq) {
log.info("데이터셋 목록 조회 - 조건: {}", searchReq);
return datasetCoreService.findDatasetList(searchReq);
}
/**
* 데이터셋 상세 조회
*
* @param id 상세 조회할 목록 Id
* @return 데이터셋 상세 정보
*/
public DatasetDto.Basic getDatasetDetail(UUID id) {
return datasetCoreService.getOneByUuid(id);
}
/**
* 데이터셋 등록
*
* @param registerReq 등록 요청
* @return 등록된 데이터셋 ID
*/
@Transactional
public Long registerDataset(DatasetDto.RegisterReq registerReq) {
log.info("데이터셋 등록 - 요청: {}", registerReq);
DatasetDto.Basic saved = datasetCoreService.save(registerReq);
log.info("데이터셋 등록 완료 - ID: {}", saved.getId());
return saved.getId();
}
/**
* 데이터셋 수정
*
* @param updateReq 수정 요청
* @return 수정된 데이터셋 ID
*/
@Transactional
public void updateDataset(UUID uuid, DatasetDto.UpdateReq updateReq) {
datasetCoreService.update(uuid, updateReq);
}
/**
* 데이터셋 삭제 (다건)
*
* @param uuid 삭제 요청
*/
@Transactional
public void deleteDatasets(UUID uuid) {
datasetCoreService.deleteDatasets(uuid);
}
/**
* 데이터셋 통계 요약
*
* @param summaryReq 요약 요청
* @return 통계 요약
*/
public DatasetDto.Summary getDatasetSummary(DatasetDto.SummaryReq summaryReq) {
log.info("데이터셋 통계 요약 - 요청: {}", summaryReq);
return datasetCoreService.getDatasetSummary(summaryReq);
}
}
package com.kamco.cd.training.dataset.service;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.node.ArrayNode;
import com.fasterxml.jackson.databind.node.ObjectNode;
import com.kamco.cd.training.common.enums.LearnDataType;
import com.kamco.cd.training.common.exception.CustomApiException;
import com.kamco.cd.training.common.service.FormatStorage;
import com.kamco.cd.training.common.utils.FIleChecker;
import com.kamco.cd.training.config.api.ApiResponseDto.ApiResponseCode;
import com.kamco.cd.training.config.api.ApiResponseDto.ResponseObj;
import com.kamco.cd.training.dataset.dto.DatasetDto;
import com.kamco.cd.training.dataset.dto.DatasetDto.AddReq;
import com.kamco.cd.training.dataset.dto.DatasetDto.DatasetMngRegDto;
import com.kamco.cd.training.dataset.dto.DatasetObjDto;
import com.kamco.cd.training.dataset.dto.DatasetObjDto.DatasetClass;
import com.kamco.cd.training.dataset.dto.DatasetObjDto.DatasetObjRegDto;
import com.kamco.cd.training.dataset.dto.DatasetObjDto.DatasetStorage;
import com.kamco.cd.training.dataset.dto.DatasetObjDto.SearchReq;
import com.kamco.cd.training.postgres.core.DatasetCoreService;
import jakarta.validation.Valid;
import java.io.IOException;
import java.io.InputStream;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.UUID;
import java.util.stream.Stream;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.core.io.InputStreamResource;
import org.springframework.core.io.Resource;
import org.springframework.data.domain.Page;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@Slf4j
@Service
@RequiredArgsConstructor
@Transactional
public class DatasetService {
private final DatasetCoreService datasetCoreService;
@Value("${file.dataset-dir}")
private String datasetDir;
private static final List<String> LABEL_DIRS = List.of("label-json", "label", "input1", "input2");
/**
* 데이터셋 목록 조회
*
* @param searchReq 검색 조건
* @return 데이터셋 목록
*/
public Page<DatasetDto.Basic> searchDatasets(DatasetDto.SearchReq searchReq) {
log.info("데이터셋 목록 조회 - 조건: {}", searchReq);
return datasetCoreService.findDatasetList(searchReq);
}
/**
* 데이터셋 상세 조회
*
* @param id 상세 조회할 목록 Id
* @return 데이터셋 상세 정보
*/
public DatasetDto.Basic getDatasetDetail(UUID id) {
return datasetCoreService.getOneByUuid(id);
}
/**
* 데이터셋 등록
*
* @param registerReq 등록 요청
* @return 등록된 데이터셋 ID
*/
@Transactional
public Long registerDataset(DatasetDto.RegisterReq registerReq) {
log.info("데이터셋 등록 - 요청: {}", registerReq);
DatasetDto.Basic saved = datasetCoreService.save(registerReq);
log.info("데이터셋 등록 완료 - ID: {}", saved.getId());
return saved.getId();
}
/**
* 데이터셋 수정
*
* @param updateReq 수정 요청
* @return 수정된 데이터셋 ID
*/
@Transactional
public void updateDataset(UUID uuid, DatasetDto.UpdateReq updateReq) {
datasetCoreService.update(uuid, updateReq);
}
/**
* 데이터셋 삭제 (다건)
*
* @param uuid 삭제 요청
*/
@Transactional
public void deleteDatasets(UUID uuid) {
datasetCoreService.deleteDatasets(uuid);
}
/**
* 데이터셋 통계 요약
*
* @param summaryReq 요약 요청
* @return 통계 요약
*/
public DatasetDto.Summary getDatasetSummary(DatasetDto.SummaryReq summaryReq) {
log.info("데이터셋 통계 요약 - 요청: {}", summaryReq);
return datasetCoreService.getDatasetSummary(summaryReq);
}
public Page<DatasetObjDto.Basic> searchDatasetObjectList(SearchReq searchReq) {
return datasetCoreService.searchDatasetObjectList(searchReq);
}
public UUID deleteDatasetObjByUuid(UUID uuid) {
return datasetCoreService.deleteDatasetObjByUuid(uuid);
}
/**
* 데이터셋 object class 조회
*
* @param uuid dataset uuid
* @param type compare, target
*/
public List<DatasetClass> getDatasetObjByUuid(UUID uuid, String type) {
return datasetCoreService.findDatasetObjClassByUuid(uuid, type);
}
/**
* 사용 가능 공간 조회
*
* @return
*/
public DatasetStorage getUsableBytes() {
// 현재 실행 위치가 속한 디스크 기준
try {
FormatStorage.DiskUsage usage = FormatStorage.getDiskUsage(Path.of("."));
log.debug("경로 : {}", usage.path());
log.debug("총 저장공간 : {}", usage.totalText());
log.debug("남은 저장공간 : {}", usage.usableText());
log.debug("사용률 : {}", usage.usedPercent());
DatasetStorage datasetStorage = new DatasetStorage();
datasetStorage.setUsableBytes(usage.usableText());
return datasetStorage;
} catch (Exception e) {
throw new CustomApiException("NOT_FOUND", HttpStatus.NOT_FOUND);
}
}
@Deprecated
@Transactional
public ResponseObj insertDatasetTo86(@Valid AddReq addReq) {
Long datasetUid = null; // master id 값, 등록하면서 가져올 예정
// 압축 해제
FIleChecker.unzipOn86Server(
addReq.getFilePath() + addReq.getFileName(),
addReq.getFilePath() + addReq.getFileName().replace(".zip", ""));
// 해제한 폴더 읽어서 데이터 저장
List<Map<String, Object>> list =
getUnzipDatasetFilesTo86(
addReq.getFilePath() + addReq.getFileName().replace(".zip", ""), "train");
int idx = 0;
for (Map<String, Object> map : list) {
datasetUid =
this.insertTrainTestData(map, addReq, idx, datasetUid, "train"); // train 데이터 insert
idx++;
}
List<Map<String, Object>> testList =
getUnzipDatasetFilesTo86(
addReq.getFilePath() + addReq.getFileName().replace(".zip", ""), "test");
int testIdx = 0;
for (Map<String, Object> test : testList) {
datasetUid =
this.insertTrainTestData(test, addReq, testIdx, datasetUid, "test"); // test 데이터 insert
testIdx++;
}
datasetCoreService.updateDatasetUploadStatus(datasetUid);
return new ResponseObj(ApiResponseCode.OK, "업로드 성공하였습니다.");
}
@Transactional
public ResponseObj insertDataset(@Valid AddReq addReq) {
Long datasetUid = null; // master id 값, 등록하면서 가져올 예정
try {
// 압축 해제
FIleChecker.unzip(addReq.getFileName(), addReq.getFilePath());
// 해제한 폴더 읽어서 데이터 저장
List<Map<String, Object>> list =
getUnzipDatasetFiles(
addReq.getFilePath() + addReq.getFileName().replace(".zip", ""), "train");
int idx = 0;
for (Map<String, Object> map : list) {
datasetUid =
this.insertTrainTestData(map, addReq, idx, datasetUid, "train"); // train 데이터 insert
idx++;
}
List<Map<String, Object>> valList =
getUnzipDatasetFiles(
addReq.getFilePath() + addReq.getFileName().replace(".zip", ""), "val");
int valIdx = 0;
for (Map<String, Object> valid : valList) {
datasetUid =
this.insertTrainTestData(valid, addReq, valIdx, datasetUid, "val"); // val 데이터 insert
valIdx++;
}
List<Map<String, Object>> testList =
getUnzipDatasetFiles(
addReq.getFilePath() + addReq.getFileName().replace(".zip", ""), "test");
int testIdx = 0;
for (Map<String, Object> test : testList) {
datasetUid =
this.insertTrainTestData(test, addReq, testIdx, datasetUid, "test"); // test 데이터 insert
testIdx++;
}
} catch (IOException e) {
log.error(e.getMessage());
return new ResponseObj(ApiResponseCode.INTERNAL_SERVER_ERROR, e.getMessage());
}
datasetCoreService.updateDatasetUploadStatus(datasetUid);
return new ResponseObj(ApiResponseCode.OK, "업로드 성공하였습니다.");
}
@Transactional
public Long insertTrainTestData(
Map<String, Object> map, AddReq addReq, int idx, Long datasetUid, String subDir) {
ObjectMapper mapper = new ObjectMapper();
String comparePath = (String) map.get("input1");
String targetPath = (String) map.get("input2");
String labelPath = (String) map.get("label");
String geojsonPath = (String) map.get("geojson_path");
Object labelJson = map.get("label-json");
JsonNode json;
if (labelJson instanceof JsonNode jn) {
json = jn;
} else {
try {
json = mapper.readTree(labelJson.toString());
} catch (IOException e) {
throw new RuntimeException("label_json parse error", e);
}
}
String fileName = Paths.get(comparePath).getFileName().toString();
String[] fileNameStr = fileName.split("_");
String compareYyyy = fileNameStr[1];
String targetYyyy = fileNameStr[2];
String mapSheetNum = fileNameStr[3];
if (idx == 0 && subDir.equals("train")) {
String title = compareYyyy + "-" + targetYyyy; // 제목 : 비교년도-기준년도
String dataType = LearnDataType.PRODUCTION.getId(); // 만들어 넣는 건 다 제작
Long stage =
datasetCoreService.getDatasetMaxStage(
Integer.parseInt(compareYyyy), Integer.parseInt(targetYyyy))
+ 1;
String uid = addReq.getFileName().replace(".zip", "");
DatasetMngRegDto mngRegDto =
DatasetMngRegDto.builder()
.uid(uid)
.dataType(dataType)
.compareYyyy(Integer.parseInt(compareYyyy))
.targetYyyy(Integer.parseInt(targetYyyy))
.title(title)
.memo(addReq.getMemo())
.roundNo(stage)
.totalSize(addReq.getFileSize())
.datasetPath(addReq.getFilePath())
.build();
datasetUid = datasetCoreService.insertDatasetMngData(mngRegDto); // tb_dataset 에 insert
}
// datasetUid 로 obj 도 등록하기
// Json 갯수만큼 for문 돌려서 insert 해야 함, features에 빈값
if (json != null && json.path("features") != null && !json.path("features").isEmpty()) {
for (JsonNode feature : json.path("features")) {
JsonNode prop = feature.path("properties");
String compareClassCd = prop.path("before").asText(null);
String targetClassCd = prop.path("after").asText(null);
// 한 개씩 자른 geojson을 FeatureCollection 으로 만들어서 넣기
ObjectNode root = mapper.createObjectNode();
root.put("type", "FeatureCollection");
ArrayNode features = mapper.createArrayNode();
features.add(feature);
root.set("features", features);
DatasetObjRegDto objRegDto =
DatasetObjRegDto.builder()
.datasetUid(datasetUid)
.compareYyyy(Integer.parseInt(compareYyyy))
.compareClassCd(compareClassCd)
.targetYyyy(Integer.parseInt(targetYyyy))
.targetClassCd(targetClassCd)
.comparePath(comparePath)
.targetPath(targetPath)
.labelPath(labelPath)
.mapSheetNum(mapSheetNum)
.geojson(root)
.geojsonPath(geojsonPath)
.fileName(fileName)
.build();
if (subDir.equals("train")) {
datasetCoreService.insertDatasetObj(objRegDto);
} else if (subDir.equals("val")) {
datasetCoreService.insertDatasetValObj(objRegDto);
} else {
datasetCoreService.insertDatasetTestObj(objRegDto);
}
}
}
return datasetUid;
}
private List<Map<String, Object>> getUnzipDatasetFiles(String unzipRootPath, String subDir) {
Path root = Paths.get(unzipRootPath).resolve(subDir);
Map<String, Map<String, Object>> grouped = new HashMap<>();
for (String dirName : LABEL_DIRS) {
Path dir = root.resolve(dirName);
if (!Files.isDirectory(dir)) {
throw new IllegalStateException("폴더가 존재하지 않습니다 : " + dir);
}
try (Stream<Path> stream = Files.list(dir)) {
stream
.filter(Files::isRegularFile)
.forEach(
path -> {
String fileName = path.getFileName().toString();
String baseName = getBaseName(fileName);
// baseName 기준 Map 생성
Map<String, Object> data =
grouped.computeIfAbsent(baseName, k -> new HashMap<>());
// 공통 메타
data.put("baseName", baseName);
// 폴더별 처리
if ("label-json".equals(dirName)) {
// json 파일이면 파싱
data.put("label-json", readJson(path));
data.put("geojson_path", path.toAbsolutePath().toString());
} else {
data.put(dirName, path.toAbsolutePath().toString());
}
});
} catch (IOException e) {
throw new RuntimeException(e);
}
}
return new ArrayList<>(grouped.values());
}
private String getBaseName(String fileName) {
int idx = fileName.lastIndexOf('.');
return (idx > 0) ? fileName.substring(0, idx) : fileName;
}
private JsonNode readJson(Path jsonPath) {
try {
ObjectMapper mapper = new ObjectMapper();
return mapper.readTree(jsonPath.toFile());
} catch (IOException e) {
throw new RuntimeException("JSON 읽기 실패: " + jsonPath, e);
}
}
public String getFilePathByUUIDPathType(UUID uuid, String pathType) {
return datasetCoreService.getFilePathByUUIDPathType(uuid, pathType);
}
@Deprecated
private List<Map<String, Object>> getUnzipDatasetFilesTo86(String unzipRootPath, String subDir) {
// String root = Paths.get(unzipRootPath)
// .resolve(subDir)
// .toString();
//
String root = normalizeLinuxPath(unzipRootPath + "/" + subDir);
Map<String, Map<String, Object>> grouped = new HashMap<>();
for (String dirName : LABEL_DIRS) {
String remoteDir = root + "/" + dirName;
// 1. 86 서버에서 해당 디렉토리의 파일 목록 조회
List<String> files = listFilesOn86Server(remoteDir);
if (files.isEmpty()) {
throw new IllegalStateException("폴더가 존재하지 않거나 파일이 없습니다 : " + remoteDir);
}
for (String fullPath : files) {
String fileName = Paths.get(fullPath).getFileName().toString();
String baseName = getBaseName(fileName);
Map<String, Object> data = grouped.computeIfAbsent(baseName, k -> new HashMap<>());
data.put("baseName", baseName);
if ("label-json".equals(dirName)) {
// 2. json 내용도 86 서버에서 읽어서 가져와야 함
String json = readRemoteFileAsString(fullPath);
data.put("label-json", parseJson(json));
data.put("geojson_path", fullPath);
} else {
data.put(dirName, fullPath);
}
}
}
return new ArrayList<>(grouped.values());
}
private List<String> listFilesOn86Server(String remoteDir) {
String command = "find " + escape(remoteDir) + " -maxdepth 1 -type f";
return FIleChecker.execCommandAndReadLines(command);
}
private String readRemoteFileAsString(String remoteFilePath) {
String command = "cat " + escape(remoteFilePath);
List<String> lines = FIleChecker.execCommandAndReadLines(command);
return String.join("\n", lines);
}
private JsonNode parseJson(String json) {
try {
ObjectMapper mapper = new ObjectMapper();
return mapper.readTree(json);
} catch (IOException e) {
throw new RuntimeException("JSON 파싱 실패", e);
}
}
private String escape(String path) {
return "'" + path.replace("'", "'\"'\"'") + "'";
}
private static String normalizeLinuxPath(String path) {
return path.replace("\\", "/");
}
public ResponseEntity<Resource> getFilePathByFile(String remoteFilePath) {
try {
Path path = Paths.get(remoteFilePath);
InputStream inputStream = Files.newInputStream(path);
InputStreamResource resource =
new InputStreamResource(inputStream) {
@Override
public long contentLength() {
return -1; // 알 수 없으면 -1
}
};
String fileName = Paths.get(remoteFilePath.replace("\\", "/")).getFileName().toString();
return ResponseEntity.ok()
.header(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=\"" + fileName + "\"")
.contentType(MediaType.APPLICATION_OCTET_STREAM)
.body(resource);
} catch (Exception e) {
throw new RuntimeException(e);
}
}
}

View File

@@ -1,41 +1,41 @@
package com.kamco.cd.training.dataset.service;
import com.kamco.cd.training.dataset.dto.MapSheetDto;
import com.kamco.cd.training.postgres.core.MapSheetCoreService;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.data.domain.Page;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@Slf4j
@Service
@RequiredArgsConstructor
@Transactional(readOnly = true)
public class MapSheetService {
private final MapSheetCoreService mapSheetCoreService;
/**
* 도엽 목록 조회
*
* @param searchReq 검색 조건
* @return 도엽 목록
*/
public Page<MapSheetDto.Basic> searchMapSheets(MapSheetDto.SearchReq searchReq) {
log.info("도엽 목록 조회 - 조건: {}", searchReq);
return mapSheetCoreService.findMapSheetList(searchReq);
}
/**
* 도엽 삭제 (다건)
*
* @param deleteReq 삭제 요청
*/
@Transactional
public void deleteMapSheets(MapSheetDto.DeleteReq deleteReq) {
log.info("도엽 삭제 - 요청: {}", deleteReq);
mapSheetCoreService.deleteMapSheets(deleteReq);
log.info("도엽 삭제 완료 - 개수: {}", deleteReq.getItemIds().size());
}
}
package com.kamco.cd.training.dataset.service;
import com.kamco.cd.training.dataset.dto.MapSheetDto;
import com.kamco.cd.training.postgres.core.MapSheetCoreService;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.data.domain.Page;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@Slf4j
@Service
@RequiredArgsConstructor
@Transactional(readOnly = true)
public class MapSheetService {
private final MapSheetCoreService mapSheetCoreService;
/**
* 도엽 목록 조회
*
* @param searchReq 검색 조건
* @return 도엽 목록
*/
public Page<MapSheetDto.Basic> searchMapSheets(MapSheetDto.SearchReq searchReq) {
log.info("도엽 목록 조회 - 조건: {}", searchReq);
return mapSheetCoreService.findMapSheetList(searchReq);
}
/**
* 도엽 삭제 (다건)
*
* @param deleteReq 삭제 요청
*/
@Transactional
public void deleteMapSheets(MapSheetDto.DeleteReq deleteReq) {
log.info("도엽 삭제 - 요청: {}", deleteReq);
mapSheetCoreService.deleteMapSheets(deleteReq);
log.info("도엽 삭제 완료 - 개수: {}", deleteReq.getItemIds().size());
}
}

View File

@@ -0,0 +1,191 @@
package com.kamco.cd.training.hyperparam;
import com.kamco.cd.training.common.dto.HyperParam;
import com.kamco.cd.training.common.enums.ModelType;
import com.kamco.cd.training.config.api.ApiResponseDto;
import com.kamco.cd.training.hyperparam.dto.HyperParamDto;
import com.kamco.cd.training.hyperparam.dto.HyperParamDto.List;
import com.kamco.cd.training.hyperparam.service.HyperParamService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.media.Content;
import io.swagger.v3.oas.annotations.media.Schema;
import io.swagger.v3.oas.annotations.responses.ApiResponse;
import io.swagger.v3.oas.annotations.responses.ApiResponses;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.validation.Valid;
import java.time.LocalDate;
import java.util.UUID;
import lombok.RequiredArgsConstructor;
import org.springframework.data.domain.Page;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.PutMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
@Tag(name = "하이퍼파라미터 관리", description = "하이퍼파라미터 관리 API")
@RestController
@RequiredArgsConstructor
@RequestMapping("/api/hyper-param")
public class HyperParamApiController {
private final HyperParamService hyperParamService;
@Operation(summary = "하이퍼파라미터 등록", description = "파라미터를 신규 저장")
@ApiResponses(
value = {
@ApiResponse(
responseCode = "200",
description = "등록 성공",
content =
@Content(
mediaType = "application/json",
schema = @Schema(implementation = String.class))),
@ApiResponse(responseCode = "400", description = "잘못된 요청", content = @Content),
@ApiResponse(responseCode = "500", description = "서버 오류", content = @Content)
})
@PostMapping
public ApiResponseDto<String> createHyperParam(@Valid @RequestBody HyperParam createReq) {
String newVersion = hyperParamService.createHyperParam(createReq);
return ApiResponseDto.ok(newVersion);
}
@Operation(summary = "하이퍼파라미터 수정", description = "파라미터를 수정")
@ApiResponses(
value = {
@ApiResponse(
responseCode = "200",
description = "등록 성공",
content =
@Content(
mediaType = "application/json",
schema = @Schema(implementation = String.class))),
@ApiResponse(responseCode = "400", description = "잘못된 요청", content = @Content),
@ApiResponse(responseCode = "422", description = "default는 삭제불가", content = @Content),
@ApiResponse(responseCode = "500", description = "서버 오류", content = @Content)
})
@PutMapping("/{uuid}")
public ApiResponseDto<String> updateHyperParam(
@PathVariable UUID uuid, @Valid @RequestBody HyperParam createReq) {
return ApiResponseDto.ok(hyperParamService.updateHyperParam(uuid, createReq));
}
@Operation(summary = "하이퍼파라미터 목록 조회", description = "하이퍼파라미터 목록 조회")
@ApiResponses(
value = {
@ApiResponse(
responseCode = "200",
description = "조회 성공",
content =
@Content(
mediaType = "application/json",
schema = @Schema(implementation = Page.class))),
@ApiResponse(responseCode = "404", description = "하이퍼파라미터를 찾을 수 없음", content = @Content),
@ApiResponse(responseCode = "500", description = "서버 오류", content = @Content)
})
@GetMapping("{model}/list")
public ApiResponseDto<Page<List>> getHyperParam(
@PathVariable ModelType model,
@Parameter(
description = "구분 CREATE_DATE(생성일), LAST_USED_DATE(최근사용일)",
example = "CREATE_DATE")
@RequestParam(required = false)
String type,
@Parameter(description = "시작일", example = "2026-02-01") @RequestParam(required = false)
LocalDate startDate,
@Parameter(description = "종료일", example = "2026-02-28") @RequestParam(required = false)
LocalDate endDate,
@Parameter(description = "버전명", example = "G_000001") @RequestParam(required = false)
String hyperVer,
@Parameter(
description = "정렬",
example = "createdDttm desc",
schema =
@Schema(
allowableValues = {
"createdDttm,desc",
"lastUsedDttm,desc",
"totalUseCnt,desc"
}))
@RequestParam(required = false)
String sort,
@Parameter(description = "페이지 번호 (0부터 시작)", example = "0") @RequestParam(defaultValue = "0")
int page,
@Parameter(description = "페이지 크기", example = "20") @RequestParam(defaultValue = "20")
int size) {
HyperParamDto.SearchReq searchReq = new HyperParamDto.SearchReq();
searchReq.setType(type);
searchReq.setStartDate(startDate);
searchReq.setEndDate(endDate);
searchReq.setHyperVer(hyperVer);
searchReq.setSort(sort);
searchReq.setPage(page);
searchReq.setSize(size);
Page<List> list = hyperParamService.getHyperParamList(model, searchReq);
return ApiResponseDto.ok(list);
}
@Operation(summary = "하이퍼파라미터 삭제", description = "하이퍼파라미터 삭제")
@ApiResponses(
value = {
@ApiResponse(responseCode = "200", description = "삭제 성공", content = @Content),
@ApiResponse(responseCode = "422", description = "default 삭제 불가", content = @Content),
@ApiResponse(responseCode = "404", description = "하이퍼파라미터를 찾을 수 없음", content = @Content),
})
@DeleteMapping("/{uuid}")
public ApiResponseDto<Void> deleteHyperParam(
@Parameter(description = "하이퍼파라미터 uuid", example = "c3b5a285-8f68-42af-84f0-e6d09162deb5")
@PathVariable
UUID uuid) {
hyperParamService.deleteHyperParam(uuid);
return ApiResponseDto.ok(null);
}
@Operation(summary = "하이퍼파라미터 상세 조회", description = "특정 버전의 하이퍼파라미터 상세 정보를 조회합니다")
@ApiResponses(
value = {
@ApiResponse(
responseCode = "200",
description = "조회 성공",
content =
@Content(
mediaType = "application/json",
schema = @Schema(implementation = HyperParamDto.Basic.class))),
@ApiResponse(responseCode = "404", description = "하이퍼파라미터를 찾을 수 없음", content = @Content),
@ApiResponse(responseCode = "500", description = "서버 오류", content = @Content)
})
@GetMapping("/{uuid}")
public ApiResponseDto<HyperParamDto.Basic> getHyperParam(
@Parameter(description = "하이퍼파라미터 uuid", example = "c3b5a285-8f68-42af-84f0-e6d09162deb5")
@PathVariable
UUID uuid) {
return ApiResponseDto.ok(hyperParamService.getHyperParam(uuid));
}
@Operation(summary = "하이퍼파라미터 최적화 값 조회", description = "하이퍼파라미터 최적화 값 조회 API")
@ApiResponses(
value = {
@ApiResponse(
responseCode = "200",
description = "조회 성공",
content =
@Content(
mediaType = "application/json",
schema = @Schema(implementation = Page.class))),
@ApiResponse(responseCode = "404", description = "하이퍼파라미터를 찾을 수 없음", content = @Content),
@ApiResponse(responseCode = "500", description = "서버 오류", content = @Content)
})
@GetMapping("/init/{model}")
public ApiResponseDto<HyperParamDto.Basic> getInitHyperParam(
@PathVariable ModelType model
) {
return ApiResponseDto.ok(hyperParamService.getInitHyperParam(model));
}
}

View File

@@ -0,0 +1,168 @@
package com.kamco.cd.training.hyperparam.dto;
import com.kamco.cd.training.common.enums.ModelType;
import com.kamco.cd.training.common.utils.enums.CodeExpose;
import com.kamco.cd.training.common.utils.enums.EnumType;
import com.kamco.cd.training.common.utils.interfaces.JsonFormatDttm;
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.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 HyperParamDto {
@Schema(name = "Basic", description = "하이퍼파라미터 조회")
@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
public static class Basic {
private ModelType model; // 20250212 modeltype추가
private UUID uuid;
private String hyperVer;
@JsonFormatDttm private ZonedDateTime createdDttm;
// -------------------------
// Important
// -------------------------
private String backbone;
private String inputSize;
private String cropSize;
private Integer batchSize;
// -------------------------
// Data
// -------------------------
private Integer trainNumWorkers;
private Integer valNumWorkers;
private Integer testNumWorkers;
private Boolean trainShuffle;
private Boolean trainPersistent;
private Boolean valPersistent;
// -------------------------
// Model Architecture
// -------------------------
private Double dropPathRate;
private Integer frozenStages;
private String neckPolicy;
private String decoderChannels;
private String classWeight;
// -------------------------
// Loss & Optimization
// -------------------------
private Double learningRate;
private Double weightDecay;
private Double layerDecayRate;
private Boolean ddpFindUnusedParams;
private Integer ignoreIndex;
private Integer numLayers;
// -------------------------
// Evaluation
// -------------------------
private String metrics;
private String saveBest;
private String saveBestRule;
private Integer valInterval;
private Integer logInterval;
private Integer visInterval;
// -------------------------
// Augmentation
// -------------------------
private Double rotProb;
private String rotDegree;
private Double flipProb;
private Double exchangeProb;
private Integer brightnessDelta;
private String contrastRange;
private String saturationRange;
private Integer hueDelta;
// -------------------------
// Memo
// -------------------------
private String memo;
// -------------------------
// Hardware
// -------------------------
private Integer gpuCnt;
private String gpuIds;
private Integer masterPort;
private Boolean isDefault;
}
@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
public static class List {
private UUID uuid;
private String hyperVer;
@JsonFormatDttm private ZonedDateTime createDttm;
@JsonFormatDttm private ZonedDateTime lastUsedDttm;
private Long m1UseCnt;
private Long m2UseCnt;
private Long m3UseCnt;
private Long totalCnt;
}
@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
public static class SearchReq {
private String type;
private LocalDate startDate;
private LocalDate endDate;
private String hyperVer;
// 페이징 파라미터
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);
}
}
@CodeExpose
@Getter
@AllArgsConstructor
public enum HyperType implements EnumType {
CREATE_DATE("생성일"),
LAST_USED_DATE("최근 사용일");
private final String desc;
@Override
public String getId() {
return name();
}
@Override
public String getText() {
return desc;
}
}
}

View File

@@ -0,0 +1,78 @@
package com.kamco.cd.training.hyperparam.service;
import com.kamco.cd.training.common.dto.HyperParam;
import com.kamco.cd.training.common.enums.ModelType;
import com.kamco.cd.training.hyperparam.dto.HyperParamDto;
import com.kamco.cd.training.hyperparam.dto.HyperParamDto.List;
import com.kamco.cd.training.hyperparam.dto.HyperParamDto.SearchReq;
import com.kamco.cd.training.postgres.core.HyperParamCoreService;
import java.util.UUID;
import lombok.RequiredArgsConstructor;
import org.springframework.data.domain.Page;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@Service
@Transactional(readOnly = true)
@RequiredArgsConstructor
public class HyperParamService {
private final HyperParamCoreService hyperParamCoreService;
/**
* 하이퍼 파라미터 목록 조회
*
* @param model
* @param req
* @return 목록
*/
public Page<List> getHyperParamList(ModelType model, SearchReq req) {
return hyperParamCoreService.findByHyperVerList(model, req);
}
/**
* 하이퍼파라미터 등록
*
* @param createReq 등록 요청
* @return 생성된 버전명
*/
@Transactional
public String createHyperParam(HyperParam createReq) {
return hyperParamCoreService.createHyperParam(createReq).getHyperVer();
}
/**
* 하이퍼파라미터 수정
*
* @param createReq
* @return
*/
@Transactional
public String updateHyperParam(UUID uuid, HyperParam createReq) {
return hyperParamCoreService.updateHyperParam(uuid, createReq);
}
/**
* 하이퍼파라미터 삭제
*
* @param uuid
*/
public void deleteHyperParam(UUID uuid) {
hyperParamCoreService.deleteHyperParam(uuid);
}
/** 하이퍼파라미터 최적화 설정값 조회 */
public HyperParamDto.Basic getInitHyperParam(ModelType model) {
return hyperParamCoreService.getInitHyperParam(model);
}
/**
* 하이퍼파라미터 상세 조회
*
* @param uuid
* @return
*/
public HyperParamDto.Basic getHyperParam(UUID uuid) {
return hyperParamCoreService.getHyperParam(uuid);
}
}

View File

@@ -1,229 +1,229 @@
package com.kamco.cd.training.log.dto;
import com.fasterxml.jackson.annotation.JsonIgnore;
import com.kamco.cd.training.common.utils.interfaces.JsonFormatDttm;
import io.swagger.v3.oas.annotations.media.Schema;
import java.time.ZonedDateTime;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Sort;
public class AuditLogDto {
@Schema(name = "AuditLogBasic", description = "감사로그 기본 정보")
@Getter
public static class Basic {
@JsonIgnore private final Long id;
private final Long userUid;
private final EventType eventType;
private final EventStatus eventStatus;
private final String menuUid;
private final String ipAddress;
private final String requestUri;
private final String requestBody;
private final Long errorLogUid;
@JsonFormatDttm private final ZonedDateTime createdDttm;
public Basic(
Long id,
Long userUid,
EventType eventType,
EventStatus eventStatus,
String menuUid,
String ipAddress,
String requestUri,
String requestBody,
Long errorLogUid,
ZonedDateTime createdDttm) {
this.id = id;
this.userUid = userUid;
this.eventType = eventType;
this.eventStatus = eventStatus;
this.menuUid = menuUid;
this.ipAddress = ipAddress;
this.requestUri = requestUri;
this.requestBody = requestBody;
this.errorLogUid = errorLogUid;
this.createdDttm = createdDttm;
}
}
@Schema(name = "AuditCommon", description = "목록 공통")
@Getter
@AllArgsConstructor
public static class AuditCommon {
private int readCount;
private int cudCount;
private int printCount;
private int downloadCount;
private Long totalCount;
}
@Schema(name = "DailyAuditList", description = "일자별 목록")
@Getter
public static class DailyAuditList extends AuditCommon {
private final String baseDate;
public DailyAuditList(
int readCount,
int cudCount,
int printCount,
int downloadCount,
Long totalCount,
String baseDate) {
super(readCount, cudCount, printCount, downloadCount, totalCount);
this.baseDate = baseDate;
}
}
@Schema(name = "MenuAuditList", description = "메뉴별 목록")
@Getter
public static class MenuAuditList extends AuditCommon {
private final String menuId;
private final String menuName;
public MenuAuditList(
String menuId,
String menuName,
int readCount,
int cudCount,
int printCount,
int downloadCount,
Long totalCount) {
super(readCount, cudCount, printCount, downloadCount, totalCount);
this.menuId = menuId;
this.menuName = menuName;
}
}
@Schema(name = "UserAuditList", description = "사용자별 목록")
@Getter
public static class UserAuditList extends AuditCommon {
private final Long accountId;
private final String loginId;
private final String username;
public UserAuditList(
Long accountId,
String loginId,
String username,
int readCount,
int cudCount,
int printCount,
int downloadCount,
Long totalCount) {
super(readCount, cudCount, printCount, downloadCount, totalCount);
this.accountId = accountId;
this.loginId = loginId;
this.username = username;
}
}
@Schema(name = "AuditDetail", description = "감사 로그 상세 공통")
@Getter
@AllArgsConstructor
public static class AuditDetail {
private Long logId;
private EventType eventType;
private LogDetail detail;
}
@Schema(name = "DailyDetail", description = "일자별 로그 상세")
@Getter
public static class DailyDetail extends AuditDetail {
private final String userName;
private final String loginId;
private final String menuName;
public DailyDetail(
Long logId,
String userName,
String loginId,
String menuName,
EventType eventType,
LogDetail detail) {
super(logId, eventType, detail);
this.userName = userName;
this.loginId = loginId;
this.menuName = menuName;
}
}
@Schema(name = "MenuDetail", description = "메뉴별 로그 상세")
@Getter
public static class MenuDetail extends AuditDetail {
private final String logDateTime;
private final String userName;
private final String loginId;
public MenuDetail(
Long logId,
String logDateTime,
String userName,
String loginId,
EventType eventType,
LogDetail detail) {
super(logId, eventType, detail);
this.logDateTime = logDateTime;
this.userName = userName;
this.loginId = loginId;
}
}
@Schema(name = "UserDetail", description = "사용자별 로그 상세")
@Getter
public static class UserDetail extends AuditDetail {
private final String logDateTime;
private final String menuNm;
public UserDetail(
Long logId, String logDateTime, String menuNm, EventType eventType, LogDetail detail) {
super(logId, eventType, detail);
this.logDateTime = logDateTime;
this.menuNm = menuNm;
}
}
@Getter
@Setter
@AllArgsConstructor
public static class LogDetail {
String serviceName;
String parentMenuName;
String menuName;
String menuUrl;
String menuDescription;
Long sortOrder;
boolean used;
}
@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);
}
}
}
package com.kamco.cd.training.log.dto;
import com.fasterxml.jackson.annotation.JsonIgnore;
import com.kamco.cd.training.common.utils.interfaces.JsonFormatDttm;
import io.swagger.v3.oas.annotations.media.Schema;
import java.time.ZonedDateTime;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Sort;
public class AuditLogDto {
@Schema(name = "AuditLogBasic", description = "감사로그 기본 정보")
@Getter
public static class Basic {
@JsonIgnore private final Long id;
private final Long userUid;
private final EventType eventType;
private final EventStatus eventStatus;
private final String menuUid;
private final String ipAddress;
private final String requestUri;
private final String requestBody;
private final Long errorLogUid;
@JsonFormatDttm private final ZonedDateTime createdDttm;
public Basic(
Long id,
Long userUid,
EventType eventType,
EventStatus eventStatus,
String menuUid,
String ipAddress,
String requestUri,
String requestBody,
Long errorLogUid,
ZonedDateTime createdDttm) {
this.id = id;
this.userUid = userUid;
this.eventType = eventType;
this.eventStatus = eventStatus;
this.menuUid = menuUid;
this.ipAddress = ipAddress;
this.requestUri = requestUri;
this.requestBody = requestBody;
this.errorLogUid = errorLogUid;
this.createdDttm = createdDttm;
}
}
@Schema(name = "AuditCommon", description = "목록 공통")
@Getter
@AllArgsConstructor
public static class AuditCommon {
private int readCount;
private int cudCount;
private int printCount;
private int downloadCount;
private Long totalCount;
}
@Schema(name = "DailyAuditList", description = "일자별 목록")
@Getter
public static class DailyAuditList extends AuditCommon {
private final String baseDate;
public DailyAuditList(
int readCount,
int cudCount,
int printCount,
int downloadCount,
Long totalCount,
String baseDate) {
super(readCount, cudCount, printCount, downloadCount, totalCount);
this.baseDate = baseDate;
}
}
@Schema(name = "MenuAuditList", description = "메뉴별 목록")
@Getter
public static class MenuAuditList extends AuditCommon {
private final String menuId;
private final String menuName;
public MenuAuditList(
String menuId,
String menuName,
int readCount,
int cudCount,
int printCount,
int downloadCount,
Long totalCount) {
super(readCount, cudCount, printCount, downloadCount, totalCount);
this.menuId = menuId;
this.menuName = menuName;
}
}
@Schema(name = "UserAuditList", description = "사용자별 목록")
@Getter
public static class UserAuditList extends AuditCommon {
private final Long accountId;
private final String loginId;
private final String username;
public UserAuditList(
Long accountId,
String loginId,
String username,
int readCount,
int cudCount,
int printCount,
int downloadCount,
Long totalCount) {
super(readCount, cudCount, printCount, downloadCount, totalCount);
this.accountId = accountId;
this.loginId = loginId;
this.username = username;
}
}
@Schema(name = "AuditDetail", description = "감사 로그 상세 공통")
@Getter
@AllArgsConstructor
public static class AuditDetail {
private Long logId;
private EventType eventType;
private LogDetail detail;
}
@Schema(name = "DailyDetail", description = "일자별 로그 상세")
@Getter
public static class DailyDetail extends AuditDetail {
private final String userName;
private final String loginId;
private final String menuName;
public DailyDetail(
Long logId,
String userName,
String loginId,
String menuName,
EventType eventType,
LogDetail detail) {
super(logId, eventType, detail);
this.userName = userName;
this.loginId = loginId;
this.menuName = menuName;
}
}
@Schema(name = "MenuDetail", description = "메뉴별 로그 상세")
@Getter
public static class MenuDetail extends AuditDetail {
private final String logDateTime;
private final String userName;
private final String loginId;
public MenuDetail(
Long logId,
String logDateTime,
String userName,
String loginId,
EventType eventType,
LogDetail detail) {
super(logId, eventType, detail);
this.logDateTime = logDateTime;
this.userName = userName;
this.loginId = loginId;
}
}
@Schema(name = "UserDetail", description = "사용자별 로그 상세")
@Getter
public static class UserDetail extends AuditDetail {
private final String logDateTime;
private final String menuNm;
public UserDetail(
Long logId, String logDateTime, String menuNm, EventType eventType, LogDetail detail) {
super(logId, eventType, detail);
this.logDateTime = logDateTime;
this.menuNm = menuNm;
}
}
@Getter
@Setter
@AllArgsConstructor
public static class LogDetail {
String serviceName;
String parentMenuName;
String menuName;
String menuUrl;
String menuDescription;
Long sortOrder;
boolean used;
}
@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);
}
}
}

View File

@@ -1,101 +1,101 @@
package com.kamco.cd.training.log.dto;
import com.kamco.cd.training.common.utils.enums.EnumType;
import io.swagger.v3.oas.annotations.media.Schema;
import java.time.LocalDate;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Sort;
public class ErrorLogDto {
@Schema(name = "ErrorLogBasic", description = "에러로그 기본 정보")
@Getter
@Setter
@AllArgsConstructor
public static class Basic {
private final Long id;
private final String serviceName;
private final String menuNm;
private final String loginId;
private final String userName;
private final EventType errorType;
private final String errorName;
private final LogErrorLevel errorLevel;
private final String errorCode;
private final String errorMessage;
private final String errorDetail;
private final String createDate; // to_char해서 가져옴
}
@Schema(name = "ErrorSearchReq", description = "에러로그 검색 요청")
@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
public static class ErrorSearchReq {
LogErrorLevel errorLevel;
EventType eventType;
LocalDate startDate;
LocalDate endDate;
// 페이징 파라미터
private int page = 0;
private int size = 20;
private String sort;
public ErrorSearchReq(
LogErrorLevel errorLevel,
EventType eventType,
LocalDate startDate,
LocalDate endDate,
int page,
int size) {
this.errorLevel = errorLevel;
this.eventType = eventType;
this.startDate = startDate;
this.endDate = endDate;
this.page = page;
this.size = size;
}
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);
}
}
public enum LogErrorLevel implements EnumType {
WARNING("Warning"),
ERROR("Error"),
CRITICAL("Critical");
private final String desc;
LogErrorLevel(String desc) {
this.desc = desc;
}
@Override
public String getId() {
return name();
}
@Override
public String getText() {
return desc;
}
}
}
package com.kamco.cd.training.log.dto;
import com.kamco.cd.training.common.utils.enums.EnumType;
import io.swagger.v3.oas.annotations.media.Schema;
import java.time.LocalDate;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Sort;
public class ErrorLogDto {
@Schema(name = "ErrorLogBasic", description = "에러로그 기본 정보")
@Getter
@Setter
@AllArgsConstructor
public static class Basic {
private final Long id;
private final String serviceName;
private final String menuNm;
private final String loginId;
private final String userName;
private final EventType errorType;
private final String errorName;
private final LogErrorLevel errorLevel;
private final String errorCode;
private final String errorMessage;
private final String errorDetail;
private final String createDate; // to_char해서 가져옴
}
@Schema(name = "ErrorSearchReq", description = "에러로그 검색 요청")
@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
public static class ErrorSearchReq {
LogErrorLevel errorLevel;
EventType eventType;
LocalDate startDate;
LocalDate endDate;
// 페이징 파라미터
private int page = 0;
private int size = 20;
private String sort;
public ErrorSearchReq(
LogErrorLevel errorLevel,
EventType eventType,
LocalDate startDate,
LocalDate endDate,
int page,
int size) {
this.errorLevel = errorLevel;
this.eventType = eventType;
this.startDate = startDate;
this.endDate = endDate;
this.page = page;
this.size = size;
}
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);
}
}
public enum LogErrorLevel implements EnumType {
WARNING("Warning"),
ERROR("Error"),
CRITICAL("Critical");
private final String desc;
LogErrorLevel(String desc) {
this.desc = desc;
}
@Override
public String getId() {
return name();
}
@Override
public String getText() {
return desc;
}
}
}

View File

@@ -1,24 +1,24 @@
package com.kamco.cd.training.log.dto;
import com.kamco.cd.training.common.utils.enums.EnumType;
import lombok.AllArgsConstructor;
import lombok.Getter;
@Getter
@AllArgsConstructor
public enum EventStatus implements EnumType {
SUCCESS("이벤트 결과 성공"),
FAILED("이벤트 결과 실패");
private final String desc;
@Override
public String getId() {
return name();
}
@Override
public String getText() {
return desc;
}
}
package com.kamco.cd.training.log.dto;
import com.kamco.cd.training.common.utils.enums.EnumType;
import lombok.AllArgsConstructor;
import lombok.Getter;
@Getter
@AllArgsConstructor
public enum EventStatus implements EnumType {
SUCCESS("이벤트 결과 성공"),
FAILED("이벤트 결과 실패");
private final String desc;
@Override
public String getId() {
return name();
}
@Override
public String getText() {
return desc;
}
}

View File

@@ -1,29 +1,29 @@
package com.kamco.cd.training.log.dto;
import com.kamco.cd.training.common.utils.enums.EnumType;
import lombok.AllArgsConstructor;
import lombok.Getter;
@Getter
@AllArgsConstructor
public enum EventType implements EnumType {
CREATE("생성"),
READ("조회"),
UPDATE("수정"),
DELETE("삭제"),
DOWNLOAD("다운로드"),
PRINT("출력"),
OTHER("기타");
private final String desc;
@Override
public String getId() {
return name();
}
@Override
public String getText() {
return desc;
}
}
package com.kamco.cd.training.log.dto;
import com.kamco.cd.training.common.utils.enums.EnumType;
import lombok.AllArgsConstructor;
import lombok.Getter;
@Getter
@AllArgsConstructor
public enum EventType implements EnumType {
CREATE("생성"),
READ("조회"),
UPDATE("수정"),
DELETE("삭제"),
DOWNLOAD("다운로드"),
PRINT("출력"),
OTHER("기타");
private final String desc;
@Override
public String getId() {
return name();
}
@Override
public String getText() {
return desc;
}
}

View File

@@ -1,46 +1,46 @@
package com.kamco.cd.training.log.service;
import com.kamco.cd.training.log.dto.AuditLogDto;
import com.kamco.cd.training.postgres.core.AuditLogCoreService;
import java.time.LocalDate;
import lombok.RequiredArgsConstructor;
import org.springframework.data.domain.Page;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@Service
@RequiredArgsConstructor
@Transactional(readOnly = true)
public class AuditLogService {
private final AuditLogCoreService auditLogCoreService;
public Page<AuditLogDto.DailyAuditList> getLogByDaily(
AuditLogDto.searchReq searchRange, LocalDate startDate, LocalDate endDate) {
return auditLogCoreService.getLogByDaily(searchRange, startDate, endDate);
}
public Page<AuditLogDto.MenuAuditList> getLogByMenu(
AuditLogDto.searchReq searchRange, String searchValue) {
return auditLogCoreService.getLogByMenu(searchRange, searchValue);
}
public Page<AuditLogDto.UserAuditList> getLogByAccount(
AuditLogDto.searchReq searchRange, String searchValue) {
return auditLogCoreService.getLogByAccount(searchRange, searchValue);
}
public Page<AuditLogDto.DailyDetail> getLogByDailyResult(
AuditLogDto.searchReq searchRange, LocalDate logDate) {
return auditLogCoreService.getLogByDailyResult(searchRange, logDate);
}
public Page<AuditLogDto.MenuDetail> getLogByMenuResult(
AuditLogDto.searchReq searchRange, String menuId) {
return auditLogCoreService.getLogByMenuResult(searchRange, menuId);
}
public Page<AuditLogDto.UserDetail> getLogByAccountResult(
AuditLogDto.searchReq searchRange, Long accountId) {
return auditLogCoreService.getLogByAccountResult(searchRange, accountId);
}
}
package com.kamco.cd.training.log.service;
import com.kamco.cd.training.log.dto.AuditLogDto;
import com.kamco.cd.training.postgres.core.AuditLogCoreService;
import java.time.LocalDate;
import lombok.RequiredArgsConstructor;
import org.springframework.data.domain.Page;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@Service
@RequiredArgsConstructor
@Transactional(readOnly = true)
public class AuditLogService {
private final AuditLogCoreService auditLogCoreService;
public Page<AuditLogDto.DailyAuditList> getLogByDaily(
AuditLogDto.searchReq searchRange, LocalDate startDate, LocalDate endDate) {
return auditLogCoreService.getLogByDaily(searchRange, startDate, endDate);
}
public Page<AuditLogDto.MenuAuditList> getLogByMenu(
AuditLogDto.searchReq searchRange, String searchValue) {
return auditLogCoreService.getLogByMenu(searchRange, searchValue);
}
public Page<AuditLogDto.UserAuditList> getLogByAccount(
AuditLogDto.searchReq searchRange, String searchValue) {
return auditLogCoreService.getLogByAccount(searchRange, searchValue);
}
public Page<AuditLogDto.DailyDetail> getLogByDailyResult(
AuditLogDto.searchReq searchRange, LocalDate logDate) {
return auditLogCoreService.getLogByDailyResult(searchRange, logDate);
}
public Page<AuditLogDto.MenuDetail> getLogByMenuResult(
AuditLogDto.searchReq searchRange, String menuId) {
return auditLogCoreService.getLogByMenuResult(searchRange, menuId);
}
public Page<AuditLogDto.UserDetail> getLogByAccountResult(
AuditLogDto.searchReq searchRange, Long accountId) {
return auditLogCoreService.getLogByAccountResult(searchRange, accountId);
}
}

View File

@@ -1,19 +1,19 @@
package com.kamco.cd.training.log.service;
import com.kamco.cd.training.log.dto.ErrorLogDto;
import com.kamco.cd.training.postgres.core.ErrorLogCoreService;
import lombok.RequiredArgsConstructor;
import org.springframework.data.domain.Page;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@Service
@RequiredArgsConstructor
@Transactional(readOnly = true)
public class ErrorLogService {
private final ErrorLogCoreService errorLogCoreService;
public Page<ErrorLogDto.Basic> findLogByError(ErrorLogDto.ErrorSearchReq searchReq) {
return errorLogCoreService.findLogByError(searchReq);
}
}
package com.kamco.cd.training.log.service;
import com.kamco.cd.training.log.dto.ErrorLogDto;
import com.kamco.cd.training.postgres.core.ErrorLogCoreService;
import lombok.RequiredArgsConstructor;
import org.springframework.data.domain.Page;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@Service
@RequiredArgsConstructor
@Transactional(readOnly = true)
public class ErrorLogService {
private final ErrorLogCoreService errorLogCoreService;
public Page<ErrorLogDto.Basic> findLogByError(ErrorLogDto.ErrorSearchReq searchReq) {
return errorLogCoreService.findLogByError(searchReq);
}
}

View File

@@ -1,241 +1,241 @@
package com.kamco.cd.training.members;
import com.kamco.cd.training.auth.CustomUserDetails;
import com.kamco.cd.training.auth.JwtTokenProvider;
import com.kamco.cd.training.common.enums.StatusType;
import com.kamco.cd.training.common.exception.CustomApiException;
import com.kamco.cd.training.config.api.ApiResponseDto;
import com.kamco.cd.training.members.dto.MembersDto;
import com.kamco.cd.training.members.dto.SignInRequest;
import com.kamco.cd.training.members.dto.TokenResponse;
import com.kamco.cd.training.members.service.AuthService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.media.Content;
import io.swagger.v3.oas.annotations.media.ExampleObject;
import io.swagger.v3.oas.annotations.media.Schema;
import io.swagger.v3.oas.annotations.responses.ApiResponse;
import io.swagger.v3.oas.annotations.responses.ApiResponses;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.servlet.http.HttpServletResponse;
import java.nio.file.AccessDeniedException;
import java.time.Duration;
import java.util.UUID;
import lombok.RequiredArgsConstructor;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseCookie;
import org.springframework.http.ResponseEntity;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.web.ErrorResponse;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@Tag(name = "인증(Auth)", description = "로그인, 토큰 재발급, 로그아웃 API")
@RestController
@RequestMapping("/api/auth")
@RequiredArgsConstructor
public class AuthController {
private final AuthenticationManager authenticationManager;
private final JwtTokenProvider jwtTokenProvider;
private final AuthService authService;
@Value("${token.refresh-cookie-name}")
private String refreshCookieName;
@Value("${token.refresh-cookie-secure:true}")
private boolean refreshCookieSecure;
@PostMapping("/signin")
@Operation(summary = "로그인", description = "사번으로 로그인하여 액세스/리프레시 토큰을 발급.")
@ApiResponses({
@ApiResponse(
responseCode = "200",
description = "로그인 성공",
content = @Content(schema = @Schema(implementation = TokenResponse.class))),
@ApiResponse(
responseCode = "401",
description = "로그인 실패 (아이디/비밀번호 오류, 계정잠금 등)",
content =
@Content(
schema = @Schema(implementation = ErrorResponse.class),
examples = {
@ExampleObject(
name = "사번 입력 오류",
description = "존재하지 않는 아이디",
value =
"""
{
"code": "LOGIN_ID_NOT_FOUND",
"message": "사번을 잘못 입력하셨습니다."
}
"""),
@ExampleObject(
name = "비밀번호 입력 오류 (4회 이하)",
description = "아이디는 정상, 비밀번호를 여러 번 틀린 경우",
value =
"""
{
"code": "LOGIN_PASSWORD_MISMATCH",
"message": "비밀번호를 잘못 입력하셨습니다."
}
"""),
@ExampleObject(
name = "비밀번호 오류 횟수 초과",
description = "비밀번호 5회 이상 오류로 계정 잠김",
value =
"""
{
"code": "LOGIN_PASSWORD_EXCEEDED",
"message": "비밀번호 오류 횟수를 초과하여 이용하실 수 없습니다. 로그인 오류에 대해 관리자에게 문의하시기 바랍니다."
}
"""),
@ExampleObject(
name = "사용 중지 된 계정의 로그인 시도",
description = "사용 중지 된 계정의 로그인 시도",
value =
"""
{
"code": "INACTIVE_ID",
"message": "사용할 수 없는 계정입니다."
}
""")
}))
})
public ApiResponseDto<TokenResponse> signin(
@io.swagger.v3.oas.annotations.parameters.RequestBody(
description = "로그인 요청 정보",
required = true)
@RequestBody
SignInRequest request,
HttpServletResponse response) {
// 사용자 상태 조회
String status = authService.getUserStatus(request);
if(StatusType.INACTIVE.getId().equals(status)) {
throw new CustomApiException("INACTIVE_ID", HttpStatus.UNAUTHORIZED);
}
Authentication authentication = null;
MembersDto.Member member = new MembersDto.Member();
authentication =
authenticationManager.authenticate(
new UsernamePasswordAuthenticationToken(request.getUsername(), request.getPassword()));
String username = authentication.getName(); // UserDetailsService 에서 사용한 username
String accessToken = jwtTokenProvider.createAccessToken(username);
String refreshToken = jwtTokenProvider.createRefreshToken(username);
// 토큰 저장
authService.tokenSave(username, refreshToken, jwtTokenProvider.getRefreshTokenValidityInMs());
// HttpOnly + Secure 쿠키에 RefreshToken 저장
ResponseCookie cookie =
ResponseCookie.from(refreshCookieName, refreshToken)
.httpOnly(true)
.secure(refreshCookieSecure)
.path("/")
.maxAge(Duration.ofMillis(jwtTokenProvider.getRefreshTokenValidityInMs()))
.sameSite("Strict")
.build();
response.addHeader(HttpHeaders.SET_COOKIE, cookie.toString());
CustomUserDetails user = (CustomUserDetails) authentication.getPrincipal();
member.setId(user.getMember().getId());
member.setName(user.getMember().getName());
member.setEmployeeNo(user.getMember().getEmployeeNo());
// PENDING 비활성 상태(새로운 패스워드 입력 해야함)
if (StatusType.PENDING.getId().equals(status)) {
member.setEmployeeNo(request.getUsername());
return ApiResponseDto.ok(new TokenResponse(status, accessToken, refreshToken, member));
}
// 인증 성공 로그인 시간 저장
authService.saveLogin(UUID.fromString(username));
return ApiResponseDto.ok(new TokenResponse(status, accessToken, refreshToken, member));
}
@PostMapping("/refresh")
@Operation(summary = "토큰 재발급", description = "리프레시 토큰으로 새로운 액세스/리프레시 토큰을 재발급합니다.")
@ApiResponses({
@ApiResponse(
responseCode = "200",
description = "재발급 성공",
content = @Content(schema = @Schema(implementation = TokenResponse.class))),
@ApiResponse(
responseCode = "403",
description = "만료되었거나 유효하지 않은 리프레시 토큰",
content = @Content(schema = @Schema(implementation = ErrorResponse.class)))
})
public ResponseEntity<TokenResponse> refresh(String refreshToken, HttpServletResponse response)
throws AccessDeniedException {
if (refreshToken == null || !jwtTokenProvider.isValidToken(refreshToken)) {
throw new AccessDeniedException("만료되었거나 유효하지 않은 리프레시 토큰 입니다.");
}
String username = jwtTokenProvider.getSubject(refreshToken);
// 저장된 RefreshToken과 일치하는지 확인
authService.validateRefreshToken(username, refreshToken);
// 새 토큰 발급
String newAccessToken = jwtTokenProvider.createAccessToken(username);
String newRefreshToken = jwtTokenProvider.createRefreshToken(username);
// 토큰 저장
authService.tokenSave(username, refreshToken, jwtTokenProvider.getRefreshTokenValidityInMs());
// 쿠키 갱신
ResponseCookie cookie =
ResponseCookie.from(refreshCookieName, newRefreshToken)
.httpOnly(true)
.secure(refreshCookieSecure)
.path("/")
.maxAge(Duration.ofMillis(jwtTokenProvider.getRefreshTokenValidityInMs()))
.sameSite("Strict")
.build();
response.addHeader(HttpHeaders.SET_COOKIE, cookie.toString());
MembersDto.Member member = new MembersDto.Member();
return ResponseEntity.ok(new TokenResponse("ACTIVE", newAccessToken, newRefreshToken, member));
}
@PostMapping("/logout")
@Operation(summary = "로그아웃", description = "현재 사용자의 토큰을 무효화(리프레시 토큰 삭제)합니다.")
@ApiResponses({
@ApiResponse(
responseCode = "200",
description = "로그아웃 성공",
content = @Content(schema = @Schema(implementation = Void.class)))
})
public ApiResponseDto<ResponseEntity<Object>> logout(
Authentication authentication, HttpServletResponse response) {
if (authentication != null) {
String username = authentication.getName();
authService.logout(username);
}
// 쿠키 삭제 (Max-Age=0)
ResponseCookie cookie =
ResponseCookie.from(refreshCookieName, "")
.httpOnly(true)
.secure(refreshCookieSecure)
.path("/")
.maxAge(0)
.sameSite("Strict")
.build();
response.addHeader(HttpHeaders.SET_COOKIE, cookie.toString());
return ApiResponseDto.createOK(ResponseEntity.noContent().build());
}
}
package com.kamco.cd.training.members;
import com.kamco.cd.training.auth.CustomUserDetails;
import com.kamco.cd.training.auth.JwtTokenProvider;
import com.kamco.cd.training.common.enums.StatusType;
import com.kamco.cd.training.common.exception.CustomApiException;
import com.kamco.cd.training.config.api.ApiResponseDto;
import com.kamco.cd.training.members.dto.MembersDto;
import com.kamco.cd.training.members.dto.SignInRequest;
import com.kamco.cd.training.members.dto.TokenResponse;
import com.kamco.cd.training.members.service.AuthService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.media.Content;
import io.swagger.v3.oas.annotations.media.ExampleObject;
import io.swagger.v3.oas.annotations.media.Schema;
import io.swagger.v3.oas.annotations.responses.ApiResponse;
import io.swagger.v3.oas.annotations.responses.ApiResponses;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.servlet.http.HttpServletResponse;
import java.nio.file.AccessDeniedException;
import java.time.Duration;
import java.util.UUID;
import lombok.RequiredArgsConstructor;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseCookie;
import org.springframework.http.ResponseEntity;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.web.ErrorResponse;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@Tag(name = "인증(Auth)", description = "로그인, 토큰 재발급, 로그아웃 API")
@RestController
@RequestMapping("/api/auth")
@RequiredArgsConstructor
public class AuthController {
private final AuthenticationManager authenticationManager;
private final JwtTokenProvider jwtTokenProvider;
private final AuthService authService;
@Value("${token.refresh-cookie-name}")
private String refreshCookieName;
@Value("${token.refresh-cookie-secure:true}")
private boolean refreshCookieSecure;
@PostMapping("/signin")
@Operation(summary = "로그인", description = "사번으로 로그인하여 액세스/리프레시 토큰을 발급.")
@ApiResponses({
@ApiResponse(
responseCode = "200",
description = "로그인 성공",
content = @Content(schema = @Schema(implementation = TokenResponse.class))),
@ApiResponse(
responseCode = "401",
description = "로그인 실패 (아이디/비밀번호 오류, 계정잠금 등)",
content =
@Content(
schema = @Schema(implementation = ErrorResponse.class),
examples = {
@ExampleObject(
name = "사번 입력 오류",
description = "존재하지 않는 아이디",
value =
"""
{
"code": "LOGIN_ID_NOT_FOUND",
"message": "사번을 잘못 입력하셨습니다."
}
"""),
@ExampleObject(
name = "비밀번호 입력 오류 (4회 이하)",
description = "아이디는 정상, 비밀번호를 여러 번 틀린 경우",
value =
"""
{
"code": "LOGIN_PASSWORD_MISMATCH",
"message": "비밀번호를 잘못 입력하셨습니다."
}
"""),
@ExampleObject(
name = "비밀번호 오류 횟수 초과",
description = "비밀번호 5회 이상 오류로 계정 잠김",
value =
"""
{
"code": "LOGIN_PASSWORD_EXCEEDED",
"message": "비밀번호 오류 횟수를 초과하여 이용하실 수 없습니다. 로그인 오류에 대해 관리자에게 문의하시기 바랍니다."
}
"""),
@ExampleObject(
name = "사용 중지 된 계정의 로그인 시도",
description = "사용 중지 된 계정의 로그인 시도",
value =
"""
{
"code": "INACTIVE_ID",
"message": "사용할 수 없는 계정입니다."
}
""")
}))
})
public ApiResponseDto<TokenResponse> signin(
@io.swagger.v3.oas.annotations.parameters.RequestBody(
description = "로그인 요청 정보",
required = true)
@RequestBody
SignInRequest request,
HttpServletResponse response) {
// 사용자 상태 조회
String status = authService.getUserStatus(request);
if (StatusType.INACTIVE.getId().equals(status)) {
throw new CustomApiException("INACTIVE_ID", HttpStatus.UNAUTHORIZED);
}
Authentication authentication = null;
MembersDto.Member member = new MembersDto.Member();
authentication =
authenticationManager.authenticate(
new UsernamePasswordAuthenticationToken(request.getUsername(), request.getPassword()));
String username = authentication.getName(); // UserDetailsService 에서 사용한 username
String accessToken = jwtTokenProvider.createAccessToken(username);
String refreshToken = jwtTokenProvider.createRefreshToken(username);
// 토큰 저장
authService.tokenSave(username, refreshToken, jwtTokenProvider.getRefreshTokenValidityInMs());
// HttpOnly + Secure 쿠키에 RefreshToken 저장
ResponseCookie cookie =
ResponseCookie.from(refreshCookieName, refreshToken)
.httpOnly(true)
.secure(refreshCookieSecure)
.path("/")
.maxAge(Duration.ofMillis(jwtTokenProvider.getRefreshTokenValidityInMs()))
.sameSite("Strict")
.build();
response.addHeader(HttpHeaders.SET_COOKIE, cookie.toString());
CustomUserDetails user = (CustomUserDetails) authentication.getPrincipal();
member.setId(user.getMember().getId());
member.setName(user.getMember().getName());
member.setEmployeeNo(user.getMember().getEmployeeNo());
// PENDING 비활성 상태(새로운 패스워드 입력 해야함)
if (StatusType.PENDING.getId().equals(status)) {
member.setEmployeeNo(request.getUsername());
return ApiResponseDto.ok(new TokenResponse(status, accessToken, refreshToken, member));
}
// 인증 성공 로그인 시간 저장
authService.saveLogin(UUID.fromString(username));
return ApiResponseDto.ok(new TokenResponse(status, accessToken, refreshToken, member));
}
@PostMapping("/refresh")
@Operation(summary = "토큰 재발급", description = "리프레시 토큰으로 새로운 액세스/리프레시 토큰을 재발급합니다.")
@ApiResponses({
@ApiResponse(
responseCode = "200",
description = "재발급 성공",
content = @Content(schema = @Schema(implementation = TokenResponse.class))),
@ApiResponse(
responseCode = "403",
description = "만료되었거나 유효하지 않은 리프레시 토큰",
content = @Content(schema = @Schema(implementation = ErrorResponse.class)))
})
public ResponseEntity<TokenResponse> refresh(String refreshToken, HttpServletResponse response)
throws AccessDeniedException {
if (refreshToken == null || !jwtTokenProvider.isValidToken(refreshToken)) {
throw new AccessDeniedException("만료되었거나 유효하지 않은 리프레시 토큰 입니다.");
}
String username = jwtTokenProvider.getSubject(refreshToken);
// 저장된 RefreshToken과 일치하는지 확인
authService.validateRefreshToken(username, refreshToken);
// 새 토큰 발급
String newAccessToken = jwtTokenProvider.createAccessToken(username);
String newRefreshToken = jwtTokenProvider.createRefreshToken(username);
// 토큰 저장
authService.tokenSave(username, refreshToken, jwtTokenProvider.getRefreshTokenValidityInMs());
// 쿠키 갱신
ResponseCookie cookie =
ResponseCookie.from(refreshCookieName, newRefreshToken)
.httpOnly(true)
.secure(refreshCookieSecure)
.path("/")
.maxAge(Duration.ofMillis(jwtTokenProvider.getRefreshTokenValidityInMs()))
.sameSite("Strict")
.build();
response.addHeader(HttpHeaders.SET_COOKIE, cookie.toString());
MembersDto.Member member = new MembersDto.Member();
return ResponseEntity.ok(new TokenResponse("ACTIVE", newAccessToken, newRefreshToken, member));
}
@PostMapping("/logout")
@Operation(summary = "로그아웃", description = "현재 사용자의 토큰을 무효화(리프레시 토큰 삭제)합니다.")
@ApiResponses({
@ApiResponse(
responseCode = "200",
description = "로그아웃 성공",
content = @Content(schema = @Schema(implementation = Void.class)))
})
public ApiResponseDto<ResponseEntity<Object>> logout(
Authentication authentication, HttpServletResponse response) {
if (authentication != null) {
String username = authentication.getName();
authService.logout(username);
}
// 쿠키 삭제 (Max-Age=0)
ResponseCookie cookie =
ResponseCookie.from(refreshCookieName, "")
.httpOnly(true)
.secure(refreshCookieSecure)
.path("/")
.maxAge(0)
.sameSite("Strict")
.build();
response.addHeader(HttpHeaders.SET_COOKIE, cookie.toString());
return ApiResponseDto.createOK(ResponseEntity.noContent().build());
}
}

View File

@@ -1,69 +1,69 @@
package com.kamco.cd.training.members;
import com.kamco.cd.training.config.api.ApiResponseDto;
import com.kamco.cd.training.members.dto.MembersDto;
import com.kamco.cd.training.members.dto.MembersDto.Basic;
import com.kamco.cd.training.members.service.MembersService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.media.Content;
import io.swagger.v3.oas.annotations.media.Schema;
import io.swagger.v3.oas.annotations.responses.ApiResponse;
import io.swagger.v3.oas.annotations.responses.ApiResponses;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
import org.springframework.data.domain.Page;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.web.bind.annotation.*;
@Tag(name = "회원정보 관리", description = "회원정보 관리 API")
@RestController
@RequestMapping("/api/members")
@RequiredArgsConstructor
public class MembersApiController {
private final AuthenticationManager authenticationManager;
private final MembersService membersService;
@Operation(summary = "회원정보 목록", description = "회원정보 조회")
@ApiResponses(
value = {
@ApiResponse(
responseCode = "200",
description = "검색 성공",
content =
@Content(
mediaType = "application/json",
schema = @Schema(implementation = Page.class))),
@ApiResponse(responseCode = "400", description = "잘못된 검색 조건", content = @Content),
@ApiResponse(responseCode = "500", description = "서버 오류", content = @Content)
})
@PostMapping("/search")
public ApiResponseDto<Page<Basic>> getMemberList(
@RequestBody @Valid MembersDto.SearchReq searchReq) {
return ApiResponseDto.ok(membersService.findByMembers(searchReq));
}
@Operation(
summary = "사용자 비밀번호 변경",
description = "로그인 성공후 status가 INACTIVE일때 로그인 id를 memberId로 path 생성필요")
@ApiResponses(
value = {
@ApiResponse(
responseCode = "201",
description = "사용자 비밀번호 변경",
content =
@Content(
mediaType = "application/json",
schema = @Schema(implementation = Long.class))),
@ApiResponse(responseCode = "400", description = "잘못된 요청 데이터", content = @Content),
@ApiResponse(responseCode = "404", description = "코드를 찾을 수 없음", content = @Content),
@ApiResponse(responseCode = "500", description = "서버 오류", content = @Content)
})
@PatchMapping("/{memberId}/password")
public ApiResponseDto<String> resetPassword(
@PathVariable String memberId, @RequestBody @Valid MembersDto.InitReq initReq) {
membersService.resetPassword(memberId, initReq);
return ApiResponseDto.createOK(memberId);
}
}
package com.kamco.cd.training.members;
import com.kamco.cd.training.config.api.ApiResponseDto;
import com.kamco.cd.training.members.dto.MembersDto;
import com.kamco.cd.training.members.dto.MembersDto.Basic;
import com.kamco.cd.training.members.service.MembersService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.media.Content;
import io.swagger.v3.oas.annotations.media.Schema;
import io.swagger.v3.oas.annotations.responses.ApiResponse;
import io.swagger.v3.oas.annotations.responses.ApiResponses;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
import org.springframework.data.domain.Page;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.web.bind.annotation.*;
@Tag(name = "회원정보 관리", description = "회원정보 관리 API")
@RestController
@RequestMapping("/api/members")
@RequiredArgsConstructor
public class MembersApiController {
private final AuthenticationManager authenticationManager;
private final MembersService membersService;
@Operation(summary = "회원정보 목록", description = "회원정보 조회")
@ApiResponses(
value = {
@ApiResponse(
responseCode = "200",
description = "검색 성공",
content =
@Content(
mediaType = "application/json",
schema = @Schema(implementation = Page.class))),
@ApiResponse(responseCode = "400", description = "잘못된 검색 조건", content = @Content),
@ApiResponse(responseCode = "500", description = "서버 오류", content = @Content)
})
@PostMapping("/search")
public ApiResponseDto<Page<Basic>> getMemberList(
@RequestBody @Valid MembersDto.SearchReq searchReq) {
return ApiResponseDto.ok(membersService.findByMembers(searchReq));
}
@Operation(
summary = "사용자 비밀번호 변경",
description = "로그인 성공후 status가 INACTIVE일때 로그인 id를 memberId로 path 생성필요")
@ApiResponses(
value = {
@ApiResponse(
responseCode = "201",
description = "사용자 비밀번호 변경",
content =
@Content(
mediaType = "application/json",
schema = @Schema(implementation = Long.class))),
@ApiResponse(responseCode = "400", description = "잘못된 요청 데이터", content = @Content),
@ApiResponse(responseCode = "404", description = "코드를 찾을 수 없음", content = @Content),
@ApiResponse(responseCode = "500", description = "서버 오류", content = @Content)
})
@PatchMapping("/{memberId}/password")
public ApiResponseDto<String> resetPassword(
@PathVariable String memberId, @RequestBody @Valid MembersDto.InitReq initReq) {
membersService.resetPassword(memberId, initReq);
return ApiResponseDto.createOK(memberId);
}
}

View File

@@ -1,183 +1,183 @@
package com.kamco.cd.training.members.dto;
import com.kamco.cd.training.common.enums.RoleType;
import com.kamco.cd.training.common.enums.StatusType;
import com.kamco.cd.training.common.utils.enums.Enums;
import com.kamco.cd.training.common.utils.interfaces.EnumValid;
import com.kamco.cd.training.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 = "K20251212001")
@Size(max = 50)
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;
}
}
package com.kamco.cd.training.members.dto;
import com.kamco.cd.training.common.enums.RoleType;
import com.kamco.cd.training.common.enums.StatusType;
import com.kamco.cd.training.common.utils.enums.Enums;
import com.kamco.cd.training.common.utils.interfaces.EnumValid;
import com.kamco.cd.training.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 = "K20251212001")
@Size(max = 50)
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;
}
}

View File

@@ -1,20 +1,20 @@
package com.kamco.cd.training.members.dto;
import com.fasterxml.jackson.annotation.JsonProperty;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Getter;
import lombok.Setter;
import lombok.ToString;
@Getter
@Setter
@ToString(exclude = "password")
public class SignInRequest {
@Schema(description = "사용자 ID", example = "1234567")
private String username;
@Schema(description = "비밀번호", example = "Admin2!@#")
@JsonProperty(access = JsonProperty.Access.WRITE_ONLY)
private String password;
}
package com.kamco.cd.training.members.dto;
import com.fasterxml.jackson.annotation.JsonProperty;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Getter;
import lombok.Setter;
import lombok.ToString;
@Getter
@Setter
@ToString(exclude = "password")
public class SignInRequest {
@Schema(description = "사용자 ID", example = "123456")
private String username;
@Schema(description = "비밀번호", example = "qwe123!@#")
@JsonProperty(access = JsonProperty.Access.WRITE_ONLY)
private String password;
}

View File

@@ -1,16 +1,16 @@
package com.kamco.cd.training.members.dto;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.Setter;
@Getter
@Setter
@AllArgsConstructor
public class TokenResponse {
private String status;
private String accessToken;
private String refreshToken;
private MembersDto.Member member;
}
package com.kamco.cd.training.members.dto;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.Setter;
@Getter
@Setter
@AllArgsConstructor
public class TokenResponse {
private String status;
private String accessToken;
private String refreshToken;
private MembersDto.Member member;
}

View File

@@ -1,50 +1,50 @@
package com.kamco.cd.training.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);
}
}
}
package com.kamco.cd.training.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

@@ -1,77 +1,77 @@
package com.kamco.cd.training.members.service;
import com.kamco.cd.training.common.enums.error.AuthErrorCode;
import com.kamco.cd.training.common.exception.CustomApiException;
import com.kamco.cd.training.members.dto.SignInRequest;
import com.kamco.cd.training.postgres.core.MembersCoreService;
import com.kamco.cd.training.postgres.core.TokenCoreService;
import java.util.UUID;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@Service
@RequiredArgsConstructor
@Transactional(readOnly = true)
public class AuthService {
private final MembersCoreService membersCoreService;
private final TokenCoreService tokenCoreService;
/**
* 토큰 저장
*
* @param subject
* @param refreshToken
* @param validityMs
*/
@Transactional
public void tokenSave(String subject, String refreshToken, long validityMs) {
tokenCoreService.save(subject, refreshToken, validityMs);
}
/**
* refreshToken을 DB와 비교 검증
*
* @param subject 사용자 식별(UUID)
* @param requestRefreshToken refresh token
*/
public void validateRefreshToken(String subject, String requestRefreshToken) {
String savedToken = tokenCoreService.getValidTokenOrThrow(subject);
if (!savedToken.equals(requestRefreshToken)) {
throw new CustomApiException(AuthErrorCode.REFRESH_TOKEN_MISMATCH);
}
}
/**
* 로그아웃(토큰폐기)
*
* @param subject 사용자 식별(UUID)
*/
@Transactional
public void logout(String subject) {
// RefreshToken 폐기
tokenCoreService.revokeBySubject(subject);
}
/**
* 로그인 일시 저장
*
* @param uuid
*/
@Transactional
public void saveLogin(UUID uuid) {
membersCoreService.saveLogin(uuid);
}
/**
* 사용자 상태 조회
*
* @param request
* @return
*/
public String getUserStatus(SignInRequest request) {
return membersCoreService.getUserStatus(request);
}
}
package com.kamco.cd.training.members.service;
import com.kamco.cd.training.common.enums.error.AuthErrorCode;
import com.kamco.cd.training.common.exception.CustomApiException;
import com.kamco.cd.training.members.dto.SignInRequest;
import com.kamco.cd.training.postgres.core.MembersCoreService;
import com.kamco.cd.training.postgres.core.TokenCoreService;
import java.util.UUID;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@Service
@RequiredArgsConstructor
@Transactional(readOnly = true)
public class AuthService {
private final MembersCoreService membersCoreService;
private final TokenCoreService tokenCoreService;
/**
* 토큰 저장
*
* @param subject
* @param refreshToken
* @param validityMs
*/
@Transactional
public void tokenSave(String subject, String refreshToken, long validityMs) {
tokenCoreService.save(subject, refreshToken, validityMs);
}
/**
* refreshToken을 DB와 비교 검증
*
* @param subject 사용자 식별(UUID)
* @param requestRefreshToken refresh token
*/
public void validateRefreshToken(String subject, String requestRefreshToken) {
String savedToken = tokenCoreService.getValidTokenOrThrow(subject);
if (!savedToken.equals(requestRefreshToken)) {
throw new CustomApiException(AuthErrorCode.REFRESH_TOKEN_MISMATCH);
}
}
/**
* 로그아웃(토큰폐기)
*
* @param subject 사용자 식별(UUID)
*/
@Transactional
public void logout(String subject) {
// RefreshToken 폐기
tokenCoreService.revokeBySubject(subject);
}
/**
* 로그인 일시 저장
*
* @param uuid
*/
@Transactional
public void saveLogin(UUID uuid) {
membersCoreService.saveLogin(uuid);
}
/**
* 사용자 상태 조회
*
* @param request
* @return
*/
public String getUserStatus(SignInRequest request) {
return membersCoreService.getUserStatus(request);
}
}

View File

@@ -1,29 +1,29 @@
package com.kamco.cd.training.members.service;
import com.kamco.cd.training.auth.CustomUserDetails;
import com.kamco.cd.training.postgres.entity.MemberEntity;
import com.kamco.cd.training.postgres.repository.members.MembersRepository;
import java.util.UUID;
import lombok.RequiredArgsConstructor;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Service;
@Service
@RequiredArgsConstructor
public class MemberDetailsService implements UserDetailsService {
private final MembersRepository membersRepository;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
UUID uuid = UUID.fromString(username);
MemberEntity member =
membersRepository
.findByUUID(uuid)
.orElseThrow(() -> new UsernameNotFoundException("USER NOT FOUND"));
return new CustomUserDetails(member);
}
}
package com.kamco.cd.training.members.service;
import com.kamco.cd.training.auth.CustomUserDetails;
import com.kamco.cd.training.postgres.entity.MemberEntity;
import com.kamco.cd.training.postgres.repository.members.MembersRepository;
import java.util.UUID;
import lombok.RequiredArgsConstructor;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Service;
@Service
@RequiredArgsConstructor
public class MemberDetailsService implements UserDetailsService {
private final MembersRepository membersRepository;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
UUID uuid = UUID.fromString(username);
MemberEntity member =
membersRepository
.findByUUID(uuid)
.orElseThrow(() -> new UsernameNotFoundException("USER NOT FOUND"));
return new CustomUserDetails(member);
}
}

View File

@@ -1,45 +1,45 @@
package com.kamco.cd.training.members.service;
import com.kamco.cd.training.common.exception.CustomApiException;
import com.kamco.cd.training.common.utils.CommonStringUtils;
import com.kamco.cd.training.members.dto.MembersDto;
import com.kamco.cd.training.members.dto.MembersDto.Basic;
import com.kamco.cd.training.postgres.core.MembersCoreService;
import lombok.RequiredArgsConstructor;
import org.springframework.data.domain.Page;
import org.springframework.http.HttpStatus;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@Service
@Transactional(readOnly = true)
@RequiredArgsConstructor
public class MembersService {
private final MembersCoreService membersCoreService;
/**
* 회원목록 조회
*
* @param searchReq
* @return
*/
public Page<Basic> findByMembers(MembersDto.SearchReq searchReq) {
return membersCoreService.findByMembers(searchReq);
}
/**
* 패스워드 사용자 변경
*
* @param id
* @param initReq
*/
@Transactional
public void resetPassword(String id, MembersDto.InitReq initReq) {
if (!CommonStringUtils.isValidPassword(initReq.getNewPassword())) {
throw new CustomApiException("WRONG_PASSWORD", HttpStatus.BAD_REQUEST);
}
membersCoreService.resetPassword(id, initReq);
}
}
package com.kamco.cd.training.members.service;
import com.kamco.cd.training.common.exception.CustomApiException;
import com.kamco.cd.training.common.utils.CommonStringUtils;
import com.kamco.cd.training.members.dto.MembersDto;
import com.kamco.cd.training.members.dto.MembersDto.Basic;
import com.kamco.cd.training.postgres.core.MembersCoreService;
import lombok.RequiredArgsConstructor;
import org.springframework.data.domain.Page;
import org.springframework.http.HttpStatus;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@Service
@Transactional(readOnly = true)
@RequiredArgsConstructor
public class MembersService {
private final MembersCoreService membersCoreService;
/**
* 회원목록 조회
*
* @param searchReq
* @return
*/
public Page<Basic> findByMembers(MembersDto.SearchReq searchReq) {
return membersCoreService.findByMembers(searchReq);
}
/**
* 패스워드 사용자 변경
*
* @param id
* @param initReq
*/
@Transactional
public void resetPassword(String id, MembersDto.InitReq initReq) {
if (!CommonStringUtils.isValidPassword(initReq.getNewPassword())) {
throw new CustomApiException("WRONG_PASSWORD", HttpStatus.BAD_REQUEST);
}
membersCoreService.resetPassword(id, initReq);
}
}

View File

@@ -1,62 +1,62 @@
package com.kamco.cd.training.menu;
import com.kamco.cd.training.config.api.ApiResponseDto;
import com.kamco.cd.training.menu.dto.MenuDto;
import com.kamco.cd.training.menu.service.MenuService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.media.Content;
import io.swagger.v3.oas.annotations.media.Schema;
import io.swagger.v3.oas.annotations.responses.ApiResponse;
import io.swagger.v3.oas.annotations.responses.ApiResponses;
import io.swagger.v3.oas.annotations.tags.Tag;
import java.util.List;
import lombok.RequiredArgsConstructor;
import org.springframework.data.domain.Page;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@Tag(name = "메뉴 관리", description = "메뉴 관리 API")
@RestController
@RequestMapping("/api/menu")
@RequiredArgsConstructor
public class MenuApiController {
private final MenuService menuService;
@Operation(summary = "메뉴 목록", description = "메뉴 목록 조회")
@ApiResponses(
value = {
@ApiResponse(
responseCode = "200",
description = "검색 성공",
content =
@Content(
mediaType = "application/json",
schema = @Schema(implementation = Page.class))),
@ApiResponse(responseCode = "400", description = "잘못된 검색 조건", content = @Content),
@ApiResponse(responseCode = "500", description = "서버 오류", content = @Content)
})
@GetMapping
public ApiResponseDto<List<MenuDto.Basic>> getFindAll() {
return ApiResponseDto.ok(menuService.getFindAll());
}
@Operation(summary = "캐시 초기화", description = "메뉴관리 캐시를 초기화합니다.")
@ApiResponses(
value = {
@ApiResponse(
responseCode = "200",
description = "캐시 초기화 성공",
content =
@Content(
mediaType = "application/json",
schema = @Schema(implementation = String.class))),
@ApiResponse(responseCode = "500", description = "서버 오류", content = @Content)
})
@GetMapping("/cache/refresh")
public ApiResponseDto<String> refreshCommonCodeCache() {
menuService.refresh();
return ApiResponseDto.ok("메뉴관리 캐시가 초기화되었습니다.");
}
}
package com.kamco.cd.training.menu;
import com.kamco.cd.training.config.api.ApiResponseDto;
import com.kamco.cd.training.menu.dto.MenuDto;
import com.kamco.cd.training.menu.service.MenuService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.media.Content;
import io.swagger.v3.oas.annotations.media.Schema;
import io.swagger.v3.oas.annotations.responses.ApiResponse;
import io.swagger.v3.oas.annotations.responses.ApiResponses;
import io.swagger.v3.oas.annotations.tags.Tag;
import java.util.List;
import lombok.RequiredArgsConstructor;
import org.springframework.data.domain.Page;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@Tag(name = "메뉴 관리", description = "메뉴 관리 API")
@RestController
@RequestMapping("/api/menu")
@RequiredArgsConstructor
public class MenuApiController {
private final MenuService menuService;
@Operation(summary = "메뉴 목록", description = "메뉴 목록 조회")
@ApiResponses(
value = {
@ApiResponse(
responseCode = "200",
description = "검색 성공",
content =
@Content(
mediaType = "application/json",
schema = @Schema(implementation = Page.class))),
@ApiResponse(responseCode = "400", description = "잘못된 검색 조건", content = @Content),
@ApiResponse(responseCode = "500", description = "서버 오류", content = @Content)
})
@GetMapping
public ApiResponseDto<List<MenuDto.Basic>> getFindAll() {
return ApiResponseDto.ok(menuService.getFindAll());
}
@Operation(summary = "캐시 초기화", description = "메뉴관리 캐시를 초기화합니다.")
@ApiResponses(
value = {
@ApiResponse(
responseCode = "200",
description = "캐시 초기화 성공",
content =
@Content(
mediaType = "application/json",
schema = @Schema(implementation = String.class))),
@ApiResponse(responseCode = "500", description = "서버 오류", content = @Content)
})
@GetMapping("/cache/refresh")
public ApiResponseDto<String> refreshCommonCodeCache() {
menuService.refresh();
return ApiResponseDto.ok("메뉴관리 캐시가 초기화되었습니다.");
}
}

View File

@@ -1,64 +1,64 @@
package com.kamco.cd.training.menu.dto;
import com.kamco.cd.training.common.utils.interfaces.JsonFormatDttm;
import io.swagger.v3.oas.annotations.media.Schema;
import java.time.ZonedDateTime;
import java.util.List;
import lombok.Getter;
import lombok.NoArgsConstructor;
public class MenuDto {
@Schema(name = "Menu Basic", description = "메뉴 기본 정보")
@Getter
@NoArgsConstructor
public static class Basic {
private String menuUid;
private String menuNm;
private String menuUrl;
private String description;
private Long menuOrder;
private Boolean isUse;
private Boolean deleted;
private Long createdUid;
private Long updatedUid;
private List<Basic> children;
@JsonFormatDttm private ZonedDateTime createdDttm;
@JsonFormatDttm private ZonedDateTime updatedDttm;
private String menuApiUrl;
public Basic(
String menuUid,
String menuNm,
String menuUrl,
String description,
Long menuOrder,
Boolean isUse,
Boolean deleted,
Long createdUid,
Long updatedUid,
List<Basic> children,
ZonedDateTime createdDttm,
ZonedDateTime updatedDttm,
String menuApiUrl) {
this.menuUid = menuUid;
this.menuNm = menuNm;
this.menuUrl = menuUrl;
this.description = description;
this.menuOrder = menuOrder;
this.isUse = isUse;
this.deleted = deleted;
this.createdUid = createdUid;
this.updatedUid = updatedUid;
this.children = children;
this.createdDttm = createdDttm;
this.updatedDttm = updatedDttm;
this.menuApiUrl = menuApiUrl;
}
}
}
package com.kamco.cd.training.menu.dto;
import com.kamco.cd.training.common.utils.interfaces.JsonFormatDttm;
import io.swagger.v3.oas.annotations.media.Schema;
import java.time.ZonedDateTime;
import java.util.List;
import lombok.Getter;
import lombok.NoArgsConstructor;
public class MenuDto {
@Schema(name = "Menu Basic", description = "메뉴 기본 정보")
@Getter
@NoArgsConstructor
public static class Basic {
private String menuUid;
private String menuNm;
private String menuUrl;
private String description;
private Long menuOrder;
private Boolean isUse;
private Boolean deleted;
private Long createdUid;
private Long updatedUid;
private List<Basic> children;
@JsonFormatDttm private ZonedDateTime createdDttm;
@JsonFormatDttm private ZonedDateTime updatedDttm;
private String menuApiUrl;
public Basic(
String menuUid,
String menuNm,
String menuUrl,
String description,
Long menuOrder,
Boolean isUse,
Boolean deleted,
Long createdUid,
Long updatedUid,
List<Basic> children,
ZonedDateTime createdDttm,
ZonedDateTime updatedDttm,
String menuApiUrl) {
this.menuUid = menuUid;
this.menuNm = menuNm;
this.menuUrl = menuUrl;
this.description = description;
this.menuOrder = menuOrder;
this.isUse = isUse;
this.deleted = deleted;
this.createdUid = createdUid;
this.updatedUid = updatedUid;
this.children = children;
this.createdDttm = createdDttm;
this.updatedDttm = updatedDttm;
this.menuApiUrl = menuApiUrl;
}
}
}

View File

@@ -1,27 +1,27 @@
package com.kamco.cd.training.menu.service;
import com.kamco.cd.training.menu.dto.MenuDto;
import com.kamco.cd.training.postgres.core.MenuCoreService;
import java.util.List;
import lombok.RequiredArgsConstructor;
import org.springframework.cache.annotation.CacheEvict;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.stereotype.Service;
// training 서버는 Redis 사용하지 않고 Spring Boot 메모리 캐시를 사용함
// => org.springframework.cache.annotation.Cacheable
@Service
@RequiredArgsConstructor
public class MenuService {
private final MenuCoreService menuCoreService;
@Cacheable("trainMenuFindAll")
public List<MenuDto.Basic> getFindAll() {
return menuCoreService.getFindAll();
}
/** 메모리 캐시 초기화 */
@CacheEvict(value = "trainMenuFindAll", allEntries = true)
public void refresh() {}
}
package com.kamco.cd.training.menu.service;
import com.kamco.cd.training.menu.dto.MenuDto;
import com.kamco.cd.training.postgres.core.MenuCoreService;
import java.util.List;
import lombok.RequiredArgsConstructor;
import org.springframework.cache.annotation.CacheEvict;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.stereotype.Service;
// training 서버는 Redis 사용하지 않고 Spring Boot 메모리 캐시를 사용함
// => org.springframework.cache.annotation.Cacheable
@Service
@RequiredArgsConstructor
public class MenuService {
private final MenuCoreService menuCoreService;
@Cacheable("trainMenuFindAll")
public List<MenuDto.Basic> getFindAll() {
return menuCoreService.getFindAll();
}
/** 메모리 캐시 초기화 */
@CacheEvict(value = "trainMenuFindAll", allEntries = true)
public void refresh() {}
}

View File

@@ -1,286 +0,0 @@
package com.kamco.cd.training.model;
import com.kamco.cd.training.config.api.ApiResponseDto;
import com.kamco.cd.training.model.dto.ModelMngDto;
import com.kamco.cd.training.model.dto.ModelMngDto.Basic;
import com.kamco.cd.training.model.service.ModelMngService;
import com.kamco.cd.training.model.service.ModelTrainService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.media.Content;
import io.swagger.v3.oas.annotations.media.Schema;
import io.swagger.v3.oas.annotations.responses.ApiResponse;
import io.swagger.v3.oas.annotations.responses.ApiResponses;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.validation.Valid;
import java.util.List;
import lombok.RequiredArgsConstructor;
import org.springframework.data.domain.Page;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
@RestController
@RequiredArgsConstructor
@Tag(name = "모델관리", description = "모델관리 (학습 모델, 하이퍼파라미터, 메모)")
@RequestMapping("/api/models")
public class ModelMngApiController {
private final ModelMngService modelMngService;
private final ModelTrainService modelTrainService;
@Operation(summary = "학습 모델 목록 조회", description = "학습 모델 목록을 조회합니다")
@ApiResponses(
value = {
@ApiResponse(
responseCode = "200",
description = "검색 성공",
content =
@Content(
mediaType = "application/json",
schema = @Schema(implementation = Page.class))),
@ApiResponse(responseCode = "400", description = "잘못된 검색 조건", content = @Content),
@ApiResponse(responseCode = "500", description = "서버 오류", content = @Content)
})
@GetMapping
public ApiResponseDto<Page<Basic>> findByModels(
@Parameter(description = "상태 코드") @RequestParam(required = false) String status,
@Parameter(description = "페이지 번호") @RequestParam(defaultValue = "0") int page,
@Parameter(description = "페이지 크기") @RequestParam(defaultValue = "20") int size) {
ModelMngDto.SearchReq searchReq = new ModelMngDto.SearchReq(status, page, size);
return ApiResponseDto.ok(modelMngService.findByModels(searchReq));
}
@Operation(summary = "학습 모델 상세 조회", description = "학습 모델의 상세 정보를 UUID로 조회합니다")
@ApiResponses(
value = {
@ApiResponse(
responseCode = "200",
description = "조회 성공",
content =
@Content(
mediaType = "application/json",
schema = @Schema(implementation = ModelMngDto.Detail.class))),
@ApiResponse(responseCode = "404", description = "모델을 찾을 수 없음", content = @Content),
@ApiResponse(responseCode = "500", description = "서버 오류", content = @Content)
})
@GetMapping("/{uuid}")
public ApiResponseDto<ModelMngDto.Detail> getModelDetail(
@Parameter(description = "모델 UUID", example = "b7e99739-6736-45f9-a224-8161ecddf287")
@PathVariable
String uuid) {
return ApiResponseDto.ok(modelMngService.getModelDetailByUuid(uuid));
}
// ==================== 학습 모델학습관리 API (5종) ====================
@Operation(summary = "학습 모델 통합 조회", description = "학습 관리 화면에서 학습 이력 리스트와 현재 상태를 조회합니다")
@ApiResponses(
value = {
@ApiResponse(
responseCode = "200",
description = "조회 성공",
content =
@Content(
mediaType = "application/json",
schema = @Schema(implementation = List.class))),
@ApiResponse(responseCode = "500", description = "서버 오류", content = @Content)
})
@GetMapping("/train")
public ApiResponseDto<List<ModelMngDto.TrainListRes>> getTrainModelList() {
return ApiResponseDto.ok(modelTrainService.getTrainModelList());
}
@Operation(summary = "학습 설정 통합 조회", description = "학습 실행 팝업 구성에 필요한 모든 데이터를 한 번에 반환합니다")
@ApiResponses(
value = {
@ApiResponse(
responseCode = "200",
description = "조회 성공",
content =
@Content(
mediaType = "application/json",
schema = @Schema(implementation = ModelMngDto.FormConfigRes.class))),
@ApiResponse(responseCode = "500", description = "서버 오류", content = @Content)
})
@GetMapping("/train/form-config")
public ApiResponseDto<ModelMngDto.FormConfigRes> getFormConfig() {
return ApiResponseDto.ok(modelTrainService.getFormConfig());
}
@Operation(summary = "하이퍼파라미터 등록", description = "Step 1 에서 파라미터를 수정하여 신규 버전으로 저장합니다")
@ApiResponses(
value = {
@ApiResponse(
responseCode = "200",
description = "등록 성공",
content =
@Content(
mediaType = "application/json",
schema = @Schema(implementation = String.class))),
@ApiResponse(responseCode = "400", description = "잘못된 요청", content = @Content),
@ApiResponse(responseCode = "500", description = "서버 오류", content = @Content)
})
@PostMapping("/hyper-params")
public ApiResponseDto<String> createHyperParam(
@Valid @RequestBody ModelMngDto.HyperParamCreateReq createReq) {
String newVersion = modelTrainService.createHyperParam(createReq);
return ApiResponseDto.ok(newVersion);
}
@Operation(summary = "하이퍼파라미터 단건 조회", description = "특정 버전의 하이퍼파라미터 상세 정보를 조회합니다")
@ApiResponses(
value = {
@ApiResponse(
responseCode = "200",
description = "조회 성공",
content =
@Content(
mediaType = "application/json",
schema = @Schema(implementation = ModelMngDto.HyperParamInfo.class))),
@ApiResponse(responseCode = "404", description = "하이퍼파라미터를 찾을 수 없음", content = @Content),
@ApiResponse(responseCode = "500", description = "서버 오류", content = @Content)
})
@GetMapping("/hyper-params/{hyperVer}")
public ApiResponseDto<ModelMngDto.HyperParamInfo> getHyperParam(
@Parameter(description = "하이퍼파라미터 버전", example = "H1") @PathVariable String hyperVer) {
return ApiResponseDto.ok(modelTrainService.getHyperParam(hyperVer));
}
@Operation(summary = "하이퍼파라미터 삭제", description = "특정 버전의 하이퍼파라미터를 삭제합니다 (H1은 삭제 불가)")
@ApiResponses(
value = {
@ApiResponse(responseCode = "200", description = "삭제 성공", content = @Content),
@ApiResponse(responseCode = "400", description = "H1은 삭제 불가", content = @Content),
@ApiResponse(responseCode = "404", description = "하이퍼파라미터를 찾을 수 없음", content = @Content),
@ApiResponse(responseCode = "500", description = "서버 오류", content = @Content)
})
@DeleteMapping("/hyper-params/{hyperVer}")
public ApiResponseDto<Void> deleteHyperParam(
@Parameter(description = "하이퍼파라미터 버전", example = "V3.99.251221.120518") @PathVariable
String hyperVer) {
modelTrainService.deleteHyperParam(hyperVer);
return ApiResponseDto.ok(null);
}
@Operation(summary = "학습 시작", description = "모든 설정(Step 1~3)을 마치고 최종적으로 학습 프로세스를 시작합니다")
@ApiResponses(
value = {
@ApiResponse(
responseCode = "200",
description = "학습 시작 성공",
content =
@Content(
mediaType = "application/json",
schema = @Schema(implementation = ModelMngDto.TrainStartRes.class))),
@ApiResponse(responseCode = "400", description = "잘못된 요청", content = @Content),
@ApiResponse(responseCode = "500", description = "서버 오류", content = @Content)
})
@PostMapping("/train")
public ApiResponseDto<ModelMngDto.TrainStartRes> startTraining(
@Valid @RequestBody ModelMngDto.TrainStartReq trainReq) {
return ApiResponseDto.ok(modelTrainService.startTraining(trainReq));
}
@Operation(summary = "학습 모델 삭제", description = "목록에서 특정 학습 모델을 삭제합니다")
@ApiResponses(
value = {
@ApiResponse(responseCode = "200", description = "삭제 성공", content = @Content),
@ApiResponse(responseCode = "400", description = "진행 중인 모델은 삭제 불가", content = @Content),
@ApiResponse(responseCode = "404", description = "모델을 찾을 수 없음", content = @Content),
@ApiResponse(responseCode = "500", description = "서버 오류", content = @Content)
})
@DeleteMapping("/train/{uuid}")
public ApiResponseDto<Void> deleteTrainModel(
@Parameter(description = "모델 UUID") @PathVariable String uuid) {
modelTrainService.deleteTrainModel(uuid);
return ApiResponseDto.ok(null);
}
// ==================== Resume Training (학습 재시작) ====================
@Operation(summary = "학습 재시작 정보 조회", description = "중단된 학습의 재시작 가능 여부와 Checkpoint 정보를 조회합니다")
@ApiResponses(
value = {
@ApiResponse(
responseCode = "200",
description = "조회 성공",
content =
@Content(
mediaType = "application/json",
schema = @Schema(implementation = ModelMngDto.ResumeInfo.class))),
@ApiResponse(responseCode = "404", description = "모델을 찾을 수 없음", content = @Content),
@ApiResponse(responseCode = "500", description = "서버 오류", content = @Content)
})
@GetMapping("/train/{uuid}/resume-info")
public ApiResponseDto<ModelMngDto.ResumeInfo> getResumeInfo(
@Parameter(description = "모델 UUID") @PathVariable String uuid) {
return ApiResponseDto.ok(modelTrainService.getResumeInfo(uuid));
}
@Operation(summary = "학습 재시작", description = "중단된 지점(Checkpoint)부터 학습을 재개합니다")
@ApiResponses(
value = {
@ApiResponse(
responseCode = "200",
description = "재시작 성공",
content =
@Content(
mediaType = "application/json",
schema = @Schema(implementation = ModelMngDto.ResumeResponse.class))),
@ApiResponse(responseCode = "400", description = "재시작 불가능한 상태", content = @Content),
@ApiResponse(responseCode = "404", description = "모델을 찾을 수 없음", content = @Content),
@ApiResponse(responseCode = "500", description = "서버 오류", content = @Content)
})
@PostMapping("/train/{uuid}/resume")
public ApiResponseDto<ModelMngDto.ResumeResponse> resumeTraining(
@Parameter(description = "모델 UUID") @PathVariable String uuid,
@Valid @RequestBody ModelMngDto.ResumeRequest resumeReq) {
return ApiResponseDto.ok(modelTrainService.resumeTraining(uuid, resumeReq));
}
// ==================== Best Epoch Setting (Best Epoch 설정) ====================
@Operation(summary = "Best Epoch 설정", description = "사용자가 직접 Best Epoch를 선택하여 설정합니다")
@ApiResponses(
value = {
@ApiResponse(
responseCode = "200",
description = "설정 성공",
content =
@Content(
mediaType = "application/json",
schema = @Schema(implementation = ModelMngDto.BestEpochResponse.class))),
@ApiResponse(responseCode = "404", description = "모델을 찾을 수 없음", content = @Content),
@ApiResponse(responseCode = "500", description = "서버 오류", content = @Content)
})
@PostMapping("/train/{uuid}/best-epoch")
public ApiResponseDto<ModelMngDto.BestEpochResponse> setBestEpoch(
@Parameter(description = "모델 UUID") @PathVariable String uuid,
@Valid @RequestBody ModelMngDto.BestEpochRequest bestEpochReq) {
return ApiResponseDto.ok(modelTrainService.setBestEpoch(uuid, bestEpochReq));
}
@Operation(summary = "Epoch별 성능 지표 조회", description = "학습된 모델의 Epoch별 성능 지표를 조회합니다")
@ApiResponses(
value = {
@ApiResponse(
responseCode = "200",
description = "조회 성공",
content =
@Content(
mediaType = "application/json",
schema = @Schema(implementation = List.class))),
@ApiResponse(responseCode = "404", description = "모델을 찾을 수 없음", content = @Content),
@ApiResponse(responseCode = "500", description = "서버 오류", content = @Content)
})
@GetMapping("/train/{uuid}/epoch-metrics")
public ApiResponseDto<List<ModelMngDto.EpochMetric>> getEpochMetrics(
@Parameter(description = "모델 UUID") @PathVariable String uuid) {
return ApiResponseDto.ok(modelTrainService.getEpochMetrics(uuid));
}
}

View File

@@ -0,0 +1,225 @@
package com.kamco.cd.training.model;
import com.kamco.cd.training.config.api.ApiResponseDto;
import com.kamco.cd.training.model.dto.ModelTrainDetailDto;
import com.kamco.cd.training.model.dto.ModelTrainDetailDto.MappingDataset;
import com.kamco.cd.training.model.dto.ModelTrainDetailDto.ModelBestEpoch;
import com.kamco.cd.training.model.dto.ModelTrainDetailDto.ModelTestMetrics;
import com.kamco.cd.training.model.dto.ModelTrainDetailDto.ModelTrainMetrics;
import com.kamco.cd.training.model.dto.ModelTrainDetailDto.ModelValidationMetrics;
import com.kamco.cd.training.model.dto.ModelTrainDetailDto.TransferDetailDto;
import com.kamco.cd.training.model.dto.ModelTrainMngDto.Basic;
import com.kamco.cd.training.model.service.ModelTrainDetailService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.media.ArraySchema;
import io.swagger.v3.oas.annotations.media.Content;
import io.swagger.v3.oas.annotations.media.Schema;
import io.swagger.v3.oas.annotations.responses.ApiResponse;
import io.swagger.v3.oas.annotations.responses.ApiResponses;
import io.swagger.v3.oas.annotations.tags.Tag;
import java.util.List;
import java.util.UUID;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
@RequiredArgsConstructor
@Tag(name = "모델학습 관리", description = "어드민 홈 > 모델학습관리 > 모델관리 > 목록")
@RequestMapping("/api/models")
public class ModelTrainDetailApiController {
private final ModelTrainDetailService modelTrainDetailService;
@Operation(summary = "모델학습관리> 모델관리 > 상세정보탭 > 학습 진행정보", description = "학습 진행정보, 모델학습 정보 API")
@ApiResponses(
value = {
@ApiResponse(
responseCode = "200",
description = "조회 성공",
content =
@Content(
mediaType = "application/json",
schema = @Schema(implementation = Basic.class))),
@ApiResponse(responseCode = "404", description = "데이터셋을 찾을 수 없음", content = @Content),
@ApiResponse(responseCode = "500", description = "서버 오류", content = @Content)
})
@GetMapping("/detail/{uuid}")
public ApiResponseDto<Basic> findByModelByUUID(
@Parameter(description = "모델학습 uuid", example = "7fbdff54-ea87-4b02-90d1-955fa2a3457e")
@PathVariable
UUID uuid) {
return ApiResponseDto.ok(modelTrainDetailService.findByModelByUUID(uuid));
}
@Operation(summary = "모델학습관리> 모델관리 > 상세정보탭 > 요약정보", description = "상세정보 탭 요약정보 API")
@ApiResponses(
value = {
@ApiResponse(
responseCode = "200",
description = "조회 성공",
content =
@Content(
mediaType = "application/json",
schema = @Schema(implementation = ModelTrainDetailDto.DetailSummary.class))),
@ApiResponse(responseCode = "404", description = "데이터셋을 찾을 수 없음", content = @Content),
@ApiResponse(responseCode = "500", description = "서버 오류", content = @Content)
})
@GetMapping("/summary/{uuid}")
public ApiResponseDto<ModelTrainDetailDto.DetailSummary> getModelDetailSummary(
@Parameter(description = "모델학습 uuid", example = "7fbdff54-ea87-4b02-90d1-955fa2a3457e")
@PathVariable
UUID uuid) {
return ApiResponseDto.ok(modelTrainDetailService.getModelDetailSummary(uuid));
}
@Operation(summary = "모델학습관리> 모델관리 > 상세정보탭 > 하이퍼파라미터 요약 정보", description = "하이퍼파라미터 요약 정보 API")
@ApiResponses(
value = {
@ApiResponse(
responseCode = "200",
description = "조회 성공",
content =
@Content(
mediaType = "application/json",
schema = @Schema(implementation = ModelTrainDetailDto.HyperSummary.class))),
@ApiResponse(responseCode = "404", description = "데이터셋을 찾을 수 없음", content = @Content),
@ApiResponse(responseCode = "500", description = "서버 오류", content = @Content)
})
@GetMapping("/hyper-summary/{uuid}")
public ApiResponseDto<ModelTrainDetailDto.HyperSummary> getByModelHyperParamSummary(
@Parameter(description = "모델학습 uuid", example = "7fbdff54-ea87-4b02-90d1-955fa2a3457e")
@PathVariable
UUID uuid) {
return ApiResponseDto.ok(modelTrainDetailService.getByModelHyperParamSummary(uuid));
}
@Operation(summary = "모델학습관리> 모델관리 > 상세정보탭 > 데이터셋 정보", description = "모델학습 상세 데이터셋 정보 API")
@ApiResponses({
@ApiResponse(
responseCode = "200",
description = "조회 성공",
content =
@Content(
mediaType = "application/json",
array = @ArraySchema(schema = @Schema(implementation = MappingDataset.class)))),
@ApiResponse(responseCode = "404", description = "데이터셋을 찾을 수 없음"),
@ApiResponse(responseCode = "500", description = "서버 오류")
})
@GetMapping("/mapp-dataset/{uuid}")
public ApiResponseDto<List<MappingDataset>> getByModelMappingDataset(
@Parameter(description = "모델학습 uuid", example = "7fbdff54-ea87-4b02-90d1-955fa2a3457e")
@PathVariable
UUID uuid) {
return ApiResponseDto.ok(modelTrainDetailService.getByModelMappingDataset(uuid));
}
@Operation(summary = "모델관리 > 전이 학습 실행설정 > 모델선택", description = "모델선택 정보 API")
@ApiResponses(
value = {
@ApiResponse(
responseCode = "200",
description = "조회 성공",
content =
@Content(
mediaType = "application/json",
schema = @Schema(implementation = TransferDetailDto.class))),
@ApiResponse(responseCode = "404", description = "데이터셋을 찾을 수 없음", content = @Content),
@ApiResponse(responseCode = "500", description = "서버 오류", content = @Content)
})
@GetMapping("/transfer/detail/{uuid}")
public ApiResponseDto<TransferDetailDto> getTransferDetail(
@Parameter(description = "모델 uuid", example = "7fbdff54-ea87-4b02-90d1-955fa2a3457e")
@PathVariable
UUID uuid) {
return ApiResponseDto.ok(modelTrainDetailService.getTransferDetail(uuid));
}
@Operation(summary = "모델관리 > 모델 상세 > 성능 정보 (Train)", description = "모델 상세 > 성능 정보 (Train) API")
@ApiResponses(
value = {
@ApiResponse(
responseCode = "200",
description = "조회 성공",
content =
@Content(
mediaType = "application/json",
schema = @Schema(implementation = TransferDetailDto.class))),
@ApiResponse(responseCode = "404", description = "데이터셋을 찾을 수 없음", content = @Content),
@ApiResponse(responseCode = "500", description = "서버 오류", content = @Content)
})
@GetMapping("/metrics/train/{uuid}")
public ApiResponseDto<List<ModelTrainMetrics>> getModelTrainMetricResult(
@Parameter(description = "모델 uuid", example = "95cb116c-380a-41c0-98d8-4d1142f15bbf")
@PathVariable
UUID uuid) {
return ApiResponseDto.ok(modelTrainDetailService.getModelTrainMetricResult(uuid));
}
@Operation(
summary = "모델관리 > 모델 상세 > 성능 정보 (Validation)",
description = "모델 상세 > 성능 정보 (Validation) API")
@ApiResponses(
value = {
@ApiResponse(
responseCode = "200",
description = "조회 성공",
content =
@Content(
mediaType = "application/json",
schema = @Schema(implementation = TransferDetailDto.class))),
@ApiResponse(responseCode = "404", description = "데이터셋을 찾을 수 없음", content = @Content),
@ApiResponse(responseCode = "500", description = "서버 오류", content = @Content)
})
@GetMapping("/metrics/validation/{uuid}")
public ApiResponseDto<List<ModelValidationMetrics>> getModelValidationMetricResult(
@Parameter(description = "모델 uuid", example = "95cb116c-380a-41c0-98d8-4d1142f15bbf")
@PathVariable
UUID uuid) {
return ApiResponseDto.ok(modelTrainDetailService.getModelValidationMetricResult(uuid));
}
@Operation(summary = "모델관리 > 모델 상세 > 성능 정보 (Test)", description = "모델 상세 > 성능 정보 (Test) API")
@ApiResponses(
value = {
@ApiResponse(
responseCode = "200",
description = "조회 성공",
content =
@Content(
mediaType = "application/json",
schema = @Schema(implementation = TransferDetailDto.class))),
@ApiResponse(responseCode = "404", description = "데이터셋을 찾을 수 없음", content = @Content),
@ApiResponse(responseCode = "500", description = "서버 오류", content = @Content)
})
@GetMapping("/metrics/test/{uuid}")
public ApiResponseDto<List<ModelTestMetrics>> getModelTestMetricResult(
@Parameter(description = "모델 uuid", example = "95cb116c-380a-41c0-98d8-4d1142f15bbf")
@PathVariable
UUID uuid) {
return ApiResponseDto.ok(modelTrainDetailService.getModelTestMetricResult(uuid));
}
@Operation(summary = "모델관리 > 모델 상세 > 성능 정보 (Test)", description = "모델 상세 > 성능 정보 (Test) API")
@ApiResponses(
value = {
@ApiResponse(
responseCode = "200",
description = "조회 성공",
content =
@Content(
mediaType = "application/json",
schema = @Schema(implementation = TransferDetailDto.class))),
@ApiResponse(responseCode = "404", description = "데이터셋을 찾을 수 없음", content = @Content),
@ApiResponse(responseCode = "500", description = "서버 오류", content = @Content)
})
@GetMapping("/best-epoch/{uuid}")
public ApiResponseDto<ModelBestEpoch> getModelTrainBestEpoch(
@Parameter(description = "모델 uuid", example = "95cb116c-380a-41c0-98d8-4d1142f15bbf")
@PathVariable
UUID uuid) {
return ApiResponseDto.ok(modelTrainDetailService.getModelTrainBestEpoch(uuid));
}
}

View File

@@ -0,0 +1,169 @@
package com.kamco.cd.training.model;
import com.kamco.cd.training.config.api.ApiResponseDto;
import com.kamco.cd.training.dataset.dto.DatasetDto;
import com.kamco.cd.training.dataset.dto.DatasetDto.DatasetReq;
import com.kamco.cd.training.dataset.dto.DatasetDto.SelectDataSet;
import com.kamco.cd.training.model.dto.ModelConfigDto;
import com.kamco.cd.training.model.dto.ModelTrainMngDto;
import com.kamco.cd.training.model.dto.ModelTrainMngDto.Basic;
import com.kamco.cd.training.model.service.ModelTrainMngService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.media.Content;
import io.swagger.v3.oas.annotations.media.Schema;
import io.swagger.v3.oas.annotations.responses.ApiResponse;
import io.swagger.v3.oas.annotations.responses.ApiResponses;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.validation.Valid;
import java.util.List;
import java.util.UUID;
import lombok.RequiredArgsConstructor;
import org.springframework.data.domain.Page;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
@RestController
@RequiredArgsConstructor
@Tag(name = "모델학습 관리", description = "어드민 홈 > 모델학습관리 > 모델관리 > 목록")
@RequestMapping("/api/models")
public class ModelTrainMngApiController {
private final ModelTrainMngService modelTrainMngService;
@Operation(summary = "모델학습 목록 조회", description = "모델학습 목록 조회 API")
@ApiResponses(
value = {
@ApiResponse(
responseCode = "200",
description = "검색 성공",
content =
@Content(
mediaType = "application/json",
schema = @Schema(implementation = Page.class))),
@ApiResponse(responseCode = "400", description = "잘못된 검색 조건", content = @Content),
@ApiResponse(responseCode = "500", description = "서버 오류", content = @Content)
})
@GetMapping("/list")
public ApiResponseDto<Page<Basic>> findByModelList(
@Parameter(
description = "상태코드",
example = "IN_PROGRESS",
schema = @Schema(allowableValues = {"", "IN_PROGRESS", "COMPLETED"}))
@RequestParam(required = false)
String status,
@Parameter(
description = "모델",
example = "G1",
schema = @Schema(allowableValues = {"G1", "G2", "G3"}))
@RequestParam(required = false)
String modelNo,
@Parameter(description = "페이지 번호") @RequestParam(defaultValue = "0") int page,
@Parameter(description = "페이지 크기") @RequestParam(defaultValue = "20") int size) {
ModelTrainMngDto.SearchReq searchReq =
new ModelTrainMngDto.SearchReq(status, modelNo, page, size);
return ApiResponseDto.ok(modelTrainMngService.getModelList(searchReq));
}
@Operation(summary = "학습 모델 삭제", description = "학습 모델 삭제 API")
@ApiResponses(
value = {
@ApiResponse(responseCode = "200", description = "삭제 성공", content = @Content),
@ApiResponse(responseCode = "409", description = "G1_000001 삭제 불가", content = @Content)
})
@DeleteMapping("/{uuid}")
public ApiResponseDto<Void> deleteModelTrain(
@Parameter(description = "학습 모델 uuid", example = "f2b02229-90f2-45f5-92ea-c56cf1c29f79")
@PathVariable UUID uuid) {
modelTrainMngService.deleteModelTrain(uuid);
return ApiResponseDto.ok(null);
}
@Operation(summary = "학습 모델 등록", description = "학습 모델 등록 API")
@ApiResponses(
value = {
@ApiResponse(responseCode = "200", description = "등록 성공", content = @Content),
@ApiResponse(responseCode = "500", description = "서버 오류", content = @Content)
})
@PostMapping
public ApiResponseDto<UUID> createModelTrain(@Valid @RequestBody ModelTrainMngDto.AddReq req) {
return ApiResponseDto.ok(modelTrainMngService.createModelTrain(req));
}
@Operation(summary = "모델학습 config 정보 조회", description = "모델학습 config 정보 조회 API")
@ApiResponses(
value = {
@ApiResponse(
responseCode = "200",
description = "검색 성공",
content =
@Content(
mediaType = "application/json",
schema = @Schema(implementation = ModelConfigDto.Basic.class))),
@ApiResponse(responseCode = "400", description = "잘못된 검색 조건", content = @Content),
@ApiResponse(responseCode = "500", description = "서버 오류", content = @Content)
})
@GetMapping("/config/{uuid}")
public ApiResponseDto<ModelConfigDto.Basic> updateModelTrain(
@Parameter(description = "모델학습 uuid", example = "7fbdff54-ea87-4b02-90d1-955fa2a3457e")
@PathVariable
UUID uuid) {
return ApiResponseDto.ok(modelTrainMngService.getModelConfigByModelId(uuid));
}
@Operation(summary = "모델별 데이터셋 목록 조회", description = "모델별 데이터셋 목록 조회 API")
@ApiResponses(
value = {
@ApiResponse(
responseCode = "200",
description = "조회 성공",
content =
@Content(
mediaType = "application/json",
schema = @Schema(implementation = DatasetDto.Basic.class))),
@ApiResponse(responseCode = "404", description = "데이터셋을 찾을 수 없음", content = @Content),
@ApiResponse(responseCode = "500", description = "서버 오류", content = @Content)
})
@GetMapping("/select-dataset-list")
public ApiResponseDto<List<SelectDataSet>> getDatasetSelectList(
@Parameter(
description = "모델 구분",
example = "",
schema = @Schema(allowableValues = {"G1", "G2", "G3"}))
@RequestParam
String modelType,
@Parameter(
description = "선택 구분",
example = "",
schema = @Schema(allowableValues = {"CURRENT", "DELIVER", "PRODUCTION"}))
@RequestParam
String selectType) {
DatasetReq req = new DatasetReq();
req.setModelNo(modelType);
req.setDataType(selectType);
return ApiResponseDto.ok(modelTrainMngService.getDatasetSelectList(req));
}
@Operation(summary = "모델학습 1단계 실행중인 것이 있는지 count", description = "모델학습 1단계 실행중인 것이 있는지 count")
@ApiResponses(
value = {
@ApiResponse(
responseCode = "200",
description = "검색 성공",
content =
@Content(
mediaType = "application/json",
schema = @Schema(implementation = Long.class))),
@ApiResponse(responseCode = "400", description = "잘못된 검색 조건", content = @Content),
@ApiResponse(responseCode = "500", description = "서버 오류", content = @Content)
})
@GetMapping("/ing-training-cnt")
public ApiResponseDto<Long> findModelStep1InProgressCnt() {
return ApiResponseDto.ok(modelTrainMngService.findModelStep1InProgressCnt());
}
}

View File

@@ -1,219 +0,0 @@
package com.kamco.cd.training.model.dto;
import com.kamco.cd.training.common.utils.interfaces.JsonFormatDttm;
import com.kamco.cd.training.postgres.entity.ModelHyperParamEntity;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotBlank;
import java.time.ZonedDateTime;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
public class HyperParamDto {
@Schema(name = "HyperParam Basic", description = "하이퍼파라미터 기본 정보")
@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
public static class Basic {
private String hyperVer;
// Important
private String backbone;
private String inputSize;
private String cropSize;
private Integer epochCnt;
private Integer batchSize;
// Architecture
private Double dropPathRate;
private Integer frozenStages;
private String neckPolicy;
private String decoderChannels;
private String classWeight;
private Integer numLayers;
// Optimization
private Double learningRate;
private Double weightDecay;
private Double layerDecayRate;
private Boolean ddpFindUnusedParams;
private Integer ignoreIndex;
// Data
private Integer trainNumWorkers;
private Integer valNumWorkers;
private Integer testNumWorkers;
private Boolean trainShuffle;
private Boolean trainPersistent;
private Boolean valPersistent;
// Evaluation
private String metrics;
private String saveBest;
private String saveBestRule;
private Integer valInterval;
private Integer logInterval;
private Integer visInterval;
// Hardware
private Integer gpuCnt;
private String gpuIds;
private Integer masterPort;
// Augmentation
private Double rotProb;
private Double flipProb;
private String rotDegree;
private Double exchangeProb;
private Integer brightnessDelta;
private String contrastRange;
private String saturationRange;
private Integer hueDelta;
// Legacy (deprecated)
private Double dropoutRatio;
private Integer cnnFilterCnt;
// Common
private String memo;
@JsonFormatDttm private ZonedDateTime createdDttm;
public Basic(ModelHyperParamEntity entity) {
this.hyperVer = entity.getHyperVer();
// Important
this.backbone = entity.getBackbone();
this.inputSize = entity.getInputSize();
this.cropSize = entity.getCropSize();
this.epochCnt = entity.getEpochCnt();
this.batchSize = entity.getBatchSize();
// Architecture
this.dropPathRate = entity.getDropPathRate();
this.frozenStages = entity.getFrozenStages();
this.neckPolicy = entity.getNeckPolicy();
this.decoderChannels = entity.getDecoderChannels();
this.classWeight = entity.getClassWeight();
this.numLayers = entity.getNumLayers();
// Optimization
this.learningRate = entity.getLearningRate();
this.weightDecay = entity.getWeightDecay();
this.layerDecayRate = entity.getLayerDecayRate();
this.ddpFindUnusedParams = entity.getDdpFindUnusedParams();
this.ignoreIndex = entity.getIgnoreIndex();
// Data
this.trainNumWorkers = entity.getTrainNumWorkers();
this.valNumWorkers = entity.getValNumWorkers();
this.testNumWorkers = entity.getTestNumWorkers();
this.trainShuffle = entity.getTrainShuffle();
this.trainPersistent = entity.getTrainPersistent();
this.valPersistent = entity.getValPersistent();
// Evaluation
this.metrics = entity.getMetrics();
this.saveBest = entity.getSaveBest();
this.saveBestRule = entity.getSaveBestRule();
this.valInterval = entity.getValInterval();
this.logInterval = entity.getLogInterval();
this.visInterval = entity.getVisInterval();
// Hardware
this.gpuCnt = entity.getGpuCnt();
this.gpuIds = entity.getGpuIds();
this.masterPort = entity.getMasterPort();
// Augmentation
this.rotProb = entity.getRotProb();
this.flipProb = entity.getFlipProb();
this.rotDegree = entity.getRotDegree();
this.exchangeProb = entity.getExchangeProb();
this.brightnessDelta = entity.getBrightnessDelta();
this.contrastRange = entity.getContrastRange();
this.saturationRange = entity.getSaturationRange();
this.hueDelta = entity.getHueDelta();
// Legacy
this.dropoutRatio = entity.getDropoutRatio();
this.cnnFilterCnt = entity.getCnnFilterCnt();
// Common
this.memo = entity.getMemo();
this.createdDttm = entity.getCreatedDttm();
}
}
@Schema(name = "HyperParam AddReq", description = "하이퍼파라미터 등록 요청")
@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
public static class AddReq {
@NotBlank(message = "버전명은 필수입니다")
private String hyperVer;
// Important
private String backbone;
private String inputSize;
private String cropSize;
private Integer epochCnt;
private Integer batchSize;
// Architecture
private Double dropPathRate;
private Integer frozenStages;
private String neckPolicy;
private String decoderChannels;
private String classWeight;
private Integer numLayers;
// Optimization
private Double learningRate;
private Double weightDecay;
private Double layerDecayRate;
private Boolean ddpFindUnusedParams;
private Integer ignoreIndex;
// Data
private Integer trainNumWorkers;
private Integer valNumWorkers;
private Integer testNumWorkers;
private Boolean trainShuffle;
private Boolean trainPersistent;
private Boolean valPersistent;
// Evaluation
private String metrics;
private String saveBest;
private String saveBestRule;
private Integer valInterval;
private Integer logInterval;
private Integer visInterval;
// Hardware
private Integer gpuCnt;
private String gpuIds;
private Integer masterPort;
// Augmentation
private Double rotProb;
private Double flipProb;
private String rotDegree;
private Double exchangeProb;
private Integer brightnessDelta;
private String contrastRange;
private String saturationRange;
private Integer hueDelta;
// Legacy (deprecated)
private Double dropoutRatio;
private Integer cnnFilterCnt;
// Common
private String memo;
}
}

View File

@@ -0,0 +1,23 @@
package com.kamco.cd.training.model.dto;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
public class ModelConfigDto {
@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
public static class Basic {
private Long configId;
private Long modelId;
private Integer epochCount;
private Float trainPercent;
private Float validationPercent;
private Float testPercent;
private String memo;
}
}

View File

@@ -1,595 +0,0 @@
package com.kamco.cd.training.model.dto;
import com.kamco.cd.training.common.utils.interfaces.JsonFormatDttm;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotEmpty;
import jakarta.validation.constraints.NotNull;
import java.time.ZonedDateTime;
import java.util.List;
import java.util.Map;
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 ModelMngDto {
@Schema(name = "모델관리 목록 조회", description = "모델관리 목록 조회")
@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
@Builder
public static class Basic {
private Long id;
private String modelNm;
@JsonFormatDttm private ZonedDateTime startDttm;
@JsonFormatDttm private ZonedDateTime trainingEndDttm;
@JsonFormatDttm private ZonedDateTime testEndDttm;
private String durationDttm;
private String processStage;
private String statusCd;
private String status;
}
@Schema(name = "searchReq", description = "모델 관리 목록조회 파라미터")
@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
public static class SearchReq {
private String status;
// 페이징 파라미터
private int page = 0;
private int size = 20;
public Pageable toPageable() {
return PageRequest.of(page, size);
}
}
@Schema(name = "Detail", description = "모델 상세 정보")
@Getter
@Builder
public static class Detail {
private String uuid;
private String modelVer;
private String hyperVer;
private String epochVer;
private String processStep;
private String statusCd;
private String statusText;
@JsonFormatDttm private ZonedDateTime trainStartDttm;
private Integer epochCnt;
private String datasetRatio;
private Integer bestEpoch;
private Integer confirmedBestEpoch;
@JsonFormatDttm private ZonedDateTime step1EndDttm;
private String step1Duration;
@JsonFormatDttm private ZonedDateTime step2EndDttm;
private String step2Duration;
private Integer progressRate;
@JsonFormatDttm private ZonedDateTime createdDttm;
@JsonFormatDttm private ZonedDateTime updatedDttm;
private String modelPath;
private String errorMsg;
}
@Schema(name = "TrainListRes", description = "학습 모델 목록 응답")
@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
@Builder
public static class TrainListRes {
private String uuid;
private String modelVer;
private String status;
private String processStep;
@JsonFormatDttm private ZonedDateTime trainStartDttm;
private Integer progressRate;
private Integer epochCnt;
@JsonFormatDttm private ZonedDateTime step1EndDttm;
private String step1Duration;
@JsonFormatDttm private ZonedDateTime step2EndDttm;
private String step2Duration;
@JsonFormatDttm private ZonedDateTime createdDttm;
private String errorMsg;
private Boolean canResume;
private Integer lastCheckpointEpoch;
}
@Schema(name = "FormConfigRes", description = "학습 설정 통합 조회 응답")
@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
@Builder
public static class FormConfigRes {
private Boolean isTrainAvailable;
private List<HyperParamInfo> hyperParams;
private List<DatasetInfo> datasets;
private String runningModelUuid;
}
@Schema(name = "HyperParamInfo", description = "하이퍼파라미터 정보")
@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
@Builder
public static class HyperParamInfo {
@Schema(description = "하이퍼파라미터 버전", example = "V3.99.251221.120518")
private String hyperVer;
// Important
@Schema(description = "백본", example = "large")
private String backbone;
@Schema(description = "입력 사이즈", example = "256,256")
private String inputSize;
@Schema(description = "크롭 사이즈", example = "256,256")
private String cropSize;
@Schema(description = "에폭 수", example = "200")
private Integer epochCnt;
@Schema(description = "배치 사이즈", example = "16")
private Integer batchSize;
// Architecture
@Schema(description = "Drop Path Rate", example = "0.3")
private Double dropPathRate;
@Schema(description = "Frozen Stages", example = "-1")
private Integer frozenStages;
@Schema(description = "Neck Policy", example = "abs_diff")
private String neckPolicy;
@Schema(description = "Decoder Channels", example = "512,256,128,64")
private String decoderChannels;
@Schema(description = "Class Weight", example = "1,1")
private String classWeight;
@Schema(description = "레이어 수", example = "24")
private Integer numLayers;
// Optimization
@Schema(description = "Learning Rate", example = "0.00006")
private Double learningRate;
@Schema(description = "Weight Decay", example = "0.05")
private Double weightDecay;
@Schema(description = "Layer Decay Rate", example = "0.9")
private Double layerDecayRate;
@Schema(description = "DDP Unused Params 찾기", example = "true")
private Boolean ddpFindUnusedParams;
@Schema(description = "Ignore Index", example = "255")
private Integer ignoreIndex;
// Data
@Schema(description = "Train Workers", example = "16")
private Integer trainNumWorkers;
@Schema(description = "Val Workers", example = "8")
private Integer valNumWorkers;
@Schema(description = "Test Workers", example = "8")
private Integer testNumWorkers;
@Schema(description = "Train Shuffle", example = "true")
private Boolean trainShuffle;
@Schema(description = "Train Persistent", example = "true")
private Boolean trainPersistent;
@Schema(description = "Val Persistent", example = "true")
private Boolean valPersistent;
// Evaluation
@Schema(description = "Metrics", example = "mFscore,mIoU")
private String metrics;
@Schema(description = "Save Best", example = "changed_fscore")
private String saveBest;
@Schema(description = "Save Best Rule", example = "greater")
private String saveBestRule;
@Schema(description = "Val Interval", example = "10")
private Integer valInterval;
@Schema(description = "Log Interval", example = "400")
private Integer logInterval;
@Schema(description = "Vis Interval", example = "1")
private Integer visInterval;
// Hardware
@Schema(description = "GPU 수", example = "4")
private Integer gpuCnt;
@Schema(description = "GPU IDs", example = "0,1,2,3")
private String gpuIds;
@Schema(description = "Master Port", example = "1122")
private Integer masterPort;
// Augmentation
@Schema(description = "Rotation 확률", example = "0.5")
private Double rotProb;
@Schema(description = "Flip 확률", example = "0.5")
private Double flipProb;
@Schema(description = "Rotation 각도", example = "-20,20")
private String rotDegree;
@Schema(description = "Exchange 확률", example = "0.5")
private Double exchangeProb;
@Schema(description = "Brightness Delta", example = "10")
private Integer brightnessDelta;
@Schema(description = "Contrast Range", example = "0.8,1.2")
private String contrastRange;
@Schema(description = "Saturation Range", example = "0.8,1.2")
private String saturationRange;
@Schema(description = "Hue Delta", example = "10")
private Integer hueDelta;
// Legacy
private Double dropoutRatio;
private Integer cnnFilterCnt;
// Common
@Schema(description = "메모", example = "안녕하세요 캠코담당자 입니다. 하이퍼파라미터 신규등록합니다")
private String memo;
@JsonFormatDttm private ZonedDateTime createdDttm;
}
@Schema(name = "DatasetInfo", description = "데이터셋 정보")
@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
@Builder
public static class DatasetInfo {
private Long id;
private String title;
private String groupTitle;
private Long totalItems;
private String totalSize;
private Map<String, Integer> classCounts;
private String memo;
@JsonFormatDttm private ZonedDateTime createdDttm;
}
@Schema(name = "HyperParamCreateReq", description = "하이퍼파라미터 등록 요청")
@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
public static class HyperParamCreateReq {
// baseHyperVer는 필수 아님 (신규 생성 시 H1으로 자동 설정)
@Schema(description = "기준이 되는 하이퍼파라미터 버전", example = "H3")
private String baseHyperVer;
@NotBlank(message = "신규 버전명은 필수입니다")
@Schema(description = "새로 생성할 하이퍼파라미터 버전명", example = "V3.99.251221.120518")
private String newHyperVer;
// Important - 필수 필드
@NotBlank(message = "Backbone은 필수입니다")
@Schema(example = "large")
private String backbone;
@NotBlank(message = "Input Size는 필수입니다")
@Schema(example = "256,256")
private String inputSize;
@NotBlank(message = "Crop Size는 필수입니다")
@Schema(example = "256,256")
private String cropSize;
@NotNull(message = "Epoch Count는 필수입니다")
@Schema(example = "200")
private Integer epochCnt;
@NotNull(message = "Batch Size는 필수입니다")
@Schema(example = "16")
private Integer batchSize;
// Architecture - 필수 필드
@NotNull(message = "Drop Path Rate는 필수입니다")
@Schema(example = "0.3")
private Double dropPathRate;
@NotNull(message = "Frozen Stages는 필수입니다")
@Schema(example = "-1")
private Integer frozenStages;
@NotBlank(message = "Neck Policy는 필수입니다")
@Schema(example = "abs_diff")
private String neckPolicy;
@NotBlank(message = "Decoder Channels는 필수입니다")
@Schema(example = "512,256,128,64")
private String decoderChannels;
@NotBlank(message = "Class Weight는 필수입니다")
@Schema(example = "1,1")
private String classWeight;
// numLayers는 필수 아님
@Schema(example = "24")
private Integer numLayers;
// Optimization - 필수 필드
@NotNull(message = "Learning Rate는 필수입니다")
@Schema(example = "0.00006")
private Double learningRate;
@NotNull(message = "Weight Decay는 필수입니다")
@Schema(example = "0.05")
private Double weightDecay;
@NotNull(message = "Layer Decay Rate는 필수입니다")
@Schema(example = "0.9")
private Double layerDecayRate;
@NotNull(message = "DDP Find Unused Params는 필수입니다")
@Schema(example = "true")
private Boolean ddpFindUnusedParams;
@NotNull(message = "Ignore Index는 필수입니다")
@Schema(example = "255")
private Integer ignoreIndex;
// Data - 필수 필드
@NotNull(message = "Train Num Workers는 필수입니다")
@Schema(example = "16")
private Integer trainNumWorkers;
@NotNull(message = "Val Num Workers는 필수입니다")
@Schema(example = "8")
private Integer valNumWorkers;
@NotNull(message = "Test Num Workers는 필수입니다")
@Schema(example = "8")
private Integer testNumWorkers;
@NotNull(message = "Train Shuffle는 필수입니다")
@Schema(example = "true")
private Boolean trainShuffle;
@NotNull(message = "Train Persistent는 필수입니다")
@Schema(example = "true")
private Boolean trainPersistent;
@NotNull(message = "Val Persistent는 필수입니다")
@Schema(example = "true")
private Boolean valPersistent;
// Evaluation - 필수 필드
@NotBlank(message = "Metrics는 필수입니다")
@Schema(example = "mFscore,mIoU")
private String metrics;
@NotBlank(message = "Save Best는 필수입니다")
@Schema(example = "changed_fscore")
private String saveBest;
@NotBlank(message = "Save Best Rule은 필수입니다")
@Schema(example = "greater")
private String saveBestRule;
@NotNull(message = "Val Interval은 필수입니다")
@Schema(example = "10")
private Integer valInterval;
@NotNull(message = "Log Interval은 필수입니다")
@Schema(example = "400")
private Integer logInterval;
@NotNull(message = "Vis Interval은 필수입니다")
@Schema(example = "1")
private Integer visInterval;
// Hardware - 필수 아님 (예외 항목)
@Schema(example = "4")
private Integer gpuCnt;
@Schema(example = "0,1,2,3")
private String gpuIds;
@Schema(example = "1122")
private Integer masterPort;
// Augmentation - 필수 필드
@NotNull(message = "Rotation Probability는 필수입니다")
@Schema(example = "0.5")
private Double rotProb;
@NotNull(message = "Flip Probability는 필수입니다")
@Schema(example = "0.5")
private Double flipProb;
@NotBlank(message = "Rotation Degree는 필수입니다")
@Schema(example = "-20,20")
private String rotDegree;
@NotNull(message = "Exchange Probability는 필수입니다")
@Schema(example = "0.5")
private Double exchangeProb;
@NotNull(message = "Brightness Delta는 필수입니다")
@Schema(example = "10")
private Integer brightnessDelta;
@NotBlank(message = "Contrast Range는 필수입니다")
@Schema(example = "0.8,1.2")
private String contrastRange;
@NotBlank(message = "Saturation Range는 필수입니다")
@Schema(example = "0.8,1.2")
private String saturationRange;
@NotNull(message = "Hue Delta는 필수입니다")
@Schema(example = "10")
private Integer hueDelta;
// Legacy - 필수 아님 (예외 항목)
private Double dropoutRatio;
private Integer cnnFilterCnt;
// Common - 필수 아님 (예외 항목)
@Schema(example = "안녕하세요 캠코담당자 입니다. 하이퍼파라미터 신규등록합니다")
private String memo;
}
@Schema(name = "TrainStartReq", description = "학습 시작 요청")
@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
public static class TrainStartReq {
@NotBlank(message = "하이퍼파라미터 버전은 필수입니다")
@Schema(example = "V3.99.251221.120518")
private String hyperVer;
@NotEmpty(message = "데이터셋은 최소 1개 이상 선택해야 합니다")
private List<Long> datasetIds;
@NotNull(message = "에폭 수는 필수입니다")
@jakarta.validation.constraints.Min(value = 1, message = "에폭 수는 최소 1 이상이어야 합니다")
@jakarta.validation.constraints.Max(value = 200, message = "에폭 수는 최대 200까지 설정 가능합니다")
@Schema(example = "200")
private Integer epoch;
@Schema(example = "7:2:1", description = "데이터 분할 비율 (Training:Validation:Test)")
private String datasetRatio;
}
@Schema(name = "TrainStartRes", description = "학습 시작 응답")
@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
@Builder
public static class TrainStartRes {
private String uuid;
private String status;
}
@Schema(name = "ResumeInfo", description = "학습 재시작 정보")
@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
@Builder
public static class ResumeInfo {
private Boolean canResume;
private Integer lastEpoch;
private Integer totalEpoch;
private String checkpointPath;
@JsonFormatDttm private ZonedDateTime failedAt;
}
@Schema(name = "ResumeRequest", description = "학습 재시작 요청")
@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
public static class ResumeRequest {
@NotNull(message = "재시작 Epoch는 필수입니다")
private Integer resumeFromEpoch;
private Integer newTotalEpoch;
}
@Schema(name = "ResumeResponse", description = "학습 재시작 응답")
@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
@Builder
public static class ResumeResponse {
private String uuid;
private String status;
private Integer resumedFromEpoch;
}
@Schema(name = "BestEpochRequest", description = "Best Epoch 설정 요청")
@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
public static class BestEpochRequest {
@NotNull(message = "Best Epoch는 필수입니다")
private Integer bestEpoch;
private String reason;
}
@Schema(name = "BestEpochResponse", description = "Best Epoch 설정 응답")
@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
@Builder
public static class BestEpochResponse {
private String uuid;
private Integer bestEpoch;
private Integer confirmedBestEpoch;
private Integer previousBestEpoch;
}
@Schema(name = "EpochMetric", description = "Epoch별 성능 지표")
@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
@Builder
public static class EpochMetric {
private Integer epoch;
private Double mIoU;
private Double mFscore;
private Double loss;
private Boolean isBest;
}
}

View File

@@ -0,0 +1,248 @@
package com.kamco.cd.training.model.dto;
import com.fasterxml.jackson.annotation.JsonInclude;
import com.kamco.cd.training.common.enums.LearnDataType;
import com.kamco.cd.training.common.enums.TrainStatusType;
import com.kamco.cd.training.common.enums.TrainType;
import com.kamco.cd.training.common.utils.enums.Enums;
import com.kamco.cd.training.common.utils.interfaces.JsonFormatDttm;
import com.kamco.cd.training.dataset.dto.DatasetDto.SelectDataSet;
import io.swagger.v3.oas.annotations.media.Schema;
import java.time.Duration;
import java.time.ZonedDateTime;
import java.util.List;
import java.util.UUID;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
public class ModelTrainDetailDto {
@Schema(name = "모델학습관리 목록", description = "모델학습관리 목록")
@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
@Builder
public static class DetailSummary {
private Long modelId;
private UUID uuid;
private String modelNo;
private String modelVer;
@JsonFormatDttm private ZonedDateTime step1StrtDttm;
@JsonFormatDttm private ZonedDateTime step2EndDttm;
private String statusCd;
private String trainType;
public String getStatusName() {
if (this.statusCd == null || this.statusCd.isBlank()) return null;
try {
return TrainStatusType.valueOf(this.statusCd).getText(); // 또는 getName()
} catch (IllegalArgumentException e) {
return this.statusCd; // 매핑 못하면 코드 그대로 반환(원하면 null 처리)
}
}
public String getTrainTypeName() {
if (this.trainType == null || this.trainType.isBlank()) return null;
try {
return TrainType.valueOf(this.trainType).getText(); // 또는 getName()
} catch (IllegalArgumentException e) {
return this.trainType; // 매핑 못하면 코드 그대로 반환(원하면 null 처리)
}
}
private String formatDuration(ZonedDateTime start, ZonedDateTime end) {
if (end == null) {
end = ZonedDateTime.now();
}
if (start == null) {
return null;
}
long totalSeconds = Math.abs(Duration.between(start, end).getSeconds());
long hours = totalSeconds / 3600;
long minutes = (totalSeconds % 3600) / 60;
long seconds = totalSeconds % 60;
return String.format("%d시간 %d분 %d초", hours, minutes, seconds);
}
public String getStepAllDuration() {
return formatDuration(this.step1StrtDttm, this.step2EndDttm);
}
}
@Schema(name = "모델학습관리 목록", description = "모델학습관리 목록")
@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
@Builder
public static class HyperSummary {
private UUID uuid;
private Long hyperParamId;
private String hyperVer;
private String backbone;
private String inputSize;
private String cropSize;
private Integer batchSize;
}
@Schema(name = "모델학습관리 전이 하이파라미터", description = "모델학습관리 전이 하이파라미터")
@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
@Builder
public static class TransferHyperSummary {
private UUID uuid;
private Long hyperParamId;
private String hyperVer;
private String backbone;
private String inputSize;
private String cropSize;
private Integer batchSize;
private UUID beforeUuid;
private Long beforeHyperParamId;
private String beforeHyperVer;
private String beforeBackbone;
private String beforeInputSize;
private String beforeCropSize;
private Integer beforeBatchSize;
}
@Schema(name = "선택한 데이터셋 목록", description = "선택한 데이터셋 목록")
@Getter
@Setter
@NoArgsConstructor
public static class MappingDataset {
private Long modelId;
private Long datasetId;
private String dataType;
private Integer compareYyyy;
private Integer targetYyyy;
private Long roundNo;
private String dataTypeName;
@JsonInclude(JsonInclude.Include.NON_NULL)
private Long buildingCnt;
@JsonInclude(JsonInclude.Include.NON_NULL)
private Long containerCnt;
@JsonInclude(JsonInclude.Include.NON_NULL)
private Long wasteCnt;
@JsonInclude(JsonInclude.Include.NON_NULL)
private Long landCoverCnt;
public MappingDataset(
Long modelId,
Long datasetId,
String dataType,
Integer compareYyyy,
Integer targetYyyy,
Long roundNo,
Long buildingCnt,
Long containerCnt,
Long wasteCnt,
Long landCoverCnt) {
this.modelId = modelId;
this.datasetId = datasetId;
this.dataType = dataType;
this.compareYyyy = compareYyyy;
this.targetYyyy = targetYyyy;
this.roundNo = roundNo;
this.buildingCnt = buildingCnt;
this.containerCnt = containerCnt;
this.wasteCnt = wasteCnt;
this.landCoverCnt = landCoverCnt;
this.dataTypeName = getDataTypeName(this.dataType);
}
public String getDataTypeName(String groupTitleCd) {
LearnDataType type = Enums.fromId(LearnDataType.class, groupTitleCd);
return type == null ? null : type.getText();
}
}
@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
public static class TransferDetailDto {
private ModelConfigDto.Basic etcConfig;
private TransferHyperSummary modelTrainHyper;
private List<SelectDataSet> modelTrainDataset;
}
@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
public static class ModelTrainMetrics {
private Integer epoch;
private Long iteration;
private Double loss;
private Double lr;
private Float durationTime;
}
@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
public static class ModelValidationMetrics {
private Integer epoch;
private Float aAcc;
private Float mFscore;
private Float mPrecision;
private Float mRecall;
private Float mIou;
private Float mAcc;
private Float changedFscore;
private Float changedPrecision;
private Float changedRecall;
private Float unchangedFscore;
private Float unchangedPrecision;
private Float unchangedRecall;
}
@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
public static class ModelTestMetrics {
private String model;
private Long tp;
private Long fp;
private Long fn;
private Float precision;
private Float recall;
private Float f1Score;
private Float accuracy;
private Float iou;
private Long detectionCount;
private Long gtCount;
}
@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
public static class ModelBestEpoch {
private Integer epoch;
private Double loss;
private Float f1Score;
private Float precision;
private Float recall;
private Float iou;
private Float accuracy;
}
}

View File

@@ -0,0 +1,212 @@
package com.kamco.cd.training.model.dto;
import com.kamco.cd.training.common.dto.HyperParam;
import com.kamco.cd.training.common.enums.TrainStatusType;
import com.kamco.cd.training.common.enums.TrainType;
import com.kamco.cd.training.common.utils.interfaces.JsonFormatDttm;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotNull;
import java.time.Duration;
import java.time.ZonedDateTime;
import java.util.List;
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 ModelTrainMngDto {
@Schema(name = "모델학습관리 목록", description = "모델학습관리 목록")
@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
@Builder
public static class Basic {
private Long id;
private UUID uuid;
private String modelVer;
@JsonFormatDttm private ZonedDateTime startDttm;
@JsonFormatDttm private ZonedDateTime step1StrtDttm;
@JsonFormatDttm private ZonedDateTime step1EndDttm;
@JsonFormatDttm private ZonedDateTime step2StrtDttm;
@JsonFormatDttm private ZonedDateTime step2EndDttm;
private String step1Status;
private String step2Status;
private String statusCd;
private String trainType;
private String modelNo;
private Long currentAttemptId;
public String getStatusName() {
if (this.statusCd == null || this.statusCd.isBlank()) return null;
try {
return TrainStatusType.valueOf(this.statusCd).getText(); // 또는 getName()
} catch (IllegalArgumentException e) {
return this.statusCd; // 매핑 못하면 코드 그대로 반환(원하면 null 처리)
}
}
public String getStep1StatusName() {
if (this.step1Status == null || this.step1Status.isBlank()) return null;
try {
return TrainStatusType.valueOf(this.step1Status).getText(); // 또는 getName()
} catch (IllegalArgumentException e) {
return this.step1Status; // 매핑 못하면 코드 그대로 반환(원하면 null 처리)
}
}
public String getStep2StatusNAme() {
if (this.step2Status == null || this.step2Status.isBlank()) return null;
try {
return TrainStatusType.valueOf(this.step2Status).getText(); // 또는 getName()
} catch (IllegalArgumentException e) {
return this.step2Status; // 매핑 못하면 코드 그대로 반환(원하면 null 처리)
}
}
public String getTrainTypeName() {
if (this.trainType == null || this.trainType.isBlank()) return null;
try {
return TrainType.valueOf(this.trainType).getText(); // 또는 getName()
} catch (IllegalArgumentException e) {
return this.trainType; // 매핑 못하면 코드 그대로 반환(원하면 null 처리)
}
}
private String formatDuration(ZonedDateTime start, ZonedDateTime end) {
if (start == null || end == null) {
return null;
}
long totalSeconds = Math.abs(Duration.between(start, end).getSeconds());
long hours = totalSeconds / 3600;
long minutes = (totalSeconds % 3600) / 60;
long seconds = totalSeconds % 60;
return String.format("%d시간 %d분 %d초", hours, minutes, seconds);
}
public String getStep1Duration() {
return formatDuration(this.step1StrtDttm, this.step1EndDttm);
}
public String getStep2Duration() {
return formatDuration(this.step2StrtDttm, this.step2EndDttm);
}
}
@Schema(name = "searchReq", description = "모델학습 관리 목록조회 파라미터")
@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
public static class SearchReq {
private String status;
private String modelNo;
// 페이징 파라미터
private int page = 0;
private int size = 20;
public Pageable toPageable() {
return PageRequest.of(page, size);
}
}
@Schema(name = "addReq", description = "모델학습 관리 등록 파라미터")
@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
public static class AddReq {
@NotNull
@Schema(description = "모델 종류 G1, G2, G3", example = "G1")
private String modelNo;
@NotNull
@Schema(description = "모델학습 실행 여부", example = "false")
private Boolean isStart;
@NotNull
@Schema(description = "학습타입 GENERAL(일반), TRANSFER(전이)", example = "GENERAL")
private String trainType;
@Schema(description = "전이학습일때 선택한 모델 id")
private Long beforeModelId;
@NotNull
@Schema(
description = "하이퍼 파라미터 선택 타입 OPTIMIZED(최적화 파라미터),EXISTING(기존 파라미터),NEW(신규 파라미터)",
example = "EXISTING")
private String hyperParamType;
@Schema(description = "하이퍼파라미터 uuid", example = "57fc9170-64c1-4128-aa7b-0657f08d6d10")
private UUID hyperUuid;
HyperParam hyperParam;
TrainingDataset trainingDataset;
ModelConfig modelConfig;
}
@Schema(name = "addReq", description = "모델학습 관리 등록 파라미터")
@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
public static class UpdateReq {
private String requestPath;
private String responsePath;
}
@Getter
@Setter
public static class TrainingDataset {
Summary summary;
List<Long> datasetList;
}
@Getter
@Setter
public static class Summary {
@Schema(description = "건물", example = "0")
private Long buildingCnt;
@Schema(description = "컨테이너", example = "0")
private Long containerCnt;
@Schema(description = "폐기물", example = "0")
private Long wasteCnt;
@Schema(
description = "도로, 비닐하우스, 밭, 과수원, 초지, 숲, 물, 모재/자갈, 토분(무덤), 일반토지, 태양광, 기타",
example = "0")
private Long LandCoverCnt;
}
@Getter
@Setter
public static class ModelConfig {
@Schema(description = "에폭 횟수", example = "0")
private Integer epochCnt;
@Schema(description = "학습데이터셋 비율 Training", example = "0")
private Float trainingCnt;
@Schema(description = "학습데이터셋 비율 Validation", example = "0")
private Float validationCnt;
@Schema(description = "학습데이터셋 비율 Test", example = "0")
private Float testCnt;
@Schema(description = "메모", example = "메모 입니다.")
private String memo;
}
}

View File

@@ -1,61 +0,0 @@
package com.kamco.cd.training.model.dto;
import com.kamco.cd.training.common.utils.interfaces.JsonFormatDttm;
import io.swagger.v3.oas.annotations.media.Schema;
import java.time.ZonedDateTime;
import lombok.Getter;
public class ModelVerDto {
@Schema(name = "modelVer Basic", description = "모델버전 엔티티 기본 정보")
@Getter
public static class Basic {
private final Long id;
private final Long modelUid;
private final String modelCate;
private final String modelVer;
private final String usedState;
private final String modelState;
private final Double qualityProb;
private final String deployState;
private final String modelPath;
@JsonFormatDttm private final ZonedDateTime createdDttm;
private final Long createdUid;
@JsonFormatDttm private final ZonedDateTime updatedDttm;
private final Long updatedUid;
public Basic(
Long id,
Long modelUid,
String modelCate,
String modelVer,
String usedState,
String modelState,
Double qualityProb,
String deployState,
String modelPath,
ZonedDateTime createdDttm,
Long createdUid,
ZonedDateTime updatedDttm,
Long updatedUid) {
this.id = id;
this.modelUid = modelUid;
this.modelCate = modelCate;
this.modelVer = modelVer;
this.usedState = usedState;
this.modelState = modelState;
this.qualityProb = qualityProb;
this.deployState = deployState;
this.modelPath = modelPath;
this.createdDttm = createdDttm;
this.createdUid = createdUid;
this.updatedDttm = updatedDttm;
this.updatedUid = updatedUid;
}
}
}

Some files were not shown because too many files have changed in this diff Show More