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>
112 lines
3.4 KiB
JavaScript
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} · {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>
|
|
);
|
|
}
|