diff --git a/src/api/auth.js b/src/api/auth.js index be8a0df..a50c564 100644 --- a/src/api/auth.js +++ b/src/api/auth.js @@ -18,3 +18,25 @@ export async function logout() { return apiFetch('/api/auth/logout', { method: 'POST' }); } +// 토큰을 리프레시하고 새 access token을 반환 +export async function refreshToken() { + const rt = localStorage.getItem('refreshToken'); + if (!rt) throw new Error('no_refresh_token'); + + const res = await fetch( + (import.meta.env.VITE_API_BASE_URL || '') + '/api/auth/refresh', + { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ refreshToken: rt }), + } + ); + + if (!res.ok) throw new Error('refresh_failed'); + + const data = await res.json(); + localStorage.setItem('token', data.token); + localStorage.setItem('refreshToken', data.refreshToken); + return data.token; +} + diff --git a/src/components/DownloadSection.css b/src/components/DownloadSection.css index 887f654..c7713ea 100644 --- a/src/components/DownloadSection.css +++ b/src/components/DownloadSection.css @@ -70,6 +70,10 @@ margin: 16px 0 0; } +.launch-hint-active { + color: #BACDB0; +} + .download-preparing { font-size: 0.9rem; color: rgba(255, 255, 255, 0.4); diff --git a/src/components/DownloadSection.jsx b/src/components/DownloadSection.jsx index 592fdc2..b6f87cc 100644 --- a/src/components/DownloadSection.jsx +++ b/src/components/DownloadSection.jsx @@ -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() {

{info.version} · {info.fileSize}

- {info?.launcherUrl && ( )} -

- 처음이거나 게임이 실행되지 않으면 런처를 다운로드해주세요. -

+ {launched ? ( +

+ 게임이 실행되지 않나요? 런처를 다운로드한 뒤 한 번 실행해주세요. +

+ ) : ( +

+ 처음이거나 게임이 실행되지 않으면 런처를 다운로드해주세요. +

+ )} ) : ( -

런처 준비 중입니다. 잠시 후 다시 확인해주세요.

+ <> +

+ {loadError + ? '서버에 연결할 수 없습니다.' + : '런처 준비 중입니다. 잠시 후 다시 확인해주세요.'} +

+ {loadError && ( + + )} + )} diff --git a/src/pages/LoginPage.css b/src/pages/LoginPage.css index 636b394..1a00ba8 100644 --- a/src/pages/LoginPage.css +++ b/src/pages/LoginPage.css @@ -164,3 +164,13 @@ .login-back:hover { color: rgba(255, 255, 255, 0.7); } + +.password-strength { + font-size: 0.8rem; + color: rgba(186, 205, 176, 0.7); + margin: -12px 0 0; +} + +.password-strength.strength-weak { + color: #e57373; +} diff --git a/src/pages/RegisterPage.jsx b/src/pages/RegisterPage.jsx index 6cc901c..4710050 100644 --- a/src/pages/RegisterPage.jsx +++ b/src/pages/RegisterPage.jsx @@ -11,6 +11,22 @@ export default function RegisterPage() { const [loading, setLoading] = useState(false); const navigate = useNavigate(); + const getPasswordStrength = (pw) => { + if (pw.length === 0) return ''; + if (pw.length < 6) return '비밀번호는 6자 이상이어야 합니다.'; + let strength = 0; + if (/[a-z]/.test(pw)) strength++; + if (/[A-Z]/.test(pw)) strength++; + if (/[0-9]/.test(pw)) strength++; + if (/[^a-zA-Z0-9]/.test(pw)) strength++; + if (strength <= 1) return '약함'; + if (strength <= 2) return '보통'; + return '강함'; + }; + + const passwordStrength = getPasswordStrength(password); + const isPasswordWeak = password.length > 0 && password.length < 6; + const handleSubmit = async (e) => { e.preventDefault(); setError(''); @@ -68,6 +84,12 @@ export default function RegisterPage() { /> + {passwordStrength && ( +

+ 강도: {passwordStrength} +

+ )} +