import { useEffect, useState, useCallback } from 'react'; import { Loader2, RefreshCw, FileSearch } from 'lucide-react'; import { Card, CardContent, CardHeader, CardTitle } from '@shared/components/ui/card'; import { Badge } from '@shared/components/ui/badge'; import { Button } from '@shared/components/ui/button'; import { PageContainer, PageHeader } from '@shared/components/layout'; import { fetchAuditLogs, fetchAuditStats, type AuditLog, type AuditStats } from '@/services/adminApi'; import { formatDateTime } from '@shared/utils/dateFormat'; /** * 감사 로그 조회 + 메트릭 카드. * 권한: admin:audit-logs (READ) */ export function AuditLogs() { const [items, setItems] = useState([]); const [stats, setStats] = useState(null); const [loading, setLoading] = useState(false); const [error, setError] = useState(''); const load = useCallback(async () => { setLoading(true); setError(''); try { const [logs, st] = await Promise.all([fetchAuditLogs(0, 100), fetchAuditStats()]); setItems(logs.content); setStats(st); } catch (e: unknown) { setError(e instanceof Error ? e.message : 'unknown'); } finally { setLoading(false); } }, []); useEffect(() => { load(); }, [load]); return ( }> 새로고침 } /> {/* 통계 카드 */} {stats && (
)} {/* 액션별 분포 */} {stats && stats.byAction.length > 0 && ( 액션별 분포 (최근 7일)
{stats.byAction.map((a) => ( {a.action} {a.count} ))}
)} {error &&
에러: {error}
} {loading &&
} {!loading && ( {items.length === 0 && } {items.map((it) => ( ))}
SN 시각 사용자 액션 리소스 결과 실패 사유 IP 상세
감사 로그가 없습니다.
{it.auditSn} {formatDateTime(it.createdAt)} {it.userAcnt || '-'} {it.actionCd} {it.resourceType ?? '-'} {it.resourceId ? `(${it.resourceId})` : ''} {it.result || '-'} {it.failReason || '-'} {it.ipAddress || '-'} {it.detail ? JSON.stringify(it.detail) : '-'}
)}
); } function MetricCard({ label, value, color }: { label: string; value: number; color: string }) { return (
{label}
{value.toLocaleString()}
); }