Compare commits
6 Commits
4f763d3c2e
...
feat/train
| Author | SHA1 | Date | |
|---|---|---|---|
| 265813e6f7 | |||
| 8190a6e9c8 | |||
| e9f8bb37fa | |||
| f3c822587f | |||
| 335f0dbb9b | |||
| 69eaba1a83 |
6
.gitignore
vendored
6
.gitignore
vendored
@@ -72,9 +72,3 @@ docker-compose.override.yml
|
|||||||
*.swo
|
*.swo
|
||||||
*~
|
*~
|
||||||
!/CLAUDE.md
|
!/CLAUDE.md
|
||||||
|
|
||||||
### SSL Certificates ###
|
|
||||||
nginx/ssl/
|
|
||||||
*.crt
|
|
||||||
*.key
|
|
||||||
*.pem
|
|
||||||
|
|||||||
20
Dockerfile
20
Dockerfile
@@ -1,20 +0,0 @@
|
|||||||
# 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,56 +0,0 @@
|
|||||||
services:
|
|
||||||
nginx:
|
|
||||||
image: nginx:alpine
|
|
||||||
container_name: kamco-cd-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
|
|
||||||
depends_on:
|
|
||||||
kamco-changedetection-api:
|
|
||||||
condition: service_healthy
|
|
||||||
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
|
|
||||||
|
|
||||||
kamco-changedetection-api:
|
|
||||||
build:
|
|
||||||
context: .
|
|
||||||
dockerfile: Dockerfile
|
|
||||||
image: kamco-cd-training-api:${IMAGE_TAG:-latest}
|
|
||||||
container_name: kamco-cd-training-api
|
|
||||||
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
|
|
||||||
|
|
||||||
volumes:
|
|
||||||
nginx-logs:
|
|
||||||
driver: local
|
|
||||||
@@ -1,397 +0,0 @@
|
|||||||
# 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
|
|
||||||
```
|
|
||||||
|
|
||||||
## 참고 자료
|
|
||||||
|
|
||||||
- [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/)
|
|
||||||
@@ -1,93 +0,0 @@
|
|||||||
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-changedetection-api:8080;
|
|
||||||
}
|
|
||||||
|
|
||||||
# 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 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;
|
|
||||||
|
|
||||||
# 타임아웃 설정 (대용량 파일 업로드 지원)
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -3,7 +3,6 @@ package com.kamco.cd.training.common.utils;
|
|||||||
import static java.lang.String.CASE_INSENSITIVE_ORDER;
|
import static java.lang.String.CASE_INSENSITIVE_ORDER;
|
||||||
|
|
||||||
import com.jcraft.jsch.ChannelExec;
|
import com.jcraft.jsch.ChannelExec;
|
||||||
import com.jcraft.jsch.ChannelSftp;
|
|
||||||
import com.jcraft.jsch.JSch;
|
import com.jcraft.jsch.JSch;
|
||||||
import com.jcraft.jsch.Session;
|
import com.jcraft.jsch.Session;
|
||||||
import com.kamco.cd.training.common.exception.CustomApiException;
|
import com.kamco.cd.training.common.exception.CustomApiException;
|
||||||
@@ -720,18 +719,26 @@ public class FIleChecker {
|
|||||||
public static void unzip(String fileName, String destDirectory) throws IOException {
|
public static void unzip(String fileName, String destDirectory) throws IOException {
|
||||||
String zipFilePath = destDirectory + File.separator + fileName;
|
String zipFilePath = destDirectory + File.separator + fileName;
|
||||||
|
|
||||||
|
log.info("fileName : {}", fileName);
|
||||||
|
log.info("destDirectory : {}", destDirectory);
|
||||||
|
log.info("zipFilePath : {}", zipFilePath);
|
||||||
// zip 이름으로 폴더 생성 (확장자 제거)
|
// zip 이름으로 폴더 생성 (확장자 제거)
|
||||||
String folderName =
|
String folderName =
|
||||||
fileName.endsWith(".zip") ? fileName.substring(0, fileName.length() - 4) : fileName;
|
fileName.endsWith(".zip") ? fileName.substring(0, fileName.length() - 4) : fileName;
|
||||||
|
log.info("folderName : {}", folderName);
|
||||||
|
|
||||||
File destDir = new File(destDirectory, folderName);
|
File destDir = new File(destDirectory, folderName);
|
||||||
|
log.info("destDir : {}", destDir);
|
||||||
|
|
||||||
// 동일 폴더가 이미 있으면 삭제
|
// 동일 폴더가 이미 있으면 삭제
|
||||||
|
log.info("111 destDir.exists() : {}", destDir.exists());
|
||||||
if (destDir.exists()) {
|
if (destDir.exists()) {
|
||||||
deleteDirectoryRecursively(destDir.toPath());
|
deleteDirectoryRecursively(destDir.toPath());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
log.info("222 destDir.exists() : {}", destDir.exists());
|
||||||
if (!destDir.exists()) {
|
if (!destDir.exists()) {
|
||||||
|
log.info("mkdirs : {}", destDir.exists());
|
||||||
destDir.mkdirs();
|
destDir.mkdirs();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -398,9 +398,26 @@ public class DatasetService {
|
|||||||
return datasetCoreService.getFilePathByUUIDPathType(uuid, pathType);
|
return datasetCoreService.getFilePathByUUIDPathType(uuid, pathType);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private String readRemoteFileAsString(String remoteFilePath) {
|
||||||
|
|
||||||
|
String command = "cat " + escape(remoteFilePath);
|
||||||
|
|
||||||
|
List<String> lines = FIleChecker.execCommandAndReadLines(command);
|
||||||
|
|
||||||
|
return String.join("\n", lines);
|
||||||
|
}
|
||||||
|
|
||||||
|
private JsonNode parseJson(String json) {
|
||||||
|
try {
|
||||||
|
ObjectMapper mapper = new ObjectMapper();
|
||||||
|
return mapper.readTree(json);
|
||||||
|
} catch (IOException e) {
|
||||||
|
throw new RuntimeException("JSON 파싱 실패", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private String escape(String path) {
|
private String escape(String path) {
|
||||||
|
// 쉘 커맨드에서 안전하게 사용할 수 있도록 문자열을 작은따옴표로 감싸면서, 내부의 작은따옴표를 이스케이프 처리
|
||||||
return "'" + path.replace("'", "'\"'\"'") + "'";
|
return "'" + path.replace("'", "'\"'\"'") + "'";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -20,71 +20,187 @@ public class TmpDatasetService {
|
|||||||
@Value("${train.docker.basePath}")
|
@Value("${train.docker.basePath}")
|
||||||
private String trainBaseDir;
|
private String trainBaseDir;
|
||||||
|
|
||||||
private final DataSetCountersService dataSetCountersService;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 다른 데이터셋 파일과 이름이 겹치면 그 파일은 skip함
|
* train, val, test 폴더별로 link
|
||||||
*
|
*
|
||||||
* @param uid
|
* @param uid 임시폴더 uuid
|
||||||
* @param type
|
* @param type train, val, test
|
||||||
* @param links
|
* @param links tif pull path
|
||||||
* @return
|
* @return
|
||||||
* @throws IOException
|
* @throws IOException
|
||||||
*/
|
*/
|
||||||
public String buildTmpDatasetSymlink(String uid, String type, List<ModelTrainLinkDto> links)
|
public void buildTmpDatasetHardlink(String uid, String type, List<ModelTrainLinkDto> links)
|
||||||
throws IOException {
|
throws IOException {
|
||||||
|
|
||||||
if (uid == null || uid.isBlank()) throw new IOException("uid is empty");
|
if (links == null || links.isEmpty()) {
|
||||||
if (type == null || type.isBlank()) throw new IOException("type is empty");
|
throw new IOException("links is empty");
|
||||||
if (links == null || links.isEmpty()) throw new IOException("links is empty");
|
}
|
||||||
|
|
||||||
log.info("========== buildTmpDatasetHardlink MERGE START ==========");
|
Path tmp = Path.of(trainBaseDir, "tmp", uid);
|
||||||
log.info("uid={}, type={}, links.size={}", uid, type, links.size());
|
|
||||||
|
long hardlinksMade = 0;
|
||||||
|
|
||||||
|
for (ModelTrainLinkDto dto : links) {
|
||||||
|
|
||||||
|
if (type == null) {
|
||||||
|
log.warn("SKIP - trainType null: {}", dto);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// type별 디렉토리 생성
|
||||||
|
Files.createDirectories(tmp.resolve(type).resolve("input1"));
|
||||||
|
Files.createDirectories(tmp.resolve(type).resolve("input2"));
|
||||||
|
Files.createDirectories(tmp.resolve(type).resolve("label"));
|
||||||
|
Files.createDirectories(tmp.resolve(type).resolve("label-json"));
|
||||||
|
|
||||||
|
// comparePath → input1
|
||||||
|
hardlinksMade += link(tmp, type, "input1", dto.getComparePath());
|
||||||
|
|
||||||
|
// targetPath → input2
|
||||||
|
hardlinksMade += link(tmp, type, "input2", dto.getTargetPath());
|
||||||
|
|
||||||
|
// labelPath → label
|
||||||
|
hardlinksMade += link(tmp, type, "label", dto.getLabelPath());
|
||||||
|
|
||||||
|
// geoJsonPath -> label-json
|
||||||
|
hardlinksMade += link(tmp, type, "label-json", dto.getGeoJsonPath());
|
||||||
|
}
|
||||||
|
|
||||||
|
if (hardlinksMade == 0) {
|
||||||
|
throw new IOException("No hardlinks created.");
|
||||||
|
}
|
||||||
|
|
||||||
|
log.info("tmp dataset created: {}, hardlinksMade={}", tmp, hardlinksMade);
|
||||||
|
}
|
||||||
|
|
||||||
|
private long link(Path tmp, String type, String part, String fullPath) throws IOException {
|
||||||
|
|
||||||
|
if (fullPath == null || fullPath.isBlank()) return 0;
|
||||||
|
|
||||||
|
Path src = Path.of(fullPath);
|
||||||
|
|
||||||
|
if (!Files.isRegularFile(src)) {
|
||||||
|
log.warn("SKIP (not file): {}", src);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
String fileName = src.getFileName().toString();
|
||||||
|
Path dst = tmp.resolve(type).resolve(part).resolve(fileName);
|
||||||
|
|
||||||
|
// 충돌 시 덮어쓰기
|
||||||
|
if (Files.exists(dst)) {
|
||||||
|
Files.delete(dst);
|
||||||
|
}
|
||||||
|
|
||||||
|
Files.createLink(dst, src);
|
||||||
|
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
private String safe(String s) {
|
||||||
|
return (s == null || s.isBlank()) ? null : s.trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* request 전체 폴더 link
|
||||||
|
*
|
||||||
|
* @param uid
|
||||||
|
* @param datasetUids
|
||||||
|
* @return
|
||||||
|
* @throws IOException
|
||||||
|
*/
|
||||||
|
public String buildTmpDatasetSymlink(String uid, List<String> datasetUids) throws IOException {
|
||||||
|
|
||||||
|
log.info("========== buildTmpDatasetHardlink START ==========");
|
||||||
|
log.info("uid={}", uid);
|
||||||
|
log.info("datasetUids={}", datasetUids);
|
||||||
|
log.info("requestDir(raw)={}", requestDir);
|
||||||
|
|
||||||
Path BASE = toPath(requestDir);
|
Path BASE = toPath(requestDir);
|
||||||
Path tmp = Path.of(trainBaseDir, "tmp", uid);
|
Path tmp = Path.of(trainBaseDir, "tmp", uid);
|
||||||
|
|
||||||
long hardlinksMade = 0;
|
log.info("BASE={}", BASE);
|
||||||
long skippedCollision = 0;
|
log.info("BASE exists? {}", Files.isDirectory(BASE));
|
||||||
long noDir = 0;
|
log.info("tmp={}", tmp);
|
||||||
|
|
||||||
// tmp/<type>/<part> 준비
|
long noDir = 0, scannedDirs = 0, regularFiles = 0, hardlinksMade = 0;
|
||||||
for (String part : List.of("input1", "input2", "label", "label-json")) {
|
|
||||||
Files.createDirectories(tmp.resolve(type).resolve(part));
|
// tmp 디렉토리 준비
|
||||||
|
for (String type : List.of("train", "val", "test")) {
|
||||||
|
for (String part : List.of("input1", "input2", "label", "label-json")) {
|
||||||
|
Path dir = tmp.resolve(type).resolve(part);
|
||||||
|
Files.createDirectories(dir);
|
||||||
|
log.info("createDirectories: {}", dir);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
for (ModelTrainLinkDto dto : links) {
|
// 하드링크는 "같은 파일시스템"에서만 가능하므로 BASE/tmp가 같은 FS인지 미리 확인(권장)
|
||||||
String datasetUid = safe(dto.getDatasetUid());
|
try {
|
||||||
if (datasetUid == null) {
|
var baseStore = Files.getFileStore(BASE);
|
||||||
log.warn("SKIP dto (datasetUid null): {}", dto);
|
var tmpStore = Files.getFileStore(tmp.getParent()); // BASE/tmp
|
||||||
continue;
|
if (!baseStore.name().equals(tmpStore.name()) || !baseStore.type().equals(tmpStore.type())) {
|
||||||
|
throw new IOException(
|
||||||
|
"Hardlink requires same filesystem. baseStore="
|
||||||
|
+ baseStore.name()
|
||||||
|
+ "("
|
||||||
|
+ baseStore.type()
|
||||||
|
+ "), tmpStore="
|
||||||
|
+ tmpStore.name()
|
||||||
|
+ "("
|
||||||
|
+ tmpStore.type()
|
||||||
|
+ ")");
|
||||||
}
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
// FileStore 비교가 환경마다 애매할 수 있어서, 여기서는 경고만 주고 실제 createLink에서 최종 판단하게 둘 수도 있음.
|
||||||
|
log.warn("FileStore check skipped/failed (will rely on createLink): {}", e.toString());
|
||||||
|
}
|
||||||
|
|
||||||
Path srcRoot = BASE.resolve(datasetUid);
|
for (String id : datasetUids) {
|
||||||
|
Path srcRoot = BASE.resolve(id);
|
||||||
|
log.info("---- dataset id={} srcRoot={} exists? {}", id, srcRoot, Files.isDirectory(srcRoot));
|
||||||
|
|
||||||
for (String part : List.of("input1", "input2", "label", "label-json")) {
|
for (String type : List.of("train", "val", "test")) {
|
||||||
|
for (String part : List.of("input1", "input2", "label", "label-json")) {
|
||||||
|
|
||||||
Path srcDir = srcRoot.resolve(type).resolve(part);
|
Path srcDir = srcRoot.resolve(type).resolve(part);
|
||||||
if (!Files.isDirectory(srcDir)) {
|
if (!Files.isDirectory(srcDir)) {
|
||||||
noDir++;
|
log.warn("SKIP (not directory): {}", srcDir);
|
||||||
continue;
|
noDir++;
|
||||||
}
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
// 하위폴더까지 전부
|
scannedDirs++;
|
||||||
try (var walk = Files.walk(srcDir)) {
|
log.info("SCAN dir={}", srcDir);
|
||||||
for (Path f : walk.filter(Files::isRegularFile).toList()) {
|
|
||||||
|
|
||||||
String fileName = f.getFileName().toString();
|
try (DirectoryStream<Path> stream = Files.newDirectoryStream(srcDir)) {
|
||||||
Path dst = tmp.resolve(type).resolve(part).resolve(fileName);
|
for (Path f : stream) {
|
||||||
|
if (!Files.isRegularFile(f)) {
|
||||||
|
log.debug("skip non-regular file: {}", f);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
// 이름 유지 + 충돌은 skip
|
regularFiles++;
|
||||||
if (Files.exists(dst)) {
|
|
||||||
skippedCollision++;
|
String dstName = id + "__" + f.getFileName();
|
||||||
continue;
|
Path dst = tmp.resolve(type).resolve(part).resolve(dstName);
|
||||||
|
|
||||||
|
// dst가 남아있으면 삭제(심볼릭링크든 파일이든)
|
||||||
|
if (Files.exists(dst) || Files.isSymbolicLink(dst)) {
|
||||||
|
Files.delete(dst);
|
||||||
|
log.debug("deleted existing: {}", dst);
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 하드링크 생성 (dst가 새 파일로 생기지만 inode는 f와 동일)
|
||||||
|
Files.createLink(dst, f);
|
||||||
|
hardlinksMade++;
|
||||||
|
log.debug("created hardlink: {} => {}", dst, f);
|
||||||
|
} catch (IOException e) {
|
||||||
|
// 여기서 바로 실패시키면 “tmp는 만들었는데 내용은 0개” 같은 상태를 방지할 수 있음
|
||||||
|
log.error("FAILED create hardlink: {} => {}", dst, f, e);
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Files.createLink(dst, f);
|
|
||||||
hardlinksMade++;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -92,35 +208,29 @@ public class TmpDatasetService {
|
|||||||
|
|
||||||
if (hardlinksMade == 0) {
|
if (hardlinksMade == 0) {
|
||||||
throw new IOException(
|
throw new IOException(
|
||||||
"No hardlinks created. noDir=" + noDir + ", skippedCollision=" + skippedCollision);
|
"No hardlinks created. regularFiles="
|
||||||
|
+ regularFiles
|
||||||
|
+ ", scannedDirs="
|
||||||
|
+ scannedDirs
|
||||||
|
+ ", noDir="
|
||||||
|
+ noDir);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
log.info("tmp dataset created: {}", tmp);
|
||||||
log.info(
|
log.info(
|
||||||
"tmp dataset merged: {} (type={}), hardlinksMade={}, skippedCollision={}, noDir={}",
|
"summary: scannedDirs={}, noDir={}, regularFiles={}, hardlinksMade={}",
|
||||||
tmp,
|
scannedDirs,
|
||||||
type,
|
noDir,
|
||||||
hardlinksMade,
|
regularFiles,
|
||||||
skippedCollision,
|
hardlinksMade);
|
||||||
noDir);
|
|
||||||
|
|
||||||
return uid;
|
return uid;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static Path toPath(String p) {
|
private static Path toPath(String p) {
|
||||||
if (p == null || p.isBlank()) {
|
if (p.startsWith("~/")) {
|
||||||
throw new IllegalArgumentException("path is null or blank");
|
return Paths.get(System.getProperty("user.home")).resolve(p.substring(2)).normalize();
|
||||||
}
|
}
|
||||||
String trimmed = p.trim();
|
return Paths.get(p).toAbsolutePath().normalize();
|
||||||
if (trimmed.startsWith("~/")) {
|
|
||||||
return Paths.get(System.getProperty("user.home"))
|
|
||||||
.resolve(trimmed.substring(2))
|
|
||||||
.toAbsolutePath()
|
|
||||||
.normalize();
|
|
||||||
}
|
|
||||||
return Paths.get(trimmed).toAbsolutePath().normalize();
|
|
||||||
}
|
|
||||||
|
|
||||||
private static String safe(String s) {
|
|
||||||
return (s == null || s.isBlank()) ? null : s.trim();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -266,6 +266,8 @@ public class TrainJobService {
|
|||||||
List<String> uids = modelTrainMngCoreService.findDatasetUid(datasetIds);
|
List<String> uids = modelTrainMngCoreService.findDatasetUid(datasetIds);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
// 데이터셋 심볼링크 생성
|
||||||
|
// String pathUid = tmpDatasetService.buildTmpDatasetSymlink(raw, uids);
|
||||||
// train path
|
// train path
|
||||||
List<ModelTrainLinkDto> trainList = modelTrainMngCoreService.findDatasetTrainPath(modelId);
|
List<ModelTrainLinkDto> trainList = modelTrainMngCoreService.findDatasetTrainPath(modelId);
|
||||||
// validation path
|
// validation path
|
||||||
@@ -274,13 +276,12 @@ public class TrainJobService {
|
|||||||
List<ModelTrainLinkDto> testList = modelTrainMngCoreService.findDatasetTestPath(modelId);
|
List<ModelTrainLinkDto> testList = modelTrainMngCoreService.findDatasetTestPath(modelId);
|
||||||
|
|
||||||
// train 데이터셋 심볼링크 생성
|
// train 데이터셋 심볼링크 생성
|
||||||
tmpDatasetService.buildTmpDatasetSymlink(raw, "train", trainList);
|
tmpDatasetService.buildTmpDatasetHardlink(raw, "train", trainList);
|
||||||
// val 데이터셋 심볼링크 생성
|
// val 데이터셋 심볼링크 생성
|
||||||
tmpDatasetService.buildTmpDatasetSymlink(raw, "val", valList);
|
tmpDatasetService.buildTmpDatasetHardlink(raw, "val", valList);
|
||||||
// test 데이터셋 심볼링크 생성
|
// test 데이터셋 심볼링크 생성
|
||||||
tmpDatasetService.buildTmpDatasetSymlink(raw, "test", testList);
|
tmpDatasetService.buildTmpDatasetHardlink(raw, "test", testList);
|
||||||
// 카운트 로그
|
|
||||||
dataSetCounters.getCount(modelId);
|
|
||||||
ModelTrainMngDto.UpdateReq updateReq = new ModelTrainMngDto.UpdateReq();
|
ModelTrainMngDto.UpdateReq updateReq = new ModelTrainMngDto.UpdateReq();
|
||||||
updateReq.setRequestPath(raw);
|
updateReq.setRequestPath(raw);
|
||||||
|
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ spring:
|
|||||||
format_sql: true # ⚠️ 선택 - SQL 포맷팅 (가독성)
|
format_sql: true # ⚠️ 선택 - SQL 포맷팅 (가독성)
|
||||||
|
|
||||||
datasource:
|
datasource:
|
||||||
url: jdbc:postgresql://kamco-cd-train-db:5432/kamco_training_db
|
url: jdbc:postgresql://127.0.01:15432/kamco_training_db
|
||||||
# url: jdbc:postgresql://localhost:15432/kamco_training_db
|
# url: jdbc:postgresql://localhost:15432/kamco_training_db
|
||||||
username: kamco_training_user
|
username: kamco_training_user
|
||||||
password: kamco_training_user_2025_!@#
|
password: kamco_training_user_2025_!@#
|
||||||
|
|||||||
Reference in New Issue
Block a user