@@ -3,6 +3,7 @@ import axios, { type AxiosError, type AxiosInstance } from "axios";
33import { postReissueToken } from "@/apis/Auth/server" ;
44import useAuthStore from "@/lib/zustand/useAuthStore" ;
55import { toast } from "@/lib/zustand/useToastStore" ;
6+ import { isTokenExpired } from "@/utils/jwtUtils" ;
67
78// --- 글로벌 변수 ---
89let reissuePromise : Promise < void > | null = null ;
@@ -34,6 +35,34 @@ const redirectToLogin = (message: string) => {
3435
3536export const convertToBearer = ( token : string ) => `Bearer ${ token } ` ;
3637
38+ const tryReissueAccessToken = async ( ) : Promise < string | null > => {
39+ if ( reissuePromise ) {
40+ await reissuePromise ;
41+ return useAuthStore . getState ( ) . accessToken ;
42+ }
43+
44+ const { setLoading, clearAccessToken, setInitialized, setRefreshStatus } = useAuthStore . getState ( ) ;
45+
46+ reissuePromise = ( async ( ) => {
47+ setRefreshStatus ( "refreshing" ) ;
48+ setLoading ( true ) ;
49+ try {
50+ await postReissueToken ( ) ;
51+ setRefreshStatus ( "success" ) ;
52+ } catch {
53+ clearAccessToken ( ) ;
54+ setRefreshStatus ( "failed" ) ;
55+ } finally {
56+ setLoading ( false ) ;
57+ setInitialized ( true ) ;
58+ reissuePromise = null ;
59+ }
60+ } ) ( ) ;
61+
62+ await reissuePromise ;
63+ return useAuthStore . getState ( ) . accessToken ;
64+ } ;
65+
3766// --- Axios 인스턴스 ---
3867// 인증이 필요 없는 공용 API 요청에 사용
3968export const publicAxiosInstance : AxiosInstance = axios . create ( {
@@ -52,40 +81,24 @@ export const axiosInstance: AxiosInstance = axios.create({
5281// 1. 요청 인터셉터 (Request Interceptor)
5382axiosInstance . interceptors . request . use (
5483 async ( config ) => {
55- const { accessToken, setLoading, clearAccessToken, setInitialized, refreshStatus, setRefreshStatus } =
56- useAuthStore . getState ( ) ;
84+ const { accessToken, clearAccessToken, refreshStatus } = useAuthStore . getState ( ) ;
85+
86+ // 만료된 access token은 즉시 제거하고 refresh 재발급 경로를 타게 한다.
87+ if ( accessToken && isTokenExpired ( accessToken ) ) {
88+ clearAccessToken ( ) ;
89+ }
90+
91+ const validAccessToken = useAuthStore . getState ( ) . accessToken ;
5792
5893 // 토큰이 있으면 헤더에 추가하고 진행
59- if ( accessToken ) {
60- config . headers . Authorization = convertToBearer ( accessToken ) ;
94+ if ( validAccessToken ) {
95+ config . headers . Authorization = convertToBearer ( validAccessToken ) ;
6196 return config ;
6297 }
6398
6499 if ( refreshStatus !== "failed" ) {
65100 try {
66- // 이미 reissue가 진행 중인지 확인
67- if ( reissuePromise ) {
68- await reissuePromise ;
69- } else {
70- // 새로운 reissue 프로세스 시작 (HTTP-only 쿠키의 refreshToken 사용)
71- reissuePromise = ( async ( ) => {
72- setRefreshStatus ( "refreshing" ) ;
73- setLoading ( true ) ;
74- try {
75- await postReissueToken ( ) ;
76- setRefreshStatus ( "success" ) ;
77- } catch {
78- clearAccessToken ( ) ;
79- setRefreshStatus ( "failed" ) ;
80- } finally {
81- setLoading ( false ) ;
82- setInitialized ( true ) ;
83- reissuePromise = null ;
84- }
85- } ) ( ) ;
86-
87- await reissuePromise ;
88- }
101+ await tryReissueAccessToken ( ) ;
89102
90103 // reissue 완료 후 업데이트된 토큰으로 헤더 설정
91104 const updatedAccessToken = useAuthStore . getState ( ) . accessToken ;
@@ -119,12 +132,31 @@ axiosInstance.interceptors.request.use(
119132) ;
120133
121134// 2. 응답 인터셉터 (Response Interceptor)
122- // 역할: 401 에러 시 로그인 페이지로 리다이렉트
135+ // 역할: 401 에러 시 access 재발급 1회 재시도 후 실패하면 로그인 페이지로 리다이렉트
123136axiosInstance . interceptors . response . use (
124137 ( response ) => response ,
125- ( error : AxiosError ) => {
126- // 401 에러 시 로그인 페이지로 리다이렉트
127- if ( error . response ?. status === 401 ) {
138+ async ( error : AxiosError ) => {
139+ const status = error . response ?. status ;
140+
141+ if ( status === 401 ) {
142+ const originalRequest = error . config as ( typeof error . config & { _retry ?: boolean } ) | undefined ;
143+
144+ if ( originalRequest && ! originalRequest . _retry && useAuthStore . getState ( ) . refreshStatus !== "failed" ) {
145+ originalRequest . _retry = true ;
146+
147+ try {
148+ const reissuedAccessToken = await tryReissueAccessToken ( ) ;
149+
150+ if ( reissuedAccessToken ) {
151+ originalRequest . headers = originalRequest . headers ?? { } ;
152+ originalRequest . headers . Authorization = convertToBearer ( reissuedAccessToken ) ;
153+ return axiosInstance ( originalRequest ) ;
154+ }
155+ } catch {
156+ // 재발급 실패 시 아래 로그인 리다이렉트로 처리
157+ }
158+ }
159+
128160 redirectToLogin ( "세션이 만료되었습니다. 다시 로그인해주세요." ) ;
129161 }
130162
0 commit comments