kcg-monitoring/frontend/src/components/common/CollectorMonitor.tsx
htlee 0fd32081b0 refactor(frontend): 패키지 구조 리팩토링 + UI 버그 수정 (#38)
Co-authored-by: htlee <htlee@gcsc.co.kr>
Co-committed-by: htlee <htlee@gcsc.co.kr>
2026-03-18 07:41:19 +09:00

182 lines
6.7 KiB
TypeScript
Raw Blame 히스토리

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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;