422 Commits

Author SHA1 Message Date
3d2a4049d3 시스템 사용율 모니터링 기능 로그 수정 2026-03-19 17:38:30 +09:00
0cbaf53e86 시스템 사용율 모니터링 기능 추가 2026-03-19 17:08:37 +09:00
80fd2bda3e 시스템 사용율 모니터링 기능 테스트 2026-03-19 16:52:26 +09:00
fb647e5991 시스템 사용율 모니터링 기능 테스트 2026-03-19 16:36:39 +09:00
87575a62f7 시스템 사용율 모니터링 기능 테스트 2026-03-19 16:27:05 +09:00
246c11f8b0 시스템 사용율 모니터링 기능 테스트 2026-03-19 16:12:27 +09:00
2c1f9bdf5c 시스템 사용율 모니터링 기능 테스트 2026-03-19 15:46:23 +09:00
5799f7dfb2 시스템 사용율 모니터링 기능 테스트 2026-03-19 15:41:18 +09:00
9f428e9572 시스템 사용율 모니터링 기능 테스트 2026-03-19 15:37:26 +09:00
904968a1be 시스템 사용율 모니터링 기능 테스트 2026-03-19 15:25:20 +09:00
4b44be6a29 시스템 사용율 모니터링 기능 테스트 2026-03-19 15:25:11 +09:00
f9f0662f8e 시스템 사용율 모니터링 기능 테스트 2026-03-19 15:24:03 +09:00
7416327cc3 ai 학습실행 run command 수정 2026-03-11 10:18:33 +09:00
da31bd9d99 ai 학습실행 run command 수정 2026-03-10 23:09:39 +09:00
f3e5347335 ai 학습실행 run command 수정 2026-03-10 22:48:10 +09:00
7d5581f60c 데이터셋 삭제 플래그 추가 2026-03-10 19:09:00 +09:00
b4428217ea hello 2026-03-10 18:01:03 +09:00
8a63fdacdd confict 2026-03-10 17:27:14 +09:00
cb2e42143a confict 2026-03-10 17:23:27 +09:00
997e85c0cc 운영환경처리 2026-03-10 17:22:27 +09:00
731ca59475 심볼릭 링크로 수정 2026-03-10 17:16:54 +09:00
fe6d37456d 하드링크 실패 시 심볼릭 링크로 만들어보기 2026-03-10 16:56:35 +09:00
6c98a48a5d 에러 수정 2026-03-10 16:34:02 +09:00
81b69caa99 운영환경처리 2026-03-10 16:00:23 +09:00
7fce070686 spotless 2026-03-10 15:25:32 +09:00
8d83505ee7 운영환경처리 2026-03-10 15:00:31 +09:00
0ff38b24d4 Merge branch 'feat/training_260202' into develop
# Conflicts:
#	src/main/java/com/kamco/cd/training/train/service/JobRecoveryOnStartupService.java
2026-03-10 14:22:14 +09:00
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
5c082f7c9d 운영환경처리 2026-03-10 08:39:42 +09:00
43d0e55cb7 Merge branch 'develop' of https://kamco.git.gs.dabeeo.com/MVPTeam/kamco-train-api into develop 2026-03-04 06:12:27 +09:00
df3bedfbda prod 2026-03-04 05:35:21 +09:00
c26a48d07d prod 2026-03-04 05:25:04 +09:00
1c7c213977 Merge pull request '학습 실패 처리 수정' (#155) from feat/training_260303 into develop
Reviewed-on: #155
2026-03-04 01:43:56 +09:00
6583a45abd 학습 실패 처리 수정 2026-03-04 01:43:34 +09:00
b15f77d894 Merge pull request '학습 실패 처리 수정' (#154) from feat/training_260303 into develop
Reviewed-on: #154
2026-03-04 01:34:40 +09:00
3bcd99f0db 학습 실패 처리 수정 2026-03-04 01:34:16 +09:00
5513cd60a0 Merge pull request '리커버리 수정' (#153) from feat/training_260303 into develop
Reviewed-on: #153
2026-03-04 01:18:19 +09:00
7b35d26a13 리커버리 수정 2026-03-04 01:18:04 +09:00
d92ff88ef7 Merge pull request '리커버리 수정' (#152) from feat/training_260303 into develop
Reviewed-on: #152
2026-03-04 01:15:04 +09:00
bfcddd0327 리커버리 수정 2026-03-04 01:14:35 +09:00
e193330f99 Merge pull request '리커버리 수정' (#151) from feat/training_260303 into develop
Reviewed-on: #151
2026-03-04 01:00:45 +09:00
6c0184597d 리커버리 수정 2026-03-04 01:00:26 +09:00
1e8a8d8dad Merge pull request '리커버리 수정 테스트 로그 추가,' (#150) from feat/training_260303 into develop
Reviewed-on: #150
2026-03-04 00:44:03 +09:00
eb7680b952 리커버리 수정 테스트 로그 추가, 2026-03-04 00:43:20 +09:00
2c357ebf27 Merge pull request '리커버리 수정 테스트,,' (#149) from feat/training_260303 into develop
Reviewed-on: #149
2026-03-04 00:30:36 +09:00
0daaa1c8cb 리커버리 수정 테스트,, 2026-03-04 00:30:17 +09:00
96383595df Merge pull request '리커버리 수정' (#148) from feat/training_260303 into develop
Reviewed-on: #148
2026-03-04 00:22:09 +09:00
c5e03f7ca8 리커버리 수정 2026-03-04 00:21:51 +09:00
df2acc4dfb Merge pull request '리커버리 수정' (#147) from feat/training_260303 into develop
Reviewed-on: #147
2026-03-04 00:06:21 +09:00
693de354d2 리커버리 수정 2026-03-04 00:06:04 +09:00
62cdc5015e Merge pull request '학습 리커버리 테스트' (#146) from feat/training_260303 into develop
Reviewed-on: #146
2026-03-03 23:45:46 +09:00
5c11263d55 랃그 2026-03-03 23:45:10 +09:00
dd5031ae3a Merge pull request 'log.info 추가' (#145) from feat/training_260303 into develop
Reviewed-on: #145
2026-03-03 23:40:44 +09:00
c369e01ada log.info 추가 2026-03-03 23:40:18 +09:00
54991622f1 log.info 추가 2026-03-03 23:31:25 +09:00
e9f8bb37fa spotless 적용 2026-03-03 23:06:45 +09:00
17d69486ec spotless 적용 2026-03-03 23:04:55 +09:00
f3c822587f spotless 적용 2026-03-03 23:02:53 +09:00
948f1061da Merge pull request 'spotless 적용' (#144) from feat/training_260202 into develop
Reviewed-on: #144
2026-03-03 23:01:39 +09:00
335f0dbb9b spotless 적용 2026-03-03 23:01:22 +09:00
42438b3cd5 Merge pull request '하드링크 수정' (#143) from feat/training_260202 into develop
Reviewed-on: #143
2026-03-03 22:51:50 +09:00
69eaba1a83 하드링크 수정 2026-03-03 22:51:10 +09:00
524ae200b0 prod 2026-03-03 08:44:31 +09:00
4f763d3c2e prod 2026-03-03 08:44:01 +09:00
9ebf525387 prod 2026-03-03 08:42:16 +09:00
4ab672a96e 중복 경고 2026-02-28 01:56:48 +09:00
7d2a367e3f Merge pull request '리커버리 삭제' (#142) from feat/training_260202 into develop
Reviewed-on: #142
2026-02-28 01:24:49 +09:00
365ad81cad 리커버리 삭제 2026-02-28 01:24:34 +09:00
67a67749c3 Merge pull request '리커버리 추가' (#141) from feat/training_260202 into develop
Reviewed-on: #141
2026-02-28 01:02:10 +09:00
9dfa54fbf9 리커버리 추가 2026-02-28 01:01:38 +09:00
251307b5c9 Merge pull request '하드링크 수정' (#140) from feat/training_260202 into develop
Reviewed-on: #140
2026-02-27 23:31:24 +09:00
12f6bb7154 하드링크 수정 2026-02-27 23:31:04 +09:00
8423a03d31 Merge pull request '하드링크 로그 추가' (#139) from feat/training_260202 into develop
Reviewed-on: #139
2026-02-27 23:12:17 +09:00
aa3af4e9d0 하드링크 로그 추가 2026-02-27 23:12:00 +09:00
d6cdf6b690 Merge pull request '하드링크 로그 추가' (#138) from feat/training_260202 into develop
Reviewed-on: #138
2026-02-27 22:51:51 +09:00
7ca37bf1e4 하드링크 로그 추가 2026-02-27 22:51:27 +09:00
9cfa299e58 Merge pull request 'feat/training_260202' (#137) from feat/training_260202 into develop
Reviewed-on: #137
2026-02-24 16:55:35 +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
a137e71420 Merge pull request 'feat/training_260202' (#136) from feat/training_260202 into develop
Reviewed-on: #136
2026-02-24 15:11:12 +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
f08f80622f Merge pull request 'feat/training_260202' (#135) from feat/training_260202 into develop
Reviewed-on: #135
2026-02-23 14:31:11 +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
e565fd7a34 Merge pull request '전이학습 상세 수정' (#134) from feat/training_260202 into develop
Reviewed-on: #134
2026-02-20 18:34:58 +09:00
c2978e41c2 전이학습 상세 수정 2026-02-20 18:34:32 +09:00
8c45b39dcc Merge pull request 'feat/training_260202' (#133) from feat/training_260202 into develop
Reviewed-on: #133
2026-02-20 18:22:45 +09:00
07429dbe8e 전이학습 상세 수정 2026-02-20 18:22:19 +09:00
83859bb9fe 전이학습 상세 - before dataset 추가 2026-02-20 16:05:29 +09:00
b119f333ac Merge pull request 'best epoch 파일 선택 수정' (#132) from feat/training_260202 into develop
Reviewed-on: #132
2026-02-20 15:41:58 +09:00
564a99448c best epoch 파일 선택 수정 2026-02-20 15:41:34 +09:00
bbe04ee458 Merge pull request 'best epoch 파일 선택 수정' (#131) from feat/training_260202 into develop
Reviewed-on: #131
2026-02-20 15:31:50 +09:00
38ae6e5575 best epoch 파일 선택 수정 2026-02-20 15:31:33 +09:00
fab3c83a69 Merge pull request 'best epoch 파일 선택 수정' (#130) from feat/training_260202 into develop
Reviewed-on: #130
2026-02-20 15:15:39 +09:00
40fe98ae0c best epoch 파일 선택 수정 2026-02-20 15:15:12 +09:00
fb87a0f32f Merge pull request '중복 수정 제거' (#129) from feat/training_260202 into develop
Reviewed-on: #129
2026-02-20 14:31:15 +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
63794ec4ec Merge pull request 'feat/training_260202' (#128) from feat/training_260202 into develop
Reviewed-on: #128
2026-02-20 14:25:13 +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
bf6dc9740f Merge pull request 'tmp 하드링크 수정' (#127) from feat/training_260202 into develop
Reviewed-on: #127
2026-02-20 13:37:04 +09:00
832e1b5681 tmp 하드링크 수정 2026-02-20 13:36:48 +09:00
a23bc8dd67 Merge pull request 'tmp 하드링크 수정' (#126) from feat/training_260202 into develop
Reviewed-on: #126
2026-02-20 12:30:23 +09:00
4f16355cda tmp 하드링크 수정 2026-02-20 12:29:57 +09:00
13023a06cc Merge pull request 'feat/training_260202' (#125) from feat/training_260202 into develop
Reviewed-on: #125
2026-02-20 12:23:37 +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
28b50bd949 Merge pull request 'test json 수정' (#124) from feat/training_260202 into develop
Reviewed-on: #124
2026-02-20 12:20:36 +09:00
62c9d73b94 test json 수정 2026-02-20 12:20:15 +09:00
78ab928459 Merge pull request 'ing-cnt 로직에 step2도 추가, transactional' (#123) from feat/training_260202 into develop
Reviewed-on: #123
2026-02-20 12:05:40 +09:00
68c0e634c5 ing-cnt 로직에 step2도 추가, transactional 2026-02-20 12:05:20 +09:00
ae3601cff5 Merge pull request '비밀번호 변경 security 로직 수정' (#122) from feat/training_260202 into develop
Reviewed-on: #122
2026-02-20 11:37:08 +09:00
ad421e3c74 비밀번호 변경 security 로직 수정 2026-02-20 11:36:21 +09:00
5f62f4a209 Merge pull request 'test 실행 시 회차별 데이터 적재하기' (#121) from feat/training_260202 into develop
Reviewed-on: #121
2026-02-19 18:19:40 +09:00
46db1512a6 test 실행 시 회차별 데이터 적재하기 2026-02-19 18:18:12 +09:00
29bf155b4f Merge pull request 'LogErrorLevel -> CodeExpose 추가' (#120) from feat/training_260202 into develop
Reviewed-on: #120
2026-02-19 17:35:45 +09:00
2034a8fcb2 LogErrorLevel -> CodeExpose 추가 2026-02-19 17:35:15 +09:00
da03f8b749 Merge pull request '모델학습관리 > 모델별 진행 상황 API 추가' (#119) from feat/training_260202 into develop
Reviewed-on: #119
2026-02-19 17:17:46 +09:00
bf212842d8 모델학습관리 > 모델별 진행 상황 API 추가 2026-02-19 17:17:11 +09:00
6a2deff93b Merge pull request '모델학습관리 > 목록 API 메모,작성자 추가로 인한 수정' (#118) from feat/training_260202 into develop
Reviewed-on: #118
2026-02-19 15:35:03 +09:00
d2ca94ea55 모델학습관리 > 목록 API 메모,작성자 추가로 인한 수정 2026-02-19 15:34:18 +09:00
b0a99afcd3 Merge pull request '모델학습 2단계 패키징 시작,종료일시,상태 로직 추가' (#117) from feat/training_260202 into develop
Reviewed-on: #117
2026-02-19 14:44:12 +09:00
5ddf6dfeeb 모델학습 2단계 패키징 시작,종료일시,상태 로직 추가 2026-02-19 14:43:14 +09:00
eedf72d7aa Merge pull request '공통코드 common-code 로 prefix 변경' (#116) from feat/training_260202 into develop
Reviewed-on: #116
2026-02-19 11:39:23 +09:00
5e13c0b396 공통코드 common-code 로 prefix 변경 2026-02-19 11:38:56 +09:00
25e9941464 Merge pull request '로그관리 로직 커밋' (#115) from feat/training_260202 into develop
Reviewed-on: #115
2026-02-19 11:16:40 +09:00
435f60dcac 로그관리 로직 커밋 2026-02-19 11:13:40 +09:00
a0da0392cf Merge pull request '압축해제 시, 동일 폴더가 있으면 삭제 후 재업로드' (#114) from feat/training_260202 into develop
Reviewed-on: #114
2026-02-18 16:37:13 +09:00
5f5eabca19 압축해제 시, 동일 폴더가 있으면 삭제 후 재업로드 2026-02-18 16:36:46 +09:00
a3ebee12b5 Merge pull request 'feat/training_260202' (#113) from feat/training_260202 into develop
Reviewed-on: #113
2026-02-18 16:29:55 +09:00
413631840f 학습데이터 다운로드 security 제외하기 2026-02-18 16:29:28 +09:00
c7f63d1ad1 압축 해제한 폴더의 갯수 맞는지 log 찍기 + 갯수 맞지 않으면 exception 리턴 2026-02-18 16:22:32 +09:00
c5b14ca09d Merge pull request '업로드 시 uid로 중복체크 -> 삭제인 row는 제외하기' (#112) from feat/training_260202 into develop
Reviewed-on: #112
2026-02-18 15:40:38 +09:00
7529d23488 업로드 시 uid로 중복체크 -> 삭제인 row는 제외하기 2026-02-18 15:40:14 +09:00
44b3b857b1 Merge pull request 'feat/training_260202' (#111) from feat/training_260202 into develop
Reviewed-on: #111
2026-02-18 15:29:25 +09:00
cb3e51d712 업로드 시 exception 메세지 처리, 에폭 10 이상으로 실행되게 수정 2026-02-18 15:28:29 +09:00
99a4597b5f train 결과 +1 했던 거 제거하기 2026-02-18 14:50:53 +09:00
63124455fd Merge pull request '1단계 실행 시, 시작시간 update 추가' (#110) from feat/training_260202 into develop
Reviewed-on: #110
2026-02-18 13:06:33 +09:00
d9da0d4610 1단계 실행 시, 시작시간 update 추가 2026-02-18 13:05:59 +09:00
e75ea8d8a5 Merge pull request '하이퍼 파라미터 수정' (#109) from feat/training_260202 into develop
Reviewed-on: #109
2026-02-13 15:00:50 +09:00
22c481556c 하이퍼 파라미터 수정 2026-02-13 15:00:30 +09:00
31ac4209c3 Merge pull request '하이퍼 파라미터 수정' (#108) from feat/training_260202 into develop
Reviewed-on: #108
2026-02-13 14:47:19 +09:00
0798b352c7 하이퍼 파라미터 수정 2026-02-13 14:46:59 +09:00
df09935789 Merge pull request '하이퍼 파라미터 수정' (#107) from feat/training_260202 into develop
Reviewed-on: #107
2026-02-13 14:42:54 +09:00
5b074bdb81 하이퍼 파라미터 수정 2026-02-13 14:42:39 +09:00
bb15b1b0f2 Merge pull request '하이퍼 파라미터 수정' (#106) from feat/training_260202 into develop
Reviewed-on: #106
2026-02-13 14:38:01 +09:00
28919345c2 하이퍼 파라미터 수정 2026-02-13 14:37:44 +09:00
f4d491ed94 Merge pull request '하이퍼 파라미터 수정' (#105) from feat/training_260202 into develop
Reviewed-on: #105
2026-02-13 14:31:13 +09:00
aa0552aaa7 하이퍼 파라미터 수정 2026-02-13 14:30:45 +09:00
96cb7d2f23 Merge pull request '사용가능 용량 API 수정' (#104) from feat/training_260202 into develop
Reviewed-on: #104
2026-02-13 14:19:26 +09:00
5d0aca14a6 사용가능 용량 API 수정 2026-02-13 14:19:09 +09:00
cc6305b0df Merge pull request '이어하기 수정' (#103) from feat/training_260202 into develop
Reviewed-on: #103
2026-02-13 14:08:51 +09:00
af8d59ddfa 이어하기 수정 2026-02-13 14:08:35 +09:00
3916b13876 Merge pull request '이어하기 수정' (#102) from feat/training_260202 into develop
Reviewed-on: #102
2026-02-13 14:04:52 +09:00
4f24e09c57 이어하기 수정 2026-02-13 14:04:33 +09:00
ee4a06df30 Merge pull request '이어하기 수정' (#101) from feat/training_260202 into develop
Reviewed-on: #101
2026-02-13 13:57:48 +09:00
4da477706f 이어하기 수정 2026-02-13 13:57:01 +09:00
bb5ff7c3cd Merge pull request '이어하기 로그 수정' (#100) from feat/training_260202 into develop
Reviewed-on: #100
2026-02-13 13:27:08 +09:00
a070566048 이어하기 로그 수정 2026-02-13 13:26:54 +09:00
312a96dda1 Merge pull request '트랜젝션처리 임시폴더 uid업데이트' (#99) from feat/training_260202 into develop
Reviewed-on: #99
2026-02-13 13:24:10 +09:00
a5b3ae613f 트랜젝션처리 임시폴더 uid업데이트 2026-02-13 13:23:53 +09:00
e38231e06d Merge pull request '트랜젝션처리 임시폴더 uid업데이트' (#98) from feat/training_260202 into develop
Reviewed-on: #98
2026-02-13 13:17:41 +09:00
979af088be 트랜젝션처리 임시폴더 uid업데이트 2026-02-13 13:17:27 +09:00
bf6e45d706 Merge pull request 'feat/training_260202' (#97) from feat/training_260202 into develop
Reviewed-on: #97
2026-02-13 12:53:32 +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
7fa8921a25 Merge pull request 'flush 추가해보기' (#96) from feat/training_260202 into develop
Reviewed-on: #96
2026-02-13 12:47:11 +09:00
195856b846 flush 추가해보기 2026-02-13 12:46:54 +09:00
7c940351d9 Merge pull request '트랜젝션처리 임시폴더 uid업데이트' (#95) from feat/training_260202 into develop
Reviewed-on: #95
2026-02-13 12:39:21 +09:00
124da48e51 트랜젝션처리 임시폴더 uid업데이트 2026-02-13 12:38:55 +09:00
6b834da912 Merge pull request 'feat/training_260202' (#94) from feat/training_260202 into develop
Reviewed-on: #94
2026-02-13 12:25:26 +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
25aaa97d65 Merge pull request '트랜젝션처리 임시폴더 uid업데이트' (#93) from feat/training_260202 into develop
Reviewed-on: #93
2026-02-13 12:21:11 +09:00
352a28b87f 트랜젝션처리 임시폴더 uid업데이트 2026-02-13 12:20:48 +09:00
da9d47ae4a Merge pull request '주석 처리' (#92) from feat/training_260202 into develop
Reviewed-on: #92
2026-02-13 12:10:46 +09:00
bf8515163c 주석 처리 2026-02-13 12:10:08 +09:00
7d6a77bf2a Merge pull request '트랜젝션처리 임시폴더 uid업데이트' (#91) from feat/training_260202 into develop
Reviewed-on: #91
2026-02-13 12:01:01 +09:00
26828d0968 add log 2026-02-13 12:00:54 +09:00
2691f6ce16 트랜젝션처리 임시폴더 uid업데이트 2026-02-13 12:00:42 +09:00
e2dbae15c0 Merge pull request '트랜젝션처리 임시폴더 uid업데이트' (#90) from feat/training_260202 into develop
Reviewed-on: #90
2026-02-13 11:58:55 +09:00
b246034632 Merge pull request '트랜젝션처리 임시폴더 uid업데이트' (#89) from feat/training_260202 into develop
Reviewed-on: #89
2026-02-13 11:58:25 +09:00
7e5aa5e713 트랜젝션처리 임시폴더 uid업데이트 2026-02-13 11:57:48 +09:00
060a815e1c 트랜젝션처리 임시폴더 uid업데이트 2026-02-13 11:55:35 +09:00
687ea82d78 Merge pull request 'feat/training_260202' (#88) from feat/training_260202 into develop
Reviewed-on: #88
2026-02-13 10:51:07 +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
4ac0f19908 Merge pull request '파일 count 기능 추가' (#87) from feat/training_260202 into develop
Reviewed-on: #87
2026-02-13 10:38:48 +09:00
11d3afe295 파일 count 기능 추가 2026-02-13 10:38:24 +09:00
9e5e7595eb Merge pull request '학습실행 step1 할 때 best epoch 업데이트' (#86) from feat/training_260202 into develop
Reviewed-on: #86
2026-02-13 10:18:26 +09:00
1e62a8b097 학습실행 step1 할 때 best epoch 업데이트 2026-02-13 10:15:04 +09:00
9cd9274e99 Merge pull request '학습데이터 목록 파일 단위 MB 나오게 하기' (#85) from feat/training_260202 into develop
Reviewed-on: #85
2026-02-13 09:48:08 +09:00
26a4623aa8 학습데이터 목록 파일 단위 MB 나오게 하기 2026-02-13 09:42:36 +09:00
5d82f3ecfe Merge pull request 'tmp 파일 링크 수정' (#84) from feat/training_260202 into develop
Reviewed-on: #84
2026-02-13 09:11:05 +09:00
ce6e4f5aea tmp 파일 링크 수정 2026-02-13 09:10:45 +09:00
2ce249ab33 Merge pull request 'tmp 파일 링크 수정' (#83) from feat/training_260202 into develop
Reviewed-on: #83
2026-02-13 08:44:37 +09:00
c2215836c0 tmp 파일 링크 수정 2026-02-13 08:44:17 +09:00
e34bf68de0 Merge pull request 'tmp 파일 링크 수정' (#82) from feat/training_260202 into develop
Reviewed-on: #82
2026-02-13 08:33:58 +09:00
8c19c996f7 tmp 파일 링크 수정 2026-02-13 08:33:36 +09:00
862bda0cb9 Merge pull request '이어하기 수정' (#81) from feat/training_260202 into develop
Reviewed-on: #81
2026-02-12 23:02:12 +09:00
b5ce3ab1fb 이어하기 수정 2026-02-12 23:01:56 +09:00
90f7b17d07 Merge pull request '학습데이터 다운로드 파일 정보 API 추가' (#80) from feat/training_260202 into develop
Reviewed-on: #80
2026-02-12 22:58:47 +09:00
e1ceb769dd 학습데이터 다운로드 파일 정보 API 추가 2026-02-12 22:47:13 +09:00
2128baa46a Merge pull request 'feat/training_260202' (#79) from feat/training_260202 into develop
Reviewed-on: #79
2026-02-12 22:26:26 +09:00
4219b88fb3 학습데이터 다운로드 API 추가 2026-02-12 22:25:55 +09:00
4f94c99b64 이어하기 수정 2026-02-12 22:09:54 +09:00
875c30f467 Merge pull request 'feat/training_260202' (#78) from feat/training_260202 into develop
Reviewed-on: #78
2026-02-12 21:52:16 +09:00
d42e1afbd4 스케줄러 api 수동 호출 2026-02-12 21:51:53 +09:00
b3b8016673 csv 결과 받아오는 것 변경 2026-02-12 21:45:38 +09:00
2b29cd1ac6 Merge pull request '파라미터 변경' (#77) from feat/training_260202 into develop
Reviewed-on: #77
2026-02-12 21:30:18 +09:00
79e8259f28 파라미터 변경 2026-02-12 21:30:03 +09:00
9206fff5d0 Merge pull request 'feat/training_260202' (#76) from feat/training_260202 into develop
Reviewed-on: #76
2026-02-12 21:17:33 +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
260 changed files with 24446 additions and 13006 deletions

6
.gitignore vendored
View File

@@ -72,3 +72,9 @@ docker-compose.override.yml
*.swo *.swo
*~ *~
!/CLAUDE.md !/CLAUDE.md
### SSL Certificates ###
nginx/ssl/
*.crt
*.key
*.pem

415
DEPLOY.md Normal file
View File

@@ -0,0 +1,415 @@
# KAMCO Training API 배포 가이드 (RedHat 9.6)
## 빠른 배포 (Quick Start)
이 문서는 RedHat 9.6 환경에서 HTTPS로 KAMCO Training API를 배포하는 방법을 설명합니다.
**접속 URL**:
- `https://api.train-kamco.com`
- `https://train-kamco.com`
## 사전 요구사항
- [x] Docker & Docker Compose 설치
- [x] Git 설치
- [x] sudo 권한
- [x] 포트 80, 443 사용 가능
## 1단계: /etc/hosts 설정
```bash
# root 권한으로 도메인 추가
echo "127.0.0.1 api.train-kamco.com train-kamco.com" | sudo tee -a /etc/hosts
# 확인
cat /etc/hosts | grep train-kamco
```
**예상 결과**:
```
127.0.0.1 api.train-kamco.com train-kamco.com
```
## 2단계: 방화벽 설정 (필요시)
```bash
# 방화벽 상태 확인
sudo firewall-cmd --state
# HTTP/HTTPS 포트 개방
sudo firewall-cmd --permanent --add-port=80/tcp
sudo firewall-cmd --permanent --add-port=443/tcp
# 방화벽 재로드
sudo firewall-cmd --reload
# 확인
sudo firewall-cmd --list-ports
```
**예상 결과**: `80/tcp 443/tcp`
## 3단계: 프로젝트 디렉토리로 이동
```bash
cd /path/to/kamco-train-api
# 현재 위치 확인
pwd
# 예상: /home/username/kamco-train-api
```
## 4단계: 파일 구조 확인
배포 전 필수 파일이 모두 있는지 확인하세요:
```bash
# SSL 인증서 확인
ls -la nginx/ssl/
# 예상 결과:
# train-kamco.com.crt (인증서)
# train-kamco.com.key (개인 키)
# openssl.cnf (설정 파일)
```
```bash
# Docker Compose 파일 확인
ls -la docker-compose-prod.yml nginx/nginx.conf
# 예상: 두 파일 모두 존재
```
## 5단계: Docker 네트워크 생성 (최초 1회)
```bash
# kamco-cds 네트워크가 있는지 확인
docker network ls | grep kamco-cds
# 없으면 생성
docker network create kamco-cds
```
## 6단계: Docker Compose 배포
```bash
# 기존 컨테이너 중지 (있는 경우)
docker-compose -f docker-compose-prod.yml down
# 새로운 이미지 빌드 및 실행
docker-compose -f docker-compose-prod.yml up -d --build
# 컨테이너 상태 확인
docker-compose -f docker-compose-prod.yml ps
```
**예상 결과**:
```
NAME STATUS
kamco-cd-nginx Up (healthy)
kamco-cd-training-api Up (healthy)
```
## 7단계: 배포 확인
### 컨테이너 로그 확인
```bash
# Nginx 로그
docker logs kamco-cd-nginx --tail 50
# API 로그
docker logs kamco-cd-training-api --tail 50
# 실시간 로그 (Ctrl+C로 종료)
docker-compose -f docker-compose-prod.yml logs -f
```
### HTTP → HTTPS 리다이렉트 테스트
```bash
# HTTP 접속 시 HTTPS로 리다이렉트되는지 확인
curl -I http://api.train-kamco.com
curl -I http://train-kamco.com
# 예상 결과: 301 Moved Permanently
# Location: https://api.train-kamco.com/ 또는 https://train-kamco.com/
```
### HTTPS 헬스체크
```bash
# -k 플래그: 사설 인증서 경고 무시
curl -k https://api.train-kamco.com/monitor/health
curl -k https://train-kamco.com/monitor/health
# 예상 결과: {"status":"UP","components":{...}}
```
### 브라우저 테스트
브라우저에서 다음 URL에 접속:
- `https://api.train-kamco.com/monitor/health`
- `https://train-kamco.com/monitor/health`
**사설 인증서 경고**:
- "안전하지 않음" 경고가 표시되면 **"고급"** → **"계속 진행"** 클릭
## 8단계: SSL 인증서 확인 (선택사항)
```bash
# 인증서 정보 확인
openssl x509 -in nginx/ssl/train-kamco.com.crt -text -noout | head -30
# 유효 기간 확인 (100년)
openssl x509 -in nginx/ssl/train-kamco.com.crt -noout -dates
# SAN (멀티 도메인) 확인
openssl x509 -in nginx/ssl/train-kamco.com.crt -text -noout | grep -A1 "Subject Alternative Name"
# 예상 결과:
# X509v3 Subject Alternative Name:
# DNS:api.train-kamco.com, DNS:train-kamco.com
```
## 트러블슈팅
### 문제 1: "Connection refused"
**원인**: 컨테이너가 실행되지 않음
**해결**:
```bash
# 컨테이너 상태 확인
docker ps -a | grep kamco-cd
# 컨테이너 재시작
docker-compose -f docker-compose-prod.yml restart
# 로그 확인
docker logs kamco-cd-nginx
docker logs kamco-cd-training-api
```
### 문제 2: "502 Bad Gateway"
**원인**: Nginx는 실행 중이지만 API 컨테이너가 준비되지 않음
**해결**:
```bash
# API 컨테이너 상태 확인
docker logs kamco-cd-training-api
# API 헬스체크 (컨테이너 내부에서)
docker exec kamco-cd-nginx wget -qO- http://kamco-changedetection-api:8080/monitor/health
# API 컨테이너 재시작
docker-compose -f docker-compose-prod.yml restart kamco-changedetection-api
```
### 문제 3: "Name or service not known"
**원인**: /etc/hosts에 도메인이 설정되지 않음
**해결**:
```bash
# /etc/hosts 확인
cat /etc/hosts | grep train-kamco
# 없으면 추가
echo "127.0.0.1 api.train-kamco.com train-kamco.com" | sudo tee -a /etc/hosts
```
### 문제 4: 포트 80 또는 443이 이미 사용 중
**원인**: 다른 프로세스가 포트를 사용 중
**해결**:
```bash
# 포트 사용 확인
sudo lsof -i :80
sudo lsof -i :443
# 사용 중인 프로세스 종료 (예: httpd, nginx)
sudo systemctl stop httpd
sudo systemctl stop nginx
# Docker Compose 재시작
docker-compose -f docker-compose-prod.yml restart
```
### 문제 5: SELinux 권한 오류
**원인**: SELinux가 Docker 볼륨 마운트를 차단
**해결**:
```bash
# SELinux 상태 확인
getenforce
# Permissive 모드로 임시 변경 (재부팅 시 초기화됨)
sudo setenforce 0
# 영구 변경 (권장하지 않음)
sudo vi /etc/selinux/config
# SELINUX=permissive 또는 SELINUX=disabled로 변경
```
## 컨테이너 관리 명령어
### 시작/중지/재시작
```bash
# 시작
docker-compose -f docker-compose-prod.yml up -d
# 중지
docker-compose -f docker-compose-prod.yml down
# 재시작
docker-compose -f docker-compose-prod.yml restart
# 특정 서비스만 재시작
docker-compose -f docker-compose-prod.yml restart nginx
docker-compose -f docker-compose-prod.yml restart kamco-changedetection-api
```
### 로그 확인
```bash
# 전체 로그
docker-compose -f docker-compose-prod.yml logs
# 특정 서비스 로그
docker-compose -f docker-compose-prod.yml logs nginx
docker-compose -f docker-compose-prod.yml logs kamco-changedetection-api
# 실시간 로그
docker-compose -f docker-compose-prod.yml logs -f
# 마지막 N줄만 보기
docker logs kamco-cd-nginx --tail 100
```
### 컨테이너 상태 확인
```bash
# 실행 중인 컨테이너
docker-compose -f docker-compose-prod.yml ps
# 상세 정보
docker inspect kamco-cd-nginx
docker inspect kamco-cd-training-api
# 리소스 사용량
docker stats kamco-cd-nginx kamco-cd-training-api
```
### 컨테이너 내부 접속
```bash
# Nginx 컨테이너 내부 접속
docker exec -it kamco-cd-nginx sh
# API 컨테이너 내부 접속
docker exec -it kamco-cd-training-api sh
# 내부에서 빠져나오기
exit
```
## 업데이트 및 재배포
### 코드 업데이트 후 재배포
```bash
# 1. Git pull (코드 업데이트)
git pull origin develop
# 2. JAR 파일 빌드 (Jenkins에서 수행하는 경우 생략)
./gradlew clean build -x test
# 3. 컨테이너 재빌드 및 재시작
docker-compose -f docker-compose-prod.yml down
docker-compose -f docker-compose-prod.yml up -d --build
# 4. 로그 확인
docker-compose -f docker-compose-prod.yml logs -f
```
### 설정 파일만 변경한 경우
```bash
# nginx.conf 또는 docker-compose-prod.yml 변경 시
docker-compose -f docker-compose-prod.yml down
docker-compose -f docker-compose-prod.yml up -d
# 또는
docker-compose -f docker-compose-prod.yml restart nginx
```
## 모니터링
### 헬스체크 엔드포인트
```bash
# API 헬스체크
curl -k https://api.train-kamco.com/monitor/health
# 예상 결과:
# {
# "status": "UP",
# "components": {
# "db": {"status": "UP"},
# "diskSpace": {"status": "UP"}
# }
# }
```
### 시스템 리소스
```bash
# 디스크 사용량
df -h
# 메모리 사용량
free -h
# Docker 이미지 및 컨테이너 용량
docker system df
```
## 보안 권장 사항
1. **사설 인증서**: 현재 사설 인증서를 사용 중입니다. 프로덕션 환경에서는 **Let's Encrypt** 또는 **GlobalSign** 같은 공인 인증서 사용을 권장합니다.
2. **방화벽**: 필요한 포트(80, 443)만 개방하고, 불필요한 포트는 차단하세요.
3. **정기 업데이트**: Docker 이미지와 시스템 패키지를 정기적으로 업데이트하세요.
4. **로그 모니터링**: 정기적으로 로그를 확인하여 비정상적인 활동을 감지하세요.
5. **백업**: SSL 인증서 키 파일(`train-kamco.com.key`)과 데이터베이스를 정기적으로 백업하세요.
## 참고 문서
- **SSL 인증서 설정**: [nginx/SSL_SETUP.md](nginx/SSL_SETUP.md)
- **프로젝트 개요**: [README.md](README.md)
- **CLAUDE.md**: [CLAUDE.md](CLAUDE.md)
## 지원
문제가 발생하면 다음을 확인하세요:
1. 컨테이너 로그: `docker-compose -f docker-compose-prod.yml logs`
2. 컨테이너 상태: `docker-compose -f docker-compose-prod.yml ps`
3. /etc/hosts 설정: `cat /etc/hosts | grep train-kamco`
4. 방화벽 상태: `sudo firewall-cmd --list-ports`
---
**배포 완료!** 🎉
접속 URL:
- `https://api.train-kamco.com`
- `https://train-kamco.com`

443
DEPLOYMENT.md Normal file
View File

@@ -0,0 +1,443 @@
# KAMCO Train API - Production Deployment Guide
프로덕션 환경 배포 가이드
## 목차
- [사전 요구사항](#사전-요구사항)
- [초기 설정](#초기-설정)
- [배포 순서](#배포-순서)
- [개별 서비스 관리](#개별-서비스-관리)
- [롤백 절차](#롤백-절차)
- [모니터링 및 헬스체크](#모니터링-및-헬스체크)
- [트러블슈팅](#트러블슈팅)
---
## 사전 요구사항
### 시스템 요구사항
- Docker Engine 20.10+
- Docker Compose 2.0+
- 최소 메모리: 4GB
- 디스크 공간: 20GB 이상
### 네트워크 요구사항
- 도메인 설정:
- `train-kamco.com` → 서버 IP (Web UI)
- `api.train-kamco.com` → 서버 IP (API)
- 포트:
- 80 (HTTP)
- 443 (HTTPS)
- 8080 (API - internal)
- 3002 (Web - internal)
### 필수 파일
- SSL 인증서:
- `nginx/ssl/train-kamco.com.crt`
- `nginx/ssl/train-kamco.com.key`
- 환경 설정:
- `application-prod.yml` (API 설정)
- `.env` 파일 (IMAGE_TAG 등)
---
## 초기 설정
### 1. Docker 네트워크 생성
```bash
# kamco-cds 네트워크 생성 (최초 1회만)
docker network create kamco-cds
# 네트워크 확인
docker network ls | grep kamco-cds
```
### 2. SSL 인증서 배치
```bash
# 인증서 디렉토리 생성
mkdir -p nginx/ssl
# 인증서 파일 복사
cp /path/to/train-kamco.com.crt nginx/ssl/
cp /path/to/train-kamco.com.key nginx/ssl/
# 권한 설정
chmod 600 nginx/ssl/train-kamco.com.key
chmod 644 nginx/ssl/train-kamco.com.crt
```
### 3. 환경 변수 설정
```bash
# .env 파일 생성
cat > .env << EOF
IMAGE_TAG=latest
SPRING_PROFILES_ACTIVE=prod
TZ=Asia/Seoul
EOF
```
### 4. 볼륨 디렉토리 생성
```bash
# 데이터 디렉토리 생성
mkdir -p ./app/model_output
mkdir -p ./app/train_dataset
# 권한 설정
chmod -R 755 ./app
```
---
## 배포 순서
### 전체 스택 초기 배포
**중요**: 반드시 아래 순서대로 실행해야 합니다.
```bash
# 1. Nginx 시작
docker-compose -f docker-compose-nginx.yml up -d
# 2. Nginx 상태 확인
docker ps | grep kamco-train-nginx
# 3. API 서비스 시작
docker-compose -f docker-compose-prod.yml up -d
# 4. API 헬스체크 대기 (최대 40초)
sleep 40
curl -f http://localhost:8080/monitor/health
# 5. Web 서비스 시작 (kamco-train-web 프로젝트에서)
# cd ../kamco-train-web
# docker-compose -f docker-compose-prod.yml up -d
# 6. 전체 상태 확인
docker ps -a | grep kamco
```
### 배포 검증
```bash
# 서비스별 헬스체크
curl -f http://localhost:8080/monitor/health # API (internal)
curl -kf https://api.train-kamco.com/monitor/health # API (external)
curl -kf https://train-kamco.com # Web (external)
# Nginx 설정 검증
docker exec kamco-train-nginx nginx -t
# 로그 확인
docker-compose -f docker-compose-nginx.yml logs --tail=50
docker-compose -f docker-compose-prod.yml logs --tail=50
```
---
## 개별 서비스 관리
### Nginx 관리
```bash
# 설정 변경 후 리로드 (다운타임 없음)
docker exec kamco-train-nginx nginx -s reload
# 재시작
docker-compose -f docker-compose-nginx.yml restart
# 로그 확인
docker-compose -f docker-compose-nginx.yml logs -f
# 컨테이너 내부 접근
docker exec -it kamco-train-nginx sh
```
### API 서비스 관리
```bash
# 재배포 (새 이미지 빌드)
docker-compose -f docker-compose-prod.yml up -d --build
# 재시작 (이미지 변경 없이)
docker-compose -f docker-compose-prod.yml restart
# 중지
docker-compose -f docker-compose-prod.yml down
# 로그 확인
docker-compose -f docker-compose-prod.yml logs -f kamco-train-api
# 컨테이너 내부 접근
docker exec -it kamco-train-api bash
```
### Web 서비스 관리
```bash
# kamco-train-web 프로젝트에서 실행
cd ../kamco-train-web
# 재배포
docker-compose -f docker-compose-prod.yml up -d --build
# 재시작
docker-compose -f docker-compose-prod.yml restart
# 로그 확인
docker-compose -f docker-compose-prod.yml logs -f
```
---
## 롤백 절차
### 이미지 기반 롤백
```bash
# 1. 사용 가능한 이미지 확인
docker images | grep kamco-train-api
# 2. 이전 이미지 태그로 롤백
export IMAGE_TAG=previous-commit-hash
docker-compose -f docker-compose-prod.yml up -d
# 3. 헬스체크 확인
curl -f http://localhost:8080/monitor/health
```
### Git 기반 롤백
```bash
# 1. 이전 커밋으로 체크아웃
git log --oneline -10
git checkout <previous-commit-hash>
# 2. 재빌드 및 배포
docker-compose -f docker-compose-prod.yml up -d --build
# 3. 검증 후 브랜치 업데이트 (필요시)
# git checkout develop
# git reset --hard <previous-commit-hash>
# git push -f origin develop
```
---
## 모니터링 및 헬스체크
### 헬스체크 엔드포인트
```bash
# API 헬스체크
curl http://localhost:8080/monitor/health
curl http://localhost:8080/monitor/health/readiness
curl http://localhost:8080/monitor/health/liveness
# Nginx를 통한 헬스체크
curl -k https://api.train-kamco.com/monitor/health
```
### 컨테이너 상태 모니터링
```bash
# 모든 컨테이너 상태
docker ps -a | grep kamco
# 리소스 사용량 실시간 모니터링
docker stats kamco-train-nginx kamco-train-api
# 헬스체크 상태
docker inspect kamco-train-api | grep -A 10 Health
```
### 로그 모니터링
```bash
# 실시간 로그 (모든 서비스)
docker-compose -f docker-compose-nginx.yml logs -f &
docker-compose -f docker-compose-prod.yml logs -f &
# 에러 로그만 필터링
docker-compose -f docker-compose-prod.yml logs | grep -i error
# 최근 100줄
docker-compose -f docker-compose-prod.yml logs --tail=100
```
---
## 트러블슈팅
### 1. Nginx 502 Bad Gateway
**원인**: API 서비스가 준비되지 않음
```bash
# API 컨테이너 상태 확인
docker ps | grep kamco-train-api
# API 로그 확인
docker logs kamco-train-api --tail=100
# 네트워크 연결 확인
docker network inspect kamco-cds | grep kamco-train-api
# 해결: API 재시작
docker-compose -f docker-compose-prod.yml restart
```
### 2. SSL 인증서 오류
**원인**: 인증서 파일 누락 또는 권한 문제
```bash
# 인증서 파일 확인
ls -la nginx/ssl/
# Nginx 설정 검증
docker exec kamco-train-nginx nginx -t
# 해결: 인증서 재배치 및 권한 설정
chmod 600 nginx/ssl/train-kamco.com.key
chmod 644 nginx/ssl/train-kamco.com.crt
docker-compose -f docker-compose-nginx.yml restart
```
### 3. 컨테이너 시작 실패
**원인**: 포트 충돌, 볼륨 권한, 메모리 부족
```bash
# 포트 사용 확인
netstat -tulpn | grep -E '80|443|8080'
# 볼륨 권한 확인
ls -la ./app/
# 메모리 사용량 확인
free -h
docker system df
# 해결: 충돌 프로세스 종료 또는 포트 변경
# 메모리 정리
docker system prune -a
```
### 4. 네트워크 연결 문제
**원인**: kamco-cds 네트워크 미생성 또는 컨테이너 미연결
```bash
# 네트워크 확인
docker network ls | grep kamco-cds
# 네트워크 상세 정보
docker network inspect kamco-cds
# 해결: 네트워크 생성
docker network create kamco-cds
# 컨테이너를 네트워크에 연결
docker network connect kamco-cds kamco-train-api
docker network connect kamco-cds kamco-train-nginx
```
### 5. 데이터베이스 연결 실패
**원인**: application-prod.yml의 DB 설정 오류
```bash
# API 로그에서 DB 연결 에러 확인
docker logs kamco-train-api | grep -i "connection"
# DB 호스트 연결 테스트
docker exec kamco-train-api ping <db-host>
# 해결: application-prod.yml 수정 후 재배포
vim src/main/resources/application-prod.yml
docker-compose -f docker-compose-prod.yml up -d --build
```
---
## Jenkins CI/CD 연동
현재 프로젝트는 Jenkins 파이프라인으로 자동 배포됩니다.
### Jenkinsfile-dev 주요 단계
1. **Checkout**: develop 브랜치 체크아웃
2. **Build**: `./gradlew clean build -x test`
3. **Extract Commit**: IMAGE_TAG로 사용
4. **Transfer**: 배포 서버로 파일 전송
5. **Deploy**: Docker Compose 빌드 및 배포
6. **Health Check**: 30초 대기 후 헬스체크
7. **Cleanup**: 오래된 이미지 정리 (최신 5개 유지)
### 배포 서버 정보
- **서버**: 192.168.2.109
- **사용자**: space
- **배포 경로**: `/home/space/kamco-training-api`
- **헬스체크**: `http://localhost:7200/monitor/health`
---
## 백업 및 복구
### 데이터 백업
```bash
# 볼륨 데이터 백업
tar -czf backup-$(date +%Y%m%d).tar.gz ./app/model_output ./app/train_dataset
# 설정 파일 백업
tar -czf config-backup-$(date +%Y%m%d).tar.gz \
nginx/nginx.conf \
nginx/ssl/ \
src/main/resources/application-prod.yml
```
### 이미지 백업
```bash
# 현재 이미지 저장
docker save kamco-train-api:latest | gzip > kamco-train-api-latest.tar.gz
# 이미지 복구
gunzip -c kamco-train-api-latest.tar.gz | docker load
```
---
## 보안 체크리스트
- [ ] SSL 인증서 유효기간 확인
- [ ] nginx/ssl/ 디렉토리 권한 600
- [ ] application-prod.yml에 DB 비밀번호 암호화
- [ ] JWT secret key 환경변수로 관리
- [ ] Docker 소켓 권한 최소화
- [ ] 방화벽 규칙 설정 (80, 443만 외부 노출)
- [ ] 정기 보안 업데이트 (docker images)
---
## 참고 문서
- [CLAUDE.md](./CLAUDE.md) - 프로젝트 개발 가이드
- [README.md](./README.md) - 프로젝트 개요
- [Jenkinsfile-dev](./Jenkinsfile-dev) - CI/CD 파이프라인
- [nginx/nginx.conf](./nginx/nginx.conf) - Nginx 설정
---
## 연락처
문제 발생 시:
1. 로그 수집: `docker-compose logs` 출력
2. 시스템 정보: `docker ps -a`, `docker network ls`
3. 이슈 리포트: GitHub Issues 또는 내부 이슈 트래커

20
Dockerfile Normal file
View File

@@ -0,0 +1,20 @@
# Stage 1: Build stage (gradle build는 Jenkins에서 이미 수행)
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
# JAR 파일 복사 (Jenkins에서 빌드된 ROOT.jar)
COPY build/libs/ROOT.jar app.jar
# 포트 노출
EXPOSE 8080
# 애플리케이션 실행
# dev 프로파일로 실행
ENTRYPOINT ["java", "-jar", "-Dspring.profiles.active=prod", "app.jar"]

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

@@ -5,6 +5,13 @@ services:
dockerfile: Dockerfile-dev dockerfile: Dockerfile-dev
image: kamco-cd-training-api:${IMAGE_TAG:-latest} image: kamco-cd-training-api:${IMAGE_TAG:-latest}
container_name: kamco-cd-training-api container_name: kamco-cd-training-api
deploy:
resources:
reservations:
devices:
- driver: nvidia
count: all
capabilities: [gpu]
ports: ports:
- "7200:8080" - "7200:8080"
environment: environment:
@@ -14,6 +21,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

28
docker-compose-nginx.yml Normal file
View File

@@ -0,0 +1,28 @@
services:
nginx:
image: nginx:alpine
container_name: kamco-train-nginx
ports:
- "80:80"
- "443:443"
volumes:
- ./nginx/nginx.conf:/etc/nginx/nginx.conf:ro
- ./nginx/ssl:/etc/nginx/ssl:ro
- nginx-logs:/var/log/nginx
networks:
- kamco-cds
restart: unless-stopped
healthcheck:
test: ["CMD", "wget", "--quiet", "--tries=1", "--spider", "--no-check-certificate", "https://localhost/monitor/health"]
interval: 30s
timeout: 10s
retries: 3
start_period: 10s
networks:
kamco-cds:
external: true
volumes:
nginx-logs:
driver: local

36
docker-compose-prod.yml Normal file
View File

@@ -0,0 +1,36 @@
services:
kamco-train-api:
build:
context: .
dockerfile: Dockerfile
image: kamco-train-api:${IMAGE_TAG:-latest}
container_name: kamco-train-api
deploy:
resources:
reservations:
devices:
- driver: nvidia
count: all
capabilities: [gpu]
expose:
- "8080"
environment:
- SPRING_PROFILES_ACTIVE=prod
- TZ=Asia/Seoul
volumes:
- ./app/model_output:/app/model-outputs
- ./app/train_dataset:/app/train-dataset
- /var/run/docker.sock:/var/run/docker.sock
networks:
- kamco-cds
restart: unless-stopped
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:8080/monitor/health"]
interval: 10s
timeout: 5s
retries: 5
start_period: 40s
networks:
kamco-cds:
external: true

0
gradlew vendored Normal file → Executable file
View File

414
nginx/SSL_SETUP.md Normal file
View File

@@ -0,0 +1,414 @@
# SSL 사설 인증서 설정 가이드 (RedHat 9.6)
## 개요
이 문서는 RedHat 9.6 환경에서 `https://api.train-kamco.com``https://train-kamco.com` 도메인을 위한 100년 유효한 사설 SSL 인증서 설정 방법을 설명합니다.
## 디렉토리 구조
```
nginx/
├── nginx.conf # Nginx 설정 파일
├── ssl/
│ ├── openssl.cnf # OpenSSL 설정 파일 (SAN 포함)
│ ├── train-kamco.com.crt # 사설 SSL 인증서 (멀티 도메인)
│ └── train-kamco.com.key # 개인 키 (비공개)
└── SSL_SETUP.md # 이 문서
```
## 인증서 정보
- **도메인**: api.train-kamco.com, train-kamco.com (멀티 도메인)
- **유효 기간**: 100년 (36500일)
- **알고리즘**: RSA 4096-bit
- **CN (Common Name)**: api.train-kamco.com
- **SAN (Subject Alternative Names)**: api.train-kamco.com, train-kamco.com
## 사설 SSL 인증서 생성 (이미 생성됨)
인증서가 이미 생성되어 있습니다. 재생성이 필요한 경우 아래 단계를 따르세요.
### 1. OpenSSL 설정 파일 생성
```bash
cd /path/to/kamco-train-api
cat > nginx/ssl/openssl.cnf << 'EOF'
[req]
default_bits = 4096
prompt = no
default_md = sha256
distinguished_name = dn
req_extensions = v3_req
[dn]
C=KR
ST=Seoul
L=Seoul
O=KAMCO
OU=Training
CN=api.train-kamco.com
[v3_req]
subjectAltName = @alt_names
[alt_names]
DNS.1 = api.train-kamco.com
DNS.2 = train-kamco.com
EOF
```
### 2. SSL 인증서 및 개인 키 생성
```bash
# nginx/ssl 디렉토리 생성 (없는 경우)
mkdir -p nginx/ssl
chmod 700 nginx/ssl
# 인증서 및 개인 키 생성 (100년 유효)
openssl req -new -x509 -newkey rsa:4096 -sha256 -nodes \
-keyout nginx/ssl/train-kamco.com.key \
-out nginx/ssl/train-kamco.com.crt \
-days 36500 \
-config nginx/ssl/openssl.cnf \
-extensions v3_req
# 파일 권한 설정
chmod 600 nginx/ssl/train-kamco.com.key
chmod 644 nginx/ssl/train-kamco.com.crt
```
### 3. 인증서 검증
```bash
# 인증서 정보 확인
openssl x509 -in nginx/ssl/train-kamco.com.crt -text -noout
# 유효 기간 확인
openssl x509 -in nginx/ssl/train-kamco.com.crt -text -noout | grep -A2 "Validity"
# SAN (멀티 도메인) 확인
openssl x509 -in nginx/ssl/train-kamco.com.crt -text -noout | grep -A1 "Subject Alternative Name"
# CN 확인
openssl x509 -in nginx/ssl/train-kamco.com.crt -noout -subject
# 개인 키 확인
openssl rsa -in nginx/ssl/train-kamco.com.key -check
```
**예상 결과**:
```
X509v3 Subject Alternative Name:
DNS:api.train-kamco.com, DNS:train-kamco.com
Validity
Not Before: Mar 2 23:39:XX 2026 GMT
Not After : Feb 6 23:39:XX 2126 GMT
```
## /etc/hosts 설정 (RedHat 9.6)
### 1. hosts 파일에 도메인 추가
```bash
# root 권한으로 실행
echo "127.0.0.1 api.train-kamco.com train-kamco.com" | sudo tee -a /etc/hosts
# 확인
cat /etc/hosts | grep train-kamco
```
**예상 결과**:
```
127.0.0.1 api.train-kamco.com train-kamco.com
```
### 2. 도메인 확인
```bash
# ping 테스트
ping -c 2 api.train-kamco.com
ping -c 2 train-kamco.com
```
## Docker Compose 배포
### 1. 기존 컨테이너 중지 (실행 중인 경우)
```bash
cd /path/to/kamco-train-api
docker-compose -f docker-compose-prod.yml down
```
### 2. Production 환경 실행
```bash
# IMAGE_TAG 환경 변수 설정 (선택사항)
export IMAGE_TAG=latest
# Docker Compose 실행
docker-compose -f docker-compose-prod.yml up -d
# 컨테이너 상태 확인
docker-compose -f docker-compose-prod.yml ps
```
### 3. 로그 확인
```bash
# Nginx 로그
docker logs kamco-cd-nginx
# API 로그
docker logs kamco-cd-training-api
# 실시간 로그 확인
docker-compose -f docker-compose-prod.yml logs -f
```
## HTTPS 접속 테스트
### 1. HTTP → HTTPS 리다이렉트 테스트
```bash
# api.train-kamco.com
curl -I http://api.train-kamco.com
# train-kamco.com
curl -I http://train-kamco.com
# 예상 결과: 301 Moved Permanently
# Location: https://api.train-kamco.com/ 또는 https://train-kamco.com/
```
### 2. HTTPS 헬스체크 (-k: 사설 인증서 경고 무시)
```bash
# api.train-kamco.com
curl -k https://api.train-kamco.com/monitor/health
# train-kamco.com
curl -k https://train-kamco.com/monitor/health
# 예상 결과: {"status":"UP","components":{...}}
```
### 3. SSL 인증서 확인
```bash
# api.train-kamco.com
openssl s_client -connect api.train-kamco.com:443 -showcerts
# train-kamco.com
openssl s_client -connect train-kamco.com:443 -showcerts
# CN 및 SAN 확인
```
### 4. 브라우저 테스트
브라우저에서 다음 URL에 접속:
- `https://api.train-kamco.com/monitor/health`
- `https://train-kamco.com/monitor/health`
**주의**: 사설 인증서이므로 "안전하지 않음" 경고가 표시됩니다.
- **Chrome/Edge**: "고급" → "계속 진행" 클릭
- **Firefox**: "위험 감수 및 계속" 클릭
## 브라우저에서 사설 인증서 신뢰 설정 (선택사항)
사설 인증서를 브라우저에 등록하면 경고 없이 접속 가능합니다.
### Chrome/Edge (RedHat Desktop)
1. `chrome://settings/certificates` 접속
2. **Authorities** 탭 선택
3. **Import** 클릭
4. `nginx/ssl/train-kamco.com.crt` 선택
5. **Trust this certificate for identifying websites** 체크
6. **OK** 클릭
### Firefox
1. `about:preferences#privacy` 접속
2. **Certificates****View Certificates** 클릭
3. **Authorities** 탭 선택
4. **Import** 클릭
5. `nginx/ssl/train-kamco.com.crt` 선택
6. **Trust this CA to identify websites** 체크
7. **OK** 클릭
## 방화벽 설정 (RedHat 9.6)
### 1. 방화벽 상태 확인
```bash
sudo firewall-cmd --state
```
### 2. HTTP (80) 및 HTTPS (443) 포트 개방
```bash
# HTTP 포트 개방
sudo firewall-cmd --permanent --add-port=80/tcp
# HTTPS 포트 개방
sudo firewall-cmd --permanent --add-port=443/tcp
# 방화벽 재로드
sudo firewall-cmd --reload
# 확인
sudo firewall-cmd --list-ports
```
**예상 결과**:
```
80/tcp 443/tcp
```
## 보안 체크리스트
- [x] `train-kamco.com.key` 파일 권한이 600으로 설정됨
- [x] ssl 디렉토리가 버전 관리에서 제외됨 (.gitignore 확인)
- [x] 두 도메인(api.train-kamco.com, train-kamco.com) 모두 SAN에 포함됨
- [ ] 방화벽에서 80, 443 포트 개방 확인
- [x] HSTS 헤더 활성화 확인
- [x] TLS 1.2 이상만 허용 확인
- [ ] /etc/hosts에 도메인 매핑 확인
## 트러블슈팅
### 인증서 관련 오류
**"certificate verify failed"**
```bash
# 해결: -k 플래그 사용 (사설 인증서 경고 무시)
curl -k https://api.train-kamco.com/monitor/health
```
**"NET::ERR_CERT_AUTHORITY_INVALID" (브라우저)**
- 정상 동작: 사설 인증서이므로 브라우저 경고는 예상된 동작입니다
- 해결: 브라우저에 인증서 등록 (위 "브라우저에서 사설 인증서 신뢰 설정" 참조)
### 연결 오류
**"Connection refused"**
```bash
# 컨테이너 상태 확인
docker ps | grep kamco-cd
# 포트 바인딩 확인
docker port kamco-cd-nginx
# 예상 결과:
# 80/tcp -> 0.0.0.0:80
# 443/tcp -> 0.0.0.0:443
```
**"502 Bad Gateway"**
```bash
# API 컨테이너 상태 확인
docker logs kamco-cd-training-api
# nginx → API 연결 확인
docker exec kamco-cd-nginx wget -qO- http://kamco-changedetection-api:8080/monitor/health
```
**"Name or service not known" (도메인 해석 실패)**
```bash
# /etc/hosts 확인
cat /etc/hosts | grep train-kamco
# 없으면 추가
echo "127.0.0.1 api.train-kamco.com train-kamco.com" | sudo tee -a /etc/hosts
```
### 방화벽 관련 오류
**외부에서 접속 안 됨**
```bash
# 방화벽 확인
sudo firewall-cmd --list-ports
# 포트 개방
sudo firewall-cmd --permanent --add-port=80/tcp
sudo firewall-cmd --permanent --add-port=443/tcp
sudo firewall-cmd --reload
```
## 인증서 만료 및 갱신
### 만료 확인
```bash
# 인증서 만료일 확인
openssl x509 -in nginx/ssl/train-kamco.com.crt -noout -enddate
# 예상 결과: notAfter=Feb 6 23:39:XX 2126 GMT (100년 후)
```
### 갱신 방법 (필요시)
100년 유효한 인증서이므로 갱신이 필요하지 않지만, 재생성이 필요한 경우:
```bash
# 기존 인증서 백업
cp nginx/ssl/train-kamco.com.crt nginx/ssl/train-kamco.com.crt.bak
cp nginx/ssl/train-kamco.com.key nginx/ssl/train-kamco.com.key.bak
# 위의 "사설 SSL 인증서 생성" 단계 재실행
# nginx 재시작
docker-compose -f docker-compose-prod.yml restart nginx
```
## 주의사항
1. **사설 인증서 경고**: 브라우저에서 "안전하지 않음" 경고가 표시됩니다. 프로덕션 환경에서는 **공인 인증서(Let's Encrypt, GlobalSign 등) 사용을 권장**합니다.
2. **포트 80/443**: Docker가 자동으로 처리하지만, 이미 사용 중인 프로세스가 있으면 충돌할 수 있습니다.
```bash
# 포트 사용 확인
sudo lsof -i :80
sudo lsof -i :443
```
3. **대용량 파일 업로드**: `client_max_body_size`를 10GB로 설정했으므로, 서버 메모리 및 디스크 용량을 충분히 확보하세요.
4. **인증서 백업**: `train-kamco.com.key` 파일은 매우 중요합니다. 안전한 곳에 백업하세요.
5. **SELinux**: RedHat 9.6에서 SELinux가 활성화된 경우, Docker 볼륨 마운트 권한 문제가 발생할 수 있습니다.
```bash
# SELinux 상태 확인
getenforce
# 필요시 permissive 모드로 변경
sudo setenforce 0
```
### 2단계: 시스템 신뢰 폴더로 복사
터미널을 열고 관리자 권한(sudo)을 사용해 인증서를 시스템 폴더로 복사합니다.
```
sudo cp mycert.crt /etc/pki/ca-trust/source/anchors/
```
### 3단계: 시스템 신뢰 목록 업데이트
아래 명령어를 입력해 추가한 인증서를 시스템에 갱신시킵니다.
```
sudo update-ca-trust
```
## 참고 자료
- [OpenSSL Documentation](https://www.openssl.org/docs/)
- [Nginx SSL Configuration](https://nginx.org/en/docs/http/configuring_https_servers.html)
- [Docker Compose Documentation](https://docs.docker.com/compose/)
- [Let's Encrypt (공인 인증서)](https://letsencrypt.org/)

179
nginx/nginx.conf Normal file
View File

@@ -0,0 +1,179 @@
events {
worker_connections 1024;
}
http {
include /etc/nginx/mime.types;
default_type application/octet-stream;
# 로그 설정
log_format main '$remote_addr - $remote_user [$time_local] "$request" '
'$status $body_bytes_sent "$http_referer" '
'"$http_user_agent" "$http_x_forwarded_for"';
access_log /var/log/nginx/access.log main;
error_log /var/log/nginx/error.log warn;
sendfile on;
keepalive_timeout 65;
# 업로드 파일 크기 제한 (10GB)
client_max_body_size 10G;
# Upstream 설정
upstream api_backend {
server kamco-train-api:8080;
}
upstream web_backend {
server kamco-train-web:3002;
}
# HTTP → HTTPS 리다이렉트 서버
server {
listen 80;
server_name api.train-kamco.com train-kamco.com;
# 모든 HTTP 요청을 HTTPS로 리다이렉트
return 301 https://$server_name$request_uri;
}
# HTTPS 서버 설정
server {
listen 443 ssl http2;
server_name api.train-kamco.com;
# SSL 인증서 설정 (사설 인증서 - 멀티 도메인)
ssl_certificate /etc/nginx/ssl/train-kamco.com.crt;
ssl_certificate_key /etc/nginx/ssl/train-kamco.com.key;
# SSL 프로토콜 및 암호화 설정
ssl_protocols TLSv1.2 TLSv1.3;
ssl_ciphers 'ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384';
ssl_prefer_server_ciphers off;
# SSL 세션 캐시
ssl_session_cache shared:SSL:10m;
ssl_session_timeout 10m;
# HSTS (HTTP Strict Transport Security)
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
# 보안 헤더
add_header X-Frame-Options "SAMEORIGIN" always;
add_header X-Content-Type-Options "nosniff" always;
add_header X-XSS-Protection "1; mode=block" always;
# 프록시 설정
location / {
proxy_pass http://api_backend;
proxy_http_version 1.1;
# 프록시 헤더 설정
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Forwarded-Host $server_name;
# 인증 헤더 및 쿠키 전달 (JWT 토큰 전달 보장)
proxy_pass_request_headers on;
proxy_set_header Cookie $http_cookie;
proxy_set_header Authorization $http_authorization;
# 타임아웃 설정 (대용량 파일 업로드 지원)
proxy_connect_timeout 300s;
proxy_send_timeout 300s;
proxy_read_timeout 300s;
# 버퍼 설정
proxy_buffering on;
proxy_buffer_size 4k;
proxy_buffers 8 4k;
proxy_busy_buffers_size 8k;
}
# 헬스체크 엔드포인트
location /monitor/health {
proxy_pass http://api_backend/monitor/health;
access_log off;
}
}
# HTTPS 서버 설정 - Next.js Web Application
server {
listen 443 ssl http2;
server_name train-kamco.com;
# SSL 인증서 설정 (사설 인증서 - 멀티 도메인)
ssl_certificate /etc/nginx/ssl/train-kamco.com.crt;
ssl_certificate_key /etc/nginx/ssl/train-kamco.com.key;
# SSL 프로토콜 및 암호화 설정
ssl_protocols TLSv1.2 TLSv1.3;
ssl_ciphers 'ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384';
ssl_prefer_server_ciphers off;
# SSL 세션 캐시
ssl_session_cache shared:SSL:10m;
ssl_session_timeout 10m;
# HSTS (HTTP Strict Transport Security)
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
# 보안 헤더
add_header X-Frame-Options "SAMEORIGIN" always;
add_header X-Content-Type-Options "nosniff" always;
add_header X-XSS-Protection "1; mode=block" always;
# API 프록시 설정 (Web에서 API 호출 시)
location /api/ {
proxy_pass http://api_backend/api/;
proxy_http_version 1.1;
# 프록시 헤더 설정
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Forwarded-Host $server_name;
# 인증 헤더 및 쿠키 전달
proxy_pass_request_headers on;
proxy_set_header Cookie $http_cookie;
# 타임아웃 설정
proxy_connect_timeout 300s;
proxy_send_timeout 300s;
proxy_read_timeout 300s;
}
# 프록시 설정
location / {
proxy_pass http://web_backend;
proxy_http_version 1.1;
# 프록시 헤더 설정
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Forwarded-Host $server_name;
# Next.js WebSocket 지원을 위한 Upgrade 헤더
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
# 타임아웃 설정
proxy_connect_timeout 600s;
proxy_send_timeout 600s;
proxy_read_timeout 600s;
# 버퍼 설정
proxy_buffering on;
proxy_buffer_size 4k;
proxy_buffers 8 4k;
proxy_busy_buffers_size 8k;
}
}
}

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

@@ -0,0 +1,8 @@
package com.kamco.cd.training.common.dto;
public class MonitorDto {
public int cpu; // CPU 사용률 (%)
public long[] memory; // "사용/전체"
public int gpu; // 🔥 전체 GPU 평균 (%)
}

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

@@ -0,0 +1,142 @@
package com.kamco.cd.training.common.service;
import jakarta.annotation.PostConstruct;
import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import lombok.extern.log4j.Log4j2;
import org.springframework.stereotype.Component;
@Component
@Log4j2
public class GpuDmonReader {
// =========================
// GPU 사용률 저장소
// key: GPU index (0,1,2...)
// value: 현재 GPU 사용률 (%)
// ConcurrentHashMap → 멀티스레드 안전
// =========================
private final Map<Integer, Integer> gpuUtilMap = new ConcurrentHashMap<>();
// =========================
// 외부 조회용
// SystemMonitorService에서 호출
// =========================
public Map<Integer, Integer> getGpuUtilMap() {
return gpuUtilMap;
}
// =========================
// Bean 초기화 시 실행
// - 별도 스레드에서 GPU 모니터링 시작
// - 메인 스레드 block 방지
// =========================
@PostConstruct
public void start() {
// nvidia-smi 없는 환경이면 GPU 모니터링 비활성화
if (!isNvidiaAvailable()) {
log.warn("nvidia-smi not found. GPU monitoring disabled.");
return;
}
// 데몬 스레드로 실행 (서버 종료 시 자동 종료)
Thread t = new Thread(this::runLoop, "gpu-dmon-thread");
t.setDaemon(true);
t.start();
}
// =========================
// 무한 루프
// - dmon 실행
// - 죽으면 자동 재시작
// =========================
private void runLoop() {
while (true) {
try {
runDmon(); // GPU 사용률 수집 시작
} catch (Exception e) {
// dmon 프로세스 종료되면 여기로 들어옴
log.warn("dmon restart: {}", e.getMessage());
}
// 5초 대기 후 재시작
sleep(5000);
}
}
// =========================
// nvidia-smi dmon 실행
// - GPU 사용률 스트리밍으로 계속 수신
// =========================
private void runDmon() throws Exception {
// -s u → GPU utilization만 출력
ProcessBuilder pb = new ProcessBuilder("nvidia-smi", "dmon", "-s", "u");
// 프로세스 실행 후 stdout 읽기
try (BufferedReader br =
new BufferedReader(new InputStreamReader(pb.start().getInputStream()))) {
String line;
// dmon은 계속 출력됨 (스트리밍)
while ((line = br.readLine()) != null) {
// 헤더 제거 (#로 시작)
if (line.startsWith("#")) continue;
line = line.trim();
if (line.isEmpty()) continue;
// 공백 기준 분리
String[] parts = line.split("\\s+");
// 첫 번째 값이 GPU index인지 확인
if (!parts[0].matches("\\d+")) continue;
int index = Integer.parseInt(parts[0]);
try {
// 두 번째 값이 GPU 사용률 (sm)
int util = Integer.parseInt(parts[1]);
// 최신 값 갱신
gpuUtilMap.put(index, util);
} catch (Exception ignored) {
// 파싱 실패 시 무시
}
}
}
// 여기까지 왔다는 건 dmon 프로세스 종료됨
// → runLoop에서 재시작하도록 예외 발생
throw new IllegalStateException("dmon stopped");
}
// =========================
// nvidia-smi 존재 여부 확인
// =========================
private boolean isNvidiaAvailable() {
try {
Process p = new ProcessBuilder("which", "nvidia-smi").start();
return p.waitFor() == 0;
} catch (Exception e) {
return false;
}
}
// =========================
// sleep 유틸
// =========================
private void sleep(long ms) {
try {
Thread.sleep(ms);
} catch (InterruptedException ignored) {
}
}
}

View File

@@ -0,0 +1,224 @@
package com.kamco.cd.training.common.service;
import com.kamco.cd.training.common.dto.MonitorDto;
import java.io.BufferedReader;
import java.io.FileReader;
import java.util.ArrayDeque;
import java.util.Deque;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import lombok.RequiredArgsConstructor;
import lombok.extern.log4j.Log4j2;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Service;
@Service
@RequiredArgsConstructor
@Log4j2
public class SystemMonitorService {
// =========================
// CPU 이전값 (delta 계산용)
// - /proc/stat은 누적값이기 때문에
// - 이전 값과 비교해서 사용률 계산
// =========================
private long prevIdle = 0;
private long prevTotal = 0;
// =========================
// 최근 30초 히스토리
// - CPU: 30개 (1초 * 30)
// - GPU: GPU별 30개
// =========================
private final Deque<Double> cpuHistory = new ArrayDeque<>();
// key: GPU index
// value: 최근 30개 사용률
private final Map<Integer, Deque<Integer>> gpuHistory = new ConcurrentHashMap<>();
// =========================
// GPU 데이터 제공 (dmon reader)
// =========================
private final GpuDmonReader gpuReader;
// =========================
// 캐시 (API 응답용)
// - 매 요청마다 계산하지 않기 위해 사용
// - volatile → 멀티스레드 안전하게 최신값 유지
// =========================
private volatile MonitorDto cached = new MonitorDto();
// =========================
// 1초마다 수집
// =========================
@Scheduled(fixedRate = 1000)
public void collect() {
try {
// =====================
// 1. CPU 수집
// =====================
double cpu = readCpu();
cpuHistory.add(cpu);
// 30개 유지 (rolling window)
if (cpuHistory.size() > 30) cpuHistory.poll();
// =====================
// 2. GPU 수집
// =====================
Map<Integer, Integer> gpuMap = gpuReader.getGpuUtilMap();
for (Map.Entry<Integer, Integer> entry : gpuMap.entrySet()) {
int index = entry.getKey();
int util = entry.getValue();
// GPU별 히스토리 생성 및 추가
gpuHistory.computeIfAbsent(index, k -> new ArrayDeque<>()).add(util);
// 30개 유지
Deque<Integer> q = gpuHistory.get(index);
if (q.size() > 30) q.poll();
}
// =====================
// 3. 캐시 업데이트
// =====================
updateCache();
} catch (Exception e) {
log.error("collect error", e);
}
}
// =========================
// CPU 사용률 계산
// - /proc/stat 사용
// - 이전값과의 차이로 계산 (delta 방식)
// =========================
private double readCpu() throws Exception {
if (!isLinux()) return 0;
try (BufferedReader br = new BufferedReader(new FileReader("/proc/stat"))) {
String[] p = br.readLine().split("\\s+");
long user = Long.parseLong(p[1]);
long nice = Long.parseLong(p[2]);
long system = Long.parseLong(p[3]);
long idle = Long.parseLong(p[4]);
long iowait = Long.parseLong(p[5]);
long irq = Long.parseLong(p[6]);
long softirq = Long.parseLong(p[7]);
long total = user + nice + system + idle + iowait + irq + softirq;
long idleAll = idle + iowait;
// 최초 실행 시 기준값만 저장
if (prevTotal == 0) {
prevTotal = total;
prevIdle = idleAll;
return 0;
}
long totalDiff = total - prevTotal;
long idleDiff = idleAll - prevIdle;
prevTotal = total;
prevIdle = idleAll;
if (totalDiff == 0) return 0;
// CPU 사용률 (%)
return (1.0 - (double) idleDiff / totalDiff) * 100;
}
}
// =========================
// Linux 환경 체크
// =========================
private boolean isLinux() {
return System.getProperty("os.name").toLowerCase().contains("linux");
}
// =========================
// Memory 조회 (/proc/meminfo)
// - OS 값 그대로 사용 (kB)
// - [사용량, 전체]
// =========================
private long[] readMemory() throws Exception {
if (!isLinux()) return new long[] {0, 0};
try (BufferedReader br = new BufferedReader(new FileReader("/proc/meminfo"))) {
long total = 0;
long available = 0;
String line;
while ((line = br.readLine()) != null) {
if (line.startsWith("MemTotal")) {
total = Long.parseLong(line.replaceAll("\\D+", ""));
} else if (line.startsWith("MemAvailable")) {
available = Long.parseLong(line.replaceAll("\\D+", ""));
}
}
long used = total - available;
return new long[] {used, total};
}
}
// =========================
// 캐시 업데이트
// - CPU: 30초 평균
// - GPU: 전체 샘플 평균
// - Memory: 현재값
// =========================
private void updateCache() throws Exception {
MonitorDto dto = new MonitorDto();
// =====================
// CPU 평균 (30초)
// =====================
dto.cpu = (int) cpuHistory.stream().mapToDouble(Double::doubleValue).average().orElse(0);
// =====================
// Memory (kB 그대로)
// =====================
dto.memory = readMemory();
// =====================
// GPU 평균 (🔥 전체 샘플 기준)
// =====================
int sum = 0;
int count = 0;
for (Deque<Integer> q : gpuHistory.values()) {
for (int v : q) {
sum += v;
count++;
}
}
dto.gpu = (count == 0) ? 0 : sum / count;
// =====================
// 캐시 교체 (atomic)
// =====================
this.cached = dto;
}
// =========================
// 외부 조회
// - Controller에서 호출
// =========================
public MonitorDto get() {
return cached;
}
}

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,142 @@
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 설정 - application.yml에서 환경별로 관리 */
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.setAllowCredentials(true); // 쿠키, Authorization 헤더, Bearer Token 등 자격증명 포함 요청을 허용할지 설정 // application.yml에서 환경별로 설정된 도메인 사용
config.setExposedHeaders(List.of("Content-Disposition")); config.setAllowedOriginPatterns(List.of("*")); // 도메인 허용
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(); config.setAllowedMethods(List.of("GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS"));
/** "/**" → 모든 API 경로에 대해 이 CORS 규칙을 적용 /api/** 같이 특정 경로만 지정 가능. */ config.setAllowedHeaders(List.of("*")); // 헤더요청 Authorization, Content-Type, X-Custom-Header
source.registerCorsConfiguration("/**", config); // CORS 정책을 등록 config.setAllowCredentials(true); // 쿠키, Authorization 헤더, Bearer Token 등 자격증명 포함 요청을 허용할지 설정
return source; config.setExposedHeaders(List.of("Content-Disposition", "Authorization"));
} config.setMaxAge(3600L); // Preflight 요청 캐시 (1시간)
@Bean UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
public HttpFirewall httpFirewall() { /** "/**" → 모든 API 경로에 대해 이 CORS 규칙을 적용 /api/** 같이 특정 경로만 지정 가능. */
StrictHttpFirewall firewall = new StrictHttpFirewall(); source.registerCorsConfiguration("/**", config); // CORS 정책을 등록
firewall.setAllowUrlEncodedSlash(true); return source;
firewall.setAllowUrlEncodedDoubleSlash(true); }
firewall.setAllowUrlEncodedPercent(true);
firewall.setAllowSemicolon(true); @Bean
return firewall; public HttpFirewall httpFirewall() {
} StrictHttpFirewall firewall = new StrictHttpFirewall();
firewall.setAllowUrlEncodedSlash(true);
/** 완전 제외(필터 자체를 안 탐) */ firewall.setAllowUrlEncodedDoubleSlash(true);
@Bean firewall.setAllowUrlEncodedPercent(true);
public WebSecurityCustomizer webSecurityCustomizer() { firewall.setAllowSemicolon(true);
return (web) -> web.ignoring().requestMatchers("/api/mapsheet/**"); return firewall;
} }
}
/** 완전 제외(필터 자체를 안 탐) */
@Bean
public WebSecurityCustomizer webSecurityCustomizer() {
return (web) -> web.ignoring().requestMatchers("/api/mapsheet/**");
}
}

View File

@@ -1,96 +1,96 @@
package com.kamco.cd.training.config; 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 2
╠════════════════════════════════════════════════════════════════════════════════╣ ╠════════════════════════════════════════════════════════════════════════════════╣
║ 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,509 @@
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 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);
} }
} }

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