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>
99 lines
3.7 KiB
Plaintext
99 lines
3.7 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">
|
|
<h1>음악퀴즈 목록</h1>
|
|
<div class="dashboardActions">
|
|
<a class="secondaryButton" href="/op/list">음악목록 수정</a>
|
|
<a class="secondaryButton" href="/op/datapack">데이터팩 수정</a>
|
|
<form method="post" action="/op/dashboard/create" class="inlineForm">
|
|
<button type="submit" class="primaryButton">음악퀴즈 추가</button>
|
|
</form>
|
|
<button type="button" class="secondaryButton" id="deleteToggle">음악퀴즈 삭제</button>
|
|
</div>
|
|
</section>
|
|
|
|
<form method="post" action="/op/dashboard/delete" id="deleteForm" class="dashboardListForm">
|
|
<section class="cardRow horizontalScroll">
|
|
<% if (items.length === 0) { %>
|
|
<p class="muted">등록된 음악퀴즈가 없습니다. "음악퀴즈 추가" 버튼으로 새로 만들어 보세요.</p>
|
|
<% } %>
|
|
<% items.forEach(function (item) { %>
|
|
<article class="packCard editableCard" data-key="<%= item.key %>">
|
|
<label class="cardCheckbox" hidden>
|
|
<input type="checkbox" name="targetKey" value="<%= item.key %>" />
|
|
<span>선택</span>
|
|
</label>
|
|
<a class="cardLink" href="/op/dashboard/<%= 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>
|
|
<li>모드 폴더 <%= item.definition.modsFolder || '없음' %></li>
|
|
</ul>
|
|
<% } %>
|
|
</a>
|
|
</article>
|
|
<% }) %>
|
|
</section>
|
|
<div class="deleteConfirmRow" id="deleteConfirm" hidden>
|
|
<button type="button" class="secondaryButton" id="deleteCancel">취소</button>
|
|
<button type="submit" class="dangerButton">삭제 확인</button>
|
|
</div>
|
|
</form>
|
|
</main>
|
|
|
|
<script>
|
|
(function () {
|
|
var toggleButton = document.getElementById('deleteToggle')
|
|
var confirmRow = document.getElementById('deleteConfirm')
|
|
var cancelButton = document.getElementById('deleteCancel')
|
|
var checkboxLabels = document.querySelectorAll('.cardCheckbox')
|
|
var cardLinks = document.querySelectorAll('.cardLink')
|
|
|
|
function setSelectMode(active) {
|
|
confirmRow.hidden = !active
|
|
checkboxLabels.forEach(function (label) {
|
|
if (active) label.removeAttribute('hidden')
|
|
else label.setAttribute('hidden', '')
|
|
})
|
|
cardLinks.forEach(function (link) {
|
|
if (active) {
|
|
link.setAttribute('data-disabled', 'true')
|
|
link.addEventListener('click', preventNavigation)
|
|
} else {
|
|
link.removeAttribute('data-disabled')
|
|
link.removeEventListener('click', preventNavigation)
|
|
}
|
|
})
|
|
}
|
|
|
|
function preventNavigation(event) {
|
|
event.preventDefault()
|
|
}
|
|
|
|
toggleButton.addEventListener('click', function () {
|
|
setSelectMode(true)
|
|
})
|
|
cancelButton.addEventListener('click', function () {
|
|
setSelectMode(false)
|
|
document.querySelectorAll('input[name="targetKey"]').forEach(function (input) {
|
|
input.checked = false
|
|
})
|
|
})
|
|
})()
|
|
</script>
|
|
</body>
|
|
</html>
|