wing-ops/docs/DEVELOPMENT-GUIDE.md

859 lines
22 KiB
Markdown

# 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
```