- src/shared/i18n.ts: 공용 i18n 로더 (dotted-key + {{placeholder}} 보간)
- locales/server/ko-kr.json: 사이트 + 라우터 + 데이터팩 출력 사전
- EJS 뷰는 res.locals.t 미들웨어로 일괄 적용
- listEditor.js 등 클라이언트 JS 는 사전을 inline <script> 로 주입받아 tt() 헬퍼 사용
관리 사이트에서 모드 플랫폼으로 fabric 을 선택하면 jar 파일 업로드 대신, 선택한 마인크래프트 버전을 기준으로 Fabric Meta v2 API 에서 호환 로더 목록을 가져와 드롭다운으로 선택하도록 했다. 설치기는 platform.loaderVersion 만 보고 최신 fabric-installer.jar 를 받아 CLI 로 자동 설치(GUI 미표시)한다.
스키마:
- PackPlatform 에 loaderVersion?: string 추가. fabric 일 때만 사용.
- normalizePackDefinition: fabric 이면 downloadUrl 무시하고 loaderVersion 만 저장, 그 외에는 기존 downloadUrl 유지.
웹 UI(views/op/editor.ejs):
- platformType 이 fabric 일 때 platformLoaderVersion select 노출. mcVersion 셀렉트 값을 가지고 https://meta.fabricmc.net/v2/versions/loader/<mcVersion> 호출.
- mcVersion 또는 platformType 변경 시 자동 재조회. 동시 요청 경쟁은 sequence 비교로 무시.
- 이전 저장값을 우선 선택하되 목록에 없으면 최신 stable 자동 선택.
- 폼 제출 시 fabric 인데 로더 미선택이면 경고.
- 라우트(op.ts): platformLoaderVersion 폼 필드 수신.
설치기(installer/main.ts):
- client:install 분기 추가. fabric 이면 installFabricLoader 호출.
- installFabricLoader: Fabric Meta installer 메타 조회 → 최신 stable installer jar 캐시 다운로드 → java -jar fabric-installer.jar client -mcversion <ver> -loader <ver> -dir <.mc_custom> -noprofile 실행. launcher_profiles 갱신은 우리 코드(updateLauncherProfile)가 담당하므로 -noprofile.
- findJavaExecutable: JAVA_HOME → .minecraft\runtime 의 번들 자바(델타/감마/베타 등 우선순위) → PATH 폴백.
- runJavaProcess: stdout/stderr 를 로그 뷰어에 prefix 와 함께 스트리밍. 실패 시 stderr 끝부분을 메시지에 포함.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Add a red asterisk in the top-right of the dashboard header (and
prefix the document title with *) whenever the dirty flag is set,
so the user can tell at a glance that the music/image list hasn't
been saved yet. The indicator is driven by markDirty/markClean so
it follows the existing tracking.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
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.
- /op/dashboard: remove underline from <a class="secondaryButton"> by adding
text-decoration:none + inline-flex centering on .primaryButton/.secondaryButton/
.dangerButton, plus margin-left:auto on .dashboardActions so the buttons stick
to the right side of the row even when the header is laid out as flex+wrap.
- /op/list/<pack>: fix the duplicate "save/clear + playlist URL" UI showing in
the active tab. .tabPanel had a baseline `display: block;` that overrode the
browser default `[hidden] { display: none }`. Add an explicit
`.tabPanel[hidden] { display: none !important }` rule so the inactive panel
actually hides.
- /op/list, /op/list/<pack>, /op/datapack: bump the gap between the "돌아가기"
ghost link and the page title from 8px to 20px.
- docs/yt-dlp-setup.md: install guide (single-binary curl + pipx + apt) since
the server doesn't have pip/pipx/yt-dlp installed. Server keeps the graceful
"수동 입력으로 진행" fallback when yt-dlp is missing.
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>
- PackDefinition: replace mods[]/resourcepacks[] with modsFolder (string) + resourcepackPath (string); drop PackAsset
- Editor: replace dynamic add/remove lists with two single inputs; remove the now-dead JS for adding/removing rows
- Server: expose GET /file/mods/<folder>/index.json that returns the list of .jar names; folder name restricted to [a-zA-Z0-9_-]+
- Installer: fetch the listing JSON and download each jar from /file/mods/<folder>/<file>.jar; download the single resourcepack from /file/resourcepacks/<file>.zip directly into resourcepacks/
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Path is set by the loader upload form below, so the manual
text input was redundant. Drop the field from the editor UI
and stop overwriting the saved value on form submit so an
uploaded loader keeps its path.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>