diff --git a/backend/index.js b/backend/index.js index bf902cb..27c39e0 100644 --- a/backend/index.js +++ b/backend/index.js @@ -80,7 +80,6 @@ app.use(async (req, res, next) => { }) app.use('/api/auth', authRoutes) -app.use('/api/games', topicsRoutes) app.use('/api/topics', topicsRoutes) app.use('/api/tierlists', tierListsRoutes) app.use('/api/admin', adminRoutes) diff --git a/backend/src/routes/admin.js b/backend/src/routes/admin.js index 6857b52..c1edb43 100644 --- a/backend/src/routes/admin.js +++ b/backend/src/routes/admin.js @@ -55,8 +55,8 @@ const { createMemoryUpload, writeOptimizedImage, getImageOptimizationQueueState const router = express.Router() -function getTemplateIdParam(req) { - return req.params.templateId || req.params.gameId || '' +function getTemplateIdFromParams(req) { + return req.params.templateId || '' } function buildUploadFilename(file) { @@ -115,7 +115,7 @@ function canManageAdminRole(actingUser, primaryAdmin) { return !!actingUser?.isAdmin && primaryAdmin?.id === actingUser.id } -router.post(['/games', '/templates'], requireAdmin, async (req, res) => { +router.post('/templates', requireAdmin, async (req, res) => { const schema = z.object({ id: z.string().min(1), name: z.string().min(1).max(60), @@ -125,7 +125,7 @@ router.post(['/games', '/templates'], requireAdmin, async (req, res) => { const parsed = schema.safeParse(req.body) if (!parsed.success) return res.status(400).json({ error: 'bad_request' }) const exists = await findTopicById(parsed.data.id) - if (exists) return res.status(409).json({ error: 'game_id_taken' }) + if (exists) return res.status(409).json({ error: 'topic_id_taken' }) const template = await createTopic({ id: parsed.data.id, name: parsed.data.name, isPublic: parsed.data.isPublic }) if (parsed.data.thumbnailSrc) { const copiedThumb = await copyUploadIntoGameAsset(parsed.data.thumbnailSrc) @@ -135,14 +135,14 @@ router.post(['/games', '/templates'], requireAdmin, async (req, res) => { res.json({ template: savedTemplate }) }) -router.patch(['/games/:gameId', '/templates/:templateId'], requireAdmin, async (req, res) => { +router.patch('/templates/:templateId', requireAdmin, async (req, res) => { const schema = z.object({ isPublic: z.boolean(), }) const parsed = schema.safeParse(req.body) if (!parsed.success) return res.status(400).json({ error: 'bad_request' }) - const templateId = getTemplateIdParam(req) + const templateId = getTemplateIdFromParams(req) const template = await findTopicById(templateId) if (!template) return res.status(404).json({ error: 'not_found' }) @@ -150,7 +150,7 @@ router.patch(['/games/:gameId', '/templates/:templateId'], requireAdmin, async ( res.json({ template: updated }) }) -router.patch(['/games/display-order', '/templates/display-order'], requireAdmin, async (req, res) => { +router.patch('/templates/display-order', requireAdmin, async (req, res) => { const schema = z.object({ topicIds: z.array(z.string().min(1)).max(50), }) @@ -164,14 +164,14 @@ router.patch(['/games/display-order', '/templates/display-order'], requireAdmin, res.json({ templates: updatedTemplates }) }) -router.patch(['/games/:gameId/items/display-order', '/templates/:templateId/items/display-order'], requireAdmin, async (req, res) => { +router.patch('/templates/:templateId/items/display-order', requireAdmin, async (req, res) => { const schema = z.object({ itemIds: z.array(z.string().min(1)).min(1), }) const parsed = schema.safeParse(req.body) if (!parsed.success) return res.status(400).json({ error: 'bad_request' }) - const templateId = getTemplateIdParam(req) + const templateId = getTemplateIdFromParams(req) const template = await findTopicById(templateId) if (!template) return res.status(404).json({ error: 'not_found' }) @@ -179,9 +179,9 @@ router.patch(['/games/:gameId/items/display-order', '/templates/:templateId/item res.json({ items }) }) -router.post(['/games/:gameId/thumbnail', '/templates/:templateId/thumbnail'], requireAdmin, upload.single('thumbnail'), async (req, res) => { +router.post('/templates/:templateId/thumbnail', requireAdmin, upload.single('thumbnail'), async (req, res) => { if (!req.file) return res.status(400).json({ error: 'file_required' }) - const templateId = getTemplateIdParam(req) + const templateId = getTemplateIdFromParams(req) const template = await findTopicById(templateId) if (!template) return res.status(404).json({ error: 'not_found' }) @@ -198,10 +198,10 @@ router.post(['/games/:gameId/thumbnail', '/templates/:templateId/thumbnail'], re res.json({ template: updated }) }) -router.post(['/games/:gameId/images', '/templates/:templateId/images'], requireAdmin, upload.array('images', 50), async (req, res) => { +router.post('/templates/:templateId/images', requireAdmin, upload.array('images', 50), async (req, res) => { const files = Array.isArray(req.files) ? req.files : [] if (!files.length) return res.status(400).json({ error: 'file_required' }) - const templateId = getTemplateIdParam(req) + const templateId = getTemplateIdFromParams(req) const template = await findTopicById(templateId) if (!template) return res.status(404).json({ error: 'not_found' }) @@ -233,15 +233,15 @@ router.post(['/games/:gameId/images', '/templates/:templateId/images'], requireA res.json({ item: items[0], items }) }) -router.delete(['/games/:gameId/items/:itemId', '/templates/:templateId/items/:itemId'], requireAdmin, async (req, res) => { - const template = await findTopicById(getTemplateIdParam(req)) +router.delete('/templates/:templateId/items/:itemId', requireAdmin, async (req, res) => { + const template = await findTopicById(getTemplateIdFromParams(req)) if (!template) return res.status(404).json({ error: 'not_found' }) await deleteTopicItem(req.params.itemId) res.json({ ok: true }) }) -router.get(['/games/:gameId/items/:itemId/usage', '/templates/:templateId/items/:itemId/usage'], requireAdmin, async (req, res) => { - const template = await findTopicById(getTemplateIdParam(req)) +router.get('/templates/:templateId/items/:itemId/usage', requireAdmin, async (req, res) => { + const template = await findTopicById(getTemplateIdFromParams(req)) if (!template) return res.status(404).json({ error: 'not_found' }) const item = await findTopicItemById(req.params.itemId) if (!item || item.topicId !== template.id) return res.status(404).json({ error: 'not_found' }) @@ -249,12 +249,12 @@ router.get(['/games/:gameId/items/:itemId/usage', '/templates/:templateId/items/ res.json({ usage }) }) -router.patch(['/games/:gameId/items/:itemId', '/templates/:templateId/items/:itemId'], requireAdmin, async (req, res) => { +router.patch('/templates/:templateId/items/:itemId', requireAdmin, async (req, res) => { const schema = z.object({ label: z.string().trim().min(1).max(60) }) const parsed = schema.safeParse(req.body) if (!parsed.success) return res.status(400).json({ error: 'bad_request' }) - const template = await findTopicById(getTemplateIdParam(req)) + const template = await findTopicById(getTemplateIdFromParams(req)) if (!template) return res.status(404).json({ error: 'not_found' }) const updated = await updateTopicItemLabel(req.params.itemId, parsed.data.label) @@ -262,8 +262,8 @@ router.patch(['/games/:gameId/items/:itemId', '/templates/:templateId/items/:ite res.json({ item: updated }) }) -router.delete(['/games/:gameId', '/templates/:templateId'], requireAdmin, async (req, res) => { - const templateId = getTemplateIdParam(req) +router.delete('/templates/:templateId', requireAdmin, async (req, res) => { + const templateId = getTemplateIdFromParams(req) const template = await findTopicById(templateId) if (!template) return res.status(404).json({ error: 'not_found' }) await deleteTopic(templateId) @@ -691,7 +691,7 @@ router.post('/tierlists/:tierListId/promote-items', requireAdmin, async (req, re res.json({ items }) }) -router.post('/tierlists/:tierListId/create-game-template', requireAdmin, async (req, res) => { +router.post('/tierlists/:tierListId/create-template', requireAdmin, async (req, res) => { const schema = z.object({ topicId: z.string().trim().min(1).max(120), name: z.string().trim().min(1).max(120), @@ -804,7 +804,7 @@ router.post('/template-requests/:requestId/review', requireAdmin, async (req, re res.json({ request }) }) -router.post('/template-requests/:requestId/link-game', requireAdmin, async (req, res) => { +router.post('/template-requests/:requestId/link-template', requireAdmin, async (req, res) => { const schema = z.object({ topicId: z.string().trim().min(1).max(120), }) diff --git a/docs/history.md b/docs/history.md index 074685f..e7b7777 100644 --- a/docs/history.md +++ b/docs/history.md @@ -1,5 +1,9 @@ # 의사결정 이력 +## 2026-04-02 v1.4.26 +- `topic/template` 표면을 거의 마감한 시점에서는 관리자 API와 관리자 화면 경로까지 계속 `/games` alias를 유지하는 편보다, 실제 사용하는 `templates` 경로만 남기고 예전 관리자 주소는 redirect로만 정리하는 편이 더 일관되고 안전하다고 판단했다. +- 공개 사용자 북마크는 여전히 `/games -> /topics` redirect가 필요하지만, 백엔드 API의 `/api/games`까지 계속 유지할 이유는 작아졌으므로 이 단계에서 `/api/topics`만 남기는 편이 맞다고 정리했다. + ## 2026-04-02 v1.4.25 - 이제 프런트와 백엔드 소비층이 `topic/template`를 기본으로 읽을 준비가 되었으므로, 응답과 payload에 `gameId / gameName` 호환 키를 오래 남기는 것보다 실제 표면을 먼저 정리하는 편이 더 낫다고 판단했다. - 다만 오래된 외부 링크까지 한 번에 끊는 건 위험하므로, 이번 단계에서는 데이터/응답/프런트 소비는 `topic`으로 마감하되 `/games/:gameId`와 관리자 route alias 같은 레거시 주소만 마지막 호환 레이어로 남기는 점진 종료가 가장 안전하다고 정리했다. diff --git a/docs/map.md b/docs/map.md index 40a0890..75a3ff0 100644 --- a/docs/map.md +++ b/docs/map.md @@ -2,18 +2,18 @@ ## `/` - 화면 파일: `frontend/src/views/HomeView.vue` -- 역할: 데스크톱 기본 4열 게임 카드 라이브러리 대시보드, 상단 메인 썸네일과 `게임명 + 작은 ID` 메타를 가진 템플릿 선택 카드, 게임 카드 클릭 이동, `직접 티어표 만들기` 진입 -- 연동 API: `GET /api/games` +- 역할: 데스크톱 기본 4열 주제 카드 라이브러리 대시보드, 상단 메인 썸네일과 `주제명 + 작은 ID` 메타를 가진 템플릿 선택 카드, 주제 카드 클릭 이동, `직접 티어표 만들기` 진입 +- 연동 API: `GET /api/topics` -## `/games/:gameId` +## `/topics/:topicId` - 화면 파일: `frontend/src/views/GameHubView.vue` -- 역할: 선택한 게임 정보 표시, 공개 티어표 목록 표시, 제목/작성자 검색, 티어표별 `상단 썸네일 / 제목+좋아요 / 작성자+최종 수정일` 카드 표시, 새 티어표 작성은 우측 하단 CTA로 진입 -- 연동 API: `GET /api/games/:gameId`, `GET /api/tierlists/public`, `POST /api/tierlists/:id/favorite`, `DELETE /api/tierlists/:id/favorite` +- 역할: 선택한 주제 정보 표시, 공개 티어표 목록 표시, 제목/작성자 검색, 티어표별 `상단 썸네일 / 제목+좋아요 / 작성자+최종 수정일` 카드 표시, 새 티어표 작성은 우측 하단 CTA로 진입 +- 연동 API: `GET /api/topics/:topicId`, `GET /api/tierlists/public`, `POST /api/tierlists/:id/favorite`, `DELETE /api/tierlists/:id/favorite` -## `/editor/:gameId/new`, `/editor/:gameId/:tierListId` +## `/editor/:topicId/new`, `/editor/:topicId/:tierListId` - 화면 파일: `frontend/src/views/TierEditorView.vue` - 역할: 티어 그룹 편집, 티어 행 추가/삭제, 보드 옆 아이템 풀에서 관리자 아이템/커스텀 아이템 다중 드래그 앤 드롭 업로드, 공통 오른쪽 레일 안에 직접 배치되는 우측 편집 섹션에서 티어표 제목/설명/대표 썸네일/공개 여부/저장 제어와 커스텀 아이템 이름 정리, 즐겨찾기 토글, PNG 다운로드, 저장된 티어표 기준 템플릿 등록/업데이트 요청 -- 연동 API: `GET /api/games/:gameId`, `GET /api/tierlists/:id`, `POST /api/tierlists/:id/favorite`, `DELETE /api/tierlists/:id/favorite`, `POST /api/tierlists/thumbnail`, `POST /api/tierlists/custom-items`, `POST /api/tierlists`, `POST /api/tierlists/template-request` +- 연동 API: `GET /api/topics/:topicId`, `GET /api/tierlists/:id`, `POST /api/tierlists/:id/favorite`, `DELETE /api/tierlists/:id/favorite`, `POST /api/tierlists/thumbnail`, `POST /api/tierlists/custom-items`, `POST /api/tierlists`, `POST /api/tierlists/template-request` ## `/login` - 화면 파일: `frontend/src/views/LoginView.vue` @@ -37,8 +37,8 @@ ## `/admin` - 화면 파일: `frontend/src/views/AdminView.vue` -- 역할: 공통 우측 패널 대신 전용 `320px` 운영 패널을 사용해 `게임 관리 / 아이템 관리 / 티어표 관리 / 회원 관리` 탭과 검색·필터·빠른 작업을 우측에 배치하고, 중앙에는 선택된 게임 상세, 커스텀 아이템 목록, 템플릿 요청/전체 티어표, 회원 카드 등 실제 관리 대상만 표시, 기본 아이템 다중 드래그 앤 드롭 업로드, 기본 아이템 이름 수정, 사용자 커스텀 아이템 검색/페이지네이션/사용 횟수 확인/미사용 이미지 개별·일괄 삭제, 사용자 커스텀 아이템의 기본 템플릿 승격, 전체 티어표 검색/페이지네이션/공개 여부 확인/썸네일 클릭 기반 완성본 보기, 티어표의 추가 커스텀 아이템을 모달 기반으로 기존 템플릿 또는 새 템플릿에 가져오기, freeform 티어표의 게임 템플릿화, 사용자 템플릿 등록/업데이트 요청 승인·반려와 일반 완성본과 같은 보드 문법의 요청 미리보기, 회원 비밀번호 초기화 포함 회원 관리, 파일 입력 초기화, 아이템 삭제, 게임 삭제 -- 연동 API: `POST /api/admin/games`, `POST /api/admin/games/:gameId/thumbnail`, `POST /api/admin/games/:gameId/images`, `PATCH /api/admin/games/:gameId/items/:itemId`, `GET /api/admin/custom-items`, `POST /api/admin/custom-items/:itemId/promote`, `DELETE /api/admin/custom-items/:itemId`, `DELETE /api/admin/custom-items`, `GET /api/admin/tierlists`, `GET /api/admin/template-requests`, `POST /api/admin/template-requests/:requestId/approve`, `POST /api/admin/template-requests/:requestId/reject`, `POST /api/admin/tierlists/:tierListId/promote-items`, `POST /api/admin/tierlists/:tierListId/create-game-template`, `GET /api/admin/users`, `PATCH /api/admin/users/:userId`, `PATCH /api/admin/users/:userId/password`, `DELETE /api/admin/users/:userId`, `DELETE /api/admin/games/:gameId/items/:itemId`, `DELETE /api/admin/games/:gameId` +- 역할: 공통 우측 패널 대신 전용 `320px` 운영 패널을 사용해 `템플릿 관리 / 아이템 관리 / 티어표 관리 / 회원 관리` 탭과 검색·필터·빠른 작업을 우측에 배치하고, 중앙에는 선택된 템플릿 상세, 커스텀 아이템 목록, 템플릿 요청/전체 티어표, 회원 카드 등 실제 관리 대상만 표시, 기본 아이템 다중 드래그 앤 드롭 업로드, 기본 아이템 이름 수정, 사용자 커스텀 아이템 검색/페이지네이션/사용 횟수 확인/미사용 이미지 개별·일괄 삭제, 사용자 커스텀 아이템의 기본 템플릿 승격, 전체 티어표 검색/페이지네이션/공개 여부 확인/썸네일 클릭 기반 완성본 보기, 티어표의 추가 커스텀 아이템을 모달 기반으로 기존 템플릿 또는 새 템플릿에 가져오기, freeform 티어표의 템플릿화, 사용자 템플릿 등록/업데이트 요청 승인·반려와 일반 완성본과 같은 보드 문법의 요청 미리보기, 회원 비밀번호 초기화 포함 회원 관리, 파일 입력 초기화, 아이템 삭제, 템플릿 삭제 +- 연동 API: `POST /api/admin/templates`, `POST /api/admin/templates/:templateId/thumbnail`, `POST /api/admin/templates/:templateId/images`, `PATCH /api/admin/templates/:templateId/items/:itemId`, `GET /api/admin/custom-items`, `POST /api/admin/custom-items/:itemId/promote`, `DELETE /api/admin/custom-items/:itemId`, `DELETE /api/admin/custom-items`, `GET /api/admin/tierlists`, `GET /api/admin/template-requests`, `POST /api/admin/template-requests/:requestId/approve`, `POST /api/admin/template-requests/:requestId/reject`, `POST /api/admin/template-requests/:requestId/link-template`, `POST /api/admin/tierlists/:tierListId/promote-items`, `POST /api/admin/tierlists/:tierListId/create-template`, `GET /api/admin/users`, `PATCH /api/admin/users/:userId`, `PATCH /api/admin/users/:userId/password`, `DELETE /api/admin/users/:userId`, `DELETE /api/admin/templates/:templateId/items/:itemId`, `DELETE /api/admin/templates/:templateId` ## `/profile` - 화면 파일: `frontend/src/views/ProfileView.vue` @@ -56,6 +56,6 @@ - 로컬 DB 실행 설정: `docker-compose.yml` - 로컬 MariaDB 가이드: `docs/local-mariadb.md` - 인증 라우트: `backend/src/routes/auth.js` -- 게임 라우트: `backend/src/routes/games.js` +- 주제 라우트: `backend/src/routes/topics.js` - 티어표 라우트: `backend/src/routes/tierlists.js` - 관리자 라우트: `backend/src/routes/admin.js` diff --git a/docs/spec.md b/docs/spec.md index 6ce7228..b5e41ca 100644 --- a/docs/spec.md +++ b/docs/spec.md @@ -114,12 +114,12 @@ - `GET /api/auth/me` - `GET /api/auth/meta` - `POST /api/auth/profile` -- 게임 - - `GET /api/games` - - `GET /api/games/:gameId` +- 주제 + - `GET /api/topics` + - `GET /api/topics/:topicId` - 티어표 - `GET /api/tierlists/public` - - `gameId` 없이 `q`만 전달하면 전체 공개 티어표 검색에 사용한다. + - `topicId` 없이 `q`만 전달하면 전체 공개 티어표 검색에 사용한다. - `GET /api/tierlists/me` - `GET /api/tierlists/favorites/me` - `GET /api/tierlists/:id` @@ -131,17 +131,18 @@ - `POST /api/tierlists/custom-items` - `POST /api/tierlists` - 관리자 - - `POST /api/admin/games` - - `POST /api/admin/games/:gameId/thumbnail` - - `POST /api/admin/games/:gameId/images` + - `POST /api/admin/templates` + - `POST /api/admin/templates/:templateId/thumbnail` + - `POST /api/admin/templates/:templateId/images` - 여러 이미지를 한 번에 업로드할 수 있고, 별도 라벨이 없으면 파일명 기준으로 기본 아이템 이름을 만든다. - - `PATCH /api/admin/games/:gameId/items/:itemId` + - `PATCH /api/admin/templates/:templateId/items/:itemId` - `GET /api/admin/tierlists` - `GET /api/admin/template-requests` - `POST /api/admin/template-requests/:requestId/approve` - `POST /api/admin/template-requests/:requestId/reject` + - `POST /api/admin/template-requests/:requestId/link-template` - `POST /api/admin/tierlists/:tierListId/promote-items` - - `POST /api/admin/tierlists/:tierListId/create-game-template` + - `POST /api/admin/tierlists/:tierListId/create-template` - `GET /api/admin/custom-items` - `POST /api/admin/custom-items/:itemId/promote` - `DELETE /api/admin/custom-items/:itemId` @@ -150,8 +151,8 @@ - `PATCH /api/admin/users/:userId` - `PATCH /api/admin/users/:userId/password` - `DELETE /api/admin/users/:userId` - - `DELETE /api/admin/games/:gameId/items/:itemId` - - `DELETE /api/admin/games/:gameId` + - `DELETE /api/admin/templates/:templateId/items/:itemId` + - `DELETE /api/admin/templates/:templateId` ## 관리자 화면 메모 - 썸네일은 16:9 비율 미리보기 후 `썸네일 적용` 버튼으로 실제 반영한다. diff --git a/docs/todo.md b/docs/todo.md index 2582ac2..9dd80b1 100644 --- a/docs/todo.md +++ b/docs/todo.md @@ -1,8 +1,11 @@ # 할 일 및 이슈 ## 단기 확인 +- `v1.4.26`에서 관리자 기본 경로를 `/admin/templates`로 바꾸고 `/api/admin/templates`만 남겼으므로, 관리자 진입/새로고침/뒤로가기와 템플릿 생성·썸네일 업로드·아이템 추가가 모두 정상인지 확인한다. +- `v1.4.26`에서 공개 API `/api/games`를 제거했으므로, 실제 서버 재시작 후 홈/주제 상세/티어표 편집기에서 `/api/topics`만으로 모두 정상 동작하는지 확인한다. +- 오래된 관리자 주소 `/admin/games`는 redirect만 남겼으므로, 북마크로 직접 진입해도 `/admin/templates`로 자연스럽게 바뀌는지 본다. - `v1.4.25`에서 티어표/요청 응답의 `gameId / gameName` 호환 키를 실제로 제거했으므로, 브라우저에서 홈 목록, 주제 상세, 저장된 티어표 열기, 즐겨찾기, 검색 결과, 관리자 템플릿 요청/전체 티어표 관리가 모두 정상 동작하는지 한 번 더 QA한다. -- `v1.4.25`에서 관리자 route query와 편집기 저장/request payload를 `topicId` 기준으로 옮겼으므로, `/admin/games?topicId=...`, `/admin/tierlists?mode=all&topicId=...`, 티어표 저장, 템플릿 요청, 추가 아이템 가져오기 흐름이 모두 정상인지 확인한다. +- `v1.4.25`에서 관리자 route query와 편집기 저장/request payload를 `topicId` 기준으로 옮겼으므로, `/admin/templates?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`를 끝까지 걷어낼지 최종 결정한다. diff --git a/docs/update.md b/docs/update.md index 17166f4..ee6f002 100644 --- a/docs/update.md +++ b/docs/update.md @@ -1,5 +1,10 @@ # 업데이트 로그 +## 2026-04-02 v1.4.26 +- 관리자 API 레거시 `/games` alias를 걷어내고 `POST /api/admin/templates`, `.../templates/:templateId/...`만 남기도록 정리했다. 관리자 템플릿 연결/가져오기 액션도 `link-template`, `create-template` path로 바꿨다. +- 백엔드 공개 주제 라우트도 이제 `/api/topics`만 마운트하고, 오래된 `/api/games` 경로는 제거했다. 관리자 화면 URL 역시 `/admin/games` 대신 `/admin/templates`를 기본 경로로 쓰고, 예전 주소는 redirect만 남겼다. +- 문서의 API/화면 매핑도 현재 구조 기준으로 갱신해, `games` 중심 설명 대신 `topics / templates` 기준으로 읽히게 맞췄다. + ## 2026-04-02 v1.4.25 - 티어표와 템플릿 요청 응답에서 `gameId / gameName / sourceGameId / targetGameId` 호환 키를 실제로 제거하고, 프런트 화면도 `topicId / topicName / sourceTopicId / targetTopicId`만 읽도록 정리했다. - 관리자 전체 티어표 관리와 템플릿 요청 관리, 나의 티어표/즐겨찾기/검색 결과 이동, 티어표 편집기 저장·요청 payload도 `topicId` 기준으로 맞춰, 화면과 요청 바디에서 보이는 `game` 흔적을 더 줄였다. diff --git a/frontend/src/composables/useAdminGameManager.js b/frontend/src/composables/useAdminGameManager.js index a4865ca..c9a0a1f 100644 --- a/frontend/src/composables/useAdminGameManager.js +++ b/frontend/src/composables/useAdminGameManager.js @@ -174,7 +174,7 @@ export function useAdminGameManager({ const data = await res.json() const createdTemplate = data.template || {} if (activeTemplateRequest.value?.type === 'create' && activeTemplateRequest.value?.id) { - const linkData = await api.linkAdminTemplateRequestGame(activeTemplateRequest.value.id, { + const linkData = await api.linkAdminTemplateRequestTemplate(activeTemplateRequest.value.id, { topicId: createdTemplate.id, }) activeTemplateRequest.value = { diff --git a/frontend/src/lib/api.js b/frontend/src/lib/api.js index 7759ec9..d4a40fc 100644 --- a/frontend/src/lib/api.js +++ b/frontend/src/lib/api.js @@ -100,11 +100,11 @@ export const api = { promoteAdminTierListItems: (tierListId, payload) => request(`/api/admin/tierlists/${encodeURIComponent(tierListId)}/promote-items`, { method: 'POST', body: payload }), createAdminTemplateFromTierList: (tierListId, payload) => - request(`/api/admin/tierlists/${encodeURIComponent(tierListId)}/create-game-template`, { method: 'POST', body: payload }), + request(`/api/admin/tierlists/${encodeURIComponent(tierListId)}/create-template`, { method: 'POST', body: payload }), startAdminTemplateRequestReview: (requestId) => request(`/api/admin/template-requests/${encodeURIComponent(requestId)}/review`, { method: 'POST', body: {} }), - linkAdminTemplateRequestGame: (requestId, payload) => - request(`/api/admin/template-requests/${encodeURIComponent(requestId)}/link-game`, { method: 'POST', body: payload }), + linkAdminTemplateRequestTemplate: (requestId, payload) => + request(`/api/admin/template-requests/${encodeURIComponent(requestId)}/link-template`, { method: 'POST', body: payload }), promoteAdminTemplateRequestItems: (requestId, payload) => request(`/api/admin/template-requests/${encodeURIComponent(requestId)}/promote-items`, { method: 'POST', body: payload }), completeAdminTemplateRequest: (requestId) => diff --git a/frontend/src/router/index.js b/frontend/src/router/index.js index 63b2581..bb32a24 100644 --- a/frontend/src/router/index.js +++ b/frontend/src/router/index.js @@ -26,7 +26,8 @@ export function createRouter() { { path: '/search', name: 'search', component: SearchResultsView }, { path: '/admin', redirect: '/admin/featured' }, { path: '/admin/featured', name: 'adminFeatured', component: AdminView }, - { path: '/admin/games', name: 'adminGames', component: AdminView }, + { path: '/admin/games', redirect: '/admin/templates' }, + { path: '/admin/templates', name: 'adminGames', component: AdminView }, { path: '/admin/items', name: 'adminItems', component: AdminView }, { path: '/admin/tierlists', name: 'adminTierlists', component: AdminView }, { path: '/admin/users', name: 'adminUsers', component: AdminView },