- 런처 미설치 시 launcher.exe만 다운로드 (게임 전체 zip 아님) - 관리자 페이지에 런처/게임 별도 업로드 섹션 분리 - 힌트 문구 업데이트 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
7
.claude/settings.local.json
Normal file
7
.claude/settings.local.json
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
{
|
||||||
|
"permissions": {
|
||||||
|
"allow": [
|
||||||
|
"Bash(cd E:/projects/a301_launcher && \"C:/Users/98kim/sdk/go1.25.1/bin/go.exe\" build -ldflags=\"-H windowsgui -s -w\" -o launcher.exe . 2>&1)"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -33,11 +33,11 @@ export default function DownloadSection() {
|
|||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
window.removeEventListener('blur', onBlur);
|
window.removeEventListener('blur', onBlur);
|
||||||
setLaunching(false);
|
setLaunching(false);
|
||||||
if (!launched && info?.url) {
|
if (!launched && info?.launcherUrl) {
|
||||||
// 런처 미설치 → 자동 다운로드
|
// 런처 미설치 → launcher.exe 다운로드
|
||||||
const a = document.createElement('a');
|
const a = document.createElement('a');
|
||||||
a.href = info.url;
|
a.href = info.launcherUrl;
|
||||||
a.download = '';
|
a.download = 'launcher.exe';
|
||||||
a.click();
|
a.click();
|
||||||
}
|
}
|
||||||
}, 2000);
|
}, 2000);
|
||||||
@@ -58,7 +58,7 @@ export default function DownloadSection() {
|
|||||||
{launching ? '실행 중...' : '게임 시작'}
|
{launching ? '실행 중...' : '게임 시작'}
|
||||||
</button>
|
</button>
|
||||||
<p className="launch-hint">
|
<p className="launch-hint">
|
||||||
처음 설치하는 경우 자동으로 다운로드됩니다
|
런처 미설치 시 자동으로 다운로드됩니다. 설치 후 다시 클릭하세요.
|
||||||
</p>
|
</p>
|
||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
|
|||||||
@@ -187,6 +187,28 @@
|
|||||||
background: rgba(229, 115, 115, 0.08);
|
background: rgba(229, 115, 115, 0.08);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Deploy block */
|
||||||
|
.admin-deploy-block {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.admin-deploy-header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 12px;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.admin-deploy-label {
|
||||||
|
font-size: 0.8rem;
|
||||||
|
font-weight: 600;
|
||||||
|
color: rgba(255, 255, 255, 0.5);
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.06em;
|
||||||
|
}
|
||||||
|
|
||||||
/* File input */
|
/* File input */
|
||||||
.admin-input-file {
|
.admin-input-file {
|
||||||
padding: 8px 0;
|
padding: 8px 0;
|
||||||
|
|||||||
@@ -4,17 +4,12 @@ import './AdminCommon.css';
|
|||||||
|
|
||||||
const BASE = import.meta.env.VITE_API_BASE_URL || '';
|
const BASE = import.meta.env.VITE_API_BASE_URL || '';
|
||||||
|
|
||||||
export default function DownloadAdmin() {
|
function UploadForm({ title, hint, accept, endpoint, onSuccess }) {
|
||||||
const [file, setFile] = useState(null);
|
const [file, setFile] = useState(null);
|
||||||
const [info, setInfo] = useState(null);
|
|
||||||
const [uploading, setUploading] = useState(false);
|
const [uploading, setUploading] = useState(false);
|
||||||
const [progress, setProgress] = useState(0);
|
const [progress, setProgress] = useState(0);
|
||||||
const [error, setError] = useState('');
|
const [error, setError] = useState('');
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
getDownloadInfo().then(setInfo).catch(() => {});
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const handleFileChange = (e) => {
|
const handleFileChange = (e) => {
|
||||||
setFile(e.target.files[0] || null);
|
setFile(e.target.files[0] || null);
|
||||||
setError('');
|
setError('');
|
||||||
@@ -37,7 +32,7 @@ export default function DownloadAdmin() {
|
|||||||
xhr.onload = () => {
|
xhr.onload = () => {
|
||||||
setUploading(false);
|
setUploading(false);
|
||||||
if (xhr.status >= 200 && xhr.status < 300) {
|
if (xhr.status >= 200 && xhr.status < 300) {
|
||||||
setInfo(JSON.parse(xhr.responseText));
|
onSuccess(JSON.parse(xhr.responseText));
|
||||||
setFile(null);
|
setFile(null);
|
||||||
setProgress(0);
|
setProgress(0);
|
||||||
} else {
|
} else {
|
||||||
@@ -53,7 +48,7 @@ export default function DownloadAdmin() {
|
|||||||
setProgress(0);
|
setProgress(0);
|
||||||
};
|
};
|
||||||
|
|
||||||
xhr.open('POST', `${BASE}/api/download/upload?filename=${encodeURIComponent(file.name)}`);
|
xhr.open('POST', `${BASE}${endpoint}?filename=${encodeURIComponent(file.name)}`);
|
||||||
xhr.setRequestHeader('Authorization', `Bearer ${token}`);
|
xhr.setRequestHeader('Authorization', `Bearer ${token}`);
|
||||||
setUploading(true);
|
setUploading(true);
|
||||||
setError('');
|
setError('');
|
||||||
@@ -61,55 +56,92 @@ export default function DownloadAdmin() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="admin-section">
|
<form className="admin-form" onSubmit={handleUpload}>
|
||||||
<h2 className="admin-section-title">게임 배포 관리</h2>
|
<div className="admin-field">
|
||||||
|
<label className="admin-label">{title}</label>
|
||||||
|
<input
|
||||||
|
type="file"
|
||||||
|
accept={accept}
|
||||||
|
className="admin-input-file"
|
||||||
|
onChange={handleFileChange}
|
||||||
|
disabled={uploading}
|
||||||
|
/>
|
||||||
|
<span className="admin-field-hint">{hint}</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
{info && (
|
{uploading && (
|
||||||
<div className="admin-current-build">
|
<div className="admin-upload-progress">
|
||||||
<span className="admin-label">현재 배포 중</span>
|
<div className="admin-upload-bar" style={{ width: `${progress}%` }} />
|
||||||
<div className="admin-meta-row">
|
<span className="admin-upload-pct">{progress}%</span>
|
||||||
{info.version && <span className="admin-meta-item">{info.version}</span>}
|
|
||||||
{info.fileName && <span className="admin-meta-item">{info.fileName}</span>}
|
|
||||||
{info.fileSize && <span className="admin-meta-item">{info.fileSize}</span>}
|
|
||||||
{info.fileHash && (
|
|
||||||
<span className="admin-meta-item admin-meta-hash" title={info.fileHash}>
|
|
||||||
SHA256: {info.fileHash.slice(0, 12)}...
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<form className="admin-form" onSubmit={handleUpload}>
|
{error && <p className="admin-error">{error}</p>}
|
||||||
<div className="admin-field">
|
|
||||||
<label className="admin-label">새 배포 파일 (zip)</label>
|
<div className="admin-form-actions">
|
||||||
<input
|
<button className="btn-admin-primary" type="submit" disabled={uploading || !file}>
|
||||||
type="file"
|
{uploading ? `업로드 중... (${progress}%)` : '업로드'}
|
||||||
accept=".zip"
|
</button>
|
||||||
className="admin-input-file"
|
</div>
|
||||||
onChange={handleFileChange}
|
</form>
|
||||||
disabled={uploading}
|
);
|
||||||
/>
|
}
|
||||||
<span className="admin-field-hint">
|
|
||||||
A301.exe가 포함된 zip 파일을 선택하세요. 버전·파일명·크기·해시가 자동으로 추출됩니다.
|
export default function DownloadAdmin() {
|
||||||
</span>
|
const [info, setInfo] = useState(null);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
getDownloadInfo().then(setInfo).catch(() => {});
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="admin-section">
|
||||||
|
<h2 className="admin-section-title">게임 배포 관리</h2>
|
||||||
|
|
||||||
|
{/* 런처 섹션 */}
|
||||||
|
<div className="admin-deploy-block">
|
||||||
|
<div className="admin-deploy-header">
|
||||||
|
<span className="admin-deploy-label">런처</span>
|
||||||
|
{info?.launcherUrl && (
|
||||||
|
<div className="admin-meta-row">
|
||||||
|
{info.launcherSize && <span className="admin-meta-item">{info.launcherSize}</span>}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
<UploadForm
|
||||||
|
title="launcher.exe"
|
||||||
|
hint="빌드된 launcher.exe 파일을 업로드하세요."
|
||||||
|
accept=".exe"
|
||||||
|
endpoint="/api/download/upload/launcher"
|
||||||
|
onSuccess={setInfo}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
{uploading && (
|
{/* 게임 섹션 */}
|
||||||
<div className="admin-upload-progress">
|
<div className="admin-deploy-block">
|
||||||
<div className="admin-upload-bar" style={{ width: `${progress}%` }} />
|
<div className="admin-deploy-header">
|
||||||
<span className="admin-upload-pct">{progress}%</span>
|
<span className="admin-deploy-label">게임</span>
|
||||||
</div>
|
{info?.url && (
|
||||||
)}
|
<div className="admin-meta-row">
|
||||||
|
{info.version && <span className="admin-meta-item">{info.version}</span>}
|
||||||
{error && <p className="admin-error">{error}</p>}
|
{info.fileName && <span className="admin-meta-item">{info.fileName}</span>}
|
||||||
|
{info.fileSize && <span className="admin-meta-item">{info.fileSize}</span>}
|
||||||
<div className="admin-form-actions">
|
{info.fileHash && (
|
||||||
<button className="btn-admin-primary" type="submit" disabled={uploading || !file}>
|
<span className="admin-meta-item admin-meta-hash" title={info.fileHash}>
|
||||||
{uploading ? `업로드 중... (${progress}%)` : '업로드'}
|
SHA256: {info.fileHash.slice(0, 12)}...
|
||||||
</button>
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</form>
|
<UploadForm
|
||||||
|
title="게임 파일 (zip)"
|
||||||
|
hint="A301.exe가 포함된 zip 파일을 업로드하세요. 버전·크기·해시가 자동으로 추출됩니다."
|
||||||
|
accept=".zip"
|
||||||
|
endpoint="/api/download/upload/game"
|
||||||
|
onSuccess={setInfo}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user