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

448 lines
15 KiB
Markdown
Raw Blame 히스토리

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 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<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 (클라이언트 상태)
```typescript
// 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 (서버 상태) — 권장
```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 규칙
### 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) 추가
```