릴리스: v1.4.25 topic 응답/요청 키 정리

This commit is contained in:
2026-04-02 20:51:03 +09:00
parent 257d50f9c5
commit 932b4e35a7
20 changed files with 188 additions and 199 deletions

View File

@@ -39,7 +39,7 @@ const {
listAdminTemplateRequests,
findTemplateRequestById,
updateTemplateRequestStatus,
updateTemplateRequestTargetGame,
updateTemplateRequestTargetTopic,
adminUpdateUser,
adminUpdateUserPassword,
adminDeleteUser,
@@ -152,16 +152,16 @@ router.patch(['/games/:gameId', '/templates/:templateId'], requireAdmin, async (
router.patch(['/games/display-order', '/templates/display-order'], requireAdmin, async (req, res) => {
const schema = z.object({
gameIds: z.array(z.string().min(1)).max(50),
topicIds: z.array(z.string().min(1)).max(50),
})
const parsed = schema.safeParse(req.body)
if (!parsed.success) return res.status(400).json({ error: 'bad_request' })
const templates = await listTopics('', { includePrivate: true })
const validGameIds = new Set(templates.map((template) => template.id))
const filteredIds = parsed.data.gameIds.filter((gameId) => validGameIds.has(gameId))
const updatedGames = await updateTopicDisplayOrder(filteredIds)
res.json({ games: updatedGames, templates: updatedGames })
const validTopicIds = new Set(templates.map((template) => template.id))
const filteredIds = parsed.data.topicIds.filter((topicId) => validTopicIds.has(topicId))
const updatedTemplates = await updateTopicDisplayOrder(filteredIds)
res.json({ templates: updatedTemplates })
})
router.patch(['/games/:gameId/items/display-order', '/templates/:templateId/items/display-order'], requireAdmin, async (req, res) => {
@@ -244,7 +244,7 @@ router.get(['/games/:gameId/items/:itemId/usage', '/templates/:templateId/items/
const template = await findTopicById(getTemplateIdParam(req))
if (!template) return res.status(404).json({ error: 'not_found' })
const item = await findTopicItemById(req.params.itemId)
if (!item || item.gameId !== template.id) return res.status(404).json({ error: 'not_found' })
if (!item || item.topicId !== template.id) return res.status(404).json({ error: 'not_found' })
const usage = await countTierListsUsingTopicItem(req.params.itemId)
res.json({ usage })
})
@@ -258,7 +258,7 @@ router.patch(['/games/:gameId/items/:itemId', '/templates/:templateId/items/:ite
if (!template) return res.status(404).json({ error: 'not_found' })
const updated = await updateTopicItemLabel(req.params.itemId, parsed.data.label)
if (!updated || updated.gameId !== template.id) return res.status(404).json({ error: 'not_found' })
if (!updated || updated.topicId !== template.id) return res.status(404).json({ error: 'not_found' })
res.json({ item: updated })
})
@@ -319,7 +319,6 @@ router.get('/tierlists', requireAdmin, async (req, res) => {
const schema = z.object({
q: z.string().trim().max(120).optional().default(''),
topicId: z.string().trim().max(120).optional().default(''),
gameId: z.string().trim().max(120).optional().default(''),
page: z.coerce.number().int().min(1).optional().default(1),
limit: z.coerce.number().int().min(1).max(200).optional().default(50),
})
@@ -328,8 +327,7 @@ router.get('/tierlists', requireAdmin, async (req, res) => {
const result = await listAdminTierLists({
queryText: parsed.data.q,
topicId: parsed.data.topicId || parsed.data.gameId,
gameId: parsed.data.gameId,
topicId: parsed.data.topicId,
page: parsed.data.page,
limit: parsed.data.limit,
currentUserId: req.session?.userId || '',
@@ -341,15 +339,13 @@ router.get('/tierlists/stats', requireAdmin, async (req, res) => {
const schema = z.object({
q: z.string().trim().max(120).optional().default(''),
topicId: z.string().trim().max(120).optional().default(''),
gameId: z.string().trim().max(120).optional().default(''),
})
const parsed = schema.safeParse(req.query)
if (!parsed.success) return res.status(400).json({ error: 'bad_request' })
const result = await summarizeAdminTierLists({
queryText: parsed.data.q,
topicId: parsed.data.topicId || parsed.data.gameId,
gameId: parsed.data.gameId,
topicId: parsed.data.topicId,
})
res.json(result)
})
@@ -646,13 +642,13 @@ router.delete('/custom-items/:itemId', requireAdmin, async (req, res) => {
router.post('/custom-items/:itemId/promote', requireAdmin, async (req, res) => {
const schema = z.object({
gameId: z.string().min(1),
topicId: z.string().min(1),
})
const parsed = schema.safeParse(req.body)
if (!parsed.success) return res.status(400).json({ error: 'bad_request' })
const template = await findTopicById(parsed.data.gameId)
if (!template) return res.status(404).json({ error: 'game_not_found' })
const template = await findTopicById(parsed.data.topicId)
if (!template) return res.status(404).json({ error: 'topic_not_found' })
const customItem = await findCustomItemById(req.params.itemId)
const templateItem = customItem ? null : await findTopicItemById(req.params.itemId)
@@ -675,14 +671,14 @@ router.post('/custom-items/:itemId/promote', requireAdmin, async (req, res) => {
router.post('/tierlists/:tierListId/promote-items', requireAdmin, async (req, res) => {
const schema = z.object({
gameId: z.string().min(1),
topicId: z.string().min(1),
itemIds: z.array(z.string().min(1)).optional().default([]),
})
const parsed = schema.safeParse(req.body)
if (!parsed.success) return res.status(400).json({ error: 'bad_request' })
const template = await findTopicById(parsed.data.gameId)
if (!template) return res.status(404).json({ error: 'game_not_found' })
const template = await findTopicById(parsed.data.topicId)
if (!template) return res.status(404).json({ error: 'topic_not_found' })
const tierList = await findTierListById(req.params.tierListId)
if (!tierList) return res.status(404).json({ error: 'not_found' })
@@ -697,15 +693,15 @@ router.post('/tierlists/:tierListId/promote-items', requireAdmin, async (req, re
router.post('/tierlists/:tierListId/create-game-template', requireAdmin, async (req, res) => {
const schema = z.object({
gameId: z.string().trim().min(1).max(120),
topicId: z.string().trim().min(1).max(120),
name: z.string().trim().min(1).max(120),
itemIds: z.array(z.string().min(1)).optional().default([]),
})
const parsed = schema.safeParse(req.body)
if (!parsed.success) return res.status(400).json({ error: 'bad_request' })
const exists = await findTopicById(parsed.data.gameId)
if (exists) return res.status(409).json({ error: 'game_id_taken' })
const exists = await findTopicById(parsed.data.topicId)
if (exists) return res.status(409).json({ error: 'topic_id_taken' })
const tierList = await findTierListById(req.params.tierListId)
if (!tierList) return res.status(404).json({ error: 'not_found' })
@@ -715,7 +711,7 @@ router.post('/tierlists/:tierListId/create-game-template', requireAdmin, async (
...tierList,
pool: parsed.data.itemIds.length ? (tierList.pool || []).filter((item) => parsed.data.itemIds.includes(item.id)) : tierList.pool,
},
templateId: parsed.data.gameId,
templateId: parsed.data.topicId,
templateName: parsed.data.name,
})
res.json(result)
@@ -755,9 +751,9 @@ router.post('/template-requests/:requestId/approve', requireAdmin, async (req, r
if (templateRequest.status !== 'pending') return res.status(409).json({ error: 'request_already_handled' })
if (templateRequest.type === 'update') {
const targetGameId = templateRequest.targetGameId || templateRequest.sourceGameId
const template = await findTopicById(targetGameId)
if (!template) return res.status(404).json({ error: 'game_not_found' })
const targetTopicId = templateRequest.targetTopicId || templateRequest.sourceTopicId
const template = await findTopicById(targetTopicId)
if (!template) return res.status(404).json({ error: 'topic_not_found' })
const items = await promoteSnapshotItemsToTemplate({
items: templateRequest.items || [],
@@ -768,18 +764,18 @@ router.post('/template-requests/:requestId/approve', requireAdmin, async (req, r
}
const schema = z.object({
gameId: z.string().trim().min(1).max(120),
topicId: z.string().trim().min(1).max(120),
name: z.string().trim().min(1).max(120),
})
const parsed = schema.safeParse(req.body)
if (!parsed.success) return res.status(400).json({ error: 'bad_request' })
const exists = await findTopicById(parsed.data.gameId)
if (exists) return res.status(409).json({ error: 'game_id_taken' })
const exists = await findTopicById(parsed.data.topicId)
if (exists) return res.status(409).json({ error: 'topic_id_taken' })
const result = await createTemplateFromRequest({
templateRequest,
templateId: parsed.data.gameId,
templateId: parsed.data.topicId,
templateName: parsed.data.name,
})
const request = await updateTemplateRequestStatus({ id: templateRequest.id, status: 'approved' })
@@ -793,10 +789,10 @@ router.post('/template-requests/:requestId/review', requireAdmin, async (req, re
return res.status(409).json({ error: 'request_already_handled' })
}
if (templateRequest.type === 'create' && templateRequest.targetGameId && !templateRequest.targetGameName) {
templateRequest = await updateTemplateRequestTargetGame({
if (templateRequest.type === 'create' && templateRequest.targetTopicId && !templateRequest.targetTopicName) {
templateRequest = await updateTemplateRequestTargetTopic({
id: templateRequest.id,
targetGameId: '',
targetTopicId: '',
})
}
@@ -810,7 +806,7 @@ router.post('/template-requests/:requestId/review', requireAdmin, async (req, re
router.post('/template-requests/:requestId/link-game', requireAdmin, async (req, res) => {
const schema = z.object({
gameId: z.string().trim().min(1).max(120),
topicId: z.string().trim().min(1).max(120),
})
const parsed = schema.safeParse(req.body)
if (!parsed.success) return res.status(400).json({ error: 'bad_request' })
@@ -822,19 +818,19 @@ router.post('/template-requests/:requestId/link-game', requireAdmin, async (req,
return res.status(409).json({ error: 'request_already_handled' })
}
const template = await findTopicById(parsed.data.gameId)
if (!template) return res.status(404).json({ error: 'game_not_found' })
const template = await findTopicById(parsed.data.topicId)
if (!template) return res.status(404).json({ error: 'topic_not_found' })
const request = await updateTemplateRequestTargetGame({
const request = await updateTemplateRequestTargetTopic({
id: templateRequest.id,
targetGameId: template.id,
targetTopicId: template.id,
})
res.json({ request })
})
router.post('/template-requests/:requestId/promote-items', requireAdmin, async (req, res) => {
const schema = z.object({
gameId: z.string().trim().min(1).max(120),
topicId: z.string().trim().min(1).max(120),
itemIds: z.array(z.string().min(1)).optional().default([]),
itemSrcs: z.array(z.string().min(1)).optional().default([]),
itemLabels: z.record(z.string(), z.string().min(1).max(60)).optional().default({}),
@@ -848,8 +844,8 @@ router.post('/template-requests/:requestId/promote-items', requireAdmin, async (
return res.status(409).json({ error: 'request_already_handled' })
}
const template = await findTopicById(parsed.data.gameId)
if (!template) return res.status(404).json({ error: 'game_not_found' })
const template = await findTopicById(parsed.data.topicId)
if (!template) return res.status(404).json({ error: 'topic_not_found' })
const promotableItems = pickTemplateRequestItems(templateRequest, parsed.data.itemIds, parsed.data.itemLabels, parsed.data.itemSrcs)
if (!promotableItems.length) {
@@ -865,7 +861,7 @@ router.post('/template-requests/:requestId/promote-items', requireAdmin, async (
} catch (error) {
console.error('[admin] template request promote-items failed', {
requestId: templateRequest.id,
gameId: template.id,
topicId: template.id,
itemCount: promotableItems.length,
message: error?.message || 'unknown_error',
code: error?.code || '',

View File

@@ -61,7 +61,6 @@ const thumbnailUpload = createMemoryUpload(multer, { fileSize: 8 * 1024 * 1024 }
const templateRequestSchema = z.object({
type: z.enum(['create', 'update']),
sourceTierListId: z.string().max(64).optional().default(''),
gameId: z.string().min(1).max(120).optional(),
topicId: z.string().min(1).max(120).optional(),
requestTitle: z.string().trim().min(1).max(120),
requestDescription: z.string().trim().min(1).max(1000),
@@ -74,7 +73,7 @@ const templateRequestSchema = z.object({
name: z.string().min(1).max(16),
itemIds: z.array(z.string()).optional().default([]),
}).passthrough().superRefine((value, ctx) => {
if (!(value.topicId || value.gameId)) {
if (!value.topicId) {
ctx.addIssue({ code: z.ZodIssueCode.custom, message: 'topicId_required', path: ['topicId'] })
}
})
@@ -91,7 +90,6 @@ const templateRequestSchema = z.object({
const tierListUpsertSchema = z.object({
id: z.string().optional(),
gameId: z.string().min(1).optional(),
topicId: z.string().min(1).optional(),
title: z.string().min(1).max(120),
thumbnailSrc: z.string().max(255).optional().default(''),
@@ -118,13 +116,13 @@ const tierListUpsertSchema = z.object({
})
),
}).superRefine((value, ctx) => {
if (!(value.topicId || value.gameId)) {
if (!value.topicId) {
ctx.addIssue({ code: z.ZodIssueCode.custom, message: 'topicId_required', path: ['topicId'] })
}
})
router.get('/public', async (req, res) => {
const topicId = typeof req.query.topicId === 'string' ? req.query.topicId : req.query.gameId
const topicId = typeof req.query.topicId === 'string' ? req.query.topicId : ''
const queryText = typeof req.query.q === 'string' ? req.query.q : ''
const lists = await listPublicTierLists(topicId, req.session?.userId || '', queryText)
res.json({ tierLists: lists })
@@ -236,7 +234,7 @@ router.post('/template-request', requireAuth, async (req, res) => {
if (!parsed.success) return res.status(400).json({ error: 'bad_request' })
const payload = parsed.data
const topicId = payload.topicId || payload.gameId
const topicId = payload.topicId
const normalizedBoardItems = payload.boardItems.map(normalizePoolItem)
const customItems = normalizedBoardItems.filter((item) => item?.origin === 'custom')
if (!customItems.length) return res.status(400).json({ error: 'custom_items_required' })
@@ -285,7 +283,7 @@ router.post('/', requireAuth, async (req, res) => {
const parsed = tierListUpsertSchema.safeParse(req.body)
if (!parsed.success) return res.status(400).json({ error: 'bad_request' })
const payload = parsed.data
const topicId = payload.topicId || payload.gameId
const topicId = payload.topicId
const normalizedPool = payload.pool.map(normalizePoolItem)
let existing = null
@@ -296,7 +294,7 @@ router.post('/', requireAuth, async (req, res) => {
const updated = await saveTierList({
id: existing.id,
authorId: existing.authorId,
topicId: existing.topicId || existing.gameId,
topicId: existing.topicId,
title: payload.title,
thumbnailSrc: payload.thumbnailSrc || '',
description: payload.description || '',