terms: seed-on-fetch + rename/delete sync (v0.3.3)

- public route `/manifest/terms/:packKey/:fileName` 가 sendFile 전에
  `ensurePackTermsDir(packKey)` 를 호출하도록 수정. 관리자가 사이트 약관
  페이지를 한 번도 열지 않은 fresh 배포에서도 설치기가 정상적으로 약관을
  받을 수 있다. `loadPackDefinition` 으로 실제 pack 만 허용해 임의 키로
  빈 폴더가 생성되는 것을 차단.
- `renamePack`: pack JSON 이름이 바뀌면 `manifest/terms/<oldKey>/` 도
  `<newKey>/` 로 함께 rename.
- `deletePackKeys`: pack 삭제 시 약관 폴더도 `fs.rm` 으로 정리 — 동일 key
  재생성 시 옛 약관 부활 방지.
- `ensurePackTermsDir` export.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
2026-05-20 01:39:28 +09:00
parent 25977d894b
commit 05dc9d7166
3 changed files with 53 additions and 13 deletions

View File

@@ -6,7 +6,7 @@ import {
manifestRootPath, manifestDirPath, manifestTermsDirPath,
fileDirPath, viewsDirPath, publicDirPath
} from '../shared/paths.js'
import { isPublicTermsFile } from '../shared/store.js'
import { ensurePackTermsDir, isPublicTermsFile, loadPackDefinition } from '../shared/store.js'
import { loadEnv } from '../shared/env.js'
import { t, localeDict } from './i18n.js'
import { indexRouter } from './routes/index.js'
@@ -66,17 +66,32 @@ app.get('/manifest.json', (_req, res) => {
// 설치기 + 사이트가 약관(markdown) 을 가져갈 수 있도록 .md 만 허용한다.
// 음악퀴즈(pack) 별로 manifest/terms/<packKey>/<file>.md 에서 노출한다.
// _meta.json 같은 시스템 파일이나 경로 탈출은 isPublicTermsFile 에서 차단.
app.get('/manifest/terms/:packKey/:fileName', (req, res) => {
const { packKey, fileName } = req.params
if (!isPublicTermsFile(packKey, fileName)) {
res.status(404).send('Not Found')
return
//
// fresh 배포에서 관리자가 약관 페이지를 한 번도 열지 않은 상태로 설치기가 약관을
// 요청하는 경우에도 작동하도록, 실제 pack 이면 ensurePackTermsDir 로 v0.3.1
// 전역 .md 들을 시드 복사한 뒤 sendFile 한다. 임의 packKey 로 빈 폴더가
// 생성되는 것은 loadPackDefinition 으로 차단.
app.get('/manifest/terms/:packKey/:fileName', async (req, res, next) => {
try {
const { packKey, fileName } = req.params
if (!isPublicTermsFile(packKey, fileName)) {
res.status(404).send('Not Found')
return
}
const pack = await loadPackDefinition(packKey)
if (!pack) {
res.status(404).send('Not Found')
return
}
await ensurePackTermsDir(packKey)
res.type('text/markdown; charset=utf-8')
res.sendFile(path.join(manifestTermsDirPath, packKey, fileName), (err) => {
if (!err || res.headersSent) return
res.status(404).send('Not Found')
})
} catch (error) {
next(error)
}
res.type('text/markdown; charset=utf-8')
res.sendFile(path.join(manifestTermsDirPath, packKey, fileName), (err) => {
if (!err || res.headersSent) return
res.status(404).send('Not Found')
})
})
// 설치기에서 개별 음악퀴즈 JSON을 가져갈 수 있도록 파일 단위로만 허용.