From 372670c3f358cd3d1e61a7cf0f891fda67e057ba Mon Sep 17 00:00:00 2001 From: icurfer Date: Tue, 20 May 2025 00:33:12 +0900 Subject: [PATCH] =?UTF-8?q?=ED=86=A0=ED=81=B0=20=EA=B0=B1=EC=8B=A0=20?= =?UTF-8?q?=EA=B8=B0=EB=8A=A5=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/api/attachInterceptors.js | 41 +++++++++++++++++++++++++++++++++++ src/api/authApi.js | 14 +++--------- src/api/blogApi.js | 14 +++--------- src/api/todoApi.js | 14 +++--------- src/api/tokenUtils.js | 16 ++++++++++++++ version | 2 +- 6 files changed, 67 insertions(+), 34 deletions(-) create mode 100644 src/api/attachInterceptors.js create mode 100644 src/api/tokenUtils.js diff --git a/src/api/attachInterceptors.js b/src/api/attachInterceptors.js new file mode 100644 index 0000000..abc01cb --- /dev/null +++ b/src/api/attachInterceptors.js @@ -0,0 +1,41 @@ +import { refreshAccessToken } from './tokenUtils'; + +export const attachAuthInterceptors = (axiosInstance) => { + // 요청 시 access token 자동 첨부 + axiosInstance.interceptors.request.use(config => { + const token = localStorage.getItem('access'); + if (token) { + config.headers.Authorization = `Bearer ${token}`; + } + return config; + }); + + // 응답 시 access 만료 → refresh 재시도 + axiosInstance.interceptors.response.use( + res => res, + async err => { + const originalRequest = err.config; + + if ( + err.response?.status === 401 && + !originalRequest._retry && + localStorage.getItem('refresh') + ) { + originalRequest._retry = true; + + try { + const newAccess = await refreshAccessToken(); + originalRequest.headers.Authorization = `Bearer ${newAccess}`; + return axiosInstance(originalRequest); + } catch (refreshError) { + console.error('토큰 갱신 실패', refreshError); + localStorage.removeItem('access'); + localStorage.removeItem('refresh'); + window.location.href = '/login'; // 또는 useNavigate 사용 가능 + } + } + + return Promise.reject(err); + } + ); +}; diff --git a/src/api/authApi.js b/src/api/authApi.js index e04c1b1..9f83077 100644 --- a/src/api/authApi.js +++ b/src/api/authApi.js @@ -1,20 +1,12 @@ // src/api/authApi.js import axios from 'axios'; +import { attachAuthInterceptors } from './attachInterceptors'; const authApi = axios.create({ baseURL: process.env.REACT_APP_API_AUTH, - headers: { - 'Content-Type': 'application/json', - } + headers: { 'Content-Type': 'application/json' } }); -// JWT 자동 포함 (로그인 이후 /me 호출 등에 사용 가능) -authApi.interceptors.request.use(config => { - const token = localStorage.getItem('access'); - if (token) { - config.headers.Authorization = `Bearer ${token}`; - } - return config; -}); +attachAuthInterceptors(authApi); export default authApi; diff --git a/src/api/blogApi.js b/src/api/blogApi.js index c70f029..e8dbeab 100644 --- a/src/api/blogApi.js +++ b/src/api/blogApi.js @@ -1,20 +1,12 @@ // src/api/blogApi.js import axios from 'axios'; +import { attachAuthInterceptors } from './attachInterceptors'; const blogApi = axios.create({ baseURL: process.env.REACT_APP_API_BLOG, - headers: { - 'Content-Type': 'application/json', - } + headers: { 'Content-Type': 'application/json' } }); -// 게시글 작성 시에도 JWT 필요하므로 interceptor 동일하게 구성 -blogApi.interceptors.request.use(config => { - const token = localStorage.getItem('access'); - if (token) { - config.headers.Authorization = `Bearer ${token}`; - } - return config; -}); +attachAuthInterceptors(blogApi); export default blogApi; diff --git a/src/api/todoApi.js b/src/api/todoApi.js index e67050a..6f5a120 100644 --- a/src/api/todoApi.js +++ b/src/api/todoApi.js @@ -1,20 +1,12 @@ // src/api/todoApi.js import axios from 'axios'; +import { attachAuthInterceptors } from './attachInterceptors'; const todoApi = axios.create({ baseURL: process.env.REACT_APP_API_TODO, - headers: { - 'Content-Type': 'application/json', - } + headers: { 'Content-Type': 'application/json' } }); -// JWT 자동 첨부 -todoApi.interceptors.request.use(config => { - const token = localStorage.getItem('access'); - if (token) { - config.headers.Authorization = `Bearer ${token}`; - } - return config; -}); +attachAuthInterceptors(todoApi); export default todoApi; diff --git a/src/api/tokenUtils.js b/src/api/tokenUtils.js new file mode 100644 index 0000000..d620ba3 --- /dev/null +++ b/src/api/tokenUtils.js @@ -0,0 +1,16 @@ +import axios from 'axios'; + +export const refreshAccessToken = async () => { + const refreshToken = localStorage.getItem('refresh'); + + if (!refreshToken) throw new Error('No refresh token'); + + const res = await axios.post( + `${process.env.REACT_APP_API_AUTH}/api/auth/token/refresh/`, + { refresh: refreshToken } + ); + + const newAccess = res.data.access; + localStorage.setItem('access', newAccess); + return newAccess; +}; diff --git a/version b/version index cbcf477..58682af 100644 --- a/version +++ b/version @@ -1 +1 @@ -0.0.10-r1 \ No newline at end of file +0.0.11 \ No newline at end of file