import { useState, useCallback, useEffect } from 'react'; import { Link } from 'react-router-dom'; import { batchApi, type DashboardResponse, type DashboardStats, type ExecutionStatisticsDto, type RecollectionStatsResponse, } 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); const [abandoning, setAbandoning] = useState(false); const [statistics, setStatistics] = useState(null); const [recollectionStats, setRecollectionStats] = useState(null); const loadStatistics = useCallback(async () => { try { const data = await batchApi.getStatistics(30); setStatistics(data); } catch { /* 통계 로드 실패는 무시 */ } }, []); useEffect(() => { loadStatistics(); }, [loadStatistics]); const loadRecollectionStats = useCallback(async () => { try { const data = await batchApi.getRecollectionStats(); setRecollectionStats(data); } catch { /* 통계 로드 실패는 무시 */ } }, []); useEffect(() => { loadRecollectionStats(); }, [loadRecollectionStats]); 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 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 : '-'}
)} {/* 재수집 현황 */} {recollectionStats && recollectionStats.totalCount > 0 && (

재수집 현황

전체 보기 →

{recollectionStats.totalCount}

전체

{recollectionStats.completedCount}

완료

{recollectionStats.failedCount}

실패

{recollectionStats.runningCount}

실행 중

{recollectionStats.overlapCount}

중복

{/* 최근 재수집 이력 (최대 5건) */} {recollectionStats.recentHistories.length > 0 && (
{recollectionStats.recentHistories.slice(0, 5).map((h) => ( ))}
이력 ID 작업명 실행자 시작 시간 상태
#{h.historyId} {h.apiKeyName || h.jobName} {h.executor || '-'} {formatDateTime(h.executionStartTime)}
)}
)} {/* 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} />
성공 실패 기타
)}
); }