"use client"; import React, { useState, useEffect, useCallback, useMemo } from "react"; import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; import { Plus, Search, RefreshCw, Database, LayoutGrid, CheckCircle, Activity, AlertTriangle, } from "lucide-react"; import { toast } from "sonner"; import { showErrorToast } from "@/lib/utils/toastUtils"; import { useRouter } from "next/navigation"; import { BatchAPI, type BatchConfig } from "@/lib/api/batch"; import apiClient from "@/lib/api/client"; import { ScrollToTop } from "@/components/common/ScrollToTop"; import { cn } from "@/lib/utils"; import BatchCard from "@/components/admin/BatchCard"; // 대시보드 통계 타입 (백엔드 GET /batch-management/stats) interface BatchStats { totalBatches: number; activeBatches: number; todayExecutions: number; todayFailures: number; prevDayExecutions?: number; prevDayFailures?: number; } // 스파크라인/최근 로그 타입은 BatchCard 내부 정의 사용 /** Cron 표현식 → 한글 설명 */ function cronToKorean(cron: string): string { if (!cron || !cron.trim()) return "-"; const parts = cron.trim().split(/\s+/); if (parts.length < 5) return cron; const [min, hour, dayOfMonth, month, dayOfWeek] = parts; // 매 30분: */30 * * * * if (min.startsWith("*/") && hour === "*" && dayOfMonth === "*" && month === "*" && dayOfWeek === "*") { const n = min.slice(2); return `매 ${n}분`; } // 매 N시간: 0 */2 * * * if (min === "0" && hour.startsWith("*/") && dayOfMonth === "*" && month === "*" && dayOfWeek === "*") { const n = hour.slice(2); return `매 ${n}시간`; } // 매일 HH:MM: 0 1 * * * 또는 0 6,18 * * * if (min === "0" && dayOfMonth === "*" && month === "*" && dayOfWeek === "*") { if (hour.includes(",")) { const times = hour.split(",").map((h) => `${h.padStart(2, "0")}:00`); return times.join(", "); } return `매일 ${hour.padStart(2, "0")}:00`; } // 매주 일 03:00: 0 3 * * 0 if (min === "0" && dayOfMonth === "*" && month === "*" && dayOfWeek !== "*" && dayOfWeek !== "?") { const dayNames = ["일", "월", "화", "수", "목", "금", "토"]; const d = dayNames[parseInt(dayOfWeek, 10)] ?? dayOfWeek; return `매주 ${d} ${hour.padStart(2, "0")}:00`; } // 매월 1일 00:00: 0 0 1 * * if (min === "0" && hour === "0" && dayOfMonth !== "*" && month === "*" && dayOfWeek === "*") { return `매월 ${dayOfMonth}일 00:00`; } return cron; } type StatusFilter = "all" | "active" | "inactive"; type TypeFilter = "all" | "mapping" | "restapi" | "node_flow"; export default function BatchManagementPage() { const router = useRouter(); const [batchConfigs, setBatchConfigs] = useState([]); const [loading, setLoading] = useState(false); const [stats, setStats] = useState(null); const [statsLoading, setStatsLoading] = useState(false); const [searchTerm, setSearchTerm] = useState(""); const [statusFilter, setStatusFilter] = useState("all"); const [typeFilter, setTypeFilter] = useState("all"); const [executingBatch, setExecutingBatch] = useState(null); const [expandedId, setExpandedId] = useState(null); const [isBatchTypeModalOpen, setIsBatchTypeModalOpen] = useState(false); const loadBatchConfigs = useCallback(async () => { setLoading(true); try { const response = await BatchAPI.getBatchConfigs({ limit: 500, search: searchTerm || undefined, }); if (response.success && response.data) { setBatchConfigs(response.data); } else { setBatchConfigs([]); } } catch (error) { console.error("배치 목록 조회 실패:", error); toast.error("배치 목록을 불러오는데 실패했습니다."); setBatchConfigs([]); } finally { setLoading(false); } }, [searchTerm]); const loadStats = useCallback(async () => { setStatsLoading(true); try { const res = await apiClient.get<{ success: boolean; data?: BatchStats }>("/batch-management/stats"); if (res.data?.success && res.data.data) { setStats(res.data.data); } else { setStats(null); } } catch { setStats(null); } finally { setStatsLoading(false); } }, []); useEffect(() => { loadBatchConfigs(); }, [loadBatchConfigs]); useEffect(() => { loadStats(); }, [loadStats]); const executeBatch = async (batchId: number) => { setExecutingBatch(batchId); try { const response = await BatchAPI.executeBatchConfig(batchId); if (response.success) { toast.success( `배치가 성공적으로 실행되었습니다! (처리: ${response.data?.totalRecords ?? 0}개, 성공: ${response.data?.successRecords ?? 0}개)` ); loadBatchConfigs(); loadStats(); } else { toast.error("배치 실행에 실패했습니다."); } } catch (error) { showErrorToast("배치 실행에 실패했습니다", error, { guidance: "배치 설정을 확인하고 다시 시도해 주세요.", }); } finally { setExecutingBatch(null); } }; const toggleBatchStatus = async (batchId: number, currentStatus: string) => { try { const newStatus = currentStatus === "Y" ? "N" : "Y"; await BatchAPI.updateBatchConfig(batchId, { isActive: newStatus === "Y" }); toast.success(`배치가 ${newStatus === "Y" ? "활성화" : "비활성화"}되었습니다.`); loadBatchConfigs(); loadStats(); } catch (error) { console.error("배치 상태 변경 실패:", error); toast.error("배치 상태 변경에 실패했습니다."); } }; const deleteBatch = async (batchId: number, batchName: string) => { if (!confirm(`'${batchName}' 배치를 삭제하시겠습니까?`)) return; try { await BatchAPI.deleteBatchConfig(batchId); toast.success("배치가 삭제되었습니다."); loadBatchConfigs(); loadStats(); } catch (error) { console.error("배치 삭제 실패:", error); toast.error("배치 삭제에 실패했습니다."); } }; const handleCreateBatch = () => setIsBatchTypeModalOpen(true); const handleBatchTypeSelect = (type: "db-to-db" | "restapi-to-db") => { setIsBatchTypeModalOpen(false); if (type === "db-to-db") { router.push("/admin/batchmng/create"); } else { router.push("/admin/batch-management-new"); } }; const filteredBatches = useMemo(() => { let list = batchConfigs; if (statusFilter === "active") list = list.filter((b) => b.is_active === "Y"); else if (statusFilter === "inactive") list = list.filter((b) => b.is_active !== "Y"); const et = (b: BatchConfig) => (b as { execution_type?: string }).execution_type; if (typeFilter === "mapping") list = list.filter((b) => !et(b) || et(b) === "mapping"); else if (typeFilter === "restapi") list = list.filter((b) => et(b) === "restapi"); else if (typeFilter === "node_flow") list = list.filter((b) => et(b) === "node_flow"); return list; }, [batchConfigs, statusFilter, typeFilter]); return (
{/* 헤더 */}

배치 관리

데이터 동기화 배치 작업을 모니터링하고 관리합니다.

{/* 통계 카드 4개 */}

전체 배치

{statsLoading ? "-" : (stats?.totalBatches ?? 0).toLocaleString()}

활성 배치

{statsLoading ? "-" : (stats?.activeBatches ?? 0).toLocaleString()}

오늘 실행

{statsLoading ? "-" : (stats?.todayExecutions ?? 0).toLocaleString()}

오늘 실패

{statsLoading ? "-" : (stats?.todayFailures ?? 0).toLocaleString()}

{/* 툴바 */}
setSearchTerm(e.target.value)} className="h-9 rounded-lg border bg-card pl-9 text-xs" />
{(["all", "active", "inactive"] as const).map((s) => ( ))}
{(["all", "mapping", "restapi", "node_flow"] as const).map((t) => ( ))}
{filteredBatches.length}
{/* 배치 테이블 */} {filteredBatches.length === 0 ? (

{searchTerm || statusFilter !== "all" || typeFilter !== "all" ? "검색 결과가 없습니다." : "배치가 없습니다."}

{!searchTerm && statusFilter === "all" && typeFilter === "all" && ( )}
) : (
{filteredBatches.map((batch) => ( setExpandedId((id) => (id === batch.id ? null : batch.id ?? null))} executingBatch={executingBatch} onExecute={executeBatch} onToggleStatus={toggleBatchStatus} onEdit={(id) => router.push(`/admin/batchmng/edit/${id}`)} onDelete={deleteBatch} cronToKorean={cronToKorean} /> ))}
배치 타입 스케줄 최근 24h 마지막 실행 액션
)} {/* 배치 타입 선택 모달 */} {isBatchTypeModalOpen && (

배치 타입 선택

)}
); }