diff --git a/src/app/App.tsx b/src/app/App.tsx index 5f78661..1634b18 100644 --- a/src/app/App.tsx +++ b/src/app/App.tsx @@ -1,19 +1,16 @@ 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 index 5f8e5b9..eaf8ec2 100644 --- a/src/app/AuthProvider.tsx +++ b/src/app/AuthProvider.tsx @@ -1,83 +1,60 @@ -import axios from 'axios'; import type { ReactNode } from 'react'; -import { createContext, useContext, useRef, useState } from 'react'; +import { useEffect } from 'react'; +import { useNavigate } from 'react-router-dom'; -import api from '@/shared/api/baseApi'; +import { getLocalAccessToken, removeLocalAccessToken } from '@/features/auth/auth.api'; +import { useUserStore } from '@/features/user/model/user.store'; +import { getMyProfile } from '@/features/user/user.api'; +import { setInterceptorEvents } 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이 헤더에 포함되어 있지 않습니다.'); + const navigate = useNavigate(); + const accessToken = getLocalAccessToken(); + const { setUserData, clearUserData } = useUserStore(state => state.actions); + + useEffect(() => { + const getUserData = async () => { + try { + if (accessToken) { + const userData = await getMyProfile().then(res => res.data); + if (!userData) throw new Error('유저 정보를 찾을 수가 없습니다.'); + setUserData(userData); + return userData; + } + clearUserData(); + } catch (error) { + console.error('유저 데이터를 불러오는 중 오류가 발생했습니다.', error); } - } 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); - } - }; + }; + + void getUserData(); + }, []); + + //인터셉터 함수 등록 effects + useEffect(() => { + const logout = async (text: string) => { + try { + await customConfirm({ + title: '로그아웃', + text, + icon: 'error', + showCancelButton: false, + }); + removeLocalAccessToken(); + clearUserData(); + navigate('/'); + } catch (error) { + console.error('로그아웃 실패:', error); + } + }; - return ( - - {children} - - ); -}; + setInterceptorEvents('logout', (text: string) => { + void logout(text); + }); + }, []); -export const useAuth = () => { - const context = useContext(AuthContext); - if (!context) { - throw new Error('AuthProvider Error'); - } - return context; + return <>{children}; }; export default AuthProvider; diff --git a/src/app/QueryProvider.tsx b/src/app/QueryProvider.tsx index 14c6a5b..827ba39 100644 --- a/src/app/QueryProvider.tsx +++ b/src/app/QueryProvider.tsx @@ -4,7 +4,7 @@ const QueryProvider = ({ children }: { children: React.ReactNode }) => { const queryClient = new QueryClient({ defaultOptions: { queries: { - staleTime: 60 * 1000, + staleTime: 0, refetchOnWindowFocus: false, retry: 1, }, diff --git a/src/app/appRouter.tsx b/src/app/appRouter.tsx index 43a4ac1..8dd28a1 100644 --- a/src/app/appRouter.tsx +++ b/src/app/appRouter.tsx @@ -1,5 +1,6 @@ import { createBrowserRouter } from 'react-router-dom'; +import { LoginLoading } from '@/features/auth/ui/LoginLoading'; import { ArchiveListPage, DetailArchivePage, @@ -73,6 +74,10 @@ const AppRouter = () => { path: '/like', element: , }, + { + path: '/login', + element: , + }, ], }, ]); diff --git a/src/features/auth/auth.api.ts b/src/features/auth/auth.api.ts new file mode 100644 index 0000000..4864fd0 --- /dev/null +++ b/src/features/auth/auth.api.ts @@ -0,0 +1,56 @@ +import { AxiosError } from 'axios'; + +import type { TokenApiResponse } from './auth.dto'; +import type { PostUserResponseDTO } from '../user/user.dto'; + +import api from '@/shared/api/baseApi'; + +export const loginWithToken = async (token: string) => { + const response = await api.get(`/token/issue?token=${token}`); + const newAccessToken = response.headers['authorization']; + if (newAccessToken) { + setLocalAccessToken(newAccessToken); + } + + return newAccessToken as string; +}; + +export const logout = async () => { + try { + await api.post('/user/logout'); + removeLocalAccessToken(); + } catch (error) { + console.error('로그아웃 실패:', error); + } +}; + +export const getLocalAccessToken = () => { + return localStorage.getItem('accessToken'); +}; + +export const setLocalAccessToken = (token: string) => { + localStorage.setItem('accessToken', token); +}; + +export const removeLocalAccessToken = () => { + localStorage.removeItem('accessToken'); +}; + +export const reissueToken = async () => { + try { + const response = await api.post('/token/reissue'); + + const newAccessToken = response.headers['authorization']; + + if (newAccessToken) { + setLocalAccessToken(newAccessToken); + return newAccessToken as string; + } else { + throw new Error('accessToken이 헤더에 포함되어 있지 않습니다.'); + } + } catch (error) { + if (error instanceof AxiosError) { + throw new Error(`토큰 재발행 중 오류가 발생했습니다. ${error.message}`); + } + } +}; diff --git a/src/features/auth/auth.dto.ts b/src/features/auth/auth.dto.ts new file mode 100644 index 0000000..d4ea9e3 --- /dev/null +++ b/src/features/auth/auth.dto.ts @@ -0,0 +1,8 @@ +import type { ApiResponse } from '@/shared/api'; + +export interface TokenResponse { + message: string; + expiresIn: number; +} + +export type TokenApiResponse = ApiResponse; diff --git a/src/features/auth/ui/GoogleLogin.tsx b/src/features/auth/ui/GoogleLogin.tsx index 8b23313..e2edb94 100644 --- a/src/features/auth/ui/GoogleLogin.tsx +++ b/src/features/auth/ui/GoogleLogin.tsx @@ -5,7 +5,13 @@ import { Button } from '@/shared/ui'; export const GoogleLogin = () => { return ( - + {userData ? ( + + ) : ( + + )} {' '} )}