f04d224b09
- Replaced existing toast error messages with the new `showErrorToast` utility across multiple components, improving consistency in error reporting. - Updated error messages to provide more specific guidance for users, enhancing the overall user experience during error scenarios. - Ensured that all relevant error handling in batch management, external call configurations, cascading management, and screen management components now utilizes the new utility for better maintainability.
341 lines
12 KiB
TypeScript
341 lines
12 KiB
TypeScript
"use client";
|
|
|
|
import React, { useState, useEffect } from "react";
|
|
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
|
import { Button } from "@/components/ui/button";
|
|
import { Input } from "@/components/ui/input";
|
|
import { Badge } from "@/components/ui/badge";
|
|
import {
|
|
Select,
|
|
SelectContent,
|
|
SelectItem,
|
|
SelectTrigger,
|
|
SelectValue,
|
|
} from "@/components/ui/select";
|
|
import {
|
|
Table,
|
|
TableBody,
|
|
TableCell,
|
|
TableHead,
|
|
TableHeader,
|
|
TableRow,
|
|
} from "@/components/ui/table";
|
|
import {
|
|
DropdownMenu,
|
|
DropdownMenuContent,
|
|
DropdownMenuItem,
|
|
DropdownMenuTrigger,
|
|
} from "@/components/ui/dropdown-menu";
|
|
import {
|
|
Plus,
|
|
Search,
|
|
MoreHorizontal,
|
|
Edit,
|
|
Trash2,
|
|
Play,
|
|
History,
|
|
RefreshCw
|
|
} from "lucide-react";
|
|
import { toast } from "sonner";
|
|
import { showErrorToast } from "@/lib/utils/toastUtils";
|
|
import { CollectionAPI, DataCollectionConfig } from "@/lib/api/collection";
|
|
import CollectionConfigModal from "@/components/admin/CollectionConfigModal";
|
|
|
|
export default function CollectionManagementPage() {
|
|
const [configs, setConfigs] = useState<DataCollectionConfig[]>([]);
|
|
const [filteredConfigs, setFilteredConfigs] = useState<DataCollectionConfig[]>([]);
|
|
const [isLoading, setIsLoading] = useState(false);
|
|
const [searchTerm, setSearchTerm] = useState("");
|
|
const [statusFilter, setStatusFilter] = useState("all");
|
|
const [typeFilter, setTypeFilter] = useState("all");
|
|
|
|
// 모달 상태
|
|
const [isModalOpen, setIsModalOpen] = useState(false);
|
|
const [selectedConfig, setSelectedConfig] = useState<DataCollectionConfig | null>(null);
|
|
|
|
const collectionTypeOptions = CollectionAPI.getCollectionTypeOptions();
|
|
|
|
useEffect(() => {
|
|
loadConfigs();
|
|
}, []);
|
|
|
|
useEffect(() => {
|
|
filterConfigs();
|
|
}, [configs, searchTerm, statusFilter, typeFilter]);
|
|
|
|
const loadConfigs = async () => {
|
|
setIsLoading(true);
|
|
try {
|
|
const data = await CollectionAPI.getCollectionConfigs();
|
|
setConfigs(data);
|
|
} catch (error) {
|
|
console.error("수집 설정 목록 조회 오류:", error);
|
|
showErrorToast("수집 설정 목록을 불러오는 데 실패했습니다", error, { guidance: "네트워크 연결을 확인해 주세요." });
|
|
} finally {
|
|
setIsLoading(false);
|
|
}
|
|
};
|
|
|
|
const filterConfigs = () => {
|
|
let filtered = configs;
|
|
|
|
// 검색어 필터
|
|
if (searchTerm) {
|
|
filtered = filtered.filter(config =>
|
|
config.config_name.toLowerCase().includes(searchTerm.toLowerCase()) ||
|
|
config.source_table.toLowerCase().includes(searchTerm.toLowerCase()) ||
|
|
config.description?.toLowerCase().includes(searchTerm.toLowerCase())
|
|
);
|
|
}
|
|
|
|
// 상태 필터
|
|
if (statusFilter !== "all") {
|
|
filtered = filtered.filter(config => config.is_active === statusFilter);
|
|
}
|
|
|
|
// 타입 필터
|
|
if (typeFilter !== "all") {
|
|
filtered = filtered.filter(config => config.collection_type === typeFilter);
|
|
}
|
|
|
|
setFilteredConfigs(filtered);
|
|
};
|
|
|
|
const handleCreate = () => {
|
|
setSelectedConfig(null);
|
|
setIsModalOpen(true);
|
|
};
|
|
|
|
const handleEdit = (config: DataCollectionConfig) => {
|
|
setSelectedConfig(config);
|
|
setIsModalOpen(true);
|
|
};
|
|
|
|
const handleDelete = async (config: DataCollectionConfig) => {
|
|
if (!confirm(`"${config.config_name}" 수집 설정을 삭제하시겠습니까?`)) {
|
|
return;
|
|
}
|
|
|
|
try {
|
|
await CollectionAPI.deleteCollectionConfig(config.id!);
|
|
toast.success("수집 설정이 삭제되었습니다.");
|
|
loadConfigs();
|
|
} catch (error) {
|
|
console.error("수집 설정 삭제 오류:", error);
|
|
toast.error("수집 설정 삭제에 실패했습니다.");
|
|
}
|
|
};
|
|
|
|
const handleExecute = async (config: DataCollectionConfig) => {
|
|
try {
|
|
await CollectionAPI.executeCollection(config.id!);
|
|
toast.success(`"${config.config_name}" 수집 작업을 시작했습니다.`);
|
|
} catch (error) {
|
|
console.error("수집 작업 실행 오류:", error);
|
|
showErrorToast("수집 작업 실행에 실패했습니다", error, { guidance: "수집 설정을 확인해 주세요." });
|
|
}
|
|
};
|
|
|
|
const handleModalSave = () => {
|
|
loadConfigs();
|
|
};
|
|
|
|
const getStatusBadge = (isActive: string) => {
|
|
return isActive === "Y" ? (
|
|
<Badge className="bg-green-100 text-green-800">활성</Badge>
|
|
) : (
|
|
<Badge className="bg-red-100 text-red-800">비활성</Badge>
|
|
);
|
|
};
|
|
|
|
const getTypeBadge = (type: string) => {
|
|
const option = collectionTypeOptions.find(opt => opt.value === type);
|
|
const colors = {
|
|
full: "bg-blue-100 text-blue-800",
|
|
incremental: "bg-purple-100 text-purple-800",
|
|
delta: "bg-orange-100 text-orange-800",
|
|
};
|
|
return (
|
|
<Badge className={colors[type as keyof typeof colors] || "bg-gray-100 text-gray-800"}>
|
|
{option?.label || type}
|
|
</Badge>
|
|
);
|
|
};
|
|
|
|
return (
|
|
<div className="min-h-screen bg-gray-50">
|
|
<div className="w-full max-w-none px-4 py-8 space-y-8">
|
|
{/* 헤더 */}
|
|
<div className="flex items-center justify-between">
|
|
<div>
|
|
<h1 className="text-2xl font-bold">수집 관리</h1>
|
|
<p className="text-muted-foreground">
|
|
외부 데이터베이스에서 데이터를 수집하는 설정을 관리합니다.
|
|
</p>
|
|
</div>
|
|
<Button onClick={handleCreate}>
|
|
<Plus className="h-4 w-4 mr-2" />
|
|
새 수집 설정
|
|
</Button>
|
|
</div>
|
|
|
|
{/* 필터 및 검색 */}
|
|
<Card>
|
|
<CardHeader>
|
|
<CardTitle>필터 및 검색</CardTitle>
|
|
</CardHeader>
|
|
<CardContent>
|
|
<div className="flex flex-col md:flex-row gap-4">
|
|
<div className="flex-1">
|
|
<div className="relative">
|
|
<Search className="absolute left-3 top-3 h-4 w-4 text-muted-foreground" />
|
|
<Input
|
|
placeholder="설정명, 테이블명, 설명으로 검색..."
|
|
value={searchTerm}
|
|
onChange={(e) => setSearchTerm(e.target.value)}
|
|
className="pl-10"
|
|
/>
|
|
</div>
|
|
</div>
|
|
|
|
<Select value={statusFilter} onValueChange={setStatusFilter}>
|
|
<SelectTrigger className="w-32">
|
|
<SelectValue placeholder="상태" />
|
|
</SelectTrigger>
|
|
<SelectContent>
|
|
<SelectItem value="all">전체</SelectItem>
|
|
<SelectItem value="Y">활성</SelectItem>
|
|
<SelectItem value="N">비활성</SelectItem>
|
|
</SelectContent>
|
|
</Select>
|
|
|
|
<Select value={typeFilter} onValueChange={setTypeFilter}>
|
|
<SelectTrigger className="w-40">
|
|
<SelectValue placeholder="수집 타입" />
|
|
</SelectTrigger>
|
|
<SelectContent>
|
|
<SelectItem value="all">전체 타입</SelectItem>
|
|
{collectionTypeOptions.map((option) => (
|
|
<SelectItem key={option.value} value={option.value}>
|
|
{option.label}
|
|
</SelectItem>
|
|
))}
|
|
</SelectContent>
|
|
</Select>
|
|
|
|
<Button variant="outline" onClick={loadConfigs} disabled={isLoading}>
|
|
<RefreshCw className={`h-4 w-4 mr-2 ${isLoading ? 'animate-spin' : ''}`} />
|
|
새로고침
|
|
</Button>
|
|
</div>
|
|
</CardContent>
|
|
</Card>
|
|
|
|
{/* 수집 설정 목록 */}
|
|
<Card>
|
|
<CardHeader>
|
|
<CardTitle>수집 설정 목록 ({filteredConfigs.length}개)</CardTitle>
|
|
</CardHeader>
|
|
<CardContent>
|
|
{isLoading ? (
|
|
<div className="text-center py-8">
|
|
<RefreshCw className="h-8 w-8 animate-spin mx-auto mb-2" />
|
|
<p>수집 설정을 불러오는 중...</p>
|
|
</div>
|
|
) : filteredConfigs.length === 0 ? (
|
|
<div className="text-center py-8 text-muted-foreground">
|
|
{configs.length === 0 ? "수집 설정이 없습니다." : "검색 결과가 없습니다."}
|
|
</div>
|
|
) : (
|
|
<Table>
|
|
<TableHeader>
|
|
<TableRow>
|
|
<TableHead className="h-12 px-6 py-3 text-sm font-semibold">설정명</TableHead>
|
|
<TableHead className="h-12 px-6 py-3 text-sm font-semibold">수집 타입</TableHead>
|
|
<TableHead className="h-12 px-6 py-3 text-sm font-semibold">소스 테이블</TableHead>
|
|
<TableHead className="h-12 px-6 py-3 text-sm font-semibold">대상 테이블</TableHead>
|
|
<TableHead className="h-12 px-6 py-3 text-sm font-semibold">스케줄</TableHead>
|
|
<TableHead className="h-12 px-6 py-3 text-sm font-semibold">상태</TableHead>
|
|
<TableHead className="h-12 px-6 py-3 text-sm font-semibold">마지막 수집</TableHead>
|
|
<TableHead className="h-12 px-6 py-3 text-sm font-semibold">작업</TableHead>
|
|
</TableRow>
|
|
</TableHeader>
|
|
<TableBody>
|
|
{filteredConfigs.map((config) => (
|
|
<TableRow key={config.id} className="bg-background transition-colors hover:bg-muted/50">
|
|
<TableCell className="h-16 px-6 py-3 text-sm">
|
|
<div>
|
|
<div className="font-medium">{config.config_name}</div>
|
|
{config.description && (
|
|
<div className="text-sm text-muted-foreground">
|
|
{config.description}
|
|
</div>
|
|
)}
|
|
</div>
|
|
</TableCell>
|
|
<TableCell className="h-16 px-6 py-3 text-sm">
|
|
{getTypeBadge(config.collection_type)}
|
|
</TableCell>
|
|
<TableCell className="h-16 px-6 py-3 font-mono text-sm">
|
|
{config.source_table}
|
|
</TableCell>
|
|
<TableCell className="h-16 px-6 py-3 font-mono text-sm">
|
|
{config.target_table || "-"}
|
|
</TableCell>
|
|
<TableCell className="h-16 px-6 py-3 font-mono text-sm">
|
|
{config.schedule_cron || "-"}
|
|
</TableCell>
|
|
<TableCell className="h-16 px-6 py-3 text-sm">
|
|
{getStatusBadge(config.is_active)}
|
|
</TableCell>
|
|
<TableCell className="h-16 px-6 py-3 text-sm">
|
|
{config.last_collected_at
|
|
? new Date(config.last_collected_at).toLocaleString()
|
|
: "-"}
|
|
</TableCell>
|
|
<TableCell className="h-16 px-6 py-3 text-sm">
|
|
<DropdownMenu>
|
|
<DropdownMenuTrigger asChild>
|
|
<Button variant="ghost" className="h-8 w-8 p-0">
|
|
<MoreHorizontal className="h-4 w-4" />
|
|
</Button>
|
|
</DropdownMenuTrigger>
|
|
<DropdownMenuContent align="end">
|
|
<DropdownMenuItem onClick={() => handleEdit(config)}>
|
|
<Edit className="h-4 w-4 mr-2" />
|
|
수정
|
|
</DropdownMenuItem>
|
|
<DropdownMenuItem
|
|
onClick={() => handleExecute(config)}
|
|
disabled={config.is_active !== "Y"}
|
|
>
|
|
<Play className="h-4 w-4 mr-2" />
|
|
실행
|
|
</DropdownMenuItem>
|
|
<DropdownMenuItem onClick={() => handleDelete(config)}>
|
|
<Trash2 className="h-4 w-4 mr-2" />
|
|
삭제
|
|
</DropdownMenuItem>
|
|
</DropdownMenuContent>
|
|
</DropdownMenu>
|
|
</TableCell>
|
|
</TableRow>
|
|
))}
|
|
</TableBody>
|
|
</Table>
|
|
)}
|
|
</CardContent>
|
|
</Card>
|
|
|
|
{/* 수집 설정 모달 */}
|
|
<CollectionConfigModal
|
|
isOpen={isModalOpen}
|
|
onClose={() => setIsModalOpen(false)}
|
|
onSave={handleModalSave}
|
|
config={selectedConfig}
|
|
/>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|