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

@@ -6,13 +6,18 @@ import {
listPackKeys,
loadPackDefinition,
loadPackList,
loadTerm,
normalizePackDefinition,
normalizePackList,
readAccounts,
renamePack,
sanitizePackKey,
savePackList
saveTerm,
savePackList,
isTermKind,
TERM_KINDS
} from '../../shared/store.js'
import type { TermKind } from '../../shared/store.js'
import { fetchReleaseVersions } from '../../shared/mojang.js'
import { fetchPlaylistEntries, fetchVideoMeta, YtDlpUnavailableError } from '../youtube.js'
import { requireAuth } from '../middleware/auth.js'
@@ -295,6 +300,56 @@ opRouter.get('/op/datapack/:packName/images-zip', requireAuth, async (req, res,
}
})
// ─── /op/agreement ─────────────────────────────────────────────────────
// 약관(Markdown) 5종 편집기. 사이트에서는 노션 스타일의 슬래시 명령으로
// 마크다운을 작성하고, 인스톨러는 /manifest/terms/<kind>.md 로 받아 표시한다.
const TERM_LABELS: Record<TermKind, string> = {
'map': '맵 약관',
'resourcepack': '리소스팩 약관',
'mod': '모드 약관',
'installer': '설치기 약관',
'installer-rp': '리소스팩 설치기 약관'
}
opRouter.get('/op/agreement', requireAuth, (req, res) => {
const items = TERM_KINDS.map((kind) => ({ kind, label: TERM_LABELS[kind] }))
res.render('op/terms', { userId: req.session.userId, items })
})
opRouter.get('/op/agreement/:kind', requireAuth, async (req, res, next) => {
try {
const kind = pickFirstValue(req.params.kind)
if (!isTermKind(kind)) {
res.status(404).send(t('errors.unknown'))
return
}
const content = await loadTerm(kind)
res.render('op/termsEditor', {
userId: req.session.userId,
kind,
label: TERM_LABELS[kind],
content
})
} catch (error) {
next(error)
}
})
opRouter.post('/op/agreement/:kind', requireAuth, async (req, res, next) => {
try {
const kind = pickFirstValue(req.params.kind)
if (!isTermKind(kind)) {
res.status(404).json({ ok: false, message: t('errors.unknown') })
return
}
const content = typeof req.body?.content === 'string' ? req.body.content : ''
await saveTerm(kind, content)
res.json({ ok: true })
} catch (error) {
next(error)
}
})
opRouter.post('/op/dashboard/:packName', requireAuth, async (req, res, next) => {
try {
const packKey = sanitizePackKey(pickFirstValue(req.params.packName))