Build music-quiz installer and management site per spec

Implements the full spec described in README.md:

Management site (Node + TypeScript + Express + EJS):
- Public main page lists packs registered in manifest.json.
- /op login (account.json, internal-only), /op/dashboard manages packs
  with horizontal-scroll cards, add/select-and-delete flow, and the
  /op/dashboard/:packName editor (Mojang release dropdown, dynamic
  mods/resourcepacks lists, platform/RAM fields, file rename).
- Routes for /manifest.json (public) and /file/* (server pack files).
- Middleware blocks /account.json and /manifest/* directory access.

Installer (Electron):
- Five page renderer driven by IPC (preload contextBridge API):
  pack pick → single/multi → server install (path no-Korean check, JDK
  detect, file download, EULA, RAM gating, local web config editor,
  UPnP/port-forward check) → client install (.mc_custom mods +
  resourcepacks + launcher_profiles.json gameDir/javaArgs) → finish
  toggles (server folder, shortcut, server start, launcher start).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
2026-05-09 21:34:27 +09:00
parent 42a7cf3426
commit 8fd7cfaaef
32 changed files with 7817 additions and 0 deletions

59
src/installer/preload.ts Normal file
View File

@@ -0,0 +1,59 @@
import { contextBridge, ipcRenderer } from 'electron'
import type { ClientInstallPayload, FetchedPack, RamCheckResult, ServerInstallPayload, PortForwardResult } from './types'
const api = {
// 1단계
loadPacks: (manifestUrl?: string): Promise<FetchedPack[]> =>
ipcRenderer.invoke('packs:load', manifestUrl),
setSelectedPack: (packKey: string): Promise<void> =>
ipcRenderer.invoke('packs:select', packKey),
// 3-1
pickFolder: (): Promise<string | null> => ipcRenderer.invoke('dialog:pickFolder'),
validateInstallPath: (target: string): Promise<{ ok: boolean; message?: string }> =>
ipcRenderer.invoke('install:validatePath', target),
// 3-2
detectJdk: (): Promise<{ found: boolean; path: string }> => ipcRenderer.invoke('jdk:detect'),
// 3-3
startServerInstall: (payload: ServerInstallPayload): Promise<void> =>
ipcRenderer.invoke('server:install', payload),
acceptEula: (installPath: string): Promise<void> =>
ipcRenderer.invoke('server:acceptEula', installPath),
checkRam: (packKey: string): Promise<RamCheckResult> =>
ipcRenderer.invoke('server:checkRam', packKey),
// 3-4
startServerConfigEditor: (installPath: string): Promise<{ url: string }> =>
ipcRenderer.invoke('server:configEditor', installPath),
// 3-5
checkPortForward: (port: number): Promise<PortForwardResult> =>
ipcRenderer.invoke('server:portForward', port),
// 4단계
installClient: (payload: ClientInstallPayload): Promise<void> =>
ipcRenderer.invoke('client:install', payload),
// 5단계
openServerFolder: (): Promise<void> => ipcRenderer.invoke('finish:openServerFolder'),
createDesktopShortcut: (): Promise<void> => ipcRenderer.invoke('finish:desktopShortcut'),
startServer: (): Promise<void> => ipcRenderer.invoke('finish:startServer'),
startMinecraftLauncher: (): Promise<void> => ipcRenderer.invoke('finish:startLauncher'),
// log stream
onLog: (handler: (line: string) => void): (() => void) => {
const listener = (_event: unknown, line: string) => handler(line)
ipcRenderer.on('log', listener)
return () => ipcRenderer.removeListener('log', listener)
}
}
contextBridge.exposeInMainWorld('installer', api)
declare global {
interface Window {
installer: typeof api
}
}