195 lines
7.1 KiB
TypeScript
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>
|
|
)
|
|
}
|