diff --git a/apps/web/src/components/Auth/FindIdPage/Mobile/EmailPage/index.tsx b/apps/web/src/components/Auth/FindIdPage/Mobile/EmailPage/index.tsx index 7593592f..792ecb74 100644 --- a/apps/web/src/components/Auth/FindIdPage/Mobile/EmailPage/index.tsx +++ b/apps/web/src/components/Auth/FindIdPage/Mobile/EmailPage/index.tsx @@ -36,6 +36,8 @@ function MobileFindIdEmailPage() { isTimer, timerValue, stopTimer, + isResendLocked, + resendButtonText, } = useEmailVerification({ email }); const onClickSendVerificationButton = () => { @@ -79,8 +81,8 @@ function MobileFindIdEmailPage() { message={emailMessage} isButton disabled={isVerified} - buttonText="인증번호 발송" - buttonDisabled={!field.value || isDisabled || isVerified} + buttonText={resendButtonText('인증번호 발송')} + buttonDisabled={!field.value || isDisabled || isVerified || isResendLocked} buttonOnClick={() => checkEmailExists({ email: field.value })} > {emailMessage?.type === 'success' && ( diff --git a/apps/web/src/components/Auth/FindIdPage/Mobile/PhonePage/index.tsx b/apps/web/src/components/Auth/FindIdPage/Mobile/PhonePage/index.tsx index 388dba66..cfc4ef71 100644 --- a/apps/web/src/components/Auth/FindIdPage/Mobile/PhonePage/index.tsx +++ b/apps/web/src/components/Auth/FindIdPage/Mobile/PhonePage/index.tsx @@ -40,6 +40,8 @@ function MobileFindIdPhonePage() { isTimer, timerValue, stopTimer, + isResendLocked, + resendButtonText, } = usePhoneVerificationInFindId({ phoneNumber }); const isFormFilled = phoneNumber && verificationCode; @@ -81,8 +83,8 @@ function MobileFindIdPhonePage() { message={phoneMessage} isButton disabled={isVerified} - buttonText="인증번호 발송" - buttonDisabled={!field.value || isDisabled || isVerified} + buttonText={resendButtonText('인증번호 발송')} + buttonDisabled={!field.value || isDisabled || isVerified || isResendLocked} buttonOnClick={() => checkPhoneExists({ phone_number: phoneNumber })} > {phoneMessage?.type === 'default' && ( diff --git a/apps/web/src/components/Auth/FindIdPage/PC/EmailPage/index.tsx b/apps/web/src/components/Auth/FindIdPage/PC/EmailPage/index.tsx index 1ed4cd39..6dea0c18 100644 --- a/apps/web/src/components/Auth/FindIdPage/PC/EmailPage/index.tsx +++ b/apps/web/src/components/Auth/FindIdPage/PC/EmailPage/index.tsx @@ -40,6 +40,8 @@ function FindIdEmailPage() { isTimer, timerValue, stopTimer, + isResendLocked, + resendButtonText, } = useEmailVerification({ email }); const onClickSendVerificationButton = () => { @@ -108,9 +110,9 @@ function FindIdEmailPage() { type="button" onClick={() => checkEmailExists({ email })} className={styles['check-button']} - disabled={!field.value || isDisabled || isVerified} + disabled={!field.value || isDisabled || isVerified || isResendLocked} > - 인증번호 발송 + {resendButtonText('인증번호 발송')} )} diff --git a/apps/web/src/components/Auth/FindIdPage/PC/PhonePage/index.tsx b/apps/web/src/components/Auth/FindIdPage/PC/PhonePage/index.tsx index 8c65f85d..00da10c7 100644 --- a/apps/web/src/components/Auth/FindIdPage/PC/PhonePage/index.tsx +++ b/apps/web/src/components/Auth/FindIdPage/PC/PhonePage/index.tsx @@ -42,6 +42,8 @@ function FindIdPhonePage() { isTimer, timerValue, stopTimer, + isResendLocked, + resendButtonText, } = usePhoneVerificationInFindId({ phoneNumber }); const isFormFilled = phoneNumber && verificationCode; @@ -108,9 +110,9 @@ function FindIdPhonePage() { type="button" onClick={() => checkPhoneExists({ phone_number: phoneNumber })} className={styles['check-button']} - disabled={!field.value || isDisabled || isVerified} + disabled={!field.value || isDisabled || isVerified || isResendLocked} > - 인증번호 발송 + {resendButtonText('인증번호 발송')} )} diff --git a/apps/web/src/components/Auth/FindPasswordPage/Mobile/MobileVerifyEmail/index.tsx b/apps/web/src/components/Auth/FindPasswordPage/Mobile/MobileVerifyEmail/index.tsx index 7720ba22..f92613ec 100644 --- a/apps/web/src/components/Auth/FindPasswordPage/Mobile/MobileVerifyEmail/index.tsx +++ b/apps/web/src/components/Auth/FindPasswordPage/Mobile/MobileVerifyEmail/index.tsx @@ -38,6 +38,8 @@ function MobileVerifyEmail({ onNext, setContactType }: MobileFindPasswordProps) checkIdMatchEmail, idMessage, setIdMessage, + isResendLocked, + resendButtonText, } = useEmailVerification({ email, onNext }); const onClickSendVerificationButton = () => { @@ -104,8 +106,8 @@ function MobileVerifyEmail({ onNext, setContactType }: MobileFindPasswordProps) message={emailMessage} isButton disabled={isVerified} - buttonText="인증번호 발송" - buttonDisabled={!field.value || isDisabled || isVerified} + buttonText={resendButtonText('인증번호 발송')} + buttonDisabled={!field.value || isDisabled || isVerified || isResendLocked} onChange={(e) => { field.onChange(e); setEmailMessage(null); diff --git a/apps/web/src/components/Auth/FindPasswordPage/Mobile/MobileVerifyPhone/index.tsx b/apps/web/src/components/Auth/FindPasswordPage/Mobile/MobileVerifyPhone/index.tsx index 00c26336..277cfef1 100644 --- a/apps/web/src/components/Auth/FindPasswordPage/Mobile/MobileVerifyPhone/index.tsx +++ b/apps/web/src/components/Auth/FindPasswordPage/Mobile/MobileVerifyPhone/index.tsx @@ -42,6 +42,8 @@ function MobileVerifyPhone({ onNext, goToEmailStep, setContactType }: MobileFind checkIdExists, checkIdMatchPhone, setIdMessage, + isResendLocked, + resendButtonText, } = usePhoneVerification({ phoneNumber, onNext }); const onClickSendVerificationButton = () => { @@ -107,8 +109,8 @@ function MobileVerifyPhone({ onNext, goToEmailStep, setContactType }: MobileFind message={phoneMessage} isButton disabled={isVerified} - buttonText="인증번호 발송" - buttonDisabled={!field.value || isDisabled || isVerified} + buttonText={resendButtonText('인증번호 발송')} + buttonDisabled={!field.value || isDisabled || isVerified || isResendLocked} onChange={(e) => { field.onChange(e); setPhoneMessage(null); diff --git a/apps/web/src/components/Auth/FindPasswordPage/PC/PCVerifyEmail/index.tsx b/apps/web/src/components/Auth/FindPasswordPage/PC/PCVerifyEmail/index.tsx index 6db26edc..863d8b6f 100644 --- a/apps/web/src/components/Auth/FindPasswordPage/PC/PCVerifyEmail/index.tsx +++ b/apps/web/src/components/Auth/FindPasswordPage/PC/PCVerifyEmail/index.tsx @@ -46,6 +46,8 @@ function PCVerifyEmail({ onNext, onBack, setContactType }: FindPasswordProps) { checkIdMatchEmail, idMessage, setIdMessage, + isResendLocked, + resendButtonText, } = useEmailVerification({ email, onNext }); const onClickSendVerificationButton = () => { @@ -156,9 +158,9 @@ function PCVerifyEmail({ onNext, onBack, setContactType }: FindPasswordProps) { checkEmailExists({ email }); }} className={styles['check-button']} - disabled={!field.value || isDisabled || isVerified} + disabled={!field.value || isDisabled || isVerified || isResendLocked} > - {isSendingVerification ? '...' : buttonText} + {isSendingVerification ? '...' : resendButtonText(buttonText)} )} diff --git a/apps/web/src/components/Auth/FindPasswordPage/PC/PCVerifyPhone/index.tsx b/apps/web/src/components/Auth/FindPasswordPage/PC/PCVerifyPhone/index.tsx index bb374952..5f9bfd60 100644 --- a/apps/web/src/components/Auth/FindPasswordPage/PC/PCVerifyPhone/index.tsx +++ b/apps/web/src/components/Auth/FindPasswordPage/PC/PCVerifyPhone/index.tsx @@ -47,6 +47,8 @@ function PCVerifyPhone({ onNext, onBack, goToEmailStep, setContactType }: FindPa stopTimer, checkIdExists, checkIdMatchPhone, + isResendLocked, + resendButtonText, } = usePhoneVerification({ phoneNumber, onNext }); const onClickSendVerificationButton = () => { @@ -167,9 +169,9 @@ function PCVerifyPhone({ onNext, onBack, goToEmailStep, setContactType }: FindPa setIsVerificationSent(true); }} className={styles['check-button']} - disabled={!field.value || isDisabled || isVerified} + disabled={!field.value || isDisabled || isVerified || isResendLocked} > - {isSendingVerification ? '...' : buttonText} + {isSendingVerification ? '...' : resendButtonText(buttonText)} {!isVerificationSent && !isCorrect && ( diff --git a/apps/web/src/components/Auth/SignupPage/Steps/MobileVerificationStep/index.tsx b/apps/web/src/components/Auth/SignupPage/Steps/MobileVerificationStep/index.tsx index 219fe284..97c07b1b 100644 --- a/apps/web/src/components/Auth/SignupPage/Steps/MobileVerificationStep/index.tsx +++ b/apps/web/src/components/Auth/SignupPage/Steps/MobileVerificationStep/index.tsx @@ -10,6 +10,7 @@ import type { InputMessage } from 'interfaces/InputMessage'; import { GENDER_OPTIONS, MESSAGES, REGEX } from 'static/auth'; import ROUTES from 'static/routes'; import { useSessionLogger } from 'utils/hooks/analytics/useSessionLogger'; +import useResendLock from 'utils/hooks/auth/useResendLock'; import useBooleanState from 'utils/hooks/state/useBooleanState'; import useCountdownTimer from 'utils/hooks/ui/useCountdownTimer'; @@ -67,6 +68,12 @@ function MobileVerification({ onNext }: MobileVerificationProps) { }, }); + const { isResendLocked, markSent, withResendLock, resendButtonText } = useResendLock( + phoneNumber, + isTimer, + timerValue, + ); + const clearVerificationMessageTimer = () => { if (verificationMessageTimerRef.current) { clearTimeout(verificationMessageTimerRef.current); @@ -76,7 +83,8 @@ function MobileVerification({ onNext }: MobileVerificationProps) { const { mutate: sendSMSToUser } = useMutation({ mutationFn: smsSend, - onSuccess: (data: SmsSendResponse) => { + onSuccess: (data: SmsSendResponse, { phone_number }) => { + markSent(phone_number); clearVerificationMessageTimer(); setPhoneMessage({ type: 'success', content: MESSAGES.PHONE.CODE_SENT }); runTimer(); @@ -236,9 +244,9 @@ function MobileVerification({ onNext }: MobileVerificationProps) { message={phoneMessage} isButton disabled={isVerified} - buttonText="인증번호 발송" - buttonDisabled={!field.value || isDisabled || isVerified} - buttonOnClick={() => checkPhoneNumber(phoneNumber)} + buttonText={resendButtonText('인증번호 발송')} + buttonDisabled={!field.value || isDisabled || isVerified || isResendLocked} + buttonOnClick={withResendLock(() => checkPhoneNumber(phoneNumber))} maxLength={11} > {phoneMessage?.type === 'success' && ( diff --git a/apps/web/src/components/Auth/SignupPage/Steps/VerificationStep/index.tsx b/apps/web/src/components/Auth/SignupPage/Steps/VerificationStep/index.tsx index 3af21bc5..56544173 100644 --- a/apps/web/src/components/Auth/SignupPage/Steps/VerificationStep/index.tsx +++ b/apps/web/src/components/Auth/SignupPage/Steps/VerificationStep/index.tsx @@ -59,6 +59,8 @@ function Verification({ onNext, onBack, setUserType }: VerificationProps) { timerValue, stopTimer, checkPhoneNumber, + isResendLocked, + resendButtonText, } = usePhoneVerification({ phoneNumber, onNext }); const onClickSendVerificationButton = () => { @@ -228,9 +230,9 @@ function Verification({ onNext, onBack, setUserType }: VerificationProps) { }); }} className={styles['check-button']} - disabled={!field.value || isDisabled || isVerified} + disabled={!field.value || isDisabled || isVerified || isResendLocked} > - {isSendingVerification ? '...' : buttonText} + {isSendingVerification ? '...' : resendButtonText(buttonText)} diff --git a/apps/web/src/utils/hooks/auth/useEmailVerification.ts b/apps/web/src/utils/hooks/auth/useEmailVerification.ts index c1be1dc3..cdebf5fc 100644 --- a/apps/web/src/utils/hooks/auth/useEmailVerification.ts +++ b/apps/web/src/utils/hooks/auth/useEmailVerification.ts @@ -14,6 +14,7 @@ import { isKoinError } from 'interfaces/APIError'; import type { InputMessage } from 'interfaces/InputMessage'; import { MESSAGES, STORAGE_KEY } from 'static/auth'; import ROUTES from 'static/routes'; +import useResendLock from 'utils/hooks/auth/useResendLock'; import useBooleanState from 'utils/hooks/state/useBooleanState'; import { useSessionStorage } from 'utils/hooks/state/useWebStorage'; import useCountdownTimer from 'utils/hooks/ui/useCountdownTimer'; @@ -54,6 +55,8 @@ function useEmailVerification({ email, onNext }: UseEmailVerificationProps) { }, }); + const { isResendLocked, markSent, withResendLock, resendButtonText } = useResendLock(email, isTimer, timerValue); + const resetVerificationState = () => { disableButton(); disableVerified(); @@ -66,7 +69,8 @@ function useEmailVerification({ email, onNext }: UseEmailVerificationProps) { const { mutate: sendVerificationEmail, isPending: isSendingVerification } = useMutation({ mutationFn: verificationEmailSend, - onSuccess: ({ total_count, remaining_count, current_count }) => { + onSuccess: ({ total_count, remaining_count, current_count }, { email: sentEmail }) => { + markSent(sentEmail); setEmailMessage({ type: 'success', content: MESSAGES.EMAIL.CODE_SENT }); runTimer(); setEmailSendCountData({ total_count, remaining_count, current_count }); @@ -151,9 +155,11 @@ function useEmailVerification({ email, onNext }: UseEmailVerificationProps) { return { emailMessage, - checkEmailExists, + checkEmailExists: withResendLock(checkEmailExists), checkVerificationEmailVerify, isSendingVerification, + isResendLocked, + resendButtonText, findEmail, isDisabled, disableButton, diff --git a/apps/web/src/utils/hooks/auth/usePhoneVerification.ts b/apps/web/src/utils/hooks/auth/usePhoneVerification.ts index e0187af0..212850cc 100644 --- a/apps/web/src/utils/hooks/auth/usePhoneVerification.ts +++ b/apps/web/src/utils/hooks/auth/usePhoneVerification.ts @@ -10,6 +10,7 @@ import type { InputMessage } from 'interfaces/InputMessage'; import { MESSAGES, STORAGE_KEY } from 'static/auth'; import ROUTES from 'static/routes'; import { useSessionLogger } from 'utils/hooks/analytics/useSessionLogger'; +import useResendLock from 'utils/hooks/auth/useResendLock'; import useBooleanState from 'utils/hooks/state/useBooleanState'; import { useSessionStorage } from 'utils/hooks/state/useWebStorage'; import useCountdownTimer from 'utils/hooks/ui/useCountdownTimer'; @@ -59,6 +60,12 @@ function usePhoneVerification({ phoneNumber, onNext, step }: UsePhoneVerificatio }, }); + const { isResendLocked, markSent, withResendLock, resendButtonText } = useResendLock( + phoneNumber, + isTimer, + timerValue, + ); + const resetVerificationState = () => { disableButton(); disableVerified(); @@ -71,7 +78,8 @@ function usePhoneVerification({ phoneNumber, onNext, step }: UsePhoneVerificatio const { mutate: sendVerificationSms, isPending: isSendingVerification } = useMutation({ mutationFn: smsSend, - onSuccess: ({ total_count, remaining_count, current_count }) => { + onSuccess: ({ total_count, remaining_count, current_count }, { phone_number }) => { + markSent(phone_number); setPhoneMessage({ type: 'success', content: MESSAGES.PHONE.CODE_SENT }); setValue('phoneMessage', { type: 'success', content: MESSAGES.PHONE.CODE_SENT }); runTimer(); @@ -155,7 +163,8 @@ function usePhoneVerification({ phoneNumber, onNext, step }: UsePhoneVerificatio const { mutate: sendSMSToUser } = useMutation({ mutationFn: smsSend, - onSuccess: (data: SmsSendResponse) => { + onSuccess: (data: SmsSendResponse, { phone_number }) => { + markSent(phone_number); setPhoneMessage({ type: 'success', content: MESSAGES.PHONE.CODE_SENT }); setValue('phoneMessage', { type: 'success', content: MESSAGES.PHONE.CODE_SENT }); runTimer(); @@ -239,9 +248,11 @@ function usePhoneVerification({ phoneNumber, onNext, step }: UsePhoneVerificatio return { phoneMessage, - checkPhoneExists, + checkPhoneExists: withResendLock(checkPhoneExists), checkVerificationSmsVerify, isSendingVerification, + isResendLocked, + resendButtonText, findId, isDisabled, disableButton, @@ -260,7 +271,7 @@ function usePhoneVerification({ phoneNumber, onNext, step }: UsePhoneVerificatio checkIdMatchPhone, idMessage, setIdMessage, - checkPhoneNumber, + checkPhoneNumber: withResendLock(checkPhoneNumber), }; } diff --git a/apps/web/src/utils/hooks/auth/usePhoneVerificationInFindId.ts b/apps/web/src/utils/hooks/auth/usePhoneVerificationInFindId.ts index da4c5284..9ce5b0a2 100644 --- a/apps/web/src/utils/hooks/auth/usePhoneVerificationInFindId.ts +++ b/apps/web/src/utils/hooks/auth/usePhoneVerificationInFindId.ts @@ -8,6 +8,7 @@ import { isKoinError } from 'interfaces/APIError'; import type { InputMessage } from 'interfaces/InputMessage'; import { MESSAGES, STORAGE_KEY } from 'static/auth'; import ROUTES from 'static/routes'; +import useResendLock from 'utils/hooks/auth/useResendLock'; import useBooleanState from 'utils/hooks/state/useBooleanState'; import { useSessionStorage } from 'utils/hooks/state/useWebStorage'; import useCountdownTimer from 'utils/hooks/ui/useCountdownTimer'; @@ -53,9 +54,16 @@ function usePhoneVerificationInFindId({ phoneNumber, onNext }: UsePhoneVerificat }, }); + const { isResendLocked, markSent, withResendLock, resendButtonText } = useResendLock( + phoneNumber, + isTimer, + timerValue, + ); + const { mutate: sendVerificationSms } = useMutation({ mutationFn: smsSend, - onSuccess: ({ total_count, remaining_count, current_count }) => { + onSuccess: ({ total_count, remaining_count, current_count }, { phone_number }) => { + markSent(phone_number); setPhoneMessage({ type: 'success', content: MESSAGES.PHONE.CODE_SENT }); runTimer(); setSmsSendCountData({ total_count, remaining_count, current_count }); @@ -113,7 +121,8 @@ function usePhoneVerificationInFindId({ phoneNumber, onNext }: UsePhoneVerificat const { mutate: sendSMSToUser } = useMutation({ mutationFn: smsSend, - onSuccess: (data: SmsSendResponse) => { + onSuccess: (data: SmsSendResponse, { phone_number }) => { + markSent(phone_number); setPhoneMessage({ type: 'success', content: MESSAGES.PHONE.CODE_SENT }); runTimer(); setSmsSendCountData({ @@ -187,8 +196,10 @@ function usePhoneVerificationInFindId({ phoneNumber, onNext }: UsePhoneVerificat return { phoneMessage, - checkPhoneExists, + checkPhoneExists: withResendLock(checkPhoneExists), checkVerificationSmsVerify, + isResendLocked, + resendButtonText, findId, isDisabled, disableButton, @@ -207,7 +218,7 @@ function usePhoneVerificationInFindId({ phoneNumber, onNext }: UsePhoneVerificat checkIdMatchPhone, idMessage, setIdMessage, - checkPhoneNumber, + checkPhoneNumber: withResendLock(checkPhoneNumber), }; } diff --git a/apps/web/src/utils/hooks/auth/useResendLock.ts b/apps/web/src/utils/hooks/auth/useResendLock.ts new file mode 100644 index 00000000..12a440bd --- /dev/null +++ b/apps/web/src/utils/hooks/auth/useResendLock.ts @@ -0,0 +1,34 @@ +import { useState } from 'react'; + +const formatSeconds = (totalSeconds: number) => { + const minutes = Math.floor(totalSeconds / 60) + .toString() + .padStart(2, '0'); + const seconds = (totalSeconds % 60).toString().padStart(2, '0'); + + return `${minutes}:${seconds}`; +}; + +/** + * 인증번호를 보낸 대상(전화번호·이메일)에는 인증 타이머가 끝날 때까지 다시 보내지 못하게 한다. + * 입력값이 바뀌면 바로 다시 보낼 수 있다. 잘못 입력한 번호를 고친 사용자를 기다리게 하지 않는다. + */ +function useResendLock(target: string, isTimerRunning: boolean, secondsLeft: number) { + const [sentTarget, setSentTarget] = useState(null); + const isResendLocked = isTimerRunning && sentTarget !== null && sentTarget === target; + + const withResendLock = + (send: (...args: Args) => void) => + (...args: Args) => { + if (isResendLocked) return; + send(...args); + }; + + /** 잠긴 동안 발송 버튼에 다시 보낼 수 있을 때까지 남은 시간을 보여준다. */ + const resendButtonText = (defaultText: string) => + isResendLocked ? `재발송 ${formatSeconds(secondsLeft)}` : defaultText; + + return { isResendLocked, markSent: setSentTarget, withResendLock, resendButtonText }; +} + +export default useResendLock;