diff --git a/backend/src/db.js b/backend/src/db.js index 8d7ced1..576f6e7 100644 --- a/backend/src/db.js +++ b/backend/src/db.js @@ -87,7 +87,6 @@ function mapGameItemRow(row) { return { id: row.id, topicId: row.topic_id, - gameId: row.topic_id, src: row.src, label: row.label, displayOrder: row.display_order == null ? null : Number(row.display_order), @@ -137,8 +136,6 @@ function mapTierListRow(row) { authorAvatarSrc: row.avatar_src || '', topicId: row.topic_id, topicName: row.topic_name || '', - gameId: row.topic_id, - gameName: row.topic_name || '', title: row.title, thumbnailSrc: row.thumbnail_src || '', description: row.description || '', @@ -167,15 +164,11 @@ function mapTemplateRequestRow(row) { sourceTierListId: row.source_tierlist_id || '', sourceTopicId: row.source_topic_id, sourceTopicName: row.source_topic_name || '', - sourceGameId: row.source_topic_id, - sourceGameName: row.source_topic_name || '', sourceTierListTitle: row.title_snapshot || '', sourceDescription: row.description_snapshot || '', thumbnailSrc: row.thumbnail_src_snapshot || '', targetTopicId: row.target_topic_id || '', targetTopicName: row.target_topic_name || '', - targetGameId: row.target_topic_id || '', - targetGameName: row.target_topic_name || '', status: row.status, items: parseJson(row.items_json, []), snapshotGroups: parseJson(row.groups_json, []), @@ -1347,15 +1340,14 @@ async function clearImageOptimizationJobs({ month } = {}) { const result = await query('DELETE FROM image_optimization_jobs') 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 resolvedTopicId = topicId || gameId - const minOrderRows = await query('SELECT MIN(display_order) AS min_display_order FROM topic_items WHERE topic_id = ?', [resolvedTopicId]) + const minOrderRows = await query('SELECT MIN(display_order) AS min_display_order FROM topic_items WHERE topic_id = ?', [topicId]) 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, - resolvedTopicId, + topicId, src, label, nextDisplayOrder, @@ -1456,8 +1448,8 @@ async function updateTopicDisplayOrder(topicIds) { await query('UPDATE topics SET display_rank = NULL WHERE id <> ?', [FREEFORM_TOPIC_ID]) await Promise.all( - normalizedIds.map((gameId, index) => - query('UPDATE topics SET display_rank = ? WHERE id = ? AND id <> ?', [index + 1, gameId, FREEFORM_TOPIC_ID]) + normalizedIds.map((topicId, index) => + 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', sourceLabel: '관리자 템플릿', canDelete: true, - sourceGameId: '', - sourceGameName: '', + sourceTopicId: '', + sourceTopicName: '', isAssetLibraryItem: true, })) @@ -1697,8 +1689,8 @@ async function listCustomItems({ queryText = '', page = 1, limit = 50, filterMod sourceType: 'template', sourceLabel: '관리자 템플릿', canDelete: true, - sourceGameId: row.topic_id, - sourceGameName: row.topic_name || row.topic_id, + sourceTopicId: row.topic_id, + sourceTopicName: row.topic_name || row.topic_id, })) const baseItems = [...customItems, ...templateItems, ...assetLibraryItems] @@ -1743,8 +1735,8 @@ async function listCustomItems({ queryText = '', page = 1, limit = 50, filterMod sourceType: entry.sourceType, ownerName: entry.ownerName, createdAt: entry.createdAt, - sourceGameId: entry.sourceGameId || '', - sourceGameName: entry.sourceGameName || '', + sourceTopicId: entry.sourceTopicId || '', + sourceTopicName: entry.sourceTopicName || '', usageCount: entry.usageCount || 0, linkedGames: entry.linkedGames || [], isAssetLibraryItem: !!entry.isAssetLibraryItem, @@ -1902,7 +1894,6 @@ async function listPublicTierLists(topicId, currentUserId = '', queryText = '') const tierLists = rows.map((row) => ({ id: row.id, topicId: row.topic_id, - gameId: row.topic_id, title: row.title, thumbnailSrc: row.thumbnail_src || '', createdAt: Number(row.created_at), @@ -2011,7 +2002,6 @@ async function listUserTierLists(userId) { const tierLists = rows.map((row) => ({ id: row.id, topicId: row.topic_id, - gameId: row.topic_id, title: row.title, thumbnailSrc: row.thumbnail_src || '', createdAt: Number(row.created_at), @@ -2057,11 +2047,11 @@ function getAutoThumbnailSrc(groups = [], pool = []) { 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 normalizedPage = Math.max(Number(page) || 1, 1) const hasQuery = !!(queryText || '').trim() - const resolvedTopicId = (topicId || gameId || '').trim() + const resolvedTopicId = (topicId || '').trim() const hasGameId = !!resolvedTopicId const search = `%${(queryText || '').trim()}%` 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 resolvedTopicId = (topicId || gameId || '').trim() + const resolvedTopicId = (topicId || '').trim() const hasGameId = !!resolvedTopicId const search = `%${(queryText || '').trim()}%` const whereParts = [] @@ -2245,10 +2235,8 @@ async function createTemplateRequest({ type, requesterId, sourceTierListId = '', - sourceGameId, - targetGameId = '', - sourceTopicId = sourceGameId, - targetTopicId = targetGameId, + sourceTopicId, + targetTopicId = '', title, description = '', thumbnailSrc = '', @@ -2401,8 +2389,8 @@ async function updateTemplateRequestStatus({ id, status }) { return findTemplateRequestById(id) } -async function updateTemplateRequestTargetGame({ id, targetGameId }) { - await query('UPDATE template_requests SET target_topic_id = ?, updated_at = ? WHERE id = ?', [targetGameId || '', now(), id]) +async function updateTemplateRequestTargetTopic({ id, targetTopicId }) { + await query('UPDATE template_requests SET target_topic_id = ?, updated_at = ? WHERE id = ?', [targetTopicId || '', now(), id]) return findTemplateRequestById(id) } @@ -2452,8 +2440,7 @@ async function deleteCustomItems(ids) { async function saveTierList({ id, authorId, - gameId, - topicId = gameId, + topicId, title, thumbnailSrc = '', description, @@ -2504,8 +2491,7 @@ async function duplicateTierListForUser({ tierList, targetUserId }) { return saveTierList({ id: duplicateId, authorId: targetUserId, - gameId: tierList.topicId || tierList.gameId, - topicId: tierList.topicId || tierList.gameId, + topicId: tierList.topicId, title: copyTitle, thumbnailSrc: tierList.thumbnailSrc || '', 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]) } -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 favoriteTopic({ userId, topicId }) { + await query('INSERT IGNORE INTO favorite_topics (user_id, topic_id, created_at) VALUES (?, ?, ?)', [userId, topicId, now()]) } -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]) +async function unfavoriteTopic({ userId, topicId }) { + await query('DELETE FROM favorite_topics WHERE user_id = ? AND topic_id = ?', [userId, topicId]) } const favoriteGame = favoriteTopic @@ -2629,5 +2613,6 @@ module.exports = { findTemplateRequestById, listAdminTemplateRequests, updateTemplateRequestStatus, - updateTemplateRequestTargetGame, + updateTemplateRequestTargetTopic, + updateTemplateRequestTargetGame: updateTemplateRequestTargetTopic, } diff --git a/backend/src/routes/admin.js b/backend/src/routes/admin.js index 9d9492a..6857b52 100644 --- a/backend/src/routes/admin.js +++ b/backend/src/routes/admin.js @@ -39,7 +39,7 @@ const { listAdminTemplateRequests, findTemplateRequestById, updateTemplateRequestStatus, - updateTemplateRequestTargetGame, + updateTemplateRequestTargetTopic, adminUpdateUser, adminUpdateUserPassword, adminDeleteUser, @@ -152,16 +152,16 @@ router.patch(['/games/:gameId', '/templates/:templateId'], requireAdmin, async ( router.patch(['/games/display-order', '/templates/display-order'], requireAdmin, async (req, res) => { const schema = z.object({ - gameIds: z.array(z.string().min(1)).max(50), + topicIds: z.array(z.string().min(1)).max(50), }) const parsed = schema.safeParse(req.body) if (!parsed.success) return res.status(400).json({ error: 'bad_request' }) const templates = await listTopics('', { includePrivate: true }) - const validGameIds = new Set(templates.map((template) => template.id)) - const filteredIds = parsed.data.gameIds.filter((gameId) => validGameIds.has(gameId)) - const updatedGames = await updateTopicDisplayOrder(filteredIds) - res.json({ games: updatedGames, templates: updatedGames }) + const validTopicIds = new Set(templates.map((template) => template.id)) + const filteredIds = parsed.data.topicIds.filter((topicId) => validTopicIds.has(topicId)) + const updatedTemplates = await updateTopicDisplayOrder(filteredIds) + res.json({ templates: updatedTemplates }) }) router.patch(['/games/:gameId/items/display-order', '/templates/:templateId/items/display-order'], requireAdmin, async (req, res) => { @@ -244,7 +244,7 @@ router.get(['/games/:gameId/items/:itemId/usage', '/templates/:templateId/items/ const template = await findTopicById(getTemplateIdParam(req)) if (!template) return res.status(404).json({ error: 'not_found' }) const item = await findTopicItemById(req.params.itemId) - if (!item || item.gameId !== template.id) return res.status(404).json({ error: 'not_found' }) + if (!item || item.topicId !== template.id) return res.status(404).json({ error: 'not_found' }) const usage = await countTierListsUsingTopicItem(req.params.itemId) res.json({ usage }) }) @@ -258,7 +258,7 @@ router.patch(['/games/:gameId/items/:itemId', '/templates/:templateId/items/:ite if (!template) return res.status(404).json({ error: 'not_found' }) const updated = await updateTopicItemLabel(req.params.itemId, parsed.data.label) - if (!updated || updated.gameId !== template.id) return res.status(404).json({ error: 'not_found' }) + if (!updated || updated.topicId !== template.id) return res.status(404).json({ error: 'not_found' }) res.json({ item: updated }) }) @@ -319,7 +319,6 @@ router.get('/tierlists', requireAdmin, async (req, res) => { const schema = z.object({ q: z.string().trim().max(120).optional().default(''), topicId: z.string().trim().max(120).optional().default(''), - gameId: z.string().trim().max(120).optional().default(''), page: z.coerce.number().int().min(1).optional().default(1), limit: z.coerce.number().int().min(1).max(200).optional().default(50), }) @@ -328,8 +327,7 @@ router.get('/tierlists', requireAdmin, async (req, res) => { const result = await listAdminTierLists({ queryText: parsed.data.q, - topicId: parsed.data.topicId || parsed.data.gameId, - gameId: parsed.data.gameId, + topicId: parsed.data.topicId, page: parsed.data.page, limit: parsed.data.limit, currentUserId: req.session?.userId || '', @@ -341,15 +339,13 @@ router.get('/tierlists/stats', requireAdmin, async (req, res) => { const schema = z.object({ q: z.string().trim().max(120).optional().default(''), topicId: z.string().trim().max(120).optional().default(''), - gameId: z.string().trim().max(120).optional().default(''), }) const parsed = schema.safeParse(req.query) if (!parsed.success) return res.status(400).json({ error: 'bad_request' }) const result = await summarizeAdminTierLists({ queryText: parsed.data.q, - topicId: parsed.data.topicId || parsed.data.gameId, - gameId: parsed.data.gameId, + topicId: parsed.data.topicId, }) res.json(result) }) @@ -646,13 +642,13 @@ router.delete('/custom-items/:itemId', requireAdmin, async (req, res) => { router.post('/custom-items/:itemId/promote', requireAdmin, async (req, res) => { const schema = z.object({ - gameId: z.string().min(1), + topicId: z.string().min(1), }) const parsed = schema.safeParse(req.body) if (!parsed.success) return res.status(400).json({ error: 'bad_request' }) - const template = await findTopicById(parsed.data.gameId) - if (!template) return res.status(404).json({ error: 'game_not_found' }) + const template = await findTopicById(parsed.data.topicId) + if (!template) return res.status(404).json({ error: 'topic_not_found' }) const customItem = await findCustomItemById(req.params.itemId) const templateItem = customItem ? null : await findTopicItemById(req.params.itemId) @@ -675,14 +671,14 @@ router.post('/custom-items/:itemId/promote', requireAdmin, async (req, res) => { router.post('/tierlists/:tierListId/promote-items', requireAdmin, async (req, res) => { const schema = z.object({ - gameId: z.string().min(1), + topicId: z.string().min(1), itemIds: z.array(z.string().min(1)).optional().default([]), }) const parsed = schema.safeParse(req.body) if (!parsed.success) return res.status(400).json({ error: 'bad_request' }) - const template = await findTopicById(parsed.data.gameId) - if (!template) return res.status(404).json({ error: 'game_not_found' }) + const template = await findTopicById(parsed.data.topicId) + if (!template) return res.status(404).json({ error: 'topic_not_found' }) const tierList = await findTierListById(req.params.tierListId) if (!tierList) return res.status(404).json({ error: 'not_found' }) @@ -697,15 +693,15 @@ router.post('/tierlists/:tierListId/promote-items', requireAdmin, async (req, re router.post('/tierlists/:tierListId/create-game-template', requireAdmin, async (req, res) => { const schema = z.object({ - gameId: z.string().trim().min(1).max(120), + topicId: z.string().trim().min(1).max(120), name: z.string().trim().min(1).max(120), itemIds: z.array(z.string().min(1)).optional().default([]), }) const parsed = schema.safeParse(req.body) if (!parsed.success) return res.status(400).json({ error: 'bad_request' }) - const exists = await findTopicById(parsed.data.gameId) - if (exists) return res.status(409).json({ error: 'game_id_taken' }) + const exists = await findTopicById(parsed.data.topicId) + if (exists) return res.status(409).json({ error: 'topic_id_taken' }) const tierList = await findTierListById(req.params.tierListId) if (!tierList) return res.status(404).json({ error: 'not_found' }) @@ -715,7 +711,7 @@ router.post('/tierlists/:tierListId/create-game-template', requireAdmin, async ( ...tierList, pool: parsed.data.itemIds.length ? (tierList.pool || []).filter((item) => parsed.data.itemIds.includes(item.id)) : tierList.pool, }, - templateId: parsed.data.gameId, + templateId: parsed.data.topicId, templateName: parsed.data.name, }) res.json(result) @@ -755,9 +751,9 @@ router.post('/template-requests/:requestId/approve', requireAdmin, async (req, r if (templateRequest.status !== 'pending') return res.status(409).json({ error: 'request_already_handled' }) if (templateRequest.type === 'update') { - const targetGameId = templateRequest.targetGameId || templateRequest.sourceGameId - const template = await findTopicById(targetGameId) - if (!template) return res.status(404).json({ error: 'game_not_found' }) + const targetTopicId = templateRequest.targetTopicId || templateRequest.sourceTopicId + const template = await findTopicById(targetTopicId) + if (!template) return res.status(404).json({ error: 'topic_not_found' }) const items = await promoteSnapshotItemsToTemplate({ items: templateRequest.items || [], @@ -768,18 +764,18 @@ router.post('/template-requests/:requestId/approve', requireAdmin, async (req, r } const schema = z.object({ - gameId: z.string().trim().min(1).max(120), + topicId: z.string().trim().min(1).max(120), name: z.string().trim().min(1).max(120), }) const parsed = schema.safeParse(req.body) if (!parsed.success) return res.status(400).json({ error: 'bad_request' }) - const exists = await findTopicById(parsed.data.gameId) - if (exists) return res.status(409).json({ error: 'game_id_taken' }) + const exists = await findTopicById(parsed.data.topicId) + if (exists) return res.status(409).json({ error: 'topic_id_taken' }) const result = await createTemplateFromRequest({ templateRequest, - templateId: parsed.data.gameId, + templateId: parsed.data.topicId, templateName: parsed.data.name, }) const request = await updateTemplateRequestStatus({ id: templateRequest.id, status: 'approved' }) @@ -793,10 +789,10 @@ router.post('/template-requests/:requestId/review', requireAdmin, async (req, re return res.status(409).json({ error: 'request_already_handled' }) } - if (templateRequest.type === 'create' && templateRequest.targetGameId && !templateRequest.targetGameName) { - templateRequest = await updateTemplateRequestTargetGame({ + if (templateRequest.type === 'create' && templateRequest.targetTopicId && !templateRequest.targetTopicName) { + templateRequest = await updateTemplateRequestTargetTopic({ id: templateRequest.id, - targetGameId: '', + targetTopicId: '', }) } @@ -810,7 +806,7 @@ router.post('/template-requests/:requestId/review', requireAdmin, async (req, re router.post('/template-requests/:requestId/link-game', requireAdmin, async (req, res) => { const schema = z.object({ - gameId: z.string().trim().min(1).max(120), + topicId: z.string().trim().min(1).max(120), }) const parsed = schema.safeParse(req.body) if (!parsed.success) return res.status(400).json({ error: 'bad_request' }) @@ -822,19 +818,19 @@ router.post('/template-requests/:requestId/link-game', requireAdmin, async (req, return res.status(409).json({ error: 'request_already_handled' }) } - const template = await findTopicById(parsed.data.gameId) - if (!template) return res.status(404).json({ error: 'game_not_found' }) + const template = await findTopicById(parsed.data.topicId) + if (!template) return res.status(404).json({ error: 'topic_not_found' }) - const request = await updateTemplateRequestTargetGame({ + const request = await updateTemplateRequestTargetTopic({ id: templateRequest.id, - targetGameId: template.id, + targetTopicId: template.id, }) res.json({ request }) }) router.post('/template-requests/:requestId/promote-items', requireAdmin, async (req, res) => { const schema = z.object({ - gameId: z.string().trim().min(1).max(120), + topicId: z.string().trim().min(1).max(120), itemIds: z.array(z.string().min(1)).optional().default([]), itemSrcs: z.array(z.string().min(1)).optional().default([]), itemLabels: z.record(z.string(), z.string().min(1).max(60)).optional().default({}), @@ -848,8 +844,8 @@ router.post('/template-requests/:requestId/promote-items', requireAdmin, async ( return res.status(409).json({ error: 'request_already_handled' }) } - const template = await findTopicById(parsed.data.gameId) - if (!template) return res.status(404).json({ error: 'game_not_found' }) + const template = await findTopicById(parsed.data.topicId) + if (!template) return res.status(404).json({ error: 'topic_not_found' }) const promotableItems = pickTemplateRequestItems(templateRequest, parsed.data.itemIds, parsed.data.itemLabels, parsed.data.itemSrcs) if (!promotableItems.length) { @@ -865,7 +861,7 @@ router.post('/template-requests/:requestId/promote-items', requireAdmin, async ( } catch (error) { console.error('[admin] template request promote-items failed', { requestId: templateRequest.id, - gameId: template.id, + topicId: template.id, itemCount: promotableItems.length, message: error?.message || 'unknown_error', code: error?.code || '', diff --git a/backend/src/routes/tierlists.js b/backend/src/routes/tierlists.js index 25b546b..9e59280 100644 --- a/backend/src/routes/tierlists.js +++ b/backend/src/routes/tierlists.js @@ -61,7 +61,6 @@ const thumbnailUpload = createMemoryUpload(multer, { fileSize: 8 * 1024 * 1024 } const templateRequestSchema = z.object({ type: z.enum(['create', 'update']), sourceTierListId: z.string().max(64).optional().default(''), - gameId: z.string().min(1).max(120).optional(), topicId: z.string().min(1).max(120).optional(), requestTitle: z.string().trim().min(1).max(120), requestDescription: z.string().trim().min(1).max(1000), @@ -74,7 +73,7 @@ const templateRequestSchema = z.object({ name: z.string().min(1).max(16), itemIds: z.array(z.string()).optional().default([]), }).passthrough().superRefine((value, ctx) => { - if (!(value.topicId || value.gameId)) { + if (!value.topicId) { ctx.addIssue({ code: z.ZodIssueCode.custom, message: 'topicId_required', path: ['topicId'] }) } }) @@ -91,7 +90,6 @@ const templateRequestSchema = z.object({ const tierListUpsertSchema = z.object({ id: z.string().optional(), - gameId: z.string().min(1).optional(), topicId: z.string().min(1).optional(), title: z.string().min(1).max(120), thumbnailSrc: z.string().max(255).optional().default(''), @@ -118,13 +116,13 @@ const tierListUpsertSchema = z.object({ }) ), }).superRefine((value, ctx) => { - if (!(value.topicId || value.gameId)) { + if (!value.topicId) { ctx.addIssue({ code: z.ZodIssueCode.custom, message: 'topicId_required', path: ['topicId'] }) } }) router.get('/public', async (req, res) => { - const topicId = typeof req.query.topicId === 'string' ? req.query.topicId : req.query.gameId + const topicId = typeof req.query.topicId === 'string' ? req.query.topicId : '' const queryText = typeof req.query.q === 'string' ? req.query.q : '' const lists = await listPublicTierLists(topicId, req.session?.userId || '', queryText) res.json({ tierLists: lists }) @@ -236,7 +234,7 @@ router.post('/template-request', requireAuth, async (req, res) => { if (!parsed.success) return res.status(400).json({ error: 'bad_request' }) const payload = parsed.data - const topicId = payload.topicId || payload.gameId + const topicId = payload.topicId const normalizedBoardItems = payload.boardItems.map(normalizePoolItem) const customItems = normalizedBoardItems.filter((item) => item?.origin === 'custom') if (!customItems.length) return res.status(400).json({ error: 'custom_items_required' }) @@ -285,7 +283,7 @@ router.post('/', requireAuth, async (req, res) => { const parsed = tierListUpsertSchema.safeParse(req.body) if (!parsed.success) return res.status(400).json({ error: 'bad_request' }) const payload = parsed.data - const topicId = payload.topicId || payload.gameId + const topicId = payload.topicId const normalizedPool = payload.pool.map(normalizePoolItem) let existing = null @@ -296,7 +294,7 @@ router.post('/', requireAuth, async (req, res) => { const updated = await saveTierList({ id: existing.id, authorId: existing.authorId, - topicId: existing.topicId || existing.gameId, + topicId: existing.topicId, title: payload.title, thumbnailSrc: payload.thumbnailSrc || '', description: payload.description || '', diff --git a/docs/history.md b/docs/history.md index 5cc7135..074685f 100644 --- a/docs/history.md +++ b/docs/history.md @@ -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 - `topic/template` 소비층이 이미 정리된 상태라면, 공개 주제 API와 관리자 템플릿 API 응답도 이제는 `game` 키를 기본으로 유지할 이유가 크지 않으므로 새 의미 키만 기본으로 내보내는 편이 맞다고 판단했다. - 다만 관리자 화면 내부 상태 구조를 한 번에 뒤집는 건 위험하므로, 응답은 줄이되 `selectedTemplate.game`처럼 화면 구조에 깊게 퍼진 부분은 프런트에서 한 번 정규화해 받는 점진 방식이 가장 안전하다고 정리했다. diff --git a/docs/todo.md b/docs/todo.md index b7382a4..2582ac2 100644 --- a/docs/todo.md +++ b/docs/todo.md @@ -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한다. - 다음 단계에서는 `mapTierListRow`, `mapTemplateRequestRow`, 관리자 route query, 저장 payload 입력 호환에 남아 있는 `gameId/gameName/sourceGameId/targetGameId`를 끝까지 걷어낼지 최종 결정한다. - `v1.4.23`에서 프런트 `api.js`의 레거시 `game` 별칭 메서드와 티어표 저장/요청 내부 payload를 더 걷어냈으므로, 실제 브라우저에서 저장/복사/템플릿 요청/관리자 요청 카드 표시가 그대로 정상인지 한 번 더 QA한다. diff --git a/docs/update.md b/docs/update.md index aaa98c2..17166f4 100644 --- a/docs/update.md +++ b/docs/update.md @@ -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 - 공개 주제 API는 이제 `topics` 목록과 `topic` 상세만 기본 응답으로 내려주고, 즐겨찾기 토글도 `topic`만 반환하도록 정리했다. 관리자 템플릿 생성/공개 상태 저장도 `template`만 기본 응답으로 맞췄다. - 홈, 주제 상세, 티어표 편집기, 관리자 템플릿 관리 화면도 이 변경에 맞춰 `data.topics`, `data.topic`, `data.template`를 직접 읽도록 바꿨다. diff --git a/frontend/src/App.vue b/frontend/src/App.vue index 2c0c0b1..0599234 100644 --- a/frontend/src/App.vue +++ b/frontend/src/App.vue @@ -23,7 +23,7 @@ const router = useRouter() const auth = useAuthStore() const { toasts, dismissToast } = useToast() 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 rightRailOpen = ref(true) diff --git a/frontend/src/components/admin/AdminGamesSection.vue b/frontend/src/components/admin/AdminGamesSection.vue index 4644d5a..4e95331 100644 --- a/frontend/src/components/admin/AdminGamesSection.vue +++ b/frontend/src/components/admin/AdminGamesSection.vue @@ -65,7 +65,7 @@ function setThumbFileElement(el) {
{{ props.activeTemplateRequest.type === 'create' - ? (props.activeTemplateRequest.targetGameId + ? (props.activeTemplateRequest.targetTopicId ? '이미 연결된 신규 템플릿이 있어요. 필요한 아이템만 골라 저장하세요.' : '새 템플릿을 한 번 만든 뒤 필요한 아이템만 골라 저장하세요.') : '필요한 아이템만 남긴 뒤 기본 아이템 추가 버튼으로 반영하세요.' @@ -76,8 +76,8 @@ function setThumbFileElement(el) { {{ props.activeTemplateRequest.type === 'create' ? '신규 템플릿 요청' : '기존 템플릿 업데이트' }} 요청 아이템 {{ props.stagedRequestDraftCount }}개 이미 반영 {{ props.appliedRequestItemCount }}개 - - 연결된 템플릿 · {{ props.activeTemplateRequest.targetGameName || props.activeTemplateRequest.targetGameId }} + + 연결된 템플릿 · {{ props.activeTemplateRequest.targetTopicName || props.activeTemplateRequest.targetTopicId }}
@@ -92,7 +92,7 @@ function setThumbFileElement(el) { 요청 티어표 보기 - diff --git a/frontend/src/composables/useAdminCustomItems.js b/frontend/src/composables/useAdminCustomItems.js index 84ee719..2b0d536 100644 --- a/frontend/src/composables/useAdminCustomItems.js +++ b/frontend/src/composables/useAdminCustomItems.js @@ -167,7 +167,7 @@ export function useAdminCustomItems({ try { item.isPromoting = true - await api.promoteAdminTemplateItem(item.id, { gameId: customItemModalTargetTemplateId.value }) + await api.promoteAdminTemplateItem(item.id, { topicId: customItemModalTargetTemplateId.value }) const targetTemplateName = templates.value.find((template) => template.id === customItemModalTargetTemplateId.value)?.name || customItemModalTargetTemplateId.value if (selectedTemplateId.value === customItemModalTargetTemplateId.value) await loadTemplate() diff --git a/frontend/src/composables/useAdminFeaturedGames.js b/frontend/src/composables/useAdminFeaturedGames.js index c3a3e31..7c6ff6d 100644 --- a/frontend/src/composables/useAdminFeaturedGames.js +++ b/frontend/src/composables/useAdminFeaturedGames.js @@ -70,8 +70,8 @@ export function useAdminFeaturedGames({ async function saveFeaturedOrder() { resetMessages() try { - const data = await api.updateAdminTemplateDisplayOrder({ gameIds: featuredTemplateIds.value }) - templates.value = data.games || [] + const data = await api.updateAdminTemplateDisplayOrder({ topicIds: featuredTemplateIds.value }) + templates.value = data.templates || [] featuredTemplateIds.value = templates.value .filter((template) => template.displayRank != null) .sort((a, b) => a.displayRank - b.displayRank) diff --git a/frontend/src/composables/useAdminGameManager.js b/frontend/src/composables/useAdminGameManager.js index 18db8da..a4865ca 100644 --- a/frontend/src/composables/useAdminGameManager.js +++ b/frontend/src/composables/useAdminGameManager.js @@ -153,8 +153,8 @@ export function useAdminGameManager({ } async function createTemplate(options = {}) { - const nextGameId = typeof options.gameId === 'string' ? options.gameId.trim() : newTemplateId.value.trim() - const nextGameName = typeof options.gameName === 'string' ? options.gameName.trim() : newTemplateName.value.trim() + const nextTopicId = typeof options.topicId === 'string' ? options.topicId.trim() : newTemplateId.value.trim() + const nextTopicName = typeof options.topicName === 'string' ? options.topicName.trim() : newTemplateName.value.trim() const preserveUploadState = !!options.preserveUploadState resetMessages() try { @@ -163,8 +163,8 @@ export function useAdminGameManager({ credentials: 'include', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ - id: nextGameId, - name: nextGameName, + id: nextTopicId, + name: nextTopicName, isPublic: !!newTemplateIsPublic.value, thumbnailSrc: activeTemplateRequest.value?.type === 'create' ? (activeTemplateRequest.value?.thumbnailSrc || '') : '', }), @@ -175,19 +175,19 @@ export function useAdminGameManager({ const createdTemplate = data.template || {} if (activeTemplateRequest.value?.type === 'create' && activeTemplateRequest.value?.id) { const linkData = await api.linkAdminTemplateRequestGame(activeTemplateRequest.value.id, { - gameId: createdTemplate.id, + topicId: createdTemplate.id, }) activeTemplateRequest.value = { ...activeTemplateRequest.value, - targetGameId: linkData.request?.targetGameId || createdTemplate.id, - targetGameName: linkData.request?.targetGameName || createdTemplate.name || nextGameName, + targetTopicId: linkData.request?.targetTopicId || createdTemplate.id, + targetTopicName: linkData.request?.targetTopicName || createdTemplate.name || nextTopicName, } const requestIndex = templateRequests.value.findIndex((entry) => entry.id === activeTemplateRequest.value.id) if (requestIndex >= 0) { templateRequests.value.splice(requestIndex, 1, { ...templateRequests.value[requestIndex], - targetGameId: linkData.request?.targetGameId || createdTemplate.id, - targetGameName: linkData.request?.targetGameName || createdTemplate.name || nextGameName, + targetTopicId: linkData.request?.targetTopicId || createdTemplate.id, + targetTopicName: linkData.request?.targetTopicName || createdTemplate.name || nextTopicName, }) } } @@ -258,16 +258,16 @@ export function useAdminGameManager({ } try { - if (!selectedTemplateId.value && activeTemplateRequest.value?.type === 'create' && !activeTemplateRequest.value?.targetGameId) { - const draftGameId = (activeTemplateRequest.value?.draftGameId || '').trim() - const draftGameName = (activeTemplateRequest.value?.draftGameName || '').trim() - if (!draftGameId || !draftGameName) { + if (!selectedTemplateId.value && activeTemplateRequest.value?.type === 'create' && !activeTemplateRequest.value?.targetTopicId) { + const draftTopicId = (activeTemplateRequest.value?.draftTopicId || '').trim() + const draftTopicName = (activeTemplateRequest.value?.draftTopicName || '').trim() + if (!draftTopicId || !draftTopicName) { error.value = '먼저 신규 템플릿의 이름과 템플릿 ID를 저장해주세요.' return } await createTemplate({ - gameId: draftGameId, - gameName: draftGameName, + topicId: draftTopicId, + topicName: draftTopicName, preserveUploadState: true, }) } @@ -301,7 +301,7 @@ export function useAdminGameManager({ for (const requestId of requestIds) { const draftsForRequest = requestDrafts.filter((entry) => entry.requestId === requestId) const result = await api.promoteAdminTemplateRequestItems(requestId, { - gameId: selectedTemplateId.value, + topicId: selectedTemplateId.value, itemIds: draftsForRequest.map((entry) => entry.itemId).filter(Boolean), itemSrcs: draftsForRequest.map((entry) => entry.src).filter(Boolean), itemLabels: draftsForRequest.reduce((acc, entry) => { @@ -327,7 +327,7 @@ export function useAdminGameManager({ error.value = `요청 아이템을 템플릿 기본 아이템으로 옮기지 못했어요.${detail}` return } - if (apiError === 'game_not_found') { + if (apiError === 'topic_not_found') { error.value = '선택한 템플릿을 찾지 못했어요.' return } diff --git a/frontend/src/composables/useAdminTemplateRequests.js b/frontend/src/composables/useAdminTemplateRequests.js index 297805f..5df505c 100644 --- a/frontend/src/composables/useAdminTemplateRequests.js +++ b/frontend/src/composables/useAdminTemplateRequests.js @@ -21,14 +21,14 @@ export function useAdminTemplateRequests({ type: request.type, status: request.status, thumbnailSrc: request.thumbnailSrc || '', - draftGameId: request.draftTopicId || request.draftGameId || '', - draftGameName: request.draftTopicName || request.draftGameName || '', - draftGameIsPublic: !!(request.draftTopicIsPublic ?? request.draftGameIsPublic), + draftTopicId: request.draftTopicId || '', + draftTopicName: request.draftTopicName || '', + draftTopicIsPublic: !!request.draftTopicIsPublic, sourceTierListId: request.sourceTierListId || '', - sourceGameId: request.sourceTopicId || request.sourceGameId || '', + sourceTopicId: request.sourceTopicId || '', sourceTierListTitle: request.sourceTierListTitle || '', - targetGameId: request.targetTopicId || request.targetGameId || '', - targetGameName: request.targetTopicName || request.targetGameName || '', + targetTopicId: request.targetTopicId || '', + targetTopicName: request.targetTopicName || '', requesterName: request.requesterName || '', } } @@ -38,8 +38,8 @@ export function useAdminTemplateRequests({ } function templateRequestSourceUrl(request) { - if (!request?.sourceGameId || !request?.sourceTierListId) return '' - return editorPath(request.sourceGameId, request.sourceTierListId, { preview: true }) + if (!request?.sourceTopicId || !request?.sourceTierListId) return '' + return editorPath(request.sourceTopicId, request.sourceTierListId, { preview: true }) } function templateRequestReviewHint(request) { @@ -55,9 +55,9 @@ export function useAdminTemplateRequests({ const syncedRequest = { ...request, ...(data.request || {}), - draftGameId: request.draftGameId || '', - draftGameName: request.draftGameName || '', - draftGameIsPublic: !!request.draftGameIsPublic, + draftTopicId: request.draftTopicId || '', + draftTopicName: request.draftTopicName || '', + draftTopicIsPublic: !!request.draftTopicIsPublic, } Object.assign(request, syncedRequest) request.status = syncedRequest.status || 'reviewing' @@ -65,18 +65,18 @@ export function useAdminTemplateRequests({ setTab('game-admin') if (request.type === 'create') { - const linkedGameId = syncedRequest.targetGameId || '' - if (linkedGameId) { - await selectAdminTemplate(linkedGameId) + const linkedTopicId = syncedRequest.targetTopicId || '' + if (linkedTopicId) { + await selectAdminTemplate(linkedTopicId) } else { openTemplateCreateModal() - newTemplateId.value = (syncedRequest.draftGameId || '').trim() - newTemplateName.value = (syncedRequest.draftGameName || '').trim() + newTemplateId.value = (syncedRequest.draftTopicId || '').trim() + newTemplateName.value = (syncedRequest.draftTopicName || '').trim() } mergeRequestItemsIntoDrafts(syncedRequest) } else { - const nextGameId = syncedRequest.targetGameId || syncedRequest.sourceGameId || '' - if (nextGameId) await selectAdminTemplate(nextGameId) + const nextTopicId = syncedRequest.targetTopicId || syncedRequest.sourceTopicId || '' + if (nextTopicId) await selectAdminTemplate(nextTopicId) mergeRequestItemsIntoDrafts(syncedRequest) } success.value = '요청 아이템을 템플릿 관리 화면으로 가져왔어요. 필요한 항목만 골라서 추가해 주세요.' diff --git a/frontend/src/lib/api.js b/frontend/src/lib/api.js index fd32187..7759ec9 100644 --- a/frontend/src/lib/api.js +++ b/frontend/src/lib/api.js @@ -74,12 +74,10 @@ export const api = { request( `/api/admin/custom-items?q=${encodeURIComponent(q)}&page=${encodeURIComponent(page)}&limit=${encodeURIComponent(limit)}&filter=${encodeURIComponent(filter)}` ), - listAdminTierLists: ({ q = '', topicId = '', gameId = '', page = 1, limit = 50 } = {}) => - request( - `/api/admin/tierlists?q=${encodeURIComponent(q)}&topicId=${encodeURIComponent(topicId || gameId)}&page=${encodeURIComponent(page)}&limit=${encodeURIComponent(limit)}` - ), - getAdminTierListStats: ({ q = '', topicId = '', gameId = '' } = {}) => - request(`/api/admin/tierlists/stats?q=${encodeURIComponent(q)}&topicId=${encodeURIComponent(topicId || gameId)}`), + listAdminTierLists: ({ q = '', topicId = '', page = 1, limit = 50 } = {}) => + request(`/api/admin/tierlists?q=${encodeURIComponent(q)}&topicId=${encodeURIComponent(topicId)}&page=${encodeURIComponent(page)}&limit=${encodeURIComponent(limit)}`), + getAdminTierListStats: ({ q = '', topicId = '' } = {}) => + request(`/api/admin/tierlists/stats?q=${encodeURIComponent(q)}&topicId=${encodeURIComponent(topicId)}`), updateAdminTierList: (tierListId, payload) => request(`/api/admin/tierlists/${encodeURIComponent(tierListId)}`, { method: 'PATCH', body: payload }), deleteAdminTierList: (tierListId) => diff --git a/frontend/src/views/AdminView.vue b/frontend/src/views/AdminView.vue index e2abc7b..1eb2d00 100644 --- a/frontend/src/views/AdminView.vue +++ b/frontend/src/views/AdminView.vue @@ -424,11 +424,11 @@ watch( (name) => { activeTab.value = tabFromAdminRoute(name) if (name === 'adminGames') { - const nextGameId = typeof route.query.gameId === 'string' ? route.query.gameId : '' - if (nextGameId && nextGameId !== selectedTemplateId.value) { - selectedTemplateId.value = nextGameId + const nextTopicId = typeof route.query.topicId === 'string' ? route.query.topicId : '' + if (nextTopicId && nextTopicId !== selectedTemplateId.value) { + selectedTemplateId.value = nextTopicId queueMicrotask(() => { - if (selectedTemplateId.value === nextGameId) void loadTemplate() + if (selectedTemplateId.value === nextTopicId) void loadTemplate() }) } return @@ -436,8 +436,8 @@ watch( if (name === 'adminTierlists') { const nextMode = route.query.mode === 'all' ? 'all' : 'requests' if (tierlistsMode.value !== nextMode) tierlistsMode.value = nextMode - const nextTierListGameId = typeof route.query.gameId === 'string' ? route.query.gameId : '' - if (adminTierListGameId.value !== nextTierListGameId) adminTierListGameId.value = nextTierListGameId + const nextTierListTopicId = typeof route.query.topicId === 'string' ? route.query.topicId : '' + if (adminTierListGameId.value !== nextTierListTopicId) adminTierListGameId.value = nextTierListTopicId } }, { immediate: true } @@ -447,7 +447,7 @@ watch( () => selectedTemplateId.value, (templateId) => { if (route.name !== 'adminGames') return - syncAdminRouteQuery({ gameId: templateId || undefined }) + syncAdminRouteQuery({ topicId: templateId || undefined }) } ) @@ -465,16 +465,16 @@ watch( if (route.name !== 'adminTierlists') return syncAdminRouteQuery({ mode: mode === 'all' ? 'all' : undefined, - gameId: mode === 'all' && adminTierListGameId.value ? adminTierListGameId.value : undefined, + topicId: mode === 'all' && adminTierListGameId.value ? adminTierListGameId.value : undefined, }) } ) watch( () => adminTierListGameId.value, - (gameId) => { + (topicId) => { 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) { const nextQuery = tab === 'game-admin' - ? { gameId: selectedTemplateId.value || undefined } + ? { topicId: selectedTemplateId.value || undefined } : tab === 'tierlists' && tierlistsMode.value === 'all' ? { mode: 'all' } : {} @@ -758,10 +758,10 @@ function setTierlistsMode(mode) { function openTemplateCreateModal() { resetMessages() - if (activeTemplateRequest.value?.type === 'create' && !activeTemplateRequest.value?.targetGameId) { - newTemplateId.value = activeTemplateRequest.value?.draftGameId || '' - newTemplateName.value = activeTemplateRequest.value?.draftGameName || '' - newTemplateIsPublic.value = !!activeTemplateRequest.value?.draftGameIsPublic + if (activeTemplateRequest.value?.type === 'create' && !activeTemplateRequest.value?.targetTopicId) { + newTemplateId.value = activeTemplateRequest.value?.draftTopicId || '' + newTemplateName.value = activeTemplateRequest.value?.draftTopicName || '' + newTemplateIsPublic.value = !!activeTemplateRequest.value?.draftTopicIsPublic } else { newTemplateId.value = '' newTemplateName.value = '' @@ -822,7 +822,7 @@ async function refreshAdminTierLists() { try { const data = await api.listAdminTierLists({ q: adminTierListQuery.value, - gameId: adminTierListGameId.value, + topicId: adminTierListGameId.value, page: adminTierListPage.value, limit: adminTierListLimit.value, }) @@ -839,7 +839,7 @@ async function refreshAdminTierLists() { async function refreshAdminTierListStats() { if (!auth.user?.isAdmin) return 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 = { total: data.total || 0, publicCount: data.publicCount || 0, @@ -857,7 +857,7 @@ async function refreshSelectedTemplateTierListStats(templateId = '') { } try { - const data = await api.getAdminTierListStats({ gameId: templateId }) + const data = await api.getAdminTierListStats({ topicId: templateId }) selectedTemplateTierListStats.value = { total: data.total || 0, publicCount: data.publicCount || 0, @@ -874,15 +874,15 @@ async function refreshTemplateRequests() { const data = await api.listAdminTemplateRequests() templateRequests.value = (data.requests || []).map((request) => ({ ...request, - draftGameId: + draftTopicId: request.type === 'create' ? ('tmpl-' + String(request.id || 'request').replace(/[^a-zA-Z0-9]+/g, '').slice(0, 12).toLowerCase()) - : request.targetGameId || request.sourceGameId || '', - draftGameName: + : request.targetTopicId || request.sourceTopicId || '', + draftTopicName: request.type === 'create' - ? `${request.sourceTierListTitle || request.sourceGameName || '새 템플릿'}` - : request.targetGameName || request.sourceGameName || '', - draftGameIsPublic: false, + ? `${request.sourceTierListTitle || request.sourceTopicName || '새 템플릿'}` + : request.targetTopicName || request.sourceTopicName || '', + draftTopicIsPublic: false, })) } catch (e) { error.value = '템플릿 요청 목록을 불러오지 못했어요.' @@ -1306,8 +1306,8 @@ function submitAdminTierListSearch() { refreshAdminTierLists() } -function setAdminTierListGameId(gameId) { - adminTierListGameId.value = gameId || '' +function setAdminTierListGameId(topicId) { + adminTierListGameId.value = topicId || '' adminTierListPage.value = 1 refreshAdminTierLists() } @@ -1551,8 +1551,8 @@ function closePreviewModal() { } function previewTierListUrl(tierList) { - if (!tierList?.gameId || !tierList?.id) return '' - return editorPath(tierList.gameId, tierList.id, { preview: true }) + if (!tierList?.topicId || !tierList?.id) return '' + return editorPath(tierList.topicId, tierList.id, { preview: true }) } function openTierListImportModal(tierList, items) { @@ -1567,9 +1567,9 @@ function openTierListImportModal(tierList, items) { importModalItems.value = nextItems importModalMode.value = 'existing' importModalTargetTemplateId.value = '' - importModalNewTemplateId.value = tierList.gameId === 'freeform' ? '' : `${tierList.gameId}-copy` + importModalNewTemplateId.value = tierList.topicId === 'freeform' ? '' : `${tierList.topicId}-copy` importModalNewTemplateName.value = - tierList.gameId === 'freeform' ? `${tierList.title} 템플릿` : `${tierList.gameName || tierList.gameId} 파생 템플릿` + tierList.topicId === 'freeform' ? `${tierList.title} 템플릿` : `${tierList.topicName || tierList.topicId} 파생 템플릿` importModalOpen.value = true } @@ -1597,26 +1597,26 @@ async function confirmTierListImport() { } const data = await api.promoteAdminTierListItems(tierList.id, { - gameId: importModalTargetTemplateId.value, + topicId: importModalTargetTemplateId.value, itemIds, }) if (selectedTemplateId.value === importModalTargetTemplateId.value) await loadTemplate() success.value = `${data.items?.length || 0}개의 아이템을 기존 템플릿에 추가했어요.` } else { - const nextGameId = (importModalNewTemplateId.value || '').trim() - const nextGameName = (importModalNewTemplateName.value || '').trim() - if (!nextGameId || !nextGameName) { + const nextTopicId = (importModalNewTemplateId.value || '').trim() + const nextTopicName = (importModalNewTemplateName.value || '').trim() + if (!nextTopicId || !nextTopicName) { error.value = '새 템플릿 ID와 이름을 모두 입력해주세요.' return } const data = await api.createAdminTemplateFromTierList(tierList.id, { - gameId: nextGameId, - name: nextGameName, + topicId: nextTopicId, + name: nextTopicName, itemIds, }) await refreshTemplates() - success.value = `"${data.template?.name || nextGameName}" 템플릿을 생성했어요.` + success.value = `"${data.template?.name || nextTopicName}" 템플릿을 생성했어요.` } closeTierListImportModal() @@ -1631,12 +1631,12 @@ function templateRequestTypeLabel(request) { function templateRequestTargetLabel(request) { if (request.type === 'create') { - if (request.targetGameName || request.targetGameId) { - return `연결된 템플릿 · ${request.targetGameName || request.targetGameId}` + if (request.targetTopicName || request.targetTopicId) { + return `연결된 템플릿 · ${request.targetTopicName || request.targetTopicId}` } return '연결된 템플릿 없음' } - return request.targetGameName || request.targetGameId || request.sourceGameName + return request.targetTopicName || request.targetTopicId || request.sourceTopicName } const displayThumbnailUrl = computed(() => { @@ -2095,7 +2095,7 @@ function userAvatarFallback(user) {