From f810719d925d02aee365e65dadc3446e7da5e3ba Mon Sep 17 00:00:00 2001 From: claude-bot Date: Mon, 18 May 2026 18:34:46 +0900 Subject: [PATCH] installer-rp: site-configured outputPackName for built zip MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds a new "생성되는 리소스팩 이름" admin field saved to the pack manifest and consumed by the rp installer when naming the final zip. Empty value falls back to _musicquiz; Windows-invalid chars are sanitized to '_'. Bumps version 0.1.1 → 0.2.0 (new feature). Co-Authored-By: Claude Opus 4.7 --- locales/server/ko-kr.json | 7 +++++-- package.json | 2 +- src/installer-rp/main.ts | 22 +++++++++++++++++++++- src/installer-rp/types.ts | 6 ++++++ src/server/routes/op.ts | 1 + src/shared/store.ts | 3 +++ src/shared/types.ts | 8 ++++++++ views/op/editor.ejs | 5 +++++ 8 files changed, 50 insertions(+), 4 deletions(-) diff --git a/locales/server/ko-kr.json b/locales/server/ko-kr.json index 3bf8937..8631c72 100644 --- a/locales/server/ko-kr.json +++ b/locales/server/ko-kr.json @@ -124,8 +124,11 @@ "serverPathHint": "/file/servers/ 아래 zip 파일 이름. 멀티 모드 전용.", "modsFolder": "모드 폴더 이름", "modsFolderHint": "/file/mods/<폴더이름>/ 안의 모든 .jar을 자동으로 받습니다. 비워두면 모드를 받지 않습니다.", - "resourcepackPath": "리소스팩 (.zip)", - "resourcepackHint": "/file/resourcepacks/ 아래 .zip 파일 이름. 비워두면 리소스팩을 받지 않습니다.", + "resourcepackPath": "베이스 리소스팩 (.zip)", + "resourcepackHint": "/file/resourcepacks/ 아래 .zip 파일 이름. 리소스팩 설치기가 이 zip 위에 음악·사진을 얹어 최종 리소스팩을 만듭니다. 비워두면 처음부터 새로 만듭니다.", + "outputPackName": "생성되는 리소스팩 이름", + "outputPackNamePlaceholder": "예: 음악퀴즈 테스트팩", + "outputPackNameHint": "리소스팩 설치기가 만들어 내는 zip 파일 이름이자, 마인크래프트 리소스팩 목록의 제목이 됩니다. 비워두면 파일이름_musicquiz 형태로 자동 지정됩니다. Windows 파일명 금지 문자(\\ / : * ? \" < > |)는 자동으로 _ 로 바뀝니다.", "ramOrderInvalid": "클라이언트 최소 램은 권장 램보다 클 수 없습니다.", "fabricLoaderRequired": "Fabric 로더 버전을 선택해 주세요." }, diff --git a/package.json b/package.json index a62d044..7292cb5 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "minecraft-music-quiz-installer", - "version": "0.1.1", + "version": "0.2.0", "description": "마인크래프트 음악퀴즈 간편설치기 + 관리 사이트", "main": "dist/installer/main.js", "scripts": { diff --git a/src/installer-rp/main.ts b/src/installer-rp/main.ts index ad61cbd..ecc493e 100644 --- a/src/installer-rp/main.ts +++ b/src/installer-rp/main.ts @@ -35,6 +35,20 @@ interface RpInstallerState { activeChildren: Set } +/** + * 사용자가 사이트에서 지정한 "생성되는 리소스팩 이름" 을 Windows 파일명으로 쓸 수 + * 있게 정리한다. 금지 문자(\<\>:"/\\|?*\x00-\x1f) 는 `_` 로, 끝의 공백/마침표는 + * 제거, 예약어(CON/PRN/...)는 앞에 `_` 를 붙인다. 빈 입력은 빈 문자열 반환 → + * 호출 측에서 폴백을 결정한다. + */ +function sanitizeOutputPackName(name: string): string { + let cleaned = (name || '').replace(/[<>:"/\\|?*\x00-\x1f]/g, '_') + cleaned = cleaned.replace(/[ .]+$/, '') + if (!cleaned) return '' + if (/^(CON|PRN|AUX|NUL|COM[1-9]|LPT[1-9])$/i.test(cleaned)) cleaned = '_' + cleaned + return cleaned +} + /** * 동시 yt-dlp 프로세스 수를 CPU 코어 수로 자동 결정. * - yt-dlp + ffmpeg 변환이 CPU 바운드라 코어 수가 가장 좋은 프록시. @@ -201,11 +215,13 @@ ipcMain.handle('rp:packs:load', async (_event, manifestUrlInput?: string): Promi const normalized = packRaw ? normalizePackDefinition(packRaw as Partial) : null const mcVersion = normalized?.mcVersion ?? '' const resourcepackPath = normalized?.resourcepackPath ?? '' + const outputPackName = normalized?.outputPackName ?? '' results.push({ key: entry.file, name: entry.name || entry.file, mcVersion, resourcepackPath, + outputPackName, list }) } catch (error) { @@ -383,7 +399,11 @@ ipcMain.handle('rp:install:start', async (): Promise<{ resourcepackPath: string // 2-5. 리소스팩 zip 빌드 (pack.mcmeta + sounds.json + 음악·이미지, 베이스 위에 얹기) throwIfCancelled() - const resourcepackName = `${state.selectedKey}_musicquiz.zip` + // 사이트에서 지정한 "생성되는 리소스팩 이름" 을 우선 사용. 비어있거나 sanitize + // 결과가 빈 문자열이면 `_musicquiz` 로 폴백. + const sanitizedOutputName = sanitizeOutputPackName(pack.outputPackName) + const resourcepackBaseName = sanitizedOutputName || `${state.selectedKey}_musicquiz` + const resourcepackName = `${resourcepackBaseName}.zip` const resourcepackDir = path.join(getMcCustomDir(), 'resourcepacks') const resourcepackPath = path.join(resourcepackDir, resourcepackName) sendLog(t('log.buildingZip', { name: resourcepackName })) diff --git a/src/installer-rp/types.ts b/src/installer-rp/types.ts index c7161cd..07129bf 100644 --- a/src/installer-rp/types.ts +++ b/src/installer-rp/types.ts @@ -10,6 +10,12 @@ export interface RpFetchedPack { * 빈 문자열이면 새 리소스팩을 처음부터 생성. */ resourcepackPath: string + /** + * /manifest/.json 의 outputPackName. 관리 사이트에서 설정한 "생성되는 + * 리소스팩 이름". 비어 있으면 설치기가 `_musicquiz` 형식으로 폴백. + * 파일명으로 쓰기 전에 Windows 금지 문자(\<\>:"/\\|?*) 는 `_` 로 치환. + */ + outputPackName: string /** /file/list/.json 의 음악·사진 목록. */ list: PackList } diff --git a/src/server/routes/op.ts b/src/server/routes/op.ts index e576058..d189619 100644 --- a/src/server/routes/op.ts +++ b/src/server/routes/op.ts @@ -314,6 +314,7 @@ opRouter.post('/op/dashboard/:packName', requireAuth, async (req, res, next) => } as PackDefinition['platform'] & { loaderVersion?: string }, modsFolder: pickFirstValue(req.body.modsFolder), resourcepackPath: pickFirstValue(req.body.resourcepackPath), + outputPackName: pickFirstValue(req.body.outputPackName), serverMinRam: Number(pickFirstValue(req.body.serverMinRam)), serverMaxRam: Number(pickFirstValue(req.body.serverMaxRam)), clientMinRam: Number(pickFirstValue(req.body.clientMinRam)), diff --git a/src/shared/store.ts b/src/shared/store.ts index f75f848..31b46bc 100644 --- a/src/shared/store.ts +++ b/src/shared/store.ts @@ -37,6 +37,7 @@ export function defaultPackDefinition(name: string): PackDefinition { platform: { type: 'vanilla' }, modsFolder: '', resourcepackPath: '', + outputPackName: '', serverMinRam: 2048, serverMaxRam: 4096, clientMinRam: 2048, @@ -95,6 +96,8 @@ export function normalizePackDefinition(input: Partial & Record< }, modsFolder: sanitizeFolderName(input.modsFolder), resourcepackPath: sanitizeZipFileName(input.resourcepackPath), + // 표시명은 사용자 입력을 보존(공백/마침표 trim 만). 파일명 안전 처리는 설치기 측에서. + outputPackName: typeof input.outputPackName === 'string' ? input.outputPackName.trim() : '', serverMinRam: clampNumber(input.serverMinRam, fallback.serverMinRam), serverMaxRam: clampNumber(input.serverMaxRam, fallback.serverMaxRam), clientMinRam: clampNumber(input.clientMinRam, fallback.clientMinRam), diff --git a/src/shared/types.ts b/src/shared/types.ts index 1f707b3..39b6abc 100644 --- a/src/shared/types.ts +++ b/src/shared/types.ts @@ -16,6 +16,14 @@ export interface PackDefinition { modsFolder: string /** /file/resourcepacks/ 의 단일 .zip을 그대로 다운로드. */ resourcepackPath: string + /** + * 리소스팩 설치기가 만들어 내는 최종 zip 파일의 이름(확장자 제외). + * 빈 문자열이면 설치기가 `_musicquiz` 형식으로 기본 이름을 만든다. + * 마인크래프트 리소스팩 목록에서 사용자에게 제목처럼 보이는 값이므로 + * 한글 등 자유 입력을 그대로 보존하고, 파일 시스템에서 사용할 때 금지 문자만 + * `_` 로 치환한다(치환 책임은 설치기 측에 있음). + */ + outputPackName: string serverMinRam: number serverMaxRam: number clientMinRam: number diff --git a/views/op/editor.ejs b/views/op/editor.ejs index c8419c1..f511dfc 100644 --- a/views/op/editor.ejs +++ b/views/op/editor.ejs @@ -98,6 +98,11 @@ <%= t('editor.resourcepackHint') %> +