'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) 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 = '

3단계. 서버 관련 설정

' + '
' 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 = '

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('#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 = '

3-2. JDK 확인

' + '

JAVA_HOME 또는 C:\\Program Files\\Java 에서 자동 탐색합니다. 없으면 "자동 설치" 로 Temurin 21 을 받아 설치할 수 있습니다.

' + '
' + '' + '' + '
' + '
' + '
' var input = host.querySelector('#jdkPath') var msg = host.querySelector('#msg') var installBtn = host.querySelector('#install') var autoBtn = host.querySelector('#auto') var pickBtn = host.querySelector('#pickJdk') var nextBtn = host.querySelector('#next') var installing = false function setInstallingUi(on) { installing = on if (on) { installBtn.textContent = '설치 취소' installBtn.classList.remove('secondaryBtn') installBtn.classList.add('dangerBtn') autoBtn.disabled = true pickBtn.disabled = true nextBtn.disabled = true input.disabled = true } else { installBtn.textContent = '자동 설치' installBtn.classList.remove('dangerBtn') installBtn.classList.add('secondaryBtn') autoBtn.disabled = false pickBtn.disabled = false nextBtn.disabled = false input.disabled = false } } autoBtn.addEventListener('click', async function () { if (installing) return 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를 자동으로 찾지 못했습니다. "자동 설치" 를 눌러 Temurin 21 을 설치하거나 직접 선택해 주세요.' msg.classList.remove('success') msg.classList.add('error') } }) pickBtn.addEventListener('click', async function () { if (installing) return var picked = await installerApi.pickFolder() if (picked) input.value = picked }) installBtn.addEventListener('click', async function () { if (installing) { // 진행 중이면 취소. msg.textContent = 'JDK 설치 취소 요청 중...' msg.classList.remove('success', 'error') await installerApi.cancelJdkInstall() return } setInstallingUi(true) msg.classList.remove('success', 'error') msg.textContent = 'Temurin 21 다운로드 중... (네트워크 상태에 따라 1~5분)' try { var result = await installerApi.installJdk() if (result.ok && result.path) { input.value = result.path state.serverInstall.jdk = result.path msg.textContent = 'JDK 자동 설치 완료: ' + result.path msg.classList.add('success') } else { msg.textContent = 'JDK 설치 ' + (result.message === '취소됨' ? '취소됨' : '실패: ' + (result.message || '알 수 없는 오류')) msg.classList.add('error') } } catch (err) { msg.textContent = 'JDK 설치 오류: ' + (err && err.message ? err.message : err) msg.classList.add('error') } finally { setInstallingUi(false) } }) host.querySelector('#back').addEventListener('click', function () { if (installing) return back() }) nextBtn.addEventListener('click', function () { if (installing) return 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') } else if (!detect.found) { msg.textContent = 'JDK를 자동으로 찾지 못했습니다. "자동 설치" 를 누르면 Temurin 21 LTS 를 받아 설치합니다.' } })() } function renderSubStep33(host, back, done) { host.innerHTML = '

3-3. 서버 다운로드 및 설치

' + '

선택한 음악퀴즈의 서버 파일을 다운로드합니다. 진행 상황은 하단 로그 뷰어에 표시됩니다.

' + '
대기 중
' + '' + '
' 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 = '

서버 파일에 포함된 eula.txt 내용입니다.

' + '
' + escapeHtml(read.content) + '
' } else { var fetched = await installerApi.fetchMinecraftEula() if (fetched.html) { bodyHtml = '

서버 파일에 eula.txt가 없어 minecraft.net의 EULA를 표시합니다 (' + fetched.url + ').

' + '' } else { bodyHtml = '

EULA 페이지를 불러올 수 없습니다. 직접 확인해 주세요: https://www.minecraft.net/en-us/eula

' } } return new Promise(function (resolve) { var overlay = document.createElement('div') overlay.className = 'modalOverlay' overlay.innerHTML = '' 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 = '

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('#back').addEventListener('click', back) host.querySelector('#next').addEventListener('click', done) } function renderSubStep35(host, back, done) { host.innerHTML = '

3-5. 포트포워딩 점검

' + '

서버의 외부 접근 가능 여부를 확인합니다. UPnP를 시도해도 안 되면 직접 포트포워딩을 안내합니다.

' + '
' + '' + '
' + '
' 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 || '직접 포트포워딩을 해주세요.') + '
외부 IP: ' + (result.externalIp || '확인 불가') + ', 포트: ' + result.port + '' 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 = '

4단계. 유저 클라이언트 설정

' + '
' 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 = '

4-1. 모드 플랫폼

' + '

선택한 음악퀴즈의 플랫폼: vanilla

' + '

바닐라이므로 별도 설치는 필요 없습니다.

' + '
' host.querySelector('#back').addEventListener('click', back) host.querySelector('#next').addEventListener('click', done) return } host.innerHTML = '

4-1. 모드 플랫폼

' + '

선택한 음악퀴즈의 플랫폼: ' + platformType + '

' + '
' + '' + '' + '
' + '
' 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 = '

4-2. 모드/리소스팩 다운로드 및 launcher_profiles 갱신

' + '

%appdata%\\.mc_custom 에 모드와 리소스팩을 설치하고, launcher_profiles.json에 프로필을 등록합니다.

' + '
설치 중...
' + '
' 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 = '

4-3. 완료 확인

' + '

모드와 리소스팩이 .mc_custom에 설치되어 있고, launcher_profiles.json도 갱신되었습니다.

' + '
' 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 = '

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 () { 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 = '완료됨' // 자동 종료는 임시 비활성화 (런처 실행 오류 메시지 확인용). 필요 시 X 버튼으로 직접 닫는다. // if (installerApi.quitApp) installerApi.quitApp() }) } renderStep1()