From 848fac500e034c03a9a9aca8c5606ae92e456472 Mon Sep 17 00:00:00 2001 From: claude-bot Date: Thu, 14 May 2026 23:22:23 +0900 Subject: [PATCH] op/datapack: add painting_variant JSON zip export button MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 데이터팩 수정 페이지에 "이미지.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 --- locales/server/ko-kr.json | 6 +++++- src/server/routes/op.ts | 43 +++++++++++++++++++++++++++++++++++++++ views/op/datapack.ejs | 18 ++++++++++++++++ 3 files changed, 66 insertions(+), 1 deletion(-) diff --git a/locales/server/ko-kr.json b/locales/server/ko-kr.json index 11ac7d0..3bf8937 100644 --- a/locales/server/ko-kr.json +++ b/locales/server/ko-kr.json @@ -143,7 +143,11 @@ "exporting": "출력 중…", "exported": "출력 완료", "failed": "실패: {{message}}", - "modalPickTitle": "음악퀴즈 선택" + "modalPickTitle": "음악퀴즈 선택", + "imagesZip": "이미지.zip 출력", + "imagesZipSizeLabel": "크기", + "imagesZipDownloading": "이미지.zip 생성 중…", + "imagesZipDone": "이미지.zip 다운로드 완료" }, "errors": { "packNotFound": "해당 음악퀴즈를 찾을 수 없습니다.", diff --git a/src/server/routes/op.ts b/src/server/routes/op.ts index a819850..e576058 100644 --- a/src/server/routes/op.ts +++ b/src/server/routes/op.ts @@ -1,4 +1,5 @@ import { Router } from 'express' +import archiver from 'archiver' import { createPack, deletePackKeys, @@ -252,6 +253,48 @@ opRouter.get('/op/datapack/:packName/generate', requireAuth, async (req, res, ne } }) +// painting_variant JSON 들을 zip 으로 묶어 내려준다. +// query.size 로 width/height (블록 단위, 기본 4, 1~16) 지정. 음악 개수만큼 cover_NN.json 생성. +opRouter.get('/op/datapack/:packName/images-zip', requireAuth, async (req, res, next) => { + try { + const packKey = sanitizePackKey(pickFirstValue(req.params.packName)) + const definition = await loadPackDefinition(packKey) + if (!definition) { + res.status(404).type('text/plain').send(t('errors.packNotFoundJson')) + return + } + const sizeRaw = Number(pickFirstValue(req.query.size)) + const size = Number.isFinite(sizeRaw) && sizeRaw >= 1 && sizeRaw <= 16 + ? Math.floor(sizeRaw) + : 4 + const list = await loadPackList(packKey) + const total = list.music.length + + res.setHeader('Content-Type', 'application/zip') + res.setHeader( + 'Content-Disposition', + `attachment; filename="${packKey}-painting-variants.zip"` + ) + const archive = archiver('zip', { zlib: { level: 9 } }) + archive.on('error', (err) => next(err)) + archive.pipe(res) + for (let i = 1; i <= total; i++) { + const nn = String(i).padStart(2, '0') + const json = { + asset_id: `musicquiz:cover_${nn}`, + width: size, + height: size, + title: { text: `Cover ${nn}` }, + author: { text: 'music quiz' } + } + archive.append(JSON.stringify(json, null, 2) + '\n', { name: `cover_${nn}.json` }) + } + await archive.finalize() + } catch (error) { + next(error) + } +}) + opRouter.post('/op/dashboard/:packName', requireAuth, async (req, res, next) => { try { const packKey = sanitizePackKey(pickFirstValue(req.params.packName)) diff --git a/views/op/datapack.ejs b/views/op/datapack.ejs index 922233a..d714ea4 100644 --- a/views/op/datapack.ejs +++ b/views/op/datapack.ejs @@ -27,6 +27,9 @@