Compare commits
5 Commits
c8da4207fc
...
v0.2.0
| Author | SHA1 | Date | |
|---|---|---|---|
| f810719d92 | |||
| ae771668de | |||
| 40c47fbeb3 | |||
| 6e170646a7 | |||
| 3017e77479 |
4
.env.build
Normal file
4
.env.build
Normal file
@@ -0,0 +1,4 @@
|
||||
# 빌드용 환경변수 — `npm run dist:win` / `npm run dist:win:rp` 로 패키징될 때
|
||||
# 설치기 exe 의 `resources/.env.build` 로 함께 배포되어 런타임에 로드됨.
|
||||
# 서버 운영용(PORT/HOST/SESSION_SECRET) 값은 여기 두지 말고 `.env` 에.
|
||||
SITE_BASE_URL=https://mc.tkrmagid.kr
|
||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -7,4 +7,3 @@ conversations/
|
||||
.env
|
||||
.env.local
|
||||
.env.*.local
|
||||
.env.build
|
||||
|
||||
@@ -11,8 +11,18 @@ files:
|
||||
- dist/installer-rp/**
|
||||
- dist/shared/**
|
||||
- installer-rp/**
|
||||
# rp 의 index.html 은 메인 설치기와 동일한 styles.css 를 공유함
|
||||
# (`<link href="../installer/styles.css">`). asar 안에 해당 파일이 없으면
|
||||
# UI 가 무스타일로 렌더링되므로 그 한 파일만 명시적으로 포함.
|
||||
- installer/styles.css
|
||||
- build/icon.*
|
||||
- package.json
|
||||
# sharp 는 플랫폼별 prebuilt 가 분리 패키지로 배포됨. Windows 빌드에서는
|
||||
# win32-x64 만 포함하고 linux/* 변종은 묶지 않아 exe 크기를 줄임.
|
||||
- "!node_modules/@img/sharp-linux-*"
|
||||
- "!node_modules/@img/sharp-linuxmusl-*"
|
||||
- "!node_modules/@img/sharp-libvips-linux-*"
|
||||
- "!node_modules/@img/sharp-libvips-linuxmusl-*"
|
||||
# 메인 설치기와 동일하게 빌드 전용 `.env.build` 와 locales 를 함께 배포.
|
||||
extraResources:
|
||||
- from: .
|
||||
|
||||
@@ -9,6 +9,12 @@ files:
|
||||
- installer/**
|
||||
- build/icon.*
|
||||
- package.json
|
||||
# sharp 는 플랫폼별 prebuilt 가 분리 패키지로 배포됨. Windows 빌드에서는
|
||||
# win32-x64 만 포함하고 linux/* 변종은 묶지 않아 exe 크기를 줄임.
|
||||
- "!node_modules/@img/sharp-linux-*"
|
||||
- "!node_modules/@img/sharp-linuxmusl-*"
|
||||
- "!node_modules/@img/sharp-libvips-linux-*"
|
||||
- "!node_modules/@img/sharp-libvips-linuxmusl-*"
|
||||
# 빌드 전용 `.env.build` 를 설치기 옆에 함께 배포(없으면 조용히 패스).
|
||||
# `.env` 는 서버/개발 실행용이라 빌드 산출물에는 포함되지 않으며, 패키지된 exe
|
||||
# 는 `resources/.env.build` 를 우선 로드함(없으면 `resources/.env` 로 폴백).
|
||||
|
||||
@@ -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 로더 버전을 선택해 주세요."
|
||||
},
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "minecraft-music-quiz-installer",
|
||||
"version": "0.1.0",
|
||||
"version": "0.2.0",
|
||||
"description": "마인크래프트 음악퀴즈 간편설치기 + 관리 사이트",
|
||||
"main": "dist/installer/main.js",
|
||||
"scripts": {
|
||||
@@ -9,8 +9,9 @@
|
||||
"dev:server": "tsc -p tsconfig.server.json && node dist/server/app.js",
|
||||
"installer": "tsc -p tsconfig.installer.json && electron .",
|
||||
"installer:rp": "tsc -p tsconfig.installer-rp.json && electron dist/installer-rp/main.js",
|
||||
"dist:win": "tsc -p tsconfig.installer.json && electron-builder --win --config electron-builder.yml",
|
||||
"dist:win:rp": "tsc -p tsconfig.installer-rp.json && electron-builder --win --config electron-builder-rp.yml"
|
||||
"preinstall:sharp-win32": "npm install --no-save --force @img/sharp-win32-x64@0.34.5",
|
||||
"dist:win": "npm run preinstall:sharp-win32 && 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"
|
||||
},
|
||||
"dependencies": {
|
||||
"@types/archiver": "^7.0.0",
|
||||
|
||||
@@ -35,6 +35,20 @@ interface RpInstallerState {
|
||||
activeChildren: Set<ChildProcess>
|
||||
}
|
||||
|
||||
/**
|
||||
* 사용자가 사이트에서 지정한 "생성되는 리소스팩 이름" 을 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<PackDefinition>) : 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
|
||||
// 결과가 빈 문자열이면 `<packKey>_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 }))
|
||||
|
||||
@@ -10,6 +10,12 @@ export interface RpFetchedPack {
|
||||
* 빈 문자열이면 새 리소스팩을 처음부터 생성.
|
||||
*/
|
||||
resourcepackPath: string
|
||||
/**
|
||||
* /manifest/<key>.json 의 outputPackName. 관리 사이트에서 설정한 "생성되는
|
||||
* 리소스팩 이름". 비어 있으면 설치기가 `<key>_musicquiz` 형식으로 폴백.
|
||||
* 파일명으로 쓰기 전에 Windows 금지 문자(\<\>:"/\\|?*) 는 `_` 로 치환.
|
||||
*/
|
||||
outputPackName: string
|
||||
/** /file/list/<key>.json 의 음악·사진 목록. */
|
||||
list: PackList
|
||||
}
|
||||
|
||||
@@ -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)),
|
||||
|
||||
@@ -4,21 +4,27 @@ import dotenv from 'dotenv'
|
||||
import { projectRoot } from './paths.js'
|
||||
|
||||
/**
|
||||
* `.env` 를 읽어 `process.env` 에 주입.
|
||||
* `.env` / `.env.build` 를 읽어 `process.env` 에 주입.
|
||||
*
|
||||
* 탐색 순서(처음 발견된 것만 사용):
|
||||
* 1. 패키징된 Electron 앱이면 `process.resourcesPath/.env.build`
|
||||
* — electron-builder 의 extraResources 로 빌드 시점 `.env.build` 가 함께
|
||||
* 배포됨. 빌드용 도메인/설정을 dev `.env` 와 분리해 관리하기 위함.
|
||||
* 2. 패키징된 Electron 앱이면 `process.resourcesPath/.env`
|
||||
* — 운영자가 패키징 후 직접 `.env` 를 옆에 두고 덮어쓰는 경우를 대비한 폴백.
|
||||
* 3. `<프로젝트 루트>/.env.build`
|
||||
* — 개발 환경에서도 빌드용 값을 그대로 테스트하고 싶을 때.
|
||||
* 4. `<프로젝트 루트>/.env`
|
||||
* — 개발 실행(npm start / npm run installer*) 및 서버 운영용.
|
||||
* 여러 파일을 순서대로 읽되 `override:false` 로 병합하므로 **먼저 로드된 값이
|
||||
* 우선**. 두 도메인(패키지 빌드용 vs 개발/서버용) 이 한 함수에서 자연스럽게
|
||||
* 분리됨:
|
||||
*
|
||||
* - 이미 설정된 환경변수는 덮어쓰지 않음(쉘/systemd 에서 넘긴 값이 우선).
|
||||
* - 파일이 없으면 조용히 통과.
|
||||
* 1. 패키징된 Electron 앱: `process.resourcesPath/.env.build`
|
||||
* — electron-builder 가 빌드 시점 `.env.build` 를 함께 배포. 패키지된 exe
|
||||
* 에서 가장 먼저 적용되는 값.
|
||||
* 2. 패키징된 Electron 앱: `process.resourcesPath/.env`
|
||||
* — 운영자가 패키징 후 직접 `.env` 를 옆에 두고 덮어쓰는 경우 폴백.
|
||||
* 3. `<프로젝트 루트>/.env`
|
||||
* — 개발 실행(npm start / npm run installer*) 및 서버 운영용. 서버의
|
||||
* `PORT/HOST/SESSION_SECRET` 처럼 dev 에서 반드시 살아 있어야 하는 값들이
|
||||
* 있어, `.env.build` 보다 먼저 로드해 우선권을 줌.
|
||||
* 4. `<프로젝트 루트>/.env.build`
|
||||
* — dev 환경에서 빌드용 값(예: 운영 도메인 SITE_BASE_URL)을 테스트하고
|
||||
* 싶을 때 사용. `.env` 에 없는 키만 채움.
|
||||
*
|
||||
* - 이미 설정된 환경변수는 덮어쓰지 않음(쉘/systemd 에서 넘긴 값이 최우선).
|
||||
* - 존재하지 않는 후보는 조용히 건너뜀.
|
||||
* - 서버/설치기/리소스팩설치기 진입점에서 한 번씩 호출.
|
||||
*/
|
||||
export function loadEnv(): void {
|
||||
@@ -28,13 +34,12 @@ export function loadEnv(): void {
|
||||
candidates.push(path.join(resourcesPath, '.env.build'))
|
||||
candidates.push(path.join(resourcesPath, '.env'))
|
||||
}
|
||||
candidates.push(path.join(projectRoot, '.env.build'))
|
||||
candidates.push(path.join(projectRoot, '.env'))
|
||||
candidates.push(path.join(projectRoot, '.env.build'))
|
||||
|
||||
for (const envPath of candidates) {
|
||||
if (fs.existsSync(envPath)) {
|
||||
dotenv.config({ path: envPath, override: false, quiet: true })
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<PackDefinition> & 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),
|
||||
|
||||
@@ -16,6 +16,14 @@ export interface PackDefinition {
|
||||
modsFolder: string
|
||||
/** /file/resourcepacks/<resourcepackPath> 의 단일 .zip을 그대로 다운로드. */
|
||||
resourcepackPath: string
|
||||
/**
|
||||
* 리소스팩 설치기가 만들어 내는 최종 zip 파일의 이름(확장자 제외).
|
||||
* 빈 문자열이면 설치기가 `<packKey>_musicquiz` 형식으로 기본 이름을 만든다.
|
||||
* 마인크래프트 리소스팩 목록에서 사용자에게 제목처럼 보이는 값이므로
|
||||
* 한글 등 자유 입력을 그대로 보존하고, 파일 시스템에서 사용할 때 금지 문자만
|
||||
* `_` 로 치환한다(치환 책임은 설치기 측에 있음).
|
||||
*/
|
||||
outputPackName: string
|
||||
serverMinRam: number
|
||||
serverMaxRam: number
|
||||
clientMinRam: number
|
||||
|
||||
@@ -98,6 +98,11 @@
|
||||
<input name="resourcepackPath" value="<%= pack.resourcepackPath %>" placeholder="my-pack.zip" pattern=".*\.zip|" />
|
||||
<small class="muted"><%= t('editor.resourcepackHint') %></small>
|
||||
</label>
|
||||
<label class="fullSpan">
|
||||
<span><%= t('editor.outputPackName') %></span>
|
||||
<input name="outputPackName" value="<%= pack.outputPackName %>" placeholder="<%= t('editor.outputPackNamePlaceholder') %>" />
|
||||
<small class="muted"><%= t('editor.outputPackNameHint') %></small>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<button class="primaryButton" type="submit"><%= t('common.save') %></button>
|
||||
|
||||
Reference in New Issue
Block a user