379 lines
14 KiB
TypeScript
379 lines
14 KiB
TypeScript
"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";
|
|
import { Button } from "@/components/ui/button";
|
|
import { ExternalCallAPI } from "@/lib/api/externalCall";
|
|
import { toast } from "sonner";
|
|
import { Globe } from "lucide-react";
|
|
import { ExternalCallSettings as ExternalCallSettingsType } from "@/types/connectionTypes";
|
|
|
|
interface ExternalCallSettingsProps {
|
|
settings: ExternalCallSettingsType;
|
|
onSettingsChange: (settings: ExternalCallSettingsType) => void;
|
|
}
|
|
|
|
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,
|
|
});
|
|
}
|
|
}
|
|
};
|
|
|
|
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">
|
|
<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>
|
|
</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") =>
|
|
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 종류
|
|
</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">
|
|
디스코드 웹훅 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>
|
|
<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>
|
|
</>
|
|
)}
|
|
</>
|
|
)}
|
|
</div>
|
|
</div>
|
|
);
|
|
};
|