feat: 최종 리소스팩(선택 설치) + 약관 표시대상 추가

- pack.finalResourcepackPath 추가(/file/resourcepacks/outputs/, "."=없음, 기본 ".").
  편집기에 "최종 리소스팩 (.zip)" 입력 필드 추가.
- 약관 표시대상에 showInFinalResourcepack("최종 리소스팩에 표시") 3번째 토글 추가
  (스키마/시드/마이그레이션/편집기 체크박스·배지·저장 라우트, index.json 노출).
- 간편설치기 완료 단계: 기존 "직접 적용" 경고 제거 → 최종 리소스팩이 "."이 아니면
  "리소스팩을 설치하시겠습니까?" 예/아니요. 아니요→완료, 예→최종 리소스팩 약관
  (showInFinalResourcepack) 동의→/file/resourcepacks/outputs/ 에서 다운로드→완료.
  "." 이면 이 페이지 자체를 건너뜀.
- 0.4.15→0.4.16.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
2026-07-23 00:33:07 +09:00
parent f53e54493d
commit e5dcc47548
13 changed files with 247 additions and 25 deletions

View File

@@ -235,7 +235,10 @@ function showAgreementError(message) {
// 약관을 한 건씩 페이지로 보여준다. 각 약관마다 "해당 약관에 동의합니다" 체크가 있고,
// 끝까지 스크롤해 읽어야 체크할 수 있으며, [다음] 을 누르면 다음 약관 페이지가 화면
// 맨 위에서부터 나온다. 마지막 약관까지 동의하면 다음 단계(step2)로 진행한다.
function renderAgreementWithKinds(KINDS) {
function renderAgreementWithKinds(KINDS, opts) {
opts = opts || {}
var onComplete = opts.onComplete || renderStep2 // 마지막 약관 동의 후
var onBack = opts.onBack || renderStep1 // 첫 약관에서 뒤로
var idx = 0
var accepted = {} // kind -> true (뒤로 갔다 와도 동의 상태 유지)
var cache = {} // kind -> 렌더된 HTML
@@ -293,11 +296,11 @@ function renderAgreementWithKinds(KINDS) {
msg.classList.add('error')
return
}
if (isLast) renderStep2()
if (isLast) onComplete()
else { idx++; renderCurrent() }
})
section.querySelector('#back').addEventListener('click', function () {
if (idx === 0) renderStep1()
if (idx === 0) onBack()
else { idx--; renderCurrent() }
})
@@ -1056,9 +1059,124 @@ function renderSubStep42(host, back, done) {
})()
}
// 최종 리소스팩 설치 여부 질문 (예/아니요).
function renderResourcepackPrompt() {
setActiveStep(5)
clearPage()
var section = document.createElement('section')
section.className = 'page'
section.innerHTML =
'<h2>' + tt('resourcepack.promptHeading') + '</h2>' +
'<p class="formMessage">' + escapeHtml(tt('resourcepack.promptBody')) + '</p>' +
'<div class="actionRow">' +
' <button class="secondaryBtn" id="rpNo">' + tt('resourcepack.no') + '</button>' +
' <button class="primaryBtn" id="rpYes">' + tt('resourcepack.yes') + '</button>' +
'</div>'
pageHost.appendChild(section)
section.querySelector('#rpNo').addEventListener('click', function () {
state.finalRpHandled = true
renderStep5()
})
section.querySelector('#rpYes').addEventListener('click', renderResourcepackTerms)
}
// "예" → 최종 리소스팩 다운로드 약관(showInFinalResourcepack) 동의 → 다운로드.
function renderResourcepackTerms() {
setActiveStep(5)
clearPage()
var loading = document.createElement('section')
loading.className = 'page'
loading.innerHTML = '<h2>' + tt('agreement.heading') + '</h2><p class="formMessage">' + tt('agreement.loading') + '</p>'
pageHost.appendChild(loading)
installerApi.getFinalTermsList().then(function (res) {
if (!res || !res.ok) {
showResourcepackTermsError((res && res.message) || 'unknown')
return
}
var terms = (res.terms || []).map(function (t) { return { id: t.kind, tab: t.label } })
if (terms.length === 0) {
// 표시할 약관이 없으면 약관 단계를 건너뛰고 바로 다운로드.
downloadFinalResourcepack()
return
}
clearPage()
renderAgreementWithKinds(terms, { onComplete: downloadFinalResourcepack, onBack: renderResourcepackPrompt })
}).catch(function (err) {
showResourcepackTermsError(err && err.message ? err.message : 'unknown')
})
}
function showResourcepackTermsError(message) {
clearPage()
var section = document.createElement('section')
section.className = 'page'
section.innerHTML =
'<h2>' + tt('agreement.heading') + '</h2>' +
'<p class="formMessage error">' + escapeHtml(tt('agreement.listLoadFailed', { message: message })) + '</p>' +
'<div class="actionRow">' +
' <button class="secondaryBtn" id="back">' + tt('common.back') + '</button>' +
' <button class="primaryBtn" id="retry">' + tt('agreement.retry') + '</button>' +
'</div>'
pageHost.appendChild(section)
section.querySelector('#back').addEventListener('click', renderResourcepackPrompt)
section.querySelector('#retry').addEventListener('click', renderResourcepackTerms)
}
// 약관 동의 후 최종 리소스팩 실제 다운로드 → 완료되면 완료 화면으로.
function downloadFinalResourcepack() {
setActiveStep(5)
clearPage()
var section = document.createElement('section')
section.className = 'page'
section.innerHTML =
'<h2>' + tt('resourcepack.downloadHeading') + '</h2>' +
'<div class="formMessage" id="rpMsg">' + tt('resourcepack.downloading') + '</div>' +
'<div class="actionRow" id="rpActions" hidden></div>'
pageHost.appendChild(section)
var msg = section.querySelector('#rpMsg')
var actions = section.querySelector('#rpActions')
function proceed() { state.finalRpHandled = true; renderStep5() }
installerApi.installFinalResourcepack().then(function (res) {
if (res && res.ok) {
msg.textContent = tt('resourcepack.downloadDone')
msg.classList.add('success')
actions.innerHTML = '<button class="primaryBtn" id="rpNext">' + tt('common.next') + '</button>'
actions.querySelector('#rpNext').addEventListener('click', proceed)
} else {
msg.textContent = tt('resourcepack.downloadFailed', { message: (res && res.message) || 'unknown' })
msg.classList.add('error')
actions.innerHTML =
'<button class="secondaryBtn" id="rpRetry">' + tt('agreement.retry') + '</button>' +
'<button class="primaryBtn" id="rpSkip">' + tt('common.next') + '</button>'
actions.querySelector('#rpRetry').addEventListener('click', downloadFinalResourcepack)
actions.querySelector('#rpSkip').addEventListener('click', proceed)
}
actions.hidden = false
}).catch(function (err) {
msg.textContent = tt('resourcepack.downloadFailed', { message: (err && err.message) ? err.message : String(err) })
msg.classList.add('error')
actions.innerHTML =
'<button class="secondaryBtn" id="rpRetry">' + tt('agreement.retry') + '</button>' +
'<button class="primaryBtn" id="rpSkip">' + tt('common.next') + '</button>'
actions.querySelector('#rpRetry').addEventListener('click', downloadFinalResourcepack)
actions.querySelector('#rpSkip').addEventListener('click', proceed)
actions.hidden = false
})
}
function renderStep5() {
setActiveStep(5)
clearPage()
// 최종 리소스팩(finalResourcepackPath)이 "." 이 아니고 아직 처리하지 않았으면,
// 완료 화면 전에 "리소스팩을 설치하시겠습니까?" 를 먼저 묻는다.
var selPack = state.packs.find(function (p) { return p.key === state.selectedPackKey })
var finalRp = (selPack && selPack.pack && selPack.pack.finalResourcepackPath) ? selPack.pack.finalResourcepackPath : '.'
if (finalRp !== '.' && !state.finalRpHandled) {
renderResourcepackPrompt()
return
}
var section = document.createElement('section')
section.className = 'page'
// 서버 마무리 액션 (바로가기/서버 실행) 은 step3 를 거친 호스트 만 노출한다.
@@ -1067,9 +1185,6 @@ function renderStep5() {
section.innerHTML =
'<h2>' + tt('step5.heading') + '</h2>' +
'<p>' + tt('step5.summary') + '</p>' +
'<div style="margin:12px 0;padding:14px 16px;border:1px solid var(--warning, #d9a441);border-radius:10px;background:rgba(217,164,65,0.12);font-size:14px;font-weight:600;line-height:1.55;">' +
'⚠ ' + escapeHtml(tt('step5.resourcepackWarning')) +
'</div>' +
(showServerActions ? '<div class="subStep">' +
'<h3>' + tt('step5.serverHeading') + '</h3>' +
'<button class="secondaryBtn" id="openFolder">' + tt('step5.openServerFolder') + '</button>' +