diff --git a/src/data/response/account/RefreshAccessTokenResponse.ts b/src/data/response/account/RefreshAccessTokenResponse.ts new file mode 100644 index 0000000..f3f2643 --- /dev/null +++ b/src/data/response/account/RefreshAccessTokenResponse.ts @@ -0,0 +1,6 @@ +import { BaseResponse } from "../BaseResponse"; + +export class RefreshAccessTokenResponse extends BaseResponse { + accessToken!: string; + refreshToken!: string; +} \ No newline at end of file diff --git a/src/layouts/Layout.tsx b/src/layouts/Layout.tsx index a19551d..2c12f57 100644 --- a/src/layouts/Layout.tsx +++ b/src/layouts/Layout.tsx @@ -32,7 +32,7 @@ export default function Layout() { >
- { authData?.isLogedIn ?
: null} + { authData ?
: null} {/*
*/}
diff --git a/src/network/BaseNetwork.ts b/src/network/BaseNetwork.ts index bdef8e9..202eb0e 100644 --- a/src/network/BaseNetwork.ts +++ b/src/network/BaseNetwork.ts @@ -6,10 +6,15 @@ import type { AxiosResponse, InternalAxiosRequestConfig, } from "axios"; +import { useAuthStore } from '@/store/authStore'; +import type { RefreshAccessTokenResponse } from '@/data/response/account/RefreshAccessTokenResponse'; export class BaseNetwork { protected instance: AxiosInstance; + private isRefreshing = false; + private refreshQueue: Array<(token:string) => void> = []; + constructor() { this.instance = axios.create({ baseURL: import.meta.env.VITE_API_URL || "http://localhost:3000", @@ -48,20 +53,86 @@ export class BaseNetwork { this.instance.interceptors.response.use( (response: AxiosResponse) => response, (error: AxiosError) => { - const message = - (error.response?.data as any)?.message || - error.message || - "Unknown error"; + const status = error.response?.status; + const errorCode = (error.response?.data as any)?.code; + const originalRequest = error.config as AxiosRequestConfig & { + _retry?: boolean; + } - return Promise.reject({ - status: error.response?.status, - message, - raw: error, - }); + if ( + status === 401 + && errorCode === 'AccessTokenExpired' + && !originalRequest._retry + ) { + originalRequest._retry = true; + return this.handleRefreshToken(originalRequest); + } + + return Promise.reject(error); } ); } + private async handleRefreshToken(originalRequest: AxiosRequestConfig) { + + const authData = useAuthStore.getState().authData; + const refreshToken = authData?.refreshToken; + + if (!authData || !refreshToken) { + useAuthStore.getState().logout(); + return Promise.reject("no refresh token"); + } + + if (this.isRefreshing) { + return new Promise((resolve) => { + this.refreshQueue.push((newToken: string) => { + originalRequest.headers = { + ...originalRequest.headers, + Authorization: `Bearer ${newToken}` + }; + resolve(this.instance(originalRequest)); + }) + }) + } + + this.isRefreshing = true; + + try { + const response = await this.get( + '/account/refresh-access-token', + { + headers: { + Authorization: `Bearer ${refreshToken}` + } + } + ) + + const newAccessToken = response.data.accessToken; + const newRefreshToken = response.data.refreshToken; + + useAuthStore.getState().login({ + ...authData, + accessToken: newAccessToken, + refreshToken: newRefreshToken + }); + + this.refreshQueue.forEach((cb) => cb(newAccessToken)); + this.refreshQueue = []; + originalRequest.headers = { + ...originalRequest.headers, + Authorization: `Bearer ${newAccessToken}`, + }; + + return this.instance(originalRequest); + } catch (err) { + useAuthStore.getState().logout(); + window.location.href = '/login?expired=1' + return Promise.reject(err); + } finally { + this.isRefreshing = false; + } + } + /** * 기본 CRUD 메서드 */ diff --git a/src/store/authStore.ts b/src/store/authStore.ts index 2ba2bba..7ba1bf7 100644 --- a/src/store/authStore.ts +++ b/src/store/authStore.ts @@ -4,6 +4,7 @@ import { create } from 'zustand'; interface AuthStoreProps { authData: AuthData | undefined; login: (data: AuthData) => void; + logout: () => void; } export const useAuthStore = create((set) => ({