릴리스: v1.4.20 백엔드 topic 이름층 정리
This commit is contained in:
@@ -737,7 +737,7 @@ async function adminDeleteUser(id) {
|
||||
await query('DELETE FROM users WHERE id = ?', [id])
|
||||
}
|
||||
|
||||
async function listGames(currentUserId = '', options = {}) {
|
||||
async function listTopics(currentUserId = '', options = {}) {
|
||||
const includePrivate = !!options.includePrivate
|
||||
const rows = await query(
|
||||
`
|
||||
@@ -753,23 +753,23 @@ async function listGames(currentUserId = '', options = {}) {
|
||||
`,
|
||||
[FREEFORM_TOPIC_ID]
|
||||
)
|
||||
const games = rows.map(mapGameRow)
|
||||
if (!currentUserId) return games.map((game) => ({ ...game, isFavorited: false }))
|
||||
const topics = rows.map(mapGameRow)
|
||||
if (!currentUserId) return topics.map((topic) => ({ ...topic, isFavorited: false }))
|
||||
|
||||
const favoriteRows = await query('SELECT topic_id FROM favorite_topics WHERE user_id = ?', [currentUserId])
|
||||
const favoriteSet = new Set(favoriteRows.map((row) => row.topic_id))
|
||||
return games.map((game) => ({
|
||||
...game,
|
||||
isFavorited: favoriteSet.has(game.id),
|
||||
return topics.map((topic) => ({
|
||||
...topic,
|
||||
isFavorited: favoriteSet.has(topic.id),
|
||||
}))
|
||||
}
|
||||
|
||||
async function findGameById(id) {
|
||||
async function findTopicById(id) {
|
||||
const rows = await query('SELECT id, name, thumbnail_src, is_public, display_rank, created_at FROM topics WHERE id = ? LIMIT 1', [id])
|
||||
return mapGameRow(rows[0])
|
||||
}
|
||||
|
||||
async function listGameItems(gameId) {
|
||||
async function listTopicItems(topicId) {
|
||||
const rows = await query(
|
||||
`
|
||||
SELECT id, topic_id, src, label, display_order, created_at
|
||||
@@ -781,24 +781,24 @@ async function listGameItems(gameId) {
|
||||
created_at DESC,
|
||||
id DESC
|
||||
`,
|
||||
[gameId]
|
||||
[topicId]
|
||||
)
|
||||
return rows.map(mapGameItemRow)
|
||||
}
|
||||
|
||||
async function findGameItemById(itemId) {
|
||||
async function findTopicItemById(itemId) {
|
||||
const rows = await query('SELECT id, topic_id, src, label, display_order, created_at FROM topic_items WHERE id = ? LIMIT 1', [itemId])
|
||||
return mapGameItemRow(rows[0])
|
||||
}
|
||||
|
||||
async function getGameDetail(gameId) {
|
||||
const game = await findGameById(gameId)
|
||||
if (!game) return null
|
||||
const items = await listGameItems(gameId)
|
||||
return { game, items }
|
||||
async function getTopicDetail(topicId) {
|
||||
const topic = await findTopicById(topicId)
|
||||
if (!topic) return null
|
||||
const items = await listTopicItems(topicId)
|
||||
return { topic, game: topic, items }
|
||||
}
|
||||
|
||||
async function createGame({ id, name, isPublic = true }) {
|
||||
async function createTopic({ id, name, isPublic = true }) {
|
||||
await query('INSERT INTO topics (id, name, thumbnail_src, is_public, display_rank, created_at) VALUES (?, ?, ?, ?, ?, ?)', [
|
||||
id,
|
||||
name,
|
||||
@@ -807,17 +807,17 @@ async function createGame({ id, name, isPublic = true }) {
|
||||
null,
|
||||
now(),
|
||||
])
|
||||
return findGameById(id)
|
||||
return findTopicById(id)
|
||||
}
|
||||
|
||||
async function updateGameThumbnail(gameId, thumbnailSrc) {
|
||||
await query('UPDATE topics SET thumbnail_src = ? WHERE id = ?', [thumbnailSrc, gameId])
|
||||
return findGameById(gameId)
|
||||
async function updateTopicThumbnail(topicId, thumbnailSrc) {
|
||||
await query('UPDATE topics SET thumbnail_src = ? WHERE id = ?', [thumbnailSrc, topicId])
|
||||
return findTopicById(topicId)
|
||||
}
|
||||
|
||||
async function updateGameVisibility(gameId, isPublic) {
|
||||
await query('UPDATE topics SET is_public = ? WHERE id = ?', [isPublic ? 1 : 0, gameId])
|
||||
return findGameById(gameId)
|
||||
async function updateTopicVisibility(topicId, isPublic) {
|
||||
await query('UPDATE topics SET is_public = ? WHERE id = ?', [isPublic ? 1 : 0, topicId])
|
||||
return findTopicById(topicId)
|
||||
}
|
||||
|
||||
async function findImageAssetByHash(contentHash) {
|
||||
@@ -1347,14 +1347,15 @@ async function clearImageOptimizationJobs({ month } = {}) {
|
||||
const result = await query('DELETE FROM image_optimization_jobs')
|
||||
return Number(result.affectedRows || 0)
|
||||
}
|
||||
async function createGameItem({ id, gameId, src, label }) {
|
||||
async function createTopicItem({ id, topicId, gameId = topicId, src, label }) {
|
||||
const createdAt = now()
|
||||
const minOrderRows = await query('SELECT MIN(display_order) AS min_display_order FROM topic_items WHERE topic_id = ?', [gameId])
|
||||
const resolvedTopicId = topicId || gameId
|
||||
const minOrderRows = await query('SELECT MIN(display_order) AS min_display_order FROM topic_items WHERE topic_id = ?', [resolvedTopicId])
|
||||
const nextDisplayOrder =
|
||||
minOrderRows[0]?.min_display_order == null ? 0 : Number(minOrderRows[0].min_display_order) - 1
|
||||
await query('INSERT INTO topic_items (id, topic_id, src, label, display_order, created_at) VALUES (?, ?, ?, ?, ?, ?)', [
|
||||
id,
|
||||
gameId,
|
||||
resolvedTopicId,
|
||||
src,
|
||||
label,
|
||||
nextDisplayOrder,
|
||||
@@ -1364,25 +1365,25 @@ async function createGameItem({ id, gameId, src, label }) {
|
||||
return mapGameItemRow(rows[0])
|
||||
}
|
||||
|
||||
async function updateGameItemLabel(itemId, label) {
|
||||
async function updateTopicItemLabel(itemId, label) {
|
||||
await query('UPDATE topic_items SET label = ? WHERE id = ?', [label, itemId])
|
||||
const rows = await query('SELECT id, topic_id, src, label, display_order, created_at FROM topic_items WHERE id = ? LIMIT 1', [itemId])
|
||||
return mapGameItemRow(rows[0])
|
||||
}
|
||||
|
||||
async function updateGameItemDisplayOrder(gameId, itemIds) {
|
||||
async function updateTopicItemDisplayOrder(topicId, itemIds) {
|
||||
const normalizedIds = Array.from(new Set((itemIds || []).filter(Boolean)))
|
||||
const existingItems = await listGameItems(gameId)
|
||||
const existingItems = await listTopicItems(topicId)
|
||||
const existingIdSet = new Set(existingItems.map((item) => item.id))
|
||||
const orderedIds = normalizedIds.filter((id) => existingIdSet.has(id))
|
||||
const remainingIds = existingItems.map((item) => item.id).filter((id) => !orderedIds.includes(id))
|
||||
const finalIds = [...orderedIds, ...remainingIds]
|
||||
|
||||
await Promise.all(
|
||||
finalIds.map((itemId, index) => query('UPDATE topic_items SET display_order = ? WHERE id = ? AND topic_id = ?', [index + 1, itemId, gameId]))
|
||||
finalIds.map((itemId, index) => query('UPDATE topic_items SET display_order = ? WHERE id = ? AND topic_id = ?', [index + 1, itemId, topicId]))
|
||||
)
|
||||
|
||||
return listGameItems(gameId)
|
||||
return listTopicItems(topicId)
|
||||
}
|
||||
|
||||
async function updateCustomItemLabel(itemId, label) {
|
||||
@@ -1413,7 +1414,7 @@ async function updateImageAssetLabel(assetId, label) {
|
||||
return mapImageAssetRow(rows[0])
|
||||
}
|
||||
|
||||
async function countTierListsUsingGameItem(itemId) {
|
||||
async function countTierListsUsingTopicItem(itemId) {
|
||||
if (!itemId) return { totalCount: 0, publicCount: 0, privateCount: 0 }
|
||||
|
||||
const rows = await query(
|
||||
@@ -1441,16 +1442,16 @@ async function countTierListsUsingGameItem(itemId) {
|
||||
return { totalCount, publicCount, privateCount }
|
||||
}
|
||||
|
||||
async function deleteGameItem(itemId) {
|
||||
async function deleteTopicItem(itemId) {
|
||||
await query('DELETE FROM topic_items WHERE id = ?', [itemId])
|
||||
}
|
||||
|
||||
async function deleteGame(gameId) {
|
||||
await query('DELETE FROM topics WHERE id = ?', [gameId])
|
||||
async function deleteTopic(topicId) {
|
||||
await query('DELETE FROM topics WHERE id = ?', [topicId])
|
||||
}
|
||||
|
||||
async function updateGameDisplayOrder(gameIds) {
|
||||
const normalizedIds = Array.from(new Set((gameIds || []).filter((id) => id && id !== FREEFORM_TOPIC_ID))).slice(0, 50)
|
||||
async function updateTopicDisplayOrder(topicIds) {
|
||||
const normalizedIds = Array.from(new Set((topicIds || []).filter((id) => id && id !== FREEFORM_TOPIC_ID))).slice(0, 50)
|
||||
|
||||
await query('UPDATE topics SET display_rank = NULL WHERE id <> ?', [FREEFORM_TOPIC_ID])
|
||||
|
||||
@@ -1460,7 +1461,7 @@ async function updateGameDisplayOrder(gameIds) {
|
||||
)
|
||||
)
|
||||
|
||||
return listGames()
|
||||
return listTopics()
|
||||
}
|
||||
|
||||
async function createCustomItem({ id, ownerId, src, label }) {
|
||||
@@ -1475,6 +1476,22 @@ async function createCustomItem({ id, ownerId, src, label }) {
|
||||
return { id, ownerId, src, label, origin: 'custom', createdAt }
|
||||
}
|
||||
|
||||
const listGames = listTopics
|
||||
const findGameById = findTopicById
|
||||
const listGameItems = listTopicItems
|
||||
const findGameItemById = findTopicItemById
|
||||
const getGameDetail = getTopicDetail
|
||||
const createGame = createTopic
|
||||
const updateGameThumbnail = updateTopicThumbnail
|
||||
const updateGameVisibility = updateTopicVisibility
|
||||
const createGameItem = createTopicItem
|
||||
const updateGameItemLabel = updateTopicItemLabel
|
||||
const updateGameItemDisplayOrder = updateTopicItemDisplayOrder
|
||||
const countTierListsUsingGameItem = countTierListsUsingTopicItem
|
||||
const deleteGameItem = deleteTopicItem
|
||||
const deleteGame = deleteTopic
|
||||
const updateGameDisplayOrder = updateTopicDisplayOrder
|
||||
|
||||
async function syncOwnedCustomItemLabels({ ownerId, items }) {
|
||||
const customItems = Array.from(
|
||||
new Map(
|
||||
@@ -2511,14 +2528,19 @@ async function unfavoriteTierList({ userId, tierListId }) {
|
||||
await query('DELETE FROM favorite_tierlists WHERE user_id = ? AND tierlist_id = ?', [userId, tierListId])
|
||||
}
|
||||
|
||||
async function favoriteGame({ userId, gameId }) {
|
||||
await query('INSERT IGNORE INTO favorite_topics (user_id, topic_id, created_at) VALUES (?, ?, ?)', [userId, gameId, now()])
|
||||
async function favoriteTopic({ userId, topicId, gameId = topicId }) {
|
||||
const resolvedTopicId = topicId || gameId
|
||||
await query('INSERT IGNORE INTO favorite_topics (user_id, topic_id, created_at) VALUES (?, ?, ?)', [userId, resolvedTopicId, now()])
|
||||
}
|
||||
|
||||
async function unfavoriteGame({ userId, gameId }) {
|
||||
await query('DELETE FROM favorite_topics WHERE user_id = ? AND topic_id = ?', [userId, gameId])
|
||||
async function unfavoriteTopic({ userId, topicId, gameId = topicId }) {
|
||||
const resolvedTopicId = topicId || gameId
|
||||
await query('DELETE FROM favorite_topics WHERE user_id = ? AND topic_id = ?', [userId, resolvedTopicId])
|
||||
}
|
||||
|
||||
const favoriteGame = favoriteTopic
|
||||
const unfavoriteGame = unfavoriteTopic
|
||||
|
||||
module.exports = {
|
||||
DB_NAME,
|
||||
ensureData,
|
||||
@@ -2533,6 +2555,14 @@ module.exports = {
|
||||
adminUpdateUser,
|
||||
adminUpdateUserPassword,
|
||||
adminDeleteUser,
|
||||
listTopics,
|
||||
findTopicById,
|
||||
listTopicItems,
|
||||
findTopicItemById,
|
||||
getTopicDetail,
|
||||
createTopic,
|
||||
updateTopicThumbnail,
|
||||
updateTopicVisibility,
|
||||
listGames,
|
||||
findGameById,
|
||||
listGameItems,
|
||||
@@ -2557,6 +2587,13 @@ module.exports = {
|
||||
clearImageOptimizationJobs,
|
||||
getImageAssetStats,
|
||||
cleanupMissingUploadReferences,
|
||||
createTopicItem,
|
||||
updateTopicItemLabel,
|
||||
updateTopicItemDisplayOrder,
|
||||
countTierListsUsingTopicItem,
|
||||
deleteTopicItem,
|
||||
deleteTopic,
|
||||
updateTopicDisplayOrder,
|
||||
createGameItem,
|
||||
updateGameItemLabel,
|
||||
updateGameItemDisplayOrder,
|
||||
@@ -2577,6 +2614,8 @@ module.exports = {
|
||||
summarizeAdminTierLists,
|
||||
findTierListById,
|
||||
updateAdminTierListMeta,
|
||||
favoriteTopic,
|
||||
unfavoriteTopic,
|
||||
favoriteTierList,
|
||||
unfavoriteTierList,
|
||||
favoriteGame,
|
||||
|
||||
@@ -7,24 +7,24 @@ const { z } = require('zod')
|
||||
const { nanoid } = require('nanoid')
|
||||
const {
|
||||
findUserById,
|
||||
findGameById,
|
||||
findGameItemById,
|
||||
listGameItems,
|
||||
findTopicById,
|
||||
findTopicItemById,
|
||||
listTopicItems,
|
||||
findImageAssetById,
|
||||
createGame,
|
||||
listGames,
|
||||
updateGameThumbnail,
|
||||
updateGameVisibility,
|
||||
createGameItem,
|
||||
updateGameItemLabel,
|
||||
updateGameItemDisplayOrder,
|
||||
countTierListsUsingGameItem,
|
||||
createTopic,
|
||||
listTopics,
|
||||
updateTopicThumbnail,
|
||||
updateTopicVisibility,
|
||||
createTopicItem,
|
||||
updateTopicItemLabel,
|
||||
updateTopicItemDisplayOrder,
|
||||
countTierListsUsingTopicItem,
|
||||
updateCustomItemLabel,
|
||||
updateImageAssetLabel,
|
||||
deleteGameItem,
|
||||
deleteGame,
|
||||
deleteTopicItem,
|
||||
deleteTopic,
|
||||
deleteTierList,
|
||||
updateGameDisplayOrder,
|
||||
updateTopicDisplayOrder,
|
||||
listCustomItems,
|
||||
findCustomItemById,
|
||||
findUnusedCustomItems,
|
||||
@@ -124,15 +124,15 @@ router.post(['/games', '/templates'], requireAdmin, async (req, res) => {
|
||||
})
|
||||
const parsed = schema.safeParse(req.body)
|
||||
if (!parsed.success) return res.status(400).json({ error: 'bad_request' })
|
||||
const exists = await findGameById(parsed.data.id)
|
||||
const exists = await findTopicById(parsed.data.id)
|
||||
if (exists) return res.status(409).json({ error: 'game_id_taken' })
|
||||
const game = await createGame({ id: parsed.data.id, name: parsed.data.name, isPublic: parsed.data.isPublic })
|
||||
const template = await createTopic({ id: parsed.data.id, name: parsed.data.name, isPublic: parsed.data.isPublic })
|
||||
if (parsed.data.thumbnailSrc) {
|
||||
const copiedThumb = await copyUploadIntoGameAsset(parsed.data.thumbnailSrc)
|
||||
await updateGameThumbnail(game.id, copiedThumb)
|
||||
await updateTopicThumbnail(template.id, copiedThumb)
|
||||
}
|
||||
const template = await findGameById(game.id)
|
||||
res.json({ game: template, template })
|
||||
const savedTemplate = await findTopicById(template.id)
|
||||
res.json({ game: savedTemplate, template: savedTemplate })
|
||||
})
|
||||
|
||||
router.patch(['/games/:gameId', '/templates/:templateId'], requireAdmin, async (req, res) => {
|
||||
@@ -143,10 +143,10 @@ router.patch(['/games/:gameId', '/templates/:templateId'], requireAdmin, async (
|
||||
if (!parsed.success) return res.status(400).json({ error: 'bad_request' })
|
||||
|
||||
const templateId = getTemplateIdParam(req)
|
||||
const game = await findGameById(templateId)
|
||||
if (!game) return res.status(404).json({ error: 'not_found' })
|
||||
const template = await findTopicById(templateId)
|
||||
if (!template) return res.status(404).json({ error: 'not_found' })
|
||||
|
||||
const updated = await updateGameVisibility(game.id, parsed.data.isPublic)
|
||||
const updated = await updateTopicVisibility(template.id, parsed.data.isPublic)
|
||||
res.json({ game: updated, template: updated })
|
||||
})
|
||||
|
||||
@@ -157,10 +157,10 @@ router.patch(['/games/display-order', '/templates/display-order'], requireAdmin,
|
||||
const parsed = schema.safeParse(req.body)
|
||||
if (!parsed.success) return res.status(400).json({ error: 'bad_request' })
|
||||
|
||||
const games = await listGames('', { includePrivate: true })
|
||||
const validGameIds = new Set(games.map((game) => game.id))
|
||||
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 updateGameDisplayOrder(filteredIds)
|
||||
const updatedGames = await updateTopicDisplayOrder(filteredIds)
|
||||
res.json({ games: updatedGames, templates: updatedGames })
|
||||
})
|
||||
|
||||
@@ -172,18 +172,18 @@ router.patch(['/games/:gameId/items/display-order', '/templates/:templateId/item
|
||||
if (!parsed.success) return res.status(400).json({ error: 'bad_request' })
|
||||
|
||||
const templateId = getTemplateIdParam(req)
|
||||
const game = await findGameById(templateId)
|
||||
if (!game) return res.status(404).json({ error: 'not_found' })
|
||||
const template = await findTopicById(templateId)
|
||||
if (!template) return res.status(404).json({ error: 'not_found' })
|
||||
|
||||
const items = await updateGameItemDisplayOrder(game.id, parsed.data.itemIds)
|
||||
const items = await updateTopicItemDisplayOrder(template.id, parsed.data.itemIds)
|
||||
res.json({ items })
|
||||
})
|
||||
|
||||
router.post(['/games/:gameId/thumbnail', '/templates/:templateId/thumbnail'], requireAdmin, upload.single('thumbnail'), async (req, res) => {
|
||||
if (!req.file) return res.status(400).json({ error: 'file_required' })
|
||||
const templateId = getTemplateIdParam(req)
|
||||
const game = await findGameById(templateId)
|
||||
if (!game) return res.status(404).json({ error: 'not_found' })
|
||||
const template = await findTopicById(templateId)
|
||||
if (!template) return res.status(404).json({ error: 'not_found' })
|
||||
|
||||
const optimized = await writeOptimizedImage({
|
||||
file: req.file,
|
||||
@@ -194,7 +194,7 @@ router.post(['/games/:gameId/thumbnail', '/templates/:templateId/thumbnail'], re
|
||||
quality: 84,
|
||||
})
|
||||
|
||||
const updated = await updateGameThumbnail(templateId, optimized.src)
|
||||
const updated = await updateTopicThumbnail(templateId, optimized.src)
|
||||
res.json({ game: updated, template: updated })
|
||||
})
|
||||
|
||||
@@ -202,8 +202,8 @@ router.post(['/games/:gameId/images', '/templates/:templateId/images'], requireA
|
||||
const files = Array.isArray(req.files) ? req.files : []
|
||||
if (!files.length) return res.status(400).json({ error: 'file_required' })
|
||||
const templateId = getTemplateIdParam(req)
|
||||
const game = await findGameById(templateId)
|
||||
if (!game) return res.status(404).json({ error: 'not_found' })
|
||||
const template = await findTopicById(templateId)
|
||||
if (!template) return res.status(404).json({ error: 'not_found' })
|
||||
|
||||
const labelsRaw = req.body?.labels
|
||||
const labels = Array.isArray(labelsRaw) ? labelsRaw : labelsRaw ? [labelsRaw] : []
|
||||
@@ -221,9 +221,9 @@ router.post(['/games/:gameId/images', '/templates/:templateId/images'], requireA
|
||||
quality: 84,
|
||||
})
|
||||
|
||||
return createGameItem({
|
||||
return createTopicItem({
|
||||
id: nanoid(),
|
||||
gameId: game.id,
|
||||
topicId: template.id,
|
||||
src: optimized.src,
|
||||
label: normalizedLabels[index] || buildItemLabelFromFilename(file),
|
||||
})
|
||||
@@ -234,18 +234,18 @@ router.post(['/games/:gameId/images', '/templates/:templateId/images'], requireA
|
||||
})
|
||||
|
||||
router.delete(['/games/:gameId/items/:itemId', '/templates/:templateId/items/:itemId'], requireAdmin, async (req, res) => {
|
||||
const game = await findGameById(getTemplateIdParam(req))
|
||||
if (!game) return res.status(404).json({ error: 'not_found' })
|
||||
await deleteGameItem(req.params.itemId)
|
||||
const template = await findTopicById(getTemplateIdParam(req))
|
||||
if (!template) return res.status(404).json({ error: 'not_found' })
|
||||
await deleteTopicItem(req.params.itemId)
|
||||
res.json({ ok: true })
|
||||
})
|
||||
|
||||
router.get(['/games/:gameId/items/:itemId/usage', '/templates/:templateId/items/:itemId/usage'], requireAdmin, async (req, res) => {
|
||||
const game = await findGameById(getTemplateIdParam(req))
|
||||
if (!game) return res.status(404).json({ error: 'not_found' })
|
||||
const item = await findGameItemById(req.params.itemId)
|
||||
if (!item || item.gameId !== game.id) return res.status(404).json({ error: 'not_found' })
|
||||
const usage = await countTierListsUsingGameItem(req.params.itemId)
|
||||
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' })
|
||||
const usage = await countTierListsUsingTopicItem(req.params.itemId)
|
||||
res.json({ usage })
|
||||
})
|
||||
|
||||
@@ -254,19 +254,19 @@ router.patch(['/games/:gameId/items/:itemId', '/templates/:templateId/items/:ite
|
||||
const parsed = schema.safeParse(req.body)
|
||||
if (!parsed.success) return res.status(400).json({ error: 'bad_request' })
|
||||
|
||||
const game = await findGameById(getTemplateIdParam(req))
|
||||
if (!game) return res.status(404).json({ error: 'not_found' })
|
||||
const template = await findTopicById(getTemplateIdParam(req))
|
||||
if (!template) return res.status(404).json({ error: 'not_found' })
|
||||
|
||||
const updated = await updateGameItemLabel(req.params.itemId, parsed.data.label)
|
||||
if (!updated || updated.gameId !== game.id) 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' })
|
||||
res.json({ item: updated })
|
||||
})
|
||||
|
||||
router.delete(['/games/:gameId', '/templates/:templateId'], requireAdmin, async (req, res) => {
|
||||
const templateId = getTemplateIdParam(req)
|
||||
const game = await findGameById(templateId)
|
||||
if (!game) return res.status(404).json({ error: 'not_found' })
|
||||
await deleteGame(templateId)
|
||||
const template = await findTopicById(templateId)
|
||||
if (!template) return res.status(404).json({ error: 'not_found' })
|
||||
await deleteTopic(templateId)
|
||||
res.json({ ok: true })
|
||||
})
|
||||
|
||||
@@ -286,7 +286,7 @@ router.patch('/custom-items/:itemId/label', requireAdmin, async (req, res) => {
|
||||
}
|
||||
|
||||
if (parsed.data.sourceType === 'template') {
|
||||
const updated = await updateGameItemLabel(itemId, parsed.data.label)
|
||||
const updated = await updateTopicItemLabel(itemId, parsed.data.label)
|
||||
if (!updated) return res.status(404).json({ error: 'not_found' })
|
||||
return res.json({ item: updated })
|
||||
}
|
||||
@@ -464,10 +464,10 @@ async function removeCustomItemFiles(items) {
|
||||
)
|
||||
}
|
||||
|
||||
async function promoteLibraryItemToGameItem({ item, gameId }) {
|
||||
return createGameItem({
|
||||
async function promoteLibraryItemToTemplateItem({ item, templateId }) {
|
||||
return createTopicItem({
|
||||
id: nanoid(),
|
||||
gameId,
|
||||
topicId: templateId,
|
||||
src: item.src || '',
|
||||
label: item.label,
|
||||
})
|
||||
@@ -504,7 +504,7 @@ function uniqueTierListPoolItems(tierList) {
|
||||
})
|
||||
}
|
||||
|
||||
async function promoteTierListItemsToGame({ tierList, gameId, itemIds = [] }) {
|
||||
async function promoteTierListItemsToTemplate({ tierList, templateId, itemIds = [] }) {
|
||||
const allowedIds = new Set((itemIds || []).filter(Boolean))
|
||||
const sourceItems = uniqueTierListPoolItems(tierList).filter((item) => item.origin === 'custom')
|
||||
const itemsToCopy = allowedIds.size ? sourceItems.filter((item) => allowedIds.has(item.id)) : sourceItems
|
||||
@@ -513,9 +513,9 @@ async function promoteTierListItemsToGame({ tierList, gameId, itemIds = [] }) {
|
||||
for (const item of itemsToCopy) {
|
||||
const copiedSrc = await copyUploadIntoGameAsset(item.src)
|
||||
createdItems.push(
|
||||
await createGameItem({
|
||||
await createTopicItem({
|
||||
id: nanoid(),
|
||||
gameId,
|
||||
topicId: templateId,
|
||||
src: copiedSrc,
|
||||
label: item.label,
|
||||
})
|
||||
@@ -525,8 +525,8 @@ async function promoteTierListItemsToGame({ tierList, gameId, itemIds = [] }) {
|
||||
return createdItems
|
||||
}
|
||||
|
||||
async function promoteSnapshotItemsToGame({ items, gameId }) {
|
||||
const existingItems = await listGameItems(gameId)
|
||||
async function promoteSnapshotItemsToTemplate({ items, templateId }) {
|
||||
const existingItems = await listTopicItems(templateId)
|
||||
const existingSrcs = new Set(
|
||||
existingItems
|
||||
.map((item) => (typeof item?.src === 'string' ? item.src.trim() : ''))
|
||||
@@ -538,9 +538,9 @@ async function promoteSnapshotItemsToGame({ items, gameId }) {
|
||||
const copiedSrc = await copyUploadIntoGameAsset(item.src)
|
||||
if (!copiedSrc || existingSrcs.has(copiedSrc)) continue
|
||||
createdItems.push(
|
||||
await createGameItem({
|
||||
await createTopicItem({
|
||||
id: nanoid(),
|
||||
gameId,
|
||||
topicId: templateId,
|
||||
src: copiedSrc,
|
||||
label: item.label,
|
||||
})
|
||||
@@ -577,43 +577,43 @@ function pickTemplateRequestItems(templateRequest, itemIds = [], itemLabels = {}
|
||||
})
|
||||
}
|
||||
|
||||
async function createGameTemplateFromTierList({ tierList, gameId, gameName }) {
|
||||
await createGame({ id: gameId, name: gameName, isPublic: false })
|
||||
async function createTemplateFromTierList({ tierList, templateId, templateName }) {
|
||||
await createTopic({ id: templateId, name: templateName, isPublic: false })
|
||||
if (tierList.thumbnailSrc) {
|
||||
const copiedThumb = await copyUploadIntoGameAsset(tierList.thumbnailSrc)
|
||||
await updateGameThumbnail(gameId, copiedThumb)
|
||||
await updateTopicThumbnail(templateId, copiedThumb)
|
||||
}
|
||||
|
||||
const createdItems = []
|
||||
for (const item of uniqueTierListPoolItems(tierList)) {
|
||||
const copiedSrc = await copyUploadIntoGameAsset(item.src)
|
||||
createdItems.push(
|
||||
await createGameItem({
|
||||
await createTopicItem({
|
||||
id: nanoid(),
|
||||
gameId,
|
||||
topicId: templateId,
|
||||
src: copiedSrc,
|
||||
label: item.label,
|
||||
})
|
||||
)
|
||||
}
|
||||
|
||||
return { game: await findGameById(gameId), items: createdItems }
|
||||
return { game: await findTopicById(templateId), items: createdItems }
|
||||
}
|
||||
|
||||
async function createGameTemplateFromRequest({ templateRequest, gameId, gameName }) {
|
||||
await createGame({ id: gameId, name: gameName, isPublic: false })
|
||||
async function createTemplateFromRequest({ templateRequest, templateId, templateName }) {
|
||||
await createTopic({ id: templateId, name: templateName, isPublic: false })
|
||||
|
||||
if (templateRequest.thumbnailSrc) {
|
||||
const copiedThumb = await copyUploadIntoGameAsset(templateRequest.thumbnailSrc)
|
||||
await updateGameThumbnail(gameId, copiedThumb)
|
||||
await updateTopicThumbnail(templateId, copiedThumb)
|
||||
}
|
||||
|
||||
const items = await promoteSnapshotItemsToGame({
|
||||
const items = await promoteSnapshotItemsToTemplate({
|
||||
items: templateRequest.items || [],
|
||||
gameId,
|
||||
templateId,
|
||||
})
|
||||
|
||||
return { game: await findGameById(gameId), items }
|
||||
return { game: await findTopicById(templateId), items }
|
||||
}
|
||||
|
||||
router.delete('/custom-items/:itemId', requireAdmin, async (req, res) => {
|
||||
@@ -630,7 +630,7 @@ router.delete('/custom-items/:itemId', requireAdmin, async (req, res) => {
|
||||
return res.json({ ok: true, sourceType: 'template-asset' })
|
||||
}
|
||||
|
||||
await deleteGameItem(target.id)
|
||||
await deleteTopicItem(target.id)
|
||||
return res.json({ ok: true, sourceType: 'template' })
|
||||
}
|
||||
|
||||
@@ -651,16 +651,16 @@ router.post('/custom-items/:itemId/promote', requireAdmin, async (req, res) => {
|
||||
const parsed = schema.safeParse(req.body)
|
||||
if (!parsed.success) return res.status(400).json({ error: 'bad_request' })
|
||||
|
||||
const game = await findGameById(parsed.data.gameId)
|
||||
if (!game) return res.status(404).json({ error: 'game_not_found' })
|
||||
const template = await findTopicById(parsed.data.gameId)
|
||||
if (!template) return res.status(404).json({ error: 'game_not_found' })
|
||||
|
||||
const customItem = await findCustomItemById(req.params.itemId)
|
||||
const gameItem = customItem ? null : await findGameItemById(req.params.itemId)
|
||||
const templateItem = customItem ? null : await findTopicItemById(req.params.itemId)
|
||||
const assetItemId = String(req.params.itemId || '')
|
||||
const imageAsset = !customItem && !gameItem && assetItemId.startsWith('asset:') ? await findImageAssetById(assetItemId.slice(6)) : null
|
||||
const imageAsset = !customItem && !templateItem && assetItemId.startsWith('asset:') ? await findImageAssetById(assetItemId.slice(6)) : null
|
||||
const sourceItem =
|
||||
customItem ||
|
||||
gameItem ||
|
||||
templateItem ||
|
||||
(imageAsset
|
||||
? {
|
||||
src: imageAsset.src || '',
|
||||
@@ -669,7 +669,7 @@ router.post('/custom-items/:itemId/promote', requireAdmin, async (req, res) => {
|
||||
: null)
|
||||
if (!sourceItem) return res.status(404).json({ error: 'not_found' })
|
||||
|
||||
const item = await promoteLibraryItemToGameItem({ item: sourceItem, gameId: game.id })
|
||||
const item = await promoteLibraryItemToTemplateItem({ item: sourceItem, templateId: template.id })
|
||||
res.json({ item })
|
||||
})
|
||||
|
||||
@@ -681,15 +681,15 @@ router.post('/tierlists/:tierListId/promote-items', requireAdmin, async (req, re
|
||||
const parsed = schema.safeParse(req.body)
|
||||
if (!parsed.success) return res.status(400).json({ error: 'bad_request' })
|
||||
|
||||
const game = await findGameById(parsed.data.gameId)
|
||||
if (!game) return res.status(404).json({ error: 'game_not_found' })
|
||||
const template = await findTopicById(parsed.data.gameId)
|
||||
if (!template) return res.status(404).json({ error: 'game_not_found' })
|
||||
|
||||
const tierList = await findTierListById(req.params.tierListId)
|
||||
if (!tierList) return res.status(404).json({ error: 'not_found' })
|
||||
|
||||
const items = await promoteTierListItemsToGame({
|
||||
const items = await promoteTierListItemsToTemplate({
|
||||
tierList,
|
||||
gameId: game.id,
|
||||
templateId: template.id,
|
||||
itemIds: parsed.data.itemIds,
|
||||
})
|
||||
res.json({ items })
|
||||
@@ -704,19 +704,19 @@ router.post('/tierlists/:tierListId/create-game-template', requireAdmin, async (
|
||||
const parsed = schema.safeParse(req.body)
|
||||
if (!parsed.success) return res.status(400).json({ error: 'bad_request' })
|
||||
|
||||
const exists = await findGameById(parsed.data.gameId)
|
||||
const exists = await findTopicById(parsed.data.gameId)
|
||||
if (exists) return res.status(409).json({ error: 'game_id_taken' })
|
||||
|
||||
const tierList = await findTierListById(req.params.tierListId)
|
||||
if (!tierList) return res.status(404).json({ error: 'not_found' })
|
||||
|
||||
const result = await createGameTemplateFromTierList({
|
||||
const result = await createTemplateFromTierList({
|
||||
tierList: {
|
||||
...tierList,
|
||||
pool: parsed.data.itemIds.length ? (tierList.pool || []).filter((item) => parsed.data.itemIds.includes(item.id)) : tierList.pool,
|
||||
},
|
||||
gameId: parsed.data.gameId,
|
||||
gameName: parsed.data.name,
|
||||
templateId: parsed.data.gameId,
|
||||
templateName: parsed.data.name,
|
||||
})
|
||||
res.json(result)
|
||||
})
|
||||
@@ -756,12 +756,12 @@ router.post('/template-requests/:requestId/approve', requireAdmin, async (req, r
|
||||
|
||||
if (templateRequest.type === 'update') {
|
||||
const targetGameId = templateRequest.targetGameId || templateRequest.sourceGameId
|
||||
const game = await findGameById(targetGameId)
|
||||
if (!game) return res.status(404).json({ error: 'game_not_found' })
|
||||
const template = await findTopicById(targetGameId)
|
||||
if (!template) return res.status(404).json({ error: 'game_not_found' })
|
||||
|
||||
const items = await promoteSnapshotItemsToGame({
|
||||
const items = await promoteSnapshotItemsToTemplate({
|
||||
items: templateRequest.items || [],
|
||||
gameId: game.id,
|
||||
templateId: template.id,
|
||||
})
|
||||
const request = await updateTemplateRequestStatus({ id: templateRequest.id, status: 'approved' })
|
||||
return res.json({ request, items })
|
||||
@@ -774,13 +774,13 @@ router.post('/template-requests/:requestId/approve', requireAdmin, async (req, r
|
||||
const parsed = schema.safeParse(req.body)
|
||||
if (!parsed.success) return res.status(400).json({ error: 'bad_request' })
|
||||
|
||||
const exists = await findGameById(parsed.data.gameId)
|
||||
const exists = await findTopicById(parsed.data.gameId)
|
||||
if (exists) return res.status(409).json({ error: 'game_id_taken' })
|
||||
|
||||
const result = await createGameTemplateFromRequest({
|
||||
const result = await createTemplateFromRequest({
|
||||
templateRequest,
|
||||
gameId: parsed.data.gameId,
|
||||
gameName: parsed.data.name,
|
||||
templateId: parsed.data.gameId,
|
||||
templateName: parsed.data.name,
|
||||
})
|
||||
const request = await updateTemplateRequestStatus({ id: templateRequest.id, status: 'approved' })
|
||||
res.json({ request, ...result })
|
||||
@@ -822,12 +822,12 @@ router.post('/template-requests/:requestId/link-game', requireAdmin, async (req,
|
||||
return res.status(409).json({ error: 'request_already_handled' })
|
||||
}
|
||||
|
||||
const game = await findGameById(parsed.data.gameId)
|
||||
if (!game) return res.status(404).json({ error: 'game_not_found' })
|
||||
const template = await findTopicById(parsed.data.gameId)
|
||||
if (!template) return res.status(404).json({ error: 'game_not_found' })
|
||||
|
||||
const request = await updateTemplateRequestTargetGame({
|
||||
id: templateRequest.id,
|
||||
targetGameId: game.id,
|
||||
targetGameId: template.id,
|
||||
})
|
||||
res.json({ request })
|
||||
})
|
||||
@@ -848,8 +848,8 @@ router.post('/template-requests/:requestId/promote-items', requireAdmin, async (
|
||||
return res.status(409).json({ error: 'request_already_handled' })
|
||||
}
|
||||
|
||||
const game = await findGameById(parsed.data.gameId)
|
||||
if (!game) return res.status(404).json({ error: 'game_not_found' })
|
||||
const template = await findTopicById(parsed.data.gameId)
|
||||
if (!template) return res.status(404).json({ error: 'game_not_found' })
|
||||
|
||||
const promotableItems = pickTemplateRequestItems(templateRequest, parsed.data.itemIds, parsed.data.itemLabels, parsed.data.itemSrcs)
|
||||
if (!promotableItems.length) {
|
||||
@@ -858,14 +858,14 @@ router.post('/template-requests/:requestId/promote-items', requireAdmin, async (
|
||||
|
||||
let items = []
|
||||
try {
|
||||
items = await promoteSnapshotItemsToGame({
|
||||
items = await promoteSnapshotItemsToTemplate({
|
||||
items: promotableItems,
|
||||
gameId: game.id,
|
||||
templateId: template.id,
|
||||
})
|
||||
} catch (error) {
|
||||
console.error('[admin] template request promote-items failed', {
|
||||
requestId: templateRequest.id,
|
||||
gameId: game.id,
|
||||
gameId: template.id,
|
||||
itemCount: promotableItems.length,
|
||||
message: error?.message || 'unknown_error',
|
||||
code: error?.code || '',
|
||||
|
||||
@@ -1,37 +1,37 @@
|
||||
const express = require('express')
|
||||
const { listGames, getGameDetail, findGameById, favoriteGame, unfavoriteGame } = require('../db')
|
||||
const { listTopics, getTopicDetail, findTopicById, favoriteTopic, unfavoriteTopic } = require('../db')
|
||||
const { requireAuth } = require('../middleware/auth')
|
||||
|
||||
const router = express.Router()
|
||||
|
||||
router.get('/', async (req, res) => {
|
||||
const games = await listGames(req.session?.userId || '', { includePrivate: !!req.session?.isAdmin })
|
||||
res.json({ games, topics: games })
|
||||
const topics = await listTopics(req.session?.userId || '', { includePrivate: !!req.session?.isAdmin })
|
||||
res.json({ games: topics, topics })
|
||||
})
|
||||
|
||||
router.post('/:gameId/favorite', requireAuth, async (req, res) => {
|
||||
const game = await findGameById(req.params.gameId)
|
||||
if (!game || game.id === 'freeform') return res.status(404).json({ error: 'not_found' })
|
||||
await favoriteGame({ userId: req.session.userId, gameId: game.id })
|
||||
const games = await listGames(req.session.userId)
|
||||
const updated = games.find((entry) => entry.id === game.id) || { ...game, isFavorited: true }
|
||||
const topic = await findTopicById(req.params.gameId)
|
||||
if (!topic || topic.id === 'freeform') return res.status(404).json({ error: 'not_found' })
|
||||
await favoriteTopic({ userId: req.session.userId, topicId: topic.id })
|
||||
const topics = await listTopics(req.session.userId)
|
||||
const updated = topics.find((entry) => entry.id === topic.id) || { ...topic, isFavorited: true }
|
||||
res.json({ game: updated, topic: updated })
|
||||
})
|
||||
|
||||
router.delete('/:gameId/favorite', requireAuth, async (req, res) => {
|
||||
const game = await findGameById(req.params.gameId)
|
||||
if (!game || game.id === 'freeform') return res.status(404).json({ error: 'not_found' })
|
||||
await unfavoriteGame({ userId: req.session.userId, gameId: game.id })
|
||||
const games = await listGames(req.session.userId)
|
||||
const updated = games.find((entry) => entry.id === game.id) || { ...game, isFavorited: false }
|
||||
const topic = await findTopicById(req.params.gameId)
|
||||
if (!topic || topic.id === 'freeform') return res.status(404).json({ error: 'not_found' })
|
||||
await unfavoriteTopic({ userId: req.session.userId, topicId: topic.id })
|
||||
const topics = await listTopics(req.session.userId)
|
||||
const updated = topics.find((entry) => entry.id === topic.id) || { ...topic, isFavorited: false }
|
||||
res.json({ game: updated, topic: updated })
|
||||
})
|
||||
|
||||
router.get('/:gameId', async (req, res) => {
|
||||
const detail = await getGameDetail(req.params.gameId)
|
||||
const detail = await getTopicDetail(req.params.gameId)
|
||||
if (!detail) return res.status(404).json({ error: 'not_found' })
|
||||
if (!detail.game.isPublic && !req.session?.isAdmin) return res.status(404).json({ error: 'not_found' })
|
||||
res.json({ game: detail.game, topic: detail.game, items: detail.items })
|
||||
if (!detail.topic.isPublic && !req.session?.isAdmin) return res.status(404).json({ error: 'not_found' })
|
||||
res.json({ game: detail.topic, topic: detail.topic, items: detail.items })
|
||||
})
|
||||
|
||||
module.exports = router
|
||||
|
||||
Reference in New Issue
Block a user