249 lines
7.7 KiB
TypeScript
249 lines
7.7 KiB
TypeScript
|
|
"use client";
|
||
|
|
|
||
|
|
import { useState, useEffect } from "react";
|
||
|
|
import { Label } from "@/components/ui/label";
|
||
|
|
import { Input } from "@/components/ui/input";
|
||
|
|
import { Button } from "@/components/ui/button";
|
||
|
|
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
|
||
|
|
import { Textarea } from "@/components/ui/textarea";
|
||
|
|
import { useFlowEditorStore } from "@/lib/stores/flowEditorStore";
|
||
|
|
import { RestAPISourceNodeData } from "@/types/node-editor";
|
||
|
|
import { Globe, Plus, Trash2 } from "lucide-react";
|
||
|
|
|
||
|
|
interface RestAPISourcePropertiesProps {
|
||
|
|
nodeId: string;
|
||
|
|
data: RestAPISourceNodeData;
|
||
|
|
}
|
||
|
|
|
||
|
|
const HTTP_METHODS = ["GET", "POST", "PUT", "DELETE", "PATCH"];
|
||
|
|
const AUTH_TYPES = [
|
||
|
|
{ value: "none", label: "인증 없음" },
|
||
|
|
{ value: "bearer", label: "Bearer Token" },
|
||
|
|
{ value: "basic", label: "Basic Auth" },
|
||
|
|
{ value: "apikey", label: "API Key" },
|
||
|
|
];
|
||
|
|
|
||
|
|
export function RestAPISourceProperties({ nodeId, data }: RestAPISourcePropertiesProps) {
|
||
|
|
const { updateNode } = useFlowEditorStore();
|
||
|
|
|
||
|
|
const [displayName, setDisplayName] = useState(data.displayName || "");
|
||
|
|
const [url, setUrl] = useState(data.url || "");
|
||
|
|
const [method, setMethod] = useState(data.method || "GET");
|
||
|
|
const [headers, setHeaders] = useState(data.headers || {});
|
||
|
|
const [newHeaderKey, setNewHeaderKey] = useState("");
|
||
|
|
const [newHeaderValue, setNewHeaderValue] = useState("");
|
||
|
|
const [body, setBody] = useState(JSON.stringify(data.body || {}, null, 2));
|
||
|
|
const [authType, setAuthType] = useState(data.authentication?.type || "none");
|
||
|
|
const [authToken, setAuthToken] = useState(data.authentication?.token || "");
|
||
|
|
const [timeout, setTimeout] = useState(data.timeout?.toString() || "30000");
|
||
|
|
const [responseMapping, setResponseMapping] = useState(data.responseMapping || "");
|
||
|
|
|
||
|
|
useEffect(() => {
|
||
|
|
setDisplayName(data.displayName || "");
|
||
|
|
setUrl(data.url || "");
|
||
|
|
setMethod(data.method || "GET");
|
||
|
|
setHeaders(data.headers || {});
|
||
|
|
setBody(JSON.stringify(data.body || {}, null, 2));
|
||
|
|
setAuthType(data.authentication?.type || "none");
|
||
|
|
setAuthToken(data.authentication?.token || "");
|
||
|
|
setTimeout(data.timeout?.toString() || "30000");
|
||
|
|
setResponseMapping(data.responseMapping || "");
|
||
|
|
}, [data]);
|
||
|
|
|
||
|
|
const handleApply = () => {
|
||
|
|
let parsedBody = {};
|
||
|
|
try {
|
||
|
|
parsedBody = body.trim() ? JSON.parse(body) : {};
|
||
|
|
} catch (e) {
|
||
|
|
alert("Body JSON 형식이 올바르지 않습니다.");
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
|
||
|
|
updateNode(nodeId, {
|
||
|
|
displayName,
|
||
|
|
url,
|
||
|
|
method: method as any,
|
||
|
|
headers,
|
||
|
|
body: parsedBody,
|
||
|
|
authentication: {
|
||
|
|
type: authType as any,
|
||
|
|
token: authToken || undefined,
|
||
|
|
},
|
||
|
|
timeout: parseInt(timeout) || 30000,
|
||
|
|
responseMapping,
|
||
|
|
});
|
||
|
|
};
|
||
|
|
|
||
|
|
const addHeader = () => {
|
||
|
|
if (newHeaderKey.trim() && newHeaderValue.trim()) {
|
||
|
|
setHeaders({ ...headers, [newHeaderKey.trim()]: newHeaderValue.trim() });
|
||
|
|
setNewHeaderKey("");
|
||
|
|
setNewHeaderValue("");
|
||
|
|
}
|
||
|
|
};
|
||
|
|
|
||
|
|
const removeHeader = (key: string) => {
|
||
|
|
const newHeaders = { ...headers };
|
||
|
|
delete newHeaders[key];
|
||
|
|
setHeaders(newHeaders);
|
||
|
|
};
|
||
|
|
|
||
|
|
return (
|
||
|
|
<div className="space-y-4 p-4">
|
||
|
|
<div className="flex items-center gap-2 rounded-md bg-teal-50 p-2">
|
||
|
|
<Globe className="h-4 w-4 text-teal-600" />
|
||
|
|
<span className="font-semibold text-teal-600">REST API 소스</span>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<div>
|
||
|
|
<Label htmlFor="displayName" className="text-xs">
|
||
|
|
표시 이름
|
||
|
|
</Label>
|
||
|
|
<Input
|
||
|
|
id="displayName"
|
||
|
|
value={displayName}
|
||
|
|
onChange={(e) => setDisplayName(e.target.value)}
|
||
|
|
placeholder="노드에 표시될 이름"
|
||
|
|
className="mt-1 text-sm"
|
||
|
|
/>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<div>
|
||
|
|
<Label htmlFor="url" className="text-xs">
|
||
|
|
API URL
|
||
|
|
</Label>
|
||
|
|
<Input
|
||
|
|
id="url"
|
||
|
|
value={url}
|
||
|
|
onChange={(e) => setUrl(e.target.value)}
|
||
|
|
placeholder="https://api.example.com/data"
|
||
|
|
className="mt-1 text-sm"
|
||
|
|
/>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<div>
|
||
|
|
<Label className="text-xs">HTTP 메서드</Label>
|
||
|
|
<Select value={method} onValueChange={setMethod}>
|
||
|
|
<SelectTrigger className="mt-1">
|
||
|
|
<SelectValue />
|
||
|
|
</SelectTrigger>
|
||
|
|
<SelectContent>
|
||
|
|
{HTTP_METHODS.map((m) => (
|
||
|
|
<SelectItem key={m} value={m}>
|
||
|
|
{m}
|
||
|
|
</SelectItem>
|
||
|
|
))}
|
||
|
|
</SelectContent>
|
||
|
|
</Select>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<div>
|
||
|
|
<Label className="text-xs">헤더</Label>
|
||
|
|
<div className="mt-1 space-y-2">
|
||
|
|
<div className="flex gap-2">
|
||
|
|
<Input
|
||
|
|
value={newHeaderKey}
|
||
|
|
onChange={(e) => setNewHeaderKey(e.target.value)}
|
||
|
|
placeholder="Key"
|
||
|
|
className="text-sm"
|
||
|
|
/>
|
||
|
|
<Input
|
||
|
|
value={newHeaderValue}
|
||
|
|
onChange={(e) => setNewHeaderValue(e.target.value)}
|
||
|
|
onKeyDown={(e) => e.key === "Enter" && addHeader()}
|
||
|
|
placeholder="Value"
|
||
|
|
className="text-sm"
|
||
|
|
/>
|
||
|
|
<Button size="sm" onClick={addHeader}>
|
||
|
|
<Plus className="h-4 w-4" />
|
||
|
|
</Button>
|
||
|
|
</div>
|
||
|
|
<div className="max-h-[100px] space-y-1 overflow-y-auto">
|
||
|
|
{Object.entries(headers).map(([key, value]) => (
|
||
|
|
<div key={key} className="flex items-center justify-between rounded bg-teal-50 px-2 py-1">
|
||
|
|
<span className="text-xs">
|
||
|
|
<span className="font-medium">{key}:</span> {value}
|
||
|
|
</span>
|
||
|
|
<Button variant="ghost" size="sm" onClick={() => removeHeader(key)}>
|
||
|
|
<Trash2 className="h-3 w-3 text-red-500" />
|
||
|
|
</Button>
|
||
|
|
</div>
|
||
|
|
))}
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
{(method === "POST" || method === "PUT" || method === "PATCH") && (
|
||
|
|
<div>
|
||
|
|
<Label htmlFor="body" className="text-xs">
|
||
|
|
Body (JSON)
|
||
|
|
</Label>
|
||
|
|
<Textarea
|
||
|
|
id="body"
|
||
|
|
value={body}
|
||
|
|
onChange={(e) => setBody(e.target.value)}
|
||
|
|
placeholder='{"key": "value"}'
|
||
|
|
className="mt-1 font-mono text-sm"
|
||
|
|
rows={5}
|
||
|
|
/>
|
||
|
|
</div>
|
||
|
|
)}
|
||
|
|
|
||
|
|
<div>
|
||
|
|
<Label className="text-xs">인증</Label>
|
||
|
|
<Select value={authType} onValueChange={setAuthType}>
|
||
|
|
<SelectTrigger className="mt-1">
|
||
|
|
<SelectValue />
|
||
|
|
</SelectTrigger>
|
||
|
|
<SelectContent>
|
||
|
|
{AUTH_TYPES.map((type) => (
|
||
|
|
<SelectItem key={type.value} value={type.value}>
|
||
|
|
{type.label}
|
||
|
|
</SelectItem>
|
||
|
|
))}
|
||
|
|
</SelectContent>
|
||
|
|
</Select>
|
||
|
|
{authType !== "none" && (
|
||
|
|
<Input
|
||
|
|
value={authToken}
|
||
|
|
onChange={(e) => setAuthToken(e.target.value)}
|
||
|
|
placeholder="토큰/키 입력"
|
||
|
|
className="mt-2 text-sm"
|
||
|
|
type="password"
|
||
|
|
/>
|
||
|
|
)}
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<div>
|
||
|
|
<Label htmlFor="timeout" className="text-xs">
|
||
|
|
타임아웃 (ms)
|
||
|
|
</Label>
|
||
|
|
<Input
|
||
|
|
id="timeout"
|
||
|
|
type="number"
|
||
|
|
value={timeout}
|
||
|
|
onChange={(e) => setTimeout(e.target.value)}
|
||
|
|
className="mt-1 text-sm"
|
||
|
|
/>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<div>
|
||
|
|
<Label htmlFor="responseMapping" className="text-xs">
|
||
|
|
응답 매핑 (JSON 경로)
|
||
|
|
</Label>
|
||
|
|
<Input
|
||
|
|
id="responseMapping"
|
||
|
|
value={responseMapping}
|
||
|
|
onChange={(e) => setResponseMapping(e.target.value)}
|
||
|
|
placeholder="예: data.items"
|
||
|
|
className="mt-1 text-sm"
|
||
|
|
/>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<Button onClick={handleApply} className="w-full">
|
||
|
|
적용
|
||
|
|
</Button>
|
||
|
|
</div>
|
||
|
|
);
|
||
|
|
}
|