diff --git a/src/api/auth/auth.ts b/src/api/auth/auth.ts index 8196aeb..ef2dc1c 100644 --- a/src/api/auth/auth.ts +++ b/src/api/auth/auth.ts @@ -4,6 +4,8 @@ import type { IEmailSendRequest, IEmailSendResponse, IEmailVerifyRequest, + ISignUpRequest, + ISignUpResponse, } from "../../types/auth/auth"; import { axiosInstance } from "@/lib/axiosInstance"; @@ -27,3 +29,14 @@ export const verifyEmail = async ({ }); return data; }; + +// 단순 회원가입 +export const signUp = async ( + data: ISignUpRequest, +): Promise> => { + const { data: responseData } = await axiosInstance.post( + "/api/users/signup", + data, + ); + return responseData; +}; diff --git a/src/components/auth/signupStep/Step01Email.tsx b/src/components/auth/signupStep/Step01Email.tsx index 6d20209..2c0eece 100644 --- a/src/components/auth/signupStep/Step01Email.tsx +++ b/src/components/auth/signupStep/Step01Email.tsx @@ -1,105 +1,30 @@ -import { useCallback, useEffect, useState } from "react"; -import { type SubmitHandler, useForm, useWatch } from "react-hook-form"; -import { zodResolver } from "@hookform/resolvers/zod"; -import { toast } from "sonner"; -import type { z } from "zod"; - -import { step01Schema } from "@/utils/validation"; - -import { useAuth } from "@/hooks/auth/useAuth"; -import { useTimer } from "@/hooks/useTimer"; +import { useEmailVerification } from "@/hooks/auth/useEmailVerification"; import CommonAuthInput from "@/components/auth/common/CommonAuthInput"; import Button from "@/components/common/Button"; -import useAuthStore from "@/store/useAuthStore"; - interface IStep01EmailProps { onNext: () => void; } -type TStep01FormValues = z.infer; - export default function SignupEmail({ onNext }: IStep01EmailProps) { - const { setEmail } = useAuthStore(); - const { useSendCode, useCheckCode } = useAuth(); - - const [sendCode, setSendCode] = useState(false); - const [, setCodeVerify] = useState(false); - const [codeError, setCodeError] = useState(""); - const { - register, - handleSubmit, - control, - trigger, - formState: { errors, isValid }, - } = useForm({ - mode: "onBlur", - resolver: zodResolver(step01Schema), - }); - - const watchedEmail = useWatch({ control, name: "email" }); - const watchedCode = useWatch({ control, name: "code" }); - - const { formattedTime, restart, stop, isExpired } = useTimer(180, { - onExpire: () => { - toast.error("인증 시간이 만료되었습니다. 다시 시도해주세요."); + form: { register }, + watchedEmail, + sendCode, + formattedTime, + isExpired, + codeError, + errors, + isValid, + isPending, + handlers: { + postSendCode, + handleResendEmail, + handleEditEmail, + handleSubmit, }, - }); - - const resetVerification = useCallback(() => { - setSendCode(false); - stop(); - }, [stop]); - - const postSendCode = async () => { - setCodeVerify(false); - const isEmailValid = await trigger("email"); - if (isEmailValid && watchedEmail) { - useSendCode.mutate( - { email: watchedEmail }, - { - onSuccess: () => { - setSendCode(true); - toast.success("인증번호가 발송되었습니다."); - restart(); - }, - onError: (error) => { - toast.error( - error.response?.data?.message || "메일 발송에 실패했습니다.", - ); - }, - }, - ); - } - }; - - const onSubmit: SubmitHandler = async (data) => { - useCheckCode.mutate( - { email: data.email, authCode: data.code }, - { - onSuccess: () => { - setEmail(data.email); - onNext(); - }, - onError: (error) => { - setCodeError( - error.response?.data?.message || "인증번호가 올바르지 않습니다.", - ); - }, - }, - ); - }; - - useEffect(() => { - setCodeVerify(false); - setCodeError(""); - }, [watchedCode, watchedEmail]); - - useEffect(() => { - resetVerification(); - }, [watchedEmail, resetVerification]); + } = useEmailVerification({ onNext }); return (
@@ -126,18 +51,28 @@ export default function SignupEmail({ onNext }: IStep01EmailProps) { className="shrink-0 h-13.5! border border-brand-400 text-status-blue bg-white hover:bg-gray-50 px-4 rounded-15 font-body2 whitespace-nowrap" onClick={postSendCode} type="button" - disabled={useSendCode.isPending} + disabled={isPending} > 인증번호 받기
) : ( - +
+ + +
)} 다음으로 @@ -175,7 +110,7 @@ export default function SignupEmail({ onNext }: IStep01EmailProps) {
diff --git a/src/constants/auth.ts b/src/constants/auth.ts new file mode 100644 index 0000000..a42594d --- /dev/null +++ b/src/constants/auth.ts @@ -0,0 +1,2 @@ +// 인증번호 입력 시간 상수화 +export const AUTH_TIMER_DURATION = 180; diff --git a/src/hooks/auth/useAuth.ts b/src/hooks/auth/useAuth.ts index 61533f4..a410563 100644 --- a/src/hooks/auth/useAuth.ts +++ b/src/hooks/auth/useAuth.ts @@ -1,13 +1,15 @@ import { useCoreMutation } from "@/hooks/customQuery"; -import { sendEmail, verifyEmail } from "@/api/auth/auth"; +import { sendEmail, signUp, verifyEmail } from "@/api/auth/auth"; export const useAuth = () => { const useSendCode = useCoreMutation(sendEmail); const useCheckCode = useCoreMutation(verifyEmail); + const useSignUp = useCoreMutation(signUp); return { useSendCode, useCheckCode, + useSignUp, }; }; diff --git a/src/hooks/auth/useEmailVerification.ts b/src/hooks/auth/useEmailVerification.ts new file mode 100644 index 0000000..721bdb8 --- /dev/null +++ b/src/hooks/auth/useEmailVerification.ts @@ -0,0 +1,129 @@ +import { useCallback, useEffect, useState } from "react"; +import { useForm, useWatch } from "react-hook-form"; +import { zodResolver } from "@hookform/resolvers/zod"; +import { toast } from "sonner"; +import type { z } from "zod"; + +import { AUTH_TIMER_DURATION } from "@/constants/auth"; + +import { step01Schema } from "@/utils/validation"; + +import { useAuth } from "@/hooks/auth/useAuth"; +import { useTimer } from "@/hooks/common/useTimer"; + +import useAuthStore from "@/store/useAuthStore"; + +export type TStep01FormValues = z.infer; + +interface IUseEmailVerificationProps { + onNext: () => void; +} + +export const useEmailVerification = ({ + onNext, +}: IUseEmailVerificationProps) => { + const { setEmail } = useAuthStore(); + const { useSendCode, useCheckCode } = useAuth(); + + const [sendCode, setSendCode] = useState(false); + const [codeError, setCodeError] = useState(""); + + const form = useForm({ + mode: "onBlur", + resolver: zodResolver(step01Schema), + }); + + const { + control, + trigger, + handleSubmit, + formState: { errors, isValid }, + } = form; + + const watchedEmail = useWatch({ control, name: "email" }); + const watchedCode = useWatch({ control, name: "code" }); + + const { formattedTime, restart, stop, isExpired } = useTimer( + AUTH_TIMER_DURATION, + { + onExpire: () => { + toast.error("인증 시간이 만료되었습니다. 다시 시도해주세요."); + }, + }, + ); + + const handleEditEmail = useCallback(() => { + setSendCode(false); + stop(); + }, [stop]); + + const sendVerificationEmail = (email: string, successMessage: string) => { + useSendCode.mutate( + { email }, + { + onSuccess: () => { + setSendCode(true); + toast.success(successMessage); + restart(); + }, + onError: (error) => { + toast.error( + error.response?.data?.message || "메일 발송에 실패했습니다.", + ); + }, + }, + ); + }; + + const handleResendEmail = () => { + if (watchedEmail) { + sendVerificationEmail(watchedEmail, "인증번호가 재발송되었습니다."); + } + }; + + const postSendCode = async () => { + const isEmailValid = await trigger("email"); + if (isEmailValid && watchedEmail) { + sendVerificationEmail(watchedEmail, "인증번호가 발송되었습니다."); + } + }; + + const onSubmit = async (data: TStep01FormValues) => { + useCheckCode.mutate( + { email: data.email, authCode: data.code }, + { + onSuccess: () => { + setEmail(data.email); + onNext(); + }, + onError: (error) => { + setCodeError( + error.response?.data?.message || "인증번호가 올바르지 않습니다.", + ); + }, + }, + ); + }; + + useEffect(() => { + setCodeError(""); + }, [watchedCode, watchedEmail]); + + return { + form, + watchedEmail, + sendCode, + formattedTime, + isExpired, + codeError, + errors, + isValid, + isPending: useSendCode.isPending || useCheckCode.isPending, + handlers: { + postSendCode, + handleResendEmail, + handleEditEmail, + handleSubmit: handleSubmit(onSubmit), + }, + }; +}; diff --git a/src/hooks/useTimer.ts b/src/hooks/common/useTimer.ts similarity index 100% rename from src/hooks/useTimer.ts rename to src/hooks/common/useTimer.ts diff --git a/src/pages/auth/Signup.tsx b/src/pages/auth/Signup.tsx index 007b37f..5fb092a 100644 --- a/src/pages/auth/Signup.tsx +++ b/src/pages/auth/Signup.tsx @@ -1,4 +1,4 @@ -import { useState } from "react"; +import { useEffect, useState } from "react"; import { Link, useLocation } from "react-router-dom"; import Step01Email from "@/components/auth/signupStep/Step01Email"; @@ -10,11 +10,19 @@ import GoogleIcon from "@/assets/auth/social/google.svg?react"; import KakaoIcon from "@/assets/auth/social/kakao.svg?react"; import MailIcon from "@/assets/auth/social/mail.svg?react"; import NaverIcon from "@/assets/auth/social/naver.svg?react"; +import useAuthStore from "@/store/useAuthStore"; export default function Signup() { const location = useLocation(); + const { resetAuth } = useAuthStore(); const [step, setStep] = useState(location.state?.step || 0); + useEffect(() => { + return () => { + resetAuth(); + }; + }, [resetAuth]); + const handleEmailStart = () => { setStep(1); }; diff --git a/src/types/auth/auth.ts b/src/types/auth/auth.ts index e9df57c..b7cd098 100644 --- a/src/types/auth/auth.ts +++ b/src/types/auth/auth.ts @@ -15,3 +15,17 @@ export interface IEmailVerifyRequest { email: string; authCode: string; } + +// 회원가입 요청 타입 +export interface ISignUpRequest { + email: string; + password: string; + name: string; + phone_number: string; +} + +// 회원가입 응답 타입 +export interface ISignUpResponse { + userId: number; + createdAt: string; +}