diff --git a/src/App.tsx b/src/App.tsx index b00975a..57a96b0 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -3,6 +3,7 @@ import { Route, Routes } from 'react-router'; import useViewport from './hooks/useViewport'; import Layout from './layouts/Layout'; import MobileLayout from './layouts/MobileLayout'; +import PrivateRoute from './layouts/PrivateRoute'; import AdminPage from './pages/Admin'; import FilteredLetterManage from './pages/Admin/FilteredLetter'; import FilteringManage from './pages/Admin/Filtering'; @@ -30,39 +31,44 @@ const App = () => { return ( }> - } /> } /> } /> + } /> + } /> + } /> } /> - - }> - } /> - } /> - } /> + + }> + + }> + } /> + } /> + } /> + + } /> + } /> - } /> - } /> - - - }> - } /> - } /> + + }> + } /> + } /> + + } /> + + }> + } /> + } /> + } /> - } /> - - }> - } /> - } /> - } /> - } /> - } /> - }> - } /> - } /> - } /> + }> + }> + } /> + } /> + } /> + ); diff --git a/src/apis/auth.ts b/src/apis/auth.ts index 6885010..9105f60 100644 --- a/src/apis/auth.ts +++ b/src/apis/auth.ts @@ -12,8 +12,10 @@ export const getUserToken = async (stateToken: string) => { if (userInfo) { return userInfo; } + return response; } catch (error) { console.error(error); + throw error; } }; @@ -29,7 +31,7 @@ export const postZipCode = async () => { export const getNewToken = async () => { try { - const response = await client.get('/api/reissue', { withCredentials: true }); + const response = await client.post('/api/reissue', {}, { withCredentials: true }); if (!response) throw new Error('getNewToken: no response data'); return response; } catch (error) { @@ -40,7 +42,7 @@ export const getNewToken = async () => { export const getMydata = async () => { try { const response = await client.get('/api/members/me'); - if (!response) throw new Error('getNewTOken: no response data'); + if (!response) throw new Error('getNewToken: no response data'); return response; } catch (error) { console.error(error); diff --git a/src/apis/client.ts b/src/apis/client.ts index 6fc9fe4..8ccefbf 100644 --- a/src/apis/client.ts +++ b/src/apis/client.ts @@ -6,60 +6,100 @@ import { getNewToken } from './auth'; const client = axios.create({ baseURL: import.meta.env.VITE_API_URL, + headers: { 'Content-Type': 'application/json' }, }); +type FailedRequest = { + resolve: (token: string) => void; + reject: (error: unknown) => void; +}; + +let isRefreshing = false; +let failedQueue: FailedRequest[] = []; + +const processQueue = (error: unknown, token: string | null = null) => { + failedQueue.forEach((prom) => { + if (error) { + prom.reject(error); + } else { + if (token) { + prom.resolve(token); + } + } + }); + + failedQueue = []; +}; + client.interceptors.request.use( (config) => { - const accessToken = useAuthStore((state) => state.accessToken); - console.log(config.url); - console.log(accessToken); + const accessToken = useAuthStore.getState().accessToken; + if (config.url !== '/auth/reissue' && accessToken) { config.headers.Authorization = `Bearer ${accessToken}`; - console.log('intercepter', config.headers); } + return config; }, - (error) => { - const logout = useAuthStore((state) => state.logout); - logout(); - window.location.replace('/login'); - return Promise.reject(error); - }, + (error) => Promise.reject(error), ); client.interceptors.response.use( (response) => response, async (error) => { - const setAccessToken = useAuthStore((state) => state.setAccessToken); - const logout = useAuthStore((state) => state.logout); - + const setAccessToken = useAuthStore.getState().setAccessToken; + const logout = useAuthStore.getState().logout; const originalRequest = error.config; - if (!originalRequest) { + if (!originalRequest) return Promise.reject(error); + + if ( + originalRequest.url === '/auth/reissue' || + originalRequest.url.includes('/api/auth/token?state=') + ) { return Promise.reject(error); } if ( - (error.response.status === 401 || - error.response.status === 403 || - error.response.data.message === 'Unauthorized') && + (error.response?.status === 401 || error.response?.status === 403) && !originalRequest._retry ) { originalRequest._retry = true; + if (isRefreshing) { + try { + return new Promise((resolve, reject) => { + failedQueue.push({ + resolve: (token: string) => { + originalRequest.headers.Authorization = `Bearer ${token}`; + resolve(client(originalRequest)); + }, + reject: (err: unknown) => reject(err), + }); + }); + } catch (e) { + return Promise.reject(e); + } + } + + isRefreshing = true; + try { const response = await getNewToken(); const newToken = response?.data.accessToken; - if (!newToken) throw new Error('Failed to Refresh Token'); + if (!newToken) throw new Error('Failed to refresh token'); setAccessToken(newToken); - originalRequest.headers.Authorization = `Bearer ${newToken}`; + processQueue(null, newToken); + isRefreshing = false; + originalRequest.headers.Authorization = `Bearer ${newToken}`; return client(originalRequest); } catch (e) { + processQueue(e, null); + isRefreshing = false; logout(); - window.location.replace('/login'); return Promise.reject(e); } } diff --git a/src/layouts/PrivateRoute.tsx b/src/layouts/PrivateRoute.tsx new file mode 100644 index 0000000..bc78b4a --- /dev/null +++ b/src/layouts/PrivateRoute.tsx @@ -0,0 +1,24 @@ +import { useEffect, useState } from 'react'; +import { useNavigate, Outlet } from 'react-router'; + +import useAuthStore from '@/stores/authStore'; + +export default function PrivateRoute() { + const isLoggedIn = useAuthStore((state) => state.isLoggedIn); + const navigate = useNavigate(); + const [shouldRender, setShouldRender] = useState(false); + + useEffect(() => { + if (!isLoggedIn) { + navigate('/login', { replace: true }); + } else { + setShouldRender(true); + } + }, [isLoggedIn, navigate]); + + if (!shouldRender) { + return null; + } + + return ; +} diff --git a/src/pages/Auth/index.tsx b/src/pages/Auth/index.tsx index cc157c7..0596a77 100644 --- a/src/pages/Auth/index.tsx +++ b/src/pages/Auth/index.tsx @@ -1,4 +1,3 @@ -/* eslint-disable @typescript-eslint/no-unused-expressions */ import { useEffect } from 'react'; import { useNavigate } from 'react-router'; @@ -10,73 +9,75 @@ const AuthCallbackPage = () => { const redirectURL = new URLSearchParams(window.location.search).get('redirect'); const login = useAuthStore((state) => state.login); + const logout = useAuthStore((state) => state.logout); const setAccessToken = useAuthStore((state) => state.setAccessToken); const setZipCode = useAuthStore((state) => state.setZipCode); - const navigate = useNavigate(); + const handleError = (error: unknown) => { + console.error('AuthCallback Error:', error); + logout(); + navigate('/login', { replace: true }); + }; + const setUserInfo = async (stateToken: string) => { try { const response = await getUserToken(stateToken); - if (!response) throw new Error('Error Fetching userInfo'); + console.log(response); + if (!response) throw new Error('Error fetching user token'); const userInfo = response.data; - if (userInfo) { - login(); - userInfo.accessToken && setAccessToken(userInfo.accessToken); - - if (redirectURL == 'home') { - const zipCodeResponse = await getMydata(); - if (!zipCodeResponse) throw new Error('Error Fetching userInfo'); - const zipCode = zipCodeResponse.data.data.zipCode; - zipCode && setZipCode(zipCode); - - console.log( - 'isLoggedIn', - useAuthStore.getState().isLoggedIn, - 'access', - useAuthStore.getState().accessToken, - 'zipCode', - useAuthStore.getState().zipCode, - ); - } else if (redirectURL === 'onboarding') { - const createZipCodeResponse = await postZipCode(); - if (!createZipCodeResponse) throw new Error('Error creating ZipCode'); - const zipCode = createZipCodeResponse.data.data.zipCode; - console.log(createZipCodeResponse); - const newAccessToken = createZipCodeResponse.headers['Authorization']; - setZipCode(zipCode); - setAccessToken(newAccessToken); - console.log( - 'isLoggedIn', - useAuthStore.getState().isLoggedIn, - 'access', - useAuthStore.getState().accessToken, - 'zipCode', - useAuthStore.getState().zipCode, - ); - } - } else { - navigate('/login'); + if (!userInfo) throw new Error('Invalid user info'); + + login(); + if (userInfo.accessToken) setAccessToken(userInfo.accessToken); + + switch (redirectURL) { + case 'home': + { + const zipCodeResponse = await getMydata(); + if (!zipCodeResponse) throw new Error('Error fetching user data'); + setZipCode(zipCodeResponse.data.data.zipCode); + } + break; + + case 'onboarding': + { + const createZipCodeResponse = await postZipCode(); + if (!createZipCodeResponse) throw new Error('Error creating ZipCode'); + + setZipCode(createZipCodeResponse.data.data.zipCode); + const newAccessToken = createZipCodeResponse.headers['authorization']?.split(' ')[1]; + if (!newAccessToken) throw new Error('Missing new access token'); + + setAccessToken(newAccessToken); + } + break; + + default: + navigate('/notFound'); + return; } + navigate(redirectURL === 'onboarding' ? '/onboarding' : '/'); } catch (error) { - console.error(error); + handleError(error); } }; - const redirection = () => { - if (redirectURL === 'onboarding') navigate('/onboarding'); - else if (redirectURL === 'home') navigate('/'); - else navigate('/notFound'); - }; - useEffect(() => { - if (stateToken) { - setUserInfo(stateToken as string); - redirection(); - } else navigate('/notFound'); - }, []); - return <>; + if (!stateToken) { + navigate('/notFound'); + return; + } + + const fetchData = async () => { + await setUserInfo(stateToken as string); + }; + + fetchData(); + }, [stateToken, navigate]); + + return null; }; export default AuthCallbackPage; diff --git a/src/pages/Home/index.tsx b/src/pages/Home/index.tsx index fee0eb5..68f528f 100644 --- a/src/pages/Home/index.tsx +++ b/src/pages/Home/index.tsx @@ -1,6 +1,10 @@ +import { useEffect } from 'react'; +import { useNavigate } from 'react-router'; + import HomeButton from '@/components/HomeButton'; import NoticeRollingPaper from '@/components/NoticeRollingPaper'; import useViewport from '@/hooks/useViewport'; +import useAuthStore from '@/stores/authStore'; import HomeBackgroundLeft from './components/HomeBackgroundLeft'; import HomeBackgroundRightBottom from './components/HomeBackgroundRightBottom'; @@ -11,7 +15,16 @@ import HomeRight from './components/HomeRight'; import LetterActions from './components/LetterActions'; const HomePage = () => { + const isLoggedIn = useAuthStore.getState().isLoggedIn; + const navigate = useNavigate(); + useViewport(); + useEffect(() => { + if (!isLoggedIn) { + navigate('/login'); + } + }, []); + return (
diff --git a/src/pages/Landing/index.tsx b/src/pages/Landing/index.tsx index 9db2fc7..b89bfda 100644 --- a/src/pages/Landing/index.tsx +++ b/src/pages/Landing/index.tsx @@ -1,13 +1,20 @@ -import { useState } from 'react'; -import { Navigate } from 'react-router'; +import { useState, useEffect } from 'react'; +import { Navigate, useNavigate } from 'react-router'; import { twMerge } from 'tailwind-merge'; import LandingImg from '@/assets/images/landing.png'; +import useAuthStore from '@/stores/authStore'; import { STYLE_CLASS } from './constants'; const Landing = () => { const [step, setStep] = useState(0); + const isLoggedIn = useAuthStore((state) => state.isLoggedIn); + const navigate = useNavigate(); + + useEffect(() => { + if (isLoggedIn) navigate('/'); + }, [isLoggedIn, navigate]); if (step === 3) return ; diff --git a/src/pages/Login/index.tsx b/src/pages/Login/index.tsx index ec651c7..2922331 100644 --- a/src/pages/Login/index.tsx +++ b/src/pages/Login/index.tsx @@ -1,12 +1,24 @@ +import { useEffect } from 'react'; +import { useNavigate } from 'react-router'; + import { socialLogin } from '@/apis/auth'; import { GoogleIcon, KakaoIcon, NaverIcon, StampIcon } from '@/assets/icons'; +import useAuthStore from '@/stores/authStore'; import Background from './components/Background'; const LoginPage = () => { + const isLoggedIn = useAuthStore((state) => state.isLoggedIn); + const navigate = useNavigate(); + const handleLogin = (loginType: LoginType) => { socialLogin(loginType); }; + + useEffect(() => { + if (isLoggedIn) navigate('/'); + }, [isLoggedIn]); + return ( <>
diff --git a/src/pages/MyPage/index.tsx b/src/pages/MyPage/index.tsx index d97ec42..4c00707 100644 --- a/src/pages/MyPage/index.tsx +++ b/src/pages/MyPage/index.tsx @@ -27,7 +27,7 @@ const MyPage = () => { const handleLeave = async () => { try { const response = await deleteUserInfo(); - if (!response) throw new Error('deletiongi failed'); + if (!response) throw new Error('deletioning failed'); console.log(response); } catch (error) { console.error(error); @@ -81,7 +81,9 @@ const MyPage = () => {

고객 센터

-

운영자에게 문의하기

+ +

운영자에게 문의하기

+

계정

@@ -92,7 +94,12 @@ const MyPage = () => { {data.email}

- @@ -100,7 +107,9 @@ const MyPage = () => { diff --git a/src/pages/Onboarding/index.tsx b/src/pages/Onboarding/index.tsx index 4c49127..c5ab71c 100644 --- a/src/pages/Onboarding/index.tsx +++ b/src/pages/Onboarding/index.tsx @@ -1,4 +1,7 @@ import { useEffect, useState } from 'react'; +import { useNavigate } from 'react-router'; + +import useAuthStore from '@/stores/authStore'; import SetZipCode from './SetZipCode'; import UserInteraction from './UserInteraction'; @@ -7,6 +10,8 @@ import WelcomeLetter from './WelcomeLetter'; const OnboardingPage = () => { const [isZipCodeSet, setIsZipCodeSet] = useState(false); const [isAnimationOver, setIsAnimationOver] = useState(false); + const isLoggedIn = useAuthStore.getState().isLoggedIn; + const navigate = useNavigate(); useEffect(() => { if (isZipCodeSet || isAnimationOver) { @@ -27,6 +32,13 @@ const OnboardingPage = () => { console.log('isZipCode', isZipCodeSet, 'isAnimation', isAnimationOver); } }, []); + + useEffect(() => { + if (!isLoggedIn) { + navigate('/login'); + } + }, []); + return (
{!isZipCodeSet ? (