Phase 6(MapLibre+deck.gl), CSS 리팩토링, RBAC, 10탭 API 전환 등 현재 시스템 상태를 정확히 반영하여 모든 문서를 처음부터 재작성. - README.md: 기술 스택(MapLibre+deck.gl), 빌드, 구조, 스킬 갱신 - CLAUDE.md: CSS @layer, RBAC, HTTP 정책, 백엔드 모듈 반영 - docs/README.md: 아키텍처 상세 (3-Layer, 인증, 권한, CSS) - docs/DEVELOPMENT-GUIDE.md: 워크플로우 전체 흐름 + 실전 예시 - docs/INSTALL_GUIDE.md: 온라인/오프라인 설치 매뉴얼 - docs/COMMON-GUIDE.md: 공통 로직 9개 섹션 (인증~CSS) - docs/MENU-TAB-GUIDE.md: 새 탭 추가 5단계 + 예시 - docs/CRUD-API-GUIDE.md: End-to-End CRUD API 패턴 - docs/MOCK-TO-API-GUIDE.md: Mock→API 전환 10단계 프로세스 - docs/_backup_20260301/: 기존 문서 백업 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
17 KiB
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 |
내보내기 | 다운로드/출력 (확장용) |
상속 규칙
- 부모 리소스의 READ가 N → 자식의 모든 오퍼레이션 강제 N (접근 자체 차단)
- 해당
(RSRC_CD, OPER_CD)명시적 레코드 있으면 → 그 값 사용 - 명시적 레코드 없으면 → 부모의 같은 OPER_CD 상속
- 최상위까지 없으면 → 기본 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.tsx의 useEffect에서 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.ts의DEFAULT_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. 백엔드 모듈 추가 절차
새 백엔드 모듈을 추가할 때:
backend/src/[모듈명]/디렉토리 생성[모듈명]Service.ts— 비즈니스 로직 (DB 쿼리)[모듈명]Router.ts— Express 라우터 (CRUD 엔드포인트 + requirePermission)backend/src/server.ts에 라우터 등록:import newRouter from './[모듈명]/[모듈명]Router.js' app.use('/api/[경로]', newRouter)- DB 테이블 필요 시
database/auth_init.sql에 DDL 추가 - 리소스 코드를
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개)