diff --git a/CLAUDE.md b/CLAUDE.md index a9bcc80..62b880a 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -90,6 +90,11 @@ wing/ - `naming.md` — 네이밍 규칙 - `testing.md` — 테스트 규칙 +## 공통 기능 문서 +- `docs/COMMON-GUIDE.md` — 공통 로직 개발 가이드 (인증, 감사로그, 메뉴, API 등) +- 공통 기능(인증, 감사로그, 메뉴 시스템, API 통신 등)을 추가/변경할 때 반드시 `docs/COMMON-GUIDE.md`를 최신화할 것 +- 개별 탭 개발자는 이 문서를 참조하여 공통 영역과의 연동을 구현 + ## 환경 설정 - Node.js 20 (`.node-version`, fnm 사용) - npm registry: Nexus proxy (`.npmrc`) diff --git a/backend/src/audit/auditRouter.ts b/backend/src/audit/auditRouter.ts new file mode 100644 index 0000000..6f8668b --- /dev/null +++ b/backend/src/audit/auditRouter.ts @@ -0,0 +1,74 @@ +import { Router } from 'express' +import { requireAuth, requireRole } from '../auth/authMiddleware.js' +import { insertAuditLog, listAuditLogs } from './auditService.js' + +const router = Router() + +// POST /api/audit/log — sendBeacon 수신 (탭 이동 등 클라이언트 감사 로그) +router.post('/log', requireAuth, async (req, res) => { + try { + // sendBeacon은 Content-Type: text/plain으로 전송하므로 body를 직접 파싱 + let body = req.body + if (typeof body === 'string') { + try { + body = JSON.parse(body) + } catch { + res.status(400).json({ error: '잘못된 요청 형식입니다.' }) + return + } + } + + const { action, detail } = body as { action?: string; detail?: string } + + if (!action) { + res.status(400).json({ error: 'action 필드는 필수입니다.' }) + return + } + + const ipAddr = (req.headers['x-forwarded-for'] as string)?.split(',')[0]?.trim() || req.ip || '' + const userAgent = req.headers['user-agent'] || '' + + await insertAuditLog({ + userId: req.user!.sub, + actionCd: action, + actionDtl: detail, + ipAddr, + userAgent, + }) + + res.json({ success: true }) + } catch (err) { + console.error('[audit] 감사 로그 기록 오류:', err) + res.status(500).json({ error: '감사 로그 기록 중 오류가 발생했습니다.' }) + } +}) + +// GET /api/audit/logs — 관리자용 감사 로그 조회 +router.get('/logs', requireAuth, requireRole('ADMIN'), async (req, res) => { + try { + const { page, size, userId, actionCd, from, to } = req.query as { + page?: string + size?: string + userId?: string + actionCd?: string + from?: string + to?: string + } + + const result = await listAuditLogs({ + page: page ? parseInt(page, 10) : undefined, + size: size ? parseInt(size, 10) : undefined, + userId, + actionCd, + from, + to, + }) + + res.json(result) + } catch (err) { + console.error('[audit] 감사 로그 조회 오류:', err) + res.status(500).json({ error: '감사 로그 조회 중 오류가 발생했습니다.' }) + } +}) + +export default router diff --git a/backend/src/audit/auditService.ts b/backend/src/audit/auditService.ts new file mode 100644 index 0000000..3915ad9 --- /dev/null +++ b/backend/src/audit/auditService.ts @@ -0,0 +1,138 @@ +import { authPool } from '../db/authDb.js' + +interface InsertAuditLogInput { + userId: string + actionCd: string + actionDtl?: string + httpMethod?: string + crudType?: string + reqUrl?: string + resStatus?: number + resSize?: number + ipAddr?: string + userAgent?: string + extra?: Record +} + +interface AuditLogItem { + logSn: number + userId: string + userName: string | null + userAccount: string | null + actionCd: string + actionDtl: string | null + httpMethod: string | null + crudType: string | null + reqUrl: string | null + reqDtm: string + resDtm: string | null + resStatus: number | null + resSize: number | null + ipAddr: string | null + userAgent: string | null + extra: Record | null +} + +interface AuditLogListResult { + items: AuditLogItem[] + total: number + page: number + size: number +} + +export async function insertAuditLog(input: InsertAuditLogInput): Promise { + await authPool.query( + `INSERT INTO AUTH_AUDIT_LOG + (USER_ID, ACTION_CD, ACTION_DTL, HTTP_METHOD, CRUD_TYPE, REQ_URL, + RES_STATUS, RES_SIZE, IP_ADDR, USER_AGENT, EXTRA) + VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11)`, + [ + input.userId, + input.actionCd, + input.actionDtl || null, + input.httpMethod || null, + input.crudType || null, + input.reqUrl || null, + input.resStatus || null, + input.resSize || null, + input.ipAddr || null, + input.userAgent || null, + input.extra ? JSON.stringify(input.extra) : null, + ] + ) +} + +export async function listAuditLogs(filters?: { + page?: number + size?: number + userId?: string + actionCd?: string + from?: string + to?: string +}): Promise { + const page = filters?.page || 1 + const size = Math.min(filters?.size || 50, 200) + const offset = (page - 1) * size + + let whereClause = 'WHERE 1=1' + const params: (string | number)[] = [] + let paramIdx = 1 + + if (filters?.userId) { + whereClause += ` AND a.USER_ID = $${paramIdx++}` + params.push(filters.userId) + } + if (filters?.actionCd) { + whereClause += ` AND a.ACTION_CD = $${paramIdx++}` + params.push(filters.actionCd) + } + if (filters?.from) { + whereClause += ` AND a.REQ_DTM >= $${paramIdx++}` + params.push(filters.from) + } + if (filters?.to) { + whereClause += ` AND a.REQ_DTM <= $${paramIdx++}` + params.push(filters.to) + } + + const countResult = await authPool.query( + `SELECT COUNT(*) as cnt FROM AUTH_AUDIT_LOG a ${whereClause}`, + params + ) + const total = parseInt(countResult.rows[0].cnt, 10) + + const dataParams = [...params, size, offset] + const result = await authPool.query( + `SELECT a.LOG_SN, a.USER_ID, u.USER_NM, u.USER_ACNT, + a.ACTION_CD, a.ACTION_DTL, a.HTTP_METHOD, a.CRUD_TYPE, + a.REQ_URL, a.REQ_DTM, a.RES_DTM, a.RES_STATUS, a.RES_SIZE, + a.IP_ADDR, a.USER_AGENT, a.EXTRA + FROM AUTH_AUDIT_LOG a + LEFT JOIN AUTH_USER u ON a.USER_ID = u.USER_ID + ${whereClause} + ORDER BY a.REQ_DTM DESC + LIMIT $${paramIdx++} OFFSET $${paramIdx}`, + dataParams + ) + + const items: AuditLogItem[] = result.rows.map((row: Record) => ({ + logSn: row.log_sn as number, + userId: row.user_id as string, + userName: row.user_nm as string | null, + userAccount: row.user_acnt as string | null, + actionCd: row.action_cd as string, + actionDtl: row.action_dtl as string | null, + httpMethod: row.http_method as string | null, + crudType: row.crud_type as string | null, + reqUrl: row.req_url as string | null, + reqDtm: row.req_dtm as string, + resDtm: row.res_dtm as string | null, + resStatus: row.res_status as number | null, + resSize: row.res_size as number | null, + ipAddr: row.ip_addr as string | null, + userAgent: row.user_agent as string | null, + extra: row.extra as Record | null, + })) + + return { items, total, page, size } +} diff --git a/backend/src/server.ts b/backend/src/server.ts index 63af491..8d312c9 100755 --- a/backend/src/server.ts +++ b/backend/src/server.ts @@ -13,6 +13,7 @@ import userRouter from './users/userRouter.js' import roleRouter from './roles/roleRouter.js' import settingsRouter from './settings/settingsRouter.js' import menuRouter from './menus/menuRouter.js' +import auditRouter from './audit/auditRouter.js' import { sanitizeBody, sanitizeQuery, @@ -105,6 +106,7 @@ app.use(cookieParser()) // 6. JSON 본문 파서 (크기 제한 적용) app.use(express.json({ limit: BODY_SIZE_LIMIT })) +app.use(express.text({ limit: BODY_SIZE_LIMIT })) app.use(express.urlencoded({ extended: false, limit: BODY_SIZE_LIMIT })) // 7. 입력값 살균 미들웨어 @@ -135,6 +137,7 @@ app.use('/api/users', userRouter) app.use('/api/roles', roleRouter) app.use('/api/settings', settingsRouter) app.use('/api/menus', menuRouter) +app.use('/api/audit', auditRouter) // API 라우트 — 업무 app.use('/api/layers', layersRouter) diff --git a/database/auth_init.sql b/database/auth_init.sql index d78b94a..d55729b 100644 --- a/database/auth_init.sql +++ b/database/auth_init.sql @@ -192,6 +192,44 @@ COMMENT ON COLUMN AUTH_SETTING.SETTING_DC IS '설정설명'; COMMENT ON COLUMN AUTH_SETTING.MDFCN_DTM IS '수정일시'; +-- ============================================================ +-- 8-2. 감사 로그 (AUTH_AUDIT_LOG) +-- ============================================================ +CREATE TABLE AUTH_AUDIT_LOG ( + LOG_SN SERIAL NOT NULL, + USER_ID UUID, + ACTION_CD VARCHAR(30) NOT NULL, + ACTION_DTL VARCHAR(100), + HTTP_METHOD VARCHAR(10), + CRUD_TYPE VARCHAR(10), + REQ_URL VARCHAR(500), + REQ_DTM TIMESTAMPTZ NOT NULL DEFAULT NOW(), + RES_DTM TIMESTAMPTZ, + RES_STATUS SMALLINT, + RES_SIZE INTEGER, + IP_ADDR VARCHAR(45), + USER_AGENT VARCHAR(500), + EXTRA JSONB, + CONSTRAINT PK_AUTH_AUDIT_LOG PRIMARY KEY (LOG_SN) +); + +COMMENT ON TABLE AUTH_AUDIT_LOG IS '감사로그'; +COMMENT ON COLUMN AUTH_AUDIT_LOG.LOG_SN IS '로그순번'; +COMMENT ON COLUMN AUTH_AUDIT_LOG.USER_ID IS '사용자아이디'; +COMMENT ON COLUMN AUTH_AUDIT_LOG.ACTION_CD IS '액션코드 (TAB_VIEW, API_CALL, LOGIN, LOGOUT, ADMIN_ACTION)'; +COMMENT ON COLUMN AUTH_AUDIT_LOG.ACTION_DTL IS '액션상세 (탭ID, API경로, 관리자작업명)'; +COMMENT ON COLUMN AUTH_AUDIT_LOG.HTTP_METHOD IS 'HTTP메소드 (GET, POST, PUT, DELETE)'; +COMMENT ON COLUMN AUTH_AUDIT_LOG.CRUD_TYPE IS 'CRUD구분 (SELECT, INSERT, UPDATE, DELETE)'; +COMMENT ON COLUMN AUTH_AUDIT_LOG.REQ_URL IS '요청URL'; +COMMENT ON COLUMN AUTH_AUDIT_LOG.REQ_DTM IS '요청일시'; +COMMENT ON COLUMN AUTH_AUDIT_LOG.RES_DTM IS '응답일시'; +COMMENT ON COLUMN AUTH_AUDIT_LOG.RES_STATUS IS '응답HTTP상태코드'; +COMMENT ON COLUMN AUTH_AUDIT_LOG.RES_SIZE IS '응답데이터크기(bytes)'; +COMMENT ON COLUMN AUTH_AUDIT_LOG.IP_ADDR IS 'IP주소'; +COMMENT ON COLUMN AUTH_AUDIT_LOG.USER_AGENT IS '유저에이전트'; +COMMENT ON COLUMN AUTH_AUDIT_LOG.EXTRA IS '추가메타데이터(JSON)'; + + -- ============================================================ -- 9. 인덱스 -- ============================================================ @@ -203,6 +241,9 @@ CREATE INDEX IDX_AUTH_PERM_ROLE ON AUTH_PERM (ROLE_SN); CREATE INDEX IDX_AUTH_PERM_RSRC ON AUTH_PERM (RSRC_CD); CREATE INDEX IDX_AUTH_LOGIN_USER ON AUTH_LOGIN_HIST (USER_ID); CREATE INDEX IDX_AUTH_LOGIN_DTM ON AUTH_LOGIN_HIST (LOGIN_DTM); +CREATE INDEX IDX_AUDIT_LOG_USER ON AUTH_AUDIT_LOG (USER_ID); +CREATE INDEX IDX_AUDIT_LOG_ACTION ON AUTH_AUDIT_LOG (ACTION_CD); +CREATE INDEX IDX_AUDIT_LOG_DTM ON AUTH_AUDIT_LOG (REQ_DTM); -- ============================================================ diff --git a/docs/COMMON-GUIDE.md b/docs/COMMON-GUIDE.md new file mode 100644 index 0000000..e4f2b25 --- /dev/null +++ b/docs/COMMON-GUIDE.md @@ -0,0 +1,326 @@ +# WING-OPS 공통 로직 개발 가이드 + +개별 탭 개발자가 공통 영역 구현을 참조하여 연동할 수 있도록 정리한 문서입니다. +공통 기능을 추가/변경할 때 반드시 이 문서를 최신화하세요. + +--- + +## 1. 인증/인가 + +### 개요 +JWT 기반 세션 인증. HttpOnly 쿠키(`WING_SESSION`)로 토큰을 관리하며, 프론트엔드에서는 Zustand `authStore`로 상태를 관리합니다. + +### 백엔드 + +#### 미들웨어 적용 +```typescript +// 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`에 담기는 정보: +```typescript +interface JwtPayload { + sub: string // 사용자 UUID (USER_ID) + acnt: string // 계정명 (USER_ACNT) + name: string // 사용자명 (USER_NM) + roles: string[] // 역할 코드 목록 (ADMIN, MANAGER, USER, VIEWER) +} +``` + +#### 라우터 패턴 +```typescript +// 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) +```typescript +// 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 클라이언트 +```typescript +// 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.tsx`의 `useEffect`에서 `activeMainTab` 변경을 감지하여 `navigator.sendBeacon`으로 자동 전송합니다. 개별 탭 개발자는 별도 작업이 필요 없습니다. + +```typescript +// 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]) +``` + +### 수동 기록 (향후 확장) +특정 작업에 대해 명시적으로 감사 로그를 기록하려면: + +```typescript +// 프론트엔드에서 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 +```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. 백엔드 모듈 추가 절차 + +새 백엔드 모듈을 추가할 때: + +1. `backend/src/[모듈명]/` 디렉토리 생성 +2. `[모듈명]Service.ts` — 비즈니스 로직 (DB 쿼리) +3. `[모듈명]Router.ts` — Express 라우터 (입력 검증, 에러 처리) +4. `backend/src/server.ts`에 라우터 등록: + ```typescript + import newRouter from './[모듈명]/[모듈명]Router.js' + app.use('/api/[경로]', newRouter) + ``` +5. DB 테이블 필요 시 `database/auth_init.sql`에 DDL 추가 + +### DB 접근 +```typescript +// 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 진입점 + 라우터 등록 +``` diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index da36a5a..de541a9 100755 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -40,6 +40,16 @@ function App() { registerMainTabSwitcher(setActiveMainTab) }, []) + // 감사 로그: 탭 이동 기록 + 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]) + // 세션 확인 중 스플래시 if (isLoading) { return ( diff --git a/frontend/src/services/authApi.ts b/frontend/src/services/authApi.ts index 0405a8b..5f58a77 100644 --- a/frontend/src/services/authApi.ts +++ b/frontend/src/services/authApi.ts @@ -219,3 +219,42 @@ export async function updateMenuConfigApi(menus: MenuConfigItem[]): Promise('/menus', { menus }) return response.data } + +// 감사 로그 API (ADMIN 전용) +export interface AuditLogItem { + logSn: number + userId: string + userName: string | null + userAccount: string | null + actionCd: string + actionDtl: string | null + httpMethod: string | null + crudType: string | null + reqUrl: string | null + reqDtm: string + resDtm: string | null + resStatus: number | null + resSize: number | null + ipAddr: string | null + userAgent: string | null + extra: Record | null +} + +export interface AuditLogListResult { + items: AuditLogItem[] + total: number + page: number + size: number +} + +export async function fetchAuditLogs(params?: { + page?: number + size?: number + userId?: string + actionCd?: string + from?: string + to?: string +}): Promise { + const response = await api.get('/audit/logs', { params }) + return response.data +}