ERP-node/frontend/components/dataflow/external-call/ExternalCallTestPanel.tsx

498 lines
18 KiB
TypeScript
Raw Normal View History

2025-09-26 17:11:18 +09:00
"use client";
import React, { useState, useCallback, useEffect } from "react";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { Button } from "@/components/ui/button";
import { Badge } from "@/components/ui/badge";
import { Alert, AlertDescription } from "@/components/ui/alert";
import { Textarea } from "@/components/ui/textarea";
import { Label } from "@/components/ui/label";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
import { ScrollArea } from "@/components/ui/scroll-area";
import {
TestTube,
Play,
CheckCircle,
XCircle,
Clock,
AlertCircle,
Copy,
RefreshCw,
Zap,
Code,
Network,
Timer,
} from "lucide-react";
import { toast } from "sonner";
// 타입 import
import {
ExternalCallTestPanelProps,
ApiTestResult,
ExternalCallContext,
} from "@/types/external-call/ExternalCallTypes";
import { ExternalCallAPI } from "@/lib/api/externalCall";
/**
* 🧪 API
*
* REST API
* 릿 , ,
*/
const ExternalCallTestPanel: React.FC<ExternalCallTestPanelProps> = ({
settings,
context,
onTestResult,
disabled = false,
}) => {
// 상태 관리
const [isLoading, setIsLoading] = useState<boolean>(false);
const [testResult, setTestResult] = useState<ApiTestResult | null>(null);
const [activeTab, setActiveTab] = useState<string>("request");
const [processedTemplate, setProcessedTemplate] = useState<string>("");
const [testContext, setTestContext] = useState<ExternalCallContext>(() => ({
relationshipId: context?.relationshipId || "test-relationship",
diagramId: context?.diagramId || "test-diagram",
userId: context?.userId || "test-user",
executionId: context?.executionId || `test-${Date.now()}`,
sourceData: context?.sourceData || {
id: 1,
name: "테스트 데이터",
value: 100,
status: "active",
},
targetData: context?.targetData,
timestamp: context?.timestamp || new Date().toISOString(),
metadata: context?.metadata,
}));
// 템플릿 변수 치환 함수
const processTemplate = useCallback((template: string, context: ExternalCallContext): string => {
let processed = template;
// 각 템플릿 변수를 실제 값으로 치환
const replacements = {
"{{sourceData}}": JSON.stringify(context.sourceData, null, 2),
"{{targetData}}": context.targetData ? JSON.stringify(context.targetData, null, 2) : "null",
"{{timestamp}}": context.timestamp,
"{{relationshipId}}": context.relationshipId,
"{{diagramId}}": context.diagramId,
"{{userId}}": context.userId,
"{{executionId}}": context.executionId,
};
Object.entries(replacements).forEach(([variable, value]) => {
processed = processed.replace(new RegExp(variable.replace(/[{}]/g, "\\$&"), "g"), value);
});
return processed;
}, []);
// 템플릿 처리 (설정이나 컨텍스트 변경 시)
useEffect(() => {
if (settings.bodyTemplate) {
const processed = processTemplate(settings.bodyTemplate, testContext);
setProcessedTemplate(processed);
}
}, [settings.bodyTemplate, testContext, processTemplate]);
// API 테스트 실행
const handleRunTest = useCallback(async () => {
if (!settings.apiUrl) {
toast.error("API URL을 입력해주세요.");
return;
}
setIsLoading(true);
setTestResult(null);
try {
// 테스트 요청 데이터 구성 (백엔드 형식에 맞춤)
const testRequest = {
settings: {
callType: "rest-api" as const,
apiType: "generic" as const,
url: settings.apiUrl,
method: settings.httpMethod,
headers: settings.headers,
body: processedTemplate,
authentication: settings.authentication, // 인증 정보 추가
timeout: settings.timeout,
retryCount: settings.retryCount,
},
templateData: testContext,
};
// API 호출
const response = await ExternalCallAPI.testExternalCall(testRequest);
if (response.success && response.result) {
// 백엔드 응답을 ApiTestResult 형태로 변환
const apiTestResult: ApiTestResult = {
success: response.result.success,
statusCode: response.result.statusCode,
responseTime: response.result.executionTime || 0,
response: response.result.response,
error: response.result.error,
timestamp: new Date().toISOString(),
};
setTestResult(apiTestResult);
onTestResult(apiTestResult);
if (apiTestResult.success) {
toast.success("API 테스트가 성공했습니다!");
setActiveTab("response");
} else {
toast.error("API 호출이 실패했습니다.");
setActiveTab("response");
}
} else {
const errorResult: ApiTestResult = {
success: false,
responseTime: 0,
error: response.error || "알 수 없는 오류가 발생했습니다.",
timestamp: new Date().toISOString(),
};
setTestResult(errorResult);
onTestResult(errorResult);
toast.error(response.error || "테스트 실행 중 오류가 발생했습니다.");
}
} catch (error) {
const errorResult: ApiTestResult = {
success: false,
responseTime: 0,
error: error instanceof Error ? error.message : "네트워크 오류가 발생했습니다.",
timestamp: new Date().toISOString(),
};
setTestResult(errorResult);
onTestResult(errorResult);
toast.error("테스트 실행 중 오류가 발생했습니다.");
} finally {
setIsLoading(false);
}
}, [settings, processedTemplate, testContext, onTestResult]);
// 테스트 데이터 복사
const handleCopyToClipboard = useCallback(async (text: string) => {
try {
await navigator.clipboard.writeText(text);
toast.success("클립보드에 복사되었습니다.");
} catch (error) {
toast.error("복사에 실패했습니다.");
}
}, []);
// 테스트 컨텍스트 리셋
const handleResetContext = useCallback(() => {
setTestContext({
relationshipId: "test-relationship",
diagramId: "test-diagram",
userId: "test-user",
executionId: `test-${Date.now()}`,
sourceData: {
id: 1,
name: "테스트 데이터",
value: 100,
status: "active",
},
timestamp: new Date().toISOString(),
});
}, []);
return (
<div className="space-y-4">
{/* 테스트 실행 헤더 */}
<Card>
<CardHeader className="pb-3">
<div className="flex items-center justify-between">
<div className="flex items-center gap-2">
<TestTube className="h-5 w-5 text-blue-500" />
<CardTitle className="text-lg">API </CardTitle>
{testResult && (
<Badge variant={testResult.success ? "default" : "destructive"}>
{testResult.success ? "성공" : "실패"}
</Badge>
)}
</div>
<div className="flex gap-2">
<Button variant="outline" size="sm" onClick={handleResetContext} disabled={disabled || isLoading}>
<RefreshCw className="mr-1 h-4 w-4" />
</Button>
<Button
onClick={handleRunTest}
disabled={disabled || isLoading || !settings.apiUrl}
className="min-w-[100px]"
>
{isLoading ? (
<>
<RefreshCw className="mr-2 h-4 w-4 animate-spin" />
...
</>
) : (
<>
<Play className="mr-2 h-4 w-4" />
</>
)}
</Button>
</div>
</div>
</CardHeader>
</Card>
{/* 테스트 결과 요약 */}
{testResult && (
<Card>
<CardContent className="pt-6">
<div className="grid grid-cols-4 gap-4 text-center">
<div className="space-y-1">
<div className="flex items-center justify-center gap-1">
{testResult.success ? (
<CheckCircle className="h-4 w-4 text-green-500" />
) : (
<XCircle className="h-4 w-4 text-red-500" />
)}
<span className="text-sm font-medium"></span>
</div>
<div className="text-muted-foreground text-xs">{testResult.success ? "성공" : "실패"}</div>
</div>
<div className="space-y-1">
<div className="flex items-center justify-center gap-1">
<Network className="h-4 w-4 text-blue-500" />
<span className="text-sm font-medium"> </span>
</div>
<div className="text-muted-foreground text-xs">{testResult.statusCode || "N/A"}</div>
</div>
<div className="space-y-1">
<div className="flex items-center justify-center gap-1">
<Timer className="h-4 w-4 text-orange-500" />
<span className="text-sm font-medium"> </span>
</div>
<div className="text-muted-foreground text-xs">{testResult.responseTime}ms</div>
</div>
<div className="space-y-1">
<div className="flex items-center justify-center gap-1">
<Clock className="h-4 w-4 text-purple-500" />
<span className="text-sm font-medium"> </span>
</div>
<div className="text-muted-foreground text-xs">
{new Date(testResult.timestamp).toLocaleTimeString()}
</div>
</div>
</div>
</CardContent>
</Card>
)}
{/* 상세 정보 탭 */}
<Tabs value={activeTab} onValueChange={setActiveTab} className="w-full">
<TabsList className="grid w-full grid-cols-3">
<TabsTrigger value="request" className="flex items-center gap-2">
<Zap className="h-4 w-4" />
</TabsTrigger>
<TabsTrigger value="response" className="flex items-center gap-2">
<Code className="h-4 w-4" />
</TabsTrigger>
<TabsTrigger value="context" className="flex items-center gap-2">
<AlertCircle className="h-4 w-4" />
</TabsTrigger>
</TabsList>
{/* 요청 정보 탭 */}
<TabsContent value="request" className="space-y-4">
<Card>
<CardHeader>
<div className="flex items-center justify-between">
<CardTitle className="text-base"> </CardTitle>
<Button
variant="outline"
size="sm"
onClick={() =>
handleCopyToClipboard(
JSON.stringify(
{
url: settings.apiUrl,
method: settings.httpMethod,
headers: settings.headers,
body: processedTemplate,
},
null,
2,
),
)
}
>
<Copy className="mr-1 h-4 w-4" />
</Button>
</div>
</CardHeader>
<CardContent className="space-y-4">
{/* URL과 메서드 */}
<div className="grid grid-cols-4 gap-4">
<div className="col-span-1">
<Label className="text-muted-foreground text-xs">HTTP </Label>
<Badge variant="outline" className="mt-1">
{settings.httpMethod}
</Badge>
</div>
<div className="col-span-3">
<Label className="text-muted-foreground text-xs">URL</Label>
<div className="bg-muted mt-1 rounded p-2 font-mono text-sm break-all">{settings.apiUrl}</div>
</div>
</div>
{/* 헤더 */}
<div>
<Label className="text-muted-foreground text-xs"></Label>
<ScrollArea className="mt-1 h-32 w-full rounded border">
<div className="p-3">
<pre className="text-xs">{JSON.stringify(settings.headers, null, 2)}</pre>
</div>
</ScrollArea>
</div>
{/* 요청 바디 (POST/PUT/PATCH인 경우) */}
{["POST", "PUT", "PATCH"].includes(settings.httpMethod) && (
<div>
<Label className="text-muted-foreground text-xs"> (릿 )</Label>
<ScrollArea className="mt-1 h-40 w-full rounded border">
<div className="p-3">
<pre className="text-xs whitespace-pre-wrap">{processedTemplate}</pre>
</div>
</ScrollArea>
</div>
)}
</CardContent>
</Card>
</TabsContent>
{/* 응답 정보 탭 */}
<TabsContent value="response" className="space-y-4">
{testResult ? (
<Card>
<CardHeader>
<div className="flex items-center justify-between">
<CardTitle className="text-base"> </CardTitle>
<Button
variant="outline"
size="sm"
onClick={() => handleCopyToClipboard(JSON.stringify(testResult, null, 2))}
>
<Copy className="mr-1 h-4 w-4" />
</Button>
</div>
</CardHeader>
<CardContent className="space-y-4">
{testResult.success ? (
<>
{/* 상태 코드 */}
{testResult.statusCode && (
<div>
<Label className="text-muted-foreground text-xs"> </Label>
<div className="mt-1 rounded border bg-green-50 p-2">
<span className="font-mono text-sm text-green-700">{testResult.statusCode}</span>
</div>
</div>
)}
{/* 응답 시간 */}
{testResult.responseTime !== undefined && (
<div>
<Label className="text-muted-foreground text-xs"> </Label>
<div className="mt-1 rounded border bg-blue-50 p-2">
<span className="font-mono text-sm text-blue-700">{testResult.responseTime}ms</span>
</div>
</div>
)}
{/* 응답 데이터 */}
{testResult.response && (
<div>
<Label className="text-muted-foreground text-xs"> </Label>
<ScrollArea className="mt-1 h-40 w-full rounded border">
<div className="p-3">
<pre className="text-xs whitespace-pre-wrap">{testResult.response}</pre>
</div>
</ScrollArea>
</div>
)}
</>
) : (
<Alert variant="destructive">
<XCircle className="h-4 w-4" />
<AlertDescription>
<div className="space-y-2">
<div className="font-medium"> </div>
<div className="text-sm">{testResult.error}</div>
{testResult.statusCode && <div className="text-sm"> : {testResult.statusCode}</div>}
</div>
</AlertDescription>
</Alert>
)}
</CardContent>
</Card>
) : (
<Card>
<CardContent className="pt-6">
<div className="text-muted-foreground py-8 text-center">
<TestTube className="mx-auto mb-2 h-8 w-8 opacity-50" />
<p> .</p>
</div>
</CardContent>
</Card>
)}
</TabsContent>
{/* 테스트 데이터 탭 */}
<TabsContent value="context" className="space-y-4">
<Card>
<CardHeader>
<div className="flex items-center justify-between">
<CardTitle className="text-base"> </CardTitle>
<Button
variant="outline"
size="sm"
onClick={() => handleCopyToClipboard(JSON.stringify(testContext, null, 2))}
>
<Copy className="mr-1 h-4 w-4" />
</Button>
</div>
</CardHeader>
<CardContent className="space-y-4">
<div className="text-muted-foreground text-sm">릿 .</div>
<ScrollArea className="h-60 w-full rounded border">
<div className="p-3">
<pre className="text-xs whitespace-pre-wrap">{JSON.stringify(testContext, null, 2)}</pre>
</div>
</ScrollArea>
<Alert>
<AlertCircle className="h-4 w-4" />
<AlertDescription className="text-sm">
. .
</AlertDescription>
</Alert>
</CardContent>
</Card>
</TabsContent>
</Tabs>
</div>
);
};
export default ExternalCallTestPanel;