diff --git a/src/pages/admin/ClubDetailEdit/Page.tsx b/src/pages/admin/ClubDetailEdit/Page.tsx index 6d06a5c5..923cfbab 100644 --- a/src/pages/admin/ClubDetailEdit/Page.tsx +++ b/src/pages/admin/ClubDetailEdit/Page.tsx @@ -48,11 +48,8 @@ export const ClubDetailEditPage = () => { color: 'white', }, duration: 1000, + onAutoClose: () => navigate(`/clubs/${clubId}`), }); - - setTimeout(() => { - navigate(`/clubs/${clubId}`); - }, 1000); }) .catch(() => { toast.error('수정 실패!', { diff --git a/src/pages/admin/Login/KakaoCallback.tsx b/src/pages/admin/Login/KakaoCallback.tsx index 052ad084..57afabac 100644 --- a/src/pages/admin/Login/KakaoCallback.tsx +++ b/src/pages/admin/Login/KakaoCallback.tsx @@ -1,41 +1,54 @@ -import axios from 'axios'; import { useEffect } from 'react'; import { useNavigate } from 'react-router-dom'; import { LoadingSpinner } from '@/shared/components/LoadingSpinner'; +import { postAuthCode } from './api/postAuthCode'; +import { setAccessToken, setTemporaryToken } from '../Signup/utils/token'; +import type { ErrorResponse } from '../Signup/type/error'; +import type { AxiosError } from 'axios'; + +interface LoginSuccessResponse { + status: 'LOGIN_SUCCESS'; + accessToken: string; + refreshToken: string; +} + +interface RegistrationRequiredResponse { + status: 'REGISTRATION_REQUIRED'; + temporaryToken: string; +} + +type LoginResponse = LoginSuccessResponse | RegistrationRequiredResponse; export const KakaoCallback = () => { const navigate = useNavigate(); useEffect(() => { const code = new URL(window.location.href).searchParams.get('code'); - if (!code) return; - console.log(code); + if (!code) { + navigate('/login'); + return; + } + const fetchToken = async () => { try { - const res = axios.post(`${import.meta.env.VITE_API_BASE_URL}/auth/kakao/login`, { - authorizationCode: code, - }); - console.log('응답res ', res); - - // CASE 1) 기존 회원 - - // 1-1. accessToken, refreshToken 발급 - // localStorage.setItem('accessToken', res.data.accessToken); - // localStorage.setItem('refreshToken ', res.data.refreshToken)- (수정전) - // refreshToken은 httpOnly 관리(수정후) - // ------------------------------------------------------------ - // 2-2 main 페이지 이동 - // navigate('/'); // 로그인 후 홈으로 이동 - - // CASE 2) 기존 회원 - // 2-1. 임시 토큰 - // 2-2. navigate('/signup') - } catch (error) { - console.log('error:', error); + const response: LoginResponse = await postAuthCode(code); + + switch (response.status) { + case 'LOGIN_SUCCESS': + setAccessToken(response.accessToken); + navigate('/'); + break; + case 'REGISTRATION_REQUIRED': + setTemporaryToken(response.temporaryToken); + navigate('/signup'); + break; + } + } catch (e) { + const error = e as AxiosError; + return new Error(error.response?.data.message); } }; - fetchToken(); }, [navigate]); diff --git a/src/pages/admin/Login/api/initInstance.ts b/src/pages/admin/Login/api/initInstance.ts new file mode 100644 index 00000000..f379341a --- /dev/null +++ b/src/pages/admin/Login/api/initInstance.ts @@ -0,0 +1,20 @@ +import axios from 'axios'; +import type { AxiosInstance, CreateAxiosDefaults } from 'axios'; + +const initInstance = (config: CreateAxiosDefaults): AxiosInstance => { + const instance = axios.create({ + timeout: 10000, + headers: { + 'Content-Type': 'application/json', + ...config.headers, + }, + // TODO 0. interceptor 적용지점(동아리 운영자) + ...config, + }); + + return instance; +}; + +export const apiInstance = initInstance({ + baseURL: import.meta.env.VITE_API_BASE_URL, +}); diff --git a/src/pages/admin/Login/api/postAuthCode.ts b/src/pages/admin/Login/api/postAuthCode.ts new file mode 100644 index 00000000..934a0169 --- /dev/null +++ b/src/pages/admin/Login/api/postAuthCode.ts @@ -0,0 +1,22 @@ +import { apiInstance } from './initInstance'; +import type { AxiosResponse } from 'axios'; + +interface LoginSuccessResponse { + status: 'LOGIN_SUCCESS'; + accessToken: string; + refreshToken: string; +} + +interface RegistrationRequiredResponse { + status: 'REGISTRATION_REQUIRED'; + temporaryToken: string; +} + +type LoginResponse = LoginSuccessResponse | RegistrationRequiredResponse; + +export const postAuthCode = async (code: string): Promise => { + const response: AxiosResponse = await apiInstance.post('/auth/kakao/login', { + authorizationCode: code, + }); + return response.data; +}; diff --git a/src/pages/admin/Signup/api/signup.ts b/src/pages/admin/Signup/api/signup.ts index 67431b40..0f1bf589 100644 --- a/src/pages/admin/Signup/api/signup.ts +++ b/src/pages/admin/Signup/api/signup.ts @@ -1,15 +1,43 @@ +import axios, { AxiosError, type AxiosResponse } from 'axios'; +import { apiInstance } from '../../Login/api/initInstance'; +import type { ErrorResponse } from '../type/error'; import type { SignupFormInputs } from '../type/signup'; -export const postSignupForm = async (formData: SignupFormInputs): Promise => { - const url = `${import.meta.env.VITE_API_BASE_URL}/auth/register`; - const response = await fetch(url, { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - }, - body: JSON.stringify(formData), - }); +export interface RegisterSuccessResponse { + status: 'REGISTER_SUCCESS'; + accessToken: string; + refreshToken: string; +} - if (!response.ok) throw new Error('회원 가입 양식을 제출하지 못했습니다.'); - return await response.json(); +export const postSignupForm = async ( + formData: SignupFormInputs, + tempToken: string, +): Promise => { + try { + const response: AxiosResponse = await apiInstance.post( + '/auth/register', + formData, + { + headers: { Authorization: `Bearer ${tempToken}` }, + }, + ); + return response.data; + } catch (e: unknown) { + if (axios.isAxiosError(e)) { + const error = e as AxiosError; + const status = error.response?.status; + const detailMsg = error.response?.data.detail; + switch (status) { + case 400: + throw new Error(`입력 오류: ${detailMsg}`); + case 401: + throw new Error(`권한 오류: ${detailMsg}`); + case 409: + throw new Error(`중복 오류: ${detailMsg}`); + default: + throw new Error(`알 수 없는 오류: ${e.message}`); + } + } + throw e; + } }; diff --git a/src/pages/admin/Signup/components/SignupForm/index.tsx b/src/pages/admin/Signup/components/SignupForm/index.tsx index 28fc77ab..d4d04459 100644 --- a/src/pages/admin/Signup/components/SignupForm/index.tsx +++ b/src/pages/admin/Signup/components/SignupForm/index.tsx @@ -1,11 +1,12 @@ import { useForm, FormProvider } from 'react-hook-form'; import { useNavigate } from 'react-router-dom'; import { toast } from 'sonner'; -import { postSignupForm } from '@/pages/admin/Signup/api/signup'; +import { postSignupForm, type RegisterSuccessResponse } from '@/pages/admin/Signup/api/signup'; import * as S from '@/pages/admin/Signup/components/SignupForm/index.styled'; import { Button } from '@/shared/components/Button'; import { OutlineInputField } from '@/shared/components/Form/InputField/OutlineInputField'; import { theme } from '@/styles/theme'; +import { getTemporaryToken, setAccessToken } from '../../utils/token'; import type { SignupFormInputs } from '@/pages/admin/Signup/type/signup'; export const SignupForm = () => { @@ -20,28 +21,38 @@ export const SignupForm = () => { phoneNumber: '', }, }); - const { errors, isSubmitting } = methods.formState; const onSubmit = async (signupFormValue: SignupFormInputs) => { + const temporaryToken = getTemporaryToken(); + + if (!temporaryToken) { + toast.error('회원가입을 위한 토큰이 존재하지 않습니다.'); + return; + } + try { - await postSignupForm(signupFormValue); + const response: RegisterSuccessResponse = await postSignupForm( + signupFormValue, + temporaryToken, + ); + + setAccessToken(response.accessToken); toast.success('회원가입 완료!', { style: { backgroundColor: theme.colors.primary, color: 'white' }, duration: 1000, + onAutoClose: () => navigate('/'), }); - setTimeout(() => { - navigate(`/login`); - }, 1000); - } catch (e) { - console.error(e); - toast.error('회원가입 실패!', { - duration: 1000, - style: { - backgroundColor: 'white', - color: theme.colors.error, - }, - }); + } catch (e: unknown) { + if (e instanceof Error) { + toast.error(e.message, { + duration: 1000, + style: { + backgroundColor: 'white', + color: theme.colors.error, + }, + }); + } } }; diff --git a/src/pages/admin/Signup/type/error.ts b/src/pages/admin/Signup/type/error.ts new file mode 100644 index 00000000..5bf09f8f --- /dev/null +++ b/src/pages/admin/Signup/type/error.ts @@ -0,0 +1,5 @@ +export interface ErrorResponse { + status: number; + message: string; + detail: string; +} diff --git a/src/pages/admin/Signup/utils/token.ts b/src/pages/admin/Signup/utils/token.ts new file mode 100644 index 00000000..032a0f94 --- /dev/null +++ b/src/pages/admin/Signup/utils/token.ts @@ -0,0 +1,5 @@ +export const setAccessToken = (token: string) => localStorage.setItem('accessToken', token); +export const getAccessToken = () => localStorage.getItem('accessToken'); +export const getRefreshToken = () => localStorage.getItem('refreshToken'); +export const setTemporaryToken = (token: string) => sessionStorage.setItem('temporaryToken', token); +export const getTemporaryToken = () => sessionStorage.getItem('temporaryToken'); diff --git a/src/pages/user/Apply/components/ApplicationForm/index.tsx b/src/pages/user/Apply/components/ApplicationForm/index.tsx index 3e8b308d..9cec999b 100644 --- a/src/pages/user/Apply/components/ApplicationForm/index.tsx +++ b/src/pages/user/Apply/components/ApplicationForm/index.tsx @@ -45,10 +45,8 @@ export const ApplicationForm = ({ questions }: Props) => { color: 'white', }, duration: 1000, + onAutoClose: () => navigate(`/clubs/${clubIdNumber}`), }); - setTimeout(() => { - navigate(`/clubs/${clubIdNumber}`); - }, 1000); }) .catch(() => { toast.error('제출 실패!', {