# WING-OPS 개발 워크플로우 가이드

## 목차

1. [전체 흐름 요약](#1-전체-흐름-요약)
2. [개발 환경 설정](#2-개발-환경-설정)
3. [계획 수립 (Plan)](#3-계획-수립-plan)
4. [브랜치 생성](#4-브랜치-생성)
5. [개발](#5-개발)
6. [검증](#6-검증)
7. [커밋 & 푸시](#7-커밋--푸시)
8. [MR(Merge Request)](#8-mrmerge-request)
9. [릴리즈 & 배포](#9-릴리즈--배포)
10. [디버깅 팁](#10-디버깅-팁)
11. [실전 예시: 기능 추가 A to Z](#11-실전-예시-기능-추가-a-to-z)

---

## 1. 전체 흐름 요약

```
Plan ─── Branch ─── Implement ─── Test ─── MR ─── Deploy
```

```
[1. Plan]            작업 범위 정의, 수정 파일 식별
      |
[2. Branch]          develop 기반 feature 브랜치 생성
      |
[3. Implement]       코드 작성 (Frontend / Backend / DB)
      |
[4. Test]            tsc --noEmit + ESLint + 수동 테스트
      |
[5. Commit & Push]   Conventional Commits 형식, pre-commit 자동 검증
      |
[6. MR -> develop]   코드 리뷰 + 승인 + Squash Merge
      |
[7. MR -> main]      릴리즈 MR + 머지
      |
[8. Auto Deploy]     Gitea Actions -> 빌드 -> 서버 배포
```

### 브랜치 흐름

```
main (보호, 배포용)
  |
  +-- develop (보호, 개발 통합)
        |
        +-- feature/ISSUE-42-login     (기능 개발)
        +-- bugfix/ISSUE-15-token-fix   (버그 수정)
        +-- hotfix/critical-patch       (긴급 수정, main에서 분기)
```

---

## 2. 개발 환경 설정

### 2-1. Node.js 설치 (fnm)

프로젝트는 Node.js 20을 사용한다. `.node-version` 파일로 버전이 고정되어 있다.

```bash
# fnm 설치 (이미 설치된 경우 생략)
curl -fsSL https://fnm.vercel.app/install | bash

# Node.js 20 설치 및 활성화
fnm install 20
fnm use 20

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

### 2-2. 프로젝트 클론 및 의존성 설치

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

# Git Hooks 경로 설정 (최초 1회)
git config core.hooksPath .githooks

# npm 레지스트리는 .npmrc에 설정됨 (Nexus 프록시)
# 별도 설정 불필요

# 의존성 설치
cd frontend && npm install
cd ../backend && npm install
```

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

**Backend** (`backend/.env`):

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

# wing DB (운영 데이터)
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 (인증/권한)
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=<클라이언트ID>
```

**Frontend** (`frontend/.env`):

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

# Google OAuth
VITE_GOOGLE_CLIENT_ID=<클라이언트ID>

# 공공 API 키 (해양기상 등)
VITE_DATA_GO_KR_API_KEY=<키>
VITE_WEATHER_API_KEY=<키>
```

> `.env` 파일은 `.gitignore`에 포함되어 있으므로 커밋되지 않는다.
> 실제 값은 팀 내부 공유 채널에서 확인한다.

### 2-4. 개발 서버 실행

터미널 2개를 열어 각각 실행한다:

```bash
# 터미널 1: 백엔드 (localhost:3001)
cd backend
npm run dev

# 터미널 2: 프론트엔드 (localhost:5173)
cd frontend
npm run dev
```

### 2-5. Path Alias

Frontend에서 두 가지 경로 별칭을 사용한다:

| Alias | 실제 경로 | 용도 |
|-------|----------|------|
| `@common/*` | `src/common/*` | 공통 모듈 (컴포넌트, 훅, 서비스, 스토어) |
| `@tabs/*` | `src/tabs/*` | 탭별 패키지 (11개 탭) |

```tsx
import { useAuth } from '@common/hooks/useAuth';
import OilSpillView from '@tabs/prediction/components/OilSpillView';
```

---

## 3. 계획 수립 (Plan)

### 계획이 필요한 경우

- 3개 이상 파일 수정이 예상되는 작업
- 아키텍처에 영향을 주는 변경
- 새로운 탭/모듈 추가
- DB 스키마 변경

### 계획에 포함할 내용

1. **수정/생성할 파일 목록**
2. **변경 범위 및 영향도** (다른 모듈에 미치는 영향)
3. **DB 마이그레이션 필요 여부** (필요 시 migration SQL 작성)
4. **구현 순서** (의존성 고려)

### 계획이 불필요한 경우

- 단일 파일 수정 (버그 수정, 텍스트 변경)
- 스타일/포맷팅 변경
- 설정 파일 변경

---

## 4. 브랜치 생성

### 네이밍 규칙

| 유형 | 형식 | 예시 | 분기 기준 |
|------|------|------|----------|
| 기능 | `feature/ISSUE-번호-설명` | `feature/ISSUE-42-user-login` | develop |
| 기능 (이슈 없음) | `feature/설명` | `feature/add-swagger-docs` | develop |
| 버그 | `bugfix/ISSUE-번호-설명` | `bugfix/ISSUE-15-token-expired` | develop |
| 긴급 | `hotfix/설명` | `hotfix/security-patch` | main |

### 브랜치 생성

```bash
# 1. develop 최신화
git checkout develop
git pull origin develop

# 2. feature 브랜치 생성
git checkout -b feature/ISSUE-42-user-login

# 확인
git branch    # * feature/ISSUE-42-user-login
```

### 브랜치 규칙

- main, develop 브랜치에 직접 커밋/푸시 **금지** (보호 브랜치)
- 머지는 반드시 **MR(Merge Request)**을 통해 수행
- 머지 후 소스 브랜치 삭제

---

## 5. 개발

### 5-1. Frontend 개발 구조

```
frontend/src/
├── common/              공통 모듈 (모든 탭에서 공유)
│   ├── components/      auth/, layer/, layout/, map/, ui/
│   ├── hooks/           useAuth, useLayers, useSubMenu
│   ├── services/        api.ts (Axios 인스턴스), authApi.ts
│   ├── store/           authStore (Zustand), menuStore
│   ├── 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/           관리자
```

**각 탭 내부 구조 패턴:**

```
tabs/{탭명}/
├── components/          UI 컴포넌트
│   ├── {Tab}View.tsx    메인 뷰 (App.tsx에서 라우팅)
│   ├── {Tab}LeftPanel.tsx
│   └── {Tab}RightPanel.tsx
├── services/            API 호출
│   └── {탭명}Api.ts
├── hooks/               탭 전용 훅 (선택)
└── types/               탭 전용 타입 (선택)
```

### 5-2. Backend 개발 구조

```
backend/src/
├── server.ts            진입점 (미들웨어, 라우터 등록)
├── auth/                인증 (JWT, OAuth, 미들웨어)
│   ├── authRouter.ts    POST /api/auth/login, /api/auth/logout 등
│   ├── authService.ts
│   └── authMiddleware.ts  requireAuth, requireRole, requirePermission
├── users/               사용자 관리
├── roles/               역할/권한
│   └── permResolver.ts  2차원 권한 해석 엔진
├── {도메인}/            도메인별 모듈
│   ├── {도메인}Router.ts
│   └── {도메인}Service.ts
├── middleware/           보안 (입력 살균, rate-limit)
└── db/
    ├── wingDb.ts        wing DB Pool
    ├── authDb.ts        wing_auth DB Pool
    └── seed.ts          시드 데이터
```

**라우터 작성 패턴:**

```typescript
import { Router } from 'express';
import { requireAuth } from '../auth/authMiddleware.js';
import * as service from './exampleService.js';

const router = Router();

// GET: 목록 조회
router.get('/', requireAuth, async (req, res) => {
  try {
    const result = await service.getList();
    res.json(result);
  } catch (err) {
    res.status(500).json({ error: '조회 실패' });
  }
});

// POST: 생성 (GET/POST only 정책)
router.post('/', requireAuth, async (req, res) => {
  try {
    const result = await service.create(req.body);
    res.json(result);
  } catch (err) {
    res.status(500).json({ error: '생성 실패' });
  }
});

export default router;
```

> **HTTP 정책**: GET/POST만 사용한다 (보안취약점 가이드 준수, PUT/DELETE 미사용).

### 5-3. DB 마이그레이션

스키마 변경이 필요한 경우 `database/migration/` 디렉토리에 SQL 파일을 추가한다.

```
database/migration/
├── 001_layer_table.sql
├── 002_hns_substance.sql
├── ...
└── 016_rescue.sql
```

**네이밍**: `{3자리 번호}_{설명}.sql` (예: `017_weather_alert.sql`)

```sql
-- 017_weather_alert.sql
-- 기상 경보 테이블 추가

CREATE TABLE WTHR_ALRT (
  ALRT_SN       SERIAL        NOT NULL,
  ALRT_TP_CD    VARCHAR(20)   NOT NULL,
  ALRT_CN       TEXT,
  REG_DTM       TIMESTAMPTZ   NOT NULL DEFAULT NOW(),
  CONSTRAINT PK_WTHR_ALRT PRIMARY KEY (ALRT_SN)
);

COMMENT ON TABLE  WTHR_ALRT IS '기상경보';
COMMENT ON COLUMN WTHR_ALRT.ALRT_SN    IS '경보순번';
COMMENT ON COLUMN WTHR_ALRT.ALRT_TP_CD IS '경보유형코드';
COMMENT ON COLUMN WTHR_ALRT.ALRT_CN    IS '경보내용';
COMMENT ON COLUMN WTHR_ALRT.REG_DTM    IS '등록일시';
```

> DB 표준화: 공공데이터베이스 표준화 관리 매뉴얼(2021.06) 기준을 따른다.
> 컬럼명은 영문약어명 조합 (30자 이내, 언더스코어 구분).

---

## 6. 검증

### 6-1. TypeScript 타입 체크

```bash
# Frontend
cd frontend && npx tsc --noEmit

# Backend
cd backend && npx tsc --noEmit
```

### 6-2. ESLint

```bash
# Frontend (flat config)
cd frontend && npx eslint src/

# 자동 수정
cd frontend && npx eslint src/ --fix
```

### 6-3. Prettier (선택)

```bash
cd frontend && npx prettier --check src/
cd frontend && npx prettier --write src/
```

### 6-4. 빌드 검증

배포 전 빌드가 성공하는지 확인한다:

```bash
# Frontend
cd frontend && npm run build    # tsc -b && vite build -> dist/

# Backend
cd backend && npm run build     # tsc -> dist/
```

### 6-5. 수동 테스트

테스트 프레임워크가 미구성이므로 수동으로 검증한다:

1. 개발 서버 실행 (`npm run dev`)
2. 브라우저에서 기능 동작 확인
3. API 호출은 브라우저 DevTools Network 탭 또는 curl로 확인

```bash
# API 헬스 체크
curl http://localhost:3001/health

# 인증이 필요한 API 테스트 (쿠키 기반)
curl -b "WING_SESSION=<JWT토큰>" http://localhost:3001/api/users
```

---

## 7. 커밋 & 푸시

### 7-1. Conventional Commits 형식

```
type(scope): subject
```

| type | 용도 | 예시 |
|------|------|------|
| `feat` | 새 기능 | `feat(auth): Google OAuth 로그인 추가` |
| `fix` | 버그 수정 | `fix(map): 레이어 겹침 오류 수정` |
| `refactor` | 리팩토링 | `refactor(api): 중복 호출 제거` |
| `docs` | 문서 | `docs: API 엔드포인트 문서 추가` |
| `chore` | 설정/빌드 | `chore: 의존성 버전 업데이트` |
| `style` | 포맷팅 | `style: ESLint 경고 수정` |
| `test` | 테스트 | `test(auth): 로그인 단위 테스트 추가` |
| `ci` | CI/CD | `ci: 백엔드 빌드 스텝 추가` |
| `perf` | 성능 개선 | `perf(map): 레이어 렌더링 최적화` |

**규칙:**

- `type` 필수, `scope` 선택 (한/영 모두 가능)
- `subject` 72자 이내, 마침표 없이 끝냄
- 한국어, 영어 모두 허용

### 7-2. pre-commit Hook

커밋 시 `.githooks/pre-commit`이 자동 실행되어 다음 항목을 검증한다:

```
[1] Frontend TypeScript 타입 체크  (tsc --noEmit)
[2] Frontend ESLint 검증           (eslint src/ --quiet)
[3] Backend TypeScript 타입 체크   (tsc --noEmit)
```

**하나라도 실패하면 커밋이 차단된다.**

```
pre-commit: [frontend] TypeScript 타입 체크 중...
pre-commit: [frontend] 타입 체크 성공
pre-commit: [frontend] ESLint 검증 중...
pre-commit: [frontend] ESLint 통과
pre-commit: [backend] TypeScript 타입 체크 중...
pre-commit: [backend] 타입 체크 성공
```

실패 예시:

```
╔══════════════════════════════════════════════════════════╗
║  [frontend] TypeScript 타입 에러! 커밋이 차단됩니다.    ║
╚══════════════════════════════════════════════════════════╝

타입/린트 에러를 수정한 후 다시 커밋해주세요.
```

### 7-3. commit-msg Hook

커밋 메시지가 Conventional Commits 형식인지 검증한다:

- 통과: `feat(auth): JWT 기반 로그인 구현`
- 차단: `로그인 기능 추가` (type 누락)
- 예외: `Merge ...`, `Revert ...` 커밋은 검증 건너뜀

### 7-4. 커밋 & 푸시 예시

```bash
# 변경 파일 확인
git status

# 스테이징 (파일 지정)
git add frontend/src/tabs/incidents/components/IncidentDetailView.tsx
git add backend/src/incidents/incidentService.ts

# 커밋 (pre-commit + commit-msg 검증 자동 실행)
git commit -m "feat(incidents): 사고 상세 조회 페이지 추가"

# 푸시
git push -u origin feature/ISSUE-42-incident-detail
```

---

## 8. MR(Merge Request)

### 8-1. MR 생성 (feature -> develop)

**Gitea Web UI:**

1. https://gitea.gc-si.dev/gc/wing-ops 접속
2. "New Pull Request" 클릭
3. Base: `develop`, Compare: `feature/ISSUE-42-incident-detail`
4. 제목/본문 작성 후 생성

**CLI (gh 또는 tea):**

```bash
# Gitea API로 MR 생성
curl -X POST "https://gitea.gc-si.dev/api/v1/repos/gc/wing-ops/pulls" \
  -H "Authorization: token <API_TOKEN>" \
  -H "Content-Type: application/json" \
  -d '{
    "title": "feat(incidents): 사고 상세 조회 페이지 추가",
    "body": "## Summary\n- 사고 상세 조회 페이지 추가\n- 사고 목록에서 클릭 시 상세 정보 표시",
    "base": "develop",
    "head": "feature/ISSUE-42-incident-detail"
  }'
```

### 8-2. MR 본문 템플릿

```markdown
## Summary
- 변경 내용을 1~3줄로 요약

## 변경 파일
- `frontend/src/tabs/incidents/components/IncidentDetailView.tsx` (신규)
- `backend/src/incidents/incidentService.ts` (수정)

## Test plan
- [ ] 사고 목록에서 상세 조회 클릭 확인
- [ ] 데이터 없을 때 빈 화면 처리 확인
- [ ] API 에러 시 에러 메시지 표시 확인
```

### 8-3. MR 리뷰 & 머지

1. **리뷰어 지정**: 최소 1명 승인 필수
2. **CI 통과**: 자동 검증이 설정된 경우 통과 필수
3. **리뷰 코멘트**: 모두 해결 후 머지
4. **머지 방식**: Squash Merge 권장 (깔끔한 히스토리)
5. **소스 브랜치**: 머지 후 삭제

### 8-4. 머지 후 로컬 동기화

```bash
git checkout develop
git pull origin develop

# 작업했던 feature 브랜치 삭제
git branch -d feature/ISSUE-42-incident-detail
```

---

## 9. 릴리즈 & 배포

### 9-1. 릴리즈 MR (develop -> main)

develop에 기능이 머지된 후, 배포를 위해 main으로 릴리즈 MR을 생성한다.

```markdown
## Release

### 포함 기능
1. feat(incidents): 사고 상세 조회 페이지 추가
2. fix(map): 레이어 오류 수정

### 배포 전 확인
- [ ] 로컬 빌드 성공 (frontend + backend)
- [ ] DB 마이그레이션 적용 완료 (해당 시)
- [ ] 서버 환경변수 설정 완료 (해당 시)
```

### 9-2. 자동 배포 (Gitea Actions)

main에 머지되면 `.gitea/workflows/deploy.yml`이 자동 실행된다:

```
main 브랜치 push
      |
[1. Checkout]        소스 코드 체크아웃
      |
[2. Setup Node.js]   Node.js 24 설정
      |
[3. npm registry]    Nexus 프록시 설정
      |
[4. Frontend]        npm ci -> vite build -> /deploy/wing-demo/
      |
[5. Backend]         npm ci -> tsc -> npm prune --omit=dev
      |                -> /deploy/wing-demo-backend/
      |
[6. Trigger]         .deploy-trigger 생성 -> 서비스 재시작
```

### 9-3. 배포 환경

| 항목 | 값 |
|------|---|
| 프론트엔드 | https://wing-demo.gc-si.dev |
| 백엔드 API | https://wing-demo.gc-si.dev/api/ |
| DB | PostgreSQL 16 + PostGIS (211.208.115.83:5432) |
| 서버 OS | Rocky Linux 9.6 |
| 프로세스 관리 | systemd (`wing-demo-api.service`) |

### 9-4. 배포 확인

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

# 백엔드 헬스 체크
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-5. 환경변수 관리

| 위치 | 용도 | 예시 |
|------|------|------|
| systemd 서비스 파일 | 서버 런타임 환경변수 | DB 접속 정보, JWT_SECRET |
| Gitea Secrets | CI/CD 빌드 시 환경변수 | NEXUS_NPM_AUTH, GOOGLE_CLIENT_ID |

**Gitea Secret 등록:**

```
Settings -> Actions -> Secrets -> Add Secret
```

---

## 10. 디버깅 팁

### 10-1. Frontend

**Vite 개발 서버 프록시 문제:**

- API 호출이 CORS 에러를 발생시키면 백엔드 `FRONTEND_URL` 환경변수를 확인한다.
- 개발 환경에서는 `localhost:5173`, `localhost:5174`, `localhost:3000`이 자동 허용된다.
- KHOA 해양 이미지(`/daily_ocean`)는 Vite 프록시 경유: `vite.config.ts` → `proxy` 설정 확인

**타입 에러:**

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

# 특정 파일만 확인하고 싶으면 IDE의 TypeScript 서버 출력을 확인한다
```

**상태 관리 디버깅:**

- Zustand DevTools: 브라우저 확장에서 스토어 상태 확인 가능
- React Query DevTools: TanStack Query의 캐시 상태 확인

### 10-2. Backend

**DB 연결 실패:**

```bash
# PostgreSQL 접속 테스트
psql -h 211.208.115.83 -p 5432 -U wing -d wing -c "SELECT 1"

# 방화벽 확인
nc -zv 211.208.115.83 5432
```

**API 디버깅:**

```bash
# 헬스 체크
curl http://localhost:3001/health

# 인증 테스트 (로그인 후 쿠키 확인)
curl -c cookies.txt -X POST http://localhost:3001/api/auth/login \
  -H "Content-Type: application/json" \
  -d '{"email":"test@test.com","password":"test1234"}'

# 인증이 필요한 API
curl -b cookies.txt http://localhost:3001/api/users
```

**Rate Limit:**

- 일반 API: 15분당 IP당 200회
- 시뮬레이션 API: 1분당 IP당 10회
- 초과 시 `429 Too Many Requests` 응답

### 10-3. 공통

**포트 충돌:**

```bash
# 사용 중인 포트 확인
lsof -i :5173    # Frontend
lsof -i :3001    # Backend

# 프로세스 종료
kill -9 <PID>
```

**node_modules 문제:**

```bash
# 의존성 초기화
rm -rf node_modules package-lock.json
npm install
```

**Git Hooks가 동작하지 않을 때:**

```bash
# hooks 경로 확인
git config core.hooksPath
# 출력이 .githooks가 아니면 재설정
git config core.hooksPath .githooks

# 실행 권한 확인
chmod +x .githooks/pre-commit .githooks/commit-msg
```

---

## 11. 실전 예시: 기능 추가 A to Z

### 시나리오: "사고 상세 조회 페이지 추가"

#### Step 1. 계획

수정/생성 파일 식별:

| 파일 | 변경 |
|------|------|
| `database/migration/017_incident_detail.sql` | DB 마이그레이션 (필요 시) |
| `backend/src/incidents/incidentService.ts` | 상세 조회 함수 추가 |
| `backend/src/incidents/incidentRouter.ts` | `GET /api/incidents/:id` 라우트 |
| `frontend/src/tabs/incidents/services/incidentsApi.ts` | API 호출 함수 |
| `frontend/src/tabs/incidents/components/IncidentDetailView.tsx` | 상세 뷰 컴포넌트 |

#### Step 2. 브랜치 생성

```bash
git checkout develop
git pull origin develop
git checkout -b feature/ISSUE-42-incident-detail
```

#### Step 3. 개발

**Backend - Service:**

```typescript
// backend/src/incidents/incidentService.ts
export async function getIncidentById(id: number) {
  const { rows } = await wingPool.query(
    'SELECT * FROM ACDNT WHERE ACDNT_SN = $1',
    [id]
  );
  return rows[0] || null;
}
```

**Backend - Router:**

```typescript
// backend/src/incidents/incidentRouter.ts
router.get('/:id', requireAuth, async (req, res) => {
  const id = Number(req.params.id);
  const incident = await getIncidentById(id);
  if (!incident) {
    return res.status(404).json({ error: '사고를 찾을 수 없습니다.' });
  }
  res.json(incident);
});
```

**Frontend - API:**

```typescript
// frontend/src/tabs/incidents/services/incidentsApi.ts
export async function fetchIncidentById(id: number) {
  const { data } = await api.get(`/incidents/${id}`);
  return data;
}
```

**Frontend - Component:**

```tsx
// frontend/src/tabs/incidents/components/IncidentDetailView.tsx
const IncidentDetailView = ({ incidentId }: IncidentDetailViewProps) => {
  const { data, isLoading } = useQuery({
    queryKey: ['incident', incidentId],
    queryFn: () => fetchIncidentById(incidentId),
  });

  if (isLoading) return <div>Loading...</div>;
  // ...렌더링
};
```

#### Step 4. 검증

```bash
cd frontend && npx tsc --noEmit && npx eslint src/
cd ../backend && npx tsc --noEmit
```

#### Step 5. 커밋 & 푸시

```bash
git add backend/src/incidents/ frontend/src/tabs/incidents/
git commit -m "feat(incidents): 사고 상세 조회 페이지 추가"
# pre-commit: TypeScript OK, ESLint OK
# commit-msg: Conventional Commits OK

git push -u origin feature/ISSUE-42-incident-detail
```

#### Step 6. MR 생성 & 리뷰

- Gitea에서 `feature/ISSUE-42-incident-detail -> develop` MR 생성
- 리뷰어 승인 후 Squash Merge
- 소스 브랜치 삭제

#### Step 7. 릴리즈 & 배포

- `develop -> main` 릴리즈 MR 생성
- 머지 -> Gitea Actions 자동 배포
- https://wing-demo.gc-si.dev 에서 확인

#### Step 8. 로컬 동기화

```bash
git checkout develop
git pull origin develop
git branch -d feature/ISSUE-42-incident-detail
```
