diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 59ed6ed..dac4254 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -58,7 +58,7 @@ importers: version: 4.0.6 zustand: specifier: ^5.0.3 - version: 5.0.3(@types/react@19.0.8)(react@18.3.1) + version: 5.0.3(@types/react@19.0.8)(immer@10.1.1)(react@18.3.1) devDependencies: '@eslint/js': specifier: ^9.19.0 @@ -1554,6 +1554,9 @@ packages: resolution: {integrity: sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==} engines: {node: '>= 4'} + immer@10.1.1: + resolution: {integrity: sha512-s2MPrmjovJcoMaHtx6K11Ra7oD05NT97w1IC5zpMkT6Atjr7H8LjaDd81iIxUYpMKSRRNMJE703M1Fhr/TctHw==} + import-fresh@3.3.1: resolution: {integrity: sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==} engines: {node: '>=6'} @@ -3890,6 +3893,9 @@ snapshots: ignore@5.3.2: {} + immer@10.1.1: + optional: true + import-fresh@3.3.1: dependencies: parent-module: 1.0.1 @@ -4673,7 +4679,8 @@ snapshots: yocto-queue@0.1.0: {} - zustand@5.0.3(@types/react@19.0.8)(react@18.3.1): + zustand@5.0.3(@types/react@19.0.8)(immer@10.1.1)(react@18.3.1): optionalDependencies: '@types/react': 19.0.8 + immer: 10.1.1 react: 18.3.1 diff --git a/src/App.tsx b/src/App.tsx index cd564f8..b00975a 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -7,6 +7,7 @@ import AdminPage from './pages/Admin'; import FilteredLetterManage from './pages/Admin/FilteredLetter'; import FilteringManage from './pages/Admin/Filtering'; import ReportManage from './pages/Admin/Report'; +import AuthCallbackPage from './pages/Auth'; import Home from './pages/Home'; import Landing from './pages/Landing'; import LetterBoardPage from './pages/LetterBoard'; @@ -54,7 +55,8 @@ const App = () => { } /> } /> - }> + } /> + } /> }> diff --git a/src/apis/auth.ts b/src/apis/auth.ts new file mode 100644 index 0000000..6885010 --- /dev/null +++ b/src/apis/auth.ts @@ -0,0 +1,70 @@ +import client from './client'; + +export const socialLogin = (loginType: LoginType) => { + window.location.href = `${import.meta.env.VITE_API_URL}/oauth2/authorization/${loginType}`; +}; + +export const getUserToken = async (stateToken: string) => { + try { + const response = await client.get(`/api/auth/token?state=${stateToken}`); + if (!response) throw new Error('getUserToken: Error while fetching user token'); + const userInfo = response.data; + if (userInfo) { + return userInfo; + } + } catch (error) { + console.error(error); + } +}; + +export const postZipCode = async () => { + try { + const response = await client.post(`/api/members/zipCode`); + if (!response) throw new Error('fail to post ZipCode'); + return response; + } catch (error) { + console.error(error); + } +}; + +export const getNewToken = async () => { + try { + const response = await client.get('/api/reissue', { withCredentials: true }); + if (!response) throw new Error('getNewToken: no response data'); + return response; + } catch (error) { + console.error(error); + } +}; + +export const getMydata = async () => { + try { + const response = await client.get('/api/members/me'); + if (!response) throw new Error('getNewTOken: no response data'); + return response; + } catch (error) { + console.error(error); + } +}; + +export const deleteUserInfo = async () => { + try { + const response = await client.delete('/api/members/me', { + withCredentials: true, + }); + if (!response) throw new Error('deleteUserInfo: no response'); + return response; + } catch (error) { + console.error(error); + } +}; + +export const postLogout = async () => { + try { + const response = await client.post('/api/logout', { withCredentials: true }); + if (!response) throw new Error('postLogout: failed to logout'); + return response; + } catch (error) { + console.error(error); + } +}; diff --git a/src/apis/client.ts b/src/apis/client.ts index d2c900a..6fc9fe4 100644 --- a/src/apis/client.ts +++ b/src/apis/client.ts @@ -1,21 +1,70 @@ import axios from 'axios'; +import useAuthStore from '@/stores/authStore'; + +import { getNewToken } from './auth'; + const client = axios.create({ baseURL: import.meta.env.VITE_API_URL, }); -// client.interceptors.request.use( -// (config) => { -// const token = localStorage.getItem('authToken'); -// if (token) { -// config.headers['Authorization'] = `Bearer ${token}`; -// } -// return config; -// }, -// (error) => { -// //TODO: 에러처리 -// return Promise.reject(error); -// }, -// ); +client.interceptors.request.use( + (config) => { + const accessToken = useAuthStore((state) => state.accessToken); + console.log(config.url); + console.log(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); + }, +); + +client.interceptors.response.use( + (response) => response, + async (error) => { + const setAccessToken = useAuthStore((state) => state.setAccessToken); + const logout = useAuthStore((state) => state.logout); + + const originalRequest = error.config; + + if (!originalRequest) { + return Promise.reject(error); + } + + if ( + (error.response.status === 401 || + error.response.status === 403 || + error.response.data.message === 'Unauthorized') && + !originalRequest._retry + ) { + originalRequest._retry = true; + + try { + const response = await getNewToken(); + const newToken = response?.data.accessToken; + + if (!newToken) throw new Error('Failed to Refresh Token'); + + setAccessToken(newToken); + originalRequest.headers.Authorization = `Bearer ${newToken}`; + + return client(originalRequest); + } catch (e) { + logout(); + window.location.replace('/login'); + return Promise.reject(e); + } + } + return Promise.reject(error); + }, +); export default client; diff --git a/src/main.tsx b/src/main.tsx index 4df6fa6..41bea78 100644 --- a/src/main.tsx +++ b/src/main.tsx @@ -15,11 +15,11 @@ queryClient.setDefaultOptions({ }); createRoot(document.getElementById('root')!).render( - - - - - - - , + // + + + + + , + // , ); diff --git a/src/pages/Auth/index.tsx b/src/pages/Auth/index.tsx new file mode 100644 index 0000000..cc157c7 --- /dev/null +++ b/src/pages/Auth/index.tsx @@ -0,0 +1,82 @@ +/* eslint-disable @typescript-eslint/no-unused-expressions */ +import { useEffect } from 'react'; +import { useNavigate } from 'react-router'; + +import { getUserToken, getMydata, postZipCode } from '@/apis/auth'; +import useAuthStore from '@/stores/authStore'; + +const AuthCallbackPage = () => { + const stateToken = new URLSearchParams(window.location.search).get('state'); + const redirectURL = new URLSearchParams(window.location.search).get('redirect'); + + const login = useAuthStore((state) => state.login); + const setAccessToken = useAuthStore((state) => state.setAccessToken); + const setZipCode = useAuthStore((state) => state.setZipCode); + + const navigate = useNavigate(); + + const setUserInfo = async (stateToken: string) => { + try { + const response = await getUserToken(stateToken); + if (!response) throw new Error('Error Fetching userInfo'); + + 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'); + } + } catch (error) { + console.error(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 <>; +}; + +export default AuthCallbackPage; diff --git a/src/pages/Login/index.tsx b/src/pages/Login/index.tsx index aa17b83..ec651c7 100644 --- a/src/pages/Login/index.tsx +++ b/src/pages/Login/index.tsx @@ -1,8 +1,12 @@ +import { socialLogin } from '@/apis/auth'; import { GoogleIcon, KakaoIcon, NaverIcon, StampIcon } from '@/assets/icons'; import Background from './components/Background'; const LoginPage = () => { + const handleLogin = (loginType: LoginType) => { + socialLogin(loginType); + }; return ( <>
@@ -22,6 +26,7 @@ const LoginPage = () => { type="button" className="rounded-full bg-[#03C75A] p-3.5" aria-label="네이버 로그인" + onClick={() => handleLogin('naver')} > @@ -29,6 +34,7 @@ const LoginPage = () => { type="button" className="rounded-full bg-[#FEE500] p-3.5" aria-label="카카오 로그인" + onClick={() => handleLogin('kakao')} > @@ -36,6 +42,7 @@ const LoginPage = () => { type="button" className="border-gray-5 rounded-full border bg-white p-3.5" aria-label="구글 로그인" + onClick={() => handleLogin('google')} > diff --git a/src/pages/MyPage/index.tsx b/src/pages/MyPage/index.tsx index 5c2fcfe..d97ec42 100644 --- a/src/pages/MyPage/index.tsx +++ b/src/pages/MyPage/index.tsx @@ -1,7 +1,9 @@ import { useState, useEffect } from 'react'; import { Link } from 'react-router'; +import { deleteUserInfo } from '@/apis/auth'; import ConfirmModal from '@/components/ConfirmModal'; +import useAuthStore from '@/stores/authStore'; import useMyPageStore from '@/stores/myPageStore'; import { TEMPERATURE_RANGE } from './constants'; @@ -13,6 +15,7 @@ const MyPage = () => { const { data, fetchMyPageInfo } = useMyPageStore(); const [isOpenModal, setIsOpenModal] = useState(false); + const logout = useAuthStore((state) => state.logout); const getDescriptionByTemperature = (temp: number) => { const range = TEMPERATURE_RANGE.find((range) => temp >= range.min && temp < range.max); @@ -21,6 +24,16 @@ const MyPage = () => { const description = getDescriptionByTemperature(Number(data.temperature)); + const handleLeave = async () => { + try { + const response = await deleteUserInfo(); + if (!response) throw new Error('deletiongi failed'); + console.log(response); + } catch (error) { + console.error(error); + } + }; + return ( <> {isOpenModal && ( @@ -30,7 +43,10 @@ const MyPage = () => { cancelText="되돌아가기" confirmText="탈퇴하기" onCancel={() => setIsOpenModal(false)} - onConfirm={() => setIsOpenModal(false)} + onConfirm={() => { + handleLeave(); + setIsOpenModal(false); + }} /> )}
@@ -76,7 +92,9 @@ const MyPage = () => { {data.email}

-

로그아웃

+