'use client'; import React, { useEffect, useState } from 'react'; import styled from 'styled-components'; import Link from 'next/link'; import { PageTitle, LabelMeta, BtnToggle } from '@/components/ui/base'; import { API_URL } from '@/lib/config'; const SISTER_ROLES: Record = { harang: 'Primary', narang: 'Secondary', darang: 'Standby', erang: 'Sync', }; const GRAPH_BARS: Record = { harang: [40, 45, 60, 55, 70, 85, 92, 84], narang: [30, 32, 35, 40, 38, 45, 50, 48], darang: [10, 10, 5, 5, 5, 5, 2, 2], erang: [20, 25, 15, 30, 40, 35, 20, 15], }; const NodeGrid = styled.section` display: flex; flex-direction: column; gap: var(--space-lg); `; const NodeEntry = styled.div` display: grid; grid-template-columns: 220px 1fr 160px; gap: 0; border: 1px solid var(--border-color); transition: border-color 0.2s; &:hover { border-color: var(--border-hover); } @media (max-width: 1199px) { grid-template-columns: 200px 1fr; } @media (max-width: 767px) { grid-template-columns: 1fr; } `; const NodeInfo = styled.div` border-right: 1px solid var(--border-color); padding: var(--space-lg); display: flex; flex-direction: column; gap: var(--space-md); @media (max-width: 767px) { border-right: none; border-bottom: 1px solid var(--border-color); } `; const NodeName = styled(Link)` display: flex; align-items: center; gap: var(--space-sm); font-size: 16px; font-weight: 600; color: var(--text-primary); text-decoration: none; transition: color 0.15s; &:hover { color: var(--accent-hover); } `; const StatusIndicator = styled.div<{ $active: boolean }>` width: 8px; height: 8px; border-radius: 50%; background: ${({ $active }) => $active ? '#FFF' : '#555'}; flex-shrink: 0; ${({ $active }) => $active && `box-shadow: 0 0 8px rgba(255,255,255,0.3);`} `; const MetaPair = styled.div` display: flex; flex-direction: column; gap: 2px; `; const MetaVal = styled.div` font-size: 13px; color: var(--text-primary); font-family: var(--font-mono); `; const NodeControls = styled.div` display: flex; gap: var(--space-sm); flex-wrap: wrap; `; const CapacitySection = styled.div` border-right: 1px solid var(--border-color); padding: var(--space-lg); display: flex; flex-direction: column; gap: var(--space-md); @media (max-width: 1199px) { border-right: none; } @media (max-width: 767px) { border-bottom: 1px solid var(--border-color); } `; const GraphContainer = styled.div` display: flex; align-items: flex-end; gap: 3px; height: 48px; `; const GraphBar = styled.div<{ $height: number; $highlight?: boolean }>` flex: 1; height: ${({ $height }) => $height}%; min-height: 2px; background: ${({ $highlight }) => $highlight ? 'var(--text-primary)' : 'var(--border-hover)'}; transition: height 0.3s; `; const SyncRow = styled.div` display: flex; justify-content: space-between; font-family: var(--font-mono); font-size: 11px; color: var(--text-secondary); padding: 2px 0; `; const SyncHistory = styled.div` padding: var(--space-lg); display: flex; flex-direction: column; gap: var(--space-sm); @media (max-width: 1199px) { display: none; } `; function getNodeStatus(s: any): boolean { return s.status === 'online' || s.status === 'working'; } function getStatusBtns(s: any): [string, string] { if (s.status === 'offline') return ['STANDBY', 'ACTIVATE']; if (s.status === 'working') return ['ACTIVE', 'REBOOT']; return ['ACTIVE', 'REBOOT']; } function formatLastSeen(lastSeen: string | null): string { if (!lastSeen) return '—'; const diff = Date.now() - new Date(lastSeen).getTime(); const h = Math.floor(diff / 3600000); const m = Math.floor((diff % 3600000) / 60000); const s = Math.floor((diff % 60000) / 1000); return `${String(h).padStart(2,'0')}:${String(m).padStart(2,'0')}:${String(s).padStart(2,'0')} ago`; } const MOCK_SISTERS = [ { id: 1, name: 'harang', status: 'online', lastSeen: new Date(Date.now() - 3600000).toISOString() }, { id: 2, name: 'narang', status: 'working', lastSeen: new Date(Date.now() - 1800000).toISOString() }, { id: 3, name: 'darang', status: 'offline', lastSeen: new Date(Date.now() - 7200000).toISOString() }, { id: 4, name: 'erang', status: 'online', lastSeen: new Date(Date.now() - 900000).toISOString() }, ]; export default function SistersPage() { const [sisters, setSisters] = useState(MOCK_SISTERS); useEffect(() => { fetch(`${API_URL}/api/sisters`) .then((r) => r.json()) .then(setSisters) .catch(() => {}); const iv = setInterval(() => { fetch(`${API_URL}/api/sisters`).then((r) => r.json()).then(setSisters).catch(() => {}); }, 15000); return () => clearInterval(iv); }, []); return ( <>
자매 노드 관리 TOTAL NODES: {String(sisters.length).padStart(2, '0')}
{sisters.map((s) => { const isActive = getNodeStatus(s); const [btn1, btn2] = getStatusBtns(s); const bars = GRAPH_BARS[s.name] ?? [5, 5, 5, 5, 5, 5, 5, 5]; const loadIndex = isActive ? `${(bars[bars.length - 1]).toFixed(1)}%` : '0.0%'; const metaLabel = s.status === 'offline' ? 'Last Active' : s.name === 'erang' ? 'Queue Depth' : 'Uptime'; const metaVal = s.status === 'offline' ? formatLastSeen(s.lastSeen) : s.name === 'erang' ? '12 PKTS' : formatLastSeen(s.lastSeen).replace(' ago', ''); return ( {s.name === 'harang' ? '하랑' : s.name === 'narang' ? '나랑' : s.name === 'darang' ? '다랑' : '이랑'}{' '} ({SISTER_ROLES[s.name] ?? s.name}) {metaLabel} {metaVal} {btn1} {btn2} CAPACITY HISTORY (24H) {bars.map((h, i) => ( = bars.length - 2} /> ))} LOAD INDEX {loadIndex} SYNC LOG {[ { time: '14:05:22', status: isActive ? 'SUCCESS' : 'IDLE' }, { time: '13:55:01', status: isActive ? 'SUCCESS' : 'IDLE' }, { time: '13:44:10', status: isActive ? 'SUCCESS' : 'IDLE' }, ].map((row) => ( {row.time} {row.status} ))} ); })} ); }