Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
140 changes: 69 additions & 71 deletions app/(routes)/(auth)/register/_components/register-form.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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',
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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('인증번호 전송에 실패했습니다. 다시 시도해주세요.');
}
Expand Down Expand Up @@ -492,13 +492,11 @@ export default function RegisterForm() {
인증번호가 일치하지 않습니다.
</p>
)}
{isWebOTPSupported &&
isCodeSent &&
formData.verificationCode.length < 3 && (
<p className='text-xs text-primary mt-1'>
SMS로 받은 인증번호를 입력해주세요.
</p>
)}
{isCodeSent && formData.verificationCode.length < 3 && (
<p className='text-xs text-primary mt-1'>
SMS로 받은 인증번호를 입력해주세요.
</p>
)}
</div>
</div>

Expand Down
Original file line number Diff line number Diff line change
@@ -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';
Expand All @@ -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('내 정보 수정하기', '전화번호 수정');
Expand Down Expand Up @@ -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);
Expand All @@ -108,8 +150,12 @@ export const EditPhoneContainer = () => {
value={phoneData.phone}
onChangeField={handleInputChange}
/>
<button className='bg-primary text-white px-4 py-2 rounded-xl whitespace-nowrap'>
인증번호 전송
<button
className='bg-primary text-white px-4 py-2 rounded-xl whitespace-nowrap disabled:opacity-50'
onClick={handleSendCode}
disabled={validationErrors.phone || sending}
>
{isCodeSent ? '재전송' : '인증번호 전송'}
</button>
</div>
</div>
Expand Down