"use client"; /** * 조건 분기 노드 속성 편집 */ import { useEffect, useState } from "react"; import { Plus, Trash2 } 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 { 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; } const OPERATORS = [ { value: "EQUALS", label: "같음 (=)" }, { value: "NOT_EQUALS", label: "같지 않음 (≠)" }, { value: "GREATER_THAN", label: "보다 큼 (>)" }, { value: "LESS_THAN", label: "보다 작음 (<)" }, { value: "GREATER_THAN_OR_EQUAL", label: "크거나 같음 (≥)" }, { value: "LESS_THAN_OR_EQUAL", label: "작거나 같음 (≤)" }, { value: "LIKE", label: "포함 (LIKE)" }, { value: "NOT_LIKE", label: "미포함 (NOT LIKE)" }, { value: "IN", label: "IN" }, { value: "NOT_IN", label: "NOT IN" }, { value: "IS_NULL", label: "NULL" }, { value: "IS_NOT_NULL", label: "NOT NULL" }, ] as const; export function ConditionProperties({ nodeId, data }: ConditionPropertiesProps) { 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(() => { setDisplayName(data.displayName || "조건 분기"); setConditions(data.conditions || []); 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 === "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" || sourceNode.type === "deleteAction" || sourceNode.type === "upsertAction" ) { // Action 노드: 재귀적으로 상위 노드 필드 수집 fields.push(...getAllSourceFields(sourceNode.id, visited)); } else { // 기타 모든 노드: 재귀적으로 상위 노드 필드 수집 (통과 노드로 처리) console.log(`✅ [ConditionProperties] 통과 노드 (${sourceNode.type}) → 상위 탐색`); 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, { field: "", operator: "EQUALS", value: "", valueType: "static", // "static" (고정값) 또는 "field" (필드 참조) }, ]); }; const handleRemoveCondition = (index: number) => { const newConditions = conditions.filter((_, i) => i !== index); setConditions(newConditions); updateNode(nodeId, { conditions: newConditions, }); }; const handleDisplayNameChange = (newDisplayName: string) => { setDisplayName(newDisplayName); updateNode(nodeId, { displayName: newDisplayName, }); }; const handleConditionChange = (index: number, field: string, value: any) => { const newConditions = [...conditions]; newConditions[index] = { ...newConditions[index], [field]: value }; setConditions(newConditions); updateNode(nodeId, { conditions: newConditions, }); }; const handleLogicChange = (newLogic: "AND" | "OR") => { setLogic(newLogic); updateNode(nodeId, { logic: newLogic, }); }; return (
{/* 기본 정보 */}

기본 정보

handleDisplayNameChange(e.target.value)} className="mt-1" placeholder="노드 표시 이름" />
{/* 조건식 */}

조건식

{conditions.length > 0 ? (
{conditions.map((condition, index) => (
조건 #{index + 1} {index > 0 && ( {logic} )}
{availableFields.length > 0 ? ( ) : (
소스 노드를 연결하세요
)}
{condition.operator !== "IS_NULL" && condition.operator !== "IS_NOT_NULL" && ( <>
{(condition as any).valueType === "field" ? ( // 필드 참조: 드롭다운으로 선택 availableFields.length > 0 ? ( ) : (
소스 노드를 연결하세요
) ) : ( // 고정값: 직접 입력 handleConditionChange(index, "value", e.target.value)} placeholder="비교할 값" className="mt-1 h-8 text-xs" /> )}
)}
))}
) : (
조건식이 없습니다. "추가" 버튼을 클릭하세요.
)}
{/* 안내 */}
🔌 소스 노드 연결: 테이블/외부DB 노드를 연결하면 자동으로 필드 목록이 표시됩니다.
🔄 비교 값 타입:
고정값: 직접 입력한 값과 비교 (예: age > 30)
필드 참조: 다른 필드의 값과 비교 (예: 주문수량 > 재고수량)
💡 AND: 모든 조건이 참이어야 TRUE 출력
💡 OR: 하나라도 참이면 TRUE 출력
⚡ TRUE 출력은 오른쪽 위, FALSE 출력은 오른쪽 아래입니다.
); }