176 lines
5.2 KiB
TypeScript
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>
|
|
);
|
|
}
|