diff --git a/src/App.jsx b/src/App.jsx
index f6c0508..483d766 100644
--- a/src/App.jsx
+++ b/src/App.jsx
@@ -33,6 +33,7 @@ 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 DailyLearning from './domains/vocab/pages/DailyLearning'
import { useChat } from './contexts/ChatContext'
import { useSettings } from './contexts/SettingsContext'
@@ -362,6 +363,7 @@ function App() {
} />
} />
} />
+ } />
} />
} />
diff --git a/src/domains/vocab/components/FlashCard.jsx b/src/domains/vocab/components/FlashCard.jsx
new file mode 100644
index 0000000..37e9f72
--- /dev/null
+++ b/src/domains/vocab/components/FlashCard.jsx
@@ -0,0 +1,137 @@
+import { Box, Typography, IconButton, Chip } from '@mui/material'
+import { VolumeUp as VolumeIcon } from '@mui/icons-material'
+import { LEVEL_LABELS, LEVEL_COLORS, CATEGORY_LABELS } from '../constants/vocabConstants'
+
+export default function FlashCard({ word, isFlipped, onFlip, onPlayTTS, isPlayingTTS }) {
+ if (!word) return null
+
+ return (
+
+
+ {/* 앞면 - 영어 */}
+
+
+ {word.english}
+
+
+ {
+ e.stopPropagation()
+ onPlayTTS?.()
+ }}
+ disabled={isPlayingTTS}
+ sx={{ mb: 2 }}
+ >
+
+
+
+ {word.example && (
+
+ "{word.example}"
+
+ )}
+
+
+ 탭하여 뜻 보기
+
+
+
+ {/* 뒷면 - 한국어 */}
+
+
+ {word.korean}
+
+
+
+
+
+
+
+
+ 탭하여 영어 보기
+
+
+
+
+ )
+}
diff --git a/src/domains/vocab/pages/DailyLearning.jsx b/src/domains/vocab/pages/DailyLearning.jsx
new file mode 100644
index 0000000..7100655
--- /dev/null
+++ b/src/domains/vocab/pages/DailyLearning.jsx
@@ -0,0 +1,383 @@
+import { useState, useEffect, useCallback } from 'react'
+import { useNavigate } from 'react-router-dom'
+import {
+ Container,
+ Box,
+ Typography,
+ LinearProgress,
+ Button,
+ IconButton,
+ Tooltip,
+ CircularProgress,
+ Alert,
+ Paper,
+ Switch,
+ FormControlLabel,
+ ToggleButton,
+ ToggleButtonGroup,
+} from '@mui/material'
+import {
+ ArrowBack as BackIcon,
+ VolumeUp as VolumeIcon,
+ Star as StarIcon,
+ StarBorder as StarBorderIcon,
+ SkipNext as SkipIcon,
+ Check as CheckIcon,
+ Close as CloseIcon,
+ Celebration as CelebrationIcon,
+} from '@mui/icons-material'
+import FlashCard from '../components/FlashCard'
+import { dailyService, userWordService, voiceService } from '../services/vocabService'
+import { DIFFICULTY, DIFFICULTY_LABELS } from '../constants/vocabConstants'
+
+const TEMP_USER_ID = import.meta.env.VITE_TEMP_USER_ID || 'user1'
+
+export default function DailyLearning() {
+ const navigate = useNavigate()
+ const [loading, setLoading] = useState(true)
+ const [error, setError] = useState(null)
+ const [words, setWords] = useState([])
+ const [currentIndex, setCurrentIndex] = useState(0)
+ const [isFlipped, setIsFlipped] = useState(false)
+ const [learnedIds, setLearnedIds] = useState(new Set())
+ const [autoPlayTTS, setAutoPlayTTS] = useState(false)
+ const [isPlayingTTS, setIsPlayingTTS] = useState(false)
+ const [isCompleted, setIsCompleted] = useState(false)
+ const [results, setResults] = useState({ correct: 0, incorrect: 0 })
+
+ useEffect(() => {
+ fetchDailyWords()
+ }, [])
+
+ const fetchDailyWords = async () => {
+ try {
+ setLoading(true)
+ setError(null)
+ const response = await dailyService.getWords(TEMP_USER_ID)
+ const allWords = [
+ ...(response?.data?.newWords || []),
+ ...(response?.data?.reviewWords || []),
+ ]
+ setWords(allWords)
+
+ // 이미 학습한 단어 체크
+ const learnedCount = response?.data?.learnedCount || 0
+ if (learnedCount > 0 && learnedCount < allWords.length) {
+ const learned = new Set(allWords.slice(0, learnedCount).map(w => w.wordId))
+ setLearnedIds(learned)
+ setCurrentIndex(learnedCount)
+ }
+
+ if (response?.data?.isCompleted) {
+ setIsCompleted(true)
+ }
+ } catch (err) {
+ console.error('Fetch daily words error:', err)
+ setError('단어를 불러오는데 실패했습니다.')
+ } finally {
+ setLoading(false)
+ }
+ }
+
+ const currentWord = words[currentIndex]
+ const progress = words.length > 0 ? (learnedIds.size / words.length) * 100 : 0
+
+ const playTTS = useCallback(async (word) => {
+ if (!word || isPlayingTTS) return
+ try {
+ setIsPlayingTTS(true)
+ const response = await voiceService.synthesize(word.wordId, word.english)
+ if (response?.data?.audioUrl) {
+ const audio = new Audio(response.data.audioUrl)
+ audio.onended = () => setIsPlayingTTS(false)
+ audio.onerror = () => setIsPlayingTTS(false)
+ await audio.play()
+ } else {
+ setIsPlayingTTS(false)
+ }
+ } catch (err) {
+ console.error('TTS error:', err)
+ setIsPlayingTTS(false)
+ }
+ }, [isPlayingTTS])
+
+ useEffect(() => {
+ if (autoPlayTTS && currentWord && !isFlipped) {
+ playTTS(currentWord)
+ }
+ }, [currentIndex, autoPlayTTS])
+
+ const handleFlip = () => {
+ setIsFlipped(!isFlipped)
+ }
+
+ const handleAnswer = async (isCorrect) => {
+ if (!currentWord) return
+
+ try {
+ // API 호출
+ await userWordService.update(TEMP_USER_ID, currentWord.wordId, isCorrect)
+
+ // 결과 업데이트
+ setResults(prev => ({
+ ...prev,
+ [isCorrect ? 'correct' : 'incorrect']: prev[isCorrect ? 'correct' : 'incorrect'] + 1
+ }))
+
+ // 학습 완료 표시
+ setLearnedIds(prev => new Set([...prev, currentWord.wordId]))
+
+ // 다음 카드로 이동
+ moveToNext()
+ } catch (err) {
+ console.error('Answer update error:', err)
+ }
+ }
+
+ const handleSkip = () => {
+ moveToNext()
+ }
+
+ const moveToNext = () => {
+ setIsFlipped(false)
+ if (currentIndex < words.length - 1) {
+ setCurrentIndex(prev => prev + 1)
+ } else {
+ setIsCompleted(true)
+ }
+ }
+
+ const handleToggleBookmark = async () => {
+ if (!currentWord) return
+ try {
+ const newBookmarked = !currentWord.bookmarked
+ await userWordService.updateTag(TEMP_USER_ID, currentWord.wordId, {
+ bookmarked: newBookmarked,
+ })
+ // 로컬 상태 업데이트
+ setWords(prev =>
+ prev.map(w =>
+ w.wordId === currentWord.wordId ? { ...w, bookmarked: newBookmarked } : w
+ )
+ )
+ } catch (err) {
+ console.error('Bookmark error:', err)
+ }
+ }
+
+ const handleSetDifficulty = async (difficulty) => {
+ if (!currentWord || !difficulty) return
+ try {
+ await userWordService.updateTag(TEMP_USER_ID, currentWord.wordId, {
+ difficulty,
+ })
+ setWords(prev =>
+ prev.map(w =>
+ w.wordId === currentWord.wordId ? { ...w, difficulty } : w
+ )
+ )
+ } catch (err) {
+ console.error('Difficulty error:', err)
+ }
+ }
+
+ const handleRestart = () => {
+ setCurrentIndex(0)
+ setLearnedIds(new Set())
+ setIsFlipped(false)
+ setIsCompleted(false)
+ setResults({ correct: 0, incorrect: 0 })
+ }
+
+ if (loading) {
+ return (
+
+
+
+
+
+ )
+ }
+
+ if (error) {
+ return (
+
+
+ {error}
+
+
+
+ )
+ }
+
+ // 학습 완료 화면
+ if (isCompleted) {
+ const totalAnswered = results.correct + results.incorrect
+ const accuracy = totalAnswered > 0 ? (results.correct / totalAnswered) * 100 : 0
+
+ return (
+
+
+
+
+ 학습 완료!
+
+
+ 오늘의 학습을 완료했습니다
+
+
+
+ 학습 결과
+
+
+
+ {results.correct}
+
+ 정답
+
+
+
+ {results.incorrect}
+
+ 오답
+
+
+
+ 정확도: {accuracy.toFixed(1)}%
+
+
+
+
+
+
+
+
+
+ )
+ }
+
+ return (
+
+ {/* 헤더 */}
+
+ navigate('/vocab')}>
+
+
+
+ 오늘의 학습 ({currentIndex + 1}/{words.length})
+
+ setAutoPlayTTS(e.target.checked)}
+ size="small"
+ />
+ }
+ label={}
+ />
+
+
+ {/* 진행률 바 */}
+
+
+
+ 진행률
+
+
+ {Math.round(progress)}%
+
+
+
+
+
+ {/* 플래시카드 */}
+
+ playTTS(currentWord)}
+ isPlayingTTS={isPlayingTTS}
+ />
+
+
+ {/* 정답/오답 버튼 */}
+
+ }
+ onClick={() => handleAnswer(false)}
+ sx={{ flex: 1, maxWidth: 160, py: 1.5 }}
+ >
+ 모르겠어요
+
+ }
+ onClick={() => handleAnswer(true)}
+ sx={{ flex: 1, maxWidth: 160, py: 1.5 }}
+ >
+ 알고있어요
+
+
+
+ {/* 액션 바 */}
+
+
+
+ {currentWord?.bookmarked ? (
+
+ ) : (
+
+ )}
+
+
+
+ handleSetDifficulty(val)}
+ size="small"
+ >
+ {Object.entries(DIFFICULTY_LABELS).map(([key, label]) => (
+
+ {label}
+
+ ))}
+
+
+
+
+
+
+
+
+
+ )
+}