diff --git a/installer/renderer.js b/installer/renderer.js
index 87ab223..c8dbaa3 100644
--- a/installer/renderer.js
+++ b/installer/renderer.js
@@ -1133,20 +1133,47 @@ function downloadFinalResourcepack() {
section.innerHTML =
'
' + tt('resourcepack.downloadHeading') + '
' +
'' + tt('resourcepack.downloading') + '
' +
+ '' +
+ '0%
' +
''
pageHost.appendChild(section)
var msg = section.querySelector('#rpMsg')
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) {
+ stopProgress()
if (res && res.ok) {
+ bar.style.width = '100%'
+ percentEl.textContent = '100%'
msg.textContent = tt('resourcepack.downloadDone')
msg.classList.add('success')
actions.innerHTML = ''
actions.querySelector('#rpNext').addEventListener('click', proceed)
} else {
+ barWrap.hidden = true
+ percentEl.hidden = true
msg.textContent = tt('resourcepack.downloadFailed', { message: (res && res.message) || 'unknown' })
msg.classList.add('error')
actions.innerHTML =
@@ -1157,6 +1184,9 @@ function downloadFinalResourcepack() {
}
actions.hidden = false
}).catch(function (err) {
+ stopProgress()
+ barWrap.hidden = true
+ percentEl.hidden = true
msg.textContent = tt('resourcepack.downloadFailed', { message: (err && err.message) ? err.message : String(err) })
msg.classList.add('error')
actions.innerHTML =
diff --git a/package.json b/package.json
index 9572b1f..f66341c 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "minecraft-music-quiz-installer",
- "version": "0.4.17",
+ "version": "0.4.18",
"description": "마인크래프트 음악퀴즈 간편설치기 + 관리 사이트",
"main": "dist/installer/main.js",
"scripts": {
diff --git a/src/installer/main.ts b/src/installer/main.ts
index 7e4ec17..f0076cc 100644
--- a/src/installer/main.ts
+++ b/src/installer/main.ts
@@ -290,8 +290,20 @@ ipcMain.handle('finalResourcepack:install', async (): Promise<{ ok: boolean; pat
const url = `${state.baseUrl}/file/resourcepacks/outputs/${encodeURIComponent(cleaned)}`
const destDir = path.join(getAppDataDir(), getMcCustomDirName(), 'resourcepacks')
const dest = path.join(destDir, cleaned)
+ await fsp.mkdir(destDir, { recursive: true })
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 }))
return { ok: true, path: dest }
} catch (error) {
diff --git a/src/installer/preload.ts b/src/installer/preload.ts
index 3de2071..adb0759 100644
--- a/src/installer/preload.ts
+++ b/src/installer/preload.ts
@@ -74,6 +74,15 @@ const api = {
const listener = (_event: unknown, line: string) => handler(line)
ipcRenderer.on('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)
}
}