외부 호출 중간 저장
This commit is contained in:
522
frontend/components/admin/ExternalCallConfigModal.tsx
Normal file
522
frontend/components/admin/ExternalCallConfigModal.tsx
Normal file
@@ -0,0 +1,522 @@
|
||||
"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, 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-h-[90vh] max-w-2xl overflow-y-auto">
|
||||
<DialogHeader>
|
||||
<DialogTitle>{editingConfig ? "외부 호출 설정 편집" : "새 외부 호출 설정"}</DialogTitle>
|
||||
</DialogHeader>
|
||||
|
||||
<div className="space-y-6">
|
||||
{/* 기본 정보 */}
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<Label htmlFor="config_name">설정 이름 *</Label>
|
||||
<Input
|
||||
id="config_name"
|
||||
value={formData.config_name}
|
||||
onChange={(e) => setFormData((prev) => ({ ...prev, config_name: e.target.value }))}
|
||||
placeholder="예: 개발팀 Discord"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Label htmlFor="description">설명</Label>
|
||||
<Textarea
|
||||
id="description"
|
||||
value={formData.description}
|
||||
onChange={(e) => setFormData((prev) => ({ ...prev, description: e.target.value }))}
|
||||
placeholder="이 외부 호출 설정에 대한 설명을 입력하세요."
|
||||
rows={2}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<Label htmlFor="call_type">호출 타입 *</Label>
|
||||
<Select value={formData.call_type} onValueChange={handleCallTypeChange}>
|
||||
<SelectTrigger>
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{CALL_TYPE_OPTIONS.map((option) => (
|
||||
<SelectItem key={option.value} value={option.value}>
|
||||
{option.label}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Label htmlFor="is_active">상태</Label>
|
||||
<Select
|
||||
value={formData.is_active}
|
||||
onValueChange={(value) => setFormData((prev) => ({ ...prev, is_active: value }))}
|
||||
>
|
||||
<SelectTrigger>
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{ACTIVE_STATUS_OPTIONS.map((option) => (
|
||||
<SelectItem key={option.value} value={option.value}>
|
||||
{option.label}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* REST API 설정 */}
|
||||
{formData.call_type === "rest-api" && (
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<Label htmlFor="api_type">API 타입 *</Label>
|
||||
<Select
|
||||
value={formData.api_type}
|
||||
onValueChange={(value) => setFormData((prev) => ({ ...prev, api_type: value }))}
|
||||
>
|
||||
<SelectTrigger>
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{API_TYPE_OPTIONS.map((option) => (
|
||||
<SelectItem key={option.value} value={option.value}>
|
||||
{option.label}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
{/* Discord 설정 */}
|
||||
{formData.api_type === "discord" && (
|
||||
<div className="space-y-3 rounded-lg border p-4">
|
||||
<h4 className="font-medium">Discord 설정</h4>
|
||||
<div>
|
||||
<Label htmlFor="discord_webhook">웹훅 URL *</Label>
|
||||
<Input
|
||||
id="discord_webhook"
|
||||
value={discordSettings.webhookUrl}
|
||||
onChange={(e) => setDiscordSettings((prev) => ({ ...prev, webhookUrl: e.target.value }))}
|
||||
placeholder="https://discord.com/api/webhooks/..."
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<Label htmlFor="discord_username">사용자명</Label>
|
||||
<Input
|
||||
id="discord_username"
|
||||
value={discordSettings.username}
|
||||
onChange={(e) => setDiscordSettings((prev) => ({ ...prev, username: e.target.value }))}
|
||||
placeholder="ERP 시스템"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<Label htmlFor="discord_avatar">아바타 URL</Label>
|
||||
<Input
|
||||
id="discord_avatar"
|
||||
value={discordSettings.avatarUrl}
|
||||
onChange={(e) => setDiscordSettings((prev) => ({ ...prev, avatarUrl: e.target.value }))}
|
||||
placeholder="https://example.com/avatar.png"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Slack 설정 */}
|
||||
{formData.api_type === "slack" && (
|
||||
<div className="space-y-3 rounded-lg border p-4">
|
||||
<h4 className="font-medium">Slack 설정</h4>
|
||||
<div>
|
||||
<Label htmlFor="slack_webhook">웹훅 URL *</Label>
|
||||
<Input
|
||||
id="slack_webhook"
|
||||
value={slackSettings.webhookUrl}
|
||||
onChange={(e) => setSlackSettings((prev) => ({ ...prev, webhookUrl: e.target.value }))}
|
||||
placeholder="https://hooks.slack.com/services/..."
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<Label htmlFor="slack_channel">채널</Label>
|
||||
<Input
|
||||
id="slack_channel"
|
||||
value={slackSettings.channel}
|
||||
onChange={(e) => setSlackSettings((prev) => ({ ...prev, channel: e.target.value }))}
|
||||
placeholder="#general"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<Label htmlFor="slack_username">사용자명</Label>
|
||||
<Input
|
||||
id="slack_username"
|
||||
value={slackSettings.username}
|
||||
onChange={(e) => setSlackSettings((prev) => ({ ...prev, username: e.target.value }))}
|
||||
placeholder="ERP Bot"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 카카오톡 설정 */}
|
||||
{formData.api_type === "kakao-talk" && (
|
||||
<div className="space-y-3 rounded-lg border p-4">
|
||||
<h4 className="font-medium">카카오톡 설정</h4>
|
||||
<div>
|
||||
<Label htmlFor="kakao_token">액세스 토큰 *</Label>
|
||||
<Input
|
||||
id="kakao_token"
|
||||
type="password"
|
||||
value={kakaoSettings.accessToken}
|
||||
onChange={(e) => setKakaoSettings((prev) => ({ ...prev, accessToken: e.target.value }))}
|
||||
placeholder="카카오 API 액세스 토큰"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<Label htmlFor="kakao_template">템플릿 ID</Label>
|
||||
<Input
|
||||
id="kakao_template"
|
||||
value={kakaoSettings.templateId}
|
||||
onChange={(e) => setKakaoSettings((prev) => ({ ...prev, templateId: e.target.value }))}
|
||||
placeholder="template_001"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 일반 API 설정 */}
|
||||
{formData.api_type === "generic" && (
|
||||
<div className="space-y-3 rounded-lg border p-4">
|
||||
<h4 className="font-medium">일반 API 설정</h4>
|
||||
<div>
|
||||
<Label htmlFor="generic_url">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"
|
||||
/>
|
||||
</div>
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<Label htmlFor="generic_method">HTTP 메서드</Label>
|
||||
<Select
|
||||
value={genericSettings.method}
|
||||
onValueChange={(value) => setGenericSettings((prev) => ({ ...prev, method: value }))}
|
||||
>
|
||||
<SelectTrigger>
|
||||
<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="generic_timeout">타임아웃 (ms)</Label>
|
||||
<Input
|
||||
id="generic_timeout"
|
||||
type="number"
|
||||
value={genericSettings.timeout}
|
||||
onChange={(e) => setGenericSettings((prev) => ({ ...prev, timeout: e.target.value }))}
|
||||
placeholder="30000"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<Label htmlFor="generic_headers">헤더 (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}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 다른 호출 타입들 (이메일, FTP, 큐) */}
|
||||
{formData.call_type !== "rest-api" && (
|
||||
<div className="text-muted-foreground rounded-lg border p-4 text-center">
|
||||
{formData.call_type} 타입의 설정은 아직 구현되지 않았습니다.
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<DialogFooter>
|
||||
<Button variant="outline" onClick={onClose} disabled={loading}>
|
||||
취소
|
||||
</Button>
|
||||
<Button onClick={handleSave} disabled={loading}>
|
||||
{loading ? "저장 중..." : editingConfig ? "수정" : "생성"}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user