kcg-monitoring/frontend/src/components/korea/AnalysisStatsPanel.tsx
2026-03-20 13:28:50 +09:00

176 lines
5.2 KiB
TypeScript

import { useState, useMemo } from 'react';
import type { AnalysisStats } from '../../hooks/useVesselAnalysis';
interface Props {
stats: AnalysisStats;
lastUpdated: number;
isLoading: boolean;
}
/** unix ms → HH:MM 형식 */
function formatTime(ms: number): string {
if (ms === 0) return '--:--';
const d = new Date(ms);
const hh = String(d.getHours()).padStart(2, '0');
const mm = String(d.getMinutes()).padStart(2, '0');
return `${hh}:${mm}`;
}
export function AnalysisStatsPanel({ stats, lastUpdated, isLoading }: Props) {
const [expanded, setExpanded] = useState(true);
const isEmpty = useMemo(() => stats.total === 0, [stats.total]);
const panelStyle: React.CSSProperties = {
position: 'absolute',
top: 60,
right: 10,
zIndex: 10,
minWidth: 160,
backgroundColor: 'rgba(12, 24, 37, 0.92)',
border: '1px solid rgba(99, 179, 237, 0.25)',
borderRadius: 8,
color: '#e2e8f0',
fontFamily: 'monospace, sans-serif',
fontSize: 11,
boxShadow: '0 4px 16px rgba(0, 0, 0, 0.5)',
overflow: 'hidden',
};
const headerStyle: React.CSSProperties = {
display: 'flex',
alignItems: 'center',
justifyContent: 'space-between',
padding: '6px 10px',
borderBottom: expanded ? '1px solid rgba(99, 179, 237, 0.15)' : 'none',
cursor: 'default',
userSelect: 'none',
};
const toggleButtonStyle: React.CSSProperties = {
background: 'none',
border: 'none',
color: '#94a3b8',
cursor: 'pointer',
fontSize: 10,
padding: '0 2px',
lineHeight: 1,
};
const bodyStyle: React.CSSProperties = {
padding: '8px 10px',
};
const rowStyle: React.CSSProperties = {
display: 'flex',
justifyContent: 'space-between',
alignItems: 'center',
marginBottom: 3,
};
const labelStyle: React.CSSProperties = {
color: '#94a3b8',
};
const valueStyle: React.CSSProperties = {
fontWeight: 700,
color: '#e2e8f0',
};
const dividerStyle: React.CSSProperties = {
borderTop: '1px solid rgba(99, 179, 237, 0.15)',
margin: '6px 0',
};
const riskRowStyle: React.CSSProperties = {
display: 'flex',
gap: 6,
justifyContent: 'space-between',
marginTop: 4,
};
const riskBadgeStyle: React.CSSProperties = {
display: 'flex',
alignItems: 'center',
gap: 2,
color: '#cbd5e1',
fontSize: 10,
};
return (
<div style={panelStyle}>
{/* 헤더 */}
<div style={headerStyle}>
<div style={{ display: 'flex', alignItems: 'center', gap: 6 }}>
<span style={{ fontWeight: 700, color: '#63b3ed', letterSpacing: 0.5 }}>AI </span>
{isLoading && (
<span style={{ fontSize: 9, color: '#fbbf24' }}>...</span>
)}
</div>
<div style={{ display: 'flex', alignItems: 'center', gap: 6 }}>
<span style={{ fontSize: 9, color: '#64748b' }}>{formatTime(lastUpdated)}</span>
<button
style={toggleButtonStyle}
onClick={() => setExpanded(prev => !prev)}
aria-label={expanded ? '패널 접기' : '패널 펼치기'}
>
{expanded ? '▲' : '▼'}
</button>
</div>
</div>
{/* 본문 */}
{expanded && (
<div style={bodyStyle}>
{isEmpty ? (
<div style={{ color: '#64748b', textAlign: 'center', padding: '6px 0' }}>
</div>
) : (
<>
<div style={rowStyle}>
<span style={labelStyle}></span>
<span style={valueStyle}>{stats.total}</span>
</div>
<div style={rowStyle}>
<span style={labelStyle}></span>
<span style={{ ...valueStyle, color: '#a855f7' }}>{stats.dark}</span>
</div>
<div style={rowStyle}>
<span style={labelStyle}>GPS스푸핑</span>
<span style={{ ...valueStyle, color: '#ef4444' }}>{stats.spoofing}</span>
</div>
<div style={rowStyle}>
<span style={labelStyle}></span>
<span style={{ ...valueStyle, color: '#06b6d4' }}>{stats.clusterCount}</span>
</div>
<div style={dividerStyle} />
{/* 위험도 수치 행 */}
<div style={riskRowStyle}>
<div style={riskBadgeStyle}>
<span>🔴</span>
<span style={{ color: '#ef4444', fontWeight: 700 }}>{stats.critical}</span>
</div>
<div style={riskBadgeStyle}>
<span>🟠</span>
<span style={{ color: '#f97316', fontWeight: 700 }}>{stats.high}</span>
</div>
<div style={riskBadgeStyle}>
<span>🟡</span>
<span style={{ color: '#eab308', fontWeight: 700 }}>{stats.medium}</span>
</div>
<div style={riskBadgeStyle}>
<span>🟢</span>
<span style={{ color: '#22c55e', fontWeight: 700 }}>{stats.low}</span>
</div>
</div>
</>
)}
</div>
)}
</div>
);
}