From e48cc4deccf67206adfa0638337a0ff7c788aa7f Mon Sep 17 00:00:00 2001 From: kjs Date: Wed, 8 Oct 2025 09:39:13 +0900 Subject: [PATCH] =?UTF-8?q?=EB=85=B8=EB=93=9C=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/services/nodeFlowExecutionService.ts | 19 +- .../dataflow/node-editor/FlowEditor.tsx | 2 + .../node-editor/nodes/ReferenceLookupNode.tsx | 108 +++ .../node-editor/panels/PropertiesPanel.tsx | 4 + .../panels/properties/ConditionProperties.tsx | 203 +++++- .../properties/ReferenceLookupProperties.tsx | 643 ++++++++++++++++++ .../node-editor/sidebar/nodePaletteConfig.ts | 8 + frontend/types/node-editor.ts | 31 + 8 files changed, 1000 insertions(+), 18 deletions(-) create mode 100644 frontend/components/dataflow/node-editor/nodes/ReferenceLookupNode.tsx create mode 100644 frontend/components/dataflow/node-editor/panels/properties/ReferenceLookupProperties.tsx diff --git a/backend-node/src/services/nodeFlowExecutionService.ts b/backend-node/src/services/nodeFlowExecutionService.ts index 43cee38a..b27e6784 100644 --- a/backend-node/src/services/nodeFlowExecutionService.ts +++ b/backend-node/src/services/nodeFlowExecutionService.ts @@ -1866,10 +1866,25 @@ export class NodeFlowExecutionService { const results = conditions.map((condition: any) => { const fieldValue = inputData[condition.field]; + + // ๐Ÿ”ฅ ๋น„๊ต ๊ฐ’ ํƒ€์ž… ํ™•์ธ: "field" (ํ•„๋“œ ์ฐธ์กฐ) ๋˜๋Š” "static" (๊ณ ์ •๊ฐ’) + let compareValue = condition.value; + if (condition.valueType === "field") { + // ํ•„๋“œ ์ฐธ์กฐ: inputData์—์„œ ํ•ด๋‹น ํ•„๋“œ์˜ ๊ฐ’์„ ๊ฐ€์ ธ์˜ด + compareValue = inputData[condition.value]; + logger.info( + `๐Ÿ”„ ํ•„๋“œ ์ฐธ์กฐ ๋น„๊ต: ${condition.field} (${fieldValue}) vs ${condition.value} (${compareValue})` + ); + } else { + logger.info( + `๐Ÿ“Š ๊ณ ์ •๊ฐ’ ๋น„๊ต: ${condition.field} (${fieldValue}) vs ${compareValue}` + ); + } + return this.evaluateCondition( fieldValue, condition.operator, - condition.value + compareValue ); }); @@ -1878,7 +1893,7 @@ export class NodeFlowExecutionService { ? results.some((r: boolean) => r) : results.every((r: boolean) => r); - logger.info(`๐Ÿ” ์กฐ๊ฑด ํ‰๊ฐ€ ๊ฒฐ๊ณผ: ${result}`); + logger.info(`๐Ÿ” ์กฐ๊ฑด ํ‰๊ฐ€ ๊ฒฐ๊ณผ: ${result} (${logic} ๋กœ์ง)`); return result; } diff --git a/frontend/components/dataflow/node-editor/FlowEditor.tsx b/frontend/components/dataflow/node-editor/FlowEditor.tsx index 42a88aff..bf1b03a6 100644 --- a/frontend/components/dataflow/node-editor/FlowEditor.tsx +++ b/frontend/components/dataflow/node-editor/FlowEditor.tsx @@ -14,6 +14,7 @@ import { PropertiesPanel } from "./panels/PropertiesPanel"; import { FlowToolbar } from "./FlowToolbar"; import { TableSourceNode } from "./nodes/TableSourceNode"; import { ExternalDBSourceNode } from "./nodes/ExternalDBSourceNode"; +import { ReferenceLookupNode } from "./nodes/ReferenceLookupNode"; import { ConditionNode } from "./nodes/ConditionNode"; import { FieldMappingNode } from "./nodes/FieldMappingNode"; import { InsertActionNode } from "./nodes/InsertActionNode"; @@ -31,6 +32,7 @@ const nodeTypes = { tableSource: TableSourceNode, externalDBSource: ExternalDBSourceNode, restAPISource: RestAPISourceNode, + referenceLookup: ReferenceLookupNode, // ๋ณ€ํ™˜/์กฐ๊ฑด condition: ConditionNode, fieldMapping: FieldMappingNode, diff --git a/frontend/components/dataflow/node-editor/nodes/ReferenceLookupNode.tsx b/frontend/components/dataflow/node-editor/nodes/ReferenceLookupNode.tsx new file mode 100644 index 00000000..181d7dad --- /dev/null +++ b/frontend/components/dataflow/node-editor/nodes/ReferenceLookupNode.tsx @@ -0,0 +1,108 @@ +"use client"; + +/** + * ์ฐธ์กฐ ํ…Œ์ด๋ธ” ์กฐํšŒ ๋…ธ๋“œ (๋‚ด๋ถ€ DB ์ „์šฉ) + * ๋‹ค๋ฅธ ํ…Œ์ด๋ธ”์—์„œ ๋ฐ์ดํ„ฐ๋ฅผ ์กฐํšŒํ•˜์—ฌ ์กฐ๊ฑด ๋น„๊ต๋‚˜ ํ•„๋“œ ๋งคํ•‘์— ์‚ฌ์šฉ + */ + +import { memo } from "react"; +import { Handle, Position, NodeProps } from "reactflow"; +import { Link2, Database } from "lucide-react"; +import type { ReferenceLookupNodeData } from "@/types/node-editor"; + +export const ReferenceLookupNode = memo(({ data, selected }: NodeProps) => { + return ( +
+ {/* ํ—ค๋” */} +
+ +
+
{data.displayName || "์ฐธ์กฐ ์กฐํšŒ"}
+
+
+ + {/* ๋ณธ๋ฌธ */} +
+
+ + ๋‚ด๋ถ€ DB ์ฐธ์กฐ +
+ + {/* ์ฐธ์กฐ ํ…Œ์ด๋ธ” */} + {data.referenceTable && ( +
+
๐Ÿ“‹ ์ฐธ์กฐ ํ…Œ์ด๋ธ”
+
+ {data.referenceTableLabel || data.referenceTable} +
+
+ )} + + {/* ์กฐ์ธ ์กฐ๊ฑด */} + {data.joinConditions && data.joinConditions.length > 0 && ( +
+
๐Ÿ”— ์กฐ์ธ ์กฐ๊ฑด:
+
+ {data.joinConditions.map((join, idx) => ( +
+ {join.sourceFieldLabel || join.sourceField} + โ†’ + {join.referenceFieldLabel || join.referenceField} +
+ ))} +
+
+ )} + + {/* WHERE ์กฐ๊ฑด */} + {data.whereConditions && data.whereConditions.length > 0 && ( +
+
โšก WHERE ์กฐ๊ฑด:
+
{data.whereConditions.length}๊ฐœ ์กฐ๊ฑด
+
+ )} + + {/* ์ถœ๋ ฅ ํ•„๋“œ */} + {data.outputFields && data.outputFields.length > 0 && ( +
+
๐Ÿ“ค ์ถœ๋ ฅ ํ•„๋“œ:
+
+ {data.outputFields.slice(0, 3).map((field, idx) => ( +
+
+ {field.alias} + โ† {field.fieldLabel || field.fieldName} +
+ ))} + {data.outputFields.length > 3 && ( +
... ์™ธ {data.outputFields.length - 3}๊ฐœ
+ )} +
+
+ )} +
+ + {/* ์ž…๋ ฅ ํ•ธ๋“ค (์™ผ์ชฝ) */} + + + {/* ์ถœ๋ ฅ ํ•ธ๋“ค (์˜ค๋ฅธ์ชฝ) */} + +
+ ); +}); + +ReferenceLookupNode.displayName = "ReferenceLookupNode"; + + diff --git a/frontend/components/dataflow/node-editor/panels/PropertiesPanel.tsx b/frontend/components/dataflow/node-editor/panels/PropertiesPanel.tsx index 0cdf68a6..76eb935a 100644 --- a/frontend/components/dataflow/node-editor/panels/PropertiesPanel.tsx +++ b/frontend/components/dataflow/node-editor/panels/PropertiesPanel.tsx @@ -8,6 +8,7 @@ import { X } from "lucide-react"; import { Button } from "@/components/ui/button"; import { useFlowEditorStore } from "@/lib/stores/flowEditorStore"; import { TableSourceProperties } from "./properties/TableSourceProperties"; +import { ReferenceLookupProperties } from "./properties/ReferenceLookupProperties"; import { InsertActionProperties } from "./properties/InsertActionProperties"; import { FieldMappingProperties } from "./properties/FieldMappingProperties"; import { ConditionProperties } from "./properties/ConditionProperties"; @@ -77,6 +78,9 @@ function NodePropertiesRenderer({ node }: { node: any }) { case "tableSource": return ; + case "referenceLookup": + return ; + case "insertAction": return ; diff --git a/frontend/components/dataflow/node-editor/panels/properties/ConditionProperties.tsx b/frontend/components/dataflow/node-editor/panels/properties/ConditionProperties.tsx index 933f9baf..4bde32fa 100644 --- a/frontend/components/dataflow/node-editor/panels/properties/ConditionProperties.tsx +++ b/frontend/components/dataflow/node-editor/panels/properties/ConditionProperties.tsx @@ -14,6 +14,13 @@ import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@ import { useFlowEditorStore } from "@/lib/stores/flowEditorStore"; import type { ConditionNodeData } from "@/types/node-editor"; +// ํ•„๋“œ ์ •์˜ +interface FieldDefinition { + name: string; + label?: string; + type?: string; +} + interface ConditionPropertiesProps { nodeId: string; data: ConditionNodeData; @@ -35,11 +42,12 @@ const OPERATORS = [ ] as const; export function ConditionProperties({ nodeId, data }: ConditionPropertiesProps) { - const { updateNode } = useFlowEditorStore(); + const { updateNode, nodes, edges } = useFlowEditorStore(); const [displayName, setDisplayName] = useState(data.displayName || "์กฐ๊ฑด ๋ถ„๊ธฐ"); const [conditions, setConditions] = useState(data.conditions || []); const [logic, setLogic] = useState<"AND" | "OR">(data.logic || "AND"); + const [availableFields, setAvailableFields] = useState([]); // ๋ฐ์ดํ„ฐ ๋ณ€๊ฒฝ ์‹œ ๋กœ์ปฌ ์ƒํƒœ ์—…๋ฐ์ดํŠธ useEffect(() => { @@ -48,6 +56,98 @@ export function ConditionProperties({ nodeId, data }: ConditionPropertiesProps) setLogic(data.logic || "AND"); }, [data]); + // ๐Ÿ”ฅ ์—ฐ๊ฒฐ๋œ ์†Œ์Šค ๋…ธ๋“œ์˜ ํ•„๋“œ๋ฅผ ์žฌ๊ท€์ ์œผ๋กœ ์ˆ˜์ง‘ + useEffect(() => { + const getAllSourceFields = (currentNodeId: string, visited: Set = new Set()): FieldDefinition[] => { + if (visited.has(currentNodeId)) return []; + visited.add(currentNodeId); + + const fields: FieldDefinition[] = []; + + // ํ˜„์žฌ ๋…ธ๋“œ๋กœ ๋“ค์–ด์˜ค๋Š” ์—ฃ์ง€ ์ฐพ๊ธฐ + const incomingEdges = edges.filter((e) => e.target === currentNodeId); + + for (const edge of incomingEdges) { + const sourceNode = nodes.find((n) => n.id === edge.source); + if (!sourceNode) continue; + + const sourceData = sourceNode.data as any; + + // ์†Œ์Šค ๋…ธ๋“œ ํƒ€์ž…๋ณ„ ํ•„๋“œ ์ˆ˜์ง‘ + if (sourceNode.type === "tableSource") { + // Table Source: fields ์‚ฌ์šฉ + if (sourceData.fields && Array.isArray(sourceData.fields)) { + console.log("๐Ÿ” [ConditionProperties] Table Source ํ•„๋“œ:", sourceData.fields); + fields.push(...sourceData.fields); + } else { + console.log("โš ๏ธ [ConditionProperties] Table Source์— ํ•„๋“œ ์—†์Œ:", sourceData); + } + } else if (sourceNode.type === "externalDBSource") { + // External DB Source: outputFields ์‚ฌ์šฉ + if (sourceData.outputFields && Array.isArray(sourceData.outputFields)) { + console.log("๐Ÿ” [ConditionProperties] External DB ํ•„๋“œ:", sourceData.outputFields); + fields.push(...sourceData.outputFields); + } else { + console.log("โš ๏ธ [ConditionProperties] External DB์— ํ•„๋“œ ์—†์Œ:", sourceData); + } + } else if (sourceNode.type === "dataTransform") { + // Data Transform: ์žฌ๊ท€์ ์œผ๋กœ ์ƒ์œ„ ๋…ธ๋“œ ํ•„๋“œ ์ˆ˜์ง‘ + const upperFields = getAllSourceFields(sourceNode.id, visited); + + // Data Transform์˜ ๋ณ€ํ™˜ ๊ฒฐ๊ณผ ์ถ”๊ฐ€ + if (sourceData.transformations && Array.isArray(sourceData.transformations)) { + const inPlaceFields = new Set(); + + for (const transform of sourceData.transformations) { + const { sourceField, targetField } = transform; + + // In-place ๋ณ€ํ™˜์ธ์ง€ ํ™•์ธ + if (!targetField || targetField === sourceField) { + inPlaceFields.add(sourceField); + } else { + // ์ƒˆ๋กœ์šด ํ•„๋“œ ์ƒ์„ฑ + fields.push({ name: targetField, label: targetField }); + } + } + + // ์›๋ณธ ํ•„๋“œ ์ค‘ in-place ๋ณ€ํ™˜๋˜์ง€ ์•Š์€ ๊ฒƒ๋“ค ์ถ”๊ฐ€ + for (const field of upperFields) { + if (!inPlaceFields.has(field.name)) { + fields.push(field); + } else { + // In-place ๋ณ€ํ™˜๋œ ํ•„๋“œ๋Š” ์›๋ณธ ์ด๋ฆ„์œผ๋กœ ์œ ์ง€ + fields.push(field); + } + } + } else { + fields.push(...upperFields); + } + } else if ( + sourceNode.type === "insertAction" || + sourceNode.type === "updateAction" || + sourceNode.type === "deleteAction" || + sourceNode.type === "upsertAction" + ) { + // Action ๋…ธ๋“œ: ์žฌ๊ท€์ ์œผ๋กœ ์ƒ์œ„ ๋…ธ๋“œ ํ•„๋“œ ์ˆ˜์ง‘ + fields.push(...getAllSourceFields(sourceNode.id, visited)); + } + } + + // ์ค‘๋ณต ์ œ๊ฑฐ + const uniqueFields = Array.from(new Map(fields.map((f) => [f.name, f])).values()); + return uniqueFields; + }; + + const fields = getAllSourceFields(nodeId); + console.log("โœ… [ConditionProperties] ์ตœ์ข… ์ˆ˜์ง‘๋œ ํ•„๋“œ:", fields); + console.log("๐Ÿ” [ConditionProperties] ํ˜„์žฌ ๋…ธ๋“œ ID:", nodeId); + console.log( + "๐Ÿ” [ConditionProperties] ์—ฐ๊ฒฐ๋œ ์—ฃ์ง€:", + edges.filter((e) => e.target === nodeId), + ); + setAvailableFields(fields); + }, [nodeId, nodes, edges]); + const handleAddCondition = () => { setConditions([ ...conditions, @@ -55,6 +155,7 @@ export function ConditionProperties({ nodeId, data }: ConditionPropertiesProps) field: "", operator: "EQUALS", value: "", + valueType: "static", // "static" (๊ณ ์ •๊ฐ’) ๋˜๋Š” "field" (ํ•„๋“œ ์ฐธ์กฐ) }, ]); }; @@ -151,12 +252,28 @@ export function ConditionProperties({ nodeId, data }: ConditionPropertiesProps)
- handleConditionChange(index, "field", e.target.value)} - placeholder="์กฐ๊ฑด์„ ๊ฒ€์‚ฌํ•  ํ•„๋“œ" - className="mt-1 h-8 text-xs" - /> + {availableFields.length > 0 ? ( + + ) : ( +
+ ์†Œ์Šค ๋…ธ๋“œ๋ฅผ ์—ฐ๊ฒฐํ•˜์„ธ์š” +
+ )}
@@ -179,15 +296,62 @@ export function ConditionProperties({ nodeId, data }: ConditionPropertiesProps)
{condition.operator !== "IS_NULL" && condition.operator !== "IS_NOT_NULL" && ( -
- - handleConditionChange(index, "value", e.target.value)} - placeholder="๋น„๊ตํ•  ๊ฐ’" - className="mt-1 h-8 text-xs" - /> -
+ <> +
+ + +
+ +
+ + {(condition as any).valueType === "field" ? ( + // ํ•„๋“œ ์ฐธ์กฐ: ๋“œ๋กญ๋‹ค์šด์œผ๋กœ ์„ ํƒ + availableFields.length > 0 ? ( + + ) : ( +
+ ์†Œ์Šค ๋…ธ๋“œ๋ฅผ ์—ฐ๊ฒฐํ•˜์„ธ์š” +
+ ) + ) : ( + // ๊ณ ์ •๊ฐ’: ์ง์ ‘ ์ž…๋ ฅ + handleConditionChange(index, "value", e.target.value)} + placeholder="๋น„๊ตํ•  ๊ฐ’" + className="mt-1 h-8 text-xs" + /> + )} +
+ )}
@@ -209,6 +373,13 @@ export function ConditionProperties({ nodeId, data }: ConditionPropertiesProps) {/* ์•ˆ๋‚ด */}
+
+ ๐Ÿ”Œ ์†Œ์Šค ๋…ธ๋“œ ์—ฐ๊ฒฐ: ํ…Œ์ด๋ธ”/์™ธ๋ถ€DB ๋…ธ๋“œ๋ฅผ ์—ฐ๊ฒฐํ•˜๋ฉด ์ž๋™์œผ๋กœ ํ•„๋“œ ๋ชฉ๋ก์ด ํ‘œ์‹œ๋ฉ๋‹ˆ๋‹ค. +
+
+ ๐Ÿ”„ ๋น„๊ต ๊ฐ’ ํƒ€์ž…:
โ€ข ๊ณ ์ •๊ฐ’: ์ง์ ‘ ์ž…๋ ฅํ•œ ๊ฐ’๊ณผ ๋น„๊ต (์˜ˆ: age > 30) +
โ€ข ํ•„๋“œ ์ฐธ์กฐ: ๋‹ค๋ฅธ ํ•„๋“œ์˜ ๊ฐ’๊ณผ ๋น„๊ต (์˜ˆ: ์ฃผ๋ฌธ์ˆ˜๋Ÿ‰ > ์žฌ๊ณ ์ˆ˜๋Ÿ‰) +
๐Ÿ’ก AND: ๋ชจ๋“  ์กฐ๊ฑด์ด ์ฐธ์ด์–ด์•ผ TRUE ์ถœ๋ ฅ
diff --git a/frontend/components/dataflow/node-editor/panels/properties/ReferenceLookupProperties.tsx b/frontend/components/dataflow/node-editor/panels/properties/ReferenceLookupProperties.tsx new file mode 100644 index 00000000..bf88336c --- /dev/null +++ b/frontend/components/dataflow/node-editor/panels/properties/ReferenceLookupProperties.tsx @@ -0,0 +1,643 @@ +"use client"; + +/** + * ์ฐธ์กฐ ํ…Œ์ด๋ธ” ์กฐํšŒ ๋…ธ๋“œ ์†์„ฑ ํŽธ์ง‘ + */ + +import { useEffect, useState, useCallback } from "react"; +import { Plus, Trash2, Search } from "lucide-react"; +import { Label } from "@/components/ui/label"; +import { Input } from "@/components/ui/input"; +import { Button } from "@/components/ui/button"; +import { ScrollArea } from "@/components/ui/scroll-area"; +import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"; +import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover"; +import { Command, CommandEmpty, CommandGroup, CommandInput, CommandItem, CommandList } from "@/components/ui/command"; +import { Check } from "lucide-react"; +import { cn } from "@/lib/utils"; +import { useFlowEditorStore } from "@/lib/stores/flowEditorStore"; +import type { ReferenceLookupNodeData } from "@/types/node-editor"; +import { tableTypeApi } from "@/lib/api/screen"; + +// ํ•„๋“œ ์ •์˜ +interface FieldDefinition { + name: string; + label?: string; + type?: string; +} + +interface ReferenceLookupPropertiesProps { + nodeId: string; + data: ReferenceLookupNodeData; +} + +const OPERATORS = [ + { value: "=", label: "๊ฐ™์Œ (=)" }, + { value: "!=", label: "๊ฐ™์ง€ ์•Š์Œ (โ‰ )" }, + { value: ">", label: "๋ณด๋‹ค ํผ (>)" }, + { value: "<", label: "๋ณด๋‹ค ์ž‘์Œ (<)" }, + { value: ">=", label: "ํฌ๊ฑฐ๋‚˜ ๊ฐ™์Œ (โ‰ฅ)" }, + { value: "<=", label: "์ž‘๊ฑฐ๋‚˜ ๊ฐ™์Œ (โ‰ค)" }, + { value: "LIKE", label: "ํฌํ•จ (LIKE)" }, + { value: "IN", label: "IN" }, +] as const; + +export function ReferenceLookupProperties({ nodeId, data }: ReferenceLookupPropertiesProps) { + const { updateNode, nodes, edges } = useFlowEditorStore(); + + // ์ƒํƒœ + const [displayName, setDisplayName] = useState(data.displayName || "์ฐธ์กฐ ์กฐํšŒ"); + const [referenceTable, setReferenceTable] = useState(data.referenceTable || ""); + const [referenceTableLabel, setReferenceTableLabel] = useState(data.referenceTableLabel || ""); + const [joinConditions, setJoinConditions] = useState(data.joinConditions || []); + const [whereConditions, setWhereConditions] = useState(data.whereConditions || []); + const [outputFields, setOutputFields] = useState(data.outputFields || []); + + // ์†Œ์Šค ํ•„๋“œ ์ˆ˜์ง‘ + const [sourceFields, setSourceFields] = useState([]); + + // ์ฐธ์กฐ ํ…Œ์ด๋ธ” ๊ด€๋ จ + const [tables, setTables] = useState([]); + const [tablesLoading, setTablesLoading] = useState(false); + const [tablesOpen, setTablesOpen] = useState(false); + const [referenceColumns, setReferenceColumns] = useState([]); + const [columnsLoading, setColumnsLoading] = useState(false); + + // ๋ฐ์ดํ„ฐ ๋ณ€๊ฒฝ ์‹œ ๋กœ์ปฌ ์ƒํƒœ ๋™๊ธฐํ™” + useEffect(() => { + setDisplayName(data.displayName || "์ฐธ์กฐ ์กฐํšŒ"); + setReferenceTable(data.referenceTable || ""); + setReferenceTableLabel(data.referenceTableLabel || ""); + setJoinConditions(data.joinConditions || []); + setWhereConditions(data.whereConditions || []); + setOutputFields(data.outputFields || []); + }, [data]); + + // ๐Ÿ” ์†Œ์Šค ํ•„๋“œ ์ˆ˜์ง‘ (์—…์ŠคํŠธ๋ฆผ ๋…ธ๋“œ์—์„œ) + useEffect(() => { + const incomingEdges = edges.filter((e) => e.target === nodeId); + const fields: FieldDefinition[] = []; + + for (const edge of incomingEdges) { + const sourceNode = nodes.find((n) => n.id === edge.source); + if (!sourceNode) continue; + + const sourceData = sourceNode.data as any; + + if (sourceNode.type === "tableSource" && sourceData.fields) { + fields.push(...sourceData.fields); + } else if (sourceNode.type === "externalDBSource" && sourceData.outputFields) { + fields.push(...sourceData.outputFields); + } + } + + setSourceFields(fields); + }, [nodeId, nodes, edges]); + + // ๐Ÿ“Š ํ…Œ์ด๋ธ” ๋ชฉ๋ก ๋กœ๋“œ + useEffect(() => { + loadTables(); + }, []); + + const loadTables = async () => { + setTablesLoading(true); + try { + const data = await tableTypeApi.getTables(); + setTables(data); + } catch (error) { + console.error("ํ…Œ์ด๋ธ” ๋กœ๋“œ ์‹คํŒจ:", error); + } finally { + setTablesLoading(false); + } + }; + + // ๐Ÿ“‹ ์ฐธ์กฐ ํ…Œ์ด๋ธ” ์ปฌ๋Ÿผ ๋กœ๋“œ + useEffect(() => { + if (referenceTable) { + loadReferenceColumns(); + } else { + setReferenceColumns([]); + } + }, [referenceTable]); + + const loadReferenceColumns = async () => { + if (!referenceTable) return; + + setColumnsLoading(true); + try { + const cols = await tableTypeApi.getColumns(referenceTable); + const formatted = cols.map((col: any) => ({ + name: col.columnName, + type: col.dataType, + label: col.displayName || col.columnName, + })); + setReferenceColumns(formatted); + } catch (error) { + console.error("์ปฌ๋Ÿผ ๋กœ๋“œ ์‹คํŒจ:", error); + setReferenceColumns([]); + } finally { + setColumnsLoading(false); + } + }; + + // ํ…Œ์ด๋ธ” ์„ ํƒ ํ•ธ๋“ค๋Ÿฌ + const handleTableSelect = (tableName: string) => { + const selectedTable = tables.find((t) => t.tableName === tableName); + if (selectedTable) { + setReferenceTable(tableName); + setReferenceTableLabel(selectedTable.label); + setTablesOpen(false); + + // ๊ธฐ์กด ์„ค์ • ์ดˆ๊ธฐํ™” + setJoinConditions([]); + setWhereConditions([]); + setOutputFields([]); + } + }; + + // ์กฐ์ธ ์กฐ๊ฑด ์ถ”๊ฐ€ + const handleAddJoinCondition = () => { + setJoinConditions([ + ...joinConditions, + { + sourceField: "", + referenceField: "", + }, + ]); + }; + + const handleRemoveJoinCondition = (index: number) => { + setJoinConditions(joinConditions.filter((_, i) => i !== index)); + }; + + const handleJoinConditionChange = (index: number, field: string, value: any) => { + const newConditions = [...joinConditions]; + newConditions[index] = { ...newConditions[index], [field]: value }; + + // ๋ผ๋ฒจ๋„ ํ•จ๊ป˜ ์ €์žฅ + if (field === "sourceField") { + const sourceField = sourceFields.find((f) => f.name === value); + newConditions[index].sourceFieldLabel = sourceField?.label || value; + } else if (field === "referenceField") { + const refField = referenceColumns.find((f) => f.name === value); + newConditions[index].referenceFieldLabel = refField?.label || value; + } + + setJoinConditions(newConditions); + }; + + // WHERE ์กฐ๊ฑด ์ถ”๊ฐ€ + const handleAddWhereCondition = () => { + setWhereConditions([ + ...whereConditions, + { + field: "", + operator: "=", + value: "", + valueType: "static", + }, + ]); + }; + + const handleRemoveWhereCondition = (index: number) => { + setWhereConditions(whereConditions.filter((_, i) => i !== index)); + }; + + const handleWhereConditionChange = (index: number, field: string, value: any) => { + const newConditions = [...whereConditions]; + newConditions[index] = { ...newConditions[index], [field]: value }; + + // ๋ผ๋ฒจ๋„ ํ•จ๊ป˜ ์ €์žฅ + if (field === "field") { + const refField = referenceColumns.find((f) => f.name === value); + newConditions[index].fieldLabel = refField?.label || value; + } + + setWhereConditions(newConditions); + }; + + // ์ถœ๋ ฅ ํ•„๋“œ ์ถ”๊ฐ€ + const handleAddOutputField = () => { + setOutputFields([ + ...outputFields, + { + fieldName: "", + alias: "", + }, + ]); + }; + + const handleRemoveOutputField = (index: number) => { + setOutputFields(outputFields.filter((_, i) => i !== index)); + }; + + const handleOutputFieldChange = (index: number, field: string, value: any) => { + const newFields = [...outputFields]; + newFields[index] = { ...newFields[index], [field]: value }; + + // ๋ผ๋ฒจ๋„ ํ•จ๊ป˜ ์ €์žฅ + if (field === "fieldName") { + const refField = referenceColumns.find((f) => f.name === value); + newFields[index].fieldLabel = refField?.label || value; + // alias ์ž๋™ ์„ค์ • + if (!newFields[index].alias) { + newFields[index].alias = `ref_${value}`; + } + } + + setOutputFields(newFields); + }; + + const handleSave = () => { + updateNode(nodeId, { + displayName, + referenceTable, + referenceTableLabel, + joinConditions, + whereConditions, + outputFields, + }); + }; + + const selectedTableLabel = tables.find((t) => t.tableName === referenceTable)?.label || referenceTable; + + return ( + +
+ {/* ๊ธฐ๋ณธ ์ •๋ณด */} +
+

๊ธฐ๋ณธ ์ •๋ณด

+ +
+
+ + setDisplayName(e.target.value)} + className="mt-1" + placeholder="๋…ธ๋“œ ํ‘œ์‹œ ์ด๋ฆ„" + /> +
+ + {/* ์ฐธ์กฐ ํ…Œ์ด๋ธ” ์„ ํƒ */} +
+ + + + + + + + + + ๊ฒ€์ƒ‰ ๊ฒฐ๊ณผ๊ฐ€ ์—†์Šต๋‹ˆ๋‹ค. + + + {tables.map((table) => ( + handleTableSelect(table.tableName)} + className="cursor-pointer" + > + +
+ {table.label} + {table.label !== table.tableName && ( + {table.tableName} + )} +
+
+ ))} +
+
+
+
+
+
+
+
+
+ + {/* ์กฐ์ธ ์กฐ๊ฑด */} +
+
+

์กฐ์ธ ์กฐ๊ฑด (FK ๋งคํ•‘)

+ +
+ + {joinConditions.length > 0 ? ( +
+ {joinConditions.map((condition, index) => ( +
+
+ ์กฐ์ธ #{index + 1} + +
+ +
+
+ + +
+ +
+ + +
+
+
+ ))} +
+ ) : ( +
+ ์กฐ์ธ ์กฐ๊ฑด์„ ์ถ”๊ฐ€ํ•˜์„ธ์š” (ํ•„์ˆ˜) +
+ )} +
+ + {/* WHERE ์กฐ๊ฑด */} +
+
+

WHERE ์กฐ๊ฑด (์„ ํƒ์‚ฌํ•ญ)

+ +
+ + {whereConditions.length > 0 && ( +
+ {whereConditions.map((condition, index) => ( +
+
+ WHERE #{index + 1} + +
+ +
+
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + {condition.valueType === "field" ? ( + + ) : ( + handleWhereConditionChange(index, "value", e.target.value)} + placeholder="๋น„๊ตํ•  ๊ฐ’" + className="mt-1 h-8 text-xs" + /> + )} +
+
+
+ ))} +
+ )} +
+ + {/* ์ถœ๋ ฅ ํ•„๋“œ */} +
+
+

์ถœ๋ ฅ ํ•„๋“œ

+ +
+ + {outputFields.length > 0 ? ( +
+ {outputFields.map((field, index) => ( +
+
+ ํ•„๋“œ #{index + 1} + +
+ +
+
+ + +
+ +
+ + handleOutputFieldChange(index, "alias", e.target.value)} + placeholder="ref_field_name" + className="mt-1 h-8 text-xs" + /> +
+
+
+ ))} +
+ ) : ( +
+ ์ถœ๋ ฅ ํ•„๋“œ๋ฅผ ์ถ”๊ฐ€ํ•˜์„ธ์š” (ํ•„์ˆ˜) +
+ )} +
+ + {/* ์ €์žฅ ๋ฒ„ํŠผ */} +
+ +
+ + {/* ์•ˆ๋‚ด */} +
+
+ ๐Ÿ”— ์กฐ์ธ ์กฐ๊ฑด: ์†Œ์Šค ๋ฐ์ดํ„ฐ์™€ ์ฐธ์กฐ ํ…Œ์ด๋ธ”์„ ์—ฐ๊ฒฐํ•˜๋Š” ํ‚ค (์˜ˆ: customer_id โ†’ id) +
+
+ โšก WHERE ์กฐ๊ฑด: ์ฐธ์กฐ ํ…Œ์ด๋ธ”์—์„œ ํŠน์ • ์กฐ๊ฑด์˜ ๋ฐ์ดํ„ฐ๋งŒ ๊ฐ€์ ธ์˜ค๊ธฐ (์˜ˆ: grade = 'VIP') +
+
+ ๐Ÿ“ค ์ถœ๋ ฅ ํ•„๋“œ: ์ฐธ์กฐ ํ…Œ์ด๋ธ”์—์„œ ๊ฐ€์ ธ์˜ฌ ํ•„๋“œ ์„ ํƒ (๋ณ„์นญ์œผ๋กœ ๊ฒฐ๊ณผ์— ์ถ”๊ฐ€๋จ) +
+
+
+
+ ); +} diff --git a/frontend/components/dataflow/node-editor/sidebar/nodePaletteConfig.ts b/frontend/components/dataflow/node-editor/sidebar/nodePaletteConfig.ts index 0ab6cb73..176ade1f 100644 --- a/frontend/components/dataflow/node-editor/sidebar/nodePaletteConfig.ts +++ b/frontend/components/dataflow/node-editor/sidebar/nodePaletteConfig.ts @@ -32,6 +32,14 @@ export const NODE_PALETTE: NodePaletteItem[] = [ category: "source", color: "#10B981", // ์ดˆ๋ก์ƒ‰ }, + { + type: "referenceLookup", + label: "์ฐธ์กฐ ์กฐํšŒ", + icon: "๐Ÿ”—", + description: "๋‹ค๋ฅธ ํ…Œ์ด๋ธ”์—์„œ ๋ฐ์ดํ„ฐ๋ฅผ ์กฐํšŒํ•ฉ๋‹ˆ๋‹ค (๋‚ด๋ถ€ DB ์ „์šฉ)", + category: "source", + color: "#A855F7", // ๋ณด๋ผ์ƒ‰ + }, // ======================================================================== // ๋ณ€ํ™˜/์กฐ๊ฑด diff --git a/frontend/types/node-editor.ts b/frontend/types/node-editor.ts index e4667159..d29f006b 100644 --- a/frontend/types/node-editor.ts +++ b/frontend/types/node-editor.ts @@ -12,6 +12,7 @@ export type NodeType = | "tableSource" // ํ…Œ์ด๋ธ” ์†Œ์Šค | "externalDBSource" // ์™ธ๋ถ€ DB ์†Œ์Šค | "restAPISource" // REST API ์†Œ์Šค + | "referenceLookup" // ์ฐธ์กฐ ํ…Œ์ด๋ธ” ์กฐํšŒ (๋‚ด๋ถ€ DB ์ „์šฉ) | "condition" // ์กฐ๊ฑด ๋ถ„๊ธฐ | "fieldMapping" // ํ•„๋“œ ๋งคํ•‘ | "dataTransform" // ๋ฐ์ดํ„ฐ ๋ณ€ํ™˜ @@ -91,6 +92,35 @@ export interface RestAPISourceNodeData { displayName?: string; } +// ์ฐธ์กฐ ํ…Œ์ด๋ธ” ์กฐํšŒ ๋…ธ๋“œ (๋‚ด๋ถ€ DB ์ „์šฉ) +export interface ReferenceLookupNodeData { + type: "referenceLookup"; + referenceTable: string; // ์ฐธ์กฐํ•  ํ…Œ์ด๋ธ”๋ช… + referenceTableLabel?: string; // ํ…Œ์ด๋ธ” ๋ผ๋ฒจ + joinConditions: Array<{ + // ์กฐ์ธ ์กฐ๊ฑด (FK ๋งคํ•‘) + sourceField: string; // ์†Œ์Šค ๋ฐ์ดํ„ฐ์˜ ํ•„๋“œ + sourceFieldLabel?: string; + referenceField: string; // ์ฐธ์กฐ ํ…Œ์ด๋ธ”์˜ ํ•„๋“œ + referenceFieldLabel?: string; + }>; + whereConditions?: Array<{ + // ์ถ”๊ฐ€ WHERE ์กฐ๊ฑด + field: string; + fieldLabel?: string; + operator: string; + value: any; + valueType?: "static" | "field"; // ๊ณ ์ •๊ฐ’ ๋˜๋Š” ์†Œ์Šค ํ•„๋“œ ์ฐธ์กฐ + }>; + outputFields: Array<{ + // ๊ฐ€์ ธ์˜ฌ ํ•„๋“œ๋“ค + fieldName: string; // ์ฐธ์กฐ ํ…Œ์ด๋ธ”์˜ ์ปฌ๋Ÿผ๋ช… + fieldLabel?: string; + alias: string; // ๊ฒฐ๊ณผ ๋ฐ์ดํ„ฐ์—์„œ ์‚ฌ์šฉํ•  ์ด๋ฆ„ + }>; + displayName?: string; +} + // ์กฐ๊ฑด ๋ถ„๊ธฐ ๋…ธ๋“œ export interface ConditionNodeData { conditions: Array<{ @@ -373,6 +403,7 @@ export type NodeData = | TableSourceNodeData | ExternalDBSourceNodeData | RestAPISourceNodeData + | ReferenceLookupNodeData | ConditionNodeData | FieldMappingNodeData | DataTransformNodeData