wing-ops/docs/DEVELOPMENT-GUIDE.md

22 KiB

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

목차

  1. 전체 흐름 요약
  2. 개발 환경 설정
  3. 계획 수립 (Plan)
  4. 브랜치 생성
  5. 개발
  6. 검증
  7. 커밋 & 푸시
  8. MR(Merge Request)
  9. 릴리즈 & 배포
  10. 디버깅 팁
  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 파일로 버전이 고정되어 있다.

# 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. 프로젝트 클론 및 의존성 설치

# 클론
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):

# 서버
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):

# 백엔드 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개를 열어 각각 실행한다:

# 터미널 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/* 공통 모듈 (컴포넌트, 훅, 서비스, 스토어)
@components/* src/components/* 탭별 패키지 (11개 탭)
import { useAuth } from '@common/hooks/useAuth';
import OilSpillView from '@components/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

브랜치 생성

# 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
└── components/         탭 단위 패키지 (11개, MPA 컴포넌트 구조)
    ├── prediction/      확산 예측
    ├── hns/             HNS 분석
    ├── rescue/          구조 시나리오
    ├── aerial/          항공 방제
    ├── weather/         해양 기상
    ├── incidents/       사건/사고 관리
    ├── board/           게시판
    ├── reports/         보고서
    ├── assets/          자산 관리
    ├── scat/            Pre-SCAT 조사
    └── admin/           관리자

각 탭 내부 구조 패턴:

components/{탭명}/
├── 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          시드 데이터

라우터 작성 패턴:

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)

-- 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 타입 체크

# Frontend
cd frontend && npx tsc --noEmit

# Backend
cd backend && npx tsc --noEmit

6-2. ESLint

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

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

6-3. Prettier (선택)

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

6-4. 빌드 검증

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

# 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로 확인
# 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. 커밋 & 푸시 예시

# 변경 파일 확인
git status

# 스테이징 (파일 지정)
git add frontend/src/components/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):

# 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 본문 템플릿

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

## 변경 파일
- `frontend/src/components/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. 머지 후 로컬 동기화

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을 생성한다.

## 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. 배포 확인

# 프론트엔드 응답 확인
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.tsproxy 설정 확인

타입 에러:

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

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

상태 관리 디버깅:

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

10-2. Backend

DB 연결 실패:

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

# 방화벽 확인
nc -zv 211.208.115.83 5432

API 디버깅:

# 헬스 체크
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. 공통

포트 충돌:

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

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

node_modules 문제:

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

Git Hooks가 동작하지 않을 때:

# 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/components/incidents/services/incidentsApi.ts API 호출 함수
frontend/src/components/incidents/components/IncidentDetailView.tsx 상세 뷰 컴포넌트

Step 2. 브랜치 생성

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

Step 3. 개발

Backend - Service:

// 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:

// 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:

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

Frontend - Component:

// frontend/src/components/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. 검증

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

Step 5. 커밋 & 푸시

git add backend/src/incidents/ frontend/src/components/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. 릴리즈 & 배포

Step 8. 로컬 동기화

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