Files
a301_client/src/pages/RegisterPage.jsx
tolelom 96f5381d1c
Some checks failed
Client CI/CD / test (push) Failing after 15m27s
Client CI/CD / deploy (push) Has been cancelled
feat: 코드 리뷰 기반 전면 개선 — 보안, 접근성, 테스트, UX
- 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>
2026-03-15 18:07:32 +09:00

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>
);
}