kcg-ai-monitoring/frontend/src/features/admin/AccessLogs.tsx
htlee 234169d540 refactor(frontend): admin 11개 페이지 디자인 시스템 하드코딩 색상 제거 (Phase 1-B)
129건 하드코딩 Tailwind 색상 → 시맨틱 토큰 치환:
- text-cyan-400 (45건) → text-label
- text-green-400/500 (51건) → text-label + Badge intent="success"
- text-red-400/500 (31건) → text-heading + Badge intent="critical"
- text-blue-400 (33건) → text-label + Badge intent="info"
- text-purple-400 (20건) → text-heading
- text-yellow/orange/amber (32건) → text-heading + Badge intent="warning"

raw <button> → <Button> 컴포넌트 교체 (DataHub/NoticeManagement/SystemConfig 등)
미사용 import 정리 (SaveButton/DataTable/lucide 아이콘)

대상: AIAgentSecurityPage, AISecurityPage, AccessControl, AccessLogs,
AdminPanel, AuditLogs, DataHub, LoginHistoryView, NoticeManagement,
PermissionsPanel, SystemConfig

검증: tsc 0 errors, eslint 0 errors, 하드코딩 색상 잔여 0건
2026-04-16 11:25:51 +09:00

141 lines
6.1 KiB
TypeScript

import { useEffect, useState, useCallback } from 'react';
import { Loader2, RefreshCw, Activity } 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 { fetchAccessLogs, fetchAccessStats, type AccessLog, type AccessStats } from '@/services/adminApi';
import { formatDateTime } from '@shared/utils/dateFormat';
import { getHttpStatusIntent } from '@shared/constants/httpStatusCodes';
/**
* 접근 이력 조회 + 메트릭 카드.
* 권한: admin:access-logs (READ)
*/
export function AccessLogs() {
const [items, setItems] = useState<AccessLog[]>([]);
const [stats, setStats] = useState<AccessStats | null>(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]);
return (
<PageContainer size="lg">
<PageHeader
icon={Activity}
iconColor="text-label"
title="접근 이력"
description="AccessLogFilter가 모든 HTTP 요청 비동기 기록"
actions={
<Button variant="primary" size="sm" onClick={load} icon={<RefreshCw className="w-3.5 h-3.5" />}>
</Button>
}
/>
{stats && (
<div className="grid grid-cols-5 gap-3">
<MetricCard label="전체 요청" value={stats.total} color="text-heading" />
<MetricCard label="24시간 내" value={stats.last24h} color="text-label" />
<MetricCard label="4xx (24h)" value={stats.error4xx} color="text-heading" />
<MetricCard label="5xx (24h)" value={stats.error5xx} color="text-heading" />
<MetricCard label="평균 응답(ms)" value={Math.round(stats.avgDurationMs)} color="text-heading" />
</div>
)}
{stats && stats.topPaths.length > 0 && (
<Card>
<CardHeader className="px-4 pt-3 pb-2"><CardTitle className="text-xs text-label"> Top 10 (24)</CardTitle></CardHeader>
<CardContent className="px-4 pb-4">
<table className="w-full text-[11px]">
<thead className="text-hint">
<tr>
<th className="text-left py-1"></th>
<th className="text-right py-1 w-24"></th>
<th className="text-right py-1 w-28">(ms)</th>
</tr>
</thead>
<tbody>
{stats.topPaths.map((p) => (
<tr key={p.path} className="border-t border-border">
<td className="py-1.5 text-heading font-mono text-[10px]">{p.path}</td>
<td className="py-1.5 text-right text-label font-bold">{p.count}</td>
<td className="py-1.5 text-right text-muted-foreground">{p.avg_ms}</td>
</tr>
))}
</tbody>
</table>
</CardContent>
</Card>
)}
{error && <div className="text-xs text-heading">: {error}</div>}
{loading && <div className="flex items-center justify-center py-12 text-muted-foreground"><Loader2 className="w-5 h-5 animate-spin" /></div>}
{!loading && (
<Card>
<CardContent className="p-0 overflow-x-auto">
<table className="w-full text-xs">
<thead className="bg-surface-overlay text-hint">
<tr>
<th className="px-3 py-2 text-left">SN</th>
<th className="px-3 py-2 text-left"></th>
<th className="px-3 py-2 text-left"></th>
<th className="px-3 py-2 text-left"></th>
<th className="px-3 py-2 text-left"></th>
<th className="px-3 py-2 text-center"></th>
<th className="px-3 py-2 text-right">(ms)</th>
<th className="px-3 py-2 text-left">IP</th>
</tr>
</thead>
<tbody>
{items.length === 0 && <tr><td colSpan={8} className="px-3 py-8 text-center text-hint"> .</td></tr>}
{items.map((it) => (
<tr key={it.accessSn} className="border-t border-border hover:bg-surface-overlay/50">
<td className="px-3 py-2 text-hint font-mono">{it.accessSn}</td>
<td className="px-3 py-2 text-muted-foreground text-[10px]">{formatDateTime(it.createdAt)}</td>
<td className="px-3 py-2 text-label">{it.userAcnt || '-'}</td>
<td className="px-3 py-2 text-heading font-mono">{it.httpMethod}</td>
<td className="px-3 py-2 text-heading font-mono text-[10px] max-w-md truncate">{it.requestPath}</td>
<td className="px-3 py-2 text-center">
<Badge intent={getHttpStatusIntent(it.statusCode)} size="sm">{it.statusCode}</Badge>
</td>
<td className="px-3 py-2 text-right text-muted-foreground">{it.durationMs}</td>
<td className="px-3 py-2 text-muted-foreground text-[10px]">{it.ipAddress || '-'}</td>
</tr>
))}
</tbody>
</table>
</CardContent>
</Card>
)}
</PageContainer>
);
}
function MetricCard({ label, value, color }: { label: string; value: number; color: string }) {
return (
<Card>
<CardContent className="p-4">
<div className="text-[10px] text-hint">{label}</div>
<div className={`text-2xl font-bold ${color} mt-1`}>{value.toLocaleString()}</div>
</CardContent>
</Card>
);
}