# WING-OPS 설치 매뉴얼

## 목차

1. [시스템 요구사항](#1-시스템-요구사항)
2. [프로젝트 구조](#2-프로젝트-구조)
3. [온라인 설치](#3-온라인-설치)
4. [오프라인 설치](#4-오프라인-설치)
5. [DB 초기화 및 마이그레이션](#5-db-초기화-및-마이그레이션)
6. [개발 서버 실행](#6-개발-서버-실행)
7. [운영 서버 배포](#7-운영-서버-배포)
8. [CI/CD 자동 배포](#8-cicd-자동-배포)
9. [접속 정보 요약](#9-접속-정보-요약)
10. [트러블슈팅](#10-트러블슈팅)

---

## 1. 시스템 요구사항

### 필수 소프트웨어

| 소프트웨어 | 최소 버전 | 권장 버전 | 용도 |
|-----------|----------|----------|------|
| Node.js | v20 | v20 LTS | Frontend/Backend 실행 |
| npm | v10 | v10+ | 패키지 관리 (Node.js 포함) |
| PostgreSQL | v15 | v16 | 운영 DB + 인증 DB |
| PostGIS | v3.3 | v3.4 | 공간 데이터 처리 |
| Git | v2.30 | v2.40+ | 소스 코드 관리 |

### 하드웨어 권장 사양

| 항목 | 개발 환경 | 운영 환경 |
|------|----------|----------|
| CPU | 2 Core | 4 Core |
| RAM | 4 GB | 8 GB |
| Disk | 10 GB | 50 GB |
| OS | macOS / Linux / Windows | Rocky Linux 9 / CentOS 9 |

### Node.js 설치 (fnm 권장)

fnm(Fast Node Manager)으로 Node.js 버전을 관리한다. 프로젝트 루트의 `.node-version` 파일에 `20`이 지정되어 있다.

**macOS / Linux:**

```bash
# fnm 설치
curl -fsSL https://fnm.vercel.app/install | bash

# 셸 재시작 후
fnm install 20
fnm use 20

# 확인
node -v    # v20.x.x
npm -v     # v10.x.x
```

**수동 설치 (fnm 없이):**

https://nodejs.org 에서 Node.js 20 LTS를 다운로드하여 설치한다.

---

## 2. 프로젝트 구조

```
wing/
├── frontend/                 React 19 + Vite 7 + TypeScript 5.9 + Tailwind CSS 3
│   ├── src/
│   │   ├── App.tsx           메인 (탭 라우팅, 감사 로그 자동 기록)
│   │   ├── common/           공통 모듈 (@common/ alias)
│   │   │   ├── components/   auth/, layer/, layout/, map/, ui/
│   │   │   ├── hooks/        useLayers, useSubMenu, useAuth
│   │   │   ├── services/     api.ts (Axios), authApi.ts, layerService.ts
│   │   │   ├── store/        authStore, menuStore (Zustand)
│   │   │   ├── types/        backtrack, boomLine, hns, navigation
│   │   │   └── utils/        coordinates, geo, sanitize
│   │   └── tabs/             탭 단위 패키지 (11개)
│   │       ├── prediction/   확산 예측
│   │       ├── hns/          HNS 분석
│   │       ├── rescue/       구조 시나리오
│   │       ├── aerial/       항공 방제
│   │       ├── weather/      해양 기상
│   │       ├── incidents/    사건/사고 관리
│   │       ├── board/        게시판
│   │       ├── reports/      보고서
│   │       ├── assets/       자산 관리
│   │       ├── scat/         Pre-SCAT 조사
│   │       └── admin/        관리자
│   ├── package.json
│   └── vite.config.ts
├── backend/                  Express 4 + TypeScript + PostgreSQL
│   ├── src/
│   │   ├── server.ts         진입점 (보안 미들웨어 + 라우터 등록)
│   │   ├── auth/             인증 (JWT, Google OAuth)
│   │   ├── users/            사용자 관리
│   │   ├── roles/            역할/권한 (RBAC 2차원 권한)
│   │   ├── db/               DB Pool (wingDb, authDb), seed
│   │   ├── middleware/       보안 (입력 살균, rate-limit)
│   │   └── {도메인}/         도메인별 모듈 (Router + Service)
│   ├── package.json
│   └── tsconfig.json
├── database/                 SQL 스크립트
│   ├── init.sql              wing DB 초기 스키마 (PostGIS)
│   ├── auth_init.sql         wing_auth DB 초기 스키마
│   └── migration/            마이그레이션 (001 ~ 016)
├── docs/                     개발 문서
├── .githooks/                Git Hooks (pre-commit, commit-msg)
├── .gitea/workflows/         CI/CD (Gitea Actions)
├── .node-version             Node.js 20
└── .npmrc                    npm 레지스트리 (Nexus 프록시)
```

---

## 3. 온라인 설치

인터넷이 가능한 환경에서의 설치 절차.

### 3-1. 소스 코드 클론

```bash
git clone https://gitea.gc-si.dev/gc/wing-ops.git wing
cd wing
```

### 3-2. Git Hooks 설정

```bash
# pre-commit (TypeScript + ESLint 검증), commit-msg (Conventional Commits 검증)
git config core.hooksPath .githooks
chmod +x .githooks/pre-commit .githooks/commit-msg
```

### 3-3. 의존성 설치

npm 레지스트리는 프로젝트 루트의 `.npmrc`에 Nexus 프록시로 설정되어 있다.

```bash
# Frontend
cd frontend
npm install

# Backend
cd ../backend
npm install
```

### 3-4. 환경변수 설정

#### Backend (`backend/.env`)

```bash
# ===========================================
# 서버 설정
# ===========================================
PORT=3001
NODE_ENV=development

# ===========================================
# wing DB (운영 데이터 - PostgreSQL + PostGIS)
# ===========================================
WING_DB_HOST=211.208.115.83
WING_DB_PORT=5432
WING_DB_USER=wing
WING_DB_PASS=<비밀번호>
WING_DB_NAME=wing

# ===========================================
# wing_auth DB (인증/권한 - PostgreSQL)
# ===========================================
AUTH_DB_HOST=211.208.115.83
AUTH_DB_PORT=5432
AUTH_DB_USER=wing_auth
AUTH_DB_PASS=<비밀번호>
AUTH_DB_NAME=wing_auth

# ===========================================
# JWT 인증
# ===========================================
JWT_SECRET=<랜덤 시크릿 문자열>
JWT_EXPIRES_IN=24h

# ===========================================
# CORS (프론트엔드 출처)
# ===========================================
FRONTEND_URL=http://localhost:5173

# ===========================================
# Google OAuth (선택)
# ===========================================
GOOGLE_CLIENT_ID=<Google OAuth 클라이언트 ID>
```

#### Frontend (`frontend/.env`)

```bash
# 백엔드 API URL
VITE_API_URL=http://localhost:3001/api

# Google OAuth (선택)
VITE_GOOGLE_CLIENT_ID=<Google OAuth 클라이언트 ID>

# 공공데이터 API 키 (해양기상, 선택)
VITE_DATA_GO_KR_API_KEY=<data.go.kr API 키>
VITE_WEATHER_API_KEY=<기상 API 키>
```

> `.env` 파일은 `.gitignore`에 포함된다. 실제 값은 팀 내부에서 공유한다.

### 3-5. 실행

```bash
# 터미널 1: 백엔드
cd backend
npm run dev
# 출력: 서버가 포트 3001에서 실행 중입니다.

# 터미널 2: 프론트엔드
cd frontend
npm run dev
# 출력: VITE vX.X.X ready in XXXms
#        -> Local: http://localhost:5173/
```

브라우저에서 http://localhost:5173 접속하여 확인한다.

---

## 4. 오프라인 설치

인터넷이 불가능한 폐쇄망 환경에서의 설치 절차.

### 4-1. 온라인 환경에서 패키지 준비

인터넷이 되는 PC에서 다음을 실행한다:

```bash
# 1. 프로젝트 클론 및 의존성 설치
git clone https://gitea.gc-si.dev/gc/wing-ops.git wing
cd wing/frontend && npm install
cd ../backend && npm install

# 2. 전체 프로젝트 압축 (node_modules 포함)
cd ../..
tar -czf wing_full.tar.gz wing/
```

### 4-2. Node.js 오프라인 설치 파일 준비

대상 OS에 맞는 설치 파일을 다운로드한다:

| OS | 파일 | 다운로드 |
|----|------|---------|
| Linux (x64) | `node-v20.x.x-linux-x64.tar.xz` | https://nodejs.org/dist/v20.x.x/ |
| macOS (arm64) | `node-v20.x.x-darwin-arm64.tar.gz` | https://nodejs.org/dist/v20.x.x/ |
| macOS (x64) | `node-v20.x.x-darwin-x64.tar.gz` | https://nodejs.org/dist/v20.x.x/ |
| Windows | `node-v20.x.x-x64.msi` | https://nodejs.org/dist/v20.x.x/ |

### 4-3. 대상 서버에 설치

```bash
# 1. Node.js 설치 (Linux 예시)
tar -xJf node-v20.x.x-linux-x64.tar.xz
export PATH=$PWD/node-v20.x.x-linux-x64/bin:$PATH
# .bashrc 또는 .profile에 PATH 영구 등록

# 2. 프로젝트 압축 해제
tar -xzf wing_full.tar.gz
cd wing

# 3. 환경변수 설정 (3-4 참조)
vi backend/.env
vi frontend/.env

# 4. 실행 (node_modules가 이미 포함되어 있으므로 npm install 불필요)
cd backend && npm run dev    # 터미널 1
cd frontend && npm run dev   # 터미널 2
```

### 4-4. 오프라인 환경 주의사항

- `npm install` 불가하므로 패키지 추가 시 온라인 환경에서 설치 후 재배포해야 한다.
- DB는 대상 환경에서 접근 가능한 PostgreSQL을 사용해야 한다.
- 공공데이터 API (기상, 해양) 키가 필요한 기능은 외부 네트워크 접근이 필요하다.

---

## 5. DB 초기화 및 마이그레이션

### 5-1. DB 구성

프로젝트는 동일 PostgreSQL 서버에 2개의 데이터베이스를 사용한다:

| DB | 용도 | 확장 |
|----|------|------|
| `wing` | 운영 데이터 (사고, 예측, 자산 등) | PostGIS |
| `wing_auth` | 인증/권한 (사용자, 역할, 메뉴) | uuid-ossp, pgcrypto |

### 5-2. 신규 DB 초기화

PostgreSQL에 최초 설치 시 아래 순서로 실행한다.

```bash
# 1. wing DB 초기화 (PostgreSQL superuser로 실행)
psql -U postgres -f database/init.sql

# 2. wing_auth DB 초기화
psql -U postgres -f database/auth_init.sql
```

`init.sql`에 포함된 내용:
- 사용자(wing) 및 데이터베이스(wing) 생성
- PostGIS 확장 설치
- 공통코드, 시뮬레이션, 사고, 예측 등 핵심 테이블 생성

`auth_init.sql`에 포함된 내용:
- 사용자(wing_auth) 및 데이터베이스(wing_auth) 생성
- uuid-ossp, pgcrypto 확장 설치
- 조직, 역할, 사용자, 권한, 메뉴, 설정, 감사로그 테이블 생성

### 5-3. 마이그레이션 적용

초기 스키마 이후 추가된 변경 사항은 `database/migration/` 디렉토리에 순번대로 관리된다.

```
database/migration/
├── 001_layer_table.sql         레이어 테이블
├── 002_hns_substance.sql       HNS 물질 데이터
├── 003_perm_tree.sql           권한 트리
├── 004_oper_cd.sql             오퍼레이션 코드
├── 005_db_consolidation.sql    DB 통합
├── 006_board.sql               게시판
├── 007_reports.sql             보고서
├── 008_assets.sql              자산
├── 008_assets_seed.sql         자산 시드 데이터
├── 009_incidents.sql           사건/사고
├── 010_postgis_geom.sql        PostGIS 지오메트리
├── 011_scat.sql                SCAT 조사
├── 012_board_ext.sql           게시판 확장
├── 013_hns_analysis.sql        HNS 분석
├── 014_prediction.sql          확산 예측
├── 015_aerial.sql              항공 방제
└── 016_rescue.sql              구조 시나리오
```

**마이그레이션 실행 (수동):**

```bash
# wing DB에 순번대로 적용
psql -h <호스트> -p 5432 -U wing -d wing -f database/migration/001_layer_table.sql
psql -h <호스트> -p 5432 -U wing -d wing -f database/migration/002_hns_substance.sql
# ... 순번대로 계속
```

> 이미 적용된 마이그레이션을 다시 실행해도 대부분 `IF NOT EXISTS` 조건이 포함되어 있어 안전하다.

### 5-4. 시드 데이터

초기 데이터(관리자 계정, 공통코드 등)를 입력한다:

```bash
cd backend
npm run db:seed
```

---

## 6. 개발 서버 실행

### 6-1. 명령어 요약

**Frontend:**

| 명령어 | 설명 |
|--------|------|
| `npm run dev` | Vite 개발 서버 (localhost:5173, HMR 지원) |
| `npm run build` | 프로덕션 빌드 (`tsc -b && vite build` -> `dist/`) |
| `npm run lint` | ESLint 검증 |
| `npm run preview` | 빌드 결과 미리보기 |

**Backend:**

| 명령어 | 설명 |
|--------|------|
| `npm run dev` | tsx watch 개발 서버 (localhost:3001, 파일 변경 시 자동 재시작) |
| `npm run build` | TypeScript 컴파일 (`tsc` -> `dist/`) |
| `npm start` | 프로덕션 실행 (`node dist/server.js`) |
| `npm run db:seed` | DB 시드 데이터 입력 |

### 6-2. 개발 서버 시작

```bash
# 터미널 1: 백엔드 (먼저 실행)
cd backend
npm run dev
# 서버가 포트 3001에서 실행 중입니다.
# wing DB 연결 성공 (211.208.115.83:5432/wing)

# 터미널 2: 프론트엔드
cd frontend
npm run dev
# VITE v7.x.x ready in XXXms
# -> Local: http://localhost:5173/
```

### 6-3. 검증 명령어

```bash
# TypeScript 타입 체크
cd frontend && npx tsc --noEmit
cd backend && npx tsc --noEmit

# ESLint
cd frontend && npx eslint src/

# Prettier
cd frontend && npx prettier --check src/
cd frontend && npx prettier --write src/    # 자동 수정
```

---

## 7. 운영 서버 배포

### 7-1. 서버 환경 준비

운영 서버(Rocky Linux 9 기준)에 다음을 설치한다:

```bash
# Node.js 20 설치 (fnm 또는 직접 설치)
curl -fsSL https://fnm.vercel.app/install | bash
source ~/.bashrc
fnm install 20
fnm use 20

# PostgreSQL 16 + PostGIS 설치 (dnf 또는 직접 설치)
# 이미 설치된 경우 생략
```

### 7-2. 수동 배포 절차

#### Frontend

```bash
cd frontend

# 의존성 설치
npm ci

# 환경변수 설정 (빌드 시 Vite가 주입)
export VITE_API_URL=/api
export VITE_GOOGLE_CLIENT_ID=<클라이언트ID>
export VITE_DATA_GO_KR_API_KEY=<키>
export VITE_WEATHER_API_KEY=<키>

# 프로덕션 빌드
npx vite build

# 빌드 결과를 웹 서버 디렉토리로 복사
cp -r dist/* /deploy/wing-demo/
```

#### Backend

```bash
cd backend

# 의존성 설치
npm ci

# TypeScript 컴파일
npx tsc

# 프로덕션 의존성만 유지 (devDependencies 제거)
npm prune --omit=dev

# 배포 디렉토리로 복사
mkdir -p /deploy/wing-demo-backend/dist
cp -r dist/* /deploy/wing-demo-backend/dist/
cp -r node_modules /deploy/wing-demo-backend/
cp package.json /deploy/wing-demo-backend/
```

### 7-3. systemd 서비스 등록

Backend를 systemd 서비스로 등록하여 자동 시작/재시작을 설정한다.

**서비스 파일 생성** (`/etc/systemd/system/wing-demo-api.service`):

```ini
[Unit]
Description=WING-OPS Backend API
After=network.target postgresql.service

[Service]
Type=simple
User=deploy
WorkingDirectory=/deploy/wing-demo-backend
ExecStart=/home/deploy/.local/share/fnm/aliases/default/bin/node dist/server.js
Restart=on-failure
RestartSec=5

# 환경변수
Environment=NODE_ENV=production
Environment=PORT=3001
Environment=WING_DB_HOST=211.208.115.83
Environment=WING_DB_PORT=5432
Environment=WING_DB_USER=wing
Environment=WING_DB_PASS=<비밀번호>
Environment=WING_DB_NAME=wing
Environment=AUTH_DB_HOST=211.208.115.83
Environment=AUTH_DB_PORT=5432
Environment=AUTH_DB_USER=wing_auth
Environment=AUTH_DB_PASS=<비밀번호>
Environment=AUTH_DB_NAME=wing_auth
Environment=JWT_SECRET=<시크릿>
Environment=JWT_EXPIRES_IN=24h
Environment=FRONTEND_URL=https://wing-demo.gc-si.dev
Environment=GOOGLE_CLIENT_ID=<클라이언트ID>

[Install]
WantedBy=multi-user.target
```

**서비스 등록 및 시작:**

```bash
sudo systemctl daemon-reload
sudo systemctl enable wing-demo-api
sudo systemctl start wing-demo-api

# 상태 확인
sudo systemctl status wing-demo-api

# 로그 확인
sudo journalctl -u wing-demo-api -f
```

### 7-4. 리버스 프록시 (Nginx)

Frontend 정적 파일과 Backend API를 하나의 도메인으로 서비스한다.

```nginx
server {
    listen 443 ssl;
    server_name wing-demo.gc-si.dev;

    ssl_certificate     /etc/ssl/certs/wing-demo.crt;
    ssl_certificate_key /etc/ssl/private/wing-demo.key;

    # Frontend 정적 파일
    root /deploy/wing-demo;
    index index.html;

    location / {
        try_files $uri $uri/ /index.html;
    }

    # Backend API 프록시
    location /api/ {
        proxy_pass http://127.0.0.1:3001/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;
    }

    # 헬스 체크
    location /health {
        proxy_pass http://127.0.0.1:3001/health;
    }
}

# HTTP -> HTTPS 리다이렉트
server {
    listen 80;
    server_name wing-demo.gc-si.dev;
    return 301 https://$host$request_uri;
}
```

---

## 8. CI/CD 자동 배포

### 8-1. Gitea Actions 파이프라인

main 브랜치에 push(머지)되면 `.gitea/workflows/deploy.yml`이 자동 실행된다.

```
main 브랜치 push
      |
[Checkout]           소스 코드 체크아웃
      |
[Setup Node.js]      Node.js 24 설정
      |
[Configure npm]      Nexus 프록시 레지스트리 설정
      |
[Frontend Build]     npm ci -> vite build -> /deploy/wing-demo/
      |
[Backend Build]      npm ci -> tsc -> npm prune --omit=dev
      |                -> /deploy/wing-demo-backend/
      |
[Deploy Trigger]     .deploy-trigger 파일 생성
                     -> cron/watchdog이 감지 -> 서비스 재시작
```

### 8-2. Gitea Secrets 설정

CI/CD에서 사용하는 시크릿을 Gitea에 등록해야 한다:

**등록 경로:** Settings -> Actions -> Secrets -> Add Secret

| Secret 이름 | 용도 |
|-------------|------|
| `NEXUS_NPM_AUTH` | npm Nexus 프록시 인증 토큰 |
| `GOOGLE_CLIENT_ID` | Google OAuth 클라이언트 ID |
| `DATA_GO_KR_API_KEY` | 공공데이터 API 키 |
| `WEATHER_API_KEY` | 기상 API 키 |

### 8-3. 배포 확인

```bash
# 프론트엔드 응답 확인
curl -s -o /dev/null -w '%{http_code}' https://wing-demo.gc-si.dev/
# 기대: 200

# 백엔드 헬스 체크
curl -s https://wing-demo.gc-si.dev/api/health
# 기대: {"status":"ok"}

# 백엔드 API 정보
curl -s https://wing-demo.gc-si.dev/api/
# 기대: {"name":"WING Backend API","version":"1.0.0","status":"running"}
```

---

## 9. 접속 정보 요약

### 개발 환경

| 서비스 | URL | 포트 |
|--------|-----|------|
| Frontend (Vite dev) | http://localhost:5173 | 5173 |
| Backend API | http://localhost:3001 | 3001 |
| Backend 헬스 체크 | http://localhost:3001/health | 3001 |
| PostgreSQL (wing) | 211.208.115.83:5432/wing | 5432 |
| PostgreSQL (wing_auth) | 211.208.115.83:5432/wing_auth | 5432 |

### 운영 환경

| 서비스 | URL |
|--------|-----|
| Frontend | https://wing-demo.gc-si.dev |
| Backend API | https://wing-demo.gc-si.dev/api/ |
| 헬스 체크 | https://wing-demo.gc-si.dev/api/health |
| Gitea | https://gitea.gc-si.dev/gc/wing-ops |

### API 엔드포인트

| 경로 | 용도 |
|------|------|
| `GET /` | API 정보 |
| `GET /health` | 헬스 체크 |
| `POST /api/auth/login` | 로그인 |
| `POST /api/auth/logout` | 로그아웃 |
| `GET /api/auth/me` | 현재 사용자 |
| `GET /api/users` | 사용자 목록 |
| `GET /api/roles` | 역할 목록 |
| `GET /api/menus` | 메뉴 목록 |
| `GET /api/settings` | 시스템 설정 |
| `GET /api/audit` | 감사 로그 |
| `GET /api/board` | 게시판 |
| `GET /api/layers` | 레이어 |
| `GET /api/simulation` | 시뮬레이션 |
| `GET /api/hns` | HNS 물질 |
| `GET /api/reports` | 보고서 |
| `GET /api/assets` | 자산 |
| `GET /api/incidents` | 사건/사고 |
| `GET /api/scat` | SCAT 조사 |
| `GET /api/prediction` | 확산 예측 |
| `GET /api/aerial` | 항공 방제 |
| `GET /api/rescue` | 구조 시나리오 |

---

## 10. 트러블슈팅

### 의존성 설치

| 증상 | 원인 | 해결 |
|------|------|------|
| `npm install` 실패 (ENETUNREACH) | Nexus 프록시 접근 불가 | `.npmrc`의 registry URL 확인, VPN/네트워크 상태 확인 |
| `npm install` 실패 (EAUTH) | Nexus 인증 토큰 만료 | `.npmrc`의 `_auth` 값 갱신 |
| `npm install` 실패 (peer dependency) | 패키지 버전 충돌 | `npm install --legacy-peer-deps` 시도 |
| `MODULE_NOT_FOUND` | node_modules 누락/손상 | `rm -rf node_modules package-lock.json && npm install` |

### 서버 실행

| 증상 | 원인 | 해결 |
|------|------|------|
| 포트 5173/3001 충돌 | 이미 다른 프로세스가 사용 중 | `lsof -i :5173` 또는 `lsof -i :3001`로 PID 확인 후 `kill -9 <PID>` |
| 백엔드 시작 시 DB 연결 실패 | DB 접속 정보 오류 또는 네트워크 | `backend/.env`의 DB 호스트/포트/사용자/비밀번호 확인, `nc -zv <호스트> 5432`로 네트워크 확인 |
| 프론트엔드에서 API 호출 CORS 에러 | 백엔드 CORS 설정 불일치 | `backend/.env`의 `FRONTEND_URL`이 프론트엔드 URL과 일치하는지 확인 |
| `env: tsx: No such file or directory` | tsx 미설치 | `cd backend && npm install` 재실행 |

### DB

| 증상 | 원인 | 해결 |
|------|------|------|
| `relation "XXX" does not exist` | 테이블 미생성 | `database/init.sql` 또는 해당 마이그레이션 SQL 실행 |
| PostGIS 함수 에러 | PostGIS 확장 미설치 | `psql -U postgres -d wing -c "CREATE EXTENSION IF NOT EXISTS postgis"` |
| 인코딩 문제 (한글 깨짐) | DB 인코딩 설정 | DB 생성 시 `ENCODING='UTF8' LC_COLLATE='ko_KR.UTF-8'` 지정 |
| seed 실패 | 이미 데이터 존재 또는 FK 제약 | 에러 메시지 확인 후 해당 테이블 데이터 정리 |

### Git Hooks

| 증상 | 원인 | 해결 |
|------|------|------|
| pre-commit이 실행되지 않음 | hooksPath 미설정 | `git config core.hooksPath .githooks` |
| pre-commit permission denied | 실행 권한 없음 | `chmod +x .githooks/pre-commit .githooks/commit-msg` |
| TypeScript 에러로 커밋 차단 | 타입 에러 존재 | `npx tsc --noEmit`으로 에러 확인 후 수정 |
| commit-msg 형식 오류 | Conventional Commits 미준수 | `type(scope): subject` 형식 준수 (type: feat, fix, docs 등) |

### 빌드

| 증상 | 원인 | 해결 |
|------|------|------|
| `vite build` 메모리 부족 | Node.js 힙 메모리 부족 | `export NODE_OPTIONS=--max-old-space-size=4096` 후 재시도 |
| `tsc` 타입 에러 | TypeScript strict 모드 위반 | `npx tsc --noEmit`으로 에러 목록 확인 후 수정 |
| 빌드 후 라우팅 404 | SPA 서버 설정 누락 | Nginx `try_files $uri $uri/ /index.html` 설정 확인 |
