제어관리 노드 작동 방식 수정

This commit is contained in:
kjs
2025-10-13 17:47:24 +09:00
parent 9d5ac1716d
commit 0dc4d53876
15 changed files with 1567 additions and 407 deletions
@@ -16,7 +16,6 @@ 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";
import { UpdateActionNode } from "./nodes/UpdateActionNode";
import { DeleteActionNode } from "./nodes/DeleteActionNode";
@@ -35,7 +34,6 @@ const nodeTypes = {
referenceLookup: ReferenceLookupNode,
// 변환/조건
condition: ConditionNode,
fieldMapping: FieldMappingNode,
dataTransform: DataTransformNode,
// 액션
insertAction: InsertActionNode,
@@ -1,66 +0,0 @@
"use client";
/**
* 필드 매핑 노드
*/
import { memo } from "react";
import { Handle, Position, NodeProps } from "reactflow";
import { ArrowLeftRight } from "lucide-react";
import type { FieldMappingNodeData } from "@/types/node-editor";
export const FieldMappingNode = memo(({ data, selected }: NodeProps<FieldMappingNodeData>) => {
return (
<div
className={`min-w-[250px] rounded-lg border-2 bg-white shadow-md transition-all ${
selected ? "border-purple-500 shadow-lg" : "border-gray-200"
}`}
>
{/* 입력 핸들 */}
<Handle type="target" position={Position.Left} className="!h-3 !w-3 !border-2 !border-purple-500 !bg-white" />
{/* 헤더 */}
<div className="flex items-center gap-2 rounded-t-lg bg-purple-500 px-3 py-2 text-white">
<ArrowLeftRight className="h-4 w-4" />
<div className="flex-1">
<div className="text-sm font-semibold"> </div>
<div className="text-xs opacity-80">{data.displayName || "데이터 매핑"}</div>
</div>
</div>
{/* 본문 */}
<div className="p-3">
{data.mappings && data.mappings.length > 0 ? (
<div className="space-y-1">
<div className="text-xs font-medium text-gray-700"> : ({data.mappings.length})</div>
<div className="max-h-[150px] space-y-1 overflow-y-auto">
{data.mappings.slice(0, 5).map((mapping) => (
<div key={mapping.id} className="rounded bg-gray-50 px-2 py-1 text-xs">
<div className="flex items-center justify-between">
<span className="font-mono text-gray-600">{mapping.sourceField || "정적값"}</span>
<span className="text-purple-500"></span>
<span className="font-mono text-gray-700">{mapping.targetField}</span>
</div>
{mapping.transform && <div className="mt-0.5 text-xs text-gray-400">: {mapping.transform}</div>}
{mapping.staticValue !== undefined && (
<div className="mt-0.5 text-xs text-gray-400">: {String(mapping.staticValue)}</div>
)}
</div>
))}
{data.mappings.length > 5 && (
<div className="text-xs text-gray-400">... {data.mappings.length - 5}</div>
)}
</div>
</div>
) : (
<div className="text-center text-xs text-gray-400"> </div>
)}
</div>
{/* 출력 핸들 */}
<Handle type="source" position={Position.Right} className="!h-3 !w-3 !border-2 !border-purple-500 !bg-white" />
</div>
);
});
FieldMappingNode.displayName = "FieldMappingNode";
@@ -10,7 +10,6 @@ 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";
import { UpdateActionProperties } from "./properties/UpdateActionProperties";
import { DeleteActionProperties } from "./properties/DeleteActionProperties";
@@ -84,9 +83,6 @@ function NodePropertiesRenderer({ node }: { node: any }) {
case "insertAction":
return <InsertActionProperties nodeId={node.id} data={node.data} />;
case "fieldMapping":
return <FieldMappingProperties nodeId={node.id} data={node.data} />;
case "condition":
return <ConditionProperties nodeId={node.id} data={node.data} />;
@@ -122,6 +122,24 @@ export function ConditionProperties({ nodeId, data }: ConditionPropertiesProps)
} else {
fields.push(...upperFields);
}
} else if (sourceNode.type === "restAPISource") {
// REST API Source: responseFields 사용
if (sourceData.responseFields && Array.isArray(sourceData.responseFields)) {
console.log("🔍 [ConditionProperties] REST API 필드:", sourceData.responseFields);
fields.push(
...sourceData.responseFields.map((f: any) => ({
name: f.name || f.fieldName,
label: f.label || f.displayName || f.name,
type: f.dataType || f.type,
})),
);
} else {
console.log("⚠️ [ConditionProperties] REST API에 필드 없음:", sourceData);
}
} else if (sourceNode.type === "condition") {
// 조건 노드: 재귀적으로 상위 노드 필드 수집 (통과 노드)
console.log("✅ [ConditionProperties] 조건 노드 통과 → 상위 탐색");
fields.push(...getAllSourceFields(sourceNode.id, visited));
} else if (
sourceNode.type === "insertAction" ||
sourceNode.type === "updateAction" ||
@@ -130,6 +148,10 @@ export function ConditionProperties({ nodeId, data }: ConditionPropertiesProps)
) {
// Action 노드: 재귀적으로 상위 노드 필드 수집
fields.push(...getAllSourceFields(sourceNode.id, visited));
} else {
// 기타 모든 노드: 재귀적으로 상위 노드 필드 수집 (통과 노드로 처리)
console.log(`✅ [ConditionProperties] 통과 노드 (${sourceNode.type}) → 상위 탐색`);
fields.push(...getAllSourceFields(sourceNode.id, visited));
}
}
@@ -1,191 +0,0 @@
"use client";
/**
* 필드 매핑 노드 속성 편집
*/
import { useEffect, useState } from "react";
import { Plus, Trash2, ArrowRight } 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 { useFlowEditorStore } from "@/lib/stores/flowEditorStore";
import type { FieldMappingNodeData } from "@/types/node-editor";
interface FieldMappingPropertiesProps {
nodeId: string;
data: FieldMappingNodeData;
}
export function FieldMappingProperties({ nodeId, data }: FieldMappingPropertiesProps) {
const { updateNode } = useFlowEditorStore();
const [displayName, setDisplayName] = useState(data.displayName || "데이터 매핑");
const [mappings, setMappings] = useState(data.mappings || []);
// 데이터 변경 시 로컬 상태 업데이트
useEffect(() => {
setDisplayName(data.displayName || "데이터 매핑");
setMappings(data.mappings || []);
}, [data]);
const handleAddMapping = () => {
setMappings([
...mappings,
{
id: `mapping_${Date.now()}`,
sourceField: "",
targetField: "",
transform: undefined,
staticValue: undefined,
},
]);
};
const handleRemoveMapping = (id: string) => {
setMappings(mappings.filter((m) => m.id !== id));
};
const handleMappingChange = (id: string, field: string, value: any) => {
const newMappings = mappings.map((m) => (m.id === id ? { ...m, [field]: value } : m));
setMappings(newMappings);
};
const handleSave = () => {
updateNode(nodeId, {
displayName,
mappings,
});
};
return (
<ScrollArea className="h-full">
<div className="space-y-4 p-4">
{/* 기본 정보 */}
<div>
<h3 className="mb-3 text-sm font-semibold"> </h3>
<div>
<Label htmlFor="displayName" className="text-xs">
</Label>
<Input
id="displayName"
value={displayName}
onChange={(e) => setDisplayName(e.target.value)}
className="mt-1"
placeholder="노드 표시 이름"
/>
</div>
</div>
{/* 매핑 규칙 */}
<div>
<div className="mb-2 flex items-center justify-between">
<h3 className="text-sm font-semibold"> </h3>
<Button size="sm" variant="outline" onClick={handleAddMapping} className="h-7">
<Plus className="mr-1 h-3 w-3" />
</Button>
</div>
{mappings.length > 0 ? (
<div className="space-y-2">
{mappings.map((mapping, index) => (
<div key={mapping.id} className="rounded border bg-purple-50 p-3">
<div className="mb-2 flex items-center justify-between">
<span className="text-xs font-medium text-purple-700"> #{index + 1}</span>
<Button
size="sm"
variant="ghost"
onClick={() => handleRemoveMapping(mapping.id)}
className="h-6 w-6 p-0"
>
<Trash2 className="h-3 w-3" />
</Button>
</div>
<div className="space-y-2">
{/* 소스 → 타겟 */}
<div className="flex items-center gap-2">
<div className="flex-1">
<Label className="text-xs text-gray-600"> </Label>
<Input
value={mapping.sourceField || ""}
onChange={(e) => handleMappingChange(mapping.id, "sourceField", e.target.value)}
placeholder="입력 필드"
className="mt-1 h-8 text-xs"
/>
</div>
<div className="pt-5">
<ArrowRight className="h-4 w-4 text-purple-500" />
</div>
<div className="flex-1">
<Label className="text-xs text-gray-600"> </Label>
<Input
value={mapping.targetField}
onChange={(e) => handleMappingChange(mapping.id, "targetField", e.target.value)}
placeholder="출력 필드"
className="mt-1 h-8 text-xs"
/>
</div>
</div>
{/* 변환 함수 */}
<div>
<Label className="text-xs text-gray-600"> ()</Label>
<Input
value={mapping.transform || ""}
onChange={(e) => handleMappingChange(mapping.id, "transform", e.target.value)}
placeholder="예: UPPER(), TRIM(), CONCAT()"
className="mt-1 h-8 text-xs"
/>
</div>
{/* 정적 값 */}
<div>
<Label className="text-xs text-gray-600"> ()</Label>
<Input
value={mapping.staticValue || ""}
onChange={(e) => handleMappingChange(mapping.id, "staticValue", e.target.value)}
placeholder="고정 값 (소스 필드 대신 사용)"
className="mt-1 h-8 text-xs"
/>
</div>
</div>
</div>
))}
</div>
) : (
<div className="rounded border border-dashed p-4 text-center text-xs text-gray-400">
. "추가" .
</div>
)}
</div>
{/* 저장 버튼 */}
<div className="flex gap-2">
<Button onClick={handleSave} className="flex-1" size="sm">
</Button>
</div>
{/* 안내 */}
<div className="space-y-2">
<div className="rounded bg-purple-50 p-3 text-xs text-purple-700">
💡 <strong> </strong>:
</div>
<div className="rounded bg-purple-50 p-3 text-xs text-purple-700">
💡 <strong> </strong>:
</div>
<div className="rounded bg-purple-50 p-3 text-xs text-purple-700">
💡 <strong> </strong>: (SQL )
</div>
</div>
</div>
</ScrollArea>
);
}
@@ -137,8 +137,10 @@ export function InsertActionProperties({ nodeId, data }: InsertActionPropertiesP
const getAllSourceFields = (
targetNodeId: string,
visitedNodes = new Set<string>(),
): { fields: Array<{ name: string; label?: string }>; hasRestAPI: boolean } => {
sourcePath: string[] = [], // 🔥 소스 경로 추적
): { fields: Array<{ name: string; label?: string; sourcePath?: string[] }>; hasRestAPI: boolean } => {
if (visitedNodes.has(targetNodeId)) {
console.log(`⚠️ 순환 참조 감지: ${targetNodeId} (이미 방문함)`);
return { fields: [], hasRestAPI: false };
}
visitedNodes.add(targetNodeId);
@@ -147,19 +149,27 @@ export function InsertActionProperties({ nodeId, data }: InsertActionPropertiesP
const sourceNodeIds = inputEdges.map((edge) => edge.source);
const sourceNodes = nodes.filter((node) => sourceNodeIds.includes(node.id));
const fields: Array<{ name: string; label?: string }> = [];
// 🔥 다중 소스 감지
if (sourceNodes.length > 1) {
console.log(`⚠️ 다중 소스 감지: ${sourceNodes.length}개 노드 연결됨`);
console.log(" 소스 노드들:", sourceNodes.map((n) => `${n.id}(${n.type})`).join(", "));
}
const fields: Array<{ name: string; label?: string; sourcePath?: string[] }> = [];
let foundRestAPI = false;
sourceNodes.forEach((node) => {
console.log(`🔍 노드 ${node.id} 타입: ${node.type}`);
console.log(`🔍 노드 ${node.id} 데이터:`, node.data);
// 데이터 변환 노드우: 변환된 필드 + 상위 노드의 원본 필드
// 🔥 현재 노드로에 추가
const currentPath = [...sourcePath, `${node.id}(${node.type})`];
// 1️⃣ 데이터 변환 노드: 변환된 필드 + 상위 노드의 원본 필드
if (node.type === "dataTransform") {
console.log("✅ 데이터 변환 노드 발견");
// 상위 노드의 원본 필드 먼저 수집
const upperResult = getAllSourceFields(node.id, visitedNodes);
const upperResult = getAllSourceFields(node.id, visitedNodes, currentPath);
const upperFields = upperResult.fields;
foundRestAPI = foundRestAPI || upperResult.hasRestAPI;
console.log(` 📤 상위 노드에서 ${upperFields.length}개 필드 가져옴`);
@@ -167,7 +177,7 @@ export function InsertActionProperties({ nodeId, data }: InsertActionPropertiesP
// 변환된 필드 추가 (in-place 변환 고려)
if ((node.data as any).transformations && Array.isArray((node.data as any).transformations)) {
console.log(` 📊 ${(node.data as any).transformations.length}개 변환 발견`);
const inPlaceFields = new Set<string>(); // in-place 변환된 필드 추적
const inPlaceFields = new Set<string>();
(node.data as any).transformations.forEach((transform: any) => {
const targetField = transform.targetField || transform.sourceField;
@@ -176,32 +186,29 @@ export function InsertActionProperties({ nodeId, data }: InsertActionPropertiesP
console.log(` 🔹 변환: ${transform.sourceField}${targetField} ${isInPlace ? "(in-place)" : ""}`);
if (isInPlace) {
// in-place: 원본 필드를 덮어쓰므로, 원본 필드는 이미 upperFields에 있음
inPlaceFields.add(transform.sourceField);
} else if (targetField) {
// 새 필드 생성
fields.push({
name: targetField,
label: transform.targetFieldLabel || targetField,
sourcePath: currentPath,
});
}
});
// 상위 필드 중 in-place 변환되지 않은 것만 추가
// 상위 필드 추가
upperFields.forEach((field) => {
if (!inPlaceFields.has(field.name)) {
fields.push(field);
} else {
// in-place 변환된 필드도 추가 (변환 후 값)
fields.push(field);
}
});
} else {
// 변환이 없으면 상위 필드만 추가
fields.push(...upperFields);
}
}
// REST API 소스 노드인 경우
// 2️⃣ REST API 소스 노드
else if (node.type === "restAPISource") {
console.log("✅ REST API 소스 노드 발견");
foundRestAPI = true;
@@ -216,6 +223,7 @@ export function InsertActionProperties({ nodeId, data }: InsertActionPropertiesP
fields.push({
name: fieldName,
label: fieldLabel,
sourcePath: currentPath,
});
}
});
@@ -223,26 +231,44 @@ export function InsertActionProperties({ nodeId, data }: InsertActionPropertiesP
console.log("⚠️ REST API 노드에 responseFields 없음");
}
}
// 일반 소스 노드인 경우 (테이블 소스 등)
else {
// 3️⃣ 테이블/외부DB 소스 노드
else if (node.type === "tableSource" || node.type === "externalDBSource") {
const nodeFields = (node.data as any).fields || (node.data as any).outputFields;
const displayName = (node.data as any).displayName || (node.data as any).tableName || node.id;
if (nodeFields && Array.isArray(nodeFields)) {
console.log(` 노드 ${node.id}에서 ${nodeFields.length}개 필드 발견`);
console.log(`${node.type}[${displayName}] 노드에서 ${nodeFields.length}개 필드 발견`);
nodeFields.forEach((field: any) => {
const fieldName = field.name || field.fieldName || field.column_name;
const fieldLabel = field.label || field.displayName || field.label_ko;
if (fieldName) {
// 🔥 다중 소스인 경우 필드명에 소스 표시
const displayLabel =
sourceNodes.length > 1 ? `${fieldLabel || fieldName} [${displayName}]` : fieldLabel || fieldName;
fields.push({
name: fieldName,
label: fieldLabel,
label: displayLabel,
sourcePath: currentPath,
});
}
});
} else {
console.log(`❌ 노드 ${node.id}에 fields 없음`);
console.log(`⚠️ ${node.type} 노드에 필드 정의 없음 → 상위 노드 탐색`);
// 필드가 없으면 상위 노드로 계속 탐색
const upperResult = getAllSourceFields(node.id, visitedNodes, currentPath);
fields.push(...upperResult.fields);
foundRestAPI = foundRestAPI || upperResult.hasRestAPI;
}
}
// 4️⃣ 통과 노드 (조건, 기타 모든 노드): 상위 노드로 계속 탐색
else {
console.log(`✅ 통과 노드 (${node.type}) → 상위 노드로 계속 탐색`);
const upperResult = getAllSourceFields(node.id, visitedNodes, currentPath);
fields.push(...upperResult.fields);
foundRestAPI = foundRestAPI || upperResult.hasRestAPI;
console.log(` 📤 상위 노드에서 ${upperResult.fields.length}개 필드 가져옴`);
}
});
return { fields, hasRestAPI: foundRestAPI };
@@ -251,8 +277,30 @@ export function InsertActionProperties({ nodeId, data }: InsertActionPropertiesP
console.log("🔍 INSERT 노드 ID:", nodeId);
const result = getAllSourceFields(nodeId);
// 중복 제거
const uniqueFields = Array.from(new Map(result.fields.map((field) => [field.name, field])).values());
console.log("📊 필드 수집 완료:");
console.log(` - 총 필드 수: ${result.fields.length}`);
console.log(` - REST API 포함: ${result.hasRestAPI}`);
// 🔥 중복 제거 개선: 필드명이 같아도 소스가 다르면 모두 표시
const fieldMap = new Map<string, (typeof result.fields)[number]>();
const duplicateFields = new Set<string>();
result.fields.forEach((field) => {
const key = `${field.name}`;
if (fieldMap.has(key)) {
duplicateFields.add(field.name);
}
// 중복이면 마지막 값으로 덮어씀 (기존 동작 유지)
fieldMap.set(key, field);
});
if (duplicateFields.size > 0) {
console.warn(`⚠️ 중복 필드명 감지: ${Array.from(duplicateFields).join(", ")}`);
console.warn(" → 마지막으로 발견된 필드만 표시됩니다.");
console.warn(" → 다중 소스 사용 시 필드명이 겹치지 않도록 주의하세요!");
}
const uniqueFields = Array.from(fieldMap.values());
setSourceFields(uniqueFields);
setHasRestAPISource(result.hasRestAPI);
@@ -166,14 +166,12 @@ export function UpdateActionProperties({ nodeId, data }: UpdateActionPropertiesP
let foundRestAPI = false;
sourceNodes.forEach((node) => {
// 데이터 변환 노드인 경우: 변환된 필드 + 상위 노드의 원본 필드
// 1️⃣ 데이터 변환 노드: 변환된 필드 + 상위 노드의 원본 필드
if (node.type === "dataTransform") {
// 상위 노드의 원본 필드 먼저 수집
const upperResult = getAllSourceFields(node.id, visitedNodes);
const upperFields = upperResult.fields;
foundRestAPI = foundRestAPI || upperResult.hasRestAPI;
// 변환된 필드 추가 (in-place 변환 고려)
if ((node.data as any).transformations && Array.isArray((node.data as any).transformations)) {
const inPlaceFields = new Set<string>();
@@ -191,7 +189,6 @@ export function UpdateActionProperties({ nodeId, data }: UpdateActionPropertiesP
}
});
// 상위 필드 추가 (모두 포함, in-place는 변환 후 값)
upperFields.forEach((field) => {
fields.push(field);
});
@@ -199,7 +196,7 @@ export function UpdateActionProperties({ nodeId, data }: UpdateActionPropertiesP
fields.push(...upperFields);
}
}
// REST API 소스 노드인 경우
// 2️⃣ REST API 소스 노드
else if (node.type === "restAPISource") {
foundRestAPI = true;
const responseFields = (node.data as any).responseFields;
@@ -216,21 +213,33 @@ export function UpdateActionProperties({ nodeId, data }: UpdateActionPropertiesP
});
}
}
// 일반 소스 노드인 경우
else if (node.type === "tableSource" && (node.data as any).fields) {
(node.data as any).fields.forEach((field: any) => {
fields.push({
name: field.name,
label: field.label || field.displayName,
// 3️⃣ 테이블/외부DB 소스 노드
else if (node.type === "tableSource" || node.type === "externalDBSource") {
const nodeFields = (node.data as any).fields || (node.data as any).outputFields;
if (nodeFields && Array.isArray(nodeFields)) {
nodeFields.forEach((field: any) => {
const fieldName = field.name || field.fieldName || field.column_name;
const fieldLabel = field.label || field.displayName || field.label_ko;
if (fieldName) {
fields.push({
name: fieldName,
label: fieldLabel,
});
}
});
});
} else if (node.type === "externalDBSource" && (node.data as any).fields) {
(node.data as any).fields.forEach((field: any) => {
fields.push({
name: field.name,
label: field.label || field.displayName,
});
});
} else {
// 필드가 없으면 상위 노드로 계속 탐색
const upperResult = getAllSourceFields(node.id, visitedNodes);
fields.push(...upperResult.fields);
foundRestAPI = foundRestAPI || upperResult.hasRestAPI;
}
}
// 4️⃣ 통과 노드 (조건, 기타 모든 노드): 상위 노드로 계속 탐색
else {
const upperResult = getAllSourceFields(node.id, visitedNodes);
fields.push(...upperResult.fields);
foundRestAPI = foundRestAPI || upperResult.hasRestAPI;
}
});
@@ -153,14 +153,12 @@ export function UpsertActionProperties({ nodeId, data }: UpsertActionPropertiesP
let foundRestAPI = false;
sourceNodes.forEach((node) => {
// 데이터 변환 노드인 경우: 변환된 필드 + 상위 노드의 원본 필드
// 1️⃣ 데이터 변환 노드: 변환된 필드 + 상위 노드의 원본 필드
if (node.type === "dataTransform") {
// 상위 노드의 원본 필드 먼저 수집
const upperResult = getAllSourceFields(node.id, visitedNodes);
const upperFields = upperResult.fields;
foundRestAPI = foundRestAPI || upperResult.hasRestAPI;
// 변환된 필드 추가 (in-place 변환 고려)
if ((node.data as any).transformations && Array.isArray((node.data as any).transformations)) {
const inPlaceFields = new Set<string>();
@@ -178,7 +176,6 @@ export function UpsertActionProperties({ nodeId, data }: UpsertActionPropertiesP
}
});
// 상위 필드 추가 (모두 포함, in-place는 변환 후 값)
upperFields.forEach((field) => {
fields.push(field);
});
@@ -186,7 +183,7 @@ export function UpsertActionProperties({ nodeId, data }: UpsertActionPropertiesP
fields.push(...upperFields);
}
}
// REST API 소스 노드인 경우
// 2️⃣ REST API 소스 노드
else if (node.type === "restAPISource") {
foundRestAPI = true;
const responseFields = (node.data as any).responseFields;
@@ -203,21 +200,33 @@ export function UpsertActionProperties({ nodeId, data }: UpsertActionPropertiesP
});
}
}
// 일반 소스 노드인 경우
else if (node.type === "tableSource" && (node.data as any).fields) {
(node.data as any).fields.forEach((field: any) => {
fields.push({
name: field.name,
label: field.label || field.displayName,
// 3️⃣ 테이블/외부DB 소스 노드
else if (node.type === "tableSource" || node.type === "externalDBSource") {
const nodeFields = (node.data as any).fields || (node.data as any).outputFields;
if (nodeFields && Array.isArray(nodeFields)) {
nodeFields.forEach((field: any) => {
const fieldName = field.name || field.fieldName || field.column_name;
const fieldLabel = field.label || field.displayName || field.label_ko;
if (fieldName) {
fields.push({
name: fieldName,
label: fieldLabel,
});
}
});
});
} else if (node.type === "externalDBSource" && (node.data as any).fields) {
(node.data as any).fields.forEach((field: any) => {
fields.push({
name: field.name,
label: field.label || field.displayName,
});
});
} else {
// 필드가 없으면 상위 노드로 계속 탐색
const upperResult = getAllSourceFields(node.id, visitedNodes);
fields.push(...upperResult.fields);
foundRestAPI = foundRestAPI || upperResult.hasRestAPI;
}
}
// 4️⃣ 통과 노드 (조건, 기타 모든 노드): 상위 노드로 계속 탐색
else {
const upperResult = getAllSourceFields(node.id, visitedNodes);
fields.push(...upperResult.fields);
foundRestAPI = foundRestAPI || upperResult.hasRestAPI;
}
});
@@ -52,14 +52,6 @@ export const NODE_PALETTE: NodePaletteItem[] = [
category: "transform",
color: "#EAB308", // 노란색
},
{
type: "fieldMapping",
label: "필드 매핑",
icon: "🔀",
description: "소스 필드를 타겟 필드로 매핑합니다",
category: "transform",
color: "#8B5CF6", // 보라색
},
{
type: "dataTransform",
label: "데이터 변환",