- 런처 미설치 시 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(() => {
|
||||
window.removeEventListener('blur', onBlur);
|
||||
setLaunching(false);
|
||||
if (!launched && info?.url) {
|
||||
// 런처 미설치 → 자동 다운로드
|
||||
if (!launched && info?.launcherUrl) {
|
||||
// 런처 미설치 → launcher.exe 다운로드
|
||||
const a = document.createElement('a');
|
||||
a.href = info.url;
|
||||
a.download = '';
|
||||
a.href = info.launcherUrl;
|
||||
a.download = 'launcher.exe';
|
||||
a.click();
|
||||
}
|
||||
}, 2000);
|
||||
@@ -58,7 +58,7 @@ export default function DownloadSection() {
|
||||
{launching ? '실행 중...' : '게임 시작'}
|
||||
</button>
|
||||
<p className="launch-hint">
|
||||
처음 설치하는 경우 자동으로 다운로드됩니다
|
||||
런처 미설치 시 자동으로 다운로드됩니다. 설치 후 다시 클릭하세요.
|
||||
</p>
|
||||
</>
|
||||
) : (
|
||||
|
||||
@@ -187,6 +187,28 @@
|
||||
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 */
|
||||
.admin-input-file {
|
||||
padding: 8px 0;
|
||||
|
||||
@@ -4,17 +4,12 @@ import './AdminCommon.css';
|
||||
|
||||
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 [info, setInfo] = useState(null);
|
||||
const [uploading, setUploading] = useState(false);
|
||||
const [progress, setProgress] = useState(0);
|
||||
const [error, setError] = useState('');
|
||||
|
||||
useEffect(() => {
|
||||
getDownloadInfo().then(setInfo).catch(() => {});
|
||||
}, []);
|
||||
|
||||
const handleFileChange = (e) => {
|
||||
setFile(e.target.files[0] || null);
|
||||
setError('');
|
||||
@@ -37,7 +32,7 @@ export default function DownloadAdmin() {
|
||||
xhr.onload = () => {
|
||||
setUploading(false);
|
||||
if (xhr.status >= 200 && xhr.status < 300) {
|
||||
setInfo(JSON.parse(xhr.responseText));
|
||||
onSuccess(JSON.parse(xhr.responseText));
|
||||
setFile(null);
|
||||
setProgress(0);
|
||||
} else {
|
||||
@@ -53,7 +48,7 @@ export default function DownloadAdmin() {
|
||||
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}`);
|
||||
setUploading(true);
|
||||
setError('');
|
||||
@@ -61,55 +56,92 @@ export default function DownloadAdmin() {
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="admin-section">
|
||||
<h2 className="admin-section-title">게임 배포 관리</h2>
|
||||
<form className="admin-form" onSubmit={handleUpload}>
|
||||
<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 && (
|
||||
<div className="admin-current-build">
|
||||
<span className="admin-label">현재 배포 중</span>
|
||||
<div className="admin-meta-row">
|
||||
{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>
|
||||
{uploading && (
|
||||
<div className="admin-upload-progress">
|
||||
<div className="admin-upload-bar" style={{ width: `${progress}%` }} />
|
||||
<span className="admin-upload-pct">{progress}%</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<form className="admin-form" onSubmit={handleUpload}>
|
||||
<div className="admin-field">
|
||||
<label className="admin-label">새 배포 파일 (zip)</label>
|
||||
<input
|
||||
type="file"
|
||||
accept=".zip"
|
||||
className="admin-input-file"
|
||||
onChange={handleFileChange}
|
||||
disabled={uploading}
|
||||
/>
|
||||
<span className="admin-field-hint">
|
||||
A301.exe가 포함된 zip 파일을 선택하세요. 버전·파일명·크기·해시가 자동으로 추출됩니다.
|
||||
</span>
|
||||
{error && <p className="admin-error">{error}</p>}
|
||||
|
||||
<div className="admin-form-actions">
|
||||
<button className="btn-admin-primary" type="submit" disabled={uploading || !file}>
|
||||
{uploading ? `업로드 중... (${progress}%)` : '업로드'}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
);
|
||||
}
|
||||
|
||||
export default function DownloadAdmin() {
|
||||
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>
|
||||
<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-upload-bar" style={{ width: `${progress}%` }} />
|
||||
<span className="admin-upload-pct">{progress}%</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{error && <p className="admin-error">{error}</p>}
|
||||
|
||||
<div className="admin-form-actions">
|
||||
<button className="btn-admin-primary" type="submit" disabled={uploading || !file}>
|
||||
{uploading ? `업로드 중... (${progress}%)` : '업로드'}
|
||||
</button>
|
||||
{/* 게임 섹션 */}
|
||||
<div className="admin-deploy-block">
|
||||
<div className="admin-deploy-header">
|
||||
<span className="admin-deploy-label">게임</span>
|
||||
{info?.url && (
|
||||
<div className="admin-meta-row">
|
||||
{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>
|
||||
</form>
|
||||
<UploadForm
|
||||
title="게임 파일 (zip)"
|
||||
hint="A301.exe가 포함된 zip 파일을 업로드하세요. 버전·크기·해시가 자동으로 추출됩니다."
|
||||
accept=".zip"
|
||||
endpoint="/api/download/upload/game"
|
||||
onSuccess={setInfo}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user