Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

FE-66 🔀 회원가입 페이지 머지 요청 #72

Merged
merged 37 commits into from
Jul 30, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
37 commits
Select commit Hold shift + click to select a range
74648e7
:lipstick: 회원가입 페이지 레이아웃 추가
jangmoonwon Jul 17, 2024
8a437d1
:lipstick: 간편 로그인 로고 추가
jangmoonwon Jul 17, 2024
9537b85
:lipstick: 회원가입 ui 추가
jangmoonwon Jul 17, 2024
61098fb
Merge pull request #37 from epigram5-9/feat/FE-67
jangmoonwon Jul 18, 2024
eb34b5d
:sparkles: 회원가입 스키마 정의
jangmoonwon Jul 18, 2024
eaaf1cc
:heavy_plus_sign: 회원가입 페이지에 스키마 적용
jangmoonwon Jul 18, 2024
dfb693a
:lipstick: 에러 메시지 뜰 때 라벨, 인풋도 같은 에러 색깔 추가
jangmoonwon Jul 18, 2024
c1305fc
:memo: 유효성 검사를 통한 버튼의 비활성화 처리
jangmoonwon Jul 18, 2024
91e1fff
:memo: 유효성 검사에 따른 인풋 테두리 색상 처리
jangmoonwon Jul 18, 2024
03d62c5
Merge pull request #40 from epigram5-9/feat/FE-68
jangmoonwon Jul 19, 2024
bfd7d3b
:fire: AuthLayout 삭제
jangmoonwon Jul 19, 2024
467a76c
:art: 회원가입 페이지 브라우저 확대시 ui 깨짐 수정
jangmoonwon Jul 19, 2024
bd48061
:truck: 정규표현식 네이밍 변경
jangmoonwon Jul 19, 2024
1256f61
Merge pull request #48 from epigram5-9/fix/FE-66
jangmoonwon Jul 20, 2024
c3b2e06
:twisted_rightwards_arrows: Merge branch 'epic/FE-66' into merge/FE-66
jangmoonwon Jul 22, 2024
36f4b58
Merge pull request #55 from epigram5-9/merge/FE-66
jangmoonwon Jul 22, 2024
70d45e4
:sparkles: 회원가입 응답 데이터 스키마 정의
jangmoonwon Jul 23, 2024
f79b702
:sparkles: 회원가입 api 생성
jangmoonwon Jul 23, 2024
8ba0103
:sparkles: useRegisterMutation hook 생성
jangmoonwon Jul 23, 2024
e5efa3e
:zap: 회원가입 폼에 mutaion hook 적용
jangmoonwon Jul 23, 2024
ca394cc
:sparkles: Toaster 컴포넌트 추가
jangmoonwon Jul 23, 2024
4e9e01a
:sparkles: toast로 에러메시지 띄우기
jangmoonwon Jul 23, 2024
ea2264f
:zap: isAxiosError로 변경
jangmoonwon Jul 24, 2024
8a35cf2
Merge pull request #69 from epigram5-9/fix/FE-69
jangmoonwon Jul 24, 2024
a367f41
Merge pull request #59 from epigram5-9/feat/FE-69
jangmoonwon Jul 25, 2024
7c1f9c4
:twisted_rightwards_arrows: 메인 pr 최신화 및 confiict 수정
jangmoonwon Jul 26, 2024
5785703
:twisted_rightwards_arrows: 충돌 해결
jangmoonwon Jul 26, 2024
a715986
Merge branch 'epic/FE-66' into merge/FE-66
jangmoonwon Jul 26, 2024
9715918
:bug: postSignup 함수 추가
jangmoonwon Jul 26, 2024
0762f98
:bug: postSignin 내보내는 방식 수정
jangmoonwon Jul 26, 2024
a45fdf4
:twisted_rightwards_arrows: conflict 수정
jangmoonwon Jul 26, 2024
7e4eba6
:wrench: lint 수정
jangmoonwon Jul 26, 2024
b5227f6
Merge pull request #74 from epigram5-9/merge/FE-66
jangmoonwon Jul 26, 2024
912b9ef
:twisted_rightwards_arrows: Merge branch 'epic/FE-66' into merge/FE-66
jangmoonwon Jul 27, 2024
ceaaeec
Merge pull request #87 from epigram5-9/merge/FE-66
jangmoonwon Jul 27, 2024
192e06e
:recycle: 에러처리 로직 수정
jangmoonwon Jul 28, 2024
3322aaa
Merge pull request #95 from epigram5-9/fix/FE-66
jangmoonwon Jul 28, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 6 additions & 3 deletions src/apis/auth.ts
Original file line number Diff line number Diff line change
@@ -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<PostSigninResponseType> => {
export const postSignin = async (request: PostSigninRequestType): Promise<PostSigninResponseType> => {
const response = await httpClient.post('/auth/signIn', request);
return response.data;
};

export default postSignin;
export const postSignup = async (request: PostSignUpRequestType): Promise<PostSignUpResponseType> => {
const response = await httpClient.post('/auth/signUp', request);
return response.data;
};
77 changes: 77 additions & 0 deletions src/hooks/useRegisterMutation.ts
Original file line number Diff line number Diff line change
@@ -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)) {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  if (!isAxiosError(error)) return;

const { status, data } = error.response || {};

if (!status) return;
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.


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;
2 changes: 1 addition & 1 deletion src/hooks/useSignInMutation.ts
Original file line number Diff line number Diff line change
@@ -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';
Expand Down
129 changes: 129 additions & 0 deletions src/pages/auth/SignUp.tsx
Original file line number Diff line number Diff line change
@@ -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<PostSignUpRequestType>({
resolver: zodResolver(PostSignUpRequest),
mode: 'onBlur',
defaultValues: {
email: '',
password: '',
passwordConfirmation: '',
nickname: '',
},
});

return (
<div className='flex flex-col justify-center items-center bg-background-100 w-full min-h-screen'>
<header className='h-full mb-[50px] md:mb-[60px]'>
<Link href='/'>
<Image src='/lg.svg' alt='logo' width={172} height={48} />
</Link>
</header>
<div className='w-full'>
<Form {...form}>
<form onSubmit={form.handleSubmit((values: PostSignUpRequestType) => mutationRegister.mutate(values))} className='flex flex-col items-center w-full h-full px-6'>
<FormField
control={form.control}
name='email'
render={({ field, fieldState }) => (
<FormItem className='flex flex-col w-full lg:max-w-[640px] md:max-w-[384px] space-y-0 md:mb-10 mb-5'>
<FormLabel className={`md:mb-5 mb-4 font-pretendard lg:text-xl md:text-base sm:text-sm ${fieldState.invalid ? 'text-state-error' : 'text-blue-900'}`}>이메일</FormLabel>
<FormControl>
<Input
type='text'
placeholder='이메일'
className={`lg:h-16 h-11 px-4 lg:text-xl md:text-base placeholder-blue-400 rounded-xl bg-blue-200 font-pretendard ${fieldState.invalid ? 'border-2 border-state-error' : ''}`}
{...field}
/>
</FormControl>
<FormMessage className='flex justify-end text-[13px] text-state-error' />
</FormItem>
)}
/>
<FormField
control={form.control}
name='password'
render={({ field, fieldState }) => (
<FormItem className='flex flex-col w-full lg:max-w-[640px] md:max-w-[384px] space-y-0 md:mb-4 mb-[10px]'>
<FormLabel className={`md:mb-5 mb-4 font-pretendard lg:text-xl md:text-base sm:text-sm ${fieldState.invalid ? 'text-state-error' : 'text-blue-900'}`}>비밀번호</FormLabel>
<FormControl>
<Input
type='password'
placeholder='비밀번호'
className={`lg:h-16 h-11 px-4 lg:text-xl md:text-base placeholder-blue-400 rounded-xl bg-blue-200 font-pretendard ${fieldState.invalid ? 'border-2 border-state-error' : ''}`}
{...field}
/>
</FormControl>
<FormMessage className='flex justify-end text-[13px] text-state-error' />
</FormItem>
)}
/>
<FormField
control={form.control}
name='passwordConfirmation'
render={({ field, fieldState }) => (
<FormItem className='flex flex-col w-full lg:max-w-[640px] md:max-w-[384px] space-y-0 md:mb-10 mb-5'>
<FormControl>
<Input
type='password'
placeholder='비밀번호 확인'
className={`lg:h-16 h-11 px-4 lg:text-xl md:text-base placeholder-blue-400 rounded-xl bg-blue-200 font-pretendard ${fieldState.invalid ? 'border-2 border-state-error' : ''}`}
{...field}
/>
</FormControl>
<FormMessage className='flex justify-end text-[13px] text-state-error' />
</FormItem>
)}
/>
<FormField
control={form.control}
name='nickname'
render={({ field, fieldState }) => (
<FormItem className='flex flex-col w-full lg:max-w-[640px] md:max-w-[384px] md:mb-10 mb-[30px] space-y-0'>
<FormLabel className={`md:mb-5 mb-4 font-pretendard lg:text-xl md:text-base sm:text-sm ${fieldState.invalid ? 'text-state-error' : 'text-blue-900'}`}>닉네임</FormLabel>
<FormControl>
<Input
type='text'
placeholder='닉네임'
className={`lg:h-16 h-11 px-4 lg:text-xl md:text-base placeholder-blue-400 rounded-xl bg-blue-200 font-pretendard ${fieldState.invalid ? 'border-2 border-state-error' : ''}`}
{...field}
/>
</FormControl>
<FormMessage className='flex justify-end text-[13px] text-state-error' />
</FormItem>
)}
/>
<Button
disabled={!form.formState.isValid}
type='submit'
className={`w-full lg:max-w-[640px] md:max-w-[384px] lg:h-16 h-11 md:mb-[60px] mb-[50px] bg-black-500 font-pretendard text-white lg:text-xl md:text-base rounded-xl ${!form.formState.isValid ? 'bg-blue-300' : 'bg-black-500'}`}
>
가입하기
</Button>
</form>
</Form>
</div>
<div className='flex justify-center gap-4'>
<Button type='button' className='md:size-[60px] p-0'>
<Image src='/logo-naver.svg' alt='logo-naver' width={60} height={60} className='md:size-[60px] size-10' />
</Button>
<Button type='button' className='md:size-[60px] p-0'>
<Image src='/logo-google.svg' alt='logo-google' width={60} height={60} className='md:size-[60px] size-10' />
</Button>
<Button type='button' className='md:size-[60px] p-0'>
<Image src='/logo-kakao.svg' alt='logo-kakao' width={60} height={60} className='md:size-[60px] size-10' />
</Button>
</div>
</div>
);
}
28 changes: 26 additions & 2 deletions src/schema/auth.ts
Original file line number Diff line number Diff line change
@@ -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: '비밀번호는 필수 입력입니다.' }),
Expand All @@ -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<typeof PostSignUpRequest>;
export type PostSignUpResponseType = z.infer<typeof PostAuthResponse>;
// NOTE: 로그인 타입
export type PostSigninRequestType = z.infer<typeof PostSigninRequest>;
export type PostSigninResponseType = z.infer<typeof PostSigninResponse>;
export type PostSigninResponseType = z.infer<typeof PostAuthResponse>;
Loading