- CollectorMonitor: 29건 인라인 → CSS 클래스 (~3건 동적만 잔존) - 팝업 공통 CSS: .popup-header, .popup-body, .popup-grid, .popup-label 추출 - AirportLayer, DamagedShipLayer, InfraLayer, SubmarineCableLayer 적용 - LoginPage: var(--kcg-*) 인라인 → Tailwind 유틸리티 전환 (hover 포함) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
143 lines
4.6 KiB
TypeScript
143 lines
4.6 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 className='collector-modal'>
|
||
{/* Header */}
|
||
<div className='collector-header'>
|
||
<h3 className='collector-title'>
|
||
수집기 모니터링
|
||
</h3>
|
||
<div className='collector-header-actions'>
|
||
{serverTime && (
|
||
<span className='collector-server-time'>
|
||
서버: {new Date(serverTime).toLocaleTimeString('ko-KR')}
|
||
</span>
|
||
)}
|
||
<button
|
||
onClick={refresh}
|
||
className='collector-refresh-btn'
|
||
>
|
||
새로고침
|
||
</button>
|
||
<button
|
||
onClick={onClose}
|
||
className='collector-close-btn'
|
||
>
|
||
×
|
||
</button>
|
||
</div>
|
||
</div>
|
||
|
||
{error && (
|
||
<div className='collector-error'>
|
||
{error}
|
||
</div>
|
||
)}
|
||
|
||
{/* Table */}
|
||
<table className='collector-table'>
|
||
<thead>
|
||
<tr>
|
||
<th className='collector-th'>상태</th>
|
||
<th className='collector-th'>수집기</th>
|
||
<th className='collector-th-right'>최근 건수</th>
|
||
<th className='collector-th-right'>성공/실패</th>
|
||
<th className='collector-th-right'>총 수집</th>
|
||
<th className='collector-th'>마지막 성공</th>
|
||
<th className='collector-th'>에러</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody>
|
||
{collectors.map((c) => (
|
||
<tr key={c.name}>
|
||
<td className='collector-td'>
|
||
<span
|
||
className='collector-status-dot'
|
||
style={{ backgroundColor: getStatusColor(c) }}
|
||
/>
|
||
</td>
|
||
<td className='collector-td-name'>{c.name}</td>
|
||
<td className='collector-td-right'>{c.lastCount}</td>
|
||
<td className='collector-td-right'>
|
||
<span className='collector-success-count'>{c.totalSuccess}</span>
|
||
{' / '}
|
||
<span style={{ color: c.totalFailure > 0 ? '#ef4444' : 'inherit' }}>{c.totalFailure}</span>
|
||
</td>
|
||
<td className='collector-td-right'>{c.totalItems.toLocaleString()}</td>
|
||
<td className='collector-td-last-success'>{formatRelativeTime(c.lastSuccess)}</td>
|
||
<td
|
||
className='collector-td-error'
|
||
style={{
|
||
color: c.lastError ? '#ef4444' : 'inherit',
|
||
opacity: c.lastError ? 1 : 0.4,
|
||
}}
|
||
title={c.lastError || ''}
|
||
>
|
||
{c.lastError || '-'}
|
||
</td>
|
||
</tr>
|
||
))}
|
||
{collectors.length === 0 && !error && (
|
||
<tr>
|
||
<td colSpan={7} className='collector-td-empty'>
|
||
수집기 데이터 로딩 중...
|
||
</td>
|
||
</tr>
|
||
)}
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
);
|
||
};
|
||
|
||
export default CollectorMonitor;
|