릴리스: v0.1.47 템플릿 요청과 관리자 승인 흐름 추가

This commit is contained in:
2026-03-27 11:10:45 +09:00
parent e0eeaa01cd
commit 3b314381a0
11 changed files with 725 additions and 15 deletions

View File

@@ -11,6 +11,7 @@ const {
deleteTierList,
saveTierList,
createCustomItem,
createTemplateRequest,
findUserById,
favoriteTierList,
unfavoriteTierList,
@@ -18,6 +19,7 @@ const {
const { requireAuth } = require('../middleware/auth')
const router = express.Router()
const FREEFORM_GAME_ID = 'freeform'
function normalizePoolItem(item) {
if (!item || item.origin !== 'game' || typeof item.src !== 'string') return item
@@ -42,6 +44,19 @@ function normalizeTierList(tierList) {
}
}
function isTierListBoardEmpty(tierList) {
return !(tierList?.groups || []).some((group) => Array.isArray(group?.itemIds) && group.itemIds.length > 0)
}
function getCustomTemplateItems(tierList) {
const seen = new Set()
return (tierList?.pool || []).filter((item) => {
if (!item?.id || item.origin !== 'custom' || seen.has(item.id)) return false
seen.add(item.id)
return true
})
}
function buildUploadFilename(file) {
const ext = path.extname(file.originalname || '').toLowerCase()
const safeExt = ext && /^[.a-z0-9]+$/.test(ext) ? ext : ''
@@ -168,6 +183,49 @@ router.post('/thumbnail', requireAuth, thumbnailUpload.single('thumbnail'), asyn
res.json({ thumbnailSrc: `/uploads/tierlists/${req.file.filename}` })
})
router.post('/:id/template-request', requireAuth, async (req, res) => {
const schema = z.object({
type: z.enum(['create', 'update']),
})
const parsed = schema.safeParse(req.body)
if (!parsed.success) return res.status(400).json({ error: 'bad_request' })
const tierList = await findTierListById(req.params.id, req.session.userId)
if (!tierList) return res.status(404).json({ error: 'not_found' })
if (tierList.authorId !== req.session.userId) return res.status(403).json({ error: 'forbidden' })
const customItems = getCustomTemplateItems(tierList)
if (!customItems.length) return res.status(400).json({ error: 'custom_items_required' })
if (parsed.data.type === 'create') {
if (tierList.gameId !== FREEFORM_GAME_ID) return res.status(400).json({ error: 'freeform_required' })
if (!isTierListBoardEmpty(tierList)) return res.status(400).json({ error: 'board_must_be_empty' })
} else {
if (tierList.gameId === FREEFORM_GAME_ID) return res.status(400).json({ error: 'game_template_required' })
}
try {
const request = await createTemplateRequest({
id: nanoid(),
type: parsed.data.type,
requesterId: req.session.userId,
sourceTierListId: tierList.id,
sourceGameId: tierList.gameId,
targetGameId: parsed.data.type === 'update' ? tierList.gameId : '',
title: tierList.title,
description: tierList.description || '',
thumbnailSrc: tierList.thumbnailSrc || '',
items: customItems,
})
return res.json({ request })
} catch (e) {
if (e?.code === 'TEMPLATE_REQUEST_EXISTS') {
return res.status(409).json({ error: 'template_request_exists' })
}
throw e
}
})
router.post('/', requireAuth, async (req, res) => {
const parsed = tierListUpsertSchema.safeParse(req.body)
if (!parsed.success) return res.status(400).json({ error: 'bad_request' })