# 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) ### 백엔드 #### 미들웨어 ```typescript 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`에 담기는 정보: ```typescript interface JwtPayload { sub: string // 사용자 UUID (USER_ID) acnt: string // 계정명 (USER_ACNT) name: string // 사용자명 (USER_NM) roles: string[] // 역할 코드 목록 (ADMIN, MANAGER, USER, VIEWER) } ``` #### 라우터 패턴 (CRUD 구조) ```typescript // 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) ```typescript 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 클라이언트 ```typescript 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.tsx`의 `useEffect`에서 `activeMainTab` 변경을 감지하여 `navigator.sendBeacon`으로 자동 전송합니다. 개별 탭 개발자는 별도 작업이 필요 없습니다. ```typescript // 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]) ``` ### 수동 기록 (향후 확장) 특정 작업에 대해 명시적으로 감사 로그를 기록하려면: ```typescript 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 ```typescript // 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) ```typescript // 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.ts`의 `DEFAULT_MENU_CONFIG` - API: `GET/PUT /api/menus` --- ## 4. API 통신 패턴 ### Axios 인스턴스 설정 ```typescript // 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 서비스 작성 패턴 ```typescript // frontend/src/services/newService.ts import { api } from './api' export interface MyData { id: string name: string } export async function fetchMyData(): Promise { const response = await api.get('/my-endpoint') return response.data } export async function createMyData(data: Omit): Promise { const response = await api.post('/my-endpoint', data) return response.data } ``` ### 에러 처리 - 401 응답: `api.ts` 인터셉터가 자동으로 로그아웃 처리 - 비즈니스 에러: `response.data.error` 메시지로 사용자에게 안내 - 백엔드에서 `AuthError` 사용 시 적절한 HTTP 상태 코드와 메시지 반환 --- ## 5. 상태 관리 ### Zustand (클라이언트 상태) ```typescript // frontend/src/store/newStore.ts import { create } from 'zustand' interface MyState { items: string[] addItem: (item: string) => void } export const useMyStore = create((set) => ({ items: [], addItem: (item) => set((state) => ({ items: [...state.items, item] })), })) ``` ### TanStack Query (서버 상태) — 권장 ```typescript 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](./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')` | ### 라우터 작성 예시 ```typescript // 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')` 유지: ```typescript 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`에 라우터 등록: ```typescript import newRouter from './[모듈명]/[모듈명]Router.js' app.use('/api/[경로]', newRouter) ``` 5. DB 테이블 필요 시 `database/auth_init.sql`에 DDL 추가 6. 리소스 코드를 `AUTH_PERM_TREE`에 등록 (마이그레이션 SQL) ### DB 접근 ```typescript // 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) 추가 ```