feat: 최종 리소스팩(선택 설치) + 약관 표시대상 추가

- pack.finalResourcepackPath 추가(/file/resourcepacks/outputs/, "."=없음, 기본 ".").
  편집기에 "최종 리소스팩 (.zip)" 입력 필드 추가.
- 약관 표시대상에 showInFinalResourcepack("최종 리소스팩에 표시") 3번째 토글 추가
  (스키마/시드/마이그레이션/편집기 체크박스·배지·저장 라우트, index.json 노출).
- 간편설치기 완료 단계: 기존 "직접 적용" 경고 제거 → 최종 리소스팩이 "."이 아니면
  "리소스팩을 설치하시겠습니까?" 예/아니요. 아니요→완료, 예→최종 리소스팩 약관
  (showInFinalResourcepack) 동의→/file/resourcepacks/outputs/ 에서 다운로드→완료.
  "." 이면 이 페이지 자체를 건너뜀.
- 0.4.15→0.4.16.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
2026-07-23 00:33:07 +09:00
parent f53e54493d
commit e5dcc47548
13 changed files with 247 additions and 25 deletions

View File

@@ -47,6 +47,7 @@ export function defaultPackDefinition(name: string): PackDefinition {
platform: { type: 'vanilla' },
modsFolder: '',
resourcepackPath: '',
finalResourcepackPath: '.',
outputPackName: '',
serverMinRam: 2048,
serverMaxRam: 4096,
@@ -97,6 +98,13 @@ export function normalizeRecommendedJdk(input: unknown): number {
return Number.isFinite(n) && n >= 8 && n <= 99 ? n : DEFAULT_JDK_MAJOR
}
/** "최종 리소스팩" 값 보정. 빈 값/`.` 은 "없음"을 뜻하는 `.` 으로, 그 외는 zip 파일명으로. */
export function normalizeFinalResourcepack(input: unknown): string {
const raw = typeof input === 'string' ? input.trim() : ''
if (raw === '' || raw === '.') return '.'
return sanitizeZipFileName(raw)
}
export function normalizePackDefinition(input: Partial<PackDefinition> & Record<string, unknown>): PackDefinition {
const fallback = defaultPackDefinition(typeof input.name === 'string' ? input.name : 'new')
const platform = (input.platform ?? {}) as Partial<PackDefinition['platform']>
@@ -125,6 +133,7 @@ export function normalizePackDefinition(input: Partial<PackDefinition> & Record<
},
modsFolder: sanitizeFolderName(input.modsFolder),
resourcepackPath: sanitizeZipFileName(input.resourcepackPath),
finalResourcepackPath: normalizeFinalResourcepack(input.finalResourcepackPath),
// 표시명은 사용자 입력을 보존(공백/마침표 trim 만). 파일명 안전 처리는 설치기 측에서.
outputPackName: typeof input.outputPackName === 'string' ? input.outputPackName.trim() : '',
serverMinRam: clampNumber(input.serverMinRam, fallback.serverMinRam),
@@ -393,12 +402,13 @@ const DEFAULT_TERM_SEEDS: Array<{
label: string
showInInstaller: boolean
showInInstallerRp: boolean
showInFinalResourcepack: boolean
}> = [
{ kind: 'map', label: '맵 약관', showInInstaller: true, showInInstallerRp: false },
{ kind: 'mod', label: '모드 약관', showInInstaller: true, showInInstallerRp: false },
{ kind: 'installer', label: '설치기 약관', showInInstaller: true, showInInstallerRp: false },
{ kind: 'resourcepack', label: '리소스팩 약관', showInInstaller: false, showInInstallerRp: true },
{ kind: 'installer-rp', label: '리소스팩 설치기 약관', showInInstaller: false, showInInstallerRp: true }
{ kind: 'map', label: '맵 약관', showInInstaller: true, showInInstallerRp: false, showInFinalResourcepack: false },
{ kind: 'mod', label: '모드 약관', showInInstaller: true, showInInstallerRp: false, showInFinalResourcepack: false },
{ kind: 'installer', label: '설치기 약관', showInInstaller: true, showInInstallerRp: false, showInFinalResourcepack: false },
{ kind: 'resourcepack', label: '리소스팩 약관', showInInstaller: false, showInInstallerRp: true, showInFinalResourcepack: true },
{ kind: 'installer-rp', label: '리소스팩 설치기 약관', showInInstaller: false, showInInstallerRp: true, showInFinalResourcepack: false }
]
const TERM_KIND_RE = /^[a-z0-9][a-z0-9-]{0,31}$/
@@ -411,6 +421,7 @@ export interface TermEntry {
label: string
showInInstaller: boolean
showInInstallerRp: boolean
showInFinalResourcepack: boolean
}
interface TermsMeta {
@@ -502,7 +513,8 @@ async function ensureMetaInitialized(dir: string, dirWasJustCreated: boolean): P
meta.terms[seed.kind] = {
label: seed.label,
showInInstaller: seed.showInInstaller,
showInInstallerRp: seed.showInInstallerRp
showInInstallerRp: seed.showInInstallerRp,
showInFinalResourcepack: seed.showInFinalResourcepack
}
changed = true
}
@@ -543,10 +555,11 @@ async function ensureMetaInitialized(dir: string, dirWasJustCreated: boolean): P
terms[seed.kind] = {
label: seed.label,
showInInstaller: seed.showInInstaller,
showInInstallerRp: seed.showInInstallerRp
showInInstallerRp: seed.showInInstallerRp,
showInFinalResourcepack: seed.showInFinalResourcepack
}
}
// 구 스키마의 사용자 정의 약관은 양쪽 인스톨러에 보이도록 기본값으로.
// 구 스키마의 사용자 정의 약관은 양쪽 인스톨러에 보이도록 기본값으로(최종 리소스팩은 기본 off).
for (const [k, label] of Object.entries(oldCustomLabels)) {
if (terms[k]) continue
try {
@@ -554,7 +567,7 @@ async function ensureMetaInitialized(dir: string, dirWasJustCreated: boolean): P
} catch {
continue
}
terms[k] = { label, showInInstaller: true, showInInstallerRp: true }
terms[k] = { label, showInInstaller: true, showInInstallerRp: true, showInFinalResourcepack: false }
}
await fsp.writeFile(metaPath, `${JSON.stringify({ terms }, null, 2)}\n`, 'utf8')
}
@@ -575,7 +588,8 @@ async function loadTermsMeta(packKey: string): Promise<TermsMeta> {
result.terms[k] = {
label,
showInInstaller: entry.showInInstaller === true,
showInInstallerRp: entry.showInInstallerRp === true
showInInstallerRp: entry.showInInstallerRp === true,
showInFinalResourcepack: entry.showInFinalResourcepack === true
}
}
}
@@ -600,6 +614,7 @@ export interface TermItem {
label: string
showInInstaller: boolean
showInInstallerRp: boolean
showInFinalResourcepack: boolean
}
/**
@@ -633,7 +648,8 @@ export async function listTermsWithLabels(packKey: string): Promise<TermItem[]>
kind: seed.kind,
label: entry.label,
showInInstaller: entry.showInInstaller,
showInInstallerRp: entry.showInInstallerRp
showInInstallerRp: entry.showInInstallerRp,
showInFinalResourcepack: entry.showInFinalResourcepack
})
seen.add(seed.kind)
}
@@ -647,7 +663,8 @@ export async function listTermsWithLabels(packKey: string): Promise<TermItem[]>
kind,
label: entry.label,
showInInstaller: entry.showInInstaller,
showInInstallerRp: entry.showInInstallerRp
showInInstallerRp: entry.showInInstallerRp,
showInFinalResourcepack: entry.showInFinalResourcepack
})
}
return items
@@ -666,7 +683,7 @@ export async function getTermEntry(packKey: string, kind: string): Promise<TermE
export async function setTermVisibility(
packKey: string,
kind: string,
visibility: { showInInstaller: boolean; showInInstallerRp: boolean }
visibility: { showInInstaller: boolean; showInInstallerRp: boolean; showInFinalResourcepack: boolean }
): Promise<void> {
if (!isTermKind(kind)) throw new Error('invalid term kind')
const meta = await loadTermsMeta(packKey)
@@ -674,6 +691,7 @@ export async function setTermVisibility(
if (!entry) throw new Error('term not found')
entry.showInInstaller = !!visibility.showInInstaller
entry.showInInstallerRp = !!visibility.showInInstallerRp
entry.showInFinalResourcepack = !!visibility.showInFinalResourcepack
await saveTermsMeta(packKey, meta)
}
@@ -723,7 +741,8 @@ export async function createTerm(packKey: string, kind: string, label: string):
meta.terms[kind] = {
label: cleanLabel,
showInInstaller: seed ? seed.showInInstaller : true,
showInInstallerRp: seed ? seed.showInInstallerRp : true
showInInstallerRp: seed ? seed.showInInstallerRp : true,
showInFinalResourcepack: seed ? seed.showInFinalResourcepack : false
}
await saveTermsMeta(packKey, meta)
}

View File

@@ -14,8 +14,14 @@ export interface PackDefinition {
platform: PackPlatform
/** /file/mods/<modsFolder>/ 폴더 안의 모든 .jar을 자동 다운로드. */
modsFolder: string
/** /file/resourcepacks/<resourcepackPath> 의 단일 .zip을 그대로 다운로드. */
/** /file/resourcepacks/<resourcepackPath> 의 단일 .zip을 그대로 다운로드(리소스팩 설치기의 베이스). */
resourcepackPath: string
/**
* 간편설치기 마지막에 선택 설치하는 "최종 리소스팩" zip 파일 이름.
* /file/resourcepacks/outputs/<finalResourcepackPath> 에서 받는다.
* `"."` 이면 없음(설치 여부 질문 자체를 건너뜀). 기본값은 `"."`.
*/
finalResourcepackPath: string
/**
* 리소스팩 설치기가 만들어 내는 최종 zip 파일의 이름(확장자 제외).
* 빈 문자열이면 설치기가 `<packKey>_resourcepack` 형식으로 기본 이름을 만든다.