데이터팩 수정 페이지에 "이미지.zip 출력" 버튼과 크기 입력(기본 4, 1~16) 을 추가. 누르면 GET /op/datapack/:key/images-zip?size=N 으로 음악 개수만큼 cover_NN.json (asset_id, width=size, height=size, title, author) 을 zip 으로 스트리밍해서 내려준다. 사용자가 맵 데이터팩의 data/musicquiz/painting_variant/ 에 그대로 풀어 넣을 수 있다. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
153 lines
6.6 KiB
Plaintext
153 lines
6.6 KiB
Plaintext
<!doctype html>
|
|
<html lang="ko">
|
|
<head>
|
|
<meta charset="utf-8" />
|
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
<title><%= t('datapack.browserTitle') %></title>
|
|
<link rel="stylesheet" href="/static/styles.css" />
|
|
</head>
|
|
<body class="siteBody">
|
|
<%- include('../partials/navbar', { userId }) %>
|
|
|
|
<main class="pageWrap">
|
|
<section class="dashboardHeader">
|
|
<div>
|
|
<a class="ghostLink" href="/op/dashboard"><%= t('common.back') %></a>
|
|
<h1 style="margin-top:20px;"><%= t('datapack.title') %></h1>
|
|
</div>
|
|
</section>
|
|
|
|
<p class="muted"><%= t('datapack.hint') %></p>
|
|
|
|
<section class="dpControls">
|
|
<button type="button" class="primaryButton" id="pickPackBtn"><%= t('datapack.pickPack') %></button>
|
|
<span class="muted" id="pickedLabel"><%= t('datapack.pickedNone') %></span>
|
|
</section>
|
|
|
|
<p class="muted" id="countLabel"></p>
|
|
|
|
<section class="dpActions" hidden id="dpActions">
|
|
<button type="button" class="secondaryButton" id="imagesZipBtn"><%= t('datapack.imagesZip') %></button>
|
|
<label class="muted" for="imagesZipSize" style="margin-left:4px;"><%= t('datapack.imagesZipSizeLabel') %></label>
|
|
<input type="number" id="imagesZipSize" value="4" min="1" max="16" style="width:60px;" />
|
|
<button type="button" class="secondaryButton" id="exportBtn"><%= t('datapack.export') %></button>
|
|
<button type="button" class="secondaryButton" id="copyBtn"><%= t('datapack.copy') %></button>
|
|
<span class="statusText" id="dp-status"></span>
|
|
</section>
|
|
|
|
<pre class="codeBlock" id="codeOut" hidden></pre>
|
|
</main>
|
|
|
|
<!-- 음악퀴즈 선택 팝업 -->
|
|
<div class="modalOverlay" id="pickModal" hidden>
|
|
<div class="modalCard">
|
|
<header><h3><%= t('datapack.modalPickTitle') %></h3>
|
|
<button class="modalClose" type="button" data-modal-close><%= t('common.close') %></button>
|
|
</header>
|
|
<div class="modalBody">
|
|
<div class="cardRow horizontalScroll" id="pickList">
|
|
<% items.forEach(function (item) { %>
|
|
<article class="packCard pickable"
|
|
data-key="<%= item.key %>"
|
|
data-name="<%= item.definition ? item.definition.name : item.key %>"
|
|
data-music-count="<%= item.musicCount %>">
|
|
<h2><%= item.definition ? item.definition.name : item.key %></h2>
|
|
<p class="muted"><%= item.key %>.json</p>
|
|
<% if (item.definition) { %>
|
|
<ul class="metaList">
|
|
<li><%= t('dashboard.mcShort') %> <%= item.definition.mcVersion %></li>
|
|
<li><%= t('site.platform') %> <%= item.definition.platform.type %></li>
|
|
</ul>
|
|
<% } %>
|
|
</article>
|
|
<% }) %>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<script>
|
|
var I18N = <%- JSON.stringify(localeDict.datapack) %>;
|
|
</script>
|
|
<script>
|
|
(function () {
|
|
var pickModal = document.getElementById('pickModal')
|
|
var pickedKey = ''
|
|
document.getElementById('pickPackBtn').addEventListener('click', function () {
|
|
pickModal.hidden = false
|
|
})
|
|
document.querySelectorAll('[data-modal-close]').forEach(function (b) {
|
|
b.addEventListener('click', function () { pickModal.hidden = true })
|
|
})
|
|
pickModal.addEventListener('click', function (e) {
|
|
if (e.target === pickModal) pickModal.hidden = true
|
|
})
|
|
// ESC 로 닫기.
|
|
document.addEventListener('keydown', function (e) {
|
|
if (e.key === 'Escape' && !pickModal.hidden) {
|
|
pickModal.hidden = true
|
|
e.preventDefault()
|
|
}
|
|
})
|
|
document.querySelectorAll('#pickList .pickable').forEach(function (card) {
|
|
card.addEventListener('click', function () {
|
|
pickedKey = card.getAttribute('data-key')
|
|
var name = card.getAttribute('data-name')
|
|
var count = card.getAttribute('data-music-count') || '0'
|
|
document.getElementById('pickedLabel').textContent = I18N.pickedLabel.replace('{{name}}', name)
|
|
document.getElementById('countLabel').textContent = I18N.totalCount.replace('{{count}}', count)
|
|
pickModal.hidden = true
|
|
document.getElementById('dpActions').hidden = false
|
|
document.getElementById('dp-status').textContent = ''
|
|
document.getElementById('dp-status').classList.remove('error')
|
|
document.getElementById('codeOut').hidden = true
|
|
document.getElementById('codeOut').textContent = ''
|
|
})
|
|
})
|
|
document.getElementById('exportBtn').addEventListener('click', function () {
|
|
if (!pickedKey) return
|
|
var s = document.getElementById('dp-status')
|
|
s.textContent = I18N.exporting; s.classList.remove('error')
|
|
fetch('/op/datapack/' + encodeURIComponent(pickedKey) + '/generate')
|
|
.then(function (r) { return r.text().then(function (t) { return { ok: r.ok, text: t } }) })
|
|
.then(function (res) {
|
|
if (!res.ok) {
|
|
s.textContent = I18N.failed.replace('{{message}}', res.text); s.classList.add('error')
|
|
return
|
|
}
|
|
var out = document.getElementById('codeOut')
|
|
out.textContent = res.text
|
|
out.hidden = false
|
|
s.textContent = I18N.exported
|
|
})
|
|
.catch(function (err) { s.textContent = I18N.failed.replace('{{message}}', err.message); s.classList.add('error') })
|
|
})
|
|
document.getElementById('imagesZipBtn').addEventListener('click', function () {
|
|
if (!pickedKey) return
|
|
var sizeInput = document.getElementById('imagesZipSize')
|
|
var size = parseInt(sizeInput.value, 10)
|
|
if (!isFinite(size) || size < 1) size = 4
|
|
if (size > 16) size = 16
|
|
sizeInput.value = String(size)
|
|
var s = document.getElementById('dp-status')
|
|
s.textContent = I18N.imagesZipDownloading; s.classList.remove('error')
|
|
// 브라우저 기본 다운로드로 위임. 인증 쿠키는 자동으로 따라간다.
|
|
var url = '/op/datapack/' + encodeURIComponent(pickedKey) + '/images-zip?size=' + size
|
|
window.location.href = url
|
|
// 다운로드 시작은 비동기지만, 사용자에게 즉시 피드백.
|
|
setTimeout(function () { s.textContent = I18N.imagesZipDone }, 500)
|
|
})
|
|
document.getElementById('copyBtn').addEventListener('click', function () {
|
|
var out = document.getElementById('codeOut')
|
|
if (out.hidden) return
|
|
navigator.clipboard.writeText(out.textContent).then(function () {
|
|
var s = document.getElementById('dp-status')
|
|
s.textContent = I18N.copied
|
|
s.classList.remove('error')
|
|
})
|
|
})
|
|
})()
|
|
</script>
|
|
</body>
|
|
</html>
|