Compare commits

..

4 Commits

Author SHA1 Message Date
df5947f8bb fix(installer): 최종 리소스팩 다운로드 실패 시 부분 파일 삭제
downloadStream 실패 시 .mc_custom/resourcepacks/<파일>.zip 에 0바이트/부분 다운로드
파일이 남아 마인크래프트 리소스팩 목록에 깨진 채로 노출될 수 있던 문제 수정.
finalResourcepack:install catch 에서 대상 zip 을 삭제. 0.4.19→0.4.20.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-07-27 00:39:18 +09:00
3c404ecd8c feat(installer): 최종 리소스팩 등록 시 베이스 리소스팩 다운로드 생략
최종 리소스팩(finalResourcepackPath !== ".")이 등록돼 있으면 완성본을 설치 마지막에
따로 받으므로, 클라이언트 설치 단계에서 베이스 리소스팩 다운로드를 건너뛴다
("등록한 최종 리소스팩만 다운로드"). 최종 리소스팩이 "."(없음)이면 기존대로 베이스를
받는다. 0.4.18→0.4.19.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-07-27 00:25:10 +09:00
e30d7418df feat(installer): 최종 리소스팩 다운로드 진행률(퍼센트) 표시
"예" 선택 후 최종 리소스팩 다운로드 시 퍼센트/진행바를 보여준다.
- finalResourcepack:install 을 downloadFile → downloadStream 으로 바꿔 content-length
  기반 진행률을 계산하고 finalResourcepack:progress 이벤트로 렌더러에 전송.
- preload onFinalResourcepackProgress 구독 추가, 렌더러에 진행바+% 표시(완료 100%,
  실패 시 바 숨김). 0.4.17→0.4.18.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-07-27 00:11:19 +09:00
bce87f1362 fix(installer): 팩 변경 시 finalRpHandled 리셋
완료 화면에서 뒤로 가 다른 퀴즈팩을 선택하면 finalRpHandled 가 true 로 남아,
새 팩에 최종 리소스팩이 있어도 "설치하시겠습니까?" 페이지가 스킵되던 문제 수정.
팩 선택 키가 바뀌면 finalRpHandled=false 로 초기화. 0.4.16→0.4.17.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-07-23 00:48:17 +09:00
5 changed files with 68 additions and 5 deletions

View File

@@ -136,6 +136,8 @@ function renderStep1() {
tt('step1.subtitle', { mc: pack.pack.mcVersion, platform: pack.pack.platform.type }) + '</small>' tt('step1.subtitle', { mc: pack.pack.mcVersion, platform: pack.pack.platform.type }) + '</small>'
if (state.selectedPackKey === pack.key) btn.classList.add('selected') if (state.selectedPackKey === pack.key) btn.classList.add('selected')
btn.addEventListener('click', function () { btn.addEventListener('click', function () {
// 팩이 바뀌면 최종 리소스팩 처리 상태를 초기화(새 팩의 설치 여부를 다시 묻도록).
if (state.selectedPackKey !== pack.key) state.finalRpHandled = false
state.selectedPackKey = pack.key state.selectedPackKey = pack.key
nextBtn.disabled = false nextBtn.disabled = false
renderList() renderList()
@@ -1131,20 +1133,47 @@ function downloadFinalResourcepack() {
section.innerHTML = section.innerHTML =
'<h2>' + tt('resourcepack.downloadHeading') + '</h2>' + '<h2>' + tt('resourcepack.downloadHeading') + '</h2>' +
'<div class="formMessage" id="rpMsg">' + tt('resourcepack.downloading') + '</div>' + '<div class="formMessage" id="rpMsg">' + tt('resourcepack.downloading') + '</div>' +
'<div id="rpBarWrap" style="margin-top:10px;height:16px;background:rgba(255,255,255,0.08);border:1px solid var(--border,#30363d);border-radius:8px;overflow:hidden;">' +
' <div id="rpBar" style="height:100%;width:0%;background:var(--accent,#6cf);transition:width 0.15s;"></div>' +
'</div>' +
'<div class="formMessage" id="rpPercent" style="margin-top:6px;">0%</div>' +
'<div class="actionRow" id="rpActions" hidden></div>' '<div class="actionRow" id="rpActions" hidden></div>'
pageHost.appendChild(section) pageHost.appendChild(section)
var msg = section.querySelector('#rpMsg') var msg = section.querySelector('#rpMsg')
var actions = section.querySelector('#rpActions') var actions = section.querySelector('#rpActions')
var bar = section.querySelector('#rpBar')
var barWrap = section.querySelector('#rpBarWrap')
var percentEl = section.querySelector('#rpPercent')
function proceed() { state.finalRpHandled = true; renderStep5() } // 다운로드 진행률(퍼센트) 구독. 완료/실패 시 해제.
var unsubscribe = null
if (installerApi.onFinalResourcepackProgress) {
unsubscribe = installerApi.onFinalResourcepackProgress(function (info) {
if (info && info.percent >= 0) {
bar.style.width = info.percent + '%'
percentEl.textContent = info.percent + '%'
} else if (info) {
// 전체 크기를 모르면 받은 용량(MB)만 표시.
percentEl.textContent = (info.loaded / 1048576).toFixed(1) + ' MB'
}
})
}
function stopProgress() { if (unsubscribe) { unsubscribe(); unsubscribe = null } }
function proceed() { stopProgress(); state.finalRpHandled = true; renderStep5() }
installerApi.installFinalResourcepack().then(function (res) { installerApi.installFinalResourcepack().then(function (res) {
stopProgress()
if (res && res.ok) { if (res && res.ok) {
bar.style.width = '100%'
percentEl.textContent = '100%'
msg.textContent = tt('resourcepack.downloadDone') msg.textContent = tt('resourcepack.downloadDone')
msg.classList.add('success') msg.classList.add('success')
actions.innerHTML = '<button class="primaryBtn" id="rpNext">' + tt('common.next') + '</button>' actions.innerHTML = '<button class="primaryBtn" id="rpNext">' + tt('common.next') + '</button>'
actions.querySelector('#rpNext').addEventListener('click', proceed) actions.querySelector('#rpNext').addEventListener('click', proceed)
} else { } else {
barWrap.hidden = true
percentEl.hidden = true
msg.textContent = tt('resourcepack.downloadFailed', { message: (res && res.message) || 'unknown' }) msg.textContent = tt('resourcepack.downloadFailed', { message: (res && res.message) || 'unknown' })
msg.classList.add('error') msg.classList.add('error')
actions.innerHTML = actions.innerHTML =
@@ -1155,6 +1184,9 @@ function downloadFinalResourcepack() {
} }
actions.hidden = false actions.hidden = false
}).catch(function (err) { }).catch(function (err) {
stopProgress()
barWrap.hidden = true
percentEl.hidden = true
msg.textContent = tt('resourcepack.downloadFailed', { message: (err && err.message) ? err.message : String(err) }) msg.textContent = tt('resourcepack.downloadFailed', { message: (err && err.message) ? err.message : String(err) })
msg.classList.add('error') msg.classList.add('error')
actions.innerHTML = actions.innerHTML =

View File

@@ -241,6 +241,7 @@
"modDownload": "모드 다운로드: {{file}}", "modDownload": "모드 다운로드: {{file}}",
"skipResourcepack": "resourcepackPath가 비어 있어 리소스팩 다운로드를 건너뜁니다.", "skipResourcepack": "resourcepackPath가 비어 있어 리소스팩 다운로드를 건너뜁니다.",
"resourcepackDownload": "리소스팩 다운로드: {{url}}", "resourcepackDownload": "리소스팩 다운로드: {{url}}",
"skipBaseForFinal": "최종 리소스팩이 등록돼 있어 베이스 리소스팩은 건너뜁니다(설치 마지막에 최종 리소스팩만 받음).",
"finalResourcepackDownload": "최종 리소스팩 다운로드: {{url}}", "finalResourcepackDownload": "최종 리소스팩 다운로드: {{url}}",
"finalResourcepackSaved": "최종 리소스팩 저장: {{path}}", "finalResourcepackSaved": "최종 리소스팩 저장: {{path}}",
"serverInstallPath": "서버 설치 경로: {{path}}", "serverInstallPath": "서버 설치 경로: {{path}}",

View File

@@ -1,6 +1,6 @@
{ {
"name": "minecraft-music-quiz-installer", "name": "minecraft-music-quiz-installer",
"version": "0.4.16", "version": "0.4.20",
"description": "마인크래프트 음악퀴즈 간편설치기 + 관리 사이트", "description": "마인크래프트 음악퀴즈 간편설치기 + 관리 사이트",
"main": "dist/installer/main.js", "main": "dist/installer/main.js",
"scripts": { "scripts": {

View File

@@ -285,16 +285,31 @@ ipcMain.handle('finalResourcepack:install', async (): Promise<{ ok: boolean; pat
if (!pack) return { ok: false, message: t('errors.packNotFound2') } if (!pack) return { ok: false, message: t('errors.packNotFound2') }
const finalName = pack.pack.finalResourcepackPath const finalName = pack.pack.finalResourcepackPath
if (!finalName || finalName === '.') return { ok: false, message: 'no final resourcepack' } if (!finalName || finalName === '.') return { ok: false, message: 'no final resourcepack' }
let dest = ''
try { try {
const cleaned = path.basename(finalName.replace(/^\/+/, '')) const cleaned = path.basename(finalName.replace(/^\/+/, ''))
const url = `${state.baseUrl}/file/resourcepacks/outputs/${encodeURIComponent(cleaned)}` const url = `${state.baseUrl}/file/resourcepacks/outputs/${encodeURIComponent(cleaned)}`
const destDir = path.join(getAppDataDir(), getMcCustomDirName(), 'resourcepacks') const destDir = path.join(getAppDataDir(), getMcCustomDirName(), 'resourcepacks')
const dest = path.join(destDir, cleaned) dest = path.join(destDir, cleaned)
await fsp.mkdir(destDir, { recursive: true })
sendLog(t('log.finalResourcepackDownload', { url })) sendLog(t('log.finalResourcepackDownload', { url }))
await downloadFile(url, dest) // 진행률(퍼센트)을 렌더러로 흘려보내며 스트리밍 다운로드.
let lastPct = -1
const controller = new AbortController()
await downloadStream(url, dest, controller.signal, (loaded, total) => {
const percent = total > 0 ? Math.min(100, Math.max(0, Math.floor((loaded / total) * 100))) : -1
if (percent !== lastPct) {
lastPct = percent
if (mainWindow && !mainWindow.isDestroyed()) {
mainWindow.webContents.send('finalResourcepack:progress', { loaded, total, percent })
}
}
})
sendLog(t('log.finalResourcepackSaved', { path: dest })) sendLog(t('log.finalResourcepackSaved', { path: dest }))
return { ok: true, path: dest } return { ok: true, path: dest }
} catch (error) { } catch (error) {
// 실패 시 부분/0바이트 zip 이 마인크래프트 리소스팩 목록에 깨진 채로 남지 않도록 삭제.
if (dest) await fsp.rm(dest, { force: true }).catch(() => {})
return { ok: false, message: (error as Error).message } return { ok: false, message: (error as Error).message }
} }
}) })
@@ -1340,7 +1355,13 @@ ipcMain.handle('client:install', async (_event, payload: ClientInstallPayload) =
} }
await downloadModsFolder(pack.pack, customRoot) await downloadModsFolder(pack.pack, customRoot)
await downloadResourcepackZip(pack.pack, customRoot) // 최종 리소스팩이 등록돼 있으면(finalResourcepackPath !== "."), 완성본을 설치 마지막에
// 따로 받으므로 베이스 리소스팩은 받지 않는다("최종 리소스팩만 다운로드").
if (pack.pack.finalResourcepackPath && pack.pack.finalResourcepackPath !== '.') {
sendLog(t('log.skipBaseForFinal'))
} else {
await downloadResourcepackZip(pack.pack, customRoot)
}
if (payload.skipMap) { if (payload.skipMap) {
// 참가자 모드: 이전 설치 흐름에서 설치러가 풀어둔 맵이 있다면 제거한다. // 참가자 모드: 이전 설치 흐름에서 설치러가 풀어둔 맵이 있다면 제거한다.

View File

@@ -74,6 +74,15 @@ const api = {
const listener = (_event: unknown, line: string) => handler(line) const listener = (_event: unknown, line: string) => handler(line)
ipcRenderer.on('log', listener) ipcRenderer.on('log', listener)
return () => ipcRenderer.removeListener('log', listener) return () => ipcRenderer.removeListener('log', listener)
},
// 최종 리소스팩 다운로드 진행률 구독. 반환값을 호출하면 구독 해제.
onFinalResourcepackProgress: (
handler: (info: { loaded: number; total: number; percent: number }) => void
): (() => void) => {
const listener = (_event: unknown, info: { loaded: number; total: number; percent: number }) => handler(info)
ipcRenderer.on('finalResourcepack:progress', listener)
return () => ipcRenderer.removeListener('finalResourcepack:progress', listener)
} }
} }