From faa2a01f6c144e4fcf3fffdb32a82d030139caaa Mon Sep 17 00:00:00 2001 From: zenn Date: Tue, 31 Mar 2026 15:26:41 +0900 Subject: [PATCH] =?UTF-8?q?=EB=A6=B4=EB=A6=AC=EC=8A=A4:=20v1.2.61=20?= =?UTF-8?q?=EA=B2=8C=EC=9E=84=20=ED=85=9C=ED=94=8C=EB=A6=BF=20=EA=B2=80?= =?UTF-8?q?=EC=83=89=EA=B3=BC=20=EC=A6=90=EA=B2=A8=EC=B0=BE=EA=B8=B0=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/src/db.js | 34 ++++++++- backend/src/routes/games.js | 23 +++++- docs/update.md | 5 ++ frontend/src/App.vue | 27 ++++--- frontend/src/lib/api.js | 2 + frontend/src/views/HomeView.vue | 123 +++++++++++++++++++++++++++----- 6 files changed, 185 insertions(+), 29 deletions(-) diff --git a/backend/src/db.js b/backend/src/db.js index fe4d442..b209e9f 100644 --- a/backend/src/db.js +++ b/backend/src/db.js @@ -252,6 +252,18 @@ async function ensureSchema() { ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 `) + await query(` + CREATE TABLE IF NOT EXISTS favorite_games ( + user_id VARCHAR(64) NOT NULL, + game_id VARCHAR(120) NOT NULL, + created_at BIGINT NOT NULL, + PRIMARY KEY (user_id, game_id), + INDEX idx_favorite_games_game_id (game_id), + CONSTRAINT fk_favorite_games_user FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE, + CONSTRAINT fk_favorite_games_game FOREIGN KEY (game_id) REFERENCES games(id) ON DELETE CASCADE + ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 + `) + await query(` CREATE TABLE IF NOT EXISTS template_requests ( id VARCHAR(64) PRIMARY KEY, @@ -431,7 +443,7 @@ async function adminDeleteUser(id) { await query('DELETE FROM users WHERE id = ?', [id]) } -async function listGames() { +async function listGames(currentUserId = '') { const rows = await query( ` SELECT id, name, thumbnail_src, display_rank, created_at @@ -445,7 +457,15 @@ async function listGames() { `, [FREEFORM_GAME_ID] ) - return rows.map(mapGameRow) + const games = rows.map(mapGameRow) + if (!currentUserId) return games.map((game) => ({ ...game, isFavorited: false })) + + const favoriteRows = await query('SELECT game_id FROM favorite_games WHERE user_id = ?', [currentUserId]) + const favoriteSet = new Set(favoriteRows.map((row) => row.game_id)) + return games.map((game) => ({ + ...game, + isFavorited: favoriteSet.has(game.id), + })) } async function findGameById(id) { @@ -1295,6 +1315,14 @@ async function unfavoriteTierList({ userId, tierListId }) { await query('DELETE FROM favorite_tierlists WHERE user_id = ? AND tierlist_id = ?', [userId, tierListId]) } +async function favoriteGame({ userId, gameId }) { + await query('INSERT IGNORE INTO favorite_games (user_id, game_id, created_at) VALUES (?, ?, ?)', [userId, gameId, now()]) +} + +async function unfavoriteGame({ userId, gameId }) { + await query('DELETE FROM favorite_games WHERE user_id = ? AND game_id = ?', [userId, gameId]) +} + module.exports = { DB_NAME, ensureData, @@ -1329,6 +1357,8 @@ module.exports = { findTierListById, favoriteTierList, unfavoriteTierList, + favoriteGame, + unfavoriteGame, deleteTierList, findCustomItemsByIds, deleteCustomItems, diff --git a/backend/src/routes/games.js b/backend/src/routes/games.js index f6c999b..37ab1d9 100644 --- a/backend/src/routes/games.js +++ b/backend/src/routes/games.js @@ -1,13 +1,32 @@ const express = require('express') -const { listGames, getGameDetail } = require('../db') +const { listGames, getGameDetail, findGameById, favoriteGame, unfavoriteGame } = require('../db') +const { requireAuth } = require('../middleware/auth') const router = express.Router() router.get('/', async (req, res) => { - const games = await listGames() + const games = await listGames(req.session?.userId || '') res.json({ games }) }) +router.post('/:gameId/favorite', requireAuth, async (req, res) => { + const game = await findGameById(req.params.gameId) + if (!game || game.id === 'freeform') return res.status(404).json({ error: 'not_found' }) + await favoriteGame({ userId: req.session.userId, gameId: game.id }) + const games = await listGames(req.session.userId) + const updated = games.find((entry) => entry.id === game.id) || { ...game, isFavorited: true } + res.json({ game: updated }) +}) + +router.delete('/:gameId/favorite', requireAuth, async (req, res) => { + const game = await findGameById(req.params.gameId) + if (!game || game.id === 'freeform') return res.status(404).json({ error: 'not_found' }) + await unfavoriteGame({ userId: req.session.userId, gameId: game.id }) + const games = await listGames(req.session.userId) + const updated = games.find((entry) => entry.id === game.id) || { ...game, isFavorited: false } + res.json({ game: updated }) +}) + router.get('/:gameId', async (req, res) => { const detail = await getGameDetail(req.params.gameId) if (!detail) return res.status(404).json({ error: 'not_found' }) diff --git a/docs/update.md b/docs/update.md index 0a48b06..16eca33 100644 --- a/docs/update.md +++ b/docs/update.md @@ -1,5 +1,10 @@ # 업데이트 로그 +## 2026-03-31 v1.2.61 +- Game Library 왼쪽 검색을 전체 티어표 검색이 아니라 게임 템플릿 검색으로 바꾸고, 홈 화면에서 검색어에 맞는 게임만 필터링하도록 조정함. +- 게임 템플릿에 사용자별 즐겨찾기 별 아이콘을 추가하고, 즐겨찾기한 게임이 관리자 고정 순서보다 우선 노출되도록 백엔드와 홈 화면을 함께 확장함. +- 앱 셸의 100vh 높이 계산을 100dvh와 고정 행 구조로 정리해, 콘텐츠가 없어도 생기던 불필요한 세로 스크롤을 줄임. + ## 2026-03-31 v1.2.60 - 관리자 티어표 관리 카드에서 사용자가 입력한 설명을 제목 아래에 함께 노출해 요청 의도를 더 빨리 파악할 수 있게 함. - 템플릿 등록/업데이트 요청은 이제 에디터 모달에서 제목과 설명을 별도로 입력받고, 예시 문구와 함께 전송하도록 정리함. diff --git a/frontend/src/App.vue b/frontend/src/App.vue index 6ccb345..a19a935 100644 --- a/frontend/src/App.vue +++ b/frontend/src/App.vue @@ -21,6 +21,7 @@ const { toasts, dismissToast } = useToast() const leftRailCollapsed = ref(false) const rightRailOpen = ref(true) const searchQuery = ref('') +const searchPlaceholder = computed(() => (route.name === 'home' ? '게임 템플릿 검색' : '전체 티어표 검색')) const isCollapsedSearchOpen = ref(false) const viewportWidth = ref(typeof window !== 'undefined' ? window.innerWidth : 1440) provide('rightRailOpen', rightRailOpen) @@ -261,6 +262,10 @@ function handleLeftRailSearch() { function submitGlobalSearch() { const query = (searchQuery.value || '').trim() isCollapsedSearchOpen.value = false + if (route.name === 'home') { + router.push(query ? `/?q=${encodeURIComponent(query)}` : '/') + return + } router.push(query ? `/search?q=${encodeURIComponent(query)}` : '/search') } @@ -310,7 +315,7 @@ function submitGlobalSearch() { - +