diff --git a/src/App.jsx b/src/App.jsx
index 9956acc..ee0fc63 100644
--- a/src/App.jsx
+++ b/src/App.jsx
@@ -36,6 +36,7 @@ import VocabDashboard from './domains/vocab/pages/VocabDashboard'
import DailyLearning from './domains/vocab/pages/DailyLearning'
import TestPage from './domains/vocab/pages/TestPage'
import WordListPage from './domains/vocab/pages/WordListPage'
+import StatsPage from './domains/vocab/pages/StatsPage'
import { useChat } from './contexts/ChatContext'
import { useSettings } from './contexts/SettingsContext'
@@ -368,6 +369,7 @@ function App() {
} />
} />
} />
+ } />
} />
} />
diff --git a/src/domains/vocab/pages/StatsPage.jsx b/src/domains/vocab/pages/StatsPage.jsx
new file mode 100644
index 0000000..66d5984
--- /dev/null
+++ b/src/domains/vocab/pages/StatsPage.jsx
@@ -0,0 +1,517 @@
+import { useState, useEffect } from 'react'
+import { useNavigate } from 'react-router-dom'
+import {
+ Container,
+ Box,
+ Typography,
+ IconButton,
+ Paper,
+ Grid,
+ Tabs,
+ Tab,
+ CircularProgress,
+ Alert,
+ Chip,
+ LinearProgress,
+ List,
+ ListItem,
+ ListItemText,
+ Tooltip,
+} from '@mui/material'
+import {
+ ArrowBack as BackIcon,
+ TrendingUp as TrendingUpIcon,
+ CalendarMonth as CalendarIcon,
+ Warning as WarningIcon,
+} from '@mui/icons-material'
+import { statsService, voiceService } from '../services/vocabService'
+import {
+ LEVEL_LABELS,
+ LEVEL_COLORS,
+ DIFFICULTY_LABELS,
+ VOICE_TYPES,
+} from '../constants/vocabConstants'
+
+const TEMP_USER_ID = import.meta.env.VITE_TEMP_USER_ID || 'user1'
+
+// 학습 캘린더 히트맵 컴포넌트
+function LearningCalendar({ data }) {
+ const today = new Date()
+ const startDate = new Date(today)
+ startDate.setDate(startDate.getDate() - 83) // 12주 전
+
+ const weeks = []
+ let currentDate = new Date(startDate)
+
+ // 12주 데이터 생성
+ for (let w = 0; w < 12; w++) {
+ const week = []
+ for (let d = 0; d < 7; d++) {
+ const dateStr = currentDate.toISOString().split('T')[0]
+ const dayData = data?.find(d => d.date === dateStr)
+ week.push({
+ date: dateStr,
+ count: dayData?.learnedCount || 0,
+ isToday: dateStr === today.toISOString().split('T')[0],
+ })
+ currentDate.setDate(currentDate.getDate() + 1)
+ }
+ weeks.push(week)
+ }
+
+ const getColor = (count) => {
+ if (count === 0) return '#ebedf0'
+ if (count < 20) return '#9be9a8'
+ if (count < 40) return '#40c463'
+ if (count < 55) return '#30a14e'
+ return '#216e39'
+ }
+
+ const dayLabels = ['일', '월', '화', '수', '목', '금', '토']
+
+ return (
+
+
+ {/* 요일 라벨 */}
+
+ {dayLabels.map((label, idx) => (
+
+ {idx % 2 === 1 ? label : ''}
+
+ ))}
+
+
+ {/* 히트맵 그리드 */}
+ {weeks.map((week, wIdx) => (
+
+ {week.map((day, dIdx) => (
+
+
+
+ ))}
+
+ ))}
+
+
+ {/* 범례 */}
+
+ 적음
+ {[0, 10, 30, 45, 55].map((count, idx) => (
+
+ ))}
+ 많음
+
+
+ )
+}
+
+// 취약 단어 목록 컴포넌트
+function WeakWordsList({ words, onPlayTTS, playingWordId }) {
+ if (!words || words.length === 0) {
+ return (
+
+ 취약 단어가 없습니다
+
+ )
+ }
+
+ return (
+
+ {words.map((item, index) => (
+
+
+
+ {item.english}
+
+
+
+ }
+ secondary={item.korean}
+ />
+ onPlayTTS?.(item)}
+ disabled={playingWordId === item.wordId}
+ >
+
+
+
+ ))}
+
+ )
+}
+
+// 레벨별 진행률 차트
+function LevelProgressChart({ data }) {
+ if (!data) return null
+
+ return (
+
+ {Object.entries(LEVEL_LABELS).map(([level, label]) => {
+ const levelData = data[level] || { total: 0, learned: 0 }
+ const progress = levelData.total > 0
+ ? (levelData.learned / levelData.total) * 100
+ : 0
+
+ return (
+
+
+
+ {label}
+
+
+ {levelData.learned}/{levelData.total}
+
+
+
+
+ )
+ })}
+
+ )
+}
+
+// 난이도 분포 차트
+function DifficultyChart({ data }) {
+ if (!data) return null
+
+ const total = Object.values(data).reduce((sum, val) => sum + val, 0)
+
+ const colors = {
+ EASY: '#4caf50',
+ NORMAL: '#2196f3',
+ HARD: '#ff9800',
+ }
+
+ return (
+
+ {/* 막대 그래프 */}
+
+ {Object.entries(DIFFICULTY_LABELS).map(([key, label]) => {
+ const count = data[key] || 0
+ const height = total > 0 ? (count / total) * 100 : 0
+
+ return (
+
+
+ {count}
+
+
+
+ {label}
+
+
+ )
+ })}
+
+
+ )
+}
+
+// 통계 요약 카드
+function StatCard({ title, value, subtitle, icon: Icon, color }) {
+ return (
+
+
+
+
+ {title}
+
+
+ {value}
+
+ {subtitle && (
+
+ {subtitle}
+
+ )}
+
+ {Icon && (
+
+
+
+ )}
+
+
+ )
+}
+
+export default function StatsPage() {
+ const navigate = useNavigate()
+ const [tab, setTab] = useState(0) // 0: 일간, 1: 주간, 2: 월간
+ const [loading, setLoading] = useState(true)
+ const [error, setError] = useState(null)
+
+ // 통계 데이터
+ const [overviewStats, setOverviewStats] = useState(null)
+ const [calendarData, setCalendarData] = useState([])
+ const [weakWords, setWeakWords] = useState([])
+ const [levelProgress, setLevelProgress] = useState(null)
+ const [difficultyDist, setDifficultyDist] = useState(null)
+
+ // TTS
+ const [playingWordId, setPlayingWordId] = useState(null)
+
+ useEffect(() => {
+ fetchAllStats()
+ }, [])
+
+ useEffect(() => {
+ fetchPeriodStats()
+ }, [tab])
+
+ const fetchAllStats = async () => {
+ try {
+ setLoading(true)
+ setError(null)
+
+ const [overviewRes, dailyRes, weakRes] = await Promise.all([
+ statsService.getOverall(TEMP_USER_ID),
+ statsService.getDaily(TEMP_USER_ID, { limit: 84 }),
+ statsService.getWeakness(TEMP_USER_ID),
+ ])
+
+ setOverviewStats(overviewRes?.data)
+ setCalendarData(dailyRes?.data?.dailyStats || [])
+ setWeakWords(weakRes?.data?.weakWords || [])
+ setLevelProgress(overviewRes?.data?.levelProgress)
+ setDifficultyDist(overviewRes?.data?.difficultyDistribution)
+ } catch (err) {
+ console.error('Fetch stats error:', err)
+ setError('통계를 불러오는데 실패했습니다.')
+ } finally {
+ setLoading(false)
+ }
+ }
+
+ const fetchPeriodStats = async () => {
+ // 기간별 통계는 getDaily로 처리
+ try {
+ const limits = [7, 30, 90] // 일간, 주간, 월간
+ const response = await statsService.getDaily(TEMP_USER_ID, {
+ limit: limits[tab],
+ })
+ // 기간별 통계 처리
+ } catch (err) {
+ console.error('Period stats error:', err)
+ }
+ }
+
+ const handlePlayTTS = async (word) => {
+ if (playingWordId) return
+
+ try {
+ setPlayingWordId(word.wordId)
+ const response = await voiceService.synthesize({
+ text: word.english,
+ voiceType: VOICE_TYPES.FEMALE,
+ })
+
+ if (response?.data?.audioUrl) {
+ const audio = new Audio(response.data.audioUrl)
+ audio.onended = () => setPlayingWordId(null)
+ audio.onerror = () => setPlayingWordId(null)
+ await audio.play()
+ } else {
+ setPlayingWordId(null)
+ }
+ } catch (err) {
+ console.error('TTS error:', err)
+ setPlayingWordId(null)
+ }
+ }
+
+ if (loading) {
+ return (
+
+
+
+
+
+ )
+ }
+
+ return (
+
+ {/* 헤더 */}
+
+ navigate('/vocab')}>
+
+
+
+ 학습 통계
+
+
+
+ {error && (
+ setError(null)}>
+ {error}
+
+ )}
+
+ {/* 기간 탭 */}
+ setTab(v)}
+ sx={{ mb: 3 }}
+ variant="fullWidth"
+ >
+
+
+
+
+
+ {/* 요약 카드 */}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {/* 학습 캘린더 */}
+
+
+ 학습 기록
+
+
+
+
+ {/* 레벨별 진행률 */}
+
+
+ 레벨별 진행률
+
+
+
+
+ {/* 난이도 분포 */}
+
+
+ 난이도 분포
+
+
+
+
+ {/* 취약 단어 */}
+
+
+
+ 취약 단어 TOP 10
+
+ navigate('/vocab/daily?mode=weak')}
+ />
+
+
+
+
+ )
+}