Minecraft Launcher 실행 핸들러가 옛 Program Files 경로 두 곳만 보고 있어서 Microsoft Store/UWP/Xbox 앱 설치 등 최근 설치 형태에서 거의 못 찾았다.
- 1순위로 shell.openExternal('minecraft://') 사용. OS에 등록된 프로토콜 핸들러가 설치 형태(UWP/Win32/Xbox)에 무관하게 처리.
- 폴백 경로 후보 확장: Program Files / Program Files (x86) 양쪽의 Minecraft, Minecraft Launcher, XboxGames 경로, LOCALAPPDATA\Programs\minecraft-launcher까지 검사.
- 못 찾았을 때 메시지에 설치처(Microsoft Store/minecraft.net) 안내 추가.
5단계 완료 버튼: 모든 단계가 끝난 뒤이므로 마무리 액션(바로가기/서버 실행/런처 실행)을 처리한 다음 app.quit으로 설치기를 자동 종료한다. 'app:quit' IPC 핸들러와 preload 노출(quitApp) 추가.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
638 lines
26 KiB
JavaScript
638 lines
26 KiB
JavaScript
'use strict'
|
||
|
||
const installerApi = window.installer
|
||
|
||
const state = {
|
||
packs: [],
|
||
selectedPackKey: null,
|
||
mode: null, // 'single' | 'multi'
|
||
serverInstall: {
|
||
path: '',
|
||
jdk: '',
|
||
eulaAccepted: false,
|
||
ram: null,
|
||
portStatus: null
|
||
},
|
||
client: {
|
||
installPlatform: true
|
||
},
|
||
finishToggles: {
|
||
desktopShortcut: true,
|
||
startServer: true,
|
||
startLauncher: true
|
||
},
|
||
stepDone: { 1: false, 2: false, 3: false, 4: false }
|
||
}
|
||
|
||
const pageHost = document.getElementById('pageHost')
|
||
const stepIndicator = document.getElementById('stepIndicator')
|
||
const logViewer = document.getElementById('logViewer')
|
||
const logBody = document.getElementById('logBody')
|
||
const logToggle = document.getElementById('logToggle')
|
||
|
||
logToggle.addEventListener('click', function () {
|
||
logViewer.classList.toggle('collapsed')
|
||
if (logViewer.classList.contains('collapsed')) {
|
||
logViewer.style.height = '36px'
|
||
logToggle.textContent = '펼치기'
|
||
} else {
|
||
logViewer.style.height = ''
|
||
logToggle.textContent = '접기'
|
||
}
|
||
})
|
||
|
||
installerApi.onLog(function (line) {
|
||
logViewer.hidden = false
|
||
logBody.textContent += line + '\n'
|
||
logBody.scrollTop = logBody.scrollHeight
|
||
})
|
||
|
||
function setActiveStep(step) {
|
||
stepIndicator.querySelectorAll('li').forEach(function (item) {
|
||
var index = Number(item.getAttribute('data-step'))
|
||
item.classList.remove('active', 'done')
|
||
if (index < step) item.classList.add('done')
|
||
if (index === step) item.classList.add('active')
|
||
})
|
||
}
|
||
|
||
function clearPage() {
|
||
pageHost.innerHTML = ''
|
||
}
|
||
|
||
function renderStep1() {
|
||
setActiveStep(1)
|
||
clearPage()
|
||
var section = document.createElement('section')
|
||
section.className = 'page'
|
||
section.innerHTML =
|
||
'<h2>1단계. 설치할 음악퀴즈 선택</h2>' +
|
||
'<div id="packList" class="cardChoice"><p class="formMessage">목록을 불러오는 중...</p></div>' +
|
||
'<div class="actionRow"><span></span><button class="primaryBtn" id="next" disabled>다음</button></div>'
|
||
pageHost.appendChild(section)
|
||
var listEl = section.querySelector('#packList')
|
||
var nextBtn = section.querySelector('#next')
|
||
|
||
function renderList() {
|
||
listEl.innerHTML = ''
|
||
if (state.packs.length === 0) {
|
||
listEl.innerHTML = '<p class="formMessage error">등록된 음악퀴즈가 없습니다.</p>'
|
||
return
|
||
}
|
||
state.packs.forEach(function (pack) {
|
||
var btn = document.createElement('button')
|
||
btn.type = 'button'
|
||
btn.innerHTML = '<strong>' + pack.name + '</strong><br><small>마인크래프트 ' + pack.pack.mcVersion + ' / ' + pack.pack.platform.type + '</small>'
|
||
if (state.selectedPackKey === pack.key) btn.classList.add('selected')
|
||
btn.addEventListener('click', function () {
|
||
state.selectedPackKey = pack.key
|
||
nextBtn.disabled = false
|
||
renderList()
|
||
})
|
||
listEl.appendChild(btn)
|
||
})
|
||
}
|
||
|
||
nextBtn.addEventListener('click', async function () {
|
||
if (!state.selectedPackKey) return
|
||
await installerApi.setSelectedPack(state.selectedPackKey)
|
||
state.stepDone[1] = true
|
||
renderStep2()
|
||
})
|
||
|
||
;(async function () {
|
||
try {
|
||
var packs = await installerApi.loadPacks()
|
||
state.packs = packs
|
||
renderList()
|
||
} catch (err) {
|
||
listEl.innerHTML = '<p class="formMessage error">목록을 가져오지 못했습니다: ' + err.message + '</p>'
|
||
}
|
||
})()
|
||
}
|
||
|
||
function renderStep2() {
|
||
setActiveStep(2)
|
||
clearPage()
|
||
var section = document.createElement('section')
|
||
section.className = 'page'
|
||
section.innerHTML =
|
||
'<h2>2단계. 싱글 / 멀티 선택</h2>' +
|
||
'<div class="cardChoice">' +
|
||
'<button id="single" type="button" data-mode="single"><strong>싱글</strong><br><small>혼자 즐기는 모드. 4단계만 진행합니다.</small></button>' +
|
||
'<button id="multi" type="button" data-mode="multi"><strong>멀티</strong><br><small>친구들과 함께. 3단계 서버 설치 후 4단계를 진행합니다.</small></button>' +
|
||
'</div>' +
|
||
'<div class="actionRow"><button class="secondaryBtn" id="back">이전</button><button class="primaryBtn" id="next" disabled>다음</button></div>'
|
||
pageHost.appendChild(section)
|
||
var nextBtn = section.querySelector('#next')
|
||
var modeButtons = section.querySelectorAll('[data-mode]')
|
||
|
||
function applySelection(mode) {
|
||
state.mode = mode
|
||
modeButtons.forEach(function (btn) {
|
||
if (btn.getAttribute('data-mode') === mode) btn.classList.add('selected')
|
||
else btn.classList.remove('selected')
|
||
})
|
||
nextBtn.disabled = false
|
||
}
|
||
|
||
modeButtons.forEach(function (btn) {
|
||
btn.addEventListener('click', function () {
|
||
applySelection(btn.getAttribute('data-mode'))
|
||
})
|
||
})
|
||
|
||
if (state.mode === 'single' || state.mode === 'multi') applySelection(state.mode)
|
||
|
||
nextBtn.addEventListener('click', function () {
|
||
if (!state.mode) return
|
||
state.stepDone[2] = true
|
||
if (state.mode === 'single') renderStep4()
|
||
else renderStep3()
|
||
})
|
||
section.querySelector('#back').addEventListener('click', renderStep1)
|
||
}
|
||
|
||
function renderStep3() {
|
||
setActiveStep(3)
|
||
clearPage()
|
||
var section = document.createElement('section')
|
||
section.className = 'page'
|
||
section.innerHTML =
|
||
'<h2>3단계. 서버 관련 설정</h2>' +
|
||
'<div class="subStep" id="subHost"></div>'
|
||
pageHost.appendChild(section)
|
||
var subHost = section.querySelector('#subHost')
|
||
|
||
function show31() { subHost.innerHTML = ''; renderSubStep31(subHost, renderStep2, show32) }
|
||
function show32() { subHost.innerHTML = ''; renderSubStep32(subHost, show31, show33) }
|
||
function show33() { subHost.innerHTML = ''; renderSubStep33(subHost, show32, show34) }
|
||
function show34() { subHost.innerHTML = ''; renderSubStep34(subHost, show33, show35) }
|
||
function show35() {
|
||
subHost.innerHTML = ''
|
||
renderSubStep35(subHost, show34, function () {
|
||
state.stepDone[3] = true
|
||
renderStep4()
|
||
})
|
||
}
|
||
show31()
|
||
}
|
||
|
||
function renderSubStep31(host, back, done) {
|
||
host.innerHTML =
|
||
'<h3>3-1. 서버 설치 경로</h3>' +
|
||
'<p class="formMessage">서버를 생성할 폴더를 선택하세요. 경로에 한글이 포함되면 안 됩니다.</p>' +
|
||
'<div class="fieldset"><label><input id="installPath" type="text" placeholder="C:\\MusicQuizServer" value="' + (state.serverInstall.path || '') + '" /></label>' +
|
||
'<button class="secondaryBtn" id="pickFolder">폴더 선택</button></div>' +
|
||
'<div class="formMessage" id="msg"></div>' +
|
||
'<div class="actionRow"><button class="secondaryBtn" id="back">이전</button><button class="primaryBtn" id="next">다음</button></div>'
|
||
var input = host.querySelector('#installPath')
|
||
var msg = host.querySelector('#msg')
|
||
host.querySelector('#pickFolder').addEventListener('click', async function () {
|
||
var picked = await installerApi.pickFolder()
|
||
if (picked) input.value = picked
|
||
})
|
||
host.querySelector('#back').addEventListener('click', back)
|
||
host.querySelector('#next').addEventListener('click', async function () {
|
||
var result = await installerApi.validateInstallPath(input.value.trim())
|
||
if (!result.ok) {
|
||
msg.textContent = result.message || '경로가 유효하지 않습니다.'
|
||
msg.classList.add('error')
|
||
return
|
||
}
|
||
msg.textContent = '경로 확정: ' + result.message
|
||
msg.classList.remove('error')
|
||
msg.classList.add('success')
|
||
state.serverInstall.path = input.value.trim()
|
||
done()
|
||
})
|
||
}
|
||
|
||
function renderSubStep32(host, back, done) {
|
||
host.innerHTML =
|
||
'<h3>3-2. JDK 확인</h3>' +
|
||
'<p class="formMessage">JAVA_HOME 또는 C:\\Program Files\\Java 에서 자동 탐색합니다. 직접 폴더를 선택해도 됩니다.</p>' +
|
||
'<div class="fieldset"><label><input id="jdkPath" type="text" placeholder="C:\\Program Files\\Java\\jdk-17" value="' + (state.serverInstall.jdk || '') + '" /></label>' +
|
||
'<button class="secondaryBtn" id="pickJdk">폴더 선택</button>' +
|
||
'<button class="secondaryBtn" id="auto">자동 탐색</button></div>' +
|
||
'<div class="formMessage" id="msg"></div>' +
|
||
'<div class="actionRow"><button class="secondaryBtn" id="back">이전</button><button class="primaryBtn" id="next">다음</button></div>'
|
||
var input = host.querySelector('#jdkPath')
|
||
var msg = host.querySelector('#msg')
|
||
host.querySelector('#auto').addEventListener('click', async function () {
|
||
var detect = await installerApi.detectJdk()
|
||
if (detect.found) {
|
||
input.value = detect.path
|
||
msg.textContent = 'JDK 발견: ' + detect.path
|
||
msg.classList.remove('error')
|
||
msg.classList.add('success')
|
||
} else {
|
||
msg.textContent = 'JDK를 자동으로 찾지 못했습니다. 직접 선택해 주세요.'
|
||
msg.classList.add('error')
|
||
}
|
||
})
|
||
host.querySelector('#pickJdk').addEventListener('click', async function () {
|
||
var picked = await installerApi.pickFolder()
|
||
if (picked) input.value = picked
|
||
})
|
||
host.querySelector('#back').addEventListener('click', back)
|
||
host.querySelector('#next').addEventListener('click', function () {
|
||
if (!input.value.trim()) {
|
||
msg.textContent = 'JDK 경로를 입력해 주세요.'
|
||
msg.classList.add('error')
|
||
return
|
||
}
|
||
state.serverInstall.jdk = input.value.trim()
|
||
done()
|
||
})
|
||
;(async function () {
|
||
var detect = await installerApi.detectJdk()
|
||
if (detect.found && !input.value) {
|
||
input.value = detect.path
|
||
msg.textContent = 'JDK 자동 탐색됨: ' + detect.path
|
||
msg.classList.add('success')
|
||
}
|
||
})()
|
||
}
|
||
|
||
function renderSubStep33(host, back, done) {
|
||
host.innerHTML =
|
||
'<h3>3-3. 서버 다운로드 및 설치</h3>' +
|
||
'<p class="formMessage">선택한 음악퀴즈의 서버 파일을 다운로드합니다. 진행 상황은 하단 로그 뷰어에 표시됩니다.</p>' +
|
||
'<div class="formMessage" id="downloadStatus">대기 중</div>' +
|
||
'<div id="ramSection" hidden style="margin-top:14px;">' +
|
||
'<h4>램 검사</h4>' +
|
||
'<div class="formMessage" id="ramMsg">검사 중...</div>' +
|
||
'</div>' +
|
||
'<div class="actionRow"><button class="secondaryBtn" id="back">이전</button><button class="primaryBtn" id="next" disabled>다음</button></div>'
|
||
|
||
var statusEl = host.querySelector('#downloadStatus')
|
||
var ramSection = host.querySelector('#ramSection')
|
||
var ramMsg = host.querySelector('#ramMsg')
|
||
var nextBtn = host.querySelector('#next')
|
||
|
||
host.querySelector('#back').addEventListener('click', back)
|
||
nextBtn.addEventListener('click', function () {
|
||
if (!state.serverInstall.eulaAccepted) return
|
||
done()
|
||
})
|
||
|
||
// 이미 통과했던 상태 복원: 사용자가 다음→이전으로 돌아왔을 때 재다운로드 강요하지 않는다.
|
||
if (state.serverInstall.eulaAccepted && state.serverInstall.ram) {
|
||
statusEl.textContent = '다운로드 및 EULA 동의 완료.'
|
||
statusEl.classList.add('success')
|
||
showRamResult(state.serverInstall.ram)
|
||
nextBtn.disabled = false
|
||
return
|
||
}
|
||
|
||
// 페이지 진입 즉시 자동 다운로드
|
||
;(async function () {
|
||
state.serverInstall.eulaAccepted = false
|
||
nextBtn.disabled = true
|
||
statusEl.classList.remove('success', 'error')
|
||
statusEl.textContent = '다운로드 중...'
|
||
try {
|
||
await installerApi.startServerInstall({
|
||
packKey: state.selectedPackKey,
|
||
installPath: state.serverInstall.path,
|
||
jdkPath: state.serverInstall.jdk
|
||
})
|
||
statusEl.textContent = 'EULA 동의가 필요합니다. 팝업을 확인해 주세요.'
|
||
var accepted = await openEulaPopup(state.serverInstall.path)
|
||
if (!accepted) {
|
||
statusEl.textContent = 'EULA 동의 실패. 다운로드를 취소했습니다. 이전→다음으로 다시 시도하세요.'
|
||
statusEl.classList.add('error')
|
||
return
|
||
}
|
||
try {
|
||
await installerApi.acceptEula(state.serverInstall.path)
|
||
} catch (err) {
|
||
statusEl.textContent = 'EULA 저장 실패: ' + err.message
|
||
statusEl.classList.add('error')
|
||
return
|
||
}
|
||
state.serverInstall.eulaAccepted = true
|
||
statusEl.textContent = '다운로드 및 EULA 동의 완료.'
|
||
statusEl.classList.add('success')
|
||
var ram = await installerApi.checkRam(state.selectedPackKey)
|
||
state.serverInstall.ram = ram
|
||
showRamResult(ram)
|
||
if (ram.decision === 'tooLow') return
|
||
nextBtn.disabled = false
|
||
} catch (err) {
|
||
statusEl.textContent = '다운로드 실패: ' + (err && err.message ? err.message : err)
|
||
statusEl.classList.add('error')
|
||
}
|
||
})()
|
||
|
||
function showRamResult(result) {
|
||
ramSection.hidden = false
|
||
ramMsg.classList.remove('error', 'warn', 'success')
|
||
if (result.decision === 'tooLow') {
|
||
var pack = state.packs.find(function (p) { return p.key === state.selectedPackKey })
|
||
var minRam = pack ? pack.pack.serverMinRam : 0
|
||
ramMsg.innerHTML = '시스템 램(' + result.systemRamMb + 'MB)이 음악퀴즈 최소 요구치(' + minRam + 'MB)에 미치지 못합니다. 설치를 중단합니다.'
|
||
ramMsg.classList.add('error')
|
||
} else if (result.decision === 'minOk') {
|
||
ramMsg.innerHTML = '시스템 램(' + result.systemRamMb + 'MB)이 권장치보다 부족합니다. 최소치(' + result.appliedRamMb + 'MB)로 진행합니다.'
|
||
ramMsg.classList.add('warn')
|
||
} else {
|
||
ramMsg.textContent = '시스템 램(' + result.systemRamMb + 'MB) 충분. ' + result.appliedRamMb + 'MB로 설정.'
|
||
ramMsg.classList.add('success')
|
||
}
|
||
}
|
||
}
|
||
|
||
// EULA 동의 팝업. resolve(true) = 동의, resolve(false) = 비동의/창 닫힘.
|
||
async function openEulaPopup(installPath) {
|
||
var read = await installerApi.readEula(installPath)
|
||
var bodyHtml = ''
|
||
if (read.exists) {
|
||
bodyHtml = '<p class="formMessage">서버 파일에 포함된 eula.txt 내용입니다.</p>' +
|
||
'<pre class="eulaPre">' + escapeHtml(read.content) + '</pre>'
|
||
} else {
|
||
var fetched = await installerApi.fetchMinecraftEula()
|
||
if (fetched.html) {
|
||
bodyHtml = '<p class="formMessage">서버 파일에 eula.txt가 없어 minecraft.net의 EULA를 표시합니다 (<a href="' + fetched.url + '" target="_blank">' + fetched.url + '</a>).</p>' +
|
||
'<iframe class="eulaFrame" sandbox srcdoc="' + escapeAttr(fetched.html) + '"></iframe>'
|
||
} else {
|
||
bodyHtml = '<p class="formMessage error">EULA 페이지를 불러올 수 없습니다. 직접 확인해 주세요: <a href="https://www.minecraft.net/en-us/eula" target="_blank">https://www.minecraft.net/en-us/eula</a></p>'
|
||
}
|
||
}
|
||
return new Promise(function (resolve) {
|
||
var overlay = document.createElement('div')
|
||
overlay.className = 'modalOverlay'
|
||
overlay.innerHTML =
|
||
'<div class="modalCard" role="dialog" aria-modal="true">' +
|
||
'<header><h3>Minecraft EULA 동의</h3><button type="button" class="modalClose" aria-label="닫기">×</button></header>' +
|
||
'<div class="modalBody">' + bodyHtml + '</div>' +
|
||
'<footer class="actionRow">' +
|
||
'<button type="button" class="secondaryBtn" data-action="reject">비동의</button>' +
|
||
'<button type="button" class="primaryBtn" data-action="accept">동의</button>' +
|
||
'</footer>' +
|
||
'</div>'
|
||
document.body.appendChild(overlay)
|
||
|
||
var settled = false
|
||
function close(result) {
|
||
if (settled) return
|
||
settled = true
|
||
overlay.remove()
|
||
resolve(result)
|
||
}
|
||
overlay.querySelector('[data-action="accept"]').addEventListener('click', function () { close(true) })
|
||
overlay.querySelector('[data-action="reject"]').addEventListener('click', function () { close(false) })
|
||
overlay.querySelector('.modalClose').addEventListener('click', function () { close(false) })
|
||
overlay.addEventListener('click', function (event) {
|
||
if (event.target === overlay) close(false)
|
||
})
|
||
})
|
||
}
|
||
|
||
function escapeHtml(text) {
|
||
return String(text).replace(/[&<>"']/g, function (ch) {
|
||
return { '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' }[ch]
|
||
})
|
||
}
|
||
|
||
function escapeAttr(text) {
|
||
return String(text).replace(/&/g, '&').replace(/"/g, '"')
|
||
}
|
||
|
||
function renderSubStep34(host, back, done) {
|
||
host.innerHTML =
|
||
'<h3>3-4. 서버 설정 편집</h3>' +
|
||
'<p class="formMessage">로컬 웹서버를 띄워 server.properties / bukkit.yml 등을 GUI로 편집합니다.</p>' +
|
||
'<button class="secondaryBtn" id="open">편집기 열기</button>' +
|
||
'<div class="formMessage" id="editorMsg"></div>' +
|
||
'<div class="actionRow"><button class="secondaryBtn" id="back">이전</button><button class="primaryBtn" id="next">다음</button></div>'
|
||
host.querySelector('#open').addEventListener('click', async function () {
|
||
var msg = host.querySelector('#editorMsg')
|
||
try {
|
||
var result = await installerApi.startServerConfigEditor(state.serverInstall.path)
|
||
msg.innerHTML = '편집기 주소: <a href="' + result.url + '" target="_blank">' + result.url + '</a>'
|
||
} catch (err) {
|
||
msg.textContent = '편집기 실행 실패: ' + err.message
|
||
msg.classList.add('error')
|
||
}
|
||
})
|
||
host.querySelector('#back').addEventListener('click', back)
|
||
host.querySelector('#next').addEventListener('click', done)
|
||
}
|
||
|
||
function renderSubStep35(host, back, done) {
|
||
host.innerHTML =
|
||
'<h3>3-5. 포트포워딩 점검</h3>' +
|
||
'<p class="formMessage">서버의 외부 접근 가능 여부를 확인합니다. UPnP를 시도해도 안 되면 직접 포트포워딩을 안내합니다.</p>' +
|
||
'<div class="fieldset"><label>포트 <input id="port" type="text" value="25565" /></label></div>' +
|
||
'<button class="secondaryBtn" id="run">재점검</button>' +
|
||
'<div class="formMessage" id="resultMsg"></div>' +
|
||
'<div class="actionRow"><button class="secondaryBtn" id="back">이전</button><button class="primaryBtn" id="next" disabled>다음</button></div>'
|
||
var resultMsg = host.querySelector('#resultMsg')
|
||
var nextBtn = host.querySelector('#next')
|
||
var runBtn = host.querySelector('#run')
|
||
host.querySelector('#back').addEventListener('click', back)
|
||
|
||
async function runCheck() {
|
||
runBtn.disabled = true
|
||
resultMsg.classList.remove('success', 'warn', 'error')
|
||
resultMsg.textContent = '확인 중...'
|
||
var port = Number(host.querySelector('#port').value) || 25565
|
||
try {
|
||
var result = await installerApi.checkPortForward(port)
|
||
state.serverInstall.portStatus = result
|
||
if (result.status === 'preForwarded') {
|
||
resultMsg.innerHTML = '이미 외부 접속 가능: ' + result.externalIp + ':' + result.port
|
||
resultMsg.classList.add('success')
|
||
} else if (result.status === 'upnpOk') {
|
||
resultMsg.innerHTML = 'UPnP로 자동 개방 완료: ' + result.externalIp + ':' + result.port
|
||
resultMsg.classList.add('success')
|
||
} else {
|
||
resultMsg.innerHTML = (result.message || '직접 포트포워딩을 해주세요.') +
|
||
'<br><small>외부 IP: ' + (result.externalIp || '확인 불가') + ', 포트: ' + result.port + '</small>'
|
||
resultMsg.classList.add('warn')
|
||
}
|
||
nextBtn.disabled = false
|
||
} catch (err) {
|
||
resultMsg.textContent = '점검 실패: ' + (err && err.message ? err.message : err)
|
||
resultMsg.classList.add('error')
|
||
} finally {
|
||
runBtn.disabled = false
|
||
}
|
||
}
|
||
|
||
runBtn.addEventListener('click', runCheck)
|
||
nextBtn.addEventListener('click', done)
|
||
// 페이지 진입 즉시 자동 점검
|
||
runCheck()
|
||
}
|
||
|
||
function renderStep4() {
|
||
setActiveStep(4)
|
||
clearPage()
|
||
var pack = state.packs.find(function (p) { return p.key === state.selectedPackKey })
|
||
var section = document.createElement('section')
|
||
section.className = 'page'
|
||
section.innerHTML =
|
||
'<h2>4단계. 유저 클라이언트 설정</h2>' +
|
||
'<div class="subStep" id="subHost"></div>'
|
||
pageHost.appendChild(section)
|
||
var subHost = section.querySelector('#subHost')
|
||
function backToPrevStep() { if (state.mode === 'multi') renderStep3(); else renderStep2() }
|
||
|
||
function show41() { subHost.innerHTML = ''; renderSubStep41(subHost, pack, backToPrevStep, show42) }
|
||
function show42() { subHost.innerHTML = ''; renderSubStep42(subHost, show41, show43) }
|
||
function show43() {
|
||
subHost.innerHTML = ''
|
||
renderSubStep43(subHost, show42, function () {
|
||
state.stepDone[4] = true
|
||
renderStep5()
|
||
})
|
||
}
|
||
show41()
|
||
}
|
||
|
||
function renderSubStep41(host, pack, back, done) {
|
||
var platformType = pack ? pack.pack.platform.type : 'vanilla'
|
||
if (platformType === 'vanilla') {
|
||
state.client.installPlatform = false
|
||
host.innerHTML =
|
||
'<h3>4-1. 모드 플랫폼</h3>' +
|
||
'<p class="formMessage">선택한 음악퀴즈의 플랫폼: <strong>vanilla</strong></p>' +
|
||
'<p class="formMessage">바닐라이므로 별도 설치는 필요 없습니다.</p>' +
|
||
'<div class="actionRow"><button class="secondaryBtn" id="back">이전</button><button class="primaryBtn" id="next">다음</button></div>'
|
||
host.querySelector('#back').addEventListener('click', back)
|
||
host.querySelector('#next').addEventListener('click', done)
|
||
return
|
||
}
|
||
|
||
host.innerHTML =
|
||
'<h3>4-1. 모드 플랫폼</h3>' +
|
||
'<p class="formMessage">선택한 음악퀴즈의 플랫폼: <strong>' + platformType + '</strong></p>' +
|
||
'<div class="cardChoice">' +
|
||
'<button type="button" data-choice="install"><strong>권장 플랫폼 설치</strong><br><small>' + platformType + ' 설치파일을 함께 다운로드해 4-2 설치 시작 시 함께 설치됩니다.</small></button>' +
|
||
'<button type="button" data-choice="skip"><strong>기본 마인크래프트로 설치</strong><br><small>플랫폼은 설치하지 않고 바닐라로 진행합니다.</small></button>' +
|
||
'</div>' +
|
||
'<div class="actionRow"><button class="secondaryBtn" id="back">이전</button><button class="primaryBtn" id="next" disabled>다음</button></div>'
|
||
|
||
var nextBtn = host.querySelector('#next')
|
||
var choiceButtons = host.querySelectorAll('[data-choice]')
|
||
|
||
function applyChoice(choice) {
|
||
state.client.installPlatform = choice === 'install'
|
||
choiceButtons.forEach(function (btn) {
|
||
if (btn.getAttribute('data-choice') === choice) btn.classList.add('selected')
|
||
else btn.classList.remove('selected')
|
||
})
|
||
nextBtn.disabled = false
|
||
}
|
||
|
||
choiceButtons.forEach(function (btn) {
|
||
btn.addEventListener('click', function () {
|
||
applyChoice(btn.getAttribute('data-choice'))
|
||
})
|
||
})
|
||
|
||
if (typeof state.client.installPlatform === 'boolean') {
|
||
applyChoice(state.client.installPlatform ? 'install' : 'skip')
|
||
}
|
||
|
||
host.querySelector('#back').addEventListener('click', back)
|
||
nextBtn.addEventListener('click', done)
|
||
}
|
||
|
||
function renderSubStep42(host, back, done) {
|
||
host.innerHTML =
|
||
'<h3>4-2. 모드/리소스팩 다운로드 및 launcher_profiles 갱신</h3>' +
|
||
'<p class="formMessage">%appdata%\\.mc_custom 에 모드와 리소스팩을 설치하고, launcher_profiles.json에 프로필을 등록합니다.</p>' +
|
||
'<div class="formMessage" id="msg">설치 중...</div>' +
|
||
'<div class="actionRow"><button class="secondaryBtn" id="back">이전</button><button class="primaryBtn" id="next" disabled>다음</button></div>'
|
||
var msg = host.querySelector('#msg')
|
||
var nextBtn = host.querySelector('#next')
|
||
host.querySelector('#back').addEventListener('click', back)
|
||
nextBtn.addEventListener('click', done)
|
||
|
||
// 이미 설치됐다면 다시 돌리지 않음
|
||
if (state.client.clientInstalled) {
|
||
msg.textContent = '클라이언트 설치 완료.'
|
||
msg.classList.add('success')
|
||
nextBtn.disabled = false
|
||
return
|
||
}
|
||
|
||
// 페이지 진입 즉시 자동 설치
|
||
;(async function () {
|
||
try {
|
||
await installerApi.installClient({
|
||
packKey: state.selectedPackKey,
|
||
installPlatform: !!state.client.installPlatform
|
||
})
|
||
msg.textContent = '클라이언트 설치 완료.'
|
||
msg.classList.add('success')
|
||
state.client.clientInstalled = true
|
||
nextBtn.disabled = false
|
||
} catch (err) {
|
||
msg.textContent = '설치 실패: ' + (err && err.message ? err.message : err)
|
||
msg.classList.add('error')
|
||
}
|
||
})()
|
||
}
|
||
|
||
function renderSubStep43(host, back, done) {
|
||
host.innerHTML =
|
||
'<h3>4-3. 완료 확인</h3>' +
|
||
'<p class="formMessage">모드와 리소스팩이 .mc_custom에 설치되어 있고, launcher_profiles.json도 갱신되었습니다.</p>' +
|
||
'<div class="actionRow"><button class="secondaryBtn" id="back">이전</button><button class="primaryBtn" id="next">5단계로</button></div>'
|
||
host.querySelector('#back').addEventListener('click', back)
|
||
host.querySelector('#next').addEventListener('click', done)
|
||
}
|
||
|
||
function renderStep5() {
|
||
setActiveStep(5)
|
||
clearPage()
|
||
var section = document.createElement('section')
|
||
section.className = 'page'
|
||
var multi = state.mode === 'multi'
|
||
section.innerHTML =
|
||
'<h2>5단계. 설치 완료</h2>' +
|
||
'<p>모든 단계가 끝났습니다. 아래 옵션을 선택해 주세요.</p>' +
|
||
(multi ? '<div class="subStep">' +
|
||
'<h3>서버</h3>' +
|
||
'<button class="secondaryBtn" id="openFolder">서버 폴더 열기</button>' +
|
||
'<label class="toggleRow"><input type="checkbox" id="shortcut" checked /> 바탕화면에 서버 실행 바로가기 만들기</label>' +
|
||
'<label class="toggleRow"><input type="checkbox" id="startServer" checked /> 서버 바로 실행</label>' +
|
||
'</div>' : '') +
|
||
'<div class="subStep">' +
|
||
'<h3>마인크래프트 런처</h3>' +
|
||
'<label class="toggleRow"><input type="checkbox" id="startLauncher" checked /> 마인크래프트 런처 실행</label>' +
|
||
'</div>' +
|
||
'<div class="actionRow"><button class="secondaryBtn" id="back">이전</button><button class="primaryBtn" id="finish">완료</button></div>'
|
||
pageHost.appendChild(section)
|
||
section.querySelector('#back').addEventListener('click', renderStep4)
|
||
if (multi) {
|
||
section.querySelector('#openFolder').addEventListener('click', function () {
|
||
installerApi.openServerFolder()
|
||
})
|
||
}
|
||
section.querySelector('#finish').addEventListener('click', async function () {
|
||
var finishBtn = section.querySelector('#finish')
|
||
finishBtn.disabled = true
|
||
finishBtn.textContent = '마무리 중…'
|
||
try {
|
||
if (multi) {
|
||
if (section.querySelector('#shortcut').checked) await installerApi.createDesktopShortcut()
|
||
if (section.querySelector('#startServer').checked) await installerApi.startServer()
|
||
}
|
||
if (section.querySelector('#startLauncher').checked) await installerApi.startMinecraftLauncher()
|
||
} catch (err) {
|
||
// 마무리 액션 실패는 무시하고 종료 진행
|
||
}
|
||
finishBtn.textContent = '완료됨'
|
||
// 모든 단계가 끝났으므로 설치기 종료
|
||
if (installerApi.quitApp) installerApi.quitApp()
|
||
})
|
||
}
|
||
|
||
renderStep1()
|