diff --git a/src/apis/auth.ts b/src/apis/auth.ts index 8244a9b4..e13efb7d 100644 --- a/src/apis/auth.ts +++ b/src/apis/auth.ts @@ -1,9 +1,12 @@ -import type { PostSigninRequestType, PostSigninResponseType } from '@/schema/auth'; +import type { PostSigninRequestType, PostSigninResponseType, PostSignUpRequestType, PostSignUpResponseType } from '@/schema/auth'; import httpClient from '.'; -const postSignin = async (request: PostSigninRequestType): Promise => { +export const postSignin = async (request: PostSigninRequestType): Promise => { const response = await httpClient.post('/auth/signIn', request); return response.data; }; -export default postSignin; +export const postSignup = async (request: PostSignUpRequestType): Promise => { + const response = await httpClient.post('/auth/signUp', request); + return response.data; +}; diff --git a/src/hooks/useRegisterMutation.ts b/src/hooks/useRegisterMutation.ts new file mode 100644 index 00000000..8cbe5fe7 --- /dev/null +++ b/src/hooks/useRegisterMutation.ts @@ -0,0 +1,77 @@ +import { postSignup } from '@/apis/auth'; +import { toast } from '@/components/ui/use-toast'; +import { useMutation } from '@tanstack/react-query'; +import { useRouter } from 'next/router'; +import { isAxiosError } from 'axios'; + +const useRegisterMutation = () => { + const router = useRouter(); + + return useMutation({ + mutationFn: postSignup, + onSuccess: (data) => { + localStorage.setItem('accessToken', data.accessToken); + localStorage.setItem('refreshToken', data.refreshToken); + router.push('/'); + }, + onError: (error) => { + if (isAxiosError(error)) { + const { status, data } = error.response || {}; + + if (!status) return; + + if (status === 400) { + const errorMessage = data?.message || '잘못된 요청입니다. 입력 값을 확인해주세요.'; + + if (errorMessage.includes('이미 사용중인 이메일')) { + toast({ + description: '이미 사용중인 이메일입니다.', + className: 'border-state-error text-state-error font-semibold', + }); + return; + } + + toast({ + description: errorMessage, + className: 'border-state-error text-state-error font-semibold', + }); + return; + } + + if (status === 500) { + const errorMessage = data?.message || '서버 오류가 발생했습니다. 잠시 후 다시 시도해주세요.'; + + // NOTE: swagger 문서에서 중복된 닉네임은 500에러와 함께 "Internal Server Error" 메시지로 응답 옴 + if (errorMessage.includes('Internal Server Error')) { + toast({ + description: '이미 존재하는 닉네임입니다.', + className: 'border-state-error text-state-error font-semibold', + }); + return; + } + + toast({ + description: errorMessage, + className: 'border-state-error text-state-error font-semibold', + }); + return; + } + + if (status >= 500) { + toast({ + description: '서버 오류가 발생했습니다. 잠시 후 다시 시도해주세요.', + className: 'border-state-error text-state-error font-semibold', + }); + return; + } + + toast({ + description: '알 수 없는 오류가 발생했습니다. 잠시 후 다시 시도해주세요.', + className: 'border-state-error text-state-error font-semibold', + }); + } + }, + }); +}; + +export default useRegisterMutation; diff --git a/src/hooks/useSignInMutation.ts b/src/hooks/useSignInMutation.ts index eaf9fd76..2f4ebb5e 100644 --- a/src/hooks/useSignInMutation.ts +++ b/src/hooks/useSignInMutation.ts @@ -1,4 +1,4 @@ -import postSignin from '@/apis/auth'; +import { postSignin } from '@/apis/auth'; import { toast } from '@/components/ui/use-toast'; import { useMutation } from '@tanstack/react-query'; import { useRouter } from 'next/router'; diff --git a/src/pages/auth/SignUp.tsx b/src/pages/auth/SignUp.tsx new file mode 100644 index 00000000..e6c52906 --- /dev/null +++ b/src/pages/auth/SignUp.tsx @@ -0,0 +1,129 @@ +import Image from 'next/image'; +import Link from 'next/link'; +import { zodResolver } from '@hookform/resolvers/zod'; +import { PostSignUpRequest, PostSignUpRequestType } from '@/schema/auth'; +import { useForm } from 'react-hook-form'; +import { Input } from '@/components/ui/input'; +import { Button } from '@/components/ui/button'; +import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage } from '@/components/ui/form'; +import useRegisterMutation from '@/hooks/useRegisterMutation'; + +export default function SignUp() { + const mutationRegister = useRegisterMutation(); + + const form = useForm({ + resolver: zodResolver(PostSignUpRequest), + mode: 'onBlur', + defaultValues: { + email: '', + password: '', + passwordConfirmation: '', + nickname: '', + }, + }); + + return ( +
+
+ + logo + +
+
+
+ mutationRegister.mutate(values))} className='flex flex-col items-center w-full h-full px-6'> + ( + + 이메일 + + + + + + )} + /> + ( + + 비밀번호 + + + + + + )} + /> + ( + + + + + + + )} + /> + ( + + 닉네임 + + + + + + )} + /> + + + +
+
+ + + +
+
+ ); +} diff --git a/src/schema/auth.ts b/src/schema/auth.ts index 33466608..0a9069a8 100644 --- a/src/schema/auth.ts +++ b/src/schema/auth.ts @@ -1,5 +1,25 @@ import * as z from 'zod'; +const PWD_VALIDATION_REGEX = /^(?=.*[a-zA-Z])(?=.*[0-9])(?=.*[!@#$%^&*])[a-zA-Z0-9!@#$%^&*]{8,}$/; + +// NOTE: 회원가입 스키마 +export const PostSignUpRequest = z + .object({ + email: z.string().min(1, { message: '이메일은 필수 입력입니다.' }).email({ message: '이메일 형식으로 작성해 주세요.' }), + password: z + .string() + .min(1, { message: '비밀번호는 필수 입력입니다.' }) + .min(8, { message: '비밀번호는 최소 8자 이상입니다.' }) + .regex(PWD_VALIDATION_REGEX, { message: '비밀번호는 숫자, 영문, 특수문자로만 가능합니다.' }), + passwordConfirmation: z.string().min(1, { message: '비밀번호 확인을 입력해주세요.' }), + nickname: z.string().min(1, { message: '닉네임은 필수 입력입니다.' }).max(20, { message: '닉네임은 최대 20자까지 가능합니다.' }), + }) + .refine((data) => data.password === data.passwordConfirmation, { + message: '비밀번호가 일치하지 않습니다.', + path: ['passwordConfirmation'], + }); + +// NOTE: 로그인 스키마 export const PostSigninRequest = z.object({ email: z.string().min(1, { message: '이메일은 필수 입력입니다.' }).email({ message: '올바른 이메일 주소가 아닙니다.' }), password: z.string().min(1, { message: '비밀번호는 필수 입력입니다.' }), @@ -15,11 +35,15 @@ const User = z.object({ image: z.string(), }); -export const PostSigninResponse = z.object({ +export const PostAuthResponse = z.object({ accessToken: z.string(), refreshToken: z.string(), user: User, }); +// NOTE: 회원가입 타입 +export type PostSignUpRequestType = z.infer; +export type PostSignUpResponseType = z.infer; +// NOTE: 로그인 타입 export type PostSigninRequestType = z.infer; -export type PostSigninResponseType = z.infer; +export type PostSigninResponseType = z.infer;