Files
minecraft_launcher/views/op/datapack.ejs
claude-bot a2817c921d Add /op/list, /op/list/:pack, /op/datapack web admin + spec lock
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>
2026-05-11 11:38:30 +09:00

122 lines
5.0 KiB
Plaintext
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!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>