wing-ops/docs/COMMON-GUIDE.md
htlee ffde4d6694 chore: 팀 워크플로우 v1.4.0 동기화 + 문서 갱신
- 에이전트 파일 YAML frontmatter 형식 갱신 (explorer, implementer, reviewer)
- subagent-policy.md 규칙 추가
- commit-msg hook 패턴 간소화
- COMMON-GUIDE.md API 연동 가이드 보강
- MOCK-TO-API-GUIDE.md mock→API 전환 가이드 추가

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-28 21:36:35 +09:00

17 KiB
Raw Blame 히스토리

WING-OPS 공통 로직 개발 가이드

개별 탭 개발자가 공통 영역 구현을 참조하여 연동할 수 있도록 정리한 문서입니다. 공통 기능을 추가/변경할 때 반드시 이 문서를 최신화하세요.


1. 인증/인가

개요

JWT 기반 세션 인증. HttpOnly 쿠키(WING_SESSION)로 토큰을 관리하며, 프론트엔드에서는 Zustand authStore로 상태를 관리합니다.

권한 모델: 리소스 × 오퍼레이션 (RBAC)

2차원 권한 모델: 리소스 트리(상속) × 오퍼레이션(RCUD, 플랫)

AUTH_PERM 테이블: (ROLE_SN, RSRC_CD, OPER_CD, GRANT_YN)

리소스 트리 (AUTH_PERM_TREE)       오퍼레이션 (플랫)
├── prediction                     READ   = 조회/열람
│   ├── prediction:analysis        CREATE = 생성
│   ├── prediction:list            UPDATE = 수정
│   └── prediction:theory          DELETE = 삭제
├── board
│   ├── board:notice
│   └── board:data
└── admin
    ├── admin:users
    └── admin:permissions

오퍼레이션 코드

OPER_CD 설명 비고
READ 조회/열람 목록, 상세 조회
CREATE 생성 새 데이터 등록
UPDATE 수정 기존 데이터 변경
DELETE 삭제 데이터 삭제
MANAGE 관리 관리자 설정 (확장용)
EXPORT 내보내기 다운로드/출력 (확장용)

상속 규칙

  1. 부모 리소스의 READ가 N → 자식의 모든 오퍼레이션 강제 N (접근 자체 차단)
  2. 해당 (RSRC_CD, OPER_CD) 명시적 레코드 있으면 → 그 값 사용
  3. 명시적 레코드 없으면 → 부모의 같은 OPER_CD 상속
  4. 최상위까지 없으면 → 기본 N (거부)
예시: board (READ:Y, CREATE:Y, UPDATE:Y, DELETE:N)
└── board:notice
    ├── READ:    상속 Y (부모 READ Y)
    ├── CREATE:  상속 Y (부모 CREATE Y)
    ├── UPDATE:  명시적 N (override 가능)
    └── DELETE:  상속 N (부모 DELETE N)

키 구분자

  • 리소스 내부 경로: : (board:notice)
  • 리소스-오퍼레이션 결합 (내부용): :: (board:notice::READ)

백엔드

미들웨어

import { requireAuth, requireRole, requirePermission } from '../auth/authMiddleware.js'

// 인증만 필요한 라우트
router.use(requireAuth)

// 역할 기반 (관리 API용)
router.use(requireRole('ADMIN'))

// 리소스×오퍼레이션 기반 (일반 비즈니스 API용)
router.post('/notice/list',   requirePermission('board:notice', 'READ'),   handler)
router.post('/notice/create', requirePermission('board:notice', 'CREATE'), handler)
router.post('/notice/update', requirePermission('board:notice', 'UPDATE'), handler)
router.post('/notice/delete', requirePermission('board:notice', 'DELETE'), handler)

requirePermission은 요청당 1회만 DB 조회하고 req.resolvedPermissions에 캐싱합니다.

JWT 페이로드 (req.user)

requireAuth 통과 후 req.user에 담기는 정보:

interface JwtPayload {
  sub: string    // 사용자 UUID (USER_ID)
  acnt: string   // 계정명 (USER_ACNT)
  name: string   // 사용자명 (USER_NM)
  roles: string[] // 역할 코드 목록 (ADMIN, MANAGER, USER, VIEWER)
}

라우터 패턴 (CRUD 구조)

// backend/src/[모듈]/[모듈]Router.ts
import { Router } from 'express'
import { requireAuth, requirePermission } from '../auth/authMiddleware.js'

const router = Router()
router.use(requireAuth)

// 리소스별 CRUD 엔드포인트
router.post('/list',   requirePermission('module:sub', 'READ'),   listHandler)
router.post('/detail', requirePermission('module:sub', 'READ'),   detailHandler)
router.post('/create', requirePermission('module:sub', 'CREATE'), createHandler)
router.post('/update', requirePermission('module:sub', 'UPDATE'), updateHandler)
router.post('/delete', requirePermission('module:sub', 'DELETE'), deleteHandler)

export default router

프론트엔드

authStore (Zustand)

import { useAuthStore } from '@common/store/authStore'

const { user, isAuthenticated, hasPermission, logout } = useAuthStore()

// 사용자 정보
user?.id       // UUID
user?.name     // 이름
user?.roles    // ['ADMIN', 'USER']
user?.permissions  // { 'prediction': ['READ','CREATE','UPDATE','DELETE'], ... }

// 권한 확인 (리소스 × 오퍼레이션)
hasPermission('prediction')              // READ 확인 (기본값)
hasPermission('prediction', 'READ')      // 명시적 READ 확인
hasPermission('board:notice', 'CREATE')  // 공지사항 생성 권한
hasPermission('board:notice', 'DELETE')  // 공지사항 삭제 권한

// 하위 호환: operation 생략 시 'READ' 기본값
hasPermission('admin')  // === hasPermission('admin', 'READ')

API 클라이언트

import { api } from '@common/services/api'

// withCredentials: true 설정으로 JWT 쿠키 자동 포함
const response = await api.post('/your-endpoint/list', params)
const response = await api.post('/your-endpoint/create', data)

// 401 응답 시 자동 로그아웃 처리 (인터셉터)
// 403 응답 시 권한 부족 (requirePermission 미들웨어)

2. 감사 로그 (Audit Log)

개요

사용자 행동을 추적하는 감사 로그 시스템. 현재 탭 이동 로그를 자동 기록하며, 향후 API 호출 로깅으로 확장 가능합니다.

자동 기록 (탭 이동)

App.tsxuseEffect에서 activeMainTab 변경을 감지하여 navigator.sendBeacon으로 자동 전송합니다. 개별 탭 개발자는 별도 작업이 필요 없습니다.

// frontend/src/App.tsx (자동 적용, 수정 불필요)
import { API_BASE_URL } from '@common/services/api'

useEffect(() => {
  if (!isAuthenticated) return
  const blob = new Blob(
    [JSON.stringify({ action: 'TAB_VIEW', detail: activeMainTab })],
    { type: 'text/plain' }
  )
  navigator.sendBeacon(`${API_BASE_URL}/audit/log`, blob)
}, [activeMainTab, isAuthenticated])

수동 기록 (향후 확장)

특정 작업에 대해 명시적으로 감사 로그를 기록하려면:

import { API_BASE_URL } from '@common/services/api'

const blob = new Blob(
  [JSON.stringify({ action: 'ADMIN_ACTION', detail: '사용자 승인' })],
  { type: 'text/plain' }
)
navigator.sendBeacon(`${API_BASE_URL}/audit/log`, blob)

감사 로그 테이블 구조 (AUTH_AUDIT_LOG)

컬럼 타입 용도 현재 사용
LOG_SN SERIAL PK 로그 순번 O
USER_ID UUID 사용자 ID O
ACTION_CD VARCHAR(30) 액션 코드 O (TAB_VIEW)
ACTION_DTL VARCHAR(100) 액션 상세 (탭ID 등) O
HTTP_METHOD VARCHAR(10) GET/POST/PUT/DELETE - (향후)
CRUD_TYPE VARCHAR(10) SELECT/INSERT/UPDATE/DELETE - (향후)
REQ_URL VARCHAR(500) 요청 URL - (향후)
REQ_DTM TIMESTAMPTZ 요청 시각 O
RES_DTM TIMESTAMPTZ 응답 완료 시각 - (향후)
RES_STATUS SMALLINT HTTP 상태 코드 - (향후)
RES_SIZE INTEGER 응답 데이터 크기(bytes) - (향후)
IP_ADDR VARCHAR(45) 클라이언트 IP O
USER_AGENT VARCHAR(500) 브라우저 정보 O
EXTRA JSONB 추가 메타데이터 - (향후)

ACTION_CD 코드 체계

코드 설명
TAB_VIEW 상단 탭 이동
API_CALL API 호출 (향후)
LOGIN 로그인 (향후)
LOGOUT 로그아웃 (향후)
ADMIN_ACTION 관리자 작업 (향후)

관리자 조회 API

// frontend/src/services/authApi.ts
import { fetchAuditLogs } from '../services/authApi'

const result = await fetchAuditLogs({
  page: 1,
  size: 50,
  actionCd: 'TAB_VIEW',
  from: '2026-02-28',
  to: '2026-02-28',
})
// result: { items: AuditLogItem[], total: number, page: number, size: number }

3. 메뉴 시스템

개요

DB 기반 동적 메뉴 구성. 관리자가 메뉴 표시 여부/순서를 설정하면 모든 사용자에게 반영됩니다. 새 메뉴 탭 추가 시 docs/MENU-TAB-GUIDE.md를 참조하세요.

메뉴 상태 (menuStore)

// frontend/src/store/menuStore.ts
import { useMenuStore } from '../store/menuStore'

const { menus, loadMenuConfig } = useMenuStore()

// menus: MenuConfigItem[] — 활성화되고 정렬된 메뉴 목록
// menus[0].id → 'prediction'
// menus[0].label → '유출유 확산예측'
// menus[0].enabled → true

메뉴 설정 저장소

  • DB: AUTH_SETTING 테이블의 menu.config 키 (JSON 배열)
  • 백엔드: backend/src/settings/settingsService.tsDEFAULT_MENU_CONFIG
  • API: GET/PUT /api/menus

4. API 통신 패턴

Axios 인스턴스 설정

// frontend/src/services/api.ts
export const api = axios.create({
  baseURL: import.meta.env.VITE_API_URL || 'http://localhost:3001/api',
  withCredentials: true,  // JWT 쿠키 자동 포함
  timeout: 30000,
})

새 API 서비스 작성 패턴

// frontend/src/services/newService.ts
import { api } from './api'

export interface MyData {
  id: string
  name: string
}

export async function fetchMyData(): Promise<MyData[]> {
  const response = await api.get<MyData[]>('/my-endpoint')
  return response.data
}

export async function createMyData(data: Omit<MyData, 'id'>): Promise<MyData> {
  const response = await api.post<MyData>('/my-endpoint', data)
  return response.data
}

에러 처리

  • 401 응답: api.ts 인터셉터가 자동으로 로그아웃 처리
  • 비즈니스 에러: response.data.error 메시지로 사용자에게 안내
  • 백엔드에서 AuthError 사용 시 적절한 HTTP 상태 코드와 메시지 반환

5. 상태 관리

Zustand (클라이언트 상태)

// frontend/src/store/newStore.ts
import { create } from 'zustand'

interface MyState {
  items: string[]
  addItem: (item: string) => void
}

export const useMyStore = create<MyState>((set) => ({
  items: [],
  addItem: (item) => set((state) => ({ items: [...state.items, item] })),
}))

TanStack Query (서버 상태) — 권장

import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'
import { fetchMyData, createMyData } from '../services/newService'

// 조회
const { data, isLoading } = useQuery({
  queryKey: ['myData'],
  queryFn: fetchMyData,
})

// 생성/수정
const queryClient = useQueryClient()
const mutation = useMutation({
  mutationFn: createMyData,
  onSuccess: () => queryClient.invalidateQueries({ queryKey: ['myData'] }),
})

6. 백엔드 API CRUD 규칙

상세 가이드 + 게시판 실전 튜토리얼: CRUD-API-GUIDE.md 참조

HTTP Method 정책 (보안 가이드 준수)

  • 보안 취약점 점검 가이드에 따라 POST 메서드를 기본으로 사용한다.
  • GET은 단순 조회 중 민감하지 않은 경우에만 허용 (필요 시 POST로 전환).
  • PUT, DELETE, PATCH 등 기타 메서드는 사용하지 않는다.

오퍼레이션 기반 권한 미들웨어

OPER_CD는 HTTP Method가 아닌 비즈니스 의미로 결정한다. requirePermission 미들웨어에 명시적으로 오퍼레이션을 지정한다.

URL 패턴 OPER_CD 미들웨어
/resource/list READ requirePermission(resource, 'READ')
/resource/detail READ requirePermission(resource, 'READ')
/resource/create CREATE requirePermission(resource, 'CREATE')
/resource/update UPDATE requirePermission(resource, 'UPDATE')
/resource/delete DELETE requirePermission(resource, 'DELETE')

라우터 작성 예시

// backend/src/board/noticeRouter.ts
import { Router } from 'express'
import { requireAuth, requirePermission } from '../auth/authMiddleware.js'

const router = Router()
router.use(requireAuth)

// 조회
router.post('/list',    requirePermission('board:notice', 'READ'),   listHandler)
router.post('/detail',  requirePermission('board:notice', 'READ'),   detailHandler)

// 생성/수정/삭제
router.post('/create',  requirePermission('board:notice', 'CREATE'), createHandler)
router.post('/update',  requirePermission('board:notice', 'UPDATE'), updateHandler)
router.post('/delete',  requirePermission('board:notice', 'DELETE'), deleteHandler)

export default router

관리 API (예외)

사용자/역할/설정 등 관리 API는 requireRole('ADMIN') 유지:

router.use(requireAuth)
router.use(requireRole('ADMIN'))

7. 백엔드 모듈 추가 절차

새 백엔드 모듈을 추가할 때:

  1. backend/src/[모듈명]/ 디렉토리 생성
  2. [모듈명]Service.ts — 비즈니스 로직 (DB 쿼리)
  3. [모듈명]Router.ts — Express 라우터 (CRUD 엔드포인트 + requirePermission)
  4. backend/src/server.ts에 라우터 등록:
    import newRouter from './[모듈명]/[모듈명]Router.js'
    app.use('/api/[경로]', newRouter)
    
  5. DB 테이블 필요 시 database/auth_init.sql에 DDL 추가
  6. 리소스 코드를 AUTH_PERM_TREE에 등록 (마이그레이션 SQL)

DB 접근

// PostgreSQL — wing DB (운영 데이터: 레이어, 사고, 예측 등)
import { wingPool } from '../db/wingDb.js'
const result = await wingPool.query('SELECT * FROM LAYER WHERE LAYER_CD = $1', [id])

// PostgreSQL — wing_auth DB (인증 데이터: 사용자, 역할, 권한 등)
import { authPool } from '../db/authDb.js'
const result = await authPool.query('SELECT * FROM AUTH_USER WHERE USER_ID = $1', [id])

8. Mock → API 전환 가이드

각 탭의 mock 데이터를 DB/API로 전환하는 프로세스는 MOCK-TO-API-GUIDE.md 참조.

전환 완료 탭

MR API 경로 비고
Board (게시판) MR#29 /api/board PUT/DELETE 사용 (레거시, POST 전환 예정)
Reports (보고서) MR#31 /api/reports GET/POST only 적용

Reports API 엔드포인트

Method Path 설명 권한
GET /api/reports/templates 템플릿 목록 + 섹션 정의 requireAuth
GET /api/reports/categories 분석 카테고리 목록 + 섹션 requireAuth
GET /api/reports 보고서 목록 (필터: jrsdCd, tmplCd, sttsCd, search) reports READ
GET /api/reports/:sn 보고서 상세 (섹션 데이터 포함) reports READ
POST /api/reports 보고서 생성 reports CREATE
POST /api/reports/:sn/update 보고서 수정 reports UPDATE
POST /api/reports/:sn/delete 보고서 삭제 (논리) reports DELETE
POST /api/reports/:sn/sections/:sectCd 개별 섹션 수정 reports UPDATE

프론트엔드 API 서비스

// frontend/src/tabs/reports/services/reportsApi.ts
import { api } from '@common/services/api'

// 조회 (GET)
const templates = await fetchTemplates()       // GET /reports/templates (캐싱)
const categories = await fetchCategories()     // GET /reports/categories (캐싱)
const list = await fetchReports({ tmplCd, sttsCd })  // GET /reports
const detail = await fetchReport(sn)           // GET /reports/:sn

// 생성/수정/삭제 (POST)
await createReportApi({ tmplSn, title, sections })    // POST /reports
await updateReportApi(sn, { title, sections })         // POST /reports/:sn/update
await deleteReportApi(sn)                              // POST /reports/:sn/delete

// 고수준 함수 (OilSpillReportData ↔ API 변환 포함)
await saveReport(reportData)                   // create 또는 update 자동 분기
const reports = await loadReportsFromApi()     // 전체 목록 + 변환
const detail = await loadReportDetail(sn)      // 상세 + 섹션 복원

파일 구조 요약

frontend/src/
├── common/
│   ├── services/api.ts          Axios 인스턴스 + API_BASE_URL + 인터셉터
│   ├── services/authApi.ts      인증/사용자/역할/설정/메뉴/감사로그 API
│   ├── store/authStore.ts       인증 상태 + hasPermission (Zustand)
│   ├── store/menuStore.ts       메뉴 상태 (Zustand)
│   └── hooks/                   useSubMenu, useFeatureTracking 등
├── tabs/                        탭별 패키지 (11개)
└── App.tsx                      탭 라우팅 + 감사 로그 자동 기록

backend/src/
├── auth/                    인증 (JWT, OAuth, 미들웨어, requirePermission)
├── users/                   사용자 관리
├── roles/                   역할/권한 관리 (permResolver, roleService)
├── board/                   게시판 CRUD (boardService, boardRouter)
├── reports/                 보고서 CRUD (reportsService, reportsRouter)
├── settings/                시스템 설정
├── menus/                   메뉴 설정
├── audit/                   감사 로그
├── db/                      DB 연결 (authDb, wingDb)
├── middleware/               보안 미들웨어
└── server.ts                Express 진입점 + 라우터 등록

database/
├── auth_init.sql            인증 DB DDL + 초기 데이터
├── init.sql                 운영 DB DDL
└── migration/               마이그레이션 스크립트
    ├── 003_perm_tree.sql    리소스 트리 (AUTH_PERM_TREE)
    ├── 004_oper_cd.sql      오퍼레이션 코드 (OPER_CD) 추가
    ├── 006_board.sql        게시판 (BOARD_POST)
    └── 007_reports.sql      보고서 (REPORT_TMPL, REPORT, REPORT_SECT_DATA 등 7개)