'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 =
'
1단계. 설치할 음악퀴즈 선택
' +
'' +
'
'
pageHost.appendChild(section)
var listEl = section.querySelector('#packList')
var nextBtn = section.querySelector('#next')
function renderList() {
listEl.innerHTML = ''
if (state.packs.length === 0) {
listEl.innerHTML = '등록된 음악퀴즈가 없습니다.
'
return
}
state.packs.forEach(function (pack) {
var btn = document.createElement('button')
btn.type = 'button'
btn.innerHTML = '' + pack.name + '
마인크래프트 ' + pack.pack.mcVersion + ' / ' + pack.pack.platform.type + ''
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 = '목록을 가져오지 못했습니다: ' + err.message + '
'
}
})()
}
function renderStep2() {
setActiveStep(2)
clearPage()
var section = document.createElement('section')
section.className = 'page'
section.innerHTML =
'2단계. 싱글 / 멀티 선택
' +
'' +
'' +
'' +
'
' +
'
'
pageHost.appendChild(section)
section.querySelector('#single').addEventListener('click', function () {
state.mode = 'single'
state.stepDone[2] = true
renderStep4()
})
section.querySelector('#multi').addEventListener('click', function () {
state.mode = 'multi'
state.stepDone[2] = true
renderStep3()
})
section.querySelector('#back').addEventListener('click', renderStep1)
}
function renderStep3() {
setActiveStep(3)
clearPage()
var section = document.createElement('section')
section.className = 'page'
section.innerHTML =
'3단계. 서버 관련 설정
' +
'' +
'' +
'' +
'' +
'' +
''
pageHost.appendChild(section)
section.querySelector('#back').addEventListener('click', renderStep2)
renderSubStep31(section.querySelector('#sub31'), function () {
section.querySelector('#sub32').hidden = false
renderSubStep32(section.querySelector('#sub32'), function () {
section.querySelector('#sub33').hidden = false
renderSubStep33(section.querySelector('#sub33'), function () {
section.querySelector('#sub34').hidden = false
renderSubStep34(section.querySelector('#sub34'), function () {
section.querySelector('#sub35').hidden = false
renderSubStep35(section.querySelector('#sub35'), function () {
section.querySelector('#proceedClient').hidden = false
section.querySelector('#proceedClient').addEventListener('click', function () {
state.stepDone[3] = true
renderStep4()
})
})
})
})
})
})
}
function renderSubStep31(host, done) {
host.innerHTML =
'3-1. 서버 설치 경로
' +
'서버를 생성할 폴더를 선택하세요. 경로에 한글이 포함되면 안 됩니다.
' +
'' +
'
' +
'' +
''
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('#confirm').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, done) {
host.innerHTML =
'3-2. JDK 확인
' +
'JAVA_HOME 또는 C:\\Program Files\\Java 에서 자동 탐색합니다. 직접 폴더를 선택해도 됩니다.
' +
'' +
'' +
'
' +
'' +
''
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('#confirm').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, done) {
host.innerHTML =
'3-3. 서버 다운로드 및 설치
' +
'선택한 음악퀴즈의 서버 파일을 다운로드합니다. 진행 상황은 하단 로그 뷰어에 표시됩니다.
' +
'대기 중
' +
'' +
'' +
'
3-3-3. EULA 동의
' +
'
Minecraft EULA: 본 설치는 Minecraft End User License Agreement (https://www.minecraft.net/ko-kr/eula) 동의가 필요합니다. 동의 시 eula.txt가 새로 작성됩니다.
' +
'
' +
'
' +
'
' +
'' +
'
3-3-4. 램 검사
' +
'
검사 중...
' +
'
' +
'
'
var startBtn = host.querySelector('#startDownload')
var statusEl = host.querySelector('#downloadStatus')
var eulaSection = host.querySelector('#eulaSection')
var ramSection = host.querySelector('#ramSection')
var ramMsg = host.querySelector('#ramMsg')
var confirmBtn = host.querySelector('#confirm')
var eulaCheck = host.querySelector('#eulaCheck')
var eulaMsg = host.querySelector('#eulaMsg')
startBtn.addEventListener('click', async function () {
startBtn.disabled = true
statusEl.textContent = '다운로드 중...'
try {
await installerApi.startServerInstall({
packKey: state.selectedPackKey,
installPath: state.serverInstall.path,
jdkPath: state.serverInstall.jdk
})
statusEl.textContent = '다운로드 완료. EULA 동의가 필요합니다.'
eulaSection.hidden = false
} catch (err) {
statusEl.textContent = '다운로드 실패: ' + err.message
startBtn.disabled = false
}
})
eulaCheck.addEventListener('change', async function () {
if (!eulaCheck.checked) return
try {
await installerApi.acceptEula(state.serverInstall.path)
eulaMsg.textContent = 'EULA 동의 저장됨.'
eulaMsg.classList.add('success')
ramSection.hidden = false
var result = await installerApi.checkRam(state.selectedPackKey)
state.serverInstall.ram = result
if (result.decision === 'tooLow') {
ramMsg.innerHTML = '시스템 램(' + result.systemRamMb + 'MB)이 음악퀴즈 최소 요구치(' + (state.packs.find(function (p) { return p.key === state.selectedPackKey }).pack.serverMinRam) + 'MB)에 미치지 못합니다. 설치를 중단합니다.'
ramMsg.classList.add('error')
return
}
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')
}
confirmBtn.hidden = false
} catch (err) {
eulaMsg.textContent = 'EULA 저장 실패: ' + err.message
eulaMsg.classList.add('error')
}
})
confirmBtn.addEventListener('click', function () {
state.serverInstall.eulaAccepted = true
done()
})
}
function renderSubStep34(host, done) {
host.innerHTML =
'3-4. 서버 설정 편집
' +
'로컬 웹서버를 띄워 server.properties / bukkit.yml 등을 GUI로 편집합니다.
' +
'' +
'' +
''
host.querySelector('#open').addEventListener('click', async function () {
var msg = host.querySelector('#editorMsg')
try {
var result = await installerApi.startServerConfigEditor(state.serverInstall.path)
msg.innerHTML = '편집기 주소: ' + result.url + ''
} catch (err) {
msg.textContent = '편집기 실행 실패: ' + err.message
msg.classList.add('error')
}
})
host.querySelector('#confirm').addEventListener('click', done)
}
function renderSubStep35(host, done) {
host.innerHTML =
'3-5. 포트포워딩 점검
' +
'서버의 외부 접근 가능 여부를 확인합니다. UPnP를 시도해도 안 되면 직접 포트포워딩을 안내합니다.
' +
'' +
'' +
'' +
''
var resultMsg = host.querySelector('#resultMsg')
var confirmBtn = host.querySelector('#confirm')
host.querySelector('#run').addEventListener('click', async function () {
var port = Number(host.querySelector('#port').value) || 25565
resultMsg.textContent = '확인 중...'
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 || '직접 포트포워딩을 해주세요.') +
'
외부 IP: ' + (result.externalIp || '확인 불가') + ', 포트: ' + result.port + ''
resultMsg.classList.add('warn')
}
confirmBtn.hidden = false
})
confirmBtn.addEventListener('click', done)
}
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 =
'4단계. 유저 클라이언트 설정
' +
'' +
'' +
'' +
''
pageHost.appendChild(section)
section.querySelector('#back').addEventListener('click', function () {
if (state.mode === 'multi') renderStep3(); else renderStep2()
})
renderSubStep41(section.querySelector('#sub41'), pack, function () {
section.querySelector('#sub42').hidden = false
renderSubStep42(section.querySelector('#sub42'), function () {
section.querySelector('#sub43').hidden = false
renderSubStep43(section.querySelector('#sub43'), function () {
var nextBtn = section.querySelector('#next')
nextBtn.hidden = false
nextBtn.addEventListener('click', function () {
state.stepDone[4] = true
renderStep5()
})
})
})
})
}
function renderSubStep41(host, pack, done) {
var platformType = pack ? pack.pack.platform.type : 'vanilla'
host.innerHTML =
'4-1. 모드 플랫폼
' +
'선택한 음악퀴즈의 플랫폼: ' + platformType + '
' +
(platformType === 'vanilla'
? '바닐라이므로 별도 설치는 필요 없습니다.
'
: '')
if (platformType === 'vanilla') {
state.client.installPlatform = false
host.querySelector('#next').addEventListener('click', done)
return
}
host.querySelector('#install').addEventListener('click', function () {
state.client.installPlatform = true
done()
})
host.querySelector('#skip').addEventListener('click', function () {
state.client.installPlatform = false
done()
})
}
function renderSubStep42(host, done) {
host.innerHTML =
'4-2. 모드/리소스팩 다운로드 및 launcher_profiles 갱신
' +
'%appdata%\\.mc_custom 에 모드와 리소스팩을 설치하고, launcher_profiles.json에 프로필을 등록합니다.
' +
'' +
''
host.querySelector('#run').addEventListener('click', async function () {
var msg = host.querySelector('#msg')
msg.textContent = '설치 중...'
msg.classList.remove('error', 'success')
try {
await installerApi.installClient({
packKey: state.selectedPackKey,
installPlatform: !!state.client.installPlatform
})
msg.textContent = '클라이언트 설치 완료.'
msg.classList.add('success')
done()
} catch (err) {
msg.textContent = '설치 실패: ' + err.message
msg.classList.add('error')
}
})
}
function renderSubStep43(host, done) {
host.innerHTML = '4-3. 완료 확인
모드와 리소스팩이 .mc_custom에 설치되어 있고, launcher_profiles.json도 갱신되었습니다.
'
host.querySelector('#confirm').addEventListener('click', done)
}
function renderStep5() {
setActiveStep(5)
clearPage()
var section = document.createElement('section')
section.className = 'page'
var multi = state.mode === 'multi'
section.innerHTML =
'5단계. 설치 완료
' +
'모든 단계가 끝났습니다. 아래 옵션을 선택해 주세요.
' +
(multi ? '' +
'
서버
' +
'' +
'' +
'' +
'' : '') +
'' +
'
마인크래프트 런처
' +
'' +
'' +
''
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 () {
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()
section.querySelector('#finish').disabled = true
section.querySelector('#finish').textContent = '완료됨'
})
}
renderStep1()