2025-05-16 16:24:17 +09:00

195 lines
7.1 KiB
TypeScript

import {
Box,
Button,
Container,
Paper,
TextField,
Typography,
LinearProgress,
InputAdornment,
IconButton
} from '@mui/material'
import { useState } from 'react'
import { useNavigate } from 'react-router-dom'
import api from '../lib/api'
import Visibility from '@mui/icons-material/Visibility'
import VisibilityOff from '@mui/icons-material/VisibilityOff'
export default function Signup() {
const [email, setEmail] = useState('')
const [emailError, setEmailError] = useState('')
const [emailStatus, setEmailStatus] = useState<'available' | 'duplicate' | ''>('')
const [password, setPassword] = useState('')
const [confirmPassword, setConfirmPassword] = useState('')
const [error, setError] = useState('')
const [strengthScore, setStrengthScore] = useState(0)
const [strengthLabel, setStrengthLabel] = useState<'약함' | '보통' | '강함' | ''>('')
const [showPassword, setShowPassword] = useState(false)
const navigate = useNavigate()
const analyzePassword = (pw: string) => {
let score = 0
if (/[a-z]/.test(pw)) score++
if (/[A-Z]/.test(pw)) score++
if (/\d/.test(pw)) score++
if (/[^A-Za-z0-9]/.test(pw)) score++
if (pw.length >= 10) score++
setStrengthScore(score)
if (score <= 2) setStrengthLabel('약함')
else if (score <= 4) setStrengthLabel('보통')
else setStrengthLabel('강함')
}
const validatePassword = (pw: string) => {
const regex = /^(?=.*[A-Za-z])(?=.*\d).{6,20}$/
return regex.test(pw)
}
const validateEmailFormat = (value: string) => {
const regex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/
return regex.test(value)
}
const checkEmailDuplicate = async (value: string) => {
if (!validateEmailFormat(value)) {
setEmailError('유효한 이메일 형식을 입력해주세요.')
setEmailStatus('')
return
}
try {
const res = await api.get(`/auth/check-email?email=${value}`)
if (res.data.available === false) {
setEmailError('이미 존재하는 이메일입니다.')
setEmailStatus('duplicate')
} else {
setEmailError('')
setEmailStatus('available')
}
} catch (err: any) {
setEmailError('서버 오류로 이메일 확인 실패')
setEmailStatus('')
}
}
const handleSignup = async () => {
setError('')
if (!validateEmailFormat(email)) {
setError('유효한 이메일 형식을 입력해주세요.')
return
}
if (!validatePassword(password)) {
setError('비밀번호는 영문자+숫자를 포함한 6~20자여야 합니다.')
return
}
if (password !== confirmPassword) {
setError('비밀번호가 일치하지 않습니다.')
return
}
try {
await api.post('/users/', {
email,
password,
})
navigate('/login')
} catch (err: any) {
setError('회원가입 실패: 이미 존재하는 이메일이거나 서버 오류입니다.')
}
}
return (
<Container maxWidth="xs">
<Paper elevation={3} sx={{ p: 4, mt: 8 }}>
<Typography variant="h5" align="center" gutterBottom>
</Typography>
<Box component="form" noValidate sx={{ display: 'flex', flexDirection: 'column', gap: 2 }}>
<TextField
label="이메일"
type="email"
fullWidth
value={email}
onChange={(e) => setEmail(e.target.value)}
onKeyUp={(e) => {
if (e.key === 'Enter') handleSignup()
else checkEmailDuplicate(email)
}}
error={!!emailError}
helperText={emailError || (emailStatus === 'available' ? '사용 가능한 이메일입니다.' : ' ')}
/>
<TextField
label="비밀번호"
type={showPassword ? 'text' : 'password'}
fullWidth
value={password}
onChange={(e) => {
const pw = e.target.value
setPassword(pw)
analyzePassword(pw)
}}
onKeyUp={(e) => e.key === 'Enter' && handleSignup()}
helperText="영문자+숫자 포함 6~20자, 특수문자 허용"
InputProps={{
endAdornment: (
<InputAdornment position="end">
<IconButton onClick={() => setShowPassword(!showPassword)} edge="end">
{showPassword ? <VisibilityOff /> : <Visibility />}
</IconButton>
</InputAdornment>
),
}}
/>
{strengthLabel && (
<Box>
<Typography variant="body2" gutterBottom>
: {strengthLabel}
</Typography>
<LinearProgress
variant="determinate"
value={(strengthScore / 5) * 100}
sx={{
height: 10,
borderRadius: 5,
backgroundColor: '#555',
'& .MuiLinearProgress-bar': {
backgroundColor:
strengthLabel === '강함' ? '#4caf50' :
strengthLabel === '보통' ? '#ff9800' :
'#f44336',
},
}}
/>
</Box>
)}
<TextField
label="비밀번호 확인"
type="password"
fullWidth
value={confirmPassword}
onChange={(e) => setConfirmPassword(e.target.value)}
onKeyUp={(e) => e.key === 'Enter' && handleSignup()}
/>
<Button variant="contained" onClick={handleSignup} disabled={!!emailError}>
</Button>
{error && <Typography color="error">{error}</Typography>}
</Box>
</Paper>
</Container>
)
}