chore: CI/CD 파이프라인 + 배포 설정 + 루트 정리

- .gitea/workflows/deploy.yml: main merge 시 frontend/backend 자동 빌드·배포
- deploy/kcg-backend.service: systemd 서비스 (JDK 17, 2~4GB 힙)
- deploy/nginx-kcg.conf: SSL + SPA 서빙 + API 프록시 + 외부 API CORS 프록시
- .githooks/pre-commit: 모노레포 대응 (frontend tsc+eslint, backend mvn compile)
- .gitignore: frontend/backend/prediction 각각 빌드 산출물 추가
- CLAUDE.md: 모노레포 구조 반영

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
htlee 2026-03-17 13:55:21 +09:00
부모 512020d6bb
커밋 fea77361d8
6개의 변경된 파일371개의 추가작업 그리고 73개의 파일을 삭제

파일 보기

@ -0,0 +1,75 @@
name: Deploy KCG
on:
push:
branches: [main]
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
# ═══ Frontend ═══
- uses: actions/setup-node@v4
with:
node-version: '24'
- name: Configure npm registry
run: |
echo "registry=https://nexus.gc-si.dev/repository/npm-public/" > frontend/.npmrc
echo "//nexus.gc-si.dev/repository/npm-public/:_auth=${{ secrets.NEXUS_NPM_AUTH }}" >> frontend/.npmrc
- name: Build frontend
working-directory: frontend
env:
VITE_GOOGLE_CLIENT_ID: ${{ secrets.GOOGLE_CLIENT_ID }}
run: |
npm ci
npx vite build
- name: Deploy frontend
run: |
mkdir -p /deploy/kcg
rm -rf /deploy/kcg/*
cp -r frontend/dist/* /deploy/kcg/
# ═══ Backend ═══
- name: Install JDK 17 + Maven
run: |
sudo apt-get update -qq
sudo apt-get install -y -qq openjdk-17-jdk-headless maven
- name: Build backend
working-directory: backend
run: mvn -B clean package -DskipTests
- name: Deploy backend
run: |
DEPLOY_DIR=/deploy/kcg-backend
mkdir -p $DEPLOY_DIR/backup
# JAR 백업 (최근 5개 유지)
if [ -f $DEPLOY_DIR/kcg.jar ]; then
cp $DEPLOY_DIR/kcg.jar $DEPLOY_DIR/backup/kcg-$(date +%Y%m%d%H%M%S).jar
ls -t $DEPLOY_DIR/backup/*.jar | tail -n +6 | xargs -r rm
fi
# 서비스 중지 → JAR 교체 → 서비스 시작
sudo systemctl stop kcg-backend || true
cp backend/target/kcg.jar $DEPLOY_DIR/kcg.jar
sudo systemctl start kcg-backend
- name: Health check
run: |
echo "Waiting for backend to start..."
for i in $(seq 1 30); do
HTTP_CODE=$(curl -s -o /dev/null -w "%{http_code}" http://localhost:8080/api/auth/me 2>/dev/null || echo "000")
if [ "$HTTP_CODE" != "000" ]; then
echo "Backend is up (HTTP $HTTP_CODE, attempt $i)"
exit 0
fi
sleep 2
done
echo "Backend health check failed after 60s"
exit 1

파일 보기

@ -1,54 +1,88 @@
#!/bin/bash
#==============================================================================
# pre-commit hook (React TypeScript)
# TypeScript 컴파일 + 린트 검증 — 실패 시 커밋 차단
# pre-commit hook (모노레포: Frontend + Backend)
# Frontend: TypeScript 컴파일 + 린트 검증
# Backend: Maven 컴파일 검증
# 실패 시 커밋 차단
#==============================================================================
echo "pre-commit: TypeScript 타입 체크 중..."
FAILED=0
# npm 확인
if ! command -v npx &>/dev/null; then
echo "경고: npx가 설치되지 않았습니다. 검증을 건너뜁니다."
exit 0
fi
#------------------------------------------------------------------------------
# Frontend 검증
#------------------------------------------------------------------------------
if [ -d "frontend" ]; then
echo "pre-commit: [Frontend] TypeScript 타입 체크 중..."
# node_modules 확인
if [ ! -d "node_modules" ]; then
echo "경고: node_modules가 없습니다. 'npm install' 실행 후 다시 시도하세요."
exit 1
fi
if ! command -v npx &>/dev/null; then
echo "경고: npx가 설치되지 않았습니다. Frontend 검증을 건너뜁니다."
elif [ ! -d "frontend/node_modules" ]; then
echo "경고: frontend/node_modules가 없습니다. 'cd frontend && npm install' 실행 후 다시 시도하세요."
FAILED=1
else
(cd frontend && npx tsc --noEmit --pretty 2>&1)
TSC_RESULT=$?
# TypeScript 타입 체크
npx tsc --noEmit --pretty 2>&1
TSC_RESULT=$?
if [ $TSC_RESULT -ne 0 ]; then
if [ $TSC_RESULT -ne 0 ]; then
echo ""
echo "╔══════════════════════════════════════════════════════════╗"
echo "║ TypeScript 타입 에러! 커밋이 차단되었습니다. ║"
echo "║ 타입 에러를 수정한 후 다시 커밋해주세요. ║"
echo "║ [Frontend] TypeScript 타입 에러! 커밋이 차단되었습니다.║"
echo "╚══════════════════════════════════════════════════════════╝"
echo ""
exit 1
fi
FAILED=1
else
echo "pre-commit: [Frontend] 타입 체크 성공"
fi
echo "pre-commit: 타입 체크 성공"
# ESLint 검증 (설정 파일이 있는 경우만)
if [ -f ".eslintrc.js" ] || [ -f ".eslintrc.json" ] || [ -f ".eslintrc.cjs" ] || [ -f "eslint.config.js" ] || [ -f "eslint.config.mjs" ]; then
echo "pre-commit: ESLint 검증 중..."
npx eslint src/ --ext .ts,.tsx --quiet 2>&1
# ESLint 검증
if [ -f "frontend/eslint.config.js" ] || [ -f "frontend/eslint.config.mjs" ] || [ -f "frontend/.eslintrc.js" ] || [ -f "frontend/.eslintrc.json" ]; then
echo "pre-commit: [Frontend] ESLint 검증 중..."
(cd frontend && npx eslint src/ --ext .ts,.tsx --quiet 2>&1)
LINT_RESULT=$?
if [ $LINT_RESULT -ne 0 ]; then
echo ""
echo "╔══════════════════════════════════════════════════════════╗"
echo "║ ESLint 에러! 커밋이 차단되었습니다. ║"
echo "║ 'npm run lint -- --fix'로 자동 수정을 시도해보세요. ║"
echo "║ [Frontend] ESLint 에러! 커밋이 차단되었습니다. ║"
echo "╚══════════════════════════════════════════════════════════╝"
echo ""
exit 1
FAILED=1
else
echo "pre-commit: [Frontend] ESLint 통과"
fi
fi
fi
echo "pre-commit: ESLint 통과"
fi
#------------------------------------------------------------------------------
# Backend 검증
#------------------------------------------------------------------------------
if [ -d "backend" ] && [ -f "backend/pom.xml" ]; then
echo "pre-commit: [Backend] Maven 컴파일 검증 중..."
if ! command -v mvn &>/dev/null; then
echo "경고: mvn이 설치되지 않았습니다. Backend 검증을 건너뜁니다."
else
(cd backend && mvn compile -q 2>&1)
MVN_RESULT=$?
if [ $MVN_RESULT -ne 0 ]; then
echo ""
echo "╔══════════════════════════════════════════════════════════╗"
echo "║ [Backend] Maven 컴파일 에러! 커밋이 차단되었습니다. ║"
echo "╚══════════════════════════════════════════════════════════╝"
FAILED=1
else
echo "pre-commit: [Backend] 컴파일 성공"
fi
fi
fi
#------------------------------------------------------------------------------
# 결과
#------------------------------------------------------------------------------
if [ $FAILED -ne 0 ]; then
echo ""
echo "pre-commit: 검증 실패! 에러를 수정한 후 다시 커밋해주세요."
exit 1
fi
echo "pre-commit: 모든 검증 통과"

15
.gitignore vendored
파일 보기

@ -68,3 +68,18 @@ dist-ssr
*.sln
*.sw?
*.pdf
# === Frontend ===
frontend/dist/
frontend/node_modules/
# === Backend ===
backend/target/
backend/.env
backend/src/main/resources/application-local.yml
backend/src/main/resources/application-prod.yml
# === Prediction ===
prediction/__pycache__/
prediction/venv/
prediction/.env

102
CLAUDE.md
파일 보기

@ -1,48 +1,90 @@
# 프로젝트 개요
- **타입**: React + TypeScript + Vite
- **타입**: 모노레포 (Frontend + Backend + Prediction)
- **Frontend**: React + TypeScript + Vite
- **Backend**: Spring Boot 3.2.5 + Java 17 + PostgreSQL
- **Prediction**: FastAPI (Python)
- **Node.js**: `.node-version` 참조
- **패키지 매니저**: npm
- **빌드 도구**: Vite
- **Java**: `backend/.sdkmanrc` 참조
- **패키지 매니저**: npm (frontend), Maven (backend), pip (prediction)
## 빌드 및 실행
### Frontend
```bash
# 의존성 설치
cd frontend
npm install
# 개발 서버
npm run dev
```
# 빌드
npm run build
### Backend
```bash
cd backend
# application-local.yml 설정 필요 (application-local.yml.example 참조)
cp src/main/resources/application-local.yml.example src/main/resources/application-local.yml
mvn spring-boot:run -Dspring-boot.run.profiles=local
```
# 테스트
npm run test
### Database
```bash
psql -U postgres -f database/init.sql
psql -U postgres -d kcgdb -f database/migration/001_initial_schema.sql
```
# 린트
npm run lint
### Prediction
```bash
cd prediction
python3 -m venv venv
source venv/bin/activate
pip install -r requirements.txt
uvicorn main:app --reload --port 8000
```
# 포맷팅
npm run format
### 린트/검증
```bash
# Frontend
cd frontend && npm run lint
# Backend
cd backend && mvn compile
```
## 프로젝트 구조
```
src/
├── assets/ # 정적 리소스 (이미지, 폰트 등)
├── components/ # 공통 UI 컴포넌트
│ ├── common/ # 범용 컴포넌트 (Button, Input 등)
│ └── layout/ # 레이아웃 컴포넌트 (Header, Sidebar 등)
├── hooks/ # 커스텀 훅
├── pages/ # 페이지 컴포넌트 (라우팅 단위)
├── services/ # API 호출 로직
├── store/ # 상태 관리 (Context, Zustand 등)
├── types/ # TypeScript 타입 정의
├── utils/ # 유틸리티 함수
├── App.tsx
└── main.tsx
frontend/
├── src/
│ ├── assets/
│ ├── components/
│ ├── hooks/
│ ├── pages/
│ ├── services/
│ ├── store/
│ ├── types/
│ ├── utils/
│ ├── App.tsx
│ └── main.tsx
├── package.json
└── vite.config.ts
backend/
├── src/main/java/gc/mda/kcg/
│ ├── config/ # 설정 (CORS, Security, Properties)
│ ├── auth/ # 인증 (Google OAuth + JWT)
│ ├── domain/ # 도메인 (event, news, osint, aircraft)
│ └── collector/ # 데이터 수집기 (GDELT, GoogleNews, CENTCOM)
├── src/main/resources/
│ ├── application.yml
│ └── application-*.yml.example
└── pom.xml
database/
├── init.sql
└── migration/
prediction/
├── main.py
└── requirements.txt
```
## 팀 규칙
@ -55,6 +97,6 @@ src/
## 의존성 관리
- Nexus 프록시 레포지토리를 통해 npm 패키지 관리 (`.npmrc`)
- 새 의존성 추가: `npm install 패키지명`
- devDependency: `npm install -D 패키지명`
- Frontend: Nexus 프록시 레포지토리를 통해 npm 패키지 관리 (`.npmrc`)
- Backend: Maven Central (pom.xml)
- Prediction: pip (requirements.txt)

파일 보기

@ -0,0 +1,25 @@
[Unit]
Description=KCG Monitoring Backend
After=network.target
[Service]
Type=simple
User=root
Group=root
WorkingDirectory=/deploy/kcg-backend
ExecStart=/usr/lib/jvm/java-17-openjdk-17.0.18.0.8-1.el9.x86_64/bin/java \
-Xms2g -Xmx4g \
-Dspring.profiles.active=prod \
-Dspring.config.additional-location=file:/deploy/kcg-backend/ \
-jar /deploy/kcg-backend/kcg.jar
Restart=on-failure
RestartSec=10
TimeoutStopSec=30
StandardOutput=journal
StandardError=journal
SyslogIdentifier=kcg-backend
[Install]
WantedBy=multi-user.target

107
deploy/nginx-kcg.conf Normal file
파일 보기

@ -0,0 +1,107 @@
server {
listen 443 ssl;
server_name kcg.gc-si.dev;
ssl_certificate /etc/letsencrypt/live/gitea.gc-si.dev/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/gitea.gc-si.dev/privkey.pem;
include /etc/letsencrypt/options-ssl-nginx.conf;
ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem;
# ── Frontend SPA ──
root /deploy/kcg;
# Static cache
location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|woff2?)$ {
expires 1y;
add_header Cache-Control "public, immutable";
}
index index.html;
location / {
try_files $uri $uri/ /index.html;
}
# ── Backend API (direct) ──
location /api/ {
proxy_pass http://127.0.0.1:8080/api/;
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_read_timeout 30s;
}
# ── Backend API (dev prefix 호환) ──
location /api/kcg/ {
rewrite ^/api/kcg/(.*) /api/$1 break;
proxy_pass http://127.0.0.1:8080;
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_read_timeout 30s;
}
# ── signal-batch 프록시 (선박 위치 API) ──
location /signal-batch/ {
proxy_pass https://wing.gc-si.dev/signal-batch/;
proxy_set_header Host wing.gc-si.dev;
proxy_ssl_server_name on;
proxy_read_timeout 30s;
}
# ── 선박 이미지 프록시 ──
location /shipimg/ {
proxy_pass https://wing.gc-si.dev/shipimg/;
proxy_set_header Host wing.gc-si.dev;
proxy_ssl_server_name on;
}
# ── 외부 API 프록시 (프론트엔드 CORS 우회) ──
location /api/airplaneslive/ {
proxy_pass https://api.airplanes.live/;
proxy_set_header Host api.airplanes.live;
proxy_ssl_server_name on;
}
location /api/opensky/ {
proxy_pass https://opensky-network.org/;
proxy_set_header Host opensky-network.org;
proxy_ssl_server_name on;
}
location /api/celestrak/ {
proxy_pass https://celestrak.org/;
proxy_set_header Host celestrak.org;
proxy_ssl_server_name on;
proxy_set_header User-Agent "Mozilla/5.0 (compatible; KCG-Monitor/1.0)";
}
location /api/gdelt/ {
proxy_pass https://api.gdeltproject.org/;
proxy_set_header Host api.gdeltproject.org;
proxy_ssl_server_name on;
}
location /api/rss/ {
proxy_pass https://news.google.com/;
proxy_set_header Host news.google.com;
proxy_ssl_server_name on;
}
location /api/ais/ {
proxy_pass https://aisapi.maritime.spglobal.com/;
proxy_set_header Host aisapi.maritime.spglobal.com;
proxy_ssl_server_name on;
}
# gzip
gzip on;
gzip_types text/plain text/css application/json application/javascript text/xml application/xml;
gzip_min_length 1024;
}
server {
listen 80;
server_name kcg.gc-si.dev;
return 301 https://$host$request_uri;
}