182 lines
6.7 KiB
TypeScript
182 lines
6.7 KiB
TypeScript
import { useState, useEffect, useCallback } from 'react';
|
||
import { fetchCollectorStatus } from '../services/collectorStatus';
|
||
import type { CollectorInfo } from '../services/collectorStatus';
|
||
|
||
interface CollectorMonitorProps {
|
||
onClose: () => void;
|
||
}
|
||
|
||
function formatRelativeTime(isoString: string): string {
|
||
if (!isoString) return '-';
|
||
const diff = Date.now() - new Date(isoString).getTime();
|
||
if (diff < 0) return '방금';
|
||
const sec = Math.floor(diff / 1000);
|
||
if (sec < 60) return `${sec}초 전`;
|
||
const min = Math.floor(sec / 60);
|
||
if (min < 60) return `${min}분 전`;
|
||
const hr = Math.floor(min / 60);
|
||
if (hr < 24) return `${hr}시간 전`;
|
||
return `${Math.floor(hr / 24)}일 전`;
|
||
}
|
||
|
||
function getStatusColor(info: CollectorInfo): string {
|
||
if (!info.lastSuccess) return '#ef4444';
|
||
const elapsed = Date.now() - new Date(info.lastSuccess).getTime();
|
||
if (elapsed < 5 * 60_000) return '#22c55e';
|
||
if (elapsed < 30 * 60_000) return '#eab308';
|
||
return '#ef4444';
|
||
}
|
||
|
||
const CollectorMonitor = ({ onClose }: CollectorMonitorProps) => {
|
||
const [collectors, setCollectors] = useState<CollectorInfo[]>([]);
|
||
const [serverTime, setServerTime] = useState('');
|
||
const [error, setError] = useState('');
|
||
|
||
const refresh = useCallback(async () => {
|
||
try {
|
||
const data = await fetchCollectorStatus();
|
||
setCollectors(data.collectors);
|
||
setServerTime(data.serverTime);
|
||
setError('');
|
||
} catch (e) {
|
||
setError(e instanceof Error ? e.message : '연결 실패');
|
||
}
|
||
}, []);
|
||
|
||
useEffect(() => {
|
||
refresh();
|
||
const interval = setInterval(refresh, 10_000);
|
||
return () => clearInterval(interval);
|
||
}, [refresh]);
|
||
|
||
return (
|
||
<div style={{
|
||
position: 'fixed',
|
||
top: '50%',
|
||
left: '50%',
|
||
transform: 'translate(-50%, -50%)',
|
||
zIndex: 10000,
|
||
background: 'var(--kcg-panel-bg, rgba(15, 23, 42, 0.95))',
|
||
border: '1px solid var(--kcg-border, rgba(100, 116, 139, 0.3))',
|
||
borderRadius: '12px',
|
||
padding: '20px',
|
||
minWidth: '600px',
|
||
maxWidth: '800px',
|
||
maxHeight: '80vh',
|
||
overflow: 'auto',
|
||
color: 'var(--kcg-text, #e2e8f0)',
|
||
fontFamily: 'monospace',
|
||
fontSize: '13px',
|
||
boxShadow: '0 25px 50px rgba(0, 0, 0, 0.5)',
|
||
}}>
|
||
{/* Header */}
|
||
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: '16px' }}>
|
||
<h3 style={{ margin: 0, fontSize: '15px', fontWeight: 600 }}>
|
||
수집기 모니터링
|
||
</h3>
|
||
<div style={{ display: 'flex', alignItems: 'center', gap: '12px' }}>
|
||
{serverTime && (
|
||
<span style={{ fontSize: '11px', opacity: 0.6 }}>
|
||
서버: {new Date(serverTime).toLocaleTimeString('ko-KR')}
|
||
</span>
|
||
)}
|
||
<button
|
||
onClick={refresh}
|
||
style={{
|
||
background: 'none',
|
||
border: '1px solid var(--kcg-border, rgba(100, 116, 139, 0.3))',
|
||
color: 'var(--kcg-text, #e2e8f0)',
|
||
borderRadius: '4px',
|
||
padding: '2px 8px',
|
||
cursor: 'pointer',
|
||
fontSize: '12px',
|
||
}}
|
||
>
|
||
새로고침
|
||
</button>
|
||
<button
|
||
onClick={onClose}
|
||
style={{
|
||
background: 'none',
|
||
border: 'none',
|
||
color: 'var(--kcg-text, #e2e8f0)',
|
||
cursor: 'pointer',
|
||
fontSize: '18px',
|
||
lineHeight: 1,
|
||
padding: '0 4px',
|
||
}}
|
||
>
|
||
×
|
||
</button>
|
||
</div>
|
||
</div>
|
||
|
||
{error && (
|
||
<div style={{ padding: '8px 12px', background: 'rgba(239, 68, 68, 0.2)', borderRadius: '6px', marginBottom: '12px' }}>
|
||
{error}
|
||
</div>
|
||
)}
|
||
|
||
{/* Table */}
|
||
<table style={{ width: '100%', borderCollapse: 'collapse' }}>
|
||
<thead>
|
||
<tr style={{ borderBottom: '1px solid var(--kcg-border, rgba(100, 116, 139, 0.3))' }}>
|
||
<th style={{ textAlign: 'left', padding: '6px 8px', fontSize: '11px', opacity: 0.7 }}>상태</th>
|
||
<th style={{ textAlign: 'left', padding: '6px 8px', fontSize: '11px', opacity: 0.7 }}>수집기</th>
|
||
<th style={{ textAlign: 'right', padding: '6px 8px', fontSize: '11px', opacity: 0.7 }}>최근 건수</th>
|
||
<th style={{ textAlign: 'right', padding: '6px 8px', fontSize: '11px', opacity: 0.7 }}>성공/실패</th>
|
||
<th style={{ textAlign: 'right', padding: '6px 8px', fontSize: '11px', opacity: 0.7 }}>총 수집</th>
|
||
<th style={{ textAlign: 'left', padding: '6px 8px', fontSize: '11px', opacity: 0.7 }}>마지막 성공</th>
|
||
<th style={{ textAlign: 'left', padding: '6px 8px', fontSize: '11px', opacity: 0.7 }}>에러</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody>
|
||
{collectors.map((c) => (
|
||
<tr key={c.name} style={{ borderBottom: '1px solid var(--kcg-border, rgba(100, 116, 139, 0.15))' }}>
|
||
<td style={{ padding: '8px' }}>
|
||
<span style={{
|
||
display: 'inline-block',
|
||
width: '8px',
|
||
height: '8px',
|
||
borderRadius: '50%',
|
||
backgroundColor: getStatusColor(c),
|
||
}} />
|
||
</td>
|
||
<td style={{ padding: '8px', fontWeight: 500 }}>{c.name}</td>
|
||
<td style={{ padding: '8px', textAlign: 'right' }}>{c.lastCount}</td>
|
||
<td style={{ padding: '8px', textAlign: 'right' }}>
|
||
<span style={{ color: '#22c55e' }}>{c.totalSuccess}</span>
|
||
{' / '}
|
||
<span style={{ color: c.totalFailure > 0 ? '#ef4444' : 'inherit' }}>{c.totalFailure}</span>
|
||
</td>
|
||
<td style={{ padding: '8px', textAlign: 'right' }}>{c.totalItems.toLocaleString()}</td>
|
||
<td style={{ padding: '8px', opacity: 0.8 }}>{formatRelativeTime(c.lastSuccess)}</td>
|
||
<td style={{
|
||
padding: '8px',
|
||
maxWidth: '200px',
|
||
overflow: 'hidden',
|
||
textOverflow: 'ellipsis',
|
||
whiteSpace: 'nowrap',
|
||
color: c.lastError ? '#ef4444' : 'inherit',
|
||
opacity: c.lastError ? 1 : 0.4,
|
||
fontSize: '11px',
|
||
}} title={c.lastError || ''}>
|
||
{c.lastError || '-'}
|
||
</td>
|
||
</tr>
|
||
))}
|
||
{collectors.length === 0 && !error && (
|
||
<tr>
|
||
<td colSpan={7} style={{ padding: '20px', textAlign: 'center', opacity: 0.5 }}>
|
||
수집기 데이터 로딩 중...
|
||
</td>
|
||
</tr>
|
||
)}
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
);
|
||
};
|
||
|
||
export default CollectorMonitor;
|