제어관리 노드 작동 방식 수정
This commit is contained in:
@@ -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));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
-191
@@ -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>
|
||||
);
|
||||
}
|
||||
+67
-19
@@ -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);
|
||||
|
||||
+28
-19
@@ -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;
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
+28
-19
@@ -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: "데이터 변환",
|
||||
|
||||
Reference in New Issue
Block a user