import { useState } from 'react'; import { useTranslation } from 'react-i18next'; import { Card, CardContent } from '@shared/components/ui/card'; import { Badge } from '@shared/components/ui/badge'; import { Button } from '@shared/components/ui/button'; import { TabBar, TabButton } from '@shared/components/ui/tabs'; import { PageContainer, PageHeader } from '@shared/components/layout'; import { DataTable, type DataColumn } from '@shared/components/common/DataTable'; import { getConnectionStatusHex } from '@shared/constants/connectionStatuses'; import type { BadgeIntent } from '@lib/theme/variants'; import { Database, RefreshCw, Wifi, WifiOff, Radio, Activity, Server, ArrowDownToLine, AlertTriangle, CheckCircle, BarChart3, Layers, Plus, Play, Square, Trash2, Edit2, Eye, FileText, HardDrive, FolderOpen, Network, } from 'lucide-react'; function jobStatusIntent(s: string): BadgeIntent { if (s === '수행중') return 'success'; if (s === '대기중') return 'warning'; if (s === '장애발생') return 'critical'; return 'muted'; } /* * SFR-03: 통합데이터 허브 수집·연계 관리 * ① 선박신호 수신 현황 — 24시간 타임라인 히트맵 * ② 선박위치정보 모니터링 — 연계 채널 테이블 */ // ─── ① 선박신호 수신 현황 데이터 ────────────── type SignalStatus = 'ok' | 'warn' | 'error'; interface SignalSource { name: string; rate: number; // 수신율 % timeline: SignalStatus[]; // 24시간 × 6 (10분 단위 = 144 슬롯) } function generateTimeline(): SignalStatus[] { return Array.from({ length: 144 }, () => { const r = Math.random(); return r < 0.75 ? 'ok' : r < 0.90 ? 'warn' : 'error'; }); } const SIGNAL_SOURCES: SignalSource[] = [ { name: 'VTS', rate: 88.9, timeline: generateTimeline() }, { name: 'VTS-AIS', rate: 85.4, timeline: generateTimeline() }, { name: 'V-PASS', rate: 84.0, timeline: generateTimeline() }, { name: 'E-NAVI', rate: 88.9, timeline: generateTimeline() }, { name: 'S&P AIS', rate: 85.4, timeline: generateTimeline() }, ]; // SIGNAL_COLORS는 connectionStatuses 카탈로그에서 가져옴 (getConnectionStatusHex) const HOURS = Array.from({ length: 25 }, (_, i) => `${String(i).padStart(2, '0')}시`); // ─── ② 선박위치정보 모니터링 데이터 ────────────── interface ChannelRecord { no: number; source: string; // 원천기관 code: string; // 기관코드 system: string; // 정보시스템명 linkInfo: string; // 연계정보 storage: string; // 저장장소 linkMethod: string; // 연계방식 cycle: string; // 수집주기 vesselCount: string; // 선박건수/신호건수 status: 'ON' | 'OFF'; // 연결상태 lastUpdate: string; // 최종갱신 [key: string]: unknown; } const CHANNELS: ChannelRecord[] = [ { no: 1, source: '부산항', code: 'BS', system: 'VTS_AIS', linkInfo: 'VTS', storage: 'signal_t_dynamic_all_reply', linkMethod: 'KAFKA', cycle: '00:00:00', vesselCount: '439 / 499', status: 'ON', lastUpdate: '2026-03-25 10:29:09' }, { no: 2, source: '부산항', code: 'BS', system: 'VTS_RT', linkInfo: 'VTS', storage: 'signal_t_dynamic_all_reply', linkMethod: 'KAFKA', cycle: '00:00:00', vesselCount: '133 / 463', status: 'ON', lastUpdate: '2026-03-25 10:29:09' }, { no: 3, source: '부산신항', code: 'BSN', system: 'VTS_AIS', linkInfo: 'VTS', storage: 'signal_t_dynamic_all_reply', linkMethod: 'KAFKA', cycle: '00:00:00', vesselCount: '255 / 278', status: 'ON', lastUpdate: '2026-03-25 10:29:09' }, { no: 4, source: '부산신항', code: 'BSN', system: 'VTS_RT', linkInfo: 'VTS', storage: 'signal_t_dynamic_all_reply', linkMethod: 'KAFKA', cycle: '00:00:00', vesselCount: '133 / 426', status: 'ON', lastUpdate: '2026-03-25 10:29:09' }, { no: 5, source: '동해안', code: 'DH', system: 'VTS_AIS', linkInfo: 'VTS', storage: 'signal_t_dynamic_all_reply', linkMethod: 'KAFKA', cycle: '수신대기중', vesselCount: '0', status: 'OFF', lastUpdate: '' }, { no: 6, source: '동해안', code: 'DH', system: 'VTS_RT', linkInfo: 'VTS', storage: 'signal_t_dynamic_all_reply', linkMethod: 'KAFKA', cycle: '수신대기중', vesselCount: '0', status: 'OFF', lastUpdate: '' }, { no: 7, source: '대산항', code: 'DS', system: 'VTS_AIS', linkInfo: 'VTS', storage: 'signal_t_dynamic_all_reply', linkMethod: 'KAFKA', cycle: '00:00:00', vesselCount: '0', status: 'OFF', lastUpdate: '2026-03-15 15:38:57' }, { no: 8, source: '대산항', code: 'DS', system: 'VTS_RT', linkInfo: 'VTS', storage: 'signal_t_dynamic_all_reply', linkMethod: 'KAFKA', cycle: '00:00:00', vesselCount: '0', status: 'OFF', lastUpdate: '2026-03-15 15:38:56' }, { no: 9, source: '경인항', code: 'GI', system: 'VTS_AIS', linkInfo: 'VTS', storage: 'signal_t_dynamic_all_reply', linkMethod: 'KAFKA', cycle: '00:00:00', vesselCount: '120 / 136', status: 'ON', lastUpdate: '2026-03-25 10:29:09' }, { no: 10, source: '경인항', code: 'GI', system: 'VTS_RT', linkInfo: 'VTS', storage: 'signal_t_dynamic_all_reply', linkMethod: 'KAFKA', cycle: '00:00:00', vesselCount: '55 / 467', status: 'ON', lastUpdate: '2026-03-25 10:29:09' }, { no: 11, source: '경인연안', code: 'GIC', system: 'VTS_AIS', linkInfo: 'VTS', storage: 'signal_t_dynamic_all_reply', linkMethod: 'KAFKA', cycle: '00:00:00', vesselCount: '180 / 216', status: 'ON', lastUpdate: '2026-03-25 10:29:09' }, { no: 12, source: '경인연안', code: 'GIC', system: 'VTS_RT', linkInfo: 'VTS', storage: 'signal_t_dynamic_all_reply', linkMethod: 'KAFKA', cycle: '수신대기중', vesselCount: '0', status: 'OFF', lastUpdate: '' }, { no: 13, source: '군산항', code: 'GS', system: 'VTS_AIS', linkInfo: 'VTS', storage: 'signal_t_dynamic_all_reply', linkMethod: 'KAFKA', cycle: '수신대기중', vesselCount: '0', status: 'OFF', lastUpdate: '' }, { no: 14, source: '군산항', code: 'GS', system: 'VTS_RT', linkInfo: 'VTS', storage: 'signal_t_dynamic_all_reply', linkMethod: 'KAFKA', cycle: '수신대기중', vesselCount: '0', status: 'OFF', lastUpdate: '' }, { no: 15, source: '인천항', code: 'IC', system: 'VTS_AIS', linkInfo: 'VTS', storage: 'signal_t_dynamic_all_reply', linkMethod: 'KAFKA', cycle: '00:00:00', vesselCount: '149 / 176', status: 'ON', lastUpdate: '2026-03-25 10:29:09' }, { no: 16, source: '인천항', code: 'IC', system: 'VTS_RT', linkInfo: 'VTS', storage: 'signal_t_dynamic_all_reply', linkMethod: 'KAFKA', cycle: '00:00:00', vesselCount: '55 / 503', status: 'ON', lastUpdate: '2026-03-25 10:29:09' }, { no: 17, source: '진도연안', code: 'JDC', system: 'VTS_AIS', linkInfo: 'VTS', storage: 'signal_t_dynamic_all_reply', linkMethod: 'KAFKA', cycle: '00:00:00', vesselCount: '433 / 524', status: 'ON', lastUpdate: '2026-03-25 10:29:09' }, { no: 18, source: '진도연안', code: 'JDC', system: 'VTS_RT', linkInfo: 'VTS', storage: 'signal_t_dynamic_all_reply', linkMethod: 'KAFKA', cycle: '00:00:00', vesselCount: '256 / 1619', status: 'ON', lastUpdate: '2026-03-25 10:29:09' }, { no: 19, source: '제주항', code: 'JJ', system: 'VTS_AIS', linkInfo: 'VTS', storage: 'signal_t_dynamic_all_reply', linkMethod: 'KAFKA', cycle: '00:00:00', vesselCount: '429 / 508', status: 'ON', lastUpdate: '2026-03-25 10:29:09' }, { no: 20, source: '제주항', code: 'JJ', system: 'VTS_RT', linkInfo: 'VTS', storage: 'signal_t_dynamic_all_reply', linkMethod: 'KAFKA', cycle: '00:00:00', vesselCount: '160 / 1592', status: 'ON', lastUpdate: '2026-03-25 10:29:09' }, { no: 21, source: '목포항', code: 'MP', system: 'VTS_AIS', linkInfo: 'VTS', storage: 'signal_t_dynamic_all_reply', linkMethod: 'KAFKA', cycle: '수신대기중', vesselCount: '0', status: 'OFF', lastUpdate: '' }, { no: 22, source: '목포항', code: 'MP', system: 'VTS_RT', linkInfo: 'VTS', storage: 'signal_t_dynamic_all_reply', linkMethod: 'KAFKA', cycle: '수신대기중', vesselCount: '0', status: 'OFF', lastUpdate: '' }, ]; // ─── 채널 테이블 컬럼 정의 ─────────────────── const channelColumns: DataColumn[] = [ { key: 'no', label: '번호', width: '50px', align: 'center', sortable: true, render: (v) => {v as number}, }, { key: 'source', label: '원천기관', width: '80px', sortable: true, render: (v) => {v as string}, }, { key: 'code', label: '기관코드', width: '65px', align: 'center', render: (v) => {v as string}, }, { key: 'system', label: '정보시스템명', width: '100px', sortable: true, render: (v) => {v as string}, }, { key: 'linkInfo', label: '연계정보', width: '65px' }, { key: 'storage', label: '저장장소', render: (v) => {v as string} }, { key: 'linkMethod', label: '연계방식', width: '70px', align: 'center', render: (v) => {v as string}, }, { key: 'cycle', label: '수집주기', width: '80px', align: 'center', render: (v) => { const s = v as string; return s === '수신대기중' ? {s} : {s}; }, }, { key: 'vesselCount', label: '선박건수/신호건수', width: '120px', align: 'right', sortable: true, render: (v) => {v as string}, }, { key: 'status', label: '연결상태', width: '80px', align: 'center', sortable: true, render: (v, row) => { const on = v === 'ON'; return (
{v as string} {row.lastUpdate && ( {row.lastUpdate as string} )}
); }, }, ]; // ─── 히트맵 컴포넌트 ────────────────────── function SignalTimeline({ source }: { source: SignalSource }) { return (
{/* 라벨 */}
{source.name}
{source.rate}%
{/* 타임라인 바 */}
{source.timeline.map((status, i) => (
))}
); } // ─── ③ 수집 작업 관리 데이터 ────────────────── type JobStatus = '정지' | '대기중' | '수행중' | '장애발생'; type ServerType = 'SQL' | 'FILE' | 'FTP'; interface CollectJob { id: string; name: string; serverType: ServerType; serverName: string; serverIp: string; status: JobStatus; schedule: string; lastRun: string; successRate: number; [key: string]: unknown; } const COLLECT_JOBS: CollectJob[] = [ { id: 'COL-001', name: '부산항 AIS 수집', serverType: 'SQL', serverName: 'vts-bs-db01', serverIp: '10.20.30.11', status: '수행중', schedule: '매 10분', lastRun: '2026-04-03 09:20:00', successRate: 98.5 }, { id: 'COL-002', name: '인천항 VTS 수집', serverType: 'SQL', serverName: 'vts-ic-db01', serverIp: '10.20.30.12', status: '수행중', schedule: '매 10분', lastRun: '2026-04-03 09:20:00', successRate: 97.2 }, { id: 'COL-003', name: 'V-PASS 파일 수집', serverType: 'FILE', serverName: 'vpass-ftp01', serverIp: '10.20.31.20', status: '수행중', schedule: '매 30분', lastRun: '2026-04-03 09:00:00', successRate: 94.1 }, { id: 'COL-004', name: 'E-NAVI 로그 수집', serverType: 'FILE', serverName: 'enavi-nas01', serverIp: '10.20.31.30', status: '대기중', schedule: '매 1시간', lastRun: '2026-04-03 08:00:00', successRate: 91.8 }, { id: 'COL-005', name: 'S&P AIS 해외 수집', serverType: 'FTP', serverName: 'sp-ais-ftp', serverIp: '203.45.67.89', status: '수행중', schedule: '매 15분', lastRun: '2026-04-03 09:15:00', successRate: 85.4 }, { id: 'COL-006', name: '동해안 VTS 수집', serverType: 'SQL', serverName: 'vts-dh-db01', serverIp: '10.20.30.15', status: '장애발생', schedule: '매 10분', lastRun: '2026-04-02 22:10:00', successRate: 0 }, { id: 'COL-007', name: '군산항 레이더 수집', serverType: 'SQL', serverName: 'vts-gs-db01', serverIp: '10.20.30.18', status: '장애발생', schedule: '매 10분', lastRun: '2026-04-01 14:30:00', successRate: 0 }, { id: 'COL-008', name: '제주항 CCTV 수집', serverType: 'FTP', serverName: 'jj-cctv-ftp', serverIp: '10.20.32.50', status: '정지', schedule: '매 1시간', lastRun: '2026-03-28 12:00:00', successRate: 0 }, { id: 'COL-009', name: '위성 SAR 수집', serverType: 'FTP', serverName: 'sat-sar-ftp', serverIp: '10.20.40.10', status: '수행중', schedule: '매 6시간', lastRun: '2026-04-03 06:00:00', successRate: 99.0 }, { id: 'COL-010', name: '해수부 VMS 연동', serverType: 'SQL', serverName: 'mof-vms-db', serverIp: '10.20.50.11', status: '수행중', schedule: '매 5분', lastRun: '2026-04-03 09:20:00', successRate: 96.3 }, ]; const collectColumns: DataColumn[] = [ { key: 'id', label: 'ID', width: '80px', render: (v) => {v as string} }, { key: 'name', label: '작업명', sortable: true, render: (v) => {v as string} }, { key: 'serverType', label: '타입', width: '60px', align: 'center', sortable: true, render: (v) => { const t = v as string; const intent: BadgeIntent = t === 'SQL' ? 'info' : t === 'FILE' ? 'success' : 'purple'; return {t}; }, }, { key: 'serverName', label: '서버명', width: '120px', render: (v) => {v as string} }, { key: 'serverIp', label: 'IP', width: '120px', render: (v) => {v as string} }, { key: 'status', label: '상태', width: '80px', align: 'center', sortable: true, render: (v) => {v as string}, }, { key: 'schedule', label: '스케줄', width: '80px' }, { key: 'lastRun', label: '최종 수행', width: '140px', sortable: true, render: (v) => {v as string} }, { key: 'successRate', label: '성공률', width: '70px', align: 'right', sortable: true, render: (v) => { const n = v as number; if (n === 0) return -; const intent: BadgeIntent = n >= 90 ? 'success' : n >= 70 ? 'warning' : 'critical'; return {n}%; }, }, { key: 'id', label: '', width: '70px', align: 'center', sortable: false, render: (_v, row) => (
{row.status === '정지' ? ( ) : row.status !== '장애발생' ? ( ) : null}
), }, ]; // ─── ④ 적재 작업 관리 데이터 ────────────────── interface LoadJob { id: string; name: string; sourceJob: string; targetTable: string; targetDb: string; status: JobStatus; schedule: string; lastRun: string; recordCount: string; [key: string]: unknown; } const LOAD_JOBS: LoadJob[] = [ { id: 'LOD-001', name: 'AIS 동적정보 적재', sourceJob: 'COL-001', targetTable: 'tb_ais_dynamic', targetDb: 'MDA_DB', status: '수행중', schedule: '매 10분', lastRun: '2026-04-03 09:20:00', recordCount: '12,450' }, { id: 'LOD-002', name: 'VTS 레이더 적재', sourceJob: 'COL-002', targetTable: 'tb_vts_radar', targetDb: 'MDA_DB', status: '수행중', schedule: '매 10분', lastRun: '2026-04-03 09:20:00', recordCount: '8,320' }, { id: 'LOD-003', name: 'V-PASS 위치 적재', sourceJob: 'COL-003', targetTable: 'tb_vpass_position', targetDb: 'MDA_DB', status: '수행중', schedule: '매 30분', lastRun: '2026-04-03 09:00:00', recordCount: '3,210' }, { id: 'LOD-004', name: 'VMS 데이터 적재', sourceJob: 'COL-010', targetTable: 'tb_vms_track', targetDb: 'MDA_DB', status: '수행중', schedule: '매 5분', lastRun: '2026-04-03 09:20:00', recordCount: '5,677' }, { id: 'LOD-005', name: 'SAR 위성 적재', sourceJob: 'COL-009', targetTable: 'tb_sat_imagery', targetDb: 'SAT_DB', status: '대기중', schedule: '매 6시간', lastRun: '2026-04-03 06:00:00', recordCount: '24' }, { id: 'LOD-006', name: '해외 AIS 적재', sourceJob: 'COL-005', targetTable: 'tb_sais_global', targetDb: 'MDA_DB', status: '수행중', schedule: '매 15분', lastRun: '2026-04-03 09:15:00', recordCount: '45,200' }, { id: 'LOD-007', name: '동해안 VTS 적재', sourceJob: 'COL-006', targetTable: 'tb_vts_dh', targetDb: 'MDA_DB', status: '장애발생', schedule: '매 10분', lastRun: '2026-04-02 22:10:00', recordCount: '0' }, ]; const loadColumns: DataColumn[] = [ { key: 'id', label: 'ID', width: '80px', render: (v) => {v as string} }, { key: 'name', label: '작업명', sortable: true, render: (v) => {v as string} }, { key: 'sourceJob', label: '수집원', width: '80px', render: (v) => {v as string} }, { key: 'targetTable', label: '대상 테이블', width: '140px', render: (v) => {v as string} }, { key: 'targetDb', label: 'DB', width: '70px', align: 'center' }, { key: 'status', label: '상태', width: '80px', align: 'center', sortable: true, render: (v) => {v as string}, }, { key: 'schedule', label: '스케줄', width: '80px' }, { key: 'lastRun', label: '최종 적재', width: '140px', sortable: true, render: (v) => {v as string} }, { key: 'recordCount', label: '적재건수', width: '80px', align: 'right', render: (v) => {v as string} }, { key: 'id', label: '', width: '70px', align: 'center', sortable: false, render: () => (
), }, ]; // ─── ⑤ 연계서버 모니터링 데이터 ──────────────── type AgentRole = '수집' | '적재'; interface AgentServer { id: string; name: string; role: AgentRole; hostname: string; ip: string; port: string; mac: string; status: JobStatus; taskCount: number; cpuUsage: number; memUsage: number; diskUsage: number; lastHeartbeat: string; [key: string]: unknown; } const AGENTS: AgentServer[] = [ { id: 'AGT-1001', name: '부산항 AIS 수집Agent', role: '수집', hostname: 'vts-bs-col01', ip: '10.20.30.11', port: '8081', mac: '00:1A:2B:3C:4D:01', status: '수행중', taskCount: 3, cpuUsage: 42, memUsage: 65, diskUsage: 38, lastHeartbeat: '2026-04-03 09:20:05' }, { id: 'AGT-1002', name: '인천항 VTS 수집Agent', role: '수집', hostname: 'vts-ic-col01', ip: '10.20.30.12', port: '8081', mac: '00:1A:2B:3C:4D:02', status: '수행중', taskCount: 2, cpuUsage: 35, memUsage: 52, diskUsage: 41, lastHeartbeat: '2026-04-03 09:20:03' }, { id: 'AGT-1003', name: '제주항 수집Agent', role: '수집', hostname: 'vts-jj-col01', ip: '10.20.30.19', port: '8081', mac: '00:1A:2B:3C:4D:03', status: '수행중', taskCount: 2, cpuUsage: 28, memUsage: 45, diskUsage: 55, lastHeartbeat: '2026-04-03 09:20:02' }, { id: 'AGT-1004', name: '동해안 수집Agent', role: '수집', hostname: 'vts-dh-col01', ip: '10.20.30.15', port: '8081', mac: '00:1A:2B:3C:4D:04', status: '장애발생', taskCount: 0, cpuUsage: 0, memUsage: 0, diskUsage: 72, lastHeartbeat: '2026-04-02 22:10:15' }, { id: 'AGT-1005', name: 'V-PASS 수집Agent', role: '수집', hostname: 'vpass-col01', ip: '10.20.31.20', port: '8082', mac: '00:1A:2B:3C:4D:05', status: '수행중', taskCount: 1, cpuUsage: 18, memUsage: 30, diskUsage: 25, lastHeartbeat: '2026-04-03 09:20:01' }, { id: 'AGT-2001', name: 'MDA DB 적재Agent', role: '적재', hostname: 'mda-lod01', ip: '10.20.40.11', port: '9091', mac: '00:1A:2B:3C:4D:11', status: '수행중', taskCount: 5, cpuUsage: 55, memUsage: 72, diskUsage: 48, lastHeartbeat: '2026-04-03 09:20:04' }, { id: 'AGT-2002', name: 'SAT DB 적재Agent', role: '적재', hostname: 'sat-lod01', ip: '10.20.40.12', port: '9091', mac: '00:1A:2B:3C:4D:12', status: '대기중', taskCount: 1, cpuUsage: 5, memUsage: 22, diskUsage: 33, lastHeartbeat: '2026-04-03 09:20:00' }, { id: 'AGT-2003', name: '백업 적재Agent', role: '적재', hostname: 'bak-lod01', ip: '10.20.40.13', port: '9091', mac: '00:1A:2B:3C:4D:13', status: '정지', taskCount: 0, cpuUsage: 0, memUsage: 12, diskUsage: 15, lastHeartbeat: '2026-03-30 18:00:00' }, ]; function UsageBar({ value, color }: { value: number; color: string }) { return (
{value}%
); } // ─── 메인 컴포넌트 ────────────────────── type Tab = 'signal' | 'monitor' | 'collect' | 'load' | 'agents'; export function DataHub() { const { t } = useTranslation('admin'); const { t: tc } = useTranslation('common'); const [tab, setTab] = useState('signal'); const [selectedDate, setSelectedDate] = useState('2026-04-02'); const [statusFilter, setStatusFilter] = useState<'' | 'ON' | 'OFF'>(''); // 수집 작업 필터 const [collectTypeFilter, setCollectTypeFilter] = useState<'' | ServerType>(''); const [collectStatusFilter, setCollectStatusFilter] = useState<'' | JobStatus>(''); // 적재 작업 필터 const [loadStatusFilter, setLoadStatusFilter] = useState<'' | JobStatus>(''); // 연계서버 필터 const [agentRoleFilter, setAgentRoleFilter] = useState<'' | AgentRole>(''); const [agentStatusFilter, setAgentStatusFilter] = useState<'' | JobStatus>(''); const onCount = CHANNELS.filter((c) => c.status === 'ON').length; const offCount = CHANNELS.filter((c) => c.status === 'OFF').length; const hasPartialOff = offCount > 0; const filteredChannels = statusFilter ? CHANNELS.filter((c) => c.status === statusFilter) : CHANNELS; const filteredCollectJobs = COLLECT_JOBS.filter((j) => (!collectTypeFilter || j.serverType === collectTypeFilter) && (!collectStatusFilter || j.status === collectStatusFilter) ); const filteredLoadJobs = LOAD_JOBS.filter((j) => (!loadStatusFilter || j.status === loadStatusFilter) ); const filteredAgents = AGENTS.filter((a) => (!agentRoleFilter || a.role === agentRoleFilter) && (!agentStatusFilter || a.status === agentStatusFilter) ); return ( }> 새로고침 } /> {/* KPI */}
{[ { label: '전체 채널', value: CHANNELS.length, icon: Layers, color: 'text-label', bg: 'bg-muted' }, { label: 'ON', value: onCount, icon: Wifi, color: 'text-label', bg: 'bg-blue-500/10' }, { label: 'OFF', value: offCount, icon: WifiOff, color: 'text-heading', bg: 'bg-red-500/10' }, { label: '평균 수신율', value: '86.5%', icon: BarChart3, color: 'text-label', bg: 'bg-green-500/10' }, { label: '데이터 소스', value: '5종', icon: Radio, color: 'text-heading', bg: 'bg-purple-500/10' }, { label: '연계 방식', value: 'KAFKA', icon: Server, color: 'text-heading', bg: 'bg-orange-500/10' }, ].map((kpi) => (
{kpi.value} {kpi.label}
))}
{/* 탭 */} {[ { key: 'signal' as Tab, icon: Activity, label: '선박신호 수신 현황' }, { key: 'monitor' as Tab, icon: Server, label: '선박위치정보 모니터링' }, { key: 'collect' as Tab, icon: ArrowDownToLine, label: '수집 작업 관리' }, { key: 'load' as Tab, icon: HardDrive, label: '적재 작업 관리' }, { key: 'agents' as Tab, icon: Network, label: '연계서버 모니터링' }, ].map((t) => ( setTab(t.key)} icon={} > {t.label} ))} {/* ── ① 선박신호 수신 현황 ── */} {tab === 'signal' && ( {/* 상단: 제목 + 날짜 */}
선박신호 수신 현황
setSelectedDate(e.target.value)} className="bg-surface-overlay border border-slate-700/50 rounded-lg px-3 py-1.5 text-[11px] text-heading focus:outline-none focus:border-cyan-500/50" />
{/* 시간축 헤더 */}
{HOURS.map((h, i) => (
{i % 2 === 0 ? h : ''}
))}
{/* 타임라인 히트맵 */}
{SIGNAL_SOURCES.map((src) => ( ))}
{/* 범례 */}
범례:
{[ { label: '정상 수신', color: '#22c55e' }, { label: '지연/경고', color: '#eab308' }, { label: '장애/미수신', color: '#ef4444' }, ].map((item) => (
{item.label}
))}
10분 단위 집계 · 24시간 (144 슬롯)
)} {/* ── ② 선박위치정보 모니터링 ── */} {tab === 'monitor' && (
{/* 상태 요약 바 */}
{hasPartialOff ? ( ) : ( )} {hasPartialOff ? `일부 OFF (${offCount}/${CHANNELS.length})` : '전체 정상'}
연계 채널 {CHANNELS.length}개 (ON: {onCount} / OFF: {offCount}) 갱신: 오후 06:57:33 {/* 상태 필터 */}
{(['', 'ON', 'OFF'] as const).map((f) => ( setStatusFilter(f)} className="px-2.5 py-1 text-[10px]" > {f || '전체'} ))}
{/* DataTable */}
)} {/* ── ③ 수집 작업 관리 ── */} {tab === 'collect' && (
서버 타입: {(['', 'SQL', 'FILE', 'FTP'] as const).map((f) => ( setCollectTypeFilter(f)} className="px-2.5 py-1 text-[10px]"> {f || '전체'} ))} 상태: {(['', '수행중', '대기중', '장애발생', '정지'] as const).map((f) => ( setCollectStatusFilter(f)} className="px-2.5 py-1 text-[10px]"> {f || '전체'} ))}
)} {/* ── ④ 적재 작업 관리 ── */} {tab === 'load' && (
상태: {(['', '수행중', '대기중', '장애발생', '정지'] as const).map((f) => ( setLoadStatusFilter(f)} className="px-2.5 py-1 text-[10px]"> {f || '전체'} ))}
)} {/* ── ⑤ 연계서버 모니터링 ── */} {tab === 'agents' && (
종류: {(['', '수집', '적재'] as const).map((f) => ( setAgentRoleFilter(f)} className="px-2.5 py-1 text-[10px]"> {f || '전체'} ))} 상태: {(['', '수행중', '대기중', '장애발생', '정지'] as const).map((f) => ( setAgentStatusFilter(f)} className="px-2.5 py-1 text-[10px]"> {f || '전체'} ))}
{/* 연계서버 카드 그리드 */}
{filteredAgents.map((agent) => { return (
{agent.name}
{agent.id} · {agent.role}에이전트
{agent.status}
Hostname{agent.hostname}
IP{agent.ip}
Port{agent.port}
MAC{agent.mac}
CPU 80 ? 'bg-red-500' : agent.cpuUsage > 50 ? 'bg-yellow-500' : 'bg-green-500'} />
MEM 80 ? 'bg-red-500' : agent.memUsage > 50 ? 'bg-yellow-500' : 'bg-green-500'} />
DISK 80 ? 'bg-red-500' : agent.diskUsage > 50 ? 'bg-yellow-500' : 'bg-green-500'} />
작업 {agent.taskCount}건 · heartbeat {agent.lastHeartbeat.slice(11)}
); })}
)} ); }