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}
- 로그아웃
+