- 📊 학습 통계: 세션 수, 학습 시간, 주간 목표 진행률
- 📈 매칭 분석: 성공률, 대기 시간, 언어별 통계
- 🎯 레벨 테스트: 진행도 추적, 월별 빈도, 언어별 분포
- 🔄 실시간 메트릭: WebSocket을 통한 서버 성능 모니터링 (Admin용)
- 차트 라이브러리: Recharts 2.x (LineChart, BarChart, PieChart, AreaChart)
- API 통신: Axios (12개 Analytics 엔드포인트)
- 아이콘: Lucide React (BarChart3, TrendingUp, Users, Globe, Clock, etc.)
- 날짜 처리: JavaScript Date API + 다국어 포맷
- WebSocket: 실시간 메트릭 스트리밍 (AnalyticsDashboard)
/analytics → AnalyticsPage (학습 통계 대시보드)
사용자 진입 (/analytics)
↓
AnalyticsPage 마운트
↓
Promise.all([
getStudyStats(timeRange),
getSessionActivity(timeRange)
])
↓
transformApiDataToAnalyticsData()
↓
State 업데이트 (analyticsData)
↓
UI 렌더링:
- Overview Cards (4개)
- Weekly Goal Progress
- WeeklyActivityChart
- Session Activity (LineChart)
- Session Types (PieChart)
- LevelTestHistoryChart
- MatchingStatsChart
- Language Progress
- Top Partners
- 초기 로드:
useEffect(() => { loadAnalyticsData(); }, [timeRange]) - Time Range 변경: 드롭다운 → API 재호출
- 새로고침: RefreshCw 버튼 →
loadAnalyticsData() - 차트 뷰 전환:
selectedViewstate → 차트 조건부 렌더링
src/
├── pages/
│ └── Analytics/
│ ├── AnalyticsPage.jsx # 메인 분석 페이지
│ └── AnalyticsDashboard.jsx # 서버 메트릭 대시보드 (Admin)
└── components/
├── analytics/
│ ├── MatchingStatsChart.jsx # 매칭 통계 차트
│ └── LevelTestHistoryChart.jsx # 레벨 테스트 차트
└── profile/
└── WeeklyActivityChart.jsx # 주간 활동 차트
파일: src/pages/Analytics/AnalyticsPage.jsx (486 lines)
State:
const [loading, setLoading] = useState(true);
const [timeRange, setTimeRange] = useState('week'); // day | week | month | year
const [analyticsData, setAnalyticsData] = useState(null);
const [error, setError] = useState(null);데이터 구조:
const analyticsData = {
overview: {
totalSessions: 24,
totalMinutes: 720,
weeklyGrowth: 15,
currentStreak: 5,
averageSessionTime: 30,
partnersCount: 8
},
sessionStats: [
{ date: '2025-01-10', sessions: 3 },
{ date: '2025-01-11', sessions: 5 }
],
languageProgress: [
{
language: 'English',
level: 'Intermediate',
progress: 65,
sessions: 12
}
],
sessionTypes: [
{ name: '1:1 회화', value: 60, color: '#00C471' },
{ name: '그룹 세션', value: 30, color: '#4285F4' },
{ name: '랜덤 매칭', value: 10, color: '#FFB800' }
],
weeklyGoals: {
current: 4,
target: 7,
streak: 2
},
topPartners: [
{
name: 'John Doe',
flag: '🇺🇸',
sessions: 8,
rating: 4.8
}
]
};주요 섹션:
<div className="grid grid-cols-2 lg:grid-cols-4 gap-3 sm:gap-4">
{/* 총 세션 */}
<div className="bg-white rounded-[16px] p-3 sm:p-4 border border-[#E7E7E7]">
<Video className="w-4 h-4 sm:w-5 sm:h-5 text-[#00C471]" />
<div className="text-[20px] font-bold">{totalSessions}</div>
<TrendingUp className="text-[#00C471]" />
<span>+{weeklyGrowth}%</span>
</div>
{/* 총 학습시간 */}
<div className="...">
<Clock className="text-[#4285F4]" />
<div>{Math.floor(totalMinutes / 60)}h {totalMinutes % 60}m</div>
<span>평균 {averageSessionTime}분/세션</span>
</div>
{/* 연속 학습 */}
<div className="...">
<Target className="text-[#FF6B6B]" />
<div>{currentStreak}</div>
<span>일 연속</span>
</div>
{/* 파트너 */}
<div className="...">
<Users className="text-[#9C27B0]" />
<div>{partnersCount}</div>
<span>활성 파트너</span>
</div>
</div><div className="bg-white rounded-[20px] p-6">
<h3>주간 목표</h3>
<button>목표 수정</button>
{/* Progress Bar */}
<div className="flex justify-between">
<span>{current}/{target} 세션 완료</span>
<span>{Math.round((current / target) * 100)}%</span>
</div>
<div className="w-full bg-[#E7E7E7] rounded-full h-3">
<div
className="bg-[#00C471] h-3 rounded-full"
style={{ width: `${Math.min(100, (current / target) * 100)}%` }}
/>
</div>
<div className="flex items-center">
<div className="w-2 h-2 bg-[#00C471] rounded-full"></div>
<span>{streak}일 연속 목표 달성</span>
</div>
</div><ResponsiveContainer width="100%" height="100%">
<LineChart data={analyticsData.sessionStats}>
<CartesianGrid strokeDasharray="3 3" stroke="#E7E7E7" />
<XAxis
dataKey="date"
tickFormatter={(value) =>
new Date(value).toLocaleDateString('ko-KR', { month: 'short', day: 'numeric' })
}
/>
<YAxis />
<Tooltip />
<Line
type="monotone"
dataKey="sessions"
stroke="#00C471"
strokeWidth={3}
dot={{ fill: '#00C471', r: 4 }}
/>
</LineChart>
</ResponsiveContainer><ResponsiveContainer width="100%" height={256}>
<PieChart>
<Pie
data={sessionTypes}
cx="50%"
cy="50%"
innerRadius={60}
outerRadius={100}
paddingAngle={5}
dataKey="value"
>
{sessionTypes.map((entry, index) => (
<Cell key={`cell-${index}`} fill={entry.color} />
))}
</Pie>
<Tooltip />
</PieChart>
</ResponsiveContainer>파일: src/components/analytics/MatchingStatsChart.jsx (712 lines)
주요 기능:
- 성공률 추이 (AreaChart)
- 매칭 빈도 (BarChart)
- 타입별 분포 (PieChart)
- 언어별 성공률 (Progress Bars)
State:
const [matchingData, setMatchingData] = useState(null);
const [selectedView, setSelectedView] = useState('success-rate');
// 'success-rate' | 'frequency' | 'type' | 'language'데이터 변환 함수:
// 일자별 통계 처리
const processMatchingEvents = (events) => {
const dailyMap = new Map();
events.forEach((event) => {
const dayKey = date.toISOString().split('T')[0];
const status = resolveStatus(event); // SUCCESS | FAILED | REQUEST
if (!dailyMap.has(dayKey)) {
dailyMap.set(dayKey, { date: dayKey, requests: 0, successful: 0, failed: 0 });
}
const dayEntry = dailyMap.get(dayKey);
if (status === 'SUCCESS') {
dayEntry.successful += 1;
dayEntry.requests += 1;
}
});
return Array.from(dailyMap.values()).map((day) => ({
...day,
successRate: day.requests > 0
? ((day.successful / day.requests) * 100).toFixed(1)
: 0
}));
};성공률 AreaChart:
<ResponsiveContainer width="100%" height={320}>
<AreaChart data={matchingData.dailyStats}>
<defs>
<linearGradient id="successGradient" x1="0" y1="0" x2="0" y2="1">
<stop offset="5%" stopColor="#00C471" stopOpacity={0.3}/>
<stop offset="95%" stopColor="#00C471" stopOpacity={0.1}/>
</linearGradient>
</defs>
<CartesianGrid strokeDasharray="3 3" stroke="var(--black-50)" />
<XAxis dataKey="day" />
<YAxis domain={[0, 100]} tickFormatter={(value) => `${value}%`} />
<Tooltip formatter={(value) => [`${value}%`, '성공률']} />
<Area
type="monotone"
dataKey="successRate"
stroke="#00C471"
fill="url(#successGradient)"
/>
</AreaChart>
</ResponsiveContainer>통계 요약 카드:
<div className="grid grid-cols-4 gap-4">
{/* 전체 성공률 */}
<div className="bg-[var(--green-50)] rounded-[12px] p-4">
<Target className="w-5 h-5 text-white" />
<div className="text-[20px] font-bold">{overallSuccessRate}%</div>
<div className="text-[12px]">전체 성공률</div>
</div>
{/* 총 매칭 시도 */}
<div className="bg-[#F0F8FF] rounded-[12px] p-4">
<Users />
<div>{totalRequests}</div>
<div>총 매칭 시도</div>
</div>
{/* 평균 대기시간 */}
<div className="bg-[#FFF8E1] rounded-[12px] p-4">
<Clock />
<div>{Math.floor(avgWaitTime / 60)}:{(avgWaitTime % 60).toString().padStart(2, '0')}</div>
<div>평균 대기시간</div>
</div>
{/* 평균 세션시간 */}
<div className="bg-[#FFE6F0] rounded-[12px] p-4">
<Heart />
<div>{avgSessionDuration}분</div>
<div>평균 세션시간</div>
</div>
</div>파일: src/components/analytics/LevelTestHistoryChart.jsx (481 lines)
주요 기능:
- 레벨 진행도 (LineChart)
- 월별 빈도 (BarChart)
- 언어별 분포 (PieChart)
레벨 시스템:
const LEVEL_ORDER = [
'Beginner', // 0
'Elementary', // 1
'Intermediate', // 2
'Upper-Intermediate', // 3
'Advanced', // 4
'Proficient' // 5
];레벨 진행도 차트:
<ResponsiveContainer width="100%" height={300}>
<LineChart data={historyData.levelProgression}>
<XAxis dataKey="month" />
<YAxis
domain={[0, 4]}
tickFormatter={(value) =>
['Beginner', 'Elementary', 'Intermediate', 'Upper-Int.', 'Advanced'][value] || value
}
/>
<Tooltip
formatter={(value, _name, props) => [
['Beginner', 'Elementary', 'Intermediate', 'Upper-Intermediate', 'Advanced'][Math.floor(value)],
`${props.payload.language} 레벨`
]}
/>
<Line
type="monotone"
dataKey="levelIndex"
stroke="#00C471"
strokeWidth={3}
/>
</LineChart>
</ResponsiveContainer>파일: src/pages/Analytics/AnalyticsDashboard.jsx (341 lines)
WebSocket 실시간 메트릭:
const connectToMetricsStream = () => {
const ws = new WebSocket(`wss://${window.location.host}/api/v1/analytics/stream`);
ws.onmessage = (event) => {
const data = JSON.parse(event.data);
if (data.type === 'metrics_update') {
setRealTimeMetrics(data.data);
}
};
window.metricsWs = ws;
};모니터링 지표:
- Total Requests
- Avg Response Time
- Error Rate
- P95 Response Time
- Active Connections
- CPU Usage
- Memory Usage
npm install recharts1. LineChart (시계열 데이터)
import { LineChart, Line, XAxis, YAxis, CartesianGrid, Tooltip } from 'recharts';
<LineChart data={data}>
<CartesianGrid strokeDasharray="3 3" stroke="#E7E7E7" />
<XAxis dataKey="date" />
<YAxis />
<Tooltip />
<Line type="monotone" dataKey="sessions" stroke="#00C471" strokeWidth={3} />
</LineChart>2. BarChart (비교 데이터)
import { BarChart, Bar, XAxis, YAxis, CartesianGrid, Tooltip } from 'recharts';
<BarChart data={data}>
<CartesianGrid strokeDasharray="3 3" />
<XAxis dataKey="month" />
<YAxis />
<Tooltip />
<Bar dataKey="tests" fill="#4285F4" radius={[4, 4, 0, 0]} />
</BarChart>3. PieChart (비율 데이터)
import { PieChart, Pie, Cell, Tooltip } from 'recharts';
<PieChart>
<Pie
data={data}
cx="50%"
cy="50%"
outerRadius={100}
paddingAngle={5}
dataKey="value"
>
{data.map((entry, index) => (
<Cell key={`cell-${index}`} fill={entry.color} />
))}
</Pie>
<Tooltip />
</PieChart>4. AreaChart (트렌드 데이터)
import { AreaChart, Area, XAxis, YAxis, CartesianGrid, Tooltip } from 'recharts';
<AreaChart data={data}>
<defs>
<linearGradient id="gradient" x1="0" y1="0" x2="0" y2="1">
<stop offset="5%" stopColor="#00C471" stopOpacity={0.3}/>
<stop offset="95%" stopColor="#00C471" stopOpacity={0.1}/>
</linearGradient>
</defs>
<CartesianGrid strokeDasharray="3 3" />
<XAxis dataKey="day" />
<YAxis />
<Tooltip />
<Area type="monotone" dataKey="successRate" stroke="#00C471" fill="url(#gradient)" />
</AreaChart>5. ResponsiveContainer (반응형)
import { ResponsiveContainer } from 'recharts';
<div className="h-64">
<ResponsiveContainer width="100%" height="100%">
<LineChart data={data}>
{/* ... */}
</LineChart>
</ResponsiveContainer>
</div>const safeNumber = (value, defaultValue = 0) => {
if (typeof value === 'number' && !Number.isNaN(value)) return value;
return defaultValue;
};
const totalSessions = safeNumber(metrics.totalSessions, 0);
const weeklyGrowth = safeNumber(metrics.weeklyGrowth, 0);const transformApiDataToAnalyticsData = (studyStats, sessionActivity) => {
const metrics = studyStats?.metrics || {};
return {
overview: {
totalSessions: safeNumber(metrics.totalSessions, 0),
totalMinutes: safeNumber(metrics.totalMinutes, 0),
// ...
},
sessionStats: sessionActivity?.metrics?.dailyStats || [],
languageProgress: Array.isArray(studyStats?.metrics?.languageProgress)
? studyStats.metrics.languageProgress
: [],
// ...
};
};const parseEventDate = (event) => {
const candidates = [
event?.createdDate,
event?.createdAt,
event?.timestamp,
event?.properties?.createdAt
];
for (const candidate of candidates) {
if (!candidate) continue;
const date = new Date(candidate);
if (!Number.isNaN(date.getTime())) {
return date;
}
}
return null;
};// Recharts XAxis tickFormatter
<XAxis
dataKey="date"
tickFormatter={(value) =>
new Date(value).toLocaleDateString('ko-KR', { month: 'short', day: 'numeric' })
}
/>
// Tooltip labelFormatter
<Tooltip
labelFormatter={(label) => `테스트 날짜: ${label}`}
formatter={(value) => [`${value}%`, '성공률']}
/>Step 1: 데이터 구조 정의
const chartData = [
{ month: '1월', value: 30 },
{ month: '2월', value: 45 },
{ month: '3월', value: 60 }
];Step 2: Recharts 컴포넌트 선택
- 시계열: LineChart, AreaChart
- 비교: BarChart
- 비율: PieChart
- 상관관계: ScatterChart
Step 3: 반응형 래핑
<div className="h-64">
<ResponsiveContainer width="100%" height="100%">
<YourChart data={chartData}>
{/* ... */}
</YourChart>
</ResponsiveContainer>
</div>const filterDataByTimeRange = (data, timeRange) => {
const now = new Date();
const start = new Date(now);
switch (timeRange) {
case 'day':
start.setDate(now.getDate() - 1);
break;
case 'week':
start.setDate(now.getDate() - 7);
break;
case 'month':
start.setMonth(now.getMonth() - 1);
break;
case 'year':
start.setFullYear(now.getFullYear() - 1);
break;
default:
return data;
}
return data.filter((item) => {
const date = new Date(item.date);
return date >= start;
});
};if (loading) {
return (
<div className="h-96 flex items-center justify-center">
<div className="w-8 h-8 border-4 border-[#00C471] border-t-transparent rounded-full animate-spin"></div>
<p>통계 로딩 중...</p>
</div>
);
}
if (error) {
return (
<div className="text-center">
<p>{error}</p>
<button onClick={retry}>다시 시도</button>
</div>
);
}
if (!data?.length) {
return (
<div className="text-center">
<p>아직 데이터가 없습니다.</p>
</div>
);
}Promise.all 병렬 처리:
const loadAnalyticsData = async () => {
const [studyStatsResponse, sessionActivityResponse] = await Promise.all([
getStudyStats(timeRange),
getSessionActivity(timeRange)
]);
const data = transformApiDataToAnalyticsData(studyStatsResponse, sessionActivityResponse);
setAnalyticsData(data);
};데이터 변환 최적화:
// ❌ Bad: 매번 filter + map
const filteredData = data.filter(...).map(...);
// ✅ Good: 한 번에 처리
const processedData = data.reduce((acc, item) => {
if (condition) {
acc.push({ ...item, transformed: true });
}
return acc;
}, []);ARIA 속성:
<select
value={timeRange}
onChange={(e) => setTimeRange(e.target.value)}
aria-label="시간 범위 선택"
>
<option value="day">오늘</option>
<option value="week">이번 주</option>
</select>Tooltip 한국어:
<Tooltip
contentStyle={{
backgroundColor: 'white',
border: '1px solid #E7E7E7',
borderRadius: '8px'
}}
formatter={(value) => [`${value}%`, '성공률']}
labelFormatter={(label) => `날짜: ${label}`}
/>✅ 3가지 주요 차트: 세션 활동, 매칭 통계, 레벨 테스트 ✅ 12개 Analytics API: getStudyStats, getSessionActivity, getMatchingStats, etc. ✅ Recharts 라이브러리: LineChart, BarChart, PieChart, AreaChart ✅ 반응형 디자인: ResponsiveContainer + Tailwind breakpoints ✅ Time Range 필터: day | week | month | year ✅ 실시간 메트릭: WebSocket 스트리밍 (Admin 대시보드) ✅ 안전한 데이터 처리: safeNumber(), parseEventDate(), 에러 핸들링
- AnalyticsPage.jsx (486 lines) - 메인 대시보드
- MatchingStatsChart.jsx (712 lines) - 매칭 통계
- LevelTestHistoryChart.jsx (481 lines) - 레벨 테스트
- AnalyticsDashboard.jsx (341 lines) - 서버 메트릭 (Admin)
- 안전한 데이터 처리: safeNumber, 날짜 파싱 헬퍼
- 병렬 API 호출: Promise.all
- 에러 핸들링: loading, error, empty states
- 반응형: ResponsiveContainer + Tailwind
- 접근성: ARIA 속성 + 한국어 레이블