2026-02-23 18:45:21 +09:00
|
|
|
"use client";
|
|
|
|
|
|
|
|
|
|
import React from "react";
|
2026-03-11 12:07:11 +09:00
|
|
|
import { ArrowRight, Link2, Unlink2, Plus, Trash2, Pencil, X, Loader2 } from "lucide-react";
|
2026-02-23 18:45:21 +09:00
|
|
|
import { Button } from "@/components/ui/button";
|
|
|
|
|
import { Label } from "@/components/ui/label";
|
2026-03-11 12:07:11 +09:00
|
|
|
import { Checkbox } from "@/components/ui/checkbox";
|
2026-02-23 18:45:21 +09:00
|
|
|
import {
|
|
|
|
|
Select,
|
|
|
|
|
SelectContent,
|
|
|
|
|
SelectItem,
|
|
|
|
|
SelectTrigger,
|
|
|
|
|
SelectValue,
|
|
|
|
|
} from "@/components/ui/select";
|
|
|
|
|
import {
|
|
|
|
|
PopComponentDefinitionV5,
|
|
|
|
|
PopDataConnection,
|
|
|
|
|
} from "../types/pop-layout";
|
|
|
|
|
import {
|
|
|
|
|
PopComponentRegistry,
|
|
|
|
|
} from "@/lib/registry/PopComponentRegistry";
|
2026-03-11 12:07:11 +09:00
|
|
|
import { getTableColumns } from "@/lib/api/tableManagement";
|
2026-02-23 18:45:21 +09:00
|
|
|
|
|
|
|
|
// ========================================
|
|
|
|
|
// Props
|
|
|
|
|
// ========================================
|
|
|
|
|
|
|
|
|
|
interface ConnectionEditorProps {
|
|
|
|
|
component: PopComponentDefinitionV5;
|
|
|
|
|
allComponents: PopComponentDefinitionV5[];
|
|
|
|
|
connections: PopDataConnection[];
|
|
|
|
|
onAddConnection?: (conn: Omit<PopDataConnection, "id">) => void;
|
|
|
|
|
onUpdateConnection?: (connectionId: string, conn: Omit<PopDataConnection, "id">) => 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 (
|
|
|
|
|
<div className="space-y-4">
|
|
|
|
|
<div className="rounded-lg bg-gray-50 p-4 text-center">
|
|
|
|
|
<Link2 className="mx-auto mb-2 h-8 w-8 text-muted-foreground" />
|
|
|
|
|
<p className="text-sm font-medium text-gray-700">연결 없음</p>
|
|
|
|
|
<p className="mt-1 text-xs text-muted-foreground">
|
|
|
|
|
이 컴포넌트는 다른 컴포넌트와 연결할 수 없습니다
|
|
|
|
|
</p>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
<div className="space-y-6">
|
|
|
|
|
{hasSendable && (
|
|
|
|
|
<SendSection
|
|
|
|
|
component={component}
|
|
|
|
|
allComponents={allComponents}
|
|
|
|
|
outgoing={outgoing}
|
|
|
|
|
onAddConnection={onAddConnection}
|
|
|
|
|
onUpdateConnection={onUpdateConnection}
|
|
|
|
|
onRemoveConnection={onRemoveConnection}
|
|
|
|
|
/>
|
|
|
|
|
)}
|
|
|
|
|
|
|
|
|
|
{hasReceivable && (
|
|
|
|
|
<ReceiveSection
|
|
|
|
|
component={component}
|
|
|
|
|
allComponents={allComponents}
|
|
|
|
|
incoming={incoming}
|
|
|
|
|
/>
|
|
|
|
|
)}
|
|
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ========================================
|
|
|
|
|
// 보내기 섹션
|
|
|
|
|
// ========================================
|
|
|
|
|
|
|
|
|
|
interface SendSectionProps {
|
|
|
|
|
component: PopComponentDefinitionV5;
|
|
|
|
|
allComponents: PopComponentDefinitionV5[];
|
|
|
|
|
outgoing: PopDataConnection[];
|
|
|
|
|
onAddConnection?: (conn: Omit<PopDataConnection, "id">) => void;
|
|
|
|
|
onUpdateConnection?: (connectionId: string, conn: Omit<PopDataConnection, "id">) => void;
|
|
|
|
|
onRemoveConnection?: (connectionId: string) => void;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function SendSection({
|
|
|
|
|
component,
|
|
|
|
|
allComponents,
|
|
|
|
|
outgoing,
|
|
|
|
|
onAddConnection,
|
|
|
|
|
onUpdateConnection,
|
|
|
|
|
onRemoveConnection,
|
|
|
|
|
}: SendSectionProps) {
|
|
|
|
|
const [editingId, setEditingId] = React.useState<string | null>(null);
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
<div className="space-y-3">
|
|
|
|
|
<Label className="flex items-center gap-1 text-xs font-medium">
|
|
|
|
|
<ArrowRight className="h-3 w-3 text-blue-500" />
|
2026-03-03 15:30:07 +09:00
|
|
|
보내기
|
2026-02-23 18:45:21 +09:00
|
|
|
</Label>
|
|
|
|
|
|
|
|
|
|
{outgoing.map((conn) => (
|
|
|
|
|
<div key={conn.id}>
|
|
|
|
|
{editingId === conn.id ? (
|
2026-03-06 11:00:31 +09:00
|
|
|
<SimpleConnectionForm
|
|
|
|
|
component={component}
|
|
|
|
|
allComponents={allComponents}
|
|
|
|
|
initial={conn}
|
|
|
|
|
onSubmit={(data) => {
|
|
|
|
|
onUpdateConnection?.(conn.id, data);
|
|
|
|
|
setEditingId(null);
|
|
|
|
|
}}
|
|
|
|
|
onCancel={() => setEditingId(null)}
|
|
|
|
|
submitLabel="수정"
|
|
|
|
|
/>
|
2026-02-23 18:45:21 +09:00
|
|
|
) : (
|
2026-03-11 12:07:11 +09:00
|
|
|
<div className="space-y-1 rounded border bg-blue-50/50 px-3 py-2">
|
|
|
|
|
<div className="flex items-center gap-1">
|
2026-02-23 18:45:21 +09:00
|
|
|
<span className="flex-1 truncate text-xs">
|
2026-03-03 15:30:07 +09:00
|
|
|
{conn.label || `→ ${allComponents.find((c) => c.id === conn.targetComponent)?.label || conn.targetComponent}`}
|
2026-02-23 18:45:21 +09:00
|
|
|
</span>
|
|
|
|
|
<button
|
|
|
|
|
onClick={() => setEditingId(conn.id)}
|
|
|
|
|
className="shrink-0 p-0.5 text-muted-foreground hover:text-primary"
|
|
|
|
|
>
|
|
|
|
|
<Pencil className="h-3 w-3" />
|
|
|
|
|
</button>
|
|
|
|
|
{onRemoveConnection && (
|
|
|
|
|
<button
|
|
|
|
|
onClick={() => onRemoveConnection(conn.id)}
|
|
|
|
|
className="shrink-0 p-0.5 text-muted-foreground hover:text-destructive"
|
|
|
|
|
>
|
|
|
|
|
<Trash2 className="h-3 w-3" />
|
|
|
|
|
</button>
|
|
|
|
|
)}
|
2026-03-11 12:07:11 +09:00
|
|
|
</div>
|
|
|
|
|
{conn.filterConfig?.targetColumn && (
|
|
|
|
|
<div className="flex flex-wrap gap-1">
|
|
|
|
|
<span className="rounded bg-white px-1.5 py-0.5 text-[9px] text-muted-foreground">
|
|
|
|
|
{conn.filterConfig.targetColumn}
|
|
|
|
|
</span>
|
|
|
|
|
<span className="rounded bg-white px-1.5 py-0.5 text-[9px] text-muted-foreground">
|
|
|
|
|
{conn.filterConfig.filterMode}
|
|
|
|
|
</span>
|
|
|
|
|
{conn.filterConfig.isSubTable && (
|
|
|
|
|
<span className="rounded bg-amber-100 px-1.5 py-0.5 text-[9px] text-amber-700">
|
|
|
|
|
하위 테이블
|
|
|
|
|
</span>
|
|
|
|
|
)}
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
2026-02-23 18:45:21 +09:00
|
|
|
</div>
|
|
|
|
|
)}
|
|
|
|
|
</div>
|
|
|
|
|
))}
|
|
|
|
|
|
2026-03-06 11:00:31 +09:00
|
|
|
<SimpleConnectionForm
|
|
|
|
|
component={component}
|
|
|
|
|
allComponents={allComponents}
|
|
|
|
|
onSubmit={(data) => onAddConnection?.(data)}
|
|
|
|
|
submitLabel="연결 추가"
|
|
|
|
|
/>
|
2026-02-23 18:45:21 +09:00
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ========================================
|
2026-03-03 15:30:07 +09:00
|
|
|
// 단순 연결 폼 (이벤트 타입: "어디로" 1개만)
|
2026-02-23 18:45:21 +09:00
|
|
|
// ========================================
|
|
|
|
|
|
2026-03-03 15:30:07 +09:00
|
|
|
interface SimpleConnectionFormProps {
|
|
|
|
|
component: PopComponentDefinitionV5;
|
|
|
|
|
allComponents: PopComponentDefinitionV5[];
|
|
|
|
|
initial?: PopDataConnection;
|
|
|
|
|
onSubmit: (data: Omit<PopDataConnection, "id">) => void;
|
|
|
|
|
onCancel?: () => void;
|
|
|
|
|
submitLabel: string;
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-11 12:07:11 +09:00
|
|
|
function extractSubTableName(comp: PopComponentDefinitionV5): string | null {
|
|
|
|
|
const cfg = comp.config as Record<string, unknown> | 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;
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-03 15:30:07 +09:00
|
|
|
function SimpleConnectionForm({
|
|
|
|
|
component,
|
|
|
|
|
allComponents,
|
|
|
|
|
initial,
|
|
|
|
|
onSubmit,
|
|
|
|
|
onCancel,
|
|
|
|
|
submitLabel,
|
|
|
|
|
}: SimpleConnectionFormProps) {
|
|
|
|
|
const [selectedTargetId, setSelectedTargetId] = React.useState(
|
|
|
|
|
initial?.targetComponent || ""
|
|
|
|
|
);
|
2026-03-11 12:07:11 +09:00
|
|
|
const [isSubTable, setIsSubTable] = React.useState(
|
|
|
|
|
initial?.filterConfig?.isSubTable || false
|
|
|
|
|
);
|
|
|
|
|
const [targetColumn, setTargetColumn] = React.useState(
|
|
|
|
|
initial?.filterConfig?.targetColumn || ""
|
|
|
|
|
);
|
|
|
|
|
const [filterMode, setFilterMode] = React.useState<string>(
|
|
|
|
|
initial?.filterConfig?.filterMode || "equals"
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
const [subColumns, setSubColumns] = React.useState<string[]>([]);
|
|
|
|
|
const [loadingColumns, setLoadingColumns] = React.useState(false);
|
2026-03-03 15:30:07 +09:00
|
|
|
|
|
|
|
|
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;
|
|
|
|
|
});
|
|
|
|
|
|
2026-03-11 12:07:11 +09:00
|
|
|
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]);
|
|
|
|
|
|
2026-03-03 15:30:07 +09:00
|
|
|
const handleSubmit = () => {
|
|
|
|
|
if (!selectedTargetId) return;
|
|
|
|
|
|
2026-03-11 12:07:11 +09:00
|
|
|
const tComp = allComponents.find((c) => c.id === selectedTargetId);
|
2026-03-03 15:30:07 +09:00
|
|
|
const srcLabel = component.label || component.id;
|
2026-03-11 12:07:11 +09:00
|
|
|
const tgtLabel = tComp?.label || tComp?.id || "?";
|
2026-03-03 15:30:07 +09:00
|
|
|
|
2026-03-11 12:07:11 +09:00
|
|
|
const conn: Omit<PopDataConnection, "id"> = {
|
2026-03-03 15:30:07 +09:00
|
|
|
sourceComponent: component.id,
|
|
|
|
|
sourceField: "",
|
|
|
|
|
sourceOutput: "_auto",
|
|
|
|
|
targetComponent: selectedTargetId,
|
|
|
|
|
targetField: "",
|
|
|
|
|
targetInput: "_auto",
|
|
|
|
|
label: `${srcLabel} → ${tgtLabel}`,
|
2026-03-11 12:07:11 +09:00
|
|
|
};
|
|
|
|
|
|
|
|
|
|
if (isFilterConnection && isSubTable && targetColumn) {
|
|
|
|
|
conn.filterConfig = {
|
|
|
|
|
targetColumn,
|
|
|
|
|
filterMode: filterMode as "equals" | "contains" | "starts_with" | "range",
|
|
|
|
|
isSubTable: true,
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
onSubmit(conn);
|
2026-03-03 15:30:07 +09:00
|
|
|
|
|
|
|
|
if (!initial) {
|
|
|
|
|
setSelectedTargetId("");
|
2026-03-11 12:07:11 +09:00
|
|
|
setIsSubTable(false);
|
|
|
|
|
setTargetColumn("");
|
|
|
|
|
setFilterMode("equals");
|
2026-03-03 15:30:07 +09:00
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
<div className="space-y-2 rounded border border-dashed p-3">
|
|
|
|
|
{onCancel && (
|
|
|
|
|
<div className="flex items-center justify-between">
|
|
|
|
|
<p className="text-[10px] font-medium text-muted-foreground">연결 수정</p>
|
|
|
|
|
<button onClick={onCancel} className="text-muted-foreground hover:text-foreground">
|
|
|
|
|
<X className="h-3 w-3" />
|
|
|
|
|
</button>
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
|
|
|
|
{!onCancel && (
|
|
|
|
|
<p className="text-[10px] font-medium text-muted-foreground">새 연결 추가</p>
|
|
|
|
|
)}
|
|
|
|
|
|
|
|
|
|
<div className="space-y-1">
|
|
|
|
|
<span className="text-[10px] text-muted-foreground">어디로?</span>
|
|
|
|
|
<Select
|
|
|
|
|
value={selectedTargetId}
|
2026-03-11 12:07:11 +09:00
|
|
|
onValueChange={(v) => {
|
|
|
|
|
setSelectedTargetId(v);
|
|
|
|
|
setIsSubTable(false);
|
|
|
|
|
setTargetColumn("");
|
|
|
|
|
}}
|
2026-03-03 15:30:07 +09:00
|
|
|
>
|
|
|
|
|
<SelectTrigger className="h-7 text-xs">
|
|
|
|
|
<SelectValue placeholder="컴포넌트 선택" />
|
|
|
|
|
</SelectTrigger>
|
|
|
|
|
<SelectContent>
|
|
|
|
|
{targetCandidates.map((c) => (
|
|
|
|
|
<SelectItem key={c.id} value={c.id} className="text-xs">
|
|
|
|
|
{c.label || c.id}
|
|
|
|
|
</SelectItem>
|
|
|
|
|
))}
|
|
|
|
|
</SelectContent>
|
|
|
|
|
</Select>
|
|
|
|
|
</div>
|
|
|
|
|
|
2026-03-11 12:07:11 +09:00
|
|
|
{isFilterConnection && selectedTargetId && subTableName && (
|
|
|
|
|
<div className="space-y-2 rounded bg-muted/50 p-2">
|
|
|
|
|
<div className="flex items-center gap-2">
|
|
|
|
|
<Checkbox
|
|
|
|
|
id={`isSubTable_${component.id}`}
|
|
|
|
|
checked={isSubTable}
|
|
|
|
|
onCheckedChange={(v) => {
|
|
|
|
|
setIsSubTable(v === true);
|
|
|
|
|
if (!v) setTargetColumn("");
|
|
|
|
|
}}
|
|
|
|
|
/>
|
|
|
|
|
<label htmlFor={`isSubTable_${component.id}`} className="text-[10px] text-muted-foreground cursor-pointer">
|
|
|
|
|
하위 테이블 기준으로 필터 ({subTableName})
|
|
|
|
|
</label>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
{isSubTable && (
|
|
|
|
|
<div className="space-y-2 pl-5">
|
|
|
|
|
<div className="space-y-1">
|
|
|
|
|
<span className="text-[10px] text-muted-foreground">대상 컬럼</span>
|
|
|
|
|
{loadingColumns ? (
|
|
|
|
|
<div className="flex items-center gap-1 py-1">
|
|
|
|
|
<Loader2 className="h-3 w-3 animate-spin text-muted-foreground" />
|
|
|
|
|
<span className="text-[10px] text-muted-foreground">컬럼 로딩 중...</span>
|
|
|
|
|
</div>
|
|
|
|
|
) : (
|
|
|
|
|
<Select value={targetColumn} onValueChange={setTargetColumn}>
|
|
|
|
|
<SelectTrigger className="h-7 text-xs">
|
|
|
|
|
<SelectValue placeholder="컬럼 선택" />
|
|
|
|
|
</SelectTrigger>
|
|
|
|
|
<SelectContent>
|
|
|
|
|
{subColumns.filter(Boolean).map((col) => (
|
|
|
|
|
<SelectItem key={col} value={col} className="text-xs">
|
|
|
|
|
{col}
|
|
|
|
|
</SelectItem>
|
|
|
|
|
))}
|
|
|
|
|
</SelectContent>
|
|
|
|
|
</Select>
|
|
|
|
|
)}
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<div className="space-y-1">
|
|
|
|
|
<span className="text-[10px] text-muted-foreground">비교 방식</span>
|
|
|
|
|
<Select value={filterMode} onValueChange={setFilterMode}>
|
|
|
|
|
<SelectTrigger className="h-7 text-xs">
|
|
|
|
|
<SelectValue />
|
|
|
|
|
</SelectTrigger>
|
|
|
|
|
<SelectContent>
|
|
|
|
|
<SelectItem value="equals" className="text-xs">일치 (equals)</SelectItem>
|
|
|
|
|
<SelectItem value="contains" className="text-xs">포함 (contains)</SelectItem>
|
|
|
|
|
<SelectItem value="starts_with" className="text-xs">시작 (starts_with)</SelectItem>
|
|
|
|
|
</SelectContent>
|
|
|
|
|
</Select>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
|
|
|
|
|
2026-03-03 15:30:07 +09:00
|
|
|
<Button
|
|
|
|
|
size="sm"
|
|
|
|
|
variant="outline"
|
|
|
|
|
className="h-7 w-full text-xs"
|
|
|
|
|
disabled={!selectedTargetId}
|
|
|
|
|
onClick={handleSubmit}
|
|
|
|
|
>
|
|
|
|
|
{!initial && <Plus className="mr-1 h-3 w-3" />}
|
|
|
|
|
{submitLabel}
|
|
|
|
|
</Button>
|
|
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-23 18:45:21 +09:00
|
|
|
// ========================================
|
2026-03-03 15:30:07 +09:00
|
|
|
// 받기 섹션 (읽기 전용: 연결된 소스만 표시)
|
2026-02-23 18:45:21 +09:00
|
|
|
// ========================================
|
|
|
|
|
|
|
|
|
|
interface ReceiveSectionProps {
|
|
|
|
|
component: PopComponentDefinitionV5;
|
|
|
|
|
allComponents: PopComponentDefinitionV5[];
|
|
|
|
|
incoming: PopDataConnection[];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function ReceiveSection({
|
|
|
|
|
component,
|
|
|
|
|
allComponents,
|
|
|
|
|
incoming,
|
|
|
|
|
}: ReceiveSectionProps) {
|
|
|
|
|
return (
|
|
|
|
|
<div className="space-y-3">
|
|
|
|
|
<Label className="flex items-center gap-1 text-xs font-medium">
|
|
|
|
|
<Unlink2 className="h-3 w-3 text-green-500" />
|
2026-03-03 15:30:07 +09:00
|
|
|
받기
|
2026-02-23 18:45:21 +09:00
|
|
|
</Label>
|
|
|
|
|
|
|
|
|
|
{incoming.length > 0 ? (
|
2026-03-03 15:30:07 +09:00
|
|
|
<div className="space-y-1">
|
2026-02-23 18:45:21 +09:00
|
|
|
{incoming.map((conn) => {
|
|
|
|
|
const sourceComp = allComponents.find(
|
|
|
|
|
(c) => c.id === conn.sourceComponent
|
|
|
|
|
);
|
|
|
|
|
return (
|
|
|
|
|
<div
|
|
|
|
|
key={conn.id}
|
2026-03-03 15:30:07 +09:00
|
|
|
className="flex items-center gap-2 rounded border bg-green-50/50 px-3 py-2 text-xs"
|
2026-02-23 18:45:21 +09:00
|
|
|
>
|
2026-03-03 15:30:07 +09:00
|
|
|
<ArrowRight className="h-3 w-3 text-green-500" />
|
2026-02-23 18:45:21 +09:00
|
|
|
<span className="truncate">
|
|
|
|
|
{sourceComp?.label || conn.sourceComponent}
|
|
|
|
|
</span>
|
|
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
})}
|
|
|
|
|
</div>
|
|
|
|
|
) : (
|
|
|
|
|
<p className="text-xs text-muted-foreground">
|
2026-03-03 15:30:07 +09:00
|
|
|
연결된 소스가 없습니다
|
2026-02-23 18:45:21 +09:00
|
|
|
</p>
|
|
|
|
|
)}
|
|
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|