issue #33
All checks were successful
Test CI / build (push) Successful in 18s

- Access 토큰 만료시 Refresh 토큰으로 Access 토큰 재갱신 요청 로직 구현 중
This commit is contained in:
2025-12-01 22:35:29 +09:00
parent 45dc4cbaaa
commit 49ca9b9ae3
4 changed files with 88 additions and 10 deletions

View File

@@ -0,0 +1,6 @@
import { BaseResponse } from "../BaseResponse";
export class RefreshAccessTokenResponse extends BaseResponse {
accessToken!: string;
refreshToken!: string;
}

View File

@@ -32,7 +32,7 @@ export default function Layout() {
>
<SideBar />
<div className="flex flex-col w-full h-full">
{ authData?.isLogedIn ? <Header /> : null}
{ authData ? <Header /> : null}
{/* <Header /> */}
<Outlet />
</div>

View File

@@ -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<RefreshAccessTokenResponse>(
'/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 메서드
*/

View File

@@ -4,6 +4,7 @@ import { create } from 'zustand';
interface AuthStoreProps {
authData: AuthData | undefined;
login: (data: AuthData) => void;
logout: () => void;
}
export const useAuthStore = create<AuthStoreProps>((set) => ({