"use client"; /** * 데이터 변환 노드 속성 편집 (개선 버전) */ import { useEffect, useState } from "react"; import { Plus, Trash2, Wand2, 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 { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"; import { useFlowEditorStore } from "@/lib/stores/flowEditorStore"; import type { DataTransformNodeData } from "@/types/node-editor"; interface DataTransformPropertiesProps { nodeId: string; data: DataTransformNodeData; } const TRANSFORM_TYPES = [ { value: "UPPERCASE", label: "대문자 변환", category: "기본" }, { value: "LOWERCASE", label: "소문자 변환", category: "기본" }, { value: "TRIM", label: "공백 제거", category: "기본" }, { value: "CONCAT", label: "문자열 결합", category: "기본" }, { value: "SPLIT", label: "문자열 분리", category: "기본" }, { value: "REPLACE", label: "문자열 치환", category: "기본" }, { value: "EXPLODE", label: "행 확장 (1→N)", category: "고급" }, { value: "CAST", label: "타입 변환", category: "고급" }, { value: "FORMAT", label: "형식화", category: "고급" }, { value: "CALCULATE", label: "계산식", category: "고급" }, { value: "JSON_EXTRACT", label: "JSON 추출", category: "고급" }, { value: "CUSTOM", label: "사용자 정의", category: "고급" }, ] as const; export function DataTransformProperties({ nodeId, data }: DataTransformPropertiesProps) { const { updateNode, nodes, edges } = useFlowEditorStore(); const [displayName, setDisplayName] = useState(data.displayName || "데이터 변환"); const [transformations, setTransformations] = useState(data.transformations || []); // 소스 필드 목록 (연결된 입력 노드에서 가져오기) const [sourceFields, setSourceFields] = useState>([]); // 데이터 변경 시 로컬 상태 업데이트 useEffect(() => { setDisplayName(data.displayName || "데이터 변환"); setTransformations(data.transformations || []); }, [data]); // 연결된 소스 노드에서 필드 가져오기 useEffect(() => { const inputEdges = edges.filter((edge) => edge.target === nodeId); const sourceNodeIds = inputEdges.map((edge) => edge.source); const sourceNodes = nodes.filter((node) => sourceNodeIds.includes(node.id)); const fields: Array<{ name: string; label?: string }> = []; sourceNodes.forEach((node) => { if (node.type === "tableSource" && node.data.fields) { node.data.fields.forEach((field: any) => { fields.push({ name: field.name, label: field.label || field.displayName, }); }); } else if (node.type === "externalDBSource" && node.data.fields) { node.data.fields.forEach((field: any) => { fields.push({ name: field.name, label: field.label || field.displayName, }); }); } }); setSourceFields(fields); }, [nodeId, nodes, edges]); const handleAddTransformation = () => { setTransformations([ ...transformations, { type: "UPPERCASE" as const, sourceField: "", targetField: "", }, ]); }; const handleRemoveTransformation = (index: number) => { const newTransformations = transformations.filter((_, i) => i !== index); setTransformations(newTransformations); // 즉시 반영 updateNode(nodeId, { displayName, transformations: newTransformations, }); }; const handleTransformationChange = (index: number, field: string, value: any) => { const newTransformations = [...transformations]; // 필드 변경 시 라벨도 함께 저장 if (field === "sourceField") { const sourceField = sourceFields.find((f) => f.name === value); newTransformations[index] = { ...newTransformations[index], sourceField: value, sourceFieldLabel: sourceField?.label, }; } else if (field === "targetField") { // 타겟 필드는 새로 생성하는 필드이므로 라벨은 사용자가 직접 입력 newTransformations[index] = { ...newTransformations[index], targetField: value, }; } else { newTransformations[index] = { ...newTransformations[index], [field]: value }; } setTransformations(newTransformations); }; const handleSave = () => { updateNode(nodeId, { displayName, transformations, }); }; const renderTransformationFields = (transform: any, index: number) => { const commonFields = ( <> {/* 소스 필드 */} {transform.type !== "CONCAT" && (
)} {/* 타겟 필드 */}
handleTransformationChange(index, "targetField", e.target.value)} placeholder="비어있으면 소스 필드에 적용" className="mt-1 h-8 text-xs" />

{transform.targetField ? ( transform.targetField === transform.sourceField ? ( ✓ 소스 필드를 덮어씁니다 ) : ( ✓ 새 필드를 생성합니다 ) ) : ( 비어있음: 소스 필드를 덮어씁니다 )}

); // 타입별 추가 필드 switch (transform.type) { case "EXPLODE": return ( <> {commonFields}
handleTransformationChange(index, "delimiter", e.target.value)} placeholder="예: , 또는 ; 또는 |" className="mt-1 h-8 text-xs" />

이 문자로 분리하여 여러 행으로 확장합니다

); case "CONCAT": return ( <> {/* CONCAT은 다중 소스 필드를 지원 - 간소화 버전 */}
handleTransformationChange(index, "separator", e.target.value)} placeholder="예: 공백 또는 , 또는 -" className="mt-1 h-8 text-xs" />
{commonFields} ); case "SPLIT": return ( <> {commonFields}
handleTransformationChange(index, "delimiter", e.target.value)} placeholder="예: , 또는 ; 또는 |" className="mt-1 h-8 text-xs" />
handleTransformationChange(index, "splitIndex", parseInt(e.target.value))} placeholder="0" className="mt-1 h-8 text-xs" />

분리된 값 중 몇 번째를 가져올지 지정 (0=첫번째)

); case "REPLACE": return ( <> {commonFields}
handleTransformationChange(index, "searchValue", e.target.value)} placeholder="예: old" className="mt-1 h-8 text-xs" />
handleTransformationChange(index, "replaceValue", e.target.value)} placeholder="예: new" className="mt-1 h-8 text-xs" />
); case "CAST": return ( <> {commonFields}
); case "CALCULATE": case "FORMAT": case "JSON_EXTRACT": case "CUSTOM": return ( <> {commonFields}
handleTransformationChange(index, "expression", e.target.value)} placeholder="예: field1 + field2" className="mt-1 h-8 text-xs" />

{transform.type === "CALCULATE" && "계산 수식을 입력하세요 (예: field1 + field2)"} {transform.type === "FORMAT" && "형식 문자열을 입력하세요 (예: {0}-{1})"} {transform.type === "JSON_EXTRACT" && "JSON 경로를 입력하세요 (예: $.data.name)"} {transform.type === "CUSTOM" && "JavaScript 표현식을 입력하세요"}

); default: // UPPERCASE, LOWERCASE, TRIM 등 return commonFields; } }; return (
{/* 헤더 */}
데이터 변환
{/* 기본 정보 */}

기본 정보

setDisplayName(e.target.value)} className="mt-1" placeholder="노드 표시 이름" />
{/* 변환 규칙 */}

변환 규칙

데이터를 변환할 규칙을 추가하세요

{transformations.length === 0 ? (
변환 규칙을 추가하세요
) : (
{transformations.map((transform, index) => (
변환 #{index + 1}
{/* 변환 타입 선택 */}
{/* 타입별 필드 렌더링 */} {renderTransformationFields(transform, index)}
))}
)}
{/* 적용 버튼 */}

✅ 변경 사항이 즉시 노드에 반영됩니다.

); }