From f2c537834797f54231e370dab6fdc30b9a9a400f Mon Sep 17 00:00:00 2001
From: Cho-heejung <66050038+he2e2@users.noreply.github.com>
Date: Sat, 30 Nov 2024 16:04:08 +0900
Subject: [PATCH] =?UTF-8?q?[Feat]=20token=20=EA=B4=80=EB=A6=AC=20(#67)?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
* feat: AuthProvider
* feat: 토큰 발급 실패 시, 팝업 알림
* feat: 로그아웃
---
src/app/App.tsx | 13 +++---
src/app/AuthProvider.tsx | 83 +++++++++++++++++++++++++++++++++++++++
src/shared/api/baseApi.ts | 33 +++++++++++++---
3 files changed, 119 insertions(+), 10 deletions(-)
create mode 100644 src/app/AuthProvider.tsx
diff --git a/src/app/App.tsx b/src/app/App.tsx
index 1634b18..5f78661 100644
--- a/src/app/App.tsx
+++ b/src/app/App.tsx
@@ -1,16 +1,19 @@
import { RouterProvider } from 'react-router-dom';
import AppRouter from './appRouter';
+import AuthProvider from './AuthProvider';
import ModalProvider from './ModalProvider';
import QueryProvider from './QueryProvider';
function App() {
return (
-
-
-
-
-
+
+
+
+
+
+
+
);
}
diff --git a/src/app/AuthProvider.tsx b/src/app/AuthProvider.tsx
new file mode 100644
index 0000000..5f8e5b9
--- /dev/null
+++ b/src/app/AuthProvider.tsx
@@ -0,0 +1,83 @@
+import axios from 'axios';
+import type { ReactNode } from 'react';
+import { createContext, useContext, useRef, useState } from 'react';
+
+import api from '@/shared/api/baseApi';
+import { customConfirm } from '@/shared/ui';
+
+interface AuthContextType {
+ accessToken: string | null;
+ setAccessToken: (token: string | null) => void;
+ reissueToken: () => Promise;
+ logout: () => Promise;
+ removeToken: () => void;
+}
+
+const AuthContext = createContext(undefined);
+
+const AuthProvider = ({ children }: { children: ReactNode }) => {
+ const [accessToken, setAccessToken] = useState(
+ localStorage.getItem('accessToken'),
+ );
+ const isRefreshing = useRef(false);
+
+ const reissueToken = async () => {
+ if (isRefreshing.current) return;
+
+ isRefreshing.current = true;
+
+ try {
+ const response = await axios.post('/token/reissue', {}, { withCredentials: true });
+
+ const newAccessToken = (response.headers['authorization'] as string)?.split(' ')[1];
+
+ if (newAccessToken) {
+ setAccessToken(newAccessToken);
+ localStorage.setItem('accessToken', newAccessToken);
+ } else {
+ throw new Error('accessToken이 헤더에 포함되어 있지 않습니다.');
+ }
+ } catch (error) {
+ console.error('토큰 재발급 실패:', error);
+ removeToken();
+ } finally {
+ // eslint-disable-next-line require-atomic-updates
+ isRefreshing.current = false;
+ }
+ };
+
+ const removeToken = () => {
+ setAccessToken(null);
+ localStorage.removeItem('accessToken');
+ customConfirm({ title: '로그인', text: '로그인 페이지로 이동합니다.', icon: 'info' });
+ };
+
+ const logout = async () => {
+ try {
+ await api.post('/user/logout');
+
+ setAccessToken(null);
+ localStorage.removeItem('accessToken');
+ } catch (error) {
+ console.error('로그아웃 실패:', error);
+ }
+ };
+
+ return (
+
+ {children}
+
+ );
+};
+
+export const useAuth = () => {
+ const context = useContext(AuthContext);
+ if (!context) {
+ throw new Error('AuthProvider Error');
+ }
+ return context;
+};
+
+export default AuthProvider;
diff --git a/src/shared/api/baseApi.ts b/src/shared/api/baseApi.ts
index cd9a46b..e70cdc8 100644
--- a/src/shared/api/baseApi.ts
+++ b/src/shared/api/baseApi.ts
@@ -1,19 +1,42 @@
-import axios, { isAxiosError } from 'axios';
+import type { AxiosRequestConfig } from 'axios';
+import axios from 'axios';
+
+import { useAuth } from '@/app/AuthProvider';
const api = axios.create({
- baseURL: ``, // Backend API Domain 주소 넣기
+ baseURL: ``, // Backend API Domain 주소
timeout: 10_000,
withCredentials: true,
});
+api.interceptors.request.use(config => {
+ const { accessToken } = useAuth();
+ if (accessToken) {
+ config.headers.Authorization = `Bearer ${accessToken}`;
+ }
+ return config;
+});
+
api.interceptors.response.use(
response => response,
async error => {
- if (isAxiosError(error)) {
- return Promise.reject(error);
+ const { reissueToken } = useAuth();
+
+ if (error.response?.status === 401) {
+ try {
+ await reissueToken();
+ const { accessToken } = useAuth();
+
+ if (accessToken && error.config) {
+ error.config.headers.Authorization = `Bearer ${accessToken}`;
+ return api.request(error.config as AxiosRequestConfig);
+ }
+ } catch (reissueError) {
+ console.error('토큰 재발급 실패:', reissueError);
+ }
}
- return Promise.reject(new Error('알 수 없는 에러 발생'));
+ return Promise.reject(error instanceof Error ? error : new Error('Unknown Error'));
},
);