import { useEffect, useState } from 'react'; import api from '../../api/client'; const SEVERITY_COLOR = { high: '#ef4444', medium: '#f59e0b', low: '#6b7280' }; const ANOMALY_LABELS = { RATE_LIMIT_IP: 'IP 속도 제한', HIGH_VOLUME_IP: 'IP 대량 플래시', MAC_REUSE: 'MAC 재사용', TOKEN_LOCK: '토큰 잠금', SUSPICIOUS_DURATION: '비정상 시간', }; function fmtMs(ms) { if (!ms) return '—'; return ms < 1000 ? `${ms}ms` : `${(ms / 1000).toFixed(1)}s`; } export default function FlashMetrics() { const [metrics, setMetrics] = useState(null); const [anomalies, setAnomalies] = useState([]); const [tab, setTab] = useState('overview'); // overview | logs | anomalies const [showResolved, setShowResolved] = useState(false); const [loading, setLoading] = useState(true); async function load() { setLoading(true); try { const [mRes, aRes] = await Promise.all([ api.get('/admin/flash/metrics'), api.get(`/admin/flash/anomalies?resolved=${showResolved}`), ]); setMetrics(mRes.data); setAnomalies(aRes.data.anomalies || []); } catch {} setLoading(false); } useEffect(() => { load(); }, [showResolved]); async function resolveAnomaly(id) { await api.put(`/admin/flash/anomalies/${id}/resolve`).catch(() => {}); load(); } if (loading) return
; const m = metrics; return (

플래시 지표

{/* 요약 카드 */} {m && (
{[ { label: '오늘 전체', value: m.today.total, icon: '⚡' }, { label: '오늘 성공', value: m.today.success, icon: '✅' }, { label: '오늘 실패', value: m.today.failed, icon: '❌', warn: m.today.failed > 0 }, { label: '전체 성공률', value: `${m.allTime.successRate}%`, icon: '📊' }, { label: '미해소 이상', value: m.anomalies.unresolved, icon: '⚠️', warn: m.anomalies.unresolved > 0 }, ].map(c => (
{c.icon}
{c.value}
{c.label}
))}
)} {/* 탭 */}
{[ { id: 'overview', label: '최근 플래시' }, { id: 'topips', label: 'Top IP' }, { id: 'anomalies', label: `이상 감지 ${m?.anomalies.unresolved > 0 ? `(${m.anomalies.unresolved})` : ''}` }, ].map(t => ( ))}
{/* 최근 플래시 로그 */} {tab === 'overview' && m && (
{['상품', 'MAC', '칩', 'IP', '소요', '결과', '시각'].map(h => ( ))} {m.recentLogs.map((l, i) => ( ))} {m.recentLogs.length === 0 && ( )}
{h}
{l.productName} {l.macAddress || '—'} {l.chipFamily || '—'} {l.clientIp} {fmtMs(l.durationMs)} {l.success ? '성공' : '실패'} {l.isLocked && 🔒잠김} {l.errorMessage && (
{l.errorMessage.slice(0, 40)}
)}
{l.flashedAt ? new Date(l.flashedAt).toLocaleString('ko-KR', { month: 'numeric', day: 'numeric', hour: '2-digit', minute: '2-digit' }) : '—'}
플래시 기록 없음
)} {/* Top IP */} {tab === 'topips' && m && (
{m.topIps.map((r, i) => ( ))} {m.topIps.length === 0 && ( )}
IP 주소 24시간 내 플래시 횟수 상태
{r.ip}
{r.count}
= 10 ? 'var(--danger)' : r.count >= 5 ? 'var(--warn)' : 'var(--accent)', borderRadius: 3, }} />
{r.count >= 10 ? 고위험 : r.count >= 5 ? 주의 : 정상}
데이터 없음
)} {/* 이상 감지 */} {tab === 'anomalies' && ( <>
{['유형', '심각도', '설명', 'IP', 'MAC', '발생', '조치'].map(h => ( ))} {anomalies.map((a, i) => ( ))} {anomalies.length === 0 && ( )}
{h}
{ANOMALY_LABELS[a.type] || a.type} {a.severity === 'high' ? '높음' : a.severity === 'medium' ? '중간' : '낮음'} {a.description} {a.clientIp || '—'} {a.macAddress || '—'} {new Date(a.createdAt).toLocaleString('ko-KR', { month: 'numeric', day: 'numeric', hour: '2-digit', minute: '2-digit' })} {a.resolved ? ( 해소됨 ) : ( )}
{showResolved ? '이상 이벤트 없음' : '미해소 이상 없음'}
)}
); }