From ac75a5b91565dacf8b40fc45423ab80b74595d01 Mon Sep 17 00:00:00 2001 From: 2yunseong Date: Tue, 24 Feb 2026 01:36:10 +0900 Subject: [PATCH] =?UTF-8?q?feat:=20=ED=9A=8C=EC=9B=90=EA=B0=80=EC=9E=85=20?= =?UTF-8?q?=EC=8B=9C=20=EC=9D=B4=EB=A9=94=EC=9D=BC=20=EC=9D=B8=EC=A6=9D=20?= =?UTF-8?q?=EC=BD=94=EB=93=9C=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/app/signup/page.tsx | 97 +++++++++++++++++-- .../components/user/SignUpForm.component.tsx | 61 +++++++++++- frontend/src/apis/user/index.ts | 11 +++ 3 files changed, 158 insertions(+), 11 deletions(-) diff --git a/frontend/app/signup/page.tsx b/frontend/app/signup/page.tsx index 9c0ad7a8..8d2fac6c 100644 --- a/frontend/app/signup/page.tsx +++ b/frontend/app/signup/page.tsx @@ -1,10 +1,10 @@ "use client"; import SignUpForm from "@/components/user/SignUpForm.component"; -import { signUp } from "@/src/apis/user"; +import { signUp, verifyCode, verifySignUpEmail } from "@/src/apis/user"; import { useMutation } from "@tanstack/react-query"; import { useRouter } from "next/navigation"; -import { useReducer } from "react"; +import { useReducer, useState } from "react"; interface setFormAction { name: keyof typeof initState; @@ -37,6 +37,10 @@ interface SignUpData { const SignUpPage = () => { const navigate = useRouter(); const [signUpData, setForm] = useReducer(formDataReducer, initState); + const [emailCode, setEmailCode] = useState(""); + const [isEmailVerified, setIsEmailVerified] = useState(false); + const [isEmailCodeWrong, setIsEmailCodeWrong] = useState(false); + const [isVerificationRequested, setIsVerificationRequested] = useState(false); const { mutate: submitSignUp, isLoading } = useMutation({ mutationFn: (data: SignUpData) => @@ -47,19 +51,85 @@ const SignUpPage = () => { year: Number(data.generation), }), - onSuccess: () => { - alert("회원가입이 완료되었습니다."); - navigate.push("/signin"); - }, - onError: (error) => { - console.error("Sign up error:", error); + onSuccess: (result) => { + if (result) { + alert("회원가입이 완료되었습니다."); + navigate.push("/signin"); + return; + } alert("회원가입 중 오류가 발생했습니다. 다시 시도해주세요."); }, }); + const { mutate: sendEmailCode, isLoading: isSendingEmailCode } = useMutation({ + mutationFn: verifySignUpEmail, + onSuccess: (result) => { + if (result) { + setIsVerificationRequested(true); + setIsEmailCodeWrong(false); + alert("인증번호가 발송되었습니다."); + return; + } + alert("인증번호 발송에 실패했습니다. 다시 시도해주세요."); + }, + onError: () => { + alert("인증번호 발송에 실패했습니다. 다시 시도해주세요."); + }, + }); + + const { mutate: confirmEmailCode, isLoading: isCheckingEmailCode } = + useMutation({ + mutationFn: verifyCode, + onSuccess: (result) => { + if (result) { + setIsEmailVerified(true); + setIsEmailCodeWrong(false); + alert("이메일 인증이 완료되었습니다."); + return; + } + setIsEmailVerified(false); + setIsEmailCodeWrong(true); + }, + onError: () => { + setIsEmailVerified(false); + setIsEmailCodeWrong(true); + }, + }); + + const onSendVerificationCode = () => { + setIsEmailVerified(false); + sendEmailCode({ email: signUpData.email }); + }; + + const onVerifyEmailCode = () => { + confirmEmailCode({ + email: signUpData.email, + code: emailCode, + }); + }; + + const onChangeEmailCode = (value: string) => { + setEmailCode(value); + setIsEmailCodeWrong(false); + }; + + const onChangeForm = (action: setFormAction) => { + if (action.name === "email" && action.value !== signUpData.email) { + setIsEmailVerified(false); + setIsVerificationRequested(false); + setEmailCode(""); + setIsEmailCodeWrong(false); + } + setForm(action); + }; + const onSubmit = (e: React.FormEvent) => { if (isLoading) return; e.preventDefault(); + if (!isEmailVerified) { + alert("이메일 인증을 완료해주세요."); + return; + } submitSignUp(signUpData); }; @@ -69,7 +139,16 @@ const SignUpPage = () => { diff --git a/frontend/components/user/SignUpForm.component.tsx b/frontend/components/user/SignUpForm.component.tsx index 473b0e5a..c20ab527 100644 --- a/frontend/components/user/SignUpForm.component.tsx +++ b/frontend/components/user/SignUpForm.component.tsx @@ -10,6 +10,8 @@ const resetWarning = { interface SignUpProps { onSubmit: (e: React.FormEvent) => void; + onSendVerificationCode: () => void; + onVerifyEmailCode: () => void; data: { email: string; password: string; @@ -21,6 +23,13 @@ interface SignUpProps { name: "email" | "password" | "passwordConfirm" | "username" | "generation"; value: string; }) => void; + emailCode: string; + onChangeEmailCode: (value: string) => void; + isEmailVerified: boolean; + isEmailCodeWrong: boolean; + isVerificationRequested: boolean; + isSendingEmailCode: boolean; + isCheckingEmailCode: boolean; isLoading?: boolean; } @@ -33,7 +42,21 @@ interface SignUpProps { * @returns {JSX.Element} - SignUpForm component * @constructor */ -const SignUpForm = ({ onSubmit, data, setForm, isLoading }: SignUpProps) => { +const SignUpForm = ({ + onSubmit, + onSendVerificationCode, + onVerifyEmailCode, + data, + setForm, + emailCode, + onChangeEmailCode, + isEmailVerified, + isEmailCodeWrong, + isVerificationRequested, + isSendingEmailCode, + isCheckingEmailCode, + isLoading, +}: SignUpProps) => { const [isWarning, setWarning] = useState({ email: false, password: false, @@ -71,6 +94,40 @@ const SignUpForm = ({ onSubmit, data, setForm, isLoading }: SignUpProps) => { isWrong={isWarning.email} wrongMessage={"잘못된 이메일 형식입니다."} /> +
+ + {isEmailVerified && ( + 이메일 인증 완료 + )} +
+ {isVerificationRequested && !isEmailVerified && ( +
+ onChangeEmailCode(e.target.value)} + isWrong={isEmailCodeWrong} + wrongMessage={"인증코드가 올바르지 않습니다."} + /> + +
+ )} { diff --git a/frontend/src/apis/user/index.ts b/frontend/src/apis/user/index.ts index 0b76d6f3..0174d22f 100644 --- a/frontend/src/apis/user/index.ts +++ b/frontend/src/apis/user/index.ts @@ -60,6 +60,17 @@ interface VerifyEmailReq { email: string; } +export const verifySignUpEmail = async ({ email }: VerifyEmailReq) => { + try { + await https.post("/signup/verify", { + email, + }); + return true; + } catch (error) { + return false; + } +}; + export const verifyEmail = async ({ email }: VerifyEmailReq) => { // TODO: 서버에서 오는 에러를 핸들링해서, 컴포넌트에서 커스텀하게 사용할 수 있게 변경해야 한다. try {