import { useState, useCallback, useEffect } from 'react'; import { Link } from 'react-router-dom'; import { batchApi, type DashboardResponse, type DashboardStats, type ExecutionStatisticsDto, } from '../api/batchApi'; import { usePoller } from '../hooks/usePoller'; import { useToastContext } from '../contexts/ToastContext'; import StatusBadge from '../components/StatusBadge'; import EmptyState from '../components/EmptyState'; import LoadingSpinner from '../components/LoadingSpinner'; import BarChart from '../components/BarChart'; import { formatDateTime, calculateDuration } from '../utils/formatters'; const POLLING_INTERVAL = 5000; interface StatCardProps { label: string; value: number; gradient: string; to?: string; } function StatCard({ label, value, gradient, to }: StatCardProps) { const content = (

{value}

{label}

); if (to) { return {content}; } return content; } export default function Dashboard() { const { showToast } = useToastContext(); const [dashboard, setDashboard] = useState(null); const [loading, setLoading] = useState(true); // Execute Job modal const [showExecuteModal, setShowExecuteModal] = useState(false); const [jobs, setJobs] = useState([]); const [selectedJob, setSelectedJob] = useState(''); const [executing, setExecuting] = useState(false); const [startDate, setStartDate] = useState(''); const [stopDate, setStopDate] = useState(''); const [abandoning, setAbandoning] = useState(false); const [statistics, setStatistics] = useState(null); const loadStatistics = useCallback(async () => { try { const data = await batchApi.getStatistics(30); setStatistics(data); } catch { /* 통계 로드 실패는 무시 */ } }, []); useEffect(() => { loadStatistics(); }, [loadStatistics]); const loadDashboard = useCallback(async () => { try { const data = await batchApi.getDashboard(); setDashboard(data); } catch (err) { console.error('Dashboard load failed:', err); } finally { setLoading(false); } }, []); usePoller(loadDashboard, POLLING_INTERVAL); const handleOpenExecuteModal = async () => { try { const jobList = await batchApi.getJobs(); setJobs(jobList); setSelectedJob(jobList[0] ?? ''); setShowExecuteModal(true); } catch (err) { showToast('작업 목록을 불러올 수 없습니다.', 'error'); console.error(err); } }; const handleExecuteJob = async () => { if (!selectedJob) return; setExecuting(true); try { const params: Record = {}; if (startDate) params.startDate = startDate; if (stopDate) params.stopDate = stopDate; const result = await batchApi.executeJob( selectedJob, Object.keys(params).length > 0 ? params : undefined, ); showToast( result.message || `${selectedJob} 실행 요청 완료`, 'success', ); setShowExecuteModal(false); setStartDate(''); setStopDate(''); await loadDashboard(); } catch (err) { showToast(`실행 실패: ${err instanceof Error ? err.message : '알 수 없는 오류'}`, 'error'); } finally { setExecuting(false); } }; const handleAbandonAllStale = async () => { setAbandoning(true); try { const result = await batchApi.abandonAllStale(); showToast( result.message || `${result.abandonedCount ?? 0}건 강제 종료 완료`, 'success', ); await loadDashboard(); } catch (err) { showToast( `강제 종료 실패: ${err instanceof Error ? err.message : '알 수 없는 오류'}`, 'error', ); } finally { setAbandoning(false); } }; if (loading) return ; const stats: DashboardStats = dashboard?.stats ?? { totalSchedules: 0, activeSchedules: 0, inactiveSchedules: 0, totalJobs: 0, }; const runningJobs = dashboard?.runningJobs ?? []; const recentExecutions = dashboard?.recentExecutions ?? []; const recentFailures = dashboard?.recentFailures ?? []; const staleExecutionCount = dashboard?.staleExecutionCount ?? 0; const failureStats = dashboard?.failureStats ?? { last24h: 0, last7d: 0 }; return (
{/* Header */}

대시보드

{/* F1: Stale Execution Warning Banner */} {staleExecutionCount > 0 && (
{staleExecutionCount}건의 오래된 실행 중 작업이 있습니다
)} {/* Stats Cards */}
{/* Quick Navigation */}
작업 관리 실행 이력 스케줄 관리 타임라인
{/* Running Jobs */}

실행 중인 작업 {runningJobs.length > 0 && ( ({runningJobs.length}건) )}

{runningJobs.length === 0 ? ( ) : (
{runningJobs.map((job) => ( ))}
작업명 실행 ID 시작 시간 상태
{job.jobName} #{job.executionId} {formatDateTime(job.startTime)}
)}
{/* Recent Executions */}

최근 실행 이력

전체 보기 →
{recentExecutions.length === 0 ? ( ) : (
{recentExecutions.slice(0, 5).map((exec) => ( ))}
실행 ID 작업명 시작 시간 종료 시간 소요 시간 상태
#{exec.executionId} {exec.jobName} {formatDateTime(exec.startTime)} {formatDateTime(exec.endTime)} {calculateDuration(exec.startTime, exec.endTime)}
)}
{/* F6: Recent Failures */} {recentFailures.length > 0 && (

최근 실패 이력 ({recentFailures.length}건)

{recentFailures.map((fail) => ( ))}
실행 ID 작업명 시작 시간 오류 메시지
#{fail.executionId} {fail.jobName} {formatDateTime(fail.startTime)} {fail.exitMessage ? fail.exitMessage.length > 50 ? `${fail.exitMessage.slice(0, 50)}...` : fail.exitMessage : '-'}
)} {/* F8: Execution Statistics Chart */} {statistics && statistics.dailyStats.length > 0 && (

실행 통계 (최근 30일)

전체 {statistics.totalExecutions} 성공 {statistics.totalSuccess} 실패 {statistics.totalFailed}
({ label: d.date.slice(5), values: [ { color: 'green', value: d.successCount }, { color: 'red', value: d.failedCount }, { color: 'gray', value: d.otherCount }, ], }))} height={180} />
성공 실패 기타
)} {/* Execute Job Modal */} {showExecuteModal && (
setShowExecuteModal(false)} >
e.stopPropagation()} >

작업 즉시 실행

setStartDate(e.target.value)} className="w-full px-3 py-2 border border-wing-border rounded-lg text-sm focus:ring-2 focus:ring-wing-accent focus:border-wing-accent mb-3" /> setStopDate(e.target.value)} className="w-full px-3 py-2 border border-wing-border rounded-lg text-sm focus:ring-2 focus:ring-wing-accent focus:border-wing-accent mb-4" />
)}
); }