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:
@@ -255,6 +255,50 @@ ipcMain.handle('terms:list', async (): Promise<{ ok: boolean; terms?: Array<{ ki
|
||||
}
|
||||
})
|
||||
|
||||
// 최종 리소스팩 다운로드용 약관 목록. index.json 에서 showInFinalResourcepack=true 만 추린다.
|
||||
ipcMain.handle('terms:listFinal', async (): Promise<{ ok: boolean; terms?: Array<{ kind: string; label: string }>; message?: string }> => {
|
||||
if (!state.selectedKey) return { ok: false, message: 'pack not selected' }
|
||||
try {
|
||||
const url = `${state.baseUrl}/manifest/terms/${encodeURIComponent(state.selectedKey)}/index.json`
|
||||
const buf = await fetchBuffer(url)
|
||||
const parsed = JSON.parse(buf.toString('utf8')) as { terms?: unknown }
|
||||
const items = Array.isArray(parsed.terms) ? parsed.terms : []
|
||||
const terms: Array<{ kind: string; label: string }> = []
|
||||
for (const it of items) {
|
||||
if (!it || typeof it !== 'object') continue
|
||||
const entry = it as Record<string, unknown>
|
||||
if (entry.showInFinalResourcepack !== true) continue
|
||||
const kind = typeof entry.kind === 'string' ? entry.kind : ''
|
||||
const label = typeof entry.label === 'string' ? entry.label : ''
|
||||
if (!TERM_KIND_RE.test(kind) || label.length === 0) continue
|
||||
terms.push({ kind, label })
|
||||
}
|
||||
return { ok: true, terms }
|
||||
} catch (error) {
|
||||
return { ok: false, message: (error as Error).message }
|
||||
}
|
||||
})
|
||||
|
||||
// 최종 리소스팩(zip)을 /file/resourcepacks/outputs/ 에서 받아 .mc_custom/resourcepacks/ 에 저장.
|
||||
ipcMain.handle('finalResourcepack:install', async (): Promise<{ ok: boolean; path?: string; message?: string }> => {
|
||||
const pack = state.selectedKey ? state.packs.get(state.selectedKey) : undefined
|
||||
if (!pack) return { ok: false, message: t('errors.packNotFound2') }
|
||||
const finalName = pack.pack.finalResourcepackPath
|
||||
if (!finalName || finalName === '.') return { ok: false, message: 'no final resourcepack' }
|
||||
try {
|
||||
const cleaned = path.basename(finalName.replace(/^\/+/, ''))
|
||||
const url = `${state.baseUrl}/file/resourcepacks/outputs/${encodeURIComponent(cleaned)}`
|
||||
const destDir = path.join(getAppDataDir(), getMcCustomDirName(), 'resourcepacks')
|
||||
const dest = path.join(destDir, cleaned)
|
||||
sendLog(t('log.finalResourcepackDownload', { url }))
|
||||
await downloadFile(url, dest)
|
||||
sendLog(t('log.finalResourcepackSaved', { path: dest }))
|
||||
return { ok: true, path: dest }
|
||||
} catch (error) {
|
||||
return { ok: false, message: (error as Error).message }
|
||||
}
|
||||
})
|
||||
|
||||
ipcMain.handle('packs:select', async (_event, packKey: string) => {
|
||||
if (!state.packs.has(packKey)) {
|
||||
throw new Error(t('errors.packNotFound'))
|
||||
|
||||
@@ -17,6 +17,12 @@ const api = {
|
||||
// 메인 인스톨러용 약관 목록 (사이트의 visibility 토글에 따라 필터링됨)
|
||||
getTermsList: (): Promise<{ ok: boolean; terms?: Array<{ kind: string; label: string }>; message?: string }> =>
|
||||
ipcRenderer.invoke('terms:list'),
|
||||
// 최종 리소스팩 다운로드용 약관 목록 (showInFinalResourcepack 필터)
|
||||
getFinalTermsList: (): Promise<{ ok: boolean; terms?: Array<{ kind: string; label: string }>; message?: string }> =>
|
||||
ipcRenderer.invoke('terms:listFinal'),
|
||||
// 최종 리소스팩 다운로드 실행
|
||||
installFinalResourcepack: (): Promise<{ ok: boolean; path?: string; message?: string }> =>
|
||||
ipcRenderer.invoke('finalResourcepack:install'),
|
||||
|
||||
// 3-1
|
||||
pickFolder: (): Promise<string | null> => ipcRenderer.invoke('dialog:pickFolder'),
|
||||
|
||||
@@ -535,6 +535,7 @@ opRouter.get('/op/agreement/:packName/:kind', requireAuth, async (req, res, next
|
||||
label: entry.label,
|
||||
showInInstaller: entry.showInInstaller,
|
||||
showInInstallerRp: entry.showInInstallerRp,
|
||||
showInFinalResourcepack: entry.showInFinalResourcepack,
|
||||
content
|
||||
})
|
||||
} catch (error) {
|
||||
@@ -562,10 +563,12 @@ opRouter.post('/op/agreement/:packName/:kind', requireAuth, async (req, res, nex
|
||||
if (
|
||||
typeof req.body?.showInInstaller === 'boolean'
|
||||
|| typeof req.body?.showInInstallerRp === 'boolean'
|
||||
|| typeof req.body?.showInFinalResourcepack === 'boolean'
|
||||
) {
|
||||
await setTermVisibility(packKey, kind, {
|
||||
showInInstaller: req.body.showInInstaller === true,
|
||||
showInInstallerRp: req.body.showInInstallerRp === true
|
||||
showInInstallerRp: req.body.showInInstallerRp === true,
|
||||
showInFinalResourcepack: req.body.showInFinalResourcepack === true
|
||||
})
|
||||
}
|
||||
res.json({ ok: true })
|
||||
@@ -593,6 +596,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),
|
||||
finalResourcepackPath: pickFirstValue(req.body.finalResourcepackPath),
|
||||
outputPackName: pickFirstValue(req.body.outputPackName),
|
||||
serverMinRam: Number(pickFirstValue(req.body.serverMinRam)),
|
||||
serverMaxRam: Number(pickFirstValue(req.body.serverMaxRam)),
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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` 형식으로 기본 이름을 만든다.
|
||||
|
||||
Reference in New Issue
Block a user