fix: 입력 검증·보안 헤더·접근성·UX 개선
All checks were successful
Client CI/CD / deploy (push) Successful in 25s

- 로그인/회원가입 입력 길이 제한 (username 50자, password 100자)
- 공지사항 관리 입력 길이 제한 (제목 200자, 내용 10000자)
- AnnouncementBoard aria-expanded 접근성 속성 추가
- DownloadSection useEffect 중복 API 호출 제거
- nginx 보안 헤더 (X-Content-Type-Options, X-Frame-Options)
- nginx /assets/ 장기 캐싱 (immutable, 1년)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-13 17:48:24 +09:00
parent 97453b1d81
commit aaf92baa9f
6 changed files with 18 additions and 5 deletions

View File

@@ -3,12 +3,21 @@ server {
root /usr/share/nginx/html;
index index.html;
# 보안 헤더
add_header X-Content-Type-Options "nosniff" always;
add_header X-Frame-Options "DENY" always;
# index.html은 캐싱 금지 (배포 후 즉시 반영)
location = /index.html {
try_files $uri =404;
add_header Cache-Control "no-store, no-cache, must-revalidate";
}
# Vite 해시 에셋 장기 캐싱
location /assets/ {
add_header Cache-Control "public, max-age=31536000, immutable";
}
# SPA fallback (react-router 사용 시 필요)
location / {
try_files $uri $uri/ /index.html;

View File

@@ -29,6 +29,7 @@ export default function AnnouncementBoard() {
<button
className="announcement-row"
onClick={() => setExpanded(expanded === item.id ? null : item.id)}
aria-expanded={expanded === item.id}
>
<span className="announcement-title">{item.title}</span>
<span className="announcement-date">{item.createdAt?.slice(0, 10)}</span>

View File

@@ -22,11 +22,7 @@ export default function DownloadSection() {
.catch(() => { setLoadError(true); setReady(true); });
};
useEffect(() => {
getDownloadInfo()
.then((data) => { setInfo(data); setReady(true); })
.catch(() => { setLoadError(true); setReady(true); });
}, []);
useEffect(() => { loadInfo(); }, []);
const handlePlay = async () => {
if (!user) {

View File

@@ -70,6 +70,7 @@ export default function AnnouncementAdmin() {
placeholder="제목"
value={form.title}
onChange={(e) => setForm({ ...form, title: e.target.value })}
maxLength={200}
/>
<textarea
className="admin-textarea"
@@ -77,6 +78,7 @@ export default function AnnouncementAdmin() {
rows={4}
value={form.content}
onChange={(e) => setForm({ ...form, content: e.target.value })}
maxLength={10000}
/>
{error && <p className="admin-error">{error}</p>}
<div className="admin-form-actions">

View File

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

View File

@@ -73,6 +73,7 @@ export default function RegisterPage() {
onChange={(e) => setUsername(e.target.value)}
placeholder="아이디를 입력하세요"
autoComplete="username"
maxLength={50}
/>
</div>
@@ -85,6 +86,7 @@ export default function RegisterPage() {
onChange={(e) => setPassword(e.target.value)}
placeholder="6자 이상 입력하세요"
autoComplete="new-password"
maxLength={100}
/>
</div>
@@ -103,6 +105,7 @@ export default function RegisterPage() {
onChange={(e) => setConfirm(e.target.value)}
placeholder="비밀번호를 다시 입력하세요"
autoComplete="new-password"
maxLength={100}
/>
</div>