import { useEffect, useState, useCallback } from 'react'; import { Loader2, RefreshCw } from 'lucide-react'; import { Card, CardContent, CardHeader, CardTitle } from '@shared/components/ui/card'; import { Badge } from '@shared/components/ui/badge'; import { fetchAccessLogs, fetchAccessStats, type AccessLog, type AccessStats } from '@/services/adminApi'; import { formatDateTime } from '@shared/utils/dateFormat'; /** * 접근 이력 조회 + 메트릭 카드. * 권한: admin:access-logs (READ) */ export function AccessLogs() { 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([fetchAccessLogs(0, 100), fetchAccessStats()]); setItems(logs.content); setStats(st); } catch (e: unknown) { setError(e instanceof Error ? e.message : 'unknown'); } finally { setLoading(false); } }, []); useEffect(() => { load(); }, [load]); const statusColor = (s: number) => s >= 500 ? 'bg-red-500/20 text-red-400' : s >= 400 ? 'bg-orange-500/20 text-orange-400' : 'bg-green-500/20 text-green-400'; return (

접근 이력

AccessLogFilter가 모든 HTTP 요청 비동기 기록

{stats && (
)} {stats && stats.topPaths.length > 0 && ( 호출 빈도 Top 10 (24시간) {stats.topPaths.map((p) => ( ))}
경로 호출수 평균(ms)
{p.path} {p.count} {p.avg_ms}
)} {error &&
에러: {error}
} {loading &&
} {!loading && ( {items.length === 0 && } {items.map((it) => ( ))}
SN 시각 사용자 메서드 경로 상태 시간(ms) IP
접근 로그가 없습니다.
{it.accessSn} {formatDateTime(it.createdAt)} {it.userAcnt || '-'} {it.httpMethod} {it.requestPath} {it.statusCode} {it.durationMs} {it.ipAddress || '-'}
)}
); } function MetricCard({ label, value, color }: { label: string; value: number; color: string }) { return (
{label}
{value.toLocaleString()}
); }