'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') section.querySelector('#back').addEventListener('click', renderStep2) function show31() { subHost.innerHTML = ''; renderSubStep31(subHost, show32) } function show32() { subHost.innerHTML = ''; renderSubStep32(subHost, show33) } function show33() { subHost.innerHTML = ''; renderSubStep33(subHost, show34) } function show34() { subHost.innerHTML = ''; renderSubStep34(subHost, show35) } function show35() { subHost.innerHTML = '' renderSubStep35(subHost, function () { state.stepDone[3] = true renderStep4() }) } show31() } 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. 서버 다운로드 및 설치

' + '

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

' + '
대기 중
' + '' + '' + '' + '
' 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) var subHost = section.querySelector('#subHost') section.querySelector('#back').addEventListener('click', function () { if (state.mode === 'multi') renderStep3(); else renderStep2() }) function show41() { subHost.innerHTML = '' renderSubStep41(subHost, pack, show42) } function show42() { subHost.innerHTML = '' renderSubStep42(subHost, show43) } function show43() { subHost.innerHTML = '' renderSubStep43(subHost, function () { state.stepDone[4] = true renderStep5() }) } show41() } function renderSubStep41(host, pack, done) { var platformType = pack ? pack.pack.platform.type : 'vanilla' if (platformType === 'vanilla') { state.client.installPlatform = false host.innerHTML = '

4-1. 모드 플랫폼

' + '

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

' + '

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

' + '
' 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') } nextBtn.addEventListener('click', done) } function renderSubStep42(host, done) { host.innerHTML = '

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

' + '

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

' + '' + '
' + '
' var runBtn = host.querySelector('#run') var msg = host.querySelector('#msg') var nextBtn = host.querySelector('#next') runBtn.addEventListener('click', async function () { runBtn.disabled = true msg.textContent = '설치 중...' msg.classList.remove('error', 'success') try { await installerApi.installClient({ packKey: state.selectedPackKey, installPlatform: !!state.client.installPlatform }) msg.textContent = '클라이언트 설치 완료.' msg.classList.add('success') nextBtn.hidden = false } catch (err) { msg.textContent = '설치 실패: ' + err.message msg.classList.add('error') runBtn.disabled = false } }) nextBtn.addEventListener('click', done) } 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()