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] =?UTF-8?q?[Feat]=20:=20AI=20=ED=94=84=EB=A6=AC=ED=86=A0?=
=?UTF-8?q?=ED=82=B9(Speaking)=20=EA=B8=B0=EB=8A=A5=20=EA=B5=AC=ED=98=84?=
=?UTF-8?q?=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')}