ERP-node/frontend/components/admin/ExternalCallConfigModal.tsx

588 lines
22 KiB
TypeScript

"use client";
import React, { useState, useEffect } from "react";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { Textarea } from "@/components/ui/textarea";
import {
Dialog,
DialogContent,
DialogHeader,
DialogTitle,
DialogDescription,
DialogFooter,
} from "@/components/ui/dialog";
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
import { toast } from "sonner";
import {
ExternalCallConfigAPI,
ExternalCallConfig,
CALL_TYPE_OPTIONS,
API_TYPE_OPTIONS,
ACTIVE_STATUS_OPTIONS,
} from "@/lib/api/externalCallConfig";
interface ExternalCallConfigModalProps {
isOpen: boolean;
onClose: () => void;
onSave: () => void;
editingConfig?: ExternalCallConfig | null;
}
export function ExternalCallConfigModal({ isOpen, onClose, onSave, editingConfig }: ExternalCallConfigModalProps) {
const [loading, setLoading] = useState(false);
const [formData, setFormData] = useState<Partial<ExternalCallConfig>>({
config_name: "",
call_type: "rest-api",
api_type: "discord",
description: "",
is_active: "Y",
config_data: {},
});
// Discord 설정 상태
const [discordSettings, setDiscordSettings] = useState({
webhookUrl: "",
username: "",
avatarUrl: "",
});
// Slack 설정 상태
const [slackSettings, setSlackSettings] = useState({
webhookUrl: "",
channel: "",
username: "",
});
// 카카오톡 설정 상태
const [kakaoSettings, setKakaoSettings] = useState({
accessToken: "",
templateId: "",
});
// 일반 API 설정 상태
const [genericSettings, setGenericSettings] = useState({
url: "",
method: "POST",
headers: "{}",
timeout: "30000",
});
// 편집 모드일 때 기존 데이터 로드
useEffect(() => {
if (isOpen && editingConfig) {
setFormData({
config_name: editingConfig.config_name,
call_type: editingConfig.call_type,
api_type: editingConfig.api_type,
description: editingConfig.description || "",
is_active: editingConfig.is_active || "Y",
config_data: editingConfig.config_data,
});
// API 타입별 설정 데이터 로드
const configData = editingConfig.config_data as any;
if (editingConfig.api_type === "discord") {
setDiscordSettings({
webhookUrl: configData.webhookUrl || "",
username: configData.username || "",
avatarUrl: configData.avatarUrl || "",
});
} else if (editingConfig.api_type === "slack") {
setSlackSettings({
webhookUrl: configData.webhookUrl || "",
channel: configData.channel || "",
username: configData.username || "",
});
} else if (editingConfig.api_type === "kakao-talk") {
setKakaoSettings({
accessToken: configData.accessToken || "",
templateId: configData.templateId || "",
});
} else if (editingConfig.api_type === "generic") {
setGenericSettings({
url: configData.url || "",
method: configData.method || "POST",
headers: JSON.stringify(configData.headers || {}, null, 2),
timeout: String(configData.timeout || 30000),
});
}
} else if (isOpen && !editingConfig) {
// 새 설정 추가 시 초기화
setFormData({
config_name: "",
call_type: "rest-api",
api_type: "discord",
description: "",
is_active: "Y",
config_data: {},
});
setDiscordSettings({ webhookUrl: "", username: "", avatarUrl: "" });
setSlackSettings({ webhookUrl: "", channel: "", username: "" });
setKakaoSettings({ accessToken: "", templateId: "" });
setGenericSettings({ url: "", method: "POST", headers: "{}", timeout: "30000" });
}
}, [isOpen, editingConfig]);
// 호출 타입 변경 시 API 타입 초기화
const handleCallTypeChange = (callType: string) => {
setFormData((prev) => ({
...prev,
call_type: callType,
api_type: callType === "rest-api" ? "discord" : undefined,
}));
};
// config_data 생성
const generateConfigData = () => {
const { api_type } = formData;
switch (api_type) {
case "discord":
return {
webhookUrl: discordSettings.webhookUrl,
username: discordSettings.username || "ERP 시스템",
avatarUrl: discordSettings.avatarUrl || null,
};
case "slack":
return {
webhookUrl: slackSettings.webhookUrl,
channel: slackSettings.channel,
username: slackSettings.username || "ERP Bot",
};
case "kakao-talk":
return {
accessToken: kakaoSettings.accessToken,
templateId: kakaoSettings.templateId,
};
case "generic":
try {
return {
url: genericSettings.url,
method: genericSettings.method,
headers: JSON.parse(genericSettings.headers),
timeout: parseInt(genericSettings.timeout),
};
} catch (error) {
throw new Error("헤더 JSON 형식이 올바르지 않습니다.");
}
default:
return {};
}
};
// 폼 검증
const validateForm = () => {
if (!formData.config_name?.trim()) {
toast.error("설정 이름을 입력해주세요.");
return false;
}
if (!formData.call_type) {
toast.error("호출 타입을 선택해주세요.");
return false;
}
// REST API인 경우 API 타입별 검증
if (formData.call_type === "rest-api") {
switch (formData.api_type) {
case "discord":
if (!discordSettings.webhookUrl.trim()) {
toast.error("Discord 웹훅 URL을 입력해주세요.");
return false;
}
break;
case "slack":
if (!slackSettings.webhookUrl.trim()) {
toast.error("Slack 웹훅 URL을 입력해주세요.");
return false;
}
break;
case "kakao-talk":
if (!kakaoSettings.accessToken.trim()) {
toast.error("카카오톡 액세스 토큰을 입력해주세요.");
return false;
}
break;
case "generic":
if (!genericSettings.url.trim()) {
toast.error("API URL을 입력해주세요.");
return false;
}
try {
JSON.parse(genericSettings.headers);
} catch {
toast.error("헤더 JSON 형식이 올바르지 않습니다.");
return false;
}
break;
}
}
return true;
};
// 저장 처리
const handleSave = async () => {
if (!validateForm()) return;
try {
setLoading(true);
const configData = generateConfigData();
const saveData = {
...formData,
config_data: configData,
};
let response;
if (editingConfig?.id) {
response = await ExternalCallConfigAPI.updateConfig(editingConfig.id, saveData);
} else {
response = await ExternalCallConfigAPI.createConfig(saveData as any);
}
if (response.success) {
toast.success(editingConfig ? "외부 호출 설정이 수정되었습니다." : "외부 호출 설정이 생성되었습니다.");
onSave();
} else {
toast.error(response.message || "저장 실패");
}
} catch (error) {
console.error("외부 호출 설정 저장 오류:", error);
toast.error("저장 중 오류가 발생했습니다.");
} finally {
setLoading(false);
}
};
return (
<Dialog open={isOpen} onOpenChange={onClose}>
<DialogContent className="max-w-[95vw] sm:max-w-2xl">
<DialogHeader>
<DialogTitle className="text-base sm:text-lg">
{editingConfig ? "외부 호출 설정 편집" : "새 외부 호출 설정"}
</DialogTitle>
</DialogHeader>
<div className="max-h-[60vh] space-y-4 overflow-y-auto sm:space-y-6">
{/* 기본 정보 */}
<div className="space-y-3 sm:space-y-4">
<div>
<Label htmlFor="config_name" className="text-xs sm:text-sm">
*
</Label>
<Input
id="config_name"
value={formData.config_name}
onChange={(e) => setFormData((prev) => ({ ...prev, config_name: e.target.value }))}
placeholder="예: 개발팀 Discord"
className="h-8 text-xs sm:h-10 sm:text-sm"
/>
</div>
<div>
<Label htmlFor="description" className="text-xs sm:text-sm">
</Label>
<Textarea
id="description"
value={formData.description}
onChange={(e) => setFormData((prev) => ({ ...prev, description: e.target.value }))}
placeholder="이 외부 호출 설정에 대한 설명을 입력하세요."
rows={2}
className="text-xs sm:text-sm"
/>
</div>
<div className="grid grid-cols-1 gap-3 sm:grid-cols-2 sm:gap-4">
<div>
<Label htmlFor="call_type" className="text-xs sm:text-sm">
*
</Label>
<Select value={formData.call_type} onValueChange={handleCallTypeChange}>
<SelectTrigger className="h-8 text-xs sm:h-10 sm:text-sm">
<SelectValue />
</SelectTrigger>
<SelectContent>
{CALL_TYPE_OPTIONS.map((option) => (
<SelectItem key={option.value} value={option.value} className="text-xs sm:text-sm">
{option.label}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
<div>
<Label htmlFor="is_active" className="text-xs sm:text-sm">
</Label>
<Select
value={formData.is_active}
onValueChange={(value) => setFormData((prev) => ({ ...prev, is_active: value }))}
>
<SelectTrigger className="h-8 text-xs sm:h-10 sm:text-sm">
<SelectValue />
</SelectTrigger>
<SelectContent>
{ACTIVE_STATUS_OPTIONS.map((option) => (
<SelectItem key={option.value} value={option.value} className="text-xs sm:text-sm">
{option.label}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
</div>
</div>
{/* REST API 설정 */}
{formData.call_type === "rest-api" && (
<div className="space-y-3 sm:space-y-4">
<div>
<Label htmlFor="api_type" className="text-xs sm:text-sm">
API *
</Label>
<Select
value={formData.api_type}
onValueChange={(value) => setFormData((prev) => ({ ...prev, api_type: value }))}
>
<SelectTrigger className="h-8 text-xs sm:h-10 sm:text-sm">
<SelectValue />
</SelectTrigger>
<SelectContent>
{API_TYPE_OPTIONS.map((option) => (
<SelectItem key={option.value} value={option.value} className="text-xs sm:text-sm">
{option.label}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
{/* Discord 설정 */}
{formData.api_type === "discord" && (
<div className="space-y-3 rounded-lg border bg-muted/20 p-3 sm:p-4">
<h4 className="text-xs font-semibold sm:text-sm">Discord </h4>
<div>
<Label htmlFor="discord_webhook" className="text-xs sm:text-sm">
URL *
</Label>
<Input
id="discord_webhook"
value={discordSettings.webhookUrl}
onChange={(e) => setDiscordSettings((prev) => ({ ...prev, webhookUrl: e.target.value }))}
placeholder="https://discord.com/api/webhooks/..."
className="h-8 text-xs sm:h-10 sm:text-sm"
/>
</div>
<div>
<Label htmlFor="discord_username" className="text-xs sm:text-sm">
</Label>
<Input
id="discord_username"
value={discordSettings.username}
onChange={(e) => setDiscordSettings((prev) => ({ ...prev, username: e.target.value }))}
placeholder="ERP 시스템"
className="h-8 text-xs sm:h-10 sm:text-sm"
/>
</div>
<div>
<Label htmlFor="discord_avatar" className="text-xs sm:text-sm">
URL
</Label>
<Input
id="discord_avatar"
value={discordSettings.avatarUrl}
onChange={(e) => setDiscordSettings((prev) => ({ ...prev, avatarUrl: e.target.value }))}
placeholder="https://example.com/avatar.png"
className="h-8 text-xs sm:h-10 sm:text-sm"
/>
</div>
</div>
)}
{/* Slack 설정 */}
{formData.api_type === "slack" && (
<div className="space-y-3 rounded-lg border bg-muted/20 p-3 sm:p-4">
<h4 className="text-xs font-semibold sm:text-sm">Slack </h4>
<div>
<Label htmlFor="slack_webhook" className="text-xs sm:text-sm">
URL *
</Label>
<Input
id="slack_webhook"
value={slackSettings.webhookUrl}
onChange={(e) => setSlackSettings((prev) => ({ ...prev, webhookUrl: e.target.value }))}
placeholder="https://hooks.slack.com/services/..."
className="h-8 text-xs sm:h-10 sm:text-sm"
/>
</div>
<div>
<Label htmlFor="slack_channel" className="text-xs sm:text-sm">
</Label>
<Input
id="slack_channel"
value={slackSettings.channel}
onChange={(e) => setSlackSettings((prev) => ({ ...prev, channel: e.target.value }))}
placeholder="#general"
className="h-8 text-xs sm:h-10 sm:text-sm"
/>
</div>
<div>
<Label htmlFor="slack_username" className="text-xs sm:text-sm">
</Label>
<Input
id="slack_username"
value={slackSettings.username}
onChange={(e) => setSlackSettings((prev) => ({ ...prev, username: e.target.value }))}
placeholder="ERP Bot"
className="h-8 text-xs sm:h-10 sm:text-sm"
/>
</div>
</div>
)}
{/* 카카오톡 설정 */}
{formData.api_type === "kakao-talk" && (
<div className="space-y-3 rounded-lg border bg-muted/20 p-3 sm:p-4">
<h4 className="text-xs font-semibold sm:text-sm"> </h4>
<div>
<Label htmlFor="kakao_token" className="text-xs sm:text-sm">
*
</Label>
<Input
id="kakao_token"
type="password"
value={kakaoSettings.accessToken}
onChange={(e) => setKakaoSettings((prev) => ({ ...prev, accessToken: e.target.value }))}
placeholder="카카오 API 액세스 토큰"
className="h-8 text-xs sm:h-10 sm:text-sm"
/>
</div>
<div>
<Label htmlFor="kakao_template" className="text-xs sm:text-sm">
릿 ID
</Label>
<Input
id="kakao_template"
value={kakaoSettings.templateId}
onChange={(e) => setKakaoSettings((prev) => ({ ...prev, templateId: e.target.value }))}
placeholder="template_001"
className="h-8 text-xs sm:h-10 sm:text-sm"
/>
</div>
</div>
)}
{/* 일반 API 설정 */}
{formData.api_type === "generic" && (
<div className="space-y-3 rounded-lg border bg-muted/20 p-3 sm:p-4">
<h4 className="text-xs font-semibold sm:text-sm"> API </h4>
<div>
<Label htmlFor="generic_url" className="text-xs sm:text-sm">
API URL *
</Label>
<Input
id="generic_url"
value={genericSettings.url}
onChange={(e) => setGenericSettings((prev) => ({ ...prev, url: e.target.value }))}
placeholder="https://api.example.com/webhook"
className="h-8 text-xs sm:h-10 sm:text-sm"
/>
</div>
<div className="grid grid-cols-1 gap-3 sm:grid-cols-2 sm:gap-4">
<div>
<Label htmlFor="generic_method" className="text-xs sm:text-sm">
HTTP
</Label>
<Select
value={genericSettings.method}
onValueChange={(value) => setGenericSettings((prev) => ({ ...prev, method: value }))}
>
<SelectTrigger className="h-8 text-xs sm:h-10 sm:text-sm">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="GET" className="text-xs sm:text-sm">GET</SelectItem>
<SelectItem value="POST" className="text-xs sm:text-sm">POST</SelectItem>
<SelectItem value="PUT" className="text-xs sm:text-sm">PUT</SelectItem>
<SelectItem value="DELETE" className="text-xs sm:text-sm">DELETE</SelectItem>
</SelectContent>
</Select>
</div>
<div>
<Label htmlFor="generic_timeout" className="text-xs sm:text-sm">
(ms)
</Label>
<Input
id="generic_timeout"
type="number"
value={genericSettings.timeout}
onChange={(e) => setGenericSettings((prev) => ({ ...prev, timeout: e.target.value }))}
placeholder="30000"
className="h-8 text-xs sm:h-10 sm:text-sm"
/>
</div>
</div>
<div>
<Label htmlFor="generic_headers" className="text-xs sm:text-sm">
(JSON)
</Label>
<Textarea
id="generic_headers"
value={genericSettings.headers}
onChange={(e) => setGenericSettings((prev) => ({ ...prev, headers: e.target.value }))}
placeholder='{"Content-Type": "application/json"}'
rows={3}
className="text-xs sm:text-sm"
/>
</div>
</div>
)}
</div>
)}
{/* 다른 호출 타입들 (이메일, FTP, 큐) */}
{formData.call_type !== "rest-api" && (
<div className="rounded-lg border bg-muted/20 p-3 text-center text-xs text-muted-foreground sm:p-4 sm:text-sm">
{formData.call_type} .
</div>
)}
</div>
<DialogFooter className="gap-2 sm:gap-0">
<Button
variant="outline"
onClick={onClose}
disabled={loading}
className="h-8 flex-1 text-xs sm:h-10 sm:flex-none sm:text-sm"
>
</Button>
<Button
onClick={handleSave}
disabled={loading}
className="h-8 flex-1 text-xs sm:h-10 sm:flex-none sm:text-sm"
>
{loading ? "저장 중..." : editingConfig ? "수정" : "생성"}
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
);
}