diff --git a/src/domains/auth/components/EmailVerification.jsx b/src/domains/auth/components/EmailVerification.jsx index 8a6c5c2..6c8fd7a 100644 --- a/src/domains/auth/components/EmailVerification.jsx +++ b/src/domains/auth/components/EmailVerification.jsx @@ -1,119 +1,288 @@ -import { useState } from 'react'; -import { useNavigate } from 'react-router-dom'; +import { useState, useRef, useEffect } from 'react' import { Box, TextField, Button, Typography, Alert, - InputAdornment, - IconButton, CircularProgress, Link, -} from '@mui/material'; +} from '@mui/material' import { + ArrowBack as ArrowBackIcon, Email as EmailIcon, - Lock as LockIcon, - Visibility, - VisibilityOff, -} from '@mui/icons-material'; -import { useAuth } from '../../../contexts/AuthContext'; - -export default function LoginForm({ onSwitchToSignUp }) { - const navigate = useNavigate(); - const { login } = useAuth(); - - const [email, setEmail] = useState(''); - const [password, setPassword] = useState(''); - const [showPassword, setShowPassword] = useState(false); - const [isLoading, setIsLoading] = useState(false); - const [error, setError] = useState(''); - - const handleSubmit = async (e) => { - e.preventDefault(); - setError(''); - - if (!email || !password) { - setError('이메일과 비밀번호를 입력해주세요.'); - return; + Refresh as RefreshIcon, +} from '@mui/icons-material' +import { useAuth } from '../../../contexts/AuthContext' + +export default function EmailVerification({ email, onComplete, onBack }) { + const { confirmEmail, resendCode } = useAuth() + + const [code, setCode] = useState(['', '', '', '', '', '']) + const [isLoading, setIsLoading] = useState(false) + const [isResending, setIsResending] = useState(false) + const [error, setError] = useState('') + const [success, setSuccess] = useState('') + const [countdown, setCountdown] = useState(0) + + const inputRefs = useRef([]) + + // 카운트다운 타이머 + useEffect(() => { + if (countdown > 0) { + const timer = setTimeout(() => setCountdown(countdown - 1), 1000) + return () => clearTimeout(timer) + } + }, [countdown]) + + // 코드 입력 처리 + const handleCodeChange = (index, value) => { + // 숫자만 허용 + if (value && !/^\d+$/.test(value)) return + + const newCode = [...code] + + // 붙여넣기 처리 + if (value.length > 1) { + const pastedCode = value.slice(0, 6).split('') + pastedCode.forEach((char, i) => { + if (index + i < 6) { + newCode[index + i] = char + } + }) + setCode(newCode) + + // 마지막 입력 필드로 포커스 + const nextIndex = Math.min(index + pastedCode.length, 5) + inputRefs.current[nextIndex]?.focus() + return } - setIsLoading(true); - const result = await login(email, password); + newCode[index] = value + setCode(newCode) + setError('') - if (result.success) { - navigate('/dashboard'); - } else { - setError(result.message); + // 다음 입력 필드로 자동 이동 + if (value && index < 5) { + inputRefs.current[index + 1]?.focus() } - setIsLoading(false); - }; + } + + // 키 입력 처리 + const handleKeyDown = (index, e) => { + // 백스페이스: 이전 필드로 이동 + if (e.key === 'Backspace' && !code[index] && index > 0) { + inputRefs.current[index - 1]?.focus() + } + // Enter: 제출 + if (e.key === 'Enter') { + handleSubmit() + } + } + + // 인증 코드 확인 + const handleSubmit = async () => { + const verificationCode = code.join('') + + if (verificationCode.length !== 6) { + setError('6자리 인증 코드를 입력해주세요.') + return + } + + setError('') + setIsLoading(true) + + try { + const result = await confirmEmail(email, verificationCode) + + if (result.success) { + setSuccess(result.message) + setTimeout(() => { + onComplete() + }, 1500) + } else { + setError(result.message) + // 코드 초기화 + setCode(['', '', '', '', '', '']) + inputRefs.current[0]?.focus() + } + } catch (err) { + setError('인증 중 오류가 발생했습니다.') + } finally { + setIsLoading(false) + } + } + + // 인증 코드 재전송 + const handleResend = async () => { + if (countdown > 0) return + + setError('') + setSuccess('') + setIsResending(true) + + try { + const result = await resendCode(email) + + if (result.success) { + setSuccess(result.message) + setCountdown(60) // 60초 쿨다운 + // 코드 초기화 + setCode(['', '', '', '', '', '']) + inputRefs.current[0]?.focus() + } else { + setError(result.message) + } + } catch (err) { + setError('코드 재전송 중 오류가 발생했습니다.') + } finally { + setIsResending(false) + } + } return ( - - - 로그인 - - - AI 언어 학습 시스템에 오신 것을 환영합니다 - - - {error && {error}} - - setEmail(e.target.value)} - disabled={isLoading} - sx={{ mb: 2 }} - InputProps={{ - startAdornment: ( - - ), - }} - /> - - setPassword(e.target.value)} - disabled={isLoading} - sx={{ mb: 3 }} - InputProps={{ - startAdornment: ( - - ), - endAdornment: ( - - setShowPassword(!showPassword)}> - {showPassword ? : } - - - ), + + {/* 뒤로가기 */} + + + {/* 타이틀 */} + + + + + + 이메일 인증 + + + {email}로 발송된 +
+ 6자리 인증 코드를 입력해주세요. +
+
+ + {/* 에러/성공 메시지 */} + {error && ( + + {error} + + )} + {success && ( + + {success} + + )} + + {/* 인증 코드 입력 */} + + > + {code.map((digit, index) => ( + (inputRefs.current[index] = el)} + value={digit} + onChange={(e) => handleCodeChange(index, e.target.value)} + onKeyDown={(e) => handleKeyDown(index, e)} + disabled={isLoading} + inputProps={{ + maxLength: 6, // 붙여넣기 허용 + style: { + textAlign: 'center', + fontSize: '1.5rem', + fontWeight: 700, + padding: '12px', + }, + }} + sx={{ + width: 48, + '& .MuiOutlinedInput-root': { + borderRadius: 2, + }, + }} + /> + ))} + + {/* 확인 버튼 */} - - 계정이 없으신가요?{' '} - - 회원가입 - - + {/* 재전송 */} + + + 코드를 받지 못하셨나요?{' '} + {countdown > 0 ? ( + + {countdown}초 후 재전송 가능 + + ) : ( + + {isResending ? ( + + ) : ( + + )} + 재전송 + + )} + +
- ); + ) } \ No newline at end of file diff --git a/src/domains/auth/components/SignupForm.jsx b/src/domains/auth/components/SignupForm.jsx index ca7c07d..1ccb746 100644 --- a/src/domains/auth/components/SignupForm.jsx +++ b/src/domains/auth/components/SignupForm.jsx @@ -21,10 +21,14 @@ import { Close as CloseIcon, } from '@mui/icons-material'; import { useAuth } from '../../../contexts/AuthContext'; +import EmailVerification from './EmailVerification' export default function SignupForm({ onSwitchToLogin }) { + const { register } = useAuth() + const [step, setStep] = useState('form') + const [formData, setFormData] = useState({ email: '', password: '', @@ -36,12 +40,20 @@ export default function SignupForm({ onSwitchToLogin }) { const [success, setSuccess] = useState('') const [isLoading, setIsLoading] = useState(false) + const passwordChecks = { + length: formData.password.length >= 8, + lowercase: /[a-z]/.test(formData.password), + number: /[0-9]/.test(formData.password), + special: /[!@#$%^&*(),.?":{}|<>]/.test(formData.password), + }; + const handleChange = (e) => { const { name, value } = e.target setFormData((prev) => ({ ...prev, [name]: value })) setError('') } + const passwordStrength = Object.values(passwordChecks).filter(Boolean).length const isPasswordValid = passwordStrength >= 4 const handleSubmit = async (e) => { @@ -84,6 +96,23 @@ export default function SignupForm({ onSwitchToLogin }) { } } + // 비밀번호 강도 표시 + const getStrengthColor = () => { + if (passwordStrength <= 2) return 'error' + if (passwordStrength <= 3) return 'warning' + return 'success' + } + + // 이메일 인증 화면 + if (step === 'verify') { + return ( + setStep('form')} + /> + ) + } return ( - {/* TODO : 비밀번호 강도 표시 추가 */} + {formData.password && ( + + {/* 세그먼트 바 + 강도 텍스트 */} + + {/* 20칸 세그먼트 바 */} + + {[...Array(20)].map((_, index) => ( + + ))} + + {/* 강도 텍스트 */} + + {passwordStrength <= 1 ? '약함' + : passwordStrength <= 2 ? '보통' + : passwordStrength <= 3 ? '좋음' + : '강함'} + + + {/* 체크 항목 */} + + + + + + + + )} + {/* 비밀번호 확인 */} @@ -264,3 +343,25 @@ export default function SignupForm({ onSwitchToLogin }) { ) } + +// 비밀번호 체크 아이템 컴포넌트 +function PasswordCheck({ checked, label }) { + return ( + + {checked ? ( + + ) : ( + + )} + {label} + + ) +} \ No newline at end of file