# 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=" 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 " \ -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`이 자동 허용된다. **타입 에러:** ```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 ``` **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
Loading...
; // ...렌더링 }; ``` #### 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 ```