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) => ({