From fea77361d86a35e5a0bc23502fe9274e62cc54df Mon Sep 17 00:00:00 2001 From: htlee Date: Tue, 17 Mar 2026 13:55:21 +0900 Subject: [PATCH] =?UTF-8?q?chore:=20CI/CD=20=ED=8C=8C=EC=9D=B4=ED=94=84?= =?UTF-8?q?=EB=9D=BC=EC=9D=B8=20+=20=EB=B0=B0=ED=8F=AC=20=EC=84=A4?= =?UTF-8?q?=EC=A0=95=20+=20=EB=A3=A8=ED=8A=B8=20=EC=A0=95=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - .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) --- .gitea/workflows/deploy.yml | 75 ++++++++++++++++++++++ .githooks/pre-commit | 120 +++++++++++++++++++++++------------- .gitignore | 15 +++++ CLAUDE.md | 102 +++++++++++++++++++++--------- deploy/kcg-backend.service | 25 ++++++++ deploy/nginx-kcg.conf | 107 ++++++++++++++++++++++++++++++++ 6 files changed, 371 insertions(+), 73 deletions(-) create mode 100644 .gitea/workflows/deploy.yml create mode 100644 deploy/kcg-backend.service create mode 100644 deploy/nginx-kcg.conf diff --git a/.gitea/workflows/deploy.yml b/.gitea/workflows/deploy.yml new file mode 100644 index 0000000..6e0c4af --- /dev/null +++ b/.gitea/workflows/deploy.yml @@ -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 diff --git a/.githooks/pre-commit b/.githooks/pre-commit index 7a28940..b713ed0 100755 --- a/.githooks/pre-commit +++ b/.githooks/pre-commit @@ -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 + echo "" + echo "╔══════════════════════════════════════════════════════════╗" + echo "║ [Frontend] TypeScript 타입 에러! 커밋이 차단되었습니다.║" + echo "╚══════════════════════════════════════════════════════════╝" + FAILED=1 + else + echo "pre-commit: [Frontend] 타입 체크 성공" + fi -if [ $TSC_RESULT -ne 0 ]; then - echo "" - echo "╔══════════════════════════════════════════════════════════╗" - echo "║ TypeScript 타입 에러! 커밋이 차단되었습니다. ║" - echo "║ 타입 에러를 수정한 후 다시 커밋해주세요. ║" - echo "╚══════════════════════════════════════════════════════════╝" - echo "" - exit 1 -fi + # 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=$? -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 - LINT_RESULT=$? - - if [ $LINT_RESULT -ne 0 ]; then - echo "" - echo "╔══════════════════════════════════════════════════════════╗" - echo "║ ESLint 에러! 커밋이 차단되었습니다. ║" - echo "║ 'npm run lint -- --fix'로 자동 수정을 시도해보세요. ║" - echo "╚══════════════════════════════════════════════════════════╝" - echo "" - exit 1 + if [ $LINT_RESULT -ne 0 ]; then + echo "" + echo "╔══════════════════════════════════════════════════════════╗" + echo "║ [Frontend] ESLint 에러! 커밋이 차단되었습니다. ║" + echo "╚══════════════════════════════════════════════════════════╝" + 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: 모든 검증 통과" diff --git a/.gitignore b/.gitignore index 9e1fa2e..83d80c2 100644 --- a/.gitignore +++ b/.gitignore @@ -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 diff --git a/CLAUDE.md b/CLAUDE.md index f10228b..7958058 100644 --- a/CLAUDE.md +++ b/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) diff --git a/deploy/kcg-backend.service b/deploy/kcg-backend.service new file mode 100644 index 0000000..1932377 --- /dev/null +++ b/deploy/kcg-backend.service @@ -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 diff --git a/deploy/nginx-kcg.conf b/deploy/nginx-kcg.conf new file mode 100644 index 0000000..b86d694 --- /dev/null +++ b/deploy/nginx-kcg.conf @@ -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; +}