"use client"; import React, { useState, useEffect } from "react"; import { useRouter } from "next/navigation"; 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 { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger, } from "@/components/ui/dropdown-menu"; import { Plus, Search, MoreHorizontal, Edit, Trash2, Play, RefreshCw, BarChart3, ArrowRight, Database, Globe } from "lucide-react"; import { toast } from "sonner"; import { BatchAPI, BatchJob } from "@/lib/api/batch"; import BatchJobModal from "@/components/admin/BatchJobModal"; import { showErrorToast } from "@/lib/utils/toastUtils"; import { ResponsiveDataView, RDVColumn, RDVCardField } from "@/components/common/ResponsiveDataView"; export default function BatchManagementPage() { const router = useRouter(); const [jobs, setJobs] = useState([]); const [filteredJobs, setFilteredJobs] = useState([]); const [isLoading, setIsLoading] = useState(false); const [searchTerm, setSearchTerm] = useState(""); const [statusFilter, setStatusFilter] = useState("all"); const [typeFilter, setTypeFilter] = useState("all"); const [jobTypes, setJobTypes] = useState>([]); const [isModalOpen, setIsModalOpen] = useState(false); const [selectedJob, setSelectedJob] = useState(null); const [isBatchTypeModalOpen, setIsBatchTypeModalOpen] = useState(false); useEffect(() => { loadJobs(); loadJobTypes(); }, []); useEffect(() => { filterJobs(); }, [jobs, searchTerm, statusFilter, typeFilter]); const loadJobs = async () => { setIsLoading(true); try { const data = await BatchAPI.getBatchJobs(); setJobs(data); } catch (error) { console.error("배치 작업 목록 조회 오류:", error); showErrorToast("배치 작업 목록을 불러오는 데 실패했습니다", error, { guidance: "네트워크 연결을 확인해 주세요." }); } finally { setIsLoading(false); } }; const loadJobTypes = async () => { try { const types = await BatchAPI.getSupportedJobTypes(); setJobTypes(types.map(t => ({ value: t, label: t }))); } catch (error) { console.error("작업 타입 조회 오류:", error); } }; const filterJobs = () => { let filtered = jobs; if (searchTerm) { filtered = filtered.filter(job => job.job_name.toLowerCase().includes(searchTerm.toLowerCase()) || job.description?.toLowerCase().includes(searchTerm.toLowerCase()) ); } if (statusFilter !== "all") { filtered = filtered.filter(job => job.is_active === statusFilter); } if (typeFilter !== "all") { filtered = filtered.filter(job => job.job_type === typeFilter); } setFilteredJobs(filtered); }; const handleCreate = () => { setIsBatchTypeModalOpen(true); }; const handleBatchTypeSelect = (type: 'db-to-db' | 'restapi-to-db') => { console.log("배치 타입 선택:", type); setIsBatchTypeModalOpen(false); if (type === 'db-to-db') { console.log("DB → DB 배치 모달 열기"); setSelectedJob(null); setIsModalOpen(true); } else if (type === 'restapi-to-db') { console.log("REST API → DB 페이지로 이동:", '/admin/batch-management-new'); router.push('/admin/batch-management-new'); } }; const handleEdit = (job: BatchJob) => { setSelectedJob(job); setIsModalOpen(true); }; const handleDelete = async (job: BatchJob) => { if (!confirm(`"${job.job_name}" 배치 작업을 삭제하시겠습니까?`)) { return; } try { await BatchAPI.deleteBatchJob(job.id!); toast.success("배치 작업이 삭제되었습니다."); loadJobs(); } catch (error) { console.error("배치 작업 삭제 오류:", error); showErrorToast("배치 작업 삭제에 실패했습니다", error, { guidance: "잠시 후 다시 시도해 주세요." }); } }; const handleExecute = async (job: BatchJob) => { try { await BatchAPI.executeBatchJob(job.id!); toast.success(`"${job.job_name}" 배치 작업을 실행했습니다.`); } catch (error) { console.error("배치 작업 실행 오류:", error); showErrorToast("배치 작업 실행에 실패했습니다", error, { guidance: "배치 설정을 확인해 주세요." }); } }; const handleModalSave = () => { loadJobs(); }; const getStatusBadge = (isActive: string) => { return isActive === "Y" ? ( 활성 ) : ( 비활성 ); }; const getTypeBadge = (type: string) => { const option = jobTypes.find(opt => opt.value === type); return ( {option?.label || type} ); }; const getSuccessRate = (job: BatchJob) => { if (!job.execution_count) return 100; return Math.round(((job.success_count ?? 0) / job.execution_count) * 100); }; const getSuccessRateColor = (rate: number) => { if (rate >= 90) return 'text-success'; if (rate >= 70) return 'text-warning'; return 'text-destructive'; }; const columns: RDVColumn[] = [ { key: "job_name", label: "작업명", render: (_val, job) => (
{job.job_name}
{job.description && (
{job.description}
)}
), }, { key: "job_type", label: "타입", hideOnMobile: true, render: (_val, job) => getTypeBadge(job.job_type), }, { key: "schedule_cron", label: "스케줄", hideOnMobile: true, render: (_val, job) => ( {job.schedule_cron || "-"} ), }, { key: "is_active", label: "상태", width: "100px", render: (_val, job) => getStatusBadge(job.is_active), }, { key: "execution_count", label: "실행", hideOnMobile: true, render: (_val, job) => (
총 {job.execution_count}회
성공 {job.success_count} / 실패 {job.failure_count}
), }, { key: "success_rate", label: "성공률", hideOnMobile: true, render: (_val, job) => { const rate = getSuccessRate(job); return ( {rate}% ); }, }, { key: "last_executed_at", label: "마지막 실행", render: (_val, job) => ( {job.last_executed_at ? new Date(job.last_executed_at).toLocaleString() : "-"} ), }, ]; const cardFields: RDVCardField[] = [ { label: "타입", render: (job) => getTypeBadge(job.job_type), }, { label: "스케줄", render: (job) => ( {job.schedule_cron || "-"} ), }, { label: "실행 횟수", render: (job) => {job.execution_count}회, }, { label: "성공률", render: (job) => { const rate = getSuccessRate(job); return ( {rate}% ); }, }, { label: "마지막 실행", render: (job) => ( {job.last_executed_at ? new Date(job.last_executed_at).toLocaleDateString() : "-"} ), }, ]; const renderDropdownActions = (job: BatchJob) => ( handleEdit(job)}> 수정 handleExecute(job)} disabled={job.is_active !== "Y"} > 실행 handleDelete(job)}> 삭제 ); return (
{/* 헤더 */}

배치 관리

스케줄된 배치 작업을 관리하고 실행 상태를 모니터링합니다.

{/* 통계 카드 */}
총 작업
{jobs.length}

활성: {jobs.filter(j => j.is_active === 'Y').length}개

총 실행
{jobs.reduce((sum, job) => sum + (job.execution_count ?? 0), 0)}

누적 실행 횟수

성공
{jobs.reduce((sum, job) => sum + (job.success_count ?? 0), 0)}

총 성공 횟수

실패
{jobs.reduce((sum, job) => sum + (job.failure_count ?? 0), 0)}

총 실패 횟수

{/* 필터 및 검색 */}
setSearchTerm(e.target.value)} className="h-10 pl-10 text-sm" />
{/* 배치 작업 목록 제목 */}
{filteredJobs.length}
data={filteredJobs} columns={columns} keyExtractor={(job) => String(job.id)} isLoading={isLoading} emptyMessage={jobs.length === 0 ? "배치 작업이 없습니다." : "검색 결과가 없습니다."} skeletonCount={5} cardTitle={(job) => job.job_name} cardSubtitle={(job) => job.description ? ( {job.description} ) : undefined} cardHeaderRight={(job) => getStatusBadge(job.is_active)} cardFields={cardFields} actionsLabel="작업" actionsWidth="80px" renderActions={renderDropdownActions} /> {/* 배치 타입 선택 모달 */} {isBatchTypeModalOpen && (
배치 타입 선택
handleBatchTypeSelect('db-to-db')} >
DB → DB
데이터베이스 간 데이터 동기화
handleBatchTypeSelect('restapi-to-db')} >
REST API → DB
REST API에서 데이터베이스로 데이터 수집
)} {/* 배치 작업 모달 */} setIsModalOpen(false)} onSave={handleModalSave} job={selectedJob} />
); }