diff --git a/backend/src/routes/admin.js b/backend/src/routes/admin.js index 0315f1d..9d9492a 100644 --- a/backend/src/routes/admin.js +++ b/backend/src/routes/admin.js @@ -132,7 +132,7 @@ router.post(['/games', '/templates'], requireAdmin, async (req, res) => { await updateTopicThumbnail(template.id, copiedThumb) } const savedTemplate = await findTopicById(template.id) - res.json({ game: savedTemplate, template: savedTemplate }) + res.json({ template: savedTemplate }) }) router.patch(['/games/:gameId', '/templates/:templateId'], requireAdmin, async (req, res) => { @@ -147,7 +147,7 @@ router.patch(['/games/:gameId', '/templates/:templateId'], requireAdmin, async ( if (!template) return res.status(404).json({ error: 'not_found' }) const updated = await updateTopicVisibility(template.id, parsed.data.isPublic) - res.json({ game: updated, template: updated }) + res.json({ template: updated }) }) router.patch(['/games/display-order', '/templates/display-order'], requireAdmin, async (req, res) => { @@ -195,7 +195,7 @@ router.post(['/games/:gameId/thumbnail', '/templates/:templateId/thumbnail'], re }) const updated = await updateTopicThumbnail(templateId, optimized.src) - res.json({ game: updated, template: updated }) + res.json({ template: updated }) }) router.post(['/games/:gameId/images', '/templates/:templateId/images'], requireAdmin, upload.array('images', 50), async (req, res) => { diff --git a/backend/src/routes/topics.js b/backend/src/routes/topics.js index 43aa4ad..bb21b72 100644 --- a/backend/src/routes/topics.js +++ b/backend/src/routes/topics.js @@ -6,7 +6,7 @@ const router = express.Router() router.get('/', async (req, res) => { const topics = await listTopics(req.session?.userId || '', { includePrivate: !!req.session?.isAdmin }) - res.json({ games: topics, topics }) + res.json({ topics }) }) router.post('/:topicId/favorite', requireAuth, async (req, res) => { @@ -15,7 +15,7 @@ router.post('/:topicId/favorite', requireAuth, async (req, res) => { await favoriteTopic({ userId: req.session.userId, topicId: topic.id }) const topics = await listTopics(req.session.userId) const updated = topics.find((entry) => entry.id === topic.id) || { ...topic, isFavorited: true } - res.json({ game: updated, topic: updated }) + res.json({ topic: updated }) }) router.delete('/:topicId/favorite', requireAuth, async (req, res) => { @@ -24,14 +24,14 @@ router.delete('/:topicId/favorite', requireAuth, async (req, res) => { await unfavoriteTopic({ userId: req.session.userId, topicId: topic.id }) const topics = await listTopics(req.session.userId) const updated = topics.find((entry) => entry.id === topic.id) || { ...topic, isFavorited: false } - res.json({ game: updated, topic: updated }) + res.json({ topic: updated }) }) router.get('/:topicId', async (req, res) => { const detail = await getTopicDetail(req.params.topicId) if (!detail) return res.status(404).json({ error: 'not_found' }) if (!detail.topic.isPublic && !req.session?.isAdmin) return res.status(404).json({ error: 'not_found' }) - res.json({ game: detail.topic, topic: detail.topic, items: detail.items }) + res.json({ topic: detail.topic, items: detail.items }) }) module.exports = router diff --git a/docs/history.md b/docs/history.md index 3b53732..5cc7135 100644 --- a/docs/history.md +++ b/docs/history.md @@ -1,5 +1,9 @@ # 의사결정 이력 +## 2026-04-02 v1.4.24 +- `topic/template` 소비층이 이미 정리된 상태라면, 공개 주제 API와 관리자 템플릿 API 응답도 이제는 `game` 키를 기본으로 유지할 이유가 크지 않으므로 새 의미 키만 기본으로 내보내는 편이 맞다고 판단했다. +- 다만 관리자 화면 내부 상태 구조를 한 번에 뒤집는 건 위험하므로, 응답은 줄이되 `selectedTemplate.game`처럼 화면 구조에 깊게 퍼진 부분은 프런트에서 한 번 정규화해 받는 점진 방식이 가장 안전하다고 정리했다. + ## 2026-04-02 v1.4.23 - 프런트가 이미 `topic/template` 메서드만 실제로 쓰고 있다면, `api.js` 안에 남은 레거시 `game` 별칭까지 계속 유지하는 건 오히려 정리 상태를 흐리므로 이 단계에서 정리하는 편이 맞다고 판단했다. - 티어표 저장과 템플릿 요청처럼 핵심 생성 흐름은 백엔드 내부 payload도 먼저 `topicId` 기준으로 맞춰 두는 편이, 이후 응답 호환 키를 걷어낼 때 충격을 더 줄인다고 정리했다. diff --git a/docs/todo.md b/docs/todo.md index afbfd98..b7382a4 100644 --- a/docs/todo.md +++ b/docs/todo.md @@ -1,6 +1,8 @@ # 할 일 및 이슈 ## 단기 확인 +- `v1.4.24`에서 공개 주제 API와 관리자 템플릿 API의 기본 응답 키를 더 줄였으므로, 실제 브라우저에서 홈 목록, 즐겨찾기 토글, 주제 상세, 티어표 편집기, 관리자 템플릿 공개 전환/생성이 모두 그대로 정상인지 한 번 더 QA한다. +- 다음 단계에서는 `mapTierListRow`, `mapTemplateRequestRow`, 관리자 route query, 저장 payload 입력 호환에 남아 있는 `gameId/gameName/sourceGameId/targetGameId`를 끝까지 걷어낼지 최종 결정한다. - `v1.4.23`에서 프런트 `api.js`의 레거시 `game` 별칭 메서드와 티어표 저장/요청 내부 payload를 더 걷어냈으므로, 실제 브라우저에서 저장/복사/템플릿 요청/관리자 요청 카드 표시가 그대로 정상인지 한 번 더 QA한다. - 다음 단계에서는 응답의 `game`, `gameId`, `gameName`, `sourceGameId`, `targetGameId` 호환 키를 실제로 제거할지, 아니면 `v1.4` 마감 후 안정화 기간을 두고 걷어낼지 최종 결정한다. - `v1.4.22`에서 공개 주제 라우트 파일을 `topics.js`로 옮겼으므로, 실제 서버 재기동 후 `/api/topics`와 `/api/games` 호환 경로가 모두 정상 응답하는지 한 번 더 QA한다. diff --git a/docs/update.md b/docs/update.md index cd28174..aaa98c2 100644 --- a/docs/update.md +++ b/docs/update.md @@ -1,5 +1,10 @@ # 업데이트 로그 +## 2026-04-02 v1.4.24 +- 공개 주제 API는 이제 `topics` 목록과 `topic` 상세만 기본 응답으로 내려주고, 즐겨찾기 토글도 `topic`만 반환하도록 정리했다. 관리자 템플릿 생성/공개 상태 저장도 `template`만 기본 응답으로 맞췄다. +- 홈, 주제 상세, 티어표 편집기, 관리자 템플릿 관리 화면도 이 변경에 맞춰 `data.topics`, `data.topic`, `data.template`를 직접 읽도록 바꿨다. +- 관리자 내부 상태는 `api.getTopic()` 응답을 받아도 `selectedTemplate.game`에 한 번 정규화하도록 보강해, UI 구조를 크게 흔들지 않으면서 응답 호환 키는 더 줄일 수 있게 정리했다. + ## 2026-04-02 v1.4.23 - 프런트 `api.js`에서 더 이상 쓰지 않는 `listGames / getGame / favoriteGame / updateAdminGame* / listPublicTierLists` 같은 레거시 별칭 메서드를 정리해, 공개/관리자 호출부가 실제로 쓰는 `topic/template` API만 남기도록 정리했다. - 관리자 템플릿 요청 상태와 전체 티어표 관리 카드도 `sourceTopicId / targetTopicId / topicName`을 우선 읽도록 더 당겨, 화면에서 `game` 키를 보는 범위를 줄였다. diff --git a/frontend/src/composables/useAdminGameManager.js b/frontend/src/composables/useAdminGameManager.js index 2da9079..18db8da 100644 --- a/frontend/src/composables/useAdminGameManager.js +++ b/frontend/src/composables/useAdminGameManager.js @@ -132,8 +132,11 @@ export function useAdminGameManager({ try { isGameLoading.value = true const data = await api.getTopic(selectedTemplateId.value) + const loadedTemplate = data.template || data.topic || null selectedTemplate.value = { ...data, + game: loadedTemplate, + template: loadedTemplate, items: (data.items || []).map((item) => ({ ...item, draftLabel: item.label, @@ -169,7 +172,7 @@ export function useAdminGameManager({ if (!res.ok) throw new Error('failed') const data = await res.json() - const createdTemplate = data.template || data.game || {} + const createdTemplate = data.template || {} if (activeTemplateRequest.value?.type === 'create' && activeTemplateRequest.value?.id) { const linkData = await api.linkAdminTemplateRequestGame(activeTemplateRequest.value.id, { gameId: createdTemplate.id, diff --git a/frontend/src/views/AdminView.vue b/frontend/src/views/AdminView.vue index 77cb78c..e2abc7b 100644 --- a/frontend/src/views/AdminView.vue +++ b/frontend/src/views/AdminView.vue @@ -788,7 +788,7 @@ async function selectAdminTemplate(templateId) { async function refreshTemplates() { try { const data = await api.listTopics() - templates.value = data.games || [] + templates.value = data.topics || [] featuredTemplateIds.value = templates.value .filter((template) => template.displayRank != null) .sort((a, b) => a.displayRank - b.displayRank) @@ -1173,7 +1173,7 @@ async function saveTemplateVisibility() { const data = await api.updateAdminTemplate(selectedTemplate.value.game.id, { isPublic: !!selectedTemplate.value.game.isPublic, }) - const nextTemplate = data.template || data.game || {} + const nextTemplate = data.template || {} selectedTemplate.value = { ...selectedTemplate.value, game: { @@ -1616,7 +1616,7 @@ async function confirmTierListImport() { itemIds, }) await refreshTemplates() - success.value = `"${data.game?.name || nextGameName}" 템플릿을 생성했어요.` + success.value = `"${data.template?.name || nextGameName}" 템플릿을 생성했어요.` } closeTierListImportModal() diff --git a/frontend/src/views/GameHubView.vue b/frontend/src/views/GameHubView.vue index 45060c2..102070a 100644 --- a/frontend/src/views/GameHubView.vue +++ b/frontend/src/views/GameHubView.vue @@ -57,7 +57,7 @@ async function loadTierLists() { api.getTopic(topicId.value), api.searchPublicTierListsByTopic(topicId.value, query.value), ]) - topicName.value = topicRes.topic?.name || topicRes.game?.name || '' + topicName.value = topicRes.topic?.name || '' brokenThumbnailIds.value = {} tierLists.value = listRes.tierLists || [] } catch (e) { diff --git a/frontend/src/views/HomeView.vue b/frontend/src/views/HomeView.vue index 1289e5b..b579dd1 100644 --- a/frontend/src/views/HomeView.vue +++ b/frontend/src/views/HomeView.vue @@ -37,7 +37,7 @@ const templates = computed(() => { async function loadTemplates() { try { const data = await api.listTopics() - templateRecords.value = data.topics || data.games || [] + templateRecords.value = data.topics || [] } catch (e) { error.value = '백엔드에 연결할 수 없어요. backend 서버가 실행 중인지 확인해주세요.' } @@ -61,7 +61,7 @@ async function toggleFavorite(template, event) { try { loadingFavoriteId.value = template.id const res = template.isFavorited ? await api.unfavoriteTopic(template.id) : await api.favoriteTopic(template.id) - templateRecords.value = templateRecords.value.map((entry) => (entry.id === template.id ? { ...entry, ...(res.topic || res.game || {}) } : entry)) + templateRecords.value = templateRecords.value.map((entry) => (entry.id === template.id ? { ...entry, ...(res.topic || {}) } : entry)) } catch (e) { error.value = '즐겨찾기 변경에 실패했어요.' } finally { diff --git a/frontend/src/views/TierEditorView.vue b/frontend/src/views/TierEditorView.vue index 34cf979..a07ec61 100644 --- a/frontend/src/views/TierEditorView.vue +++ b/frontend/src/views/TierEditorView.vue @@ -899,7 +899,7 @@ onMounted(() => { try { const topicRes = await api.getTopic(templateId.value) - templateName.value = topicRes.topic?.name || topicRes.game?.name || templateId.value + templateName.value = topicRes.topic?.name || templateId.value const base = (topicRes.items || []).map((img) => ({ id: img.id, src: img.src,