From 1ab431a4b270e3944eca17321d854d3e07643522 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=ED=95=9C=EC=8A=B9=EC=99=84?= Date: Wed, 17 Dec 2025 13:45:45 +0900 Subject: [PATCH 1/2] =?UTF-8?q?feat:=20=EB=B9=84=EB=B0=80=EB=B2=88?= =?UTF-8?q?=ED=98=B8=20=EC=B0=BE=EA=B8=B0=EC=99=80=20=ED=9A=8C=EC=9B=90?= =?UTF-8?q?=EA=B0=80=EC=9E=85=20=EC=9D=B4=EB=A9=94=EC=9D=BC=20=EC=9D=B8?= =?UTF-8?q?=EC=A6=9D=20=ED=83=80=EC=9E=85=20=EA=B5=AC=EB=B6=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit @0zuth 코드 확인 및 검증 부탁드립니다 --- src/entities/auth/DTO.d.ts | 5 ++++ .../auth/hooks/useEmailCertification.tsx | 8 +++++-- src/entities/user/api.ts | 9 +++++--- src/pages/auth/find-password.tsx | 7 +++++- src/widgets/auth/ui/EmailForm.tsx | 23 ++++++++++++------- 5 files changed, 38 insertions(+), 14 deletions(-) diff --git a/src/entities/auth/DTO.d.ts b/src/entities/auth/DTO.d.ts index 63f69a23..343ed195 100644 --- a/src/entities/auth/DTO.d.ts +++ b/src/entities/auth/DTO.d.ts @@ -57,3 +57,8 @@ export interface AppleCallbackRequest { id_token: string; fcmToken?: string; } + +export interface SendEmailCertificationRequest { + email: string; + type: "EMAIL" | "TEMPORARY_PASSWORD"; +} diff --git a/src/entities/auth/hooks/useEmailCertification.tsx b/src/entities/auth/hooks/useEmailCertification.tsx index ba411d9a..c8ec9977 100644 --- a/src/entities/auth/hooks/useEmailCertification.tsx +++ b/src/entities/auth/hooks/useEmailCertification.tsx @@ -7,8 +7,12 @@ import { toast } from "@/shared/hooks/useToast"; * 이메일 인증 번호 발송 API 호출 */ export const useSendEmailCertification = () => { - return useMutation({ - mutationFn: (email) => sendEmailCertification(email), + return useMutation< + boolean, + Error, + { email: string; type?: "EMAIL" | "TEMPORARY_PASSWORD" } + >({ + mutationFn: ({ email, type }) => sendEmailCertification(email, type), onError: (error) => { let errorMessage = "잠시 후 다시 시도해주세요."; diff --git a/src/entities/user/api.ts b/src/entities/user/api.ts index 7001d49d..eff367b6 100644 --- a/src/entities/user/api.ts +++ b/src/entities/user/api.ts @@ -1,6 +1,7 @@ import { getDefaultStore } from "jotai/vanilla"; import { getAccessToken, signOut } from "@/entities/auth/api"; +import { SendEmailCertificationRequest } from "@/entities/auth/DTO.d"; import { userAtom } from "@/entities/auth/model"; import { apiCall } from "@/shared/api/utils"; import { API_PATHS } from "@/shared/config/api"; @@ -195,16 +196,18 @@ export const updateUserRole = async ( /** * 이메일 인증 번호 발송 * @param email 인증 번호를 받을 이메일 + * @param type 인증 타입 (EMAIL | TEMPORARY_PASSWORD) * @returns 발송 성공 여부 */ export const sendEmailCertification = async ( - email: string + email: string, + type: "EMAIL" | "TEMPORARY_PASSWORD" = "EMAIL" ): Promise => { try { - await apiCall<{ email: string }, void>({ + await apiCall({ method: "POST", path: API_PATHS.USER.FIND_PASSWORD, - data: { email }, + data: { email, type }, withCredentials: true, }); diff --git a/src/pages/auth/find-password.tsx b/src/pages/auth/find-password.tsx index 9cdf439c..3b632a09 100644 --- a/src/pages/auth/find-password.tsx +++ b/src/pages/auth/find-password.tsx @@ -62,7 +62,12 @@ export default function FindPasswordPage() { }; const stepComponents = { - 1: , + 1: ( + + ), 2: ( ; +interface EmailFormProps { + onNext: (data: EmailFormValues) => void; + certificationType?: "EMAIL" | "TEMPORARY_PASSWORD"; +} + export function EmailForm({ onNext, -}: { - onNext: (data: EmailFormValues) => void; -}) { + certificationType = "EMAIL", +}: EmailFormProps) { const { mutate: sendEmail, isPending } = useSendEmailCertification(); const form = useForm({ @@ -27,11 +31,14 @@ export function EmailForm({ }); const onSubmit = (data: EmailFormValues) => { - sendEmail(data.email, { - onSuccess: (success: boolean) => { - if (success) onNext(data); - }, - }); + sendEmail( + { email: data.email, type: certificationType }, + { + onSuccess: (success: boolean) => { + if (success) onNext(data); + }, + } + ); }; return ( From 7d91c44be3a374d099c83d6787fec2d2c0f7908b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=ED=95=9C=EC=8A=B9=EC=99=84?= Date: Wed, 17 Dec 2025 15:49:20 +0900 Subject: [PATCH 2/2] =?UTF-8?q?feat:=20=EB=B9=84=EB=B0=80=EB=B2=88?= =?UTF-8?q?=ED=98=B8=20=EC=B0=BE=EA=B8=B0=EC=99=80=20=ED=9A=8C=EC=9B=90?= =?UTF-8?q?=EA=B0=80=EC=9E=85=20=EC=9D=B4=EB=A9=94=EC=9D=BC=20=EC=9D=B8?= =?UTF-8?q?=EC=A6=9D=20=ED=83=80=EC=9E=85=20=EA=B5=AC=EB=B6=84=20(2)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/entities/auth/DTO.d.ts | 3 +- .../auth/hooks/useEmailCertification.tsx | 5 ++- src/entities/user/api.ts | 6 +-- src/pages/auth/find-password.tsx | 6 +-- .../auth/ui/EmailCertificationForm.tsx | 42 ++++++++++++++----- src/widgets/auth/ui/EmailForm.tsx | 2 +- 6 files changed, 42 insertions(+), 22 deletions(-) diff --git a/src/entities/auth/DTO.d.ts b/src/entities/auth/DTO.d.ts index 343ed195..e5c32547 100644 --- a/src/entities/auth/DTO.d.ts +++ b/src/entities/auth/DTO.d.ts @@ -36,6 +36,7 @@ export interface TokenRefreshResponse { export interface ResetPasswordRequest { email: string; + code: string; } export interface ResetPasswordResponse { @@ -60,5 +61,5 @@ export interface AppleCallbackRequest { export interface SendEmailCertificationRequest { email: string; - type: "EMAIL" | "TEMPORARY_PASSWORD"; + certificationType: "EMAIL" | "TEMPORARY_PASSWORD"; } diff --git a/src/entities/auth/hooks/useEmailCertification.tsx b/src/entities/auth/hooks/useEmailCertification.tsx index c8ec9977..39ff2b8e 100644 --- a/src/entities/auth/hooks/useEmailCertification.tsx +++ b/src/entities/auth/hooks/useEmailCertification.tsx @@ -10,9 +10,10 @@ export const useSendEmailCertification = () => { return useMutation< boolean, Error, - { email: string; type?: "EMAIL" | "TEMPORARY_PASSWORD" } + { email: string; certificationType?: "EMAIL" | "TEMPORARY_PASSWORD" } >({ - mutationFn: ({ email, type }) => sendEmailCertification(email, type), + mutationFn: ({ email, certificationType }) => + sendEmailCertification(email, certificationType), onError: (error) => { let errorMessage = "잠시 후 다시 시도해주세요."; diff --git a/src/entities/user/api.ts b/src/entities/user/api.ts index eff367b6..bc1c7495 100644 --- a/src/entities/user/api.ts +++ b/src/entities/user/api.ts @@ -196,18 +196,18 @@ export const updateUserRole = async ( /** * 이메일 인증 번호 발송 * @param email 인증 번호를 받을 이메일 - * @param type 인증 타입 (EMAIL | TEMPORARY_PASSWORD) + * @param certificationType 인증 타입 (EMAIL | TEMPORARY_PASSWORD) * @returns 발송 성공 여부 */ export const sendEmailCertification = async ( email: string, - type: "EMAIL" | "TEMPORARY_PASSWORD" = "EMAIL" + certificationType: "EMAIL" | "TEMPORARY_PASSWORD" = "EMAIL" ): Promise => { try { await apiCall({ method: "POST", path: API_PATHS.USER.FIND_PASSWORD, - data: { email, type }, + data: { email, certificationType }, withCredentials: true, }); diff --git a/src/pages/auth/find-password.tsx b/src/pages/auth/find-password.tsx index 3b632a09..a7648472 100644 --- a/src/pages/auth/find-password.tsx +++ b/src/pages/auth/find-password.tsx @@ -1,4 +1,3 @@ -import { useResetPassword } from "@/entities/auth/hooks"; import { URL_PATHS } from "@/shared/constants/url-path"; import useFormData from "@/shared/hooks/useFormdata"; import { useStepNavigation } from "@/shared/hooks/useStepNavigation"; @@ -34,8 +33,6 @@ const FIND_PASSWORD_STEP_CONFIGS = { }; export default function FindPasswordPage() { - const { mutate: resetPassword } = useResetPassword(); - const { currentStep: step, goToPreviousStep, @@ -57,7 +54,7 @@ export default function FindPasswordPage() { goToNextStep(); }, 2: () => { - resetPassword({ email: formData.email! }); + // 임시 비밀번호 발급 완료 }, }; @@ -72,6 +69,7 @@ export default function FindPasswordPage() { ), }; diff --git a/src/widgets/auth/ui/EmailCertificationForm.tsx b/src/widgets/auth/ui/EmailCertificationForm.tsx index 99a70071..d3ab8090 100644 --- a/src/widgets/auth/ui/EmailCertificationForm.tsx +++ b/src/widgets/auth/ui/EmailCertificationForm.tsx @@ -2,7 +2,7 @@ import { zodResolver } from "@hookform/resolvers/zod"; import { useForm } from "react-hook-form"; import * as z from "zod"; -import { useCheckEmailCertification } from "@/entities/auth/hooks"; +import { useCheckEmailCertification, useResetPassword } from "@/entities/auth/hooks"; import CertificationField from "@/features/form/ui/fields/CertificationField"; import SubmitButton from "@/features/form/ui/SubmitButton"; import { Form } from "@/shared/ui/form"; @@ -15,14 +15,21 @@ export type EmailCertificationFormValues = z.infer; interface EmailCertificationFormProps { email: string; - onNext: () => void; + onNext: (certification?: string) => void; + mode?: "signup" | "findPassword"; } export function EmailCertificationForm({ email, onNext, + mode = "signup", }: EmailCertificationFormProps) { - const { mutate: checkEmail, isPending } = useCheckEmailCertification(); + const { mutate: checkEmail, isPending: isCheckPending } = + useCheckEmailCertification(); + const { mutate: resetPassword, isPending: isResetPending } = + useResetPassword(); + + const isPending = isCheckPending || isResetPending; const form = useForm({ resolver: zodResolver(step2Schema), @@ -31,14 +38,27 @@ export function EmailCertificationForm({ }); const onSubmit = (data: EmailCertificationFormValues) => { - checkEmail( - { email, certification: data.certification }, - { - onSuccess: (success: boolean) => { - if (success) onNext(); - }, - } - ); + if (mode === "findPassword") { + // 비밀번호 찾기: 임시 비밀번호 발급 API 호출 + resetPassword( + { email, code: data.certification }, + { + onSuccess: () => { + onNext(data.certification); + }, + } + ); + } else { + // 회원가입: 인증번호 검증만 + checkEmail( + { email, certification: data.certification }, + { + onSuccess: (success: boolean) => { + if (success) onNext(data.certification); + }, + } + ); + } }; return ( diff --git a/src/widgets/auth/ui/EmailForm.tsx b/src/widgets/auth/ui/EmailForm.tsx index d0caf5ed..796efab1 100644 --- a/src/widgets/auth/ui/EmailForm.tsx +++ b/src/widgets/auth/ui/EmailForm.tsx @@ -32,7 +32,7 @@ export function EmailForm({ const onSubmit = (data: EmailFormValues) => { sendEmail( - { email: data.email, type: certificationType }, + { email: data.email, certificationType }, { onSuccess: (success: boolean) => { if (success) onNext(data);