docs/add.md - 사진 PNG 규격을 1024×1024 (4×4 블록 슬롯 × ×16 배율) 로 못박음 - 짧은 변 기준 가운데 정사각 크롭 + 1024 초과 시만 축소, 미만은 native 유지 신규 라우트 (모두 requireAuth): - GET /op/list → manifest 카드 목록 - GET /op/list/:pack → 음악목록·사진목록 탭 편집기 - POST /op/list/:pack → file/list/<pack>.json 저장 (JSON) - POST /op/list/:pack/playlist → yt-dlp 로 플레이리스트 펼치기 - GET /op/datapack → 음악퀴즈 선택 + 출력 - GET /op/datapack/:pack/generate → 임시 포맷 mcfunction 텍스트 shared/types.ts: PackList / MusicListEntry / ImageListEntry shared/store.ts: loadPackList, savePackList, normalizePackList shared/paths.ts: fileListDirPath = file/list/ server/youtube.ts: yt-dlp 플레이리스트 펼치기 (--flat-playlist --dump-json), 설치 안 됐을 때 NO_YTDLP 코드로 503. UI: - views/op/list.ejs: 가로 카드 목록 + 돌아가기 버튼 - views/op/listEditor.ejs + public/listEditor.js: 탭 전환, 드래그 정렬, 우클릭 컨텍스트 메뉴(수정/삭제), 사진 수정 모달의 [유튜브 / 이미지] 토글, 목록 저장·초기화·플레이리스트 불러오기 확인 팝업 - views/op/datapack.ejs: 음악퀴즈 카드 선택 팝업 → 데이터팩 출력 + 복사 - views/op/dashboard.ejs: 상단에 [음악목록 수정] [데이터팩 수정] 버튼 - public/styles.css: 탭, 트랙 로우, 이미지 그리드, 컨텍스트 메뉴, 모달, 코드블록 .gitignore: conversations/ 추가. 스모크: login → /op/list 렌더, POST 저장 라운드트립 OK, /op/datapack/.../generate 텍스트 출력 OK, 플레이리스트 fetch는 yt-dlp 미설치 환경에서 503 NO_YTDLP 메시지 정상. Section 1 (리소스팩 설치기 EXE: yt-dlp 음악 다운로드, painting variant 텍스처 패키징, 리소스팩 zip 배치) 은 후속 커밋에서 작업. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
122 lines
5.0 KiB
Plaintext
122 lines
5.0 KiB
Plaintext
<!doctype html>
|
||
<html lang="ko">
|
||
<head>
|
||
<meta charset="utf-8" />
|
||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||
<title>데이터팩 수정</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">← 돌아가기</a>
|
||
<h1 style="margin-top:8px;">데이터팩 수정</h1>
|
||
</div>
|
||
</section>
|
||
|
||
<section class="dpControls">
|
||
<button type="button" class="primaryButton" id="pickPackBtn">음악퀴즈 선택</button>
|
||
<span class="muted" id="pickedLabel">선택된 음악퀴즈 없음</span>
|
||
</section>
|
||
|
||
<p class="muted" id="countLabel"></p>
|
||
|
||
<section class="dpActions" hidden id="dpActions">
|
||
<button type="button" class="secondaryButton" id="exportBtn">데이터팩 출력</button>
|
||
<button type="button" class="secondaryButton" id="copyBtn">복사</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>음악퀴즈 선택</h3>
|
||
<button class="modalClose" type="button" data-modal-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 %>">
|
||
<h2><%= item.definition ? item.definition.name : item.key %></h2>
|
||
<p class="muted"><%= item.key %>.json</p>
|
||
<% if (item.definition) { %>
|
||
<ul class="metaList">
|
||
<li>MC <%= item.definition.mcVersion %></li>
|
||
<li>플랫폼 <%= item.definition.platform.type %></li>
|
||
</ul>
|
||
<% } %>
|
||
</article>
|
||
<% }) %>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<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
|
||
})
|
||
document.querySelectorAll('#pickList .pickable').forEach(function (card) {
|
||
card.addEventListener('click', function () {
|
||
pickedKey = card.getAttribute('data-key')
|
||
var name = card.getAttribute('data-name')
|
||
document.getElementById('pickedLabel').textContent = '선택: ' + name
|
||
pickModal.hidden = true
|
||
document.getElementById('dpActions').hidden = false
|
||
// 곡 수 미리 가져오기
|
||
fetch('/op/list/' + encodeURIComponent(pickedKey)).catch(function () {})
|
||
// 더 직접적으로: generate 호출 시점에 카운트도 나옴. 일단 비워둠.
|
||
document.getElementById('countLabel').textContent = ''
|
||
document.getElementById('codeOut').hidden = true
|
||
})
|
||
})
|
||
document.getElementById('exportBtn').addEventListener('click', function () {
|
||
if (!pickedKey) return
|
||
var s = document.getElementById('dp-status')
|
||
s.textContent = '출력 중…'; 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 = '실패: ' + res.text; s.classList.add('error')
|
||
return
|
||
}
|
||
var out = document.getElementById('codeOut')
|
||
out.textContent = res.text
|
||
out.hidden = false
|
||
// 첫줄/둘째줄에서 카운트 가져와 표기
|
||
var m = res.text.match(/총\s+(\d+)곡/)
|
||
if (m) document.getElementById('countLabel').textContent = '총 ' + m[1] + '개의 음악을 찾았습니다.'
|
||
s.textContent = '출력 완료'
|
||
})
|
||
.catch(function (err) { s.textContent = '실패: ' + err.message; s.classList.add('error') })
|
||
})
|
||
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 = '복사됨'
|
||
s.classList.remove('error')
|
||
})
|
||
})
|
||
})()
|
||
</script>
|
||
</body>
|
||
</html>
|