diff --git a/frontend/src/api/authApi.ts b/frontend/src/api/authApi.ts index 993f63a..09c5d47 100644 --- a/frontend/src/api/authApi.ts +++ b/frontend/src/api/authApi.ts @@ -1,20 +1,8 @@ import api from '@/api/axiosConfig'; import { AxiosError } from 'axios'; -export const reissueTokenApi = async () => { - try { - const response = await api.post('/api/auth/reissue', null, { - withCredentials: true, - }); - return response.data; - } catch (error) { - if (error instanceof AxiosError) { - console.error('토큰 재발급 실패:', error.response?.data?.message || '알 수 없는 오류'); - } else { - console.error('알 수 없는 오류가 발생했습니다.'); - } - throw error; - } +export const reissueTokenApi = () => { + return api.post('/api/auth/reissue', null, { withCredentials: true }).then((response) => response.data); }; export const fetchProfileApi = async () => { diff --git a/frontend/src/api/axiosConfig.ts b/frontend/src/api/axiosConfig.ts index ab7892d..8509475 100644 --- a/frontend/src/api/axiosConfig.ts +++ b/frontend/src/api/axiosConfig.ts @@ -1,4 +1,4 @@ -import axios from 'axios'; +import axios, { AxiosError, AxiosResponse, InternalAxiosRequestConfig } from 'axios'; import useAuthStore from '@/stores/useAuthStore'; const baseURL = 'https://j11s002.p.ssafy.io'; @@ -8,56 +8,83 @@ const api = axios.create({ withCredentials: true, }); -api.interceptors.request.use((config) => { +let isTokenRefreshing = false; + +type FailedRequest = { + resolve: (value?: string | undefined) => void; + reject: (reason?: unknown) => void; +}; + +let failedQueue: FailedRequest[] = []; + +const processQueue = (error: Error | null, token: string | undefined = undefined): void => { + failedQueue.forEach((prom) => { + if (error) { + prom.reject(error); + } else { + prom.resolve(token); + } + }); + failedQueue = []; +}; + +api.interceptors.request.use((config: InternalAxiosRequestConfig) => { const token = localStorage.getItem('accessToken'); - if (token) { + if (token && config.headers) { config.headers.Authorization = `Bearer ${token}`; } return config; }); api.interceptors.response.use( - (response) => response, - async (error) => { - const originalRequest = error.config; - - if (error.response?.status === 401 && originalRequest._retry) { - console.error('토큰 재발급 중 401 오류가 발생했습니다. 로그아웃 처리합니다.'); - alert('세션이 만료되었습니다. 다시 로그인해 주세요.'); - useAuthStore.getState().clearAuth(); - window.location.href = '/'; - return Promise.reject(error); - } + (response: AxiosResponse) => response, + async (error: AxiosError) => { + const originalRequest = error.config as InternalAxiosRequestConfig & { _retry?: boolean }; if (error.response?.status === 401 && !originalRequest._retry) { + if (isTokenRefreshing) { + return new Promise((resolve, reject) => { + failedQueue.push({ resolve, reject }); + }) + .then((token) => { + if (token && originalRequest.headers) { + originalRequest.headers.Authorization = `Bearer ${token}`; + } + return api(originalRequest); + }) + .catch((err) => Promise.reject(err)); + } + originalRequest._retry = true; + isTokenRefreshing = true; - try { - const response = await api.post('/api/auth/reissue', null, { - withCredentials: true, - }); + return api + .post('/api/auth/reissue', null, { withCredentials: true }) + .then((response) => { + const newAccessToken = response.data?.data?.accessToken; + if (!newAccessToken) { + throw new Error('Invalid token reissue response'); + } - if (!response.data?.data?.accessToken) { - alert('잘못된 토큰 재발급 응답입니다. 다시 로그인해 주세요.'); + useAuthStore.getState().setLoggedIn(true, newAccessToken); + localStorage.setItem('accessToken', newAccessToken); + processQueue(null, newAccessToken); + + if (originalRequest.headers) { + originalRequest.headers.Authorization = `Bearer ${newAccessToken}`; + } + return api(originalRequest); + }) + .catch((reissueError: Error) => { + processQueue(reissueError, undefined); + console.error('토큰 재발급 실패:', reissueError); useAuthStore.getState().clearAuth(); window.location.href = '/'; - return Promise.reject(new Error('Invalid token reissue response')); - } - - const newAccessToken = response.data.data.accessToken; - - useAuthStore.getState().setLoggedIn(true, newAccessToken); - localStorage.setItem('accessToken', newAccessToken); - originalRequest.headers.Authorization = `Bearer ${newAccessToken}`; - - return api(originalRequest); - } catch (reissueError) { - console.error('토큰 재발급 실패:', reissueError); - alert('토큰 재발급에 실패했습니다. 다시 로그인해 주세요.'); - useAuthStore.getState().clearAuth(); - window.location.href = '/'; - return Promise.reject(reissueError); - } + return Promise.reject(reissueError); + }) + .finally(() => { + isTokenRefreshing = false; + }); } if (error.response?.status === 400) { diff --git a/frontend/src/components/Home/index.tsx b/frontend/src/components/Home/index.tsx index f275bf0..dc941d0 100644 --- a/frontend/src/components/Home/index.tsx +++ b/frontend/src/components/Home/index.tsx @@ -3,7 +3,7 @@ import { useNavigate } from 'react-router-dom'; import GoogleLogo from '@/assets/icons/web_neutral_rd_ctn@1x.png'; import useAuthStore from '@/stores/useAuthStore'; import { Button } from '@/components/ui/button'; -import { fetchProfileApi } from '@/api/authApi'; +import { fetchProfileApi, reissueTokenApi } from '@/api/authApi'; const DOMAIN = 'https://j11s002.p.ssafy.io'; @@ -42,6 +42,19 @@ export default function Home() { navigate('/browse'); }; + const handleReissueToken = async () => { + try { + const response = await reissueTokenApi(); + console.log('토큰 재발급 성공:', response); + alert('토큰 재발급 성공! 새로운 액세스 토큰을 콘솔에서 확인하세요.'); + } catch (error) { + console.error('토큰 재발급 실패:', error); + alert('토큰 재발급에 실패했습니다. 다시 시도해 주세요.'); + } + }; + + const isHidden = true; + return (
@@ -75,13 +88,24 @@ export default function Home() { /> ) : ( - + <> + + + )}
);