293 Commits

Author SHA1 Message Date
265813e6f7 Merge branch 'feat/training_260202' of https://kamco.git.gs.dabeeo.com/MVPTeam/kamco-train-api into feat/training_260202 2026-03-10 14:19:40 +09:00
8190a6e9c8 unzip 2026-03-10 14:19:22 +09:00
e9f8bb37fa spotless 적용 2026-03-03 23:06:45 +09:00
f3c822587f spotless 적용 2026-03-03 23:02:53 +09:00
335f0dbb9b spotless 적용 2026-03-03 23:01:22 +09:00
69eaba1a83 하드링크 수정 2026-03-03 22:51:10 +09:00
365ad81cad 리커버리 삭제 2026-02-28 01:24:34 +09:00
9dfa54fbf9 리커버리 추가 2026-02-28 01:01:38 +09:00
12f6bb7154 하드링크 수정 2026-02-27 23:31:04 +09:00
aa3af4e9d0 하드링크 로그 추가 2026-02-27 23:12:00 +09:00
7ca37bf1e4 하드링크 로그 추가 2026-02-27 22:51:27 +09:00
901dde066d 하이파라미터 상세조회 수정 2026-02-24 16:54:58 +09:00
cb0a38274a val_interval 기본값 1로 수정 2026-02-24 16:24:16 +09:00
b8194df9ae 학습 실패여부 확인 기능 추가 2026-02-24 15:43:35 +09:00
7c5f07683e 학습 실패여부 확인 기능 추가 2026-02-24 15:10:48 +09:00
159fb281d4 최근 사용일시 업데이트 2026-02-23 15:40:24 +09:00
97192ff811 최근 사용일시 업데이트 2026-02-23 15:39:25 +09:00
4f3fb675be 하이퍼 파라미터 사용회수 카운트 기능 추가 및 조회 수정 2026-02-23 15:37:28 +09:00
e6caea05b3 하이퍼 파라미터 사용회수 카운트 기능 추가 및 조회 수정 2026-02-23 15:19:40 +09:00
fd63824edc 하이퍼 파라미터 미사용 컬럼제거, 사용횟수 컬럼 추가 2026-02-23 14:30:29 +09:00
8a44df26b8 하이퍼 파라미터 상세조회 삭제여부 조건 제거 2026-02-23 14:17:34 +09:00
cb97c5e59e 하이퍼 파라미터 dto 주석 수정 2026-02-23 14:13:25 +09:00
8f75b16dc6 학습실행 주석 추가 2026-02-23 12:30:54 +09:00
c2978e41c2 전이학습 상세 수정 2026-02-20 18:34:32 +09:00
07429dbe8e 전이학습 상세 수정 2026-02-20 18:22:19 +09:00
83859bb9fe 전이학습 상세 - before dataset 추가 2026-02-20 16:05:29 +09:00
564a99448c best epoch 파일 선택 수정 2026-02-20 15:41:34 +09:00
38ae6e5575 best epoch 파일 선택 수정 2026-02-20 15:31:33 +09:00
40fe98ae0c best epoch 파일 선택 수정 2026-02-20 15:15:12 +09:00
255ff10a56 Merge remote-tracking branch 'origin/feat/training_260202' into feat/training_260202 2026-02-20 14:30:42 +09:00
f674f73330 중복 수정 제거 2026-02-20 14:30:34 +09:00
db2bc32e7d test metrics insert 로직 수정 2026-02-20 14:24:23 +09:00
37786a1e44 선택한 테스트 에폭 로그 추가 2026-02-20 14:13:49 +09:00
901ea83fb7 test 선택한 에폭 log 확인 추가 2026-02-20 14:13:22 +09:00
832e1b5681 tmp 하드링크 수정 2026-02-20 13:36:48 +09:00
4f16355cda tmp 하드링크 수정 2026-02-20 12:29:57 +09:00
df46a8f79f Merge remote-tracking branch 'origin/feat/training_260202' into feat/training_260202 2026-02-20 12:21:50 +09:00
fcd48831c5 tmp 하드링크 수정 2026-02-20 12:21:41 +09:00
62c9d73b94 test json 수정 2026-02-20 12:20:15 +09:00
68c0e634c5 ing-cnt 로직에 step2도 추가, transactional 2026-02-20 12:05:20 +09:00
ad421e3c74 비밀번호 변경 security 로직 수정 2026-02-20 11:36:21 +09:00
46db1512a6 test 실행 시 회차별 데이터 적재하기 2026-02-19 18:18:12 +09:00
2034a8fcb2 LogErrorLevel -> CodeExpose 추가 2026-02-19 17:35:15 +09:00
bf212842d8 모델학습관리 > 모델별 진행 상황 API 추가 2026-02-19 17:17:11 +09:00
d2ca94ea55 모델학습관리 > 목록 API 메모,작성자 추가로 인한 수정 2026-02-19 15:34:18 +09:00
5ddf6dfeeb 모델학습 2단계 패키징 시작,종료일시,상태 로직 추가 2026-02-19 14:43:14 +09:00
5e13c0b396 공통코드 common-code 로 prefix 변경 2026-02-19 11:38:56 +09:00
435f60dcac 로그관리 로직 커밋 2026-02-19 11:13:40 +09:00
5f5eabca19 압축해제 시, 동일 폴더가 있으면 삭제 후 재업로드 2026-02-18 16:36:46 +09:00
413631840f 학습데이터 다운로드 security 제외하기 2026-02-18 16:29:28 +09:00
c7f63d1ad1 압축 해제한 폴더의 갯수 맞는지 log 찍기 + 갯수 맞지 않으면 exception 리턴 2026-02-18 16:22:32 +09:00
7529d23488 업로드 시 uid로 중복체크 -> 삭제인 row는 제외하기 2026-02-18 15:40:14 +09:00
cb3e51d712 업로드 시 exception 메세지 처리, 에폭 10 이상으로 실행되게 수정 2026-02-18 15:28:29 +09:00
99a4597b5f train 결과 +1 했던 거 제거하기 2026-02-18 14:50:53 +09:00
d9da0d4610 1단계 실행 시, 시작시간 update 추가 2026-02-18 13:05:59 +09:00
22c481556c 하이퍼 파라미터 수정 2026-02-13 15:00:30 +09:00
0798b352c7 하이퍼 파라미터 수정 2026-02-13 14:46:59 +09:00
5b074bdb81 하이퍼 파라미터 수정 2026-02-13 14:42:39 +09:00
28919345c2 하이퍼 파라미터 수정 2026-02-13 14:37:44 +09:00
aa0552aaa7 하이퍼 파라미터 수정 2026-02-13 14:30:45 +09:00
5d0aca14a6 사용가능 용량 API 수정 2026-02-13 14:19:09 +09:00
af8d59ddfa 이어하기 수정 2026-02-13 14:08:35 +09:00
4f24e09c57 이어하기 수정 2026-02-13 14:04:33 +09:00
4da477706f 이어하기 수정 2026-02-13 13:57:01 +09:00
a070566048 이어하기 로그 수정 2026-02-13 13:26:54 +09:00
a5b3ae613f 트랜젝션처리 임시폴더 uid업데이트 2026-02-13 13:23:53 +09:00
979af088be 트랜젝션처리 임시폴더 uid업데이트 2026-02-13 13:17:27 +09:00
e5a1cab36b Merge branch 'feat/training_260202' of https://kamco.git.gs.dabeeo.com/MVPTeam/kamco-train-api into feat/training_260202 2026-02-13 12:53:04 +09:00
bb67996742 text 2026-02-13 12:53:00 +09:00
1981d6d1ce Merge remote-tracking branch 'origin/feat/training_260202' into feat/training_260202 2026-02-13 12:53:00 +09:00
47f4ffd4db 트랜젝션처리 임시폴더 uid업데이트 2026-02-13 12:52:11 +09:00
195856b846 flush 추가해보기 2026-02-13 12:46:54 +09:00
124da48e51 트랜젝션처리 임시폴더 uid업데이트 2026-02-13 12:38:55 +09:00
02724e9508 주석한거 원복 2026-02-13 12:24:50 +09:00
7ed91ccab9 Merge branch 'feat/training_260202' of https://kamco.git.gs.dabeeo.com/MVPTeam/kamco-train-api into feat/training_260202 2026-02-13 12:23:12 +09:00
a7c13b985d responsePath 셋팅 삭제 2026-02-13 12:23:01 +09:00
352a28b87f 트랜젝션처리 임시폴더 uid업데이트 2026-02-13 12:20:48 +09:00
bf8515163c 주석 처리 2026-02-13 12:10:08 +09:00
2691f6ce16 트랜젝션처리 임시폴더 uid업데이트 2026-02-13 12:00:42 +09:00
7e5aa5e713 트랜젝션처리 임시폴더 uid업데이트 2026-02-13 11:57:48 +09:00
060a815e1c 트랜젝션처리 임시폴더 uid업데이트 2026-02-13 11:55:35 +09:00
1eb4d04779 Merge branch 'feat/training_260202' of https://kamco.git.gs.dabeeo.com/MVPTeam/kamco-train-api into feat/training_260202 2026-02-13 10:50:35 +09:00
f30c0c6d45 다운로드 시 Access-Control-Expose-Headers 추가 2026-02-13 10:50:28 +09:00
12994aab60 파일 count 기능 추가 2026-02-13 10:44:49 +09:00
11d3afe295 파일 count 기능 추가 2026-02-13 10:38:24 +09:00
1e62a8b097 학습실행 step1 할 때 best epoch 업데이트 2026-02-13 10:15:04 +09:00
26a4623aa8 학습데이터 목록 파일 단위 MB 나오게 하기 2026-02-13 09:42:36 +09:00
ce6e4f5aea tmp 파일 링크 수정 2026-02-13 09:10:45 +09:00
c2215836c0 tmp 파일 링크 수정 2026-02-13 08:44:17 +09:00
8c19c996f7 tmp 파일 링크 수정 2026-02-13 08:33:36 +09:00
b5ce3ab1fb 이어하기 수정 2026-02-12 23:01:56 +09:00
e1ceb769dd 학습데이터 다운로드 파일 정보 API 추가 2026-02-12 22:47:13 +09:00
4219b88fb3 학습데이터 다운로드 API 추가 2026-02-12 22:25:55 +09:00
4f94c99b64 이어하기 수정 2026-02-12 22:09:54 +09:00
d42e1afbd4 스케줄러 api 수동 호출 2026-02-12 21:51:53 +09:00
b3b8016673 csv 결과 받아오는 것 변경 2026-02-12 21:45:38 +09:00
79e8259f28 파라미터 변경 2026-02-12 21:30:03 +09:00
032c82c2f0 file 경로 넣기 2026-02-12 21:17:10 +09:00
6204a6e5fa Merge remote-tracking branch 'origin/develop' into feat/training_260202
# Conflicts:
#	src/main/resources/application-prod.yml
2026-02-12 21:15:21 +09:00
4d9c9a86b4 패키징 zip파일 만들기 커밋 2026-02-12 21:09:40 +09:00
83204abfe9 Merge pull request '성공시 csv 파일 테이블에 저장 연결' (#74) from feat/training_260202 into develop
Reviewed-on: #74
2026-02-12 21:01:48 +09:00
5b682c1386 성공시 csv 파일 테이블에 저장 연결 2026-02-12 21:01:24 +09:00
452494d44d Merge pull request '테스트 실행 경로 수정' (#73) from feat/training_260202 into develop
Reviewed-on: #73
2026-02-12 20:49:30 +09:00
8ada26448b 테스트 실행 경로 수정 2026-02-12 20:49:14 +09:00
e442f105bc Merge pull request '도커명 변경' (#72) from feat/training_260202 into develop
Reviewed-on: #72
2026-02-12 20:37:00 +09:00
5e0a771848 도커명 변경 2026-02-12 20:36:38 +09:00
b4c2685059 Merge pull request '도커 설정 추가' (#71) from feat/training_260202 into develop
Reviewed-on: #71
2026-02-12 20:25:16 +09:00
e238f3ca88 도커 설정 추가 2026-02-12 20:24:57 +09:00
97b06eb3b3 Merge pull request '임시파일생성 경로 수정' (#70) from feat/training_260202 into develop
Reviewed-on: #70
2026-02-12 20:03:32 +09:00
ad32ca18ca 임시파일생성 경로 수정 2026-02-12 20:03:03 +09:00
98a1283ebe Merge pull request '임시파일생성 경로 수정' (#69) from feat/training_260202 into develop
Reviewed-on: #69
2026-02-12 19:36:06 +09:00
a10fccaae3 임시파일생성 경로 수정 2026-02-12 19:35:47 +09:00
c3c9191d9d Merge pull request 'hyperparam_with_modeltype' (#68) from feat/dean/hyperparam_with_modelType-bug into develop
Reviewed-on: #68
2026-02-12 19:30:29 +09:00
9fd5a15a72 hyperparam_with_modeltype 2026-02-12 19:30:08 +09:00
12f9de7367 hyperparam_with_modeltype 2026-02-12 19:16:24 +09:00
5455da1e96 hyperparam_with_modeltype 2026-02-12 19:16:13 +09:00
9e803661cd Merge pull request 'feat/training_260202' (#67) from feat/training_260202 into develop
Reviewed-on: #67
2026-02-12 19:14:39 +09:00
b0cf9e77ec Merge branch 'develop' of https://kamco.git.gs.dabeeo.com/MVPTeam/kamco-train-api into develop 2026-02-12 19:14:10 +09:00
c92426aefc Merge branch 'feat/training_260202' of https://kamco.git.gs.dabeeo.com/MVPTeam/kamco-train-api into feat/training_260202 2026-02-12 19:14:09 +09:00
d5b2b8ecec hyperparam_with_modeltype 2026-02-12 19:14:01 +09:00
6185a18a7c 모델목록 검색 조건 상태값 변경 2026-02-12 19:13:56 +09:00
49d3e37458 Merge pull request '임시파일생성 경로 수정' (#66) from feat/training_260202 into develop
Reviewed-on: #66
2026-02-12 19:12:37 +09:00
1fb10830b9 임시파일생성 경로 수정 2026-02-12 19:11:51 +09:00
d7766edd24 Merge pull request 'return 형식 수정' (#65) from feat/training_260202 into develop
Reviewed-on: #65
2026-02-12 18:59:37 +09:00
0bc4453c9c hyperparam_with_modeltype 2026-02-12 18:56:32 +09:00
ae0d30e5da return 형식 수정 2026-02-12 18:55:42 +09:00
37d776dd2c Merge pull request 'hyperparam_with_modeltype' (#64) from feat/dean/hyperparam_with_modelType into develop
Reviewed-on: #64
2026-02-12 18:50:32 +09:00
0c34ea7dcb hyperparam_with_modeltype 2026-02-12 18:48:14 +09:00
3106d36431 Merge pull request '업로드 시 같은 uid로 업로드하지 못하게 조건 추가' (#63) from feat/training_260202 into develop
Reviewed-on: #63
2026-02-12 18:44:49 +09:00
ed48f697a4 업로드 시 같은 uid로 업로드하지 못하게 조건 추가 2026-02-12 18:44:04 +09:00
da92b28d97 Merge pull request '임시파일생성 소프트링크에서 하드링크로 변경' (#62) from feat/training_260202 into develop
Reviewed-on: #62
2026-02-12 18:20:30 +09:00
6c865d26fd 임시파일생성 소프트링크에서 하드링크로 변경 2026-02-12 18:18:44 +09:00
e3f00876f1 Merge pull request '문제되는 하이퍼파라미터 주석처리' (#61) from feat/training_260202 into develop
Reviewed-on: #61
2026-02-12 17:53:11 +09:00
16e156b5b4 문제되는 하이퍼파라미터 주석처리 2026-02-12 17:52:42 +09:00
60962bbc75 Merge pull request '학습실행 mount 경로 수정' (#60) from feat/training_260202 into develop
Reviewed-on: #60
2026-02-12 17:44:15 +09:00
6a939118ff 임시폴더생성 api 추가 2026-02-12 17:43:41 +09:00
64d37dcc08 Merge pull request '임시폴더생성 api 추가' (#59) from feat/training_260202 into develop
Reviewed-on: #59
2026-02-12 17:23:53 +09:00
0c0ae16c2b 임시폴더생성 api 추가 2026-02-12 17:23:34 +09:00
a2490f30e6 Merge pull request '임시폴더생성 api 수정' (#58) from feat/training_260202 into develop
Reviewed-on: #58
2026-02-12 17:14:52 +09:00
953f95aed6 임시폴더생성 api 추가 2026-02-12 17:14:26 +09:00
bd04e1f4e8 Merge pull request '임시폴더생성 api 추가' (#57) from feat/training_260202 into develop
Reviewed-on: #57
2026-02-12 17:03:39 +09:00
85633c8bab 임시폴더생성 api 추가 2026-02-12 17:03:21 +09:00
5fc15937c0 Merge pull request 'feat/training_260202' (#56) from feat/training_260202 into develop
Reviewed-on: #56
2026-02-12 17:00:08 +09:00
8b3940b446 Merge remote-tracking branch 'origin/feat/training_260202' into feat/training_260202 2026-02-12 16:59:44 +09:00
201cfefb6b 임시폴더생성 api 추가 2026-02-12 16:59:39 +09:00
9958b0999a csv 읽는 경로 수정하기, 변수명 수정 2026-02-12 16:58:28 +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
248 changed files with 22358 additions and 12952 deletions

View File

@@ -1,6 +1,11 @@
# Stage 1: Build stage (gradle build는 Jenkins에서 이미 수행) # Stage 1: Build stage (gradle build는 Jenkins에서 이미 수행)
FROM eclipse-temurin:21-jre-jammy FROM eclipse-temurin:21-jre-jammy
# docker CLI 설치 (컨테이너에서 호스트 Docker 제어용) 260212 추가
RUN apt-get update && \
apt-get install -y --no-install-recommends docker.io ca-certificates && \
rm -rf /var/lib/apt/lists/*
# 작업 디렉토리 설정 # 작업 디렉토리 설정
WORKDIR /app WORKDIR /app

View File

@@ -3,6 +3,7 @@ plugins {
id 'org.springframework.boot' version '3.5.7' id 'org.springframework.boot' version '3.5.7'
id 'io.spring.dependency-management' version '1.1.7' id 'io.spring.dependency-management' version '1.1.7'
id 'com.diffplug.spotless' version '6.25.0' id 'com.diffplug.spotless' version '6.25.0'
id 'idea'
} }
group = 'com.kamco.cd' group = 'com.kamco.cd'
@@ -21,11 +22,23 @@ configurations {
} }
} }
// QueryDSL 생성된 소스 디렉토리 정의
def generatedSourcesDir = file("$buildDir/generated/sources/annotationProcessor/java/main")
repositories { repositories {
mavenCentral() mavenCentral()
maven { url "https://repo.osgeo.org/repository/release/" } maven { url "https://repo.osgeo.org/repository/release/" }
} }
// Gradle이 생성된 소스를 컴파일 경로에 포함하도록 설정
sourceSets {
main {
java {
srcDirs += generatedSourcesDir
}
}
}
dependencies { dependencies {
implementation 'org.springframework.boot:spring-boot-starter-data-jpa' implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
implementation 'org.springframework.boot:spring-boot-starter-web' implementation 'org.springframework.boot:spring-boot-starter-web'
@@ -83,6 +96,23 @@ dependencies {
implementation 'io.hypersistence:hypersistence-utils-hibernate-63:3.7.0' implementation 'io.hypersistence:hypersistence-utils-hibernate-63:3.7.0'
implementation 'org.reflections:reflections:0.10.2' 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 { configurations.configureEach {
@@ -93,6 +123,21 @@ tasks.named('test') {
useJUnitPlatform() useJUnitPlatform()
} }
// 컴파일 전 생성된 소스 디렉토리 생성 보장
tasks.named('compileJava') {
doFirst {
generatedSourcesDir.mkdirs()
}
}
// 생성된 소스 정리 태스크
tasks.register('cleanGeneratedSources', Delete) {
delete generatedSourcesDir
}
tasks.named('clean') {
dependsOn 'cleanGeneratedSources'
}
bootJar { bootJar {
archiveFileName = 'ROOT.jar' archiveFileName = 'ROOT.jar'

View File

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

0
gradlew vendored Normal file → Executable file
View File

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,48 @@
package com.kamco.cd.training.common.download;
import com.kamco.cd.training.common.download.dto.DownloadSpec;
import com.kamco.cd.training.common.utils.UserUtil;
import java.io.IOException;
import java.io.InputStream;
import java.nio.file.Files;
import lombok.RequiredArgsConstructor;
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Service;
import org.springframework.web.servlet.mvc.method.annotation.StreamingResponseBody;
@Service
@RequiredArgsConstructor
public class DownloadExecutor {
private final UserUtil userUtil;
public ResponseEntity<StreamingResponseBody> stream(DownloadSpec spec) throws IOException {
if (!Files.isReadable(spec.filePath())) {
return ResponseEntity.notFound().build();
}
StreamingResponseBody body =
os -> {
try (InputStream in = Files.newInputStream(spec.filePath())) {
in.transferTo(os);
os.flush();
} catch (Exception e) {
// 고용량은 중간 끊김 흔하니까 throw 금지
}
};
String fileName =
spec.downloadName() != null
? spec.downloadName()
: spec.filePath().getFileName().toString();
return ResponseEntity.ok()
.contentType(
spec.contentType() != null ? spec.contentType() : MediaType.APPLICATION_OCTET_STREAM)
.header(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=\"" + fileName + "\"")
.body(body);
}
}

View File

@@ -0,0 +1,19 @@
package com.kamco.cd.training.common.download;
import org.springframework.util.AntPathMatcher;
public final class DownloadPaths {
private DownloadPaths() {}
public static final String[] PATTERNS = {
"/api/inference/download/**", "/api/training-data/stage/download/**"
};
public static boolean matches(String uri) {
AntPathMatcher m = new AntPathMatcher();
for (String p : PATTERNS) {
if (m.match(p, uri)) return true;
}
return false;
}
}

View File

@@ -0,0 +1,81 @@
package com.kamco.cd.training.common.download;
import jakarta.servlet.http.HttpServletRequest;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.List;
import org.springframework.core.io.FileSystemResource;
import org.springframework.core.io.Resource;
import org.springframework.core.io.support.ResourceRegion;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpRange;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Component;
@Component
public class RangeDownloadResponder {
public ResponseEntity<?> buildZipResponse(
Path filePath, String downloadFileName, HttpServletRequest request) throws IOException {
if (!Files.isRegularFile(filePath)) {
return ResponseEntity.notFound().build();
}
long totalSize = Files.size(filePath);
Resource resource = new FileSystemResource(filePath);
String disposition = "attachment; filename=\"" + downloadFileName + "\"";
String rangeHeader = request.getHeader(HttpHeaders.RANGE);
// 🔥 공통 헤더 (여기 고정)
ResponseEntity.BodyBuilder base =
ResponseEntity.ok()
.contentType(MediaType.APPLICATION_OCTET_STREAM)
.header(HttpHeaders.CONTENT_DISPOSITION, disposition)
.header(HttpHeaders.ACCEPT_RANGES, "bytes")
.header("Access-Control-Expose-Headers", "Content-Disposition")
.header("X-Accel-Buffering", "no");
if (rangeHeader == null || rangeHeader.isBlank()) {
return base.contentLength(totalSize).body(resource);
}
List<HttpRange> ranges;
try {
ranges = HttpRange.parseRanges(rangeHeader);
} catch (IllegalArgumentException ex) {
return ResponseEntity.status(416)
.header(HttpHeaders.CONTENT_RANGE, "bytes */" + totalSize)
.header("X-Accel-Buffering", "no")
.build();
}
HttpRange range = ranges.get(0);
long start = range.getRangeStart(totalSize);
long end = range.getRangeEnd(totalSize);
if (start >= totalSize) {
return ResponseEntity.status(416)
.header(HttpHeaders.CONTENT_RANGE, "bytes */" + totalSize)
.header("X-Accel-Buffering", "no")
.build();
}
long regionLength = end - start + 1;
ResourceRegion region = new ResourceRegion(resource, start, regionLength);
return ResponseEntity.status(206)
.contentType(MediaType.APPLICATION_OCTET_STREAM)
.header(HttpHeaders.CONTENT_DISPOSITION, disposition)
.header(HttpHeaders.ACCEPT_RANGES, "bytes")
.header("Access-Control-Expose-Headers", "Content-Disposition")
.header("X-Accel-Buffering", "no")
.header(HttpHeaders.CONTENT_RANGE, "bytes " + start + "-" + end + "/" + totalSize)
.contentLength(regionLength)
.body(region);
}
}

View File

@@ -0,0 +1,12 @@
package com.kamco.cd.training.common.download.dto;
import java.nio.file.Path;
import java.util.UUID;
import org.springframework.http.MediaType;
public record DownloadSpec(
UUID uuid, // 다운로드 식별(로그/정책용)
Path filePath, // 실제 파일 경로
String downloadName, // 사용자에게 보일 파일명
MediaType contentType // 보통 OCTET_STREAM
) {}

View File

@@ -0,0 +1,160 @@
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 {
@Schema(description = "모델", example = "G1")
private ModelType model; // G1, G2, G3
// -------------------------
// Important
// -------------------------
@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; package com.kamco.cd.training.common.enums;
import com.kamco.cd.training.common.utils.enums.CodeExpose; 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.enums.EnumType;
import lombok.AllArgsConstructor; import lombok.AllArgsConstructor;
import lombok.Getter; import lombok.Getter;
@CodeExpose @CodeExpose
@Getter @Getter
@AllArgsConstructor @AllArgsConstructor
public enum DeployTargetType implements EnumType { public enum DeployTargetType implements EnumType {
// @formatter:off // @formatter:off
GUKU("GUKU", "국토교통부"), GUKU("GUKU", "국토교통부"),
PROD("PROD", "운영계"); PROD("PROD", "운영계");
// @formatter:on // @formatter:on
private final String id; private final String id;
private final String text; private final String text;
} }

View File

@@ -1,55 +1,56 @@
package com.kamco.cd.training.common.enums; package com.kamco.cd.training.common.enums;
import lombok.AllArgsConstructor; import lombok.AllArgsConstructor;
import lombok.Getter; import lombok.Getter;
@Getter @Getter
@AllArgsConstructor @AllArgsConstructor
public enum DetectionClassification { public enum DetectionClassification {
BUILDING("building", "건물", 10), ROAD("road", "도로", 10),
CONTAINER("container", "컨테이너", 20), BUILDING("building", "건물", 20),
FIELD("field", "경작지", 30), GREENHOUSE("greenhouse", "비닐하우스", 30),
FOREST("forest", "", 40), FIELD("field", "논/밭", 40),
GRASS("grass", "초지", 50), ORCHARD("orchard", "과수원", 50),
GREENHOUSE("greenhouse", "비닐하우스", 60), GRASS("grass", "초지", 60),
LAND("land", "일반토지", 70), FOREST("forest", "", 70),
ORCHARD("orchard", "과수원", 80), WATER("water", "", 80),
ROAD("road", "도로", 90), STONE("stone", "모래/자갈", 90),
STONE("stone", "모래/자갈", 100), WASTE("waste", "적치물", 100),
TANK("tank", "물탱크", 110), CONTAINER("container", "컨테이너", 110),
TUMULUS("tumulus", "토분(무덤)", 120), LAND("land", "일반토지", 120),
WASTE("waste", "폐기물", 130), SOLAR("solar", "태양광", 130),
WATER("water", "", 140), TANK("tank", "탱크", 140),
ETC("ETC", "기타", 200); // For 'etc' (miscellaneous/other) NDC("NDC", "미분류", 150),
ETC("ETC", "기타", 160);
private final String id;
private final String desc; private final String id;
private final int order; 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. * 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()) { public static DetectionClassification fromString(String text) {
return ETC; if (text == null || text.trim().isEmpty()) {
} return ETC;
}
try {
return DetectionClassification.valueOf(text.toUpperCase()); try {
} catch (IllegalArgumentException e) { return DetectionClassification.valueOf(text.toUpperCase());
// If the string doesn't match any enum constant name, return ETC } catch (IllegalArgumentException e) {
return ETC; // If the string doesn't match any enum constant name, return ETC
} return ETC;
} }
}
/**
* Desc 한글명 get 하기 /**
* * Desc 한글명 get 하기
* @return *
*/ * @return
public static String fromStrDesc(String text) { */
DetectionClassification dtf = fromString(text); public static String fromStrDesc(String text) {
return dtf.getDesc(); 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

@@ -0,0 +1,27 @@
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 JobStatusType implements EnumType {
QUEUED("대기중"),
RUNNING("실행중"),
SUCCESS("성공"),
FAILED("실패"),
CANCELED("취소");
private final String desc;
@Override
public String getId() {
return name();
}
@Override
public String getText() {
return desc;
}
}

View File

@@ -0,0 +1,24 @@
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 JobType implements EnumType {
TRAIN("학습"),
TEST("테스트");
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; package com.kamco.cd.training.common.enums;
import com.kamco.cd.training.common.utils.enums.EnumType; import com.kamco.cd.training.common.utils.enums.EnumType;
import lombok.AllArgsConstructor; import lombok.AllArgsConstructor;
import lombok.Getter; import lombok.Getter;
@Getter @Getter
@AllArgsConstructor @AllArgsConstructor
public enum LearnDataRegister implements EnumType { public enum LearnDataRegister implements EnumType {
READY("준비"), READY("준비"),
UPLOADING("업로드중"), UPLOADING("업로드중"),
UPLOAD_FAILED("업로드 실패"), UPLOAD_FAILED("업로드 실패"),
COMPLETED("완료"); COMPLETED("완료");
private final String desc; private final String desc;
@Override @Override
public String getId() { public String getId() {
return name(); return name();
} }
@Override @Override
public String getText() { public String getText() {
return desc; return desc;
} }
} }

View File

@@ -1,26 +1,26 @@
package com.kamco.cd.training.common.enums; package com.kamco.cd.training.common.enums;
import com.kamco.cd.training.common.utils.enums.CodeExpose; 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.enums.EnumType;
import lombok.AllArgsConstructor; import lombok.AllArgsConstructor;
import lombok.Getter; import lombok.Getter;
@CodeExpose @CodeExpose
@Getter @Getter
@AllArgsConstructor @AllArgsConstructor
public enum LearnDataType implements EnumType { public enum LearnDataType implements EnumType {
DELIVER("납품"), DELIVER("납품"),
PRODUCTION("제작"); PRODUCTION("제작");
private final String desc; private final String desc;
@Override @Override
public String getId() { public String getId() {
return name(); return name();
} }
@Override @Override
public String getText() { public String getText() {
return desc; return desc;
} }
} }

View File

@@ -1,27 +1,27 @@
package com.kamco.cd.training.common.enums; package com.kamco.cd.training.common.enums;
import com.kamco.cd.training.common.utils.enums.CodeExpose; 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.enums.EnumType;
import lombok.AllArgsConstructor; import lombok.AllArgsConstructor;
import lombok.Getter; import lombok.Getter;
@CodeExpose @CodeExpose
@Getter @Getter
@AllArgsConstructor @AllArgsConstructor
public enum ModelMngStatusType implements EnumType { public enum ModelMngStatusType implements EnumType {
READY("준비"), READY("준비"),
IN_PROGRESS("진행중"), IN_PROGRESS("진행중"),
COMPLETED("완료"); COMPLETED("완료");
private String desc; private String desc;
@Override @Override
public String getId() { public String getId() {
return name(); return name();
} }
@Override @Override
public String getText() { public String getText() {
return desc; return desc;
} }
} }

View File

@@ -0,0 +1,35 @@
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; package com.kamco.cd.training.common.enums;
import com.kamco.cd.training.common.utils.enums.CodeExpose; 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.enums.EnumType;
import lombok.AllArgsConstructor; import lombok.AllArgsConstructor;
import lombok.Getter; import lombok.Getter;
@CodeExpose @CodeExpose
@Getter @Getter
@AllArgsConstructor @AllArgsConstructor
public enum ProcessStepType implements EnumType { public enum ProcessStepType implements EnumType {
// @formatter:off // @formatter:off
STEP1("STEP1", "학습 중"), STEP1("STEP1", "학습 중"),
STEP2("STEP2", "테스트 중"), STEP2("STEP2", "테스트 중"),
STEP3("STEP3", "완료"); STEP3("STEP3", "완료");
// @formatter:on // @formatter:on
private final String id; private final String id;
private final String text; private final String text;
} }

View File

@@ -1,25 +1,25 @@
package com.kamco.cd.training.common.enums; package com.kamco.cd.training.common.enums;
import com.kamco.cd.training.common.utils.enums.EnumType; import com.kamco.cd.training.common.utils.enums.EnumType;
import lombok.AllArgsConstructor; import lombok.AllArgsConstructor;
import lombok.Getter; import lombok.Getter;
@Getter @Getter
@AllArgsConstructor @AllArgsConstructor
public enum RoleType implements EnumType { public enum RoleType implements EnumType {
ROLE_ADMIN("시스템 관리자"), ROLE_ADMIN("시스템 관리자"),
ROLE_LABELER("라벨러"), ROLE_LABELER("라벨러"),
ROLE_REVIEWER("검수자"); ROLE_REVIEWER("검수자");
private final String desc; private final String desc;
@Override @Override
public String getId() { public String getId() {
return name(); return name();
} }
@Override @Override
public String getText() { public String getText() {
return desc; return desc;
} }
} }

View File

@@ -1,25 +1,25 @@
package com.kamco.cd.training.common.enums; package com.kamco.cd.training.common.enums;
import com.kamco.cd.training.common.utils.enums.EnumType; import com.kamco.cd.training.common.utils.enums.EnumType;
import lombok.AllArgsConstructor; import lombok.AllArgsConstructor;
import lombok.Getter; import lombok.Getter;
@Getter @Getter
@AllArgsConstructor @AllArgsConstructor
public enum StatusType implements EnumType { public enum StatusType implements EnumType {
ACTIVE("사용"), ACTIVE("사용"),
INACTIVE("미사용"), INACTIVE("미사용"),
PENDING("계정등록"); PENDING("계정등록");
private final String desc; private final String desc;
@Override @Override
public String getId() { public String getId() {
return name(); return name();
} }
@Override @Override
public String getText() { public String getText() {
return desc; return desc;
} }
} }

View File

@@ -1,22 +1,30 @@
package com.kamco.cd.training.common.enums; package com.kamco.cd.training.common.enums;
import com.kamco.cd.training.common.utils.enums.CodeExpose; 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.enums.EnumType;
import lombok.AllArgsConstructor; import lombok.AllArgsConstructor;
import lombok.Getter; import lombok.Getter;
@CodeExpose @CodeExpose
@Getter @Getter
@AllArgsConstructor @AllArgsConstructor
public enum TrainStatusType implements EnumType { public enum TrainStatusType implements EnumType {
// @formatter:off // @formatter:off
READY("READY", "대기"), READY("대기"),
ING("ING", "진행중"), IN_PROGRESS("진행중"),
COMPLETED("COMPLETED", "완료"), COMPLETED("완료"),
STOPPED("STOPPED", "중단됨"), STOPPED("중단됨"),
ERROR("ERROR", "오류"); ERROR("오류");
// @formatter:on
private final String desc;
private final String id;
private final String text; @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; package com.kamco.cd.training.common.enums.error;
import com.kamco.cd.training.common.utils.ErrorCode; import com.kamco.cd.training.common.utils.ErrorCode;
import lombok.Getter; import lombok.Getter;
import org.springframework.http.HttpStatus; import org.springframework.http.HttpStatus;
@Getter @Getter
public enum AuthErrorCode implements ErrorCode { public enum AuthErrorCode implements ErrorCode {
LOGIN_ID_NOT_FOUND("LOGIN_ID_NOT_FOUND", HttpStatus.UNAUTHORIZED), LOGIN_ID_NOT_FOUND("LOGIN_ID_NOT_FOUND", HttpStatus.UNAUTHORIZED),
LOGIN_PASSWORD_MISMATCH("LOGIN_PASSWORD_MISMATCH", HttpStatus.UNAUTHORIZED), LOGIN_PASSWORD_MISMATCH("LOGIN_PASSWORD_MISMATCH", HttpStatus.UNAUTHORIZED),
LOGIN_PASSWORD_EXCEEDED("LOGIN_PASSWORD_EXCEEDED", HttpStatus.UNAUTHORIZED), LOGIN_PASSWORD_EXCEEDED("LOGIN_PASSWORD_EXCEEDED", HttpStatus.UNAUTHORIZED),
REFRESH_TOKEN_EXPIRED_OR_REVOKED("REFRESH_TOKEN_EXPIRED_OR_REVOKED", HttpStatus.UNAUTHORIZED), REFRESH_TOKEN_EXPIRED_OR_REVOKED("REFRESH_TOKEN_EXPIRED_OR_REVOKED", HttpStatus.UNAUTHORIZED),
REFRESH_TOKEN_MISMATCH("REFRESH_TOKEN_MISMATCH", HttpStatus.UNAUTHORIZED); REFRESH_TOKEN_MISMATCH("REFRESH_TOKEN_MISMATCH", HttpStatus.UNAUTHORIZED);
private final String code; private final String code;
private final HttpStatus status; private final HttpStatus status;
AuthErrorCode(String code, HttpStatus status) { AuthErrorCode(String code, HttpStatus status) {
this.code = code; this.code = code;
this.status = status; this.status = status;
} }
} }

View File

@@ -1,10 +1,10 @@
package com.kamco.cd.training.common.exception; package com.kamco.cd.training.common.exception;
import org.springframework.http.HttpStatus; import org.springframework.http.HttpStatus;
public class BadRequestException extends CustomApiException { public class BadRequestException extends CustomApiException {
public BadRequestException(String message) { public BadRequestException(String message) {
super("BAD_REQUEST", HttpStatus.BAD_REQUEST, message); super("BAD_REQUEST", HttpStatus.BAD_REQUEST, message);
} }
} }

View File

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

View File

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

View File

@@ -1,10 +1,10 @@
package com.kamco.cd.training.common.exception; package com.kamco.cd.training.common.exception;
import org.springframework.http.HttpStatus; import org.springframework.http.HttpStatus;
public class NotFoundException extends CustomApiException { public class NotFoundException extends CustomApiException {
public NotFoundException(String message) { public NotFoundException(String message) {
super("NOT_FOUND", HttpStatus.NOT_FOUND, message); super("NOT_FOUND", HttpStatus.NOT_FOUND, message);
} }
} }

View File

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

View File

@@ -1,38 +1,38 @@
package com.kamco.cd.training.common.service; package com.kamco.cd.training.common.service;
import org.springframework.data.domain.Page; import org.springframework.data.domain.Page;
/** /**
* Base Core Service Interface * Base Core Service Interface
* *
* <p>CRUD operations를 정의하는 기본 서비스 인터페이스 * <p>CRUD operations를 정의하는 기본 서비스 인터페이스
* *
* @param <T> Entity 타입 * @param <T> Entity 타입
* @param <ID> Entity의 ID 타입 * @param <ID> Entity의 ID 타입
* @param <S> Search Request 타입 * @param <S> Search Request 타입
*/ */
public interface BaseCoreService<T, ID, S> { public interface BaseCoreService<T, ID, S> {
/** /**
* ID로 엔티티를 삭제합니다. * ID로 엔티티를 삭제합니다.
* *
* @param id 삭제할 엔티티의 ID * @param id 삭제할 엔티티의 ID
*/ */
void remove(ID id); void remove(ID id);
/** /**
* ID로 단건 조회합니다. * ID로 단건 조회합니다.
* *
* @param id 조회할 엔티티의 ID * @param id 조회할 엔티티의 ID
* @return 조회된 엔티티 * @return 조회된 엔티티
*/ */
T getOneById(ID id); T getOneById(ID id);
/** /**
* 검색 조건과 페이징으로 조회합니다. * 검색 조건과 페이징으로 조회합니다.
* *
* @param searchReq 검색 조건 * @param searchReq 검색 조건
* @return 페이징 처리된 검색 결과 * @return 페이징 처리된 검색 결과
*/ */
Page<T> search(S searchReq); 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; package com.kamco.cd.training.common.utils;
import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.ObjectMapper;
import com.kamco.cd.training.code.dto.CommonCodeDto.Basic; import com.kamco.cd.training.code.dto.CommonCodeDto.Basic;
import com.kamco.cd.training.code.service.CommonCodeService; import com.kamco.cd.training.code.service.CommonCodeService;
import com.kamco.cd.training.config.api.ApiResponseDto; import com.kamco.cd.training.config.api.ApiResponseDto;
import java.util.List; import java.util.List;
import java.util.Optional; import java.util.Optional;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component; import org.springframework.stereotype.Component;
/** /**
* 공통코드 조회 유틸리티 클래스 애플리케이션 전역에서 공통코드를 조회하기 위한 유틸리티입니다. training 서버는 Redis 사용하고 Spring 내장 메모리 캐시 * 공통코드 조회 유틸리티 클래스 애플리케이션 전역에서 공통코드를 조회하기 위한 유틸리티입니다. training 서버는 Redis 사용하고 Spring 내장 메모리 캐시
* 사용합니다. * 사용합니다.
*/ */
// //
@Slf4j @Slf4j
@Component @Component
public class CommonCodeUtil { public class CommonCodeUtil {
private final CommonCodeService commonCodeService; private final CommonCodeService commonCodeService;
@Autowired private ObjectMapper objectMapper; @Autowired private ObjectMapper objectMapper;
public CommonCodeUtil(CommonCodeService commonCodeService) { public CommonCodeUtil(CommonCodeService commonCodeService) {
this.commonCodeService = commonCodeService; this.commonCodeService = commonCodeService;
} }
/** /**
* 모든 공통코드 조회 * 모든 공통코드 조회
* *
* @return 캐시된 모든 공통코드 목록 * @return 캐시된 모든 공통코드 목록
*/ */
public List<Basic> getAllCommonCodes() { public List<Basic> getAllCommonCodes() {
try { try {
return commonCodeService.getFindAll(); return commonCodeService.getFindAll();
} catch (Exception e) { } catch (Exception e) {
log.error("공통코드 전체 조회 중 오류 발생", e); log.error("공통코드 전체 조회 중 오류 발생", e);
return List.of(); return List.of();
} }
} }
/** /**
* 특정 코드로 공통코드 조회 * 특정 코드로 공통코드 조회
* *
* @param code 코드값 * @param code 코드값
* @return 해당 코드의 공통코드 목록 * @return 해당 코드의 공통코드 목록
*/ */
public List<Basic> getCommonCodesByCode(String code) { public List<Basic> getCommonCodesByCode(String code) {
if (code == null || code.isEmpty()) { if (code == null || code.isEmpty()) {
log.warn("유효하지 않은 코드: {}", code); log.warn("유효하지 않은 코드: {}", code);
return List.of(); return List.of();
} }
try { try {
return commonCodeService.findByCode(code); return commonCodeService.findByCode(code);
} catch (Exception e) { } catch (Exception e) {
log.error("코드 기반 공통코드 조회 중 오류 발생: {}", code, e); log.error("코드 기반 공통코드 조회 중 오류 발생: {}", code, e);
return List.of(); return List.of();
} }
} }
/** /**
* 특정 ID로 공통코드 단건 조회 * 특정 ID로 공통코드 단건 조회
* *
* @param id 공통코드 ID * @param id 공통코드 ID
* @return 조회된 공통코드 * @return 조회된 공통코드
*/ */
public Optional<Basic> getCommonCodeById(Long id) { public Optional<Basic> getCommonCodeById(Long id) {
if (id == null || id <= 0) { if (id == null || id <= 0) {
log.warn("유효하지 않은 ID: {}", id); log.warn("유효하지 않은 ID: {}", id);
return Optional.empty(); return Optional.empty();
} }
try { try {
return Optional.of(commonCodeService.getOneById(id)); return Optional.of(commonCodeService.getOneById(id));
} catch (Exception e) { } catch (Exception e) {
log.error("ID 기반 공통코드 조회 중 오류 발생: {}", id, e); log.error("ID 기반 공통코드 조회 중 오류 발생: {}", id, e);
return Optional.empty(); return Optional.empty();
} }
} }
/** /**
* 상위 코드와 하위 코드로 공통코드명 조회 * 상위 코드와 하위 코드로 공통코드명 조회
* *
* @param parentCode 상위 코드 * @param parentCode 상위 코드
* @param childCode 하위 코드 * @param childCode 하위 코드
* @return 공통코드명 * @return 공통코드명
*/ */
public Optional<String> getCodeName(String parentCode, String childCode) { public Optional<String> getCodeName(String parentCode, String childCode) {
if (parentCode == null || parentCode.isEmpty() || childCode == null || childCode.isEmpty()) { if (parentCode == null || parentCode.isEmpty() || childCode == null || childCode.isEmpty()) {
log.warn("유효하지 않은 코드: parentCode={}, childCode={}", parentCode, childCode); log.warn("유효하지 않은 코드: parentCode={}, childCode={}", parentCode, childCode);
return Optional.empty(); return Optional.empty();
} }
try { try {
return commonCodeService.getCode(parentCode, childCode); return commonCodeService.getCode(parentCode, childCode);
} catch (Exception e) { } catch (Exception e) {
log.error("코드명 조회 중 오류 발생: parentCode={}, childCode={}", parentCode, childCode, e); log.error("코드명 조회 중 오류 발생: parentCode={}, childCode={}", parentCode, childCode, e);
return Optional.empty(); return Optional.empty();
} }
} }
/** /**
* 상위 코드를 기반으로 하위 코드 조회 * 상위 코드를 기반으로 하위 코드 조회
* *
* @param parentCode 상위 코드 * @param parentCode 상위 코드
* @return 해당 상위 코드의 하위 공통코드 목록 * @return 해당 상위 코드의 하위 공통코드 목록
*/ */
public List<Basic> getChildCodesByParentCode(String parentCode) { public List<Basic> getChildCodesByParentCode(String parentCode) {
if (parentCode == null || parentCode.isEmpty()) { if (parentCode == null || parentCode.isEmpty()) {
log.warn("유효하지 않은 상위 코드: {}", parentCode); log.warn("유효하지 않은 상위 코드: {}", parentCode);
return List.of(); return List.of();
} }
try { try {
return commonCodeService.getFindAll().stream() return commonCodeService.getFindAll().stream()
.filter(code -> parentCode.equals(code.getCode())) .filter(code -> parentCode.equals(code.getCode()))
.findFirst() .findFirst()
.map(Basic::getChildren) .map(Basic::getChildren)
.orElse(List.of()); .orElse(List.of());
} catch (Exception e) { } catch (Exception e) {
log.error("상위 코드 기반 하위 코드 조회 중 오류 발생: {}", parentCode, e); log.error("상위 코드 기반 하위 코드 조회 중 오류 발생: {}", parentCode, e);
return List.of(); return List.of();
} }
} }
/** /**
* 코드 사용 가능 여부 확인 * 코드 사용 가능 여부 확인
* *
* @param parentId 상위 코드 ID -> 1depth 는 null 허용 (파라미터 미필수로 변경) * @param parentId 상위 코드 ID -> 1depth 는 null 허용 (파라미터 미필수로 변경)
* @param code 확인할 코드값 * @param code 확인할 코드값
* @return 사용 가능 여부 (true: 사용 가능, false: 중복 또는 오류) * @return 사용 가능 여부 (true: 사용 가능, false: 중복 또는 오류)
*/ */
public boolean isCodeAvailable(Long parentId, String code) { public boolean isCodeAvailable(Long parentId, String code) {
if (parentId <= 0 || code == null || code.isEmpty()) { if (parentId <= 0 || code == null || code.isEmpty()) {
log.warn("유효하지 않은 입력: parentId={}, code={}", parentId, code); log.warn("유효하지 않은 입력: parentId={}, code={}", parentId, code);
return false; return false;
} }
try { try {
ApiResponseDto.ResponseObj response = commonCodeService.getCodeCheckDuplicate(parentId, code); ApiResponseDto.ResponseObj response = commonCodeService.getCodeCheckDuplicate(parentId, code);
// ResponseObj의 code 필드 : OK이면 성공, 아니면 실패 // ResponseObj의 code 필드 : OK이면 성공, 아니면 실패
return response.getCode() != null return response.getCode() != null
&& response.getCode().equals(ApiResponseDto.ApiResponseCode.OK); && response.getCode().equals(ApiResponseDto.ApiResponseCode.OK);
} catch (Exception e) { } catch (Exception e) {
log.error("코드 중복 확인 중 오류 발생: parentId={}, code={}", parentId, code, e); log.error("코드 중복 확인 중 오류 발생: parentId={}, code={}", parentId, code, e);
return false; return false;
} }
} }
} }

View File

@@ -1,32 +1,32 @@
package com.kamco.cd.training.common.utils; package com.kamco.cd.training.common.utils;
import com.kamco.cd.training.auth.BCryptSaltGenerator; import com.kamco.cd.training.auth.BCryptSaltGenerator;
import java.util.regex.Pattern; import java.util.regex.Pattern;
import org.mindrot.jbcrypt.BCrypt; import org.mindrot.jbcrypt.BCrypt;
public class CommonStringUtils { public class CommonStringUtils {
/** /**
* 영문, 숫자, 특수문자를 모두 포함하여 8~20자 이내의 비밀번호 * 영문, 숫자, 특수문자를 모두 포함하여 8~20자 이내의 비밀번호
* *
* @param password 벨리데이션 필요한 패스워드 * @param password 벨리데이션 필요한 패스워드
* @return * @return
*/ */
public static boolean isValidPassword(String password) { public static boolean isValidPassword(String password) {
String passwordPattern = String passwordPattern =
"^(?=.*[A-Za-z])(?=.*\\d)(?=.*[!@#$%^&*()_+\\-\\[\\]{};':\"\\\\|,.<>/?]).{8,20}$"; "^(?=.*[A-Za-z])(?=.*\\d)(?=.*[!@#$%^&*()_+\\-\\[\\]{};':\"\\\\|,.<>/?]).{8,20}$";
return Pattern.matches(passwordPattern, password); return Pattern.matches(passwordPattern, password);
} }
/** /**
* 패스워드 암호화 * 패스워드 암호화
* *
* @param password 암호화 필요한 패스워드 * @param password 암호화 필요한 패스워드
* @param employeeNo salt 생성에 필요한 사원번호 * @param employeeNo salt 생성에 필요한 사원번호
* @return * @return
*/ */
public static String hashPassword(String password, String employeeNo) { public static String hashPassword(String password, String employeeNo) {
String salt = BCryptSaltGenerator.generateSaltWithEmployeeNo(employeeNo.trim()); String salt = BCryptSaltGenerator.generateSaltWithEmployeeNo(employeeNo.trim());
return BCrypt.hashpw(password.trim(), salt); return BCrypt.hashpw(password.trim(), salt);
} }
} }

View File

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

View File

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

View File

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

View File

@@ -1,41 +1,41 @@
package com.kamco.cd.training.common.utils; package com.kamco.cd.training.common.utils;
import com.kamco.cd.training.auth.CustomUserDetails; import com.kamco.cd.training.auth.CustomUserDetails;
import com.kamco.cd.training.members.dto.MembersDto; import com.kamco.cd.training.members.dto.MembersDto;
import com.kamco.cd.training.postgres.entity.MemberEntity; import com.kamco.cd.training.postgres.entity.MemberEntity;
import java.util.Optional; import java.util.Optional;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.stereotype.Component; import org.springframework.stereotype.Component;
@Component @Component
@RequiredArgsConstructor @RequiredArgsConstructor
public class UserUtil { public class UserUtil {
public MembersDto.Member getCurrentUser() { public MembersDto.Member getCurrentUser() {
return Optional.ofNullable(SecurityContextHolder.getContext().getAuthentication()) return Optional.ofNullable(SecurityContextHolder.getContext().getAuthentication())
.filter(auth -> auth.getPrincipal() instanceof CustomUserDetails) .filter(auth -> auth.getPrincipal() instanceof CustomUserDetails)
.map( .map(
auth -> { auth -> {
CustomUserDetails user = (CustomUserDetails) auth.getPrincipal(); CustomUserDetails user = (CustomUserDetails) auth.getPrincipal();
MemberEntity m = user.getMember(); MemberEntity m = user.getMember();
return new MembersDto.Member(m.getId(), m.getName(), m.getEmployeeNo()); return new MembersDto.Member(m.getId(), m.getName(), m.getEmployeeNo());
}) })
.orElse(null); .orElse(null);
} }
public Long getId() { public Long getId() {
MembersDto.Member user = getCurrentUser(); MembersDto.Member user = getCurrentUser();
return user != null ? user.getId() : null; return user != null ? user.getId() : null;
} }
public String getName() { public String getName() {
MembersDto.Member user = getCurrentUser(); MembersDto.Member user = getCurrentUser();
return user != null ? user.getName() : null; return user != null ? user.getName() : null;
} }
public String getEmployeeNo() { public String getEmployeeNo() {
MembersDto.Member user = getCurrentUser(); MembersDto.Member user = getCurrentUser();
return user != null ? user.getEmployeeNo() : null; return user != null ? user.getEmployeeNo() : null;
} }
} }

View File

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

View File

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

View File

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

View File

@@ -1,26 +1,26 @@
package com.kamco.cd.training.common.utils.enums; package com.kamco.cd.training.common.utils.enums;
import com.kamco.cd.training.common.utils.interfaces.EnumValid; import com.kamco.cd.training.common.utils.interfaces.EnumValid;
import jakarta.validation.ConstraintValidator; import jakarta.validation.ConstraintValidator;
import jakarta.validation.ConstraintValidatorContext; import jakarta.validation.ConstraintValidatorContext;
import java.util.Arrays; import java.util.Arrays;
import java.util.Set; import java.util.Set;
import java.util.stream.Collectors; import java.util.stream.Collectors;
public class EnumValidator implements ConstraintValidator<EnumValid, String> { public class EnumValidator implements ConstraintValidator<EnumValid, String> {
private Set<String> acceptedValues; private Set<String> acceptedValues;
@Override @Override
public void initialize(EnumValid constraintAnnotation) { public void initialize(EnumValid constraintAnnotation) {
acceptedValues = acceptedValues =
Arrays.stream(constraintAnnotation.enumClass().getEnumConstants()) Arrays.stream(constraintAnnotation.enumClass().getEnumConstants())
.map(Enum::name) .map(Enum::name)
.collect(Collectors.toSet()); .collect(Collectors.toSet());
} }
@Override @Override
public boolean isValid(String value, ConstraintValidatorContext context) { public boolean isValid(String value, ConstraintValidatorContext context) {
return value != null && acceptedValues.contains(value); return value != null && acceptedValues.contains(value);
} }
} }

View File

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

View File

@@ -1,36 +1,36 @@
package com.kamco.cd.training.common.utils.geometry; package com.kamco.cd.training.common.utils.geometry;
import com.fasterxml.jackson.core.JacksonException; import com.fasterxml.jackson.core.JacksonException;
import com.fasterxml.jackson.core.JsonParser; import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.databind.DeserializationContext; import com.fasterxml.jackson.databind.DeserializationContext;
import com.fasterxml.jackson.databind.deser.std.StdDeserializer; import com.fasterxml.jackson.databind.deser.std.StdDeserializer;
import java.io.IOException; import java.io.IOException;
import org.locationtech.jts.geom.Geometry; import org.locationtech.jts.geom.Geometry;
import org.locationtech.jts.io.geojson.GeoJsonReader; import org.locationtech.jts.io.geojson.GeoJsonReader;
import org.springframework.util.StringUtils; import org.springframework.util.StringUtils;
public class GeometryDeserializer<T extends Geometry> extends StdDeserializer<T> { public class GeometryDeserializer<T extends Geometry> extends StdDeserializer<T> {
public GeometryDeserializer(Class<T> targetType) { public GeometryDeserializer(Class<T> targetType) {
super(targetType); super(targetType);
} }
// TODO: test code // TODO: test code
@SuppressWarnings("unchecked") @SuppressWarnings("unchecked")
@Override @Override
public T deserialize(JsonParser jsonParser, DeserializationContext deserializationContext) public T deserialize(JsonParser jsonParser, DeserializationContext deserializationContext)
throws IOException, JacksonException { throws IOException, JacksonException {
String json = jsonParser.readValueAsTree().toString(); String json = jsonParser.readValueAsTree().toString();
if (!StringUtils.hasText(json)) { if (!StringUtils.hasText(json)) {
return null; return null;
} }
try { try {
GeoJsonReader reader = new GeoJsonReader(); GeoJsonReader reader = new GeoJsonReader();
return (T) reader.read(json); return (T) reader.read(json);
} catch (Exception e) { } catch (Exception e) {
throw new IllegalArgumentException("Failed to deserialize GeoJSON into Geometry", 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; package com.kamco.cd.training.common.utils.geometry;
import com.fasterxml.jackson.core.JsonGenerator; import com.fasterxml.jackson.core.JsonGenerator;
import com.fasterxml.jackson.databind.SerializerProvider; import com.fasterxml.jackson.databind.SerializerProvider;
import com.fasterxml.jackson.databind.ser.std.StdSerializer; import com.fasterxml.jackson.databind.ser.std.StdSerializer;
import java.io.IOException; import java.io.IOException;
import java.util.Objects; import java.util.Objects;
import org.locationtech.jts.geom.Geometry; import org.locationtech.jts.geom.Geometry;
import org.locationtech.jts.io.geojson.GeoJsonWriter; import org.locationtech.jts.io.geojson.GeoJsonWriter;
public class GeometrySerializer<T extends Geometry> extends StdSerializer<T> { public class GeometrySerializer<T extends Geometry> extends StdSerializer<T> {
// TODO: test code // TODO: test code
public GeometrySerializer(Class<T> targetType) { public GeometrySerializer(Class<T> targetType) {
super(targetType); super(targetType);
} }
@Override @Override
public void serialize( public void serialize(
T geometry, JsonGenerator jsonGenerator, SerializerProvider serializerProvider) T geometry, JsonGenerator jsonGenerator, SerializerProvider serializerProvider)
throws IOException { throws IOException {
if (Objects.nonNull(geometry)) { if (Objects.nonNull(geometry)) {
// default: 8자리 강제로 반올림시킴. 16자리로 늘려줌 // default: 8자리 강제로 반올림시킴. 16자리로 늘려줌
GeoJsonWriter writer = new GeoJsonWriter(16); GeoJsonWriter writer = new GeoJsonWriter(16);
String json = writer.write(geometry); String json = writer.write(geometry);
jsonGenerator.writeRawValue(json); jsonGenerator.writeRawValue(json);
} else { } else {
jsonGenerator.writeNull(); jsonGenerator.writeNull();
} }
} }
} }

View File

@@ -1,18 +1,18 @@
package com.kamco.cd.training.common.utils.html; package com.kamco.cd.training.common.utils.html;
import com.fasterxml.jackson.core.JacksonException; import com.fasterxml.jackson.core.JacksonException;
import com.fasterxml.jackson.core.JsonParser; import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.databind.DeserializationContext; import com.fasterxml.jackson.databind.DeserializationContext;
import com.fasterxml.jackson.databind.JsonDeserializer; import com.fasterxml.jackson.databind.JsonDeserializer;
import java.io.IOException; import java.io.IOException;
import org.springframework.web.util.HtmlUtils; import org.springframework.web.util.HtmlUtils;
public class HtmlEscapeDeserializer extends JsonDeserializer<Object> { public class HtmlEscapeDeserializer extends JsonDeserializer<Object> {
@Override @Override
public Object deserialize(JsonParser jsonParser, DeserializationContext deserializationContext) public Object deserialize(JsonParser jsonParser, DeserializationContext deserializationContext)
throws IOException, JacksonException { throws IOException, JacksonException {
String value = jsonParser.getValueAsString(); String value = jsonParser.getValueAsString();
return value == null ? null : HtmlUtils.htmlEscape(value); return value == null ? null : HtmlUtils.htmlEscape(value);
} }
} }

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,23 @@
package com.kamco.cd.training.config;
import java.util.concurrent.Executor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.annotation.EnableAsync;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
@Configuration
@EnableAsync
public class AsyncConfig {
@Bean(name = "trainJobExecutor")
public Executor trainJobExecutor() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
executor.setCorePoolSize(4); // 동시에 4개 실행
executor.setMaxPoolSize(8); // 최대 8개
executor.setQueueCapacity(200); // 대기 큐
executor.setThreadNamePrefix("train-job-");
executor.initialize();
return executor;
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,28 +1,28 @@
package com.kamco.cd.training.config.api; package com.kamco.cd.training.config.api;
import jakarta.servlet.FilterChain; import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException; import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse; import jakarta.servlet.http.HttpServletResponse;
import java.io.IOException; import java.io.IOException;
import org.springframework.stereotype.Component; import org.springframework.stereotype.Component;
import org.springframework.web.filter.OncePerRequestFilter; import org.springframework.web.filter.OncePerRequestFilter;
import org.springframework.web.util.ContentCachingRequestWrapper; import org.springframework.web.util.ContentCachingRequestWrapper;
import org.springframework.web.util.ContentCachingResponseWrapper; import org.springframework.web.util.ContentCachingResponseWrapper;
@Component @Component
public class ApiLogFilter extends OncePerRequestFilter { public class ApiLogFilter extends OncePerRequestFilter {
@Override @Override
protected void doFilterInternal( protected void doFilterInternal(
HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
throws ServletException, IOException { throws ServletException, IOException {
ContentCachingRequestWrapper wrappedRequest = new ContentCachingRequestWrapper(request); ContentCachingRequestWrapper wrappedRequest = new ContentCachingRequestWrapper(request);
ContentCachingResponseWrapper wrappedResponse = new ContentCachingResponseWrapper(response); ContentCachingResponseWrapper wrappedResponse = new ContentCachingResponseWrapper(response);
filterChain.doFilter(wrappedRequest, wrappedResponse); filterChain.doFilter(wrappedRequest, wrappedResponse);
// 반드시 response body copy // 반드시 response body copy
wrappedResponse.copyBodyToResponse(); wrappedResponse.copyBodyToResponse();
} }
} }

View File

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

View File

@@ -1,124 +1,163 @@
package com.kamco.cd.training.config.api; package com.kamco.cd.training.config.api;
import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.ObjectMapper;
import com.kamco.cd.training.auth.CustomUserDetails; import com.kamco.cd.training.auth.CustomUserDetails;
import com.kamco.cd.training.menu.service.MenuService; import com.kamco.cd.training.common.utils.HeaderUtil;
import com.kamco.cd.training.postgres.entity.AuditLogEntity; import com.kamco.cd.training.log.dto.EventType;
import com.kamco.cd.training.postgres.repository.log.AuditLogRepository; import com.kamco.cd.training.menu.dto.MenuDto;
import jakarta.servlet.http.HttpServletRequest; import com.kamco.cd.training.menu.service.MenuService;
import org.springframework.beans.factory.annotation.Autowired; import com.kamco.cd.training.postgres.entity.AuditLogEntity;
import org.springframework.core.MethodParameter; import com.kamco.cd.training.postgres.repository.log.AuditLogRepository;
import org.springframework.http.MediaType; import jakarta.servlet.http.HttpServletRequest;
import org.springframework.http.converter.HttpMessageConverter; import java.util.LinkedHashMap;
import org.springframework.http.server.ServerHttpRequest; import java.util.List;
import org.springframework.http.server.ServerHttpResponse; import java.util.Optional;
import org.springframework.http.server.ServletServerHttpRequest; import lombok.extern.slf4j.Slf4j;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.RestControllerAdvice; import org.springframework.core.MethodParameter;
import org.springframework.web.servlet.mvc.method.annotation.ResponseBodyAdvice; import org.springframework.http.MediaType;
import org.springframework.web.util.ContentCachingRequestWrapper; import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.http.server.ServerHttpRequest;
/** import org.springframework.http.server.ServerHttpResponse;
* ApiResponseDto의 내장된 HTTP 상태 코드를 실제 HTTP 응답에 적용하는 Advice import org.springframework.http.server.ServletServerHttpRequest;
* import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
* <p>createOK() → 201 CREATED ok() → 200 OK deleteOk() → 204 NO_CONTENT import org.springframework.web.bind.annotation.RestControllerAdvice;
*/ import org.springframework.web.servlet.mvc.method.annotation.ResponseBodyAdvice;
@RestControllerAdvice import org.springframework.web.util.ContentCachingRequestWrapper;
public class ApiResponseAdvice implements ResponseBodyAdvice<Object> {
/**
private final AuditLogRepository auditLogRepository; * ApiResponseDto의 내장된 HTTP 상태 코드를 실제 HTTP 응답에 적용하는 Advice
private final MenuService menuService; *
* <p>createOK() → 201 CREATED ok() → 200 OK deleteOk() → 204 NO_CONTENT
@Autowired private ObjectMapper objectMapper; */
@Slf4j
public ApiResponseAdvice(AuditLogRepository auditLogRepository, MenuService menuService) { @RestControllerAdvice
this.auditLogRepository = auditLogRepository; public class ApiResponseAdvice implements ResponseBodyAdvice<Object> {
this.menuService = menuService;
} private final AuditLogRepository auditLogRepository;
private final MenuService menuService;
@Override
public boolean supports( @Autowired private ObjectMapper objectMapper;
MethodParameter returnType, Class<? extends HttpMessageConverter<?>> converterType) {
// ApiResponseDto를 반환하는 경우에만 적용 public ApiResponseAdvice(AuditLogRepository auditLogRepository, MenuService menuService) {
return returnType.getParameterType().equals(ApiResponseDto.class); this.auditLogRepository = auditLogRepository;
} this.menuService = menuService;
}
@Override
public Object beforeBodyWrite( @Override
Object body, public boolean supports(
MethodParameter returnType, MethodParameter returnType, Class<? extends HttpMessageConverter<?>> converterType) {
MediaType selectedContentType, // ApiResponseDto를 반환하는 경우에만 적용
Class<? extends HttpMessageConverter<?>> selectedConverterType, return returnType.getParameterType().equals(ApiResponseDto.class);
ServerHttpRequest request, }
ServerHttpResponse response) {
@Override
HttpServletRequest servletRequest = ((ServletServerHttpRequest) request).getServletRequest(); public Object beforeBodyWrite(
ContentCachingRequestWrapper contentWrapper = null; Object body,
if (servletRequest instanceof ContentCachingRequestWrapper wrapper) { MethodParameter returnType,
contentWrapper = wrapper; MediaType selectedContentType,
} Class<? extends HttpMessageConverter<?>> selectedConverterType,
ServerHttpRequest request,
if (body instanceof ApiResponseDto<?> apiResponse) { ServerHttpResponse response) {
response.setStatusCode(apiResponse.getHttpStatus());
HttpServletRequest servletRequest = ((ServletServerHttpRequest) request).getServletRequest();
String ip = ApiLogFunction.getClientIp(servletRequest); ContentCachingRequestWrapper contentWrapper = null;
Long userid = null; if (servletRequest instanceof ContentCachingRequestWrapper wrapper) {
contentWrapper = wrapper;
if (servletRequest.getUserPrincipal() instanceof UsernamePasswordAuthenticationToken auth }
&& auth.getPrincipal() instanceof CustomUserDetails customUserDetails) {
userid = customUserDetails.getMember().getId(); if (body instanceof ApiResponseDto<?> apiResponse) {
} response.setStatusCode(apiResponse.getHttpStatus());
String requestBody; String actionType = HeaderUtil.get(servletRequest, "kamco-action-type");
// 멀티파트 요청은 바디 로깅을 생략 (파일 바이너리로 인한 문제 예방) // actionType 이 없으면 로그 저장하지 않기 || download 는 FileDownloadInterceptor 에서 하기
MediaType reqContentType = null; // (file down URL prefix 추가는 WebConfig.java 에 하기)
try { if (actionType == null || actionType.equalsIgnoreCase("download")) {
String ct = servletRequest.getContentType(); return body;
reqContentType = ct != null ? MediaType.valueOf(ct) : null; }
} catch (Exception ignored) {
} String ip =
if (reqContentType != null && MediaType.MULTIPART_FORM_DATA.includes(reqContentType)) { Optional.ofNullable(HeaderUtil.get(servletRequest, "kamco-user-ip"))
requestBody = "(multipart omitted)"; .orElseGet(() -> ApiLogFunction.getXFowardedForIp(servletRequest));
} else { Long userid = null;
requestBody = ApiLogFunction.getRequestBody(servletRequest, contentWrapper); String loginAttemptId = null;
requestBody = maskSensitiveFields(requestBody);
} // 로그인 시도할 때
if (servletRequest.getRequestURI().contains("/api/auth/signin")) {
AuditLogEntity log = loginAttemptId = HeaderUtil.get(servletRequest, "kamco-login-attempt-id");
new AuditLogEntity( } else {
userid, if (servletRequest.getUserPrincipal() instanceof UsernamePasswordAuthenticationToken auth
ApiLogFunction.getEventType(servletRequest), && auth.getPrincipal() instanceof CustomUserDetails customUserDetails) {
ApiLogFunction.isSuccessFail(apiResponse), userid = customUserDetails.getMember().getId();
ApiLogFunction.getUriMenuInfo( }
menuService.getFindAll(), servletRequest.getRequestURI()), }
ip,
servletRequest.getRequestURI(), String requestBody;
requestBody, // 멀티파트 요청은 바디 로깅을 생략 (파일 바이너리로 인한 문제 예방)
apiResponse.getErrorLogUid()); MediaType reqContentType = null;
auditLogRepository.save(log); try {
} String ct = servletRequest.getContentType();
reqContentType = ct != null ? MediaType.valueOf(ct) : null;
return body; } catch (Exception ignored) {
} }
if (reqContentType != null && MediaType.MULTIPART_FORM_DATA.includes(reqContentType)) {
/** requestBody = "(multipart omitted)";
* 마스킹 } else {
* requestBody = ApiLogFunction.getRequestBody(servletRequest, contentWrapper);
* @param json requestBody = maskSensitiveFields(requestBody);
* @return }
*/
private String maskSensitiveFields(String json) { List<?> list = menuService.getFindAll();
if (json == null) { List<MenuDto.Basic> result =
return null; list.stream()
} .map(
item -> {
// password 마스킹 if (item instanceof LinkedHashMap<?, ?> map) {
json = json.replaceAll("\"password\"\\s*:\\s*\"[^\"]*\"", "\"password\":\"****\""); return objectMapper.convertValue(map, MenuDto.Basic.class);
} else if (item instanceof MenuDto.Basic dto) {
// 토큰 마스킹 return dto;
json = json.replaceAll("\"accessToken\"\\s*:\\s*\"[^\"]*\"", "\"accessToken\":\"****\""); } else {
json = json.replaceAll("\"refreshToken\"\\s*:\\s*\"[^\"]*\"", "\"refreshToken\":\"****\""); throw new IllegalStateException("Unsupported cache type: " + item.getClass());
}
return json; })
} .toList();
}
AuditLogEntity log =
new AuditLogEntity(
userid,
EventType.fromName(actionType),
ApiLogFunction.isSuccessFail(apiResponse),
ApiLogFunction.getUriMenuInfo(result, servletRequest.getRequestURI()),
ip,
servletRequest.getRequestURI(),
ApiLogFunction.cutRequestBody(requestBody),
apiResponse.getErrorLogUid(),
null,
loginAttemptId);
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; package com.kamco.cd.training.config.api;
import com.fasterxml.jackson.annotation.JsonIgnore; import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.annotation.JsonInclude; import com.fasterxml.jackson.annotation.JsonInclude;
import com.kamco.cd.training.common.utils.enums.EnumType; import com.kamco.cd.training.common.utils.enums.EnumType;
import lombok.Getter; import lombok.Getter;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import lombok.ToString; import lombok.ToString;
import org.springframework.http.HttpStatus; import org.springframework.http.HttpStatus;
@Getter @Getter
@ToString @ToString
public class ApiResponseDto<T> { public class ApiResponseDto<T> {
private T data; private T data;
@JsonInclude(JsonInclude.Include.NON_NULL) @JsonInclude(JsonInclude.Include.NON_NULL)
private Error error; private Error error;
@JsonInclude(JsonInclude.Include.NON_NULL) @JsonInclude(JsonInclude.Include.NON_NULL)
private T errorData; private T errorData;
@JsonIgnore private HttpStatus httpStatus; @JsonIgnore private HttpStatus httpStatus;
@JsonIgnore private Long errorLogUid; @JsonIgnore private Long errorLogUid;
public ApiResponseDto(T data) { public ApiResponseDto(T data) {
this.data = data; this.data = data;
} }
private ApiResponseDto(T data, HttpStatus httpStatus) { private ApiResponseDto(T data, HttpStatus httpStatus) {
this.data = data; this.data = data;
this.httpStatus = httpStatus; this.httpStatus = httpStatus;
} }
public ApiResponseDto(ApiResponseCode code) { public ApiResponseDto(ApiResponseCode code) {
this.error = new Error(code.getId(), code.getMessage()); this.error = new Error(code.getId(), code.getMessage());
} }
public ApiResponseDto(ApiResponseCode code, String message) { public ApiResponseDto(ApiResponseCode code, String message) {
this.error = new Error(code.getId(), message); this.error = new Error(code.getId(), message);
} }
public ApiResponseDto(ApiResponseCode code, String message, HttpStatus httpStatus) { public ApiResponseDto(ApiResponseCode code, String message, HttpStatus httpStatus) {
this.error = new Error(code.getId(), message); this.error = new Error(code.getId(), message);
this.httpStatus = httpStatus; this.httpStatus = httpStatus;
} }
public ApiResponseDto( public ApiResponseDto(
ApiResponseCode code, String message, HttpStatus httpStatus, Long errorLogUid) { ApiResponseCode code, String message, HttpStatus httpStatus, Long errorLogUid) {
this.error = new Error(code.getId(), message); this.error = new Error(code.getId(), message);
this.httpStatus = httpStatus; this.httpStatus = httpStatus;
this.errorLogUid = errorLogUid; this.errorLogUid = errorLogUid;
} }
public ApiResponseDto(ApiResponseCode code, String message, T errorData) { public ApiResponseDto(ApiResponseCode code, String message, T errorData) {
this.error = new Error(code.getId(), message); this.error = new Error(code.getId(), message);
this.errorData = errorData; this.errorData = errorData;
} }
// HTTP 상태 코드가 내장된 ApiResponseDto 반환 메서드들 // HTTP 상태 코드가 내장된 ApiResponseDto 반환 메서드들
public static <T> ApiResponseDto<T> createOK(T data) { public static <T> ApiResponseDto<T> createOK(T data) {
return new ApiResponseDto<>(data, HttpStatus.CREATED); return new ApiResponseDto<>(data, HttpStatus.CREATED);
} }
public static <T> ApiResponseDto<T> ok(T data) { public static <T> ApiResponseDto<T> ok(T data) {
return new ApiResponseDto<>(data, HttpStatus.OK); return new ApiResponseDto<>(data, HttpStatus.OK);
} }
public static <T> ApiResponseDto<ResponseObj> okObject(ResponseObj data) { public static <T> ApiResponseDto<ResponseObj> okObject(ResponseObj data) {
if (data.getCode().equals(ApiResponseCode.OK)) { if (data.getCode().equals(ApiResponseCode.OK)) {
return new ApiResponseDto<>(data, HttpStatus.NO_CONTENT); return new ApiResponseDto<>(data, HttpStatus.NO_CONTENT);
} else { } else {
return new ApiResponseDto<>(data.getCode(), data.getMessage(), HttpStatus.CONFLICT); return new ApiResponseDto<>(data.getCode(), data.getMessage(), HttpStatus.CONFLICT);
} }
} }
public static <T> ApiResponseDto<T> deleteOk(T data) { public static <T> ApiResponseDto<T> deleteOk(T data) {
return new ApiResponseDto<>(data, HttpStatus.NO_CONTENT); return new ApiResponseDto<>(data, HttpStatus.NO_CONTENT);
} }
public static ApiResponseDto<String> createException(ApiResponseCode code) { public static ApiResponseDto<String> createException(ApiResponseCode code) {
return new ApiResponseDto<>(code); return new ApiResponseDto<>(code);
} }
public static ApiResponseDto<String> createException(ApiResponseCode code, String message) { public static ApiResponseDto<String> createException(ApiResponseCode code, String message) {
return new ApiResponseDto<>(code, message); return new ApiResponseDto<>(code, message);
} }
public static ApiResponseDto<String> createException( public static ApiResponseDto<String> createException(
ApiResponseCode code, String message, HttpStatus httpStatus) { ApiResponseCode code, String message, HttpStatus httpStatus) {
return new ApiResponseDto<>(code, message, httpStatus); return new ApiResponseDto<>(code, message, httpStatus);
} }
public static ApiResponseDto<String> createException( public static ApiResponseDto<String> createException(
ApiResponseCode code, String message, HttpStatus httpStatus, Long errorLogUid) { ApiResponseCode code, String message, HttpStatus httpStatus, Long errorLogUid) {
return new ApiResponseDto<>(code, message, httpStatus, errorLogUid); return new ApiResponseDto<>(code, message, httpStatus, errorLogUid);
} }
public static <T> ApiResponseDto<T> createException( public static <T> ApiResponseDto<T> createException(
ApiResponseCode code, String message, T data) { ApiResponseCode code, String message, T data) {
return new ApiResponseDto<>(code, message, data); return new ApiResponseDto<>(code, message, data);
} }
@Getter @Getter
public static class Error { public static class Error {
private final String code; private final String code;
private final String message; private final String message;
public Error(String code, String message) { public Error(String code, String message) {
this.code = code; this.code = code;
this.message = message; this.message = message;
} }
} }
/** Error가 아닌 Business상 성공이거나 실패인 경우, 메세지 함께 전달하기 위한 object */ /** Error가 아닌 Business상 성공이거나 실패인 경우, 메세지 함께 전달하기 위한 object */
@Getter @Getter
public static class ResponseObj { public static class ResponseObj {
private final ApiResponseCode code; private final ApiResponseCode code;
private final String message; private final String message;
public ResponseObj(ApiResponseCode code, String message) { public ResponseObj(ApiResponseCode code, String message) {
this.code = code; this.code = code;
this.message = message; this.message = message;
} }
} }
@Getter @Getter
@RequiredArgsConstructor @RequiredArgsConstructor
public enum ApiResponseCode implements EnumType { public enum ApiResponseCode implements EnumType {
// @formatter:off // @formatter:off
OK("요청이 성공하였습니다."), OK("요청이 성공하였습니다."),
BAD_REQUEST("요청 파라미터가 잘못되었습니다."), BAD_REQUEST("요청 파라미터가 잘못되었습니다."),
BAD_GATEWAY("네트워크 상태가 불안정합니다."), BAD_GATEWAY("네트워크 상태가 불안정합니다."),
ALREADY_EXIST_MALL("이미 등록된 쇼핑센터입니다."), ALREADY_EXIST_MALL("이미 등록된 쇼핑센터입니다."),
NOT_FOUND_MAP("지도를 찾을 수 없습니다."), NOT_FOUND_MAP("지도를 찾을 수 없습니다."),
UNAUTHORIZED("권한이 없습니다."), UNAUTHORIZED("권한이 없습니다."),
CONFLICT("이미 등록된 컨텐츠입니다."), CONFLICT("이미 등록된 컨텐츠입니다."),
NOT_FOUND("Resource를 찾을 수 없습니다."), NOT_FOUND("Resource를 찾을 수 없습니다."),
NOT_FOUND_DATA("데이터를 찾을 수 없습니다."), NOT_FOUND_DATA("데이터를 찾을 수 없습니다."),
NOT_FOUND_WEATHER_DATA("날씨 데이터를 찾을 수 없습니다."), NOT_FOUND_WEATHER_DATA("날씨 데이터를 찾을 수 없습니다."),
FAIL_SEND_MESSAGE("메시지를 전송하지 못했습니다."), FAIL_SEND_MESSAGE("메시지를 전송하지 못했습니다."),
TOO_MANY_CONNECTED_MACHINES("연결된 기기가 너무 많습니다."), TOO_MANY_CONNECTED_MACHINES("연결된 기기가 너무 많습니다."),
UNAUTHENTICATED("인증에 실패하였습니다."), UNAUTHENTICATED("인증에 실패하였습니다."),
INVALID_TOKEN("잘못된 토큰입니다."), INVALID_TOKEN("잘못된 토큰입니다."),
EXPIRED_TOKEN("만료된 토큰입니다."), EXPIRED_TOKEN("만료된 토큰입니다."),
INTERNAL_SERVER_ERROR("서버에 문제가 발생 하였습니다."), INTERNAL_SERVER_ERROR("서버에 문제가 발생 하였습니다."),
FORBIDDEN("권한을 확인해주세요."), FORBIDDEN("권한을 확인해주세요."),
INVALID_PASSWORD("잘못된 비밀번호 입니다."), INVALID_PASSWORD("잘못된 비밀번호 입니다."),
NOT_FOUND_CAR_IN("입차정보가 없습니다."), NOT_FOUND_CAR_IN("입차정보가 없습니다."),
WRONG_STATUS("잘못된 상태입니다."), WRONG_STATUS("잘못된 상태입니다."),
FAIL_VERIFICATION("인증에 실패하였습니다."), FAIL_VERIFICATION("인증에 실패하였습니다."),
INVALID_EMAIL("잘못된 형식의 이메일입니다."), INVALID_EMAIL("잘못된 형식의 이메일입니다."),
REQUIRED_EMAIL("이메일은 필수 항목입니다."), REQUIRED_EMAIL("이메일은 필수 항목입니다."),
WRONG_PASSWORD("잘못된 패스워드입니다."), WRONG_PASSWORD("잘못된 패스워드입니다."),
DUPLICATE_EMAIL("이미 가입된 이메일입니다."), DUPLICATE_EMAIL("이미 가입된 이메일입니다."),
DUPLICATE_DATA("이미 등록되어 있습니다."), DUPLICATE_DATA("이미 등록되어 있습니다."),
DATA_INTEGRITY_ERROR("데이터 무결성이 위반되어 요청을 처리할수 없습니다."), DATA_INTEGRITY_ERROR("데이터 무결성이 위반되어 요청을 처리할수 없습니다."),
FOREIGN_KEY_ERROR("참조 중인 데이터가 있어 삭제할 수 없습니다."), FOREIGN_KEY_ERROR("참조 중인 데이터가 있어 삭제할 수 없습니다."),
DUPLICATE_EMPLOYEEID("이미 가입된 사번입니다."), DUPLICATE_EMPLOYEEID("이미 가입된 사번입니다."),
NOT_FOUND_USER_FOR_EMAIL("이메일로 유저를 찾을 수 없습니다."), NOT_FOUND_USER_FOR_EMAIL("이메일로 유저를 찾을 수 없습니다."),
NOT_FOUND_USER("사용자를 찾을 수 없습니다."), NOT_FOUND_USER("사용자를 찾을 수 없습니다."),
UNPROCESSABLE_ENTITY("이 데이터는 삭제할 수 없습니다."), UNPROCESSABLE_ENTITY("이 데이터는 삭제할 수 없습니다."),
LOGIN_ID_NOT_FOUND("아이디를 잘못 입력하셨습니다."), UNPROCESSABLE_ENTITY_UPDATE("이 데이터는 수정할 수 없습니다."),
LOGIN_PASSWORD_MISMATCH("비밀번호를 잘못 입력하셨습니다."), LOGIN_ID_NOT_FOUND("아이디를 잘못 입력하셨습니다."),
LOGIN_PASSWORD_EXCEEDED("비밀번호 오류 횟수를 초과하여 이용하실 수 없습니다.\n로그인 오류에 대해 관리자에게 문의하시기 바랍니다."), LOGIN_PASSWORD_MISMATCH("비밀번호를 잘못 입력하셨습니다."),
REFRESH_TOKEN_EXPIRED_OR_REVOKED("토큰 정보가 만료 되었습니다."), LOGIN_PASSWORD_EXCEEDED("비밀번호 오류 횟수를 초과하여 이용하실 수 없습니다.\n로그인 오류에 대해 관리자에게 문의하시기 바랍니다."),
REFRESH_TOKEN_MISMATCH("토큰 정보가 일치하지 않습니다."), REFRESH_TOKEN_EXPIRED_OR_REVOKED("토큰 정보가 만료 되었습니다."),
INACTIVE_ID("사용할 수 없는 계정입니다."), REFRESH_TOKEN_MISMATCH("토큰 정보가 일치하지 않습니다."),
INVALID_EMAIL_TOKEN( INACTIVE_ID("사용할 수 없는 계정입니다."),
"You can only reset your password within 24 hours from when the email was sent.\n" INVALID_EMAIL_TOKEN(
+ "To reset your password again, please submit a new request through \"Forgot" "You can only reset your password within 24 hours from when the email was sent.\n"
+ " Password.\""), + "To reset your password again, please submit a new request through \"Forgot"
; + " Password.\""),
// @formatter:on ;
private final String message; // @formatter:on
private final String message;
@Override
public String getId() { @Override
return name(); public String getId() {
} return name();
}
@Override
public String getText() { @Override
return message; public String getText() {
} return message;
}
public static ApiResponseCode getCode(String name) {
return ApiResponseCode.valueOf(name.toUpperCase()); public static ApiResponseCode getCode(String name) {
} if (name == null || name.isBlank()) return null;
try {
public static String getMessage(String name) { return ApiResponseCode.valueOf(name.toUpperCase());
return ApiResponseCode.valueOf(name.toUpperCase()).getText(); } 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; package com.kamco.cd.training.config.swagger;
import io.swagger.v3.oas.annotations.enums.SecuritySchemeType; import io.swagger.v3.oas.annotations.enums.SecuritySchemeType;
import io.swagger.v3.oas.annotations.security.SecurityScheme; import io.swagger.v3.oas.annotations.security.SecurityScheme;
import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Configuration;
@Configuration @Configuration
@SecurityScheme( @SecurityScheme(
name = "BearerAuth", name = "BearerAuth",
type = SecuritySchemeType.HTTP, type = SecuritySchemeType.HTTP,
scheme = "bearer", scheme = "bearer",
bearerFormat = "JWT") bearerFormat = "JWT")
public class SwaggerConfig {} 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,251 @@
package com.kamco.cd.training.dataset; package com.kamco.cd.training.dataset;
import com.kamco.cd.training.config.api.ApiResponseDto; import com.kamco.cd.training.config.api.ApiResponseDto;
import com.kamco.cd.training.dataset.dto.DatasetDto; import com.kamco.cd.training.dataset.dto.DatasetDto;
import com.kamco.cd.training.dataset.service.DatasetService; import com.kamco.cd.training.dataset.dto.DatasetObjDto;
import io.swagger.v3.oas.annotations.Operation; import com.kamco.cd.training.dataset.dto.DatasetObjDto.DatasetClass;
import io.swagger.v3.oas.annotations.Parameter; import com.kamco.cd.training.dataset.dto.DatasetObjDto.DatasetStorage;
import io.swagger.v3.oas.annotations.media.Content; import com.kamco.cd.training.dataset.service.DatasetService;
import io.swagger.v3.oas.annotations.media.Schema; import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.responses.ApiResponse; import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.responses.ApiResponses; import io.swagger.v3.oas.annotations.media.Content;
import io.swagger.v3.oas.annotations.tags.Tag; import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.Valid; import io.swagger.v3.oas.annotations.responses.ApiResponse;
import java.util.UUID; import io.swagger.v3.oas.annotations.responses.ApiResponses;
import lombok.RequiredArgsConstructor; import io.swagger.v3.oas.annotations.tags.Tag;
import org.springframework.data.domain.Page; import jakarta.validation.Valid;
import org.springframework.web.bind.annotation.*; import java.io.IOException;
import java.nio.file.FileStore;
@Tag(name = "데이터셋 관리", description = "어드민 홈 > 학습데이터관리 > 전체데이터 API") import java.nio.file.Files;
@RestController import java.nio.file.Path;
@RequestMapping("/api/datasets") import java.util.List;
@RequiredArgsConstructor import java.util.UUID;
public class DatasetApiController { import lombok.RequiredArgsConstructor;
import org.springframework.core.io.Resource;
private final DatasetService datasetService; import org.springframework.data.domain.Page;
import org.springframework.http.ResponseEntity;
@Operation(summary = "데이터셋 목록 조회", description = "데이터셋(회차) 목록을 조회합니다.") import org.springframework.web.bind.annotation.*;
@ApiResponses(
value = { @Tag(name = "학습데이터 관리", description = "어드민 홈 > 학습데이터관리 > 전체데이터 API")
@ApiResponse( @RestController
responseCode = "200", @RequestMapping("/api/datasets")
description = "조회 성공", @RequiredArgsConstructor
content = public class DatasetApiController {
@Content(
mediaType = "application/json", private final DatasetService datasetService;
schema = @Schema(implementation = Page.class))),
@ApiResponse(responseCode = "400", description = "잘못된 검색 조건", content = @Content), @Operation(summary = "학습데이터 관리 목록 조회", description = "학습데이터 목록을 조회합니다.")
@ApiResponse(responseCode = "500", description = "서버 오류", content = @Content) @ApiResponses(
}) value = {
@GetMapping @ApiResponse(
public ApiResponseDto<Page<DatasetDto.Basic>> searchDatasets( responseCode = "200",
@Parameter(description = "구분", example = "DELIVER(납품), PRODUCTION(제작)") description = "조회 성공",
@RequestParam(required = false) content =
String groupTitle, @Content(
@Parameter(description = "제목", example = "") @RequestParam(required = false) String title, mediaType = "application/json",
@Parameter(description = "페이지 번호 (0부터 시작)", example = "0") @RequestParam(defaultValue = "0") schema = @Schema(implementation = Page.class))),
int page, @ApiResponse(responseCode = "400", description = "잘못된 검색 조건", content = @Content),
@Parameter(description = "페이지 크기", example = "20") @RequestParam(defaultValue = "20") @ApiResponse(responseCode = "500", description = "서버 오류", content = @Content)
int size) { })
DatasetDto.SearchReq searchReq = new DatasetDto.SearchReq(); @GetMapping
searchReq.setTitle(title); public ApiResponseDto<Page<DatasetDto.Basic>> searchDatasets(
searchReq.setGroupTitle(groupTitle); @Parameter(
searchReq.setPage(page); description = "구분",
searchReq.setSize(size); example = "",
return ApiResponseDto.ok(datasetService.searchDatasets(searchReq)); schema = @Schema(allowableValues = {"DELIVER", "PRODUCTION"}))
} @RequestParam(required = false)
String dataType,
@Operation(summary = "데이터셋 상세 조회", description = "데이터셋 상세 정보를 조회합니다.") @Parameter(description = "제목", example = "") @RequestParam(required = false) String title,
@ApiResponses( @Parameter(description = "페이지 번호 (0부터 시작)", example = "0") @RequestParam(defaultValue = "0")
value = { int page,
@ApiResponse( @Parameter(description = "페이지 크기", example = "20") @RequestParam(defaultValue = "20")
responseCode = "200", int size) {
description = "조회 성공", DatasetDto.SearchReq searchReq = new DatasetDto.SearchReq();
content = searchReq.setTitle(title);
@Content( searchReq.setDataType(dataType);
mediaType = "application/json", searchReq.setPage(page);
schema = @Schema(implementation = DatasetDto.Basic.class))), searchReq.setSize(size);
@ApiResponse(responseCode = "404", description = "데이터셋을 찾을 수 없음", content = @Content), return ApiResponseDto.ok(datasetService.searchDatasets(searchReq));
@ApiResponse(responseCode = "500", description = "서버 오류", content = @Content) }
})
@GetMapping("/{uuid}") @Operation(summary = "학습데이터관리 상세 조회", description = "학습데이터관리 상세 정보를 조회합니다.")
public ApiResponseDto<DatasetDto.Basic> getDatasetDetail(@PathVariable UUID uuid) { @ApiResponses(
return ApiResponseDto.ok(datasetService.getDatasetDetail(uuid)); value = {
} @ApiResponse(
responseCode = "200",
@Operation(summary = "데이터셋 등록", description = "신규 데이터셋(회차)을 생성합니다.") description = "조회 성공",
@ApiResponses( content =
value = { @Content(
@ApiResponse( mediaType = "application/json",
responseCode = "201", schema = @Schema(implementation = DatasetDto.Basic.class))),
description = "등록 성공", @ApiResponse(responseCode = "404", description = "데이터셋을 찾을 수 없음", content = @Content),
content = @ApiResponse(responseCode = "500", description = "서버 오류", content = @Content)
@Content( })
mediaType = "application/json", @GetMapping("/{uuid}")
schema = @Schema(implementation = Long.class))), public ApiResponseDto<DatasetDto.Basic> getDatasetDetail(@PathVariable UUID uuid) {
@ApiResponse(responseCode = "400", description = "잘못된 요청 데이터", content = @Content), return ApiResponseDto.ok(datasetService.getDatasetDetail(uuid));
@ApiResponse(responseCode = "500", description = "서버 오류", content = @Content) }
})
@PostMapping("/register") @Operation(summary = "학습데이터 수정", description = "학습데이터 제목, 메모 수정")
public ApiResponseDto<Long> registerDataset( @ApiResponses(
@RequestBody @Valid DatasetDto.RegisterReq registerReq) { value = {
Long id = datasetService.registerDataset(registerReq); @ApiResponse(
return ApiResponseDto.createOK(id); responseCode = "200",
} description = "수정 성공",
content =
@Operation(summary = "데이터셋 수정", description = "데이터셋 정보를 수정합니다.") @Content(
@ApiResponses( mediaType = "application/json",
value = { schema = @Schema(implementation = Long.class))),
@ApiResponse( @ApiResponse(responseCode = "400", description = "잘못된 요청 데이터", content = @Content),
responseCode = "200", @ApiResponse(responseCode = "404", description = "데이터셋을 찾을 수 없음", content = @Content),
description = "수정 성공", @ApiResponse(responseCode = "500", description = "서버 오류", content = @Content)
content = })
@Content( @PutMapping("/{uuid}")
mediaType = "application/json", public ApiResponseDto<UUID> updateDataset(
schema = @Schema(implementation = Long.class))), @PathVariable UUID uuid, @RequestBody DatasetDto.UpdateReq updateReq) {
@ApiResponse(responseCode = "400", description = "잘못된 요청 데이터", content = @Content), datasetService.updateDataset(uuid, updateReq);
@ApiResponse(responseCode = "404", description = "데이터셋을 찾을 수 없음", content = @Content), return ApiResponseDto.ok(uuid);
@ApiResponse(responseCode = "500", description = "서버 오류", content = @Content) }
})
@PutMapping("/{uuid}") @Operation(summary = "학습데이터 삭제", description = "학습데이터를 삭제합니다.(납품 데이터는 삭제 불가)")
public ApiResponseDto<UUID> updateDataset( @ApiResponses(
@PathVariable UUID uuid, @RequestBody DatasetDto.UpdateReq updateReq) { value = {
datasetService.updateDataset(uuid, updateReq); @ApiResponse(responseCode = "201", description = "삭제 성공", content = @Content),
return ApiResponseDto.ok(uuid); @ApiResponse(responseCode = "400", description = "잘못된 요청", content = @Content),
} @ApiResponse(responseCode = "404", description = "데이터셋을 찾을 수 없음", content = @Content),
@ApiResponse(responseCode = "500", description = "서버 오류", content = @Content)
@Operation(summary = "데이터셋 삭제", description = "데이터셋을 삭제합니다.") })
@ApiResponses( @DeleteMapping("/{uuid}")
value = { public ApiResponseDto<UUID> deleteDatasets(@PathVariable UUID uuid) {
@ApiResponse(responseCode = "201", description = "삭제 성공", content = @Content),
@ApiResponse(responseCode = "400", description = "잘못된 요청", content = @Content), datasetService.deleteDatasets(uuid);
@ApiResponse(responseCode = "404", description = "데이터셋을 찾을 수 없음", content = @Content), return ApiResponseDto.ok(uuid);
@ApiResponse(responseCode = "500", description = "서버 오류", content = @Content) }
})
@DeleteMapping("/{uuid}") @Operation(summary = "학습데이터 관리 목록 조회", description = "학습데이터 목록을 조회합니다.")
public ApiResponseDto<UUID> deleteDatasets(@PathVariable UUID uuid) { @ApiResponses(
value = {
datasetService.deleteDatasets(uuid); @ApiResponse(
return ApiResponseDto.ok(uuid); responseCode = "200",
} description = "조회 성공",
content =
/* @Content(
@Operation(summary = "데이터셋 통계 요약", description = "선택 데이터셋의 통계를 요약합니다.") mediaType = "application/json",
@ApiResponses( schema = @Schema(implementation = Page.class))),
value = { @ApiResponse(responseCode = "400", description = "잘못된 검색 조건", content = @Content),
@ApiResponse( @ApiResponse(responseCode = "500", description = "서버 오류", content = @Content)
responseCode = "200", })
description = "조회 성공", @GetMapping("/obj-list")
content = public ApiResponseDto<Page<DatasetObjDto.Basic>> searchDatasetObjectList(
@Content( @Parameter(description = "회차 uuid", example = "e9a6774b-4f81-4402-b080-51d27fac1f01")
mediaType = "application/json", @RequestParam(required = true)
schema = @Schema(implementation = DatasetDto.Summary.class))), UUID uuid,
@ApiResponse(responseCode = "400", description = "잘못된 요청", content = @Content), @Parameter(description = "비교년도분류", example = "container") @RequestParam(required = false)
@ApiResponse(responseCode = "500", description = "서버 오류", content = @Content) String compareClassCd,
}) @Parameter(description = "기준년도분류", example = "waste") @RequestParam(required = false)
@PostMapping("/summary") String targetClassCd,
public ApiResponseDto<DatasetDto.Summary> getDatasetSummary( @Parameter(description = "도엽번호", example = "36713060") @RequestParam(required = false)
@RequestBody @Valid DatasetDto.SummaryReq summaryReq) { String mapSheetNum,
return ApiResponseDto.ok(datasetService.getDatasetSummary(summaryReq)); @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() throws IOException {
FileStore store = Files.getFileStore(Path.of("."));
long usable = store.getUsableSpace();
DatasetStorage storage = new DatasetStorage();
storage.setUsableBytes(String.valueOf(usable));
// datasetService.getUsableBytes();
return ApiResponseDto.ok(storage);
}
@Operation(summary = "학습데이터 zip파일 등록", description = "학습데이터 zip파일 등록 합니다.")
@PostMapping
public ApiResponseDto<ApiResponseDto.ResponseObj> insertDataset(
@RequestBody @Valid DatasetDto.AddReq addReq) {
return ApiResponseDto.okObject(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,535 @@
package com.kamco.cd.training.dataset.dto; package com.kamco.cd.training.dataset.dto;
import com.kamco.cd.training.common.enums.LearnDataRegister; import com.fasterxml.jackson.annotation.JsonIgnore;
import com.kamco.cd.training.common.enums.LearnDataType; import com.kamco.cd.training.common.enums.LearnDataRegister;
import com.kamco.cd.training.common.utils.enums.Enums; import com.kamco.cd.training.common.enums.LearnDataType;
import com.kamco.cd.training.common.utils.interfaces.JsonFormatDttm; import com.kamco.cd.training.common.enums.ModelType;
import io.swagger.v3.oas.annotations.media.Schema; import com.kamco.cd.training.common.utils.enums.Enums;
import jakarta.validation.constraints.NotBlank; import com.kamco.cd.training.common.utils.interfaces.JsonFormatDttm;
import jakarta.validation.constraints.NotNull; import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.Size; import jakarta.validation.constraints.NotBlank;
import java.time.ZonedDateTime; import jakarta.validation.constraints.NotNull;
import java.util.List; import jakarta.validation.constraints.Size;
import java.util.UUID; import java.time.ZonedDateTime;
import lombok.AllArgsConstructor; import java.util.List;
import lombok.Getter; import java.util.UUID;
import lombok.NoArgsConstructor; import lombok.AllArgsConstructor;
import lombok.Setter; import lombok.Builder;
import org.springframework.data.domain.PageRequest; import lombok.Getter;
import org.springframework.data.domain.Pageable; import lombok.NoArgsConstructor;
import org.springframework.data.domain.Sort; import lombok.Setter;
import lombok.extern.slf4j.Slf4j;
public class DatasetDto { import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Pageable;
@Schema(name = "Dataset Basic", description = "데이터셋 기본 정보") import org.springframework.data.domain.Sort;
@Getter
@Setter @Slf4j
@NoArgsConstructor public class DatasetDto {
@AllArgsConstructor
public static class Basic { @Schema(name = "Dataset Basic", description = "데이터셋 기본 정보")
@Getter
private Long id; @Setter
private UUID uuid; @NoArgsConstructor
private String groupTitle; @AllArgsConstructor
private String groupTitleCd; public static class Basic {
private String title;
private Long roundNo; private Long id;
private String totalSize; private UUID uuid;
private String memo; private String title;
@JsonFormatDttm private ZonedDateTime createdDttm; private Long roundNo;
private String status; private Integer compareYyyy;
private String statusCd; private Integer targetYyyy;
private Boolean deleted; private String totalSize;
private String memo;
public Basic( @JsonFormatDttm private ZonedDateTime createdDttm;
Long id, private String status;
UUID uuid, private String statusCd;
String groupTitle, private Boolean deleted;
String title, private String dataType;
Long roundNo,
Long totalSize, public Basic(
String memo, Long id,
ZonedDateTime createdDttm, UUID uuid,
String status, String title,
Boolean deleted) { Long roundNo,
this.id = id; Integer compareYyyy,
this.uuid = uuid; Integer targetYyyy,
this.groupTitle = getGroupTitle(groupTitle); Long totalSize,
this.groupTitleCd = groupTitle; String memo,
this.title = title; ZonedDateTime createdDttm,
this.roundNo = roundNo; String status,
this.totalSize = getTotalSize(totalSize); Boolean deleted,
this.memo = memo; String dataType) {
this.createdDttm = createdDttm; this.id = id;
this.status = getStatus(status); this.uuid = uuid;
this.statusCd = status; this.title = title;
this.deleted = deleted; this.roundNo = roundNo;
} this.compareYyyy = compareYyyy;
this.targetYyyy = targetYyyy;
public String getTotalSize(Long totalSize) { this.totalSize = getTotalSize(totalSize);
if (totalSize == null) return "0G"; this.memo = memo;
double giga = totalSize / (1024.0 * 1024 * 1024); this.createdDttm = createdDttm;
return String.format("%.2fG", giga); this.status = getStatus(status);
} this.statusCd = status;
this.deleted = deleted;
public String getGroupTitle(String groupTitleCd) { this.dataType = dataType;
LearnDataType type = Enums.fromId(LearnDataType.class, groupTitleCd); }
return type == null ? null : type.getText();
} public String getTotalSize(Long totalSize) {
if (totalSize == null || totalSize <= 0) return "0M";
public String getStatus(String status) {
LearnDataRegister type = Enums.fromId(LearnDataRegister.class, status); double giga = totalSize / (1024.0 * 1024 * 1024);
return type == null ? null : type.getText();
} if (giga >= 1) {
} return String.format("%.2fG", giga);
} else {
@Schema(name = "Dataset Detail", description = "데이터셋 상세 정보") double mega = totalSize / (1024.0 * 1024);
@Getter return String.format("%.2fM", mega);
@Setter }
@NoArgsConstructor }
@AllArgsConstructor
public static class Detail { public String getStatus(String status) {
LearnDataRegister type = Enums.fromId(LearnDataRegister.class, status);
private Long id; return type == null ? null : type.getText();
private String groupTitle; }
private String title;
private Long roundNo; public String getYear() {
private String totalSize; return this.compareYyyy + "-" + this.targetYyyy;
private String memo; }
@JsonFormatDttm private ZonedDateTime createdDttm;
private String status; public String getDataTypeName() {
private Boolean deleted; LearnDataType type = Enums.fromId(LearnDataType.class, this.dataType);
} return type == null ? null : type.getText();
}
@Schema(name = "DatasetSearchReq", description = "데이터셋 목록 조회 요청") }
@Getter
@Setter @Schema(name = "Dataset Detail", description = "데이터셋 상세 정보")
@NoArgsConstructor @Getter
@AllArgsConstructor @Setter
public static class SearchReq { @NoArgsConstructor
@AllArgsConstructor
@Schema(description = "구분", example = "DELIVER(납품), PRODUCTION(제작)") public static class Detail {
private String groupTitle;
private Long id;
@Schema(description = "제목 (부분 검색)", example = "1차") private String groupTitle;
private String title; private String title;
private Long roundNo;
@Schema(description = "페이지 번호 (1부터 시작)", example = "1") private String totalSize;
private int page = 1; private String memo;
@JsonFormatDttm private ZonedDateTime createdDttm;
@Schema(description = "페이지 크기", example = "20") private String status;
private int size = 20; private Boolean deleted;
}
public Pageable toPageable() {
// API에서는 1부터 시작하지만 내부적으로는 0부터 시작 @Schema(name = "DatasetSearchReq", description = "데이터셋 목록 조회 요청")
int pageIndex = Math.max(0, page - 1); @Getter
return PageRequest.of(pageIndex, size, Sort.by(Sort.Direction.DESC, "createdDttm")); @Setter
} @NoArgsConstructor
} @AllArgsConstructor
public static class SearchReq {
@Schema(name = "DatasetDetailReq", description = "데이터셋 상세 조회 요청")
@Getter @Schema(description = "구분")
@Setter private String dataType;
@NoArgsConstructor
@AllArgsConstructor @Schema(description = "제목 (부분 검색)", example = "1차")
public static class DetailReq { private String title;
@NotNull(message = "데이터셋 ID는 필수입니다") @Schema(description = "페이지 번호 (1부터 시작)", example = "1")
@Schema(description = "데이터셋 ID", example = "101") private int page = 1;
private Long datasetId;
} @Schema(description = "페이지 크기", example = "20")
private int size = 20;
@Schema(name = "DatasetRegisterReq", description = "데이터셋 등록 요청")
@Getter public Pageable toPageable() {
@Setter return PageRequest.of(page, size, Sort.by(Sort.Direction.DESC, "createdDttm"));
@NoArgsConstructor }
@AllArgsConstructor }
public static class RegisterReq {
@Schema(name = "DatasetDetailReq", description = "데이터셋 상세 조회 요청")
@NotBlank(message = "제목은 필수입니다") @Getter
@Size(max = 200, message = "제목은 최대 200자까지 입력 가능합니다") @Setter
@Schema(description = "제목", example = "1차 제작") @NoArgsConstructor
private String title; @AllArgsConstructor
public static class DetailReq {
@NotBlank(message = "연도는 필수입니다")
@Size(max = 4, message = "연도는 4자리입니다") @NotNull(message = "데이터셋 ID는 필수입니다")
@Schema(description = "연도 (YYYY)", example = "2024") @Schema(description = "데이터셋 ID", example = "101")
private String year; private Long datasetId;
}
@Schema(description = "회차", example = "1")
private Long roundNo; @Schema(name = "DatasetRegisterReq", description = "데이터셋 등록 요청")
@Getter
@Schema(description = "메모", example = "데이터셋 설명") @Setter
private String memo; @NoArgsConstructor
} @AllArgsConstructor
public static class RegisterReq {
@Schema(name = "DatasetUpdateReq", description = "데이터셋 수정 요청")
@Getter @NotBlank(message = "제목은 필수입니다")
@Setter @Size(max = 200, message = "제목은 최대 200자까지 입력 가능합니다")
@NoArgsConstructor @Schema(description = "제목", example = "1차 제작")
@AllArgsConstructor private String title;
public static class UpdateReq {
@Schema(description = "비교연도 (YYYY)", example = "2023")
@Size(max = 200, message = "제목은 최대 200자까지 입력 가능합니다") private Integer compareYear;
@Schema(description = "제목", example = "1차 제작")
private String title; @Schema(description = "기준연도 (YYYY)", example = "2024")
private Integer targetYyyy;
@Schema(description = "메모", example = "데이터셋 설명")
private String memo; @Schema(description = "회차", example = "1")
} private Long roundNo;
@Schema(name = "DatasetSummaryReq", description = "데이터셋 통계 요약 요청") @Schema(description = "메모", example = "데이터셋 설명")
@Getter private String memo;
@Setter }
@NoArgsConstructor
@AllArgsConstructor @Schema(name = "DatasetUpdateReq", description = "데이터셋 수정 요청")
public static class SummaryReq { @Getter
@Setter
@NotNull(message = "데이터셋 ID 목록은 필수입니다") @NoArgsConstructor
@Schema(description = "데이터셋 ID 목록", example = "[101, 105]") @AllArgsConstructor
private List<Long> datasetIds; public static class UpdateReq {
}
@Size(max = 200, message = "제목은 최대 200자까지 입력 가능합니다")
@Schema(name = "DatasetSummary", description = "데이터셋 통계 요약") @Schema(description = "제목", example = "1차 제작")
@Getter private String title;
@Setter
@NoArgsConstructor @Schema(description = "메모", example = "데이터셋 설명")
@AllArgsConstructor private String memo;
public static class Summary { }
@Schema(description = "총 데이터셋 수", example = "2") @Schema(name = "DatasetSummaryReq", description = "데이터셋 통계 요약 요청")
private int totalDatasets; @Getter
@Setter
@Schema(description = "총 도엽 수", example = "1500") @NoArgsConstructor
private long totalMapSheets; @AllArgsConstructor
public static class SummaryReq {
@Schema(description = "총 파일 크기 (bytes)", example = "10737418240")
private long totalFileSize; @NotNull(message = "데이터셋 ID 목록은 필수입니다")
@Schema(description = "데이터셋 ID 목록", example = "[101, 105]")
@Schema(description = "평균 도엽 수", example = "750") private List<Long> datasetIds;
private double averageMapSheets; }
}
} @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
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;
}
}
@Schema(name = "SelectTransferDataSet", description = "전이학습 데이터셋 선택 리스트")
@Getter
@Setter
@NoArgsConstructor
public static class SelectTransferDataSet {
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;
private String beforeModelNo; // G1, G2, G3 모델 타입
private Long beforeDatasetId;
private UUID beforeUuid;
private String beforeDataType;
private String beforeTitle;
private Long beforeRoundNo;
private Integer beforeCompareYyyy;
private Integer beforeTargetYyyy;
private String beforeMemo;
@JsonIgnore private Long beforeClassCount;
private Integer beforeBuildingCnt;
private Integer beforeContainerCnt;
private String beforeDataTypeName;
private Long beforeWasteCnt;
private Long beforeLandCoverCnt;
public SelectTransferDataSet(
// 현재
String modelNo,
Long datasetId,
UUID uuid,
String dataType,
String title,
Long roundNo,
Integer compareYyyy,
Integer targetYyyy,
String memo,
Long classCount,
// 이전(before)
String beforeModelNo,
Long beforeDatasetId,
UUID beforeUuid,
String beforeDataType,
String beforeTitle,
Long beforeRoundNo,
Integer beforeCompareYyyy,
Integer beforeTargetYyyy,
String beforeMemo,
Long beforeClassCount) {
// 현재
this.modelNo = modelNo;
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 != null && modelNo.equals(ModelType.G2.getId())) {
this.wasteCnt = classCount;
} else if (modelNo != null && modelNo.equals(ModelType.G3.getId())) {
this.landCoverCnt = classCount;
}
// 이전(before)
this.beforeModelNo = beforeModelNo;
this.beforeDatasetId = beforeDatasetId;
this.beforeUuid = beforeUuid;
this.beforeDataType = beforeDataType;
this.beforeDataTypeName = getDataTypeName(beforeDataType);
this.beforeTitle = beforeTitle;
this.beforeRoundNo = beforeRoundNo;
this.beforeCompareYyyy = beforeCompareYyyy;
this.beforeTargetYyyy = beforeTargetYyyy;
this.beforeMemo = beforeMemo;
this.beforeClassCount = beforeClassCount;
if (beforeModelNo != null && beforeModelNo.equals(ModelType.G2.getId())) {
this.beforeWasteCnt = beforeClassCount;
} else if (beforeModelNo != null && beforeModelNo.equals(ModelType.G3.getId())) {
this.beforeLandCoverCnt = beforeClassCount;
}
}
public SelectTransferDataSet(
// 현재
String modelNo,
Long datasetId,
UUID uuid,
String dataType,
String title,
Long roundNo,
Integer compareYyyy,
Integer targetYyyy,
String memo,
Integer buildingCnt,
Integer containerCnt,
// 이전(before)
String beforeModelNo,
Long beforeDatasetId,
UUID beforeUuid,
String beforeDataType,
String beforeTitle,
Long beforeRoundNo,
Integer beforeCompareYyyy,
Integer beforeTargetYyyy,
String beforeMemo,
Integer beforeBuildingCnt,
Integer beforeContainerCnt) {
// 현재
this.modelNo = modelNo;
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;
// 이전(before)
this.beforeModelNo = beforeModelNo;
this.beforeDatasetId = beforeDatasetId;
this.beforeUuid = beforeUuid;
this.beforeDataType = beforeDataType;
this.beforeDataTypeName = getDataTypeName(beforeDataType);
this.beforeTitle = beforeTitle;
this.beforeRoundNo = beforeRoundNo;
this.beforeCompareYyyy = beforeCompareYyyy;
this.beforeTargetYyyy = beforeTargetYyyy;
this.beforeMemo = beforeMemo;
this.beforeBuildingCnt = beforeBuildingCnt;
this.beforeContainerCnt = beforeContainerCnt;
}
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;
}
public String getBeforeYear() {
if (this.beforeCompareYyyy == null || this.beforeTargetYyyy == null) {
return null;
}
return this.beforeCompareYyyy + "-" + this.beforeTargetYyyy;
}
}
@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; package com.kamco.cd.training.dataset.dto;
import com.kamco.cd.training.common.utils.interfaces.JsonFormatDttm; import com.kamco.cd.training.common.utils.interfaces.JsonFormatDttm;
import io.swagger.v3.oas.annotations.media.Schema; import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotNull; import jakarta.validation.constraints.NotNull;
import java.time.ZonedDateTime; import java.time.ZonedDateTime;
import java.util.List; import java.util.List;
import lombok.AllArgsConstructor; import lombok.AllArgsConstructor;
import lombok.Getter; import lombok.Getter;
import lombok.NoArgsConstructor; import lombok.NoArgsConstructor;
import lombok.Setter; import lombok.Setter;
import org.springframework.data.domain.PageRequest; import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Pageable; import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Sort; import org.springframework.data.domain.Sort;
public class MapSheetDto { public class MapSheetDto {
@Schema(name = "MapSheet Basic", description = "도엽 기본 정보") @Schema(name = "MapSheet Basic", description = "도엽 기본 정보")
@Getter @Getter
@Setter @Setter
@NoArgsConstructor @NoArgsConstructor
@AllArgsConstructor @AllArgsConstructor
public static class Basic { public static class Basic {
private Long id; private Long id;
private Long datasetId; private Long datasetId;
private String sheetNum; private String sheetNum;
private String fileName; private String fileName;
private Long fileSize; private Long fileSize;
private String filePath; private String filePath;
private String status; private String status;
private String memo; private String memo;
private Boolean deleted; private Boolean deleted;
@JsonFormatDttm private ZonedDateTime createdDttm; @JsonFormatDttm private ZonedDateTime createdDttm;
@JsonFormatDttm private ZonedDateTime updatedDttm; @JsonFormatDttm private ZonedDateTime updatedDttm;
} }
@Schema(name = "MapSheetSearchReq", description = "도엽 목록 조회 요청") @Schema(name = "MapSheetSearchReq", description = "도엽 목록 조회 요청")
@Getter @Getter
@Setter @Setter
@NoArgsConstructor @NoArgsConstructor
@AllArgsConstructor @AllArgsConstructor
public static class SearchReq { public static class SearchReq {
@NotNull(message = "데이터셋 ID는 필수입니다") @NotNull(message = "데이터셋 ID는 필수입니다")
@Schema(description = "데이터셋 ID", example = "101") @Schema(description = "데이터셋 ID", example = "101")
private Long datasetId; private Long datasetId;
@Schema(description = "페이지 번호 (1부터 시작)", example = "1") @Schema(description = "페이지 번호 (1부터 시작)", example = "1")
private int page = 1; private int page = 1;
@Schema(description = "페이지 크기", example = "20") @Schema(description = "페이지 크기", example = "20")
private int size = 20; private int size = 20;
public Pageable toPageable() { public Pageable toPageable() {
int pageIndex = Math.max(0, page - 1); int pageIndex = Math.max(0, page - 1);
return PageRequest.of(pageIndex, size, Sort.by(Sort.Direction.DESC, "createdDttm")); return PageRequest.of(pageIndex, size, Sort.by(Sort.Direction.DESC, "createdDttm"));
} }
} }
@Schema(name = "MapSheetDeleteReq", description = "도엽 삭제 요청 (다건)") @Schema(name = "MapSheetDeleteReq", description = "도엽 삭제 요청 (다건)")
@Getter @Getter
@Setter @Setter
@NoArgsConstructor @NoArgsConstructor
@AllArgsConstructor @AllArgsConstructor
public static class DeleteReq { public static class DeleteReq {
@NotNull(message = "삭제할 도엽 ID 목록은 필수입니다") @NotNull(message = "삭제할 도엽 ID 목록은 필수입니다")
@Schema(description = "삭제할 도엽 ID 목록", example = "[9991, 9992]") @Schema(description = "삭제할 도엽 ID 목록", example = "[9991, 9992]")
private List<Long> itemIds; private List<Long> itemIds;
} }
@Schema(name = "MapSheetCheckReq", description = "도엽 번호 유효성 검증 요청") @Schema(name = "MapSheetCheckReq", description = "도엽 번호 유효성 검증 요청")
@Getter @Getter
@Setter @Setter
@NoArgsConstructor @NoArgsConstructor
@AllArgsConstructor @AllArgsConstructor
public static class CheckReq { public static class CheckReq {
@NotNull(message = "도엽 번호는 필수입니다") @NotNull(message = "도엽 번호는 필수입니다")
@Schema(description = "도엽 번호", example = "377055") @Schema(description = "도엽 번호", example = "377055")
private String sheetNum; private String sheetNum;
} }
@Schema(name = "MapSheetCheckRes", description = "도엽 번호 유효성 검증 응답") @Schema(name = "MapSheetCheckRes", description = "도엽 번호 유효성 검증 응답")
@Getter @Getter
@Setter @Setter
@NoArgsConstructor @NoArgsConstructor
@AllArgsConstructor @AllArgsConstructor
public static class CheckRes { public static class CheckRes {
@Schema(description = "유효 여부", example = "true") @Schema(description = "유효 여부", example = "true")
private boolean valid; private boolean valid;
@Schema(description = "메시지", example = "유효한 도엽 번호입니다") @Schema(description = "메시지", example = "유효한 도엽 번호입니다")
private String message; private String message;
@Schema(description = "중복 여부", example = "false") @Schema(description = "중복 여부", example = "false")
private boolean duplicate; private boolean duplicate;
} }
} }

View File

@@ -1,86 +1,527 @@
package com.kamco.cd.training.dataset.service; package com.kamco.cd.training.dataset.service;
import com.kamco.cd.training.dataset.dto.DatasetDto; import com.fasterxml.jackson.databind.JsonNode;
import com.kamco.cd.training.postgres.core.DatasetCoreService; import com.fasterxml.jackson.databind.ObjectMapper;
import java.util.UUID; import com.fasterxml.jackson.databind.node.ArrayNode;
import lombok.RequiredArgsConstructor; import com.fasterxml.jackson.databind.node.ObjectNode;
import lombok.extern.slf4j.Slf4j; import com.kamco.cd.training.common.enums.LearnDataType;
import org.springframework.data.domain.Page; import com.kamco.cd.training.common.exception.CustomApiException;
import org.springframework.stereotype.Service; import com.kamco.cd.training.common.service.FormatStorage;
import org.springframework.transaction.annotation.Transactional; import com.kamco.cd.training.common.utils.FIleChecker;
import com.kamco.cd.training.config.api.ApiResponseDto.ApiResponseCode;
@Slf4j import com.kamco.cd.training.config.api.ApiResponseDto.ResponseObj;
@Service import com.kamco.cd.training.dataset.dto.DatasetDto;
@RequiredArgsConstructor import com.kamco.cd.training.dataset.dto.DatasetDto.AddReq;
@Transactional(readOnly = true) import com.kamco.cd.training.dataset.dto.DatasetDto.DatasetMngRegDto;
public class DatasetService { import com.kamco.cd.training.dataset.dto.DatasetObjDto;
import com.kamco.cd.training.dataset.dto.DatasetObjDto.DatasetClass;
private final DatasetCoreService datasetCoreService; 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;
* @param searchReq 검색 조건 import java.io.IOException;
* @return 데이터셋 목록 import java.io.InputStream;
*/ import java.nio.file.Files;
public Page<DatasetDto.Basic> searchDatasets(DatasetDto.SearchReq searchReq) { import java.nio.file.Path;
log.info("데이터셋 목록 조회 - 조건: {}", searchReq); import java.nio.file.Paths;
return datasetCoreService.findDatasetList(searchReq); import java.util.ArrayList;
} import java.util.Arrays;
import java.util.HashMap;
/** import java.util.HashSet;
* 데이터셋 상세 조회 import java.util.List;
* import java.util.Map;
* @param id 상세 조회할 목록 Id import java.util.Set;
* @return 데이터셋 상세 정보 import java.util.UUID;
*/ import java.util.stream.Collectors;
public DatasetDto.Basic getDatasetDetail(UUID id) { import java.util.stream.Stream;
return datasetCoreService.getOneByUuid(id); 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;
* @param registerReq 등록 요청 import org.springframework.http.HttpHeaders;
* @return 등록된 데이터셋 ID import org.springframework.http.HttpStatus;
*/ import org.springframework.http.MediaType;
@Transactional import org.springframework.http.ResponseEntity;
public Long registerDataset(DatasetDto.RegisterReq registerReq) { import org.springframework.stereotype.Service;
log.info("데이터셋 등록 - 요청: {}", registerReq); import org.springframework.transaction.annotation.Transactional;
DatasetDto.Basic saved = datasetCoreService.save(registerReq);
log.info("데이터셋 등록 완료 - ID: {}", saved.getId()); @Slf4j
return saved.getId(); @Service
} @RequiredArgsConstructor
@Transactional
/** public class DatasetService {
* 데이터셋 수정
* private final DatasetCoreService datasetCoreService;
* @param updateReq 수정 요청
* @return 수정된 데이터셋 ID @Value("${file.dataset-dir}")
*/ private String datasetDir;
@Transactional
public void updateDataset(UUID uuid, DatasetDto.UpdateReq updateReq) { private static final List<String> LABEL_DIRS = List.of("label-json", "label", "input1", "input2");
datasetCoreService.update(uuid, updateReq); private static final List<String> REQUIRED_DIRS = Arrays.asList("train", "val", "test");
} private static final List<String> CHECK_DIRS = List.of("label", "input1", "input2");
/** /**
* 데이터셋 삭제 (다건) * 데이터셋 목록 조회
* *
* @param uuid 삭제 요청 * @param searchReq 검색 조건
*/ * @return 데이터셋 목록
@Transactional */
public void deleteDatasets(UUID uuid) { public Page<DatasetDto.Basic> searchDatasets(DatasetDto.SearchReq searchReq) {
datasetCoreService.deleteDatasets(uuid); log.info("데이터셋 목록 조회 - 조건: {}", searchReq);
} return datasetCoreService.findDatasetList(searchReq);
}
/**
* 데이터셋 통계 요약 /**
* * 데이터셋 상세 조회
* @param summaryReq 요약 요청 *
* @return 통계 요약 * @param id 상세 조회할 목록 Id
*/ * @return 데이터셋 상세 정보
public DatasetDto.Summary getDatasetSummary(DatasetDto.SummaryReq summaryReq) { */
log.info("데이터셋 통계 요약 - 요청: {}", summaryReq); public DatasetDto.Basic getDatasetDetail(UUID id) {
return datasetCoreService.getDatasetSummary(summaryReq); 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);
}
}
@Transactional
public ResponseObj insertDataset(@Valid AddReq addReq) {
Long datasetUid = null; // master id 값, 등록하면서 가져올 예정
try {
// 같은 uid 로 등록한 파일이 있는지 확인
Long existsCnt =
datasetCoreService.findDatasetByUidExistsCnt(addReq.getFileName().replace(".zip", ""));
if (existsCnt > 0) {
return new ResponseObj(ApiResponseCode.DUPLICATE_DATA, "이미 등록된 회차 데이터 파일입니다. 확인 부탁드립니다.");
}
// 압축 해제
FIleChecker.unzip(addReq.getFileName(), addReq.getFilePath());
// 압축 해제한 폴더 하위에 train,val,test 폴더 모두 존재하는지 확인
validateTrainValTestDirs(addReq.getFilePath() + addReq.getFileName().replace(".zip", ""));
// 압축 해제한 폴더의 갯수 맞는지 log 찍기
validateDirFileCount(addReq.getFilePath() + addReq.getFileName().replace(".zip", ""));
// 해제한 폴더 읽어서 데이터 저장
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 CustomApiException(
ApiResponseCode.NOT_FOUND_DATA.getId(),
HttpStatus.CONFLICT,
"폴더가 존재하지 않습니다. 업로드 된 파일을 확인하세요. : " + 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);
}
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);
}
}
/** unzipRootDir: 압축 해제된 폴더 경로 (ex: /data/xxx/myzipname) */
public static void validateTrainValTestDirs(String unzipRootDir) {
Path root = Paths.get(unzipRootDir);
// 루트 폴더 자체 존재 확인
if (!Files.exists(root) || !Files.isDirectory(root)) {
throw new CustomApiException(
ApiResponseCode.NOT_FOUND_DATA.getId(),
HttpStatus.CONFLICT,
"압축 해제 폴더가 존재하지 않습니다: " + unzipRootDir);
}
// 필요한 폴더들 존재/디렉토리 여부 확인
List<String> missing =
REQUIRED_DIRS.stream()
.filter(d -> !Files.isDirectory(root.resolve(d)))
.collect(Collectors.toList());
if (!missing.isEmpty()) {
throw new CustomApiException(
ApiResponseCode.NOT_FOUND_DATA.getId(),
HttpStatus.CONFLICT,
"데이터 폴더 구조가 올바르지 않습니다. 누락된 폴더: "
+ String.join(", ", missing)
+ " (필수: train, val, test)");
}
}
public static void validateDirFileCount(String unzipRootDir) {
Path root = Paths.get(unzipRootDir);
for (String split : REQUIRED_DIRS) {
Path splitPath = root.resolve(split);
Map<String, Long> fileCountMap = new HashMap<>();
for (String subDir : CHECK_DIRS) { // input1, input2, label 폴더만 수행하기
Path subDirPath = splitPath.resolve(subDir);
if (!Files.isDirectory(subDirPath)) {
throw new CustomApiException(
ApiResponseCode.NOT_FOUND_DATA.getId(),
HttpStatus.CONFLICT,
split + " 폴더 하위에 " + subDir + " 폴더가 존재하지 않습니다.");
}
long count;
try (Stream<Path> files = Files.list(subDirPath)) {
count = files.filter(Files::isRegularFile).count();
log.info("dir: " + subDirPath + ", count: " + count);
} catch (IOException e) {
throw new CustomApiException(
ApiResponseCode.NOT_FOUND_DATA.getId(),
HttpStatus.CONFLICT,
split + "/" + subDir + " 파일 개수 확인 중 오류 발생");
}
fileCountMap.put(subDir, count);
}
// 모든 폴더 파일 개수가 동일한지 확인
Set<Long> uniqueCounts = new HashSet<>(fileCountMap.values());
if (uniqueCounts.size() != 1) {
throw new CustomApiException(
ApiResponseCode.NOT_FOUND_DATA.getId(),
HttpStatus.CONFLICT,
split + " 데이터 파일 개수가 일치하지 않습니다. " + fileCountMap.toString());
}
}
}
}

View File

@@ -1,41 +1,41 @@
package com.kamco.cd.training.dataset.service; package com.kamco.cd.training.dataset.service;
import com.kamco.cd.training.dataset.dto.MapSheetDto; import com.kamco.cd.training.dataset.dto.MapSheetDto;
import com.kamco.cd.training.postgres.core.MapSheetCoreService; import com.kamco.cd.training.postgres.core.MapSheetCoreService;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.springframework.data.domain.Page; import org.springframework.data.domain.Page;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional; import org.springframework.transaction.annotation.Transactional;
@Slf4j @Slf4j
@Service @Service
@RequiredArgsConstructor @RequiredArgsConstructor
@Transactional(readOnly = true) @Transactional(readOnly = true)
public class MapSheetService { public class MapSheetService {
private final MapSheetCoreService mapSheetCoreService; private final MapSheetCoreService mapSheetCoreService;
/** /**
* 도엽 목록 조회 * 도엽 목록 조회
* *
* @param searchReq 검색 조건 * @param searchReq 검색 조건
* @return 도엽 목록 * @return 도엽 목록
*/ */
public Page<MapSheetDto.Basic> searchMapSheets(MapSheetDto.SearchReq searchReq) { public Page<MapSheetDto.Basic> searchMapSheets(MapSheetDto.SearchReq searchReq) {
log.info("도엽 목록 조회 - 조건: {}", searchReq); log.info("도엽 목록 조회 - 조건: {}", searchReq);
return mapSheetCoreService.findMapSheetList(searchReq); return mapSheetCoreService.findMapSheetList(searchReq);
} }
/** /**
* 도엽 삭제 (다건) * 도엽 삭제 (다건)
* *
* @param deleteReq 삭제 요청 * @param deleteReq 삭제 요청
*/ */
@Transactional @Transactional
public void deleteMapSheets(MapSheetDto.DeleteReq deleteReq) { public void deleteMapSheets(MapSheetDto.DeleteReq deleteReq) {
log.info("도엽 삭제 - 요청: {}", deleteReq); log.info("도엽 삭제 - 요청: {}", deleteReq);
mapSheetCoreService.deleteMapSheets(deleteReq); mapSheetCoreService.deleteMapSheets(deleteReq);
log.info("도엽 삭제 완료 - 개수: {}", deleteReq.getItemIds().size()); 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("/list")
public ApiResponseDto<Page<List>> getHyperParam(
@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-03-31") @RequestParam(required = false)
LocalDate endDate,
@Parameter(description = "버전명", example = "G1_000019") @RequestParam(required = false)
String hyperVer,
@Parameter(description = "모델 타입 (G1, G2, G3 중 하나)", example = "G1")
@RequestParam(required = false)
ModelType model,
@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 = "57fc9170-64c1-4128-aa7b-0657f08d6d10")
@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 = "57fc9170-64c1-4128-aa7b-0657f08d6d10")
@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,169 @@
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;
@JsonFormatDttm private ZonedDateTime lastUsedDttm;
private Integer totalUseCnt;
// -------------------------
// 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 ModelType model;
private String hyperVer;
@JsonFormatDttm private ZonedDateTime createDttm;
@JsonFormatDttm private ZonedDateTime lastUsedDttm;
private String memo;
private Integer totalUseCnt;
}
@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

@@ -0,0 +1,99 @@
package com.kamco.cd.training.log;
import com.kamco.cd.training.config.api.ApiResponseDto;
import com.kamco.cd.training.log.dto.AuditLogDto;
import com.kamco.cd.training.log.service.AuditLogService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import java.time.LocalDate;
import lombok.RequiredArgsConstructor;
import org.springframework.data.domain.Page;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
@Tag(name = "감사 로그", description = "감사 로그 관리 API")
@RequiredArgsConstructor
@RestController
@RequestMapping("/api/logs/audit")
public class AuditLogApiController {
private final AuditLogService auditLogService;
@Operation(summary = "일자별 로그 조회")
@GetMapping("/daily")
public ApiResponseDto<Page<AuditLogDto.DailyAuditList>> getDailyLogs(
@RequestParam(required = false) LocalDate startDate,
@RequestParam(required = false) LocalDate endDate,
@RequestParam int page,
@RequestParam(defaultValue = "20") int size) {
AuditLogDto.searchReq searchReq = new AuditLogDto.searchReq(page, size, "created_dttm,desc");
Page<AuditLogDto.DailyAuditList> result =
auditLogService.getLogByDaily(searchReq, startDate, endDate);
return ApiResponseDto.ok(result);
}
@Operation(summary = "일자별 로그 상세")
@GetMapping("/daily/result")
public ApiResponseDto<Page<AuditLogDto.DailyDetail>> getDailyResultLogs(
@RequestParam LocalDate logDate,
@RequestParam int page,
@RequestParam(defaultValue = "20") int size) {
AuditLogDto.searchReq searchReq = new AuditLogDto.searchReq(page, size, "created_dttm,desc");
Page<AuditLogDto.DailyDetail> result = auditLogService.getLogByDailyResult(searchReq, logDate);
return ApiResponseDto.ok(result);
}
@Operation(summary = "메뉴별 로그 조회")
@GetMapping("/menu")
public ApiResponseDto<Page<AuditLogDto.MenuAuditList>> getMenuLogs(
@RequestParam(required = false) String searchValue,
@RequestParam int page,
@RequestParam(defaultValue = "20") int size) {
AuditLogDto.searchReq searchReq = new AuditLogDto.searchReq(page, size, "created_dttm,desc");
Page<AuditLogDto.MenuAuditList> result = auditLogService.getLogByMenu(searchReq, searchValue);
return ApiResponseDto.ok(result);
}
@Operation(summary = "메뉴별 로그 상세")
@GetMapping("/menu/result")
public ApiResponseDto<Page<AuditLogDto.MenuDetail>> getMenuResultLogs(
@RequestParam String menuId,
@RequestParam int page,
@RequestParam(defaultValue = "20") int size) {
AuditLogDto.searchReq searchReq = new AuditLogDto.searchReq(page, size, "created_dttm,desc");
Page<AuditLogDto.MenuDetail> result = auditLogService.getLogByMenuResult(searchReq, menuId);
return ApiResponseDto.ok(result);
}
@Operation(summary = "사용자별 로그 조회")
@GetMapping("/account")
public ApiResponseDto<Page<AuditLogDto.UserAuditList>> getAccountLogs(
@RequestParam(required = false) String searchValue,
@RequestParam int page,
@RequestParam(defaultValue = "20") int size) {
AuditLogDto.searchReq searchReq = new AuditLogDto.searchReq(page, size, "created_dttm,desc");
Page<AuditLogDto.UserAuditList> result =
auditLogService.getLogByAccount(searchReq, searchValue);
return ApiResponseDto.ok(result);
}
@Operation(summary = "사용자별 로그 상세")
@GetMapping("/account/result")
public ApiResponseDto<Page<AuditLogDto.UserDetail>> getAccountResultLogs(
@RequestParam Long userUid,
@RequestParam int page,
@RequestParam(defaultValue = "20") int size) {
AuditLogDto.searchReq searchReq = new AuditLogDto.searchReq(page, size, "created_dttm,desc");
Page<AuditLogDto.UserDetail> result = auditLogService.getLogByAccountResult(searchReq, userUid);
return ApiResponseDto.ok(result);
}
}

View File

@@ -0,0 +1,40 @@
package com.kamco.cd.training.log;
import com.kamco.cd.training.config.api.ApiResponseDto;
import com.kamco.cd.training.log.dto.ErrorLogDto;
import com.kamco.cd.training.log.dto.EventType;
import com.kamco.cd.training.log.service.ErrorLogService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import java.time.LocalDate;
import lombok.RequiredArgsConstructor;
import org.springframework.data.domain.Page;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
@Tag(name = "에러 로그", description = "에러 로그 관리 API")
@RequiredArgsConstructor
@RestController
@RequestMapping({"/api/logs/system"})
public class ErrorLogApiController {
private final ErrorLogService errorLogService;
@Operation(summary = "에러로그 조회")
@GetMapping("/error")
public ApiResponseDto<Page<ErrorLogDto.Basic>> getErrorLogs(
@RequestParam(required = false) ErrorLogDto.LogErrorLevel logErrorLevel,
@RequestParam(required = false) EventType eventType,
@RequestParam(required = false) LocalDate startDate,
@RequestParam(required = false) LocalDate endDate,
@RequestParam int page,
@RequestParam(defaultValue = "20") int size) {
ErrorLogDto.ErrorSearchReq searchReq =
new ErrorLogDto.ErrorSearchReq(
logErrorLevel, eventType, startDate, endDate, page, size, "created_dttm,desc");
Page<ErrorLogDto.Basic> result = errorLogService.findLogByError(searchReq);
return ApiResponseDto.ok(result);
}
}

View File

@@ -1,229 +1,265 @@
package com.kamco.cd.training.log.dto; package com.kamco.cd.training.log.dto;
import com.fasterxml.jackson.annotation.JsonIgnore; import com.fasterxml.jackson.annotation.JsonIgnore;
import com.kamco.cd.training.common.utils.interfaces.JsonFormatDttm; import com.kamco.cd.training.common.utils.interfaces.JsonFormatDttm;
import io.swagger.v3.oas.annotations.media.Schema; import io.swagger.v3.oas.annotations.media.Schema;
import java.time.ZonedDateTime; import java.time.LocalDate;
import lombok.AllArgsConstructor; import java.time.ZonedDateTime;
import lombok.Getter; import java.util.UUID;
import lombok.NoArgsConstructor; import lombok.AllArgsConstructor;
import lombok.Setter; import lombok.Getter;
import org.springframework.data.domain.PageRequest; import lombok.NoArgsConstructor;
import org.springframework.data.domain.Pageable; import lombok.Setter;
import org.springframework.data.domain.Sort; import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Pageable;
public class AuditLogDto { import org.springframework.data.domain.Sort;
@Schema(name = "AuditLogBasic", description = "감사로그 기본 정보") public class AuditLogDto {
@Getter
public static class Basic { @Schema(name = "AuditLogBasic", description = "감사로그 기본 정보")
@Getter
@JsonIgnore private final Long id; public static class Basic {
private final Long userUid;
private final EventType eventType; @JsonIgnore private final Long id;
private final EventStatus eventStatus; private final Long userUid;
private final String menuUid; private final EventType eventType;
private final String ipAddress; private final EventStatus eventStatus;
private final String requestUri; private final String menuUid;
private final String requestBody; private final String ipAddress;
private final Long errorLogUid; private final String requestUri;
private final String requestBody;
@JsonFormatDttm private final ZonedDateTime createdDttm; private final Long errorLogUid;
public Basic( @JsonFormatDttm private final ZonedDateTime createdDttm;
Long id,
Long userUid, public Basic(
EventType eventType, Long id,
EventStatus eventStatus, Long userUid,
String menuUid, EventType eventType,
String ipAddress, EventStatus eventStatus,
String requestUri, String menuUid,
String requestBody, String ipAddress,
Long errorLogUid, String requestUri,
ZonedDateTime createdDttm) { String requestBody,
this.id = id; Long errorLogUid,
this.userUid = userUid; ZonedDateTime createdDttm) {
this.eventType = eventType; this.id = id;
this.eventStatus = eventStatus; this.userUid = userUid;
this.menuUid = menuUid; this.eventType = eventType;
this.ipAddress = ipAddress; this.eventStatus = eventStatus;
this.requestUri = requestUri; this.menuUid = menuUid;
this.requestBody = requestBody; this.ipAddress = ipAddress;
this.errorLogUid = errorLogUid; this.requestUri = requestUri;
this.createdDttm = createdDttm; this.requestBody = requestBody;
} this.errorLogUid = errorLogUid;
} this.createdDttm = createdDttm;
}
@Schema(name = "AuditCommon", description = "목록 공통") }
@Getter
@AllArgsConstructor @Schema(name = "AuditCommon", description = "목록 공통")
public static class AuditCommon { @Getter
private int readCount; @AllArgsConstructor
private int cudCount; public static class AuditCommon {
private int printCount;
private int downloadCount; private int readCount;
private Long totalCount; private int cudCount;
} private int printCount;
private int downloadCount;
@Schema(name = "DailyAuditList", description = "일자별 목록") private Long totalCount;
@Getter }
public static class DailyAuditList extends AuditCommon {
private final String baseDate; @Schema(name = "DailyAuditList", description = "일자별 목록")
@Getter
public DailyAuditList( public static class DailyAuditList extends AuditCommon {
int readCount,
int cudCount, private final String baseDate;
int printCount,
int downloadCount, public DailyAuditList(
Long totalCount, int readCount,
String baseDate) { int cudCount,
super(readCount, cudCount, printCount, downloadCount, totalCount); int printCount,
this.baseDate = baseDate; int downloadCount,
} Long totalCount,
} String baseDate) {
super(readCount, cudCount, printCount, downloadCount, totalCount);
@Schema(name = "MenuAuditList", description = "메뉴별 목록") this.baseDate = baseDate;
@Getter }
public static class MenuAuditList extends AuditCommon { }
private final String menuId;
private final String menuName; @Schema(name = "MenuAuditList", description = "메뉴별 목록")
@Getter
public MenuAuditList( public static class MenuAuditList extends AuditCommon {
String menuId,
String menuName, private final String menuId;
int readCount, private final String menuName;
int cudCount,
int printCount, public MenuAuditList(
int downloadCount, String menuId,
Long totalCount) { String menuName,
super(readCount, cudCount, printCount, downloadCount, totalCount); int readCount,
this.menuId = menuId; int cudCount,
this.menuName = menuName; int printCount,
} int downloadCount,
} Long totalCount) {
super(readCount, cudCount, printCount, downloadCount, totalCount);
@Schema(name = "UserAuditList", description = "사용자별 목록") this.menuId = menuId;
@Getter this.menuName = menuName;
public static class UserAuditList extends AuditCommon { }
private final Long accountId; }
private final String loginId;
private final String username; @Schema(name = "UserAuditList", description = "사용자별 목록")
@Getter
public UserAuditList( public static class UserAuditList extends AuditCommon {
Long accountId,
String loginId, private final Long accountId;
String username, private final String loginId;
int readCount, private final String username;
int cudCount,
int printCount, public UserAuditList(
int downloadCount, Long accountId,
Long totalCount) { String loginId,
super(readCount, cudCount, printCount, downloadCount, totalCount); String username,
this.accountId = accountId; int readCount,
this.loginId = loginId; int cudCount,
this.username = username; int printCount,
} int downloadCount,
} Long totalCount) {
super(readCount, cudCount, printCount, downloadCount, totalCount);
@Schema(name = "AuditDetail", description = "감사 로그 상세 공통") this.accountId = accountId;
@Getter this.loginId = loginId;
@AllArgsConstructor this.username = username;
public static class AuditDetail { }
private Long logId; }
private EventType eventType;
private LogDetail detail; @Schema(name = "AuditDetail", description = "감사 로그 상세 공통")
} @Getter
@AllArgsConstructor
@Schema(name = "DailyDetail", description = "일자별 로그 상세") public static class AuditDetail {
@Getter
public static class DailyDetail extends AuditDetail { private Long logId;
private final String userName; private EventType eventType;
private final String loginId; private LogDetail detail;
private final String menuName; }
public DailyDetail( @Schema(name = "DailyDetail", description = "일자별 로그 상세")
Long logId, @Getter
String userName, public static class DailyDetail extends AuditDetail {
String loginId,
String menuName, private final String userName;
EventType eventType, private final String loginId;
LogDetail detail) { private final String menuName;
super(logId, eventType, detail); private final String logDateTime;
this.userName = userName;
this.loginId = loginId; public DailyDetail(
this.menuName = menuName; Long logId,
} String userName,
} String loginId,
String menuName,
@Schema(name = "MenuDetail", description = "메뉴별 로그 상세") EventType eventType,
@Getter String logDateTime,
public static class MenuDetail extends AuditDetail { LogDetail detail) {
private final String logDateTime; super(logId, eventType, detail);
private final String userName; this.userName = userName;
private final String loginId; this.loginId = loginId;
this.menuName = menuName;
public MenuDetail( this.logDateTime = logDateTime;
Long logId, }
String logDateTime, }
String userName,
String loginId, @Schema(name = "MenuDetail", description = "메뉴별 로그 상세")
EventType eventType, @Getter
LogDetail detail) { public static class MenuDetail extends AuditDetail {
super(logId, eventType, detail);
this.logDateTime = logDateTime; private final String logDateTime;
this.userName = userName; private final String userName;
this.loginId = loginId; private final String loginId;
}
} public MenuDetail(
Long logId,
@Schema(name = "UserDetail", description = "사용자별 로그 상세") String logDateTime,
@Getter String userName,
public static class UserDetail extends AuditDetail { String loginId,
private final String logDateTime; EventType eventType,
private final String menuNm; LogDetail detail) {
super(logId, eventType, detail);
public UserDetail( this.logDateTime = logDateTime;
Long logId, String logDateTime, String menuNm, EventType eventType, LogDetail detail) { this.userName = userName;
super(logId, eventType, detail); this.loginId = loginId;
this.logDateTime = logDateTime; }
this.menuNm = menuNm; }
}
} @Schema(name = "UserDetail", description = "사용자별 로그 상세")
@Getter
@Getter public static class UserDetail extends AuditDetail {
@Setter
@AllArgsConstructor private final String logDateTime;
public static class LogDetail { private final String menuNm;
String serviceName;
String parentMenuName; public UserDetail(
String menuName; Long logId, String logDateTime, String menuNm, EventType eventType, LogDetail detail) {
String menuUrl; super(logId, eventType, detail);
String menuDescription; this.logDateTime = logDateTime;
Long sortOrder; this.menuNm = menuNm;
boolean used; }
} }
@Schema(name = "searchReq", description = "일자별 로그 검색 요청") @Getter
@Getter @Setter
@Setter @AllArgsConstructor
@NoArgsConstructor public static class LogDetail {
@AllArgsConstructor
public static class searchReq { String serviceName;
String parentMenuName;
// 페이징 파라미터 String menuName;
private int page = 0; String menuUrl;
private int size = 20; String menuDescription;
private String sort; Long sortOrder;
boolean used;
public Pageable toPageable() { }
if (sort != null && !sort.isEmpty()) {
String[] sortParams = sort.split(","); @Schema(name = "searchReq", description = "일자별 로그 검색 요청")
String property = sortParams[0]; @Getter
Sort.Direction direction = @Setter
sortParams.length > 1 ? Sort.Direction.fromString(sortParams[1]) : Sort.Direction.ASC; @NoArgsConstructor
return PageRequest.of(page, size, Sort.by(direction, property)); @AllArgsConstructor
} public static class searchReq {
return PageRequest.of(page, size);
} // 페이징 파라미터
} 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
@Setter
public static class DownloadReq {
UUID uuid;
LocalDate startDate;
LocalDate endDate;
String searchValue;
String menuId;
String requestUri;
}
@Getter
@Setter
@AllArgsConstructor
public static class DownloadRes {
String name;
String employeeNo;
@JsonFormatDttm ZonedDateTime downloadDttm;
}
}

View File

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

View File

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

View File

@@ -1,29 +1,42 @@
package com.kamco.cd.training.log.dto; package com.kamco.cd.training.log.dto;
import com.kamco.cd.training.common.utils.enums.EnumType; import com.kamco.cd.training.common.utils.enums.CodeExpose;
import lombok.AllArgsConstructor; import com.kamco.cd.training.common.utils.enums.EnumType;
import lombok.Getter; import lombok.AllArgsConstructor;
import lombok.Getter;
@Getter
@AllArgsConstructor @CodeExpose
public enum EventType implements EnumType { @Getter
CREATE("생성"), @AllArgsConstructor
READ("조회"), public enum EventType implements EnumType {
UPDATE("수정"), LIST("목록"),
DELETE("삭제"), DETAIL("상세"),
DOWNLOAD("다운로드"), POPUP("팝업"),
PRINT("출력"), STATUS("상태"),
OTHER("기타"); ADDED("추가"),
MODIFIED("수정"),
private final String desc; REMOVE("삭제"),
DOWNLOAD("다운로드"),
@Override LOGIN("로그인"),
public String getId() { OTHER("기타");
return name();
} private final String desc;
@Override public static EventType fromName(String name) {
public String getText() { try {
return desc; return EventType.valueOf(name.toUpperCase());
} } catch (Exception e) {
} return OTHER;
}
}
@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; package com.kamco.cd.training.log.service;
import com.kamco.cd.training.log.dto.AuditLogDto; import com.kamco.cd.training.log.dto.AuditLogDto;
import com.kamco.cd.training.postgres.core.AuditLogCoreService; import com.kamco.cd.training.postgres.core.AuditLogCoreService;
import java.time.LocalDate; import java.time.LocalDate;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import org.springframework.data.domain.Page; import org.springframework.data.domain.Page;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional; import org.springframework.transaction.annotation.Transactional;
@Service @Service
@RequiredArgsConstructor @RequiredArgsConstructor
@Transactional(readOnly = true) @Transactional(readOnly = true)
public class AuditLogService { public class AuditLogService {
private final AuditLogCoreService auditLogCoreService; private final AuditLogCoreService auditLogCoreService;
public Page<AuditLogDto.DailyAuditList> getLogByDaily( public Page<AuditLogDto.DailyAuditList> getLogByDaily(
AuditLogDto.searchReq searchRange, LocalDate startDate, LocalDate endDate) { AuditLogDto.searchReq searchRange, LocalDate startDate, LocalDate endDate) {
return auditLogCoreService.getLogByDaily(searchRange, startDate, endDate); return auditLogCoreService.getLogByDaily(searchRange, startDate, endDate);
} }
public Page<AuditLogDto.MenuAuditList> getLogByMenu( public Page<AuditLogDto.MenuAuditList> getLogByMenu(
AuditLogDto.searchReq searchRange, String searchValue) { AuditLogDto.searchReq searchRange, String searchValue) {
return auditLogCoreService.getLogByMenu(searchRange, searchValue); return auditLogCoreService.getLogByMenu(searchRange, searchValue);
} }
public Page<AuditLogDto.UserAuditList> getLogByAccount( public Page<AuditLogDto.UserAuditList> getLogByAccount(
AuditLogDto.searchReq searchRange, String searchValue) { AuditLogDto.searchReq searchRange, String searchValue) {
return auditLogCoreService.getLogByAccount(searchRange, searchValue); return auditLogCoreService.getLogByAccount(searchRange, searchValue);
} }
public Page<AuditLogDto.DailyDetail> getLogByDailyResult( public Page<AuditLogDto.DailyDetail> getLogByDailyResult(
AuditLogDto.searchReq searchRange, LocalDate logDate) { AuditLogDto.searchReq searchRange, LocalDate logDate) {
return auditLogCoreService.getLogByDailyResult(searchRange, logDate); return auditLogCoreService.getLogByDailyResult(searchRange, logDate);
} }
public Page<AuditLogDto.MenuDetail> getLogByMenuResult( public Page<AuditLogDto.MenuDetail> getLogByMenuResult(
AuditLogDto.searchReq searchRange, String menuId) { AuditLogDto.searchReq searchRange, String menuId) {
return auditLogCoreService.getLogByMenuResult(searchRange, menuId); return auditLogCoreService.getLogByMenuResult(searchRange, menuId);
} }
public Page<AuditLogDto.UserDetail> getLogByAccountResult( public Page<AuditLogDto.UserDetail> getLogByAccountResult(
AuditLogDto.searchReq searchRange, Long accountId) { AuditLogDto.searchReq searchRange, Long accountId) {
return auditLogCoreService.getLogByAccountResult(searchRange, accountId); return auditLogCoreService.getLogByAccountResult(searchRange, accountId);
} }
} }

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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