terms: agreement pages + site Notion-style editor + rp cancel fix

- 5종 약관(map/resourcepack/mod/installer/installer-rp) markdown 시드 + manifest/terms/ 노출
- 사이트 /op/agreement 목록 + Notion 스타일 markdown 에디터 (슬래시 명령어, 미리보기)
- 메인 installer: 음악퀴즈 선택 직후 약관 동의 페이지(맵·모드·설치기) 추가
- rp installer: 음악퀴즈 선택 직후 약관 동의 페이지(리소스팩·설치기) 추가
- rp installer 취소 버그 수정: buildResourcepackZip 단계간 + archive.abort() 폴링
- rp installer 취소 UX: 즉시 "취소 중…" 표시, 취소 시 installFailed 알림 생략
- 0.2.6 → 0.3.0 (큰 기능)

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
2026-05-20 00:55:36 +09:00
parent bc3841147f
commit ffb2048627
26 changed files with 1323 additions and 18 deletions

View File

@@ -1,7 +1,10 @@
import fs from 'node:fs'
import fsp from 'node:fs/promises'
import path from 'node:path'
import { manifestRootPath, manifestDirPath, accountFilePath, fileListDirPath } from './paths.js'
import {
manifestRootPath, manifestDirPath, manifestTermsDirPath,
accountFilePath, fileListDirPath
} from './paths.js'
import type {
Manifest, ManifestEntry, PackDefinition, AccountEntry, LoaderType,
PackList, MusicListEntry, ImageListEntry
@@ -291,6 +294,35 @@ export async function savePackList(packKey: string, list: PackList): Promise<voi
await fsp.writeFile(filePath, `${JSON.stringify(normalized, null, 2)}\n`, 'utf8')
}
// ─── Terms (Markdown 약관) ─────────────────────────────────────────────
// 사이트와 인스톨러가 약관을 보여주기 위해 사용하는 markdown 파일.
// 화이트리스트로 5종만 허용한다.
export type TermKind = 'map' | 'resourcepack' | 'mod' | 'installer' | 'installer-rp'
export const TERM_KINDS: readonly TermKind[] = [
'map', 'resourcepack', 'mod', 'installer', 'installer-rp'
]
export function isTermKind(value: unknown): value is TermKind {
return typeof value === 'string' && (TERM_KINDS as readonly string[]).includes(value)
}
export async function loadTerm(kind: TermKind): Promise<string> {
const filePath = path.join(manifestTermsDirPath, `${kind}.md`)
try {
return await fsp.readFile(filePath, 'utf8')
} catch (error) {
if ((error as NodeJS.ErrnoException).code === 'ENOENT') return ''
throw error
}
}
export async function saveTerm(kind: TermKind, markdown: string): Promise<void> {
await fsp.mkdir(manifestTermsDirPath, { recursive: true })
const filePath = path.join(manifestTermsDirPath, `${kind}.md`)
const normalized = (markdown ?? '').replace(/\r\n/g, '\n')
await fsp.writeFile(filePath, normalized.endsWith('\n') ? normalized : `${normalized}\n`, 'utf8')
}
export async function readAccounts(): Promise<AccountEntry[]> {
try {
const raw = await fsp.readFile(accountFilePath, 'utf8')