@@ -160,19 +160,17 @@ function renderStep3() {
section . className = 'page'
section . innerHTML =
'<h2>3단계. 서버 관련 설정</h2>' +
'<div class="subStep" id="subHost"></div>' +
'<div class="actionRow"><button class="secondaryBtn" id="back">이전</button><span></span></div>'
'<div class="subStep" id="subHost"></div>'
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 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 , function ( ) {
renderSubStep35 ( subHost , show34 , function ( ) {
state . stepDone [ 3 ] = true
renderStep4 ( )
} )
@@ -180,21 +178,22 @@ function renderStep3() {
show31 ( )
}
function renderSubStep31 ( host , done ) {
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>' +
'<button class="prim aryBtn" id="confirm">확인 </button>'
'<div class="actionRow">< button class="second aryBtn" 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 ( '#confirm ' ) . addEventListener ( 'click' , async function ( ) {
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 || '경로가 유효하지 않습니다.'
@@ -209,7 +208,7 @@ function renderSubStep31(host, done) {
} )
}
function renderSubStep32 ( host , done ) {
function renderSubStep32 ( host , back , done ) {
host . innerHTML =
'<h3>3-2. JDK 확인</h3>' +
'<p class="formMessage">JAVA_HOME 또는 C:\\Program Files\\Java 에서 자동 탐색합니다. 직접 폴더를 선택해도 됩니다.</p>' +
@@ -217,7 +216,7 @@ function renderSubStep32(host, done) {
'<button class="secondaryBtn" id="pickJdk">폴더 선택</button>' +
'<button class="secondaryBtn" id="auto">자동 탐색</button></div>' +
'<div class="formMessage" id="msg"></div>' +
'<button class="prim aryBtn" id="confirm">확인 </button>'
'<div class="actionRow">< button class="second aryBtn" 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 ( ) {
@@ -236,7 +235,8 @@ function renderSubStep32(host, done) {
var picked = await installerApi . pickFolder ( )
if ( picked ) input . value = picked
} )
host . querySelector ( '#confirm ' ) . addEventListener ( 'click' , function ( ) {
host . querySelector ( '#back ' ) . addEventListener ( 'click' , back )
host . querySelector ( '#next' ) . addEventListener ( 'click' , function ( ) {
if ( ! input . value . trim ( ) ) {
msg . textContent = 'JDK 경로를 입력해 주세요.'
msg . classList . add ( 'error' )
@@ -255,35 +255,39 @@ function renderSubStep32(host, done) {
} ) ( )
}
function renderSubStep33 ( host , done ) {
function renderSubStep33 ( host , back , done ) {
host . innerHTML =
'<h3>3-3. 서버 다운로드 및 설치</h3>' +
'<p class="formMessage">선택한 음악퀴즈의 서버 파일을 다운로드합니다. 진행 상황은 하단 로그 뷰어에 표시됩니다.</p>' +
'<div class="formMessage" id="downloadStatus">대기 중</div>' +
'<button class="primaryBtn" id="startDownload">다운로드 시작</button>' +
'<div id="eulaSection" hidden style="margin-top:14px;">' +
'<h3>3-3-3. EULA 동의</h3>' +
'<div class="eulaBox">Minecraft EULA: 본 설치는 Minecraft End User License Agreement (https://www.minecraft.net/ko-kr/eula) 동의가 필요합니다. 동의 시 eula.txt가 새로 작성됩니다.</div>' +
'<label class="toggleRow"><input id="eulaCheck" type="checkbox" /> Minecraft EULA에 동의합니다.</label>' +
'<div class="formMessage" id="eulaMsg"></div>' +
'</div>' +
'<div id="ramSection" hidden style="margin-top:14px;">' +
'<h3>3-3-4. 램 검사</h3 >' +
'<h4> 램 검사</h4 >' +
'<div class="formMessage" id="ramMsg">검사 중...</div>' +
'</div>' +
'<div class="actionRow"><span></spa n><button class="primaryBtn" id="confirm" hidden >다음</button></div>'
'<div class="actionRow"><button class="secondaryBtn" id="back">이전</butto n><button class="primaryBtn" id="next" disabled >다음</button></div>'
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 confirm Btn = host . querySelector ( '#confirm ' )
var eulaCheck = host . querySelector ( '#eulaCheck' )
var eulaMsg = host . querySelector ( '#eulaMsg' )
var next Btn = host . querySelector ( '#next ' )
host . querySelector ( '#back' ) . addEventListener ( 'click' , back )
// 이미 통과했던 상태 복원: 사용자가 다음→이전으로 돌아왔을 때 재다운로드 강요하지 않는다.
if ( state . serverInstall . eulaAccepted && state . serverInstall . ram ) {
statusEl . textContent = '다운로드 및 EULA 동의 완료.'
statusEl . classList . add ( 'success' )
showRamResult ( state . serverInstall . ram )
nextBtn . disabled = false
}
startBtn . addEventListener ( 'click' , async function ( ) {
startBtn . disabled = true
state . serverInstall . eulaAccepted = false
nextBtn . disabled = true
statusEl . classList . remove ( 'success' , 'error' )
statusEl . textContent = '다운로드 중...'
try {
await installerApi . startServerInstall ( {
@@ -291,55 +295,123 @@ function renderSubStep33(host, done) {
installPath : state . serverInstall . path ,
jdkPath : state . serverInstall . jdk
} )
statusEl . textContent = '다운로드 완료. EULA 동의가 필요합니다.'
eulaSection . hidden = false
statusEl . textContent = 'EULA 동의가 필요합니다. 팝업을 확인해 주세요. '
var accepted = await openEulaPopup ( state . serverInstall . path )
if ( ! accepted ) {
statusEl . textContent = 'EULA 동의 실패. 다운로드를 취소합니다. "다운로드 시작"으로 다시 시도하세요.'
statusEl . classList . add ( 'error' )
startBtn . disabled = false
return
}
try {
await installerApi . acceptEula ( state . serverInstall . path )
} catch ( err ) {
statusEl . textContent = 'EULA 저장 실패: ' + err . message
statusEl . classList . add ( 'error' )
startBtn . disabled = false
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 . message
statusEl . classList . add ( 'error' )
startBtn . disabled = false
}
} )
eulaCheck . addEventListener ( 'change' , async function ( ) {
if ( ! eulaCheck . check ed) 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' )
}
nextBtn . addEventListener ( 'click' , function ( ) {
if ( ! state . serverInstall . eulaAccept ed) return
done ( )
} )
confirmBtn . addEventListener ( 'click' , function ( ) {
state . serverInstall . eulaAccepted = tru e
done ( )
function showRamResult ( result ) {
ramSection . hidden = fals e
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 renderSubStep34 ( host , done ) {
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>' +
'<button class="prim aryBtn" id="confirm">확인 </button>'
'<div class="actionRow">< button class="second aryBtn" id="back">이전</button><button class="primaryBtn" id="next">다음 </button></div> '
host . querySelector ( '#open' ) . addEventListener ( 'click' , async function ( ) {
var msg = host . querySelector ( '#editorMsg' )
try {
@@ -350,19 +422,22 @@ function renderSubStep34(host, done) {
msg . classList . add ( 'error' )
}
} )
host . querySelector ( '#confirm ' ) . addEventListener ( 'click' , done )
host . querySelector ( '#back ' ) . addEventListener ( 'click' , back )
host . querySelector ( '#next' ) . addEventListener ( 'click' , done )
}
function renderSubStep35 ( host , 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>' +
'<button class="prim aryBtn" id="confirm " h idden>확인 </button>'
'<div class="actionRow">< button class="second aryBtn" id="back">이전</button><button class="primaryBtn " id="next" disabled>다음 </button></div> '
var resultMsg = host . querySelector ( '#resultMsg' )
var confirm Btn = host . querySelector ( '#confirm ' )
var next Btn = host . querySelector ( '#next ' )
if ( state . serverInstall . portStatus ) nextBtn . disabled = false
host . querySelector ( '#back' ) . addEventListener ( 'click' , back )
host . querySelector ( '#run' ) . addEventListener ( 'click' , async function ( ) {
var port = Number ( host . querySelector ( '#port' ) . value ) || 25565
resultMsg . textContent = '확인 중...'
@@ -379,9 +454,9 @@ function renderSubStep35(host, done) {
'<br><small>외부 IP: ' + ( result . externalIp || '확인 불가' ) + ', 포트: ' + result . port + '</small>'
resultMsg . classList . add ( 'warn' )
}
confirm Btn. hidden = false
next Btn. disabled = false
} )
confirm Btn. addEventListener ( 'click' , done )
next Btn. addEventListener ( 'click' , done )
}
function renderStep4 ( ) {
@@ -392,25 +467,16 @@ function renderStep4() {
section . className = 'page'
section . innerHTML =
'<h2>4단계. 유저 클라이언트 설정</h2>' +
'<div class="subStep" id="subHost"></div>' +
'<div class="actionRow"><button class="secondaryBtn" id="back">이전</button><span></span></div>'
'<div class="subStep" id="subHost"></div>'
pageHost . appendChild ( section )
var subHost = section . querySelector ( '#subHost' )
se ction. querySelector ( '#back' ) . addEventList ener( 'click' , function ( ) {
if ( state . mode === 'multi' ) renderStep3 ( ) ; else renderStep2 ( )
} )
fun ction backToPrevStep ( ) { if ( state . mode === 'multi' ) r end erStep3 ( ) ; else renderStep2 ( ) }
function show41 ( ) {
subHost . innerHTML = ''
renderSubStep41 ( subHost , pack , show42 )
}
function show42 ( ) {
subHost . innerHTML = ''
renderSubStep42 ( subHost , show43 )
}
function show41 ( ) { subHost . innerHTML = '' ; renderSubStep41 ( subHost , pack , backToPrevStep , show42 ) }
function show42 ( ) { subHost . innerHTML = '' ; renderSubStep42 ( subHost , show41 , show43 ) }
function show43 ( ) {
subHost . innerHTML = ''
renderSubStep43 ( subHost , function ( ) {
renderSubStep43 ( subHost , show42 , function ( ) {
state . stepDone [ 4 ] = true
renderStep5 ( )
} )
@@ -418,7 +484,7 @@ function renderStep4() {
show41 ( )
}
function renderSubStep41 ( host , pack , done ) {
function renderSubStep41 ( host , pack , back , done ) {
var platformType = pack ? pack . pack . platform . type : 'vanilla'
if ( platformType === 'vanilla' ) {
state . client . installPlatform = false
@@ -426,7 +492,8 @@ function renderSubStep41(host, pack, done) {
'<h3>4-1. 모드 플랫폼</h3>' +
'<p class="formMessage">선택한 음악퀴즈의 플랫폼: <strong>vanilla</strong></p>' +
'<p class="formMessage">바닐라이므로 별도 설치는 필요 없습니다.</p>' +
'<div class="actionRow"><span></spa n><button class="primaryBtn" id="next">다음</button></div>'
'<div class="actionRow"><button class="secondaryBtn" id="back">이전</butto n><button class="primaryBtn" id="next">다음</button></div>'
host . querySelector ( '#back' ) . addEventListener ( 'click' , back )
host . querySelector ( '#next' ) . addEventListener ( 'click' , done )
return
}
@@ -438,7 +505,7 @@ function renderSubStep41(host, pack, done) {
'<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"><span></spa n><button class="primaryBtn" id="next" disabled>다음</button></div>'
'<div class="actionRow"><button class="secondaryBtn" id="back">이전</butto n><button class="primaryBtn" id="next" disabled>다음</button></div>'
var nextBtn = host . querySelector ( '#next' )
var choiceButtons = host . querySelectorAll ( '[data-choice]' )
@@ -462,19 +529,22 @@ function renderSubStep41(host, pack, done) {
applyChoice ( state . client . installPlatform ? 'install' : 'skip' )
}
host . querySelector ( '#back' ) . addEventListener ( 'click' , back )
nextBtn . addEventListener ( 'click' , done )
}
function renderSubStep42 ( host , done ) {
function renderSubStep42 ( host , back , done ) {
host . innerHTML =
'<h3>4-2. 모드/리소스팩 다운로드 및 launcher_profiles 갱신</h3>' +
'<p class="formMessage">%appdata%\\.mc_custom 에 모드와 리소스팩을 설치하고, launcher_profiles.json에 프로필을 등록합니다.</p>' +
'<button class="primaryBtn" id="run">설치 시작</button>' +
'<div class="formMessage" id="msg"></div>' +
'<div class="actionRow"><span></spa n><button class="primaryBtn" id="next" hidden >다음</button></div>'
'<div class="actionRow"><button class="secondaryBtn" id="back">이전</butto n><button class="primaryBtn" id="next" disabled >다음</button></div>'
var runBtn = host . querySelector ( '#run' )
var msg = host . querySelector ( '#msg' )
var nextBtn = host . querySelector ( '#next' )
if ( state . client . clientInstalled ) nextBtn . disabled = false
host . querySelector ( '#back' ) . addEventListener ( 'click' , back )
runBtn . addEventListener ( 'click' , async function ( ) {
runBtn . disabled = true
msg . textContent = '설치 중...'
@@ -486,7 +556,8 @@ function renderSubStep42(host, done) {
} )
msg . textContent = '클라이언트 설치 완료.'
msg . classList . add ( 'success' )
nextBtn . hidden = fals e
state . client . clientInstalled = tru e
nextBtn . disabled = false
} catch ( err ) {
msg . textContent = '설치 실패: ' + err . message
msg . classList . add ( 'error' )
@@ -496,9 +567,13 @@ function renderSubStep42(host, done) {
nextBtn . addEventListener ( 'click' , done )
}
function renderSubStep43 ( host , done ) {
host . innerHTML = '<h3>4-3. 완료 확인</h3><p class="formMessage">모드와 리소스팩이 .mc_custom에 설치되어 있고, launcher_profiles.json도 갱신되었습니다.</p><button class="primaryBtn" id="confirm">5단계로</button>'
host . querySelector ( '#confirm' ) . addEventListener ( 'click' , done )
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 ( ) {