diff --git a/installer-rp/renderer.js b/installer-rp/renderer.js index 34e0d38..5e789cb 100644 --- a/installer-rp/renderer.js +++ b/installer-rp/renderer.js @@ -189,76 +189,110 @@ function showAgreementError(message) { section.querySelector('#retry').addEventListener('click', renderAgreement) } +// 약관을 한 건씩 페이지로 보여준다. 각 약관마다 "해당 약관에 동의합니다" 체크가 있고, +// 끝까지 스크롤해 읽어야 체크할 수 있으며, [다음] 을 누르면 다음 약관 페이지가 화면 +// 맨 위에서부터 나온다. 마지막 약관까지 동의하면 다음 단계(step2)로 진행한다. function renderAgreementWithKinds(KINDS) { + var idx = 0 + var accepted = {} // kind -> true (뒤로 갔다 와도 동의 상태 유지) + var cache = {} // kind -> 렌더된 HTML + var section = document.createElement('section') section.className = 'page' - section.innerHTML = - '

' + escapeHtml(tt('agreement.heading')) + '

' + - '

' + escapeHtml(tt('agreement.intro')) + '

' + - '
' + - KINDS.map(function (k, i) { - return '' - }).join('') + - '
' + - '
' + escapeHtml(tt('agreement.loading')) + '
' + - '' + - '
' + - '
' + - ' ' + - ' ' + - '
' pageHost.appendChild(section) - var body = section.querySelector('#agBody') - var tabs = section.querySelectorAll('[data-ag]') - var nextBtn = section.querySelector('#next') - var accept = section.querySelector('#agAccept') - var msg = section.querySelector('#agMsg') + function renderCurrent() { + var k = KINDS[idx] + var isLast = idx === KINDS.length - 1 + section.innerHTML = + '

' + escapeHtml(tt('agreement.heading')) + '

' + + '

' + escapeHtml(tt('agreement.stepLabel', { current: idx + 1, total: KINDS.length, label: k.tab })) + '

' + + '
' + escapeHtml(tt('agreement.loading')) + '
' + + '
' + escapeHtml(tt('agreement.readToBottom')) + '
' + + '' + + '
' + + '
' + + ' ' + + ' ' + + '
' - // 본문 캐시. 탭 전환 시 재요청하지 않음. - var cache = {} + var body = section.querySelector('#agBody') + var accept = section.querySelector('#agAccept') + var nextBtn = section.querySelector('#next') + var msg = section.querySelector('#agMsg') + var hint = section.querySelector('#agHint') - function showKind(kind) { - if (cache[kind]) { body.innerHTML = cache[kind]; return } - body.textContent = tt('agreement.loading') - api.getTerm(kind).then(function (res) { - if (!res.ok) { - body.innerHTML = '

' + escapeHtml(tt('agreement.loadFailed', { message: res.message || '' })) + '

' + // 새 약관 페이지는 화면 맨 위에서부터 보이도록 스크롤을 올린다. + if (pageHost) pageHost.scrollTop = 0 + + function markReadable() { + if (!accept.disabled) return + accept.disabled = false + hint.textContent = '' + } + function checkScrolled() { + if (body.scrollHeight - body.scrollTop - body.clientHeight <= 6) markReadable() + } + body.addEventListener('scroll', checkScrolled) + + accept.addEventListener('change', function () { + accepted[k.id] = accept.checked + nextBtn.disabled = !accept.checked + if (accept.checked) { msg.textContent = ''; msg.classList.remove('error') } + }) + + nextBtn.addEventListener('click', function () { + if (!accept.checked) { + msg.textContent = tt('agreement.agreeRequired') + msg.classList.add('error') return } - var html = renderTermsMarkdown(res.content || '') - cache[kind] = html - body.innerHTML = html - }).catch(function (err) { - body.innerHTML = '

' + escapeHtml(tt('agreement.loadFailed', { message: err.message })) + '

' + if (isLast) renderStep2() + else { idx++; renderCurrent() } }) + section.querySelector('#back').addEventListener('click', function () { + if (idx === 0) renderStep1() + else { idx--; renderCurrent() } + }) + + function afterLoad() { + body.scrollTop = 0 + if (accepted[k.id]) { + accept.disabled = false + accept.checked = true + nextBtn.disabled = false + hint.textContent = '' + } + setTimeout(function () { + if (body.scrollHeight - body.clientHeight <= 6) markReadable() + else checkScrolled() + }, 0) + } + + if (cache[k.id]) { + body.innerHTML = cache[k.id] + afterLoad() + } else { + body.textContent = tt('agreement.loading') + api.getTerm(k.id).then(function (res) { + if (!res.ok) { + body.innerHTML = '

' + escapeHtml(tt('agreement.loadFailed', { message: res.message || '' })) + '

' + markReadable() + return + } + var html = renderTermsMarkdown(res.content || '') + cache[k.id] = html + body.innerHTML = html + afterLoad() + }).catch(function (err) { + body.innerHTML = '

' + escapeHtml(tt('agreement.loadFailed', { message: err.message })) + '

' + markReadable() + }) + } } - tabs.forEach(function (b) { - b.addEventListener('click', function () { - tabs.forEach(function (x) { x.classList.remove('active') }) - b.classList.add('active') - showKind(b.getAttribute('data-ag')) - }) - }) - - accept.addEventListener('change', function () { - nextBtn.disabled = !accept.checked - if (accept.checked) msg.textContent = '' - }) - - nextBtn.addEventListener('click', function () { - if (!accept.checked) { - msg.textContent = tt('agreement.agreeRequired') - msg.classList.add('error') - return - } - renderStep2() - }) - section.querySelector('#back').addEventListener('click', renderStep1) - - showKind(KINDS[0].id) + renderCurrent() } // 인스톨러용 미니 markdown 렌더러. 사이트 termsEditor 와 같은 규칙을 처리한다. diff --git a/installer/renderer.js b/installer/renderer.js index d124519..6da0a9a 100644 --- a/installer/renderer.js +++ b/installer/renderer.js @@ -232,76 +232,110 @@ function showAgreementError(message) { section.querySelector('#retry').addEventListener('click', renderAgreement) } +// 약관을 한 건씩 페이지로 보여준다. 각 약관마다 "해당 약관에 동의합니다" 체크가 있고, +// 끝까지 스크롤해 읽어야 체크할 수 있으며, [다음] 을 누르면 다음 약관 페이지가 화면 +// 맨 위에서부터 나온다. 마지막 약관까지 동의하면 다음 단계(step2)로 진행한다. function renderAgreementWithKinds(KINDS) { + var idx = 0 + var accepted = {} // kind -> true (뒤로 갔다 와도 동의 상태 유지) + var cache = {} // kind -> 렌더된 HTML + var section = document.createElement('section') section.className = 'page' - section.innerHTML = - '

' + tt('agreement.heading') + '

' + - '

' + tt('agreement.intro') + '

' + - '
' + - KINDS.map(function (k, i) { - return '' - }).join('') + - '
' + - '
' + tt('agreement.loading') + '
' + - '' + - '
' + - '
' pageHost.appendChild(section) - var body = section.querySelector('#agBody') - var tabs = section.querySelectorAll('[data-ag]') - var nextBtn = section.querySelector('#next') - var accept = section.querySelector('#agAccept') - var msg = section.querySelector('#agMsg') + function renderCurrent() { + var k = KINDS[idx] + var isLast = idx === KINDS.length - 1 + section.innerHTML = + '

' + tt('agreement.heading') + '

' + + '

' + escapeHtml(tt('agreement.stepLabel', { current: idx + 1, total: KINDS.length, label: k.tab })) + '

' + + '
' + tt('agreement.loading') + '
' + + '
' + tt('agreement.readToBottom') + '
' + + '' + + '
' + + '
' - // 약관 본문은 한 번 받으면 캐시. 탭 전환 시 재요청하지 않는다. - var cache = {} + var body = section.querySelector('#agBody') + var accept = section.querySelector('#agAccept') + var nextBtn = section.querySelector('#next') + var msg = section.querySelector('#agMsg') + var hint = section.querySelector('#agHint') - function showKind(kind) { - if (cache[kind]) { - body.innerHTML = cache[kind] - return + // 새 약관 페이지는 화면 맨 위에서부터 보이도록 스크롤을 올린다. + if (pageHost) pageHost.scrollTop = 0 + + function markReadable() { + if (!accept.disabled) return + accept.disabled = false + hint.textContent = '' } - body.textContent = tt('agreement.loading') - installerApi.getTerm(kind).then(function (res) { - if (!res.ok) { - body.innerHTML = '

' + tt('agreement.loadFailed', { message: res.message || '' }) + '

' + function checkScrolled() { + // 본문을 끝까지(하단 근처) 내렸으면 동의 체크를 허용한다. + if (body.scrollHeight - body.scrollTop - body.clientHeight <= 6) markReadable() + } + body.addEventListener('scroll', checkScrolled) + + accept.addEventListener('change', function () { + accepted[k.id] = accept.checked + nextBtn.disabled = !accept.checked + if (accept.checked) { msg.textContent = ''; msg.classList.remove('error') } + }) + + nextBtn.addEventListener('click', function () { + if (!accept.checked) { + msg.textContent = tt('agreement.agreeRequired') + msg.classList.add('error') return } - var html = renderTermsMarkdown(res.content || '') - cache[kind] = html - body.innerHTML = html - }).catch(function (err) { - body.innerHTML = '

' + tt('agreement.loadFailed', { message: err.message }) + '

' + if (isLast) renderStep2() + else { idx++; renderCurrent() } }) + section.querySelector('#back').addEventListener('click', function () { + if (idx === 0) renderStep1() + else { idx--; renderCurrent() } + }) + + function afterLoad() { + body.scrollTop = 0 + // 이미 동의했던 약관으로 되돌아온 경우: 체크/다음 활성화 유지. + if (accepted[k.id]) { + accept.disabled = false + accept.checked = true + nextBtn.disabled = false + hint.textContent = '' + } + // 레이아웃 확정 후: 스크롤이 필요 없을 만큼 짧은 약관이면 바로 동의 허용. + setTimeout(function () { + if (body.scrollHeight - body.clientHeight <= 6) markReadable() + else checkScrolled() + }, 0) + } + + if (cache[k.id]) { + body.innerHTML = cache[k.id] + afterLoad() + } else { + body.textContent = tt('agreement.loading') + installerApi.getTerm(k.id).then(function (res) { + if (!res.ok) { + body.innerHTML = '

' + tt('agreement.loadFailed', { message: res.message || '' }) + '

' + markReadable() + return + } + var html = renderTermsMarkdown(res.content || '') + cache[k.id] = html + body.innerHTML = html + afterLoad() + }).catch(function (err) { + body.innerHTML = '

' + tt('agreement.loadFailed', { message: err.message }) + '

' + markReadable() + }) + } } - tabs.forEach(function (b) { - b.addEventListener('click', function () { - tabs.forEach(function (x) { x.classList.remove('active') }) - b.classList.add('active') - showKind(b.getAttribute('data-ag')) - }) - }) - - accept.addEventListener('change', function () { - nextBtn.disabled = !accept.checked - if (accept.checked) msg.textContent = '' - }) - - nextBtn.addEventListener('click', function () { - if (!accept.checked) { - msg.textContent = tt('agreement.agreeRequired') - msg.classList.add('error') - return - } - renderStep2() - }) - section.querySelector('#back').addEventListener('click', renderStep1) - - showKind(KINDS[0].id) + renderCurrent() } // 인스톨러용 미니 markdown 렌더러. 사이트 termsEditor 와 동일한 규칙을 처리한다. diff --git a/locales/installer-rp/ko-kr.json b/locales/installer-rp/ko-kr.json index 5fe64b0..795d72f 100644 --- a/locales/installer-rp/ko-kr.json +++ b/locales/installer-rp/ko-kr.json @@ -33,7 +33,7 @@ }, "agreement": { "heading": "약관 동의", - "intro": "리소스팩을 설치하기 전에 아래 약관을 모두 확인하고 동의해 주세요.", + "intro": "리소스팩을 설치하기 전에 아래 약관을 한 건씩 끝까지 읽고 동의해 주세요.", "tabResourcepack": "리소스팩 약관", "tabInstaller": "리소스팩 설치기 약관", "loading": "약관을 불러오는 중...", @@ -41,6 +41,9 @@ "listLoadFailed": "약관 표시에 실패하여 설치를 진행할 수 없습니다.\n사유: {{message}}\n네트워크 상태를 확인하고 다시 시도하거나, 처음 단계로 돌아가 주세요.", "retry": "다시 시도", "agreeAll": "위 모든 약관에 동의합니다.", + "agreeThis": "해당 약관에 동의합니다.", + "readToBottom": "약관을 끝까지 내려서 확인하면 동의할 수 있습니다.", + "stepLabel": "약관 {{current}}/{{total}} — {{label}}", "agreeRequired": "약관에 동의해야 다음 단계로 진행할 수 있습니다.", "cancelling": "취소 중…" }, diff --git a/locales/installer/ko-kr.json b/locales/installer/ko-kr.json index 849d0c5..7b14c11 100644 --- a/locales/installer/ko-kr.json +++ b/locales/installer/ko-kr.json @@ -41,7 +41,7 @@ }, "agreement": { "heading": "약관 동의", - "intro": "설치 전에 아래 약관을 모두 확인하고 동의해 주세요.", + "intro": "설치 전에 아래 약관을 한 건씩 끝까지 읽고 동의해 주세요.", "tabMap": "맵 약관", "tabMod": "모드 약관", "tabInstaller": "설치기 약관", @@ -50,6 +50,9 @@ "listLoadFailed": "약관 표시에 실패하여 설치를 진행할 수 없습니다.\n사유: {{message}}\n네트워크 상태를 확인하고 다시 시도하거나, 처음 단계로 돌아가 주세요.", "retry": "다시 시도", "agreeAll": "위 모든 약관에 동의합니다.", + "agreeThis": "해당 약관에 동의합니다.", + "readToBottom": "약관을 끝까지 내려서 확인하면 동의할 수 있습니다.", + "stepLabel": "약관 {{current}}/{{total}} — {{label}}", "agreeRequired": "약관에 동의해야 다음 단계로 진행할 수 있습니다." }, "step1": { diff --git a/package.json b/package.json index 9b526d9..cc30d96 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "minecraft-music-quiz-installer", - "version": "0.4.0", + "version": "0.4.1", "description": "마인크래프트 음악퀴즈 간편설치기 + 관리 사이트", "main": "dist/installer/main.js", "scripts": {