import { useState, useEffect, useMemo, useCallback } from 'react'; import { AreaChart, Area, XAxis, YAxis, CartesianGrid, Tooltip, Legend, ResponsiveContainer, } from 'recharts'; import type { UserStatsResponse, UserRoleDistribution } from '../../types/statistics'; import { getUserStats } from '../../services/statisticsService'; import PeriodFilter from '../../components/PeriodFilter'; import Badge from '../../components/ui/Badge'; import { CHART_COLORS_HEX } from '../../constants/chart'; import { useTheme } from '../../hooks/useTheme'; // 역할별 Badge variant 매핑 const ROLE_BADGE_VARIANT: Record = { ADMIN: 'rose', MANAGER: 'blue', USER: 'teal', }; const ROLE_DONUT_COLORS_HEX = { light: ['#507FB9', '#B59854', '#B5607D', '#3D998A', '#8B6DB5', '#C07850'], dark: ['#6D94C5', '#C4B48C', '#D4839B', '#5BB5A6', '#B79FD4', '#D4956B'], } as const; const successRateClass = (rate: number) => { if (rate >= 90) return 'text-[var(--color-success)]'; if (rate >= 70) return 'text-[var(--color-warning)]'; return 'text-[var(--color-danger)]'; }; // SVG 도넛 차트 (역할별 분포) interface DonutChartProps { data: UserRoleDistribution[]; colors: readonly string[]; } const DonutChart = ({ data, colors }: DonutChartProps) => { const total = data.reduce((sum, d) => sum + d.requestCount, 0); const size = 100; const cx = size / 2; const cy = size / 2; const r = 36; const strokeWidth = 18; const circumference = 2 * Math.PI * r; let cumulativePct = 0; return (
{/* 배경 트랙 */} {total === 0 ? null : data.map((segment, idx) => { const pct = segment.requestCount / total; const dashArray = circumference * pct; const dashOffset = circumference * (1 - cumulativePct); cumulativePct += pct; return ( ); })} 전체 {total.toLocaleString()} {/* 범례 */}
{data.map((segment, idx) => { const pct = total > 0 ? ((segment.requestCount / total) * 100).toFixed(1) : '0.0'; return (
{segment.role} {segment.requestCount.toLocaleString()} {pct}%
); })}
); }; // KPI 카드 interface KpiCardProps { icon: 'users' | 'key' | 'activity'; label: string; value: number; total?: number; } const KpiCard = ({ icon, label, value, total }: KpiCardProps) => { const pct = total && total > 0 ? Math.min(100, (value / total) * 100) : null; return (
{icon === 'users' && ( )} {icon === 'key' && ( )} {icon === 'activity' && ( )} {label}

{value.toLocaleString()}

{pct !== null && (
{pct.toFixed(1)}%
)}
); }; // 메인 페이지 const UserStatsPage = () => { const { theme } = useTheme(); const chartColors = CHART_COLORS_HEX[theme]; const donutColors = ROLE_DONUT_COLORS_HEX[theme]; const [startDate, setStartDate] = useState(() => { const d = new Date(); d.setDate(d.getDate() - 7); return d.toISOString().slice(0, 10); }); const [endDate, setEndDate] = useState(() => new Date().toISOString().slice(0, 10)); const [data, setData] = useState(null); const [isLoading, setIsLoading] = useState(true); const fetchData = useCallback(async () => { setIsLoading(true); try { const res = await getUserStats(startDate, endDate); if (res.success && res.data) { setData(res.data); } } finally { setIsLoading(false); } }, [startDate, endDate]); useEffect(() => { fetchData(); }, [fetchData]); const maxRequests = useMemo(() => { if (!data || data.topUsers.length === 0) return 1; return Math.max(...data.topUsers.map((u) => u.requestCount), 1); }, [data]); if (isLoading) { return (
로딩 중...
); } return (
{/* 헤더: 제목 + PeriodFilter */}

사용자 통계

사용자별 API 사용 현황을 분석합니다

{!data ? (

데이터가 없습니다

) : ( <> {/* 1행: KPI 카드 3개 */}
{/* 2행: 일별 추이(좌 60%) + 역할별 분포(우 40%) */}
{/* 좌: 일별 활성 사용자 추이 */}

일별 API 요청 사용자 추이

{data.dailyActiveUsers.length > 0 ? ( ) : (

데이터가 없습니다

)}
{/* 우: 역할별 요청 분포 (CSS 도넛) */}

역할별 요청 분포

{data.roleDistribution.length > 0 ? (
) : (

데이터가 없습니다

)}
{/* 3행: 상위 사용자 Top 5 테이블 */}

상위 사용자 Top 5

{data.topUsers.length > 0 ? (
{data.topUsers.slice(0, 5).map((user, idx) => { const rank = idx + 1; const rankBg = rank <= 3 ? 'bg-[var(--color-primary-subtle)] text-[var(--color-primary-text)]' : 'bg-[var(--color-bg-base)] text-[var(--color-text-secondary)]'; const reqPct = maxRequests > 0 ? (user.requestCount / maxRequests) * 100 : 0; return ( {/* 순위 뱃지 */} {/* 사용자명 (마스킹) */} {/* 역할 Badge pill */} {/* 요청 수 + 프로그레스 바 */} {/* 성공률 색상 코딩 */} ); })}
순위 사용자명 역할 요청 수 성공률
{rank}
{user.userName.length > 0 ? `${user.userName.charAt(0)}***` : '-'}
{user.role}
{user.requestCount.toLocaleString()}
{user.successRate.toFixed(1)}%
) : (

데이터가 없습니다

)}
)}
); }; export default UserStatsPage;