snp-connection-monitoring/frontend/src/pages/statistics/ServiceStatsPage.tsx
HYOJIN c2a71c1b77 feat(design): 디자인 시스템 적용 (CSS 토큰, Button/Badge, 차트, 다크모드) (#48)
- 디자인 시스템 가이드 문서 11개 생성 (docs/design/)
- CSS 변수 토큰 시스템 (@theme + :root/.dark 전환)
- cn() 유틸리티 (clsx + tailwind-merge)
- Button/Badge 공통 컴포넌트 (variant/size, 다크모드 대응)
- 하드코딩 Tailwind 색상 → CSS 변수 토큰 리팩토링 (30개 파일)
- 차트 팔레트 다크모드 색상 업데이트 (CHART_COLORS_HEX)
- 버튼 다크모드 채도/대비 강화 (primary-600 기반)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-15 16:38:00 +09:00

213 lines
8.6 KiB
TypeScript

import { useState, useEffect, useMemo, useCallback } from 'react';
import {
BarChart, Bar, XAxis, YAxis, CartesianGrid, Tooltip, Legend, ResponsiveContainer,
LineChart, Line, Cell,
} from 'recharts';
import type { ServiceStatsResponse } from '../../types/statistics';
import { getServiceStats } from '../../services/statisticsService';
import DateRangeFilter from '../../components/DateRangeFilter';
import { CHART_COLORS_HEX } from '../../constants/chart';
import { useTheme } from '../../hooks/useTheme';
const getToday = () => new Date().toISOString().slice(0, 10);
const ServiceStatsPage = () => {
const { theme } = useTheme();
const chartColors = CHART_COLORS_HEX[theme];
const [startDate, setStartDate] = useState(getToday());
const [endDate, setEndDate] = useState(getToday());
const [data, setData] = useState<ServiceStatsResponse | null>(null);
const [isLoading, setIsLoading] = useState(true);
const fetchData = useCallback(async () => {
setIsLoading(true);
try {
const res = await getServiceStats(startDate, endDate);
if (res.success && res.data) {
setData(res.data);
}
} finally {
setIsLoading(false);
}
}, [startDate, endDate]);
useEffect(() => {
fetchData();
}, [fetchData]);
const handlePreset = (days: number) => {
const today = getToday();
if (days === 0) {
setStartDate(today);
} else {
const d = new Date();
d.setDate(d.getDate() - days);
setStartDate(d.toISOString().slice(0, 10));
}
setEndDate(today);
};
const hourlyTrendPivoted = useMemo(() => {
if (!data) return { data: [], serviceNames: [] };
const serviceNames = [...new Set(data.hourlyTrend.map((e) => e.serviceName))];
const byHour: Record<number, Record<string, number | string>> = {};
for (const item of data.hourlyTrend) {
if (!byHour[item.hour]) {
byHour[item.hour] = { hour: item.hour };
}
byHour[item.hour][item.serviceName] = item.count;
}
return { data: Object.values(byHour), serviceNames };
}, [data]);
const barChartData = useMemo(() => {
if (!data) return [];
return data.serviceStats.map((s) => ({
serviceName: s.serviceName,
totalRequests: s.totalRequests,
}));
}, [data]);
if (isLoading) {
return (
<div className="flex items-center justify-center h-64">
<div className="text-[var(--color-text-secondary)]"> ...</div>
</div>
);
}
return (
<div className="max-w-7xl mx-auto">
<h1 className="text-2xl font-bold text-[var(--color-text-primary)] mb-6"> </h1>
<DateRangeFilter
startDate={startDate}
endDate={endDate}
onStartDateChange={setStartDate}
onEndDateChange={setEndDate}
onPreset={handlePreset}
/>
{!data || data.serviceStats.length === 0 ? (
<p className="text-[var(--color-text-tertiary)] text-center py-20"> </p>
) : (
<>
{/* Summary Cards */}
<div className="flex flex-wrap gap-4 mb-6">
{data.serviceStats.map((svc) => (
<div key={svc.serviceId} className="bg-[var(--color-bg-surface)] rounded-lg shadow p-6 flex-1 min-w-[200px]">
<p className="text-sm font-medium text-[var(--color-text-secondary)]">{svc.serviceName}</p>
<p className="text-2xl font-bold text-[var(--color-text-primary)] mt-1">
{svc.totalRequests.toLocaleString()}
</p>
<div className="flex items-center gap-3 mt-2 text-sm">
<span className="text-green-600"> {svc.successRate.toFixed(1)}%</span>
<span className="text-[var(--color-text-secondary)]">{svc.avgResponseTime.toFixed(0)}ms</span>
</div>
</div>
))}
</div>
{/* Charts */}
<div className="grid grid-cols-2 gap-6 mb-6">
{/* Chart 1: Service Request Count Bar */}
<div className="bg-[var(--color-bg-surface)] rounded-lg shadow p-6">
<h3 className="text-lg font-semibold text-[var(--color-text-primary)] mb-4"> </h3>
<ResponsiveContainer width="100%" height={300}>
<BarChart data={barChartData} layout="vertical">
<CartesianGrid strokeDasharray="3 3" />
<XAxis type="number" />
<YAxis dataKey="serviceName" type="category" width={120} />
<Tooltip />
<Legend />
<Bar dataKey="totalRequests" fill={chartColors[0]} name="요청 수" />
</BarChart>
</ResponsiveContainer>
</div>
{/* Chart 2: Hourly Service Trend */}
<div className="bg-[var(--color-bg-surface)] rounded-lg shadow p-6">
<h3 className="text-lg font-semibold text-[var(--color-text-primary)] mb-4"> </h3>
{hourlyTrendPivoted.data.length > 0 ? (
<ResponsiveContainer width="100%" height={300}>
<LineChart data={hourlyTrendPivoted.data}>
<CartesianGrid strokeDasharray="3 3" />
<XAxis dataKey="hour" tickFormatter={(h: number) => `${h}`} />
<YAxis />
<Tooltip labelFormatter={(h) => `${h}`} />
<Legend />
{hourlyTrendPivoted.serviceNames.map((name, idx) => (
<Line
key={name}
type="monotone"
dataKey={name}
stroke={chartColors[idx % chartColors.length]}
/>
))}
</LineChart>
</ResponsiveContainer>
) : (
<p className="text-[var(--color-text-tertiary)] text-center py-20"> </p>
)}
</div>
</div>
{/* Charts Row 2: Error Rate + Response Time */}
<div className="grid grid-cols-2 gap-6">
{/* Chart: Error Rate Comparison */}
<div className="bg-[var(--color-bg-surface)] rounded-lg shadow p-6">
<h3 className="text-lg font-semibold text-[var(--color-text-primary)] mb-4"> </h3>
<ResponsiveContainer width="100%" height={300}>
<BarChart
data={data.serviceStats.map((s) => ({
serviceName: s.serviceName,
successRate: Number(s.successRate.toFixed(1)),
errorRate: Number((100 - s.successRate).toFixed(1)),
}))}
layout="vertical"
>
<CartesianGrid strokeDasharray="3 3" />
<XAxis type="number" domain={[0, 100]} unit="%" />
<YAxis dataKey="serviceName" type="category" width={120} />
<Tooltip />
<Legend />
<Bar dataKey="successRate" stackId="a" fill="#10b981" name="성공률" />
<Bar dataKey="errorRate" stackId="a" fill="#ef4444" name="에러율" />
</BarChart>
</ResponsiveContainer>
</div>
{/* Chart: Avg Response Time Comparison */}
<div className="bg-[var(--color-bg-surface)] rounded-lg shadow p-6">
<h3 className="text-lg font-semibold text-[var(--color-text-primary)] mb-4"> </h3>
<ResponsiveContainer width="100%" height={300}>
<BarChart
data={data.serviceStats.map((s) => ({
serviceName: s.serviceName,
avgResponseTime: Number(s.avgResponseTime.toFixed(0)),
}))}
layout="vertical"
>
<CartesianGrid strokeDasharray="3 3" />
<XAxis type="number" unit="ms" />
<YAxis dataKey="serviceName" type="category" width={120} />
<Tooltip />
<Bar dataKey="avgResponseTime" name="평균 응답시간 (ms)">
{data.serviceStats.map((s, idx) => {
const rt = s.avgResponseTime;
const color = rt < 100 ? '#10b981' : rt < 300 ? '#f59e0b' : '#ef4444';
return <Cell key={idx} fill={color} />;
})}
</Bar>
</BarChart>
</ResponsiveContainer>
</div>
</div>
</>
)}
</div>
);
};
export default ServiceStatsPage;