ERP-node/frontend/components/dataflow/node-editor/panels/properties/HttpRequestActionProperties...

569 lines
21 KiB
TypeScript

"use client";
/**
* HTTP 요청 노드 속성 편집
*/
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, Globe, Key, FileJson, Settings } from "lucide-react";
import { useFlowEditorStore } from "@/lib/stores/flowEditorStore";
import type { HttpRequestActionNodeData } from "@/types/node-editor";
interface HttpRequestActionPropertiesProps {
nodeId: string;
data: HttpRequestActionNodeData;
}
export function HttpRequestActionProperties({ nodeId, data }: HttpRequestActionPropertiesProps) {
const { updateNode } = useFlowEditorStore();
// 로컬 상태
const [displayName, setDisplayName] = useState(data.displayName || "HTTP 요청");
const [url, setUrl] = useState(data.url || "");
const [method, setMethod] = useState<HttpRequestActionNodeData["method"]>(data.method || "GET");
const [bodyType, setBodyType] = useState<HttpRequestActionNodeData["bodyType"]>(data.bodyType || "none");
const [body, setBody] = useState(data.body || "");
// 인증
const [authType, setAuthType] = useState<NonNullable<HttpRequestActionNodeData["authentication"]>["type"]>(
data.authentication?.type || "none"
);
const [authUsername, setAuthUsername] = useState(data.authentication?.username || "");
const [authPassword, setAuthPassword] = useState(data.authentication?.password || "");
const [authToken, setAuthToken] = useState(data.authentication?.token || "");
const [authApiKey, setAuthApiKey] = useState(data.authentication?.apiKey || "");
const [authApiKeyName, setAuthApiKeyName] = useState(data.authentication?.apiKeyName || "X-API-Key");
const [authApiKeyLocation, setAuthApiKeyLocation] = useState<"header" | "query">(
data.authentication?.apiKeyLocation || "header"
);
// 옵션
const [timeout, setTimeout] = useState(data.options?.timeout?.toString() || "30000");
const [followRedirects, setFollowRedirects] = useState(data.options?.followRedirects ?? true);
const [retryCount, setRetryCount] = useState(data.options?.retryCount?.toString() || "0");
const [retryDelay, setRetryDelay] = useState(data.options?.retryDelay?.toString() || "1000");
// 헤더
const [headers, setHeaders] = useState<Array<{ key: string; value: string }>>(
Object.entries(data.headers || {}).map(([key, value]) => ({ key, value }))
);
// 쿼리 파라미터
const [queryParams, setQueryParams] = useState<Array<{ key: string; value: string }>>(
Object.entries(data.queryParams || {}).map(([key, value]) => ({ key, value }))
);
// 응답 처리
const [extractPath, setExtractPath] = useState(data.responseHandling?.extractPath || "");
const [saveToVariable, setSaveToVariable] = useState(data.responseHandling?.saveToVariable || "");
// 데이터 변경 시 로컬 상태 동기화
useEffect(() => {
setDisplayName(data.displayName || "HTTP 요청");
setUrl(data.url || "");
setMethod(data.method || "GET");
setBodyType(data.bodyType || "none");
setBody(data.body || "");
setAuthType(data.authentication?.type || "none");
setAuthUsername(data.authentication?.username || "");
setAuthPassword(data.authentication?.password || "");
setAuthToken(data.authentication?.token || "");
setAuthApiKey(data.authentication?.apiKey || "");
setAuthApiKeyName(data.authentication?.apiKeyName || "X-API-Key");
setAuthApiKeyLocation(data.authentication?.apiKeyLocation || "header");
setTimeout(data.options?.timeout?.toString() || "30000");
setFollowRedirects(data.options?.followRedirects ?? true);
setRetryCount(data.options?.retryCount?.toString() || "0");
setRetryDelay(data.options?.retryDelay?.toString() || "1000");
setHeaders(Object.entries(data.headers || {}).map(([key, value]) => ({ key, value })));
setQueryParams(Object.entries(data.queryParams || {}).map(([key, value]) => ({ key, value })));
setExtractPath(data.responseHandling?.extractPath || "");
setSaveToVariable(data.responseHandling?.saveToVariable || "");
}, [data]);
// 노드 업데이트 함수
const updateNodeData = useCallback(
(updates: Partial<HttpRequestActionNodeData>) => {
updateNode(nodeId, {
...data,
...updates,
});
},
[nodeId, data, updateNode]
);
// 표시명 변경
const handleDisplayNameChange = (value: string) => {
setDisplayName(value);
updateNodeData({ displayName: value });
};
// URL 업데이트
const updateUrl = () => {
updateNodeData({ url });
};
// 메서드 변경
const handleMethodChange = (value: HttpRequestActionNodeData["method"]) => {
setMethod(value);
updateNodeData({ method: value });
};
// 바디 타입 변경
const handleBodyTypeChange = (value: HttpRequestActionNodeData["bodyType"]) => {
setBodyType(value);
updateNodeData({ bodyType: value });
};
// 바디 업데이트
const updateBody = () => {
updateNodeData({ body });
};
// 인증 업데이트
const updateAuthentication = useCallback(() => {
updateNodeData({
authentication: {
type: authType,
username: authUsername || undefined,
password: authPassword || undefined,
token: authToken || undefined,
apiKey: authApiKey || undefined,
apiKeyName: authApiKeyName || undefined,
apiKeyLocation: authApiKeyLocation,
},
});
}, [authType, authUsername, authPassword, authToken, authApiKey, authApiKeyName, authApiKeyLocation, updateNodeData]);
// 옵션 업데이트
const updateOptions = useCallback(() => {
updateNodeData({
options: {
timeout: parseInt(timeout) || 30000,
followRedirects,
retryCount: parseInt(retryCount) || 0,
retryDelay: parseInt(retryDelay) || 1000,
},
});
}, [timeout, followRedirects, retryCount, retryDelay, updateNodeData]);
// 응답 처리 업데이트
const updateResponseHandling = useCallback(() => {
updateNodeData({
responseHandling: {
extractPath: extractPath || undefined,
saveToVariable: saveToVariable || undefined,
},
});
}, [extractPath, saveToVariable, updateNodeData]);
// 헤더 추가
const addHeader = () => {
setHeaders([...headers, { key: "", value: "" }]);
};
// 헤더 삭제
const removeHeader = (index: number) => {
const newHeaders = headers.filter((_, i) => i !== index);
setHeaders(newHeaders);
const headersObj = Object.fromEntries(newHeaders.filter(h => h.key).map(h => [h.key, h.value]));
updateNodeData({ headers: headersObj });
};
// 헤더 업데이트
const updateHeader = (index: number, field: "key" | "value", value: string) => {
const newHeaders = [...headers];
newHeaders[index][field] = value;
setHeaders(newHeaders);
};
// 헤더 저장
const saveHeaders = () => {
const headersObj = Object.fromEntries(headers.filter(h => h.key).map(h => [h.key, h.value]));
updateNodeData({ headers: headersObj });
};
// 쿼리 파라미터 추가
const addQueryParam = () => {
setQueryParams([...queryParams, { key: "", value: "" }]);
};
// 쿼리 파라미터 삭제
const removeQueryParam = (index: number) => {
const newParams = queryParams.filter((_, i) => i !== index);
setQueryParams(newParams);
const paramsObj = Object.fromEntries(newParams.filter(p => p.key).map(p => [p.key, p.value]));
updateNodeData({ queryParams: paramsObj });
};
// 쿼리 파라미터 업데이트
const updateQueryParam = (index: number, field: "key" | "value", value: string) => {
const newParams = [...queryParams];
newParams[index][field] = value;
setQueryParams(newParams);
};
// 쿼리 파라미터 저장
const saveQueryParams = () => {
const paramsObj = Object.fromEntries(queryParams.filter(p => p.key).map(p => [p.key, p.value]));
updateNodeData({ queryParams: paramsObj });
};
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="HTTP 요청"
className="h-8 text-sm"
/>
</div>
{/* URL */}
<div className="space-y-2">
<Label className="text-xs font-medium">URL *</Label>
<Input
value={url}
onChange={(e) => setUrl(e.target.value)}
onBlur={updateUrl}
placeholder="https://api.example.com/endpoint"
className="h-8 text-sm"
/>
</div>
{/* 메서드 */}
<div className="space-y-2">
<Label className="text-xs font-medium">HTTP </Label>
<Select value={method} onValueChange={handleMethodChange}>
<SelectTrigger className="h-8 text-sm">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="GET">GET</SelectItem>
<SelectItem value="POST">POST</SelectItem>
<SelectItem value="PUT">PUT</SelectItem>
<SelectItem value="PATCH">PATCH</SelectItem>
<SelectItem value="DELETE">DELETE</SelectItem>
<SelectItem value="HEAD">HEAD</SelectItem>
<SelectItem value="OPTIONS">OPTIONS</SelectItem>
</SelectContent>
</Select>
</div>
<Tabs defaultValue="headers" className="w-full">
<TabsList className="grid w-full grid-cols-4">
<TabsTrigger value="headers" className="text-xs">
<Globe className="mr-1 h-3 w-3" />
</TabsTrigger>
<TabsTrigger value="body" className="text-xs">
<FileJson className="mr-1 h-3 w-3" />
</TabsTrigger>
<TabsTrigger value="auth" className="text-xs">
<Key className="mr-1 h-3 w-3" />
</TabsTrigger>
<TabsTrigger value="options" className="text-xs">
<Settings className="mr-1 h-3 w-3" />
</TabsTrigger>
</TabsList>
{/* 헤더 탭 */}
<TabsContent value="headers" className="space-y-3 pt-3">
{/* 쿼리 파라미터 */}
<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={addQueryParam}>
<Plus className="mr-1 h-3 w-3" />
</Button>
</div>
{queryParams.map((param, index) => (
<div key={index} className="flex gap-2">
<Input
value={param.key}
onChange={(e) => updateQueryParam(index, "key", e.target.value)}
onBlur={saveQueryParams}
placeholder="파라미터명"
className="h-8 text-sm"
/>
<Input
value={param.value}
onChange={(e) => updateQueryParam(index, "value", e.target.value)}
onBlur={saveQueryParams}
placeholder="값"
className="h-8 text-sm"
/>
<Button variant="ghost" size="sm" className="h-8 w-8 p-0" onClick={() => removeQueryParam(index)}>
<Trash2 className="h-3 w-3 text-red-500" />
</Button>
</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={addHeader}>
<Plus className="mr-1 h-3 w-3" />
</Button>
</div>
{headers.map((header, index) => (
<div key={index} className="flex gap-2">
<Input
value={header.key}
onChange={(e) => updateHeader(index, "key", e.target.value)}
onBlur={saveHeaders}
placeholder="헤더명"
className="h-8 text-sm"
/>
<Input
value={header.value}
onChange={(e) => updateHeader(index, "value", e.target.value)}
onBlur={saveHeaders}
placeholder="값"
className="h-8 text-sm"
/>
<Button variant="ghost" size="sm" className="h-8 w-8 p-0" onClick={() => removeHeader(index)}>
<Trash2 className="h-3 w-3 text-red-500" />
</Button>
</div>
))}
</div>
</TabsContent>
{/* 바디 탭 */}
<TabsContent value="body" className="space-y-3 pt-3">
<div className="space-y-2">
<Label className="text-xs"> </Label>
<Select value={bodyType} onValueChange={handleBodyTypeChange}>
<SelectTrigger className="h-8 text-sm">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="none"></SelectItem>
<SelectItem value="json">JSON</SelectItem>
<SelectItem value="form">Form Data</SelectItem>
<SelectItem value="text"></SelectItem>
</SelectContent>
</Select>
</div>
{bodyType !== "none" && (
<div className="space-y-2">
<Label className="text-xs"> </Label>
<Textarea
value={body}
onChange={(e) => setBody(e.target.value)}
onBlur={updateBody}
placeholder={bodyType === "json" ? '{\n "key": "value"\n}' : "바디 내용"}
className="min-h-[200px] font-mono text-xs"
/>
</div>
)}
<Card className="bg-gray-50">
<CardContent className="p-3 text-xs text-gray-600">
<div className="font-medium mb-1">릿 :</div>
<code className="block">{"{{sourceData}}"}</code>
<code className="block">{"{{필드명}}"}</code>
</CardContent>
</Card>
</TabsContent>
{/* 인증 탭 */}
<TabsContent value="auth" className="space-y-3 pt-3">
<div className="space-y-2">
<Label className="text-xs"> </Label>
<Select value={authType} onValueChange={(v: any) => {
setAuthType(v);
updateNodeData({ authentication: { ...data.authentication, type: v } });
}}>
<SelectTrigger className="h-8 text-sm">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="none"> </SelectItem>
<SelectItem value="basic">Basic Auth</SelectItem>
<SelectItem value="bearer">Bearer Token</SelectItem>
<SelectItem value="apikey">API Key</SelectItem>
</SelectContent>
</Select>
</div>
{authType === "basic" && (
<>
<div className="space-y-2">
<Label className="text-xs"></Label>
<Input
value={authUsername}
onChange={(e) => setAuthUsername(e.target.value)}
onBlur={updateAuthentication}
placeholder="username"
className="h-8 text-sm"
/>
</div>
<div className="space-y-2">
<Label className="text-xs"></Label>
<Input
type="password"
value={authPassword}
onChange={(e) => setAuthPassword(e.target.value)}
onBlur={updateAuthentication}
placeholder="password"
className="h-8 text-sm"
/>
</div>
</>
)}
{authType === "bearer" && (
<div className="space-y-2">
<Label className="text-xs">Bearer Token</Label>
<Input
value={authToken}
onChange={(e) => setAuthToken(e.target.value)}
onBlur={updateAuthentication}
placeholder="your-token-here"
className="h-8 text-sm"
/>
</div>
)}
{authType === "apikey" && (
<>
<div className="space-y-2">
<Label className="text-xs">API Key</Label>
<Input
value={authApiKey}
onChange={(e) => setAuthApiKey(e.target.value)}
onBlur={updateAuthentication}
placeholder="your-api-key"
className="h-8 text-sm"
/>
</div>
<div className="space-y-2">
<Label className="text-xs">/ </Label>
<Input
value={authApiKeyName}
onChange={(e) => setAuthApiKeyName(e.target.value)}
onBlur={updateAuthentication}
placeholder="X-API-Key"
className="h-8 text-sm"
/>
</div>
<div className="space-y-2">
<Label className="text-xs"></Label>
<Select value={authApiKeyLocation} onValueChange={(v: "header" | "query") => {
setAuthApiKeyLocation(v);
updateNodeData({
authentication: { ...data.authentication, type: authType, apiKeyLocation: v },
});
}}>
<SelectTrigger className="h-8 text-sm">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="header"></SelectItem>
<SelectItem value="query"> </SelectItem>
</SelectContent>
</Select>
</div>
</>
)}
</TabsContent>
{/* 옵션 탭 */}
<TabsContent value="options" className="space-y-3 pt-3">
<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="30000"
className="h-8 text-sm"
/>
</div>
<div className="flex items-center space-x-2">
<Switch
checked={followRedirects}
onCheckedChange={(checked) => {
setFollowRedirects(checked);
updateNodeData({ options: { ...data.options, followRedirects: checked } });
}}
/>
<Label className="text-xs"> </Label>
</div>
<div className="grid grid-cols-2 gap-2">
<div className="space-y-2">
<Label className="text-xs"> </Label>
<Input
type="number"
value={retryCount}
onChange={(e) => setRetryCount(e.target.value)}
onBlur={updateOptions}
placeholder="0"
className="h-8 text-sm"
/>
</div>
<div className="space-y-2">
<Label className="text-xs"> (ms)</Label>
<Input
type="number"
value={retryDelay}
onChange={(e) => setRetryDelay(e.target.value)}
onBlur={updateOptions}
placeholder="1000"
className="h-8 text-sm"
/>
</div>
</div>
<div className="space-y-2">
<Label className="text-xs"> (JSONPath)</Label>
<Input
value={extractPath}
onChange={(e) => setExtractPath(e.target.value)}
onBlur={updateResponseHandling}
placeholder="data.items"
className="h-8 text-sm"
/>
</div>
<div className="space-y-2">
<Label className="text-xs"> </Label>
<Input
value={saveToVariable}
onChange={(e) => setSaveToVariable(e.target.value)}
onBlur={updateResponseHandling}
placeholder="apiResponse"
className="h-8 text-sm"
/>
</div>
</TabsContent>
</Tabs>
</div>
);
}