Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 932b4e35a7 |
@@ -87,7 +87,6 @@ function mapGameItemRow(row) {
|
|||||||
return {
|
return {
|
||||||
id: row.id,
|
id: row.id,
|
||||||
topicId: row.topic_id,
|
topicId: row.topic_id,
|
||||||
gameId: row.topic_id,
|
|
||||||
src: row.src,
|
src: row.src,
|
||||||
label: row.label,
|
label: row.label,
|
||||||
displayOrder: row.display_order == null ? null : Number(row.display_order),
|
displayOrder: row.display_order == null ? null : Number(row.display_order),
|
||||||
@@ -137,8 +136,6 @@ function mapTierListRow(row) {
|
|||||||
authorAvatarSrc: row.avatar_src || '',
|
authorAvatarSrc: row.avatar_src || '',
|
||||||
topicId: row.topic_id,
|
topicId: row.topic_id,
|
||||||
topicName: row.topic_name || '',
|
topicName: row.topic_name || '',
|
||||||
gameId: row.topic_id,
|
|
||||||
gameName: row.topic_name || '',
|
|
||||||
title: row.title,
|
title: row.title,
|
||||||
thumbnailSrc: row.thumbnail_src || '',
|
thumbnailSrc: row.thumbnail_src || '',
|
||||||
description: row.description || '',
|
description: row.description || '',
|
||||||
@@ -167,15 +164,11 @@ function mapTemplateRequestRow(row) {
|
|||||||
sourceTierListId: row.source_tierlist_id || '',
|
sourceTierListId: row.source_tierlist_id || '',
|
||||||
sourceTopicId: row.source_topic_id,
|
sourceTopicId: row.source_topic_id,
|
||||||
sourceTopicName: row.source_topic_name || '',
|
sourceTopicName: row.source_topic_name || '',
|
||||||
sourceGameId: row.source_topic_id,
|
|
||||||
sourceGameName: row.source_topic_name || '',
|
|
||||||
sourceTierListTitle: row.title_snapshot || '',
|
sourceTierListTitle: row.title_snapshot || '',
|
||||||
sourceDescription: row.description_snapshot || '',
|
sourceDescription: row.description_snapshot || '',
|
||||||
thumbnailSrc: row.thumbnail_src_snapshot || '',
|
thumbnailSrc: row.thumbnail_src_snapshot || '',
|
||||||
targetTopicId: row.target_topic_id || '',
|
targetTopicId: row.target_topic_id || '',
|
||||||
targetTopicName: row.target_topic_name || '',
|
targetTopicName: row.target_topic_name || '',
|
||||||
targetGameId: row.target_topic_id || '',
|
|
||||||
targetGameName: row.target_topic_name || '',
|
|
||||||
status: row.status,
|
status: row.status,
|
||||||
items: parseJson(row.items_json, []),
|
items: parseJson(row.items_json, []),
|
||||||
snapshotGroups: parseJson(row.groups_json, []),
|
snapshotGroups: parseJson(row.groups_json, []),
|
||||||
@@ -1347,15 +1340,14 @@ async function clearImageOptimizationJobs({ month } = {}) {
|
|||||||
const result = await query('DELETE FROM image_optimization_jobs')
|
const result = await query('DELETE FROM image_optimization_jobs')
|
||||||
return Number(result.affectedRows || 0)
|
return Number(result.affectedRows || 0)
|
||||||
}
|
}
|
||||||
async function createTopicItem({ id, topicId, gameId = topicId, src, label }) {
|
async function createTopicItem({ id, topicId, src, label }) {
|
||||||
const createdAt = now()
|
const createdAt = now()
|
||||||
const resolvedTopicId = topicId || gameId
|
const minOrderRows = await query('SELECT MIN(display_order) AS min_display_order FROM topic_items WHERE topic_id = ?', [topicId])
|
||||||
const minOrderRows = await query('SELECT MIN(display_order) AS min_display_order FROM topic_items WHERE topic_id = ?', [resolvedTopicId])
|
|
||||||
const nextDisplayOrder =
|
const nextDisplayOrder =
|
||||||
minOrderRows[0]?.min_display_order == null ? 0 : Number(minOrderRows[0].min_display_order) - 1
|
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 (?, ?, ?, ?, ?, ?)', [
|
await query('INSERT INTO topic_items (id, topic_id, src, label, display_order, created_at) VALUES (?, ?, ?, ?, ?, ?)', [
|
||||||
id,
|
id,
|
||||||
resolvedTopicId,
|
topicId,
|
||||||
src,
|
src,
|
||||||
label,
|
label,
|
||||||
nextDisplayOrder,
|
nextDisplayOrder,
|
||||||
@@ -1456,8 +1448,8 @@ async function updateTopicDisplayOrder(topicIds) {
|
|||||||
await query('UPDATE topics SET display_rank = NULL WHERE id <> ?', [FREEFORM_TOPIC_ID])
|
await query('UPDATE topics SET display_rank = NULL WHERE id <> ?', [FREEFORM_TOPIC_ID])
|
||||||
|
|
||||||
await Promise.all(
|
await Promise.all(
|
||||||
normalizedIds.map((gameId, index) =>
|
normalizedIds.map((topicId, index) =>
|
||||||
query('UPDATE topics SET display_rank = ? WHERE id = ? AND id <> ?', [index + 1, gameId, FREEFORM_TOPIC_ID])
|
query('UPDATE topics SET display_rank = ? WHERE id = ? AND id <> ?', [index + 1, topicId, FREEFORM_TOPIC_ID])
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -1679,8 +1671,8 @@ async function listCustomItems({ queryText = '', page = 1, limit = 50, filterMod
|
|||||||
sourceType: 'template',
|
sourceType: 'template',
|
||||||
sourceLabel: '관리자 템플릿',
|
sourceLabel: '관리자 템플릿',
|
||||||
canDelete: true,
|
canDelete: true,
|
||||||
sourceGameId: '',
|
sourceTopicId: '',
|
||||||
sourceGameName: '',
|
sourceTopicName: '',
|
||||||
isAssetLibraryItem: true,
|
isAssetLibraryItem: true,
|
||||||
}))
|
}))
|
||||||
|
|
||||||
@@ -1697,8 +1689,8 @@ async function listCustomItems({ queryText = '', page = 1, limit = 50, filterMod
|
|||||||
sourceType: 'template',
|
sourceType: 'template',
|
||||||
sourceLabel: '관리자 템플릿',
|
sourceLabel: '관리자 템플릿',
|
||||||
canDelete: true,
|
canDelete: true,
|
||||||
sourceGameId: row.topic_id,
|
sourceTopicId: row.topic_id,
|
||||||
sourceGameName: row.topic_name || row.topic_id,
|
sourceTopicName: row.topic_name || row.topic_id,
|
||||||
}))
|
}))
|
||||||
|
|
||||||
const baseItems = [...customItems, ...templateItems, ...assetLibraryItems]
|
const baseItems = [...customItems, ...templateItems, ...assetLibraryItems]
|
||||||
@@ -1743,8 +1735,8 @@ async function listCustomItems({ queryText = '', page = 1, limit = 50, filterMod
|
|||||||
sourceType: entry.sourceType,
|
sourceType: entry.sourceType,
|
||||||
ownerName: entry.ownerName,
|
ownerName: entry.ownerName,
|
||||||
createdAt: entry.createdAt,
|
createdAt: entry.createdAt,
|
||||||
sourceGameId: entry.sourceGameId || '',
|
sourceTopicId: entry.sourceTopicId || '',
|
||||||
sourceGameName: entry.sourceGameName || '',
|
sourceTopicName: entry.sourceTopicName || '',
|
||||||
usageCount: entry.usageCount || 0,
|
usageCount: entry.usageCount || 0,
|
||||||
linkedGames: entry.linkedGames || [],
|
linkedGames: entry.linkedGames || [],
|
||||||
isAssetLibraryItem: !!entry.isAssetLibraryItem,
|
isAssetLibraryItem: !!entry.isAssetLibraryItem,
|
||||||
@@ -1902,7 +1894,6 @@ async function listPublicTierLists(topicId, currentUserId = '', queryText = '')
|
|||||||
const tierLists = rows.map((row) => ({
|
const tierLists = rows.map((row) => ({
|
||||||
id: row.id,
|
id: row.id,
|
||||||
topicId: row.topic_id,
|
topicId: row.topic_id,
|
||||||
gameId: row.topic_id,
|
|
||||||
title: row.title,
|
title: row.title,
|
||||||
thumbnailSrc: row.thumbnail_src || '',
|
thumbnailSrc: row.thumbnail_src || '',
|
||||||
createdAt: Number(row.created_at),
|
createdAt: Number(row.created_at),
|
||||||
@@ -2011,7 +2002,6 @@ async function listUserTierLists(userId) {
|
|||||||
const tierLists = rows.map((row) => ({
|
const tierLists = rows.map((row) => ({
|
||||||
id: row.id,
|
id: row.id,
|
||||||
topicId: row.topic_id,
|
topicId: row.topic_id,
|
||||||
gameId: row.topic_id,
|
|
||||||
title: row.title,
|
title: row.title,
|
||||||
thumbnailSrc: row.thumbnail_src || '',
|
thumbnailSrc: row.thumbnail_src || '',
|
||||||
createdAt: Number(row.created_at),
|
createdAt: Number(row.created_at),
|
||||||
@@ -2057,11 +2047,11 @@ function getAutoThumbnailSrc(groups = [], pool = []) {
|
|||||||
return fallbackItem?.src || ''
|
return fallbackItem?.src || ''
|
||||||
}
|
}
|
||||||
|
|
||||||
async function listAdminTierLists({ queryText = '', gameId = '', topicId = '', page = 1, limit = 50, currentUserId = '' } = {}) {
|
async function listAdminTierLists({ queryText = '', topicId = '', page = 1, limit = 50, currentUserId = '' } = {}) {
|
||||||
const normalizedLimit = Math.min(Math.max(Number(limit) || 50, 1), 200)
|
const normalizedLimit = Math.min(Math.max(Number(limit) || 50, 1), 200)
|
||||||
const normalizedPage = Math.max(Number(page) || 1, 1)
|
const normalizedPage = Math.max(Number(page) || 1, 1)
|
||||||
const hasQuery = !!(queryText || '').trim()
|
const hasQuery = !!(queryText || '').trim()
|
||||||
const resolvedTopicId = (topicId || gameId || '').trim()
|
const resolvedTopicId = (topicId || '').trim()
|
||||||
const hasGameId = !!resolvedTopicId
|
const hasGameId = !!resolvedTopicId
|
||||||
const search = `%${(queryText || '').trim()}%`
|
const search = `%${(queryText || '').trim()}%`
|
||||||
const whereParts = []
|
const whereParts = []
|
||||||
@@ -2144,9 +2134,9 @@ async function listAdminTierLists({ queryText = '', gameId = '', topicId = '', p
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function summarizeAdminTierLists({ queryText = '', gameId = '', topicId = '' } = {}) {
|
async function summarizeAdminTierLists({ queryText = '', topicId = '' } = {}) {
|
||||||
const hasQuery = !!(queryText || '').trim()
|
const hasQuery = !!(queryText || '').trim()
|
||||||
const resolvedTopicId = (topicId || gameId || '').trim()
|
const resolvedTopicId = (topicId || '').trim()
|
||||||
const hasGameId = !!resolvedTopicId
|
const hasGameId = !!resolvedTopicId
|
||||||
const search = `%${(queryText || '').trim()}%`
|
const search = `%${(queryText || '').trim()}%`
|
||||||
const whereParts = []
|
const whereParts = []
|
||||||
@@ -2245,10 +2235,8 @@ async function createTemplateRequest({
|
|||||||
type,
|
type,
|
||||||
requesterId,
|
requesterId,
|
||||||
sourceTierListId = '',
|
sourceTierListId = '',
|
||||||
sourceGameId,
|
sourceTopicId,
|
||||||
targetGameId = '',
|
targetTopicId = '',
|
||||||
sourceTopicId = sourceGameId,
|
|
||||||
targetTopicId = targetGameId,
|
|
||||||
title,
|
title,
|
||||||
description = '',
|
description = '',
|
||||||
thumbnailSrc = '',
|
thumbnailSrc = '',
|
||||||
@@ -2401,8 +2389,8 @@ async function updateTemplateRequestStatus({ id, status }) {
|
|||||||
return findTemplateRequestById(id)
|
return findTemplateRequestById(id)
|
||||||
}
|
}
|
||||||
|
|
||||||
async function updateTemplateRequestTargetGame({ id, targetGameId }) {
|
async function updateTemplateRequestTargetTopic({ id, targetTopicId }) {
|
||||||
await query('UPDATE template_requests SET target_topic_id = ?, updated_at = ? WHERE id = ?', [targetGameId || '', now(), id])
|
await query('UPDATE template_requests SET target_topic_id = ?, updated_at = ? WHERE id = ?', [targetTopicId || '', now(), id])
|
||||||
return findTemplateRequestById(id)
|
return findTemplateRequestById(id)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2452,8 +2440,7 @@ async function deleteCustomItems(ids) {
|
|||||||
async function saveTierList({
|
async function saveTierList({
|
||||||
id,
|
id,
|
||||||
authorId,
|
authorId,
|
||||||
gameId,
|
topicId,
|
||||||
topicId = gameId,
|
|
||||||
title,
|
title,
|
||||||
thumbnailSrc = '',
|
thumbnailSrc = '',
|
||||||
description,
|
description,
|
||||||
@@ -2504,8 +2491,7 @@ async function duplicateTierListForUser({ tierList, targetUserId }) {
|
|||||||
return saveTierList({
|
return saveTierList({
|
||||||
id: duplicateId,
|
id: duplicateId,
|
||||||
authorId: targetUserId,
|
authorId: targetUserId,
|
||||||
gameId: tierList.topicId || tierList.gameId,
|
topicId: tierList.topicId,
|
||||||
topicId: tierList.topicId || tierList.gameId,
|
|
||||||
title: copyTitle,
|
title: copyTitle,
|
||||||
thumbnailSrc: tierList.thumbnailSrc || '',
|
thumbnailSrc: tierList.thumbnailSrc || '',
|
||||||
description: tierList.description || '',
|
description: tierList.description || '',
|
||||||
@@ -2528,14 +2514,12 @@ async function unfavoriteTierList({ userId, tierListId }) {
|
|||||||
await query('DELETE FROM favorite_tierlists WHERE user_id = ? AND tierlist_id = ?', [userId, tierListId])
|
await query('DELETE FROM favorite_tierlists WHERE user_id = ? AND tierlist_id = ?', [userId, tierListId])
|
||||||
}
|
}
|
||||||
|
|
||||||
async function favoriteTopic({ userId, topicId, gameId = topicId }) {
|
async function favoriteTopic({ userId, topicId }) {
|
||||||
const resolvedTopicId = topicId || gameId
|
await query('INSERT IGNORE INTO favorite_topics (user_id, topic_id, created_at) VALUES (?, ?, ?)', [userId, topicId, now()])
|
||||||
await query('INSERT IGNORE INTO favorite_topics (user_id, topic_id, created_at) VALUES (?, ?, ?)', [userId, resolvedTopicId, now()])
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async function unfavoriteTopic({ userId, topicId, gameId = topicId }) {
|
async function unfavoriteTopic({ userId, topicId }) {
|
||||||
const resolvedTopicId = topicId || gameId
|
await query('DELETE FROM favorite_topics WHERE user_id = ? AND topic_id = ?', [userId, topicId])
|
||||||
await query('DELETE FROM favorite_topics WHERE user_id = ? AND topic_id = ?', [userId, resolvedTopicId])
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const favoriteGame = favoriteTopic
|
const favoriteGame = favoriteTopic
|
||||||
@@ -2629,5 +2613,6 @@ module.exports = {
|
|||||||
findTemplateRequestById,
|
findTemplateRequestById,
|
||||||
listAdminTemplateRequests,
|
listAdminTemplateRequests,
|
||||||
updateTemplateRequestStatus,
|
updateTemplateRequestStatus,
|
||||||
updateTemplateRequestTargetGame,
|
updateTemplateRequestTargetTopic,
|
||||||
|
updateTemplateRequestTargetGame: updateTemplateRequestTargetTopic,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -39,7 +39,7 @@ const {
|
|||||||
listAdminTemplateRequests,
|
listAdminTemplateRequests,
|
||||||
findTemplateRequestById,
|
findTemplateRequestById,
|
||||||
updateTemplateRequestStatus,
|
updateTemplateRequestStatus,
|
||||||
updateTemplateRequestTargetGame,
|
updateTemplateRequestTargetTopic,
|
||||||
adminUpdateUser,
|
adminUpdateUser,
|
||||||
adminUpdateUserPassword,
|
adminUpdateUserPassword,
|
||||||
adminDeleteUser,
|
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) => {
|
router.patch(['/games/display-order', '/templates/display-order'], requireAdmin, async (req, res) => {
|
||||||
const schema = z.object({
|
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)
|
const parsed = schema.safeParse(req.body)
|
||||||
if (!parsed.success) return res.status(400).json({ error: 'bad_request' })
|
if (!parsed.success) return res.status(400).json({ error: 'bad_request' })
|
||||||
|
|
||||||
const templates = await listTopics('', { includePrivate: true })
|
const templates = await listTopics('', { includePrivate: true })
|
||||||
const validGameIds = new Set(templates.map((template) => template.id))
|
const validTopicIds = new Set(templates.map((template) => template.id))
|
||||||
const filteredIds = parsed.data.gameIds.filter((gameId) => validGameIds.has(gameId))
|
const filteredIds = parsed.data.topicIds.filter((topicId) => validTopicIds.has(topicId))
|
||||||
const updatedGames = await updateTopicDisplayOrder(filteredIds)
|
const updatedTemplates = await updateTopicDisplayOrder(filteredIds)
|
||||||
res.json({ games: updatedGames, templates: updatedGames })
|
res.json({ templates: updatedTemplates })
|
||||||
})
|
})
|
||||||
|
|
||||||
router.patch(['/games/:gameId/items/display-order', '/templates/:templateId/items/display-order'], requireAdmin, async (req, res) => {
|
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))
|
const template = await findTopicById(getTemplateIdParam(req))
|
||||||
if (!template) return res.status(404).json({ error: 'not_found' })
|
if (!template) return res.status(404).json({ error: 'not_found' })
|
||||||
const item = await findTopicItemById(req.params.itemId)
|
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)
|
const usage = await countTierListsUsingTopicItem(req.params.itemId)
|
||||||
res.json({ usage })
|
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' })
|
if (!template) return res.status(404).json({ error: 'not_found' })
|
||||||
|
|
||||||
const updated = await updateTopicItemLabel(req.params.itemId, parsed.data.label)
|
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 })
|
res.json({ item: updated })
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -319,7 +319,6 @@ router.get('/tierlists', requireAdmin, async (req, res) => {
|
|||||||
const schema = z.object({
|
const schema = z.object({
|
||||||
q: z.string().trim().max(120).optional().default(''),
|
q: z.string().trim().max(120).optional().default(''),
|
||||||
topicId: 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),
|
page: z.coerce.number().int().min(1).optional().default(1),
|
||||||
limit: z.coerce.number().int().min(1).max(200).optional().default(50),
|
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({
|
const result = await listAdminTierLists({
|
||||||
queryText: parsed.data.q,
|
queryText: parsed.data.q,
|
||||||
topicId: parsed.data.topicId || parsed.data.gameId,
|
topicId: parsed.data.topicId,
|
||||||
gameId: parsed.data.gameId,
|
|
||||||
page: parsed.data.page,
|
page: parsed.data.page,
|
||||||
limit: parsed.data.limit,
|
limit: parsed.data.limit,
|
||||||
currentUserId: req.session?.userId || '',
|
currentUserId: req.session?.userId || '',
|
||||||
@@ -341,15 +339,13 @@ router.get('/tierlists/stats', requireAdmin, async (req, res) => {
|
|||||||
const schema = z.object({
|
const schema = z.object({
|
||||||
q: z.string().trim().max(120).optional().default(''),
|
q: z.string().trim().max(120).optional().default(''),
|
||||||
topicId: 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)
|
const parsed = schema.safeParse(req.query)
|
||||||
if (!parsed.success) return res.status(400).json({ error: 'bad_request' })
|
if (!parsed.success) return res.status(400).json({ error: 'bad_request' })
|
||||||
|
|
||||||
const result = await summarizeAdminTierLists({
|
const result = await summarizeAdminTierLists({
|
||||||
queryText: parsed.data.q,
|
queryText: parsed.data.q,
|
||||||
topicId: parsed.data.topicId || parsed.data.gameId,
|
topicId: parsed.data.topicId,
|
||||||
gameId: parsed.data.gameId,
|
|
||||||
})
|
})
|
||||||
res.json(result)
|
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) => {
|
router.post('/custom-items/:itemId/promote', requireAdmin, async (req, res) => {
|
||||||
const schema = z.object({
|
const schema = z.object({
|
||||||
gameId: z.string().min(1),
|
topicId: z.string().min(1),
|
||||||
})
|
})
|
||||||
const parsed = schema.safeParse(req.body)
|
const parsed = schema.safeParse(req.body)
|
||||||
if (!parsed.success) return res.status(400).json({ error: 'bad_request' })
|
if (!parsed.success) return res.status(400).json({ error: 'bad_request' })
|
||||||
|
|
||||||
const template = await findTopicById(parsed.data.gameId)
|
const template = await findTopicById(parsed.data.topicId)
|
||||||
if (!template) return res.status(404).json({ error: 'game_not_found' })
|
if (!template) return res.status(404).json({ error: 'topic_not_found' })
|
||||||
|
|
||||||
const customItem = await findCustomItemById(req.params.itemId)
|
const customItem = await findCustomItemById(req.params.itemId)
|
||||||
const templateItem = customItem ? null : await findTopicItemById(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) => {
|
router.post('/tierlists/:tierListId/promote-items', requireAdmin, async (req, res) => {
|
||||||
const schema = z.object({
|
const schema = z.object({
|
||||||
gameId: z.string().min(1),
|
topicId: z.string().min(1),
|
||||||
itemIds: z.array(z.string().min(1)).optional().default([]),
|
itemIds: z.array(z.string().min(1)).optional().default([]),
|
||||||
})
|
})
|
||||||
const parsed = schema.safeParse(req.body)
|
const parsed = schema.safeParse(req.body)
|
||||||
if (!parsed.success) return res.status(400).json({ error: 'bad_request' })
|
if (!parsed.success) return res.status(400).json({ error: 'bad_request' })
|
||||||
|
|
||||||
const template = await findTopicById(parsed.data.gameId)
|
const template = await findTopicById(parsed.data.topicId)
|
||||||
if (!template) return res.status(404).json({ error: 'game_not_found' })
|
if (!template) return res.status(404).json({ error: 'topic_not_found' })
|
||||||
|
|
||||||
const tierList = await findTierListById(req.params.tierListId)
|
const tierList = await findTierListById(req.params.tierListId)
|
||||||
if (!tierList) return res.status(404).json({ error: 'not_found' })
|
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) => {
|
router.post('/tierlists/:tierListId/create-game-template', requireAdmin, async (req, res) => {
|
||||||
const schema = z.object({
|
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),
|
name: z.string().trim().min(1).max(120),
|
||||||
itemIds: z.array(z.string().min(1)).optional().default([]),
|
itemIds: z.array(z.string().min(1)).optional().default([]),
|
||||||
})
|
})
|
||||||
const parsed = schema.safeParse(req.body)
|
const parsed = schema.safeParse(req.body)
|
||||||
if (!parsed.success) return res.status(400).json({ error: 'bad_request' })
|
if (!parsed.success) return res.status(400).json({ error: 'bad_request' })
|
||||||
|
|
||||||
const exists = await findTopicById(parsed.data.gameId)
|
const exists = await findTopicById(parsed.data.topicId)
|
||||||
if (exists) return res.status(409).json({ error: 'game_id_taken' })
|
if (exists) return res.status(409).json({ error: 'topic_id_taken' })
|
||||||
|
|
||||||
const tierList = await findTierListById(req.params.tierListId)
|
const tierList = await findTierListById(req.params.tierListId)
|
||||||
if (!tierList) return res.status(404).json({ error: 'not_found' })
|
if (!tierList) return res.status(404).json({ error: 'not_found' })
|
||||||
@@ -715,7 +711,7 @@ router.post('/tierlists/:tierListId/create-game-template', requireAdmin, async (
|
|||||||
...tierList,
|
...tierList,
|
||||||
pool: parsed.data.itemIds.length ? (tierList.pool || []).filter((item) => parsed.data.itemIds.includes(item.id)) : tierList.pool,
|
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,
|
templateName: parsed.data.name,
|
||||||
})
|
})
|
||||||
res.json(result)
|
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.status !== 'pending') return res.status(409).json({ error: 'request_already_handled' })
|
||||||
|
|
||||||
if (templateRequest.type === 'update') {
|
if (templateRequest.type === 'update') {
|
||||||
const targetGameId = templateRequest.targetGameId || templateRequest.sourceGameId
|
const targetTopicId = templateRequest.targetTopicId || templateRequest.sourceTopicId
|
||||||
const template = await findTopicById(targetGameId)
|
const template = await findTopicById(targetTopicId)
|
||||||
if (!template) return res.status(404).json({ error: 'game_not_found' })
|
if (!template) return res.status(404).json({ error: 'topic_not_found' })
|
||||||
|
|
||||||
const items = await promoteSnapshotItemsToTemplate({
|
const items = await promoteSnapshotItemsToTemplate({
|
||||||
items: templateRequest.items || [],
|
items: templateRequest.items || [],
|
||||||
@@ -768,18 +764,18 @@ router.post('/template-requests/:requestId/approve', requireAdmin, async (req, r
|
|||||||
}
|
}
|
||||||
|
|
||||||
const schema = z.object({
|
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),
|
name: z.string().trim().min(1).max(120),
|
||||||
})
|
})
|
||||||
const parsed = schema.safeParse(req.body)
|
const parsed = schema.safeParse(req.body)
|
||||||
if (!parsed.success) return res.status(400).json({ error: 'bad_request' })
|
if (!parsed.success) return res.status(400).json({ error: 'bad_request' })
|
||||||
|
|
||||||
const exists = await findTopicById(parsed.data.gameId)
|
const exists = await findTopicById(parsed.data.topicId)
|
||||||
if (exists) return res.status(409).json({ error: 'game_id_taken' })
|
if (exists) return res.status(409).json({ error: 'topic_id_taken' })
|
||||||
|
|
||||||
const result = await createTemplateFromRequest({
|
const result = await createTemplateFromRequest({
|
||||||
templateRequest,
|
templateRequest,
|
||||||
templateId: parsed.data.gameId,
|
templateId: parsed.data.topicId,
|
||||||
templateName: parsed.data.name,
|
templateName: parsed.data.name,
|
||||||
})
|
})
|
||||||
const request = await updateTemplateRequestStatus({ id: templateRequest.id, status: 'approved' })
|
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' })
|
return res.status(409).json({ error: 'request_already_handled' })
|
||||||
}
|
}
|
||||||
|
|
||||||
if (templateRequest.type === 'create' && templateRequest.targetGameId && !templateRequest.targetGameName) {
|
if (templateRequest.type === 'create' && templateRequest.targetTopicId && !templateRequest.targetTopicName) {
|
||||||
templateRequest = await updateTemplateRequestTargetGame({
|
templateRequest = await updateTemplateRequestTargetTopic({
|
||||||
id: templateRequest.id,
|
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) => {
|
router.post('/template-requests/:requestId/link-game', requireAdmin, async (req, res) => {
|
||||||
const schema = z.object({
|
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)
|
const parsed = schema.safeParse(req.body)
|
||||||
if (!parsed.success) return res.status(400).json({ error: 'bad_request' })
|
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' })
|
return res.status(409).json({ error: 'request_already_handled' })
|
||||||
}
|
}
|
||||||
|
|
||||||
const template = await findTopicById(parsed.data.gameId)
|
const template = await findTopicById(parsed.data.topicId)
|
||||||
if (!template) return res.status(404).json({ error: 'game_not_found' })
|
if (!template) return res.status(404).json({ error: 'topic_not_found' })
|
||||||
|
|
||||||
const request = await updateTemplateRequestTargetGame({
|
const request = await updateTemplateRequestTargetTopic({
|
||||||
id: templateRequest.id,
|
id: templateRequest.id,
|
||||||
targetGameId: template.id,
|
targetTopicId: template.id,
|
||||||
})
|
})
|
||||||
res.json({ request })
|
res.json({ request })
|
||||||
})
|
})
|
||||||
|
|
||||||
router.post('/template-requests/:requestId/promote-items', requireAdmin, async (req, res) => {
|
router.post('/template-requests/:requestId/promote-items', requireAdmin, async (req, res) => {
|
||||||
const schema = z.object({
|
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([]),
|
itemIds: z.array(z.string().min(1)).optional().default([]),
|
||||||
itemSrcs: 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({}),
|
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' })
|
return res.status(409).json({ error: 'request_already_handled' })
|
||||||
}
|
}
|
||||||
|
|
||||||
const template = await findTopicById(parsed.data.gameId)
|
const template = await findTopicById(parsed.data.topicId)
|
||||||
if (!template) return res.status(404).json({ error: 'game_not_found' })
|
if (!template) return res.status(404).json({ error: 'topic_not_found' })
|
||||||
|
|
||||||
const promotableItems = pickTemplateRequestItems(templateRequest, parsed.data.itemIds, parsed.data.itemLabels, parsed.data.itemSrcs)
|
const promotableItems = pickTemplateRequestItems(templateRequest, parsed.data.itemIds, parsed.data.itemLabels, parsed.data.itemSrcs)
|
||||||
if (!promotableItems.length) {
|
if (!promotableItems.length) {
|
||||||
@@ -865,7 +861,7 @@ router.post('/template-requests/:requestId/promote-items', requireAdmin, async (
|
|||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('[admin] template request promote-items failed', {
|
console.error('[admin] template request promote-items failed', {
|
||||||
requestId: templateRequest.id,
|
requestId: templateRequest.id,
|
||||||
gameId: template.id,
|
topicId: template.id,
|
||||||
itemCount: promotableItems.length,
|
itemCount: promotableItems.length,
|
||||||
message: error?.message || 'unknown_error',
|
message: error?.message || 'unknown_error',
|
||||||
code: error?.code || '',
|
code: error?.code || '',
|
||||||
|
|||||||
@@ -61,7 +61,6 @@ const thumbnailUpload = createMemoryUpload(multer, { fileSize: 8 * 1024 * 1024 }
|
|||||||
const templateRequestSchema = z.object({
|
const templateRequestSchema = z.object({
|
||||||
type: z.enum(['create', 'update']),
|
type: z.enum(['create', 'update']),
|
||||||
sourceTierListId: z.string().max(64).optional().default(''),
|
sourceTierListId: z.string().max(64).optional().default(''),
|
||||||
gameId: z.string().min(1).max(120).optional(),
|
|
||||||
topicId: z.string().min(1).max(120).optional(),
|
topicId: z.string().min(1).max(120).optional(),
|
||||||
requestTitle: z.string().trim().min(1).max(120),
|
requestTitle: z.string().trim().min(1).max(120),
|
||||||
requestDescription: z.string().trim().min(1).max(1000),
|
requestDescription: z.string().trim().min(1).max(1000),
|
||||||
@@ -74,7 +73,7 @@ const templateRequestSchema = z.object({
|
|||||||
name: z.string().min(1).max(16),
|
name: z.string().min(1).max(16),
|
||||||
itemIds: z.array(z.string()).optional().default([]),
|
itemIds: z.array(z.string()).optional().default([]),
|
||||||
}).passthrough().superRefine((value, ctx) => {
|
}).passthrough().superRefine((value, ctx) => {
|
||||||
if (!(value.topicId || value.gameId)) {
|
if (!value.topicId) {
|
||||||
ctx.addIssue({ code: z.ZodIssueCode.custom, message: 'topicId_required', path: ['topicId'] })
|
ctx.addIssue({ code: z.ZodIssueCode.custom, message: 'topicId_required', path: ['topicId'] })
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
@@ -91,7 +90,6 @@ const templateRequestSchema = z.object({
|
|||||||
|
|
||||||
const tierListUpsertSchema = z.object({
|
const tierListUpsertSchema = z.object({
|
||||||
id: z.string().optional(),
|
id: z.string().optional(),
|
||||||
gameId: z.string().min(1).optional(),
|
|
||||||
topicId: z.string().min(1).optional(),
|
topicId: z.string().min(1).optional(),
|
||||||
title: z.string().min(1).max(120),
|
title: z.string().min(1).max(120),
|
||||||
thumbnailSrc: z.string().max(255).optional().default(''),
|
thumbnailSrc: z.string().max(255).optional().default(''),
|
||||||
@@ -118,13 +116,13 @@ const tierListUpsertSchema = z.object({
|
|||||||
})
|
})
|
||||||
),
|
),
|
||||||
}).superRefine((value, ctx) => {
|
}).superRefine((value, ctx) => {
|
||||||
if (!(value.topicId || value.gameId)) {
|
if (!value.topicId) {
|
||||||
ctx.addIssue({ code: z.ZodIssueCode.custom, message: 'topicId_required', path: ['topicId'] })
|
ctx.addIssue({ code: z.ZodIssueCode.custom, message: 'topicId_required', path: ['topicId'] })
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
router.get('/public', async (req, res) => {
|
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 queryText = typeof req.query.q === 'string' ? req.query.q : ''
|
||||||
const lists = await listPublicTierLists(topicId, req.session?.userId || '', queryText)
|
const lists = await listPublicTierLists(topicId, req.session?.userId || '', queryText)
|
||||||
res.json({ tierLists: lists })
|
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' })
|
if (!parsed.success) return res.status(400).json({ error: 'bad_request' })
|
||||||
|
|
||||||
const payload = parsed.data
|
const payload = parsed.data
|
||||||
const topicId = payload.topicId || payload.gameId
|
const topicId = payload.topicId
|
||||||
const normalizedBoardItems = payload.boardItems.map(normalizePoolItem)
|
const normalizedBoardItems = payload.boardItems.map(normalizePoolItem)
|
||||||
const customItems = normalizedBoardItems.filter((item) => item?.origin === 'custom')
|
const customItems = normalizedBoardItems.filter((item) => item?.origin === 'custom')
|
||||||
if (!customItems.length) return res.status(400).json({ error: 'custom_items_required' })
|
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)
|
const parsed = tierListUpsertSchema.safeParse(req.body)
|
||||||
if (!parsed.success) return res.status(400).json({ error: 'bad_request' })
|
if (!parsed.success) return res.status(400).json({ error: 'bad_request' })
|
||||||
const payload = parsed.data
|
const payload = parsed.data
|
||||||
const topicId = payload.topicId || payload.gameId
|
const topicId = payload.topicId
|
||||||
const normalizedPool = payload.pool.map(normalizePoolItem)
|
const normalizedPool = payload.pool.map(normalizePoolItem)
|
||||||
|
|
||||||
let existing = null
|
let existing = null
|
||||||
@@ -296,7 +294,7 @@ router.post('/', requireAuth, async (req, res) => {
|
|||||||
const updated = await saveTierList({
|
const updated = await saveTierList({
|
||||||
id: existing.id,
|
id: existing.id,
|
||||||
authorId: existing.authorId,
|
authorId: existing.authorId,
|
||||||
topicId: existing.topicId || existing.gameId,
|
topicId: existing.topicId,
|
||||||
title: payload.title,
|
title: payload.title,
|
||||||
thumbnailSrc: payload.thumbnailSrc || '',
|
thumbnailSrc: payload.thumbnailSrc || '',
|
||||||
description: payload.description || '',
|
description: payload.description || '',
|
||||||
|
|||||||
@@ -1,5 +1,9 @@
|
|||||||
# 의사결정 이력
|
# 의사결정 이력
|
||||||
|
|
||||||
|
## 2026-04-02 v1.4.25
|
||||||
|
- 이제 프런트와 백엔드 소비층이 `topic/template`를 기본으로 읽을 준비가 되었으므로, 응답과 payload에 `gameId / gameName` 호환 키를 오래 남기는 것보다 실제 표면을 먼저 정리하는 편이 더 낫다고 판단했다.
|
||||||
|
- 다만 오래된 외부 링크까지 한 번에 끊는 건 위험하므로, 이번 단계에서는 데이터/응답/프런트 소비는 `topic`으로 마감하되 `/games/:gameId`와 관리자 route alias 같은 레거시 주소만 마지막 호환 레이어로 남기는 점진 종료가 가장 안전하다고 정리했다.
|
||||||
|
|
||||||
## 2026-04-02 v1.4.24
|
## 2026-04-02 v1.4.24
|
||||||
- `topic/template` 소비층이 이미 정리된 상태라면, 공개 주제 API와 관리자 템플릿 API 응답도 이제는 `game` 키를 기본으로 유지할 이유가 크지 않으므로 새 의미 키만 기본으로 내보내는 편이 맞다고 판단했다.
|
- `topic/template` 소비층이 이미 정리된 상태라면, 공개 주제 API와 관리자 템플릿 API 응답도 이제는 `game` 키를 기본으로 유지할 이유가 크지 않으므로 새 의미 키만 기본으로 내보내는 편이 맞다고 판단했다.
|
||||||
- 다만 관리자 화면 내부 상태 구조를 한 번에 뒤집는 건 위험하므로, 응답은 줄이되 `selectedTemplate.game`처럼 화면 구조에 깊게 퍼진 부분은 프런트에서 한 번 정규화해 받는 점진 방식이 가장 안전하다고 정리했다.
|
- 다만 관리자 화면 내부 상태 구조를 한 번에 뒤집는 건 위험하므로, 응답은 줄이되 `selectedTemplate.game`처럼 화면 구조에 깊게 퍼진 부분은 프런트에서 한 번 정규화해 받는 점진 방식이 가장 안전하다고 정리했다.
|
||||||
|
|||||||
@@ -1,6 +1,9 @@
|
|||||||
# 할 일 및 이슈
|
# 할 일 및 이슈
|
||||||
|
|
||||||
## 단기 확인
|
## 단기 확인
|
||||||
|
- `v1.4.25`에서 티어표/요청 응답의 `gameId / gameName` 호환 키를 실제로 제거했으므로, 브라우저에서 홈 목록, 주제 상세, 저장된 티어표 열기, 즐겨찾기, 검색 결과, 관리자 템플릿 요청/전체 티어표 관리가 모두 정상 동작하는지 한 번 더 QA한다.
|
||||||
|
- `v1.4.25`에서 관리자 route query와 편집기 저장/request payload를 `topicId` 기준으로 옮겼으므로, `/admin/games?topicId=...`, `/admin/tierlists?mode=all&topicId=...`, 티어표 저장, 템플릿 요청, 추가 아이템 가져오기 흐름이 모두 정상인지 확인한다.
|
||||||
|
- 남은 `gameId`는 의도적으로 유지한 레거시 주소 alias(`/games/:gameId`)와 관리자 alias route path뿐이므로, 오래된 외부 링크 진입 후 주소가 새 `topic` 체계로 자연스럽게 정규화되는지만 마지막으로 본다.
|
||||||
- `v1.4.24`에서 공개 주제 API와 관리자 템플릿 API의 기본 응답 키를 더 줄였으므로, 실제 브라우저에서 홈 목록, 즐겨찾기 토글, 주제 상세, 티어표 편집기, 관리자 템플릿 공개 전환/생성이 모두 그대로 정상인지 한 번 더 QA한다.
|
- `v1.4.24`에서 공개 주제 API와 관리자 템플릿 API의 기본 응답 키를 더 줄였으므로, 실제 브라우저에서 홈 목록, 즐겨찾기 토글, 주제 상세, 티어표 편집기, 관리자 템플릿 공개 전환/생성이 모두 그대로 정상인지 한 번 더 QA한다.
|
||||||
- 다음 단계에서는 `mapTierListRow`, `mapTemplateRequestRow`, 관리자 route query, 저장 payload 입력 호환에 남아 있는 `gameId/gameName/sourceGameId/targetGameId`를 끝까지 걷어낼지 최종 결정한다.
|
- 다음 단계에서는 `mapTierListRow`, `mapTemplateRequestRow`, 관리자 route query, 저장 payload 입력 호환에 남아 있는 `gameId/gameName/sourceGameId/targetGameId`를 끝까지 걷어낼지 최종 결정한다.
|
||||||
- `v1.4.23`에서 프런트 `api.js`의 레거시 `game` 별칭 메서드와 티어표 저장/요청 내부 payload를 더 걷어냈으므로, 실제 브라우저에서 저장/복사/템플릿 요청/관리자 요청 카드 표시가 그대로 정상인지 한 번 더 QA한다.
|
- `v1.4.23`에서 프런트 `api.js`의 레거시 `game` 별칭 메서드와 티어표 저장/요청 내부 payload를 더 걷어냈으므로, 실제 브라우저에서 저장/복사/템플릿 요청/관리자 요청 카드 표시가 그대로 정상인지 한 번 더 QA한다.
|
||||||
|
|||||||
@@ -1,5 +1,10 @@
|
|||||||
# 업데이트 로그
|
# 업데이트 로그
|
||||||
|
|
||||||
|
## 2026-04-02 v1.4.25
|
||||||
|
- 티어표와 템플릿 요청 응답에서 `gameId / gameName / sourceGameId / targetGameId` 호환 키를 실제로 제거하고, 프런트 화면도 `topicId / topicName / sourceTopicId / targetTopicId`만 읽도록 정리했다.
|
||||||
|
- 관리자 전체 티어표 관리와 템플릿 요청 관리, 나의 티어표/즐겨찾기/검색 결과 이동, 티어표 편집기 저장·요청 payload도 `topicId` 기준으로 맞춰, 화면과 요청 바디에서 보이는 `game` 흔적을 더 줄였다.
|
||||||
|
- 관리자 템플릿 정렬 저장과 템플릿 아이템/요청 반영 API body도 `topicIds / topicId` 기준으로 옮겼고, 남은 `gameId`는 이제 레거시 주소 호환용 `/games/:gameId`와 관리자 alias route path 쪽에만 남도록 정리했다.
|
||||||
|
|
||||||
## 2026-04-02 v1.4.24
|
## 2026-04-02 v1.4.24
|
||||||
- 공개 주제 API는 이제 `topics` 목록과 `topic` 상세만 기본 응답으로 내려주고, 즐겨찾기 토글도 `topic`만 반환하도록 정리했다. 관리자 템플릿 생성/공개 상태 저장도 `template`만 기본 응답으로 맞췄다.
|
- 공개 주제 API는 이제 `topics` 목록과 `topic` 상세만 기본 응답으로 내려주고, 즐겨찾기 토글도 `topic`만 반환하도록 정리했다. 관리자 템플릿 생성/공개 상태 저장도 `template`만 기본 응답으로 맞췄다.
|
||||||
- 홈, 주제 상세, 티어표 편집기, 관리자 템플릿 관리 화면도 이 변경에 맞춰 `data.topics`, `data.topic`, `data.template`를 직접 읽도록 바꿨다.
|
- 홈, 주제 상세, 티어표 편집기, 관리자 템플릿 관리 화면도 이 변경에 맞춰 `data.topics`, `data.topic`, `data.template`를 직접 읽도록 바꿨다.
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ const router = useRouter()
|
|||||||
const auth = useAuthStore()
|
const auth = useAuthStore()
|
||||||
const { toasts, dismissToast } = useToast()
|
const { toasts, dismissToast } = useToast()
|
||||||
const RIGHT_RAIL_COPYRIGHT_URL = 'https://zenn.town/@murabito'
|
const RIGHT_RAIL_COPYRIGHT_URL = 'https://zenn.town/@murabito'
|
||||||
const currentTopicId = computed(() => route.params.topicId || route.params.gameId || '')
|
const currentTopicId = computed(() => route.params.topicId || '')
|
||||||
|
|
||||||
const leftRailCollapsed = ref(false)
|
const leftRailCollapsed = ref(false)
|
||||||
const rightRailOpen = ref(true)
|
const rightRailOpen = ref(true)
|
||||||
|
|||||||
@@ -65,7 +65,7 @@ function setThumbFileElement(el) {
|
|||||||
<div class="hint hint--tight">
|
<div class="hint hint--tight">
|
||||||
{{
|
{{
|
||||||
props.activeTemplateRequest.type === 'create'
|
props.activeTemplateRequest.type === 'create'
|
||||||
? (props.activeTemplateRequest.targetGameId
|
? (props.activeTemplateRequest.targetTopicId
|
||||||
? '이미 연결된 신규 템플릿이 있어요. 필요한 아이템만 골라 저장하세요.'
|
? '이미 연결된 신규 템플릿이 있어요. 필요한 아이템만 골라 저장하세요.'
|
||||||
: '새 템플릿을 한 번 만든 뒤 필요한 아이템만 골라 저장하세요.')
|
: '새 템플릿을 한 번 만든 뒤 필요한 아이템만 골라 저장하세요.')
|
||||||
: '필요한 아이템만 남긴 뒤 기본 아이템 추가 버튼으로 반영하세요.'
|
: '필요한 아이템만 남긴 뒤 기본 아이템 추가 버튼으로 반영하세요.'
|
||||||
@@ -76,8 +76,8 @@ function setThumbFileElement(el) {
|
|||||||
<span class="pill pill--accent">{{ props.activeTemplateRequest.type === 'create' ? '신규 템플릿 요청' : '기존 템플릿 업데이트' }}</span>
|
<span class="pill pill--accent">{{ props.activeTemplateRequest.type === 'create' ? '신규 템플릿 요청' : '기존 템플릿 업데이트' }}</span>
|
||||||
<span class="pill">요청 아이템 {{ props.stagedRequestDraftCount }}개</span>
|
<span class="pill">요청 아이템 {{ props.stagedRequestDraftCount }}개</span>
|
||||||
<span v-if="props.appliedRequestItemCount" class="pill pill--soft">이미 반영 {{ props.appliedRequestItemCount }}개</span>
|
<span v-if="props.appliedRequestItemCount" class="pill pill--soft">이미 반영 {{ props.appliedRequestItemCount }}개</span>
|
||||||
<span v-if="props.activeTemplateRequest.type === 'create' && (props.activeTemplateRequest.targetGameName || props.activeTemplateRequest.targetGameId)" class="pill pill--soft">
|
<span v-if="props.activeTemplateRequest.type === 'create' && (props.activeTemplateRequest.targetTopicName || props.activeTemplateRequest.targetTopicId)" class="pill pill--soft">
|
||||||
연결된 템플릿 · {{ props.activeTemplateRequest.targetGameName || props.activeTemplateRequest.targetGameId }}
|
연결된 템플릿 · {{ props.activeTemplateRequest.targetTopicName || props.activeTemplateRequest.targetTopicId }}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -92,7 +92,7 @@ function setThumbFileElement(el) {
|
|||||||
요청 티어표 보기
|
요청 티어표 보기
|
||||||
</a>
|
</a>
|
||||||
<button
|
<button
|
||||||
v-if="props.activeTemplateRequest.type === 'create' && !props.activeTemplateRequest.targetGameId"
|
v-if="props.activeTemplateRequest.type === 'create' && !props.activeTemplateRequest.targetTopicId"
|
||||||
class="btn btn--ghost btn--small"
|
class="btn btn--ghost btn--small"
|
||||||
type="button"
|
type="button"
|
||||||
@click="props.openTemplateCreateModal"
|
@click="props.openTemplateCreateModal"
|
||||||
|
|||||||
@@ -53,18 +53,18 @@ const props = defineProps({
|
|||||||
<template v-if="request.type === 'create'">
|
<template v-if="request.type === 'create'">
|
||||||
<label class="templateRequestField">
|
<label class="templateRequestField">
|
||||||
<span class="templateRequestField__label">템플릿 이름</span>
|
<span class="templateRequestField__label">템플릿 이름</span>
|
||||||
<input v-model="request.draftGameName" class="input" placeholder="새 템플릿 이름" />
|
<input v-model="request.draftTopicName" class="input" placeholder="새 템플릿 이름" />
|
||||||
</label>
|
</label>
|
||||||
<label class="templateRequestField">
|
<label class="templateRequestField">
|
||||||
<span class="templateRequestField__label">템플릿 ID</span>
|
<span class="templateRequestField__label">템플릿 ID</span>
|
||||||
<input v-model="request.draftGameId" class="input" placeholder="임시 템플릿 ID" />
|
<input v-model="request.draftTopicId" class="input" placeholder="임시 템플릿 ID" />
|
||||||
</label>
|
</label>
|
||||||
</template>
|
</template>
|
||||||
<template v-else>
|
<template v-else>
|
||||||
<div class="templateRequestCard__thumbLabel">템플릿 이름</div>
|
<div class="templateRequestCard__thumbLabel">템플릿 이름</div>
|
||||||
<div class="templateRequestCard__thumbValue">{{ request.draftGameName || request.sourceGameName || '-' }}</div>
|
<div class="templateRequestCard__thumbValue">{{ request.draftTopicName || request.sourceTopicName || '-' }}</div>
|
||||||
<div class="templateRequestCard__thumbLabel">템플릿 ID</div>
|
<div class="templateRequestCard__thumbLabel">템플릿 ID</div>
|
||||||
<div class="templateRequestCard__thumbValue">{{ request.draftGameId || request.sourceGameId || '-' }}</div>
|
<div class="templateRequestCard__thumbValue">{{ request.draftTopicId || request.sourceTopicId || '-' }}</div>
|
||||||
</template>
|
</template>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -89,8 +89,8 @@ const props = defineProps({
|
|||||||
|
|
||||||
<div class="tierAdminCard__stats">
|
<div class="tierAdminCard__stats">
|
||||||
<span class="pill">추가 아이템 {{ request.items?.length || 0 }}개</span>
|
<span class="pill">추가 아이템 {{ request.items?.length || 0 }}개</span>
|
||||||
<span v-if="request.type === 'create' && (request.targetGameName || request.targetGameId)" class="pill pill--soft">
|
<span v-if="request.type === 'create' && (request.targetTopicName || request.targetTopicId)" class="pill pill--soft">
|
||||||
연결됨 · {{ request.targetGameName || request.targetGameId }}
|
연결됨 · {{ request.targetTopicName || request.targetTopicId }}
|
||||||
</span>
|
</span>
|
||||||
<span class="pill" :class="{ 'pill--accent': request.status === 'reviewing' }">{{ props.templateRequestStatusLabel(request) }}</span>
|
<span class="pill" :class="{ 'pill--accent': request.status === 'reviewing' }">{{ props.templateRequestStatusLabel(request) }}</span>
|
||||||
</div>
|
</div>
|
||||||
@@ -109,7 +109,7 @@ const props = defineProps({
|
|||||||
{{
|
{{
|
||||||
request.isHandling
|
request.isHandling
|
||||||
? '이동중...'
|
? '이동중...'
|
||||||
: request.type === 'create' && (request.targetGameName || request.targetGameId)
|
: request.type === 'create' && (request.targetTopicName || request.targetTopicId)
|
||||||
? '연결된 템플릿 열기'
|
? '연결된 템플릿 열기'
|
||||||
: '확인하기'
|
: '확인하기'
|
||||||
}}
|
}}
|
||||||
@@ -148,7 +148,7 @@ const props = defineProps({
|
|||||||
<div class="tierAdminCard__title">{{ tierList.title }}</div>
|
<div class="tierAdminCard__title">{{ tierList.title }}</div>
|
||||||
<div v-if="tierList.description" class="tierAdminCard__desc">{{ tierList.description }}</div>
|
<div v-if="tierList.description" class="tierAdminCard__desc">{{ tierList.description }}</div>
|
||||||
<div class="tierAdminCard__meta">
|
<div class="tierAdminCard__meta">
|
||||||
{{ tierList.topicName || tierList.gameName || tierList.topicId || tierList.gameId }} · {{ props.tierListAuthorDisplayName(tierList) }}
|
{{ tierList.topicName || tierList.topicId }} · {{ props.tierListAuthorDisplayName(tierList) }}
|
||||||
</div>
|
</div>
|
||||||
<div class="tierAdminCard__meta">{{ props.fmt(tierList.updatedAt) }}</div>
|
<div class="tierAdminCard__meta">{{ props.fmt(tierList.updatedAt) }}</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -170,7 +170,7 @@ const props = defineProps({
|
|||||||
</div>
|
</div>
|
||||||
<div class="tierAdminSection__actions">
|
<div class="tierAdminSection__actions">
|
||||||
<button class="btn btn--ghost btn--small" @click="props.openTierListImportModal(tierList, tierList.extraItems)">추가 아이템 전체 가져오기</button>
|
<button class="btn btn--ghost btn--small" @click="props.openTierListImportModal(tierList, tierList.extraItems)">추가 아이템 전체 가져오기</button>
|
||||||
<button v-if="(tierList.topicId || tierList.gameId) === 'freeform'" class="btn btn--primary btn--small" @click="props.openTierListImportModal(tierList, tierList.extraItems)">
|
<button v-if="tierList.topicId === 'freeform'" class="btn btn--primary btn--small" @click="props.openTierListImportModal(tierList, tierList.extraItems)">
|
||||||
새 템플릿으로 가져오기
|
새 템플릿으로 가져오기
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -167,7 +167,7 @@ export function useAdminCustomItems({
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
item.isPromoting = true
|
item.isPromoting = true
|
||||||
await api.promoteAdminTemplateItem(item.id, { gameId: customItemModalTargetTemplateId.value })
|
await api.promoteAdminTemplateItem(item.id, { topicId: customItemModalTargetTemplateId.value })
|
||||||
const targetTemplateName =
|
const targetTemplateName =
|
||||||
templates.value.find((template) => template.id === customItemModalTargetTemplateId.value)?.name || customItemModalTargetTemplateId.value
|
templates.value.find((template) => template.id === customItemModalTargetTemplateId.value)?.name || customItemModalTargetTemplateId.value
|
||||||
if (selectedTemplateId.value === customItemModalTargetTemplateId.value) await loadTemplate()
|
if (selectedTemplateId.value === customItemModalTargetTemplateId.value) await loadTemplate()
|
||||||
|
|||||||
@@ -70,8 +70,8 @@ export function useAdminFeaturedGames({
|
|||||||
async function saveFeaturedOrder() {
|
async function saveFeaturedOrder() {
|
||||||
resetMessages()
|
resetMessages()
|
||||||
try {
|
try {
|
||||||
const data = await api.updateAdminTemplateDisplayOrder({ gameIds: featuredTemplateIds.value })
|
const data = await api.updateAdminTemplateDisplayOrder({ topicIds: featuredTemplateIds.value })
|
||||||
templates.value = data.games || []
|
templates.value = data.templates || []
|
||||||
featuredTemplateIds.value = templates.value
|
featuredTemplateIds.value = templates.value
|
||||||
.filter((template) => template.displayRank != null)
|
.filter((template) => template.displayRank != null)
|
||||||
.sort((a, b) => a.displayRank - b.displayRank)
|
.sort((a, b) => a.displayRank - b.displayRank)
|
||||||
|
|||||||
@@ -153,8 +153,8 @@ export function useAdminGameManager({
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function createTemplate(options = {}) {
|
async function createTemplate(options = {}) {
|
||||||
const nextGameId = typeof options.gameId === 'string' ? options.gameId.trim() : newTemplateId.value.trim()
|
const nextTopicId = typeof options.topicId === 'string' ? options.topicId.trim() : newTemplateId.value.trim()
|
||||||
const nextGameName = typeof options.gameName === 'string' ? options.gameName.trim() : newTemplateName.value.trim()
|
const nextTopicName = typeof options.topicName === 'string' ? options.topicName.trim() : newTemplateName.value.trim()
|
||||||
const preserveUploadState = !!options.preserveUploadState
|
const preserveUploadState = !!options.preserveUploadState
|
||||||
resetMessages()
|
resetMessages()
|
||||||
try {
|
try {
|
||||||
@@ -163,8 +163,8 @@ export function useAdminGameManager({
|
|||||||
credentials: 'include',
|
credentials: 'include',
|
||||||
headers: { 'Content-Type': 'application/json' },
|
headers: { 'Content-Type': 'application/json' },
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
id: nextGameId,
|
id: nextTopicId,
|
||||||
name: nextGameName,
|
name: nextTopicName,
|
||||||
isPublic: !!newTemplateIsPublic.value,
|
isPublic: !!newTemplateIsPublic.value,
|
||||||
thumbnailSrc: activeTemplateRequest.value?.type === 'create' ? (activeTemplateRequest.value?.thumbnailSrc || '') : '',
|
thumbnailSrc: activeTemplateRequest.value?.type === 'create' ? (activeTemplateRequest.value?.thumbnailSrc || '') : '',
|
||||||
}),
|
}),
|
||||||
@@ -175,19 +175,19 @@ export function useAdminGameManager({
|
|||||||
const createdTemplate = data.template || {}
|
const createdTemplate = data.template || {}
|
||||||
if (activeTemplateRequest.value?.type === 'create' && activeTemplateRequest.value?.id) {
|
if (activeTemplateRequest.value?.type === 'create' && activeTemplateRequest.value?.id) {
|
||||||
const linkData = await api.linkAdminTemplateRequestGame(activeTemplateRequest.value.id, {
|
const linkData = await api.linkAdminTemplateRequestGame(activeTemplateRequest.value.id, {
|
||||||
gameId: createdTemplate.id,
|
topicId: createdTemplate.id,
|
||||||
})
|
})
|
||||||
activeTemplateRequest.value = {
|
activeTemplateRequest.value = {
|
||||||
...activeTemplateRequest.value,
|
...activeTemplateRequest.value,
|
||||||
targetGameId: linkData.request?.targetGameId || createdTemplate.id,
|
targetTopicId: linkData.request?.targetTopicId || createdTemplate.id,
|
||||||
targetGameName: linkData.request?.targetGameName || createdTemplate.name || nextGameName,
|
targetTopicName: linkData.request?.targetTopicName || createdTemplate.name || nextTopicName,
|
||||||
}
|
}
|
||||||
const requestIndex = templateRequests.value.findIndex((entry) => entry.id === activeTemplateRequest.value.id)
|
const requestIndex = templateRequests.value.findIndex((entry) => entry.id === activeTemplateRequest.value.id)
|
||||||
if (requestIndex >= 0) {
|
if (requestIndex >= 0) {
|
||||||
templateRequests.value.splice(requestIndex, 1, {
|
templateRequests.value.splice(requestIndex, 1, {
|
||||||
...templateRequests.value[requestIndex],
|
...templateRequests.value[requestIndex],
|
||||||
targetGameId: linkData.request?.targetGameId || createdTemplate.id,
|
targetTopicId: linkData.request?.targetTopicId || createdTemplate.id,
|
||||||
targetGameName: linkData.request?.targetGameName || createdTemplate.name || nextGameName,
|
targetTopicName: linkData.request?.targetTopicName || createdTemplate.name || nextTopicName,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -258,16 +258,16 @@ export function useAdminGameManager({
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
if (!selectedTemplateId.value && activeTemplateRequest.value?.type === 'create' && !activeTemplateRequest.value?.targetGameId) {
|
if (!selectedTemplateId.value && activeTemplateRequest.value?.type === 'create' && !activeTemplateRequest.value?.targetTopicId) {
|
||||||
const draftGameId = (activeTemplateRequest.value?.draftGameId || '').trim()
|
const draftTopicId = (activeTemplateRequest.value?.draftTopicId || '').trim()
|
||||||
const draftGameName = (activeTemplateRequest.value?.draftGameName || '').trim()
|
const draftTopicName = (activeTemplateRequest.value?.draftTopicName || '').trim()
|
||||||
if (!draftGameId || !draftGameName) {
|
if (!draftTopicId || !draftTopicName) {
|
||||||
error.value = '먼저 신규 템플릿의 이름과 템플릿 ID를 저장해주세요.'
|
error.value = '먼저 신규 템플릿의 이름과 템플릿 ID를 저장해주세요.'
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
await createTemplate({
|
await createTemplate({
|
||||||
gameId: draftGameId,
|
topicId: draftTopicId,
|
||||||
gameName: draftGameName,
|
topicName: draftTopicName,
|
||||||
preserveUploadState: true,
|
preserveUploadState: true,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -301,7 +301,7 @@ export function useAdminGameManager({
|
|||||||
for (const requestId of requestIds) {
|
for (const requestId of requestIds) {
|
||||||
const draftsForRequest = requestDrafts.filter((entry) => entry.requestId === requestId)
|
const draftsForRequest = requestDrafts.filter((entry) => entry.requestId === requestId)
|
||||||
const result = await api.promoteAdminTemplateRequestItems(requestId, {
|
const result = await api.promoteAdminTemplateRequestItems(requestId, {
|
||||||
gameId: selectedTemplateId.value,
|
topicId: selectedTemplateId.value,
|
||||||
itemIds: draftsForRequest.map((entry) => entry.itemId).filter(Boolean),
|
itemIds: draftsForRequest.map((entry) => entry.itemId).filter(Boolean),
|
||||||
itemSrcs: draftsForRequest.map((entry) => entry.src).filter(Boolean),
|
itemSrcs: draftsForRequest.map((entry) => entry.src).filter(Boolean),
|
||||||
itemLabels: draftsForRequest.reduce((acc, entry) => {
|
itemLabels: draftsForRequest.reduce((acc, entry) => {
|
||||||
@@ -327,7 +327,7 @@ export function useAdminGameManager({
|
|||||||
error.value = `요청 아이템을 템플릿 기본 아이템으로 옮기지 못했어요.${detail}`
|
error.value = `요청 아이템을 템플릿 기본 아이템으로 옮기지 못했어요.${detail}`
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if (apiError === 'game_not_found') {
|
if (apiError === 'topic_not_found') {
|
||||||
error.value = '선택한 템플릿을 찾지 못했어요.'
|
error.value = '선택한 템플릿을 찾지 못했어요.'
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -21,14 +21,14 @@ export function useAdminTemplateRequests({
|
|||||||
type: request.type,
|
type: request.type,
|
||||||
status: request.status,
|
status: request.status,
|
||||||
thumbnailSrc: request.thumbnailSrc || '',
|
thumbnailSrc: request.thumbnailSrc || '',
|
||||||
draftGameId: request.draftTopicId || request.draftGameId || '',
|
draftTopicId: request.draftTopicId || '',
|
||||||
draftGameName: request.draftTopicName || request.draftGameName || '',
|
draftTopicName: request.draftTopicName || '',
|
||||||
draftGameIsPublic: !!(request.draftTopicIsPublic ?? request.draftGameIsPublic),
|
draftTopicIsPublic: !!request.draftTopicIsPublic,
|
||||||
sourceTierListId: request.sourceTierListId || '',
|
sourceTierListId: request.sourceTierListId || '',
|
||||||
sourceGameId: request.sourceTopicId || request.sourceGameId || '',
|
sourceTopicId: request.sourceTopicId || '',
|
||||||
sourceTierListTitle: request.sourceTierListTitle || '',
|
sourceTierListTitle: request.sourceTierListTitle || '',
|
||||||
targetGameId: request.targetTopicId || request.targetGameId || '',
|
targetTopicId: request.targetTopicId || '',
|
||||||
targetGameName: request.targetTopicName || request.targetGameName || '',
|
targetTopicName: request.targetTopicName || '',
|
||||||
requesterName: request.requesterName || '',
|
requesterName: request.requesterName || '',
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -38,8 +38,8 @@ export function useAdminTemplateRequests({
|
|||||||
}
|
}
|
||||||
|
|
||||||
function templateRequestSourceUrl(request) {
|
function templateRequestSourceUrl(request) {
|
||||||
if (!request?.sourceGameId || !request?.sourceTierListId) return ''
|
if (!request?.sourceTopicId || !request?.sourceTierListId) return ''
|
||||||
return editorPath(request.sourceGameId, request.sourceTierListId, { preview: true })
|
return editorPath(request.sourceTopicId, request.sourceTierListId, { preview: true })
|
||||||
}
|
}
|
||||||
|
|
||||||
function templateRequestReviewHint(request) {
|
function templateRequestReviewHint(request) {
|
||||||
@@ -55,9 +55,9 @@ export function useAdminTemplateRequests({
|
|||||||
const syncedRequest = {
|
const syncedRequest = {
|
||||||
...request,
|
...request,
|
||||||
...(data.request || {}),
|
...(data.request || {}),
|
||||||
draftGameId: request.draftGameId || '',
|
draftTopicId: request.draftTopicId || '',
|
||||||
draftGameName: request.draftGameName || '',
|
draftTopicName: request.draftTopicName || '',
|
||||||
draftGameIsPublic: !!request.draftGameIsPublic,
|
draftTopicIsPublic: !!request.draftTopicIsPublic,
|
||||||
}
|
}
|
||||||
Object.assign(request, syncedRequest)
|
Object.assign(request, syncedRequest)
|
||||||
request.status = syncedRequest.status || 'reviewing'
|
request.status = syncedRequest.status || 'reviewing'
|
||||||
@@ -65,18 +65,18 @@ export function useAdminTemplateRequests({
|
|||||||
setTab('game-admin')
|
setTab('game-admin')
|
||||||
|
|
||||||
if (request.type === 'create') {
|
if (request.type === 'create') {
|
||||||
const linkedGameId = syncedRequest.targetGameId || ''
|
const linkedTopicId = syncedRequest.targetTopicId || ''
|
||||||
if (linkedGameId) {
|
if (linkedTopicId) {
|
||||||
await selectAdminTemplate(linkedGameId)
|
await selectAdminTemplate(linkedTopicId)
|
||||||
} else {
|
} else {
|
||||||
openTemplateCreateModal()
|
openTemplateCreateModal()
|
||||||
newTemplateId.value = (syncedRequest.draftGameId || '').trim()
|
newTemplateId.value = (syncedRequest.draftTopicId || '').trim()
|
||||||
newTemplateName.value = (syncedRequest.draftGameName || '').trim()
|
newTemplateName.value = (syncedRequest.draftTopicName || '').trim()
|
||||||
}
|
}
|
||||||
mergeRequestItemsIntoDrafts(syncedRequest)
|
mergeRequestItemsIntoDrafts(syncedRequest)
|
||||||
} else {
|
} else {
|
||||||
const nextGameId = syncedRequest.targetGameId || syncedRequest.sourceGameId || ''
|
const nextTopicId = syncedRequest.targetTopicId || syncedRequest.sourceTopicId || ''
|
||||||
if (nextGameId) await selectAdminTemplate(nextGameId)
|
if (nextTopicId) await selectAdminTemplate(nextTopicId)
|
||||||
mergeRequestItemsIntoDrafts(syncedRequest)
|
mergeRequestItemsIntoDrafts(syncedRequest)
|
||||||
}
|
}
|
||||||
success.value = '요청 아이템을 템플릿 관리 화면으로 가져왔어요. 필요한 항목만 골라서 추가해 주세요.'
|
success.value = '요청 아이템을 템플릿 관리 화면으로 가져왔어요. 필요한 항목만 골라서 추가해 주세요.'
|
||||||
|
|||||||
@@ -74,12 +74,10 @@ export const api = {
|
|||||||
request(
|
request(
|
||||||
`/api/admin/custom-items?q=${encodeURIComponent(q)}&page=${encodeURIComponent(page)}&limit=${encodeURIComponent(limit)}&filter=${encodeURIComponent(filter)}`
|
`/api/admin/custom-items?q=${encodeURIComponent(q)}&page=${encodeURIComponent(page)}&limit=${encodeURIComponent(limit)}&filter=${encodeURIComponent(filter)}`
|
||||||
),
|
),
|
||||||
listAdminTierLists: ({ q = '', topicId = '', gameId = '', page = 1, limit = 50 } = {}) =>
|
listAdminTierLists: ({ q = '', topicId = '', page = 1, limit = 50 } = {}) =>
|
||||||
request(
|
request(`/api/admin/tierlists?q=${encodeURIComponent(q)}&topicId=${encodeURIComponent(topicId)}&page=${encodeURIComponent(page)}&limit=${encodeURIComponent(limit)}`),
|
||||||
`/api/admin/tierlists?q=${encodeURIComponent(q)}&topicId=${encodeURIComponent(topicId || gameId)}&page=${encodeURIComponent(page)}&limit=${encodeURIComponent(limit)}`
|
getAdminTierListStats: ({ q = '', topicId = '' } = {}) =>
|
||||||
),
|
request(`/api/admin/tierlists/stats?q=${encodeURIComponent(q)}&topicId=${encodeURIComponent(topicId)}`),
|
||||||
getAdminTierListStats: ({ q = '', topicId = '', gameId = '' } = {}) =>
|
|
||||||
request(`/api/admin/tierlists/stats?q=${encodeURIComponent(q)}&topicId=${encodeURIComponent(topicId || gameId)}`),
|
|
||||||
updateAdminTierList: (tierListId, payload) =>
|
updateAdminTierList: (tierListId, payload) =>
|
||||||
request(`/api/admin/tierlists/${encodeURIComponent(tierListId)}`, { method: 'PATCH', body: payload }),
|
request(`/api/admin/tierlists/${encodeURIComponent(tierListId)}`, { method: 'PATCH', body: payload }),
|
||||||
deleteAdminTierList: (tierListId) =>
|
deleteAdminTierList: (tierListId) =>
|
||||||
|
|||||||
@@ -424,11 +424,11 @@ watch(
|
|||||||
(name) => {
|
(name) => {
|
||||||
activeTab.value = tabFromAdminRoute(name)
|
activeTab.value = tabFromAdminRoute(name)
|
||||||
if (name === 'adminGames') {
|
if (name === 'adminGames') {
|
||||||
const nextGameId = typeof route.query.gameId === 'string' ? route.query.gameId : ''
|
const nextTopicId = typeof route.query.topicId === 'string' ? route.query.topicId : ''
|
||||||
if (nextGameId && nextGameId !== selectedTemplateId.value) {
|
if (nextTopicId && nextTopicId !== selectedTemplateId.value) {
|
||||||
selectedTemplateId.value = nextGameId
|
selectedTemplateId.value = nextTopicId
|
||||||
queueMicrotask(() => {
|
queueMicrotask(() => {
|
||||||
if (selectedTemplateId.value === nextGameId) void loadTemplate()
|
if (selectedTemplateId.value === nextTopicId) void loadTemplate()
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
return
|
return
|
||||||
@@ -436,8 +436,8 @@ watch(
|
|||||||
if (name === 'adminTierlists') {
|
if (name === 'adminTierlists') {
|
||||||
const nextMode = route.query.mode === 'all' ? 'all' : 'requests'
|
const nextMode = route.query.mode === 'all' ? 'all' : 'requests'
|
||||||
if (tierlistsMode.value !== nextMode) tierlistsMode.value = nextMode
|
if (tierlistsMode.value !== nextMode) tierlistsMode.value = nextMode
|
||||||
const nextTierListGameId = typeof route.query.gameId === 'string' ? route.query.gameId : ''
|
const nextTierListTopicId = typeof route.query.topicId === 'string' ? route.query.topicId : ''
|
||||||
if (adminTierListGameId.value !== nextTierListGameId) adminTierListGameId.value = nextTierListGameId
|
if (adminTierListGameId.value !== nextTierListTopicId) adminTierListGameId.value = nextTierListTopicId
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{ immediate: true }
|
{ immediate: true }
|
||||||
@@ -447,7 +447,7 @@ watch(
|
|||||||
() => selectedTemplateId.value,
|
() => selectedTemplateId.value,
|
||||||
(templateId) => {
|
(templateId) => {
|
||||||
if (route.name !== 'adminGames') return
|
if (route.name !== 'adminGames') return
|
||||||
syncAdminRouteQuery({ gameId: templateId || undefined })
|
syncAdminRouteQuery({ topicId: templateId || undefined })
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -465,16 +465,16 @@ watch(
|
|||||||
if (route.name !== 'adminTierlists') return
|
if (route.name !== 'adminTierlists') return
|
||||||
syncAdminRouteQuery({
|
syncAdminRouteQuery({
|
||||||
mode: mode === 'all' ? 'all' : undefined,
|
mode: mode === 'all' ? 'all' : undefined,
|
||||||
gameId: mode === 'all' && adminTierListGameId.value ? adminTierListGameId.value : undefined,
|
topicId: mode === 'all' && adminTierListGameId.value ? adminTierListGameId.value : undefined,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
watch(
|
watch(
|
||||||
() => adminTierListGameId.value,
|
() => adminTierListGameId.value,
|
||||||
(gameId) => {
|
(topicId) => {
|
||||||
if (route.name !== 'adminTierlists' || tierlistsMode.value !== 'all') return
|
if (route.name !== 'adminTierlists' || tierlistsMode.value !== 'all') return
|
||||||
syncAdminRouteQuery({ gameId: gameId || undefined })
|
syncAdminRouteQuery({ topicId: topicId || undefined })
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -733,7 +733,7 @@ function setTab(tab) {
|
|||||||
if (nextRouteName && route.name !== nextRouteName) {
|
if (nextRouteName && route.name !== nextRouteName) {
|
||||||
const nextQuery =
|
const nextQuery =
|
||||||
tab === 'game-admin'
|
tab === 'game-admin'
|
||||||
? { gameId: selectedTemplateId.value || undefined }
|
? { topicId: selectedTemplateId.value || undefined }
|
||||||
: tab === 'tierlists' && tierlistsMode.value === 'all'
|
: tab === 'tierlists' && tierlistsMode.value === 'all'
|
||||||
? { mode: 'all' }
|
? { mode: 'all' }
|
||||||
: {}
|
: {}
|
||||||
@@ -758,10 +758,10 @@ function setTierlistsMode(mode) {
|
|||||||
|
|
||||||
function openTemplateCreateModal() {
|
function openTemplateCreateModal() {
|
||||||
resetMessages()
|
resetMessages()
|
||||||
if (activeTemplateRequest.value?.type === 'create' && !activeTemplateRequest.value?.targetGameId) {
|
if (activeTemplateRequest.value?.type === 'create' && !activeTemplateRequest.value?.targetTopicId) {
|
||||||
newTemplateId.value = activeTemplateRequest.value?.draftGameId || ''
|
newTemplateId.value = activeTemplateRequest.value?.draftTopicId || ''
|
||||||
newTemplateName.value = activeTemplateRequest.value?.draftGameName || ''
|
newTemplateName.value = activeTemplateRequest.value?.draftTopicName || ''
|
||||||
newTemplateIsPublic.value = !!activeTemplateRequest.value?.draftGameIsPublic
|
newTemplateIsPublic.value = !!activeTemplateRequest.value?.draftTopicIsPublic
|
||||||
} else {
|
} else {
|
||||||
newTemplateId.value = ''
|
newTemplateId.value = ''
|
||||||
newTemplateName.value = ''
|
newTemplateName.value = ''
|
||||||
@@ -822,7 +822,7 @@ async function refreshAdminTierLists() {
|
|||||||
try {
|
try {
|
||||||
const data = await api.listAdminTierLists({
|
const data = await api.listAdminTierLists({
|
||||||
q: adminTierListQuery.value,
|
q: adminTierListQuery.value,
|
||||||
gameId: adminTierListGameId.value,
|
topicId: adminTierListGameId.value,
|
||||||
page: adminTierListPage.value,
|
page: adminTierListPage.value,
|
||||||
limit: adminTierListLimit.value,
|
limit: adminTierListLimit.value,
|
||||||
})
|
})
|
||||||
@@ -839,7 +839,7 @@ async function refreshAdminTierLists() {
|
|||||||
async function refreshAdminTierListStats() {
|
async function refreshAdminTierListStats() {
|
||||||
if (!auth.user?.isAdmin) return
|
if (!auth.user?.isAdmin) return
|
||||||
try {
|
try {
|
||||||
const data = await api.getAdminTierListStats({ q: adminTierListQuery.value, gameId: adminTierListGameId.value })
|
const data = await api.getAdminTierListStats({ q: adminTierListQuery.value, topicId: adminTierListGameId.value })
|
||||||
adminTierListStats.value = {
|
adminTierListStats.value = {
|
||||||
total: data.total || 0,
|
total: data.total || 0,
|
||||||
publicCount: data.publicCount || 0,
|
publicCount: data.publicCount || 0,
|
||||||
@@ -857,7 +857,7 @@ async function refreshSelectedTemplateTierListStats(templateId = '') {
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const data = await api.getAdminTierListStats({ gameId: templateId })
|
const data = await api.getAdminTierListStats({ topicId: templateId })
|
||||||
selectedTemplateTierListStats.value = {
|
selectedTemplateTierListStats.value = {
|
||||||
total: data.total || 0,
|
total: data.total || 0,
|
||||||
publicCount: data.publicCount || 0,
|
publicCount: data.publicCount || 0,
|
||||||
@@ -874,15 +874,15 @@ async function refreshTemplateRequests() {
|
|||||||
const data = await api.listAdminTemplateRequests()
|
const data = await api.listAdminTemplateRequests()
|
||||||
templateRequests.value = (data.requests || []).map((request) => ({
|
templateRequests.value = (data.requests || []).map((request) => ({
|
||||||
...request,
|
...request,
|
||||||
draftGameId:
|
draftTopicId:
|
||||||
request.type === 'create'
|
request.type === 'create'
|
||||||
? ('tmpl-' + String(request.id || 'request').replace(/[^a-zA-Z0-9]+/g, '').slice(0, 12).toLowerCase())
|
? ('tmpl-' + String(request.id || 'request').replace(/[^a-zA-Z0-9]+/g, '').slice(0, 12).toLowerCase())
|
||||||
: request.targetGameId || request.sourceGameId || '',
|
: request.targetTopicId || request.sourceTopicId || '',
|
||||||
draftGameName:
|
draftTopicName:
|
||||||
request.type === 'create'
|
request.type === 'create'
|
||||||
? `${request.sourceTierListTitle || request.sourceGameName || '새 템플릿'}`
|
? `${request.sourceTierListTitle || request.sourceTopicName || '새 템플릿'}`
|
||||||
: request.targetGameName || request.sourceGameName || '',
|
: request.targetTopicName || request.sourceTopicName || '',
|
||||||
draftGameIsPublic: false,
|
draftTopicIsPublic: false,
|
||||||
}))
|
}))
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
error.value = '템플릿 요청 목록을 불러오지 못했어요.'
|
error.value = '템플릿 요청 목록을 불러오지 못했어요.'
|
||||||
@@ -1306,8 +1306,8 @@ function submitAdminTierListSearch() {
|
|||||||
refreshAdminTierLists()
|
refreshAdminTierLists()
|
||||||
}
|
}
|
||||||
|
|
||||||
function setAdminTierListGameId(gameId) {
|
function setAdminTierListGameId(topicId) {
|
||||||
adminTierListGameId.value = gameId || ''
|
adminTierListGameId.value = topicId || ''
|
||||||
adminTierListPage.value = 1
|
adminTierListPage.value = 1
|
||||||
refreshAdminTierLists()
|
refreshAdminTierLists()
|
||||||
}
|
}
|
||||||
@@ -1551,8 +1551,8 @@ function closePreviewModal() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function previewTierListUrl(tierList) {
|
function previewTierListUrl(tierList) {
|
||||||
if (!tierList?.gameId || !tierList?.id) return ''
|
if (!tierList?.topicId || !tierList?.id) return ''
|
||||||
return editorPath(tierList.gameId, tierList.id, { preview: true })
|
return editorPath(tierList.topicId, tierList.id, { preview: true })
|
||||||
}
|
}
|
||||||
|
|
||||||
function openTierListImportModal(tierList, items) {
|
function openTierListImportModal(tierList, items) {
|
||||||
@@ -1567,9 +1567,9 @@ function openTierListImportModal(tierList, items) {
|
|||||||
importModalItems.value = nextItems
|
importModalItems.value = nextItems
|
||||||
importModalMode.value = 'existing'
|
importModalMode.value = 'existing'
|
||||||
importModalTargetTemplateId.value = ''
|
importModalTargetTemplateId.value = ''
|
||||||
importModalNewTemplateId.value = tierList.gameId === 'freeform' ? '' : `${tierList.gameId}-copy`
|
importModalNewTemplateId.value = tierList.topicId === 'freeform' ? '' : `${tierList.topicId}-copy`
|
||||||
importModalNewTemplateName.value =
|
importModalNewTemplateName.value =
|
||||||
tierList.gameId === 'freeform' ? `${tierList.title} 템플릿` : `${tierList.gameName || tierList.gameId} 파생 템플릿`
|
tierList.topicId === 'freeform' ? `${tierList.title} 템플릿` : `${tierList.topicName || tierList.topicId} 파생 템플릿`
|
||||||
importModalOpen.value = true
|
importModalOpen.value = true
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1597,26 +1597,26 @@ async function confirmTierListImport() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const data = await api.promoteAdminTierListItems(tierList.id, {
|
const data = await api.promoteAdminTierListItems(tierList.id, {
|
||||||
gameId: importModalTargetTemplateId.value,
|
topicId: importModalTargetTemplateId.value,
|
||||||
itemIds,
|
itemIds,
|
||||||
})
|
})
|
||||||
if (selectedTemplateId.value === importModalTargetTemplateId.value) await loadTemplate()
|
if (selectedTemplateId.value === importModalTargetTemplateId.value) await loadTemplate()
|
||||||
success.value = `${data.items?.length || 0}개의 아이템을 기존 템플릿에 추가했어요.`
|
success.value = `${data.items?.length || 0}개의 아이템을 기존 템플릿에 추가했어요.`
|
||||||
} else {
|
} else {
|
||||||
const nextGameId = (importModalNewTemplateId.value || '').trim()
|
const nextTopicId = (importModalNewTemplateId.value || '').trim()
|
||||||
const nextGameName = (importModalNewTemplateName.value || '').trim()
|
const nextTopicName = (importModalNewTemplateName.value || '').trim()
|
||||||
if (!nextGameId || !nextGameName) {
|
if (!nextTopicId || !nextTopicName) {
|
||||||
error.value = '새 템플릿 ID와 이름을 모두 입력해주세요.'
|
error.value = '새 템플릿 ID와 이름을 모두 입력해주세요.'
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
const data = await api.createAdminTemplateFromTierList(tierList.id, {
|
const data = await api.createAdminTemplateFromTierList(tierList.id, {
|
||||||
gameId: nextGameId,
|
topicId: nextTopicId,
|
||||||
name: nextGameName,
|
name: nextTopicName,
|
||||||
itemIds,
|
itemIds,
|
||||||
})
|
})
|
||||||
await refreshTemplates()
|
await refreshTemplates()
|
||||||
success.value = `"${data.template?.name || nextGameName}" 템플릿을 생성했어요.`
|
success.value = `"${data.template?.name || nextTopicName}" 템플릿을 생성했어요.`
|
||||||
}
|
}
|
||||||
|
|
||||||
closeTierListImportModal()
|
closeTierListImportModal()
|
||||||
@@ -1631,12 +1631,12 @@ function templateRequestTypeLabel(request) {
|
|||||||
|
|
||||||
function templateRequestTargetLabel(request) {
|
function templateRequestTargetLabel(request) {
|
||||||
if (request.type === 'create') {
|
if (request.type === 'create') {
|
||||||
if (request.targetGameName || request.targetGameId) {
|
if (request.targetTopicName || request.targetTopicId) {
|
||||||
return `연결된 템플릿 · ${request.targetGameName || request.targetGameId}`
|
return `연결된 템플릿 · ${request.targetTopicName || request.targetTopicId}`
|
||||||
}
|
}
|
||||||
return '연결된 템플릿 없음'
|
return '연결된 템플릿 없음'
|
||||||
}
|
}
|
||||||
return request.targetGameName || request.targetGameId || request.sourceGameName
|
return request.targetTopicName || request.targetTopicId || request.sourceTopicName
|
||||||
}
|
}
|
||||||
|
|
||||||
const displayThumbnailUrl = computed(() => {
|
const displayThumbnailUrl = computed(() => {
|
||||||
@@ -2095,7 +2095,7 @@ function userAvatarFallback(user) {
|
|||||||
<div class="modalCard" role="dialog" aria-modal="true">
|
<div class="modalCard" role="dialog" aria-modal="true">
|
||||||
<div class="modalCard__title">티어표 관리</div>
|
<div class="modalCard__title">티어표 관리</div>
|
||||||
<div class="modalCard__desc">
|
<div class="modalCard__desc">
|
||||||
{{ modalTargetAdminTierList ? `${modalTargetAdminTierList.gameName || modalTargetAdminTierList.gameId} · ${tierListAuthorDisplayName(modalTargetAdminTierList)}` : '' }}
|
{{ modalTargetAdminTierList ? `${modalTargetAdminTierList.topicName || modalTargetAdminTierList.topicId} · ${tierListAuthorDisplayName(modalTargetAdminTierList)}` : '' }}
|
||||||
</div>
|
</div>
|
||||||
<div class="modalCard__form">
|
<div class="modalCard__form">
|
||||||
<label class="field">
|
<label class="field">
|
||||||
|
|||||||
@@ -48,7 +48,7 @@ async function loadFavorites() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function openTierList(tierList) {
|
function openTierList(tierList) {
|
||||||
router.push(editorPath(tierList.topicId || tierList.gameId, tierList.id))
|
router.push(editorPath(tierList.topicId, tierList.id))
|
||||||
}
|
}
|
||||||
|
|
||||||
onMounted(loadFavorites)
|
onMounted(loadFavorites)
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ const route = useRoute()
|
|||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
const auth = useAuthStore()
|
const auth = useAuthStore()
|
||||||
|
|
||||||
const topicId = computed(() => route.params.topicId || route.params.gameId)
|
const topicId = computed(() => route.params.topicId)
|
||||||
const topicName = ref('')
|
const topicName = ref('')
|
||||||
const tierLists = ref([])
|
const tierLists = ref([])
|
||||||
const error = ref('')
|
const error = ref('')
|
||||||
|
|||||||
@@ -60,7 +60,7 @@ onMounted(async () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
function openList(t) {
|
function openList(t) {
|
||||||
router.push(editorPath(t.topicId || t.gameId, t.id))
|
router.push(editorPath(t.topicId, t.id))
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|||||||
@@ -38,7 +38,7 @@ function tierListThumbnailUrl(tierList) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function openTierList(tierList) {
|
function openTierList(tierList) {
|
||||||
router.push(editorPath(tierList.topicId || tierList.gameId, tierList.id))
|
router.push(editorPath(tierList.topicId, tierList.id))
|
||||||
}
|
}
|
||||||
|
|
||||||
async function loadResults() {
|
async function loadResults() {
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ const auth = useAuthStore()
|
|||||||
const toast = useToast()
|
const toast = useToast()
|
||||||
const globalRightRailOpen = inject('rightRailOpen', ref(true))
|
const globalRightRailOpen = inject('rightRailOpen', ref(true))
|
||||||
const localRightRailTarget = inject('localRightRailTarget', '#local-right-rail-root')
|
const localRightRailTarget = inject('localRightRailTarget', '#local-right-rail-root')
|
||||||
const templateId = computed(() => route.params.topicId || route.params.gameId)
|
const templateId = computed(() => route.params.topicId)
|
||||||
const tierListId = computed(() => route.params.tierListId)
|
const tierListId = computed(() => route.params.tierListId)
|
||||||
const previewMode = computed(() => route.query.preview === '1')
|
const previewMode = computed(() => route.query.preview === '1')
|
||||||
const templateName = ref('')
|
const templateName = ref('')
|
||||||
@@ -673,7 +673,7 @@ function buildPayload(existingId) {
|
|||||||
const finalTitle = effectiveTitle.value
|
const finalTitle = effectiveTitle.value
|
||||||
return {
|
return {
|
||||||
id: existingId || undefined,
|
id: existingId || undefined,
|
||||||
gameId: templateId.value,
|
topicId: templateId.value,
|
||||||
title: finalTitle,
|
title: finalTitle,
|
||||||
thumbnailSrc: thumbnailSrc.value || '',
|
thumbnailSrc: thumbnailSrc.value || '',
|
||||||
description: (description.value || '').trim(),
|
description: (description.value || '').trim(),
|
||||||
@@ -842,7 +842,7 @@ async function requestTemplate(type) {
|
|||||||
await api.requestTierListTemplate({
|
await api.requestTierListTemplate({
|
||||||
type,
|
type,
|
||||||
sourceTierListId: sourceId,
|
sourceTierListId: sourceId,
|
||||||
gameId: templateId.value,
|
topicId: templateId.value,
|
||||||
requestTitle: templateRequestDraftTitle.value.trim(),
|
requestTitle: templateRequestDraftTitle.value.trim(),
|
||||||
requestDescription: templateRequestDraftDescription.value.trim(),
|
requestDescription: templateRequestDraftDescription.value.trim(),
|
||||||
thumbnailSrc: saved.tierList?.thumbnailSrc || thumbnailSrc.value || '',
|
thumbnailSrc: saved.tierList?.thumbnailSrc || thumbnailSrc.value || '',
|
||||||
|
|||||||
Reference in New Issue
Block a user