Compare commits
460 Commits
495ef7d86c
...
feat/train
| Author | SHA1 | Date | |
|---|---|---|---|
| 960e4215e0 | |||
| a9b49faa6c | |||
| b760e9874c | |||
| 8698bf61d1 | |||
| 680e137284 | |||
| f4a81a34d6 | |||
| 479ad710e0 | |||
| 3cb9840248 | |||
| fc9543f195 | |||
| 73d0e03b08 | |||
| 50c965cb79 | |||
|
|
abca9467d8 | ||
|
|
4ed03f6e94 | ||
| 04eddfce54 | |||
| f49b7cc850 | |||
| 888c0e314b | |||
| 6c043b0031 | |||
| 531da09c5f | |||
| 4da2a1f0d7 | |||
| 50b3f1ba62 | |||
| f1f88c83e1 | |||
| ff478452a6 | |||
| bf77725ef8 | |||
| dec0f26999 | |||
| dfd4a42379 | |||
| 79272137ab | |||
| 73ea6176b4 | |||
| 3d2a4049d3 | |||
| 26caf505b9 | |||
| 0cbaf53e86 | |||
| bd54854bc6 | |||
| 80fd2bda3e | |||
| ca3d115d0e | |||
| fb647e5991 | |||
| 831ba3e616 | |||
| 87575a62f7 | |||
| a4b5e20db2 | |||
| 246c11f8b0 | |||
| da260f35ea | |||
| 2c1f9bdf5c | |||
| 6cf81bf60f | |||
| 5799f7dfb2 | |||
| ed95829a34 | |||
| 9f428e9572 | |||
| 52ffe53815 | |||
| 904968a1be | |||
| 4b44be6a29 | |||
| 5887a954ea | |||
| f9f0662f8e | |||
|
|
72bc2fd47b | ||
| 7416327cc3 | |||
| da31bd9d99 | |||
| f3e5347335 | |||
| 7d5581f60c | |||
| b4428217ea | |||
| 8a63fdacdd | |||
| cb2e42143a | |||
| 997e85c0cc | |||
| 731ca59475 | |||
| fe6d37456d | |||
| 6c98a48a5d | |||
| 81b69caa99 | |||
| 7fce070686 | |||
| 8d83505ee7 | |||
| 0ff38b24d4 | |||
| 265813e6f7 | |||
| 8190a6e9c8 | |||
| 5c082f7c9d | |||
| 43d0e55cb7 | |||
| df3bedfbda | |||
| c26a48d07d | |||
| 1c7c213977 | |||
| 6583a45abd | |||
| b15f77d894 | |||
| 3bcd99f0db | |||
| 5513cd60a0 | |||
| 7b35d26a13 | |||
| d92ff88ef7 | |||
| bfcddd0327 | |||
| e193330f99 | |||
| 6c0184597d | |||
| 1e8a8d8dad | |||
| eb7680b952 | |||
| 2c357ebf27 | |||
| 0daaa1c8cb | |||
| 96383595df | |||
| c5e03f7ca8 | |||
| df2acc4dfb | |||
| 693de354d2 | |||
| 62cdc5015e | |||
| 5c11263d55 | |||
| dd5031ae3a | |||
| c369e01ada | |||
| 54991622f1 | |||
| e9f8bb37fa | |||
| 17d69486ec | |||
| f3c822587f | |||
| 948f1061da | |||
| 335f0dbb9b | |||
| 42438b3cd5 | |||
| 69eaba1a83 | |||
| 524ae200b0 | |||
| 4f763d3c2e | |||
| 9ebf525387 | |||
| 4ab672a96e | |||
| 7d2a367e3f | |||
| 365ad81cad | |||
| 67a67749c3 | |||
| 9dfa54fbf9 | |||
| 251307b5c9 | |||
| 12f6bb7154 | |||
| 8423a03d31 | |||
| aa3af4e9d0 | |||
| d6cdf6b690 | |||
| 7ca37bf1e4 | |||
| 9cfa299e58 | |||
| 901dde066d | |||
| cb0a38274a | |||
| b8194df9ae | |||
| a137e71420 | |||
| 7c5f07683e | |||
| 159fb281d4 | |||
| 97192ff811 | |||
| 4f3fb675be | |||
| e6caea05b3 | |||
| f08f80622f | |||
| fd63824edc | |||
| 8a44df26b8 | |||
| cb97c5e59e | |||
| 8f75b16dc6 | |||
| e565fd7a34 | |||
| c2978e41c2 | |||
| 8c45b39dcc | |||
| 07429dbe8e | |||
| 83859bb9fe | |||
| b119f333ac | |||
| 564a99448c | |||
| bbe04ee458 | |||
| 38ae6e5575 | |||
| fab3c83a69 | |||
| 40fe98ae0c | |||
| fb87a0f32f | |||
| 255ff10a56 | |||
| f674f73330 | |||
| 63794ec4ec | |||
| db2bc32e7d | |||
| 37786a1e44 | |||
| 901ea83fb7 | |||
| bf6dc9740f | |||
| 832e1b5681 | |||
| a23bc8dd67 | |||
| 4f16355cda | |||
| 13023a06cc | |||
| df46a8f79f | |||
| fcd48831c5 | |||
| 28b50bd949 | |||
| 62c9d73b94 | |||
| 78ab928459 | |||
| 68c0e634c5 | |||
| ae3601cff5 | |||
| ad421e3c74 | |||
| 5f62f4a209 | |||
| 46db1512a6 | |||
| 29bf155b4f | |||
| 2034a8fcb2 | |||
| da03f8b749 | |||
| bf212842d8 | |||
| 6a2deff93b | |||
| d2ca94ea55 | |||
| b0a99afcd3 | |||
| 5ddf6dfeeb | |||
| eedf72d7aa | |||
| 5e13c0b396 | |||
| 25e9941464 | |||
| 435f60dcac | |||
| a0da0392cf | |||
| 5f5eabca19 | |||
| a3ebee12b5 | |||
| 413631840f | |||
| c7f63d1ad1 | |||
| c5b14ca09d | |||
| 7529d23488 | |||
| 44b3b857b1 | |||
| cb3e51d712 | |||
| 99a4597b5f | |||
| 63124455fd | |||
| d9da0d4610 | |||
| e75ea8d8a5 | |||
| 22c481556c | |||
| 31ac4209c3 | |||
| 0798b352c7 | |||
| df09935789 | |||
| 5b074bdb81 | |||
| bb15b1b0f2 | |||
| 28919345c2 | |||
| f4d491ed94 | |||
| aa0552aaa7 | |||
| 96cb7d2f23 | |||
| 5d0aca14a6 | |||
| cc6305b0df | |||
| af8d59ddfa | |||
| 3916b13876 | |||
| 4f24e09c57 | |||
| ee4a06df30 | |||
| 4da477706f | |||
| bb5ff7c3cd | |||
| a070566048 | |||
| 312a96dda1 | |||
| a5b3ae613f | |||
| e38231e06d | |||
| 979af088be | |||
| bf6e45d706 | |||
| e5a1cab36b | |||
| bb67996742 | |||
| 1981d6d1ce | |||
| 47f4ffd4db | |||
| 7fa8921a25 | |||
| 195856b846 | |||
| 7c940351d9 | |||
| 124da48e51 | |||
| 6b834da912 | |||
| 02724e9508 | |||
| 7ed91ccab9 | |||
| a7c13b985d | |||
| 25aaa97d65 | |||
| 352a28b87f | |||
| da9d47ae4a | |||
| bf8515163c | |||
| 7d6a77bf2a | |||
| 26828d0968 | |||
| 2691f6ce16 | |||
| e2dbae15c0 | |||
| b246034632 | |||
| 7e5aa5e713 | |||
| 060a815e1c | |||
| 687ea82d78 | |||
| 1eb4d04779 | |||
| f30c0c6d45 | |||
| 12994aab60 | |||
| 4ac0f19908 | |||
| 11d3afe295 | |||
| 9e5e7595eb | |||
| 1e62a8b097 | |||
| 9cd9274e99 | |||
| 26a4623aa8 | |||
| 5d82f3ecfe | |||
| ce6e4f5aea | |||
| 2ce249ab33 | |||
| c2215836c0 | |||
| e34bf68de0 | |||
| 8c19c996f7 | |||
| 862bda0cb9 | |||
| b5ce3ab1fb | |||
| 90f7b17d07 | |||
| e1ceb769dd | |||
| 2128baa46a | |||
| 4219b88fb3 | |||
| 4f94c99b64 | |||
| 875c30f467 | |||
| d42e1afbd4 | |||
| b3b8016673 | |||
| 2b29cd1ac6 | |||
| 79e8259f28 | |||
| 9206fff5d0 | |||
| 032c82c2f0 | |||
| 6204a6e5fa | |||
| 4d9c9a86b4 | |||
| 83204abfe9 | |||
| 5b682c1386 | |||
| 452494d44d | |||
| 8ada26448b | |||
| e442f105bc | |||
| 5e0a771848 | |||
| b4c2685059 | |||
| e238f3ca88 | |||
| 97b06eb3b3 | |||
| ad32ca18ca | |||
| 98a1283ebe | |||
| a10fccaae3 | |||
| c3c9191d9d | |||
| 9fd5a15a72 | |||
| 12f9de7367 | |||
| 5455da1e96 | |||
| 9e803661cd | |||
| b0cf9e77ec | |||
| c92426aefc | |||
| d5b2b8ecec | |||
| 6185a18a7c | |||
| 49d3e37458 | |||
| 1fb10830b9 | |||
| d7766edd24 | |||
| 0bc4453c9c | |||
| ae0d30e5da | |||
| 37d776dd2c | |||
| 0c34ea7dcb | |||
| 3106d36431 | |||
| ed48f697a4 | |||
| da92b28d97 | |||
| 6c865d26fd | |||
| e3f00876f1 | |||
| 16e156b5b4 | |||
| 60962bbc75 | |||
| 6a939118ff | |||
| 64d37dcc08 | |||
| 0c0ae16c2b | |||
| a2490f30e6 | |||
| 953f95aed6 | |||
| bd04e1f4e8 | |||
| 85633c8bab | |||
| 5fc15937c0 | |||
| 8b3940b446 | |||
| 201cfefb6b | |||
| 9958b0999a | |||
| 3547c28361 | |||
| 6c70bfed18 | |||
| 95a75e63f4 | |||
| 2a1dbee290 | |||
| 384a321bf3 | |||
| f4e97d389b | |||
| 590810ff0a | |||
| a01c872982 | |||
| 905a245070 | |||
| 860ce35a8f | |||
| 7f3f5dca40 | |||
| 4a0a4e35ed | |||
| ae055dca1e | |||
| 26e8e1492f | |||
| 8fa722011c | |||
| 17d47d6200 | |||
| e178f58fe2 | |||
| cd0cf5726d | |||
| 8e4bea53da | |||
| 7a22d8ba73 | |||
| 2df4a7a80b | |||
| b451f697bc | |||
| 7e9c867f34 | |||
| 130e85f8a1 | |||
| 9e713cb49d | |||
| 51dfa97900 | |||
| 87c6b599b4 | |||
| f50855a822 | |||
| 8d416317a8 | |||
| 22aa071476 | |||
| a83bd09f8f | |||
| 96035f864a | |||
| fd7dfd7e7f | |||
| 190b93bee8 | |||
| c5f19cc961 | |||
| c56c0ca605 | |||
| c6e721aa37 | |||
| 6572e17f00 | |||
| be6365807c | |||
| d2fff7dfde | |||
| f66bc22c95 | |||
| 3367d0e7be | |||
| 352ec6ccb0 | |||
| 6a989255a3 | |||
| 878b21573f | |||
| 0602db1436 | |||
| 2f8bd1f98c | |||
| 75231ccbba | |||
| 1249a80da5 | |||
| 00c78eb42f | |||
| 35767adba1 | |||
| 47a2a159ef | |||
| 95548223cd | |||
| 2debdc5312 | |||
| 207cc47f1b | |||
| b6338bce8e | |||
| 2cfa2adcf5 | |||
| d7e19abfc9 | |||
| c843703ee7 | |||
| 133ea6b1ba | |||
| 0df977ae81 | |||
| 3e39006822 | |||
| 3ec1a71406 | |||
| 16009f1623 | |||
| 41911014c9 | |||
| 8ea32ce675 | |||
| a4ac80c787 | |||
| 3a5d136d34 | |||
| 2f63b9ddcd | |||
| 92de48b55e | |||
| 224ddae68b | |||
| 885b72a0c6 | |||
| 9ac00d37c5 | |||
| fbb5a34867 | |||
| e25fc01b25 | |||
| 6b3f22dd66 | |||
| abc2c8e806 | |||
| 29e1d0ec7e | |||
| 8d379d064c | |||
| a2072e0148 | |||
| 6352f74b08 | |||
| 025b573859 | |||
| 0e9fa80092 | |||
| 2d5de88a6b | |||
| 89744d2aa1 | |||
| eda1d19942 | |||
| b4a4486560 | |||
| 653717a074 | |||
| 7cc3392856 | |||
| def84d2b1c | |||
| 679795d14d | |||
| 0a7f01a2f5 | |||
| 9655c62d35 | |||
| db6844f0e7 | |||
| af16933378 | |||
| 29b653a4e9 | |||
| 693e3ef3ab | |||
| 03135a972a | |||
| 381b7d7e0b | |||
| 947cba2742 | |||
| f038fdd1db | |||
| 474a3c119e | |||
| b2be43a76e | |||
| ce69bacb01 | |||
| 200b384e19 | |||
| 350d622e5a | |||
| b25fc6fe68 | |||
| 6cdf4efda6 | |||
| 7d866e5869 | |||
| 5bc59c0e0b | |||
| 3c0a12da4e | |||
| cdac9d6148 | |||
| abe4272227 | |||
| d238debcc8 | |||
| fdfda049f8 | |||
| 3e780ef007 | |||
| 2c825b14ee | |||
| f9d081970d | |||
| 2110f395b7 | |||
| 60d45ee2ce | |||
| 50464c1aa8 | |||
| f1ad59d0b1 | |||
| 19644e5c9f | |||
| 4c80017fc5 | |||
| 5dfcd3d181 | |||
| 6e99c209d6 | |||
| 38b037da31 | |||
| d66711e4f4 | |||
| 44878e9c37 | |||
| 53c8e1dd50 | |||
| 15aa2fa041 | |||
| 335e9d33d6 | |||
| d6f16544e2 | |||
| 3bb52bb344 | |||
| 77912b7081 | |||
| 3a8d6e3ef0 | |||
| 25d4cbf672 | |||
| e2757d3ca0 | |||
| 9956ca7d63 | |||
| 210306151b | |||
| d168e9712e | |||
| 999e413305 | |||
| 8619ded142 | |||
| 4b64b03e53 | |||
| c7d7be9d06 | |||
| 37f8d728fa | |||
| a1ffad1c4e |
6
.gitignore
vendored
6
.gitignore
vendored
@@ -72,3 +72,9 @@ docker-compose.override.yml
|
||||
*.swo
|
||||
*~
|
||||
!/CLAUDE.md
|
||||
|
||||
### SSL Certificates ###
|
||||
nginx/ssl/
|
||||
*.crt
|
||||
*.key
|
||||
*.pem
|
||||
|
||||
415
DEPLOY.md
Normal file
415
DEPLOY.md
Normal 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
443
DEPLOYMENT.md
Normal 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
20
Dockerfile
Normal 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"]
|
||||
@@ -1,6 +1,11 @@
|
||||
# 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
|
||||
|
||||
|
||||
45
build.gradle
45
build.gradle
@@ -3,6 +3,7 @@ plugins {
|
||||
id 'org.springframework.boot' version '3.5.7'
|
||||
id 'io.spring.dependency-management' version '1.1.7'
|
||||
id 'com.diffplug.spotless' version '6.25.0'
|
||||
id 'idea'
|
||||
}
|
||||
|
||||
group = 'com.kamco.cd'
|
||||
@@ -21,11 +22,23 @@ configurations {
|
||||
}
|
||||
}
|
||||
|
||||
// QueryDSL 생성된 소스 디렉토리 정의
|
||||
def generatedSourcesDir = file("$buildDir/generated/sources/annotationProcessor/java/main")
|
||||
|
||||
repositories {
|
||||
mavenCentral()
|
||||
maven { url "https://repo.osgeo.org/repository/release/" }
|
||||
}
|
||||
|
||||
// Gradle이 생성된 소스를 컴파일 경로에 포함하도록 설정
|
||||
sourceSets {
|
||||
main {
|
||||
java {
|
||||
srcDirs += generatedSourcesDir
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
|
||||
implementation 'org.springframework.boot:spring-boot-starter-web'
|
||||
@@ -83,6 +96,23 @@ dependencies {
|
||||
implementation 'io.hypersistence:hypersistence-utils-hibernate-63:3.7.0'
|
||||
implementation 'org.reflections:reflections:0.10.2'
|
||||
|
||||
implementation 'com.jcraft:jsch:0.1.55'
|
||||
implementation 'org.apache.commons:commons-csv:1.10.0'
|
||||
}
|
||||
|
||||
// IntelliJ가 생성된 소스를 인식하도록 설정
|
||||
idea {
|
||||
module {
|
||||
// 소스 디렉토리로 인식
|
||||
sourceDirs += generatedSourcesDir
|
||||
|
||||
// Generated Sources Root로 마킹 (IntelliJ에서 특별 처리)
|
||||
generatedSourceDirs += generatedSourcesDir
|
||||
|
||||
// 소스 및 Javadoc 다운로드
|
||||
downloadJavadoc = true
|
||||
downloadSources = true
|
||||
}
|
||||
}
|
||||
|
||||
configurations.configureEach {
|
||||
@@ -93,6 +123,21 @@ tasks.named('test') {
|
||||
useJUnitPlatform()
|
||||
}
|
||||
|
||||
// 컴파일 전 생성된 소스 디렉토리 생성 보장
|
||||
tasks.named('compileJava') {
|
||||
doFirst {
|
||||
generatedSourcesDir.mkdirs()
|
||||
}
|
||||
}
|
||||
|
||||
// 생성된 소스 정리 태스크
|
||||
tasks.register('cleanGeneratedSources', Delete) {
|
||||
delete generatedSourcesDir
|
||||
}
|
||||
|
||||
tasks.named('clean') {
|
||||
dependsOn 'cleanGeneratedSources'
|
||||
}
|
||||
|
||||
bootJar {
|
||||
archiveFileName = 'ROOT.jar'
|
||||
|
||||
@@ -5,6 +5,13 @@ services:
|
||||
dockerfile: Dockerfile-dev
|
||||
image: kamco-cd-training-api:${IMAGE_TAG:-latest}
|
||||
container_name: kamco-cd-training-api
|
||||
deploy:
|
||||
resources:
|
||||
reservations:
|
||||
devices:
|
||||
- driver: nvidia
|
||||
count: all
|
||||
capabilities: [gpu]
|
||||
ports:
|
||||
- "7200:8080"
|
||||
environment:
|
||||
@@ -14,6 +21,8 @@ services:
|
||||
- /mnt/nfs_share/images:/app/original-images
|
||||
- /mnt/nfs_share/model_output:/app/model-outputs
|
||||
- /mnt/nfs_share/train_dataset:/app/train-dataset
|
||||
- /home/kcomu/data:/home/kcomu/data
|
||||
- /var/run/docker.sock:/var/run/docker.sock
|
||||
networks:
|
||||
- kamco-cds
|
||||
restart: unless-stopped
|
||||
|
||||
28
docker-compose-nginx.yml
Normal file
28
docker-compose-nginx.yml
Normal 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
36
docker-compose-prod.yml
Normal 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
|
||||
414
nginx/SSL_SETUP.md
Normal file
414
nginx/SSL_SETUP.md
Normal 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
179
nginx/nginx.conf
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,14 +1,14 @@
|
||||
package com.kamco.cd.training;
|
||||
|
||||
import org.springframework.boot.SpringApplication;
|
||||
import org.springframework.boot.autoconfigure.SpringBootApplication;
|
||||
import org.springframework.scheduling.annotation.EnableScheduling;
|
||||
|
||||
@SpringBootApplication
|
||||
@EnableScheduling
|
||||
public class KamcoTrainingApplication {
|
||||
|
||||
public static void main(String[] args) {
|
||||
SpringApplication.run(KamcoTrainingApplication.class, args);
|
||||
}
|
||||
}
|
||||
package com.kamco.cd.training;
|
||||
|
||||
import org.springframework.boot.SpringApplication;
|
||||
import org.springframework.boot.autoconfigure.SpringBootApplication;
|
||||
import org.springframework.scheduling.annotation.EnableScheduling;
|
||||
|
||||
@SpringBootApplication
|
||||
@EnableScheduling
|
||||
public class KamcoTrainingApplication {
|
||||
|
||||
public static void main(String[] args) {
|
||||
SpringApplication.run(KamcoTrainingApplication.class, args);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,22 +1,22 @@
|
||||
package com.kamco.cd.training.auth;
|
||||
|
||||
import java.security.SecureRandom;
|
||||
import java.util.Base64;
|
||||
|
||||
public class BCryptSaltGenerator {
|
||||
|
||||
public static String generateSaltWithEmployeeNo(String employeeNo) {
|
||||
|
||||
// bcrypt salt는 16바이트(128비트) 필요
|
||||
byte[] randomBytes = new byte[16];
|
||||
new SecureRandom().nextBytes(randomBytes);
|
||||
|
||||
String base64 = Base64.getEncoder().encodeToString(randomBytes);
|
||||
|
||||
// 사번을 포함 (22자 제한 → 잘라내기)
|
||||
String mixedSalt = (employeeNo + base64).substring(0, 22);
|
||||
|
||||
// bcrypt 포맷에 맞게 구성
|
||||
return "$2a$10$" + mixedSalt;
|
||||
}
|
||||
}
|
||||
package com.kamco.cd.training.auth;
|
||||
|
||||
import java.security.SecureRandom;
|
||||
import java.util.Base64;
|
||||
|
||||
public class BCryptSaltGenerator {
|
||||
|
||||
public static String generateSaltWithEmployeeNo(String employeeNo) {
|
||||
|
||||
// bcrypt salt는 16바이트(128비트) 필요
|
||||
byte[] randomBytes = new byte[16];
|
||||
new SecureRandom().nextBytes(randomBytes);
|
||||
|
||||
String base64 = Base64.getEncoder().encodeToString(randomBytes);
|
||||
|
||||
// 사번을 포함 (22자 제한 → 잘라내기)
|
||||
String mixedSalt = (employeeNo + base64).substring(0, 22);
|
||||
|
||||
// bcrypt 포맷에 맞게 구성
|
||||
return "$2a$10$" + mixedSalt;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,62 +1,62 @@
|
||||
package com.kamco.cd.training.auth;
|
||||
|
||||
import com.kamco.cd.training.common.enums.StatusType;
|
||||
import com.kamco.cd.training.common.enums.error.AuthErrorCode;
|
||||
import com.kamco.cd.training.common.exception.CustomApiException;
|
||||
import com.kamco.cd.training.postgres.entity.MemberEntity;
|
||||
import com.kamco.cd.training.postgres.repository.members.MembersRepository;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.mindrot.jbcrypt.BCrypt;
|
||||
import org.springframework.security.authentication.AuthenticationProvider;
|
||||
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
|
||||
import org.springframework.security.core.Authentication;
|
||||
import org.springframework.security.core.AuthenticationException;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
@Component
|
||||
@RequiredArgsConstructor
|
||||
public class CustomAuthenticationProvider implements AuthenticationProvider {
|
||||
|
||||
private final MembersRepository membersRepository;
|
||||
|
||||
@Override
|
||||
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
|
||||
String username = authentication.getName();
|
||||
String rawPassword = authentication.getCredentials().toString();
|
||||
|
||||
// 유저 조회
|
||||
MemberEntity member =
|
||||
membersRepository
|
||||
.findByEmployeeNo(username)
|
||||
.orElseThrow(() -> new CustomApiException(AuthErrorCode.LOGIN_ID_NOT_FOUND));
|
||||
|
||||
// 미사용 상태
|
||||
if (member.getStatus().equals(StatusType.INACTIVE.getId())) {
|
||||
throw new CustomApiException(AuthErrorCode.LOGIN_ID_NOT_FOUND);
|
||||
}
|
||||
|
||||
// jBCrypt + 커스텀 salt 로 저장된 패스워드 비교
|
||||
if (!BCrypt.checkpw(rawPassword, member.getPassword())) {
|
||||
// 실패 카운트 저장
|
||||
int cnt = member.getLoginFailCount() + 1;
|
||||
member.setLoginFailCount(cnt);
|
||||
membersRepository.save(member);
|
||||
throw new CustomApiException(AuthErrorCode.LOGIN_PASSWORD_MISMATCH);
|
||||
}
|
||||
|
||||
// 로그인 실패 체크
|
||||
if (member.getLoginFailCount() >= 5) {
|
||||
throw new CustomApiException(AuthErrorCode.LOGIN_PASSWORD_EXCEEDED);
|
||||
}
|
||||
|
||||
// 인증 성공 → UserDetails 생성
|
||||
CustomUserDetails userDetails = new CustomUserDetails(member);
|
||||
|
||||
return new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities());
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean supports(Class<?> authentication) {
|
||||
return UsernamePasswordAuthenticationToken.class.isAssignableFrom(authentication);
|
||||
}
|
||||
}
|
||||
package com.kamco.cd.training.auth;
|
||||
|
||||
import com.kamco.cd.training.common.enums.StatusType;
|
||||
import com.kamco.cd.training.common.enums.error.AuthErrorCode;
|
||||
import com.kamco.cd.training.common.exception.CustomApiException;
|
||||
import com.kamco.cd.training.postgres.entity.MemberEntity;
|
||||
import com.kamco.cd.training.postgres.repository.members.MembersRepository;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.mindrot.jbcrypt.BCrypt;
|
||||
import org.springframework.security.authentication.AuthenticationProvider;
|
||||
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
|
||||
import org.springframework.security.core.Authentication;
|
||||
import org.springframework.security.core.AuthenticationException;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
@Component
|
||||
@RequiredArgsConstructor
|
||||
public class CustomAuthenticationProvider implements AuthenticationProvider {
|
||||
|
||||
private final MembersRepository membersRepository;
|
||||
|
||||
@Override
|
||||
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
|
||||
String username = authentication.getName();
|
||||
String rawPassword = authentication.getCredentials().toString();
|
||||
|
||||
// 유저 조회
|
||||
MemberEntity member =
|
||||
membersRepository
|
||||
.findByEmployeeNo(username)
|
||||
.orElseThrow(() -> new CustomApiException(AuthErrorCode.LOGIN_ID_NOT_FOUND));
|
||||
|
||||
// 미사용 상태
|
||||
if (member.getStatus().equals(StatusType.INACTIVE.getId())) {
|
||||
throw new CustomApiException(AuthErrorCode.LOGIN_ID_NOT_FOUND);
|
||||
}
|
||||
|
||||
// jBCrypt + 커스텀 salt 로 저장된 패스워드 비교
|
||||
if (!BCrypt.checkpw(rawPassword, member.getPassword())) {
|
||||
// 실패 카운트 저장
|
||||
int cnt = member.getLoginFailCount() + 1;
|
||||
member.setLoginFailCount(cnt);
|
||||
membersRepository.save(member);
|
||||
throw new CustomApiException(AuthErrorCode.LOGIN_PASSWORD_MISMATCH);
|
||||
}
|
||||
|
||||
// 로그인 실패 체크
|
||||
if (member.getLoginFailCount() >= 5) {
|
||||
throw new CustomApiException(AuthErrorCode.LOGIN_PASSWORD_EXCEEDED);
|
||||
}
|
||||
|
||||
// 인증 성공 → UserDetails 생성
|
||||
CustomUserDetails userDetails = new CustomUserDetails(member);
|
||||
|
||||
return new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities());
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean supports(Class<?> authentication) {
|
||||
return UsernamePasswordAuthenticationToken.class.isAssignableFrom(authentication);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,56 +1,56 @@
|
||||
package com.kamco.cd.training.auth;
|
||||
|
||||
import com.kamco.cd.training.postgres.entity.MemberEntity;
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
import org.springframework.security.core.GrantedAuthority;
|
||||
import org.springframework.security.core.authority.SimpleGrantedAuthority;
|
||||
import org.springframework.security.core.userdetails.UserDetails;
|
||||
|
||||
public class CustomUserDetails implements UserDetails {
|
||||
|
||||
private final MemberEntity member;
|
||||
|
||||
public CustomUserDetails(MemberEntity member) {
|
||||
this.member = member;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Collection<? extends GrantedAuthority> getAuthorities() {
|
||||
return List.of(new SimpleGrantedAuthority("ROLE_" + member.getUserRole()));
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getPassword() {
|
||||
return member.getPassword();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getUsername() {
|
||||
return String.valueOf(member.getUuid());
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isAccountNonExpired() {
|
||||
return true; // 추후 상태 필드에 따라 수정 가능
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isAccountNonLocked() {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isCredentialsNonExpired() {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isEnabled() {
|
||||
return "ACTIVE".equals(member.getStatus());
|
||||
}
|
||||
|
||||
public MemberEntity getMember() {
|
||||
return member;
|
||||
}
|
||||
}
|
||||
package com.kamco.cd.training.auth;
|
||||
|
||||
import com.kamco.cd.training.postgres.entity.MemberEntity;
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
import org.springframework.security.core.GrantedAuthority;
|
||||
import org.springframework.security.core.authority.SimpleGrantedAuthority;
|
||||
import org.springframework.security.core.userdetails.UserDetails;
|
||||
|
||||
public class CustomUserDetails implements UserDetails {
|
||||
|
||||
private final MemberEntity member;
|
||||
|
||||
public CustomUserDetails(MemberEntity member) {
|
||||
this.member = member;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Collection<? extends GrantedAuthority> getAuthorities() {
|
||||
return List.of(new SimpleGrantedAuthority("ROLE_" + member.getUserRole()));
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getPassword() {
|
||||
return member.getPassword();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getUsername() {
|
||||
return String.valueOf(member.getUuid());
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isAccountNonExpired() {
|
||||
return true; // 추후 상태 필드에 따라 수정 가능
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isAccountNonLocked() {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isCredentialsNonExpired() {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isEnabled() {
|
||||
return "ACTIVE".equals(member.getStatus());
|
||||
}
|
||||
|
||||
public MemberEntity getMember() {
|
||||
return member;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,70 +1,71 @@
|
||||
package com.kamco.cd.training.auth;
|
||||
|
||||
import jakarta.servlet.FilterChain;
|
||||
import jakarta.servlet.ServletException;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import jakarta.servlet.http.HttpServletResponse;
|
||||
import java.io.IOException;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.lang.NonNull;
|
||||
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
|
||||
import org.springframework.security.core.context.SecurityContextHolder;
|
||||
import org.springframework.security.core.userdetails.UserDetails;
|
||||
import org.springframework.security.core.userdetails.UserDetailsService;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.util.AntPathMatcher;
|
||||
import org.springframework.web.filter.OncePerRequestFilter;
|
||||
|
||||
@Component
|
||||
@RequiredArgsConstructor
|
||||
public class JwtAuthenticationFilter extends OncePerRequestFilter {
|
||||
|
||||
private final JwtTokenProvider jwtTokenProvider;
|
||||
private final UserDetailsService userDetailsService;
|
||||
private static final AntPathMatcher PATH_MATCHER = new AntPathMatcher();
|
||||
private static final String[] EXCLUDE_PATHS = {
|
||||
"/api/auth/signin", "/api/auth/refresh", "/api/auth/logout", "/api/members/*/password"
|
||||
};
|
||||
|
||||
@Override
|
||||
protected void doFilterInternal(
|
||||
@NonNull HttpServletRequest request,
|
||||
@NonNull HttpServletResponse response,
|
||||
@NonNull FilterChain filterChain)
|
||||
throws ServletException, IOException {
|
||||
|
||||
String token = resolveToken(request);
|
||||
|
||||
if (token != null && jwtTokenProvider.isValidToken(token)) {
|
||||
String username = jwtTokenProvider.getSubject(token);
|
||||
|
||||
UserDetails userDetails = userDetailsService.loadUserByUsername(username);
|
||||
UsernamePasswordAuthenticationToken authentication =
|
||||
new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities());
|
||||
SecurityContextHolder.getContext().setAuthentication(authentication);
|
||||
}
|
||||
|
||||
filterChain.doFilter(request, response);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected boolean shouldNotFilter(HttpServletRequest request) {
|
||||
String path = request.getServletPath();
|
||||
|
||||
// JWT 필터를 타지 않게 할 URL 패턴들
|
||||
for (String pattern : EXCLUDE_PATHS) {
|
||||
if (PATH_MATCHER.match(pattern, path)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private String resolveToken(HttpServletRequest request) {
|
||||
String bearer = request.getHeader("Authorization");
|
||||
if (bearer != null && bearer.startsWith("Bearer ")) {
|
||||
return bearer.substring(7);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
package com.kamco.cd.training.auth;
|
||||
|
||||
import jakarta.servlet.FilterChain;
|
||||
import jakarta.servlet.ServletException;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import jakarta.servlet.http.HttpServletResponse;
|
||||
import java.io.IOException;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.lang.NonNull;
|
||||
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
|
||||
import org.springframework.security.core.context.SecurityContextHolder;
|
||||
import org.springframework.security.core.userdetails.UserDetails;
|
||||
import org.springframework.security.core.userdetails.UserDetailsService;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.util.AntPathMatcher;
|
||||
import org.springframework.web.filter.OncePerRequestFilter;
|
||||
|
||||
@Component
|
||||
@RequiredArgsConstructor
|
||||
public class JwtAuthenticationFilter extends OncePerRequestFilter {
|
||||
|
||||
private final JwtTokenProvider jwtTokenProvider;
|
||||
private final UserDetailsService userDetailsService;
|
||||
private static final AntPathMatcher PATH_MATCHER = new AntPathMatcher();
|
||||
private static final String[] EXCLUDE_PATHS = {
|
||||
// "/api/auth/signin", "/api/auth/refresh", "/api/auth/logout", "/api/members/*/password"
|
||||
"/api/auth/signin", "/api/auth/refresh", "/api/auth/logout"
|
||||
};
|
||||
|
||||
@Override
|
||||
protected void doFilterInternal(
|
||||
@NonNull HttpServletRequest request,
|
||||
@NonNull HttpServletResponse response,
|
||||
@NonNull FilterChain filterChain)
|
||||
throws ServletException, IOException {
|
||||
|
||||
String token = resolveToken(request);
|
||||
|
||||
if (token != null && jwtTokenProvider.isValidToken(token)) {
|
||||
String username = jwtTokenProvider.getSubject(token);
|
||||
|
||||
UserDetails userDetails = userDetailsService.loadUserByUsername(username);
|
||||
UsernamePasswordAuthenticationToken authentication =
|
||||
new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities());
|
||||
SecurityContextHolder.getContext().setAuthentication(authentication);
|
||||
}
|
||||
|
||||
filterChain.doFilter(request, response);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected boolean shouldNotFilter(HttpServletRequest request) {
|
||||
String path = request.getServletPath();
|
||||
|
||||
// JWT 필터를 타지 않게 할 URL 패턴들
|
||||
for (String pattern : EXCLUDE_PATHS) {
|
||||
if (PATH_MATCHER.match(pattern, path)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private String resolveToken(HttpServletRequest request) {
|
||||
String bearer = request.getHeader("Authorization");
|
||||
if (bearer != null && bearer.startsWith("Bearer ")) {
|
||||
return bearer.substring(7);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,72 +1,72 @@
|
||||
package com.kamco.cd.training.auth;
|
||||
|
||||
import io.jsonwebtoken.Claims;
|
||||
import io.jsonwebtoken.Jws;
|
||||
import io.jsonwebtoken.Jwts;
|
||||
import io.jsonwebtoken.security.Keys;
|
||||
import jakarta.annotation.PostConstruct;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.Date;
|
||||
import javax.crypto.SecretKey;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
@Component
|
||||
public class JwtTokenProvider {
|
||||
|
||||
@Value("${jwt.secret}")
|
||||
private String secret;
|
||||
|
||||
@Value("${jwt.access-token-validity-in-ms}")
|
||||
private long accessTokenValidityInMs;
|
||||
|
||||
@Value("${jwt.refresh-token-validity-in-ms}")
|
||||
private long refreshTokenValidityInMs;
|
||||
|
||||
private SecretKey key;
|
||||
|
||||
@PostConstruct
|
||||
public void init() {
|
||||
// HS256용 SecretKey
|
||||
this.key = Keys.hmacShaKeyFor(secret.getBytes(StandardCharsets.UTF_8));
|
||||
}
|
||||
|
||||
public String createAccessToken(String subject) {
|
||||
return createToken(subject, accessTokenValidityInMs);
|
||||
}
|
||||
|
||||
public String createRefreshToken(String subject) {
|
||||
return createToken(subject, refreshTokenValidityInMs);
|
||||
}
|
||||
|
||||
private String createToken(String subject, long validityInMs) {
|
||||
Date now = new Date();
|
||||
Date expiry = new Date(now.getTime() + validityInMs);
|
||||
return Jwts.builder().subject(subject).issuedAt(now).expiration(expiry).signWith(key).compact();
|
||||
}
|
||||
|
||||
public String getSubject(String token) {
|
||||
var claims = parseClaims(token).getPayload();
|
||||
return claims.getSubject();
|
||||
}
|
||||
|
||||
public boolean isValidToken(String token) {
|
||||
try {
|
||||
Jws<Claims> claims = parseClaims(token);
|
||||
return !claims.getPayload().getExpiration().before(new Date());
|
||||
} catch (Exception e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private Jws<Claims> parseClaims(String token) {
|
||||
return Jwts.parser()
|
||||
.verifyWith(key) // SecretKey 타입
|
||||
.build()
|
||||
.parseSignedClaims(token);
|
||||
}
|
||||
|
||||
public long getRefreshTokenValidityInMs() {
|
||||
return refreshTokenValidityInMs;
|
||||
}
|
||||
}
|
||||
package com.kamco.cd.training.auth;
|
||||
|
||||
import io.jsonwebtoken.Claims;
|
||||
import io.jsonwebtoken.Jws;
|
||||
import io.jsonwebtoken.Jwts;
|
||||
import io.jsonwebtoken.security.Keys;
|
||||
import jakarta.annotation.PostConstruct;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.Date;
|
||||
import javax.crypto.SecretKey;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
@Component
|
||||
public class JwtTokenProvider {
|
||||
|
||||
@Value("${jwt.secret}")
|
||||
private String secret;
|
||||
|
||||
@Value("${jwt.access-token-validity-in-ms}")
|
||||
private long accessTokenValidityInMs;
|
||||
|
||||
@Value("${jwt.refresh-token-validity-in-ms}")
|
||||
private long refreshTokenValidityInMs;
|
||||
|
||||
private SecretKey key;
|
||||
|
||||
@PostConstruct
|
||||
public void init() {
|
||||
// HS256용 SecretKey
|
||||
this.key = Keys.hmacShaKeyFor(secret.getBytes(StandardCharsets.UTF_8));
|
||||
}
|
||||
|
||||
public String createAccessToken(String subject) {
|
||||
return createToken(subject, accessTokenValidityInMs);
|
||||
}
|
||||
|
||||
public String createRefreshToken(String subject) {
|
||||
return createToken(subject, refreshTokenValidityInMs);
|
||||
}
|
||||
|
||||
private String createToken(String subject, long validityInMs) {
|
||||
Date now = new Date();
|
||||
Date expiry = new Date(now.getTime() + validityInMs);
|
||||
return Jwts.builder().subject(subject).issuedAt(now).expiration(expiry).signWith(key).compact();
|
||||
}
|
||||
|
||||
public String getSubject(String token) {
|
||||
var claims = parseClaims(token).getPayload();
|
||||
return claims.getSubject();
|
||||
}
|
||||
|
||||
public boolean isValidToken(String token) {
|
||||
try {
|
||||
Jws<Claims> claims = parseClaims(token);
|
||||
return !claims.getPayload().getExpiration().before(new Date());
|
||||
} catch (Exception e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private Jws<Claims> parseClaims(String token) {
|
||||
return Jwts.parser()
|
||||
.verifyWith(key) // SecretKey 타입
|
||||
.build()
|
||||
.parseSignedClaims(token);
|
||||
}
|
||||
|
||||
public long getRefreshTokenValidityInMs() {
|
||||
return refreshTokenValidityInMs;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,299 +1,299 @@
|
||||
package com.kamco.cd.training.code;
|
||||
|
||||
import com.kamco.cd.training.code.dto.CommonCodeDto;
|
||||
import com.kamco.cd.training.code.service.CommonCodeService;
|
||||
import com.kamco.cd.training.common.utils.CommonCodeUtil;
|
||||
import com.kamco.cd.training.common.utils.enums.CodeDto;
|
||||
import com.kamco.cd.training.config.api.ApiResponseDto;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.media.Content;
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import io.swagger.v3.oas.annotations.responses.ApiResponse;
|
||||
import io.swagger.v3.oas.annotations.responses.ApiResponses;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import jakarta.validation.Valid;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
@Tag(name = "공통코드 관리", description = "공통코드 관리 API")
|
||||
@RestController
|
||||
@RequiredArgsConstructor
|
||||
@RequestMapping("/api/code")
|
||||
public class CommonCodeApiController {
|
||||
|
||||
private final CommonCodeService commonCodeService;
|
||||
private final CommonCodeUtil commonCodeUtil;
|
||||
|
||||
@Operation(summary = "목록 조회", description = "모든 공통코드 조회")
|
||||
@ApiResponses(
|
||||
value = {
|
||||
@ApiResponse(
|
||||
responseCode = "200",
|
||||
description = "조회 성공",
|
||||
content =
|
||||
@Content(
|
||||
mediaType = "application/json",
|
||||
schema = @Schema(implementation = CommonCodeDto.Basic.class))),
|
||||
@ApiResponse(responseCode = "404", description = "코드를 찾을 수 없음", content = @Content),
|
||||
@ApiResponse(responseCode = "500", description = "서버 오류", content = @Content)
|
||||
})
|
||||
@GetMapping
|
||||
public ApiResponseDto<List<CommonCodeDto.Basic>> getFindAll() {
|
||||
return ApiResponseDto.createOK(commonCodeService.getFindAll());
|
||||
}
|
||||
|
||||
@Operation(summary = "단건 조회", description = "단건 조회")
|
||||
@ApiResponses(
|
||||
value = {
|
||||
@ApiResponse(
|
||||
responseCode = "200",
|
||||
description = "조회 성공",
|
||||
content =
|
||||
@Content(
|
||||
mediaType = "application/json",
|
||||
schema = @Schema(implementation = CommonCodeDto.Basic.class))),
|
||||
@ApiResponse(responseCode = "404", description = "코드를 찾을 수 없음", content = @Content),
|
||||
@ApiResponse(responseCode = "500", description = "서버 오류", content = @Content)
|
||||
})
|
||||
@GetMapping("/{id}")
|
||||
public ApiResponseDto<CommonCodeDto.Basic> getOneById(
|
||||
@io.swagger.v3.oas.annotations.parameters.RequestBody(description = "단건 조회", required = true)
|
||||
@PathVariable
|
||||
Long id) {
|
||||
return ApiResponseDto.ok(commonCodeService.getOneById(id));
|
||||
}
|
||||
|
||||
@Operation(summary = "저장", description = "공통코드를 저장 합니다.")
|
||||
@ApiResponses(
|
||||
value = {
|
||||
@ApiResponse(
|
||||
responseCode = "201",
|
||||
description = "공통코드 저장 성공",
|
||||
content =
|
||||
@Content(
|
||||
mediaType = "application/json",
|
||||
schema = @Schema(implementation = Long.class))),
|
||||
@ApiResponse(responseCode = "400", description = "잘못된 요청 데이터", content = @Content),
|
||||
@ApiResponse(responseCode = "404", description = "코드를 찾을 수 없음", content = @Content),
|
||||
@ApiResponse(responseCode = "500", description = "서버 오류", content = @Content)
|
||||
})
|
||||
@PostMapping
|
||||
public ApiResponseDto<ApiResponseDto.ResponseObj> save(
|
||||
@io.swagger.v3.oas.annotations.parameters.RequestBody(
|
||||
description = "공통코드 생성 요청 정보",
|
||||
required = true,
|
||||
content =
|
||||
@Content(
|
||||
mediaType = "application/json",
|
||||
schema = @Schema(implementation = CommonCodeDto.AddReq.class)))
|
||||
@RequestBody
|
||||
@Valid
|
||||
CommonCodeDto.AddReq req) {
|
||||
return ApiResponseDto.okObject(commonCodeService.save(req));
|
||||
}
|
||||
|
||||
@Operation(summary = "수정", description = "공통코드를 수정 합니다.")
|
||||
@ApiResponses(
|
||||
value = {
|
||||
@ApiResponse(
|
||||
responseCode = "204",
|
||||
description = "공통코드 수정 성공",
|
||||
content =
|
||||
@Content(
|
||||
mediaType = "application/json",
|
||||
schema = @Schema(implementation = Long.class))),
|
||||
@ApiResponse(responseCode = "400", description = "잘못된 요청 데이터", content = @Content),
|
||||
@ApiResponse(responseCode = "404", description = "코드를 찾을 수 없음", content = @Content),
|
||||
@ApiResponse(responseCode = "500", description = "서버 오류", content = @Content)
|
||||
})
|
||||
@PutMapping("/{id}")
|
||||
public ApiResponseDto<ApiResponseDto.ResponseObj> update(
|
||||
@io.swagger.v3.oas.annotations.parameters.RequestBody(
|
||||
description = "공통코드 수정 요청 정보",
|
||||
required = true,
|
||||
content =
|
||||
@Content(
|
||||
mediaType = "application/json",
|
||||
schema = @Schema(implementation = CommonCodeDto.ModifyReq.class)))
|
||||
@PathVariable
|
||||
Long id,
|
||||
@RequestBody @Valid CommonCodeDto.ModifyReq req) {
|
||||
return ApiResponseDto.okObject(commonCodeService.update(id, req));
|
||||
}
|
||||
|
||||
@Operation(summary = "삭제", description = "공통코드를 삭제 합니다.")
|
||||
@ApiResponses(
|
||||
value = {
|
||||
@ApiResponse(
|
||||
responseCode = "204",
|
||||
description = "공통코드 삭제 성공",
|
||||
content =
|
||||
@Content(
|
||||
mediaType = "application/json",
|
||||
schema = @Schema(implementation = Long.class))),
|
||||
@ApiResponse(responseCode = "400", description = "잘못된 요청 데이터", content = @Content),
|
||||
@ApiResponse(responseCode = "404", description = "코드를 찾을 수 없음", content = @Content),
|
||||
@ApiResponse(responseCode = "500", description = "서버 오류", content = @Content)
|
||||
})
|
||||
@DeleteMapping("/{id}")
|
||||
public ApiResponseDto<ApiResponseDto.ResponseObj> remove(
|
||||
@io.swagger.v3.oas.annotations.parameters.RequestBody(
|
||||
description = "공통코드 삭제 요청 정보",
|
||||
required = true)
|
||||
@PathVariable
|
||||
Long id) {
|
||||
return ApiResponseDto.okObject(commonCodeService.removeCode(id));
|
||||
}
|
||||
|
||||
@Operation(summary = "순서 변경", description = "공통코드 순서를 변경 합니다.")
|
||||
@ApiResponses(
|
||||
value = {
|
||||
@ApiResponse(
|
||||
responseCode = "204",
|
||||
description = "공통코드 순서 변경 성공",
|
||||
content =
|
||||
@Content(
|
||||
mediaType = "application/json",
|
||||
schema = @Schema(implementation = Long.class))),
|
||||
@ApiResponse(responseCode = "400", description = "잘못된 요청 데이터", content = @Content),
|
||||
@ApiResponse(responseCode = "404", description = "코드를 찾을 수 없음", content = @Content),
|
||||
@ApiResponse(responseCode = "500", description = "서버 오류", content = @Content)
|
||||
})
|
||||
@PutMapping("/order")
|
||||
public ApiResponseDto<ApiResponseDto.ResponseObj> updateOrder(
|
||||
@io.swagger.v3.oas.annotations.parameters.RequestBody(
|
||||
description = "공통코드 순서변경 요청 정보",
|
||||
required = true,
|
||||
content =
|
||||
@Content(
|
||||
mediaType = "application/json",
|
||||
schema = @Schema(implementation = CommonCodeDto.OrderReq.class)))
|
||||
@RequestBody
|
||||
@Valid
|
||||
CommonCodeDto.OrderReq req) {
|
||||
|
||||
return ApiResponseDto.okObject(commonCodeService.updateOrder(req));
|
||||
}
|
||||
|
||||
@Operation(summary = "code 기반 조회", description = "code 기반 조회")
|
||||
@ApiResponses(
|
||||
value = {
|
||||
@ApiResponse(
|
||||
responseCode = "200",
|
||||
description = "code 기반 조회 성공",
|
||||
content =
|
||||
@Content(
|
||||
mediaType = "application/json",
|
||||
schema = @Schema(implementation = Long.class))),
|
||||
@ApiResponse(responseCode = "400", description = "잘못된 요청 데이터", content = @Content),
|
||||
@ApiResponse(responseCode = "404", description = "코드를 찾을 수 없음", content = @Content),
|
||||
@ApiResponse(responseCode = "500", description = "서버 오류", content = @Content)
|
||||
})
|
||||
@GetMapping("/used")
|
||||
public ApiResponseDto<List<CommonCodeDto.Basic>> getByCode(
|
||||
@io.swagger.v3.oas.annotations.parameters.RequestBody(
|
||||
description = "공통코드 순서변경 요청 정보",
|
||||
required = true)
|
||||
@RequestParam
|
||||
String code) {
|
||||
return ApiResponseDto.ok(commonCodeService.findByCode(code));
|
||||
}
|
||||
|
||||
@Operation(summary = "변화탐지 분류 코드 목록", description = "변화탐지 분류 코드 목록(공통코드 기반)")
|
||||
@GetMapping("/clazz")
|
||||
public ApiResponseDto<List<CommonCodeDto.Clazzes>> getClasses() {
|
||||
|
||||
// List<Clazzes> list =
|
||||
// Arrays.stream(DetectionClassification.values())
|
||||
// .sorted(Comparator.comparingInt(DetectionClassification::getOrder))
|
||||
// .map(Clazzes::new)
|
||||
// .toList();
|
||||
|
||||
// 변화탐지 clazz API : enum -> 공통코드로 변경
|
||||
List<CommonCodeDto.Clazzes> list =
|
||||
commonCodeUtil.getChildCodesByParentCode("0000").stream()
|
||||
.map(
|
||||
child ->
|
||||
new CommonCodeDto.Clazzes(
|
||||
child.getCode(), child.getName(), child.getOrder(), child.getProps2()))
|
||||
.toList();
|
||||
|
||||
return ApiResponseDto.ok(list);
|
||||
}
|
||||
|
||||
@Operation(summary = "공통코드 중복여부 체크", description = "공통코드 중복여부 체크")
|
||||
@ApiResponses(
|
||||
value = {
|
||||
@ApiResponse(
|
||||
responseCode = "200",
|
||||
description = "조회 성공",
|
||||
content =
|
||||
@Content(
|
||||
mediaType = "application/json",
|
||||
schema = @Schema(implementation = CommonCodeDto.Basic.class))),
|
||||
@ApiResponse(responseCode = "404", description = "코드를 찾을 수 없음", content = @Content),
|
||||
@ApiResponse(responseCode = "500", description = "서버 오류", content = @Content)
|
||||
})
|
||||
@GetMapping("/check-duplicate")
|
||||
public ApiResponseDto<ApiResponseDto.ResponseObj> getCodeCheckDuplicate(
|
||||
@io.swagger.v3.oas.annotations.parameters.RequestBody(description = "단건 조회", required = true)
|
||||
@RequestParam(required = false)
|
||||
Long parentId,
|
||||
@RequestParam String code) {
|
||||
return ApiResponseDto.okObject(commonCodeService.getCodeCheckDuplicate(parentId, code));
|
||||
}
|
||||
|
||||
@Operation(summary = "코드 조회", description = "코드 리스트 조회")
|
||||
@ApiResponses(
|
||||
value = {
|
||||
@ApiResponse(
|
||||
responseCode = "200",
|
||||
description = "코드 조회 성공",
|
||||
content =
|
||||
@Content(
|
||||
mediaType = "application/json",
|
||||
schema = @Schema(implementation = CodeDto.class))),
|
||||
@ApiResponse(responseCode = "500", description = "서버 오류", content = @Content)
|
||||
})
|
||||
@GetMapping("/type/codes")
|
||||
public ApiResponseDto<Map<String, List<CodeDto>>> getTypeCodes() {
|
||||
return ApiResponseDto.ok(commonCodeService.getTypeCodes());
|
||||
}
|
||||
|
||||
@Operation(summary = "코드 단건 조회", description = "코드 조회")
|
||||
@ApiResponses(
|
||||
value = {
|
||||
@ApiResponse(
|
||||
responseCode = "200",
|
||||
description = "코드 조회 성공",
|
||||
content =
|
||||
@Content(
|
||||
mediaType = "application/json",
|
||||
schema = @Schema(implementation = CodeDto.class))),
|
||||
@ApiResponse(responseCode = "500", description = "서버 오류", content = @Content)
|
||||
})
|
||||
@GetMapping("/type/{type}")
|
||||
public ApiResponseDto<List<CodeDto>> getTypeCode(@PathVariable String type) {
|
||||
return ApiResponseDto.ok(commonCodeService.getTypeCode(type));
|
||||
}
|
||||
|
||||
@Operation(summary = "캐시 초기화", description = "공통코드 캐시를 초기화합니다.")
|
||||
@ApiResponses(
|
||||
value = {
|
||||
@ApiResponse(
|
||||
responseCode = "200",
|
||||
description = "캐시 초기화 성공",
|
||||
content =
|
||||
@Content(
|
||||
mediaType = "application/json",
|
||||
schema = @Schema(implementation = String.class))),
|
||||
@ApiResponse(responseCode = "500", description = "서버 오류", content = @Content)
|
||||
})
|
||||
@PostMapping("/cache/refresh")
|
||||
public ApiResponseDto<String> refreshCommonCodeCache() {
|
||||
commonCodeService.refresh();
|
||||
return ApiResponseDto.ok("공통코드 캐시가 초기화 되었습니다.");
|
||||
}
|
||||
}
|
||||
package com.kamco.cd.training.code;
|
||||
|
||||
import com.kamco.cd.training.code.dto.CommonCodeDto;
|
||||
import com.kamco.cd.training.code.service.CommonCodeService;
|
||||
import com.kamco.cd.training.common.utils.CommonCodeUtil;
|
||||
import com.kamco.cd.training.common.utils.enums.CodeDto;
|
||||
import com.kamco.cd.training.config.api.ApiResponseDto;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.media.Content;
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import io.swagger.v3.oas.annotations.responses.ApiResponse;
|
||||
import io.swagger.v3.oas.annotations.responses.ApiResponses;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import jakarta.validation.Valid;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
@Tag(name = "공통코드 관리", description = "공통코드 관리 API")
|
||||
@RestController
|
||||
@RequiredArgsConstructor
|
||||
@RequestMapping("/api/common-code")
|
||||
public class CommonCodeApiController {
|
||||
|
||||
private final CommonCodeService commonCodeService;
|
||||
private final CommonCodeUtil commonCodeUtil;
|
||||
|
||||
@Operation(summary = "목록 조회", description = "모든 공통코드 조회")
|
||||
@ApiResponses(
|
||||
value = {
|
||||
@ApiResponse(
|
||||
responseCode = "200",
|
||||
description = "조회 성공",
|
||||
content =
|
||||
@Content(
|
||||
mediaType = "application/json",
|
||||
schema = @Schema(implementation = CommonCodeDto.Basic.class))),
|
||||
@ApiResponse(responseCode = "404", description = "코드를 찾을 수 없음", content = @Content),
|
||||
@ApiResponse(responseCode = "500", description = "서버 오류", content = @Content)
|
||||
})
|
||||
@GetMapping
|
||||
public ApiResponseDto<List<CommonCodeDto.Basic>> getFindAll() {
|
||||
return ApiResponseDto.createOK(commonCodeService.getFindAll());
|
||||
}
|
||||
|
||||
@Operation(summary = "단건 조회", description = "단건 조회")
|
||||
@ApiResponses(
|
||||
value = {
|
||||
@ApiResponse(
|
||||
responseCode = "200",
|
||||
description = "조회 성공",
|
||||
content =
|
||||
@Content(
|
||||
mediaType = "application/json",
|
||||
schema = @Schema(implementation = CommonCodeDto.Basic.class))),
|
||||
@ApiResponse(responseCode = "404", description = "코드를 찾을 수 없음", content = @Content),
|
||||
@ApiResponse(responseCode = "500", description = "서버 오류", content = @Content)
|
||||
})
|
||||
@GetMapping("/{id}")
|
||||
public ApiResponseDto<CommonCodeDto.Basic> getOneById(
|
||||
@io.swagger.v3.oas.annotations.parameters.RequestBody(description = "단건 조회", required = true)
|
||||
@PathVariable
|
||||
Long id) {
|
||||
return ApiResponseDto.ok(commonCodeService.getOneById(id));
|
||||
}
|
||||
|
||||
@Operation(summary = "저장", description = "공통코드를 저장 합니다.")
|
||||
@ApiResponses(
|
||||
value = {
|
||||
@ApiResponse(
|
||||
responseCode = "201",
|
||||
description = "공통코드 저장 성공",
|
||||
content =
|
||||
@Content(
|
||||
mediaType = "application/json",
|
||||
schema = @Schema(implementation = Long.class))),
|
||||
@ApiResponse(responseCode = "400", description = "잘못된 요청 데이터", content = @Content),
|
||||
@ApiResponse(responseCode = "404", description = "코드를 찾을 수 없음", content = @Content),
|
||||
@ApiResponse(responseCode = "500", description = "서버 오류", content = @Content)
|
||||
})
|
||||
@PostMapping
|
||||
public ApiResponseDto<ApiResponseDto.ResponseObj> save(
|
||||
@io.swagger.v3.oas.annotations.parameters.RequestBody(
|
||||
description = "공통코드 생성 요청 정보",
|
||||
required = true,
|
||||
content =
|
||||
@Content(
|
||||
mediaType = "application/json",
|
||||
schema = @Schema(implementation = CommonCodeDto.AddReq.class)))
|
||||
@RequestBody
|
||||
@Valid
|
||||
CommonCodeDto.AddReq req) {
|
||||
return ApiResponseDto.okObject(commonCodeService.save(req));
|
||||
}
|
||||
|
||||
@Operation(summary = "수정", description = "공통코드를 수정 합니다.")
|
||||
@ApiResponses(
|
||||
value = {
|
||||
@ApiResponse(
|
||||
responseCode = "204",
|
||||
description = "공통코드 수정 성공",
|
||||
content =
|
||||
@Content(
|
||||
mediaType = "application/json",
|
||||
schema = @Schema(implementation = Long.class))),
|
||||
@ApiResponse(responseCode = "400", description = "잘못된 요청 데이터", content = @Content),
|
||||
@ApiResponse(responseCode = "404", description = "코드를 찾을 수 없음", content = @Content),
|
||||
@ApiResponse(responseCode = "500", description = "서버 오류", content = @Content)
|
||||
})
|
||||
@PutMapping("/{id}")
|
||||
public ApiResponseDto<ApiResponseDto.ResponseObj> update(
|
||||
@io.swagger.v3.oas.annotations.parameters.RequestBody(
|
||||
description = "공통코드 수정 요청 정보",
|
||||
required = true,
|
||||
content =
|
||||
@Content(
|
||||
mediaType = "application/json",
|
||||
schema = @Schema(implementation = CommonCodeDto.ModifyReq.class)))
|
||||
@PathVariable
|
||||
Long id,
|
||||
@RequestBody @Valid CommonCodeDto.ModifyReq req) {
|
||||
return ApiResponseDto.okObject(commonCodeService.update(id, req));
|
||||
}
|
||||
|
||||
@Operation(summary = "삭제", description = "공통코드를 삭제 합니다.")
|
||||
@ApiResponses(
|
||||
value = {
|
||||
@ApiResponse(
|
||||
responseCode = "204",
|
||||
description = "공통코드 삭제 성공",
|
||||
content =
|
||||
@Content(
|
||||
mediaType = "application/json",
|
||||
schema = @Schema(implementation = Long.class))),
|
||||
@ApiResponse(responseCode = "400", description = "잘못된 요청 데이터", content = @Content),
|
||||
@ApiResponse(responseCode = "404", description = "코드를 찾을 수 없음", content = @Content),
|
||||
@ApiResponse(responseCode = "500", description = "서버 오류", content = @Content)
|
||||
})
|
||||
@DeleteMapping("/{id}")
|
||||
public ApiResponseDto<ApiResponseDto.ResponseObj> remove(
|
||||
@io.swagger.v3.oas.annotations.parameters.RequestBody(
|
||||
description = "공통코드 삭제 요청 정보",
|
||||
required = true)
|
||||
@PathVariable
|
||||
Long id) {
|
||||
return ApiResponseDto.okObject(commonCodeService.removeCode(id));
|
||||
}
|
||||
|
||||
@Operation(summary = "순서 변경", description = "공통코드 순서를 변경 합니다.")
|
||||
@ApiResponses(
|
||||
value = {
|
||||
@ApiResponse(
|
||||
responseCode = "204",
|
||||
description = "공통코드 순서 변경 성공",
|
||||
content =
|
||||
@Content(
|
||||
mediaType = "application/json",
|
||||
schema = @Schema(implementation = Long.class))),
|
||||
@ApiResponse(responseCode = "400", description = "잘못된 요청 데이터", content = @Content),
|
||||
@ApiResponse(responseCode = "404", description = "코드를 찾을 수 없음", content = @Content),
|
||||
@ApiResponse(responseCode = "500", description = "서버 오류", content = @Content)
|
||||
})
|
||||
@PutMapping("/order")
|
||||
public ApiResponseDto<ApiResponseDto.ResponseObj> updateOrder(
|
||||
@io.swagger.v3.oas.annotations.parameters.RequestBody(
|
||||
description = "공통코드 순서변경 요청 정보",
|
||||
required = true,
|
||||
content =
|
||||
@Content(
|
||||
mediaType = "application/json",
|
||||
schema = @Schema(implementation = CommonCodeDto.OrderReq.class)))
|
||||
@RequestBody
|
||||
@Valid
|
||||
CommonCodeDto.OrderReq req) {
|
||||
|
||||
return ApiResponseDto.okObject(commonCodeService.updateOrder(req));
|
||||
}
|
||||
|
||||
@Operation(summary = "code 기반 조회", description = "code 기반 조회")
|
||||
@ApiResponses(
|
||||
value = {
|
||||
@ApiResponse(
|
||||
responseCode = "200",
|
||||
description = "code 기반 조회 성공",
|
||||
content =
|
||||
@Content(
|
||||
mediaType = "application/json",
|
||||
schema = @Schema(implementation = Long.class))),
|
||||
@ApiResponse(responseCode = "400", description = "잘못된 요청 데이터", content = @Content),
|
||||
@ApiResponse(responseCode = "404", description = "코드를 찾을 수 없음", content = @Content),
|
||||
@ApiResponse(responseCode = "500", description = "서버 오류", content = @Content)
|
||||
})
|
||||
@GetMapping("/used")
|
||||
public ApiResponseDto<List<CommonCodeDto.Basic>> getByCode(
|
||||
@io.swagger.v3.oas.annotations.parameters.RequestBody(
|
||||
description = "공통코드 순서변경 요청 정보",
|
||||
required = true)
|
||||
@RequestParam
|
||||
String code) {
|
||||
return ApiResponseDto.ok(commonCodeService.findByCode(code));
|
||||
}
|
||||
|
||||
@Operation(summary = "변화탐지 분류 코드 목록", description = "변화탐지 분류 코드 목록(공통코드 기반)")
|
||||
@GetMapping("/clazz")
|
||||
public ApiResponseDto<List<CommonCodeDto.Clazzes>> getClasses() {
|
||||
|
||||
// List<Clazzes> list =
|
||||
// Arrays.stream(DetectionClassification.values())
|
||||
// .sorted(Comparator.comparingInt(DetectionClassification::getOrder))
|
||||
// .map(Clazzes::new)
|
||||
// .toList();
|
||||
|
||||
// 변화탐지 clazz API : enum -> 공통코드로 변경
|
||||
List<CommonCodeDto.Clazzes> list =
|
||||
commonCodeUtil.getChildCodesByParentCode("0000").stream()
|
||||
.map(
|
||||
child ->
|
||||
new CommonCodeDto.Clazzes(
|
||||
child.getCode(), child.getName(), child.getOrder(), child.getProps2()))
|
||||
.toList();
|
||||
|
||||
return ApiResponseDto.ok(list);
|
||||
}
|
||||
|
||||
@Operation(summary = "공통코드 중복여부 체크", description = "공통코드 중복여부 체크")
|
||||
@ApiResponses(
|
||||
value = {
|
||||
@ApiResponse(
|
||||
responseCode = "200",
|
||||
description = "조회 성공",
|
||||
content =
|
||||
@Content(
|
||||
mediaType = "application/json",
|
||||
schema = @Schema(implementation = CommonCodeDto.Basic.class))),
|
||||
@ApiResponse(responseCode = "404", description = "코드를 찾을 수 없음", content = @Content),
|
||||
@ApiResponse(responseCode = "500", description = "서버 오류", content = @Content)
|
||||
})
|
||||
@GetMapping("/check-duplicate")
|
||||
public ApiResponseDto<ApiResponseDto.ResponseObj> getCodeCheckDuplicate(
|
||||
@io.swagger.v3.oas.annotations.parameters.RequestBody(description = "단건 조회", required = true)
|
||||
@RequestParam(required = false)
|
||||
Long parentId,
|
||||
@RequestParam String code) {
|
||||
return ApiResponseDto.okObject(commonCodeService.getCodeCheckDuplicate(parentId, code));
|
||||
}
|
||||
|
||||
@Operation(summary = "코드 조회", description = "코드 리스트 조회")
|
||||
@ApiResponses(
|
||||
value = {
|
||||
@ApiResponse(
|
||||
responseCode = "200",
|
||||
description = "코드 조회 성공",
|
||||
content =
|
||||
@Content(
|
||||
mediaType = "application/json",
|
||||
schema = @Schema(implementation = CodeDto.class))),
|
||||
@ApiResponse(responseCode = "500", description = "서버 오류", content = @Content)
|
||||
})
|
||||
@GetMapping("/type/codes")
|
||||
public ApiResponseDto<Map<String, List<CodeDto>>> getTypeCodes() {
|
||||
return ApiResponseDto.ok(commonCodeService.getTypeCodes());
|
||||
}
|
||||
|
||||
@Operation(summary = "코드 단건 조회", description = "코드 조회")
|
||||
@ApiResponses(
|
||||
value = {
|
||||
@ApiResponse(
|
||||
responseCode = "200",
|
||||
description = "코드 조회 성공",
|
||||
content =
|
||||
@Content(
|
||||
mediaType = "application/json",
|
||||
schema = @Schema(implementation = CodeDto.class))),
|
||||
@ApiResponse(responseCode = "500", description = "서버 오류", content = @Content)
|
||||
})
|
||||
@GetMapping("/type/{type}")
|
||||
public ApiResponseDto<List<CodeDto>> getTypeCode(@PathVariable String type) {
|
||||
return ApiResponseDto.ok(commonCodeService.getTypeCode(type));
|
||||
}
|
||||
|
||||
@Operation(summary = "캐시 초기화", description = "공통코드 캐시를 초기화합니다.")
|
||||
@ApiResponses(
|
||||
value = {
|
||||
@ApiResponse(
|
||||
responseCode = "200",
|
||||
description = "캐시 초기화 성공",
|
||||
content =
|
||||
@Content(
|
||||
mediaType = "application/json",
|
||||
schema = @Schema(implementation = String.class))),
|
||||
@ApiResponse(responseCode = "500", description = "서버 오류", content = @Content)
|
||||
})
|
||||
@PostMapping("/cache/refresh")
|
||||
public ApiResponseDto<String> refreshCommonCodeCache() {
|
||||
commonCodeService.refresh();
|
||||
return ApiResponseDto.ok("공통코드 캐시가 초기화 되었습니다.");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,179 +1,179 @@
|
||||
package com.kamco.cd.training.code.dto;
|
||||
|
||||
import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
|
||||
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
|
||||
import com.kamco.cd.training.common.utils.html.HtmlEscapeDeserializer;
|
||||
import com.kamco.cd.training.common.utils.html.HtmlUnescapeSerializer;
|
||||
import com.kamco.cd.training.common.utils.interfaces.JsonFormatDttm;
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import jakarta.validation.constraints.NotEmpty;
|
||||
import jakarta.validation.constraints.NotNull;
|
||||
import java.time.ZonedDateTime;
|
||||
import java.util.List;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Getter;
|
||||
import lombok.NoArgsConstructor;
|
||||
import lombok.Setter;
|
||||
import org.springframework.data.domain.PageRequest;
|
||||
import org.springframework.data.domain.Pageable;
|
||||
import org.springframework.data.domain.Sort;
|
||||
|
||||
public class CommonCodeDto {
|
||||
|
||||
@Schema(name = "CodeAddReq", description = "공통코드 저장 정보")
|
||||
@Getter
|
||||
@Setter
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public static class AddReq {
|
||||
|
||||
@NotEmpty private String code;
|
||||
@NotEmpty private String name;
|
||||
private String description;
|
||||
private int order;
|
||||
private boolean used;
|
||||
private Long parentId;
|
||||
|
||||
@JsonDeserialize(using = HtmlEscapeDeserializer.class)
|
||||
private String props1;
|
||||
|
||||
@JsonDeserialize(using = HtmlEscapeDeserializer.class)
|
||||
private String props2;
|
||||
|
||||
@JsonDeserialize(using = HtmlEscapeDeserializer.class)
|
||||
private String props3;
|
||||
}
|
||||
|
||||
@Schema(name = "CodeModifyReq", description = "공통코드 수정 정보")
|
||||
@Getter
|
||||
@Setter
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public static class ModifyReq {
|
||||
@NotEmpty private String name;
|
||||
private String description;
|
||||
private boolean used;
|
||||
|
||||
@JsonDeserialize(using = HtmlEscapeDeserializer.class)
|
||||
private String props1;
|
||||
|
||||
@JsonDeserialize(using = HtmlEscapeDeserializer.class)
|
||||
private String props2;
|
||||
|
||||
@JsonDeserialize(using = HtmlEscapeDeserializer.class)
|
||||
private String props3;
|
||||
}
|
||||
|
||||
@Schema(name = "CodeOrderReq", description = "공통코드 순서 변경 정보")
|
||||
@Getter
|
||||
@Setter
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public static class OrderReq {
|
||||
@NotNull private Long id;
|
||||
@NotNull private Integer order;
|
||||
}
|
||||
|
||||
@Schema(name = "CommonCode Basic", description = "공통코드 기본 정보")
|
||||
@Getter
|
||||
public static class Basic {
|
||||
|
||||
private Long id;
|
||||
private String code;
|
||||
private String description;
|
||||
private String name;
|
||||
private Integer order;
|
||||
private Boolean used;
|
||||
private Boolean deleted;
|
||||
private List<CommonCodeDto.Basic> children;
|
||||
|
||||
@JsonFormatDttm private ZonedDateTime createdDttm;
|
||||
|
||||
@JsonFormatDttm private ZonedDateTime updatedDttm;
|
||||
|
||||
@JsonSerialize(using = HtmlUnescapeSerializer.class)
|
||||
private String props1;
|
||||
|
||||
@JsonSerialize(using = HtmlUnescapeSerializer.class)
|
||||
private String props2;
|
||||
|
||||
@JsonSerialize(using = HtmlUnescapeSerializer.class)
|
||||
private String props3;
|
||||
|
||||
@JsonFormatDttm private ZonedDateTime deletedDttm;
|
||||
|
||||
public Basic(
|
||||
Long id,
|
||||
String code,
|
||||
String description,
|
||||
String name,
|
||||
Integer order,
|
||||
Boolean used,
|
||||
Boolean deleted,
|
||||
List<CommonCodeDto.Basic> children,
|
||||
ZonedDateTime createdDttm,
|
||||
ZonedDateTime updatedDttm,
|
||||
String props1,
|
||||
String props2,
|
||||
String props3,
|
||||
ZonedDateTime deletedDttm) {
|
||||
this.id = id;
|
||||
this.code = code;
|
||||
this.description = description;
|
||||
this.name = name;
|
||||
this.order = order;
|
||||
this.used = used;
|
||||
this.deleted = deleted;
|
||||
this.children = children;
|
||||
this.createdDttm = createdDttm;
|
||||
this.updatedDttm = updatedDttm;
|
||||
this.props1 = props1;
|
||||
this.props2 = props2;
|
||||
this.props3 = props3;
|
||||
this.deletedDttm = deletedDttm;
|
||||
}
|
||||
}
|
||||
|
||||
@Schema(name = "SearchReq", description = "검색 요청")
|
||||
@Getter
|
||||
@Setter
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public static class SearchReq {
|
||||
|
||||
// 검색 조건
|
||||
private String name;
|
||||
|
||||
// 페이징 파라미터
|
||||
private int page = 0;
|
||||
private int size = 20;
|
||||
private String sort;
|
||||
|
||||
public Pageable toPageable() {
|
||||
if (sort != null && !sort.isEmpty()) {
|
||||
String[] sortParams = sort.split(",");
|
||||
String property = sortParams[0];
|
||||
Sort.Direction direction =
|
||||
sortParams.length > 1 ? Sort.Direction.fromString(sortParams[1]) : Sort.Direction.ASC;
|
||||
return PageRequest.of(page, size, Sort.by(direction, property));
|
||||
}
|
||||
return PageRequest.of(page, size);
|
||||
}
|
||||
}
|
||||
|
||||
@Getter
|
||||
public static class Clazzes {
|
||||
|
||||
private String code;
|
||||
private String name;
|
||||
private Integer order;
|
||||
private String color;
|
||||
|
||||
public Clazzes(String code, String name, Integer order, String color) {
|
||||
this.code = code;
|
||||
this.name = name;
|
||||
this.order = order;
|
||||
this.color = color;
|
||||
}
|
||||
}
|
||||
}
|
||||
package com.kamco.cd.training.code.dto;
|
||||
|
||||
import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
|
||||
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
|
||||
import com.kamco.cd.training.common.utils.html.HtmlEscapeDeserializer;
|
||||
import com.kamco.cd.training.common.utils.html.HtmlUnescapeSerializer;
|
||||
import com.kamco.cd.training.common.utils.interfaces.JsonFormatDttm;
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import jakarta.validation.constraints.NotEmpty;
|
||||
import jakarta.validation.constraints.NotNull;
|
||||
import java.time.ZonedDateTime;
|
||||
import java.util.List;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Getter;
|
||||
import lombok.NoArgsConstructor;
|
||||
import lombok.Setter;
|
||||
import org.springframework.data.domain.PageRequest;
|
||||
import org.springframework.data.domain.Pageable;
|
||||
import org.springframework.data.domain.Sort;
|
||||
|
||||
public class CommonCodeDto {
|
||||
|
||||
@Schema(name = "CodeAddReq", description = "공통코드 저장 정보")
|
||||
@Getter
|
||||
@Setter
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public static class AddReq {
|
||||
|
||||
@NotEmpty private String code;
|
||||
@NotEmpty private String name;
|
||||
private String description;
|
||||
private int order;
|
||||
private boolean used;
|
||||
private Long parentId;
|
||||
|
||||
@JsonDeserialize(using = HtmlEscapeDeserializer.class)
|
||||
private String props1;
|
||||
|
||||
@JsonDeserialize(using = HtmlEscapeDeserializer.class)
|
||||
private String props2;
|
||||
|
||||
@JsonDeserialize(using = HtmlEscapeDeserializer.class)
|
||||
private String props3;
|
||||
}
|
||||
|
||||
@Schema(name = "CodeModifyReq", description = "공통코드 수정 정보")
|
||||
@Getter
|
||||
@Setter
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public static class ModifyReq {
|
||||
@NotEmpty private String name;
|
||||
private String description;
|
||||
private boolean used;
|
||||
|
||||
@JsonDeserialize(using = HtmlEscapeDeserializer.class)
|
||||
private String props1;
|
||||
|
||||
@JsonDeserialize(using = HtmlEscapeDeserializer.class)
|
||||
private String props2;
|
||||
|
||||
@JsonDeserialize(using = HtmlEscapeDeserializer.class)
|
||||
private String props3;
|
||||
}
|
||||
|
||||
@Schema(name = "CodeOrderReq", description = "공통코드 순서 변경 정보")
|
||||
@Getter
|
||||
@Setter
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public static class OrderReq {
|
||||
@NotNull private Long id;
|
||||
@NotNull private Integer order;
|
||||
}
|
||||
|
||||
@Schema(name = "CommonCode Basic", description = "공통코드 기본 정보")
|
||||
@Getter
|
||||
public static class Basic {
|
||||
|
||||
private Long id;
|
||||
private String code;
|
||||
private String description;
|
||||
private String name;
|
||||
private Integer order;
|
||||
private Boolean used;
|
||||
private Boolean deleted;
|
||||
private List<CommonCodeDto.Basic> children;
|
||||
|
||||
@JsonFormatDttm private ZonedDateTime createdDttm;
|
||||
|
||||
@JsonFormatDttm private ZonedDateTime updatedDttm;
|
||||
|
||||
@JsonSerialize(using = HtmlUnescapeSerializer.class)
|
||||
private String props1;
|
||||
|
||||
@JsonSerialize(using = HtmlUnescapeSerializer.class)
|
||||
private String props2;
|
||||
|
||||
@JsonSerialize(using = HtmlUnescapeSerializer.class)
|
||||
private String props3;
|
||||
|
||||
@JsonFormatDttm private ZonedDateTime deletedDttm;
|
||||
|
||||
public Basic(
|
||||
Long id,
|
||||
String code,
|
||||
String description,
|
||||
String name,
|
||||
Integer order,
|
||||
Boolean used,
|
||||
Boolean deleted,
|
||||
List<CommonCodeDto.Basic> children,
|
||||
ZonedDateTime createdDttm,
|
||||
ZonedDateTime updatedDttm,
|
||||
String props1,
|
||||
String props2,
|
||||
String props3,
|
||||
ZonedDateTime deletedDttm) {
|
||||
this.id = id;
|
||||
this.code = code;
|
||||
this.description = description;
|
||||
this.name = name;
|
||||
this.order = order;
|
||||
this.used = used;
|
||||
this.deleted = deleted;
|
||||
this.children = children;
|
||||
this.createdDttm = createdDttm;
|
||||
this.updatedDttm = updatedDttm;
|
||||
this.props1 = props1;
|
||||
this.props2 = props2;
|
||||
this.props3 = props3;
|
||||
this.deletedDttm = deletedDttm;
|
||||
}
|
||||
}
|
||||
|
||||
@Schema(name = "SearchReq", description = "검색 요청")
|
||||
@Getter
|
||||
@Setter
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public static class SearchReq {
|
||||
|
||||
// 검색 조건
|
||||
private String name;
|
||||
|
||||
// 페이징 파라미터
|
||||
private int page = 0;
|
||||
private int size = 20;
|
||||
private String sort;
|
||||
|
||||
public Pageable toPageable() {
|
||||
if (sort != null && !sort.isEmpty()) {
|
||||
String[] sortParams = sort.split(",");
|
||||
String property = sortParams[0];
|
||||
Sort.Direction direction =
|
||||
sortParams.length > 1 ? Sort.Direction.fromString(sortParams[1]) : Sort.Direction.ASC;
|
||||
return PageRequest.of(page, size, Sort.by(direction, property));
|
||||
}
|
||||
return PageRequest.of(page, size);
|
||||
}
|
||||
}
|
||||
|
||||
@Getter
|
||||
public static class Clazzes {
|
||||
|
||||
private String code;
|
||||
private String name;
|
||||
private Integer order;
|
||||
private String color;
|
||||
|
||||
public Clazzes(String code, String name, Integer order, String color) {
|
||||
this.code = code;
|
||||
this.name = name;
|
||||
this.order = order;
|
||||
this.color = color;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,155 +1,158 @@
|
||||
package com.kamco.cd.training.code.service;
|
||||
|
||||
import com.kamco.cd.training.code.dto.CommonCodeDto.AddReq;
|
||||
import com.kamco.cd.training.code.dto.CommonCodeDto.Basic;
|
||||
import com.kamco.cd.training.code.dto.CommonCodeDto.ModifyReq;
|
||||
import com.kamco.cd.training.code.dto.CommonCodeDto.OrderReq;
|
||||
import com.kamco.cd.training.common.utils.enums.CodeDto;
|
||||
import com.kamco.cd.training.common.utils.enums.Enums;
|
||||
import com.kamco.cd.training.config.api.ApiResponseDto;
|
||||
import com.kamco.cd.training.postgres.core.CommonCodeCoreService;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.cache.annotation.CacheEvict;
|
||||
import org.springframework.cache.annotation.Cacheable;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
// training 서버는 Redis 사용하지 않고 Spring Boot 메모리 캐시를 사용함
|
||||
// => org.springframework.cache.annotation.Cacheable
|
||||
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
@Transactional(readOnly = true)
|
||||
public class CommonCodeService {
|
||||
|
||||
private final CommonCodeCoreService commonCodeCoreService;
|
||||
|
||||
/**
|
||||
* 공통코드 목록 조회
|
||||
*
|
||||
* @return 모튼 코드 정보
|
||||
*/
|
||||
@Cacheable("trainCommonCodes")
|
||||
public List<Basic> getFindAll() {
|
||||
return commonCodeCoreService.findAll();
|
||||
}
|
||||
|
||||
/**
|
||||
* 공통코드 단건 조회
|
||||
*
|
||||
* @param id
|
||||
* @return 코드 아이디로 조회한 코드 정보
|
||||
*/
|
||||
public Basic getOneById(Long id) {
|
||||
return commonCodeCoreService.getOneById(id);
|
||||
}
|
||||
|
||||
/**
|
||||
* 공통코드 생성 요청
|
||||
*
|
||||
* @param req 생성요청 정보
|
||||
* @return 생성된 코드 id
|
||||
*/
|
||||
@Transactional
|
||||
@CacheEvict(value = "trainCommonCodes", allEntries = true)
|
||||
public ApiResponseDto.ResponseObj save(AddReq req) {
|
||||
return commonCodeCoreService.save(req);
|
||||
}
|
||||
|
||||
/**
|
||||
* 공통코드 수정 요청
|
||||
*
|
||||
* @param id 코드 아이디
|
||||
* @param req 수정요청 정보
|
||||
*/
|
||||
@Transactional
|
||||
@CacheEvict(value = "trainCommonCodes", allEntries = true)
|
||||
public ApiResponseDto.ResponseObj update(Long id, ModifyReq req) {
|
||||
return commonCodeCoreService.update(id, req);
|
||||
}
|
||||
|
||||
/**
|
||||
* 공통코드 삭제 처리
|
||||
*
|
||||
* @param id 코드 아이디
|
||||
*/
|
||||
@Transactional
|
||||
@CacheEvict(value = "trainCommonCodes", allEntries = true)
|
||||
public ApiResponseDto.ResponseObj removeCode(Long id) {
|
||||
return commonCodeCoreService.removeCode(id);
|
||||
}
|
||||
|
||||
/**
|
||||
* 공통코드 순서 변경
|
||||
*
|
||||
* @param req id, order 정보를 가진 List
|
||||
*/
|
||||
@Transactional
|
||||
@CacheEvict(value = "trainCommonCodes", allEntries = true)
|
||||
public ApiResponseDto.ResponseObj updateOrder(OrderReq req) {
|
||||
return commonCodeCoreService.updateOrder(req);
|
||||
}
|
||||
|
||||
/**
|
||||
* 코드기반 조회
|
||||
*
|
||||
* @param code 코드
|
||||
* @return 코드로 조회한 공통코드 정보
|
||||
*/
|
||||
public List<Basic> findByCode(String code) {
|
||||
return commonCodeCoreService.findByCode(code);
|
||||
}
|
||||
|
||||
/**
|
||||
* 중복 체크
|
||||
*
|
||||
* @param parentId
|
||||
* @param code
|
||||
* @return
|
||||
*/
|
||||
public ApiResponseDto.ResponseObj getCodeCheckDuplicate(Long parentId, String code) {
|
||||
return commonCodeCoreService.getCodeCheckDuplicate(parentId, code);
|
||||
}
|
||||
|
||||
/**
|
||||
* 공통코드 이름 조회
|
||||
*
|
||||
* @param parentCodeCd 상위 코드
|
||||
* @param childCodeCd 하위 코드
|
||||
* @return 공통코드명
|
||||
*/
|
||||
public Optional<String> getCode(String parentCodeCd, String childCodeCd) {
|
||||
return commonCodeCoreService.getCode(parentCodeCd, childCodeCd);
|
||||
}
|
||||
|
||||
/**
|
||||
* 공통코드 이름 조회
|
||||
*
|
||||
* @param parentCodeCd 상위 코드
|
||||
* @param childCodeCd 하위 코드
|
||||
* @return 공통코드명
|
||||
*/
|
||||
public Optional<String> getTypeCode(String parentCodeCd, String childCodeCd) {
|
||||
return commonCodeCoreService.getCode(parentCodeCd, childCodeCd);
|
||||
}
|
||||
|
||||
public List<CodeDto> getTypeCode(String type) {
|
||||
return Enums.getCodes(type);
|
||||
}
|
||||
|
||||
/**
|
||||
* 공통코드 리스트 조회
|
||||
*
|
||||
* @return
|
||||
*/
|
||||
public Map<String, List<CodeDto>> getTypeCodes() {
|
||||
return Enums.getAllCodes();
|
||||
}
|
||||
|
||||
/** 메모리 캐시 초기화 */
|
||||
@CacheEvict(value = "trainCommonCodes", allEntries = true)
|
||||
public void refresh() {}
|
||||
}
|
||||
package com.kamco.cd.training.code.service;
|
||||
|
||||
import com.kamco.cd.training.code.dto.CommonCodeDto.AddReq;
|
||||
import com.kamco.cd.training.code.dto.CommonCodeDto.Basic;
|
||||
import com.kamco.cd.training.code.dto.CommonCodeDto.ModifyReq;
|
||||
import com.kamco.cd.training.code.dto.CommonCodeDto.OrderReq;
|
||||
import com.kamco.cd.training.common.utils.enums.CodeDto;
|
||||
import com.kamco.cd.training.common.utils.enums.Enums;
|
||||
import com.kamco.cd.training.config.api.ApiResponseDto;
|
||||
import com.kamco.cd.training.postgres.core.CommonCodeCoreService;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.cache.annotation.CacheEvict;
|
||||
import org.springframework.cache.annotation.Cacheable;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
// training 서버는 Redis 사용하지 않고 Spring Boot 메모리 캐시를 사용함
|
||||
// => org.springframework.cache.annotation.Cacheable
|
||||
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
@Transactional(readOnly = true)
|
||||
public class CommonCodeService {
|
||||
|
||||
private final CommonCodeCoreService commonCodeCoreService;
|
||||
|
||||
/**
|
||||
* 공통코드 목록 조회
|
||||
*
|
||||
* @return 모튼 코드 정보
|
||||
*/
|
||||
@Cacheable("trainCommonCodes")
|
||||
public List<Basic> getFindAll() {
|
||||
return commonCodeCoreService.findAll();
|
||||
}
|
||||
|
||||
/**
|
||||
* 공통코드 단건 조회
|
||||
*
|
||||
* @param id
|
||||
* @return 코드 아이디로 조회한 코드 정보
|
||||
*/
|
||||
public Basic getOneById(Long id) {
|
||||
return commonCodeCoreService.getOneById(id);
|
||||
}
|
||||
|
||||
/**
|
||||
* 공통코드 생성 요청
|
||||
*
|
||||
* @param req 생성요청 정보
|
||||
* @return 생성된 코드 id
|
||||
*/
|
||||
@Transactional
|
||||
@CacheEvict(value = "trainCommonCodes", allEntries = true)
|
||||
public ApiResponseDto.ResponseObj save(AddReq req) {
|
||||
return commonCodeCoreService.save(req);
|
||||
}
|
||||
|
||||
/**
|
||||
* 공통코드 수정 요청
|
||||
*
|
||||
* @param id 코드 아이디
|
||||
* @param req 수정요청 정보
|
||||
*/
|
||||
@Transactional
|
||||
@CacheEvict(value = "trainCommonCodes", allEntries = true)
|
||||
public ApiResponseDto.ResponseObj update(Long id, ModifyReq req) {
|
||||
return commonCodeCoreService.update(id, req);
|
||||
}
|
||||
|
||||
/**
|
||||
* 공통코드 삭제 처리
|
||||
*
|
||||
* @param id 코드 아이디
|
||||
*/
|
||||
@Transactional
|
||||
@CacheEvict(value = "trainCommonCodes", allEntries = true)
|
||||
public ApiResponseDto.ResponseObj removeCode(Long id) {
|
||||
return commonCodeCoreService.removeCode(id);
|
||||
}
|
||||
|
||||
/**
|
||||
* 공통코드 순서 변경
|
||||
*
|
||||
* @param req id, order 정보를 가진 List
|
||||
*/
|
||||
@Transactional
|
||||
@CacheEvict(value = "trainCommonCodes", allEntries = true)
|
||||
public ApiResponseDto.ResponseObj updateOrder(OrderReq req) {
|
||||
return commonCodeCoreService.updateOrder(req);
|
||||
}
|
||||
|
||||
/**
|
||||
* 코드기반 조회
|
||||
*
|
||||
* @param code 코드
|
||||
* @return 코드로 조회한 공통코드 정보
|
||||
*/
|
||||
public List<Basic> findByCode(String code) {
|
||||
return commonCodeCoreService.findByCode(code);
|
||||
}
|
||||
|
||||
/**
|
||||
* 중복 체크
|
||||
*
|
||||
* @param parentId
|
||||
* @param code
|
||||
* @return
|
||||
*/
|
||||
public ApiResponseDto.ResponseObj getCodeCheckDuplicate(Long parentId, String code) {
|
||||
return commonCodeCoreService.getCodeCheckDuplicate(parentId, code);
|
||||
}
|
||||
|
||||
/**
|
||||
* 공통코드 이름 조회
|
||||
*
|
||||
* @param parentCodeCd 상위 코드
|
||||
* @param childCodeCd 하위 코드
|
||||
* @return 공통코드명
|
||||
*/
|
||||
public Optional<String> getCode(String parentCodeCd, String childCodeCd) {
|
||||
return commonCodeCoreService.getCode(parentCodeCd, childCodeCd);
|
||||
}
|
||||
|
||||
// TODO 미사용시작
|
||||
/**
|
||||
* 공통코드 이름 조회
|
||||
*
|
||||
* @param parentCodeCd 상위 코드
|
||||
* @param childCodeCd 하위 코드
|
||||
* @return 공통코드명
|
||||
*/
|
||||
public Optional<String> getTypeCode(String parentCodeCd, String childCodeCd) {
|
||||
return commonCodeCoreService.getCode(parentCodeCd, childCodeCd);
|
||||
}
|
||||
|
||||
// TODO 미사용 끝
|
||||
|
||||
public List<CodeDto> getTypeCode(String type) {
|
||||
return Enums.getCodes(type);
|
||||
}
|
||||
|
||||
/**
|
||||
* 공통코드 리스트 조회
|
||||
*
|
||||
* @return
|
||||
*/
|
||||
public Map<String, List<CodeDto>> getTypeCodes() {
|
||||
return Enums.getAllCodes();
|
||||
}
|
||||
|
||||
/** 메모리 캐시 초기화 */
|
||||
@CacheEvict(value = "trainCommonCodes", allEntries = true)
|
||||
public void refresh() {}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,50 @@
|
||||
// TODO 미사용시작
|
||||
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);
|
||||
}
|
||||
}
|
||||
// TODO 미사용 끝
|
||||
@@ -0,0 +1,21 @@
|
||||
// TODO 미사용시작
|
||||
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;
|
||||
}
|
||||
}
|
||||
// TODO 미사용 끝
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
) {}
|
||||
160
src/main/java/com/kamco/cd/training/common/dto/HyperParam.java
Normal file
160
src/main/java/com/kamco/cd/training/common/dto/HyperParam.java
Normal 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
|
||||
}
|
||||
@@ -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 평균 (%)
|
||||
}
|
||||
@@ -1,19 +1,21 @@
|
||||
package com.kamco.cd.training.common.enums;
|
||||
|
||||
import com.kamco.cd.training.common.utils.enums.CodeExpose;
|
||||
import com.kamco.cd.training.common.utils.enums.EnumType;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Getter;
|
||||
|
||||
@CodeExpose
|
||||
@Getter
|
||||
@AllArgsConstructor
|
||||
public enum DeployTargetType implements EnumType {
|
||||
// @formatter:off
|
||||
GUKU("GUKU", "국토교통부"),
|
||||
PROD("PROD", "운영계");
|
||||
// @formatter:on
|
||||
|
||||
private final String id;
|
||||
private final String text;
|
||||
}
|
||||
// TODO 미사용시작
|
||||
package com.kamco.cd.training.common.enums;
|
||||
|
||||
import com.kamco.cd.training.common.utils.enums.CodeExpose;
|
||||
import com.kamco.cd.training.common.utils.enums.EnumType;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Getter;
|
||||
|
||||
@CodeExpose
|
||||
@Getter
|
||||
@AllArgsConstructor
|
||||
public enum DeployTargetType implements EnumType {
|
||||
// @formatter:off
|
||||
GUKU("GUKU", "국토교통부"),
|
||||
PROD("PROD", "운영계");
|
||||
// @formatter:on
|
||||
|
||||
private final String id;
|
||||
private final String text;
|
||||
}
|
||||
// TODO 미사용 끝
|
||||
|
||||
@@ -1,55 +1,56 @@
|
||||
package com.kamco.cd.training.common.enums;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Getter;
|
||||
|
||||
@Getter
|
||||
@AllArgsConstructor
|
||||
public enum DetectionClassification {
|
||||
BUILDING("building", "건물", 10),
|
||||
CONTAINER("container", "컨테이너", 20),
|
||||
FIELD("field", "경작지", 30),
|
||||
FOREST("forest", "숲", 40),
|
||||
GRASS("grass", "초지", 50),
|
||||
GREENHOUSE("greenhouse", "비닐하우스", 60),
|
||||
LAND("land", "일반토지", 70),
|
||||
ORCHARD("orchard", "과수원", 80),
|
||||
ROAD("road", "도로", 90),
|
||||
STONE("stone", "모래/자갈", 100),
|
||||
TANK("tank", "물탱크", 110),
|
||||
TUMULUS("tumulus", "토분(무덤)", 120),
|
||||
WASTE("waste", "폐기물", 130),
|
||||
WATER("water", "물", 140),
|
||||
ETC("ETC", "기타", 200); // For 'etc' (miscellaneous/other)
|
||||
|
||||
private final String id;
|
||||
private final String desc;
|
||||
private final int order;
|
||||
|
||||
/**
|
||||
* Optional: Helper method to get the enum from a String, case-insensitive, or return ETC if not
|
||||
* found.
|
||||
*/
|
||||
public static DetectionClassification fromString(String text) {
|
||||
if (text == null || text.trim().isEmpty()) {
|
||||
return ETC;
|
||||
}
|
||||
|
||||
try {
|
||||
return DetectionClassification.valueOf(text.toUpperCase());
|
||||
} catch (IllegalArgumentException e) {
|
||||
// If the string doesn't match any enum constant name, return ETC
|
||||
return ETC;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Desc 한글명 get 하기
|
||||
*
|
||||
* @return
|
||||
*/
|
||||
public static String fromStrDesc(String text) {
|
||||
DetectionClassification dtf = fromString(text);
|
||||
return dtf.getDesc();
|
||||
}
|
||||
}
|
||||
package com.kamco.cd.training.common.enums;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Getter;
|
||||
|
||||
@Getter
|
||||
@AllArgsConstructor
|
||||
public enum DetectionClassification {
|
||||
ROAD("road", "도로", 10),
|
||||
BUILDING("building", "건물", 20),
|
||||
GREENHOUSE("greenhouse", "비닐하우스", 30),
|
||||
FIELD("field", "논/밭", 40),
|
||||
ORCHARD("orchard", "과수원", 50),
|
||||
GRASS("grass", "초지", 60),
|
||||
FOREST("forest", "숲", 70),
|
||||
WATER("water", "물", 80),
|
||||
STONE("stone", "모래/자갈", 90),
|
||||
WASTE("waste", "적치물", 100),
|
||||
CONTAINER("container", "컨테이너", 110),
|
||||
LAND("land", "일반토지", 120),
|
||||
SOLAR("solar", "태양광", 130),
|
||||
TANK("tank", "물탱크", 140),
|
||||
NDC("NDC", "미분류", 150),
|
||||
ETC("ETC", "기타", 160);
|
||||
|
||||
private final String id;
|
||||
private final String desc;
|
||||
private final int order;
|
||||
|
||||
/**
|
||||
* Optional: Helper method to get the enum from a String, case-insensitive, or return ETC if not
|
||||
* found.
|
||||
*/
|
||||
public static DetectionClassification fromString(String text) {
|
||||
if (text == null || text.trim().isEmpty()) {
|
||||
return ETC;
|
||||
}
|
||||
|
||||
try {
|
||||
return DetectionClassification.valueOf(text.toUpperCase());
|
||||
} catch (IllegalArgumentException e) {
|
||||
// If the string doesn't match any enum constant name, return ETC
|
||||
return ETC;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Desc 한글명 get 하기
|
||||
*
|
||||
* @return
|
||||
*/
|
||||
public static String fromStrDesc(String text) {
|
||||
DetectionClassification dtf = fromString(text);
|
||||
return dtf.getDesc();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -1,26 +1,26 @@
|
||||
package com.kamco.cd.training.common.enums;
|
||||
|
||||
import com.kamco.cd.training.common.utils.enums.EnumType;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Getter;
|
||||
|
||||
@Getter
|
||||
@AllArgsConstructor
|
||||
public enum LearnDataRegister implements EnumType {
|
||||
READY("준비"),
|
||||
UPLOADING("업로드중"),
|
||||
UPLOAD_FAILED("업로드 실패"),
|
||||
COMPLETED("완료");
|
||||
|
||||
private final String desc;
|
||||
|
||||
@Override
|
||||
public String getId() {
|
||||
return name();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getText() {
|
||||
return desc;
|
||||
}
|
||||
}
|
||||
package com.kamco.cd.training.common.enums;
|
||||
|
||||
import com.kamco.cd.training.common.utils.enums.EnumType;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Getter;
|
||||
|
||||
@Getter
|
||||
@AllArgsConstructor
|
||||
public enum LearnDataRegister implements EnumType {
|
||||
READY("준비"),
|
||||
UPLOADING("업로드중"),
|
||||
UPLOAD_FAILED("업로드 실패"),
|
||||
COMPLETED("완료");
|
||||
|
||||
private final String desc;
|
||||
|
||||
@Override
|
||||
public String getId() {
|
||||
return name();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getText() {
|
||||
return desc;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,26 +1,26 @@
|
||||
package com.kamco.cd.training.common.enums;
|
||||
|
||||
import com.kamco.cd.training.common.utils.enums.CodeExpose;
|
||||
import com.kamco.cd.training.common.utils.enums.EnumType;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Getter;
|
||||
|
||||
@CodeExpose
|
||||
@Getter
|
||||
@AllArgsConstructor
|
||||
public enum LearnDataType implements EnumType {
|
||||
DELIVER("납품"),
|
||||
PRODUCTION("제작");
|
||||
|
||||
private final String desc;
|
||||
|
||||
@Override
|
||||
public String getId() {
|
||||
return name();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getText() {
|
||||
return desc;
|
||||
}
|
||||
}
|
||||
package com.kamco.cd.training.common.enums;
|
||||
|
||||
import com.kamco.cd.training.common.utils.enums.CodeExpose;
|
||||
import com.kamco.cd.training.common.utils.enums.EnumType;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Getter;
|
||||
|
||||
@CodeExpose
|
||||
@Getter
|
||||
@AllArgsConstructor
|
||||
public enum LearnDataType implements EnumType {
|
||||
DELIVER("납품"),
|
||||
PRODUCTION("제작");
|
||||
|
||||
private final String desc;
|
||||
|
||||
@Override
|
||||
public String getId() {
|
||||
return name();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getText() {
|
||||
return desc;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,27 +1,29 @@
|
||||
package com.kamco.cd.training.common.enums;
|
||||
|
||||
import com.kamco.cd.training.common.utils.enums.CodeExpose;
|
||||
import com.kamco.cd.training.common.utils.enums.EnumType;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Getter;
|
||||
|
||||
@CodeExpose
|
||||
@Getter
|
||||
@AllArgsConstructor
|
||||
public enum ModelMngStatusType implements EnumType {
|
||||
READY("준비"),
|
||||
IN_PROGRESS("진행중"),
|
||||
COMPLETED("완료");
|
||||
|
||||
private String desc;
|
||||
|
||||
@Override
|
||||
public String getId() {
|
||||
return name();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getText() {
|
||||
return desc;
|
||||
}
|
||||
}
|
||||
// TODO 미사용시작
|
||||
package com.kamco.cd.training.common.enums;
|
||||
|
||||
import com.kamco.cd.training.common.utils.enums.CodeExpose;
|
||||
import com.kamco.cd.training.common.utils.enums.EnumType;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Getter;
|
||||
|
||||
@CodeExpose
|
||||
@Getter
|
||||
@AllArgsConstructor
|
||||
public enum ModelMngStatusType implements EnumType {
|
||||
READY("준비"),
|
||||
IN_PROGRESS("진행중"),
|
||||
COMPLETED("완료");
|
||||
|
||||
private String desc;
|
||||
|
||||
@Override
|
||||
public String getId() {
|
||||
return name();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getText() {
|
||||
return desc;
|
||||
}
|
||||
}
|
||||
// TODO 미사용 끝
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -1,20 +1,22 @@
|
||||
package com.kamco.cd.training.common.enums;
|
||||
|
||||
import com.kamco.cd.training.common.utils.enums.CodeExpose;
|
||||
import com.kamco.cd.training.common.utils.enums.EnumType;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Getter;
|
||||
|
||||
@CodeExpose
|
||||
@Getter
|
||||
@AllArgsConstructor
|
||||
public enum ProcessStepType implements EnumType {
|
||||
// @formatter:off
|
||||
STEP1("STEP1", "학습 중"),
|
||||
STEP2("STEP2", "테스트 중"),
|
||||
STEP3("STEP3", "완료");
|
||||
// @formatter:on
|
||||
|
||||
private final String id;
|
||||
private final String text;
|
||||
}
|
||||
// TODO 미사용시작
|
||||
package com.kamco.cd.training.common.enums;
|
||||
|
||||
import com.kamco.cd.training.common.utils.enums.CodeExpose;
|
||||
import com.kamco.cd.training.common.utils.enums.EnumType;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Getter;
|
||||
|
||||
@CodeExpose
|
||||
@Getter
|
||||
@AllArgsConstructor
|
||||
public enum ProcessStepType implements EnumType {
|
||||
// @formatter:off
|
||||
STEP1("STEP1", "학습 중"),
|
||||
STEP2("STEP2", "테스트 중"),
|
||||
STEP3("STEP3", "완료");
|
||||
// @formatter:on
|
||||
|
||||
private final String id;
|
||||
private final String text;
|
||||
}
|
||||
// TODO 미사용 끝
|
||||
|
||||
@@ -1,25 +1,25 @@
|
||||
package com.kamco.cd.training.common.enums;
|
||||
|
||||
import com.kamco.cd.training.common.utils.enums.EnumType;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Getter;
|
||||
|
||||
@Getter
|
||||
@AllArgsConstructor
|
||||
public enum RoleType implements EnumType {
|
||||
ROLE_ADMIN("시스템 관리자"),
|
||||
ROLE_LABELER("라벨러"),
|
||||
ROLE_REVIEWER("검수자");
|
||||
|
||||
private final String desc;
|
||||
|
||||
@Override
|
||||
public String getId() {
|
||||
return name();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getText() {
|
||||
return desc;
|
||||
}
|
||||
}
|
||||
package com.kamco.cd.training.common.enums;
|
||||
|
||||
import com.kamco.cd.training.common.utils.enums.EnumType;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Getter;
|
||||
|
||||
@Getter
|
||||
@AllArgsConstructor
|
||||
public enum RoleType implements EnumType {
|
||||
ROLE_ADMIN("시스템 관리자"),
|
||||
ROLE_LABELER("라벨러"),
|
||||
ROLE_REVIEWER("검수자");
|
||||
|
||||
private final String desc;
|
||||
|
||||
@Override
|
||||
public String getId() {
|
||||
return name();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getText() {
|
||||
return desc;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,25 +1,25 @@
|
||||
package com.kamco.cd.training.common.enums;
|
||||
|
||||
import com.kamco.cd.training.common.utils.enums.EnumType;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Getter;
|
||||
|
||||
@Getter
|
||||
@AllArgsConstructor
|
||||
public enum StatusType implements EnumType {
|
||||
ACTIVE("사용"),
|
||||
INACTIVE("미사용"),
|
||||
PENDING("계정등록");
|
||||
|
||||
private final String desc;
|
||||
|
||||
@Override
|
||||
public String getId() {
|
||||
return name();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getText() {
|
||||
return desc;
|
||||
}
|
||||
}
|
||||
package com.kamco.cd.training.common.enums;
|
||||
|
||||
import com.kamco.cd.training.common.utils.enums.EnumType;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Getter;
|
||||
|
||||
@Getter
|
||||
@AllArgsConstructor
|
||||
public enum StatusType implements EnumType {
|
||||
ACTIVE("사용"),
|
||||
INACTIVE("미사용"),
|
||||
PENDING("계정등록");
|
||||
|
||||
private final String desc;
|
||||
|
||||
@Override
|
||||
public String getId() {
|
||||
return name();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getText() {
|
||||
return desc;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,22 +1,30 @@
|
||||
package com.kamco.cd.training.common.enums;
|
||||
|
||||
import com.kamco.cd.training.common.utils.enums.CodeExpose;
|
||||
import com.kamco.cd.training.common.utils.enums.EnumType;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Getter;
|
||||
|
||||
@CodeExpose
|
||||
@Getter
|
||||
@AllArgsConstructor
|
||||
public enum TrainStatusType implements EnumType {
|
||||
// @formatter:off
|
||||
READY("READY", "대기"),
|
||||
ING("ING", "진행중"),
|
||||
COMPLETED("COMPLETED", "완료"),
|
||||
STOPPED("STOPPED", "중단됨"),
|
||||
ERROR("ERROR", "오류");
|
||||
// @formatter:on
|
||||
|
||||
private final String id;
|
||||
private final String text;
|
||||
}
|
||||
package com.kamco.cd.training.common.enums;
|
||||
|
||||
import com.kamco.cd.training.common.utils.enums.CodeExpose;
|
||||
import com.kamco.cd.training.common.utils.enums.EnumType;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Getter;
|
||||
|
||||
@CodeExpose
|
||||
@Getter
|
||||
@AllArgsConstructor
|
||||
public enum TrainStatusType implements EnumType {
|
||||
// @formatter:off
|
||||
READY("대기"),
|
||||
IN_PROGRESS("진행중"),
|
||||
COMPLETED("완료"),
|
||||
STOPPED("중단됨"),
|
||||
ERROR("오류");
|
||||
|
||||
private final String desc;
|
||||
|
||||
@Override
|
||||
public String getId() {
|
||||
return name();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getText() {
|
||||
return desc;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -1,26 +1,26 @@
|
||||
package com.kamco.cd.training.common.enums.error;
|
||||
|
||||
import com.kamco.cd.training.common.utils.ErrorCode;
|
||||
import lombok.Getter;
|
||||
import org.springframework.http.HttpStatus;
|
||||
|
||||
@Getter
|
||||
public enum AuthErrorCode implements ErrorCode {
|
||||
LOGIN_ID_NOT_FOUND("LOGIN_ID_NOT_FOUND", HttpStatus.UNAUTHORIZED),
|
||||
|
||||
LOGIN_PASSWORD_MISMATCH("LOGIN_PASSWORD_MISMATCH", HttpStatus.UNAUTHORIZED),
|
||||
|
||||
LOGIN_PASSWORD_EXCEEDED("LOGIN_PASSWORD_EXCEEDED", HttpStatus.UNAUTHORIZED),
|
||||
|
||||
REFRESH_TOKEN_EXPIRED_OR_REVOKED("REFRESH_TOKEN_EXPIRED_OR_REVOKED", HttpStatus.UNAUTHORIZED),
|
||||
|
||||
REFRESH_TOKEN_MISMATCH("REFRESH_TOKEN_MISMATCH", HttpStatus.UNAUTHORIZED);
|
||||
|
||||
private final String code;
|
||||
private final HttpStatus status;
|
||||
|
||||
AuthErrorCode(String code, HttpStatus status) {
|
||||
this.code = code;
|
||||
this.status = status;
|
||||
}
|
||||
}
|
||||
package com.kamco.cd.training.common.enums.error;
|
||||
|
||||
import com.kamco.cd.training.common.utils.ErrorCode;
|
||||
import lombok.Getter;
|
||||
import org.springframework.http.HttpStatus;
|
||||
|
||||
@Getter
|
||||
public enum AuthErrorCode implements ErrorCode {
|
||||
LOGIN_ID_NOT_FOUND("LOGIN_ID_NOT_FOUND", HttpStatus.UNAUTHORIZED),
|
||||
|
||||
LOGIN_PASSWORD_MISMATCH("LOGIN_PASSWORD_MISMATCH", HttpStatus.UNAUTHORIZED),
|
||||
|
||||
LOGIN_PASSWORD_EXCEEDED("LOGIN_PASSWORD_EXCEEDED", HttpStatus.UNAUTHORIZED),
|
||||
|
||||
REFRESH_TOKEN_EXPIRED_OR_REVOKED("REFRESH_TOKEN_EXPIRED_OR_REVOKED", HttpStatus.UNAUTHORIZED),
|
||||
|
||||
REFRESH_TOKEN_MISMATCH("REFRESH_TOKEN_MISMATCH", HttpStatus.UNAUTHORIZED);
|
||||
|
||||
private final String code;
|
||||
private final HttpStatus status;
|
||||
|
||||
AuthErrorCode(String code, HttpStatus status) {
|
||||
this.code = code;
|
||||
this.status = status;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
package com.kamco.cd.training.common.exception;
|
||||
|
||||
import org.springframework.http.HttpStatus;
|
||||
|
||||
public class BadRequestException extends CustomApiException {
|
||||
|
||||
public BadRequestException(String message) {
|
||||
super("BAD_REQUEST", HttpStatus.BAD_REQUEST, message);
|
||||
}
|
||||
}
|
||||
package com.kamco.cd.training.common.exception;
|
||||
|
||||
import org.springframework.http.HttpStatus;
|
||||
|
||||
public class BadRequestException extends CustomApiException {
|
||||
|
||||
public BadRequestException(String message) {
|
||||
super("BAD_REQUEST", HttpStatus.BAD_REQUEST, message);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,28 +1,28 @@
|
||||
package com.kamco.cd.training.common.exception;
|
||||
|
||||
import com.kamco.cd.training.common.utils.ErrorCode;
|
||||
import lombok.Getter;
|
||||
import org.springframework.http.HttpStatus;
|
||||
|
||||
@Getter
|
||||
public class CustomApiException extends RuntimeException {
|
||||
|
||||
private final String codeName; // ApiResponseCode enum name과 맞추는 용도 (예: "UNPROCESSABLE_ENTITY")
|
||||
private final HttpStatus status; // 응답으로 내려줄 HttpStatus
|
||||
|
||||
public CustomApiException(String codeName, HttpStatus status, String message) {
|
||||
super(message);
|
||||
this.codeName = codeName;
|
||||
this.status = status;
|
||||
}
|
||||
|
||||
public CustomApiException(String codeName, HttpStatus status) {
|
||||
this.codeName = codeName;
|
||||
this.status = status;
|
||||
}
|
||||
|
||||
public CustomApiException(ErrorCode errorCode) {
|
||||
this.codeName = errorCode.getCode();
|
||||
this.status = errorCode.getStatus();
|
||||
}
|
||||
}
|
||||
package com.kamco.cd.training.common.exception;
|
||||
|
||||
import com.kamco.cd.training.common.utils.ErrorCode;
|
||||
import lombok.Getter;
|
||||
import org.springframework.http.HttpStatus;
|
||||
|
||||
@Getter
|
||||
public class CustomApiException extends RuntimeException {
|
||||
|
||||
private final String codeName; // ApiResponseCode enum name과 맞추는 용도 (예: "UNPROCESSABLE_ENTITY")
|
||||
private final HttpStatus status; // 응답으로 내려줄 HttpStatus
|
||||
|
||||
public CustomApiException(String codeName, HttpStatus status, String message) {
|
||||
super(message);
|
||||
this.codeName = codeName;
|
||||
this.status = status;
|
||||
}
|
||||
|
||||
public CustomApiException(String codeName, HttpStatus status) {
|
||||
this.codeName = codeName;
|
||||
this.status = status;
|
||||
}
|
||||
|
||||
public CustomApiException(ErrorCode errorCode) {
|
||||
this.codeName = errorCode.getCode();
|
||||
this.status = errorCode.getStatus();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
package com.kamco.cd.training.common.exception;
|
||||
|
||||
public class DuplicateFileException extends RuntimeException {
|
||||
public DuplicateFileException(String message) {
|
||||
super(message);
|
||||
}
|
||||
}
|
||||
package com.kamco.cd.training.common.exception;
|
||||
|
||||
public class DuplicateFileException extends RuntimeException {
|
||||
public DuplicateFileException(String message) {
|
||||
super(message);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
package com.kamco.cd.training.common.exception;
|
||||
|
||||
import org.springframework.http.HttpStatus;
|
||||
|
||||
public class NotFoundException extends CustomApiException {
|
||||
|
||||
public NotFoundException(String message) {
|
||||
super("NOT_FOUND", HttpStatus.NOT_FOUND, message);
|
||||
}
|
||||
}
|
||||
package com.kamco.cd.training.common.exception;
|
||||
|
||||
import org.springframework.http.HttpStatus;
|
||||
|
||||
public class NotFoundException extends CustomApiException {
|
||||
|
||||
public NotFoundException(String message) {
|
||||
super("NOT_FOUND", HttpStatus.NOT_FOUND, message);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
package com.kamco.cd.training.common.exception;
|
||||
|
||||
public class ValidationException extends RuntimeException {
|
||||
public ValidationException(String message) {
|
||||
super(message);
|
||||
}
|
||||
}
|
||||
package com.kamco.cd.training.common.exception;
|
||||
|
||||
public class ValidationException extends RuntimeException {
|
||||
public ValidationException(String message) {
|
||||
super(message);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,38 +1,38 @@
|
||||
package com.kamco.cd.training.common.service;
|
||||
|
||||
import org.springframework.data.domain.Page;
|
||||
|
||||
/**
|
||||
* Base Core Service Interface
|
||||
*
|
||||
* <p>CRUD operations를 정의하는 기본 서비스 인터페이스
|
||||
*
|
||||
* @param <T> Entity 타입
|
||||
* @param <ID> Entity의 ID 타입
|
||||
* @param <S> Search Request 타입
|
||||
*/
|
||||
public interface BaseCoreService<T, ID, S> {
|
||||
|
||||
/**
|
||||
* ID로 엔티티를 삭제합니다.
|
||||
*
|
||||
* @param id 삭제할 엔티티의 ID
|
||||
*/
|
||||
void remove(ID id);
|
||||
|
||||
/**
|
||||
* ID로 단건 조회합니다.
|
||||
*
|
||||
* @param id 조회할 엔티티의 ID
|
||||
* @return 조회된 엔티티
|
||||
*/
|
||||
T getOneById(ID id);
|
||||
|
||||
/**
|
||||
* 검색 조건과 페이징으로 조회합니다.
|
||||
*
|
||||
* @param searchReq 검색 조건
|
||||
* @return 페이징 처리된 검색 결과
|
||||
*/
|
||||
Page<T> search(S searchReq);
|
||||
}
|
||||
package com.kamco.cd.training.common.service;
|
||||
|
||||
import org.springframework.data.domain.Page;
|
||||
|
||||
/**
|
||||
* Base Core Service Interface
|
||||
*
|
||||
* <p>CRUD operations를 정의하는 기본 서비스 인터페이스
|
||||
*
|
||||
* @param <T> Entity 타입
|
||||
* @param <ID> Entity의 ID 타입
|
||||
* @param <S> Search Request 타입
|
||||
*/
|
||||
public interface BaseCoreService<T, ID, S> {
|
||||
|
||||
/**
|
||||
* ID로 엔티티를 삭제합니다.
|
||||
*
|
||||
* @param id 삭제할 엔티티의 ID
|
||||
*/
|
||||
void remove(ID id);
|
||||
|
||||
/**
|
||||
* ID로 단건 조회합니다.
|
||||
*
|
||||
* @param id 조회할 엔티티의 ID
|
||||
* @return 조회된 엔티티
|
||||
*/
|
||||
T getOneById(ID id);
|
||||
|
||||
/**
|
||||
* 검색 조건과 페이징으로 조회합니다.
|
||||
*
|
||||
* @param searchReq 검색 조건
|
||||
* @return 페이징 처리된 검색 결과
|
||||
*/
|
||||
Page<T> search(S searchReq);
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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) {
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -1,152 +1,152 @@
|
||||
package com.kamco.cd.training.common.utils;
|
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.kamco.cd.training.code.dto.CommonCodeDto.Basic;
|
||||
import com.kamco.cd.training.code.service.CommonCodeService;
|
||||
import com.kamco.cd.training.config.api.ApiResponseDto;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
/**
|
||||
* 공통코드 조회 유틸리티 클래스 애플리케이션 전역에서 공통코드를 조회하기 위한 유틸리티입니다. training 서버는 Redis 사용하고 Spring 내장 메모리 캐시
|
||||
* 사용합니다.
|
||||
*/
|
||||
//
|
||||
@Slf4j
|
||||
@Component
|
||||
public class CommonCodeUtil {
|
||||
|
||||
private final CommonCodeService commonCodeService;
|
||||
|
||||
@Autowired private ObjectMapper objectMapper;
|
||||
|
||||
public CommonCodeUtil(CommonCodeService commonCodeService) {
|
||||
this.commonCodeService = commonCodeService;
|
||||
}
|
||||
|
||||
/**
|
||||
* 모든 공통코드 조회
|
||||
*
|
||||
* @return 캐시된 모든 공통코드 목록
|
||||
*/
|
||||
public List<Basic> getAllCommonCodes() {
|
||||
try {
|
||||
return commonCodeService.getFindAll();
|
||||
} catch (Exception e) {
|
||||
log.error("공통코드 전체 조회 중 오류 발생", e);
|
||||
return List.of();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 특정 코드로 공통코드 조회
|
||||
*
|
||||
* @param code 코드값
|
||||
* @return 해당 코드의 공통코드 목록
|
||||
*/
|
||||
public List<Basic> getCommonCodesByCode(String code) {
|
||||
if (code == null || code.isEmpty()) {
|
||||
log.warn("유효하지 않은 코드: {}", code);
|
||||
return List.of();
|
||||
}
|
||||
|
||||
try {
|
||||
return commonCodeService.findByCode(code);
|
||||
} catch (Exception e) {
|
||||
log.error("코드 기반 공통코드 조회 중 오류 발생: {}", code, e);
|
||||
return List.of();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 특정 ID로 공통코드 단건 조회
|
||||
*
|
||||
* @param id 공통코드 ID
|
||||
* @return 조회된 공통코드
|
||||
*/
|
||||
public Optional<Basic> getCommonCodeById(Long id) {
|
||||
if (id == null || id <= 0) {
|
||||
log.warn("유효하지 않은 ID: {}", id);
|
||||
return Optional.empty();
|
||||
}
|
||||
|
||||
try {
|
||||
return Optional.of(commonCodeService.getOneById(id));
|
||||
} catch (Exception e) {
|
||||
log.error("ID 기반 공통코드 조회 중 오류 발생: {}", id, e);
|
||||
return Optional.empty();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 상위 코드와 하위 코드로 공통코드명 조회
|
||||
*
|
||||
* @param parentCode 상위 코드
|
||||
* @param childCode 하위 코드
|
||||
* @return 공통코드명
|
||||
*/
|
||||
public Optional<String> getCodeName(String parentCode, String childCode) {
|
||||
if (parentCode == null || parentCode.isEmpty() || childCode == null || childCode.isEmpty()) {
|
||||
log.warn("유효하지 않은 코드: parentCode={}, childCode={}", parentCode, childCode);
|
||||
return Optional.empty();
|
||||
}
|
||||
|
||||
try {
|
||||
return commonCodeService.getCode(parentCode, childCode);
|
||||
} catch (Exception e) {
|
||||
log.error("코드명 조회 중 오류 발생: parentCode={}, childCode={}", parentCode, childCode, e);
|
||||
return Optional.empty();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 상위 코드를 기반으로 하위 코드 조회
|
||||
*
|
||||
* @param parentCode 상위 코드
|
||||
* @return 해당 상위 코드의 하위 공통코드 목록
|
||||
*/
|
||||
public List<Basic> getChildCodesByParentCode(String parentCode) {
|
||||
if (parentCode == null || parentCode.isEmpty()) {
|
||||
log.warn("유효하지 않은 상위 코드: {}", parentCode);
|
||||
return List.of();
|
||||
}
|
||||
|
||||
try {
|
||||
return commonCodeService.getFindAll().stream()
|
||||
.filter(code -> parentCode.equals(code.getCode()))
|
||||
.findFirst()
|
||||
.map(Basic::getChildren)
|
||||
.orElse(List.of());
|
||||
} catch (Exception e) {
|
||||
log.error("상위 코드 기반 하위 코드 조회 중 오류 발생: {}", parentCode, e);
|
||||
return List.of();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 코드 사용 가능 여부 확인
|
||||
*
|
||||
* @param parentId 상위 코드 ID -> 1depth 는 null 허용 (파라미터 미필수로 변경)
|
||||
* @param code 확인할 코드값
|
||||
* @return 사용 가능 여부 (true: 사용 가능, false: 중복 또는 오류)
|
||||
*/
|
||||
public boolean isCodeAvailable(Long parentId, String code) {
|
||||
if (parentId <= 0 || code == null || code.isEmpty()) {
|
||||
log.warn("유효하지 않은 입력: parentId={}, code={}", parentId, code);
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
ApiResponseDto.ResponseObj response = commonCodeService.getCodeCheckDuplicate(parentId, code);
|
||||
// ResponseObj의 code 필드 : OK이면 성공, 아니면 실패
|
||||
return response.getCode() != null
|
||||
&& response.getCode().equals(ApiResponseDto.ApiResponseCode.OK);
|
||||
} catch (Exception e) {
|
||||
log.error("코드 중복 확인 중 오류 발생: parentId={}, code={}", parentId, code, e);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
package com.kamco.cd.training.common.utils;
|
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.kamco.cd.training.code.dto.CommonCodeDto.Basic;
|
||||
import com.kamco.cd.training.code.service.CommonCodeService;
|
||||
import com.kamco.cd.training.config.api.ApiResponseDto;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
/**
|
||||
* 공통코드 조회 유틸리티 클래스 애플리케이션 전역에서 공통코드를 조회하기 위한 유틸리티입니다. training 서버는 Redis 사용하고 Spring 내장 메모리 캐시
|
||||
* 사용합니다.
|
||||
*/
|
||||
//
|
||||
@Slf4j
|
||||
@Component
|
||||
public class CommonCodeUtil {
|
||||
|
||||
private final CommonCodeService commonCodeService;
|
||||
|
||||
@Autowired private ObjectMapper objectMapper;
|
||||
|
||||
public CommonCodeUtil(CommonCodeService commonCodeService) {
|
||||
this.commonCodeService = commonCodeService;
|
||||
}
|
||||
|
||||
/**
|
||||
* 모든 공통코드 조회
|
||||
*
|
||||
* @return 캐시된 모든 공통코드 목록
|
||||
*/
|
||||
public List<Basic> getAllCommonCodes() {
|
||||
try {
|
||||
return commonCodeService.getFindAll();
|
||||
} catch (Exception e) {
|
||||
log.error("공통코드 전체 조회 중 오류 발생", e);
|
||||
return List.of();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 특정 코드로 공통코드 조회
|
||||
*
|
||||
* @param code 코드값
|
||||
* @return 해당 코드의 공통코드 목록
|
||||
*/
|
||||
public List<Basic> getCommonCodesByCode(String code) {
|
||||
if (code == null || code.isEmpty()) {
|
||||
log.warn("유효하지 않은 코드: {}", code);
|
||||
return List.of();
|
||||
}
|
||||
|
||||
try {
|
||||
return commonCodeService.findByCode(code);
|
||||
} catch (Exception e) {
|
||||
log.error("코드 기반 공통코드 조회 중 오류 발생: {}", code, e);
|
||||
return List.of();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 특정 ID로 공통코드 단건 조회
|
||||
*
|
||||
* @param id 공통코드 ID
|
||||
* @return 조회된 공통코드
|
||||
*/
|
||||
public Optional<Basic> getCommonCodeById(Long id) {
|
||||
if (id == null || id <= 0) {
|
||||
log.warn("유효하지 않은 ID: {}", id);
|
||||
return Optional.empty();
|
||||
}
|
||||
|
||||
try {
|
||||
return Optional.of(commonCodeService.getOneById(id));
|
||||
} catch (Exception e) {
|
||||
log.error("ID 기반 공통코드 조회 중 오류 발생: {}", id, e);
|
||||
return Optional.empty();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 상위 코드와 하위 코드로 공통코드명 조회
|
||||
*
|
||||
* @param parentCode 상위 코드
|
||||
* @param childCode 하위 코드
|
||||
* @return 공통코드명
|
||||
*/
|
||||
public Optional<String> getCodeName(String parentCode, String childCode) {
|
||||
if (parentCode == null || parentCode.isEmpty() || childCode == null || childCode.isEmpty()) {
|
||||
log.warn("유효하지 않은 코드: parentCode={}, childCode={}", parentCode, childCode);
|
||||
return Optional.empty();
|
||||
}
|
||||
|
||||
try {
|
||||
return commonCodeService.getCode(parentCode, childCode);
|
||||
} catch (Exception e) {
|
||||
log.error("코드명 조회 중 오류 발생: parentCode={}, childCode={}", parentCode, childCode, e);
|
||||
return Optional.empty();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 상위 코드를 기반으로 하위 코드 조회
|
||||
*
|
||||
* @param parentCode 상위 코드
|
||||
* @return 해당 상위 코드의 하위 공통코드 목록
|
||||
*/
|
||||
public List<Basic> getChildCodesByParentCode(String parentCode) {
|
||||
if (parentCode == null || parentCode.isEmpty()) {
|
||||
log.warn("유효하지 않은 상위 코드: {}", parentCode);
|
||||
return List.of();
|
||||
}
|
||||
|
||||
try {
|
||||
return commonCodeService.getFindAll().stream()
|
||||
.filter(code -> parentCode.equals(code.getCode()))
|
||||
.findFirst()
|
||||
.map(Basic::getChildren)
|
||||
.orElse(List.of());
|
||||
} catch (Exception e) {
|
||||
log.error("상위 코드 기반 하위 코드 조회 중 오류 발생: {}", parentCode, e);
|
||||
return List.of();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 코드 사용 가능 여부 확인
|
||||
*
|
||||
* @param parentId 상위 코드 ID -> 1depth 는 null 허용 (파라미터 미필수로 변경)
|
||||
* @param code 확인할 코드값
|
||||
* @return 사용 가능 여부 (true: 사용 가능, false: 중복 또는 오류)
|
||||
*/
|
||||
public boolean isCodeAvailable(Long parentId, String code) {
|
||||
if (parentId <= 0 || code == null || code.isEmpty()) {
|
||||
log.warn("유효하지 않은 입력: parentId={}, code={}", parentId, code);
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
ApiResponseDto.ResponseObj response = commonCodeService.getCodeCheckDuplicate(parentId, code);
|
||||
// ResponseObj의 code 필드 : OK이면 성공, 아니면 실패
|
||||
return response.getCode() != null
|
||||
&& response.getCode().equals(ApiResponseDto.ApiResponseCode.OK);
|
||||
} catch (Exception e) {
|
||||
log.error("코드 중복 확인 중 오류 발생: parentId={}, code={}", parentId, code, e);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,32 +1,32 @@
|
||||
package com.kamco.cd.training.common.utils;
|
||||
|
||||
import com.kamco.cd.training.auth.BCryptSaltGenerator;
|
||||
import java.util.regex.Pattern;
|
||||
import org.mindrot.jbcrypt.BCrypt;
|
||||
|
||||
public class CommonStringUtils {
|
||||
|
||||
/**
|
||||
* 영문, 숫자, 특수문자를 모두 포함하여 8~20자 이내의 비밀번호
|
||||
*
|
||||
* @param password 벨리데이션 필요한 패스워드
|
||||
* @return
|
||||
*/
|
||||
public static boolean isValidPassword(String password) {
|
||||
String passwordPattern =
|
||||
"^(?=.*[A-Za-z])(?=.*\\d)(?=.*[!@#$%^&*()_+\\-\\[\\]{};':\"\\\\|,.<>/?]).{8,20}$";
|
||||
return Pattern.matches(passwordPattern, password);
|
||||
}
|
||||
|
||||
/**
|
||||
* 패스워드 암호화
|
||||
*
|
||||
* @param password 암호화 필요한 패스워드
|
||||
* @param employeeNo salt 생성에 필요한 사원번호
|
||||
* @return
|
||||
*/
|
||||
public static String hashPassword(String password, String employeeNo) {
|
||||
String salt = BCryptSaltGenerator.generateSaltWithEmployeeNo(employeeNo.trim());
|
||||
return BCrypt.hashpw(password.trim(), salt);
|
||||
}
|
||||
}
|
||||
package com.kamco.cd.training.common.utils;
|
||||
|
||||
import com.kamco.cd.training.auth.BCryptSaltGenerator;
|
||||
import java.util.regex.Pattern;
|
||||
import org.mindrot.jbcrypt.BCrypt;
|
||||
|
||||
public class CommonStringUtils {
|
||||
|
||||
/**
|
||||
* 영문, 숫자, 특수문자를 모두 포함하여 8~20자 이내의 비밀번호
|
||||
*
|
||||
* @param password 벨리데이션 필요한 패스워드
|
||||
* @return
|
||||
*/
|
||||
public static boolean isValidPassword(String password) {
|
||||
String passwordPattern =
|
||||
"^(?=.*[A-Za-z])(?=.*\\d)(?=.*[!@#$%^&*()_+\\-\\[\\]{};':\"\\\\|,.<>/?]).{8,20}$";
|
||||
return Pattern.matches(passwordPattern, password);
|
||||
}
|
||||
|
||||
/**
|
||||
* 패스워드 암호화
|
||||
*
|
||||
* @param password 암호화 필요한 패스워드
|
||||
* @param employeeNo salt 생성에 필요한 사원번호
|
||||
* @return
|
||||
*/
|
||||
public static String hashPassword(String password, String employeeNo) {
|
||||
String salt = BCryptSaltGenerator.generateSaltWithEmployeeNo(employeeNo.trim());
|
||||
return BCrypt.hashpw(password.trim(), salt);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
package com.kamco.cd.training.common.utils;
|
||||
|
||||
import org.springframework.http.HttpStatus;
|
||||
|
||||
public interface ErrorCode {
|
||||
|
||||
String getCode();
|
||||
|
||||
HttpStatus getStatus();
|
||||
}
|
||||
package com.kamco.cd.training.common.utils;
|
||||
|
||||
import org.springframework.http.HttpStatus;
|
||||
|
||||
public interface ErrorCode {
|
||||
|
||||
String getCode();
|
||||
|
||||
HttpStatus getStatus();
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -1,43 +1,43 @@
|
||||
package com.kamco.cd.training.common.utils;
|
||||
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
public class NameValidator {
|
||||
|
||||
private static final String HANGUL_REGEX = ".*\\p{IsHangul}.*";
|
||||
private static final Pattern HANGUL_PATTERN = Pattern.compile(HANGUL_REGEX);
|
||||
|
||||
private static final String WHITESPACE_REGEX = ".*\\s.*";
|
||||
private static final Pattern WHITESPACE_PATTERN = Pattern.compile(WHITESPACE_REGEX);
|
||||
|
||||
public static boolean containsKorean(String str) {
|
||||
if (str == null || str.isEmpty()) {
|
||||
return false;
|
||||
}
|
||||
Matcher matcher = HANGUL_PATTERN.matcher(str);
|
||||
return matcher.matches();
|
||||
}
|
||||
|
||||
public static boolean containsWhitespaceRegex(String str) {
|
||||
if (str == null || str.isEmpty()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
Matcher matcher = WHITESPACE_PATTERN.matcher(str);
|
||||
// find()를 사용하여 문자열 내에서 패턴이 일치하는 부분이 있는지 확인
|
||||
return matcher.find();
|
||||
}
|
||||
|
||||
public static boolean isNullOrEmpty(String str) {
|
||||
if (str == null) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (str.isEmpty()) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
package com.kamco.cd.training.common.utils;
|
||||
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
public class NameValidator {
|
||||
|
||||
private static final String HANGUL_REGEX = ".*\\p{IsHangul}.*";
|
||||
private static final Pattern HANGUL_PATTERN = Pattern.compile(HANGUL_REGEX);
|
||||
|
||||
private static final String WHITESPACE_REGEX = ".*\\s.*";
|
||||
private static final Pattern WHITESPACE_PATTERN = Pattern.compile(WHITESPACE_REGEX);
|
||||
|
||||
public static boolean containsKorean(String str) {
|
||||
if (str == null || str.isEmpty()) {
|
||||
return false;
|
||||
}
|
||||
Matcher matcher = HANGUL_PATTERN.matcher(str);
|
||||
return matcher.matches();
|
||||
}
|
||||
|
||||
public static boolean containsWhitespaceRegex(String str) {
|
||||
if (str == null || str.isEmpty()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
Matcher matcher = WHITESPACE_PATTERN.matcher(str);
|
||||
// find()를 사용하여 문자열 내에서 패턴이 일치하는 부분이 있는지 확인
|
||||
return matcher.find();
|
||||
}
|
||||
|
||||
public static boolean isNullOrEmpty(String str) {
|
||||
if (str == null) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (str.isEmpty()) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,41 +1,41 @@
|
||||
package com.kamco.cd.training.common.utils;
|
||||
|
||||
import com.kamco.cd.training.auth.CustomUserDetails;
|
||||
import com.kamco.cd.training.members.dto.MembersDto;
|
||||
import com.kamco.cd.training.postgres.entity.MemberEntity;
|
||||
import java.util.Optional;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.security.core.context.SecurityContextHolder;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
@Component
|
||||
@RequiredArgsConstructor
|
||||
public class UserUtil {
|
||||
|
||||
public MembersDto.Member getCurrentUser() {
|
||||
return Optional.ofNullable(SecurityContextHolder.getContext().getAuthentication())
|
||||
.filter(auth -> auth.getPrincipal() instanceof CustomUserDetails)
|
||||
.map(
|
||||
auth -> {
|
||||
CustomUserDetails user = (CustomUserDetails) auth.getPrincipal();
|
||||
MemberEntity m = user.getMember();
|
||||
return new MembersDto.Member(m.getId(), m.getName(), m.getEmployeeNo());
|
||||
})
|
||||
.orElse(null);
|
||||
}
|
||||
|
||||
public Long getId() {
|
||||
MembersDto.Member user = getCurrentUser();
|
||||
return user != null ? user.getId() : null;
|
||||
}
|
||||
|
||||
public String getName() {
|
||||
MembersDto.Member user = getCurrentUser();
|
||||
return user != null ? user.getName() : null;
|
||||
}
|
||||
|
||||
public String getEmployeeNo() {
|
||||
MembersDto.Member user = getCurrentUser();
|
||||
return user != null ? user.getEmployeeNo() : null;
|
||||
}
|
||||
}
|
||||
package com.kamco.cd.training.common.utils;
|
||||
|
||||
import com.kamco.cd.training.auth.CustomUserDetails;
|
||||
import com.kamco.cd.training.members.dto.MembersDto;
|
||||
import com.kamco.cd.training.postgres.entity.MemberEntity;
|
||||
import java.util.Optional;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.security.core.context.SecurityContextHolder;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
@Component
|
||||
@RequiredArgsConstructor
|
||||
public class UserUtil {
|
||||
|
||||
public MembersDto.Member getCurrentUser() {
|
||||
return Optional.ofNullable(SecurityContextHolder.getContext().getAuthentication())
|
||||
.filter(auth -> auth.getPrincipal() instanceof CustomUserDetails)
|
||||
.map(
|
||||
auth -> {
|
||||
CustomUserDetails user = (CustomUserDetails) auth.getPrincipal();
|
||||
MemberEntity m = user.getMember();
|
||||
return new MembersDto.Member(m.getId(), m.getName(), m.getEmployeeNo());
|
||||
})
|
||||
.orElse(null);
|
||||
}
|
||||
|
||||
public Long getId() {
|
||||
MembersDto.Member user = getCurrentUser();
|
||||
return user != null ? user.getId() : null;
|
||||
}
|
||||
|
||||
public String getName() {
|
||||
MembersDto.Member user = getCurrentUser();
|
||||
return user != null ? user.getName() : null;
|
||||
}
|
||||
|
||||
public String getEmployeeNo() {
|
||||
MembersDto.Member user = getCurrentUser();
|
||||
return user != null ? user.getEmployeeNo() : null;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,20 +1,20 @@
|
||||
package com.kamco.cd.training.common.utils.enums;
|
||||
|
||||
public class CodeDto {
|
||||
|
||||
private String code;
|
||||
private String name;
|
||||
|
||||
public CodeDto(String code, String name) {
|
||||
this.code = code;
|
||||
this.name = name;
|
||||
}
|
||||
|
||||
public String getCode() {
|
||||
return code;
|
||||
}
|
||||
|
||||
public String getName() {
|
||||
return name;
|
||||
}
|
||||
}
|
||||
package com.kamco.cd.training.common.utils.enums;
|
||||
|
||||
public class CodeDto {
|
||||
|
||||
private String code;
|
||||
private String name;
|
||||
|
||||
public CodeDto(String code, String name) {
|
||||
this.code = code;
|
||||
this.name = name;
|
||||
}
|
||||
|
||||
public String getCode() {
|
||||
return code;
|
||||
}
|
||||
|
||||
public String getName() {
|
||||
return name;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
package com.kamco.cd.training.common.utils.enums;
|
||||
|
||||
import java.lang.annotation.ElementType;
|
||||
import java.lang.annotation.Retention;
|
||||
import java.lang.annotation.RetentionPolicy;
|
||||
import java.lang.annotation.Target;
|
||||
|
||||
@Target(ElementType.TYPE)
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
public @interface CodeExpose {}
|
||||
package com.kamco.cd.training.common.utils.enums;
|
||||
|
||||
import java.lang.annotation.ElementType;
|
||||
import java.lang.annotation.Retention;
|
||||
import java.lang.annotation.RetentionPolicy;
|
||||
import java.lang.annotation.Target;
|
||||
|
||||
@Target(ElementType.TYPE)
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
public @interface CodeExpose {}
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
package com.kamco.cd.training.common.utils.enums;
|
||||
|
||||
public interface EnumType {
|
||||
|
||||
String getId();
|
||||
|
||||
String getText();
|
||||
}
|
||||
package com.kamco.cd.training.common.utils.enums;
|
||||
|
||||
public interface EnumType {
|
||||
|
||||
String getId();
|
||||
|
||||
String getText();
|
||||
}
|
||||
|
||||
@@ -1,26 +1,26 @@
|
||||
package com.kamco.cd.training.common.utils.enums;
|
||||
|
||||
import com.kamco.cd.training.common.utils.interfaces.EnumValid;
|
||||
import jakarta.validation.ConstraintValidator;
|
||||
import jakarta.validation.ConstraintValidatorContext;
|
||||
import java.util.Arrays;
|
||||
import java.util.Set;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
public class EnumValidator implements ConstraintValidator<EnumValid, String> {
|
||||
|
||||
private Set<String> acceptedValues;
|
||||
|
||||
@Override
|
||||
public void initialize(EnumValid constraintAnnotation) {
|
||||
acceptedValues =
|
||||
Arrays.stream(constraintAnnotation.enumClass().getEnumConstants())
|
||||
.map(Enum::name)
|
||||
.collect(Collectors.toSet());
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isValid(String value, ConstraintValidatorContext context) {
|
||||
return value != null && acceptedValues.contains(value);
|
||||
}
|
||||
}
|
||||
package com.kamco.cd.training.common.utils.enums;
|
||||
|
||||
import com.kamco.cd.training.common.utils.interfaces.EnumValid;
|
||||
import jakarta.validation.ConstraintValidator;
|
||||
import jakarta.validation.ConstraintValidatorContext;
|
||||
import java.util.Arrays;
|
||||
import java.util.Set;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
public class EnumValidator implements ConstraintValidator<EnumValid, String> {
|
||||
|
||||
private Set<String> acceptedValues;
|
||||
|
||||
@Override
|
||||
public void initialize(EnumValid constraintAnnotation) {
|
||||
acceptedValues =
|
||||
Arrays.stream(constraintAnnotation.enumClass().getEnumConstants())
|
||||
.map(Enum::name)
|
||||
.collect(Collectors.toSet());
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isValid(String value, ConstraintValidatorContext context) {
|
||||
return value != null && acceptedValues.contains(value);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,76 +1,76 @@
|
||||
package com.kamco.cd.training.common.utils.enums;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import org.reflections.Reflections;
|
||||
|
||||
public class Enums {
|
||||
|
||||
private static final String BASE_PACKAGE = "com.kamco.cd.training";
|
||||
|
||||
/** 노출 가능한 enum만 모아둔 맵 key: enum simpleName (예: RoleType) value: enum Class */
|
||||
private static final Map<String, Class<? extends Enum<?>>> exposedEnumMap = scanExposedEnumMap();
|
||||
|
||||
// code로 enum 찾기
|
||||
public static <E extends Enum<E> & EnumType> E fromId(Class<E> enumClass, String id) {
|
||||
if (id == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
for (E e : enumClass.getEnumConstants()) {
|
||||
if (id.equalsIgnoreCase(e.getId())) {
|
||||
return e;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
// enum -> CodeDto list
|
||||
public static List<CodeDto> toList(Class<? extends Enum<?>> enumClass) {
|
||||
Object[] enums = enumClass.getEnumConstants();
|
||||
|
||||
return Arrays.stream(enums)
|
||||
.map(e -> (EnumType) e)
|
||||
.map(e -> new CodeDto(e.getId(), e.getText()))
|
||||
.toList();
|
||||
}
|
||||
|
||||
/** 특정 타입(enum)만 조회 /codes/{type} -> type = RoleType 같은 값 */
|
||||
public static List<CodeDto> getCodes(String type) {
|
||||
Class<? extends Enum<?>> enumClass = exposedEnumMap.get(type);
|
||||
if (enumClass == null) {
|
||||
throw new IllegalArgumentException("지원하지 않는 코드 타입: " + type);
|
||||
}
|
||||
return toList(enumClass);
|
||||
}
|
||||
|
||||
/** 전체 enum 코드 조회 */
|
||||
public static Map<String, List<CodeDto>> getAllCodes() {
|
||||
Map<String, List<CodeDto>> result = new HashMap<>();
|
||||
for (Map.Entry<String, Class<? extends Enum<?>>> e : exposedEnumMap.entrySet()) {
|
||||
result.put(e.getKey(), toList(e.getValue()));
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* @CodeExpose + EnumType 인 enum만 스캔해서 Map 구성
|
||||
*/
|
||||
private static Map<String, Class<? extends Enum<?>>> scanExposedEnumMap() {
|
||||
Reflections reflections = new Reflections(BASE_PACKAGE);
|
||||
|
||||
Set<Class<?>> types = reflections.getTypesAnnotatedWith(CodeExpose.class);
|
||||
|
||||
Map<String, Class<? extends Enum<?>>> result = new HashMap<>();
|
||||
|
||||
for (Class<?> clazz : types) {
|
||||
if (clazz.isEnum() && EnumType.class.isAssignableFrom(clazz)) {
|
||||
result.put(clazz.getSimpleName(), (Class<? extends Enum<?>>) clazz);
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
}
|
||||
package com.kamco.cd.training.common.utils.enums;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import org.reflections.Reflections;
|
||||
|
||||
public class Enums {
|
||||
|
||||
private static final String BASE_PACKAGE = "com.kamco.cd.training";
|
||||
|
||||
/** 노출 가능한 enum만 모아둔 맵 key: enum simpleName (예: RoleType) value: enum Class */
|
||||
private static final Map<String, Class<? extends Enum<?>>> exposedEnumMap = scanExposedEnumMap();
|
||||
|
||||
// code로 enum 찾기
|
||||
public static <E extends Enum<E> & EnumType> E fromId(Class<E> enumClass, String id) {
|
||||
if (id == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
for (E e : enumClass.getEnumConstants()) {
|
||||
if (id.equalsIgnoreCase(e.getId())) {
|
||||
return e;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
// enum -> CodeDto list
|
||||
public static List<CodeDto> toList(Class<? extends Enum<?>> enumClass) {
|
||||
Object[] enums = enumClass.getEnumConstants();
|
||||
|
||||
return Arrays.stream(enums)
|
||||
.map(e -> (EnumType) e)
|
||||
.map(e -> new CodeDto(e.getId(), e.getText()))
|
||||
.toList();
|
||||
}
|
||||
|
||||
/** 특정 타입(enum)만 조회 /codes/{type} -> type = RoleType 같은 값 */
|
||||
public static List<CodeDto> getCodes(String type) {
|
||||
Class<? extends Enum<?>> enumClass = exposedEnumMap.get(type);
|
||||
if (enumClass == null) {
|
||||
throw new IllegalArgumentException("지원하지 않는 코드 타입: " + type);
|
||||
}
|
||||
return toList(enumClass);
|
||||
}
|
||||
|
||||
/** 전체 enum 코드 조회 */
|
||||
public static Map<String, List<CodeDto>> getAllCodes() {
|
||||
Map<String, List<CodeDto>> result = new HashMap<>();
|
||||
for (Map.Entry<String, Class<? extends Enum<?>>> e : exposedEnumMap.entrySet()) {
|
||||
result.put(e.getKey(), toList(e.getValue()));
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* @CodeExpose + EnumType 인 enum만 스캔해서 Map 구성
|
||||
*/
|
||||
private static Map<String, Class<? extends Enum<?>>> scanExposedEnumMap() {
|
||||
Reflections reflections = new Reflections(BASE_PACKAGE);
|
||||
|
||||
Set<Class<?>> types = reflections.getTypesAnnotatedWith(CodeExpose.class);
|
||||
|
||||
Map<String, Class<? extends Enum<?>>> result = new HashMap<>();
|
||||
|
||||
for (Class<?> clazz : types) {
|
||||
if (clazz.isEnum() && EnumType.class.isAssignableFrom(clazz)) {
|
||||
result.put(clazz.getSimpleName(), (Class<? extends Enum<?>>) clazz);
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,36 +1,36 @@
|
||||
package com.kamco.cd.training.common.utils.geometry;
|
||||
|
||||
import com.fasterxml.jackson.core.JacksonException;
|
||||
import com.fasterxml.jackson.core.JsonParser;
|
||||
import com.fasterxml.jackson.databind.DeserializationContext;
|
||||
import com.fasterxml.jackson.databind.deser.std.StdDeserializer;
|
||||
import java.io.IOException;
|
||||
import org.locationtech.jts.geom.Geometry;
|
||||
import org.locationtech.jts.io.geojson.GeoJsonReader;
|
||||
import org.springframework.util.StringUtils;
|
||||
|
||||
public class GeometryDeserializer<T extends Geometry> extends StdDeserializer<T> {
|
||||
|
||||
public GeometryDeserializer(Class<T> targetType) {
|
||||
super(targetType);
|
||||
}
|
||||
|
||||
// TODO: test code
|
||||
@SuppressWarnings("unchecked")
|
||||
@Override
|
||||
public T deserialize(JsonParser jsonParser, DeserializationContext deserializationContext)
|
||||
throws IOException, JacksonException {
|
||||
String json = jsonParser.readValueAsTree().toString();
|
||||
|
||||
if (!StringUtils.hasText(json)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
GeoJsonReader reader = new GeoJsonReader();
|
||||
return (T) reader.read(json);
|
||||
} catch (Exception e) {
|
||||
throw new IllegalArgumentException("Failed to deserialize GeoJSON into Geometry", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
package com.kamco.cd.training.common.utils.geometry;
|
||||
|
||||
import com.fasterxml.jackson.core.JacksonException;
|
||||
import com.fasterxml.jackson.core.JsonParser;
|
||||
import com.fasterxml.jackson.databind.DeserializationContext;
|
||||
import com.fasterxml.jackson.databind.deser.std.StdDeserializer;
|
||||
import java.io.IOException;
|
||||
import org.locationtech.jts.geom.Geometry;
|
||||
import org.locationtech.jts.io.geojson.GeoJsonReader;
|
||||
import org.springframework.util.StringUtils;
|
||||
|
||||
public class GeometryDeserializer<T extends Geometry> extends StdDeserializer<T> {
|
||||
|
||||
public GeometryDeserializer(Class<T> targetType) {
|
||||
super(targetType);
|
||||
}
|
||||
|
||||
// TODO: test code
|
||||
@SuppressWarnings("unchecked")
|
||||
@Override
|
||||
public T deserialize(JsonParser jsonParser, DeserializationContext deserializationContext)
|
||||
throws IOException, JacksonException {
|
||||
String json = jsonParser.readValueAsTree().toString();
|
||||
|
||||
if (!StringUtils.hasText(json)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
GeoJsonReader reader = new GeoJsonReader();
|
||||
return (T) reader.read(json);
|
||||
} catch (Exception e) {
|
||||
throw new IllegalArgumentException("Failed to deserialize GeoJSON into Geometry", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,31 +1,31 @@
|
||||
package com.kamco.cd.training.common.utils.geometry;
|
||||
|
||||
import com.fasterxml.jackson.core.JsonGenerator;
|
||||
import com.fasterxml.jackson.databind.SerializerProvider;
|
||||
import com.fasterxml.jackson.databind.ser.std.StdSerializer;
|
||||
import java.io.IOException;
|
||||
import java.util.Objects;
|
||||
import org.locationtech.jts.geom.Geometry;
|
||||
import org.locationtech.jts.io.geojson.GeoJsonWriter;
|
||||
|
||||
public class GeometrySerializer<T extends Geometry> extends StdSerializer<T> {
|
||||
|
||||
// TODO: test code
|
||||
public GeometrySerializer(Class<T> targetType) {
|
||||
super(targetType);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void serialize(
|
||||
T geometry, JsonGenerator jsonGenerator, SerializerProvider serializerProvider)
|
||||
throws IOException {
|
||||
if (Objects.nonNull(geometry)) {
|
||||
// default: 8자리 강제로 반올림시킴. 16자리로 늘려줌
|
||||
GeoJsonWriter writer = new GeoJsonWriter(16);
|
||||
String json = writer.write(geometry);
|
||||
jsonGenerator.writeRawValue(json);
|
||||
} else {
|
||||
jsonGenerator.writeNull();
|
||||
}
|
||||
}
|
||||
}
|
||||
package com.kamco.cd.training.common.utils.geometry;
|
||||
|
||||
import com.fasterxml.jackson.core.JsonGenerator;
|
||||
import com.fasterxml.jackson.databind.SerializerProvider;
|
||||
import com.fasterxml.jackson.databind.ser.std.StdSerializer;
|
||||
import java.io.IOException;
|
||||
import java.util.Objects;
|
||||
import org.locationtech.jts.geom.Geometry;
|
||||
import org.locationtech.jts.io.geojson.GeoJsonWriter;
|
||||
|
||||
public class GeometrySerializer<T extends Geometry> extends StdSerializer<T> {
|
||||
|
||||
// TODO: test code
|
||||
public GeometrySerializer(Class<T> targetType) {
|
||||
super(targetType);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void serialize(
|
||||
T geometry, JsonGenerator jsonGenerator, SerializerProvider serializerProvider)
|
||||
throws IOException {
|
||||
if (Objects.nonNull(geometry)) {
|
||||
// default: 8자리 강제로 반올림시킴. 16자리로 늘려줌
|
||||
GeoJsonWriter writer = new GeoJsonWriter(16);
|
||||
String json = writer.write(geometry);
|
||||
jsonGenerator.writeRawValue(json);
|
||||
} else {
|
||||
jsonGenerator.writeNull();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,18 +1,18 @@
|
||||
package com.kamco.cd.training.common.utils.html;
|
||||
|
||||
import com.fasterxml.jackson.core.JacksonException;
|
||||
import com.fasterxml.jackson.core.JsonParser;
|
||||
import com.fasterxml.jackson.databind.DeserializationContext;
|
||||
import com.fasterxml.jackson.databind.JsonDeserializer;
|
||||
import java.io.IOException;
|
||||
import org.springframework.web.util.HtmlUtils;
|
||||
|
||||
public class HtmlEscapeDeserializer extends JsonDeserializer<Object> {
|
||||
|
||||
@Override
|
||||
public Object deserialize(JsonParser jsonParser, DeserializationContext deserializationContext)
|
||||
throws IOException, JacksonException {
|
||||
String value = jsonParser.getValueAsString();
|
||||
return value == null ? null : HtmlUtils.htmlEscape(value);
|
||||
}
|
||||
}
|
||||
package com.kamco.cd.training.common.utils.html;
|
||||
|
||||
import com.fasterxml.jackson.core.JacksonException;
|
||||
import com.fasterxml.jackson.core.JsonParser;
|
||||
import com.fasterxml.jackson.databind.DeserializationContext;
|
||||
import com.fasterxml.jackson.databind.JsonDeserializer;
|
||||
import java.io.IOException;
|
||||
import org.springframework.web.util.HtmlUtils;
|
||||
|
||||
public class HtmlEscapeDeserializer extends JsonDeserializer<Object> {
|
||||
|
||||
@Override
|
||||
public Object deserialize(JsonParser jsonParser, DeserializationContext deserializationContext)
|
||||
throws IOException, JacksonException {
|
||||
String value = jsonParser.getValueAsString();
|
||||
return value == null ? null : HtmlUtils.htmlEscape(value);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,20 +1,20 @@
|
||||
package com.kamco.cd.training.common.utils.html;
|
||||
|
||||
import com.fasterxml.jackson.core.JsonGenerator;
|
||||
import com.fasterxml.jackson.databind.JsonSerializer;
|
||||
import com.fasterxml.jackson.databind.SerializerProvider;
|
||||
import java.io.IOException;
|
||||
import org.springframework.web.util.HtmlUtils;
|
||||
|
||||
public class HtmlUnescapeSerializer extends JsonSerializer<String> {
|
||||
@Override
|
||||
public void serialize(
|
||||
String value, JsonGenerator jsonGenerator, SerializerProvider serializerProvider)
|
||||
throws IOException {
|
||||
if (value == null) {
|
||||
jsonGenerator.writeNull();
|
||||
} else {
|
||||
jsonGenerator.writeString(HtmlUtils.htmlUnescape(value));
|
||||
}
|
||||
}
|
||||
}
|
||||
package com.kamco.cd.training.common.utils.html;
|
||||
|
||||
import com.fasterxml.jackson.core.JsonGenerator;
|
||||
import com.fasterxml.jackson.databind.JsonSerializer;
|
||||
import com.fasterxml.jackson.databind.SerializerProvider;
|
||||
import java.io.IOException;
|
||||
import org.springframework.web.util.HtmlUtils;
|
||||
|
||||
public class HtmlUnescapeSerializer extends JsonSerializer<String> {
|
||||
@Override
|
||||
public void serialize(
|
||||
String value, JsonGenerator jsonGenerator, SerializerProvider serializerProvider)
|
||||
throws IOException {
|
||||
if (value == null) {
|
||||
jsonGenerator.writeNull();
|
||||
} else {
|
||||
jsonGenerator.writeString(HtmlUtils.htmlUnescape(value));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,23 +1,23 @@
|
||||
package com.kamco.cd.training.common.utils.interfaces;
|
||||
|
||||
import com.kamco.cd.training.common.utils.enums.EnumValidator;
|
||||
import jakarta.validation.Constraint;
|
||||
import jakarta.validation.Payload;
|
||||
import java.lang.annotation.ElementType;
|
||||
import java.lang.annotation.Retention;
|
||||
import java.lang.annotation.RetentionPolicy;
|
||||
import java.lang.annotation.Target;
|
||||
|
||||
@Target(ElementType.FIELD)
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
@Constraint(validatedBy = EnumValidator.class)
|
||||
public @interface EnumValid {
|
||||
|
||||
String message() default "올바르지 않은 값입니다.";
|
||||
|
||||
Class<?>[] groups() default {};
|
||||
|
||||
Class<? extends Payload>[] payload() default {};
|
||||
|
||||
Class<? extends Enum<?>> enumClass();
|
||||
}
|
||||
package com.kamco.cd.training.common.utils.interfaces;
|
||||
|
||||
import com.kamco.cd.training.common.utils.enums.EnumValidator;
|
||||
import jakarta.validation.Constraint;
|
||||
import jakarta.validation.Payload;
|
||||
import java.lang.annotation.ElementType;
|
||||
import java.lang.annotation.Retention;
|
||||
import java.lang.annotation.RetentionPolicy;
|
||||
import java.lang.annotation.Target;
|
||||
|
||||
@Target(ElementType.FIELD)
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
@Constraint(validatedBy = EnumValidator.class)
|
||||
public @interface EnumValid {
|
||||
|
||||
String message() default "올바르지 않은 값입니다.";
|
||||
|
||||
Class<?>[] groups() default {};
|
||||
|
||||
Class<? extends Payload>[] payload() default {};
|
||||
|
||||
Class<? extends Enum<?>> enumClass();
|
||||
}
|
||||
|
||||
@@ -1,15 +1,15 @@
|
||||
package com.kamco.cd.training.common.utils.interfaces;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JacksonAnnotationsInside;
|
||||
import com.fasterxml.jackson.annotation.JsonFormat;
|
||||
import java.lang.annotation.*;
|
||||
|
||||
@Target({ElementType.FIELD, ElementType.METHOD})
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
@Documented
|
||||
@JacksonAnnotationsInside
|
||||
@JsonFormat(
|
||||
shape = JsonFormat.Shape.STRING,
|
||||
pattern = "yyyy-MM-dd'T'HH:mm:ssXXX",
|
||||
timezone = "Asia/Seoul")
|
||||
public @interface JsonFormatDttm {}
|
||||
package com.kamco.cd.training.common.utils.interfaces;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JacksonAnnotationsInside;
|
||||
import com.fasterxml.jackson.annotation.JsonFormat;
|
||||
import java.lang.annotation.*;
|
||||
|
||||
@Target({ElementType.FIELD, ElementType.METHOD})
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
@Documented
|
||||
@JacksonAnnotationsInside
|
||||
@JsonFormat(
|
||||
shape = JsonFormat.Shape.STRING,
|
||||
pattern = "yyyy-MM-dd'T'HH:mm:ssXXX",
|
||||
timezone = "Asia/Seoul")
|
||||
public @interface JsonFormatDttm {}
|
||||
|
||||
37
src/main/java/com/kamco/cd/training/config/AsyncConfig.java
Normal file
37
src/main/java/com/kamco/cd/training/config/AsyncConfig.java
Normal file
@@ -0,0 +1,37 @@
|
||||
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;
|
||||
}
|
||||
|
||||
@Bean("datasetExecutor")
|
||||
public Executor datasetExecutor() {
|
||||
|
||||
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
|
||||
|
||||
executor.setCorePoolSize(5);
|
||||
executor.setMaxPoolSize(10);
|
||||
executor.setQueueCapacity(100);
|
||||
executor.setThreadNamePrefix("dataset-");
|
||||
|
||||
executor.initialize();
|
||||
return executor;
|
||||
}
|
||||
}
|
||||
@@ -1,11 +1,11 @@
|
||||
package com.kamco.cd.training.config;
|
||||
|
||||
import org.springframework.cache.annotation.EnableCaching;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
|
||||
@EnableCaching
|
||||
@Configuration
|
||||
public class CacheConfig {
|
||||
// training 서버는 Redis 사용하지 않고 Spring Boot 메모리 캐시를 사용함
|
||||
// => org.springframework.cache.annotation.Cacheable
|
||||
}
|
||||
package com.kamco.cd.training.config;
|
||||
|
||||
import org.springframework.cache.annotation.EnableCaching;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
|
||||
@EnableCaching
|
||||
@Configuration
|
||||
public class CacheConfig {
|
||||
// training 서버는 Redis 사용하지 않고 Spring Boot 메모리 캐시를 사용함
|
||||
// => org.springframework.cache.annotation.Cacheable
|
||||
}
|
||||
|
||||
@@ -1,27 +1,27 @@
|
||||
package com.kamco.cd.training.config;
|
||||
|
||||
import lombok.Getter;
|
||||
import lombok.Setter;
|
||||
import org.springframework.boot.context.properties.ConfigurationProperties;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
/** GeoJSON 파일 모니터링 설정 */
|
||||
@Component
|
||||
@ConfigurationProperties(prefix = "file.config")
|
||||
@Getter
|
||||
@Setter
|
||||
public class FileConfig {
|
||||
|
||||
// private String rootDir = "D:\\app/";
|
||||
private String rootDir = "/app/";
|
||||
private String rootSyncDir = rootDir + "original-images/";
|
||||
private String tmpSyncDir = rootSyncDir + "tmp/";
|
||||
|
||||
private String dataSetDir = rootDir + "dataset/";
|
||||
private String tmpDataSetDir = dataSetDir + "tmp/";
|
||||
|
||||
// private String rootSyncDir = "/app/original-images/";
|
||||
// private String tmpSyncDir = rootSyncDir + "tmp/";
|
||||
|
||||
private String syncFileExt = "tfw,tif";
|
||||
}
|
||||
package com.kamco.cd.training.config;
|
||||
|
||||
import lombok.Getter;
|
||||
import lombok.Setter;
|
||||
import org.springframework.boot.context.properties.ConfigurationProperties;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
/** GeoJSON 파일 모니터링 설정 */
|
||||
@Component
|
||||
@ConfigurationProperties(prefix = "file.config")
|
||||
@Getter
|
||||
@Setter
|
||||
public class FileConfig {
|
||||
|
||||
// private String rootDir = "D:\\app/";
|
||||
private String rootDir = "/app/";
|
||||
private String rootSyncDir = rootDir + "original-images/";
|
||||
private String tmpSyncDir = rootSyncDir + "tmp/";
|
||||
|
||||
private String dataSetDir = rootDir + "dataset/";
|
||||
private String tmpDataSetDir = dataSetDir + "tmp/";
|
||||
|
||||
// private String rootSyncDir = "/app/original-images/";
|
||||
// private String tmpSyncDir = rootSyncDir + "tmp/";
|
||||
|
||||
private String syncFileExt = "tfw,tif";
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,77 +1,77 @@
|
||||
package com.kamco.cd.training.config;
|
||||
|
||||
import io.swagger.v3.oas.models.Components;
|
||||
import io.swagger.v3.oas.models.OpenAPI;
|
||||
import io.swagger.v3.oas.models.info.Info;
|
||||
import io.swagger.v3.oas.models.security.SecurityRequirement;
|
||||
import io.swagger.v3.oas.models.security.SecurityScheme;
|
||||
import io.swagger.v3.oas.models.servers.Server;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
|
||||
@Configuration
|
||||
public class OpenApiConfig {
|
||||
|
||||
@Value("${swagger.local-port}")
|
||||
private String localPort;
|
||||
|
||||
@Value("${spring.profiles.active:local}")
|
||||
private String profile;
|
||||
|
||||
@Value("${swagger.dev-url:https://kamco.training-dev-api.gs.dabeeo.com}")
|
||||
private String devUrl;
|
||||
|
||||
@Value("${swagger.prod-url:https://api.training-kamco.com}")
|
||||
private String prodUrl;
|
||||
|
||||
@Bean
|
||||
public OpenAPI kamcoOpenAPI() {
|
||||
// 1) SecurityScheme 정의 (Bearer JWT)
|
||||
SecurityScheme bearerAuth =
|
||||
new SecurityScheme()
|
||||
.type(SecurityScheme.Type.HTTP)
|
||||
.scheme("bearer")
|
||||
.bearerFormat("JWT")
|
||||
.in(SecurityScheme.In.HEADER)
|
||||
.name("Authorization");
|
||||
|
||||
// 2) SecurityRequirement (기본으로 BearerAuth 사용)
|
||||
SecurityRequirement securityRequirement = new SecurityRequirement().addList("BearerAuth");
|
||||
|
||||
// 3) Components 에 SecurityScheme 등록
|
||||
Components components = new Components().addSecuritySchemes("BearerAuth", bearerAuth);
|
||||
|
||||
// profile 별 server url 분기
|
||||
List<Server> servers = new ArrayList<>();
|
||||
if ("dev".equals(profile)) {
|
||||
servers.add(new Server().url(devUrl).description("개발 서버"));
|
||||
servers.add(new Server().url("http://localhost:" + localPort).description("로컬 서버"));
|
||||
// servers.add(new Server().url(prodUrl).description("운영 서버"));
|
||||
} else if ("prod".equals(profile)) {
|
||||
// servers.add(new Server().url(prodUrl).description("운영 서버"));
|
||||
servers.add(new Server().url("http://localhost:" + localPort).description("로컬 서버"));
|
||||
servers.add(new Server().url(devUrl).description("개발 서버"));
|
||||
} else {
|
||||
servers.add(new Server().url("http://localhost:" + localPort).description("로컬 서버"));
|
||||
servers.add(new Server().url(devUrl).description("개발 서버"));
|
||||
// servers.add(new Server().url(prodUrl).description("운영 서버"));
|
||||
}
|
||||
|
||||
return new OpenAPI()
|
||||
.info(
|
||||
new Info()
|
||||
.title("KAMCO Change Detection API")
|
||||
.description(
|
||||
"KAMCO 변화 탐지 시스템 API 문서\n\n"
|
||||
+ "이 API는 지리공간 데이터를 활용한 변화 탐지 시스템을 제공합니다.\n"
|
||||
+ "GeoJSON 형식의 공간 데이터를 처리하며, PostgreSQL/PostGIS 기반으로 동작합니다.")
|
||||
.version("v1.0.0"))
|
||||
.servers(servers)
|
||||
// 만들어둔 components를 넣어야 함
|
||||
.components(components)
|
||||
.addSecurityItem(securityRequirement);
|
||||
}
|
||||
}
|
||||
package com.kamco.cd.training.config;
|
||||
|
||||
import io.swagger.v3.oas.models.Components;
|
||||
import io.swagger.v3.oas.models.OpenAPI;
|
||||
import io.swagger.v3.oas.models.info.Info;
|
||||
import io.swagger.v3.oas.models.security.SecurityRequirement;
|
||||
import io.swagger.v3.oas.models.security.SecurityScheme;
|
||||
import io.swagger.v3.oas.models.servers.Server;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
|
||||
@Configuration
|
||||
public class OpenApiConfig {
|
||||
|
||||
@Value("${swagger.local-port}")
|
||||
private String localPort;
|
||||
|
||||
@Value("${spring.profiles.active:local}")
|
||||
private String profile;
|
||||
|
||||
@Value("${swagger.dev-url:https://kamco.training-dev-api.gs.dabeeo.com}")
|
||||
private String devUrl;
|
||||
|
||||
@Value("${swagger.prod-url:https://api.train-kamco.com}")
|
||||
private String prodUrl;
|
||||
|
||||
@Bean
|
||||
public OpenAPI kamcoOpenAPI() {
|
||||
// 1) SecurityScheme 정의 (Bearer JWT)
|
||||
SecurityScheme bearerAuth =
|
||||
new SecurityScheme()
|
||||
.type(SecurityScheme.Type.HTTP)
|
||||
.scheme("bearer")
|
||||
.bearerFormat("JWT")
|
||||
.in(SecurityScheme.In.HEADER)
|
||||
.name("Authorization");
|
||||
|
||||
// 2) SecurityRequirement (기본으로 BearerAuth 사용)
|
||||
SecurityRequirement securityRequirement = new SecurityRequirement().addList("BearerAuth");
|
||||
|
||||
// 3) Components 에 SecurityScheme 등록
|
||||
Components components = new Components().addSecuritySchemes("BearerAuth", bearerAuth);
|
||||
|
||||
// profile 별 server url 분기
|
||||
List<Server> servers = new ArrayList<>();
|
||||
if ("dev".equals(profile)) {
|
||||
servers.add(new Server().url(devUrl).description("개발 서버"));
|
||||
servers.add(new Server().url("http://localhost:" + localPort).description("로컬 서버"));
|
||||
// servers.add(new Server().url(prodUrl).description("운영 서버"));
|
||||
} else if ("prod".equals(profile)) {
|
||||
// servers.add(new Server().url(prodUrl).description("운영 서버"));
|
||||
servers.add(new Server().url("http://localhost:" + localPort).description("로컬 서버"));
|
||||
servers.add(new Server().url(prodUrl).description("개발 서버"));
|
||||
} else {
|
||||
servers.add(new Server().url("http://localhost:" + localPort).description("로컬 서버"));
|
||||
servers.add(new Server().url(devUrl).description("운영 서버"));
|
||||
// servers.add(new Server().url(prodUrl).description("운영 서버"));
|
||||
}
|
||||
|
||||
return new OpenAPI()
|
||||
.info(
|
||||
new Info()
|
||||
.title("KAMCO Change Detection API")
|
||||
.description(
|
||||
"KAMCO 변화 탐지 시스템 API 문서\n\n"
|
||||
+ "이 API는 지리공간 데이터를 활용한 변화 탐지 시스템을 제공합니다.\n"
|
||||
+ "GeoJSON 형식의 공간 데이터를 처리하며, PostgreSQL/PostGIS 기반으로 동작합니다.")
|
||||
.version("v1.0.0"))
|
||||
.servers(servers)
|
||||
// 만들어둔 components를 넣어야 함
|
||||
.components(components)
|
||||
.addSecurityItem(securityRequirement);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,136 +1,142 @@
|
||||
package com.kamco.cd.training.config;
|
||||
|
||||
import com.kamco.cd.training.auth.CustomAuthenticationProvider;
|
||||
import com.kamco.cd.training.auth.JwtAuthenticationFilter;
|
||||
import java.util.List;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.http.HttpMethod;
|
||||
import org.springframework.security.authentication.AuthenticationManager;
|
||||
import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration;
|
||||
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
|
||||
import org.springframework.security.config.annotation.web.configuration.WebSecurityCustomizer;
|
||||
import org.springframework.security.config.http.SessionCreationPolicy;
|
||||
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
|
||||
import org.springframework.security.crypto.password.PasswordEncoder;
|
||||
import org.springframework.security.web.SecurityFilterChain;
|
||||
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
|
||||
import org.springframework.security.web.firewall.HttpFirewall;
|
||||
import org.springframework.security.web.firewall.StrictHttpFirewall;
|
||||
import org.springframework.web.cors.CorsConfiguration;
|
||||
import org.springframework.web.cors.CorsConfigurationSource;
|
||||
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
|
||||
|
||||
@Configuration
|
||||
@EnableWebSecurity
|
||||
public class SecurityConfig {
|
||||
|
||||
@Bean
|
||||
public SecurityFilterChain securityFilterChain(
|
||||
org.springframework.security.config.annotation.web.builders.HttpSecurity http,
|
||||
JwtAuthenticationFilter jwtAuthenticationFilter,
|
||||
CustomAuthenticationProvider customAuthenticationProvider)
|
||||
throws Exception {
|
||||
|
||||
http.cors(cors -> cors.configurationSource(corsConfigurationSource()))
|
||||
.csrf(csrf -> csrf.disable())
|
||||
.sessionManagement(sm -> sm.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
|
||||
.formLogin(form -> form.disable())
|
||||
|
||||
// /monitor 에서 Basic 인증을 쓰려면 disable 하면 안됨
|
||||
.httpBasic(basic -> {})
|
||||
.logout(logout -> logout.disable())
|
||||
.authenticationProvider(customAuthenticationProvider)
|
||||
.authorizeHttpRequests(
|
||||
auth ->
|
||||
auth
|
||||
|
||||
// monitor
|
||||
.requestMatchers("/monitor/health", "/monitor/health/**")
|
||||
.permitAll()
|
||||
.requestMatchers("/monitor/**")
|
||||
.authenticated() // Basic으로 인증되게끔
|
||||
|
||||
// mapsheet
|
||||
.requestMatchers("/api/mapsheet/**")
|
||||
.permitAll()
|
||||
.requestMatchers(HttpMethod.POST, "/api/mapsheet/upload")
|
||||
.permitAll()
|
||||
|
||||
// test role
|
||||
.requestMatchers("/api/test/admin")
|
||||
.hasRole("ADMIN")
|
||||
.requestMatchers("/api/test/label")
|
||||
.hasAnyRole("ADMIN", "LABELER")
|
||||
.requestMatchers("/api/test/review")
|
||||
.hasAnyRole("ADMIN", "REVIEWER")
|
||||
|
||||
// common permit
|
||||
.requestMatchers("/error")
|
||||
.permitAll()
|
||||
.requestMatchers(HttpMethod.OPTIONS, "/**")
|
||||
.permitAll()
|
||||
.requestMatchers(
|
||||
"/api/auth/signin",
|
||||
"/api/auth/refresh",
|
||||
"/api/auth/logout",
|
||||
"/swagger-ui/**",
|
||||
"/v3/api-docs/**",
|
||||
"/api/members/*/password",
|
||||
"/api/upload/chunk-upload-dataset",
|
||||
"/api/upload/chunk-upload-complete")
|
||||
.permitAll()
|
||||
|
||||
// default
|
||||
.anyRequest()
|
||||
.authenticated())
|
||||
|
||||
// JWT 필터는 앞단에
|
||||
.addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class);
|
||||
|
||||
return http.build();
|
||||
}
|
||||
|
||||
@Bean
|
||||
public AuthenticationManager authenticationManager(AuthenticationConfiguration configuration)
|
||||
throws Exception {
|
||||
return configuration.getAuthenticationManager();
|
||||
}
|
||||
|
||||
@Bean
|
||||
public PasswordEncoder passwordEncoder() {
|
||||
return new BCryptPasswordEncoder();
|
||||
}
|
||||
|
||||
/** CORS 설정 */
|
||||
@Bean
|
||||
public CorsConfigurationSource corsConfigurationSource() {
|
||||
CorsConfiguration config = new CorsConfiguration(); // CORS 객체 생성
|
||||
config.setAllowedOriginPatterns(List.of("*")); // 도메인 허용
|
||||
config.setAllowedMethods(List.of("GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS"));
|
||||
config.setAllowedHeaders(List.of("*")); // 헤더요청 Authorization, Content-Type, X-Custom-Header
|
||||
config.setAllowCredentials(true); // 쿠키, Authorization 헤더, Bearer Token 등 자격증명 포함 요청을 허용할지 설정
|
||||
config.setExposedHeaders(List.of("Content-Disposition"));
|
||||
|
||||
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
|
||||
/** "/**" → 모든 API 경로에 대해 이 CORS 규칙을 적용 /api/** 같이 특정 경로만 지정 가능. */
|
||||
source.registerCorsConfiguration("/**", config); // CORS 정책을 등록
|
||||
return source;
|
||||
}
|
||||
|
||||
@Bean
|
||||
public HttpFirewall httpFirewall() {
|
||||
StrictHttpFirewall firewall = new StrictHttpFirewall();
|
||||
firewall.setAllowUrlEncodedSlash(true);
|
||||
firewall.setAllowUrlEncodedDoubleSlash(true);
|
||||
firewall.setAllowUrlEncodedPercent(true);
|
||||
firewall.setAllowSemicolon(true);
|
||||
return firewall;
|
||||
}
|
||||
|
||||
/** 완전 제외(필터 자체를 안 탐) */
|
||||
@Bean
|
||||
public WebSecurityCustomizer webSecurityCustomizer() {
|
||||
return (web) -> web.ignoring().requestMatchers("/api/mapsheet/**");
|
||||
}
|
||||
}
|
||||
package com.kamco.cd.training.config;
|
||||
|
||||
import com.kamco.cd.training.auth.CustomAuthenticationProvider;
|
||||
import com.kamco.cd.training.auth.JwtAuthenticationFilter;
|
||||
import java.util.List;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.http.HttpMethod;
|
||||
import org.springframework.security.authentication.AuthenticationManager;
|
||||
import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration;
|
||||
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
|
||||
import org.springframework.security.config.annotation.web.configuration.WebSecurityCustomizer;
|
||||
import org.springframework.security.config.http.SessionCreationPolicy;
|
||||
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
|
||||
import org.springframework.security.crypto.password.PasswordEncoder;
|
||||
import org.springframework.security.web.SecurityFilterChain;
|
||||
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
|
||||
import org.springframework.security.web.firewall.HttpFirewall;
|
||||
import org.springframework.security.web.firewall.StrictHttpFirewall;
|
||||
import org.springframework.web.cors.CorsConfiguration;
|
||||
import org.springframework.web.cors.CorsConfigurationSource;
|
||||
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
|
||||
|
||||
@Configuration
|
||||
@EnableWebSecurity
|
||||
public class SecurityConfig {
|
||||
|
||||
@Bean
|
||||
public SecurityFilterChain securityFilterChain(
|
||||
org.springframework.security.config.annotation.web.builders.HttpSecurity http,
|
||||
JwtAuthenticationFilter jwtAuthenticationFilter,
|
||||
CustomAuthenticationProvider customAuthenticationProvider)
|
||||
throws Exception {
|
||||
|
||||
http.cors(cors -> cors.configurationSource(corsConfigurationSource()))
|
||||
.csrf(csrf -> csrf.disable())
|
||||
.sessionManagement(sm -> sm.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
|
||||
.formLogin(form -> form.disable())
|
||||
|
||||
// /monitor 에서 Basic 인증을 쓰려면 disable 하면 안됨
|
||||
.httpBasic(basic -> {})
|
||||
.logout(logout -> logout.disable())
|
||||
.authenticationProvider(customAuthenticationProvider)
|
||||
.authorizeHttpRequests(
|
||||
auth ->
|
||||
auth
|
||||
|
||||
// monitor
|
||||
.requestMatchers("/monitor/health", "/monitor/health/**")
|
||||
.permitAll()
|
||||
.requestMatchers("/monitor/**")
|
||||
.authenticated() // Basic으로 인증되게끔
|
||||
|
||||
// mapsheet
|
||||
.requestMatchers("/api/mapsheet/**")
|
||||
.permitAll()
|
||||
.requestMatchers(HttpMethod.POST, "/api/mapsheet/upload")
|
||||
.permitAll()
|
||||
|
||||
// test role
|
||||
.requestMatchers("/api/test/admin")
|
||||
.hasRole("ADMIN")
|
||||
.requestMatchers("/api/test/label")
|
||||
.hasAnyRole("ADMIN", "LABELER")
|
||||
.requestMatchers("/api/test/review")
|
||||
.hasAnyRole("ADMIN", "REVIEWER")
|
||||
|
||||
// common permit
|
||||
.requestMatchers("/error")
|
||||
.permitAll()
|
||||
.requestMatchers(HttpMethod.OPTIONS, "/**")
|
||||
.permitAll()
|
||||
.requestMatchers(
|
||||
"/api/auth/signin",
|
||||
"/api/auth/refresh",
|
||||
"/api/auth/logout",
|
||||
"/swagger-ui/**",
|
||||
"/v3/api-docs/**",
|
||||
"/api/upload/chunk-upload-dataset",
|
||||
"/api/upload/chunk-upload-complete",
|
||||
"/download_progress_test.html",
|
||||
"/api/models/download/**")
|
||||
.permitAll()
|
||||
.requestMatchers("/api/members/*/password")
|
||||
.authenticated()
|
||||
// default
|
||||
.anyRequest()
|
||||
.authenticated())
|
||||
|
||||
// JWT 필터는 앞단에
|
||||
.addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class);
|
||||
|
||||
return http.build();
|
||||
}
|
||||
|
||||
@Bean
|
||||
public AuthenticationManager authenticationManager(AuthenticationConfiguration configuration)
|
||||
throws Exception {
|
||||
return configuration.getAuthenticationManager();
|
||||
}
|
||||
|
||||
@Bean
|
||||
public PasswordEncoder passwordEncoder() {
|
||||
return new BCryptPasswordEncoder();
|
||||
}
|
||||
|
||||
/** CORS 설정 - application.yml에서 환경별로 관리 */
|
||||
@Bean
|
||||
public CorsConfigurationSource corsConfigurationSource() {
|
||||
CorsConfiguration config = new CorsConfiguration(); // CORS 객체 생성
|
||||
|
||||
// application.yml에서 환경별로 설정된 도메인 사용
|
||||
config.setAllowedOriginPatterns(List.of("*")); // 도메인 허용
|
||||
|
||||
config.setAllowedMethods(List.of("GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS"));
|
||||
config.setAllowedHeaders(List.of("*")); // 헤더요청 Authorization, Content-Type, X-Custom-Header
|
||||
config.setAllowCredentials(true); // 쿠키, Authorization 헤더, Bearer Token 등 자격증명 포함 요청을 허용할지 설정
|
||||
config.setExposedHeaders(List.of("Content-Disposition", "Authorization"));
|
||||
config.setMaxAge(3600L); // Preflight 요청 캐시 (1시간)
|
||||
|
||||
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
|
||||
/** "/**" → 모든 API 경로에 대해 이 CORS 규칙을 적용 /api/** 같이 특정 경로만 지정 가능. */
|
||||
source.registerCorsConfiguration("/**", config); // CORS 정책을 등록
|
||||
return source;
|
||||
}
|
||||
|
||||
@Bean
|
||||
public HttpFirewall httpFirewall() {
|
||||
StrictHttpFirewall firewall = new StrictHttpFirewall();
|
||||
firewall.setAllowUrlEncodedSlash(true);
|
||||
firewall.setAllowUrlEncodedDoubleSlash(true);
|
||||
firewall.setAllowUrlEncodedPercent(true);
|
||||
firewall.setAllowSemicolon(true);
|
||||
return firewall;
|
||||
}
|
||||
|
||||
/** 완전 제외(필터 자체를 안 탐) */
|
||||
@Bean
|
||||
public WebSecurityCustomizer webSecurityCustomizer() {
|
||||
return (web) -> web.ignoring().requestMatchers("/api/mapsheet/**");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,96 +1,96 @@
|
||||
package com.kamco.cd.training.config;
|
||||
|
||||
import com.zaxxer.hikari.HikariDataSource;
|
||||
import javax.sql.DataSource;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.boot.context.event.ApplicationReadyEvent;
|
||||
import org.springframework.context.event.EventListener;
|
||||
import org.springframework.core.env.Environment;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
@Slf4j
|
||||
@Component
|
||||
@RequiredArgsConstructor
|
||||
public class StartupLogger {
|
||||
|
||||
private final Environment environment;
|
||||
private final DataSource dataSource;
|
||||
|
||||
@EventListener(ApplicationReadyEvent.class)
|
||||
public void logStartupInfo() {
|
||||
String[] activeProfiles = environment.getActiveProfiles();
|
||||
String profileInfo = activeProfiles.length > 0 ? String.join(", ", activeProfiles) : "default";
|
||||
|
||||
// Database connection information
|
||||
String dbUrl = environment.getProperty("spring.datasource.url");
|
||||
String dbUsername = environment.getProperty("spring.datasource.username");
|
||||
String dbDriver = environment.getProperty("spring.datasource.driver-class-name");
|
||||
|
||||
// HikariCP pool settings
|
||||
String poolInfo = "";
|
||||
if (dataSource instanceof HikariDataSource hikariDs) {
|
||||
poolInfo =
|
||||
String.format(
|
||||
"""
|
||||
│ Pool Size : min=%d, max=%d
|
||||
│ Connection Timeout: %dms
|
||||
│ Idle Timeout : %dms
|
||||
│ Max Lifetime : %dms""",
|
||||
hikariDs.getMinimumIdle(),
|
||||
hikariDs.getMaximumPoolSize(),
|
||||
hikariDs.getConnectionTimeout(),
|
||||
hikariDs.getIdleTimeout(),
|
||||
hikariDs.getMaxLifetime());
|
||||
}
|
||||
|
||||
// JPA/Hibernate settings
|
||||
String showSql = environment.getProperty("spring.jpa.show-sql", "false");
|
||||
String ddlAuto = environment.getProperty("spring.jpa.hibernate.ddl-auto", "none");
|
||||
String batchSize =
|
||||
environment.getProperty("spring.jpa.properties.hibernate.jdbc.batch_size", "N/A");
|
||||
String batchFetchSize =
|
||||
environment.getProperty("spring.jpa.properties.hibernate.default_batch_fetch_size", "N/A");
|
||||
|
||||
String startupMessage =
|
||||
String.format(
|
||||
"""
|
||||
|
||||
╔════════════════════════════════════════════════════════════════════════════════╗
|
||||
║ 🚀 APPLICATION STARTUP INFORMATION ║
|
||||
╠════════════════════════════════════════════════════════════════════════════════╣
|
||||
║ PROFILE CONFIGURATION ║
|
||||
╠────────────────────────────────────────────────────────────────────────────────╣
|
||||
│ Active Profile(s): %s
|
||||
╠════════════════════════════════════════════════════════════════════════════════╣
|
||||
║ DATABASE CONFIGURATION ║
|
||||
╠────────────────────────────────────────────────────────────────────────────────╣
|
||||
│ Database URL : %s
|
||||
│ Username : %s
|
||||
│ Driver : %s
|
||||
╠════════════════════════════════════════════════════════════════════════════════╣
|
||||
║ HIKARICP CONNECTION POOL ║
|
||||
╠────────────────────────────────────────────────────────────────────────────────╣
|
||||
%s
|
||||
╠════════════════════════════════════════════════════════════════════════════════╣
|
||||
║ JPA/HIBERNATE CONFIGURATION ║
|
||||
╠────────────────────────────────────────────────────────────────────────────────╣
|
||||
│ Show SQL : %s
|
||||
│ DDL Auto : %s
|
||||
│ JDBC Batch Size : %s
|
||||
│ Fetch Batch Size : %s
|
||||
╚════════════════════════════════════════════════════════════════════════════════╝
|
||||
""",
|
||||
profileInfo,
|
||||
dbUrl != null ? dbUrl : "N/A",
|
||||
dbUsername != null ? dbUsername : "N/A",
|
||||
dbDriver != null ? dbDriver : "PostgreSQL JDBC Driver (auto-detected)",
|
||||
poolInfo,
|
||||
showSql,
|
||||
ddlAuto,
|
||||
batchSize,
|
||||
batchFetchSize);
|
||||
|
||||
log.info(startupMessage);
|
||||
}
|
||||
}
|
||||
package com.kamco.cd.training.config;
|
||||
|
||||
import com.zaxxer.hikari.HikariDataSource;
|
||||
import javax.sql.DataSource;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.boot.context.event.ApplicationReadyEvent;
|
||||
import org.springframework.context.event.EventListener;
|
||||
import org.springframework.core.env.Environment;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
@Slf4j
|
||||
@Component
|
||||
@RequiredArgsConstructor
|
||||
public class StartupLogger {
|
||||
|
||||
private final Environment environment;
|
||||
private final DataSource dataSource;
|
||||
|
||||
@EventListener(ApplicationReadyEvent.class)
|
||||
public void logStartupInfo() {
|
||||
String[] activeProfiles = environment.getActiveProfiles();
|
||||
String profileInfo = activeProfiles.length > 0 ? String.join(", ", activeProfiles) : "default";
|
||||
|
||||
// Database connection information
|
||||
String dbUrl = environment.getProperty("spring.datasource.url");
|
||||
String dbUsername = environment.getProperty("spring.datasource.username");
|
||||
String dbDriver = environment.getProperty("spring.datasource.driver-class-name");
|
||||
|
||||
// HikariCP pool settings
|
||||
String poolInfo = "";
|
||||
if (dataSource instanceof HikariDataSource hikariDs) {
|
||||
poolInfo =
|
||||
String.format(
|
||||
"""
|
||||
│ Pool Size : min=%d, max=%d
|
||||
│ Connection Timeout: %dms
|
||||
│ Idle Timeout : %dms
|
||||
│ Max Lifetime : %dms""",
|
||||
hikariDs.getMinimumIdle(),
|
||||
hikariDs.getMaximumPoolSize(),
|
||||
hikariDs.getConnectionTimeout(),
|
||||
hikariDs.getIdleTimeout(),
|
||||
hikariDs.getMaxLifetime());
|
||||
}
|
||||
|
||||
// JPA/Hibernate settings
|
||||
String showSql = environment.getProperty("spring.jpa.show-sql", "false");
|
||||
String ddlAuto = environment.getProperty("spring.jpa.hibernate.ddl-auto", "none");
|
||||
String batchSize =
|
||||
environment.getProperty("spring.jpa.properties.hibernate.jdbc.batch_size", "N/A");
|
||||
String batchFetchSize =
|
||||
environment.getProperty("spring.jpa.properties.hibernate.default_batch_fetch_size", "N/A");
|
||||
|
||||
String startupMessage =
|
||||
String.format(
|
||||
"""
|
||||
|
||||
╔════════════════════════════════════════════════════════════════════════════════╗
|
||||
║ 🚀 APPLICATION STARTUP INFORMATION 2 ║
|
||||
╠════════════════════════════════════════════════════════════════════════════════╣
|
||||
║ PROFILE CONFIGURATION ║
|
||||
╠────────────────────────────────────────────────────────────────────────────────╣
|
||||
│ Active Profile(s): %s
|
||||
╠════════════════════════════════════════════════════════════════════════════════╣
|
||||
║ DATABASE CONFIGURATION ║
|
||||
╠────────────────────────────────────────────────────────────────────────────────╣
|
||||
│ Database URL : %s
|
||||
│ Username : %s
|
||||
│ Driver : %s
|
||||
╠════════════════════════════════════════════════════════════════════════════════╣
|
||||
║ HIKARICP CONNECTION POOL ║
|
||||
╠────────────────────────────────────────────────────────────────────────────────╣
|
||||
%s
|
||||
╠════════════════════════════════════════════════════════════════════════════════╣
|
||||
║ JPA/HIBERNATE CONFIGURATION ║
|
||||
╠────────────────────────────────────────────────────────────────────────────────╣
|
||||
│ Show SQL : %s
|
||||
│ DDL Auto : %s
|
||||
│ JDBC Batch Size : %s
|
||||
│ Fetch Batch Size : %s
|
||||
╚════════════════════════════════════════════════════════════════════════════════╝
|
||||
""",
|
||||
profileInfo,
|
||||
dbUrl != null ? dbUrl : "N/A",
|
||||
dbUsername != null ? dbUsername : "N/A",
|
||||
dbDriver != null ? dbDriver : "PostgreSQL JDBC Driver (auto-detected)",
|
||||
poolInfo,
|
||||
showSql,
|
||||
ddlAuto,
|
||||
batchSize,
|
||||
batchFetchSize);
|
||||
|
||||
log.info(startupMessage);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,32 +1,32 @@
|
||||
package com.kamco.cd.training.config;
|
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.fasterxml.jackson.databind.module.SimpleModule;
|
||||
import com.kamco.cd.training.common.utils.geometry.GeometryDeserializer;
|
||||
import com.kamco.cd.training.common.utils.geometry.GeometrySerializer;
|
||||
import org.locationtech.jts.geom.Geometry;
|
||||
import org.locationtech.jts.geom.Point;
|
||||
import org.locationtech.jts.geom.Polygon;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.http.converter.json.Jackson2ObjectMapperBuilder;
|
||||
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
|
||||
|
||||
@Configuration
|
||||
public class WebConfig implements WebMvcConfigurer {
|
||||
|
||||
@Bean
|
||||
public ObjectMapper objectMapper() {
|
||||
SimpleModule module = new SimpleModule();
|
||||
module.addSerializer(Geometry.class, new GeometrySerializer<>(Geometry.class));
|
||||
module.addDeserializer(Geometry.class, new GeometryDeserializer<>(Geometry.class));
|
||||
|
||||
module.addSerializer(Polygon.class, new GeometrySerializer<>(Polygon.class));
|
||||
module.addDeserializer(Polygon.class, new GeometryDeserializer<>(Polygon.class));
|
||||
|
||||
module.addSerializer(Point.class, new GeometrySerializer<>(Point.class));
|
||||
module.addDeserializer(Point.class, new GeometryDeserializer<>(Point.class));
|
||||
|
||||
return Jackson2ObjectMapperBuilder.json().modulesToInstall(module).build();
|
||||
}
|
||||
}
|
||||
package com.kamco.cd.training.config;
|
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.fasterxml.jackson.databind.module.SimpleModule;
|
||||
import com.kamco.cd.training.common.utils.geometry.GeometryDeserializer;
|
||||
import com.kamco.cd.training.common.utils.geometry.GeometrySerializer;
|
||||
import org.locationtech.jts.geom.Geometry;
|
||||
import org.locationtech.jts.geom.Point;
|
||||
import org.locationtech.jts.geom.Polygon;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.http.converter.json.Jackson2ObjectMapperBuilder;
|
||||
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
|
||||
|
||||
@Configuration
|
||||
public class WebConfig implements WebMvcConfigurer {
|
||||
|
||||
@Bean
|
||||
public ObjectMapper objectMapper() {
|
||||
SimpleModule module = new SimpleModule();
|
||||
module.addSerializer(Geometry.class, new GeometrySerializer<>(Geometry.class));
|
||||
module.addDeserializer(Geometry.class, new GeometryDeserializer<>(Geometry.class));
|
||||
|
||||
module.addSerializer(Polygon.class, new GeometrySerializer<>(Polygon.class));
|
||||
module.addDeserializer(Polygon.class, new GeometryDeserializer<>(Polygon.class));
|
||||
|
||||
module.addSerializer(Point.class, new GeometrySerializer<>(Point.class));
|
||||
module.addDeserializer(Point.class, new GeometryDeserializer<>(Point.class));
|
||||
|
||||
return Jackson2ObjectMapperBuilder.json().modulesToInstall(module).build();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,28 +1,34 @@
|
||||
package com.kamco.cd.training.config.api;
|
||||
|
||||
import jakarta.servlet.FilterChain;
|
||||
import jakarta.servlet.ServletException;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import jakarta.servlet.http.HttpServletResponse;
|
||||
import java.io.IOException;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.web.filter.OncePerRequestFilter;
|
||||
import org.springframework.web.util.ContentCachingRequestWrapper;
|
||||
import org.springframework.web.util.ContentCachingResponseWrapper;
|
||||
|
||||
@Component
|
||||
public class ApiLogFilter extends OncePerRequestFilter {
|
||||
@Override
|
||||
protected void doFilterInternal(
|
||||
HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
|
||||
throws ServletException, IOException {
|
||||
ContentCachingRequestWrapper wrappedRequest = new ContentCachingRequestWrapper(request);
|
||||
|
||||
ContentCachingResponseWrapper wrappedResponse = new ContentCachingResponseWrapper(response);
|
||||
|
||||
filterChain.doFilter(wrappedRequest, wrappedResponse);
|
||||
|
||||
// 반드시 response body copy
|
||||
wrappedResponse.copyBodyToResponse();
|
||||
}
|
||||
}
|
||||
package com.kamco.cd.training.config.api;
|
||||
|
||||
import jakarta.servlet.FilterChain;
|
||||
import jakarta.servlet.ServletException;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import jakarta.servlet.http.HttpServletResponse;
|
||||
import java.io.IOException;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.web.filter.OncePerRequestFilter;
|
||||
import org.springframework.web.util.ContentCachingRequestWrapper;
|
||||
import org.springframework.web.util.ContentCachingResponseWrapper;
|
||||
|
||||
@Component
|
||||
public class ApiLogFilter extends OncePerRequestFilter {
|
||||
@Override
|
||||
protected void doFilterInternal(
|
||||
HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
|
||||
throws ServletException, IOException {
|
||||
String uri = request.getRequestURI();
|
||||
if (uri.contains("/download/")) {
|
||||
filterChain.doFilter(request, response);
|
||||
return;
|
||||
}
|
||||
|
||||
ContentCachingRequestWrapper wrappedRequest = new ContentCachingRequestWrapper(request);
|
||||
|
||||
ContentCachingResponseWrapper wrappedResponse = new ContentCachingResponseWrapper(response);
|
||||
|
||||
filterChain.doFilter(wrappedRequest, wrappedResponse);
|
||||
|
||||
// 반드시 response body copy
|
||||
wrappedResponse.copyBodyToResponse();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,132 +1,153 @@
|
||||
package com.kamco.cd.training.config.api;
|
||||
|
||||
import com.kamco.cd.training.log.dto.EventStatus;
|
||||
import com.kamco.cd.training.log.dto.EventType;
|
||||
import com.kamco.cd.training.menu.dto.MenuDto;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import java.io.UnsupportedEncodingException;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.stream.Collectors;
|
||||
import org.springframework.web.util.ContentCachingRequestWrapper;
|
||||
|
||||
public class ApiLogFunction {
|
||||
|
||||
// 클라이언트 IP 추출
|
||||
public static String getClientIp(HttpServletRequest request) {
|
||||
String[] headers = {
|
||||
"X-Forwarded-For",
|
||||
"Proxy-Client-IP",
|
||||
"WL-Proxy-Client-IP",
|
||||
"HTTP_CLIENT_IP",
|
||||
"HTTP_X_FORWARDED_FOR"
|
||||
};
|
||||
for (String header : headers) {
|
||||
String ip = request.getHeader(header);
|
||||
if (ip != null && ip.length() != 0 && !"unknown".equalsIgnoreCase(ip)) {
|
||||
return ip.split(",")[0];
|
||||
}
|
||||
}
|
||||
String ip = request.getRemoteAddr();
|
||||
if ("0:0:0:0:0:0:0:1".equals(ip)) { // local 일 때
|
||||
ip = "127.0.0.1";
|
||||
}
|
||||
return ip;
|
||||
}
|
||||
|
||||
// 사용자 ID 추출 예시 (Spring Security 기준)
|
||||
public static String getUserId(HttpServletRequest request) {
|
||||
try {
|
||||
return request.getUserPrincipal() != null ? request.getUserPrincipal().getName() : null;
|
||||
} catch (Exception e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public static EventType getEventType(HttpServletRequest request) {
|
||||
String method = request.getMethod().toUpperCase();
|
||||
String uri = request.getRequestURI().toLowerCase();
|
||||
|
||||
// URL 기반 DOWNLOAD/PRINT 분류
|
||||
if (uri.contains("/download") || uri.contains("/export")) {
|
||||
return EventType.DOWNLOAD;
|
||||
}
|
||||
if (uri.contains("/print")) {
|
||||
return EventType.PRINT;
|
||||
}
|
||||
|
||||
// 일반 CRUD
|
||||
return switch (method) {
|
||||
case "POST" -> EventType.CREATE;
|
||||
case "GET" -> EventType.READ;
|
||||
case "DELETE" -> EventType.DELETE;
|
||||
case "PUT", "PATCH" -> EventType.UPDATE;
|
||||
default -> EventType.OTHER;
|
||||
};
|
||||
}
|
||||
|
||||
public static String getRequestBody(
|
||||
HttpServletRequest servletRequest, ContentCachingRequestWrapper contentWrapper) {
|
||||
StringBuilder resultBody = new StringBuilder();
|
||||
// GET, form-urlencoded POST 파라미터
|
||||
Map<String, String[]> paramMap = servletRequest.getParameterMap();
|
||||
|
||||
String queryParams =
|
||||
paramMap.entrySet().stream()
|
||||
.map(e -> e.getKey() + "=" + String.join(",", e.getValue()))
|
||||
.collect(Collectors.joining("&"));
|
||||
|
||||
resultBody.append(queryParams.isEmpty() ? "" : queryParams);
|
||||
|
||||
// JSON Body
|
||||
if ("POST".equalsIgnoreCase(servletRequest.getMethod())
|
||||
&& servletRequest.getContentType() != null
|
||||
&& servletRequest.getContentType().contains("application/json")) {
|
||||
try {
|
||||
// json인 경우는 Wrapper를 통해 가져오기
|
||||
resultBody.append(getBodyData(contentWrapper));
|
||||
|
||||
} catch (Exception e) {
|
||||
resultBody.append("cannot read JSON body ").append(e.toString());
|
||||
}
|
||||
}
|
||||
|
||||
// Multipart form-data
|
||||
if ("POST".equalsIgnoreCase(servletRequest.getMethod())
|
||||
&& servletRequest.getContentType() != null
|
||||
&& servletRequest.getContentType().startsWith("multipart/form-data")) {
|
||||
resultBody.append("multipart/form-data request");
|
||||
}
|
||||
|
||||
return resultBody.toString();
|
||||
}
|
||||
|
||||
// JSON Body 읽기
|
||||
public static String getBodyData(ContentCachingRequestWrapper request) {
|
||||
byte[] buf = request.getContentAsByteArray();
|
||||
if (buf.length == 0) {
|
||||
return null;
|
||||
}
|
||||
try {
|
||||
return new String(buf, request.getCharacterEncoding());
|
||||
} catch (UnsupportedEncodingException e) {
|
||||
return new String(buf);
|
||||
}
|
||||
}
|
||||
|
||||
// ApiResponse 의 Status가 2xx 범위이면 SUCCESS, 아니면 FAILED
|
||||
public static EventStatus isSuccessFail(ApiResponseDto<?> apiResponse) {
|
||||
return apiResponse.getHttpStatus().is2xxSuccessful() ? EventStatus.SUCCESS : EventStatus.FAILED;
|
||||
}
|
||||
|
||||
public static String getUriMenuInfo(List<MenuDto.Basic> menuList, String uri) {
|
||||
|
||||
MenuDto.Basic m =
|
||||
menuList.stream()
|
||||
.filter(menu -> menu.getMenuApiUrl() != null && uri.contains(menu.getMenuApiUrl()))
|
||||
.findFirst()
|
||||
.orElse(null);
|
||||
|
||||
return m != null ? m.getMenuUid() : "SYSTEM";
|
||||
}
|
||||
}
|
||||
package com.kamco.cd.training.config.api;
|
||||
|
||||
import com.kamco.cd.training.log.dto.EventStatus;
|
||||
import com.kamco.cd.training.log.dto.EventType;
|
||||
import com.kamco.cd.training.menu.dto.MenuDto;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import java.io.UnsupportedEncodingException;
|
||||
import java.util.Comparator;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.stream.Collectors;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.web.util.ContentCachingRequestWrapper;
|
||||
|
||||
@Slf4j
|
||||
public class ApiLogFunction {
|
||||
|
||||
// 클라이언트 IP 추출
|
||||
public static String getClientIp(HttpServletRequest request) {
|
||||
String[] headers = {
|
||||
"X-Forwarded-For",
|
||||
"Proxy-Client-IP",
|
||||
"WL-Proxy-Client-IP",
|
||||
"HTTP_CLIENT_IP",
|
||||
"HTTP_X_FORWARDED_FOR"
|
||||
};
|
||||
for (String header : headers) {
|
||||
String ip = request.getHeader(header);
|
||||
if (ip != null && ip.length() != 0 && !"unknown".equalsIgnoreCase(ip)) {
|
||||
return ip.split(",")[0];
|
||||
}
|
||||
}
|
||||
String ip = request.getRemoteAddr();
|
||||
if ("0:0:0:0:0:0:0:1".equals(ip)) { // local 일 때
|
||||
ip = "127.0.0.1";
|
||||
}
|
||||
return ip;
|
||||
}
|
||||
|
||||
public static String getXFowardedForIp(HttpServletRequest request) {
|
||||
String ip = request.getHeader("X-Forwarded-For");
|
||||
if (ip != null) {
|
||||
ip = ip.split(",")[0].trim();
|
||||
}
|
||||
return ip;
|
||||
}
|
||||
|
||||
// 사용자 ID 추출 예시 (Spring Security 기준)
|
||||
public static String getUserId(HttpServletRequest request) {
|
||||
try {
|
||||
return request.getUserPrincipal() != null ? request.getUserPrincipal().getName() : null;
|
||||
} catch (Exception e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public static EventType getEventType(HttpServletRequest request) {
|
||||
String method = request.getMethod().toUpperCase();
|
||||
String uri = request.getRequestURI().toLowerCase();
|
||||
|
||||
// URL 기반 DOWNLOAD/PRINT 분류 -> /download는 FileDownloadInterceptor로 옮김
|
||||
if (uri.contains("/download") || uri.contains("/export")) {
|
||||
return EventType.DOWNLOAD;
|
||||
}
|
||||
if (uri.contains("/print")) {
|
||||
return EventType.OTHER;
|
||||
}
|
||||
|
||||
// 일반 CRUD
|
||||
return switch (method) {
|
||||
case "POST" -> EventType.ADDED;
|
||||
case "GET" -> EventType.LIST;
|
||||
case "DELETE" -> EventType.REMOVE;
|
||||
case "PUT", "PATCH" -> EventType.MODIFIED;
|
||||
default -> EventType.OTHER;
|
||||
};
|
||||
}
|
||||
|
||||
public static String getRequestBody(
|
||||
HttpServletRequest servletRequest, ContentCachingRequestWrapper contentWrapper) {
|
||||
StringBuilder resultBody = new StringBuilder();
|
||||
// GET, form-urlencoded POST 파라미터
|
||||
Map<String, String[]> paramMap = servletRequest.getParameterMap();
|
||||
|
||||
String queryParams =
|
||||
paramMap.entrySet().stream()
|
||||
.map(e -> e.getKey() + "=" + String.join(",", e.getValue()))
|
||||
.collect(Collectors.joining("&"));
|
||||
|
||||
resultBody.append(queryParams.isEmpty() ? "" : queryParams);
|
||||
|
||||
// JSON Body
|
||||
if ("POST".equalsIgnoreCase(servletRequest.getMethod())
|
||||
&& servletRequest.getContentType() != null
|
||||
&& servletRequest.getContentType().contains("application/json")) {
|
||||
try {
|
||||
// json인 경우는 Wrapper를 통해 가져오기
|
||||
resultBody.append(getBodyData(contentWrapper));
|
||||
|
||||
} catch (Exception e) {
|
||||
resultBody.append("cannot read JSON body ").append(e.toString());
|
||||
}
|
||||
}
|
||||
|
||||
// Multipart form-data
|
||||
if ("POST".equalsIgnoreCase(servletRequest.getMethod())
|
||||
&& servletRequest.getContentType() != null
|
||||
&& servletRequest.getContentType().startsWith("multipart/form-data")) {
|
||||
resultBody.append("multipart/form-data request");
|
||||
}
|
||||
|
||||
return resultBody.toString();
|
||||
}
|
||||
|
||||
// JSON Body 읽기
|
||||
public static String getBodyData(ContentCachingRequestWrapper request) {
|
||||
byte[] buf = request.getContentAsByteArray();
|
||||
if (buf.length == 0) {
|
||||
return null;
|
||||
}
|
||||
try {
|
||||
return new String(buf, request.getCharacterEncoding());
|
||||
} catch (UnsupportedEncodingException e) {
|
||||
return new String(buf);
|
||||
}
|
||||
}
|
||||
|
||||
// ApiResponse 의 Status가 2xx 범위이면 SUCCESS, 아니면 FAILED
|
||||
public static EventStatus isSuccessFail(ApiResponseDto<?> apiResponse) {
|
||||
return apiResponse.getHttpStatus().is2xxSuccessful() ? EventStatus.SUCCESS : EventStatus.FAILED;
|
||||
}
|
||||
|
||||
public static String getUriMenuInfo(List<MenuDto.Basic> menuList, String uri) {
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,124 +1,163 @@
|
||||
package com.kamco.cd.training.config.api;
|
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.kamco.cd.training.auth.CustomUserDetails;
|
||||
import com.kamco.cd.training.menu.service.MenuService;
|
||||
import com.kamco.cd.training.postgres.entity.AuditLogEntity;
|
||||
import com.kamco.cd.training.postgres.repository.log.AuditLogRepository;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.core.MethodParameter;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.http.converter.HttpMessageConverter;
|
||||
import org.springframework.http.server.ServerHttpRequest;
|
||||
import org.springframework.http.server.ServerHttpResponse;
|
||||
import org.springframework.http.server.ServletServerHttpRequest;
|
||||
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
|
||||
import org.springframework.web.bind.annotation.RestControllerAdvice;
|
||||
import org.springframework.web.servlet.mvc.method.annotation.ResponseBodyAdvice;
|
||||
import org.springframework.web.util.ContentCachingRequestWrapper;
|
||||
|
||||
/**
|
||||
* ApiResponseDto의 내장된 HTTP 상태 코드를 실제 HTTP 응답에 적용하는 Advice
|
||||
*
|
||||
* <p>createOK() → 201 CREATED ok() → 200 OK deleteOk() → 204 NO_CONTENT
|
||||
*/
|
||||
@RestControllerAdvice
|
||||
public class ApiResponseAdvice implements ResponseBodyAdvice<Object> {
|
||||
|
||||
private final AuditLogRepository auditLogRepository;
|
||||
private final MenuService menuService;
|
||||
|
||||
@Autowired private ObjectMapper objectMapper;
|
||||
|
||||
public ApiResponseAdvice(AuditLogRepository auditLogRepository, MenuService menuService) {
|
||||
this.auditLogRepository = auditLogRepository;
|
||||
this.menuService = menuService;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean supports(
|
||||
MethodParameter returnType, Class<? extends HttpMessageConverter<?>> converterType) {
|
||||
// ApiResponseDto를 반환하는 경우에만 적용
|
||||
return returnType.getParameterType().equals(ApiResponseDto.class);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object beforeBodyWrite(
|
||||
Object body,
|
||||
MethodParameter returnType,
|
||||
MediaType selectedContentType,
|
||||
Class<? extends HttpMessageConverter<?>> selectedConverterType,
|
||||
ServerHttpRequest request,
|
||||
ServerHttpResponse response) {
|
||||
|
||||
HttpServletRequest servletRequest = ((ServletServerHttpRequest) request).getServletRequest();
|
||||
ContentCachingRequestWrapper contentWrapper = null;
|
||||
if (servletRequest instanceof ContentCachingRequestWrapper wrapper) {
|
||||
contentWrapper = wrapper;
|
||||
}
|
||||
|
||||
if (body instanceof ApiResponseDto<?> apiResponse) {
|
||||
response.setStatusCode(apiResponse.getHttpStatus());
|
||||
|
||||
String ip = ApiLogFunction.getClientIp(servletRequest);
|
||||
Long userid = null;
|
||||
|
||||
if (servletRequest.getUserPrincipal() instanceof UsernamePasswordAuthenticationToken auth
|
||||
&& auth.getPrincipal() instanceof CustomUserDetails customUserDetails) {
|
||||
userid = customUserDetails.getMember().getId();
|
||||
}
|
||||
|
||||
String requestBody;
|
||||
// 멀티파트 요청은 바디 로깅을 생략 (파일 바이너리로 인한 문제 예방)
|
||||
MediaType reqContentType = null;
|
||||
try {
|
||||
String ct = servletRequest.getContentType();
|
||||
reqContentType = ct != null ? MediaType.valueOf(ct) : null;
|
||||
} catch (Exception ignored) {
|
||||
}
|
||||
if (reqContentType != null && MediaType.MULTIPART_FORM_DATA.includes(reqContentType)) {
|
||||
requestBody = "(multipart omitted)";
|
||||
} else {
|
||||
requestBody = ApiLogFunction.getRequestBody(servletRequest, contentWrapper);
|
||||
requestBody = maskSensitiveFields(requestBody);
|
||||
}
|
||||
|
||||
AuditLogEntity log =
|
||||
new AuditLogEntity(
|
||||
userid,
|
||||
ApiLogFunction.getEventType(servletRequest),
|
||||
ApiLogFunction.isSuccessFail(apiResponse),
|
||||
ApiLogFunction.getUriMenuInfo(
|
||||
menuService.getFindAll(), servletRequest.getRequestURI()),
|
||||
ip,
|
||||
servletRequest.getRequestURI(),
|
||||
requestBody,
|
||||
apiResponse.getErrorLogUid());
|
||||
auditLogRepository.save(log);
|
||||
}
|
||||
|
||||
return body;
|
||||
}
|
||||
|
||||
/**
|
||||
* 마스킹
|
||||
*
|
||||
* @param json
|
||||
* @return
|
||||
*/
|
||||
private String maskSensitiveFields(String json) {
|
||||
if (json == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// password 마스킹
|
||||
json = json.replaceAll("\"password\"\\s*:\\s*\"[^\"]*\"", "\"password\":\"****\"");
|
||||
|
||||
// 토큰 마스킹
|
||||
json = json.replaceAll("\"accessToken\"\\s*:\\s*\"[^\"]*\"", "\"accessToken\":\"****\"");
|
||||
json = json.replaceAll("\"refreshToken\"\\s*:\\s*\"[^\"]*\"", "\"refreshToken\":\"****\"");
|
||||
|
||||
return json;
|
||||
}
|
||||
}
|
||||
package com.kamco.cd.training.config.api;
|
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.kamco.cd.training.auth.CustomUserDetails;
|
||||
import com.kamco.cd.training.common.utils.HeaderUtil;
|
||||
import com.kamco.cd.training.log.dto.EventType;
|
||||
import com.kamco.cd.training.menu.dto.MenuDto;
|
||||
import com.kamco.cd.training.menu.service.MenuService;
|
||||
import com.kamco.cd.training.postgres.entity.AuditLogEntity;
|
||||
import com.kamco.cd.training.postgres.repository.log.AuditLogRepository;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.core.MethodParameter;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.http.converter.HttpMessageConverter;
|
||||
import org.springframework.http.server.ServerHttpRequest;
|
||||
import org.springframework.http.server.ServerHttpResponse;
|
||||
import org.springframework.http.server.ServletServerHttpRequest;
|
||||
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
|
||||
import org.springframework.web.bind.annotation.RestControllerAdvice;
|
||||
import org.springframework.web.servlet.mvc.method.annotation.ResponseBodyAdvice;
|
||||
import org.springframework.web.util.ContentCachingRequestWrapper;
|
||||
|
||||
/**
|
||||
* ApiResponseDto의 내장된 HTTP 상태 코드를 실제 HTTP 응답에 적용하는 Advice
|
||||
*
|
||||
* <p>createOK() → 201 CREATED ok() → 200 OK deleteOk() → 204 NO_CONTENT
|
||||
*/
|
||||
@Slf4j
|
||||
@RestControllerAdvice
|
||||
public class ApiResponseAdvice implements ResponseBodyAdvice<Object> {
|
||||
|
||||
private final AuditLogRepository auditLogRepository;
|
||||
private final MenuService menuService;
|
||||
|
||||
@Autowired private ObjectMapper objectMapper;
|
||||
|
||||
public ApiResponseAdvice(AuditLogRepository auditLogRepository, MenuService menuService) {
|
||||
this.auditLogRepository = auditLogRepository;
|
||||
this.menuService = menuService;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean supports(
|
||||
MethodParameter returnType, Class<? extends HttpMessageConverter<?>> converterType) {
|
||||
// ApiResponseDto를 반환하는 경우에만 적용
|
||||
return returnType.getParameterType().equals(ApiResponseDto.class);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object beforeBodyWrite(
|
||||
Object body,
|
||||
MethodParameter returnType,
|
||||
MediaType selectedContentType,
|
||||
Class<? extends HttpMessageConverter<?>> selectedConverterType,
|
||||
ServerHttpRequest request,
|
||||
ServerHttpResponse response) {
|
||||
|
||||
HttpServletRequest servletRequest = ((ServletServerHttpRequest) request).getServletRequest();
|
||||
ContentCachingRequestWrapper contentWrapper = null;
|
||||
if (servletRequest instanceof ContentCachingRequestWrapper wrapper) {
|
||||
contentWrapper = wrapper;
|
||||
}
|
||||
|
||||
if (body instanceof ApiResponseDto<?> apiResponse) {
|
||||
response.setStatusCode(apiResponse.getHttpStatus());
|
||||
|
||||
String actionType = HeaderUtil.get(servletRequest, "kamco-action-type");
|
||||
// actionType 이 없으면 로그 저장하지 않기 || download 는 FileDownloadInterceptor 에서 하기
|
||||
// (file down URL prefix 추가는 WebConfig.java 에 하기)
|
||||
if (actionType == null || actionType.equalsIgnoreCase("download")) {
|
||||
return body;
|
||||
}
|
||||
|
||||
String ip =
|
||||
Optional.ofNullable(HeaderUtil.get(servletRequest, "kamco-user-ip"))
|
||||
.orElseGet(() -> ApiLogFunction.getXFowardedForIp(servletRequest));
|
||||
Long userid = null;
|
||||
String loginAttemptId = null;
|
||||
|
||||
// 로그인 시도할 때
|
||||
if (servletRequest.getRequestURI().contains("/api/auth/signin")) {
|
||||
loginAttemptId = HeaderUtil.get(servletRequest, "kamco-login-attempt-id");
|
||||
} else {
|
||||
if (servletRequest.getUserPrincipal() instanceof UsernamePasswordAuthenticationToken auth
|
||||
&& auth.getPrincipal() instanceof CustomUserDetails customUserDetails) {
|
||||
userid = customUserDetails.getMember().getId();
|
||||
}
|
||||
}
|
||||
|
||||
String requestBody;
|
||||
// 멀티파트 요청은 바디 로깅을 생략 (파일 바이너리로 인한 문제 예방)
|
||||
MediaType reqContentType = null;
|
||||
try {
|
||||
String ct = servletRequest.getContentType();
|
||||
reqContentType = ct != null ? MediaType.valueOf(ct) : null;
|
||||
} catch (Exception ignored) {
|
||||
}
|
||||
if (reqContentType != null && MediaType.MULTIPART_FORM_DATA.includes(reqContentType)) {
|
||||
requestBody = "(multipart omitted)";
|
||||
} else {
|
||||
requestBody = ApiLogFunction.getRequestBody(servletRequest, contentWrapper);
|
||||
requestBody = maskSensitiveFields(requestBody);
|
||||
}
|
||||
|
||||
List<?> list = menuService.getFindAll();
|
||||
List<MenuDto.Basic> result =
|
||||
list.stream()
|
||||
.map(
|
||||
item -> {
|
||||
if (item instanceof LinkedHashMap<?, ?> map) {
|
||||
return objectMapper.convertValue(map, MenuDto.Basic.class);
|
||||
} else if (item instanceof MenuDto.Basic dto) {
|
||||
return dto;
|
||||
} else {
|
||||
throw new IllegalStateException("Unsupported cache type: " + item.getClass());
|
||||
}
|
||||
})
|
||||
.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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,199 +1,205 @@
|
||||
package com.kamco.cd.training.config.api;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonIgnore;
|
||||
import com.fasterxml.jackson.annotation.JsonInclude;
|
||||
import com.kamco.cd.training.common.utils.enums.EnumType;
|
||||
import lombok.Getter;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.ToString;
|
||||
import org.springframework.http.HttpStatus;
|
||||
|
||||
@Getter
|
||||
@ToString
|
||||
public class ApiResponseDto<T> {
|
||||
|
||||
private T data;
|
||||
|
||||
@JsonInclude(JsonInclude.Include.NON_NULL)
|
||||
private Error error;
|
||||
|
||||
@JsonInclude(JsonInclude.Include.NON_NULL)
|
||||
private T errorData;
|
||||
|
||||
@JsonIgnore private HttpStatus httpStatus;
|
||||
|
||||
@JsonIgnore private Long errorLogUid;
|
||||
|
||||
public ApiResponseDto(T data) {
|
||||
this.data = data;
|
||||
}
|
||||
|
||||
private ApiResponseDto(T data, HttpStatus httpStatus) {
|
||||
this.data = data;
|
||||
this.httpStatus = httpStatus;
|
||||
}
|
||||
|
||||
public ApiResponseDto(ApiResponseCode code) {
|
||||
this.error = new Error(code.getId(), code.getMessage());
|
||||
}
|
||||
|
||||
public ApiResponseDto(ApiResponseCode code, String message) {
|
||||
this.error = new Error(code.getId(), message);
|
||||
}
|
||||
|
||||
public ApiResponseDto(ApiResponseCode code, String message, HttpStatus httpStatus) {
|
||||
this.error = new Error(code.getId(), message);
|
||||
this.httpStatus = httpStatus;
|
||||
}
|
||||
|
||||
public ApiResponseDto(
|
||||
ApiResponseCode code, String message, HttpStatus httpStatus, Long errorLogUid) {
|
||||
this.error = new Error(code.getId(), message);
|
||||
this.httpStatus = httpStatus;
|
||||
this.errorLogUid = errorLogUid;
|
||||
}
|
||||
|
||||
public ApiResponseDto(ApiResponseCode code, String message, T errorData) {
|
||||
this.error = new Error(code.getId(), message);
|
||||
this.errorData = errorData;
|
||||
}
|
||||
|
||||
// HTTP 상태 코드가 내장된 ApiResponseDto 반환 메서드들
|
||||
public static <T> ApiResponseDto<T> createOK(T data) {
|
||||
return new ApiResponseDto<>(data, HttpStatus.CREATED);
|
||||
}
|
||||
|
||||
public static <T> ApiResponseDto<T> ok(T data) {
|
||||
return new ApiResponseDto<>(data, HttpStatus.OK);
|
||||
}
|
||||
|
||||
public static <T> ApiResponseDto<ResponseObj> okObject(ResponseObj data) {
|
||||
if (data.getCode().equals(ApiResponseCode.OK)) {
|
||||
return new ApiResponseDto<>(data, HttpStatus.NO_CONTENT);
|
||||
} else {
|
||||
return new ApiResponseDto<>(data.getCode(), data.getMessage(), HttpStatus.CONFLICT);
|
||||
}
|
||||
}
|
||||
|
||||
public static <T> ApiResponseDto<T> deleteOk(T data) {
|
||||
return new ApiResponseDto<>(data, HttpStatus.NO_CONTENT);
|
||||
}
|
||||
|
||||
public static ApiResponseDto<String> createException(ApiResponseCode code) {
|
||||
return new ApiResponseDto<>(code);
|
||||
}
|
||||
|
||||
public static ApiResponseDto<String> createException(ApiResponseCode code, String message) {
|
||||
return new ApiResponseDto<>(code, message);
|
||||
}
|
||||
|
||||
public static ApiResponseDto<String> createException(
|
||||
ApiResponseCode code, String message, HttpStatus httpStatus) {
|
||||
return new ApiResponseDto<>(code, message, httpStatus);
|
||||
}
|
||||
|
||||
public static ApiResponseDto<String> createException(
|
||||
ApiResponseCode code, String message, HttpStatus httpStatus, Long errorLogUid) {
|
||||
return new ApiResponseDto<>(code, message, httpStatus, errorLogUid);
|
||||
}
|
||||
|
||||
public static <T> ApiResponseDto<T> createException(
|
||||
ApiResponseCode code, String message, T data) {
|
||||
return new ApiResponseDto<>(code, message, data);
|
||||
}
|
||||
|
||||
@Getter
|
||||
public static class Error {
|
||||
|
||||
private final String code;
|
||||
private final String message;
|
||||
|
||||
public Error(String code, String message) {
|
||||
this.code = code;
|
||||
this.message = message;
|
||||
}
|
||||
}
|
||||
|
||||
/** Error가 아닌 Business상 성공이거나 실패인 경우, 메세지 함께 전달하기 위한 object */
|
||||
@Getter
|
||||
public static class ResponseObj {
|
||||
|
||||
private final ApiResponseCode code;
|
||||
private final String message;
|
||||
|
||||
public ResponseObj(ApiResponseCode code, String message) {
|
||||
this.code = code;
|
||||
this.message = message;
|
||||
}
|
||||
}
|
||||
|
||||
@Getter
|
||||
@RequiredArgsConstructor
|
||||
public enum ApiResponseCode implements EnumType {
|
||||
|
||||
// @formatter:off
|
||||
OK("요청이 성공하였습니다."),
|
||||
BAD_REQUEST("요청 파라미터가 잘못되었습니다."),
|
||||
BAD_GATEWAY("네트워크 상태가 불안정합니다."),
|
||||
ALREADY_EXIST_MALL("이미 등록된 쇼핑센터입니다."),
|
||||
NOT_FOUND_MAP("지도를 찾을 수 없습니다."),
|
||||
UNAUTHORIZED("권한이 없습니다."),
|
||||
CONFLICT("이미 등록된 컨텐츠입니다."),
|
||||
NOT_FOUND("Resource를 찾을 수 없습니다."),
|
||||
NOT_FOUND_DATA("데이터를 찾을 수 없습니다."),
|
||||
NOT_FOUND_WEATHER_DATA("날씨 데이터를 찾을 수 없습니다."),
|
||||
FAIL_SEND_MESSAGE("메시지를 전송하지 못했습니다."),
|
||||
TOO_MANY_CONNECTED_MACHINES("연결된 기기가 너무 많습니다."),
|
||||
UNAUTHENTICATED("인증에 실패하였습니다."),
|
||||
INVALID_TOKEN("잘못된 토큰입니다."),
|
||||
EXPIRED_TOKEN("만료된 토큰입니다."),
|
||||
INTERNAL_SERVER_ERROR("서버에 문제가 발생 하였습니다."),
|
||||
FORBIDDEN("권한을 확인해주세요."),
|
||||
INVALID_PASSWORD("잘못된 비밀번호 입니다."),
|
||||
NOT_FOUND_CAR_IN("입차정보가 없습니다."),
|
||||
WRONG_STATUS("잘못된 상태입니다."),
|
||||
FAIL_VERIFICATION("인증에 실패하였습니다."),
|
||||
INVALID_EMAIL("잘못된 형식의 이메일입니다."),
|
||||
REQUIRED_EMAIL("이메일은 필수 항목입니다."),
|
||||
WRONG_PASSWORD("잘못된 패스워드입니다."),
|
||||
DUPLICATE_EMAIL("이미 가입된 이메일입니다."),
|
||||
DUPLICATE_DATA("이미 등록되어 있습니다."),
|
||||
DATA_INTEGRITY_ERROR("데이터 무결성이 위반되어 요청을 처리할수 없습니다."),
|
||||
FOREIGN_KEY_ERROR("참조 중인 데이터가 있어 삭제할 수 없습니다."),
|
||||
DUPLICATE_EMPLOYEEID("이미 가입된 사번입니다."),
|
||||
NOT_FOUND_USER_FOR_EMAIL("이메일로 유저를 찾을 수 없습니다."),
|
||||
NOT_FOUND_USER("사용자를 찾을 수 없습니다."),
|
||||
UNPROCESSABLE_ENTITY("이 데이터는 삭제할 수 없습니다."),
|
||||
LOGIN_ID_NOT_FOUND("아이디를 잘못 입력하셨습니다."),
|
||||
LOGIN_PASSWORD_MISMATCH("비밀번호를 잘못 입력하셨습니다."),
|
||||
LOGIN_PASSWORD_EXCEEDED("비밀번호 오류 횟수를 초과하여 이용하실 수 없습니다.\n로그인 오류에 대해 관리자에게 문의하시기 바랍니다."),
|
||||
REFRESH_TOKEN_EXPIRED_OR_REVOKED("토큰 정보가 만료 되었습니다."),
|
||||
REFRESH_TOKEN_MISMATCH("토큰 정보가 일치하지 않습니다."),
|
||||
INACTIVE_ID("사용할 수 없는 계정입니다."),
|
||||
INVALID_EMAIL_TOKEN(
|
||||
"You can only reset your password within 24 hours from when the email was sent.\n"
|
||||
+ "To reset your password again, please submit a new request through \"Forgot"
|
||||
+ " Password.\""),
|
||||
;
|
||||
// @formatter:on
|
||||
private final String message;
|
||||
|
||||
@Override
|
||||
public String getId() {
|
||||
return name();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getText() {
|
||||
return message;
|
||||
}
|
||||
|
||||
public static ApiResponseCode getCode(String name) {
|
||||
return ApiResponseCode.valueOf(name.toUpperCase());
|
||||
}
|
||||
|
||||
public static String getMessage(String name) {
|
||||
return ApiResponseCode.valueOf(name.toUpperCase()).getText();
|
||||
}
|
||||
}
|
||||
}
|
||||
package com.kamco.cd.training.config.api;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonIgnore;
|
||||
import com.fasterxml.jackson.annotation.JsonInclude;
|
||||
import com.kamco.cd.training.common.utils.enums.EnumType;
|
||||
import lombok.Getter;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.ToString;
|
||||
import org.springframework.http.HttpStatus;
|
||||
|
||||
@Getter
|
||||
@ToString
|
||||
public class ApiResponseDto<T> {
|
||||
|
||||
private T data;
|
||||
|
||||
@JsonInclude(JsonInclude.Include.NON_NULL)
|
||||
private Error error;
|
||||
|
||||
@JsonInclude(JsonInclude.Include.NON_NULL)
|
||||
private T errorData;
|
||||
|
||||
@JsonIgnore private HttpStatus httpStatus;
|
||||
|
||||
@JsonIgnore private Long errorLogUid;
|
||||
|
||||
public ApiResponseDto(T data) {
|
||||
this.data = data;
|
||||
}
|
||||
|
||||
private ApiResponseDto(T data, HttpStatus httpStatus) {
|
||||
this.data = data;
|
||||
this.httpStatus = httpStatus;
|
||||
}
|
||||
|
||||
public ApiResponseDto(ApiResponseCode code) {
|
||||
this.error = new Error(code.getId(), code.getMessage());
|
||||
}
|
||||
|
||||
public ApiResponseDto(ApiResponseCode code, String message) {
|
||||
this.error = new Error(code.getId(), message);
|
||||
}
|
||||
|
||||
public ApiResponseDto(ApiResponseCode code, String message, HttpStatus httpStatus) {
|
||||
this.error = new Error(code.getId(), message);
|
||||
this.httpStatus = httpStatus;
|
||||
}
|
||||
|
||||
public ApiResponseDto(
|
||||
ApiResponseCode code, String message, HttpStatus httpStatus, Long errorLogUid) {
|
||||
this.error = new Error(code.getId(), message);
|
||||
this.httpStatus = httpStatus;
|
||||
this.errorLogUid = errorLogUid;
|
||||
}
|
||||
|
||||
public ApiResponseDto(ApiResponseCode code, String message, T errorData) {
|
||||
this.error = new Error(code.getId(), message);
|
||||
this.errorData = errorData;
|
||||
}
|
||||
|
||||
// HTTP 상태 코드가 내장된 ApiResponseDto 반환 메서드들
|
||||
public static <T> ApiResponseDto<T> createOK(T data) {
|
||||
return new ApiResponseDto<>(data, HttpStatus.CREATED);
|
||||
}
|
||||
|
||||
public static <T> ApiResponseDto<T> ok(T data) {
|
||||
return new ApiResponseDto<>(data, HttpStatus.OK);
|
||||
}
|
||||
|
||||
public static <T> ApiResponseDto<ResponseObj> okObject(ResponseObj data) {
|
||||
if (data.getCode().equals(ApiResponseCode.OK)) {
|
||||
return new ApiResponseDto<>(data, HttpStatus.NO_CONTENT);
|
||||
} else {
|
||||
return new ApiResponseDto<>(data.getCode(), data.getMessage(), HttpStatus.CONFLICT);
|
||||
}
|
||||
}
|
||||
|
||||
public static <T> ApiResponseDto<T> deleteOk(T data) {
|
||||
return new ApiResponseDto<>(data, HttpStatus.NO_CONTENT);
|
||||
}
|
||||
|
||||
public static ApiResponseDto<String> createException(ApiResponseCode code) {
|
||||
return new ApiResponseDto<>(code);
|
||||
}
|
||||
|
||||
public static ApiResponseDto<String> createException(ApiResponseCode code, String message) {
|
||||
return new ApiResponseDto<>(code, message);
|
||||
}
|
||||
|
||||
public static ApiResponseDto<String> createException(
|
||||
ApiResponseCode code, String message, HttpStatus httpStatus) {
|
||||
return new ApiResponseDto<>(code, message, httpStatus);
|
||||
}
|
||||
|
||||
public static ApiResponseDto<String> createException(
|
||||
ApiResponseCode code, String message, HttpStatus httpStatus, Long errorLogUid) {
|
||||
return new ApiResponseDto<>(code, message, httpStatus, errorLogUid);
|
||||
}
|
||||
|
||||
public static <T> ApiResponseDto<T> createException(
|
||||
ApiResponseCode code, String message, T data) {
|
||||
return new ApiResponseDto<>(code, message, data);
|
||||
}
|
||||
|
||||
@Getter
|
||||
public static class Error {
|
||||
|
||||
private final String code;
|
||||
private final String message;
|
||||
|
||||
public Error(String code, String message) {
|
||||
this.code = code;
|
||||
this.message = message;
|
||||
}
|
||||
}
|
||||
|
||||
/** Error가 아닌 Business상 성공이거나 실패인 경우, 메세지 함께 전달하기 위한 object */
|
||||
@Getter
|
||||
public static class ResponseObj {
|
||||
|
||||
private final ApiResponseCode code;
|
||||
private final String message;
|
||||
|
||||
public ResponseObj(ApiResponseCode code, String message) {
|
||||
this.code = code;
|
||||
this.message = message;
|
||||
}
|
||||
}
|
||||
|
||||
@Getter
|
||||
@RequiredArgsConstructor
|
||||
public enum ApiResponseCode implements EnumType {
|
||||
|
||||
// @formatter:off
|
||||
OK("요청이 성공하였습니다."),
|
||||
BAD_REQUEST("요청 파라미터가 잘못되었습니다."),
|
||||
BAD_GATEWAY("네트워크 상태가 불안정합니다."),
|
||||
ALREADY_EXIST_MALL("이미 등록된 쇼핑센터입니다."),
|
||||
NOT_FOUND_MAP("지도를 찾을 수 없습니다."),
|
||||
UNAUTHORIZED("권한이 없습니다."),
|
||||
CONFLICT("이미 등록된 컨텐츠입니다."),
|
||||
NOT_FOUND("Resource를 찾을 수 없습니다."),
|
||||
NOT_FOUND_DATA("데이터를 찾을 수 없습니다."),
|
||||
NOT_FOUND_WEATHER_DATA("날씨 데이터를 찾을 수 없습니다."),
|
||||
FAIL_SEND_MESSAGE("메시지를 전송하지 못했습니다."),
|
||||
TOO_MANY_CONNECTED_MACHINES("연결된 기기가 너무 많습니다."),
|
||||
UNAUTHENTICATED("인증에 실패하였습니다."),
|
||||
INVALID_TOKEN("잘못된 토큰입니다."),
|
||||
EXPIRED_TOKEN("만료된 토큰입니다."),
|
||||
INTERNAL_SERVER_ERROR("서버에 문제가 발생 하였습니다."),
|
||||
FORBIDDEN("권한을 확인해주세요."),
|
||||
INVALID_PASSWORD("잘못된 비밀번호 입니다."),
|
||||
NOT_FOUND_CAR_IN("입차정보가 없습니다."),
|
||||
WRONG_STATUS("잘못된 상태입니다."),
|
||||
FAIL_VERIFICATION("인증에 실패하였습니다."),
|
||||
INVALID_EMAIL("잘못된 형식의 이메일입니다."),
|
||||
REQUIRED_EMAIL("이메일은 필수 항목입니다."),
|
||||
WRONG_PASSWORD("잘못된 패스워드입니다."),
|
||||
DUPLICATE_EMAIL("이미 가입된 이메일입니다."),
|
||||
DUPLICATE_DATA("이미 등록되어 있습니다."),
|
||||
DATA_INTEGRITY_ERROR("데이터 무결성이 위반되어 요청을 처리할수 없습니다."),
|
||||
FOREIGN_KEY_ERROR("참조 중인 데이터가 있어 삭제할 수 없습니다."),
|
||||
DUPLICATE_EMPLOYEEID("이미 가입된 사번입니다."),
|
||||
NOT_FOUND_USER_FOR_EMAIL("이메일로 유저를 찾을 수 없습니다."),
|
||||
NOT_FOUND_USER("사용자를 찾을 수 없습니다."),
|
||||
UNPROCESSABLE_ENTITY("이 데이터는 삭제할 수 없습니다."),
|
||||
UNPROCESSABLE_ENTITY_UPDATE("이 데이터는 수정할 수 없습니다."),
|
||||
LOGIN_ID_NOT_FOUND("아이디를 잘못 입력하셨습니다."),
|
||||
LOGIN_PASSWORD_MISMATCH("비밀번호를 잘못 입력하셨습니다."),
|
||||
LOGIN_PASSWORD_EXCEEDED("비밀번호 오류 횟수를 초과하여 이용하실 수 없습니다.\n로그인 오류에 대해 관리자에게 문의하시기 바랍니다."),
|
||||
REFRESH_TOKEN_EXPIRED_OR_REVOKED("토큰 정보가 만료 되었습니다."),
|
||||
REFRESH_TOKEN_MISMATCH("토큰 정보가 일치하지 않습니다."),
|
||||
INACTIVE_ID("사용할 수 없는 계정입니다."),
|
||||
INVALID_EMAIL_TOKEN(
|
||||
"You can only reset your password within 24 hours from when the email was sent.\n"
|
||||
+ "To reset your password again, please submit a new request through \"Forgot"
|
||||
+ " Password.\""),
|
||||
;
|
||||
// @formatter:on
|
||||
private final String message;
|
||||
|
||||
@Override
|
||||
public String getId() {
|
||||
return name();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getText() {
|
||||
return message;
|
||||
}
|
||||
|
||||
public static ApiResponseCode getCode(String name) {
|
||||
if (name == null || name.isBlank()) return null;
|
||||
try {
|
||||
return ApiResponseCode.valueOf(name.toUpperCase());
|
||||
} catch (IllegalArgumentException ex) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public static String getMessage(String name) {
|
||||
return ApiResponseCode.valueOf(name.toUpperCase()).getText();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
package com.kamco.cd.training.config;
|
||||
|
||||
import io.swagger.v3.oas.annotations.enums.SecuritySchemeType;
|
||||
import io.swagger.v3.oas.annotations.security.SecurityScheme;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
|
||||
@Configuration
|
||||
@SecurityScheme(
|
||||
name = "BearerAuth",
|
||||
type = SecuritySchemeType.HTTP,
|
||||
scheme = "bearer",
|
||||
bearerFormat = "JWT")
|
||||
public class SwaggerConfig {}
|
||||
package com.kamco.cd.training.config.swagger;
|
||||
|
||||
import io.swagger.v3.oas.annotations.enums.SecuritySchemeType;
|
||||
import io.swagger.v3.oas.annotations.security.SecurityScheme;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
|
||||
@Configuration
|
||||
@SecurityScheme(
|
||||
name = "BearerAuth",
|
||||
type = SecuritySchemeType.HTTP,
|
||||
scheme = "bearer",
|
||||
bearerFormat = "JWT")
|
||||
public class SwaggerConfig {}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -1,154 +1,300 @@
|
||||
package com.kamco.cd.training.dataset;
|
||||
|
||||
import com.kamco.cd.training.config.api.ApiResponseDto;
|
||||
import com.kamco.cd.training.dataset.dto.DatasetDto;
|
||||
import com.kamco.cd.training.dataset.service.DatasetService;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.Parameter;
|
||||
import io.swagger.v3.oas.annotations.media.Content;
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import io.swagger.v3.oas.annotations.responses.ApiResponse;
|
||||
import io.swagger.v3.oas.annotations.responses.ApiResponses;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import jakarta.validation.Valid;
|
||||
import java.util.UUID;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.data.domain.Page;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
@Tag(name = "데이터셋 관리", description = "어드민 홈 > 학습데이터관리 > 전체데이터 API")
|
||||
@RestController
|
||||
@RequestMapping("/api/datasets")
|
||||
@RequiredArgsConstructor
|
||||
public class DatasetApiController {
|
||||
|
||||
private final DatasetService datasetService;
|
||||
|
||||
@Operation(summary = "데이터셋 목록 조회", description = "데이터셋(회차) 목록을 조회합니다.")
|
||||
@ApiResponses(
|
||||
value = {
|
||||
@ApiResponse(
|
||||
responseCode = "200",
|
||||
description = "조회 성공",
|
||||
content =
|
||||
@Content(
|
||||
mediaType = "application/json",
|
||||
schema = @Schema(implementation = Page.class))),
|
||||
@ApiResponse(responseCode = "400", description = "잘못된 검색 조건", content = @Content),
|
||||
@ApiResponse(responseCode = "500", description = "서버 오류", content = @Content)
|
||||
})
|
||||
@GetMapping
|
||||
public ApiResponseDto<Page<DatasetDto.Basic>> searchDatasets(
|
||||
@Parameter(description = "구분", example = "DELIVER(납품), PRODUCTION(제작)")
|
||||
@RequestParam(required = false)
|
||||
String groupTitle,
|
||||
@Parameter(description = "제목", example = "") @RequestParam(required = false) String title,
|
||||
@Parameter(description = "페이지 번호 (0부터 시작)", example = "0") @RequestParam(defaultValue = "0")
|
||||
int page,
|
||||
@Parameter(description = "페이지 크기", example = "20") @RequestParam(defaultValue = "20")
|
||||
int size) {
|
||||
DatasetDto.SearchReq searchReq = new DatasetDto.SearchReq();
|
||||
searchReq.setTitle(title);
|
||||
searchReq.setGroupTitle(groupTitle);
|
||||
searchReq.setPage(page);
|
||||
searchReq.setSize(size);
|
||||
return ApiResponseDto.ok(datasetService.searchDatasets(searchReq));
|
||||
}
|
||||
|
||||
@Operation(summary = "데이터셋 상세 조회", description = "데이터셋 상세 정보를 조회합니다.")
|
||||
@ApiResponses(
|
||||
value = {
|
||||
@ApiResponse(
|
||||
responseCode = "200",
|
||||
description = "조회 성공",
|
||||
content =
|
||||
@Content(
|
||||
mediaType = "application/json",
|
||||
schema = @Schema(implementation = DatasetDto.Basic.class))),
|
||||
@ApiResponse(responseCode = "404", description = "데이터셋을 찾을 수 없음", content = @Content),
|
||||
@ApiResponse(responseCode = "500", description = "서버 오류", content = @Content)
|
||||
})
|
||||
@GetMapping("/{uuid}")
|
||||
public ApiResponseDto<DatasetDto.Basic> getDatasetDetail(@PathVariable UUID uuid) {
|
||||
return ApiResponseDto.ok(datasetService.getDatasetDetail(uuid));
|
||||
}
|
||||
|
||||
@Operation(summary = "데이터셋 등록", description = "신규 데이터셋(회차)을 생성합니다.")
|
||||
@ApiResponses(
|
||||
value = {
|
||||
@ApiResponse(
|
||||
responseCode = "201",
|
||||
description = "등록 성공",
|
||||
content =
|
||||
@Content(
|
||||
mediaType = "application/json",
|
||||
schema = @Schema(implementation = Long.class))),
|
||||
@ApiResponse(responseCode = "400", description = "잘못된 요청 데이터", content = @Content),
|
||||
@ApiResponse(responseCode = "500", description = "서버 오류", content = @Content)
|
||||
})
|
||||
@PostMapping("/register")
|
||||
public ApiResponseDto<Long> registerDataset(
|
||||
@RequestBody @Valid DatasetDto.RegisterReq registerReq) {
|
||||
Long id = datasetService.registerDataset(registerReq);
|
||||
return ApiResponseDto.createOK(id);
|
||||
}
|
||||
|
||||
@Operation(summary = "데이터셋 수정", description = "데이터셋 정보를 수정합니다.")
|
||||
@ApiResponses(
|
||||
value = {
|
||||
@ApiResponse(
|
||||
responseCode = "200",
|
||||
description = "수정 성공",
|
||||
content =
|
||||
@Content(
|
||||
mediaType = "application/json",
|
||||
schema = @Schema(implementation = Long.class))),
|
||||
@ApiResponse(responseCode = "400", description = "잘못된 요청 데이터", content = @Content),
|
||||
@ApiResponse(responseCode = "404", description = "데이터셋을 찾을 수 없음", content = @Content),
|
||||
@ApiResponse(responseCode = "500", description = "서버 오류", content = @Content)
|
||||
})
|
||||
@PutMapping("/{uuid}")
|
||||
public ApiResponseDto<UUID> updateDataset(
|
||||
@PathVariable UUID uuid, @RequestBody DatasetDto.UpdateReq updateReq) {
|
||||
datasetService.updateDataset(uuid, updateReq);
|
||||
return ApiResponseDto.ok(uuid);
|
||||
}
|
||||
|
||||
@Operation(summary = "데이터셋 삭제", description = "데이터셋을 삭제합니다.")
|
||||
@ApiResponses(
|
||||
value = {
|
||||
@ApiResponse(responseCode = "201", description = "삭제 성공", content = @Content),
|
||||
@ApiResponse(responseCode = "400", description = "잘못된 요청", content = @Content),
|
||||
@ApiResponse(responseCode = "404", description = "데이터셋을 찾을 수 없음", content = @Content),
|
||||
@ApiResponse(responseCode = "500", description = "서버 오류", content = @Content)
|
||||
})
|
||||
@DeleteMapping("/{uuid}")
|
||||
public ApiResponseDto<UUID> deleteDatasets(@PathVariable UUID uuid) {
|
||||
|
||||
datasetService.deleteDatasets(uuid);
|
||||
return ApiResponseDto.ok(uuid);
|
||||
}
|
||||
|
||||
/*
|
||||
@Operation(summary = "데이터셋 통계 요약", description = "선택 데이터셋의 통계를 요약합니다.")
|
||||
@ApiResponses(
|
||||
value = {
|
||||
@ApiResponse(
|
||||
responseCode = "200",
|
||||
description = "조회 성공",
|
||||
content =
|
||||
@Content(
|
||||
mediaType = "application/json",
|
||||
schema = @Schema(implementation = DatasetDto.Summary.class))),
|
||||
@ApiResponse(responseCode = "400", description = "잘못된 요청", content = @Content),
|
||||
@ApiResponse(responseCode = "500", description = "서버 오류", content = @Content)
|
||||
})
|
||||
@PostMapping("/summary")
|
||||
public ApiResponseDto<DatasetDto.Summary> getDatasetSummary(
|
||||
@RequestBody @Valid DatasetDto.SummaryReq summaryReq) {
|
||||
return ApiResponseDto.ok(datasetService.getDatasetSummary(summaryReq));
|
||||
}
|
||||
|
||||
*/
|
||||
|
||||
}
|
||||
package com.kamco.cd.training.dataset;
|
||||
|
||||
import com.kamco.cd.training.config.api.ApiResponseDto;
|
||||
import com.kamco.cd.training.dataset.dto.DatasetDto;
|
||||
import com.kamco.cd.training.dataset.dto.DatasetDto.AddDeliveriesReq;
|
||||
import com.kamco.cd.training.dataset.dto.DatasetObjDto;
|
||||
import com.kamco.cd.training.dataset.dto.DatasetObjDto.DatasetClass;
|
||||
import com.kamco.cd.training.dataset.dto.DatasetObjDto.DatasetStorage;
|
||||
import com.kamco.cd.training.dataset.service.DatasetAsyncService;
|
||||
import com.kamco.cd.training.dataset.service.DatasetService;
|
||||
import com.kamco.cd.training.model.dto.FileDto.FoldersDto;
|
||||
import com.kamco.cd.training.model.dto.FileDto.SrchFoldersDto;
|
||||
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.io.IOException;
|
||||
import java.nio.file.FileStore;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.core.io.Resource;
|
||||
import org.springframework.data.domain.Page;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
@Tag(name = "학습데이터 관리", description = "어드민 홈 > 학습데이터관리 > 전체데이터 API")
|
||||
@RestController
|
||||
@RequestMapping("/api/datasets")
|
||||
@RequiredArgsConstructor
|
||||
public class DatasetApiController {
|
||||
|
||||
private final DatasetService datasetService;
|
||||
private final DatasetAsyncService datasetAsyncService;
|
||||
|
||||
@Operation(summary = "학습데이터 관리 목록 조회", description = "학습데이터 목록을 조회합니다.")
|
||||
@ApiResponses(
|
||||
value = {
|
||||
@ApiResponse(
|
||||
responseCode = "200",
|
||||
description = "조회 성공",
|
||||
content =
|
||||
@Content(
|
||||
mediaType = "application/json",
|
||||
schema = @Schema(implementation = Page.class))),
|
||||
@ApiResponse(responseCode = "400", description = "잘못된 검색 조건", content = @Content),
|
||||
@ApiResponse(responseCode = "500", description = "서버 오류", content = @Content)
|
||||
})
|
||||
@GetMapping
|
||||
public ApiResponseDto<Page<DatasetDto.Basic>> searchDatasets(
|
||||
@Parameter(
|
||||
description = "구분",
|
||||
example = "",
|
||||
schema = @Schema(allowableValues = {"DELIVER", "PRODUCTION"}))
|
||||
@RequestParam(required = false)
|
||||
String dataType,
|
||||
@Parameter(description = "제목", example = "") @RequestParam(required = false) String title,
|
||||
@Parameter(description = "페이지 번호 (0부터 시작)", example = "0") @RequestParam(defaultValue = "0")
|
||||
int page,
|
||||
@Parameter(description = "페이지 크기", example = "20") @RequestParam(defaultValue = "20")
|
||||
int size) {
|
||||
DatasetDto.SearchReq searchReq = new DatasetDto.SearchReq();
|
||||
searchReq.setTitle(title);
|
||||
searchReq.setDataType(dataType);
|
||||
searchReq.setPage(page);
|
||||
searchReq.setSize(size);
|
||||
return ApiResponseDto.ok(datasetService.searchDatasets(searchReq));
|
||||
}
|
||||
|
||||
@Operation(summary = "학습데이터관리 상세 조회", description = "학습데이터관리 상세 정보를 조회합니다.")
|
||||
@ApiResponses(
|
||||
value = {
|
||||
@ApiResponse(
|
||||
responseCode = "200",
|
||||
description = "조회 성공",
|
||||
content =
|
||||
@Content(
|
||||
mediaType = "application/json",
|
||||
schema = @Schema(implementation = DatasetDto.Basic.class))),
|
||||
@ApiResponse(responseCode = "404", description = "데이터셋을 찾을 수 없음", content = @Content),
|
||||
@ApiResponse(responseCode = "500", description = "서버 오류", content = @Content)
|
||||
})
|
||||
@GetMapping("/{uuid}")
|
||||
public ApiResponseDto<DatasetDto.Basic> getDatasetDetail(@PathVariable UUID uuid) {
|
||||
return ApiResponseDto.ok(datasetService.getDatasetDetail(uuid));
|
||||
}
|
||||
|
||||
@Operation(summary = "학습데이터 수정", description = "학습데이터 제목, 메모 수정")
|
||||
@ApiResponses(
|
||||
value = {
|
||||
@ApiResponse(
|
||||
responseCode = "200",
|
||||
description = "수정 성공",
|
||||
content =
|
||||
@Content(
|
||||
mediaType = "application/json",
|
||||
schema = @Schema(implementation = Long.class))),
|
||||
@ApiResponse(responseCode = "400", description = "잘못된 요청 데이터", content = @Content),
|
||||
@ApiResponse(responseCode = "404", description = "데이터셋을 찾을 수 없음", content = @Content),
|
||||
@ApiResponse(responseCode = "500", description = "서버 오류", content = @Content)
|
||||
})
|
||||
@PutMapping("/{uuid}")
|
||||
public ApiResponseDto<UUID> updateDataset(
|
||||
@PathVariable UUID uuid, @RequestBody DatasetDto.UpdateReq updateReq) {
|
||||
datasetService.updateDataset(uuid, updateReq);
|
||||
return ApiResponseDto.ok(uuid);
|
||||
}
|
||||
|
||||
@Operation(summary = "학습데이터 삭제", description = "학습데이터를 삭제합니다.(납품 데이터는 삭제 불가)")
|
||||
@ApiResponses(
|
||||
value = {
|
||||
@ApiResponse(responseCode = "201", description = "삭제 성공", content = @Content),
|
||||
@ApiResponse(responseCode = "400", description = "잘못된 요청", content = @Content),
|
||||
@ApiResponse(responseCode = "404", description = "데이터셋을 찾을 수 없음", content = @Content),
|
||||
@ApiResponse(responseCode = "500", description = "서버 오류", content = @Content)
|
||||
})
|
||||
@DeleteMapping("/{uuid}")
|
||||
public ApiResponseDto<UUID> deleteDatasets(@PathVariable UUID uuid) {
|
||||
|
||||
datasetService.deleteDatasets(uuid);
|
||||
return ApiResponseDto.ok(uuid);
|
||||
}
|
||||
|
||||
@Operation(summary = "학습데이터 관리 목록 조회", description = "학습데이터 목록을 조회합니다.")
|
||||
@ApiResponses(
|
||||
value = {
|
||||
@ApiResponse(
|
||||
responseCode = "200",
|
||||
description = "조회 성공",
|
||||
content =
|
||||
@Content(
|
||||
mediaType = "application/json",
|
||||
schema = @Schema(implementation = Page.class))),
|
||||
@ApiResponse(responseCode = "400", description = "잘못된 검색 조건", content = @Content),
|
||||
@ApiResponse(responseCode = "500", description = "서버 오류", content = @Content)
|
||||
})
|
||||
@GetMapping("/obj-list")
|
||||
public ApiResponseDto<Page<DatasetObjDto.Basic>> searchDatasetObjectList(
|
||||
@Parameter(description = "회차 uuid", example = "e9a6774b-4f81-4402-b080-51d27fac1f01")
|
||||
@RequestParam(required = true)
|
||||
UUID uuid,
|
||||
@Parameter(description = "비교년도분류", example = "container") @RequestParam(required = false)
|
||||
String compareClassCd,
|
||||
@Parameter(description = "기준년도분류", example = "waste") @RequestParam(required = false)
|
||||
String targetClassCd,
|
||||
@Parameter(description = "도엽번호", example = "36713060") @RequestParam(required = false)
|
||||
String mapSheetNum,
|
||||
@RequestParam(defaultValue = "0") int page,
|
||||
@RequestParam(defaultValue = "20") int size) {
|
||||
DatasetObjDto.SearchReq searchReq = new DatasetObjDto.SearchReq();
|
||||
searchReq.setUuid(uuid);
|
||||
searchReq.setCompareClassCd(compareClassCd);
|
||||
searchReq.setTargetClassCd(targetClassCd);
|
||||
searchReq.setMapSheetNum(mapSheetNum);
|
||||
searchReq.setPage(page);
|
||||
searchReq.setSize(size);
|
||||
return ApiResponseDto.ok(datasetService.searchDatasetObjectList(searchReq));
|
||||
}
|
||||
|
||||
@Operation(summary = "학습데이터 관리 obj 삭제", description = "학습데이터 관리 obj 삭제 API")
|
||||
@ApiResponses(
|
||||
value = {
|
||||
@ApiResponse(
|
||||
responseCode = "200",
|
||||
description = "삭제 성공",
|
||||
content =
|
||||
@Content(
|
||||
mediaType = "application/json",
|
||||
schema = @Schema(implementation = Page.class))),
|
||||
@ApiResponse(responseCode = "400", description = "잘못된 검색 조건", content = @Content),
|
||||
@ApiResponse(responseCode = "500", description = "서버 오류", content = @Content)
|
||||
})
|
||||
@DeleteMapping("/obj/{uuid}")
|
||||
public ApiResponseDto<UUID> deleteDatasetObjByUuid(@PathVariable UUID uuid) {
|
||||
return ApiResponseDto.ok(datasetService.deleteDatasetObjByUuid(uuid));
|
||||
}
|
||||
|
||||
@Operation(summary = "학습데이터 결과 class 조회", description = "학습데이터 결과 class 조회 API")
|
||||
@ApiResponses(
|
||||
value = {
|
||||
@ApiResponse(
|
||||
responseCode = "200",
|
||||
description = "조회 성공",
|
||||
content =
|
||||
@Content(
|
||||
mediaType = "application/json",
|
||||
schema = @Schema(implementation = Page.class))),
|
||||
@ApiResponse(responseCode = "400", description = "잘못된 검색 조건", content = @Content),
|
||||
@ApiResponse(responseCode = "500", description = "서버 오류", content = @Content)
|
||||
})
|
||||
@GetMapping("/class/{uuid}")
|
||||
public ApiResponseDto<List<DatasetClass>> getDatasetObjByUuid(
|
||||
@Parameter(description = "dataset uuid", example = "e1416f32-769f-495c-a883-3ebfacef4bac")
|
||||
@PathVariable
|
||||
UUID uuid,
|
||||
@Parameter(description = "compare, target", example = "compare") @RequestParam String type) {
|
||||
return ApiResponseDto.ok(datasetService.getDatasetObjByUuid(uuid, type));
|
||||
}
|
||||
|
||||
@Operation(summary = "남은 저장공간 조회", description = "남은 저장공간 조회 API")
|
||||
@ApiResponses(
|
||||
value = {
|
||||
@ApiResponse(
|
||||
responseCode = "200",
|
||||
description = "조회 성공",
|
||||
content =
|
||||
@Content(
|
||||
mediaType = "application/json",
|
||||
schema = @Schema(implementation = Page.class))),
|
||||
@ApiResponse(responseCode = "404", description = "저장 공간 조회 오류", content = @Content),
|
||||
@ApiResponse(responseCode = "500", description = "서버 오류", content = @Content)
|
||||
})
|
||||
@GetMapping("/usable-bytes")
|
||||
public ApiResponseDto<DatasetStorage> getUsableBytes() 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);
|
||||
}
|
||||
|
||||
@Operation(summary = "납품 폴더 조회", description = "납품 폴더 조회 API")
|
||||
@ApiResponses(
|
||||
value = {
|
||||
@ApiResponse(
|
||||
responseCode = "200",
|
||||
description = "조회 성공",
|
||||
content =
|
||||
@Content(
|
||||
mediaType = "application/json",
|
||||
schema = @Schema(implementation = FoldersDto.class))),
|
||||
@ApiResponse(responseCode = "404", description = "조회 오류", content = @Content),
|
||||
@ApiResponse(responseCode = "500", description = "서버 오류", content = @Content)
|
||||
})
|
||||
@PostMapping("/folder-list")
|
||||
public ApiResponseDto<FoldersDto> getDir(@RequestBody SrchFoldersDto srchDto) throws IOException {
|
||||
|
||||
return ApiResponseDto.createOK(datasetService.getFolderAll(srchDto));
|
||||
}
|
||||
|
||||
@Operation(summary = "납품 학습데이터셋 등록", description = "납품 학습데이터셋 등록 API")
|
||||
@ApiResponses(
|
||||
value = {
|
||||
@ApiResponse(
|
||||
responseCode = "200",
|
||||
description = "등록 성공",
|
||||
content =
|
||||
@Content(
|
||||
mediaType = "application/json",
|
||||
schema = @Schema(implementation = String.class))),
|
||||
@ApiResponse(responseCode = "404", description = "조회 오류", content = @Content),
|
||||
@ApiResponse(responseCode = "500", description = "서버 오류", content = @Content)
|
||||
})
|
||||
@PostMapping("/deliveries")
|
||||
public ApiResponseDto<String> insertDeliveriesDataset(@RequestBody AddDeliveriesReq req) {
|
||||
|
||||
// 폴더 구조 검증
|
||||
DatasetService.validateTrainValTestDirs(req.getFilePath());
|
||||
// 파일 개수 검증
|
||||
DatasetService.validateDirFileCount(req.getFilePath());
|
||||
|
||||
datasetAsyncService.insertDeliveriesDatasetAsync(req);
|
||||
return ApiResponseDto.createOK("ok");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -1,212 +1,562 @@
|
||||
package com.kamco.cd.training.dataset.dto;
|
||||
|
||||
import com.kamco.cd.training.common.enums.LearnDataRegister;
|
||||
import com.kamco.cd.training.common.enums.LearnDataType;
|
||||
import com.kamco.cd.training.common.utils.enums.Enums;
|
||||
import com.kamco.cd.training.common.utils.interfaces.JsonFormatDttm;
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import jakarta.validation.constraints.NotBlank;
|
||||
import jakarta.validation.constraints.NotNull;
|
||||
import jakarta.validation.constraints.Size;
|
||||
import java.time.ZonedDateTime;
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Getter;
|
||||
import lombok.NoArgsConstructor;
|
||||
import lombok.Setter;
|
||||
import org.springframework.data.domain.PageRequest;
|
||||
import org.springframework.data.domain.Pageable;
|
||||
import org.springframework.data.domain.Sort;
|
||||
|
||||
public class DatasetDto {
|
||||
|
||||
@Schema(name = "Dataset Basic", description = "데이터셋 기본 정보")
|
||||
@Getter
|
||||
@Setter
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public static class Basic {
|
||||
|
||||
private Long id;
|
||||
private UUID uuid;
|
||||
private String groupTitle;
|
||||
private String groupTitleCd;
|
||||
private String title;
|
||||
private Long roundNo;
|
||||
private String totalSize;
|
||||
private String memo;
|
||||
@JsonFormatDttm private ZonedDateTime createdDttm;
|
||||
private String status;
|
||||
private String statusCd;
|
||||
private Boolean deleted;
|
||||
|
||||
public Basic(
|
||||
Long id,
|
||||
UUID uuid,
|
||||
String groupTitle,
|
||||
String title,
|
||||
Long roundNo,
|
||||
Long totalSize,
|
||||
String memo,
|
||||
ZonedDateTime createdDttm,
|
||||
String status,
|
||||
Boolean deleted) {
|
||||
this.id = id;
|
||||
this.uuid = uuid;
|
||||
this.groupTitle = getGroupTitle(groupTitle);
|
||||
this.groupTitleCd = groupTitle;
|
||||
this.title = title;
|
||||
this.roundNo = roundNo;
|
||||
this.totalSize = getTotalSize(totalSize);
|
||||
this.memo = memo;
|
||||
this.createdDttm = createdDttm;
|
||||
this.status = getStatus(status);
|
||||
this.statusCd = status;
|
||||
this.deleted = deleted;
|
||||
}
|
||||
|
||||
public String getTotalSize(Long totalSize) {
|
||||
if (totalSize == null) return "0G";
|
||||
double giga = totalSize / (1024.0 * 1024 * 1024);
|
||||
return String.format("%.2fG", giga);
|
||||
}
|
||||
|
||||
public String getGroupTitle(String groupTitleCd) {
|
||||
LearnDataType type = Enums.fromId(LearnDataType.class, groupTitleCd);
|
||||
return type == null ? null : type.getText();
|
||||
}
|
||||
|
||||
public String getStatus(String status) {
|
||||
LearnDataRegister type = Enums.fromId(LearnDataRegister.class, status);
|
||||
return type == null ? null : type.getText();
|
||||
}
|
||||
}
|
||||
|
||||
@Schema(name = "Dataset Detail", description = "데이터셋 상세 정보")
|
||||
@Getter
|
||||
@Setter
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public static class Detail {
|
||||
|
||||
private Long id;
|
||||
private String groupTitle;
|
||||
private String title;
|
||||
private Long roundNo;
|
||||
private String totalSize;
|
||||
private String memo;
|
||||
@JsonFormatDttm private ZonedDateTime createdDttm;
|
||||
private String status;
|
||||
private Boolean deleted;
|
||||
}
|
||||
|
||||
@Schema(name = "DatasetSearchReq", description = "데이터셋 목록 조회 요청")
|
||||
@Getter
|
||||
@Setter
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public static class SearchReq {
|
||||
|
||||
@Schema(description = "구분", example = "DELIVER(납품), PRODUCTION(제작)")
|
||||
private String groupTitle;
|
||||
|
||||
@Schema(description = "제목 (부분 검색)", example = "1차")
|
||||
private String title;
|
||||
|
||||
@Schema(description = "페이지 번호 (1부터 시작)", example = "1")
|
||||
private int page = 1;
|
||||
|
||||
@Schema(description = "페이지 크기", example = "20")
|
||||
private int size = 20;
|
||||
|
||||
public Pageable toPageable() {
|
||||
// API에서는 1부터 시작하지만 내부적으로는 0부터 시작
|
||||
int pageIndex = Math.max(0, page - 1);
|
||||
return PageRequest.of(pageIndex, size, Sort.by(Sort.Direction.DESC, "createdDttm"));
|
||||
}
|
||||
}
|
||||
|
||||
@Schema(name = "DatasetDetailReq", description = "데이터셋 상세 조회 요청")
|
||||
@Getter
|
||||
@Setter
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public static class DetailReq {
|
||||
|
||||
@NotNull(message = "데이터셋 ID는 필수입니다")
|
||||
@Schema(description = "데이터셋 ID", example = "101")
|
||||
private Long datasetId;
|
||||
}
|
||||
|
||||
@Schema(name = "DatasetRegisterReq", description = "데이터셋 등록 요청")
|
||||
@Getter
|
||||
@Setter
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public static class RegisterReq {
|
||||
|
||||
@NotBlank(message = "제목은 필수입니다")
|
||||
@Size(max = 200, message = "제목은 최대 200자까지 입력 가능합니다")
|
||||
@Schema(description = "제목", example = "1차 제작")
|
||||
private String title;
|
||||
|
||||
@NotBlank(message = "연도는 필수입니다")
|
||||
@Size(max = 4, message = "연도는 4자리입니다")
|
||||
@Schema(description = "연도 (YYYY)", example = "2024")
|
||||
private String year;
|
||||
|
||||
@Schema(description = "회차", example = "1")
|
||||
private Long roundNo;
|
||||
|
||||
@Schema(description = "메모", example = "데이터셋 설명")
|
||||
private String memo;
|
||||
}
|
||||
|
||||
@Schema(name = "DatasetUpdateReq", description = "데이터셋 수정 요청")
|
||||
@Getter
|
||||
@Setter
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public static class UpdateReq {
|
||||
|
||||
@Size(max = 200, message = "제목은 최대 200자까지 입력 가능합니다")
|
||||
@Schema(description = "제목", example = "1차 제작")
|
||||
private String title;
|
||||
|
||||
@Schema(description = "메모", example = "데이터셋 설명")
|
||||
private String memo;
|
||||
}
|
||||
|
||||
@Schema(name = "DatasetSummaryReq", description = "데이터셋 통계 요약 요청")
|
||||
@Getter
|
||||
@Setter
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public static class SummaryReq {
|
||||
|
||||
@NotNull(message = "데이터셋 ID 목록은 필수입니다")
|
||||
@Schema(description = "데이터셋 ID 목록", example = "[101, 105]")
|
||||
private List<Long> datasetIds;
|
||||
}
|
||||
|
||||
@Schema(name = "DatasetSummary", description = "데이터셋 통계 요약")
|
||||
@Getter
|
||||
@Setter
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public static class Summary {
|
||||
|
||||
@Schema(description = "총 데이터셋 수", example = "2")
|
||||
private int totalDatasets;
|
||||
|
||||
@Schema(description = "총 도엽 수", example = "1500")
|
||||
private long totalMapSheets;
|
||||
|
||||
@Schema(description = "총 파일 크기 (bytes)", example = "10737418240")
|
||||
private long totalFileSize;
|
||||
|
||||
@Schema(description = "평균 도엽 수", example = "750")
|
||||
private double averageMapSheets;
|
||||
}
|
||||
}
|
||||
package com.kamco.cd.training.dataset.dto;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonIgnore;
|
||||
import com.kamco.cd.training.common.enums.LearnDataRegister;
|
||||
import com.kamco.cd.training.common.enums.LearnDataType;
|
||||
import com.kamco.cd.training.common.enums.ModelType;
|
||||
import com.kamco.cd.training.common.utils.enums.Enums;
|
||||
import com.kamco.cd.training.common.utils.interfaces.JsonFormatDttm;
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import jakarta.validation.constraints.NotBlank;
|
||||
import jakarta.validation.constraints.NotNull;
|
||||
import jakarta.validation.constraints.Size;
|
||||
import java.time.ZonedDateTime;
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Getter;
|
||||
import lombok.NoArgsConstructor;
|
||||
import lombok.Setter;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.data.domain.PageRequest;
|
||||
import org.springframework.data.domain.Pageable;
|
||||
import org.springframework.data.domain.Sort;
|
||||
|
||||
@Slf4j
|
||||
public class DatasetDto {
|
||||
|
||||
@Schema(name = "Dataset Basic", description = "데이터셋 기본 정보")
|
||||
@Getter
|
||||
@Setter
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public static class Basic {
|
||||
|
||||
private Long id;
|
||||
private UUID uuid;
|
||||
private String title;
|
||||
private Long roundNo;
|
||||
private Integer compareYyyy;
|
||||
private Integer targetYyyy;
|
||||
private String totalSize;
|
||||
private String memo;
|
||||
@JsonFormatDttm private ZonedDateTime createdDttm;
|
||||
private String status;
|
||||
private String statusCd;
|
||||
private Boolean deleted;
|
||||
private String dataType;
|
||||
|
||||
public Basic(
|
||||
Long id,
|
||||
UUID uuid,
|
||||
String title,
|
||||
Long roundNo,
|
||||
Integer compareYyyy,
|
||||
Integer targetYyyy,
|
||||
Long totalSize,
|
||||
String memo,
|
||||
ZonedDateTime createdDttm,
|
||||
String status,
|
||||
Boolean deleted,
|
||||
String dataType) {
|
||||
this.id = id;
|
||||
this.uuid = uuid;
|
||||
this.title = title;
|
||||
this.roundNo = roundNo;
|
||||
this.compareYyyy = compareYyyy;
|
||||
this.targetYyyy = targetYyyy;
|
||||
this.totalSize = getTotalSize(totalSize);
|
||||
this.memo = memo;
|
||||
this.createdDttm = createdDttm;
|
||||
this.status = getStatus(status);
|
||||
this.statusCd = status;
|
||||
this.deleted = deleted;
|
||||
this.dataType = dataType;
|
||||
}
|
||||
|
||||
public String getTotalSize(Long totalSize) {
|
||||
if (totalSize == null || totalSize <= 0) return "0M";
|
||||
|
||||
double giga = totalSize / (1024.0 * 1024 * 1024);
|
||||
|
||||
if (giga >= 1) {
|
||||
return String.format("%.2fG", giga);
|
||||
} else {
|
||||
double mega = totalSize / (1024.0 * 1024);
|
||||
return String.format("%.2fM", mega);
|
||||
}
|
||||
}
|
||||
|
||||
public String getStatus(String status) {
|
||||
LearnDataRegister type = Enums.fromId(LearnDataRegister.class, status);
|
||||
return type == null ? null : type.getText();
|
||||
}
|
||||
|
||||
public String getYear() {
|
||||
return this.compareYyyy + "-" + this.targetYyyy;
|
||||
}
|
||||
|
||||
public String getDataTypeName() {
|
||||
LearnDataType type = Enums.fromId(LearnDataType.class, this.dataType);
|
||||
return type == null ? null : type.getText();
|
||||
}
|
||||
}
|
||||
|
||||
@Schema(name = "Dataset Detail", description = "데이터셋 상세 정보")
|
||||
@Getter
|
||||
@Setter
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public static class Detail {
|
||||
|
||||
private Long id;
|
||||
private String groupTitle;
|
||||
private String title;
|
||||
private Long roundNo;
|
||||
private String totalSize;
|
||||
private String memo;
|
||||
@JsonFormatDttm private ZonedDateTime createdDttm;
|
||||
private String status;
|
||||
private Boolean deleted;
|
||||
}
|
||||
|
||||
@Schema(name = "DatasetSearchReq", description = "데이터셋 목록 조회 요청")
|
||||
@Getter
|
||||
@Setter
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public static class SearchReq {
|
||||
|
||||
@Schema(description = "구분")
|
||||
private String dataType;
|
||||
|
||||
@Schema(description = "제목 (부분 검색)", example = "1차")
|
||||
private String title;
|
||||
|
||||
@Schema(description = "페이지 번호 (1부터 시작)", example = "1")
|
||||
private int page = 1;
|
||||
|
||||
@Schema(description = "페이지 크기", example = "20")
|
||||
private int size = 20;
|
||||
|
||||
public Pageable toPageable() {
|
||||
return PageRequest.of(page, size, Sort.by(Sort.Direction.DESC, "createdDttm"));
|
||||
}
|
||||
}
|
||||
|
||||
// TODO 미사용시작
|
||||
@Schema(name = "DatasetDetailReq", description = "데이터셋 상세 조회 요청")
|
||||
@Getter
|
||||
@Setter
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public static class DetailReq {
|
||||
|
||||
@NotNull(message = "데이터셋 ID는 필수입니다")
|
||||
@Schema(description = "데이터셋 ID", example = "101")
|
||||
private Long datasetId;
|
||||
}
|
||||
|
||||
// TODO 미사용 끝
|
||||
|
||||
@Schema(name = "DatasetRegisterReq", description = "데이터셋 등록 요청")
|
||||
@Getter
|
||||
@Setter
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public static class RegisterReq {
|
||||
|
||||
@NotBlank(message = "제목은 필수입니다")
|
||||
@Size(max = 200, message = "제목은 최대 200자까지 입력 가능합니다")
|
||||
@Schema(description = "제목", example = "1차 제작")
|
||||
private String title;
|
||||
|
||||
@Schema(description = "비교연도 (YYYY)", example = "2023")
|
||||
private Integer compareYear;
|
||||
|
||||
@Schema(description = "기준연도 (YYYY)", example = "2024")
|
||||
private Integer targetYyyy;
|
||||
|
||||
@Schema(description = "회차", example = "1")
|
||||
private Long roundNo;
|
||||
|
||||
@Schema(description = "메모", example = "데이터셋 설명")
|
||||
private String memo;
|
||||
}
|
||||
|
||||
@Schema(name = "DatasetUpdateReq", description = "데이터셋 수정 요청")
|
||||
@Getter
|
||||
@Setter
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public static class UpdateReq {
|
||||
|
||||
@Size(max = 200, message = "제목은 최대 200자까지 입력 가능합니다")
|
||||
@Schema(description = "제목", example = "1차 제작")
|
||||
private String title;
|
||||
|
||||
@Schema(description = "메모", example = "데이터셋 설명")
|
||||
private String memo;
|
||||
}
|
||||
|
||||
@Schema(name = "DatasetSummaryReq", description = "데이터셋 통계 요약 요청")
|
||||
@Getter
|
||||
@Setter
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public static class SummaryReq {
|
||||
|
||||
@NotNull(message = "데이터셋 ID 목록은 필수입니다")
|
||||
@Schema(description = "데이터셋 ID 목록", example = "[101, 105]")
|
||||
private List<Long> datasetIds;
|
||||
}
|
||||
|
||||
@Schema(name = "DatasetSummary", description = "데이터셋 통계 요약")
|
||||
@Getter
|
||||
@Setter
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public static class Summary {
|
||||
|
||||
@Schema(description = "총 데이터셋 수", example = "2")
|
||||
private int totalDatasets;
|
||||
|
||||
@Schema(description = "총 도엽 수", example = "1500")
|
||||
private long totalMapSheets;
|
||||
|
||||
@Schema(description = "총 파일 크기 (bytes)", example = "10737418240")
|
||||
private long totalFileSize;
|
||||
|
||||
@Schema(description = "평균 도엽 수", example = "750")
|
||||
private double averageMapSheets;
|
||||
}
|
||||
|
||||
@Schema(name = "SelectDataSet", description = "데이터셋 선택 리스트")
|
||||
@Getter
|
||||
@Setter
|
||||
@NoArgsConstructor
|
||||
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;
|
||||
}
|
||||
|
||||
@Getter
|
||||
@Setter
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public static class AddDeliveriesReq {
|
||||
@Schema(description = "경로", example = "/")
|
||||
private String filePath;
|
||||
|
||||
@Schema(description = "제목", example = "")
|
||||
private String title;
|
||||
|
||||
@Schema(description = "메모", example = "")
|
||||
private String memo;
|
||||
|
||||
@Schema(description = "비교년도", example = "")
|
||||
private Integer compareYyyy;
|
||||
|
||||
@Schema(description = "기준년도", example = "")
|
||||
private Integer targetYyyy;
|
||||
|
||||
@Schema(description = "회차", example = "")
|
||||
private Long roundNo;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -1,103 +1,106 @@
|
||||
package com.kamco.cd.training.dataset.dto;
|
||||
|
||||
import com.kamco.cd.training.common.utils.interfaces.JsonFormatDttm;
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import jakarta.validation.constraints.NotNull;
|
||||
import java.time.ZonedDateTime;
|
||||
import java.util.List;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Getter;
|
||||
import lombok.NoArgsConstructor;
|
||||
import lombok.Setter;
|
||||
import org.springframework.data.domain.PageRequest;
|
||||
import org.springframework.data.domain.Pageable;
|
||||
import org.springframework.data.domain.Sort;
|
||||
|
||||
public class MapSheetDto {
|
||||
|
||||
@Schema(name = "MapSheet Basic", description = "도엽 기본 정보")
|
||||
@Getter
|
||||
@Setter
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public static class Basic {
|
||||
|
||||
private Long id;
|
||||
private Long datasetId;
|
||||
private String sheetNum;
|
||||
private String fileName;
|
||||
private Long fileSize;
|
||||
private String filePath;
|
||||
private String status;
|
||||
private String memo;
|
||||
private Boolean deleted;
|
||||
|
||||
@JsonFormatDttm private ZonedDateTime createdDttm;
|
||||
|
||||
@JsonFormatDttm private ZonedDateTime updatedDttm;
|
||||
}
|
||||
|
||||
@Schema(name = "MapSheetSearchReq", description = "도엽 목록 조회 요청")
|
||||
@Getter
|
||||
@Setter
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public static class SearchReq {
|
||||
|
||||
@NotNull(message = "데이터셋 ID는 필수입니다")
|
||||
@Schema(description = "데이터셋 ID", example = "101")
|
||||
private Long datasetId;
|
||||
|
||||
@Schema(description = "페이지 번호 (1부터 시작)", example = "1")
|
||||
private int page = 1;
|
||||
|
||||
@Schema(description = "페이지 크기", example = "20")
|
||||
private int size = 20;
|
||||
|
||||
public Pageable toPageable() {
|
||||
int pageIndex = Math.max(0, page - 1);
|
||||
return PageRequest.of(pageIndex, size, Sort.by(Sort.Direction.DESC, "createdDttm"));
|
||||
}
|
||||
}
|
||||
|
||||
@Schema(name = "MapSheetDeleteReq", description = "도엽 삭제 요청 (다건)")
|
||||
@Getter
|
||||
@Setter
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public static class DeleteReq {
|
||||
|
||||
@NotNull(message = "삭제할 도엽 ID 목록은 필수입니다")
|
||||
@Schema(description = "삭제할 도엽 ID 목록", example = "[9991, 9992]")
|
||||
private List<Long> itemIds;
|
||||
}
|
||||
|
||||
@Schema(name = "MapSheetCheckReq", description = "도엽 번호 유효성 검증 요청")
|
||||
@Getter
|
||||
@Setter
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public static class CheckReq {
|
||||
|
||||
@NotNull(message = "도엽 번호는 필수입니다")
|
||||
@Schema(description = "도엽 번호", example = "377055")
|
||||
private String sheetNum;
|
||||
}
|
||||
|
||||
@Schema(name = "MapSheetCheckRes", description = "도엽 번호 유효성 검증 응답")
|
||||
@Getter
|
||||
@Setter
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public static class CheckRes {
|
||||
|
||||
@Schema(description = "유효 여부", example = "true")
|
||||
private boolean valid;
|
||||
|
||||
@Schema(description = "메시지", example = "유효한 도엽 번호입니다")
|
||||
private String message;
|
||||
|
||||
@Schema(description = "중복 여부", example = "false")
|
||||
private boolean duplicate;
|
||||
}
|
||||
}
|
||||
// TODO 미사용시작
|
||||
package com.kamco.cd.training.dataset.dto;
|
||||
|
||||
import com.kamco.cd.training.common.utils.interfaces.JsonFormatDttm;
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import jakarta.validation.constraints.NotNull;
|
||||
import java.time.ZonedDateTime;
|
||||
import java.util.List;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Getter;
|
||||
import lombok.NoArgsConstructor;
|
||||
import lombok.Setter;
|
||||
import org.springframework.data.domain.PageRequest;
|
||||
import org.springframework.data.domain.Pageable;
|
||||
import org.springframework.data.domain.Sort;
|
||||
|
||||
public class MapSheetDto {
|
||||
|
||||
@Schema(name = "MapSheet Basic", description = "도엽 기본 정보")
|
||||
@Getter
|
||||
@Setter
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public static class Basic {
|
||||
|
||||
private Long id;
|
||||
private Long datasetId;
|
||||
private String sheetNum;
|
||||
private String fileName;
|
||||
private Long fileSize;
|
||||
private String filePath;
|
||||
private String status;
|
||||
private String memo;
|
||||
private Boolean deleted;
|
||||
|
||||
@JsonFormatDttm private ZonedDateTime createdDttm;
|
||||
|
||||
@JsonFormatDttm private ZonedDateTime updatedDttm;
|
||||
}
|
||||
|
||||
@Schema(name = "MapSheetSearchReq", description = "도엽 목록 조회 요청")
|
||||
@Getter
|
||||
@Setter
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public static class SearchReq {
|
||||
|
||||
@NotNull(message = "데이터셋 ID는 필수입니다")
|
||||
@Schema(description = "데이터셋 ID", example = "101")
|
||||
private Long datasetId;
|
||||
|
||||
@Schema(description = "페이지 번호 (1부터 시작)", example = "1")
|
||||
private int page = 1;
|
||||
|
||||
@Schema(description = "페이지 크기", example = "20")
|
||||
private int size = 20;
|
||||
|
||||
public Pageable toPageable() {
|
||||
int pageIndex = Math.max(0, page - 1);
|
||||
return PageRequest.of(pageIndex, size, Sort.by(Sort.Direction.DESC, "createdDttm"));
|
||||
}
|
||||
}
|
||||
|
||||
@Schema(name = "MapSheetDeleteReq", description = "도엽 삭제 요청 (다건)")
|
||||
@Getter
|
||||
@Setter
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public static class DeleteReq {
|
||||
|
||||
@NotNull(message = "삭제할 도엽 ID 목록은 필수입니다")
|
||||
@Schema(description = "삭제할 도엽 ID 목록", example = "[9991, 9992]")
|
||||
private List<Long> itemIds;
|
||||
}
|
||||
|
||||
// TODO 미사용시작
|
||||
@Schema(name = "MapSheetCheckReq", description = "도엽 번호 유효성 검증 요청")
|
||||
@Getter
|
||||
@Setter
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public static class CheckReq {
|
||||
|
||||
@NotNull(message = "도엽 번호는 필수입니다")
|
||||
@Schema(description = "도엽 번호", example = "377055")
|
||||
private String sheetNum;
|
||||
}
|
||||
|
||||
@Schema(name = "MapSheetCheckRes", description = "도엽 번호 유효성 검증 응답")
|
||||
@Getter
|
||||
@Setter
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public static class CheckRes {
|
||||
|
||||
@Schema(description = "유효 여부", example = "true")
|
||||
private boolean valid;
|
||||
|
||||
@Schema(description = "메시지", example = "유효한 도엽 번호입니다")
|
||||
private String message;
|
||||
|
||||
@Schema(description = "중복 여부", example = "false")
|
||||
private boolean duplicate;
|
||||
}
|
||||
}
|
||||
// TODO 미사용 끝
|
||||
|
||||
@@ -0,0 +1,124 @@
|
||||
package com.kamco.cd.training.dataset.service;
|
||||
|
||||
import com.kamco.cd.training.common.enums.LearnDataRegister;
|
||||
import com.kamco.cd.training.dataset.dto.DatasetDto.AddDeliveriesReq;
|
||||
import com.kamco.cd.training.dataset.dto.DatasetDto.DatasetMngRegDto;
|
||||
import com.kamco.cd.training.postgres.core.DatasetCoreService;
|
||||
import java.util.UUID;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.log4j.Log4j2;
|
||||
import org.springframework.scheduling.annotation.Async;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
@Service
|
||||
@Log4j2
|
||||
@RequiredArgsConstructor
|
||||
public class DatasetAsyncService {
|
||||
|
||||
private final DatasetService datasetService;
|
||||
private final DatasetCoreService datasetCoreService;
|
||||
|
||||
private static final String LOG_PREFIX = "[납품 데이터셋]";
|
||||
|
||||
/**
|
||||
* 납품 데이터셋 등록 비동기 업로드 처리 1) 데이터셋 구조/파일 검증 2) UID 생성 및 마스터 데이터 저장 3) 상태를 UPLOADING으로 변경 4) 실제
|
||||
* 데이터(train/val/test) 등록 5) 완료 시 COMPLETED 상태로 변경 6) 실패 시 상태를 UPLOAD_FAILED로 변경 후 데이터 정리(삭제)
|
||||
*
|
||||
* @param req
|
||||
*/
|
||||
@Async("datasetExecutor")
|
||||
public void insertDeliveriesDatasetAsync(AddDeliveriesReq req) {
|
||||
|
||||
long startTime = System.currentTimeMillis();
|
||||
|
||||
log.info("{} 업로드 시작 ==========", LOG_PREFIX);
|
||||
log.info(
|
||||
"{} filePath={}, targetYyyy={}, compareYyyy={}, roundNo={}",
|
||||
LOG_PREFIX,
|
||||
req.getFilePath(),
|
||||
req.getTargetYyyy(),
|
||||
req.getCompareYyyy(),
|
||||
req.getRoundNo());
|
||||
|
||||
Long datasetUid = null;
|
||||
|
||||
try {
|
||||
|
||||
// ===== 1. UID 생성 =====
|
||||
String uid = UUID.randomUUID().toString().replace("-", "").toUpperCase();
|
||||
log.info("{} 생성된 UID: {}", LOG_PREFIX, uid);
|
||||
|
||||
// ===== 2. 마스터 데이터 생성 =====
|
||||
String title = req.getTitle();
|
||||
|
||||
if (title == null || title.isBlank()) {
|
||||
Integer compareYyyy = req.getCompareYyyy();
|
||||
Integer targetYyyy = req.getTargetYyyy();
|
||||
|
||||
if (compareYyyy != null && targetYyyy != null) {
|
||||
title = compareYyyy + "-" + targetYyyy;
|
||||
} else {
|
||||
title = null;
|
||||
}
|
||||
}
|
||||
|
||||
DatasetMngRegDto datasetMngRegDto = new DatasetMngRegDto();
|
||||
datasetMngRegDto.setUid(uid);
|
||||
datasetMngRegDto.setDataType("DELIVER");
|
||||
datasetMngRegDto.setCompareYyyy(req.getCompareYyyy() == null ? 0 : req.getCompareYyyy());
|
||||
datasetMngRegDto.setTargetYyyy(req.getTargetYyyy() == null ? 0 : req.getTargetYyyy());
|
||||
datasetMngRegDto.setRoundNo(req.getRoundNo());
|
||||
datasetMngRegDto.setTitle(title);
|
||||
datasetMngRegDto.setMemo(req.getMemo());
|
||||
datasetMngRegDto.setDatasetPath(req.getFilePath());
|
||||
|
||||
// 마스터 저장
|
||||
datasetUid = datasetCoreService.insertDatasetMngData(datasetMngRegDto);
|
||||
|
||||
log.info("{} 마스터 저장 완료. datasetUid={}", LOG_PREFIX, datasetUid);
|
||||
|
||||
// ===== 3. 상태 변경 (업로드중) =====
|
||||
datasetCoreService.updateDatasetUploadStatus(datasetUid, LearnDataRegister.UPLOADING);
|
||||
log.info("{} 상태 변경 → UPLOADING. datasetUid={}", LOG_PREFIX, datasetUid);
|
||||
|
||||
// ===== 4. 데이터 등록 =====
|
||||
long insertStart = System.currentTimeMillis();
|
||||
|
||||
// 납품 데이터 obj 등록
|
||||
datasetService.insertDeliveriesDataset(req, datasetUid);
|
||||
|
||||
log.info(
|
||||
"{} 데이터 등록 완료. datasetUid={}, 소요시간={} ms",
|
||||
LOG_PREFIX,
|
||||
datasetUid,
|
||||
System.currentTimeMillis() - insertStart);
|
||||
|
||||
// ===== 5. 상태 변경 (완료) =====
|
||||
datasetCoreService.updateDatasetUploadStatus(datasetUid, LearnDataRegister.COMPLETED);
|
||||
log.info("{} 상태 변경 → COMPLETED. datasetUid={}", LOG_PREFIX, datasetUid);
|
||||
|
||||
log.info(
|
||||
"{} 업로드 완료. 총 소요시간={} ms ==========", LOG_PREFIX, System.currentTimeMillis() - startTime);
|
||||
|
||||
} catch (Exception e) {
|
||||
|
||||
log.error(
|
||||
"{} 업로드 실패. datasetUid={}, filePath={}", LOG_PREFIX, datasetUid, req.getFilePath(), e);
|
||||
|
||||
if (datasetUid != null) {
|
||||
try {
|
||||
// ===== 실패 처리 =====
|
||||
datasetCoreService.updateDatasetUploadStatus(datasetUid, LearnDataRegister.UPLOAD_FAILED);
|
||||
log.error("{} 상태 변경 → 업로드 실패. datasetUid={}", LOG_PREFIX, datasetUid);
|
||||
|
||||
// 실패 시 데이터 정리
|
||||
datasetCoreService.deleteAllDatasetObj(datasetUid);
|
||||
log.error("{} 데이터 정리 완료. datasetUid={}", LOG_PREFIX, datasetUid);
|
||||
|
||||
} catch (Exception ex) {
|
||||
log.error("{} 실패 후 정리 작업 중 오류. datasetUid={}", LOG_PREFIX, datasetUid, ex);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,199 @@
|
||||
package com.kamco.cd.training.dataset.service;
|
||||
|
||||
import com.fasterxml.jackson.databind.JsonNode;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.fasterxml.jackson.databind.node.ArrayNode;
|
||||
import com.fasterxml.jackson.databind.node.ObjectNode;
|
||||
import com.kamco.cd.training.dataset.dto.DatasetObjDto.DatasetObjRegDto;
|
||||
import com.kamco.cd.training.postgres.core.DatasetCoreService;
|
||||
import java.nio.file.Paths;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.log4j.Log4j2;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
@Service
|
||||
@Log4j2
|
||||
@RequiredArgsConstructor
|
||||
public class DatasetBatchService {
|
||||
|
||||
private final DatasetCoreService datasetCoreService;
|
||||
private final ObjectMapper mapper;
|
||||
|
||||
/**
|
||||
* 배치 단위 데이터 저장
|
||||
*
|
||||
* <p>- 전달받은 데이터 목록을 순회하며 개별 insert 처리 - batch 단위로 트랜잭션 관리
|
||||
*/
|
||||
@Transactional
|
||||
public void saveBatch(List<Map<String, Object>> batch, Long datasetUid, String type) {
|
||||
for (Map<String, Object> map : batch) {
|
||||
try {
|
||||
insertTrainTestData(map, datasetUid, type);
|
||||
} catch (Exception e) {
|
||||
log.error("파일 단위 실패. skip. file={}", batch, e);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 단일 데이터 처리 및 insert DTO 생성
|
||||
*
|
||||
* <p>처리 흐름: 1) 경로/JSON 데이터 추출 2) 파일명에서 연도 및 도엽번호 파싱 3) label JSON → feature 단위 분리 4) feature별 DTO
|
||||
* 생성 후 DB insert
|
||||
*/
|
||||
private void insertTrainTestData(Map<String, Object> map, Long datasetUid, String subDir) {
|
||||
|
||||
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");
|
||||
|
||||
// JSON 파싱
|
||||
JsonNode json;
|
||||
try {
|
||||
json = parseJson(labelJson);
|
||||
if (json == null) {
|
||||
log.warn("json null. skip. file={}", labelJson);
|
||||
return;
|
||||
}
|
||||
} catch (Exception e) {
|
||||
// 실패하면 skip, 다음 진행
|
||||
log.error("GeoJSON 파싱 실패. skip. file={}", geojsonPath, e);
|
||||
return;
|
||||
}
|
||||
|
||||
// 파일명 파싱
|
||||
String fileName = Paths.get(comparePath).getFileName().toString();
|
||||
String[] fileNameStr = fileName.split("_");
|
||||
|
||||
if (fileNameStr.length < 4) {
|
||||
log.error("파일명 파싱 실패: {}", fileName);
|
||||
return;
|
||||
// throw new IllegalArgumentException("잘못된 파일명 형식: " + fileName);
|
||||
}
|
||||
|
||||
int compareYyyy = 0;
|
||||
int targetYyyy = 0;
|
||||
try {
|
||||
compareYyyy = parseInt(fileNameStr[1], "compareYyyy", fileName);
|
||||
targetYyyy = parseInt(fileNameStr[2], "targetYyyy", fileName);
|
||||
} catch (Exception e) {
|
||||
log.error("기준년도 파싱 실패: {}", fileName);
|
||||
return;
|
||||
}
|
||||
|
||||
String mapSheetNum = fileNameStr[3];
|
||||
|
||||
// JSON 유효성 체크
|
||||
JsonNode featuresNode = json.path("features");
|
||||
|
||||
// 2. 비어있는지 확인
|
||||
if (featuresNode.isEmpty()) {
|
||||
log.warn("features empty. skip. file={}", geojsonPath);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!featuresNode.isArray()) {
|
||||
log.warn("features array 아님. skip. file={}", geojsonPath);
|
||||
return; // skip
|
||||
}
|
||||
|
||||
if (featuresNode.isMissingNode() || !featuresNode.isArray() || featuresNode.isEmpty()) {
|
||||
return; // skip
|
||||
}
|
||||
|
||||
ObjectNode base = mapper.createObjectNode();
|
||||
base.put("type", "FeatureCollection");
|
||||
|
||||
for (JsonNode feature : featuresNode) {
|
||||
|
||||
try {
|
||||
JsonNode prop = feature.path("properties");
|
||||
|
||||
String compareClassCd = prop.path("before").asText(null);
|
||||
String targetClassCd = prop.path("after").asText(null);
|
||||
|
||||
// null 방어
|
||||
if (compareClassCd == null || targetClassCd == null) {
|
||||
log.warn("class 값 없음. skip. file={}", fileName);
|
||||
continue;
|
||||
}
|
||||
|
||||
ArrayNode arr = mapper.createArrayNode();
|
||||
arr.add(feature);
|
||||
|
||||
ObjectNode root = base.deepCopy();
|
||||
root.set("features", arr);
|
||||
|
||||
DatasetObjRegDto objRegDto =
|
||||
DatasetObjRegDto.builder()
|
||||
.datasetUid(datasetUid)
|
||||
.compareYyyy(compareYyyy)
|
||||
.compareClassCd(compareClassCd)
|
||||
.targetYyyy(targetYyyy)
|
||||
.targetClassCd(targetClassCd)
|
||||
.comparePath(comparePath)
|
||||
.targetPath(targetPath)
|
||||
.labelPath(labelPath)
|
||||
.mapSheetNum(mapSheetNum)
|
||||
.geojson(root)
|
||||
.geojsonPath(geojsonPath)
|
||||
.fileName(fileName)
|
||||
.build();
|
||||
|
||||
// 데이터 타입별 insert
|
||||
insertByType(subDir, objRegDto);
|
||||
} catch (Exception e) {
|
||||
// 개별 feature skip
|
||||
log.error("feature 처리 실패. skip. file={}", fileName, e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** 데이터 타입별 insert 처리 - type 값에 따라 대상 테이블 분기 - 잘못된 타입 입력 시 예외 발생 */
|
||||
private void insertByType(String type, DatasetObjRegDto dto) {
|
||||
|
||||
switch (type) {
|
||||
case "train" -> datasetCoreService.insertDatasetObj(dto);
|
||||
case "val" -> datasetCoreService.insertDatasetValObj(dto);
|
||||
case "test" -> datasetCoreService.insertDatasetTestObj(dto);
|
||||
default -> throw new IllegalArgumentException("잘못된 타입: " + type);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* label_json → JsonNode 변환
|
||||
*
|
||||
* <p>- JsonNode면 그대로 사용 - 문자열이면 파싱 수행 - 실패 시 로그 후 예외 발생
|
||||
*/
|
||||
private JsonNode parseJson(Object labelJson) {
|
||||
try {
|
||||
if (labelJson instanceof JsonNode jn) {
|
||||
return jn;
|
||||
}
|
||||
return mapper.readTree(labelJson.toString());
|
||||
} catch (Exception e) {
|
||||
log.error("label_json parse error: {}", labelJson, e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 문자열 → 정수 변환
|
||||
*
|
||||
* <p>- 파싱 실패 시 어떤 필드/파일에서 발생했는지 로그 기록 - 잘못된 데이터는 즉시 예외 처리
|
||||
*/
|
||||
private int parseInt(String value, String field, String fileName) {
|
||||
try {
|
||||
return Integer.parseInt(value);
|
||||
} catch (NumberFormatException e) {
|
||||
log.error("{} 파싱 실패. fileName={}, value={}", field, fileName, value);
|
||||
throw new IllegalArgumentException(field + " 파싱 실패: " + fileName);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,86 +1,679 @@
|
||||
package com.kamco.cd.training.dataset.service;
|
||||
|
||||
import com.kamco.cd.training.dataset.dto.DatasetDto;
|
||||
import com.kamco.cd.training.postgres.core.DatasetCoreService;
|
||||
import java.util.UUID;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.data.domain.Page;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
@Slf4j
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
@Transactional(readOnly = true)
|
||||
public class DatasetService {
|
||||
|
||||
private final DatasetCoreService datasetCoreService;
|
||||
|
||||
/**
|
||||
* 데이터셋 목록 조회
|
||||
*
|
||||
* @param searchReq 검색 조건
|
||||
* @return 데이터셋 목록
|
||||
*/
|
||||
public Page<DatasetDto.Basic> searchDatasets(DatasetDto.SearchReq searchReq) {
|
||||
log.info("데이터셋 목록 조회 - 조건: {}", searchReq);
|
||||
return datasetCoreService.findDatasetList(searchReq);
|
||||
}
|
||||
|
||||
/**
|
||||
* 데이터셋 상세 조회
|
||||
*
|
||||
* @param id 상세 조회할 목록 Id
|
||||
* @return 데이터셋 상세 정보
|
||||
*/
|
||||
public DatasetDto.Basic getDatasetDetail(UUID id) {
|
||||
return datasetCoreService.getOneByUuid(id);
|
||||
}
|
||||
|
||||
/**
|
||||
* 데이터셋 등록
|
||||
*
|
||||
* @param registerReq 등록 요청
|
||||
* @return 등록된 데이터셋 ID
|
||||
*/
|
||||
@Transactional
|
||||
public Long registerDataset(DatasetDto.RegisterReq registerReq) {
|
||||
log.info("데이터셋 등록 - 요청: {}", registerReq);
|
||||
DatasetDto.Basic saved = datasetCoreService.save(registerReq);
|
||||
log.info("데이터셋 등록 완료 - ID: {}", saved.getId());
|
||||
return saved.getId();
|
||||
}
|
||||
|
||||
/**
|
||||
* 데이터셋 수정
|
||||
*
|
||||
* @param updateReq 수정 요청
|
||||
* @return 수정된 데이터셋 ID
|
||||
*/
|
||||
@Transactional
|
||||
public void updateDataset(UUID uuid, DatasetDto.UpdateReq updateReq) {
|
||||
datasetCoreService.update(uuid, updateReq);
|
||||
}
|
||||
|
||||
/**
|
||||
* 데이터셋 삭제 (다건)
|
||||
*
|
||||
* @param uuid 삭제 요청
|
||||
*/
|
||||
@Transactional
|
||||
public void deleteDatasets(UUID uuid) {
|
||||
datasetCoreService.deleteDatasets(uuid);
|
||||
}
|
||||
|
||||
/**
|
||||
* 데이터셋 통계 요약
|
||||
*
|
||||
* @param summaryReq 요약 요청
|
||||
* @return 통계 요약
|
||||
*/
|
||||
public DatasetDto.Summary getDatasetSummary(DatasetDto.SummaryReq summaryReq) {
|
||||
log.info("데이터셋 통계 요약 - 요청: {}", summaryReq);
|
||||
return datasetCoreService.getDatasetSummary(summaryReq);
|
||||
}
|
||||
}
|
||||
package com.kamco.cd.training.dataset.service;
|
||||
|
||||
import com.fasterxml.jackson.databind.JsonNode;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.fasterxml.jackson.databind.node.ArrayNode;
|
||||
import com.fasterxml.jackson.databind.node.ObjectNode;
|
||||
import com.kamco.cd.training.common.enums.LearnDataRegister;
|
||||
import com.kamco.cd.training.common.enums.LearnDataType;
|
||||
import com.kamco.cd.training.common.exception.CustomApiException;
|
||||
import com.kamco.cd.training.common.service.FormatStorage;
|
||||
import com.kamco.cd.training.common.utils.FIleChecker;
|
||||
import com.kamco.cd.training.config.api.ApiResponseDto.ApiResponseCode;
|
||||
import com.kamco.cd.training.config.api.ApiResponseDto.ResponseObj;
|
||||
import com.kamco.cd.training.dataset.dto.DatasetDto;
|
||||
import com.kamco.cd.training.dataset.dto.DatasetDto.AddDeliveriesReq;
|
||||
import com.kamco.cd.training.dataset.dto.DatasetDto.AddReq;
|
||||
import com.kamco.cd.training.dataset.dto.DatasetDto.DatasetMngRegDto;
|
||||
import com.kamco.cd.training.dataset.dto.DatasetObjDto;
|
||||
import com.kamco.cd.training.dataset.dto.DatasetObjDto.DatasetClass;
|
||||
import com.kamco.cd.training.dataset.dto.DatasetObjDto.DatasetObjRegDto;
|
||||
import com.kamco.cd.training.dataset.dto.DatasetObjDto.DatasetStorage;
|
||||
import com.kamco.cd.training.dataset.dto.DatasetObjDto.SearchReq;
|
||||
import com.kamco.cd.training.model.dto.FileDto.FoldersDto;
|
||||
import com.kamco.cd.training.model.dto.FileDto.SrchFoldersDto;
|
||||
import com.kamco.cd.training.postgres.core.DatasetCoreService;
|
||||
import jakarta.validation.Valid;
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.Paths;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.Comparator;
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.UUID;
|
||||
import java.util.stream.Collectors;
|
||||
import java.util.stream.Stream;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.core.io.InputStreamResource;
|
||||
import org.springframework.core.io.Resource;
|
||||
import org.springframework.data.domain.Page;
|
||||
import org.springframework.http.HttpHeaders;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
@Slf4j
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class DatasetService {
|
||||
|
||||
private final DatasetCoreService datasetCoreService;
|
||||
private final DatasetBatchService datasetBatchService;
|
||||
|
||||
private static final List<String> LABEL_DIRS = List.of("label-json", "label", "input1", "input2");
|
||||
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 searchReq 검색 조건
|
||||
* @return 데이터셋 목록
|
||||
*/
|
||||
public Page<DatasetDto.Basic> searchDatasets(DatasetDto.SearchReq searchReq) {
|
||||
log.info("데이터셋 목록 조회 - 조건: {}", searchReq);
|
||||
return datasetCoreService.findDatasetList(searchReq);
|
||||
}
|
||||
|
||||
/**
|
||||
* 데이터셋 상세 조회
|
||||
*
|
||||
* @param id 상세 조회할 목록 Id
|
||||
* @return 데이터셋 상세 정보
|
||||
*/
|
||||
public DatasetDto.Basic getDatasetDetail(UUID id) {
|
||||
return datasetCoreService.getOneByUuid(id);
|
||||
}
|
||||
|
||||
// TODO 미사용시작
|
||||
/**
|
||||
* 데이터셋 등록
|
||||
*
|
||||
* @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();
|
||||
}
|
||||
|
||||
// TODO 미사용 끝
|
||||
/**
|
||||
* 데이터셋 수정
|
||||
*
|
||||
* @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, LearnDataRegister.COMPLETED);
|
||||
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 파일이면 파싱
|
||||
try {
|
||||
data.put("label-json", readJson(path));
|
||||
} catch (Exception e) {
|
||||
log.error("파일 JSON 읽기 실패. skip. file={}", path, e);
|
||||
return; // skip
|
||||
}
|
||||
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());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 폴더 조회
|
||||
*
|
||||
* @param srchDto 폴더 경로
|
||||
* @return 폴더 리스트
|
||||
* @throws IOException
|
||||
*/
|
||||
public FoldersDto getFolderAll(SrchFoldersDto srchDto) throws IOException {
|
||||
|
||||
File dir = new File(srchDto.getDirPath() == null ? "/" : srchDto.getDirPath());
|
||||
|
||||
// 존재 + 디렉토리 체크
|
||||
if (!dir.exists() || !dir.isDirectory()) {
|
||||
throw new CustomApiException("BAD_REQUEST", HttpStatus.BAD_REQUEST, "잘못된 경로입니다.");
|
||||
}
|
||||
|
||||
// 권한 없을때
|
||||
if (!dir.canRead()) {
|
||||
throw new CustomApiException(
|
||||
ApiResponseCode.FORBIDDEN.getId(), HttpStatus.FORBIDDEN, "디렉토리에 접근할 권한이 없습니다.");
|
||||
}
|
||||
|
||||
String canonicalPath = dir.getCanonicalPath();
|
||||
|
||||
File[] files = dir.listFiles();
|
||||
|
||||
if (files == null) {
|
||||
return new FoldersDto(canonicalPath, 0, 0, Collections.emptyList());
|
||||
}
|
||||
|
||||
List<FIleChecker.Folder> folders = new ArrayList<>();
|
||||
|
||||
int folderTotCnt = 0;
|
||||
int folderErrTotCnt = 0;
|
||||
|
||||
for (File f : files) {
|
||||
|
||||
// 숨김 제외
|
||||
if (f.isHidden()) continue;
|
||||
|
||||
if (f.isDirectory()) {
|
||||
|
||||
// 폴더 개수 증가
|
||||
folderTotCnt++;
|
||||
|
||||
// 폴더 유효성 여부 (기본 true, 이후 검증 로직으로 변경 가능)
|
||||
boolean isValid = true;
|
||||
|
||||
// 유효하지 않은 폴더 카운트 증가
|
||||
if (!isValid) folderErrTotCnt++;
|
||||
|
||||
// 현재 폴더 이름 (ex: train, images 등)
|
||||
String folderNm = f.getName();
|
||||
|
||||
// 부모 경로 (ex: /data/datasets)
|
||||
String parentPath = f.getParent();
|
||||
|
||||
// 부모 폴더 이름 (ex: datasets)
|
||||
String parentFolderNm = new File(parentPath).getName();
|
||||
|
||||
// 전체 절대 경로 (ex: /data/datasets/train)
|
||||
String fullPath = f.getAbsolutePath();
|
||||
|
||||
// 폴더 깊이 (경로 기준 depth)
|
||||
// ex: /a/b/c → depth = 3
|
||||
int depth = f.toPath().getNameCount();
|
||||
|
||||
// 하위 폴더 개수
|
||||
long childCnt = FIleChecker.getChildFolderCount(f);
|
||||
|
||||
// 마지막 수정 시간 (문자열 포맷)
|
||||
String lastModified = FIleChecker.getLastModified(f);
|
||||
|
||||
// Folder DTO 생성 및 리스트에 추가
|
||||
folders.add(
|
||||
new FIleChecker.Folder(
|
||||
folderNm, // 폴더명
|
||||
parentFolderNm, // 부모 폴더명
|
||||
parentPath, // 부모 경로
|
||||
fullPath, // 전체 경로
|
||||
depth, // 깊이
|
||||
childCnt, // 하위 폴더 개수
|
||||
lastModified, // 수정일시
|
||||
isValid // 유효성 여부
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
// 폴더 정렬
|
||||
folders.sort(
|
||||
Comparator.comparing(FIleChecker.Folder::getFolderNm, String.CASE_INSENSITIVE_ORDER));
|
||||
|
||||
return new FoldersDto(canonicalPath, folderTotCnt, folderErrTotCnt, folders);
|
||||
}
|
||||
|
||||
/**
|
||||
* 납품 데이터 등록
|
||||
*
|
||||
* @param req 폴더경로, 메모
|
||||
* @return 성공/실패 여부0
|
||||
*/
|
||||
public void insertDeliveriesDataset(AddDeliveriesReq req, Long datasetUid) {
|
||||
|
||||
long startTime = System.currentTimeMillis();
|
||||
|
||||
// 처리
|
||||
processType(req.getFilePath(), datasetUid, "train");
|
||||
processType(req.getFilePath(), datasetUid, "val");
|
||||
processType(req.getFilePath(), datasetUid, "test");
|
||||
|
||||
log.info("========== 전체 완료. 총 소요시간: {} ms ==========", System.currentTimeMillis() - startTime);
|
||||
}
|
||||
|
||||
/**
|
||||
* 납품 데이터 등록 처리
|
||||
*
|
||||
* @param path
|
||||
* @param datasetUid
|
||||
* @param type
|
||||
*/
|
||||
private void processType(String path, Long datasetUid, String type) {
|
||||
long start = System.currentTimeMillis();
|
||||
|
||||
log.info("[납품 데이터 등록 처리][{}] 시작", type.toUpperCase());
|
||||
|
||||
List<Map<String, Object>> list = getUnzipDatasetFiles(path, type);
|
||||
|
||||
int batchSize = 1000;
|
||||
int total = list.size();
|
||||
int processed = 0;
|
||||
|
||||
for (int i = 0; i < total; i += batchSize) {
|
||||
|
||||
List<Map<String, Object>> batch = list.subList(i, Math.min(i + batchSize, total));
|
||||
|
||||
try {
|
||||
log.info("[납품 데이터 등록 처리][{}] batch 시작: {} ~ {}", type, i, i + batch.size());
|
||||
|
||||
datasetBatchService.saveBatch(batch, datasetUid, type);
|
||||
|
||||
processed += batch.size();
|
||||
} catch (Exception e) {
|
||||
log.error("batch 실패 row 데이터: {}", batch);
|
||||
log.error(
|
||||
"[납품 데이터 등록 처리][{}] batch 실패. range: {} ~ {}, datasetUid={}",
|
||||
type,
|
||||
i,
|
||||
i + batch.size(),
|
||||
datasetUid,
|
||||
e);
|
||||
}
|
||||
}
|
||||
|
||||
log.info(
|
||||
"[납품 데이터 등록 처리][{}] 완료. 총 {}건, 소요시간: {} ms",
|
||||
type,
|
||||
total,
|
||||
System.currentTimeMillis() - start);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,41 +1,43 @@
|
||||
package com.kamco.cd.training.dataset.service;
|
||||
|
||||
import com.kamco.cd.training.dataset.dto.MapSheetDto;
|
||||
import com.kamco.cd.training.postgres.core.MapSheetCoreService;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.data.domain.Page;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
@Slf4j
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
@Transactional(readOnly = true)
|
||||
public class MapSheetService {
|
||||
|
||||
private final MapSheetCoreService mapSheetCoreService;
|
||||
|
||||
/**
|
||||
* 도엽 목록 조회
|
||||
*
|
||||
* @param searchReq 검색 조건
|
||||
* @return 도엽 목록
|
||||
*/
|
||||
public Page<MapSheetDto.Basic> searchMapSheets(MapSheetDto.SearchReq searchReq) {
|
||||
log.info("도엽 목록 조회 - 조건: {}", searchReq);
|
||||
return mapSheetCoreService.findMapSheetList(searchReq);
|
||||
}
|
||||
|
||||
/**
|
||||
* 도엽 삭제 (다건)
|
||||
*
|
||||
* @param deleteReq 삭제 요청
|
||||
*/
|
||||
@Transactional
|
||||
public void deleteMapSheets(MapSheetDto.DeleteReq deleteReq) {
|
||||
log.info("도엽 삭제 - 요청: {}", deleteReq);
|
||||
mapSheetCoreService.deleteMapSheets(deleteReq);
|
||||
log.info("도엽 삭제 완료 - 개수: {}", deleteReq.getItemIds().size());
|
||||
}
|
||||
}
|
||||
// TODO 미사용시작
|
||||
package com.kamco.cd.training.dataset.service;
|
||||
|
||||
import com.kamco.cd.training.dataset.dto.MapSheetDto;
|
||||
import com.kamco.cd.training.postgres.core.MapSheetCoreService;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.data.domain.Page;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
@Slf4j
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
@Transactional(readOnly = true)
|
||||
public class MapSheetService {
|
||||
|
||||
private final MapSheetCoreService mapSheetCoreService;
|
||||
|
||||
/**
|
||||
* 도엽 목록 조회
|
||||
*
|
||||
* @param searchReq 검색 조건
|
||||
* @return 도엽 목록
|
||||
*/
|
||||
public Page<MapSheetDto.Basic> searchMapSheets(MapSheetDto.SearchReq searchReq) {
|
||||
log.info("도엽 목록 조회 - 조건: {}", searchReq);
|
||||
return mapSheetCoreService.findMapSheetList(searchReq);
|
||||
}
|
||||
|
||||
/**
|
||||
* 도엽 삭제 (다건)
|
||||
*
|
||||
* @param deleteReq 삭제 요청
|
||||
*/
|
||||
@Transactional
|
||||
public void deleteMapSheets(MapSheetDto.DeleteReq deleteReq) {
|
||||
log.info("도엽 삭제 - 요청: {}", deleteReq);
|
||||
mapSheetCoreService.deleteMapSheets(deleteReq);
|
||||
log.info("도엽 삭제 완료 - 개수: {}", deleteReq.getItemIds().size());
|
||||
}
|
||||
}
|
||||
// TODO 미사용 끝
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -1,229 +1,265 @@
|
||||
package com.kamco.cd.training.log.dto;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonIgnore;
|
||||
import com.kamco.cd.training.common.utils.interfaces.JsonFormatDttm;
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import java.time.ZonedDateTime;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Getter;
|
||||
import lombok.NoArgsConstructor;
|
||||
import lombok.Setter;
|
||||
import org.springframework.data.domain.PageRequest;
|
||||
import org.springframework.data.domain.Pageable;
|
||||
import org.springframework.data.domain.Sort;
|
||||
|
||||
public class AuditLogDto {
|
||||
|
||||
@Schema(name = "AuditLogBasic", description = "감사로그 기본 정보")
|
||||
@Getter
|
||||
public static class Basic {
|
||||
|
||||
@JsonIgnore private final Long id;
|
||||
private final Long userUid;
|
||||
private final EventType eventType;
|
||||
private final EventStatus eventStatus;
|
||||
private final String menuUid;
|
||||
private final String ipAddress;
|
||||
private final String requestUri;
|
||||
private final String requestBody;
|
||||
private final Long errorLogUid;
|
||||
|
||||
@JsonFormatDttm private final ZonedDateTime createdDttm;
|
||||
|
||||
public Basic(
|
||||
Long id,
|
||||
Long userUid,
|
||||
EventType eventType,
|
||||
EventStatus eventStatus,
|
||||
String menuUid,
|
||||
String ipAddress,
|
||||
String requestUri,
|
||||
String requestBody,
|
||||
Long errorLogUid,
|
||||
ZonedDateTime createdDttm) {
|
||||
this.id = id;
|
||||
this.userUid = userUid;
|
||||
this.eventType = eventType;
|
||||
this.eventStatus = eventStatus;
|
||||
this.menuUid = menuUid;
|
||||
this.ipAddress = ipAddress;
|
||||
this.requestUri = requestUri;
|
||||
this.requestBody = requestBody;
|
||||
this.errorLogUid = errorLogUid;
|
||||
this.createdDttm = createdDttm;
|
||||
}
|
||||
}
|
||||
|
||||
@Schema(name = "AuditCommon", description = "목록 공통")
|
||||
@Getter
|
||||
@AllArgsConstructor
|
||||
public static class AuditCommon {
|
||||
private int readCount;
|
||||
private int cudCount;
|
||||
private int printCount;
|
||||
private int downloadCount;
|
||||
private Long totalCount;
|
||||
}
|
||||
|
||||
@Schema(name = "DailyAuditList", description = "일자별 목록")
|
||||
@Getter
|
||||
public static class DailyAuditList extends AuditCommon {
|
||||
private final String baseDate;
|
||||
|
||||
public DailyAuditList(
|
||||
int readCount,
|
||||
int cudCount,
|
||||
int printCount,
|
||||
int downloadCount,
|
||||
Long totalCount,
|
||||
String baseDate) {
|
||||
super(readCount, cudCount, printCount, downloadCount, totalCount);
|
||||
this.baseDate = baseDate;
|
||||
}
|
||||
}
|
||||
|
||||
@Schema(name = "MenuAuditList", description = "메뉴별 목록")
|
||||
@Getter
|
||||
public static class MenuAuditList extends AuditCommon {
|
||||
private final String menuId;
|
||||
private final String menuName;
|
||||
|
||||
public MenuAuditList(
|
||||
String menuId,
|
||||
String menuName,
|
||||
int readCount,
|
||||
int cudCount,
|
||||
int printCount,
|
||||
int downloadCount,
|
||||
Long totalCount) {
|
||||
super(readCount, cudCount, printCount, downloadCount, totalCount);
|
||||
this.menuId = menuId;
|
||||
this.menuName = menuName;
|
||||
}
|
||||
}
|
||||
|
||||
@Schema(name = "UserAuditList", description = "사용자별 목록")
|
||||
@Getter
|
||||
public static class UserAuditList extends AuditCommon {
|
||||
private final Long accountId;
|
||||
private final String loginId;
|
||||
private final String username;
|
||||
|
||||
public UserAuditList(
|
||||
Long accountId,
|
||||
String loginId,
|
||||
String username,
|
||||
int readCount,
|
||||
int cudCount,
|
||||
int printCount,
|
||||
int downloadCount,
|
||||
Long totalCount) {
|
||||
super(readCount, cudCount, printCount, downloadCount, totalCount);
|
||||
this.accountId = accountId;
|
||||
this.loginId = loginId;
|
||||
this.username = username;
|
||||
}
|
||||
}
|
||||
|
||||
@Schema(name = "AuditDetail", description = "감사 로그 상세 공통")
|
||||
@Getter
|
||||
@AllArgsConstructor
|
||||
public static class AuditDetail {
|
||||
private Long logId;
|
||||
private EventType eventType;
|
||||
private LogDetail detail;
|
||||
}
|
||||
|
||||
@Schema(name = "DailyDetail", description = "일자별 로그 상세")
|
||||
@Getter
|
||||
public static class DailyDetail extends AuditDetail {
|
||||
private final String userName;
|
||||
private final String loginId;
|
||||
private final String menuName;
|
||||
|
||||
public DailyDetail(
|
||||
Long logId,
|
||||
String userName,
|
||||
String loginId,
|
||||
String menuName,
|
||||
EventType eventType,
|
||||
LogDetail detail) {
|
||||
super(logId, eventType, detail);
|
||||
this.userName = userName;
|
||||
this.loginId = loginId;
|
||||
this.menuName = menuName;
|
||||
}
|
||||
}
|
||||
|
||||
@Schema(name = "MenuDetail", description = "메뉴별 로그 상세")
|
||||
@Getter
|
||||
public static class MenuDetail extends AuditDetail {
|
||||
private final String logDateTime;
|
||||
private final String userName;
|
||||
private final String loginId;
|
||||
|
||||
public MenuDetail(
|
||||
Long logId,
|
||||
String logDateTime,
|
||||
String userName,
|
||||
String loginId,
|
||||
EventType eventType,
|
||||
LogDetail detail) {
|
||||
super(logId, eventType, detail);
|
||||
this.logDateTime = logDateTime;
|
||||
this.userName = userName;
|
||||
this.loginId = loginId;
|
||||
}
|
||||
}
|
||||
|
||||
@Schema(name = "UserDetail", description = "사용자별 로그 상세")
|
||||
@Getter
|
||||
public static class UserDetail extends AuditDetail {
|
||||
private final String logDateTime;
|
||||
private final String menuNm;
|
||||
|
||||
public UserDetail(
|
||||
Long logId, String logDateTime, String menuNm, EventType eventType, LogDetail detail) {
|
||||
super(logId, eventType, detail);
|
||||
this.logDateTime = logDateTime;
|
||||
this.menuNm = menuNm;
|
||||
}
|
||||
}
|
||||
|
||||
@Getter
|
||||
@Setter
|
||||
@AllArgsConstructor
|
||||
public static class LogDetail {
|
||||
String serviceName;
|
||||
String parentMenuName;
|
||||
String menuName;
|
||||
String menuUrl;
|
||||
String menuDescription;
|
||||
Long sortOrder;
|
||||
boolean used;
|
||||
}
|
||||
|
||||
@Schema(name = "searchReq", description = "일자별 로그 검색 요청")
|
||||
@Getter
|
||||
@Setter
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public static class searchReq {
|
||||
|
||||
// 페이징 파라미터
|
||||
private int page = 0;
|
||||
private int size = 20;
|
||||
private String sort;
|
||||
|
||||
public Pageable toPageable() {
|
||||
if (sort != null && !sort.isEmpty()) {
|
||||
String[] sortParams = sort.split(",");
|
||||
String property = sortParams[0];
|
||||
Sort.Direction direction =
|
||||
sortParams.length > 1 ? Sort.Direction.fromString(sortParams[1]) : Sort.Direction.ASC;
|
||||
return PageRequest.of(page, size, Sort.by(direction, property));
|
||||
}
|
||||
return PageRequest.of(page, size);
|
||||
}
|
||||
}
|
||||
}
|
||||
package com.kamco.cd.training.log.dto;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonIgnore;
|
||||
import com.kamco.cd.training.common.utils.interfaces.JsonFormatDttm;
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import java.time.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 AuditLogDto {
|
||||
|
||||
@Schema(name = "AuditLogBasic", description = "감사로그 기본 정보")
|
||||
@Getter
|
||||
public static class Basic {
|
||||
|
||||
@JsonIgnore private final Long id;
|
||||
private final Long userUid;
|
||||
private final EventType eventType;
|
||||
private final EventStatus eventStatus;
|
||||
private final String menuUid;
|
||||
private final String ipAddress;
|
||||
private final String requestUri;
|
||||
private final String requestBody;
|
||||
private final Long errorLogUid;
|
||||
|
||||
@JsonFormatDttm private final ZonedDateTime createdDttm;
|
||||
|
||||
public Basic(
|
||||
Long id,
|
||||
Long userUid,
|
||||
EventType eventType,
|
||||
EventStatus eventStatus,
|
||||
String menuUid,
|
||||
String ipAddress,
|
||||
String requestUri,
|
||||
String requestBody,
|
||||
Long errorLogUid,
|
||||
ZonedDateTime createdDttm) {
|
||||
this.id = id;
|
||||
this.userUid = userUid;
|
||||
this.eventType = eventType;
|
||||
this.eventStatus = eventStatus;
|
||||
this.menuUid = menuUid;
|
||||
this.ipAddress = ipAddress;
|
||||
this.requestUri = requestUri;
|
||||
this.requestBody = requestBody;
|
||||
this.errorLogUid = errorLogUid;
|
||||
this.createdDttm = createdDttm;
|
||||
}
|
||||
}
|
||||
|
||||
@Schema(name = "AuditCommon", description = "목록 공통")
|
||||
@Getter
|
||||
@AllArgsConstructor
|
||||
public static class AuditCommon {
|
||||
|
||||
private int readCount;
|
||||
private int cudCount;
|
||||
private int printCount;
|
||||
private int downloadCount;
|
||||
private Long totalCount;
|
||||
}
|
||||
|
||||
@Schema(name = "DailyAuditList", description = "일자별 목록")
|
||||
@Getter
|
||||
public static class DailyAuditList extends AuditCommon {
|
||||
|
||||
private final String baseDate;
|
||||
|
||||
public DailyAuditList(
|
||||
int readCount,
|
||||
int cudCount,
|
||||
int printCount,
|
||||
int downloadCount,
|
||||
Long totalCount,
|
||||
String baseDate) {
|
||||
super(readCount, cudCount, printCount, downloadCount, totalCount);
|
||||
this.baseDate = baseDate;
|
||||
}
|
||||
}
|
||||
|
||||
@Schema(name = "MenuAuditList", description = "메뉴별 목록")
|
||||
@Getter
|
||||
public static class MenuAuditList extends AuditCommon {
|
||||
|
||||
private final String menuId;
|
||||
private final String menuName;
|
||||
|
||||
public MenuAuditList(
|
||||
String menuId,
|
||||
String menuName,
|
||||
int readCount,
|
||||
int cudCount,
|
||||
int printCount,
|
||||
int downloadCount,
|
||||
Long totalCount) {
|
||||
super(readCount, cudCount, printCount, downloadCount, totalCount);
|
||||
this.menuId = menuId;
|
||||
this.menuName = menuName;
|
||||
}
|
||||
}
|
||||
|
||||
@Schema(name = "UserAuditList", description = "사용자별 목록")
|
||||
@Getter
|
||||
public static class UserAuditList extends AuditCommon {
|
||||
|
||||
private final Long accountId;
|
||||
private final String loginId;
|
||||
private final String username;
|
||||
|
||||
public UserAuditList(
|
||||
Long accountId,
|
||||
String loginId,
|
||||
String username,
|
||||
int readCount,
|
||||
int cudCount,
|
||||
int printCount,
|
||||
int downloadCount,
|
||||
Long totalCount) {
|
||||
super(readCount, cudCount, printCount, downloadCount, totalCount);
|
||||
this.accountId = accountId;
|
||||
this.loginId = loginId;
|
||||
this.username = username;
|
||||
}
|
||||
}
|
||||
|
||||
@Schema(name = "AuditDetail", description = "감사 로그 상세 공통")
|
||||
@Getter
|
||||
@AllArgsConstructor
|
||||
public static class AuditDetail {
|
||||
|
||||
private Long logId;
|
||||
private EventType eventType;
|
||||
private LogDetail detail;
|
||||
}
|
||||
|
||||
@Schema(name = "DailyDetail", description = "일자별 로그 상세")
|
||||
@Getter
|
||||
public static class DailyDetail extends AuditDetail {
|
||||
|
||||
private final String userName;
|
||||
private final String loginId;
|
||||
private final String menuName;
|
||||
private final String logDateTime;
|
||||
|
||||
public DailyDetail(
|
||||
Long logId,
|
||||
String userName,
|
||||
String loginId,
|
||||
String menuName,
|
||||
EventType eventType,
|
||||
String logDateTime,
|
||||
LogDetail detail) {
|
||||
super(logId, eventType, detail);
|
||||
this.userName = userName;
|
||||
this.loginId = loginId;
|
||||
this.menuName = menuName;
|
||||
this.logDateTime = logDateTime;
|
||||
}
|
||||
}
|
||||
|
||||
@Schema(name = "MenuDetail", description = "메뉴별 로그 상세")
|
||||
@Getter
|
||||
public static class MenuDetail extends AuditDetail {
|
||||
|
||||
private final String logDateTime;
|
||||
private final String userName;
|
||||
private final String loginId;
|
||||
|
||||
public MenuDetail(
|
||||
Long logId,
|
||||
String logDateTime,
|
||||
String userName,
|
||||
String loginId,
|
||||
EventType eventType,
|
||||
LogDetail detail) {
|
||||
super(logId, eventType, detail);
|
||||
this.logDateTime = logDateTime;
|
||||
this.userName = userName;
|
||||
this.loginId = loginId;
|
||||
}
|
||||
}
|
||||
|
||||
@Schema(name = "UserDetail", description = "사용자별 로그 상세")
|
||||
@Getter
|
||||
public static class UserDetail extends AuditDetail {
|
||||
|
||||
private final String logDateTime;
|
||||
private final String menuNm;
|
||||
|
||||
public UserDetail(
|
||||
Long logId, String logDateTime, String menuNm, EventType eventType, LogDetail detail) {
|
||||
super(logId, eventType, detail);
|
||||
this.logDateTime = logDateTime;
|
||||
this.menuNm = menuNm;
|
||||
}
|
||||
}
|
||||
|
||||
@Getter
|
||||
@Setter
|
||||
@AllArgsConstructor
|
||||
public static class LogDetail {
|
||||
|
||||
String serviceName;
|
||||
String parentMenuName;
|
||||
String menuName;
|
||||
String menuUrl;
|
||||
String menuDescription;
|
||||
Long sortOrder;
|
||||
boolean used;
|
||||
}
|
||||
|
||||
@Schema(name = "searchReq", description = "일자별 로그 검색 요청")
|
||||
@Getter
|
||||
@Setter
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public static class searchReq {
|
||||
|
||||
// 페이징 파라미터
|
||||
private int page = 0;
|
||||
private int size = 20;
|
||||
private String sort;
|
||||
|
||||
public Pageable toPageable() {
|
||||
if (sort != null && !sort.isEmpty()) {
|
||||
String[] sortParams = sort.split(",");
|
||||
String property = sortParams[0];
|
||||
Sort.Direction direction =
|
||||
sortParams.length > 1 ? Sort.Direction.fromString(sortParams[1]) : Sort.Direction.ASC;
|
||||
return PageRequest.of(page, size, Sort.by(direction, property));
|
||||
}
|
||||
return PageRequest.of(page, size);
|
||||
}
|
||||
}
|
||||
|
||||
@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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,101 +1,103 @@
|
||||
package com.kamco.cd.training.log.dto;
|
||||
|
||||
import com.kamco.cd.training.common.utils.enums.EnumType;
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import java.time.LocalDate;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Getter;
|
||||
import lombok.NoArgsConstructor;
|
||||
import lombok.Setter;
|
||||
import org.springframework.data.domain.PageRequest;
|
||||
import org.springframework.data.domain.Pageable;
|
||||
import org.springframework.data.domain.Sort;
|
||||
|
||||
public class ErrorLogDto {
|
||||
|
||||
@Schema(name = "ErrorLogBasic", description = "에러로그 기본 정보")
|
||||
@Getter
|
||||
@Setter
|
||||
@AllArgsConstructor
|
||||
public static class Basic {
|
||||
|
||||
private final Long id;
|
||||
private final String serviceName;
|
||||
private final String menuNm;
|
||||
private final String loginId;
|
||||
private final String userName;
|
||||
private final EventType errorType;
|
||||
private final String errorName;
|
||||
private final LogErrorLevel errorLevel;
|
||||
private final String errorCode;
|
||||
private final String errorMessage;
|
||||
private final String errorDetail;
|
||||
private final String createDate; // to_char해서 가져옴
|
||||
}
|
||||
|
||||
@Schema(name = "ErrorSearchReq", description = "에러로그 검색 요청")
|
||||
@Getter
|
||||
@Setter
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public static class ErrorSearchReq {
|
||||
|
||||
LogErrorLevel errorLevel;
|
||||
EventType eventType;
|
||||
LocalDate startDate;
|
||||
LocalDate endDate;
|
||||
|
||||
// 페이징 파라미터
|
||||
private int page = 0;
|
||||
private int size = 20;
|
||||
private String sort;
|
||||
|
||||
public ErrorSearchReq(
|
||||
LogErrorLevel errorLevel,
|
||||
EventType eventType,
|
||||
LocalDate startDate,
|
||||
LocalDate endDate,
|
||||
int page,
|
||||
int size) {
|
||||
this.errorLevel = errorLevel;
|
||||
this.eventType = eventType;
|
||||
this.startDate = startDate;
|
||||
this.endDate = endDate;
|
||||
this.page = page;
|
||||
this.size = size;
|
||||
}
|
||||
|
||||
public Pageable toPageable() {
|
||||
if (sort != null && !sort.isEmpty()) {
|
||||
String[] sortParams = sort.split(",");
|
||||
String property = sortParams[0];
|
||||
Sort.Direction direction =
|
||||
sortParams.length > 1 ? Sort.Direction.fromString(sortParams[1]) : Sort.Direction.ASC;
|
||||
return PageRequest.of(page, size, Sort.by(direction, property));
|
||||
}
|
||||
return PageRequest.of(page, size);
|
||||
}
|
||||
}
|
||||
|
||||
public enum LogErrorLevel implements EnumType {
|
||||
WARNING("Warning"),
|
||||
ERROR("Error"),
|
||||
CRITICAL("Critical");
|
||||
|
||||
private final String desc;
|
||||
|
||||
LogErrorLevel(String desc) {
|
||||
this.desc = desc;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getId() {
|
||||
return name();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getText() {
|
||||
return desc;
|
||||
}
|
||||
}
|
||||
}
|
||||
package com.kamco.cd.training.log.dto;
|
||||
|
||||
import com.kamco.cd.training.common.utils.enums.CodeExpose;
|
||||
import com.kamco.cd.training.common.utils.enums.EnumType;
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import java.time.LocalDate;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Getter;
|
||||
import lombok.NoArgsConstructor;
|
||||
import lombok.Setter;
|
||||
import org.springframework.data.domain.PageRequest;
|
||||
import org.springframework.data.domain.Pageable;
|
||||
import org.springframework.data.domain.Sort;
|
||||
|
||||
public class ErrorLogDto {
|
||||
|
||||
@Schema(name = "ErrorLogBasic", description = "에러로그 기본 정보")
|
||||
@Getter
|
||||
@Setter
|
||||
@AllArgsConstructor
|
||||
public static class Basic {
|
||||
|
||||
private final Long id;
|
||||
private final String serviceName;
|
||||
private final String menuNm;
|
||||
private final String loginId;
|
||||
private final String userName;
|
||||
private final EventType errorType;
|
||||
private final String errorName;
|
||||
private final LogErrorLevel errorLevel;
|
||||
private final String errorCode;
|
||||
private final String errorMessage;
|
||||
private final String errorDetail;
|
||||
private final String createDate; // to_char해서 가져옴
|
||||
}
|
||||
|
||||
@Schema(name = "ErrorSearchReq", description = "에러로그 검색 요청")
|
||||
@Getter
|
||||
@Setter
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public static class ErrorSearchReq {
|
||||
|
||||
LogErrorLevel errorLevel;
|
||||
EventType eventType;
|
||||
LocalDate startDate;
|
||||
LocalDate endDate;
|
||||
|
||||
// 페이징 파라미터
|
||||
private int page = 0;
|
||||
private int size = 20;
|
||||
private String sort;
|
||||
|
||||
public ErrorSearchReq(
|
||||
LogErrorLevel errorLevel,
|
||||
EventType eventType,
|
||||
LocalDate startDate,
|
||||
LocalDate endDate,
|
||||
int page,
|
||||
int size) {
|
||||
this.errorLevel = errorLevel;
|
||||
this.eventType = eventType;
|
||||
this.startDate = startDate;
|
||||
this.endDate = endDate;
|
||||
this.page = page;
|
||||
this.size = size;
|
||||
}
|
||||
|
||||
public Pageable toPageable() {
|
||||
if (sort != null && !sort.isEmpty()) {
|
||||
String[] sortParams = sort.split(",");
|
||||
String property = sortParams[0];
|
||||
Sort.Direction direction =
|
||||
sortParams.length > 1 ? Sort.Direction.fromString(sortParams[1]) : Sort.Direction.ASC;
|
||||
return PageRequest.of(page, size, Sort.by(direction, property));
|
||||
}
|
||||
return PageRequest.of(page, size);
|
||||
}
|
||||
}
|
||||
|
||||
@CodeExpose
|
||||
public enum LogErrorLevel implements EnumType {
|
||||
WARNING("Warning"),
|
||||
ERROR("Error"),
|
||||
CRITICAL("Critical");
|
||||
|
||||
private final String desc;
|
||||
|
||||
LogErrorLevel(String desc) {
|
||||
this.desc = desc;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getId() {
|
||||
return name();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getText() {
|
||||
return desc;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,24 +1,24 @@
|
||||
package com.kamco.cd.training.log.dto;
|
||||
|
||||
import com.kamco.cd.training.common.utils.enums.EnumType;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Getter;
|
||||
|
||||
@Getter
|
||||
@AllArgsConstructor
|
||||
public enum EventStatus implements EnumType {
|
||||
SUCCESS("이벤트 결과 성공"),
|
||||
FAILED("이벤트 결과 실패");
|
||||
|
||||
private final String desc;
|
||||
|
||||
@Override
|
||||
public String getId() {
|
||||
return name();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getText() {
|
||||
return desc;
|
||||
}
|
||||
}
|
||||
package com.kamco.cd.training.log.dto;
|
||||
|
||||
import com.kamco.cd.training.common.utils.enums.EnumType;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Getter;
|
||||
|
||||
@Getter
|
||||
@AllArgsConstructor
|
||||
public enum EventStatus implements EnumType {
|
||||
SUCCESS("이벤트 결과 성공"),
|
||||
FAILED("이벤트 결과 실패");
|
||||
|
||||
private final String desc;
|
||||
|
||||
@Override
|
||||
public String getId() {
|
||||
return name();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getText() {
|
||||
return desc;
|
||||
}
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user