feat: Add procedure and function management in flow controller
- Introduced new endpoints in FlowController for listing procedures and retrieving procedure parameters, enhancing the flow management capabilities. - Updated FlowDataMoveService to support procedure calls during data movement, ensuring seamless integration with external and internal databases. - Enhanced NodeFlowExecutionService to execute procedure call actions, allowing for dynamic execution of stored procedures within flow nodes. - Updated frontend components to support procedure selection and parameter management, improving user experience in configuring flow steps. - Added necessary types and API functions for handling procedure-related data, ensuring type safety and clarity in implementation.
This commit is contained in:
@@ -32,6 +32,7 @@ import { LogNode } from "./nodes/LogNode";
|
||||
import { EmailActionNode } from "./nodes/EmailActionNode";
|
||||
import { ScriptActionNode } from "./nodes/ScriptActionNode";
|
||||
import { HttpRequestActionNode } from "./nodes/HttpRequestActionNode";
|
||||
import { ProcedureCallActionNode } from "./nodes/ProcedureCallActionNode";
|
||||
import { validateFlow } from "@/lib/utils/flowValidation";
|
||||
import type { FlowValidation } from "@/lib/utils/flowValidation";
|
||||
|
||||
@@ -55,6 +56,7 @@ const nodeTypes = {
|
||||
emailAction: EmailActionNode,
|
||||
scriptAction: ScriptActionNode,
|
||||
httpRequestAction: HttpRequestActionNode,
|
||||
procedureCallAction: ProcedureCallActionNode,
|
||||
// 유틸리티
|
||||
comment: CommentNode,
|
||||
log: LogNode,
|
||||
|
||||
@@ -0,0 +1,121 @@
|
||||
"use client";
|
||||
|
||||
/**
|
||||
* 프로시저/함수 호출 액션 노드
|
||||
* 내부 또는 외부 DB의 프로시저/함수를 호출하는 노드
|
||||
*/
|
||||
|
||||
import { memo } from "react";
|
||||
import { Handle, Position, NodeProps } from "reactflow";
|
||||
import { Database, Workflow } from "lucide-react";
|
||||
import type { ProcedureCallActionNodeData } from "@/types/node-editor";
|
||||
|
||||
export const ProcedureCallActionNode = memo(
|
||||
({ data, selected }: NodeProps<ProcedureCallActionNodeData>) => {
|
||||
const hasProcedure = !!data.procedureName;
|
||||
const inParams = data.parameters?.filter((p) => p.mode === "IN" || p.mode === "INOUT") ?? [];
|
||||
const outParams = data.parameters?.filter((p) => p.mode === "OUT" || p.mode === "INOUT") ?? [];
|
||||
|
||||
return (
|
||||
<div
|
||||
className={`min-w-[250px] rounded-lg border-2 bg-white shadow-md transition-all ${
|
||||
selected ? "border-violet-500 shadow-lg" : "border-gray-200"
|
||||
}`}
|
||||
>
|
||||
{/* 입력 핸들 */}
|
||||
<Handle
|
||||
type="target"
|
||||
position={Position.Left}
|
||||
className="!h-3 !w-3 !border-2 !border-white !bg-violet-500"
|
||||
/>
|
||||
|
||||
{/* 헤더 */}
|
||||
<div className="flex items-center gap-2 rounded-t-lg bg-violet-500 px-3 py-2 text-white">
|
||||
<Workflow className="h-4 w-4" />
|
||||
<div className="flex-1">
|
||||
<div className="text-sm font-semibold">
|
||||
{data.displayName || "프로시저 호출"}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 본문 */}
|
||||
<div className="space-y-2 p-3">
|
||||
{/* DB 소스 */}
|
||||
<div className="flex items-center gap-2">
|
||||
<Database className="h-3 w-3 text-gray-400" />
|
||||
<span className="text-xs text-gray-600">
|
||||
{data.dbSource === "external" ? (
|
||||
<span className="rounded bg-amber-100 px-2 py-0.5 text-amber-700">
|
||||
{data.connectionName || "외부 DB"}
|
||||
</span>
|
||||
) : (
|
||||
<span className="rounded bg-blue-100 px-2 py-0.5 text-blue-700">
|
||||
내부 DB
|
||||
</span>
|
||||
)}
|
||||
</span>
|
||||
<span
|
||||
className={`ml-auto rounded px-2 py-0.5 text-xs font-medium ${
|
||||
data.callType === "function"
|
||||
? "bg-cyan-100 text-cyan-700"
|
||||
: "bg-violet-100 text-violet-700"
|
||||
}`}
|
||||
>
|
||||
{data.callType === "function" ? "FUNCTION" : "PROCEDURE"}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{/* 프로시저명 */}
|
||||
<div className="flex items-center gap-2 text-xs">
|
||||
<Workflow className="h-3 w-3 text-gray-400" />
|
||||
{hasProcedure ? (
|
||||
<span className="font-mono text-green-600 truncate">
|
||||
{data.procedureSchema && data.procedureSchema !== "public"
|
||||
? `${data.procedureSchema}.`
|
||||
: ""}
|
||||
{data.procedureName}()
|
||||
</span>
|
||||
) : (
|
||||
<span className="text-orange-500">프로시저 선택 필요</span>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* 파라미터 수 */}
|
||||
{hasProcedure && inParams.length > 0 && (
|
||||
<div className="text-xs text-gray-500">
|
||||
입력 파라미터: {inParams.length}개
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 반환 필드 */}
|
||||
{hasProcedure && outParams.length > 0 && (
|
||||
<div className="mt-1 space-y-1 border-t border-gray-100 pt-1">
|
||||
<div className="text-[10px] font-medium text-green-600">
|
||||
반환 필드:
|
||||
</div>
|
||||
{outParams.map((p) => (
|
||||
<div
|
||||
key={p.name}
|
||||
className="flex items-center justify-between rounded bg-green-50 px-2 py-0.5 text-[10px]"
|
||||
>
|
||||
<span className="font-mono text-green-700">{p.name}</span>
|
||||
<span className="text-gray-400">{p.dataType}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* 출력 핸들 */}
|
||||
<Handle
|
||||
type="source"
|
||||
position={Position.Right}
|
||||
className="!h-3 !w-3 !border-2 !border-white !bg-violet-500"
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
ProcedureCallActionNode.displayName = "ProcedureCallActionNode";
|
||||
@@ -23,6 +23,7 @@ import { LogProperties } from "./properties/LogProperties";
|
||||
import { EmailActionProperties } from "./properties/EmailActionProperties";
|
||||
import { ScriptActionProperties } from "./properties/ScriptActionProperties";
|
||||
import { HttpRequestActionProperties } from "./properties/HttpRequestActionProperties";
|
||||
import { ProcedureCallActionProperties } from "./properties/ProcedureCallActionProperties";
|
||||
import type { NodeType } from "@/types/node-editor";
|
||||
|
||||
export function PropertiesPanel() {
|
||||
@@ -147,6 +148,9 @@ function NodePropertiesRenderer({ node }: { node: any }) {
|
||||
case "httpRequestAction":
|
||||
return <HttpRequestActionProperties nodeId={node.id} data={node.data} />;
|
||||
|
||||
case "procedureCallAction":
|
||||
return <ProcedureCallActionProperties nodeId={node.id} data={node.data} />;
|
||||
|
||||
default:
|
||||
return (
|
||||
<div className="p-4">
|
||||
@@ -185,6 +189,7 @@ function getNodeTypeLabel(type: NodeType): string {
|
||||
emailAction: "메일 발송",
|
||||
scriptAction: "스크립트 실행",
|
||||
httpRequestAction: "HTTP 요청",
|
||||
procedureCallAction: "프로시저 호출",
|
||||
comment: "주석",
|
||||
log: "로그",
|
||||
};
|
||||
|
||||
@@ -20,6 +20,7 @@ import { tableTypeApi } from "@/lib/api/screen";
|
||||
import { getTestedExternalConnections, getExternalTables, getExternalColumns } from "@/lib/api/nodeExternalConnections";
|
||||
import { getNumberingRules } from "@/lib/api/numberingRule";
|
||||
import type { NumberingRuleConfig } from "@/types/numbering-rule";
|
||||
import { getFlowProcedureParameters } from "@/lib/api/flow";
|
||||
import type { InsertActionNodeData } from "@/types/node-editor";
|
||||
import type { ExternalConnection, ExternalTable, ExternalColumn } from "@/lib/api/nodeExternalConnections";
|
||||
|
||||
@@ -171,10 +172,19 @@ export function InsertActionProperties({ nodeId, data }: InsertActionPropertiesP
|
||||
|
||||
// 연결된 소스 노드에서 필드 가져오기 (재귀적으로 모든 상위 노드 탐색)
|
||||
useEffect(() => {
|
||||
// 프로시저 노드 정보를 수집하여 비동기 파라미터 조회에 사용
|
||||
const procedureNodes: Array<{
|
||||
procedureName: string;
|
||||
dbSource: "internal" | "external";
|
||||
connectionId?: number;
|
||||
schema?: string;
|
||||
sourcePath: string[];
|
||||
}> = [];
|
||||
|
||||
const getAllSourceFields = (
|
||||
targetNodeId: string,
|
||||
visitedNodes = new Set<string>(),
|
||||
sourcePath: string[] = [], // 🔥 소스 경로 추적
|
||||
sourcePath: string[] = [],
|
||||
): { fields: Array<{ name: string; label?: string; sourcePath?: string[] }>; hasRestAPI: boolean } => {
|
||||
if (visitedNodes.has(targetNodeId)) {
|
||||
console.log(`⚠️ 순환 참조 감지: ${targetNodeId} (이미 방문함)`);
|
||||
@@ -366,7 +376,48 @@ export function InsertActionProperties({ nodeId, data }: InsertActionPropertiesP
|
||||
foundRestAPI = foundRestAPI || upperResult.hasRestAPI;
|
||||
}
|
||||
}
|
||||
// 5️⃣ 통과 노드 (조건, 기타 모든 노드): 상위 노드로 계속 탐색
|
||||
// 5️⃣ 프로시저 호출 노드: 상위 필드 + OUT 파라미터(반환 필드) 추가
|
||||
else if (node.type === "procedureCallAction") {
|
||||
console.log("✅ 프로시저 호출 노드 발견");
|
||||
const upperResult = getAllSourceFields(node.id, visitedNodes, currentPath);
|
||||
fields.push(...upperResult.fields);
|
||||
foundRestAPI = foundRestAPI || upperResult.hasRestAPI;
|
||||
|
||||
const nodeData = node.data as any;
|
||||
const procParams = nodeData.parameters;
|
||||
let hasOutParams = false;
|
||||
|
||||
if (Array.isArray(procParams)) {
|
||||
for (const p of procParams) {
|
||||
if (p.mode === "OUT" || p.mode === "INOUT") {
|
||||
hasOutParams = true;
|
||||
fields.push({
|
||||
name: p.name,
|
||||
label: `${p.name} (프로시저 반환)`,
|
||||
sourcePath: currentPath,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// OUT 파라미터가 저장되어 있지 않으면 API로 동적 조회 예약
|
||||
if (!hasOutParams && nodeData.procedureName) {
|
||||
procedureNodes.push({
|
||||
procedureName: nodeData.procedureName,
|
||||
dbSource: nodeData.dbSource || "internal",
|
||||
connectionId: nodeData.connectionId,
|
||||
schema: nodeData.procedureSchema || "public",
|
||||
sourcePath: currentPath,
|
||||
});
|
||||
}
|
||||
|
||||
fields.push({
|
||||
name: "_procedureReturn",
|
||||
label: "프로시저 반환값",
|
||||
sourcePath: currentPath,
|
||||
});
|
||||
}
|
||||
// 6️⃣ 통과 노드 (조건, 기타 모든 노드): 상위 노드로 계속 탐색
|
||||
else {
|
||||
console.log(`✅ 통과 노드 (${node.type}) → 상위 노드로 계속 탐색`);
|
||||
const upperResult = getAllSourceFields(node.id, visitedNodes, currentPath);
|
||||
@@ -386,31 +437,66 @@ export function InsertActionProperties({ nodeId, data }: InsertActionPropertiesP
|
||||
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>();
|
||||
const applyFields = (allFields: typeof result.fields) => {
|
||||
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);
|
||||
allFields.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(", ")}`);
|
||||
}
|
||||
// 중복이면 마지막 값으로 덮어씀 (기존 동작 유지)
|
||||
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);
|
||||
console.log("✅ 최종 소스 필드 목록:", uniqueFields);
|
||||
};
|
||||
|
||||
// 프로시저 노드에 OUT 파라미터가 저장되지 않은 경우, API로 동적 조회
|
||||
if (procedureNodes.length > 0) {
|
||||
console.log(`🔄 프로시저 ${procedureNodes.length}개의 반환 필드를 API로 조회`);
|
||||
applyFields(result.fields);
|
||||
|
||||
Promise.all(
|
||||
procedureNodes.map(async (pn) => {
|
||||
try {
|
||||
const res = await getFlowProcedureParameters(
|
||||
pn.procedureName,
|
||||
pn.dbSource,
|
||||
pn.connectionId,
|
||||
pn.schema
|
||||
);
|
||||
if (res.success && res.data) {
|
||||
return res.data
|
||||
.filter((p: any) => p.mode === "OUT" || p.mode === "INOUT")
|
||||
.map((p: any) => ({
|
||||
name: p.name,
|
||||
label: `${p.name} (프로시저 반환)`,
|
||||
sourcePath: pn.sourcePath,
|
||||
}));
|
||||
}
|
||||
} catch (e) {
|
||||
console.error("프로시저 파라미터 조회 실패:", e);
|
||||
}
|
||||
return [];
|
||||
})
|
||||
).then((extraFieldArrays) => {
|
||||
const extraFields = extraFieldArrays.flat();
|
||||
if (extraFields.length > 0) {
|
||||
console.log(`✅ 프로시저 반환 필드 ${extraFields.length}개 추가 발견`);
|
||||
applyFields([...result.fields, ...extraFields]);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
applyFields(result.fields);
|
||||
}
|
||||
|
||||
const uniqueFields = Array.from(fieldMap.values());
|
||||
|
||||
setSourceFields(uniqueFields);
|
||||
setHasRestAPISource(result.hasRestAPI);
|
||||
console.log("✅ 최종 소스 필드 목록:", uniqueFields);
|
||||
console.log("✅ REST API 소스 연결:", result.hasRestAPI);
|
||||
}, [nodeId, nodes, edges]);
|
||||
|
||||
/**
|
||||
|
||||
@@ -0,0 +1,641 @@
|
||||
"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<number | undefined>(
|
||||
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<ExternalConnection[]>([]);
|
||||
const [procedures, setProcedures] = useState<ProcedureListItem[]>([]);
|
||||
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<string>()
|
||||
): 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<ProcedureCallActionNodeData>) => {
|
||||
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 (
|
||||
<div className="space-y-4 p-4">
|
||||
{/* 표시명 */}
|
||||
<div className="space-y-2">
|
||||
<Label className="text-xs font-medium">표시명</Label>
|
||||
<Input
|
||||
value={displayName}
|
||||
onChange={(e) => {
|
||||
setDisplayName(e.target.value);
|
||||
updateNodeData({ displayName: e.target.value });
|
||||
}}
|
||||
placeholder="프로시저 호출"
|
||||
className="h-8 text-sm"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* DB 소스 */}
|
||||
<div className="space-y-2">
|
||||
<Label className="text-xs font-medium">DB 소스</Label>
|
||||
<Select
|
||||
value={dbSource}
|
||||
onValueChange={(v: "internal" | "external") => {
|
||||
setDbSource(v);
|
||||
setConnectionId(undefined);
|
||||
setProcedureName("");
|
||||
setParameters([]);
|
||||
setProcedures([]);
|
||||
updateNodeData({
|
||||
dbSource: v,
|
||||
connectionId: undefined,
|
||||
connectionName: undefined,
|
||||
procedureName: "",
|
||||
parameters: [],
|
||||
});
|
||||
}}
|
||||
>
|
||||
<SelectTrigger className="h-8 text-sm">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="internal">내부 DB (PostgreSQL)</SelectItem>
|
||||
<SelectItem value="external">외부 DB</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
{/* 외부 DB 연결 선택 */}
|
||||
{dbSource === "external" && (
|
||||
<div className="space-y-2">
|
||||
<Label className="text-xs font-medium">외부 DB 연결</Label>
|
||||
<Select
|
||||
value={connectionId?.toString() || ""}
|
||||
onValueChange={(v) => {
|
||||
const id = parseInt(v);
|
||||
setConnectionId(id);
|
||||
setProcedureName("");
|
||||
setParameters([]);
|
||||
const conn = connections.find((c) => c.id === id);
|
||||
updateNodeData({
|
||||
connectionId: id,
|
||||
connectionName: conn?.connection_name,
|
||||
procedureName: "",
|
||||
parameters: [],
|
||||
});
|
||||
}}
|
||||
>
|
||||
<SelectTrigger className="h-8 text-sm">
|
||||
<SelectValue placeholder="연결 선택" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{connections.map((c) => (
|
||||
<SelectItem key={c.id} value={c.id.toString()}>
|
||||
{c.connection_name} ({c.db_type})
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 스키마 */}
|
||||
<div className="space-y-2">
|
||||
<Label className="text-xs font-medium">스키마</Label>
|
||||
<div className="flex gap-2">
|
||||
<Input
|
||||
value={procedureSchema}
|
||||
onChange={(e) => setProcedureSchema(e.target.value)}
|
||||
onBlur={() => {
|
||||
updateNodeData({ procedureSchema });
|
||||
fetchProcedures();
|
||||
}}
|
||||
placeholder="public"
|
||||
className="h-8 text-sm"
|
||||
/>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
className="h-8 w-8 shrink-0 p-0"
|
||||
onClick={fetchProcedures}
|
||||
disabled={loadingProcedures}
|
||||
>
|
||||
{loadingProcedures ? (
|
||||
<Loader2 className="h-3 w-3 animate-spin" />
|
||||
) : (
|
||||
<RefreshCw className="h-3 w-3" />
|
||||
)}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 프로시저 선택 */}
|
||||
<div className="space-y-2">
|
||||
<Label className="text-xs font-medium">프로시저/함수 선택</Label>
|
||||
{loadingProcedures ? (
|
||||
<div className="flex items-center gap-2 rounded border p-2 text-xs text-gray-500">
|
||||
<Loader2 className="h-3 w-3 animate-spin" />
|
||||
목록 조회 중...
|
||||
</div>
|
||||
) : (
|
||||
<Select
|
||||
value={procedureName}
|
||||
onValueChange={handleProcedureSelect}
|
||||
>
|
||||
<SelectTrigger className="h-8 text-sm">
|
||||
<SelectValue placeholder="프로시저 선택" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{procedures.map((p) => (
|
||||
<SelectItem key={`${p.schema}.${p.name}`} value={p.name}>
|
||||
<div className="flex items-center gap-2">
|
||||
<span
|
||||
className={`rounded px-1 py-0.5 text-[10px] font-medium ${
|
||||
p.type === "FUNCTION"
|
||||
? "bg-cyan-100 text-cyan-700"
|
||||
: "bg-violet-100 text-violet-700"
|
||||
}`}
|
||||
>
|
||||
{p.type === "FUNCTION" ? "FN" : "SP"}
|
||||
</span>
|
||||
<span className="font-mono text-xs">{p.name}</span>
|
||||
</div>
|
||||
</SelectItem>
|
||||
))}
|
||||
{procedures.length === 0 && (
|
||||
<SelectItem value="" disabled>
|
||||
프로시저가 없습니다
|
||||
</SelectItem>
|
||||
)}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* 호출 타입 */}
|
||||
{procedureName && (
|
||||
<div className="space-y-2">
|
||||
<Label className="text-xs font-medium">호출 타입</Label>
|
||||
<Select
|
||||
value={callType}
|
||||
onValueChange={(v: "procedure" | "function") => {
|
||||
setCallType(v);
|
||||
updateNodeData({ callType: v });
|
||||
}}
|
||||
>
|
||||
<SelectTrigger className="h-8 text-sm">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="function">SELECT (함수)</SelectItem>
|
||||
<SelectItem value="procedure">CALL (프로시저)</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 파라미터 매핑 */}
|
||||
{procedureName && parameters.length > 0 && (
|
||||
<div className="space-y-3">
|
||||
{loadingParams ? (
|
||||
<div className="flex items-center gap-2 rounded border p-2 text-xs text-gray-500">
|
||||
<Loader2 className="h-3 w-3 animate-spin" />
|
||||
파라미터 조회 중...
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
{/* IN 파라미터 */}
|
||||
{parameters.filter((p) => p.mode === "IN" || p.mode === "INOUT")
|
||||
.length > 0 && (
|
||||
<div className="space-y-2">
|
||||
<Label className="flex items-center gap-1.5 text-xs font-medium">
|
||||
<span className="rounded bg-blue-100 px-1.5 py-0.5 text-[10px] text-blue-700">
|
||||
IN
|
||||
</span>
|
||||
입력 파라미터
|
||||
</Label>
|
||||
<div className="space-y-2">
|
||||
{parameters.map((param, idx) => {
|
||||
if (param.mode !== "IN" && param.mode !== "INOUT")
|
||||
return null;
|
||||
return (
|
||||
<Card key={idx} className="bg-gray-50">
|
||||
<CardContent className="space-y-2 p-3">
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="font-mono text-xs font-medium">
|
||||
{param.name}
|
||||
</span>
|
||||
<span className="rounded bg-gray-200 px-1.5 py-0.5 text-[10px]">
|
||||
{param.dataType}
|
||||
</span>
|
||||
</div>
|
||||
<Select
|
||||
value={param.source}
|
||||
onValueChange={(v) =>
|
||||
handleParamChange(idx, "source", v)
|
||||
}
|
||||
>
|
||||
<SelectTrigger className="h-7 text-xs">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="record_field">
|
||||
레코드 필드
|
||||
</SelectItem>
|
||||
<SelectItem value="static">고정값</SelectItem>
|
||||
<SelectItem value="step_variable">
|
||||
스텝 변수
|
||||
</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
{param.source === "record_field" &&
|
||||
(sourceFields.length > 0 ? (
|
||||
<Select
|
||||
value={param.field || ""}
|
||||
onValueChange={(v) =>
|
||||
handleParamChange(idx, "field", v)
|
||||
}
|
||||
>
|
||||
<SelectTrigger className="h-7 text-xs">
|
||||
<SelectValue placeholder="필드 선택" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{sourceFields.map((f) => (
|
||||
<SelectItem
|
||||
key={f.name}
|
||||
value={f.name}
|
||||
>
|
||||
<span className="font-mono text-xs">
|
||||
{f.name}
|
||||
</span>
|
||||
{f.label && f.label !== f.name && (
|
||||
<span className="ml-1 text-[10px] text-gray-400">
|
||||
({f.label})
|
||||
</span>
|
||||
)}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
) : (
|
||||
<Input
|
||||
value={param.field || ""}
|
||||
onChange={(e) =>
|
||||
handleParamChange(
|
||||
idx,
|
||||
"field",
|
||||
e.target.value
|
||||
)
|
||||
}
|
||||
placeholder="컬럼명 (이전 노드를 먼저 연결하세요)"
|
||||
className="h-7 text-xs"
|
||||
/>
|
||||
))}
|
||||
{param.source === "static" && (
|
||||
<Input
|
||||
value={param.value || ""}
|
||||
onChange={(e) =>
|
||||
handleParamChange(
|
||||
idx,
|
||||
"value",
|
||||
e.target.value
|
||||
)
|
||||
}
|
||||
placeholder="고정값 입력"
|
||||
className="h-7 text-xs"
|
||||
/>
|
||||
)}
|
||||
{param.source === "step_variable" && (
|
||||
<Input
|
||||
value={param.field || ""}
|
||||
onChange={(e) =>
|
||||
handleParamChange(
|
||||
idx,
|
||||
"field",
|
||||
e.target.value
|
||||
)
|
||||
}
|
||||
placeholder="변수명"
|
||||
className="h-7 text-xs"
|
||||
/>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* OUT 파라미터 (반환 필드) */}
|
||||
{parameters.filter((p) => p.mode === "OUT" || p.mode === "INOUT")
|
||||
.length > 0 && (
|
||||
<div className="space-y-2">
|
||||
<Label className="flex items-center gap-1.5 text-xs font-medium">
|
||||
<span className="rounded bg-green-100 px-1.5 py-0.5 text-[10px] text-green-700">
|
||||
OUT
|
||||
</span>
|
||||
반환 필드
|
||||
<span className="text-[10px] font-normal text-gray-400">
|
||||
(다음 노드에서 사용 가능)
|
||||
</span>
|
||||
</Label>
|
||||
<div className="rounded-md border border-green-200 bg-green-50 p-2">
|
||||
<div className="space-y-1">
|
||||
{parameters
|
||||
.filter(
|
||||
(p) => p.mode === "OUT" || p.mode === "INOUT"
|
||||
)
|
||||
.map((param, idx) => (
|
||||
<div
|
||||
key={idx}
|
||||
className="flex items-center justify-between rounded bg-white px-2 py-1.5"
|
||||
>
|
||||
<span className="font-mono text-xs font-medium text-green-700">
|
||||
{param.name}
|
||||
</span>
|
||||
<span className="rounded bg-gray-100 px-1.5 py-0.5 text-[10px] text-gray-500">
|
||||
{param.dataType}
|
||||
</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 안내 메시지 */}
|
||||
<Card className="bg-violet-50">
|
||||
<CardContent className="p-3 text-xs text-violet-700">
|
||||
<div className="mb-1 flex items-center gap-1 font-medium">
|
||||
<Workflow className="h-3 w-3" />
|
||||
프로시저 실행 안내
|
||||
</div>
|
||||
<p>
|
||||
이 노드에 연결된 이전 노드의 데이터가 프로시저의 입력 파라미터로
|
||||
전달됩니다. 프로시저 실행이 실패하면 전체 트랜잭션이 롤백됩니다.
|
||||
</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -17,6 +17,7 @@ import { cn } from "@/lib/utils";
|
||||
import { useFlowEditorStore } from "@/lib/stores/flowEditorStore";
|
||||
import { tableTypeApi } from "@/lib/api/screen";
|
||||
import { getTestedExternalConnections, getExternalTables, getExternalColumns } from "@/lib/api/nodeExternalConnections";
|
||||
import { getFlowProcedureParameters } from "@/lib/api/flow";
|
||||
import type { UpdateActionNodeData } from "@/types/node-editor";
|
||||
import type { ExternalConnection, ExternalTable, ExternalColumn } from "@/lib/api/nodeExternalConnections";
|
||||
|
||||
@@ -165,6 +166,13 @@ export function UpdateActionProperties({ nodeId, data }: UpdateActionPropertiesP
|
||||
|
||||
// 연결된 소스 노드에서 필드 가져오기 (재귀적으로 모든 상위 노드 탐색)
|
||||
useEffect(() => {
|
||||
const procedureNodes: Array<{
|
||||
procedureName: string;
|
||||
dbSource: "internal" | "external";
|
||||
connectionId?: number;
|
||||
schema?: string;
|
||||
}> = [];
|
||||
|
||||
const getAllSourceFields = (
|
||||
targetNodeId: string,
|
||||
visitedNodes = new Set<string>(),
|
||||
@@ -310,7 +318,33 @@ export function UpdateActionProperties({ nodeId, data }: UpdateActionPropertiesP
|
||||
foundRestAPI = foundRestAPI || upperResult.hasRestAPI;
|
||||
}
|
||||
}
|
||||
// 5️⃣ 통과 노드 (조건, 기타 모든 노드): 상위 노드로 계속 탐색
|
||||
// 5️⃣ 프로시저 호출 노드: 상위 필드 + OUT 파라미터 추가
|
||||
else if (node.type === "procedureCallAction") {
|
||||
const upperResult = getAllSourceFields(node.id, visitedNodes);
|
||||
fields.push(...upperResult.fields);
|
||||
foundRestAPI = foundRestAPI || upperResult.hasRestAPI;
|
||||
const nodeData = node.data as any;
|
||||
const procParams = nodeData.parameters;
|
||||
let hasOutParams = false;
|
||||
if (Array.isArray(procParams)) {
|
||||
for (const p of procParams) {
|
||||
if (p.mode === "OUT" || p.mode === "INOUT") {
|
||||
hasOutParams = true;
|
||||
fields.push({ name: p.name, label: `${p.name} (프로시저 반환)` });
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!hasOutParams && nodeData.procedureName) {
|
||||
procedureNodes.push({
|
||||
procedureName: nodeData.procedureName,
|
||||
dbSource: nodeData.dbSource || "internal",
|
||||
connectionId: nodeData.connectionId,
|
||||
schema: nodeData.procedureSchema || "public",
|
||||
});
|
||||
}
|
||||
fields.push({ name: "_procedureReturn", label: "프로시저 반환값" });
|
||||
}
|
||||
// 6️⃣ 통과 노드 (조건, 기타 모든 노드): 상위 노드로 계속 탐색
|
||||
else {
|
||||
const upperResult = getAllSourceFields(node.id, visitedNodes);
|
||||
fields.push(...upperResult.fields);
|
||||
@@ -323,11 +357,33 @@ export function UpdateActionProperties({ nodeId, data }: UpdateActionPropertiesP
|
||||
|
||||
const result = getAllSourceFields(nodeId);
|
||||
|
||||
// 중복 제거
|
||||
const uniqueFields = Array.from(new Map(result.fields.map((field) => [field.name, field])).values());
|
||||
const applyFields = (allFields: typeof result.fields) => {
|
||||
const uniqueFields = Array.from(new Map(allFields.map((field) => [field.name, field])).values());
|
||||
setSourceFields(uniqueFields);
|
||||
setHasRestAPISource(result.hasRestAPI);
|
||||
};
|
||||
|
||||
setSourceFields(uniqueFields);
|
||||
setHasRestAPISource(result.hasRestAPI);
|
||||
if (procedureNodes.length > 0) {
|
||||
applyFields(result.fields);
|
||||
Promise.all(
|
||||
procedureNodes.map(async (pn) => {
|
||||
try {
|
||||
const res = await getFlowProcedureParameters(pn.procedureName, pn.dbSource, pn.connectionId, pn.schema);
|
||||
if (res.success && res.data) {
|
||||
return res.data
|
||||
.filter((p: any) => p.mode === "OUT" || p.mode === "INOUT")
|
||||
.map((p: any) => ({ name: p.name, label: `${p.name} (프로시저 반환)` }));
|
||||
}
|
||||
} catch (e) { console.error("프로시저 파라미터 조회 실패:", e); }
|
||||
return [];
|
||||
})
|
||||
).then((extraFieldArrays) => {
|
||||
const extraFields = extraFieldArrays.flat();
|
||||
if (extraFields.length > 0) applyFields([...result.fields, ...extraFields]);
|
||||
});
|
||||
} else {
|
||||
applyFields(result.fields);
|
||||
}
|
||||
}, [nodeId, nodes, edges]);
|
||||
|
||||
const loadTables = async () => {
|
||||
|
||||
@@ -17,6 +17,7 @@ import { cn } from "@/lib/utils";
|
||||
import { useFlowEditorStore } from "@/lib/stores/flowEditorStore";
|
||||
import { tableTypeApi } from "@/lib/api/screen";
|
||||
import { getTestedExternalConnections, getExternalTables, getExternalColumns } from "@/lib/api/nodeExternalConnections";
|
||||
import { getFlowProcedureParameters } from "@/lib/api/flow";
|
||||
import type { UpsertActionNodeData } from "@/types/node-editor";
|
||||
import type { ExternalConnection, ExternalTable, ExternalColumn } from "@/lib/api/nodeExternalConnections";
|
||||
|
||||
@@ -148,6 +149,13 @@ export function UpsertActionProperties({ nodeId, data }: UpsertActionPropertiesP
|
||||
|
||||
// 연결된 소스 노드에서 필드 가져오기 (재귀적으로 모든 상위 노드 탐색)
|
||||
useEffect(() => {
|
||||
const procedureNodes: Array<{
|
||||
procedureName: string;
|
||||
dbSource: "internal" | "external";
|
||||
connectionId?: number;
|
||||
schema?: string;
|
||||
}> = [];
|
||||
|
||||
const getAllSourceFields = (
|
||||
targetNodeId: string,
|
||||
visitedNodes = new Set<string>(),
|
||||
@@ -293,7 +301,33 @@ export function UpsertActionProperties({ nodeId, data }: UpsertActionPropertiesP
|
||||
foundRestAPI = foundRestAPI || upperResult.hasRestAPI;
|
||||
}
|
||||
}
|
||||
// 5️⃣ 통과 노드 (조건, 기타 모든 노드): 상위 노드로 계속 탐색
|
||||
// 5️⃣ 프로시저 호출 노드: 상위 필드 + OUT 파라미터 추가
|
||||
else if (node.type === "procedureCallAction") {
|
||||
const upperResult = getAllSourceFields(node.id, visitedNodes);
|
||||
fields.push(...upperResult.fields);
|
||||
foundRestAPI = foundRestAPI || upperResult.hasRestAPI;
|
||||
const nodeData = node.data as any;
|
||||
const procParams = nodeData.parameters;
|
||||
let hasOutParams = false;
|
||||
if (Array.isArray(procParams)) {
|
||||
for (const p of procParams) {
|
||||
if (p.mode === "OUT" || p.mode === "INOUT") {
|
||||
hasOutParams = true;
|
||||
fields.push({ name: p.name, label: `${p.name} (프로시저 반환)` });
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!hasOutParams && nodeData.procedureName) {
|
||||
procedureNodes.push({
|
||||
procedureName: nodeData.procedureName,
|
||||
dbSource: nodeData.dbSource || "internal",
|
||||
connectionId: nodeData.connectionId,
|
||||
schema: nodeData.procedureSchema || "public",
|
||||
});
|
||||
}
|
||||
fields.push({ name: "_procedureReturn", label: "프로시저 반환값" });
|
||||
}
|
||||
// 6️⃣ 통과 노드 (조건, 기타 모든 노드): 상위 노드로 계속 탐색
|
||||
else {
|
||||
const upperResult = getAllSourceFields(node.id, visitedNodes);
|
||||
fields.push(...upperResult.fields);
|
||||
@@ -306,11 +340,33 @@ export function UpsertActionProperties({ nodeId, data }: UpsertActionPropertiesP
|
||||
|
||||
const result = getAllSourceFields(nodeId);
|
||||
|
||||
// 중복 제거
|
||||
const uniqueFields = Array.from(new Map(result.fields.map((field) => [field.name, field])).values());
|
||||
const applyFields = (allFields: typeof result.fields) => {
|
||||
const uniqueFields = Array.from(new Map(allFields.map((field) => [field.name, field])).values());
|
||||
setSourceFields(uniqueFields);
|
||||
setHasRestAPISource(result.hasRestAPI);
|
||||
};
|
||||
|
||||
setSourceFields(uniqueFields);
|
||||
setHasRestAPISource(result.hasRestAPI);
|
||||
if (procedureNodes.length > 0) {
|
||||
applyFields(result.fields);
|
||||
Promise.all(
|
||||
procedureNodes.map(async (pn) => {
|
||||
try {
|
||||
const res = await getFlowProcedureParameters(pn.procedureName, pn.dbSource, pn.connectionId, pn.schema);
|
||||
if (res.success && res.data) {
|
||||
return res.data
|
||||
.filter((p: any) => p.mode === "OUT" || p.mode === "INOUT")
|
||||
.map((p: any) => ({ name: p.name, label: `${p.name} (프로시저 반환)` }));
|
||||
}
|
||||
} catch (e) { console.error("프로시저 파라미터 조회 실패:", e); }
|
||||
return [];
|
||||
})
|
||||
).then((extraFieldArrays) => {
|
||||
const extraFields = extraFieldArrays.flat();
|
||||
if (extraFields.length > 0) applyFields([...result.fields, ...extraFields]);
|
||||
});
|
||||
} else {
|
||||
applyFields(result.fields);
|
||||
}
|
||||
}, [nodeId, nodes, edges]);
|
||||
|
||||
// 🔥 외부 커넥션 로딩 함수
|
||||
|
||||
@@ -132,6 +132,14 @@ export const NODE_PALETTE: NodePaletteItem[] = [
|
||||
category: "external",
|
||||
color: "#06B6D4", // 시안
|
||||
},
|
||||
{
|
||||
type: "procedureCallAction",
|
||||
label: "프로시저 호출",
|
||||
icon: "",
|
||||
description: "DB 프로시저/함수를 호출합니다",
|
||||
category: "external",
|
||||
color: "#8B5CF6", // 보라색
|
||||
},
|
||||
|
||||
// ========================================================================
|
||||
// 유틸리티
|
||||
|
||||
Reference in New Issue
Block a user