wing-ops/docs/COMMON-GUIDE.md
2026-02-28 12:24:20 +09:00

9.3 KiB

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

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


1. 인증/인가

개요

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

백엔드

미들웨어 적용

// backend/src/auth/authMiddleware.ts
import { requireAuth, requireRole } from '../auth/authMiddleware.js'

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

// 특정 역할 필요
router.use(requireRole('ADMIN'))
router.use(requireRole('ADMIN', 'MANAGER'))

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)
}

라우터 패턴

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

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

router.get('/', async (req, res) => {
  try {
    const userId = req.user!.sub
    // 비즈니스 로직...
    res.json(result)
  } catch (err) {
    console.error('[모듈] 오류:', err)
    res.status(500).json({ error: '처리 중 오류가 발생했습니다.' })
  }
})

export default router

프론트엔드

authStore (Zustand)

// frontend/src/store/authStore.ts
import { useAuthStore } from '../store/authStore'

// 컴포넌트 내에서 사용
const { user, isAuthenticated, hasPermission, logout } = useAuthStore()

// 사용자 정보
user?.id       // UUID
user?.name     // 이름
user?.roles    // ['ADMIN', 'USER']

// 권한 확인 (탭 ID 기준)
hasPermission('prediction') // true/false
hasPermission('admin')      // true/false

API 클라이언트

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

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

// 401 응답 시 자동 로그아웃 처리 (인터셉터)

2. 감사 로그 (Audit Log)

개요

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

자동 기록 (탭 이동)

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

// frontend/src/App.tsx (자동 적용, 수정 불필요)
useEffect(() => {
  if (!isAuthenticated) return
  const blob = new Blob(
    [JSON.stringify({ action: 'TAB_VIEW', detail: activeMainTab })],
    { type: 'text/plain' }
  )
  navigator.sendBeacon('/api/audit/log', blob)
}, [activeMainTab, isAuthenticated])

수동 기록 (향후 확장)

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

// 프론트엔드에서 sendBeacon 사용
const blob = new Blob(
  [JSON.stringify({ action: 'ADMIN_ACTION', detail: '사용자 승인' })],
  { type: 'text/plain' }
)
navigator.sendBeacon('/api/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. 백엔드 모듈 추가 절차

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

  1. backend/src/[모듈명]/ 디렉토리 생성
  2. [모듈명]Service.ts — 비즈니스 로직 (DB 쿼리)
  3. [모듈명]Router.ts — Express 라우터 (입력 검증, 에러 처리)
  4. backend/src/server.ts에 라우터 등록:
    import newRouter from './[모듈명]/[모듈명]Router.js'
    app.use('/api/[경로]', newRouter)
    
  5. DB 테이블 필요 시 database/auth_init.sql에 DDL 추가

DB 접근

// PostgreSQL (인증 DB)
import { authPool } from '../db/authDb.js'
const result = await authPool.query('SELECT * FROM TABLE WHERE id = $1', [id])

// SQLite (레이어 DB)
import { getDb } from '../db/database.js'
const db = getDb()
const rows = db.prepare('SELECT * FROM table').all()

파일 구조 요약

frontend/src/
├── services/api.ts          Axios 인스턴스 + 인터셉터
├── services/authApi.ts      인증/사용자/역할/설정/메뉴/감사로그 API
├── store/authStore.ts       인증 상태 (Zustand)
├── store/menuStore.ts       메뉴 상태 (Zustand)
└── App.tsx                  탭 라우팅 + 감사 로그 자동 기록

backend/src/
├── auth/                    인증 (JWT, OAuth, 미들웨어)
├── users/                   사용자 관리
├── roles/                   역할/권한 관리
├── settings/                시스템 설정
├── menus/                   메뉴 설정
├── audit/                   감사 로그
├── db/                      DB 연결 (authDb, database)
├── middleware/               보안 미들웨어
└── server.ts                Express 진입점 + 라우터 등록