ERP-node/frontend/components/dataflow/connection/ExternalCallSettings.tsx

379 lines
14 KiB
TypeScript
Raw Normal View History

2025-09-16 15:43:18 +09:00
"use client";
import React from "react";
import { Label } from "@/components/ui/label";
import { Input } from "@/components/ui/input";
import { Textarea } from "@/components/ui/textarea";
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
2025-09-17 11:47:57 +09:00
import { Button } from "@/components/ui/button";
import { ExternalCallAPI } from "@/lib/api/externalCall";
import { toast } from "sonner";
2025-09-16 15:43:18 +09:00
import { Globe } from "lucide-react";
import { ExternalCallSettings as ExternalCallSettingsType } from "@/types/connectionTypes";
interface ExternalCallSettingsProps {
settings: ExternalCallSettingsType;
onSettingsChange: (settings: ExternalCallSettingsType) => void;
}
2025-09-17 11:47:57 +09:00
const handleTestExternalCall = async (settings: ExternalCallSettingsType) => {
let loadingToastId: string | number | undefined;
try {
// 설정을 백엔드 형식으로 변환
const backendSettings: Record<string, unknown> = {
callType: settings.callType,
timeout: 10000, // 10초 타임아웃 설정
};
if (settings.callType === "rest-api") {
backendSettings.apiType = settings.apiType;
switch (settings.apiType) {
case "slack":
backendSettings.webhookUrl = settings.slackWebhookUrl;
backendSettings.message =
settings.slackMessage || "테스트 메시지: {{recordCount}}건의 데이터가 처리되었습니다.";
backendSettings.channel = settings.slackChannel;
break;
case "kakao-talk":
backendSettings.accessToken = settings.kakaoAccessToken;
backendSettings.message =
settings.kakaoMessage || "테스트 메시지: {{recordCount}}건의 데이터가 처리되었습니다.";
break;
case "discord":
backendSettings.webhookUrl = settings.discordWebhookUrl;
backendSettings.message =
settings.discordMessage || "테스트 메시지: {{recordCount}}건의 데이터가 처리되었습니다.";
backendSettings.username = settings.discordUsername;
break;
case "generic":
default:
backendSettings.url = settings.apiUrl;
backendSettings.method = settings.httpMethod || "POST";
try {
backendSettings.headers = settings.headers ? JSON.parse(settings.headers) : {};
} catch (error) {
console.warn("Headers JSON 파싱 실패, 기본값 사용:", error);
backendSettings.headers = {};
}
backendSettings.body = settings.bodyTemplate || "{}";
break;
}
}
// 로딩 토스트 시작
loadingToastId = toast.loading("외부 호출 테스트 중...", {
duration: 12000, // 12초 후 자동으로 사라짐
});
// 타임아웃을 위한 Promise.race 사용
const timeoutPromise = new Promise<never>((_, reject) => {
setTimeout(() => reject(new Error("테스트 요청이 10초 내에 완료되지 않았습니다.")), 10000);
});
const testPromise = ExternalCallAPI.testExternalCall({
settings: backendSettings,
templateData: {
recordCount: 5,
tableName: "test_table",
timestamp: new Date().toISOString(),
message: "데이터플로우 테스트 실행",
},
});
const result = await Promise.race([testPromise, timeoutPromise]);
// 로딩 토스트 제거
if (loadingToastId) {
toast.dismiss(loadingToastId);
}
if (result.success && result.result?.success) {
toast.success("외부 호출 테스트 성공!", {
description: `응답 시간: ${result.result.executionTime}ms`,
duration: 4000,
});
} else {
toast.error("외부 호출 테스트 실패", {
description: result.result?.error || result.error || "알 수 없는 오류",
duration: 6000,
});
}
} catch (error) {
console.error("테스트 실행 중 오류:", error);
// 로딩 토스트 제거
if (loadingToastId) {
toast.dismiss(loadingToastId);
}
if (error instanceof Error) {
toast.error("테스트 실행 중 오류가 발생했습니다.", {
description: error.message,
duration: 6000,
});
} else {
toast.error("테스트 실행 중 알 수 없는 오류가 발생했습니다.", {
duration: 6000,
});
}
}
};
2025-09-16 15:43:18 +09:00
export const ExternalCallSettings: React.FC<ExternalCallSettingsProps> = ({ settings, onSettingsChange }) => {
return (
<div className="rounded-lg border border-l-4 border-l-orange-500 bg-orange-50/30 p-4">
2025-09-17 11:47:57 +09:00
<div className="mb-3 flex items-center justify-between">
<div className="flex items-center gap-2">
<Globe className="h-4 w-4 text-orange-500" />
<span className="text-sm font-medium"> </span>
</div>
<Button
type="button"
variant="outline"
size="sm"
onClick={() => handleTestExternalCall(settings)}
className="h-7 px-2 text-xs"
>
</Button>
2025-09-16 15:43:18 +09:00
</div>
<div className="space-y-3">
<div>
<Label htmlFor="callType" className="text-sm">
</Label>
<Select
value={settings.callType}
onValueChange={(value: "rest-api" | "email" | "ftp" | "queue") =>
2025-09-16 15:43:18 +09:00
onSettingsChange({ ...settings, callType: value })
}
>
<SelectTrigger className="text-sm">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="rest-api">REST API </SelectItem>
<SelectItem value="email"> </SelectItem>
<SelectItem value="ftp">FTP </SelectItem>
<SelectItem value="queue"> </SelectItem>
</SelectContent>
</Select>
</div>
{settings.callType === "rest-api" && (
<>
<div>
<Label htmlFor="apiType" className="text-sm">
API
2025-09-16 15:43:18 +09:00
</Label>
<Select
value={settings.apiType || "generic"}
onValueChange={(value: "slack" | "kakao-talk" | "discord" | "generic") =>
onSettingsChange({ ...settings, apiType: value })
}
>
<SelectTrigger className="text-sm">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="slack"></SelectItem>
<SelectItem value="kakao-talk"></SelectItem>
<SelectItem value="discord"></SelectItem>
<SelectItem value="generic"> ( API)</SelectItem>
</SelectContent>
</Select>
</div>
{/* 슬랙 설정 */}
{settings.apiType === "slack" && (
<>
<div>
<Label htmlFor="slackWebhookUrl" className="text-sm">
URL
</Label>
<Input
id="slackWebhookUrl"
value={settings.slackWebhookUrl || ""}
onChange={(e) => onSettingsChange({ ...settings, slackWebhookUrl: e.target.value })}
placeholder="https://hooks.slack.com/services/..."
className="text-sm"
/>
</div>
<div>
<Label htmlFor="slackChannel" className="text-sm">
</Label>
<Input
id="slackChannel"
value={settings.slackChannel || ""}
onChange={(e) => onSettingsChange({ ...settings, slackChannel: e.target.value })}
placeholder="#general"
className="text-sm"
/>
</div>
<div>
<Label htmlFor="slackMessage" className="text-sm">
릿
</Label>
<Textarea
id="slackMessage"
value={settings.slackMessage || ""}
onChange={(e) => onSettingsChange({ ...settings, slackMessage: e.target.value })}
placeholder="데이터 처리가 완료되었습니다. 총 {{recordCount}}건이 처리되었습니다."
rows={2}
className="text-sm"
/>
</div>
</>
)}
{/* 카카오톡 설정 */}
{settings.apiType === "kakao-talk" && (
<>
<div>
<Label htmlFor="kakaoAccessToken" className="text-sm">
<span className="text-red-500">*</span>
</Label>
<Input
id="kakaoAccessToken"
type="password"
value={settings.kakaoAccessToken || ""}
onChange={(e) => onSettingsChange({ ...settings, kakaoAccessToken: e.target.value })}
placeholder="카카오 API 액세스 토큰을 입력하세요"
className="text-sm"
/>
</div>
<div>
<Label htmlFor="kakaoMessage" className="text-sm">
</Label>
<Textarea
id="kakaoMessage"
value={settings.kakaoMessage || ""}
onChange={(e) => onSettingsChange({ ...settings, kakaoMessage: e.target.value })}
placeholder="데이터 처리 완료! 총 {{recordCount}}건 처리되었습니다."
rows={2}
className="text-sm"
/>
</div>
</>
)}
{/* 디스코드 설정 */}
{settings.apiType === "discord" && (
<>
<div>
<Label htmlFor="discordWebhookUrl" className="text-sm">
2025-09-17 11:47:57 +09:00
URL <span className="text-red-500">*</span>
</Label>
<Input
id="discordWebhookUrl"
value={settings.discordWebhookUrl || ""}
onChange={(e) => onSettingsChange({ ...settings, discordWebhookUrl: e.target.value })}
placeholder="https://discord.com/api/webhooks/..."
className="text-sm"
/>
</div>
2025-09-17 11:47:57 +09:00
<div>
<Label htmlFor="discordUsername" className="text-sm">
</Label>
<Input
id="discordUsername"
value={settings.discordUsername || ""}
onChange={(e) => onSettingsChange({ ...settings, discordUsername: e.target.value })}
placeholder="ERP 시스템"
className="text-sm"
/>
</div>
<div>
<Label htmlFor="discordMessage" className="text-sm">
</Label>
<Textarea
id="discordMessage"
value={settings.discordMessage || ""}
onChange={(e) => onSettingsChange({ ...settings, discordMessage: e.target.value })}
placeholder="데이터 처리가 완료되었습니다! 🎉"
rows={2}
className="text-sm"
/>
</div>
</>
)}
{/* 일반 API 설정 */}
{settings.apiType === "generic" && (
<>
<div>
<Label htmlFor="apiUrl" className="text-sm">
API URL
</Label>
<Input
id="apiUrl"
value={settings.apiUrl || ""}
onChange={(e) => onSettingsChange({ ...settings, apiUrl: e.target.value })}
placeholder="https://api.example.com/webhook"
className="text-sm"
/>
</div>
<div className="grid grid-cols-2 gap-3">
<div>
<Label htmlFor="httpMethod" className="text-sm">
HTTP Method
</Label>
<Select
value={settings.httpMethod}
onValueChange={(value: "GET" | "POST" | "PUT" | "DELETE") =>
onSettingsChange({ ...settings, httpMethod: value })
}
>
<SelectTrigger className="text-sm">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="GET">GET</SelectItem>
<SelectItem value="POST">POST</SelectItem>
<SelectItem value="PUT">PUT</SelectItem>
<SelectItem value="DELETE">DELETE</SelectItem>
</SelectContent>
</Select>
</div>
<div>
<Label htmlFor="headers" className="text-sm">
Headers
</Label>
<Textarea
id="headers"
value={settings.headers}
onChange={(e) => onSettingsChange({ ...settings, headers: e.target.value })}
placeholder="{}"
rows={1}
className="text-sm"
/>
</div>
</div>
<div>
<Label htmlFor="bodyTemplate" className="text-sm">
Body Template
</Label>
<Textarea
id="bodyTemplate"
value={settings.bodyTemplate}
onChange={(e) => onSettingsChange({ ...settings, bodyTemplate: e.target.value })}
placeholder="{}"
rows={2}
className="text-sm"
/>
</div>
</>
)}
</>
)}
2025-09-16 15:43:18 +09:00
</div>
</div>
);
};