import { useState, useEffect } from 'react'; import { getDownloadInfo } from '../../api/download'; import { tryRefresh } from '../../api/client'; import './AdminCommon.css'; const BASE = import.meta.env.VITE_API_BASE_URL || ''; function sendXhr(url, token, file, { onProgress, onDone, onError }) { const xhr = new XMLHttpRequest(); xhr.upload.onprogress = (event) => { if (event.lengthComputable) { onProgress(Math.round((event.loaded / event.total) * 100)); } }; xhr.onload = () => onDone(xhr); xhr.onerror = () => onError(); xhr.open('POST', url); xhr.setRequestHeader('Authorization', `Bearer ${token}`); xhr.setRequestHeader('Content-Type', 'application/octet-stream'); xhr.send(file); } function UploadForm({ title, hint, accept, endpoint, onSuccess }) { const [file, setFile] = useState(null); const [uploading, setUploading] = useState(false); const [progress, setProgress] = useState(0); const [error, setError] = useState(''); const handleFileChange = (e) => { setFile(e.target.files[0] || null); setError(''); setProgress(0); }; const handleUpload = (e) => { e.preventDefault(); if (!file) return; const token = localStorage.getItem('token'); const url = `${BASE}${endpoint}?filename=${encodeURIComponent(file.name)}`; setUploading(true); setError(''); const handleDone = (xhr) => { // 401 시 토큰 갱신 후 재시도 if (xhr.status === 401) { tryRefresh() .then((newToken) => { sendXhr(url, newToken, file, { onProgress: (p) => setProgress(p), onDone: (retryXhr) => { setUploading(false); if (retryXhr.status === 401) { window.dispatchEvent(new Event('auth:unauthorized')); return; } parseXhrResponse(retryXhr); }, onError: handleError, }); }) .catch(() => { setUploading(false); window.dispatchEvent(new Event('auth:unauthorized')); }); return; } setUploading(false); parseXhrResponse(xhr); }; const parseXhrResponse = (xhr) => { try { const body = JSON.parse(xhr.responseText || '{}'); if (xhr.status >= 200 && xhr.status < 300) { onSuccess(body); setFile(null); setProgress(0); } else { setError(body.error || '업로드에 실패했습니다.'); setProgress(0); } } catch { if (xhr.status >= 200 && xhr.status < 300) { setError('응답을 처리할 수 없습니다.'); } else { setError('업로드에 실패했습니다.'); } setProgress(0); } }; const handleError = () => { setUploading(false); setError('네트워크 오류가 발생했습니다.'); setProgress(0); }; sendXhr(url, token, file, { onProgress: (p) => setProgress(p), onDone: handleDone, onError: handleError, }); }; return (
{hint}
{uploading && (
{progress}%
)} {error &&

{error}

}
); } export default function DownloadAdmin() { const [info, setInfo] = useState(null); useEffect(() => { getDownloadInfo().then(setInfo).catch((err) => { console.error('다운로드 정보 로드 실패:', err); }); }, []); return (

게임 배포 관리

{/* 런처 섹션 */}
런처 {info?.launcherUrl && (
{info.launcherSize && {info.launcherSize}}
)}
{/* 게임 섹션 */}
게임 {info?.url && (
{info.version && {info.version}} {info.fileName && {info.fileName}} {info.fileSize && {info.fileSize}} {info.fileHash && ( SHA256: {info.fileHash.slice(0, 12)}... )}
)}
); }