ERP-node/frontend/components/dataflow/node-editor/panels/properties/ScriptActionProperties.tsx

576 lines
20 KiB
TypeScript

"use client";
/**
* 스크립트 실행 노드 속성 편집
*/
import { useEffect, useState, useCallback } from "react";
import { Label } from "@/components/ui/label";
import { Input } from "@/components/ui/input";
import { Textarea } from "@/components/ui/textarea";
import { Switch } from "@/components/ui/switch";
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
import { Card, CardContent } from "@/components/ui/card";
import { Button } from "@/components/ui/button";
import { Plus, Trash2, Terminal, FileCode, Settings, Play } from "lucide-react";
import { useFlowEditorStore } from "@/lib/stores/flowEditorStore";
import type { ScriptActionNodeData } from "@/types/node-editor";
interface ScriptActionPropertiesProps {
nodeId: string;
data: ScriptActionNodeData;
}
export function ScriptActionProperties({ nodeId, data }: ScriptActionPropertiesProps) {
const { updateNode } = useFlowEditorStore();
// 로컬 상태
const [displayName, setDisplayName] = useState(data.displayName || "스크립트 실행");
const [scriptType, setScriptType] = useState<ScriptActionNodeData["scriptType"]>(data.scriptType || "python");
const [executionMode, setExecutionMode] = useState<"inline" | "file">(data.executionMode || "inline");
const [inlineScript, setInlineScript] = useState(data.inlineScript || "");
const [scriptPath, setScriptPath] = useState(data.scriptPath || "");
const [executablePath, setExecutablePath] = useState(data.executablePath || "");
const [inputMethod, setInputMethod] = useState<ScriptActionNodeData["inputMethod"]>(data.inputMethod || "stdin");
const [inputFormat, setInputFormat] = useState<"json" | "csv" | "text">(data.inputFormat || "json");
const [workingDirectory, setWorkingDirectory] = useState(data.workingDirectory || "");
const [timeout, setTimeout] = useState(data.options?.timeout?.toString() || "60000");
const [maxBuffer, setMaxBuffer] = useState(data.options?.maxBuffer?.toString() || "1048576");
const [shell, setShell] = useState(data.options?.shell || "");
const [captureStdout, setCaptureStdout] = useState(data.outputHandling?.captureStdout ?? true);
const [captureStderr, setCaptureStderr] = useState(data.outputHandling?.captureStderr ?? true);
const [parseOutput, setParseOutput] = useState<"json" | "lines" | "text">(data.outputHandling?.parseOutput || "text");
// 환경변수
const [envVars, setEnvVars] = useState<Array<{ key: string; value: string }>>(
Object.entries(data.environmentVariables || {}).map(([key, value]) => ({ key, value }))
);
// 명령줄 인자
const [args, setArgs] = useState<string[]>(data.arguments || []);
// 데이터 변경 시 로컬 상태 동기화
useEffect(() => {
setDisplayName(data.displayName || "스크립트 실행");
setScriptType(data.scriptType || "python");
setExecutionMode(data.executionMode || "inline");
setInlineScript(data.inlineScript || "");
setScriptPath(data.scriptPath || "");
setExecutablePath(data.executablePath || "");
setInputMethod(data.inputMethod || "stdin");
setInputFormat(data.inputFormat || "json");
setWorkingDirectory(data.workingDirectory || "");
setTimeout(data.options?.timeout?.toString() || "60000");
setMaxBuffer(data.options?.maxBuffer?.toString() || "1048576");
setShell(data.options?.shell || "");
setCaptureStdout(data.outputHandling?.captureStdout ?? true);
setCaptureStderr(data.outputHandling?.captureStderr ?? true);
setParseOutput(data.outputHandling?.parseOutput || "text");
setEnvVars(Object.entries(data.environmentVariables || {}).map(([key, value]) => ({ key, value })));
setArgs(data.arguments || []);
}, [data]);
// 노드 업데이트 함수
const updateNodeData = useCallback(
(updates: Partial<ScriptActionNodeData>) => {
updateNode(nodeId, {
...data,
...updates,
});
},
[nodeId, data, updateNode]
);
// 표시명 변경
const handleDisplayNameChange = (value: string) => {
setDisplayName(value);
updateNodeData({ displayName: value });
};
// 스크립트 타입 변경
const handleScriptTypeChange = (value: ScriptActionNodeData["scriptType"]) => {
setScriptType(value);
updateNodeData({ scriptType: value });
};
// 실행 모드 변경
const handleExecutionModeChange = (value: "inline" | "file") => {
setExecutionMode(value);
updateNodeData({ executionMode: value });
};
// 스크립트 내용 업데이트
const updateScriptContent = useCallback(() => {
updateNodeData({
inlineScript,
scriptPath,
executablePath,
});
}, [inlineScript, scriptPath, executablePath, updateNodeData]);
// 입력 설정 업데이트
const updateInputSettings = useCallback(() => {
updateNodeData({
inputMethod,
inputFormat,
workingDirectory: workingDirectory || undefined,
});
}, [inputMethod, inputFormat, workingDirectory, updateNodeData]);
// 옵션 업데이트
const updateOptions = useCallback(() => {
updateNodeData({
options: {
timeout: parseInt(timeout) || 60000,
maxBuffer: parseInt(maxBuffer) || 1048576,
shell: shell || undefined,
},
});
}, [timeout, maxBuffer, shell, updateNodeData]);
// 출력 처리 업데이트
const updateOutputHandling = useCallback(() => {
updateNodeData({
outputHandling: {
captureStdout,
captureStderr,
parseOutput,
},
});
}, [captureStdout, captureStderr, parseOutput, updateNodeData]);
// 환경변수 추가
const addEnvVar = () => {
const newEnvVars = [...envVars, { key: "", value: "" }];
setEnvVars(newEnvVars);
};
// 환경변수 삭제
const removeEnvVar = (index: number) => {
const newEnvVars = envVars.filter((_, i) => i !== index);
setEnvVars(newEnvVars);
const envObj = Object.fromEntries(newEnvVars.filter(e => e.key).map(e => [e.key, e.value]));
updateNodeData({ environmentVariables: envObj });
};
// 환경변수 업데이트
const updateEnvVar = (index: number, field: "key" | "value", value: string) => {
const newEnvVars = [...envVars];
newEnvVars[index][field] = value;
setEnvVars(newEnvVars);
};
// 환경변수 저장
const saveEnvVars = () => {
const envObj = Object.fromEntries(envVars.filter(e => e.key).map(e => [e.key, e.value]));
updateNodeData({ environmentVariables: envObj });
};
// 인자 추가
const addArg = () => {
const newArgs = [...args, ""];
setArgs(newArgs);
};
// 인자 삭제
const removeArg = (index: number) => {
const newArgs = args.filter((_, i) => i !== index);
setArgs(newArgs);
updateNodeData({ arguments: newArgs });
};
// 인자 업데이트
const updateArg = (index: number, value: string) => {
const newArgs = [...args];
newArgs[index] = value;
setArgs(newArgs);
};
// 인자 저장
const saveArgs = () => {
updateNodeData({ arguments: args.filter(a => a) });
};
// 스크립트 타입별 기본 스크립트 템플릿
const getScriptTemplate = (type: string) => {
switch (type) {
case "python":
return `import sys
import json
# 입력 데이터 읽기 (stdin)
input_data = json.loads(sys.stdin.read())
# 처리 로직
result = {
"status": "success",
"data": input_data
}
# 결과 출력
print(json.dumps(result))`;
case "shell":
return `#!/bin/bash
# 입력 데이터 읽기
INPUT=$(cat)
# 처리 로직
echo "입력 데이터: $INPUT"
# 결과 출력
echo '{"status": "success"}'`;
case "powershell":
return `# 입력 데이터 읽기
$input = $input | ConvertFrom-Json
# 처리 로직
$result = @{
status = "success"
data = $input
}
# 결과 출력
$result | ConvertTo-Json`;
case "node":
return `const readline = require('readline');
let input = '';
process.stdin.on('data', (chunk) => {
input += chunk;
});
process.stdin.on('end', () => {
const data = JSON.parse(input);
// 처리 로직
const result = {
status: 'success',
data: data
};
console.log(JSON.stringify(result));
});`;
default:
return "";
}
};
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) => handleDisplayNameChange(e.target.value)}
placeholder="스크립트 실행"
className="h-8 text-sm"
/>
</div>
{/* 스크립트 타입 */}
<div className="space-y-2">
<Label className="text-xs font-medium"> </Label>
<Select value={scriptType} onValueChange={handleScriptTypeChange}>
<SelectTrigger className="h-8 text-sm">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="python">Python</SelectItem>
<SelectItem value="shell">Shell (Bash)</SelectItem>
<SelectItem value="powershell">PowerShell</SelectItem>
<SelectItem value="node">Node.js</SelectItem>
<SelectItem value="executable"> </SelectItem>
</SelectContent>
</Select>
</div>
{/* 실행 모드 */}
<div className="space-y-2">
<Label className="text-xs font-medium"> </Label>
<Select value={executionMode} onValueChange={handleExecutionModeChange}>
<SelectTrigger className="h-8 text-sm">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="inline"> </SelectItem>
<SelectItem value="file"> </SelectItem>
</SelectContent>
</Select>
</div>
<Tabs defaultValue="script" className="w-full">
<TabsList className="grid w-full grid-cols-4">
<TabsTrigger value="script" className="text-xs">
<FileCode className="mr-1 h-3 w-3" />
</TabsTrigger>
<TabsTrigger value="input" className="text-xs">
<Play className="mr-1 h-3 w-3" />
</TabsTrigger>
<TabsTrigger value="env" className="text-xs">
<Terminal className="mr-1 h-3 w-3" />
</TabsTrigger>
<TabsTrigger value="output" className="text-xs">
<Settings className="mr-1 h-3 w-3" />
</TabsTrigger>
</TabsList>
{/* 스크립트 탭 */}
<TabsContent value="script" className="space-y-3 pt-3">
{executionMode === "inline" ? (
<div className="space-y-2">
<div className="flex items-center justify-between">
<Label className="text-xs"> </Label>
<Button
variant="outline"
size="sm"
className="h-6 text-xs"
onClick={() => setInlineScript(getScriptTemplate(scriptType))}
>
릿
</Button>
</div>
<Textarea
value={inlineScript}
onChange={(e) => setInlineScript(e.target.value)}
onBlur={updateScriptContent}
placeholder="스크립트 코드를 입력하세요..."
className="min-h-[250px] font-mono text-xs"
/>
</div>
) : (
<div className="space-y-3">
{scriptType === "executable" ? (
<div className="space-y-2">
<Label className="text-xs"> </Label>
<Input
value={executablePath}
onChange={(e) => setExecutablePath(e.target.value)}
onBlur={updateScriptContent}
placeholder="/path/to/executable"
className="h-8 text-sm"
/>
</div>
) : (
<div className="space-y-2">
<Label className="text-xs"> </Label>
<Input
value={scriptPath}
onChange={(e) => setScriptPath(e.target.value)}
onBlur={updateScriptContent}
placeholder={`/path/to/script.${scriptType === "python" ? "py" : scriptType === "shell" ? "sh" : scriptType === "powershell" ? "ps1" : "js"}`}
className="h-8 text-sm"
/>
</div>
)}
</div>
)}
{/* 명령줄 인자 */}
<div className="space-y-2">
<div className="flex items-center justify-between">
<Label className="text-xs"> </Label>
<Button variant="outline" size="sm" className="h-6 text-xs" onClick={addArg}>
<Plus className="mr-1 h-3 w-3" />
</Button>
</div>
{args.map((arg, index) => (
<div key={index} className="flex gap-2">
<Input
value={arg}
onChange={(e) => updateArg(index, e.target.value)}
onBlur={saveArgs}
placeholder={`인자 ${index + 1}`}
className="h-8 text-sm"
/>
<Button variant="ghost" size="sm" className="h-8 w-8 p-0" onClick={() => removeArg(index)}>
<Trash2 className="h-3 w-3 text-red-500" />
</Button>
</div>
))}
</div>
</TabsContent>
{/* 입력 탭 */}
<TabsContent value="input" className="space-y-3 pt-3">
<div className="space-y-2">
<Label className="text-xs"> </Label>
<Select value={inputMethod} onValueChange={(v: ScriptActionNodeData["inputMethod"]) => {
setInputMethod(v);
updateNodeData({ inputMethod: v });
}}>
<SelectTrigger className="h-8 text-sm">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="stdin"> (stdin)</SelectItem>
<SelectItem value="args"> </SelectItem>
<SelectItem value="env"></SelectItem>
<SelectItem value="file"> </SelectItem>
</SelectContent>
</Select>
</div>
{(inputMethod === "stdin" || inputMethod === "file") && (
<div className="space-y-2">
<Label className="text-xs"> </Label>
<Select value={inputFormat} onValueChange={(v: "json" | "csv" | "text") => {
setInputFormat(v);
updateNodeData({ inputFormat: v });
}}>
<SelectTrigger className="h-8 text-sm">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="json">JSON</SelectItem>
<SelectItem value="csv">CSV</SelectItem>
<SelectItem value="text"></SelectItem>
</SelectContent>
</Select>
</div>
)}
<div className="space-y-2">
<Label className="text-xs"> </Label>
<Input
value={workingDirectory}
onChange={(e) => setWorkingDirectory(e.target.value)}
onBlur={updateInputSettings}
placeholder="/path/to/working/directory"
className="h-8 text-sm"
/>
</div>
<div className="space-y-2">
<Label className="text-xs"> (ms)</Label>
<Input
type="number"
value={timeout}
onChange={(e) => setTimeout(e.target.value)}
onBlur={updateOptions}
placeholder="60000"
className="h-8 text-sm"
/>
</div>
</TabsContent>
{/* 환경변수 탭 */}
<TabsContent value="env" className="space-y-3 pt-3">
<div className="flex items-center justify-between">
<Label className="text-xs"></Label>
<Button variant="outline" size="sm" className="h-6 text-xs" onClick={addEnvVar}>
<Plus className="mr-1 h-3 w-3" />
</Button>
</div>
{envVars.map((env, index) => (
<div key={index} className="flex gap-2">
<Input
value={env.key}
onChange={(e) => updateEnvVar(index, "key", e.target.value)}
onBlur={saveEnvVars}
placeholder="변수명"
className="h-8 text-sm"
/>
<Input
value={env.value}
onChange={(e) => updateEnvVar(index, "value", e.target.value)}
onBlur={saveEnvVars}
placeholder="값"
className="h-8 text-sm"
/>
<Button variant="ghost" size="sm" className="h-8 w-8 p-0" onClick={() => removeEnvVar(index)}>
<Trash2 className="h-3 w-3 text-red-500" />
</Button>
</div>
))}
{envVars.length === 0 && (
<Card className="bg-gray-50">
<CardContent className="p-3 text-xs text-gray-500">
. .
</CardContent>
</Card>
)}
<div className="space-y-2">
<Label className="text-xs"> </Label>
<Input
value={shell}
onChange={(e) => setShell(e.target.value)}
onBlur={updateOptions}
placeholder="/bin/bash (기본값 사용 시 비워두기)"
className="h-8 text-sm"
/>
</div>
</TabsContent>
{/* 출력 탭 */}
<TabsContent value="output" className="space-y-3 pt-3">
<div className="flex items-center space-x-2">
<Switch
checked={captureStdout}
onCheckedChange={(checked) => {
setCaptureStdout(checked);
updateNodeData({
outputHandling: { ...data.outputHandling, captureStdout: checked, captureStderr, parseOutput },
});
}}
/>
<Label className="text-xs"> (stdout) </Label>
</div>
<div className="flex items-center space-x-2">
<Switch
checked={captureStderr}
onCheckedChange={(checked) => {
setCaptureStderr(checked);
updateNodeData({
outputHandling: { ...data.outputHandling, captureStdout, captureStderr: checked, parseOutput },
});
}}
/>
<Label className="text-xs"> (stderr) </Label>
</div>
<div className="space-y-2">
<Label className="text-xs"> </Label>
<Select value={parseOutput} onValueChange={(v: "json" | "lines" | "text") => {
setParseOutput(v);
updateNodeData({
outputHandling: { ...data.outputHandling, captureStdout, captureStderr, parseOutput: v },
});
}}>
<SelectTrigger className="h-8 text-sm">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="json">JSON </SelectItem>
<SelectItem value="lines"> </SelectItem>
<SelectItem value="text"> </SelectItem>
</SelectContent>
</Select>
</div>
<Card className="bg-gray-50">
<CardContent className="p-3 text-xs text-gray-600">
<div className="font-medium mb-1"> :</div>
<code>{"{{scriptOutput}}"}</code> .
</CardContent>
</Card>
</TabsContent>
</Tabs>
</div>
);
}