From bc58205fabe5fb390c443aa94c8af1db184a40c0 Mon Sep 17 00:00:00 2001
From: hyein Heo <128613248+hye-inA@users.noreply.github.com>
Date: Sat, 24 Jan 2026 01:31:20 +0900
Subject: [PATCH 1/4] =?UTF-8?q?[Feat]=20:=20AI=20=ED=94=84=EB=A6=AC?=
=?UTF-8?q?=ED=86=A0=ED=82=B9(Speaking)=20=EA=B8=B0=EB=8A=A5=20=EA=B5=AC?=
=?UTF-8?q?=ED=98=84=20(#195)?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
* feat : AI 말하기 연습 화면 구현
* refactor : Rest API 변경에 따른 화면 구현 변경
* feature : AI와 대화하기 UI & STT서비스 구현
---
src/App.jsx | 161 +++++-----
src/api/axios.js | 21 +-
src/api/speakingApi.js | 41 +++
.../components/SpeakingChatMessage.jsx | 163 ++++++++++
.../speaking/components/SpeakingInput.jsx | 294 ++++++++++++++++++
.../speaking/constants/speakingConstants.js | 31 ++
src/domains/speaking/hooks/useSpeaking.js | 119 +++++++
src/domains/speaking/index.js | 15 +
src/domains/speaking/pages/SpeakingPage.jsx | 222 +++++++++++++
src/domains/speaking/services/speakingApi.js | 68 ++++
.../speaking/services/speakingService.js | 29 ++
11 files changed, 1075 insertions(+), 89 deletions(-)
create mode 100644 src/api/speakingApi.js
create mode 100644 src/domains/speaking/components/SpeakingChatMessage.jsx
create mode 100644 src/domains/speaking/components/SpeakingInput.jsx
create mode 100644 src/domains/speaking/constants/speakingConstants.js
create mode 100644 src/domains/speaking/hooks/useSpeaking.js
create mode 100644 src/domains/speaking/index.js
create mode 100644 src/domains/speaking/pages/SpeakingPage.jsx
create mode 100644 src/domains/speaking/services/speakingApi.js
create mode 100644 src/domains/speaking/services/speakingService.js
diff --git a/src/App.jsx b/src/App.jsx
index dab5d55..d5f3895 100644
--- a/src/App.jsx
+++ b/src/App.jsx
@@ -1,6 +1,6 @@
-import {useState, useEffect} from 'react'
-import {Navigate, Route, Routes, useNavigate} from 'react-router-dom'
-import {Box, Button, Card, CardContent, CircularProgress, Collapse, Container, Grid, Typography} from '@mui/material'
+import { useState, useEffect } from 'react'
+import { Navigate, Route, Routes, useNavigate } from 'react-router-dom'
+import { Box, Button, Card, CardContent, CircularProgress, Collapse, Container, Grid, Typography } from '@mui/material'
import {
ChevronRight as ChevronRightIcon,
Create as WritingCategoryIcon,
@@ -19,6 +19,7 @@ import {
} from '@mui/icons-material'
import MainLayout from './layouts/MainLayout'
import FreetalkPeoplePage from './domains/freetalk/pages/FreetalkPeoplePage'
+import { SpeakingPage } from './domains/speaking'
import ChatRoomPage from './domains/freetalk/pages/ChatRoomPage'
import ChatRoomModal from './domains/freetalk/components/ChatRoomModal'
import VocabDashboard from './domains/vocab/pages/VocabDashboard'
@@ -26,8 +27,8 @@ 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 {WritingPage} from './domains/grammar'
-import {BadgeSection} from './domains/badge'
+import { WritingPage } from './domains/grammar'
+import { BadgeSection } from './domains/badge'
import CatchmindLobbyPage from './domains/games/pages/CatchmindLobbyPage'
import CatchmindWaitingPage from './domains/games/pages/CatchmindWaitingPage'
import CatchmindPlayPage from './domains/games/pages/CatchmindPlayPage'
@@ -41,8 +42,8 @@ import LoginPage from './pages/Login'
import SignUpPage from './pages/SignUp'
-function ProtectedRoute({children}) {
- const {isAuthenticated, isLoading} = useAuth()
+function ProtectedRoute({ children }) {
+ const { isAuthenticated, isLoading } = useAuth()
if (isLoading) {
return (
@@ -52,21 +53,21 @@ function ProtectedRoute({children}) {
alignItems: 'center',
justifyContent: 'center'
}}>
-
+
)
}
if (!isAuthenticated) {
- return
+ return
}
return children
}
// 이미 로그인된 경우 대시보드로
-function PublicRoute({children}) {
- const {isAuthenticated, isLoading} = useAuth()
+function PublicRoute({ children }) {
+ const { isAuthenticated, isLoading } = useAuth()
if (isLoading) {
return (
@@ -76,13 +77,13 @@ function PublicRoute({children}) {
alignItems: 'center',
justifyContent: 'center'
}}>
-
+
)
}
if (isAuthenticated) {
- return
+ return
}
return children
@@ -92,6 +93,7 @@ function PublicRoute({children}) {
function Dashboard() {
const navigate = useNavigate()
const [expandedCard, setExpandedCard] = useState(null)
+ const { t } = useSettings()
const {t, isKorean} = useSettings()
const [activityData, setActivityData] = useState(null)
const [loadingActivity, setLoadingActivity] = useState(true)
@@ -286,9 +288,9 @@ function Dashboard() {
}
return (
-
+
{/* Header */}
-
+
-
+
-
+
{t('dashboard.greeting')}
@@ -323,7 +325,7 @@ function Dashboard() {
const hasChildren = mode.children && mode.children.length > 0
return (
-
+
handleCardHover(mode.id)}
onMouseLeave={handleCardLeave}
@@ -344,8 +346,8 @@ function Dashboard() {
minHeight: isExpanded ? 'auto' : 140,
}}
>
-
-
+
+
{/* Icon */}
-
+
{/* Text */}
-
+
)}
-
+
{mode.description}
@@ -441,14 +443,14 @@ function Dashboard() {
boxShadow: '0 2px 8px -2px rgba(0,0,0,0.1)',
}}
>
-
+
+ sx={{ mb: 0.5 }}>
{child.title}
+ sx={{ lineHeight: 1.3 }}>
{child.description}
@@ -655,22 +657,13 @@ function Dashboard() {
// Placeholder Pages
function OpicPage() {
return (
-
+
OPIC Practice
Level-based training
)
}
-function FreetalkAiPage() {
- return (
-
- AI Conversation
- Free conversation with AI
-
- )
-}
-
function ReportsPage() {
const {isKorean} = useSettings()
@@ -735,9 +728,9 @@ function ReportsPage() {
}
return (
-
+
{/* 헤더 */}
-
+
-
+
@@ -765,9 +758,9 @@ function ReportsPage() {
{/* 통계 요약 카드 */}
-
-
-
+
+
+
{isKorean ? '총 학습일' : 'Study Days'}
@@ -779,8 +772,8 @@ function ReportsPage() {
-
-
+
+
{isKorean ? '학습한 단어' : 'Words Learned'}
@@ -792,8 +785,8 @@ function ReportsPage() {
-
-
+
+
{isKorean ? '테스트 완료' : 'Tests Taken'}
@@ -805,8 +798,8 @@ function ReportsPage() {
-
-
+
+
{isKorean ? '평균 점수' : 'Average Score'}
@@ -852,12 +845,12 @@ function ReportsPage() {
)}
{/* 연속 학습 */}
-
+
{isKorean ? '연속 학습 기록' : 'Study Streak'}
-
+
-
+
{/* 배지 섹션 */}
-
+
)
}
function SettingsPage() {
- const {settings, setTtsVoice, setLanguage, t} = useSettings()
+ const { settings, setTtsVoice, setLanguage, t } = useSettings()
const languageOptions = [
- {value: 'ko', label: '한국어', flag: '🇰🇷'},
- {value: 'en', label: 'English', flag: '🇺🇸'},
+ { value: 'ko', label: '한국어', flag: '🇰🇷' },
+ { value: 'en', label: 'English', flag: '🇺🇸' },
]
return (
-
-
+
+
-
+
-
+
{t('settings.title')}
@@ -939,24 +932,24 @@ function SettingsPage() {
{/* Language Settings */}
-
+
-
+
{t('settings.language')}
-
+
{t('settings.languageDesc')}
-
+
{languageOptions.map((option) => (
-
+
setLanguage(option.value)}
sx={{
@@ -974,7 +967,7 @@ function SettingsPage() {
},
}}
>
-
+
{option.flag}
{/* TTS Voice Settings */}
-
+
-
+
{t('settings.ttsVoice')}
-
+
{t('settings.ttsVoiceDesc')}
-
+
-
+
setTtsVoice('FEMALE')}
sx={{
@@ -1039,7 +1032,7 @@ function SettingsPage() {
mb: 1.5,
}}
>
- 👩
+ 👩
-
+
setTtsVoice('MALE')}
sx={{
@@ -1081,7 +1074,7 @@ function SettingsPage() {
mb: 1.5,
}}
>
- 👨
+ 👨
@@ -1120,10 +1113,10 @@ function NotFound() {
>
404
-
+
{t('notFound.title')}
-
+
{t('notFound.message')}
{/* Today's Activity Stats */}
-
+
{loadingActivity ? (
{[...Array(4)].map((_, i) => (
-
-
-
-
+
+
+
+
@@ -482,7 +483,7 @@ function Dashboard() {
) : (
{/* 오늘 외운 단어 */}
-
+
navigate('/vocab')}
sx={{
@@ -496,7 +497,7 @@ function Dashboard() {
},
}}
>
-
+
-
+
{activityData?.todayWords || 0}
@@ -527,7 +528,7 @@ function Dashboard() {
{/* 읽은 뉴스 */}
-
+
navigate('/news')}
sx={{
@@ -541,7 +542,7 @@ function Dashboard() {
},
}}
>
-
+
-
+
{activityData?.newsRead || 0}
@@ -567,7 +568,7 @@ function Dashboard() {
{/* 총 학습 단어 */}
-
+
navigate('/vocab/words')}
sx={{
@@ -581,7 +582,7 @@ function Dashboard() {
},
}}
>
-
+
-
+
{activityData?.totalWords || 0}
@@ -607,7 +608,7 @@ function Dashboard() {
{/* 연속 학습 */}
-
+
navigate('/reports')}
sx={{
@@ -621,7 +622,7 @@ function Dashboard() {
},
}}
>
-
+
-
+
{Math.max(activityData?.vocabStreak || 0, activityData?.newsStreak || 0)}
@@ -665,7 +666,7 @@ function OpicPage() {
function ReportsPage() {
- const {isKorean} = useSettings()
+ const { isKorean } = useSettings()
const [loading, setLoading] = useState(true)
const [stats, setStats] = useState({
totalStudyDays: 0,
@@ -684,7 +685,7 @@ function ReportsPage() {
setLoading(true)
const [vocabStatsRes, vocabHistoryRes, newsStatsRes] = await Promise.allSettled([
statsService.getOverall().catch(() => null),
- statsService.getDaily(null, {limit: 30}).catch(() => null),
+ statsService.getDaily(null, { limit: 30 }).catch(() => null),
getNewsStats().catch(() => null),
])
@@ -718,9 +719,9 @@ function ReportsPage() {
if (loading) {
return (
-
+
-
+
)
@@ -814,13 +815,13 @@ function ReportsPage() {
{/* 뉴스 학습 통계 */}
{(stats.newsRead > 0 || stats.newsQuizScore > 0) && (
-
+
{isKorean ? '뉴스 학습' : 'News Learning'}
-
-
+
+
{stats.newsRead}
@@ -829,8 +830,8 @@ function ReportsPage() {
-
-
+
+
{stats.newsQuizScore}%
@@ -1160,27 +1161,27 @@ function App() {
}>
- }/>
- }/>
- }/>
- }/>
- }/>
- }/>
- }/>
- }/>
- }/>
- }/>
- }/>
- }/>
- }/>
- }/>
- }/>
- }/>
- }/>
- }/>
- }/>
- }/>
- }/>
+ } />
+ } />
+ } />
+ } />
+ } />
+ } />
+ } />
+ } />
+ } />
+ } />
+ } />
+ } />
+ } />
+ } />
+ } />
+ } />
+ } />
+ } />
+ } />
+ } />
+ } />
{/* 404 */}
From 05e792887bc0c0185acf0c77623920ce4e2c1a32 Mon Sep 17 00:00:00 2001
From: hyein Heo <128613248+hye-inA@users.noreply.github.com>
Date: Sat, 24 Jan 2026 11:51:10 +0900
Subject: [PATCH 4/4] =?UTF-8?q?refactor=20:=20AI=20=EB=A7=90=ED=95=98?=
=?UTF-8?q?=EA=B8=B0=20routing=20=ED=8E=98=EC=9D=B4=EC=A7=80=20=EC=88=98?=
=?UTF-8?q?=EC=A0=95=20(#202)?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
* feat : AI 말하기 연습 화면 구현
* refactor : Rest API 변경에 따른 화면 구현 변경
* feature : AI와 대화하기 UI & STT서비스 구현
* refactor : AI 말하기 라우팅 페이지 수정
---
src/App.jsx | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/src/App.jsx b/src/App.jsx
index 45f8415..e8a4800 100644
--- a/src/App.jsx
+++ b/src/App.jsx
@@ -1164,7 +1164,7 @@ function App() {
} />
} />
} />
- } />
+ } />
} />
} />
} />