From 4104b02da45f9aa9380a3e167138e77cb34a48e5 Mon Sep 17 00:00:00 2001
From: VarGun
Date: Fri, 27 Jun 2025 16:24:43 +0900
Subject: [PATCH] =?UTF-8?q?feat:=20=EC=A0=84=ED=99=94=EB=B2=88=ED=98=B8=20?=
=?UTF-8?q?=EC=88=98=EC=A0=95=20sms=20=EC=A0=81=EC=9A=A9=20(#193)?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
.../register/_components/register-form.tsx | 140 +++++++++---------
.../_components/edit-phone-container.tsx | 58 +++++++-
2 files changed, 121 insertions(+), 77 deletions(-)
diff --git a/app/(routes)/(auth)/register/_components/register-form.tsx b/app/(routes)/(auth)/register/_components/register-form.tsx
index 41faeb2..d6d1361 100644
--- a/app/(routes)/(auth)/register/_components/register-form.tsx
+++ b/app/(routes)/(auth)/register/_components/register-form.tsx
@@ -12,15 +12,15 @@ import { Label } from '@/components/ui/label';
import { formatPhoneNumber, formatTelNo, validateField } from '@/lib/utils';
// WebOTP API 타입 정의
-interface OTPCredential extends Credential {
- code: string;
-}
-
-interface OTPCredentialRequestOptions extends CredentialRequestOptions {
- otp: {
- transport: string[];
- };
-}
+// interface OTPCredential extends Credential {
+// code: string;
+// }
+//
+// interface OTPCredentialRequestOptions extends CredentialRequestOptions {
+// otp: {
+// transport: string[];
+// };
+// }
export interface FormData {
name: string;
@@ -97,42 +97,42 @@ export default function RegisterForm() {
typeof window !== 'undefined' && 'OTPCredential' in window;
// WebOTP 이벤트 리스너 설정
- useEffect(() => {
- if (!isWebOTPSupported || currentStep !== 2) return; // phone step이 아닐 때는 리스너 제거
-
- const abortController = new AbortController();
-
- const handleWebOTP = async () => {
- try {
- const credential = (await navigator.credentials.get({
- otp: { transport: ['sms'] },
- signal: abortController.signal,
- } as OTPCredentialRequestOptions)) as OTPCredential;
-
- if (credential && credential.code) {
- // WebOTP로 받은 코드를 인증번호 필드에 자동 입력
- handleInputChange('verificationCode', credential.code);
- }
- } catch (error) {
- // 사용자가 취소하거나 지원하지 않는 경우 무시
- console.log('WebOTP not available or cancelled:', error);
- }
- };
-
- // 페이지가 포커스될 때 WebOTP 요청
- const handleFocus = () => {
- if (isCodeSent) {
- handleWebOTP();
- }
- };
-
- window.addEventListener('focus', handleFocus);
-
- return () => {
- abortController.abort();
- window.removeEventListener('focus', handleFocus);
- };
- }, [isWebOTPSupported, currentStep, isCodeSent]);
+ // useEffect(() => {
+ // if (!isWebOTPSupported || currentStep !== 2) return; // phone step이 아닐 때는 리스너 제거
+ //
+ // const abortController = new AbortController();
+ //
+ // const handleWebOTP = async () => {
+ // try {
+ // const credential = (await navigator.credentials.get({
+ // otp: { transport: ['sms'] },
+ // signal: abortController.signal,
+ // } as OTPCredentialRequestOptions)) as OTPCredential;
+ //
+ // if (credential && credential.code) {
+ // // WebOTP로 받은 코드를 인증번호 필드에 자동 입력
+ // handleInputChange('verificationCode', credential.code);
+ // }
+ // } catch (error) {
+ // // 사용자가 취소하거나 지원하지 않는 경우 무시
+ // console.log('WebOTP not available or cancelled:', error);
+ // }
+ // };
+ //
+ // // 페이지가 포커스될 때 WebOTP 요청
+ // const handleFocus = () => {
+ // if (isCodeSent) {
+ // handleWebOTP();
+ // }
+ // };
+ //
+ // window.addEventListener('focus', handleFocus);
+ //
+ // return () => {
+ // abortController.abort();
+ // window.removeEventListener('focus', handleFocus);
+ // };
+ // }, [isWebOTPSupported, currentStep, isCodeSent]);
const steps: (keyof FormData)[] = [
'name',
@@ -195,21 +195,21 @@ export default function RegisterForm() {
case 'rrn':
return validateField('rrn', formData.rrn, formData);
- // case 'phone':
- // return (
- // validateField('phone', formData.phone, formData) &&
- // validateField(
- // 'verificationCode',
- // formData.verificationCode,
- // formData
- // ) &&
- // isCodeSent &&
- // formData.verificationCode === sentCode
- // );
+ case 'phone':
+ return (
+ validateField('phone', formData.phone, formData) &&
+ validateField(
+ 'verificationCode',
+ formData.verificationCode,
+ formData
+ ) &&
+ isCodeSent &&
+ formData.verificationCode === sentCode
+ );
// 문자 인증 패스
- case 'phone':
- return true;
+ // case 'phone':
+ // return true;
case 'address':
return validateField('address', formData.address, formData);
@@ -317,12 +317,12 @@ export default function RegisterForm() {
setIsCodeSent(true);
toast.success('인증번호가 전송되었습니다.');
// WebOTP 지원 시 자동으로 인증번호 입력 요청
- if (isWebOTPSupported) {
- setTimeout(() => {
- // 페이지 포커스 시 WebOTP 요청
- window.focus();
- }, 1000);
- }
+ // if (isWebOTPSupported) {
+ // setTimeout(() => {
+ // // 페이지 포커스 시 WebOTP 요청
+ // window.focus();
+ // }, 1000);
+ // }
} else {
alert('인증번호 전송에 실패했습니다. 다시 시도해주세요.');
}
@@ -492,13 +492,11 @@ export default function RegisterForm() {
인증번호가 일치하지 않습니다.
)}
- {isWebOTPSupported &&
- isCodeSent &&
- formData.verificationCode.length < 3 && (
-
- SMS로 받은 인증번호를 입력해주세요.
-
- )}
+ {isCodeSent && formData.verificationCode.length < 3 && (
+
+ SMS로 받은 인증번호를 입력해주세요.
+
+ )}
diff --git a/app/(routes)/mypage/edit-profile/phone/_components/edit-phone-container.tsx b/app/(routes)/mypage/edit-profile/phone/_components/edit-phone-container.tsx
index 184d9c9..32f11ad 100644
--- a/app/(routes)/mypage/edit-profile/phone/_components/edit-phone-container.tsx
+++ b/app/(routes)/mypage/edit-profile/phone/_components/edit-phone-container.tsx
@@ -1,6 +1,7 @@
'use client';
import { useEffect, useState } from 'react';
+import toast from 'react-hot-toast';
import { useRouter } from 'next/navigation';
import { FormData } from '@/app/(routes)/(auth)/register/_components/register-form';
import { useHeader } from '@/context/header-context';
@@ -25,6 +26,9 @@ export const EditPhoneContainer = () => {
const router = useRouter();
const [loading, setLoading] = useState(false);
+ const [sending, setSending] = useState(false); // 인증번호 전송 중 여부
+ const [isCodeSent, setIsCodeSent] = useState(false);
+ const [sentCode, setSentCode] = useState(''); // 전송된 코드
useEffect(() => {
setHeader('내 정보 수정하기', '전화번호 수정');
@@ -74,14 +78,52 @@ export const EditPhoneContainer = () => {
const raw = value.replace(/\D/g, '').slice(0, 3);
setPhoneData((prev) => ({ ...prev, verificationCode: raw }));
- setValidationErrors((prev) => ({
- ...prev,
- verificationCode: !validateField('verificationCode', raw, phoneData),
- }));
+ if (raw.length === 3 && raw === sentCode) {
+ setValidationErrors((prev) => ({ ...prev, verificationCode: false }));
+ } else {
+ setValidationErrors((prev) => ({
+ ...prev,
+ verificationCode: !validateField('verificationCode', raw, phoneData),
+ }));
+ }
return;
}
};
+ // 인증번호 전송
+ const handleSendCode = async () => {
+ if (validationErrors.phone || !phoneData.phone) {
+ toast.error('올바른 전화번호를 입력해주세요.');
+ return;
+ }
+
+ setSending(true);
+ try {
+ // 임시 3자리 코드 생성
+ const code = Math.floor(100 + Math.random() * 900).toString();
+
+ const res = await fetch('/api/auth/sms', {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify({ phone: phoneData.phone, code }),
+ });
+ const data = await res.json();
+
+ if (data.success) {
+ setIsCodeSent(true);
+ setSentCode(code);
+ toast.success('인증번호가 전송되었습니다.');
+ } else {
+ toast.error('인증번호 전송에 실패했습니다.');
+ }
+ } catch (e) {
+ console.error(e);
+ toast.error('인증번호 전송 중 오류가 발생했습니다.');
+ } finally {
+ setSending(false);
+ }
+ };
+
const submitData = async () => {
const data = { phone: phoneData.phone };
setLoading(true);
@@ -108,8 +150,12 @@ export const EditPhoneContainer = () => {
value={phoneData.phone}
onChangeField={handleInputChange}
/>
-