import { useState, useEffect, useCallback, useMemo } from 'react'; import { useNavigate } from 'react-router-dom'; import { LineChart, Line, XAxis, YAxis, CartesianGrid, Tooltip, Legend, ResponsiveContainer, PieChart, Pie, Cell, AreaChart, Area, } from 'recharts'; import type { DashboardStats, HourlyTrend, ServiceRatio, ErrorTrend, TopApi, TenantRatio } from '../types/dashboard'; import type { RequestLog } from '../types/monitoring'; import type { HeartbeatStatus } from '../types/service'; import { getSummary, getHourlyTrend, getServiceRatio, getErrorTrend, getTopApis, getRecentLogs, getHeartbeat, getTenantRequestRatio, getTenantUserRatio, } from '../services/dashboardService'; const PIE_COLORS = ['#3b82f6', '#10b981', '#f59e0b', '#ef4444', '#8b5cf6', '#06b6d4']; const SERVICE_TAG_STYLES = [ 'bg-blue-100 text-blue-700', 'bg-emerald-100 text-emerald-700', 'bg-amber-100 text-amber-700', 'bg-red-100 text-red-700', 'bg-violet-100 text-violet-700', 'bg-cyan-100 text-cyan-700', ]; const STATUS_BADGE: Record = { SUCCESS: 'bg-green-100 text-green-800', FAIL: 'bg-red-100 text-red-800', DENIED: 'bg-red-100 text-red-800', EXPIRED: 'bg-orange-100 text-orange-800', INVALID_KEY: 'bg-red-100 text-red-800', ERROR: 'bg-orange-100 text-orange-800', FAILED: 'bg-gray-100 text-gray-800', }; const AUTO_REFRESH_MS = 30000; const extractSettled = (result: PromiseSettledResult<{ data?: T }>, fallback: T): T => { if (result.status === 'fulfilled' && result.value.data !== undefined) { return result.value.data; } return fallback; }; const truncate = (str: string, max: number): string => { return str.length > max ? str.slice(0, max) + '...' : str; }; const DashboardPage = () => { const navigate = useNavigate(); const [stats, setStats] = useState(null); const [heartbeat, setHeartbeat] = useState([]); const [hourlyTrend, setHourlyTrend] = useState([]); const [serviceRatio, setServiceRatio] = useState([]); const [errorTrend, setErrorTrend] = useState([]); const [topApis, setTopApis] = useState([]); const [tenantRequestRatio, setTenantRequestRatio] = useState([]); const [tenantUserRatio, setTenantUserRatio] = useState([]); const [recentLogs, setRecentLogs] = useState([]); const [lastUpdated, setLastUpdated] = useState(''); const [isLoading, setIsLoading] = useState(true); const fetchAll = useCallback(async () => { try { const [summaryRes, heartbeatRes, hourlyRes, serviceRes, errorRes, topRes, tenantReqRes, tenantUserRes, logsRes] = await Promise.allSettled([ getSummary(), getHeartbeat(), getHourlyTrend(), getServiceRatio(), getErrorTrend(), getTopApis(), getTenantRequestRatio(), getTenantUserRatio(), getRecentLogs(), ]); setStats(extractSettled(summaryRes, null)); setHeartbeat(extractSettled(heartbeatRes, [])); setHourlyTrend(extractSettled(hourlyRes, [])); setServiceRatio(extractSettled(serviceRes, [])); setErrorTrend(extractSettled(errorRes, [])); setTopApis(extractSettled(topRes, [])); setTenantRequestRatio(extractSettled(tenantReqRes, [])); setTenantUserRatio(extractSettled(tenantUserRes, [])); setRecentLogs(extractSettled(logsRes, [])); setLastUpdated(new Date().toLocaleTimeString('ko-KR')); } finally { setIsLoading(false); } }, []); useEffect(() => { fetchAll(); const interval = setInterval(fetchAll, AUTO_REFRESH_MS); return () => clearInterval(interval); }, [fetchAll]); const errorTrendPivoted = useMemo(() => { const serviceNames = [...new Set(errorTrend.map((e) => e.serviceName))]; const byHour: Record> = {}; for (const item of errorTrend) { if (!byHour[item.hour]) { byHour[item.hour] = { hour: item.hour }; } byHour[item.hour][item.serviceName] = item.errorRate; } return { data: Object.values(byHour), serviceNames, }; }, [errorTrend]); const topApiServiceColorMap = useMemo(() => { const serviceNames = [...new Set(topApis.map((a) => a.serviceName))]; const map: Record = {}; serviceNames.forEach((name, i) => { map[name] = { tag: SERVICE_TAG_STYLES[i % SERVICE_TAG_STYLES.length], bar: PIE_COLORS[i % PIE_COLORS.length], }; }); return map; }, [topApis]); if (isLoading) { return (
Loading...
); } return (
{/* Header */}

Dashboard

{lastUpdated && ( 마지막 갱신: {lastUpdated} )}
{/* Row 1: Summary Cards */} {stats && (

오늘 총 요청

{stats.totalRequests.toLocaleString()}

0 ? 'text-green-600' : stats.changePercent < 0 ? 'text-red-600' : 'text-gray-500'}`}> {stats.changePercent > 0 ? '▲' : stats.changePercent < 0 ? '▼' : ''} 전일 대비 {stats.changePercent}%

성공률

{stats.successRate.toFixed(1)}%

실패 {stats.failureCount}건

평균 응답 시간

{stats.avgResponseTime.toFixed(0)}ms

활성 사용자

{stats.activeUserCount}

오늘

)} {/* Row 2: Heartbeat Status Bar */}
{heartbeat.length > 0 ? (
{heartbeat.map((svc) => (
navigate('/monitoring/service-status')} >
{svc.serviceName} {svc.healthResponseTime !== null && ( {svc.healthResponseTime}ms )} {svc.healthCheckedAt && ( {svc.healthCheckedAt} )}
))}
) : (

등록된 서비스가 없습니다

)}
{/* Row 3: Charts 2x2 */}
{/* Chart 1: Hourly Trend */}

시간별 요청 추이

{hourlyTrend.length > 0 ? ( `${h}시`} /> `${h}시`} /> ) : (

데이터가 없습니다

)}
{/* Chart 2: Service Ratio */}

서비스별 요청 비율

{serviceRatio.length > 0 ? ( {serviceRatio.map((_, idx) => ( ))} ) : (

데이터가 없습니다

)}
{/* Chart 3: Error Trend */}

에러율 추이

{errorTrendPivoted.data.length > 0 ? ( `${h}시`} /> `${h}시`} /> {errorTrendPivoted.serviceNames.map((name, idx) => ( ))} ) : (

데이터가 없습니다

)}
{/* Chart 4: Top APIs */}

상위 호출 API

{topApis.length > 0 ? (
{topApis.map((api, idx) => { const maxCount = topApis[0]?.count || 1; const pct = (api.count / maxCount) * 100; const colors = topApiServiceColorMap[api.serviceName] || { tag: SERVICE_TAG_STYLES[0], bar: PIE_COLORS[0] }; return (
{idx + 1} {api.serviceName} {api.apiName}
{api.count}
); })}
) : (

데이터가 없습니다

)}
{/* Row 4: Tenant Stats */}

테넌트별 요청 비율

{tenantRequestRatio.length > 0 ? ( {tenantRequestRatio.map((_, idx) => ( ))} ) : (

데이터가 없습니다

)}

테넌트별 사용자 비율

{tenantUserRatio.length > 0 ? ( {tenantUserRatio.map((_, idx) => ( ))} ) : (

데이터가 없습니다

)}
{/* Row 5: Recent Logs */}

최근 요청 로그

{recentLogs.length > 0 ? ( <>
{recentLogs.slice(0, 5).map((log) => ( navigate(`/monitoring/request-logs/${log.logId}`)} > ))}
시간 서비스 사용자 URL 상태 응답시간
{log.requestedAt} {log.serviceName ?? '-'} {log.userName ?? '-'} {truncate(log.requestUrl, 40)} {log.requestStatus} {log.responseTime !== null ? `${log.responseTime}ms` : '-'}
) : (

요청 로그가 없습니다

)}
); }; export default DashboardPage;