'use client'; import React, { useEffect, useState } from 'react'; import styled from 'styled-components'; import { API_URL } from '@/lib/config'; // ─── Styled ─── const MainViewport = styled.div` display: flex; flex-direction: column; gap: var(--space-xl); min-height: 80vh; `; const HeaderInfo = styled.div` display: flex; gap: var(--space-xl); font-family: var(--font-mono); font-size: 11px; color: var(--text-secondary); border-bottom: 1px solid var(--border-color); padding-bottom: var(--space-md); span { color: var(--text-primary); } `; const TreeContainer = styled.div` display: flex; flex-direction: column; align-items: center; overflow-x: auto; padding: var(--space-xl) 0; @media (max-width: 767px) { align-items: flex-start; } `; const NodeGroup = styled.div` display: flex; flex-direction: column; align-items: center; position: relative; `; const TerminalCard = styled.div<{ $level: 'high' | 'mid' | 'low' | 'none' }>` border: 1px solid ${({ $level }) => ({ high: '#f43f5e', mid: '#3b82f6', low: '#10b981', none: '#525252', })[$level]}; border-left-width: 3px; padding: var(--space-md) var(--space-lg); background: var(--bg-main); min-width: 200px; max-width: 260px; box-shadow: 0 10px 30px rgba(0,0,0,0.5); transition: border-color 0.2s, box-shadow 0.2s; position: relative; cursor: default; &:hover { box-shadow: 0 12px 36px rgba(0,0,0,0.7); } `; const NodeLabel = styled.div` font-size: 14px; font-weight: 600; color: var(--text-primary); margin-bottom: var(--space-sm); span { color: var(--text-secondary); margin-right: var(--space-xs); font-family: var(--font-mono); font-size: 11px; } `; const BadgeRow = styled.div` display: flex; gap: var(--space-xs); flex-wrap: wrap; `; const Badge = styled.div` background: #1a1a1a; border: 1px solid var(--border-color); padding: 2px 6px; font-size: 9px; font-family: var(--font-mono); font-weight: 700; text-transform: uppercase; letter-spacing: 0.04em; color: var(--text-secondary); b { color: var(--text-primary); font-weight: normal; } `; const TreeConnector = styled.div` width: 1px; height: 24px; background: var(--border-color); margin: 0 auto; `; const TreeBranch = styled.div` display: flex; flex-direction: column; align-items: center; `; const TreeChildren = styled.div` display: flex; gap: var(--space-xl); align-items: flex-start; position: relative; &::before { content: ''; position: absolute; top: 0; left: calc(var(--space-xl) / 2 + 100px); right: calc(var(--space-xl) / 2 + 100px); height: 1px; background: var(--border-color); } @media (max-width: 767px) { flex-direction: column; gap: var(--space-lg); &::before { display: none; } } `; const PageFooter = styled.footer` margin-top: auto; width: 100%; padding: var(--space-lg) 0; display: flex; justify-content: space-between; border-top: 1px solid var(--border-color); font-family: var(--font-mono); color: var(--text-secondary); font-size: 10px; `; type Level = 'high' | 'mid' | 'low' | 'none'; interface OrgData { owner: { name: string; role: string }; sisters: Array<{ name: string; role: string; description?: string; status?: string; lxcId?: number; }>; pipeline: Array<{ from: string; to: string; label: string }>; } const LEVEL_MAP: Record = { Orchestrator: 'high', Generator: 'mid', Evaluator: 'mid', 'Infra Manager': 'low', }; const ROLE_TAG: Record = { Orchestrator: 'ORCH', Generator: 'GEN', Evaluator: 'EVAL', 'Infra Manager': 'INFRA', }; const LXC_ID: Record = { harang: 104, narang: 105, darang: 106, erang: 107, }; export default function OrgPage() { const [orgData, setOrgData] = useState(null); const ts = new Date().toLocaleDateString('ko-KR').replace(/\. /g, '.').replace('.', '') + '_' + new Date().toLocaleTimeString('ko-KR', { hour12: false, hour: '2-digit', minute: '2-digit' }); useEffect(() => { fetch(`${API_URL}/api/org`) .then((r) => r.json()) .then(setOrgData) .catch(() => {}); }, []); const sisters = orgData?.sisters ?? []; return (
SYS_TYPE: CORE_ROOT_01
TIMESTAMP: {ts}
{/* HQ */} [HQ] 하나랑 글로벌 MEM: {sisters.length + 1} S_ADMIN LV_09 {/* 자매 레벨 */} {/* 하랑이 (Orchestrator) */} {sisters.filter((s) => s.role === 'Orchestrator').map((s) => ( [{ROLE_TAG[s.role] ?? s.role}] {s.name === 'harang' ? '하랑이' : s.name} LXC: {LXC_ID[s.name] ?? '---'} {ROLE_TAG[s.role] ?? s.role} {s.status === 'online' ? 'ACTIVE' : s.status === 'offline' ? 'STBY' : 'RUN'} {/* 나랑/다랑/이랑 */} {sisters.filter((s2) => s2.role !== 'Orchestrator').map((s2) => ( {s2.name === 'narang' ? '나랑이' : s2.name === 'darang' ? '다랑이' : '이랑이'} LXC: {LXC_ID[s2.name] ?? '---'} {ROLE_TAG[s2.role] ?? s2.role} {s2.status === 'online' ? 'ON' : s2.status === 'offline' ? '--' : 'RUN'} ))} ))} {/* Orchestrator 없을 때 fallback */} {sisters.filter((s) => s.role === 'Orchestrator').length === 0 && ( 데이터 없음 N/A )}
TOTAL_NODES: {String(sisters.length + 1).padStart(2, '0')}
STATUS: SYNC_COMPLETE
); }