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

refactor: 로그인/회원가입 폼에 react-hook-form 적용 #223

Merged
merged 10 commits into from
Jun 23, 2024
4 changes: 3 additions & 1 deletion src/home/components/SignupContents/EmailForm/EmailForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,9 @@ export const EmailForm = ({ onConfirm }: EmailFormProps) => {
disabled={email === '' || disabled}
onClick={() => handleClick(onEmailSubmit)}
>
<StyledSignupButtonText>인증 메일 받기</StyledSignupButtonText>
<StyledSignupButtonText>
{disabled ? '잠시만 기다려주세요...' : '인증 메일 받기'}
</StyledSignupButtonText>
</BoxButton>
</StyledButtonsContainer>
</StyledSignupContentContainer>
Expand Down
5 changes: 3 additions & 2 deletions src/home/components/SignupContents/EmailForm/useEmailForm.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,18 +2,19 @@ import { useState } from 'react';

import { postAuthVerificationEmail } from '@/home/apis/authVerification.ts';
import { EmailFormProps } from '@/home/components/SignupContents/EmailForm/EmailForm.type.ts';
import { useFullEmail } from '@/hooks/useFullEmail';
import { useParseFullEmail } from '@/hooks/useParseFullEmail';

export const useEmailForm = ({ onConfirm }: EmailFormProps) => {
const [email, setEmail] = useState('');
const [emailError, setEmailError] = useState<string | undefined>(undefined);
const fullEmail = useFullEmail(email);
const parseFullEmail = useParseFullEmail();

const onChange = (e: React.ChangeEvent<HTMLInputElement>) => {
setEmail(e.target.value);
};

const onEmailSubmit = async () => {
const fullEmail = parseFullEmail(email);
const res = await postAuthVerificationEmail({ email: fullEmail, verificationType: 'SIGN_UP' });
if (res.data) {
onConfirm(fullEmail);
Expand Down
2 changes: 1 addition & 1 deletion src/home/components/SignupContents/SignupContents.style.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import styled from 'styled-components';

export const StyledSignupContentContainer = styled.div`
export const StyledSignupContentContainer = styled.form`
width: 100%;
display: flex;
flex-direction: column;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,8 @@ export interface SignupFormProps {
onConfirm: () => void;
email: string;
}

export interface SignupFormStates {
nickname: string;
password: string;
}
56 changes: 33 additions & 23 deletions src/home/components/SignupContents/SignupForm/SignupForm.tsx
Original file line number Diff line number Diff line change
@@ -1,61 +1,71 @@
import { BoxButton, PasswordTextField, SimpleTextField } from '@yourssu/design-system-react';
import { useForm } from 'react-hook-form';

import { SignupFormProps } from '@/home/components/SignupContents/SignupForm/SignUpForm.type.ts';
import { useSignUpForm } from '@/home/components/SignupContents/SignupForm/useSignUpForm.ts';
import { usePreventDuplicateClick } from '@/hooks/usePreventDuplicateClick.ts';
import { useSignupFormValidation } from '@/hooks/useSignupFormValidator.ts';
import {
SignupFormProps,
SignupFormStates,
} from '@/home/components/SignupContents/SignupForm/SignUpForm.type.ts';

import {
StyledSignupButtonText,
StyledSignupContentContainer,
StyledSignupContentTitle,
} from '../SignupContents.style';

import { useSignupFormConfirm } from './useSignupFormConfirm';
import { useSignupFormValidation } from './useSignupFormValidation';

export const SignupForm = ({ email, onConfirm }: SignupFormProps) => {
const { nickname, password, onFormConfirm, setNickname, setPassword } = useSignUpForm({
email,
onConfirm,
const { register, handleSubmit, setValue, watch, formState } = useForm<SignupFormStates>({
defaultValues: {
nickname: '',
password: '',
},
});

const { nicknameValidOnce, passwordValidOnce, isFormValid, isNicknameValid, isPasswordValid } =
useSignupFormValidation(nickname, password);
const onFormConfirm = useSignupFormConfirm({ email, onConfirm });

const { disabled, handleClick } = usePreventDuplicateClick();
const { nicknameValidOnce, passwordValidOnce, isFormValid, isNicknameValid, isPasswordValid } =
useSignupFormValidation({
nickname: watch('nickname'),
password: watch('password'),
});
Comment on lines -15 to +32
Copy link
Member

Choose a reason for hiding this comment

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

https://github.com/react-hook-form/resolvers RHF에 resolver를 사용할 수도 있습니다 ㅎㅎ


return (
<StyledSignupContentContainer>
<StyledSignupContentContainer onSubmit={handleSubmit(onFormConfirm)}>
<StyledSignupContentTitle>회원가입</StyledSignupContentTitle>
<SimpleTextField
value={nickname}
{...register('nickname', {
onChange: () => {
if (isNicknameValid) nicknameValidOnce.current = true;
},
})}
fieldLabel="사용할 닉네임을 입력해주세요."
helperLabel="한글, 영어, 숫자를 사용해 2~12자로 입력해주세요"
placeholder="ppushoong"
isNegative={!isNicknameValid && nicknameValidOnce.current}
onChange={(e) => {
if (isNicknameValid) nicknameValidOnce.current = true;
setNickname(e.target.value);
}}
onClickClearButton={() => {
nicknameValidOnce.current = false;
setNickname('');
setValue('nickname', '');
}}
/>
<PasswordTextField
{...register('password', {
onChange: () => {
if (isPasswordValid) passwordValidOnce.current = true;
},
})}
fieldLabel="사용할 비밀번호를 입력해주세요."
helperLabel="숫자, 영문자, 특수문자 조합으로 8자 이상 입력해주세요"
placeholder="비밀번호"
isNegative={!isPasswordValid && passwordValidOnce.current}
onChange={(e) => {
if (isPasswordValid) passwordValidOnce.current = true;
setPassword(e.target.value);
}}
/>
<BoxButton
type="submit"
rounding={8}
size="large"
variant="filled"
onClick={() => handleClick(onFormConfirm)}
disabled={!isFormValid || disabled}
disabled={!isFormValid || formState.isSubmitting}
>
<StyledSignupButtonText>회원가입</StyledSignupButtonText>
</BoxButton>
Expand Down
43 changes: 0 additions & 43 deletions src/home/components/SignupContents/SignupForm/useSignUpForm.ts

This file was deleted.

Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import { AxiosError } from 'axios';
import { SubmitHandler } from 'react-hook-form';

import { STORAGE_KEYS } from '@/constants/storage.constant';
import { postAuthSignUp } from '@/home/apis/postAuthSignUp';
import { AuthErrorData } from '@/home/types/Auth.type';

import { SignupFormProps, SignupFormStates } from './SignUpForm.type';

export const useSignupFormConfirm = ({ email, onConfirm }: SignupFormProps) => {
const onSignupError = (error: AxiosError) => {
alert(
(error as AxiosError<AuthErrorData>).response?.data.message || '회원가입에 실패했습니다.'
);
};

const onSignupSuccess = () => {
sessionStorage.removeItem(STORAGE_KEYS.EMAIL_AUTH_SESSION_TOKEN);
sessionStorage.removeItem(STORAGE_KEYS.EMAIL_AUTH_SESSION_TOKEN_EXPIRED_IN);
onConfirm();
};

const onFormConfirm: SubmitHandler<SignupFormStates> = async ({ nickname, password }) => {
const sessionToken = sessionStorage.getItem(STORAGE_KEYS.EMAIL_AUTH_SESSION_TOKEN);

if (!sessionToken) {
alert('세션 토큰이 없습니다.');
return;
}

const signUpParams = {
nickName: nickname,
password: password,
sessionToken: sessionToken,
email: email,
};

const { data, error } = await postAuthSignUp(signUpParams);

if (data) onSignupSuccess();
else if (error) onSignupError(error);
};

return onFormConfirm;
};
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,9 @@ import { useMemo, useRef } from 'react';

import { hasNumberAndEnglishWithSymbols, hasNumberOrEnglishOrHangulOrSpace } from '@yourssu/utils';

export const useSignupFormValidation = (nickname: string, password: string) => {
import { SignupFormStates } from './SignUpForm.type';

export const useSignupFormValidation = ({ nickname, password }: SignupFormStates) => {
const nicknameValidOnce = useRef(false);
const passwordValidOnce = useRef(false);

Expand Down
2 changes: 1 addition & 1 deletion src/home/pages/Login/Login.style.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import styled from 'styled-components';

export const StyledLoginContainer = styled.div`
export const StyledLoginContainer = styled.form`
width: 100%;
display: flex;
Expand Down
41 changes: 20 additions & 21 deletions src/home/pages/Login/Login.tsx
Copy link
Collaborator

Choose a reason for hiding this comment

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

StyledLoginContainer가 form 태그로 바뀌면서, 내부에 있는 버튼 모두가 submit button으로 작동합니다!
그래서 StyledBottomButtonContainer 내부에 있는 버튼 3개에서 의도치 않은 폼 제출이 일어나게 돼요

button

If your buttons are not for submitting form data to a server, be sure to set their type attribute to button. Otherwise, they will try to submit form data and to load the (nonexistent) response, possibly destroying the current state of the document. - MDN

폼 제출용 버튼(로그인)이 아닌 경우에 모두 type="button" 지정이 필요합니다

Copy link
Collaborator

Choose a reason for hiding this comment

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

EmailAuth 내에 있는 다음 버튼에도 같은 수정이 필요하겠네요!
다음 눌렀는데 폼 제출한 걸로 인식해서 새로고침 -> 약관 동의로 돌아가는 거 같음 ㅠㅠ..!!

button2

Copy link
Collaborator Author

@fecapark fecapark Jun 22, 2024

Choose a reason for hiding this comment

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

로그인 및 회원가입 폼 내의 모든 버튼들에 대해 type=button 추가 했습니다!
submit 타입인거는 submit으로 처리해줬습니다

Original file line number Diff line number Diff line change
@@ -1,18 +1,17 @@
import { useState } from 'react';

import {
BoxButton,
PasswordTextField,
PlainButton,
SuffixTextField,
} from '@yourssu/design-system-react';
import { SubmitHandler, useForm } from 'react-hook-form';
import { useNavigate } from 'react-router-dom';

import { EMAIL_DOMAIN } from '@/constants/email.constant';
import { StyledSignupContentTitle } from '@/home/components/SignupContents/SignupContents.style';
import { SignupFrame } from '@/home/components/SignupFrame/SignupFrame';
import { usePostLogin } from '@/home/hooks/usePostLogin';
import { useFullEmail } from '@/hooks/useFullEmail';
import { useParseFullEmail } from '@/hooks/useParseFullEmail';
import { useRedirectLoggedInEffect } from '@/hooks/useRedirectLoggedInEffect';

import {
Expand All @@ -22,15 +21,21 @@ import {
StyledLoginContainer,
} from './Login.style';

interface LoginFormStates {
email: string;
password: string;
}

export const Login = () => {
const [email, setEmail] = useState('');
const [password, setPassword] = useState('');
const { register, handleSubmit } = useForm<LoginFormStates>();

const navigate = useNavigate();
const fullEmail = useFullEmail(email);
const parseFullEmail = useParseFullEmail();
const loginMutation = usePostLogin();

const handleClickLogin = () => {
const onSubmit: SubmitHandler<LoginFormStates> = ({ email, password }) => {
const fullEmail = parseFullEmail(email);

loginMutation.mutate(
{ email: fullEmail, password },
{
Expand All @@ -46,50 +51,44 @@ export const Login = () => {

return (
<SignupFrame>
<StyledLoginContainer>
<StyledLoginContainer onSubmit={handleSubmit(onSubmit)}>
<StyledSignupContentTitle>로그인</StyledSignupContentTitle>
<SuffixTextField
{...register('email')}
fieldLabel="학교 메일"
value={email}
onChange={(e) => setEmail(e.target.value)}
placeholder="ppushoong"
isNegative={loginMutation.isError}
suffix={EMAIL_DOMAIN}
/>
<PasswordTextField
{...register('password')}
fieldLabel="비밀번호"
helperLabel={
loginMutation.isError
? '학교 메일 또는 비밀번호를 잘못 입력했습니다. 입력하신 내용을 다시 확인해주세요.'
: ''
}
placeholder="영문숫자포함8글자"
onChange={(e) => setPassword(e.target.value)}
placeholder="영문숫자특수문자포함8글자"
isNegative={loginMutation.isError}
/>

<BoxButton
size="large"
rounding={8}
variant="filled"
onClick={handleClickLogin}
disabled={loginMutation.isPending}
>
<BoxButton type="submit" size="large" rounding={8} variant="filled">
로그인
</BoxButton>

<StyledBottomButtonContainer>
{/* TODO: 학교 메일 찾기 route 완성되면 navigate 기능 추가 */}
<PlainButton size="medium" isPointed={false} isWarned={false}>
<PlainButton type="button" size="medium" isPointed={false} isWarned={false}>
<StyledBottomButtonWrapper>학교 메일 찾기</StyledBottomButtonWrapper>
</PlainButton>
<StyledButtonButtonSeparator>|</StyledButtonButtonSeparator>
{/* 비밀번호 찾기 route 완성되면 navigate 기능 추가 */}
<PlainButton size="medium" isPointed={false} isWarned={false}>
<PlainButton type="button" size="medium" isPointed={false} isWarned={false}>
<StyledBottomButtonWrapper>비밀번호 찾기</StyledBottomButtonWrapper>
</PlainButton>
<StyledButtonButtonSeparator>|</StyledButtonButtonSeparator>
<PlainButton
type="button"
size="medium"
isPointed={false}
isWarned={false}
Expand Down
12 changes: 0 additions & 12 deletions src/hooks/useFullEmail.ts

This file was deleted.

12 changes: 12 additions & 0 deletions src/hooks/useParseFullEmail.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { useCallback } from 'react';

import { EMAIL_DOMAIN } from '@/constants/email.constant';

export const useParseFullEmail = () => {
const parseFullEmail = useCallback((email: string) => {
if (email.endsWith(EMAIL_DOMAIN)) return email;
return email + EMAIL_DOMAIN;
}, []);

return parseFullEmail;
};