From c905746ef226db09c51d0396f0d7c916e093b52f Mon Sep 17 00:00:00 2001 From: ddingjoo Date: Wed, 7 Jan 2026 14:31:46 +0900 Subject: [PATCH] =?UTF-8?q?[FEAT]=20=EB=8B=A8=EC=96=B4=20=ED=95=99?= =?UTF-8?q?=EC=8A=B5=20=EB=8C=80=EC=8B=9C=EB=B3=B4=EB=93=9C=20=EA=B5=AC?= =?UTF-8?q?=ED=98=84=20(#59)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - VocabDashboard 페이지 컴포넌트 생성 (#71) - 오늘의 학습 진행률 카드 구현 (#72) - 퀵 액션 카드 (통계, 시험, 단어장) 구현 (#73) - 주간 학습 캘린더 컴포넌트 구현 (#74) - 약점 단어 리스트 컴포넌트 구현 (#75) - /vocab 라우팅 및 사이드바/메인 대시보드 메뉴 추가 - 하위 메뉴: 단어 외우기, 시험 보기, 단어장 --- src/App.jsx | 19 + src/domains/vocab/pages/VocabDashboard.jsx | 416 +++++++++++++++++++++ src/layouts/MainLayout/Sidebar/index.jsx | 32 ++ 3 files changed, 467 insertions(+) create mode 100644 src/domains/vocab/pages/VocabDashboard.jsx diff --git a/src/App.jsx b/src/App.jsx index 4398e54..f6c0508 100644 --- a/src/App.jsx +++ b/src/App.jsx @@ -23,11 +23,16 @@ import { People as PeopleIcon, SmartToy as AiIcon, ChevronRight as ChevronRightIcon, + MenuBook as VocabIcon, + School as LearnIcon, + Quiz as QuizIcon, + LibraryBooks as WordListIcon, } from '@mui/icons-material' import MainLayout from './layouts/MainLayout' import FreetalkPeoplePage from './domains/freetalk/pages/FreetalkPeoplePage' import ChatRoomPage from './domains/freetalk/pages/ChatRoomPage' import ChatRoomModal from './domains/freetalk/components/ChatRoomModal' +import VocabDashboard from './domains/vocab/pages/VocabDashboard' import { useChat } from './contexts/ChatContext' import { useSettings } from './contexts/SettingsContext' @@ -59,6 +64,18 @@ function Dashboard() { { id: 'writing-practice', title: '작문연습', icon: WritingIcon, path: '/writing', description: '문법 교정 & 피드백' }, ], }, + { + id: 'vocab', + title: '단어 학습', + description: '매일 55개 단어로 어휘력 향상', + icon: VocabIcon, + color: '#9c27b0', + children: [ + { id: 'vocab-daily', title: '단어 외우기', icon: LearnIcon, path: '/vocab', description: '매일 55개 단어 학습' }, + { id: 'vocab-test', title: '시험 보기', icon: QuizIcon, path: '/vocab/test', description: '4지선다 퀴즈' }, + { id: 'vocab-words', title: '단어장', icon: WordListIcon, path: '/vocab/words', description: '전체 단어 목록' }, + ], + }, ] const handleCardHover = (modeId) => { @@ -96,6 +113,7 @@ function Dashboard() { handleCardHover(mode.id)} onMouseLeave={handleCardLeave} + onClick={() => !hasChildren && mode.path && navigate(mode.path)} sx={{ cursor: 'pointer', transition: 'all 0.3s ease', @@ -343,6 +361,7 @@ function App() { } /> } /> } /> + } /> } /> } /> diff --git a/src/domains/vocab/pages/VocabDashboard.jsx b/src/domains/vocab/pages/VocabDashboard.jsx new file mode 100644 index 0000000..eb7e2d7 --- /dev/null +++ b/src/domains/vocab/pages/VocabDashboard.jsx @@ -0,0 +1,416 @@ +import { useState, useEffect } from 'react' +import { useNavigate } from 'react-router-dom' +import { + Container, + Box, + Typography, + Grid, + Card, + CardContent, + CardActionArea, + LinearProgress, + Button, + Chip, + IconButton, + Tooltip, + CircularProgress, + Alert, +} from '@mui/material' +import { + MenuBook as VocabIcon, + PlayArrow as PlayIcon, + Assessment as StatsIcon, + Quiz as TestIcon, + LibraryBooks as WordListIcon, + VolumeUp as VolumeIcon, + Star as StarIcon, + StarBorder as StarBorderIcon, + CheckCircle as CheckIcon, + RadioButtonUnchecked as UncheckedIcon, +} from '@mui/icons-material' +import { dailyService, statsService, userWordService, voiceService } from '../services/vocabService' +import { + LEVEL_LABELS, + LEVEL_COLORS, + CATEGORY_LABELS, + DAILY_GOAL, +} from '../constants/vocabConstants' + +const TEMP_USER_ID = import.meta.env.VITE_TEMP_USER_ID || 'user1' + +export default function VocabDashboard() { + const navigate = useNavigate() + const [loading, setLoading] = useState(true) + const [error, setError] = useState(null) + const [dailyData, setDailyData] = useState(null) + const [statsData, setStatsData] = useState(null) + const [weeklyStats, setWeeklyStats] = useState([]) + const [weakWords, setWeakWords] = useState([]) + const [playingTTS, setPlayingTTS] = useState(null) + + useEffect(() => { + fetchDashboardData() + }, []) + + const fetchDashboardData = async () => { + try { + setLoading(true) + setError(null) + + const [daily, stats, weekly, weakness] = await Promise.all([ + dailyService.getWords(TEMP_USER_ID).catch(() => null), + statsService.getOverall(TEMP_USER_ID).catch(() => null), + statsService.getDaily(TEMP_USER_ID, { limit: 7 }).catch(() => null), + statsService.getWeakness(TEMP_USER_ID).catch(() => null), + ]) + + setDailyData(daily?.data) + setStatsData(stats?.data) + setWeeklyStats(weekly?.data?.dailyStats || []) + setWeakWords(weakness?.data?.weakestWords?.slice(0, 5) || []) + } catch (err) { + console.error('Dashboard fetch error:', err) + setError('데이터를 불러오는데 실패했습니다.') + } finally { + setLoading(false) + } + } + + const handlePlayTTS = async (word) => { + try { + setPlayingTTS(word.wordId) + const response = await voiceService.synthesize(word.wordId, word.english) + if (response?.data?.audioUrl) { + const audio = new Audio(response.data.audioUrl) + audio.onended = () => setPlayingTTS(null) + audio.onerror = () => setPlayingTTS(null) + await audio.play() + } + } catch (err) { + console.error('TTS error:', err) + setPlayingTTS(null) + } + } + + const handleToggleBookmark = async (word) => { + try { + await userWordService.updateTag(TEMP_USER_ID, word.wordId, { + bookmarked: !word.bookmarked, + }) + // 리스트 업데이트 + setWeakWords((prev) => + prev.map((w) => + w.wordId === word.wordId ? { ...w, bookmarked: !w.bookmarked } : w + ) + ) + } catch (err) { + console.error('Bookmark error:', err) + } + } + + if (loading) { + return ( + + + + + + ) + } + + const learnedCount = dailyData?.learnedCount || 0 + const totalWords = dailyData?.totalWords || DAILY_GOAL.TOTAL + const progress = totalWords > 0 ? (learnedCount / totalWords) * 100 : 0 + const newWordsCount = dailyData?.newWords?.length || 0 + const reviewWordsCount = dailyData?.reviewWords?.length || 0 + + return ( + + {/* 헤더 */} + + + + + 단어 학습 + + + + 매일 55개 단어로 영어 실력을 키워보세요 + + + + {error && ( + + {error} + + )} + + {/* 오늘의 학습 진행률 카드 */} + + + + 오늘의 학습 진행률 + + + + + + {learnedCount} / {totalWords} 단어 + + + {Math.round(progress)}% + + + + + + + + + 새 단어 + + + {newWordsCount} / {DAILY_GOAL.NEW_WORDS} + + + + + 복습 단어 + + + {reviewWordsCount} / {DAILY_GOAL.REVIEW_WORDS} + + + + + + + + + {/* 퀵 액션 카드 */} + + + + navigate('/vocab/stats')}> + + + + 전체 통계 + + + + {statsData?.totalWords || 0}개 + + + 학습한 단어 + + + + 정확도 {statsData?.accuracy?.toFixed(1) || 0}% + + + + + + + + + navigate('/vocab/test')}> + + + + 시험 보기 + + + + {statsData?.avgSuccessRate?.toFixed(1) || 0}% + + + 평균 성적 + + + + {statsData?.testCount || 0}회 응시 + + + + + + + + + navigate('/vocab/words')}> + + + + 단어장 + + + + {statsData?.wordStatusCounts?.MASTERED || 0}개 + + + 암기 완료 + + + + 북마크 {statsData?.bookmarkedCount || 0}개 + + + + + + + + {/* 주간 학습 현황 */} + + + + 주간 학습 현황 + + + {['월', '화', '수', '목', '금', '토', '일'].map((day, index) => { + const stat = weeklyStats[index] + const isCompleted = stat?.isCompleted + const hasProgress = stat?.learnedCount > 0 + + return ( + + + {day} + + + {isCompleted ? ( + + ) : hasProgress ? ( + + ) : ( + + )} + + + {stat?.learnedCount || '-'} + + + ) + })} + + + + + {/* 약점 단어 TOP 5 */} + {weakWords.length > 0 && ( + + + + 약점 단어 TOP 5 + + + 자주 틀리는 단어들을 집중 학습해보세요 + + + {weakWords.map((word) => ( + + + + {word.english} + + + + {word.korean} + + + + + + + handlePlayTTS(word)} + disabled={playingTTS === word.wordId} + > + + + + + handleToggleBookmark(word)}> + {word.bookmarked ? ( + + ) : ( + + )} + + + + + ))} + + + )} + + ) +} diff --git a/src/layouts/MainLayout/Sidebar/index.jsx b/src/layouts/MainLayout/Sidebar/index.jsx index 0fab047..63daf2f 100644 --- a/src/layouts/MainLayout/Sidebar/index.jsx +++ b/src/layouts/MainLayout/Sidebar/index.jsx @@ -29,6 +29,10 @@ import { SmartToy as AiIcon, ExpandMore as ExpandMoreIcon, ExpandLess as ExpandLessIcon, + MenuBook as VocabIcon, + School as LearnIcon, + Quiz as QuizIcon, + LibraryBooks as WordListIcon, } from '@mui/icons-material' const DRAWER_WIDTH = 260 @@ -80,6 +84,34 @@ const menuItems = [ }, ], }, + { + id: 'vocab', + label: '단어 학습', + icon: VocabIcon, + children: [ + { + id: 'vocab-daily', + label: '단어 외우기', + icon: LearnIcon, + path: '/vocab', + description: '매일 55개 단어 학습' + }, + { + id: 'vocab-test', + label: '시험 보기', + icon: QuizIcon, + path: '/vocab/test', + description: '4지선다 퀴즈' + }, + { + id: 'vocab-words', + label: '단어장', + icon: WordListIcon, + path: '/vocab/words', + description: '전체 단어 목록' + }, + ], + }, ], }, {