From 337bee8900e9948ced4c3efe8cf3fdc3ce7b307c Mon Sep 17 00:00:00 2001 From: zenn Date: Thu, 2 Apr 2026 20:25:49 +0900 Subject: [PATCH] =?UTF-8?q?=EB=A6=B4=EB=A6=AC=EC=8A=A4:=20v1.4.21=20?= =?UTF-8?q?=ED=94=84=EB=9F=B0=ED=8A=B8=20topic=20=EC=9D=91=EB=8B=B5=20?= =?UTF-8?q?=EC=86=8C=EB=B9=84=20=EC=A0=95=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/history.md | 4 ++++ docs/todo.md | 2 ++ docs/update.md | 4 ++++ frontend/src/composables/useAdminGameManager.js | 15 ++++++++------- frontend/src/views/AdminView.vue | 5 +++-- frontend/src/views/FavoriteTierListsView.vue | 2 +- frontend/src/views/GameHubView.vue | 4 ++-- frontend/src/views/HomeView.vue | 4 ++-- frontend/src/views/MyTierListsView.vue | 2 +- frontend/src/views/SearchResultsView.vue | 2 +- frontend/src/views/TierEditorView.vue | 6 +++--- 11 files changed, 31 insertions(+), 19 deletions(-) diff --git a/docs/history.md b/docs/history.md index 088678c..df73690 100644 --- a/docs/history.md +++ b/docs/history.md @@ -1,5 +1,9 @@ # 의사결정 이력 +## 2026-04-02 v1.4.21 +- 백엔드에서 `topic/template` 응답을 내보내더라도 프런트가 계속 `game` 키만 읽으면 호환 레이어가 끝나지 않으므로, 이번 단계부터는 실제 사용자 화면과 관리자 저장 흐름도 새 키를 우선 읽게 맞추는 편이 맞다고 판단했다. +- 이 구간은 외부 API를 끊는 작업이 아니라 “프런트가 새 의미를 먼저 받아들이는 단계”이므로, 기존 `game` 키는 fallback으로만 남겨 두고 단계적으로 걷어내는 편이 가장 안전하다고 정리했다. + ## 2026-04-02 v1.4.20 - 스키마만 `topic`으로 옮기고 함수명/라우트 내부가 계속 `game`으로 남아 있으면 이후 유지보수에서 계속 의미 충돌이 생기므로, 이번 단계부터는 백엔드 export와 주요 라우트 내부 이름도 `topic/template`를 기본으로 읽히게 정리하는 편이 맞다고 판단했다. - 다만 외부 API와 프런트 호환을 한 번에 끊는 건 위험하므로, 실제 구현은 새 `topic` 이름을 기본으로 쓰되 기존 `game` 이름은 alias와 호환 응답으로 잠시 유지하는 점진 전환이 가장 안전하다고 정리했다. diff --git a/docs/todo.md b/docs/todo.md index 438477f..a87f7ef 100644 --- a/docs/todo.md +++ b/docs/todo.md @@ -1,6 +1,8 @@ # 할 일 및 이슈 ## 단기 확인 +- `v1.4.21`에서 홈/주제 상세/에디터/나의 티어표/즐겨찾기/검색 결과/관리자 템플릿 생성이 `topic/template` 응답 키를 우선 읽도록 바뀌었으므로, 실제 브라우저에서 즐겨찾기 토글과 에디터 이동, 관리자 신규 템플릿 생성이 모두 정상인지 한 번 더 QA한다. +- 다음 단계에서는 실제 응답의 `game`, `gameId`, `gameName` 호환 키를 어디까지 남길지, 그리고 `/api/games` 호환 경로와 `games.js` 파일명을 언제 걷어낼지 최종 범위를 정한다. - `v1.4.20`에서 백엔드 `db` export와 공개/관리자 라우트 내부 이름을 `topic/template` 기준으로 정리했으므로, 실제 브라우저와 관리자 화면에서 주제 목록/즐겨찾기/템플릿 생성/요청 반영 흐름이 모두 정상인지 한 번 더 QA한다. - 다음 단계에서는 남아 있는 호환 응답 키 `game`, `gameId`, `gameName`과 레거시 route 파일명 `games.js`를 어디까지 실제 `topic` 이름으로 마감할지 범위를 결정한다. - `v1.4.19`에서 템플릿 기본 아이템 삭제는 기존 저장 티어표를 보존하도록 정책이 바뀌었으므로, 실제 운영 데이터에서 삭제 후 예전 티어표의 배치/대기풀이 그대로 유지되는지와 새 티어표 생성 시에만 아이템이 빠지는지 한 번 더 QA한다. diff --git a/docs/update.md b/docs/update.md index fbb30a3..a33dc46 100644 --- a/docs/update.md +++ b/docs/update.md @@ -1,5 +1,9 @@ # 업데이트 로그 +## 2026-04-02 v1.4.21 +- 프런트의 실제 소비 지점도 `topic/template` 응답 키를 우선 읽도록 옮겼다. 홈의 즐겨찾기 토글, 주제 상세 헤더, 티어표 편집기 템플릿 로딩, 나의 티어표/즐겨찾기/검색 결과의 에디터 이동이 이제 `topic`, `topicId`, `template`를 먼저 사용한다. +- 관리자 템플릿 공개 상태 저장과 신규 템플릿 생성 흐름도 `data.template`를 우선 읽고, 기존 `data.game`은 fallback으로만 남겨 프런트와 백엔드의 의미 이름이 한 단계 더 가까워지게 맞췄다. + ## 2026-04-02 v1.4.20 - 백엔드 `db`와 라우트 내부 이름층을 한 단계 더 `topic` 기준으로 옮겼다. `listTopics / findTopicById / getTopicDetail / createTopic / updateTopicThumbnail / updateTopicVisibility`, `createTopicItem / updateTopicItemLabel / updateTopicItemDisplayOrder / deleteTopicItem / deleteTopic` 같은 이름을 실제 export로 추가하고, 기존 `game` 이름은 호환 alias로만 남겼다. - 공개 주제 라우트는 이제 `listTopics`, `getTopicDetail`, `favoriteTopic` 기준으로 동작하고, 백엔드 진입점도 `gamesRoutes` 대신 `topicsRoutes`라는 이름으로 읽히도록 정리했다. diff --git a/frontend/src/composables/useAdminGameManager.js b/frontend/src/composables/useAdminGameManager.js index fc1a072..2da9079 100644 --- a/frontend/src/composables/useAdminGameManager.js +++ b/frontend/src/composables/useAdminGameManager.js @@ -169,27 +169,28 @@ export function useAdminGameManager({ if (!res.ok) throw new Error('failed') const data = await res.json() + const createdTemplate = data.template || data.game || {} if (activeTemplateRequest.value?.type === 'create' && activeTemplateRequest.value?.id) { const linkData = await api.linkAdminTemplateRequestGame(activeTemplateRequest.value.id, { - gameId: data.game.id, + gameId: createdTemplate.id, }) activeTemplateRequest.value = { ...activeTemplateRequest.value, - targetGameId: linkData.request?.targetGameId || data.game.id, - targetGameName: linkData.request?.targetGameName || data.game.name || nextGameName, + targetGameId: linkData.request?.targetGameId || createdTemplate.id, + targetGameName: linkData.request?.targetGameName || createdTemplate.name || nextGameName, } 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 || data.game.id, - targetGameName: linkData.request?.targetGameName || data.game.name || nextGameName, + targetGameId: linkData.request?.targetGameId || createdTemplate.id, + targetGameName: linkData.request?.targetGameName || createdTemplate.name || nextGameName, }) } } await refreshTemplates() - selectedTemplateId.value = data.game.id - if (customItemModalOpen.value) customItemModalTargetTemplateId.value = data.game.id + selectedTemplateId.value = createdTemplate.id + if (customItemModalOpen.value) customItemModalTargetTemplateId.value = createdTemplate.id closeTemplateCreateModal() await loadTemplate({ preserveUploadState }) if (!preserveUploadState && activeTemplateRequest.value?.id) { diff --git a/frontend/src/views/AdminView.vue b/frontend/src/views/AdminView.vue index 194f06b..77cb78c 100644 --- a/frontend/src/views/AdminView.vue +++ b/frontend/src/views/AdminView.vue @@ -1173,15 +1173,16 @@ async function saveTemplateVisibility() { const data = await api.updateAdminTemplate(selectedTemplate.value.game.id, { isPublic: !!selectedTemplate.value.game.isPublic, }) + const nextTemplate = data.template || data.game || {} selectedTemplate.value = { ...selectedTemplate.value, game: { ...selectedTemplate.value.game, - ...data.game, + ...nextTemplate, }, } await refreshTemplates() - success.value = data.game?.isPublic ? '템플릿을 공개 상태로 전환했어요.' : '템플릿을 비공개 상태로 전환했어요.' + success.value = nextTemplate?.isPublic ? '템플릿을 공개 상태로 전환했어요.' : '템플릿을 비공개 상태로 전환했어요.' return true } catch (e) { error.value = '템플릿 공개 상태를 저장하지 못했어요.' diff --git a/frontend/src/views/FavoriteTierListsView.vue b/frontend/src/views/FavoriteTierListsView.vue index 327efb9..a5d7b3c 100644 --- a/frontend/src/views/FavoriteTierListsView.vue +++ b/frontend/src/views/FavoriteTierListsView.vue @@ -48,7 +48,7 @@ async function loadFavorites() { } function openTierList(tierList) { - router.push(editorPath(tierList.gameId, tierList.id)) + router.push(editorPath(tierList.topicId || tierList.gameId, tierList.id)) } onMounted(loadFavorites) diff --git a/frontend/src/views/GameHubView.vue b/frontend/src/views/GameHubView.vue index 2e3b60c..45060c2 100644 --- a/frontend/src/views/GameHubView.vue +++ b/frontend/src/views/GameHubView.vue @@ -53,11 +53,11 @@ function handleThumbnailError(tierListId) { async function loadTierLists() { isTopicLoading.value = true try { - const [gameRes, listRes] = await Promise.all([ + const [topicRes, listRes] = await Promise.all([ api.getTopic(topicId.value), api.searchPublicTierListsByTopic(topicId.value, query.value), ]) - topicName.value = gameRes.game?.name || '' + topicName.value = topicRes.topic?.name || topicRes.game?.name || '' brokenThumbnailIds.value = {} tierLists.value = listRes.tierLists || [] } catch (e) { diff --git a/frontend/src/views/HomeView.vue b/frontend/src/views/HomeView.vue index 7bfa606..1289e5b 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.games || [] + templateRecords.value = data.topics || data.games || [] } 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.game } : entry)) + templateRecords.value = templateRecords.value.map((entry) => (entry.id === template.id ? { ...entry, ...(res.topic || res.game || {}) } : entry)) } catch (e) { error.value = '즐겨찾기 변경에 실패했어요.' } finally { diff --git a/frontend/src/views/MyTierListsView.vue b/frontend/src/views/MyTierListsView.vue index 2aef889..5dace39 100644 --- a/frontend/src/views/MyTierListsView.vue +++ b/frontend/src/views/MyTierListsView.vue @@ -60,7 +60,7 @@ onMounted(async () => { }) function openList(t) { - router.push(editorPath(t.gameId, t.id)) + router.push(editorPath(t.topicId || t.gameId, t.id)) } diff --git a/frontend/src/views/SearchResultsView.vue b/frontend/src/views/SearchResultsView.vue index 6da65f9..5b83090 100644 --- a/frontend/src/views/SearchResultsView.vue +++ b/frontend/src/views/SearchResultsView.vue @@ -38,7 +38,7 @@ function tierListThumbnailUrl(tierList) { } function openTierList(tierList) { - router.push(editorPath(tierList.gameId, tierList.id)) + router.push(editorPath(tierList.topicId || tierList.gameId, tierList.id)) } async function loadResults() { diff --git a/frontend/src/views/TierEditorView.vue b/frontend/src/views/TierEditorView.vue index 134f62a..34cf979 100644 --- a/frontend/src/views/TierEditorView.vue +++ b/frontend/src/views/TierEditorView.vue @@ -898,9 +898,9 @@ onMounted(() => { } try { - const gameRes = await api.getTopic(templateId.value) - templateName.value = gameRes.game?.name || templateId.value - const base = (gameRes.items || []).map((img) => ({ + const topicRes = await api.getTopic(templateId.value) + templateName.value = topicRes.topic?.name || topicRes.game?.name || templateId.value + const base = (topicRes.items || []).map((img) => ({ id: img.id, src: img.src, label: img.label,