Files
a301_client/src/components/DownloadSection.jsx
tolelom aaf92baa9f
All checks were successful
Client CI/CD / deploy (push) Successful in 25s
fix: 입력 검증·보안 헤더·접근성·UX 개선
- 로그인/회원가입 입력 길이 제한 (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>
2026-03-13 17:48:24 +09:00

112 lines
3.4 KiB
JavaScript

import { useState, useEffect } from 'react';
import { useNavigate } from 'react-router-dom';
import { useAuth } from '../context/useAuth';
import { getDownloadInfo } from '../api/download';
import { refreshToken } from '../api/auth';
import './DownloadSection.css';
export default function DownloadSection() {
const [info, setInfo] = useState(null);
const [ready, setReady] = useState(false);
const [loadError, setLoadError] = useState(false);
const [launched, setLaunched] = useState(false);
const [launching, setLaunching] = useState(false);
const { user } = useAuth();
const navigate = useNavigate();
const loadInfo = () => {
setReady(false);
setLoadError(false);
getDownloadInfo()
.then((data) => { setInfo(data); setReady(true); })
.catch(() => { setLoadError(true); setReady(true); });
};
useEffect(() => { loadInfo(); }, []);
const handlePlay = async () => {
if (!user) {
navigate('/login');
return;
}
// 토큰이 없으면 (다른 탭에서 로그아웃 등) 로그인 유도
let token = localStorage.getItem('token');
if (!token) {
navigate('/login');
return;
}
setLaunching(true);
// 토큰 만료 대비: 런처에 전달하기 전에 리프레시 시도
try {
token = await refreshToken();
} catch {
// 리프레시 실패해도 기존 토큰으로 시도 (아직 유효할 수 있음)
}
window.location.href = 'a301://launch?token=' + encodeURIComponent(token);
// 런처가 실행되지 않았을 수 있으므로 안내 표시
setLaunched(true);
setLaunching(false);
};
const handleDownloadLauncher = () => {
if (info?.launcherUrl) {
const a = document.createElement('a');
a.href = info.launcherUrl;
a.download = 'launcher.exe';
a.click();
}
};
if (!ready) return null;
return (
<section className="download-section">
<div className="download-content">
<h2 className="download-title">One of the plans</h2>
{info ? (
<>
<p className="download-meta">
{info.version} &middot; {info.fileSize}
</p>
<button onClick={handlePlay} className="btn-play" disabled={launching}>
{launching ? '준비 중...' : '게임 시작'}
</button>
{info.launcherUrl && (
<button onClick={handleDownloadLauncher} className="btn-launcher-download">
런처 다운로드
</button>
)}
{launched ? (
<p className="launch-hint launch-hint-active">
게임이 실행되지 않나요? 런처를 다운로드한 실행해주세요.
</p>
) : (
<p className="launch-hint">
처음이거나 게임이 실행되지 않으면 런처를 다운로드해주세요.
</p>
)}
</>
) : (
<>
<p className="download-preparing">
{loadError
? '서버에 연결할 수 없습니다.'
: '런처 준비 중입니다. 잠시 후 다시 확인해주세요.'}
</p>
{loadError && (
<button onClick={loadInfo} className="btn-launcher-download">
다시 시도
</button>
)}
</>
)}
</div>
</section>
);
}