kcg-ai-monitoring/frontend/src/services/adminApi.ts
htlee 6fe7a7daf4 feat: 메뉴 DB SSOT 구조화 — auth_perm_tree 기반 메뉴·권한·i18n 통합
## 핵심 변경
- auth_perm_tree를 메뉴 SSOT로 확장 (V020~V024)
  - url_path, label_key, component_key, nav_group, nav_sub_group, nav_sort 컬럼
  - labels JSONB (다국어: {"ko":"...", "en":"..."})
- 보이지 않는 도메인 그룹 8개 삭제 (surveillance, detection, risk-assessment 등)
  - 권한 트리 = 메뉴 트리 완전 동기화
  - 그룹 레벨 권한 → 개별 자식 권한으로 확장 후 그룹 삭제
- 패널 노드 parent_cd를 실제 소속 페이지로 수정
  (어구식별→어구탐지, 전역제외→후보제외, 역할관리→권한관리)
- vessel:vessel-detail 권한 노드 제거 (드릴다운 전용, 인증만 체크)

## 백엔드
- MenuConfigService: auth_perm_tree에서 menuConfig DTO 생성
- /api/auth/me 응답에 menuConfig 포함 (로그인 시 프리로드)
- @RequirePermission 12곳 수정 (삭제된 그룹명 → 구체적 자식 리소스)
- Caffeine 캐시 menuConfig 추가

## 프론트엔드
- NAV_ENTRIES 하드코딩 제거 → menuStore(Zustand) 동적 렌더링
- PATH_TO_RESOURCE 하드코딩 제거 → DB 기반 longest-match
- App.tsx 36개 정적 import/33개 Route → DynamicRoutes + componentRegistry
- PermissionsPanel: DB labels JSONB 기반 표시명 + 페이지/패널 아이콘 구분
- DB migration README.md 전면 재작성 (V001~V024, 49테이블, 149인덱스)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-09 15:54:04 +09:00

273 lines
7.4 KiB
TypeScript

/**
* 관리자 API 클라이언트 (감사 로그, 접근 이력, 로그인 이력, 권한 트리, 역할).
*/
const API_BASE = import.meta.env.VITE_API_URL ?? '/api';
export interface PageResponse<T> {
content: T[];
totalElements: number;
totalPages: number;
number: number;
size: number;
}
export interface AuditLog {
auditSn: number;
userId: string | null;
userAcnt: string | null;
actionCd: string;
resourceType: string | null;
resourceId: string | null;
detail: Record<string, unknown> | null;
ipAddress: string | null;
result: string | null;
failReason: string | null;
createdAt: string;
}
export interface AccessLog {
accessSn: number;
userId: string | null;
userAcnt: string | null;
httpMethod: string;
requestPath: string;
queryString: string | null;
statusCode: number;
durationMs: number;
ipAddress: string | null;
userAgent: string | null;
createdAt: string;
}
export interface LoginHistory {
histSn: number;
userId: string | null;
userAcnt: string;
loginDtm: string;
loginIp: string | null;
userAgent: string | null;
result: string;
failReason: string | null;
authProvider: string | null;
}
export interface PermTreeNode {
rsrcCd: string;
parentCd: string | null;
rsrcNm: string;
rsrcDesc: string | null;
icon: string | null;
rsrcLevel: number;
sortOrd: number;
useYn: string;
/** V021: 메뉴 SSOT */
labelKey: string | null;
urlPath: string | null;
navSort: number;
/** V022: DB i18n JSONB {"ko":"...", "en":"..."} */
labels: Record<string, string>;
}
export interface RoleWithPermissions {
roleSn: number;
roleCd: string;
roleNm: string;
roleDc: string;
/** UI 표기 색상 (#RRGGBB). NULL이면 프론트 기본 팔레트에서 결정 */
colorHex: string | null;
dfltYn: string;
builtinYn: string;
permissions: { permSn: number; roleSn: number; rsrcCd: string; operCd: string; grantYn: string }[];
}
async function apiGet<T>(path: string): Promise<T> {
const res = await fetch(`${API_BASE}${path}`, { credentials: 'include' });
if (!res.ok) throw new Error(`API ${res.status}: ${path}`);
return res.json();
}
export function fetchAuditLogs(page = 0, size = 50) {
return apiGet<PageResponse<AuditLog>>(`/admin/audit-logs?page=${page}&size=${size}`);
}
export function fetchAccessLogs(page = 0, size = 50) {
return apiGet<PageResponse<AccessLog>>(`/admin/access-logs?page=${page}&size=${size}`);
}
export function fetchLoginHistory(page = 0, size = 50) {
return apiGet<PageResponse<LoginHistory>>(`/admin/login-history?page=${page}&size=${size}`);
}
export function fetchPermTree() {
return apiGet<PermTreeNode[]>('/perm-tree');
}
import { updateRoleColorCache } from '@shared/constants/userRoles';
export async function fetchRoles(): Promise<RoleWithPermissions[]> {
const roles = await apiGet<RoleWithPermissions[]>('/roles');
// 역할 색상 캐시 즉시 갱신 → 모든 사용처 자동 반영
updateRoleColorCache(roles);
return roles;
}
// ─── 역할 CRUD ───────────────────────────────
export interface RoleCreatePayload {
roleCd: string;
roleNm: string;
roleDc?: string;
colorHex?: string;
dfltYn?: string;
}
export interface RoleUpdatePayload {
roleNm?: string;
roleDc?: string;
colorHex?: string;
dfltYn?: string;
}
async function apiSend<T>(method: string, path: string, body?: unknown): Promise<T> {
const res = await fetch(`${API_BASE}${path}`, {
method,
credentials: 'include',
headers: { 'Content-Type': 'application/json' },
body: body ? JSON.stringify(body) : undefined,
});
if (!res.ok) {
let msg = `API ${res.status}`;
try { const b = await res.json(); if (b?.message) msg += `: ${b.message}`; } catch { /* */ }
throw new Error(msg);
}
return res.json();
}
export function createRole(payload: RoleCreatePayload) {
return apiSend<{ roleSn: number; roleCd: string; roleNm: string }>('POST', '/roles', payload);
}
export function updateRole(roleSn: number, payload: RoleUpdatePayload) {
return apiSend('PUT', `/roles/${roleSn}`, payload);
}
export function deleteRole(roleSn: number) {
return apiSend('DELETE', `/roles/${roleSn}`);
}
// ─── 권한 매트릭스 갱신 ────────────────────────
export interface PermEntry {
rsrcCd: string;
operCd: string;
grantYn: 'Y' | 'N' | null; // null = 명시 권한 제거 (상속 모드)
}
export function updateRolePermissions(roleSn: number, permissions: PermEntry[]) {
return apiSend<{ ok: boolean; changed: number }>('PUT', `/roles/${roleSn}/permissions`, { permissions });
}
// ─── 사용자 역할 배정 ─────────────────────────
export function assignUserRoles(userId: string, roleSns: number[]) {
return apiSend<{ userId: string; roles: string[] }>('PUT', `/admin/users/${userId}/roles`, { roleSns });
}
// ============================================================================
// 사용자 관리
// ============================================================================
export interface AdminUser {
userId: string;
userAcnt: string;
userNm: string;
rnkpNm: string | null;
email: string | null;
userSttsCd: string;
authProvider: string;
failCnt: number;
lastLoginDtm: string | null;
createdAt: string;
roles: string[];
}
export interface UserStats {
total: number;
active: number;
locked: number;
inactive: number;
pending: number;
byStatus: Record<string, number>;
byProvider: Record<string, number>;
byRole: Record<string, number>;
}
export function fetchUsers() {
return apiGet<AdminUser[]>('/admin/users');
}
export function fetchUserStats() {
return apiGet<UserStats>('/admin/users/stats');
}
export async function unlockUser(userId: string) {
const res = await fetch(`${API_BASE}/admin/users/${userId}/unlock`, {
method: 'POST',
credentials: 'include',
});
if (!res.ok) throw new Error(`API ${res.status}: unlock`);
return res.json();
}
export async function changeUserStatus(userId: string, status: string) {
const res = await fetch(`${API_BASE}/admin/users/${userId}/status`, {
method: 'PUT',
credentials: 'include',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ status }),
});
if (!res.ok) throw new Error(`API ${res.status}: status`);
return res.json();
}
// ============================================================================
// 통계 (대시보드 카드)
// ============================================================================
export interface AuditStats {
total: number;
last24h: number;
failed24h: number;
byAction: { action: string; count: number }[];
hourly24: { hour: string; count: number }[];
}
export interface AccessStats {
total: number;
last24h: number;
error4xx: number;
error5xx: number;
avgDurationMs: number;
topPaths: { path: string; count: number; avg_ms: number }[];
}
export interface LoginStats {
total: number;
success24h: number;
failed24h: number;
locked24h: number;
successRate: number;
byUser: { user_acnt: string; count: number }[];
daily7d: { day: string; success: number; failed: number; locked: number }[];
}
export function fetchAuditStats() {
return apiGet<AuditStats>('/admin/stats/audit');
}
export function fetchAccessStats() {
return apiGet<AccessStats>('/admin/stats/access');
}
export function fetchLoginStats() {
return apiGet<LoginStats>('/admin/stats/login');
}