diff --git a/electron-builder-dev.yml b/electron-builder-dev.yml new file mode 100644 index 0000000..844c159 --- /dev/null +++ b/electron-builder-dev.yml @@ -0,0 +1,35 @@ +appId: kr.tkrmagid.musicquiz.installer.dev +productName: MusicQuizInstaller +# 개발자용 빌드: manifest 의 public===false 인 pack 만 노출한다. +# musicQuizAudience 를 package.json 에 박아 런타임에서 대상(audience)을 판별. +extraMetadata: + main: dist/installer/main.js + musicQuizAudience: developer +directories: + output: release + buildResources: build +files: + - dist/installer/** + - dist/shared/** + - installer/** + - build/icon.* + - package.json + - "!node_modules/@img/sharp-linux-*" + - "!node_modules/@img/sharp-linuxmusl-*" + - "!node_modules/@img/sharp-libvips-linux-*" + - "!node_modules/@img/sharp-libvips-linuxmusl-*" +extraResources: + - from: . + to: . + filter: + - .env.build + - from: locales + to: locales + filter: + - "**/*" +win: + target: portable + artifactName: MusicQuizInstaller-Dev-${version}-Portable.${ext} + icon: build/icon.ico +portable: + artifactName: MusicQuizInstaller-Dev-${version}-Portable.${ext} diff --git a/electron-builder-rp-dev.yml b/electron-builder-rp-dev.yml new file mode 100644 index 0000000..f39fefd --- /dev/null +++ b/electron-builder-rp-dev.yml @@ -0,0 +1,35 @@ +appId: kr.tkrmagid.musicquiz.installer-rp.dev +productName: MusicQuizResourcepackInstaller +# 개발자용 리소스팩 설치기: manifest 의 public===false 인 pack 만 노출. +extraMetadata: + main: dist/installer-rp/main.js + musicQuizAudience: developer +directories: + output: release + buildResources: build +files: + - dist/installer-rp/** + - dist/shared/** + - installer-rp/** + - installer/styles.css + - build/icon.* + - package.json + - "!node_modules/@img/sharp-linux-*" + - "!node_modules/@img/sharp-linuxmusl-*" + - "!node_modules/@img/sharp-libvips-linux-*" + - "!node_modules/@img/sharp-libvips-linuxmusl-*" +extraResources: + - from: . + to: . + filter: + - .env.build + - from: locales + to: locales + filter: + - "**/*" +win: + target: portable + artifactName: MusicQuizResourcepackInstaller-Dev-${version}-Portable.${ext} + icon: build/icon.ico +portable: + artifactName: MusicQuizResourcepackInstaller-Dev-${version}-Portable.${ext} diff --git a/package.json b/package.json index ee10330..50feef8 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "minecraft-music-quiz-installer", - "version": "0.3.19", + "version": "0.3.20", "description": "마인크래프트 음악퀴즈 간편설치기 + 관리 사이트", "main": "dist/installer/main.js", "scripts": { @@ -14,7 +14,9 @@ "build:launcher-icon": "node scripts/build-launcher-icon.cjs", "dist:win": "npm run preinstall:sharp-win32 && npm run build:launcher-icon && tsc -p tsconfig.installer.json && electron-builder --win --config electron-builder.yml", "dist:win:rp": "npm run preinstall:sharp-win32 && tsc -p tsconfig.installer-rp.json && electron-builder --win --config electron-builder-rp.yml", - "dist:win:pf": "tsc -p tsconfig.installer-pf.json && electron-builder --win --config electron-builder-pf.yml" + "dist:win:pf": "tsc -p tsconfig.installer-pf.json && electron-builder --win --config electron-builder-pf.yml", + "dist:win:dev": "npm run preinstall:sharp-win32 && npm run build:launcher-icon && tsc -p tsconfig.installer.json && electron-builder --win --config electron-builder-dev.yml", + "dist:win:rp:dev": "npm run preinstall:sharp-win32 && tsc -p tsconfig.installer-rp.json && electron-builder --win --config electron-builder-rp-dev.yml" }, "dependencies": { "@types/archiver": "^7.0.0", diff --git a/src/installer-rp/main.ts b/src/installer-rp/main.ts index ec90f55..e307bed 100644 --- a/src/installer-rp/main.ts +++ b/src/installer-rp/main.ts @@ -12,6 +12,7 @@ import { normalizePackDefinition } from '../shared/store.js' import { getAppDataDir, getMcCustomDir } from '../shared/paths.js' import { loadEnv, getManifestUrl } from '../shared/env.js' import { loadComponentI18n } from '../shared/i18n.js' +import { resolveAudience, isPackVisibleForAudience, type Audience } from '../shared/audience.js' import type { RpFetchedPack } from './types.js' import { ensureYtDlpExe } from './ytdlp.js' import { ensureFfmpegExe } from './ffmpeg.js' @@ -112,6 +113,16 @@ const state: RpInstallerState = { let mainWindow: BrowserWindow | null = null +// 이 빌드의 대상(일반/개발자용). package.json 의 musicQuizAudience 로 결정. +function getAudience(): Audience { + try { + const pkg = JSON.parse(fs.readFileSync(path.join(app.getAppPath(), 'package.json'), 'utf8')) + return resolveAudience(pkg.musicQuizAudience) + } catch { + return resolveAudience(undefined) + } +} + function deriveBaseUrl(manifestUrl: string): string { try { const parsed = new URL(manifestUrl) @@ -202,9 +213,12 @@ ipcMain.handle('rp:packs:load', async (_event, manifestUrlInput?: string): Promi } sendLog(t('log.manifestDownload', { url: state.manifestUrl })) const manifest = await fetchJson(state.manifestUrl) + const audience = getAudience() const results: RpFetchedPack[] = [] for (const entry of manifest.packs ?? []) { if (typeof entry?.file !== 'string') continue + // 대상(audience)에 맞지 않는 pack 은 건너뛴다(일반=public, 개발자용=non-public). + if (!isPackVisibleForAudience(entry.public, audience)) continue const listUrl = `${state.baseUrl}/file/list/${encodeURIComponent(entry.file)}.json` const packUrl = `${state.baseUrl}/manifest/${encodeURIComponent(entry.file)}.json` try { diff --git a/src/installer/main.ts b/src/installer/main.ts index bb5c695..bcc1c9b 100644 --- a/src/installer/main.ts +++ b/src/installer/main.ts @@ -22,10 +22,21 @@ import type { Manifest, PackDefinition } from '../shared/types.js' import { normalizePackDefinition } from '../shared/store.js' import { loadEnv, getManifestUrl } from '../shared/env.js' import { loadComponentI18n } from '../shared/i18n.js' +import { resolveAudience, isPackVisibleForAudience, type Audience } from '../shared/audience.js' import { LAUNCHER_PROFILE_ICON } from './launcherIcon.js' loadEnv() +// 이 빌드의 대상(일반/개발자용). package.json 의 musicQuizAudience 로 결정. +function getAudience(): Audience { + try { + const pkg = JSON.parse(fs.readFileSync(path.join(app.getAppPath(), 'package.json'), 'utf8')) + return resolveAudience(pkg.musicQuizAudience) + } catch { + return resolveAudience(undefined) + } +} + const i18n = loadComponentI18n('installer') const t = i18n.t export const localeDict = i18n.dict @@ -136,9 +147,12 @@ ipcMain.handle('packs:load', async (_event, manifestUrlInput?: string): Promise< } sendLog(t('log.manifestDownload', { url: state.manifestUrl })) const manifest = await fetchJson(state.manifestUrl) + const audience = getAudience() const results: FetchedPack[] = [] for (const entry of manifest.packs ?? []) { if (typeof entry?.file !== 'string') continue + // 대상(audience)에 맞지 않는 pack 은 건너뛴다(일반=public, 개발자용=non-public). + if (!isPackVisibleForAudience(entry.public, audience)) continue const packUrl = `${state.baseUrl}/manifest.json`.replace(/manifest\.json$/, `manifest/${entry.file}.json`) try { const raw = await fetchJson>(packUrl) diff --git a/src/server/routes/op.ts b/src/server/routes/op.ts index 43fe1c4..0e9caf3 100644 --- a/src/server/routes/op.ts +++ b/src/server/routes/op.ts @@ -16,10 +16,12 @@ import { normalizePackDefinition, normalizePackList, readAccounts, + readManifest, renamePack, sanitizePackKey, saveTerm, savePackList, + setPackPublic, setTermVisibility } from '../../shared/store.js' import { fetchReleaseVersions } from '../../shared/mojang.js' @@ -122,11 +124,15 @@ opRouter.get('/op/dashboard/:packName', requireAuth, async (req, res, next) => { return } const releases = await fetchReleaseVersions() + const manifest = await readManifest() + const entry = manifest.packs.find((e) => e.file === packKey) + const isPublic = entry ? entry.public !== false : true res.render('op/editor', { userId: req.session.userId, packKey, pack: definition, - releases + releases, + isPublic }) } catch (error) { next(error) @@ -519,6 +525,9 @@ opRouter.post('/op/dashboard/:packName', requireAuth, async (req, res, next) => return } const finalKey = await renamePack(packKey, requestedKey, normalized) + // 체크박스는 체크됐을 때만 전송되므로, 없으면 비공개(개발자용). + const isPublic = pickFirstValue(req.body.isPublic) === 'on' || pickFirstValue(req.body.isPublic) === 'true' + await setPackPublic(finalKey, isPublic) res.redirect(`/op/dashboard/${finalKey}`) } catch (error) { next(error) diff --git a/src/shared/audience.ts b/src/shared/audience.ts new file mode 100644 index 0000000..bc7870d --- /dev/null +++ b/src/shared/audience.ts @@ -0,0 +1,20 @@ +// 설치기 "대상(audience)" 구분. 빌드 시 package.json 에 박히는 musicQuizAudience +// 값으로 결정한다(electron-builder extraMetadata). +// - 'public' : 일반 설치기. manifest 의 public!==false 인 pack 만 노출. +// - 'developer' : 개발자용 설치기. public===false 인 pack 만 노출. +// 이 모듈은 electron 에 의존하지 않는 순수 로직만 둔다(server 빌드에도 포함되므로). + +export type Audience = 'public' | 'developer' + +export function resolveAudience(raw: unknown): Audience { + return raw === 'developer' ? 'developer' : 'public' +} + +/** + * 주어진 pack 의 public 플래그가 현재 audience 에게 보여야 하는지. + * public 미지정(undefined)은 공개로 간주한다(하위호환). + */ +export function isPackVisibleForAudience(isPublic: boolean | undefined, audience: Audience): boolean { + const isPublicPack = isPublic !== false + return audience === 'developer' ? !isPublicPack : isPublicPack +} diff --git a/src/shared/store.ts b/src/shared/store.ts index 37d14ac..3b76027 100644 --- a/src/shared/store.ts +++ b/src/shared/store.ts @@ -18,8 +18,15 @@ export async function readManifest(): Promise { return { packs: [] } } return { - packs: parsed.packs.filter((entry): entry is ManifestEntry => - typeof entry?.name === 'string' && typeof entry?.file === 'string') + packs: parsed.packs + .filter((entry): entry is ManifestEntry => + typeof entry?.name === 'string' && typeof entry?.file === 'string') + .map((entry) => ({ + name: entry.name, + file: entry.file, + // public 은 명시적 boolean 만 보존. 미지정은 그대로 undefined(=공개 취급). + ...(typeof entry.public === 'boolean' ? { public: entry.public } : {}) + })) } } catch (error) { if ((error as NodeJS.ErrnoException).code === 'ENOENT') { @@ -245,15 +252,32 @@ type ManifestSyncAction = 'add' | 'remove' | 'upsert' async function syncManifestWith(key: string, name: string, action: ManifestSyncAction): Promise { const manifest = await readManifest() + const existing = manifest.packs.find((entry) => entry.file === key) const filtered = manifest.packs.filter((entry) => entry.file !== key) if (action === 'remove') { await writeManifest({ packs: filtered }) return } - filtered.push({ name: name || key, file: key }) + const entry: ManifestEntry = { name: name || key, file: key } + // 기존 public 값은 이름 변경/수정(upsert) 시에도 보존. 신규(add)는 기본 공개(true). + entry.public = existing && typeof existing.public === 'boolean' ? existing.public : true + filtered.push(entry) await writeManifest({ packs: filtered }) } +/** manifest 엔트리의 public(공개 여부) 플래그를 설정한다. */ +export async function setPackPublic(key: string, isPublic: boolean): Promise { + const manifest = await readManifest() + let changed = false + for (const entry of manifest.packs) { + if (entry.file === key) { + entry.public = isPublic + changed = true + } + } + if (changed) await writeManifest(manifest) +} + function defaultPackList(): PackList { return { musicPlaylistUrl: '', imagePlaylistUrl: '', music: [], images: [] } } diff --git a/src/shared/types.ts b/src/shared/types.ts index 854de10..6214ca4 100644 --- a/src/shared/types.ts +++ b/src/shared/types.ts @@ -37,6 +37,11 @@ export interface PackDefinition { export interface ManifestEntry { name: string file: string + /** + * 공개 여부. true(또는 미지정)면 일반 설치기(간편/리소스팩)에 노출, + * false 면 개발자용 설치기에만 노출된다. 하위호환: 값이 없으면 공개로 간주. + */ + public?: boolean } export interface Manifest { diff --git a/views/op/editor.ejs b/views/op/editor.ejs index f511dfc..2613034 100644 --- a/views/op/editor.ejs +++ b/views/op/editor.ejs @@ -30,6 +30,11 @@ + +