- HttpOnly 쿠키 refresh token (localStorage 제거) - 런치 티켓 방식 (JWT URL 노출 방지) - JWT 디코드로 role 결정 (localStorage 신뢰 제거) - apiUpload withCredentials 추가 - ErrorBoundary 컴포넌트 추가 - 404 catch-all 라우트 추가 - ARIA 접근성 (tab pattern, aria-label, aria-live) - Toast CSS 추출 + toastId useRef - UploadForm 별도 파일 분리 + apiUpload 함수 - UserAdmin fetchError 상태 + retry 버튼 - AuthRedirect 일관성 (모든 경로 → /login) - DownloadSection localStorage 중복 제거 - CI lint + test + build 검증 단계 추가 - Vitest 테스트 (client 8, Register 10, Login 8) - AuthPage.css 공유 의도 명확화 Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
133 lines
4.1 KiB
JavaScript
133 lines
4.1 KiB
JavaScript
import { useState } from 'react';
|
|
import { useNavigate, Link } from 'react-router-dom';
|
|
import { register } from '../api/auth';
|
|
import './AuthPage.css';
|
|
|
|
export default function RegisterPage() {
|
|
const [username, setUsername] = useState('');
|
|
const [password, setPassword] = useState('');
|
|
const [confirm, setConfirm] = useState('');
|
|
const [error, setError] = useState('');
|
|
const [loading, setLoading] = useState(false);
|
|
const navigate = useNavigate();
|
|
|
|
const getPasswordStrength = (pw) => {
|
|
if (pw.length === 0) return '';
|
|
if (pw.length < 6) return '비밀번호는 6자 이상이어야 합니다.';
|
|
let strength = 0;
|
|
if (/[a-z]/.test(pw)) strength++;
|
|
if (/[A-Z]/.test(pw)) strength++;
|
|
if (/[0-9]/.test(pw)) strength++;
|
|
if (/[^a-zA-Z0-9]/.test(pw)) strength++;
|
|
if (strength <= 1) return '약함';
|
|
if (strength <= 2) return '보통';
|
|
return '강함';
|
|
};
|
|
|
|
const passwordStrength = getPasswordStrength(password);
|
|
const isPasswordWeak = password.length > 0 && password.length < 6;
|
|
|
|
const handleSubmit = async (e) => {
|
|
e.preventDefault();
|
|
setError('');
|
|
|
|
const trimmed = username.trim().toLowerCase();
|
|
if (!trimmed) {
|
|
setError('아이디를 입력해주세요.');
|
|
return;
|
|
}
|
|
if (trimmed.length < 3) {
|
|
setError('아이디는 3자 이상이어야 합니다.');
|
|
return;
|
|
}
|
|
if (!/^[a-z0-9_-]+$/.test(trimmed)) {
|
|
setError('아이디는 영문 소문자, 숫자, _, -만 사용 가능합니다.');
|
|
return;
|
|
}
|
|
if (password !== confirm) {
|
|
setError('비밀번호가 일치하지 않습니다.');
|
|
return;
|
|
}
|
|
if (password.length < 6) {
|
|
setError('비밀번호는 6자 이상이어야 합니다.');
|
|
return;
|
|
}
|
|
|
|
setLoading(true);
|
|
try {
|
|
await register(trimmed, password);
|
|
navigate('/login', { state: { registered: true } });
|
|
} catch (err) {
|
|
setError(err.message || '회원가입에 실패했습니다.');
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
};
|
|
|
|
return (
|
|
<div className="login-page">
|
|
<div className="login-panel">
|
|
<div className="login-header">
|
|
<h1 className="game-title">One of the plans</h1>
|
|
<p className="game-subtitle">MULTIPLAYER</p>
|
|
</div>
|
|
|
|
<form className="login-form" onSubmit={handleSubmit}>
|
|
<div className="input-group">
|
|
<label htmlFor="username">아이디</label>
|
|
<input
|
|
id="username"
|
|
type="text"
|
|
value={username}
|
|
onChange={(e) => setUsername(e.target.value)}
|
|
placeholder="아이디를 입력하세요"
|
|
autoComplete="username"
|
|
maxLength={50}
|
|
/>
|
|
</div>
|
|
|
|
<div className="input-group">
|
|
<label htmlFor="password">비밀번호</label>
|
|
<input
|
|
id="password"
|
|
type="password"
|
|
value={password}
|
|
onChange={(e) => setPassword(e.target.value)}
|
|
placeholder="6자 이상 입력하세요"
|
|
autoComplete="new-password"
|
|
maxLength={72}
|
|
/>
|
|
</div>
|
|
|
|
{passwordStrength && (
|
|
<p className={`password-strength ${isPasswordWeak ? 'strength-weak' : ''}`}>
|
|
강도: {passwordStrength}
|
|
</p>
|
|
)}
|
|
|
|
<div className="input-group">
|
|
<label htmlFor="confirm">비밀번호 확인</label>
|
|
<input
|
|
id="confirm"
|
|
type="password"
|
|
value={confirm}
|
|
onChange={(e) => setConfirm(e.target.value)}
|
|
placeholder="비밀번호를 다시 입력하세요"
|
|
autoComplete="new-password"
|
|
maxLength={72}
|
|
/>
|
|
</div>
|
|
|
|
{error && <p className="login-error">{error}</p>}
|
|
|
|
<button type="submit" className="btn-login" disabled={loading}>
|
|
{loading ? '처리 중...' : '회원가입'}
|
|
</button>
|
|
</form>
|
|
|
|
<Link to="/login" className="login-back">이미 계정이 있으신가요? 로그인</Link>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|