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')}