Files
minecraft_launcher/views/op/editor.ejs
claude-bot e5dcc47548 feat: 최종 리소스팩(선택 설치) + 약관 표시대상 추가
- pack.finalResourcepackPath 추가(/file/resourcepacks/outputs/, "."=없음, 기본 ".").
  편집기에 "최종 리소스팩 (.zip)" 입력 필드 추가.
- 약관 표시대상에 showInFinalResourcepack("최종 리소스팩에 표시") 3번째 토글 추가
  (스키마/시드/마이그레이션/편집기 체크박스·배지·저장 라우트, index.json 노출).
- 간편설치기 완료 단계: 기존 "직접 적용" 경고 제거 → 최종 리소스팩이 "."이 아니면
  "리소스팩을 설치하시겠습니까?" 예/아니요. 아니요→완료, 예→최종 리소스팩 약관
  (showInFinalResourcepack) 동의→/file/resourcepacks/outputs/ 에서 다운로드→완료.
  "." 이면 이 페이지 자체를 건너뜀.
- 0.4.15→0.4.16.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-07-23 00:33:07 +09:00

328 lines
15 KiB
Plaintext

<!doctype html>
<html lang="ko">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title><%= t('editor.browserTitle', { name: pack.name }) %></title>
<link rel="stylesheet" href="/static/styles.css" />
</head>
<body class="siteBody">
<%- include('../partials/navbar', { userId }) %>
<main class="pageWrap">
<section class="editorHeader">
<div>
<p class="eyebrow"><%= t('editor.eyebrow') %></p>
<h1><%= pack.name %></h1>
</div>
<a class="ghostLink" href="/op/dashboard"><%= t('common.backToList') %></a>
</section>
<form method="post" class="editorForm" id="editorForm">
<div class="gridTwo">
<label>
<span><%= t('editor.displayName') %></span>
<input name="displayName" value="<%= pack.name %>" required />
</label>
<label>
<span><%= t('editor.fileName') %></span>
<input name="fileName" value="<%= packKey %>" required pattern="[a-zA-Z0-9_\-]+" />
</label>
</div>
<label style="display:flex;align-items:center;gap:10px;margin:8px 0;">
<input type="checkbox" name="isPublic" <%= (typeof isPublic === 'undefined' || isPublic) ? 'checked' : '' %> style="width:auto;" />
<span>공개 (체크 시 일반 설치기에 표시 / 해제 시 개발자용 설치기에만 표시)</span>
</label>
<div class="gridTwo">
<label>
<span><%= t('editor.mcVersion') %></span>
<select name="mcVersion" required>
<% releases.forEach(function (release) { %>
<option value="<%= release %>" <%= release === pack.mcVersion ? 'selected' : '' %>><%= release %></option>
<% }) %>
</select>
</label>
<label>
<span><%= t('editor.platformType') %></span>
<select name="platformType" id="platformType">
<% ['vanilla','forge','fabric','neoforge'].forEach(function (loader) { %>
<option value="<%= loader %>" <%= pack.platform.type === loader ? 'selected' : '' %>><%= loader %></option>
<% }) %>
</select>
</label>
<label class="fullSpan" id="platformDownloadField"<%= pack.platform.type === 'vanilla' ? ' hidden' : '' %>>
<span><%= t('editor.platformDownloadUrl') %></span>
<input name="platformDownloadUrl" value="<%= pack.platform.downloadUrl || '' %>" placeholder="/forge-installer.jar 또는 https://example.com/forge-installer.jar" />
<small class="muted"><%- t('editor.platformDownloadHint') %></small>
</label>
<label class="fullSpan" id="platformLoaderField"<%= pack.platform.type === 'fabric' ? '' : ' hidden' %>>
<span><%= t('editor.platformLoaderVersion') %></span>
<select name="platformLoaderVersion" id="platformLoaderVersion" data-current="<%= pack.platform.loaderVersion || '' %>">
<option value=""><%= t('common.loading') %></option>
</select>
<small class="muted"><%= t('editor.platformLoaderHint') %></small>
</label>
<label>
<span><%= t('editor.serverMinRam') %></span>
<input type="number" name="serverMinRam" value="<%= pack.serverMinRam %>" min="512" required />
</label>
<label>
<span><%= t('editor.serverMaxRam') %></span>
<input type="number" name="serverMaxRam" value="<%= pack.serverMaxRam %>" min="512" required />
</label>
<label>
<span><%= t('editor.clientMinRam') %></span>
<input type="number" name="clientMinRam" value="<%= pack.clientMinRam %>" min="512" required />
</label>
<label>
<span><%= t('editor.clientRecommendedRam') %></span>
<input type="number" name="clientRecommendedRam" value="<%= pack.clientRecommendedRam %>" min="512" required />
</label>
<label>
<span><%= t('editor.recommendedJdk') %></span>
<select name="recommendedJdk" id="recommendedJdk" data-current="<%= pack.recommendedJdk %>" required>
<option value="<%= pack.recommendedJdk %>" selected>Java <%= pack.recommendedJdk %></option>
</select>
<label class="muted" style="font-weight:normal;display:flex;align-items:center;gap:6px;margin-top:4px;">
<input type="checkbox" id="jdkShowDetails" style="width:auto;" /> <%= t('editor.jdkShowDetails') %>
</label>
<div id="jdkDetails" class="muted" hidden style="margin-top:4px;font-size:12px;line-height:1.6;word-break:break-all;"></div>
<small class="muted"><%= t('editor.recommendedJdkHint') %></small>
</label>
<label>
<span><%= t('editor.mapPath') %></span>
<input name="mapPath" value="<%= pack.mapPath %>" placeholder="my-map.zip" pattern=".+\.zip" />
<small class="muted"><%= t('editor.mapPathHint') %></small>
</label>
<label>
<span><%= t('editor.serverPath') %></span>
<input name="serverPath" value="<%= pack.serverPath %>" placeholder="my-server.zip" pattern=".+\.zip" />
<small class="muted"><%= t('editor.serverPathHint') %></small>
</label>
</div>
<div class="gridTwo">
<label>
<span><%= t('editor.modsFolder') %></span>
<input name="modsFolder" value="<%= pack.modsFolder %>" placeholder="my-pack" pattern="[a-zA-Z0-9_\-]*" />
<small class="muted"><%- t('editor.modsFolderHint') %></small>
</label>
<label>
<span><%= t('editor.resourcepackPath') %></span>
<input name="resourcepackPath" value="<%= pack.resourcepackPath %>" placeholder="my-pack.zip" pattern=".*\.zip|" />
<small class="muted"><%= t('editor.resourcepackHint') %></small>
</label>
<label class="fullSpan">
<span><%= t('editor.outputPackName') %></span>
<input name="outputPackName" value="<%= pack.outputPackName %>" placeholder="<%= t('editor.outputPackNamePlaceholder') %>" />
<small class="muted"><%= t('editor.outputPackNameHint') %></small>
</label>
<label class="fullSpan">
<span><%= t('editor.finalResourcepackPath') %></span>
<input name="finalResourcepackPath" value="<%= pack.finalResourcepackPath %>" placeholder="my-pack.zip ( . = 없음 )" />
<small class="muted"><%= t('editor.finalResourcepackHint') %></small>
</label>
</div>
<button class="primaryButton" type="submit"><%= t('common.save') %></button>
</form>
</main>
<script>
var I18N = {
ramOrderInvalid: <%- JSON.stringify(t('editor.ramOrderInvalid')) %>,
fabricLoaderRequired: <%- JSON.stringify(t('editor.fabricLoaderRequired')) %>,
loaderEmpty: <%- JSON.stringify(t('editor.platformLoaderEmpty')) %>,
loaderPickMc: <%- JSON.stringify(t('editor.platformLoaderPickMc')) %>,
loaderLoadFailedPrefix: <%- JSON.stringify(t('editor.platformLoaderLoadFailed', { message: '__M__' })) %>,
loading: <%- JSON.stringify(t('common.loading')) %>
}
function formatLoaderLoadFailed(message) {
return I18N.loaderLoadFailedPrefix.replace('__M__', message)
}
var JDK = {
current: <%= pack.recommendedJdk %>,
fallback: <%- JSON.stringify(jdkOptions) %>,
detailsGa: <%- JSON.stringify(t('editor.jdkDetailsGa')) %>,
detailsEa: <%- JSON.stringify(t('editor.jdkDetailsEa')) %>,
detailsLoading: <%- JSON.stringify(t('common.loading')) %>,
detailsNone: <%- JSON.stringify(t('editor.jdkDetailsNone')) %>,
ltsSuffix: <%- JSON.stringify(t('editor.jdkLtsSuffix')) %>
}
</script>
<script>
(function () {
var sel = document.getElementById('recommendedJdk')
var details = document.getElementById('jdkDetails')
var toggle = document.getElementById('jdkShowDetails')
if (!sel || !toggle) return
var current = Number(sel.getAttribute('data-current')) || JDK.current
var avail = null // { available:[], lts:[] }
function rebuild() {
var availList, ltsList
if (avail) { availList = avail.available; ltsList = avail.lts }
else { availList = JDK.fallback.slice().sort(function (a, b) { return b - a }); ltsList = availList }
var ltsSet = {}
ltsList.forEach(function (m) { ltsSet[m] = true })
var list = toggle.checked ? availList.slice() : ltsList.slice()
if (list.indexOf(current) < 0) list = [current].concat(list)
var seen = {}, html = ''
list.forEach(function (m) {
if (seen[m]) return
seen[m] = true
var label = 'Java ' + m + (ltsSet[m] ? JDK.ltsSuffix : '')
html += '<option value="' + m + '"' + (m === current ? ' selected' : '') + '>' + label + '</option>'
})
sel.innerHTML = html
sel.value = String(current)
}
function escHtml(s) {
return String(s).replace(/[&<>"']/g, function (c) {
return c === '&' ? '&amp;' : c === '<' ? '&lt;' : c === '>' ? '&gt;' : c === '"' ? '&quot;' : '&#39;'
})
}
function detailRow(label, names) {
return '<div><strong>' + escHtml(label) + ':</strong> ' +
names.slice(0, 6).map(escHtml).join(', ') + '</div>'
}
function loadDetails(major) {
details.hidden = false
details.textContent = JDK.detailsLoading
fetch('/op/jdk-versions/' + major)
.then(function (r) { if (!r.ok) throw new Error('HTTP ' + r.status); return r.json() })
.then(function (d) {
// release_name 은 외부 API 값이라 반드시 escape 후 삽입(관리 페이지 XSS 방지).
var html = ''
if (d.ga && d.ga.length) html += detailRow(JDK.detailsGa, d.ga)
if (d.ea && d.ea.length) html += detailRow(JDK.detailsEa, d.ea)
details.innerHTML = html || escHtml(JDK.detailsNone)
})
.catch(function () { details.textContent = JDK.detailsNone })
}
sel.addEventListener('change', function () {
current = Number(sel.value)
if (toggle.checked) loadDetails(current)
})
toggle.addEventListener('change', function () {
rebuild()
if (toggle.checked) loadDetails(current)
else { details.hidden = true; details.innerHTML = '' }
})
fetch('/op/jdk-versions')
.then(function (r) { if (!r.ok) throw new Error('HTTP ' + r.status); return r.json() })
.then(function (d) { avail = d; rebuild() })
.catch(function () { rebuild() })
})()
</script>
<script>
(function () {
var platformSelect = document.getElementById('platformType')
var mcVersionSelect = document.querySelector('select[name="mcVersion"]')
var downloadField = document.getElementById('platformDownloadField')
var loaderField = document.getElementById('platformLoaderField')
var loaderSelect = document.getElementById('platformLoaderVersion')
var currentLoader = loaderSelect.getAttribute('data-current') || ''
var loaderCache = {} // mcVersion -> [loader versions]
var loaderFetchSeq = 0
function syncPlatformVisibility() {
var type = platformSelect.value
if (type === 'fabric') {
downloadField.removeAttribute('hidden')
loaderField.removeAttribute('hidden')
loadFabricLoaders()
} else if (type === 'vanilla') {
downloadField.setAttribute('hidden', '')
loaderField.setAttribute('hidden', '')
downloadField.querySelector('input').value = ''
loaderSelect.innerHTML = '<option value=""></option>'
} else {
downloadField.removeAttribute('hidden')
loaderField.setAttribute('hidden', '')
loaderSelect.innerHTML = '<option value=""></option>'
}
}
function populateLoaderOptions(versions, preselect) {
if (!versions || versions.length === 0) {
loaderSelect.innerHTML = '<option value="">' + I18N.loaderEmpty + '</option>'
return
}
var html = ''
for (var i = 0; i < versions.length; i++) {
var v = versions[i]
var sel = v.version === preselect ? ' selected' : ''
var label = v.version + (v.stable ? '' : ' (beta)')
html += '<option value="' + v.version + '"' + sel + '>' + label + '</option>'
}
loaderSelect.innerHTML = html
// 사용자가 저장해둔 값이 목록에 없으면 첫 번째(최신) 자동 선택.
if (preselect && !versions.some(function (v) { return v.version === preselect })) {
loaderSelect.value = versions[0].version
}
}
function loadFabricLoaders() {
var mc = (mcVersionSelect && mcVersionSelect.value) || ''
if (!mc) {
loaderSelect.innerHTML = '<option value="">' + I18N.loaderPickMc + '</option>'
return
}
if (loaderCache[mc]) {
populateLoaderOptions(loaderCache[mc], currentLoader)
return
}
var seq = ++loaderFetchSeq
loaderSelect.innerHTML = '<option value="">' + I18N.loading + '</option>'
fetch('https://meta.fabricmc.net/v2/versions/loader/' + encodeURIComponent(mc))
.then(function (res) {
if (!res.ok) throw new Error('HTTP ' + res.status)
return res.json()
})
.then(function (list) {
if (seq !== loaderFetchSeq) return // 더 새로운 요청이 들어왔으면 무시
// 응답: [{ loader: { version, stable, ... }, intermediary: {...} }, ...]
var versions = (list || []).map(function (item) {
return { version: item.loader.version, stable: !!item.loader.stable }
})
loaderCache[mc] = versions
populateLoaderOptions(versions, currentLoader)
})
.catch(function (err) {
if (seq !== loaderFetchSeq) return
var msg = (err && err.message) ? err.message : String(err)
loaderSelect.innerHTML = '<option value="">' + formatLoaderLoadFailed(msg) + '</option>'
})
}
platformSelect.addEventListener('change', syncPlatformVisibility)
if (mcVersionSelect) mcVersionSelect.addEventListener('change', function () {
if (platformSelect.value === 'fabric') loadFabricLoaders()
})
syncPlatformVisibility()
var form = document.getElementById('editorForm')
form.addEventListener('submit', function (event) {
var clientMin = Number(form.clientMinRam.value)
var clientReco = Number(form.clientRecommendedRam.value)
if (clientMin > clientReco) {
event.preventDefault()
alert(I18N.ramOrderInvalid)
return
}
if (platformSelect.value === 'fabric' && !loaderSelect.value) {
event.preventDefault()
alert(I18N.fabricLoaderRequired)
}
})
})()
</script>
</body>
</html>