ERP-node/frontend/app/(main)/admin/automaticMng/exCallConfList/page.tsx

404 lines
15 KiB
TypeScript
Raw Normal View History

2025-09-17 17:14:59 +09:00
"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 (
2025-10-22 14:52:13 +09:00
<div className="flex min-h-screen flex-col bg-background">
<div className="space-y-6 p-6">
{/* 페이지 헤더 */}
<div className="space-y-2 border-b pb-4">
<h1 className="text-3xl font-bold tracking-tight"> </h1>
<p className="text-sm text-muted-foreground">Discord, Slack, .</p>
2025-09-17 17:14:59 +09:00
</div>
2025-10-22 14:52:13 +09:00
{/* 검색 및 필터 영역 */}
<div className="space-y-4">
{/* 첫 번째 줄: 검색 + 추가 버튼 */}
<div className="flex flex-col gap-4 sm:flex-row sm:items-center sm:justify-between">
<div className="flex flex-col gap-3 sm:flex-row sm:items-center">
<div className="w-full sm:w-[320px]">
<div className="relative">
<Search className="absolute left-3 top-1/2 h-4 w-4 -translate-y-1/2 text-muted-foreground" />
<Input
placeholder="설정 이름 또는 설명으로 검색..."
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
onKeyPress={handleSearchKeyPress}
className="h-10 pl-10 text-sm"
/>
</div>
</div>
<Button onClick={handleSearch} variant="outline" className="h-10 gap-2 text-sm font-medium">
<Search className="h-4 w-4" />
</Button>
2025-09-17 17:14:59 +09:00
</div>
2025-10-22 14:52:13 +09:00
<Button onClick={handleAddConfig} className="h-10 gap-2 text-sm font-medium">
<Plus className="h-4 w-4" />
2025-09-17 17:14:59 +09:00
</Button>
</div>
2025-10-22 14:52:13 +09:00
{/* 두 번째 줄: 필터 */}
<div className="grid grid-cols-1 gap-3 sm:grid-cols-3">
<Select
value={filter.call_type || "all"}
onValueChange={(value) =>
setFilter((prev) => ({
...prev,
call_type: value === "all" ? undefined : value,
}))
}
>
<SelectTrigger className="h-10">
<SelectValue placeholder="호출 타입" />
</SelectTrigger>
<SelectContent>
<SelectItem value="all"></SelectItem>
{CALL_TYPE_OPTIONS.map((option) => (
<SelectItem key={option.value} value={option.value}>
{option.label}
</SelectItem>
))}
</SelectContent>
</Select>
2025-09-17 17:14:59 +09:00
2025-10-22 14:52:13 +09:00
<Select
value={filter.api_type || "all"}
onValueChange={(value) =>
setFilter((prev) => ({
...prev,
api_type: value === "all" ? undefined : value,
}))
}
>
<SelectTrigger className="h-10">
<SelectValue placeholder="API 타입" />
</SelectTrigger>
<SelectContent>
<SelectItem value="all"></SelectItem>
{API_TYPE_OPTIONS.map((option) => (
<SelectItem key={option.value} value={option.value}>
{option.label}
</SelectItem>
))}
</SelectContent>
</Select>
2025-09-17 17:14:59 +09:00
2025-10-22 14:52:13 +09:00
<Select
value={filter.is_active || "Y"}
onValueChange={(value) =>
setFilter((prev) => ({
...prev,
is_active: value,
}))
}
>
<SelectTrigger className="h-10">
<SelectValue placeholder="상태" />
</SelectTrigger>
<SelectContent>
{ACTIVE_STATUS_OPTIONS.map((option) => (
<SelectItem key={option.value} value={option.value}>
{option.label}
</SelectItem>
))}
</SelectContent>
</Select>
2025-09-17 17:14:59 +09:00
</div>
2025-10-22 14:52:13 +09:00
</div>
2025-09-17 17:14:59 +09:00
2025-10-22 14:52:13 +09:00
{/* 설정 목록 */}
<div className="rounded-lg border bg-card shadow-sm">
2025-09-17 17:14:59 +09:00
{loading ? (
// 로딩 상태
2025-10-22 14:52:13 +09:00
<div className="flex h-64 items-center justify-center">
<div className="text-sm text-muted-foreground"> ...</div>
2025-09-17 17:14:59 +09:00
</div>
) : configs.length === 0 ? (
// 빈 상태
2025-10-22 14:52:13 +09:00
<div className="flex h-64 flex-col items-center justify-center">
<div className="flex flex-col items-center gap-2 text-center">
<p className="text-sm text-muted-foreground"> .</p>
<p className="text-xs text-muted-foreground"> .</p>
2025-09-17 17:14:59 +09:00
</div>
</div>
) : (
// 설정 테이블 목록
<Table>
<TableHeader>
2025-10-22 14:52:13 +09:00
<TableRow className="border-b bg-muted/50 hover:bg-muted/50">
<TableHead className="h-12 text-sm font-semibold"></TableHead>
<TableHead className="h-12 text-sm font-semibold"> </TableHead>
<TableHead className="h-12 text-sm font-semibold">API </TableHead>
<TableHead className="h-12 text-sm font-semibold"></TableHead>
<TableHead className="h-12 text-sm font-semibold"></TableHead>
<TableHead className="h-12 text-sm font-semibold"></TableHead>
<TableHead className="h-12 text-center text-sm font-semibold"></TableHead>
2025-09-17 17:14:59 +09:00
</TableRow>
</TableHeader>
<TableBody>
{configs.map((config) => (
2025-10-22 14:52:13 +09:00
<TableRow key={config.id} className="border-b transition-colors hover:bg-muted/50">
<TableCell className="h-16 text-sm font-medium">{config.config_name}</TableCell>
<TableCell className="h-16 text-sm">
2025-09-17 17:14:59 +09:00
<Badge variant="outline">{getCallTypeLabel(config.call_type)}</Badge>
</TableCell>
2025-10-22 14:52:13 +09:00
<TableCell className="h-16 text-sm">
2025-09-17 17:14:59 +09:00
{config.api_type ? (
<Badge variant="secondary">{getApiTypeLabel(config.api_type)}</Badge>
) : (
2025-10-22 14:52:13 +09:00
<span className="text-muted-foreground">-</span>
2025-09-17 17:14:59 +09:00
)}
</TableCell>
2025-10-22 14:52:13 +09:00
<TableCell className="h-16 text-sm">
2025-09-17 17:14:59 +09:00
<div className="max-w-xs">
{config.description ? (
2025-10-22 14:52:13 +09:00
<span className="block truncate text-muted-foreground" title={config.description}>
2025-09-17 17:14:59 +09:00
{config.description}
</span>
) : (
2025-10-22 14:52:13 +09:00
<span className="text-muted-foreground">-</span>
2025-09-17 17:14:59 +09:00
)}
</div>
</TableCell>
2025-10-22 14:52:13 +09:00
<TableCell className="h-16 text-sm">
2025-09-17 17:14:59 +09:00
<Badge variant={config.is_active === "Y" ? "default" : "destructive"}>
{config.is_active === "Y" ? "활성" : "비활성"}
</Badge>
</TableCell>
2025-10-22 14:52:13 +09:00
<TableCell className="h-16 text-sm text-muted-foreground">
2025-09-17 17:14:59 +09:00
{config.created_date ? new Date(config.created_date).toLocaleDateString() : "-"}
</TableCell>
2025-10-22 14:52:13 +09:00
<TableCell className="h-16 text-sm">
2025-09-17 17:14:59 +09:00
<div className="flex justify-center gap-1">
2025-10-22 14:52:13 +09:00
<Button
variant="ghost"
size="icon"
className="h-8 w-8"
onClick={() => handleTestConfig(config)}
title="테스트"
>
<TestTube className="h-4 w-4" />
2025-09-17 17:14:59 +09:00
</Button>
2025-10-22 14:52:13 +09:00
<Button
variant="ghost"
size="icon"
className="h-8 w-8"
onClick={() => handleEditConfig(config)}
title="편집"
>
<Edit className="h-4 w-4" />
2025-09-17 17:14:59 +09:00
</Button>
<Button
2025-10-22 14:52:13 +09:00
variant="ghost"
size="icon"
className="h-8 w-8 text-destructive hover:text-destructive"
2025-09-17 17:14:59 +09:00
onClick={() => handleDeleteConfig(config)}
title="삭제"
>
2025-10-22 14:52:13 +09:00
<Trash2 className="h-4 w-4" />
2025-09-17 17:14:59 +09:00
</Button>
</div>
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
)}
2025-10-22 14:52:13 +09:00
</div>
2025-09-17 17:14:59 +09:00
{/* 외부 호출 설정 모달 */}
<ExternalCallConfigModal
isOpen={isModalOpen}
onClose={() => setIsModalOpen(false)}
onSave={handleModalSave}
editingConfig={editingConfig}
/>
{/* 삭제 확인 다이얼로그 */}
<AlertDialog open={deleteDialogOpen} onOpenChange={setDeleteDialogOpen}>
2025-10-22 14:52:13 +09:00
<AlertDialogContent className="max-w-[95vw] sm:max-w-[500px]">
2025-09-17 17:14:59 +09:00
<AlertDialogHeader>
2025-10-22 14:52:13 +09:00
<AlertDialogTitle className="text-base sm:text-lg"> </AlertDialogTitle>
<AlertDialogDescription className="text-xs sm:text-sm">
2025-09-17 17:14:59 +09:00
"{configToDelete?.config_name}" ?
<br /> .
</AlertDialogDescription>
</AlertDialogHeader>
2025-10-22 14:52:13 +09:00
<AlertDialogFooter className="gap-2 sm:gap-0">
<AlertDialogCancel className="h-8 flex-1 text-xs sm:h-10 sm:flex-none sm:text-sm">
</AlertDialogCancel>
<AlertDialogAction
onClick={confirmDeleteConfig}
className="h-8 flex-1 bg-destructive text-xs hover:bg-destructive/90 sm:h-10 sm:flex-none sm:text-sm"
>
2025-09-17 17:14:59 +09:00
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
</div>
2025-09-17 17:14:59 +09:00
</div>
);
}