- src/shared/i18n.ts: 공용 i18n 로더 (dotted-key + {{placeholder}} 보간)
- locales/server/ko-kr.json: 사이트 + 라우터 + 데이터팩 출력 사전
- EJS 뷰는 res.locals.t 미들웨어로 일괄 적용
- listEditor.js 등 클라이언트 JS 는 사전을 inline <script> 로 주입받아 tt() 헬퍼 사용
VS Code surfaced the TS deprecation:
'moduleResolution=node10' is deprecated and won't work in TS 7.0.
Fix: switch the root tsconfig.json from
module: CommonJS, moduleResolution: node
to
module: Node16, moduleResolution: Node16
TypeScript 7 only supports node16/nodenext/bundler. node16 matches the
runtime semantics we already use (Node ≥ 16, CommonJS output via the
absence of "type": "module" in package.json), so the emit is unchanged.
Side effect of Node16 resolution: relative imports must carry the .js
extension. Added .js to every relative import across src/* (17 sites,
8 files). Bare module specifiers (express, electron, node:fs, ...) are
unaffected.
Verified:
- tsc -p tsconfig.server.json — 0 errors
- tsc -p tsconfig.installer.json — 0 errors
- node dist/server/app.js boots; /op login → 302, /op/list → 200
Music list tab:
- Title + artist are now contenteditable in-place. Typing updates state on
every input event; Enter blurs (no embedded newlines), and on Save the
current DOM text is re-synced into state.music[i].title/.artist before
the JSON POST so a quick-save right after typing doesn't drop the last
keystroke. While focused, the field is highlighted with the accent
outline and unclamped so long titles wrap. Empty cells show a muted
placeholder via :empty::before { content: attr(data-placeholder) }.
- Edit modal "save" now POSTs the new URL to /op/list/<pack>/video-meta
(new endpoint backed by yt-dlp --dump-json --no-playlist) and patches
state.music[i] with the returned title/channel/durationSec. If yt-dlp
is unavailable or the lookup fails, the modal asks the user whether to
apply just the URL change without metadata.
- Drag-and-drop UX: replaced the simple "highlight target" feedback with a
"gap opens above the target" animation. The hovered row gets a
.dropAbove class that animates margin-top to 64px via CSS transition,
visually carving out the slot the dragged item will land in. Insertion
math is now strictly "drop before the hover target" (with the
srcIdx<dstIdx → dstIdx-1 adjustment after the splice removal), matching
what the gap implies. Contenteditable spans no longer hijack drag start
— on focus the parent <li>.draggable is flipped off so the user can
freely select text, and back on at blur.
Image list tab:
- New "음악목록에서 가져오기" button. Copies state.music[*].url into
state.images, which (via the existing thumbUrl() helper) renders each
song's YouTube thumbnail as the image. Behind a confirm prompt because
it replaces the entire image list.
- Same drag gap animation (.dropAbove → margin-left 80px) applied to the
grid cards for consistency.
Server:
- youtube.ts: add fetchVideoMeta(url) using the same ensureYtDlp() path
(auto-installed binary under %appdata%/.mc_custom). Returns one
YtPlaylistEntry or null.
- routes/op.ts: POST /op/list/:packName/video-meta. 400 on missing URL,
503 NO_YTDLP if the auto-install failed, 500 on other yt-dlp errors,
200 { ok: true, entry } otherwise.
Smoke test (Rick Astley URL) returns
title=Rick Astley - Never Gonna..., channel=Rick Astley, durationSec=213.
The previous flow required the operator to manually install yt-dlp on the
server. Now the server downloads the correct binary for the current OS/arch
from GitHub Releases (latest) the first time a playlist fetch is requested,
caches it under %appdata%/.mc_custom (resolved per-OS), chmod +x's it on
POSIX, and skips the download on every subsequent call.
- shared/paths.ts: add getAppDataDir() + getMcCustomDir() that map %appdata%
to ~/.config (Linux), ~/Library/Application Support (macOS), or
process.env.APPDATA (Windows) so the path is OS-agnostic.
- server/youtube.ts: replace the old "probe PATH and bail" probeYtDlp with
ensureYtDlp(): picks the right GitHub asset name per platform.arch
(yt-dlp.exe / yt-dlp_macos / yt-dlp_linux / yt-dlp_linux_aarch64 /
yt-dlp_linux_armv7l), downloads it with redirect-following https.get to
the install path, chmods +x, then verifies with `--version` before
reporting success. Uses an installPromise singleton so concurrent
requests don't race the download. On any failure it cleans up the
partial file and throws YtDlpUnavailableError with a real reason.
- docs/yt-dlp-setup.md: note that auto-install is now the default; manual
install is only for environments where the auto-download fails.
Smoke-tested on this Linux x64 box: first call downloads to
~/.config/.mc_custom/yt-dlp_linux and reports `2026.03.17`; second call
takes ~700ms (just the version probe) and reuses the cached file.
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>