"use client"; import { useLayoutEffect, useRef, useState } from "react"; import { ChevronRight, ChevronDown, MoreHorizontal, Info, Users, Layers, AlertTriangle, ArrowUpRight, KeyRound, Copy, PauseCircle, Trash2, UserPlus, RefreshCw, } from "lucide-react"; import StatusDot from "./StatusDot"; const TABS = [ { key: "overview", label: "개요", Icon: Info, soon: false }, { key: "members", label: "구성원", Icon: Users, soon: true }, { key: "templates", label: "템플릿", Icon: Layers, soon: true }, { key: "danger", label: "위험 영역", Icon: AlertTriangle, soon: false }, ] as const; /** * 단일 회사 row. 클릭 시 accordion 펼쳐지며 탭 4개 (개요/구성원/템플릿/위험영역) 표시. * 목업 v9 AccRow 포팅. API 데이터 스키마는 /companies-stats 응답. */ export default function CompanyAccordionRow({ r, open, onToggle, }: { r: Record; open: boolean; onToggle: () => void; }) { const [tab, setTab] = useState<"overview" | "members" | "templates" | "danger">("overview"); const tabsWrapRef = useRef(null); const tabBtnRefs = useRef>({}); const [indicator, setIndicator] = useState<{ left: number; width: number }>({ left: 0, width: 0 }); useLayoutEffect(() => { if (!open) return; const el = tabBtnRefs.current[tab]; const wrap = tabsWrapRef.current; if (el && wrap) { const r1 = el.getBoundingClientRect(); const r2 = wrap.getBoundingClientRect(); setIndicator({ left: r1.left - r2.left, width: r1.width }); } }, [tab, open]); const sub = r.subdomain || ""; const name = r.company_name || r.name || r.company_code; const plan = (r.plan || "Starter").toString(); const dbName = r.db_name || `${sub}_vexplor`; const dbPct = Number(r.db_pct) || 0; const users = Number(r.users) || 0; const active30 = Number(r.active30) || 0; return (
{/* tabs */}
{TABS.map(({ key: k, label: l, Icon: IconC, soon }) => ( ))}
{r.created && <>생성 {formatDate(r.created)}} {r.writer && <> · writer {r.writer}}
{/* sliding indicator */}
{tab === "overview" && (
{/* 기본정보 */}
기본 정보
{( [ ["회사 코드", r.company_code, true], ["회사명", name, false], ["서브도메인", sub ? : "—", true], ["DB명", dbName, true], ["사업자번호", r.brn || "—", true], ["플랜", plan, false], ["업종", r.industry || "—", false], ["대표자", r.owner || "—", false], ] as const ).map(([l, v, mono], i) => (
{l} {v as any}
))}
{/* 운영지표 + 액션 */}
운영 지표
{( [ ["총 사용자", users], ["30일 활성", active30], ["DB", r.db_size || "—"], ["설치 템플릿", r.templates || 0], ] as const ).map(([l, v], i) => (
{l}
{v}
))}
} onClick={(e) => { e.stopPropagation(); if (sub) window.open(`http://${sub}.invyone.com`, "_blank"); }} > 회사 사이트 열기 } soon>관리자 계정 } soon>템플릿 재복제
)} {tab === "members" && ( 구성원 목록은 회사별 tenant DB 에서 실시간 조회로 표시 예정 (Phase 4). 현재 총 {users}명. )} {tab === "templates" && ( 이 회사에 설치된 템플릿 그룹 {r.templates || 0}개. 상세 보기는 Phase 4 에서. )} {tab === "danger" && (
{[ { t: "회사 비활성화", d: "사용자 로그인 차단 · 데이터 보존 · 언제든 재활성 가능", b: "비활성화", c: "var(--v5-amber)", Icon: PauseCircle, }, { t: "관리자 비밀번호 재설정", d: "무작위 비밀번호 재설정 · 1회 표시", b: "재설정", c: "var(--v5-primary)", Icon: KeyRound, }, { t: "회사 영구 삭제", d: "회사 + 테넌트 DB 영구 삭제 · 복구 불가", b: "삭제 예약", c: "var(--v5-red)", Icon: Trash2, }, ].map((row, i) => { const IconC = row.Icon; return (
{row.t}
{row.d}
); })}
)}
); } const labelSm: React.CSSProperties = { fontSize: "0.58rem", color: "var(--v5-text-sec)", letterSpacing: "0.06em", textTransform: "uppercase", fontWeight: 700, marginBottom: 2, fontFamily: "var(--v5-font-mono)", }; const sectionTitle: React.CSSProperties = { fontSize: "0.62rem", color: "var(--v5-text-sec)", letterSpacing: "0.06em", textTransform: "uppercase", fontWeight: 700, marginBottom: "0.55rem", fontFamily: "var(--v5-font-mono)", }; function SubdomainLine({ sub }: { sub: string }) { return ( {sub} .invyone.com ); } function ABtn({ children, icon, onClick, soon, }: { children: React.ReactNode; icon?: React.ReactNode; onClick?: (e: React.MouseEvent) => void; soon?: boolean; }) { return ( ); } function EmptyNote({ children }: { children: React.ReactNode }) { return (
{children}
); } function formatDate(v: any): string { if (!v) return "—"; try { const d = typeof v === "number" ? new Date(v) : new Date(String(v)); if (isNaN(d.getTime())) return String(v); return d.toISOString().slice(0, 10); } catch { return String(v); } } function formatRelative(v: any): string { if (!v) return ""; try { const d = typeof v === "number" ? new Date(v) : new Date(String(v)); if (isNaN(d.getTime())) return ""; const diffMs = Date.now() - d.getTime(); const sec = Math.round(diffMs / 1000); if (sec < 60) return "방금"; const min = Math.round(sec / 60); if (min < 60) return `${min}분 전`; const hr = Math.round(min / 60); if (hr < 24) return `${hr}시간 전`; const day = Math.round(hr / 24); if (day < 7) return `${day}일 전`; const wk = Math.round(day / 7); if (wk < 5) return `${wk}주 전`; const mo = Math.round(day / 30); if (mo < 12) return `${mo}개월 전`; const yr = Math.round(day / 365); return `${yr}년 전`; } catch { return ""; } } const PLAN_STYLE: Record = { ENTERPRISE: { bg: "rgba(var(--v5-primary-rgb), 0.12)", color: "var(--v5-primary)", border: "rgba(var(--v5-primary-rgb), 0.3)", }, STANDARD: { bg: "rgba(var(--v5-cyan-rgb), 0.1)", color: "var(--v5-cyan)", border: "rgba(var(--v5-cyan-rgb), 0.3)", }, STARTER: { bg: "var(--v5-bg-subtle)", color: "var(--v5-text-sec)", border: "var(--v5-border)", }, }; function PlanBadge({ plan }: { plan: string }) { const key = plan.toUpperCase(); const s = PLAN_STYLE[key] || PLAN_STYLE.STARTER; return ( {key} ); }