fix: 입력 검증 일관성·UX 개선
All checks were successful
Client CI/CD / deploy (push) Successful in 35s

- 회원가입 username 검증을 서버와 동일하게 맞춤
- 비밀번호 maxLength를 bcrypt 제한(72)에 맞춤
- 공지사항 줄바꿈 CSS 처리 (pre→white-space)
- 어드민 페이지 에러 로깅 추가
- 다운로드 섹션 로딩 스켈레톤 추가

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-13 21:40:09 +09:00
parent aaf92baa9f
commit f93d81b6d9
7 changed files with 33 additions and 9 deletions

View File

@@ -58,6 +58,8 @@
font-size: 0.9rem;
color: rgba(255, 255, 255, 0.6);
line-height: 1.6;
white-space: pre-wrap;
word-break: break-word;
}
.announcement-error {

View File

@@ -62,7 +62,16 @@ export default function DownloadSection() {
}
};
if (!ready) return null;
if (!ready) {
return (
<section className="download-section">
<div className="download-content">
<h2 className="download-title">One of the plans</h2>
<p className="download-meta">불러오는 ...</p>
</div>
</section>
);
}
return (
<section className="download-section">

View File

@@ -10,7 +10,9 @@ export default function AnnouncementAdmin() {
const [error, setError] = useState('');
const load = useCallback(() => {
getAnnouncements().then(setList).catch(() => {});
getAnnouncements().then(setList).catch((err) => {
console.error('공지사항 로드 실패:', err);
});
}, []);
useEffect(() => { load(); }, [load]);

View File

@@ -143,7 +143,9 @@ export default function DownloadAdmin() {
const [info, setInfo] = useState(null);
useEffect(() => {
getDownloadInfo().then(setInfo).catch(() => {});
getDownloadInfo().then(setInfo).catch((err) => {
console.error('다운로드 정보 로드 실패:', err);
});
}, []);
return (

View File

@@ -12,7 +12,7 @@ export default function UserAdmin() {
const load = useCallback(() => {
getUsers()
.then(setUsers)
.catch(() => {})
.catch((err) => { console.error('유저 목록 로드 실패:', err); })
.finally(() => setLoading(false));
}, []);
useEffect(() => { load(); }, [load]);

View File

@@ -63,7 +63,7 @@ export default function LoginPage() {
onChange={(e) => setPassword(e.target.value)}
placeholder="비밀번호를 입력하세요"
autoComplete="current-password"
maxLength={100}
maxLength={72}
/>
</div>

View File

@@ -31,10 +31,19 @@ export default function RegisterPage() {
e.preventDefault();
setError('');
if (!username.trim()) {
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;
@@ -46,7 +55,7 @@ export default function RegisterPage() {
setLoading(true);
try {
await register(username, password);
await register(trimmed, password);
navigate('/login', { state: { registered: true } });
} catch (err) {
setError(err.message || '회원가입에 실패했습니다.');
@@ -86,7 +95,7 @@ export default function RegisterPage() {
onChange={(e) => setPassword(e.target.value)}
placeholder="6자 이상 입력하세요"
autoComplete="new-password"
maxLength={100}
maxLength={72}
/>
</div>
@@ -105,7 +114,7 @@ export default function RegisterPage() {
onChange={(e) => setConfirm(e.target.value)}
placeholder="비밀번호를 다시 입력하세요"
autoComplete="new-password"
maxLength={100}
maxLength={72}
/>
</div>