wing-ops/docs/COMMON-GUIDE.md
htlee 8657190578 feat(auth): RBAC 오퍼레이션 기반 2차원 권한 시스템 구현
리소스 가시성(READ/HIDE) 단일 차원에서 리소스 × 오퍼레이션(RCUD) 2차원
권한 모델로 전환하여 세밀한 CRUD 권한 제어 지원.

- DB: AUTH_PERM에 OPER_CD 컬럼 추가, 마이그레이션 004 작성
- DB: AUTH_PERM_TREE 리소스 트리 테이블 추가 (마이그레이션 003)
- Backend: permResolver 2차원 권한 해석 엔진 (상속 + 오퍼레이션)
- Backend: requirePermission 미들웨어 신규 (리소스×오퍼레이션 검증)
- Backend: authService permissions → Record<string, string[]> 반환
- Backend: roleService/roleRouter OPER_CD 지원 API
- Backend: Helmet CORP 설정 (sendBeacon cross-origin 허용)
- Frontend: authStore.hasPermission(resource, operation?) 하위 호환 확장
- Frontend: PermissionsPanel 역할탭 + RCUD 4열 매트릭스 UI 전면 재작성
- Frontend: sendBeacon API_BASE_URL 절대경로 전환
- Docs: COMMON-GUIDE 권한 체계 + CRUD API 규칙 문서화

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-28 17:55:06 +09:00

15 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 규칙

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

파일 구조 요약

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)
├── 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) 추가