642 lines
23 KiB
TypeScript
642 lines
23 KiB
TypeScript
|
|
"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>
|
||
|
|
);
|
||
|
|
}
|