"use client"; /** * 프로시저/함수 호출 노드 속성 편집 */ import { useEffect, useState, useCallback } from "react"; import { Label } from "@/components/ui/label"; import { Input } from "@/components/ui/input"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, } from "@/components/ui/select"; import { Card, CardContent } from "@/components/ui/card"; import { Button } from "@/components/ui/button"; import { Database, Workflow, RefreshCw, Loader2 } from "lucide-react"; import { useFlowEditorStore } from "@/lib/stores/flowEditorStore"; import { getFlowProcedures, getFlowProcedureParameters, } from "@/lib/api/flow"; import { ExternalDbConnectionAPI } from "@/lib/api/externalDbConnection"; import type { ProcedureCallActionNodeData } from "@/types/node-editor"; import type { ProcedureListItem, ProcedureParameterInfo } from "@/types/flowExternalDb"; interface ExternalConnection { id: number; connection_name: string; db_type: string; } interface ProcedureCallActionPropertiesProps { nodeId: string; data: ProcedureCallActionNodeData; } export function ProcedureCallActionProperties({ nodeId, data, }: ProcedureCallActionPropertiesProps) { const { updateNode, nodes, edges } = useFlowEditorStore(); const [displayName, setDisplayName] = useState( data.displayName || "프로시저 호출" ); const [dbSource, setDbSource] = useState<"internal" | "external">( data.dbSource || "internal" ); const [connectionId, setConnectionId] = useState( data.connectionId ); const [procedureName, setProcedureName] = useState( data.procedureName || "" ); const [procedureSchema, setProcedureSchema] = useState( data.procedureSchema || "public" ); const [callType, setCallType] = useState<"procedure" | "function">( data.callType || "function" ); const [parameters, setParameters] = useState(data.parameters || []); const [connections, setConnections] = useState([]); const [procedures, setProcedures] = useState([]); const [loadingProcedures, setLoadingProcedures] = useState(false); const [loadingParams, setLoadingParams] = useState(false); const [sourceFields, setSourceFields] = useState< Array<{ name: string; label?: string }> >([]); // 이전 노드에서 소스 필드 목록 수집 (재귀) useEffect(() => { const getUpstreamFields = ( targetId: string, visited = new Set() ): Array<{ name: string; label?: string }> => { if (visited.has(targetId)) return []; visited.add(targetId); const inEdges = edges.filter((e) => e.target === targetId); const parentNodes = nodes.filter((n) => inEdges.some((e) => e.source === n.id) ); const fields: Array<{ name: string; label?: string }> = []; for (const pNode of parentNodes) { if ( pNode.type === "tableSource" || pNode.type === "externalDBSource" ) { const nodeFields = (pNode.data as any).fields || (pNode.data as any).outputFields || []; if (Array.isArray(nodeFields)) { for (const f of nodeFields) { const name = typeof f === "string" ? f : f.name || f.columnName || f.field; if (name) { fields.push({ name, label: f.label || f.columnLabel || name, }); } } } } else if (pNode.type === "dataTransform") { const upper = getUpstreamFields(pNode.id, visited); fields.push(...upper); const transforms = (pNode.data as any).transformations; if (Array.isArray(transforms)) { for (const t of transforms) { if (t.targetField) { fields.push({ name: t.targetField, label: t.targetFieldLabel || t.targetField, }); } } } } else if (pNode.type === "formulaTransform") { const upper = getUpstreamFields(pNode.id, visited); fields.push(...upper); const transforms = (pNode.data as any).transformations; if (Array.isArray(transforms)) { for (const t of transforms) { if (t.outputField) { fields.push({ name: t.outputField, label: t.outputFieldLabel || t.outputField, }); } } } } else { fields.push(...getUpstreamFields(pNode.id, visited)); } } return fields; }; const collected = getUpstreamFields(nodeId); const unique = Array.from( new Map(collected.map((f) => [f.name, f])).values() ); setSourceFields(unique); }, [nodeId, nodes, edges]); useEffect(() => { setDisplayName(data.displayName || "프로시저 호출"); setDbSource(data.dbSource || "internal"); setConnectionId(data.connectionId); setProcedureName(data.procedureName || ""); setProcedureSchema(data.procedureSchema || "public"); setCallType(data.callType || "function"); setParameters(data.parameters || []); }, [data]); // 외부 DB 연결 목록 조회 useEffect(() => { if (dbSource === "external") { ExternalDbConnectionAPI.getConnections({ is_active: "true" }) .then((list) => setConnections( list.map((c: any) => ({ id: c.id, connection_name: c.connection_name, db_type: c.db_type, })) ) ) .catch(console.error); } }, [dbSource]); const updateNodeData = useCallback( (updates: Partial) => { updateNode(nodeId, { ...data, ...updates }); }, [nodeId, data, updateNode] ); // 프로시저 목록 조회 const fetchProcedures = useCallback(async () => { if (dbSource === "external" && !connectionId) return; setLoadingProcedures(true); try { const res = await getFlowProcedures( dbSource, connectionId, procedureSchema || undefined ); if (res.success && res.data) { setProcedures(res.data); } } catch (e) { console.error("프로시저 목록 조회 실패:", e); } finally { setLoadingProcedures(false); } }, [dbSource, connectionId, procedureSchema]); // dbSource/connectionId 변경 시 프로시저 목록 자동 조회 useEffect(() => { if (dbSource === "internal" || (dbSource === "external" && connectionId)) { fetchProcedures(); } }, [dbSource, connectionId, fetchProcedures]); // 프로시저 선택 시 파라미터 조회 const handleProcedureSelect = useCallback( async (name: string) => { setProcedureName(name); const selected = procedures.find((p) => p.name === name); const newCallType = selected?.type === "PROCEDURE" ? "procedure" : "function"; setCallType(newCallType); updateNodeData({ procedureName: name, callType: newCallType, procedureSchema, }); setLoadingParams(true); try { const res = await getFlowProcedureParameters( name, dbSource, connectionId, procedureSchema || undefined ); if (res.success && res.data) { const newParams = res.data.map((p: ProcedureParameterInfo) => ({ name: p.name, dataType: p.dataType, mode: p.mode, source: "record_field" as const, field: "", value: "", })); setParameters(newParams); updateNodeData({ procedureName: name, callType: newCallType, procedureSchema, parameters: newParams, }); } } catch (e) { console.error("파라미터 조회 실패:", e); } finally { setLoadingParams(false); } }, [dbSource, connectionId, procedureSchema, procedures, updateNodeData] ); const handleParamChange = ( index: number, field: string, value: string ) => { const newParams = [...parameters]; (newParams[index] as any)[field] = value; setParameters(newParams); updateNodeData({ parameters: newParams }); }; return (
{/* 표시명 */}
{ setDisplayName(e.target.value); updateNodeData({ displayName: e.target.value }); }} placeholder="프로시저 호출" className="h-8 text-sm" />
{/* DB 소스 */}
{/* 외부 DB 연결 선택 */} {dbSource === "external" && (
)} {/* 스키마 */}
setProcedureSchema(e.target.value)} onBlur={() => { updateNodeData({ procedureSchema }); fetchProcedures(); }} placeholder="public" className="h-8 text-sm" />
{/* 프로시저 선택 */}
{loadingProcedures ? (
목록 조회 중...
) : ( )}
{/* 호출 타입 */} {procedureName && (
)} {/* 파라미터 매핑 */} {procedureName && parameters.length > 0 && (
{loadingParams ? (
파라미터 조회 중...
) : ( <> {/* IN 파라미터 */} {parameters.filter((p) => p.mode === "IN" || p.mode === "INOUT") .length > 0 && (
{parameters.map((param, idx) => { if (param.mode !== "IN" && param.mode !== "INOUT") return null; return (
{param.name} {param.dataType}
{param.source === "record_field" && (sourceFields.length > 0 ? ( ) : ( handleParamChange( idx, "field", e.target.value ) } placeholder="컬럼명 (이전 노드를 먼저 연결하세요)" className="h-7 text-xs" /> ))} {param.source === "static" && ( handleParamChange( idx, "value", e.target.value ) } placeholder="고정값 입력" className="h-7 text-xs" /> )} {param.source === "step_variable" && ( handleParamChange( idx, "field", e.target.value ) } placeholder="변수명" className="h-7 text-xs" /> )}
); })}
)} {/* OUT 파라미터 (반환 필드) */} {parameters.filter((p) => p.mode === "OUT" || p.mode === "INOUT") .length > 0 && (
{parameters .filter( (p) => p.mode === "OUT" || p.mode === "INOUT" ) .map((param, idx) => (
{param.name} {param.dataType}
))}
)} )}
)} {/* 안내 메시지 */}
프로시저 실행 안내

이 노드에 연결된 이전 노드의 데이터가 프로시저의 입력 파라미터로 전달됩니다. 프로시저 실행이 실패하면 전체 트랜잭션이 롤백됩니다.

); }