"use client"; import React from "react"; import { ArrowRight, Link2, Unlink2, Plus, Trash2, Pencil, X, Loader2 } from "lucide-react"; import { Button } from "@/components/ui/button"; import { Label } from "@/components/ui/label"; import { Checkbox } from "@/components/ui/checkbox"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, } from "@/components/ui/select"; import { PopComponentDefinitionV5, PopDataConnection, } from "../types/pop-layout"; import { PopComponentRegistry, } from "@/lib/registry/PopComponentRegistry"; import { getTableColumns } from "@/lib/api/tableManagement"; // ======================================== // Props // ======================================== interface ConnectionEditorProps { component: PopComponentDefinitionV5; allComponents: PopComponentDefinitionV5[]; connections: PopDataConnection[]; onAddConnection?: (conn: Omit) => void; onUpdateConnection?: (connectionId: string, conn: Omit) => void; onRemoveConnection?: (connectionId: string) => void; } // ======================================== // ConnectionEditor // ======================================== export default function ConnectionEditor({ component, allComponents, connections, onAddConnection, onUpdateConnection, onRemoveConnection, }: ConnectionEditorProps) { const registeredComp = PopComponentRegistry.getComponent(component.type); const meta = registeredComp?.connectionMeta; const outgoing = connections.filter( (c) => c.sourceComponent === component.id ); const incoming = connections.filter( (c) => c.targetComponent === component.id ); const hasSendable = meta?.sendable && meta.sendable.length > 0; const hasReceivable = meta?.receivable && meta.receivable.length > 0; if (!hasSendable && !hasReceivable) { return (

연결 없음

이 컴포넌트는 다른 컴포넌트와 연결할 수 없습니다

); } return (
{hasSendable && ( )} {hasReceivable && ( )}
); } // ======================================== // 보내기 섹션 // ======================================== interface SendSectionProps { component: PopComponentDefinitionV5; allComponents: PopComponentDefinitionV5[]; outgoing: PopDataConnection[]; onAddConnection?: (conn: Omit) => void; onUpdateConnection?: (connectionId: string, conn: Omit) => void; onRemoveConnection?: (connectionId: string) => void; } function SendSection({ component, allComponents, outgoing, onAddConnection, onUpdateConnection, onRemoveConnection, }: SendSectionProps) { const [editingId, setEditingId] = React.useState(null); return (
{outgoing.map((conn) => (
{editingId === conn.id ? ( { onUpdateConnection?.(conn.id, data); setEditingId(null); }} onCancel={() => setEditingId(null)} submitLabel="수정" /> ) : (
{conn.label || `→ ${allComponents.find((c) => c.id === conn.targetComponent)?.label || conn.targetComponent}`} {onRemoveConnection && ( )}
{conn.filterConfig?.targetColumn && (
{conn.filterConfig.targetColumn} {conn.filterConfig.filterMode} {conn.filterConfig.isSubTable && ( 하위 테이블 )}
)}
)}
))} onAddConnection?.(data)} submitLabel="연결 추가" />
); } // ======================================== // 단순 연결 폼 (이벤트 타입: "어디로" 1개만) // ======================================== interface SimpleConnectionFormProps { component: PopComponentDefinitionV5; allComponents: PopComponentDefinitionV5[]; initial?: PopDataConnection; onSubmit: (data: Omit) => void; onCancel?: () => void; submitLabel: string; } function extractSubTableName(comp: PopComponentDefinitionV5): string | null { const cfg = comp.config as Record | undefined; if (!cfg) return null; const grid = cfg.cardGrid as { cells?: Array<{ timelineSource?: { processTable?: string } }> } | undefined; if (grid?.cells) { for (const cell of grid.cells) { if (cell.timelineSource?.processTable) return cell.timelineSource.processTable; } } return null; } function SimpleConnectionForm({ component, allComponents, initial, onSubmit, onCancel, submitLabel, }: SimpleConnectionFormProps) { const [selectedTargetId, setSelectedTargetId] = React.useState( initial?.targetComponent || "" ); const [isSubTable, setIsSubTable] = React.useState( initial?.filterConfig?.isSubTable || false ); const [targetColumn, setTargetColumn] = React.useState( initial?.filterConfig?.targetColumn || "" ); const [filterMode, setFilterMode] = React.useState( initial?.filterConfig?.filterMode || "equals" ); const [subColumns, setSubColumns] = React.useState([]); const [loadingColumns, setLoadingColumns] = React.useState(false); const targetCandidates = allComponents.filter((c) => { if (c.id === component.id) return false; const reg = PopComponentRegistry.getComponent(c.type); return reg?.connectionMeta?.receivable && reg.connectionMeta.receivable.length > 0; }); const sourceReg = PopComponentRegistry.getComponent(component.type); const targetComp = allComponents.find((c) => c.id === selectedTargetId); const targetReg = targetComp ? PopComponentRegistry.getComponent(targetComp.type) : null; const isFilterConnection = sourceReg?.connectionMeta?.sendable?.some((s) => s.type === "filter_value") && targetReg?.connectionMeta?.receivable?.some((r) => r.type === "filter_value"); const subTableName = targetComp ? extractSubTableName(targetComp) : null; React.useEffect(() => { if (!isSubTable || !subTableName) { setSubColumns([]); return; } setLoadingColumns(true); getTableColumns(subTableName) .then((res) => { const cols = res.success && res.data?.columns; if (Array.isArray(cols)) { setSubColumns(cols.map((c) => c.columnName || "").filter(Boolean)); } }) .catch(() => setSubColumns([])) .finally(() => setLoadingColumns(false)); }, [isSubTable, subTableName]); const handleSubmit = () => { if (!selectedTargetId) return; const tComp = allComponents.find((c) => c.id === selectedTargetId); const srcLabel = component.label || component.id; const tgtLabel = tComp?.label || tComp?.id || "?"; const conn: Omit = { sourceComponent: component.id, sourceField: "", sourceOutput: "_auto", targetComponent: selectedTargetId, targetField: "", targetInput: "_auto", label: `${srcLabel} → ${tgtLabel}`, }; if (isFilterConnection && isSubTable && targetColumn) { conn.filterConfig = { targetColumn, filterMode: filterMode as "equals" | "contains" | "starts_with" | "range", isSubTable: true, }; } onSubmit(conn); if (!initial) { setSelectedTargetId(""); setIsSubTable(false); setTargetColumn(""); setFilterMode("equals"); } }; return (
{onCancel && (

연결 수정

)} {!onCancel && (

새 연결 추가

)}
어디로?
{isFilterConnection && selectedTargetId && subTableName && (
{ setIsSubTable(v === true); if (!v) setTargetColumn(""); }} />
{isSubTable && (
대상 컬럼 {loadingColumns ? (
컬럼 로딩 중...
) : ( )}
비교 방식
)}
)}
); } // ======================================== // 받기 섹션 (읽기 전용: 연결된 소스만 표시) // ======================================== interface ReceiveSectionProps { component: PopComponentDefinitionV5; allComponents: PopComponentDefinitionV5[]; incoming: PopDataConnection[]; } function ReceiveSection({ component, allComponents, incoming, }: ReceiveSectionProps) { return (
{incoming.length > 0 ? (
{incoming.map((conn) => { const sourceComp = allComponents.find( (c) => c.id === conn.sourceComponent ); return (
{sourceComp?.label || conn.sourceComponent}
); })}
) : (

연결된 소스가 없습니다

)}
); }