'use client'; import React, { useState } from 'react'; import styled from 'styled-components'; import Link from 'next/link'; import StatusBadge from '../common/StatusBadge'; import { theme } from '@/styles/theme'; type Status = 'online' | 'offline' | 'working' | 'unknown'; interface OrgNodeData { id?: number; name: string; role: string; description?: string; status?: Status; lastSeen?: string | null; lxcId?: number; isOwner?: boolean; } interface PopupState { node: OrgNodeData; x: number; y: number; } const sisterEmojis: Record = { harang: '🦊', narang: '🦊', darang: '🐱', erang: '🐺', μžκΈ°μ•Ό: 'πŸ‘€', }; const sisterDisplayNames: Record = { harang: 'ν•˜λž‘μ΄', narang: 'λ‚˜λž‘μ΄', darang: 'λ‹€λž‘μ΄', erang: 'μ΄λž‘μ΄', }; const statusColors: Record = { online: '#00FF00', offline: '#FF1744', working: '#2979FF', unknown: 'var(--text-secondary)', }; // ─── 쑰직도 λ ˆμ΄μ•„μ›ƒ ─── const Tree = styled.div` display: flex; flex-direction: column; align-items: center; gap: 0; padding: 20px 0; position: relative; overflow-x: auto; `; const Level = styled.div` display: flex; justify-content: center; gap: 24px; position: relative; `; const ConnectorV = styled.div` width: 2px; height: 32px; background: linear-gradient(to bottom, var(--border-color), rgba(88,166,255,0.4)); margin: 0 auto; `; const ConnectorH = styled.div` position: absolute; top: 0; left: 50%; transform: translateX(-50%); height: 2px; background: rgba(88,166,255,0.3); width: calc(100% - 100px); `; const BranchWrap = styled.div` position: relative; display: flex; gap: 24px; align-items: flex-start; `; const NodeCard = styled.div<{ $status?: Status; $isOwner?: boolean }>` background: var(--bg-surface); backdrop-filter: blur(12px); border: 1px solid ${({ $status }) => $status ? `${statusColors[$status]}66` : 'var(--border-color)'}; border-radius: 12px; padding: ${({ $isOwner }) => $isOwner ? '14px 24px' : '12px 18px'}; min-width: ${({ $isOwner }) => $isOwner ? '140px' : '130px'}; text-align: center; cursor: pointer; transition: transform 0.2s, box-shadow 0.2s, border-color 0.2s; position: relative; &:hover { transform: translateY(-3px); box-shadow: 0 8px 24px rgba(0,0,0,0.4); border-color: ${({ $status }) => $status ? statusColors[$status] : '#58A6FF'}; } `; const NodeEmoji = styled.div` font-size: 22px; margin-bottom: 4px; `; const NodeName = styled.div` font-size: 14px; font-weight: 700; color: var(--text-primary); margin-bottom: 2px; `; const NodeRole = styled.div` font-size: 11px; color: var(--text-secondary); `; const StatusDot = styled.div<{ $status: Status }>` width: 8px; height: 8px; border-radius: 50%; background: ${({ $status }) => statusColors[$status]}; position: absolute; top: 8px; right: 8px; ${({ $status }) => $status === 'online' && ` box-shadow: 0 0 6px #00FF00; `} `; const PipelineLabel = styled.div` font-size: 10px; color: rgba(88,166,255,0.6); text-align: center; margin-top: 2px; `; // Popup const Popup = styled.div<{ $x: number; $y: number }>` position: fixed; left: ${({ $x }) => $x}px; top: ${({ $y }) => $y}px; z-index: 200; background: rgba(22,27,34,0.97); border: 1px solid var(--border-color); border-radius: 10px; padding: 14px 16px; min-width: 200px; max-width: 260px; pointer-events: none; box-shadow: 0 8px 32px rgba(0,0,0,0.5); `; const PopupName = styled.div` font-size: 14px; font-weight: 700; color: var(--text-primary); margin-bottom: 4px; `; const PopupRole = styled.div` font-size: 12px; color: #58A6FF; margin-bottom: 8px; `; const PopupDesc = styled.div` font-size: 12px; color: var(--text-secondary); line-height: 1.5; `; function formatLastSeen(lastSeen: string | null | undefined): string { if (!lastSeen) return '기둝 μ—†μŒ'; const diff = Date.now() - new Date(lastSeen).getTime(); const min = Math.floor(diff / 60000); if (min < 1) return '방금 μ „'; if (min < 60) return `${min}λΆ„ μ „`; return `${Math.floor(min / 60)}μ‹œκ°„ μ „`; } interface OrgTreeProps { sisters: OrgNodeData[]; } export default function OrgTree({ sisters }: OrgTreeProps) { const [popup, setPopup] = useState(null); const harang = sisters.find((s) => s.name === 'harang'); const narang = sisters.find((s) => s.name === 'narang'); const darang = sisters.find((s) => s.name === 'darang'); const erang = sisters.find((s) => s.name === 'erang'); const handleHover = (node: OrgNodeData, e: React.MouseEvent) => { const rect = (e.target as HTMLElement).getBoundingClientRect(); setPopup({ node, x: rect.right + 10, y: rect.top }); }; const renderNode = (node: OrgNodeData | undefined, isOwner = false) => { if (!node) return null; const displayName = sisterDisplayNames[node.name] ?? node.name; const status = node.status ?? 'unknown'; const isClickable = !isOwner; return ( handleHover(node, e)} onMouseLeave={() => setPopup(null)} > {!isOwner && } {sisterEmojis[node.name] ?? 'πŸ€–'} {displayName} {node.role} ); }; return ( <> {/* μžκΈ°μ•Ό */} {renderNode({ name: 'μžκΈ°μ•Ό', role: 'Owner', isOwner: true }, true)} {/* ν•˜λž‘μ΄ */} {renderNode(harang)} {/* νŒŒμ΄ν”„λΌμΈ 라벨 */}
ν•Έλ“œμ˜€ν”„ ↓ ↓ 배포 μš”μ²­
{/* λ‚˜λž‘μ΄ / λ‹€λž‘μ΄ / μ΄λž‘μ΄ */}
{renderNode(narang)} {renderNode(darang)} {renderNode(erang)}
{/* νŒŒμ΄ν”„λΌμΈ μ„€λͺ… */}
Generator ↔ 리뷰/μˆ˜μ • ↔ Evaluator Infra
{popup && ( {sisterEmojis[popup.node.name] ?? 'πŸ€–'}{' '} {sisterDisplayNames[popup.node.name] ?? popup.node.name} {popup.node.role} {popup.node.description && ( {popup.node.description} )} {!popup.node.isOwner && ( μƒνƒœ: {popup.node.status ?? 'unknown'}
λ§ˆμ§€λ§‰ ν™œλ™: {formatLastSeen(popup.node.lastSeen)}
)}
)} ); }