/** * 관리자 API 클라이언트 (감사 로그, 접근 이력, 로그인 이력, 권한 트리, 역할). */ const API_BASE = import.meta.env.VITE_API_URL ?? '/api'; export interface PageResponse { 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 | 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; } 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(path: string): Promise { 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>(`/admin/audit-logs?page=${page}&size=${size}`); } export function fetchAccessLogs(page = 0, size = 50) { return apiGet>(`/admin/access-logs?page=${page}&size=${size}`); } export function fetchLoginHistory(page = 0, size = 50) { return apiGet>(`/admin/login-history?page=${page}&size=${size}`); } export function fetchPermTree() { return apiGet('/perm-tree'); } import { updateRoleColorCache } from '@shared/constants/userRoles'; export async function fetchRoles(): Promise { const roles = await apiGet('/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(method: string, path: string, body?: unknown): Promise { 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; byProvider: Record; byRole: Record; } export function fetchUsers() { return apiGet('/admin/users'); } export function fetchUserStats() { return apiGet('/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('/admin/stats/audit'); } export function fetchAccessStats() { return apiGet('/admin/stats/access'); } export function fetchLoginStats() { return apiGet('/admin/stats/login'); }