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

342 lines
12 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"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 || "");
const [responseFields, setResponseFields] = useState(data.responseFields || []);
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 || "");
setResponseFields(data.responseFields || []);
}, [data]);
const handleApply = () => {
let parsedBody = {};
try {
parsedBody = body.trim() ? JSON.parse(body) : {};
} catch (e) {
alert("Body JSON 형식이 올바르지 않습니다.");
return;
}
console.log("🔧 REST API 노드 업데이트 중...");
console.log("📦 responseFields:", responseFields);
console.log("📊 responseFields 개수:", responseFields.length);
updateNode(nodeId, {
displayName,
url,
method: method as any,
headers,
body: parsedBody,
authentication: {
type: authType as any,
token: authToken || undefined,
},
timeout: parseInt(timeout) || 30000,
responseMapping,
responseFields,
});
console.log("✅ REST API 노드 업데이트 완료");
};
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);
};
const addResponseField = () => {
const newFields = [
...responseFields,
{
name: "",
label: "",
dataType: "TEXT",
},
];
console.log(" 응답 필드 추가:", newFields);
setResponseFields(newFields);
};
const updateResponseField = (index: number, field: string, value: string) => {
const updated = [...responseFields];
updated[index] = { ...updated[index], [field]: value };
console.log(`✏️ 응답 필드 ${index} 업데이트 (${field}=${value}):`, updated);
setResponseFields(updated);
};
const removeResponseField = (index: number) => {
const newFields = responseFields.filter((_, i) => i !== index);
console.log("🗑️ 응답 필드 삭제:", newFields);
setResponseFields(newFields);
};
return (
<div className="space-y-4 p-4 pb-8">
<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"
/>
<p className="mt-1 text-xs text-gray-500"> (: data.items, result.users)</p>
</div>
<div>
<div className="mb-2 flex items-center justify-between">
<Label className="text-xs"> </Label>
<Button size="sm" variant="outline" onClick={addResponseField}>
<Plus className="mr-1 h-3 w-3" />
</Button>
</div>
<div className="max-h-[300px] space-y-2 overflow-y-auto">
{responseFields.length === 0 ? (
<div className="rounded border border-dashed p-3 text-center text-xs text-gray-400">
</div>
) : (
responseFields.map((field, index) => (
<div key={index} className="rounded border bg-gray-50 p-2">
<div className="mb-2 flex items-center justify-between">
<span className="text-xs font-medium text-gray-700"> {index + 1}</span>
<Button variant="ghost" size="sm" onClick={() => removeResponseField(index)}>
<Trash2 className="h-3 w-3 text-red-500" />
</Button>
</div>
<div className="space-y-2">
<Input
value={field.name}
onChange={(e) => updateResponseField(index, "name", e.target.value)}
placeholder="필드명 (예: userId)"
className="text-xs"
/>
<Input
value={field.label || ""}
onChange={(e) => updateResponseField(index, "label", e.target.value)}
placeholder="표시명 (예: 사용자 ID)"
className="text-xs"
/>
<Select
value={field.dataType || "TEXT"}
onValueChange={(value) => updateResponseField(index, "dataType", value)}
>
<SelectTrigger className="text-xs">
<SelectValue placeholder="데이터 타입" />
</SelectTrigger>
<SelectContent>
<SelectItem value="TEXT"></SelectItem>
<SelectItem value="NUMBER"></SelectItem>
<SelectItem value="DATE"></SelectItem>
<SelectItem value="BOOLEAN">/</SelectItem>
<SelectItem value="JSON">JSON</SelectItem>
</SelectContent>
</Select>
</div>
</div>
))
)}
</div>
</div>
<Button onClick={handleApply} className="w-full">
</Button>
</div>
);
}