외부호출 노드들

This commit is contained in:
kjs
2025-12-09 12:13:30 +09:00
parent cf73ce6ebb
commit bb98e9319f
12 changed files with 2671 additions and 5 deletions

View File

@@ -0,0 +1,103 @@
"use client";
/**
* 메일 발송 액션 노드
* SMTP를 통해 이메일을 발송하는 노드
*/
import { memo } from "react";
import { Handle, Position, NodeProps } from "reactflow";
import { Mail, Server } from "lucide-react";
import type { EmailActionNodeData } from "@/types/node-editor";
export const EmailActionNode = memo(({ data, selected }: NodeProps<EmailActionNodeData>) => {
const hasSmtpConfig = data.smtpConfig?.host && data.smtpConfig?.port;
const hasRecipient = data.to && data.to.trim().length > 0;
const hasSubject = data.subject && data.subject.trim().length > 0;
return (
<div
className={`min-w-[250px] rounded-lg border-2 bg-white shadow-md transition-all ${
selected ? "border-pink-500 shadow-lg" : "border-gray-200"
}`}
>
{/* 입력 핸들 */}
<Handle
type="target"
position={Position.Left}
className="!h-3 !w-3 !border-2 !border-white !bg-pink-500"
/>
{/* 헤더 */}
<div className="flex items-center gap-2 rounded-t-lg bg-pink-500 px-3 py-2 text-white">
<Mail 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">
{/* SMTP 설정 상태 */}
<div className="flex items-center gap-2 text-xs">
<Server className="h-3 w-3 text-gray-400" />
<span className="text-gray-600">
{hasSmtpConfig ? (
<span className="text-green-600">
{data.smtpConfig.host}:{data.smtpConfig.port}
</span>
) : (
<span className="text-orange-500">SMTP </span>
)}
</span>
</div>
{/* 수신자 */}
<div className="text-xs">
<span className="text-gray-500">: </span>
{hasRecipient ? (
<span className="text-gray-700">{data.to}</span>
) : (
<span className="text-orange-500"></span>
)}
</div>
{/* 제목 */}
<div className="text-xs">
<span className="text-gray-500">: </span>
{hasSubject ? (
<span className="truncate text-gray-700">{data.subject}</span>
) : (
<span className="text-orange-500"></span>
)}
</div>
{/* 본문 형식 */}
<div className="flex items-center gap-2">
<span
className={`rounded px-1.5 py-0.5 text-xs ${
data.bodyType === "html" ? "bg-blue-100 text-blue-700" : "bg-gray-100 text-gray-700"
}`}
>
{data.bodyType === "html" ? "HTML" : "TEXT"}
</span>
{data.attachments && data.attachments.length > 0 && (
<span className="rounded bg-purple-100 px-1.5 py-0.5 text-xs text-purple-700">
{data.attachments.length}
</span>
)}
</div>
</div>
{/* 출력 핸들 */}
<Handle
type="source"
position={Position.Right}
className="!h-3 !w-3 !border-2 !border-white !bg-pink-500"
/>
</div>
);
});
EmailActionNode.displayName = "EmailActionNode";

View File

@@ -0,0 +1,124 @@
"use client";
/**
* HTTP 요청 액션 노드
* REST API를 호출하는 노드
*/
import { memo } from "react";
import { Handle, Position, NodeProps } from "reactflow";
import { Globe, Lock, Unlock } from "lucide-react";
import type { HttpRequestActionNodeData } from "@/types/node-editor";
// HTTP 메서드별 색상
const METHOD_COLORS: Record<string, { bg: string; text: string }> = {
GET: { bg: "bg-green-100", text: "text-green-700" },
POST: { bg: "bg-blue-100", text: "text-blue-700" },
PUT: { bg: "bg-orange-100", text: "text-orange-700" },
PATCH: { bg: "bg-yellow-100", text: "text-yellow-700" },
DELETE: { bg: "bg-red-100", text: "text-red-700" },
HEAD: { bg: "bg-gray-100", text: "text-gray-700" },
OPTIONS: { bg: "bg-purple-100", text: "text-purple-700" },
};
export const HttpRequestActionNode = memo(({ data, selected }: NodeProps<HttpRequestActionNodeData>) => {
const methodColor = METHOD_COLORS[data.method] || METHOD_COLORS.GET;
const hasUrl = data.url && data.url.trim().length > 0;
const hasAuth = data.authentication?.type && data.authentication.type !== "none";
// URL에서 도메인 추출
const getDomain = (url: string) => {
try {
const urlObj = new URL(url);
return urlObj.hostname;
} catch {
return url;
}
};
return (
<div
className={`min-w-[250px] rounded-lg border-2 bg-white shadow-md transition-all ${
selected ? "border-cyan-500 shadow-lg" : "border-gray-200"
}`}
>
{/* 입력 핸들 */}
<Handle
type="target"
position={Position.Left}
className="!h-3 !w-3 !border-2 !border-white !bg-cyan-500"
/>
{/* 헤더 */}
<div className="flex items-center gap-2 rounded-t-lg bg-cyan-500 px-3 py-2 text-white">
<Globe className="h-4 w-4" />
<div className="flex-1">
<div className="text-sm font-semibold">{data.displayName || "HTTP 요청"}</div>
</div>
</div>
{/* 본문 */}
<div className="space-y-2 p-3">
{/* 메서드 & 인증 */}
<div className="flex items-center gap-2">
<span className={`rounded px-2 py-0.5 text-xs font-bold ${methodColor.bg} ${methodColor.text}`}>
{data.method}
</span>
{hasAuth ? (
<span className="flex items-center gap-1 rounded bg-green-100 px-1.5 py-0.5 text-xs text-green-700">
<Lock className="h-3 w-3" />
{data.authentication?.type}
</span>
) : (
<span className="flex items-center gap-1 rounded bg-gray-100 px-1.5 py-0.5 text-xs text-gray-500">
<Unlock className="h-3 w-3" />
</span>
)}
</div>
{/* URL */}
<div className="text-xs">
<span className="text-gray-500">URL: </span>
{hasUrl ? (
<span className="truncate text-gray-700" title={data.url}>
{getDomain(data.url)}
</span>
) : (
<span className="text-orange-500">URL </span>
)}
</div>
{/* 바디 타입 */}
{data.bodyType && data.bodyType !== "none" && (
<div className="text-xs">
<span className="text-gray-500">Body: </span>
<span className="rounded bg-gray-100 px-1.5 py-0.5 text-gray-600">
{data.bodyType.toUpperCase()}
</span>
</div>
)}
{/* 타임아웃 & 재시도 */}
<div className="flex gap-2 text-xs text-gray-500">
{data.options?.timeout && (
<span>: {Math.round(data.options.timeout / 1000)}</span>
)}
{data.options?.retryCount && data.options.retryCount > 0 && (
<span>: {data.options.retryCount}</span>
)}
</div>
</div>
{/* 출력 핸들 */}
<Handle
type="source"
position={Position.Right}
className="!h-3 !w-3 !border-2 !border-white !bg-cyan-500"
/>
</div>
);
});
HttpRequestActionNode.displayName = "HttpRequestActionNode";

View File

@@ -0,0 +1,118 @@
"use client";
/**
* 스크립트 실행 액션 노드
* Python, Shell, PowerShell 등 외부 스크립트를 실행하는 노드
*/
import { memo } from "react";
import { Handle, Position, NodeProps } from "reactflow";
import { Terminal, FileCode, Play } from "lucide-react";
import type { ScriptActionNodeData } from "@/types/node-editor";
// 스크립트 타입별 아이콘 색상
const SCRIPT_TYPE_COLORS: Record<string, { bg: string; text: string; label: string }> = {
python: { bg: "bg-yellow-100", text: "text-yellow-700", label: "Python" },
shell: { bg: "bg-green-100", text: "text-green-700", label: "Shell" },
powershell: { bg: "bg-blue-100", text: "text-blue-700", label: "PowerShell" },
node: { bg: "bg-emerald-100", text: "text-emerald-700", label: "Node.js" },
executable: { bg: "bg-gray-100", text: "text-gray-700", label: "실행파일" },
};
export const ScriptActionNode = memo(({ data, selected }: NodeProps<ScriptActionNodeData>) => {
const scriptTypeInfo = SCRIPT_TYPE_COLORS[data.scriptType] || SCRIPT_TYPE_COLORS.executable;
const hasScript = data.executionMode === "inline" ? !!data.inlineScript : !!data.scriptPath;
return (
<div
className={`min-w-[250px] rounded-lg border-2 bg-white shadow-md transition-all ${
selected ? "border-emerald-500 shadow-lg" : "border-gray-200"
}`}
>
{/* 입력 핸들 */}
<Handle
type="target"
position={Position.Left}
className="!h-3 !w-3 !border-2 !border-white !bg-emerald-500"
/>
{/* 헤더 */}
<div className="flex items-center gap-2 rounded-t-lg bg-emerald-500 px-3 py-2 text-white">
<Terminal 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">
{/* 스크립트 타입 */}
<div className="flex items-center gap-2">
<span className={`rounded px-2 py-0.5 text-xs font-medium ${scriptTypeInfo.bg} ${scriptTypeInfo.text}`}>
{scriptTypeInfo.label}
</span>
<span className="rounded bg-gray-100 px-2 py-0.5 text-xs text-gray-600">
{data.executionMode === "inline" ? "인라인" : "파일"}
</span>
</div>
{/* 스크립트 정보 */}
<div className="flex items-center gap-2 text-xs">
{data.executionMode === "inline" ? (
<>
<FileCode className="h-3 w-3 text-gray-400" />
<span className="text-gray-600">
{hasScript ? (
<span className="text-green-600">
{data.inlineScript!.split("\n").length}
</span>
) : (
<span className="text-orange-500"> </span>
)}
</span>
</>
) : (
<>
<Play className="h-3 w-3 text-gray-400" />
<span className="text-gray-600">
{hasScript ? (
<span className="truncate text-green-600">{data.scriptPath}</span>
) : (
<span className="text-orange-500"> </span>
)}
</span>
</>
)}
</div>
{/* 입력 방식 */}
<div className="text-xs">
<span className="text-gray-500">: </span>
<span className="text-gray-700">
{data.inputMethod === "stdin" && "표준입력 (stdin)"}
{data.inputMethod === "args" && "명령줄 인자"}
{data.inputMethod === "env" && "환경변수"}
{data.inputMethod === "file" && "파일"}
</span>
</div>
{/* 타임아웃 */}
{data.options?.timeout && (
<div className="text-xs text-gray-500">
: {Math.round(data.options.timeout / 1000)}
</div>
)}
</div>
{/* 출력 핸들 */}
<Handle
type="source"
position={Position.Right}
className="!h-3 !w-3 !border-2 !border-white !bg-emerald-500"
/>
</div>
);
});
ScriptActionNode.displayName = "ScriptActionNode";