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 ? : }
-
-
- ),
+
+ {/* 뒤로가기 */}
+ }
+ onClick={onBack}
+ sx={{ alignSelf: 'flex-start', ml: -1 }}
+ >
+ 돌아가기
+
+
+ {/* 타이틀 */}
+
+
+
+
+
+ 이메일 인증
+
+
+ {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