-
Notifications
You must be signed in to change notification settings - Fork 2
feat: 회원가입 시 이메일 인증 코드 추가 #324
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
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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("인증번호 발송에 실패했습니다. 다시 시도해주세요."); | ||
| }, | ||
| }); | ||
|
Comment on lines
+64
to
+78
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 현재
1. API 함수 수정 제안 ( export const verifySignUpEmail = async ({ email }: VerifyEmailReq) => {
// try-catch를 제거하여 에러가 발생하면 useMutation의 onError에서 처리되도록 합니다.
await https.post("/signup/verify", {
email,
});
};2. |
||
|
|
||
| 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<HTMLFormElement>) => { | ||
| if (isLoading) return; | ||
| e.preventDefault(); | ||
| if (!isEmailVerified) { | ||
| alert("이메일 인증을 완료해주세요."); | ||
| return; | ||
| } | ||
|
|
||
| submitSignUp(signUpData); | ||
| }; | ||
|
|
@@ -69,7 +139,16 @@ const SignUpPage = () => { | |
| <SignUpForm | ||
| onSubmit={onSubmit} | ||
| data={signUpData} | ||
| setForm={setForm} | ||
| setForm={onChangeForm} | ||
| emailCode={emailCode} | ||
| onChangeEmailCode={onChangeEmailCode} | ||
| onSendVerificationCode={onSendVerificationCode} | ||
| onVerifyEmailCode={onVerifyEmailCode} | ||
| isEmailVerified={isEmailVerified} | ||
| isEmailCodeWrong={isEmailCodeWrong} | ||
| isVerificationRequested={isVerificationRequested} | ||
| isSendingEmailCode={isSendingEmailCode} | ||
| isCheckingEmailCode={isCheckingEmailCode} | ||
| isLoading={isLoading} | ||
| /> | ||
| </div> | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -10,6 +10,8 @@ const resetWarning = { | |
|
|
||
| interface SignUpProps { | ||
| onSubmit: (e: React.FormEvent<HTMLFormElement>) => 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; | ||
|
Comment on lines
+26
to
33
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
이렇게 하면 |
||
| } | ||
|
|
||
|
|
@@ -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={"잘못된 이메일 형식입니다."} | ||
| /> | ||
| <div className="flex items-center gap-3"> | ||
| <button | ||
| className="px-4 py-2 rounded-md bg-blue-500 hover:bg-blue-600 text-white disabled:bg-slate-300 disabled:cursor-not-allowed" | ||
| type="button" | ||
| onClick={onSendVerificationCode} | ||
| disabled={isSendingEmailCode || !isEmail(email)} | ||
| > | ||
| {isSendingEmailCode ? "인증코드 발송 중" : "인증코드 받기"} | ||
| </button> | ||
| {isEmailVerified && ( | ||
| <span className="text-sm text-blue-600">이메일 인증 완료</span> | ||
| )} | ||
| </div> | ||
| {isVerificationRequested && !isEmailVerified && ( | ||
| <div className="flex items-end gap-3"> | ||
| <InputFormItem | ||
| label={"인증코드"} | ||
| placeholder={"인증코드를 입력해주세요."} | ||
| type="text" | ||
| value={emailCode} | ||
| onChange={(e) => onChangeEmailCode(e.target.value)} | ||
| isWrong={isEmailCodeWrong} | ||
| wrongMessage={"인증코드가 올바르지 않습니다."} | ||
| /> | ||
| <button | ||
| className="h-[3.5rem] px-4 py-2 rounded-md bg-blue-500 hover:bg-blue-600 text-white disabled:bg-slate-300 disabled:cursor-not-allowed" | ||
| type="button" | ||
| onClick={onVerifyEmailCode} | ||
| disabled={isCheckingEmailCode || emailCode.trim().length === 0} | ||
| > | ||
| {isCheckingEmailCode ? "확인 중" : "인증코드 확인"} | ||
| </button> | ||
| </div> | ||
| )} | ||
| <InputFormItem | ||
| label={"이름"} | ||
| placeholder={"에코노"} | ||
|
|
@@ -111,7 +168,7 @@ const SignUpForm = ({ onSubmit, data, setForm, isLoading }: SignUpProps) => { | |
| <button | ||
| className="w-full p-4 rounded-md bg-blue-500 hover:bg-blue-600 text-white disabled:cursor-not-allowed" | ||
| type="submit" | ||
| disabled={isLoading} | ||
| disabled={isLoading || !isEmailVerified} | ||
| > | ||
| 회원가입 | ||
| </button> | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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; | ||
| } | ||
|
Comment on lines
+64
to
+71
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
이러한 변경은 await https.post("/signup/verify", {
email,
});
return true; |
||
| }; | ||
|
|
||
| export const verifyEmail = async ({ email }: VerifyEmailReq) => { | ||
| // TODO: 서버에서 오는 에러를 핸들링해서, 컴포넌트에서 커스텀하게 사용할 수 있게 변경해야 한다. | ||
| try { | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
이메일 인증과 관련된 여러 상태(
isEmailVerified,isEmailCodeWrong,isVerificationRequested)를 개별useState로 관리하고 있습니다. 기능이 더 복잡해지면 상태 간의 의존성으로 인해 관리가 어려워질 수 있습니다. 이와 같은 관련 상태들은 하나의 객체로 묶어useState로 관리하거나, 상태 전이가 명확한useReducer를 사용하여 관리하는 것을 고려해볼 수 있습니다. 이렇게 하면 상태 업데이트 로직을 중앙에서 관리할 수 있고, 유효하지 않은 상태 조합을 방지하는 데 도움이 됩니다.예시:
(위 코드는 예시이며, 실제 구현은 프로젝트의 컨텍스트에 맞게 조정이 필요합니다.)