diff --git a/src/home/components/SignupContents/AgreeTerms/AgreeTerms.tsx b/src/home/components/SignupContents/AgreeTerms/AgreeTerms.tsx index 516be5fd..fe83a3bd 100644 --- a/src/home/components/SignupContents/AgreeTerms/AgreeTerms.tsx +++ b/src/home/components/SignupContents/AgreeTerms/AgreeTerms.tsx @@ -170,6 +170,7 @@ export const AgreeTerms = ({ onConfirm }: AgreeTermsProps) => { })} { {error && {error}} { { | - + 학교 메일 열기 diff --git a/src/home/components/SignupContents/EmailForm/EmailForm.tsx b/src/home/components/SignupContents/EmailForm/EmailForm.tsx index 2ccc4cb6..8a4f28c0 100644 --- a/src/home/components/SignupContents/EmailForm/EmailForm.tsx +++ b/src/home/components/SignupContents/EmailForm/EmailForm.tsx @@ -41,18 +41,21 @@ export const EmailForm = ({ onConfirm }: EmailFormProps) => { - + 학교 메일 찾기 handleClick(onEmailSubmit)} > - 인증 메일 받기 + + {disabled ? '잠시만 기다려주세요...' : '인증 메일 받기'} + diff --git a/src/home/components/SignupContents/EmailForm/useEmailForm.ts b/src/home/components/SignupContents/EmailForm/useEmailForm.ts index 57bdde54..577b972f 100644 --- a/src/home/components/SignupContents/EmailForm/useEmailForm.ts +++ b/src/home/components/SignupContents/EmailForm/useEmailForm.ts @@ -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(undefined); - const fullEmail = useFullEmail(email); + const parseFullEmail = useParseFullEmail(); const onChange = (e: React.ChangeEvent) => { 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); diff --git a/src/home/components/SignupContents/SignupContents.style.ts b/src/home/components/SignupContents/SignupContents.style.ts index f8181358..1e25051a 100644 --- a/src/home/components/SignupContents/SignupContents.style.ts +++ b/src/home/components/SignupContents/SignupContents.style.ts @@ -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; diff --git a/src/home/components/SignupContents/SignupEnd/SignupEnd.tsx b/src/home/components/SignupContents/SignupEnd/SignupEnd.tsx index b0fcb521..30a5e8ee 100644 --- a/src/home/components/SignupContents/SignupEnd/SignupEnd.tsx +++ b/src/home/components/SignupContents/SignupEnd/SignupEnd.tsx @@ -13,7 +13,7 @@ export const SignupEnd = () => { 회원가입이 완료되었습니다 - + 로그인 페이지로 이동 diff --git a/src/home/components/SignupContents/SignupForm/SignUpForm.type.ts b/src/home/components/SignupContents/SignupForm/SignUpForm.type.ts index 60f44441..ef97e696 100644 --- a/src/home/components/SignupContents/SignupForm/SignUpForm.type.ts +++ b/src/home/components/SignupContents/SignupForm/SignUpForm.type.ts @@ -2,3 +2,8 @@ export interface SignupFormProps { onConfirm: () => void; email: string; } + +export interface SignupFormStates { + nickname: string; + password: string; +} diff --git a/src/home/components/SignupContents/SignupForm/SignupForm.tsx b/src/home/components/SignupContents/SignupForm/SignupForm.tsx index 750ccc04..0b579146 100644 --- a/src/home/components/SignupContents/SignupForm/SignupForm.tsx +++ b/src/home/components/SignupContents/SignupForm/SignupForm.tsx @@ -1,9 +1,11 @@ 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 { useSignupFormValidation } from '@/hooks/useSignupFormValidation'; import { StyledSignupButtonText, @@ -11,51 +13,59 @@ import { StyledSignupContentTitle, } from '../SignupContents.style'; +import { useSignupFormConfirm } from './useSignupFormConfirm'; + export const SignupForm = ({ email, onConfirm }: SignupFormProps) => { - const { nickname, password, onFormConfirm, setNickname, setPassword } = useSignUpForm({ - email, - onConfirm, + const { register, handleSubmit, setValue, watch, formState } = useForm({ + 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'), + }); return ( - + 회원가입 { + 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', ''); }} /> { + if (isPasswordValid) passwordValidOnce.current = true; + }, + })} fieldLabel="사용할 비밀번호를 입력해주세요." helperLabel="숫자, 영문자, 특수문자 조합으로 8자 이상 입력해주세요" placeholder="비밀번호" isNegative={!isPasswordValid && passwordValidOnce.current} - onChange={(e) => { - if (isPasswordValid) passwordValidOnce.current = true; - setPassword(e.target.value); - }} /> handleClick(onFormConfirm)} - disabled={!isFormValid || disabled} + disabled={!isFormValid || formState.isSubmitting} > 회원가입 diff --git a/src/home/components/SignupContents/SignupForm/useSignUpForm.ts b/src/home/components/SignupContents/SignupForm/useSignUpForm.ts deleted file mode 100644 index 27e52349..00000000 --- a/src/home/components/SignupContents/SignupForm/useSignUpForm.ts +++ /dev/null @@ -1,43 +0,0 @@ -import { useState } from 'react'; - -import { AxiosError } from 'axios'; - -import { STORAGE_KEYS } from '@/constants/storage.constant.ts'; -import { postAuthSignUp } from '@/home/apis/postAuthSignUp.ts'; -import { SignupFormProps } from '@/home/components/SignupContents/SignupForm/SignUpForm.type.ts'; -import { AuthErrorData } from '@/home/types/Auth.type.ts'; - -export const useSignUpForm = ({ email, onConfirm }: SignupFormProps) => { - const [nickname, setNickname] = useState(''); - const [password, setPassword] = useState(''); - - const onFormConfirm = async () => { - 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) { - sessionStorage.removeItem(STORAGE_KEYS.EMAIL_AUTH_SESSION_TOKEN); - sessionStorage.removeItem(STORAGE_KEYS.EMAIL_AUTH_SESSION_TOKEN_EXPIRED_IN); - onConfirm(); - } else if (error) { - alert( - (error as AxiosError).response?.data.message || '회원가입에 실패했습니다.' - ); - } - }; - - return { nickname, password, onFormConfirm, setNickname, setPassword }; -}; diff --git a/src/home/components/SignupContents/SignupForm/useSignupFormConfirm.tsx b/src/home/components/SignupContents/SignupForm/useSignupFormConfirm.tsx new file mode 100644 index 00000000..5f8bec4e --- /dev/null +++ b/src/home/components/SignupContents/SignupForm/useSignupFormConfirm.tsx @@ -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).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 = 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; +}; diff --git a/src/home/pages/Login/Login.style.ts b/src/home/pages/Login/Login.style.ts index 87ff0f7a..c4fac775 100644 --- a/src/home/pages/Login/Login.style.ts +++ b/src/home/pages/Login/Login.style.ts @@ -1,6 +1,6 @@ import styled from 'styled-components'; -export const StyledLoginContainer = styled.div` +export const StyledLoginContainer = styled.form` width: 100%; display: flex; diff --git a/src/home/pages/Login/Login.tsx b/src/home/pages/Login/Login.tsx index efbc8eee..ac366333 100644 --- a/src/home/pages/Login/Login.tsx +++ b/src/home/pages/Login/Login.tsx @@ -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 { @@ -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(); const navigate = useNavigate(); - const fullEmail = useFullEmail(email); + const parseFullEmail = useParseFullEmail(); const loginMutation = usePostLogin(); - const handleClickLogin = () => { + const onSubmit: SubmitHandler = ({ email, password }) => { + const fullEmail = parseFullEmail(email); + loginMutation.mutate( { email: fullEmail, password }, { @@ -46,50 +51,44 @@ export const Login = () => { return ( - + 로그인 setEmail(e.target.value)} placeholder="ppushoong" isNegative={loginMutation.isError} suffix={EMAIL_DOMAIN} /> setPassword(e.target.value)} + placeholder="영문숫자특수문자포함8글자" isNegative={loginMutation.isError} /> - + 로그인 {/* TODO: 학교 메일 찾기 route 완성되면 navigate 기능 추가 */} - + 학교 메일 찾기 | {/* 비밀번호 찾기 route 완성되면 navigate 기능 추가 */} - + 비밀번호 찾기 | { - const fullEmail = useMemo(() => { - if (email.endsWith(EMAIL_DOMAIN)) return email; - return email + EMAIL_DOMAIN; - }, [email]); - - return fullEmail; -}; diff --git a/src/hooks/useParseFullEmail.ts b/src/hooks/useParseFullEmail.ts new file mode 100644 index 00000000..ea4d7a63 --- /dev/null +++ b/src/hooks/useParseFullEmail.ts @@ -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; +}; diff --git a/src/hooks/useSignupFormValidator.ts b/src/hooks/useSignupFormValidation.ts similarity index 82% rename from src/hooks/useSignupFormValidator.ts rename to src/hooks/useSignupFormValidation.ts index f3de8a89..cbeae906 100644 --- a/src/hooks/useSignupFormValidator.ts +++ b/src/hooks/useSignupFormValidation.ts @@ -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 '@/home/components/SignupContents/SignupForm/SignUpForm.type'; + +export const useSignupFormValidation = ({ nickname, password }: SignupFormStates) => { const nicknameValidOnce = useRef(false); const passwordValidOnce = useRef(false);