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 ♻️ 회원가입 페이지 수정 #159

Merged
merged 9 commits into from
Aug 2, 2024
Binary file added public/horizen.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
2 changes: 1 addition & 1 deletion src/components/ui/input.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ const Input = React.forwardRef<HTMLInputElement, InputProps>(({ className, type,
<input
type={type}
className={cn(
'flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50',
'flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:outline-none disabled:cursor-not-allowed disabled:opacity-50',
className,
)}
ref={ref}
Expand Down
2 changes: 1 addition & 1 deletion src/components/ui/textarea.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ export interface TextareaProps extends React.TextareaHTMLAttributes<HTMLTextArea
const Textarea = React.forwardRef<HTMLTextAreaElement, TextareaProps>(({ className, ...props }, ref) => (
<textarea
className={cn(
'flex min-h-[80px] w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50',
'flex min-h-[80px] w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background placeholder:text-muted-foreground focus-visible:outline-none disabled:cursor-not-allowed disabled:opacity-50',
className,
)}
ref={ref}
Expand Down
84 changes: 46 additions & 38 deletions src/hooks/useRegisterMutation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,72 +4,80 @@ import { useMutation } from '@tanstack/react-query';
import { useRouter } from 'next/router';
import { isAxiosError } from 'axios';

const useRegisterMutation = () => {
const useRegisterMutation = (onRegisterError: (field: 'email' | 'nickname') => void) => {
const router = useRouter();

return useMutation({
mutationFn: postSignup,
onSuccess: (data) => {
localStorage.setItem('accessToken', data.accessToken);
localStorage.setItem('refreshToken', data.refreshToken);
router.push('/');
router.push('/auth/SignIn');
toast({
title: '회원가입 성공!',
description: '로그인 후 이용해주세요.',
className: 'bg-illust-green text-white font-semibold',
});
},
onError: (error) => {
if (isAxiosError(error)) {
const { status, data } = error.response || {};

if (!status) return;
if (!isAxiosError(error)) {
return;
}

if (status === 400) {
const errorMessage = data?.message || '잘못된 요청입니다. 입력 값을 확인해주세요.';
const { status, data } = error.response || {};

if (errorMessage.includes('이미 사용중인 이메일')) {
toast({
description: '이미 사용중인 이메일입니다.',
className: 'border-state-error text-state-error font-semibold',
});
return;
}
if (status === 400) {
const errorMessage = data?.message || '잘못된 요청입니다. 입력 값을 확인해주세요.';

if (errorMessage.includes('이미 사용중인 이메일')) {
toast({
description: errorMessage,
className: 'border-state-error text-state-error font-semibold',
description: '이미 사용중인 이메일입니다.',
className: 'bg-state-error text-white font-semibold',
});
onRegisterError('email');
return;
}

if (status === 500) {
const errorMessage = data?.message || '서버 오류가 발생했습니다. 잠시 후 다시 시도해주세요.';
toast({
description: errorMessage,
className: 'bg-state-error text-white font-semibold',
});
return;
}

// NOTE: swagger 문서에서 중복된 닉네임은 500에러와 함께 "Internal Server Error" 메시지로 응답 옴
if (errorMessage.includes('Internal Server Error')) {
toast({
description: '이미 존재하는 닉네임입니다.',
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: errorMessage,
className: 'border-state-error text-state-error font-semibold',
description: '이미 존재하는 닉네임입니다.',
className: 'bg-state-error text-white font-semibold',
});
onRegisterError('nickname');
return;
}

if (status >= 500) {
toast({
description: '서버 오류가 발생했습니다. 잠시 후 다시 시도해주세요.',
className: 'border-state-error text-state-error font-semibold',
});
return;
}
toast({
description: errorMessage,
className: 'bg-state-error text-white font-semibold',
});
return;
}

// NOTE: status값은 항상 있으며 undefined와 숫자를 비교연산 할 수 없어 Number로 설정
if (Number(status) >= 500) {
toast({
description: '알 수 없는 오류가 발생했습니다. 잠시 후 다시 시도해주세요.',
className: 'border-state-error text-state-error font-semibold',
description: '서버 오류가 발생했습니다. 잠시 후 다시 시도해주세요.',
className: 'bg-state-error text-white font-semibold',
});
return;
}

toast({
description: '알 수 없는 오류가 발생했습니다. 잠시 후 다시 시도해주세요.',
className: 'bg-state-error text-white font-semibold',
});
},
});
};
Expand Down
83 changes: 63 additions & 20 deletions src/pages/auth/SignUp.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { useState } from 'react';
import Image from 'next/image';
import Link from 'next/link';
import { zodResolver } from '@hookform/resolvers/zod';
Expand All @@ -9,7 +10,7 @@ import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage } from '
import useRegisterMutation from '@/hooks/useRegisterMutation';

export default function SignUp() {
const mutationRegister = useRegisterMutation();
const [focusedField, setFocusedField] = useState<string | null>(null);

const form = useForm<PostSignUpRequestType>({
resolver: zodResolver(PostSignUpRequest),
Expand All @@ -22,6 +23,20 @@ export default function SignUp() {
},
});

const { setFocus, setValue, trigger } = form;

const handleFieldError = (field: 'email' | 'nickname') => {
setFocus(field);
setFocusedField(field);
};

const mutationRegister = useRegisterMutation(handleFieldError);

const trimWhitespace = (fieldName: keyof PostSignUpRequestType, value: string) => {
setValue(fieldName, value.trim(), { shouldValidate: true, shouldDirty: true });
trigger(fieldName);
};

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]'>
Expand All @@ -37,13 +52,20 @@ export default function SignUp() {
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>
<FormLabel className={`md:mb-5 mb-4 font-pretendard lg:text-xl md:text-base sm:text-sm ${fieldState.invalid || focusedField === 'email' ? 'text-state-error' : 'text-blue-900'}`}>
이메일
</FormLabel>
<FormControl>
<Input
{...field}
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}
onBlur={(e) => {
trimWhitespace('email', e.target.value);
}}
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 || focusedField === 'email' ? 'border-2 border-state-error' : 'focus:border-blue-500'
}`}
/>
</FormControl>
<FormMessage className='flex justify-end text-[13px] text-state-error' />
Expand All @@ -58,10 +80,11 @@ export default function SignUp() {
<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
{...field}
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}
onBlur={(e) => trimWhitespace('password', e.target.value)}
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' : 'focus:border-blue-500'}`}
/>
</FormControl>
<FormMessage className='flex justify-end text-[13px] text-state-error' />
Expand All @@ -75,10 +98,11 @@ export default function SignUp() {
<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
{...field}
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}
onBlur={(e) => trimWhitespace('passwordConfirmation', e.target.value)}
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' : 'focus:border-blue-500'}`}
/>
</FormControl>
<FormMessage className='flex justify-end text-[13px] text-state-error' />
Expand All @@ -90,13 +114,16 @@ export default function SignUp() {
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>
<FormLabel className={`md:mb-5 mb-4 font-pretendard lg:text-xl md:text-base sm:text-sm ${fieldState.invalid || focusedField === 'nickname' ? 'text-state-error' : 'text-blue-900'}`}>
닉네임
</FormLabel>
<FormControl>
<Input
{...field}
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}
onBlur={(e) => trimWhitespace('nickname', e.target.value)}
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 || focusedField === 'nickname' ? 'border-2 border-state-error' : 'focus:border-blue-500'}`}
/>
</FormControl>
<FormMessage className='flex justify-end text-[13px] text-state-error' />
Expand All @@ -113,16 +140,32 @@ export default function SignUp() {
</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'>
<div className='flex flex-col items-center w-full lg:gap-10 gap-6'>
<div className='flex justify-center items-center lg:gap-6 gap-[14px] w-full lg:max-w-[640px] md:max-w-[384px] lg:px-0 md:px-0 px-6'>
<div className='flex-grow'>
<Image src='/horizen.png' alt='horizen' width={180} height={0} className='w-full h-[2px]' />
</div>
<h3 className='lg:text-xl text-xs font-pretendard text-blue-400 whitespace-nowrap'>SNS 계정으로 로그인하기</h3>
<div className='flex-grow'>
<Image src='/horizen.png' alt='horizen' width={180} height={0} className='w-full h-[2px]' />
</div>
</div>
<div className='flex justify-center gap-4'>
<Link
href={`https://nid.naver.com/oauth2.0/authorize?response_type=code&client_id=${process.env.NEXT_PUBLIC_NAVER_CLIENT_ID}&state=${'test'}&redirect_uri=${process.env.NEXT_PUBLIC_NAVER_REDIRECT_URI}`}
>
<Image src='/logo-naver.svg' alt='logo-naver' width={60} height={60} className='md:size-[60px] size-10' />
</Link>
{/* // FIXME: 구글 간편 로그인 리다이렉트시 500에러가 발생하는 부분으로 주석 처리하였음 */}
{/* <Link
href={`https://accounts.google.com/o/oauth2/v2/auth?client_id=${process.env.NEXT_PUBLIC_GOOGLE_CLIENT_ID}&redirect_uri=${process.env.NEXT_PUBLIC_GOOGLE_REDIRECT_URI}&response_type=code&scope=email%20profile`}
> */}
<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>
{/* </Link> */}
<Link href={`https://kauth.kakao.com/oauth/authorize?client_id=${process.env.NEXT_PUBLIC_KAKAO_CLIENT_ID}&redirect_uri=${process.env.NEXT_PUBLIC_REDIRECT_URI}&response_type=code`}>
<Image src='/logo-kakao.svg' alt='logo-kakao' width={60} height={60} className='md:size-[60px] size-10' />
</Link>
</div>
</div>
</div>
);
Expand Down
2 changes: 1 addition & 1 deletion src/schema/auth.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import * as z from 'zod';

const PWD_VALIDATION_REGEX = /^(?=.*[a-zA-Z])(?=.*[0-9])(?=.*[!@#$%^&*])[a-zA-Z0-9!@#$%^&*]{8,}$/;
const PWD_VALIDATION_REGEX = /^([a-z]|[A-Z]|[0-9]|[!@#$%^&])+$/;

// NOTE: 회원가입 스키마
export const PostSignUpRequest = z
Expand Down
Loading