Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 25 additions & 1 deletion src/App.jsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { useState, useEffect } from 'react'
import { Navigate, Route, Routes, useNavigate } from 'react-router-dom'
import { useDispatch } from 'react-redux'
import { Box, Button, Card, CardContent, CircularProgress, Collapse, Container, Grid, Typography } from '@mui/material'
import {
ChevronRight as ChevronRightIcon,
Expand Down Expand Up @@ -32,6 +33,9 @@ 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'
import WordchainLobbyPage from './domains/games/pages/WordchainLobbyPage'
import WordchainWaitingPage from './domains/games/pages/WordchainWaitingPage'
import WordchainPlayPage from './domains/games/pages/WordchainPlayPage'
import { NewsListPage, NewsDetailPage, NewsQuizPage, NewsWordsPage, NewsStatsPage } from './domains/news'
import { dailyService, statsService } from './domains/vocab/services/vocabService'
import { getNewsStats, getDashboardStats } from './domains/news/services/newsService'
Expand All @@ -40,6 +44,7 @@ import { useSettings } from './contexts/SettingsContext'
import { useAuth } from './contexts/AuthContext'
import LoginPage from './pages/Login'
import SignUpPage from './pages/SignUp'
import { fetchMyProfile } from "./domains/profile/store/profileSlice";


function ProtectedRoute({ children }) {
Expand Down Expand Up @@ -269,6 +274,13 @@ function Dashboard() {
path: '/games/catchmind',
description: t('games.catchmindDesc')
},
{
id: 'wordchain',
title: t('games.wordchainTitle'),
icon: GameIcon,
path: '/games/wordchain',
description: t('games.wordchainDesc')
},
],
},
]
Expand Down Expand Up @@ -1131,8 +1143,17 @@ function NotFound() {
}

function App() {
const dispatch = useDispatch()
const { isAuthenticated } = useAuth()
const { activeRoom, closeChatRoom } = useChat()

useEffect(() => {
if (isAuthenticated) {
// redux로 프로필 정보 API(/users/profile/me) 호출
dispatch(fetchMyProfile())
}
}, [isAuthenticated, dispatch])

const handleRefreshRooms = () => {
// Refresh rooms list after leaving a room
}
Expand Down Expand Up @@ -1164,7 +1185,7 @@ function App() {
<Route path="/dashboard" element={<Dashboard />} />
<Route path="/opic" element={<OpicPage />} />
<Route path="/freetalk/people" element={<FreetalkPeoplePage />} />
<Route path="/freetalk/ai" element={<FreetalkAiPage />} />
<Route path="/freetalk/ai" element={<SpeakingPage />} />
<Route path="/writing" element={<WritingPage />} />
<Route path="/vocab" element={<VocabDashboard />} />
<Route path="/vocab/daily" element={<DailyLearning />} />
Expand All @@ -1176,6 +1197,9 @@ function App() {
<Route path="/games/catchmind" element={<CatchmindLobbyPage />} />
<Route path="/games/catchmind/:roomId/waiting" element={<CatchmindWaitingPage />} />
<Route path="/games/catchmind/:roomId/play" element={<CatchmindPlayPage />} />
<Route path="/games/wordchain" element={<WordchainLobbyPage />} />
<Route path="/games/wordchain/:roomId/waiting" element={<WordchainWaitingPage />} />
<Route path="/games/wordchain/:roomId/play" element={<WordchainPlayPage />} />
<Route path="/news" element={<NewsListPage />} />
<Route path="/news/:articleId" element={<NewsDetailPage />} />
<Route path="/news/:articleId/quiz" element={<NewsQuizPage />} />
Expand Down
15 changes: 13 additions & 2 deletions src/api/axios.js
Original file line number Diff line number Diff line change
Expand Up @@ -42,18 +42,29 @@ api.interceptors.response.use(
// 401 에러 && 재시도하지 않을 경우
if (error.response?.status === 401 && !originalRequest._retry) {
originalRequest._retry = true
console.log('[Axios] 401 detected, attempting token refresh...')

try {
// 토큰 갱신 시도
// 토큰 갱신 시도 (API Gateway는 idToken을 기대)
const session = await fetchAuthSession({ forceRefresh: true })
const newToken = session.tokens?.accessToken?.toString()
console.log('[Axios] Session fetched:', !!session, 'tokens:', !!session?.tokens)
const newToken = session.tokens?.idToken?.toString()
console.log('[Axios] New token obtained:', !!newToken, 'length:', newToken?.length)

if (newToken) {
localStorage.setItem('accessToken', newToken)
originalRequest.headers['Authorization'] = `Bearer ${newToken}`
console.log('[Axios] Retrying request with new token')
return api(originalRequest)
} else {
console.log('[Axios] No token received, redirecting to login')
localStorage.removeItem('accessToken')
if (window.location.pathname !== '/login') {
window.location.href = '/login'
}
}
} catch (refreshError) {
console.error('[Axios] Token refresh failed:', refreshError)
try {
await signOut()
} catch (e) {
Expand Down
41 changes: 11 additions & 30 deletions src/api/badgeApi.js
Original file line number Diff line number Diff line change
@@ -1,32 +1,13 @@
import axios from 'axios'

const badgeApi = axios.create({
baseURL: import.meta.env.VITE_BADGE_API_URL || import.meta.env.VITE_API_URL,
timeout: 10000,
headers: {
'Content-Type': 'application/json',
},
})

// Request interceptor for JWT token
badgeApi.interceptors.request.use(
(config) => {
const token = localStorage.getItem('accessToken')
if (token) {
config.headers.Authorization = `Bearer ${token}`
}
return config
},
(error) => Promise.reject(error)
)

// Response interceptor for error handling
badgeApi.interceptors.response.use(
(response) => response.data,
(error) => {
console.error('Badge API Error:', error.response?.data || error.message)
return Promise.reject(error)
}
)
import api from './axios'

// 공통 axios 인스턴스 사용 (토큰 갱신 로직 포함)
// response.data 자동 추출을 위한 래퍼
const badgeApi = {
get: (url, config) => api.get(url, config).then(res => res.data),
post: (url, data, config) => api.post(url, data, config).then(res => res.data),
put: (url, data, config) => api.put(url, data, config).then(res => res.data),
patch: (url, data, config) => api.patch(url, data, config).then(res => res.data),
delete: (url, config) => api.delete(url, config).then(res => res.data),
}

export default badgeApi
49 changes: 11 additions & 38 deletions src/api/chatApi.js
Original file line number Diff line number Diff line change
@@ -1,40 +1,13 @@
import axios from 'axios'

const chatApi = axios.create({
baseURL: import.meta.env.VITE_CHAT_API_URL || import.meta.env.VITE_API_URL,
timeout: 10000,
headers: {
'Content-Type': 'application/json',
},
})

// Request interceptor - JWT 토큰 자동 추가
chatApi.interceptors.request.use(
(config) => {
const token = localStorage.getItem('accessToken')
if (token) {
config.headers.Authorization = `Bearer ${token}`
}
return config
},
(error) => {
return Promise.reject(error)
}
)

// Response interceptor - 401 에러 시 로그인 페이지로 이동
chatApi.interceptors.response.use(
(response) => response.data,
(error) => {
console.error('Chat API Error:', error.response?.data || error.message)

if (error.response?.status === 401) {
localStorage.removeItem('accessToken')
window.location.href = '/login'
}

return Promise.reject(error)
}
)
import api from './axios'

// 공통 axios 인스턴스 사용 (토큰 갱신 로직 포함)
// response.data 자동 추출을 위한 래퍼
const chatApi = {
get: (url, config) => api.get(url, config).then(res => res.data),
post: (url, data, config) => api.post(url, data, config).then(res => res.data),
put: (url, data, config) => api.put(url, data, config).then(res => res.data),
patch: (url, data, config) => api.patch(url, data, config).then(res => res.data),
delete: (url, config) => api.delete(url, config).then(res => res.data),
}

export default chatApi
51 changes: 13 additions & 38 deletions src/api/grammarApi.js
Original file line number Diff line number Diff line change
@@ -1,40 +1,15 @@
import axios from 'axios'

const grammarApi = axios.create({
baseURL: import.meta.env.VITE_GRAMMAR_API_URL || import.meta.env.VITE_API_URL,
timeout: 30000,
headers: {
'Content-Type': 'application/json',
},
})

// Request interceptor
grammarApi.interceptors.request.use(
(config) => {
const token = localStorage.getItem('accessToken')
if (token) {
config.headers.Authorization = `Bearer ${token}`
}
return config
},
(error) => {
return Promise.reject(error)
}
)

// Response interceptor
grammarApi.interceptors.response.use(
(response) => response.data,
(error) => {
console.error('Grammar API Error:', error.response?.data || error.message)

if (error.response?.status === 401) {
localStorage.removeItem('accessToken')
window.location.href = '/login'
}

return Promise.reject(error)
}
)
import api from './axios'

// 공통 axios 인스턴스 사용 (토큰 갱신 로직 포함)
// Grammar API는 AI 처리로 인해 timeout 30초 필요
const GRAMMAR_TIMEOUT = 30000

const grammarApi = {
get: (url, config) => api.get(url, { timeout: GRAMMAR_TIMEOUT, ...config }).then(res => res.data),
post: (url, data, config) => api.post(url, data, { timeout: GRAMMAR_TIMEOUT, ...config }).then(res => res.data),
put: (url, data, config) => api.put(url, data, { timeout: GRAMMAR_TIMEOUT, ...config }).then(res => res.data),
patch: (url, data, config) => api.patch(url, data, { timeout: GRAMMAR_TIMEOUT, ...config }).then(res => res.data),
delete: (url, config) => api.delete(url, { timeout: GRAMMAR_TIMEOUT, ...config }).then(res => res.data),
}

export default grammarApi
52 changes: 13 additions & 39 deletions src/api/speakingApi.js
Original file line number Diff line number Diff line change
@@ -1,41 +1,15 @@
import axios from 'axios'

// Bedrock/Polly 사용으로 응답 시간(timeout) 제한 늘림
const speakingApi = axios.create({
baseURL: import.meta.env.VITE_API_URL,
timeout: 30000, // 30초
headers: {
'Content-Type': 'application/json',
},
})

// Request interceptor - JWT 토큰 자동 추가
speakingApi.interceptors.request.use(
(config) => {
const token = localStorage.getItem('accessToken')
if (token) {
config.headers.Authorization = `Bearer ${token}`
}
return config
},
(error) => {
return Promise.reject(error)
}
)

// Response interceptor - 401 에러 처리 및 데이터 추출
speakingApi.interceptors.response.use(
(response) => response.data, // response.data를 바로 반환하도록 설정
(error) => {
console.error('Speaking API Error:', error.response?.data || error.message)

if (error.response?.status === 401) {
localStorage.removeItem('accessToken')
window.location.href = '/login'
}

return Promise.reject(error)
}
)
import api from './axios'

// 공통 axios 인스턴스 사용 (토큰 갱신 로직 포함)
// Bedrock/Polly 사용으로 timeout 30초 필요
const SPEAKING_TIMEOUT = 30000

const speakingApi = {
get: (url, config) => api.get(url, { timeout: SPEAKING_TIMEOUT, ...config }).then(res => res.data),
post: (url, data, config) => api.post(url, data, { timeout: SPEAKING_TIMEOUT, ...config }).then(res => res.data),
put: (url, data, config) => api.put(url, data, { timeout: SPEAKING_TIMEOUT, ...config }).then(res => res.data),
patch: (url, data, config) => api.patch(url, data, { timeout: SPEAKING_TIMEOUT, ...config }).then(res => res.data),
delete: (url, config) => api.delete(url, { timeout: SPEAKING_TIMEOUT, ...config }).then(res => res.data),
}

export default speakingApi
49 changes: 11 additions & 38 deletions src/api/vocabApi.js
Original file line number Diff line number Diff line change
@@ -1,40 +1,13 @@
import axios from 'axios'

const vocabApi = axios.create({
baseURL: import.meta.env.VITE_VOCAB_API_URL || import.meta.env.VITE_API_URL,
timeout: 10000,
headers: {
'Content-Type': 'application/json',
},
})

// Request interceptor - JWT 토큰 자동 추가
vocabApi.interceptors.request.use(
(config) => {
const token = localStorage.getItem('accessToken')
if (token) {
config.headers.Authorization = `Bearer ${token}`
}
return config
},
(error) => {
return Promise.reject(error)
}
)

// Response interceptor - 401 에러 시 로그인 페이지로 이동
vocabApi.interceptors.response.use(
(response) => response.data,
(error) => {
console.error('Vocab API Error:', error.response?.data || error.message)

if (error.response?.status === 401) {
localStorage.removeItem('accessToken')
window.location.href = '/login'
}

return Promise.reject(error)
}
)
import api from './axios'

// 공통 axios 인스턴스 사용 (토큰 갱신 로직 포함)
// response.data 자동 추출을 위한 래퍼
const vocabApi = {
get: (url, config) => api.get(url, config).then(res => res.data),
post: (url, data, config) => api.post(url, data, config).then(res => res.data),
put: (url, data, config) => api.put(url, data, config).then(res => res.data),
patch: (url, data, config) => api.patch(url, data, config).then(res => res.data),
delete: (url, config) => api.delete(url, config).then(res => res.data),
}

export default vocabApi
4 changes: 2 additions & 2 deletions src/aws-config.js
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
const awsConfig = {
Auth: {
Cognito: {
userPoolId: 'ap-northeast-2_ezDwzFCzR',
userPoolClientId: '4ns077jcr1pkue2vvisr6qdpu5',
userPoolId: import.meta.env.VITE_COGNITO_POOL_ID,
userPoolClientId: import.meta.env.VITE_COGNITO_CLIENT_ID,

loginWith: {
email: true,
Expand Down
Loading