From e8516d9d6b1f8498e93b30a6563e4bed4685fa80 Mon Sep 17 00:00:00 2001 From: leeheejin Date: Fri, 9 Jan 2026 11:44:14 +0900 Subject: [PATCH] =?UTF-8?q?feat:=20DELETE=20=EB=85=B8=EB=93=9C=20WHERE=20?= =?UTF-8?q?=EC=A1=B0=EA=B1=B4=EC=97=90=20=EC=86=8C=EC=8A=A4=20=ED=95=84?= =?UTF-8?q?=EB=93=9C=20=EC=84=A0=ED=83=9D=20=EA=B8=B0=EB=8A=A5=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 소스 필드 목록을 연결된 입력 노드에서 자동으로 로딩 - WHERE 조건에 소스 필드 선택 Combobox 추가 - 정적 값과 소스 필드 중 선택 가능 - 조건 변경 시 자동 저장 기능 추가 --- .../properties/DeleteActionProperties.tsx | 250 +++++++++++++++++- 1 file changed, 244 insertions(+), 6 deletions(-) diff --git a/frontend/components/dataflow/node-editor/panels/properties/DeleteActionProperties.tsx b/frontend/components/dataflow/node-editor/panels/properties/DeleteActionProperties.tsx index 16eca3cd..b30bc1f4 100644 --- a/frontend/components/dataflow/node-editor/panels/properties/DeleteActionProperties.tsx +++ b/frontend/components/dataflow/node-editor/panels/properties/DeleteActionProperties.tsx @@ -4,7 +4,7 @@ * DELETE 액션 노드 속성 편집 */ -import { useEffect, useState } from "react"; +import { useEffect, useState, useCallback } from "react"; import { Plus, Trash2, AlertTriangle, Database, Globe, Link2, Check, ChevronsUpDown } from "lucide-react"; import { Label } from "@/components/ui/label"; import { Input } from "@/components/ui/input"; @@ -24,6 +24,12 @@ interface DeleteActionPropertiesProps { data: DeleteActionNodeData; } +// 소스 필드 타입 +interface SourceField { + name: string; + label?: string; +} + const OPERATORS = [ { value: "EQUALS", label: "=" }, { value: "NOT_EQUALS", label: "≠" }, @@ -34,7 +40,7 @@ const OPERATORS = [ ] as const; export function DeleteActionProperties({ nodeId, data }: DeleteActionPropertiesProps) { - const { updateNode, getExternalConnectionsCache } = useFlowEditorStore(); + const { updateNode, getExternalConnectionsCache, nodes, edges } = useFlowEditorStore(); // 🔥 타겟 타입 상태 const [targetType, setTargetType] = useState<"internal" | "external" | "api">(data.targetType || "internal"); @@ -43,6 +49,10 @@ export function DeleteActionProperties({ nodeId, data }: DeleteActionPropertiesP const [targetTable, setTargetTable] = useState(data.targetTable); const [whereConditions, setWhereConditions] = useState(data.whereConditions || []); + // 🆕 소스 필드 목록 (연결된 입력 노드에서 가져오기) + const [sourceFields, setSourceFields] = useState([]); + const [sourceFieldsOpenState, setSourceFieldsOpenState] = useState([]); + // 🔥 외부 DB 관련 상태 const [externalConnections, setExternalConnections] = useState([]); const [externalConnectionsLoading, setExternalConnectionsLoading] = useState(false); @@ -124,8 +134,106 @@ export function DeleteActionProperties({ nodeId, data }: DeleteActionPropertiesP // whereConditions 변경 시 fieldOpenState 초기화 useEffect(() => { setFieldOpenState(new Array(whereConditions.length).fill(false)); + setSourceFieldsOpenState(new Array(whereConditions.length).fill(false)); }, [whereConditions.length]); + // 🆕 소스 필드 로딩 (연결된 입력 노드에서) + const loadSourceFields = useCallback(async () => { + // 현재 노드로 연결된 엣지 찾기 + const incomingEdges = edges.filter((e) => e.target === nodeId); + console.log("🔍 DELETE 노드 연결 엣지:", incomingEdges); + + if (incomingEdges.length === 0) { + console.log("⚠️ 연결된 소스 노드가 없습니다"); + setSourceFields([]); + return; + } + + const fields: SourceField[] = []; + const processedFields = new Set(); + + for (const edge of incomingEdges) { + const sourceNode = nodes.find((n) => n.id === edge.source); + if (!sourceNode) continue; + + console.log("🔗 소스 노드:", sourceNode.type, sourceNode.data); + + // 소스 노드 타입에 따라 필드 추출 + if (sourceNode.type === "trigger" && sourceNode.data.tableName) { + // 트리거 노드: 테이블 컬럼 조회 + try { + const columns = await tableTypeApi.getColumns(sourceNode.data.tableName); + if (columns && Array.isArray(columns)) { + columns.forEach((col: any) => { + const colName = col.columnName || col.column_name; + if (!processedFields.has(colName)) { + processedFields.add(colName); + fields.push({ + name: colName, + label: col.columnLabel || col.column_label || colName, + }); + } + }); + } + } catch (error) { + console.error("트리거 노드 컬럼 로딩 실패:", error); + } + } else if (sourceNode.type === "tableSource" && sourceNode.data.tableName) { + // 테이블 소스 노드 + try { + const columns = await tableTypeApi.getColumns(sourceNode.data.tableName); + if (columns && Array.isArray(columns)) { + columns.forEach((col: any) => { + const colName = col.columnName || col.column_name; + if (!processedFields.has(colName)) { + processedFields.add(colName); + fields.push({ + name: colName, + label: col.columnLabel || col.column_label || colName, + }); + } + }); + } + } catch (error) { + console.error("테이블 소스 노드 컬럼 로딩 실패:", error); + } + } else if (sourceNode.type === "condition") { + // 조건 노드: 연결된 이전 노드에서 필드 가져오기 + const conditionIncomingEdges = edges.filter((e) => e.target === sourceNode.id); + for (const condEdge of conditionIncomingEdges) { + const condSourceNode = nodes.find((n) => n.id === condEdge.source); + if (condSourceNode?.type === "trigger" && condSourceNode.data.tableName) { + try { + const columns = await tableTypeApi.getColumns(condSourceNode.data.tableName); + if (columns && Array.isArray(columns)) { + columns.forEach((col: any) => { + const colName = col.columnName || col.column_name; + if (!processedFields.has(colName)) { + processedFields.add(colName); + fields.push({ + name: colName, + label: col.columnLabel || col.column_label || colName, + }); + } + }); + } + } catch (error) { + console.error("조건 노드 소스 컬럼 로딩 실패:", error); + } + } + } + } + } + + console.log("✅ DELETE 노드 소스 필드:", fields); + setSourceFields(fields); + }, [nodeId, nodes, edges]); + + // 소스 필드 로딩 + useEffect(() => { + loadSourceFields(); + }, [loadSourceFields]); + const loadExternalConnections = async () => { try { setExternalConnectionsLoading(true); @@ -239,22 +347,41 @@ export function DeleteActionProperties({ nodeId, data }: DeleteActionPropertiesP field: "", operator: "EQUALS", value: "", + sourceField: undefined, + staticValue: undefined, }, ]; setWhereConditions(newConditions); setFieldOpenState(new Array(newConditions.length).fill(false)); + setSourceFieldsOpenState(new Array(newConditions.length).fill(false)); + + // 자동 저장 + updateNode(nodeId, { + whereConditions: newConditions, + }); }; const handleRemoveCondition = (index: number) => { const newConditions = whereConditions.filter((_, i) => i !== index); setWhereConditions(newConditions); setFieldOpenState(new Array(newConditions.length).fill(false)); + setSourceFieldsOpenState(new Array(newConditions.length).fill(false)); + + // 자동 저장 + updateNode(nodeId, { + whereConditions: newConditions, + }); }; const handleConditionChange = (index: number, field: string, value: any) => { const newConditions = [...whereConditions]; newConditions[index] = { ...newConditions[index], [field]: value }; setWhereConditions(newConditions); + + // 자동 저장 + updateNode(nodeId, { + whereConditions: newConditions, + }); }; const handleSave = () => { @@ -840,14 +967,125 @@ export function DeleteActionProperties({ nodeId, data }: DeleteActionPropertiesP + {/* 🆕 소스 필드 - Combobox */}
- + + {sourceFields.length > 0 ? ( + { + const newState = [...sourceFieldsOpenState]; + newState[index] = open; + setSourceFieldsOpenState(newState); + }} + > + + + + + + + + 필드를 찾을 수 없습니다. + + { + handleConditionChange(index, "sourceField", undefined); + const newState = [...sourceFieldsOpenState]; + newState[index] = false; + setSourceFieldsOpenState(newState); + }} + className="text-xs text-gray-400 sm:text-sm" + > + + 없음 (정적 값 사용) + + {sourceFields.map((field) => ( + { + handleConditionChange(index, "sourceField", currentValue); + const newState = [...sourceFieldsOpenState]; + newState[index] = false; + setSourceFieldsOpenState(newState); + }} + className="text-xs sm:text-sm" + > + +
+ {field.label || field.name} + {field.label && field.label !== field.name && ( + + {field.name} + + )} +
+
+ ))} +
+
+
+
+
+ ) : ( +
+ 연결된 소스 노드가 없습니다 +
+ )} +

소스 데이터에서 값을 가져올 필드

+
+ + {/* 정적 값 */} +
+ handleConditionChange(index, "value", e.target.value)} - placeholder="비교 값" + value={condition.staticValue || condition.value || ""} + onChange={(e) => { + handleConditionChange(index, "staticValue", e.target.value || undefined); + handleConditionChange(index, "value", e.target.value); + }} + placeholder="비교할 고정 값" className="mt-1 h-8 text-xs" /> +

소스 필드가 비어있을 때 사용됩니다