404 lines
14 KiB
TypeScript
404 lines
14 KiB
TypeScript
"use client";
|
|
|
|
import React, { useState, useEffect } from "react";
|
|
import { Button } from "@/components/ui/button";
|
|
import { Input } from "@/components/ui/input";
|
|
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
|
import { Badge } from "@/components/ui/badge";
|
|
import { Plus, Search, Edit, Trash2, TestTube, Filter } from "lucide-react";
|
|
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table";
|
|
import { toast } from "sonner";
|
|
import {
|
|
ExternalCallConfigAPI,
|
|
ExternalCallConfig,
|
|
ExternalCallConfigFilter,
|
|
CALL_TYPE_OPTIONS,
|
|
API_TYPE_OPTIONS,
|
|
ACTIVE_STATUS_OPTIONS,
|
|
} from "@/lib/api/externalCallConfig";
|
|
import { ExternalCallConfigModal } from "@/components/admin/ExternalCallConfigModal";
|
|
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
|
|
import {
|
|
AlertDialog,
|
|
AlertDialogAction,
|
|
AlertDialogCancel,
|
|
AlertDialogContent,
|
|
AlertDialogDescription,
|
|
AlertDialogFooter,
|
|
AlertDialogHeader,
|
|
AlertDialogTitle,
|
|
} from "@/components/ui/alert-dialog";
|
|
|
|
export default function ExternalCallConfigsPage() {
|
|
const [configs, setConfigs] = useState<ExternalCallConfig[]>([]);
|
|
const [loading, setLoading] = useState(true);
|
|
const [searchQuery, setSearchQuery] = useState("");
|
|
const [filter, setFilter] = useState<ExternalCallConfigFilter>({
|
|
is_active: "Y",
|
|
});
|
|
|
|
// 모달 상태
|
|
const [isModalOpen, setIsModalOpen] = useState(false);
|
|
const [editingConfig, setEditingConfig] = useState<ExternalCallConfig | null>(null);
|
|
|
|
// 삭제 확인 다이얼로그 상태
|
|
const [deleteDialogOpen, setDeleteDialogOpen] = useState(false);
|
|
const [configToDelete, setConfigToDelete] = useState<ExternalCallConfig | null>(null);
|
|
|
|
// 외부 호출 설정 목록 조회
|
|
const fetchConfigs = async () => {
|
|
try {
|
|
setLoading(true);
|
|
const response = await ExternalCallConfigAPI.getConfigs({
|
|
...filter,
|
|
search: searchQuery.trim() || undefined,
|
|
});
|
|
|
|
if (response.success) {
|
|
setConfigs(response.data || []);
|
|
} else {
|
|
toast.error(response.message || "외부 호출 설정 조회 실패");
|
|
}
|
|
} catch (error) {
|
|
console.error("외부 호출 설정 조회 오류:", error);
|
|
toast.error("외부 호출 설정 조회 중 오류가 발생했습니다.");
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
};
|
|
|
|
// 초기 로드 및 필터/검색 변경 시 재조회
|
|
useEffect(() => {
|
|
fetchConfigs();
|
|
}, [filter]);
|
|
|
|
// 검색 실행
|
|
const handleSearch = () => {
|
|
fetchConfigs();
|
|
};
|
|
|
|
// 검색 입력 시 엔터키 처리
|
|
const handleSearchKeyPress = (e: React.KeyboardEvent) => {
|
|
if (e.key === "Enter") {
|
|
handleSearch();
|
|
}
|
|
};
|
|
|
|
// 새 설정 추가
|
|
const handleAddConfig = () => {
|
|
setEditingConfig(null);
|
|
setIsModalOpen(true);
|
|
};
|
|
|
|
// 설정 편집
|
|
const handleEditConfig = (config: ExternalCallConfig) => {
|
|
setEditingConfig(config);
|
|
setIsModalOpen(true);
|
|
};
|
|
|
|
// 설정 삭제 확인
|
|
const handleDeleteConfig = (config: ExternalCallConfig) => {
|
|
setConfigToDelete(config);
|
|
setDeleteDialogOpen(true);
|
|
};
|
|
|
|
// 설정 삭제 실행
|
|
const confirmDeleteConfig = async () => {
|
|
if (!configToDelete?.id) return;
|
|
|
|
try {
|
|
const response = await ExternalCallConfigAPI.deleteConfig(configToDelete.id);
|
|
|
|
if (response.success) {
|
|
toast.success("외부 호출 설정이 삭제되었습니다.");
|
|
fetchConfigs();
|
|
} else {
|
|
toast.error(response.message || "외부 호출 설정 삭제 실패");
|
|
}
|
|
} catch (error) {
|
|
console.error("외부 호출 설정 삭제 오류:", error);
|
|
toast.error("외부 호출 설정 삭제 중 오류가 발생했습니다.");
|
|
} finally {
|
|
setDeleteDialogOpen(false);
|
|
setConfigToDelete(null);
|
|
}
|
|
};
|
|
|
|
// 설정 테스트
|
|
const handleTestConfig = async (config: ExternalCallConfig) => {
|
|
if (!config.id) return;
|
|
|
|
try {
|
|
const response = await ExternalCallConfigAPI.testConfig(config.id);
|
|
|
|
if (response.success && response.data?.success) {
|
|
toast.success(`테스트 성공: ${response.data.message}`);
|
|
} else {
|
|
toast.error(`테스트 실패: ${response.data?.message || response.message}`);
|
|
}
|
|
} catch (error) {
|
|
console.error("외부 호출 설정 테스트 오류:", error);
|
|
toast.error("외부 호출 설정 테스트 중 오류가 발생했습니다.");
|
|
}
|
|
};
|
|
|
|
// 모달 저장 완료 시 목록 새로고침
|
|
const handleModalSave = () => {
|
|
setIsModalOpen(false);
|
|
setEditingConfig(null);
|
|
fetchConfigs();
|
|
};
|
|
|
|
// 호출 타입 라벨 가져오기
|
|
const getCallTypeLabel = (callType: string) => {
|
|
return CALL_TYPE_OPTIONS.find((option) => option.value === callType)?.label || callType;
|
|
};
|
|
|
|
// API 타입 라벨 가져오기
|
|
const getApiTypeLabel = (apiType?: string) => {
|
|
if (!apiType) return "";
|
|
return API_TYPE_OPTIONS.find((option) => option.value === apiType)?.label || apiType;
|
|
};
|
|
|
|
return (
|
|
<div className="min-h-screen bg-gray-50">
|
|
<div className="container mx-auto p-6 space-y-6">
|
|
{/* 페이지 헤더 */}
|
|
<div className="flex items-center justify-between">
|
|
<div>
|
|
<h1 className="text-3xl font-bold">외부 호출 관리</h1>
|
|
<p className="text-muted-foreground mt-1">Discord, Slack, 카카오톡 등 외부 호출 설정을 관리합니다.</p>
|
|
</div>
|
|
<Button onClick={handleAddConfig} className="flex items-center gap-2">
|
|
<Plus size={16} />새 외부 호출 추가
|
|
</Button>
|
|
</div>
|
|
|
|
{/* 검색 및 필터 */}
|
|
<Card>
|
|
<CardHeader>
|
|
<CardTitle className="flex items-center gap-2">
|
|
<Filter size={18} />
|
|
검색 및 필터
|
|
</CardTitle>
|
|
</CardHeader>
|
|
<CardContent className="space-y-4">
|
|
{/* 검색 */}
|
|
<div className="flex gap-2">
|
|
<div className="flex-1">
|
|
<Input
|
|
placeholder="설정 이름 또는 설명으로 검색..."
|
|
value={searchQuery}
|
|
onChange={(e) => setSearchQuery(e.target.value)}
|
|
onKeyPress={handleSearchKeyPress}
|
|
/>
|
|
</div>
|
|
<Button onClick={handleSearch} variant="outline">
|
|
<Search size={16} />
|
|
</Button>
|
|
</div>
|
|
|
|
{/* 필터 */}
|
|
<div className="grid grid-cols-1 gap-4 md:grid-cols-3">
|
|
<div>
|
|
<label className="mb-1 block text-sm font-medium">호출 타입</label>
|
|
<Select
|
|
value={filter.call_type || "all"}
|
|
onValueChange={(value) =>
|
|
setFilter((prev) => ({
|
|
...prev,
|
|
call_type: value === "all" ? undefined : value,
|
|
}))
|
|
}
|
|
>
|
|
<SelectTrigger>
|
|
<SelectValue />
|
|
</SelectTrigger>
|
|
<SelectContent>
|
|
<SelectItem value="all">전체</SelectItem>
|
|
{CALL_TYPE_OPTIONS.map((option) => (
|
|
<SelectItem key={option.value} value={option.value}>
|
|
{option.label}
|
|
</SelectItem>
|
|
))}
|
|
</SelectContent>
|
|
</Select>
|
|
</div>
|
|
|
|
<div>
|
|
<label className="mb-1 block text-sm font-medium">API 타입</label>
|
|
<Select
|
|
value={filter.api_type || "all"}
|
|
onValueChange={(value) =>
|
|
setFilter((prev) => ({
|
|
...prev,
|
|
api_type: value === "all" ? undefined : value,
|
|
}))
|
|
}
|
|
>
|
|
<SelectTrigger>
|
|
<SelectValue />
|
|
</SelectTrigger>
|
|
<SelectContent>
|
|
<SelectItem value="all">전체</SelectItem>
|
|
{API_TYPE_OPTIONS.map((option) => (
|
|
<SelectItem key={option.value} value={option.value}>
|
|
{option.label}
|
|
</SelectItem>
|
|
))}
|
|
</SelectContent>
|
|
</Select>
|
|
</div>
|
|
|
|
<div>
|
|
<label className="mb-1 block text-sm font-medium">상태</label>
|
|
<Select
|
|
value={filter.is_active || "Y"}
|
|
onValueChange={(value) =>
|
|
setFilter((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>
|
|
</CardContent>
|
|
</Card>
|
|
|
|
{/* 설정 목록 */}
|
|
<Card>
|
|
<CardHeader>
|
|
<CardTitle>외부 호출 설정 목록</CardTitle>
|
|
</CardHeader>
|
|
<CardContent>
|
|
{loading ? (
|
|
// 로딩 상태
|
|
<div className="py-8 text-center">
|
|
<div className="text-muted-foreground">로딩 중...</div>
|
|
</div>
|
|
) : configs.length === 0 ? (
|
|
// 빈 상태
|
|
<div className="py-12 text-center">
|
|
<div className="text-muted-foreground">
|
|
<Plus size={48} className="mx-auto mb-4 opacity-20" />
|
|
<p className="text-lg font-medium">등록된 외부 호출 설정이 없습니다.</p>
|
|
<p className="text-sm">새 외부 호출을 추가해보세요.</p>
|
|
</div>
|
|
</div>
|
|
) : (
|
|
// 설정 테이블 목록
|
|
<Table>
|
|
<TableHeader>
|
|
<TableRow>
|
|
<TableHead>설정명</TableHead>
|
|
<TableHead>호출 타입</TableHead>
|
|
<TableHead>API 타입</TableHead>
|
|
<TableHead>설명</TableHead>
|
|
<TableHead>상태</TableHead>
|
|
<TableHead>생성일</TableHead>
|
|
<TableHead className="text-center">작업</TableHead>
|
|
</TableRow>
|
|
</TableHeader>
|
|
<TableBody>
|
|
{configs.map((config) => (
|
|
<TableRow key={config.id} className="hover:bg-muted/50">
|
|
<TableCell className="font-medium">{config.config_name}</TableCell>
|
|
<TableCell>
|
|
<Badge variant="outline">{getCallTypeLabel(config.call_type)}</Badge>
|
|
</TableCell>
|
|
<TableCell>
|
|
{config.api_type ? (
|
|
<Badge variant="secondary">{getApiTypeLabel(config.api_type)}</Badge>
|
|
) : (
|
|
<span className="text-muted-foreground text-sm">-</span>
|
|
)}
|
|
</TableCell>
|
|
<TableCell>
|
|
<div className="max-w-xs">
|
|
{config.description ? (
|
|
<span className="text-muted-foreground block truncate text-sm" title={config.description}>
|
|
{config.description}
|
|
</span>
|
|
) : (
|
|
<span className="text-muted-foreground text-sm">-</span>
|
|
)}
|
|
</div>
|
|
</TableCell>
|
|
<TableCell>
|
|
<Badge variant={config.is_active === "Y" ? "default" : "destructive"}>
|
|
{config.is_active === "Y" ? "활성" : "비활성"}
|
|
</Badge>
|
|
</TableCell>
|
|
<TableCell className="text-muted-foreground text-sm">
|
|
{config.created_date ? new Date(config.created_date).toLocaleDateString() : "-"}
|
|
</TableCell>
|
|
<TableCell>
|
|
<div className="flex justify-center gap-1">
|
|
<Button size="sm" variant="outline" onClick={() => handleTestConfig(config)} title="테스트">
|
|
<TestTube size={14} />
|
|
</Button>
|
|
<Button size="sm" variant="outline" onClick={() => handleEditConfig(config)} title="편집">
|
|
<Edit size={14} />
|
|
</Button>
|
|
<Button
|
|
size="sm"
|
|
variant="outline"
|
|
onClick={() => handleDeleteConfig(config)}
|
|
className="text-destructive hover:text-destructive"
|
|
title="삭제"
|
|
>
|
|
<Trash2 size={14} />
|
|
</Button>
|
|
</div>
|
|
</TableCell>
|
|
</TableRow>
|
|
))}
|
|
</TableBody>
|
|
</Table>
|
|
)}
|
|
</CardContent>
|
|
</Card>
|
|
|
|
{/* 외부 호출 설정 모달 */}
|
|
<ExternalCallConfigModal
|
|
isOpen={isModalOpen}
|
|
onClose={() => setIsModalOpen(false)}
|
|
onSave={handleModalSave}
|
|
editingConfig={editingConfig}
|
|
/>
|
|
|
|
{/* 삭제 확인 다이얼로그 */}
|
|
<AlertDialog open={deleteDialogOpen} onOpenChange={setDeleteDialogOpen}>
|
|
<AlertDialogContent>
|
|
<AlertDialogHeader>
|
|
<AlertDialogTitle>외부 호출 설정 삭제</AlertDialogTitle>
|
|
<AlertDialogDescription>
|
|
"{configToDelete?.config_name}" 설정을 삭제하시겠습니까?
|
|
<br />이 작업은 되돌릴 수 없습니다.
|
|
</AlertDialogDescription>
|
|
</AlertDialogHeader>
|
|
<AlertDialogFooter>
|
|
<AlertDialogCancel>취소</AlertDialogCancel>
|
|
<AlertDialogAction onClick={confirmDeleteConfig} className="bg-destructive hover:bg-destructive/90">
|
|
삭제
|
|
</AlertDialogAction>
|
|
</AlertDialogFooter>
|
|
</AlertDialogContent>
|
|
</AlertDialog>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|