fix: 게임 시작 흐름 안정화 및 UX 개선
All checks were successful
Client CI/CD / deploy (push) Successful in 12s

- 게임 시작 전 토큰 리프레시 (만료 토큰 전달 방지)
- 토큰 null 가드 (다른 탭 로그아웃 시 로그인 유도)
- 토큰 URL 인코딩 (encodeURIComponent)
- 런처 미설치 시 힌트 강조 표시
- 게임 시작 버튼 로딩 상태 + 더블 클릭 방지
- 다운로드 정보 실패 시 재시도 버튼 추가
- 비밀번호 강도 실시간 피드백 (약함/보통/강함)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-06 11:11:10 +09:00
parent 6fb7e2cbc5
commit 90e9922bde
5 changed files with 114 additions and 14 deletions

View File

@@ -1,31 +1,56 @@
import { useState, useEffect } from 'react';
import { useState, useEffect, useCallback } 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();
useEffect(() => {
const loadInfo = useCallback(() => {
setReady(false);
setLoadError(false);
getDownloadInfo()
.then((data) => { setInfo(data); setReady(true); })
.catch(() => setReady(true));
.catch(() => { setLoadError(true); setReady(true); });
}, []);
const handlePlay = () => {
useEffect(() => { loadInfo(); }, [loadInfo]);
const handlePlay = async () => {
if (!user) {
navigate('/login');
return;
}
// user.token은 로그인 시점 값으로 만료됐을 수 있으므로
// tryRefresh가 갱신한 최신 토큰을 localStorage에서 직접 읽음
const token = localStorage.getItem('token');
window.location.href = 'a301://launch?token=' + token;
// 토큰이 없으면 (다른 탭에서 로그아웃 등) 로그인 유도
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 = () => {
@@ -48,20 +73,37 @@ export default function DownloadSection() {
<p className="download-meta">
{info.version} &middot; {info.fileSize}
</p>
<button onClick={handlePlay} className="btn-play">
게임 시작
<button onClick={handlePlay} className="btn-play" disabled={launching}>
{launching ? '준비 중...' : '게임 시작'}
</button>
{info?.launcherUrl && (
<button onClick={handleDownloadLauncher} className="btn-launcher-download">
런처 다운로드
</button>
)}
<p className="launch-hint">
처음이거나 게임이 실행되지 않으면 런처를 다운로드해주세요.
</p>
{launched ? (
<p className="launch-hint launch-hint-active">
게임이 실행되지 않나요? 런처를 다운로드한 실행해주세요.
</p>
) : (
<p className="launch-hint">
처음이거나 게임이 실행되지 않으면 런처를 다운로드해주세요.
</p>
)}
</>
) : (
<p className="download-preparing">런처 준비 중입니다. 잠시 다시 확인해주세요.</p>
<>
<p className="download-preparing">
{loadError
? '서버에 연결할 수 없습니다.'
: '런처 준비 중입니다. 잠시 후 다시 확인해주세요.'}
</p>
{loadError && (
<button onClick={loadInfo} className="btn-launcher-download">
다시 시도
</button>
)}
</>
)}
</div>
</section>