diff --git a/backend/src/db.js b/backend/src/db.js index 617617a..590e60c 100644 --- a/backend/src/db.js +++ b/backend/src/db.js @@ -9,7 +9,6 @@ const DB_PASSWORD = process.env.DB_PASSWORD || '' const DB_NAME = process.env.DB_NAME || 'tier_cursor' const DB_CONNECTION_LIMIT = process.env.DB_CONNECTION_LIMIT ? Number(process.env.DB_CONNECTION_LIMIT) : 10 const FREEFORM_TOPIC_ID = 'freeform' -const FREEFORM_GAME_ID = FREEFORM_TOPIC_ID let poolPromise = null let initPromise = null @@ -31,20 +30,6 @@ function serializeJson(value) { return JSON.stringify(value || []) } -function normalizeLegacyItemOrigins(items) { - let changed = false - const normalized = (items || []).map((item) => { - if (!item || typeof item !== 'object') return item - if (item.origin !== 'game') return item - changed = true - return { - ...item, - origin: 'template', - } - }) - return { normalized, changed } -} - function collectUploadSrcsFromItems(items, bucket) { for (const item of items || []) { if (typeof item?.src === 'string' && item.src.startsWith('/uploads/')) { @@ -284,10 +269,6 @@ async function closePool() { async function ensureSchema() { if (initPromise) return initPromise initPromise = (async () => { - const legacyGamesExists = await tableExists('games') - const legacyGameItemsExists = await tableExists('game_items') - const legacyFavoriteGamesExists = await tableExists('favorite_games') - await query(` CREATE TABLE IF NOT EXISTS users ( id VARCHAR(64) PRIMARY KEY, @@ -322,14 +303,6 @@ async function ensureSchema() { await query('ALTER TABLE topics ADD COLUMN display_rank INT NULL DEFAULT NULL AFTER thumbnail_src') } - if (legacyGamesExists) { - await query(` - INSERT IGNORE INTO topics (id, name, thumbnail_src, is_public, display_rank, created_at) - SELECT id, name, thumbnail_src, COALESCE(is_public, 1), display_rank, created_at - FROM games - `) - } - await query(` CREATE TABLE IF NOT EXISTS topic_items ( id VARCHAR(64) PRIMARY KEY, @@ -348,17 +321,6 @@ async function ensureSchema() { await query('ALTER TABLE topic_items ADD COLUMN display_order INT NULL DEFAULT NULL AFTER label') } - if (legacyGameItemsExists) { - const legacyItemDisplayOrderColumns = await query("SHOW COLUMNS FROM game_items LIKE 'display_order'") - await query( - ` - INSERT IGNORE INTO topic_items (id, topic_id, src, label, display_order, created_at) - SELECT id, game_id, src, label, ${legacyItemDisplayOrderColumns.length ? 'display_order' : 'NULL'}, created_at - FROM game_items - ` - ) - } - await query(` CREATE TABLE IF NOT EXISTS custom_items ( id VARCHAR(64) PRIMARY KEY, @@ -421,14 +383,6 @@ async function ensureSchema() { ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 `) - if (legacyFavoriteGamesExists) { - await query(` - INSERT IGNORE INTO favorite_topics (user_id, topic_id, created_at) - SELECT user_id, game_id, created_at - FROM favorite_games - `) - } - await query(` CREATE TABLE IF NOT EXISTS image_assets ( id VARCHAR(64) PRIMARY KEY, @@ -501,16 +455,10 @@ async function ensureSchema() { const hasSourceTopicId = await columnExists('template_requests', 'source_topic_id') if (!hasSourceTopicId) { await query("ALTER TABLE template_requests ADD COLUMN source_topic_id VARCHAR(120) NOT NULL DEFAULT 'freeform' AFTER source_tierlist_id") - if (await columnExists('template_requests', 'source_game_id')) { - await query('UPDATE template_requests SET source_topic_id = source_game_id WHERE source_topic_id = ?', [FREEFORM_TOPIC_ID]) - } } const hasTargetTopicId = await columnExists('template_requests', 'target_topic_id') if (!hasTargetTopicId) { await query("ALTER TABLE template_requests ADD COLUMN target_topic_id VARCHAR(120) NOT NULL DEFAULT '' AFTER source_topic_id") - if (await columnExists('template_requests', 'target_game_id')) { - await query("UPDATE template_requests SET target_topic_id = target_game_id WHERE target_topic_id = ''") - } } const templateRequestStatusColumns = await query("SHOW COLUMNS FROM template_requests LIKE 'status'") if (!templateRequestStatusColumns.length) { @@ -536,9 +484,6 @@ async function ensureSchema() { const tierListTopicIdColumns = await query("SHOW COLUMNS FROM tierlists LIKE 'topic_id'") if (!tierListTopicIdColumns.length) { await query("ALTER TABLE tierlists ADD COLUMN topic_id VARCHAR(120) NOT NULL DEFAULT 'freeform' AFTER author_id") - if (await columnExists('tierlists', 'game_id')) { - await query('UPDATE tierlists SET topic_id = game_id WHERE topic_id = ?', [FREEFORM_TOPIC_ID]) - } } const tierListShowNamesColumns = await query("SHOW COLUMNS FROM tierlists LIKE 'show_character_names'") if (!tierListShowNamesColumns.length) { @@ -582,7 +527,7 @@ async function ensureSchema() { (?, ?, ?, ?), (?, ?, ?, ?) `, - ['example-game', '예시 게임', '', createdAt, 'another-game', '다른 예시 게임', '', createdAt] + ['example-topic', '예시 주제', '', createdAt, 'another-topic', '다른 예시 주제', '', createdAt] ) await query( @@ -594,12 +539,12 @@ async function ensureSchema() { `, [ 'img-1', - 'example-game', + 'example-topic', '/uploads/seeds/example1.png', '샘플 1', createdAt, 'img-2', - 'example-game', + 'example-topic', '/uploads/seeds/example2.png', '샘플 2', createdAt, @@ -613,25 +558,6 @@ async function ensureSchema() { async function ensureData() { await ensureSchema() - - const tierListRows = await query('SELECT id, pool_json FROM tierlists') - for (const row of tierListRows) { - const { normalized, changed } = normalizeLegacyItemOrigins(parseJson(row.pool_json, [])) - if (!changed) continue - await query('UPDATE tierlists SET pool_json = ? WHERE id = ?', [serializeJson(normalized), row.id]) - } - - const requestRows = await query('SELECT id, items_json, board_items_json FROM template_requests') - for (const row of requestRows) { - const itemsResult = normalizeLegacyItemOrigins(parseJson(row.items_json, [])) - const boardItemsResult = normalizeLegacyItemOrigins(parseJson(row.board_items_json, [])) - if (!itemsResult.changed && !boardItemsResult.changed) continue - await query('UPDATE template_requests SET items_json = ?, board_items_json = ? WHERE id = ?', [ - serializeJson(itemsResult.normalized), - serializeJson(boardItemsResult.normalized), - row.id, - ]) - } } async function countUsers() { diff --git a/backend/src/routes/tierlists.js b/backend/src/routes/tierlists.js index 53fe83c..115cd21 100644 --- a/backend/src/routes/tierlists.js +++ b/backend/src/routes/tierlists.js @@ -24,7 +24,7 @@ const FREEFORM_TOPIC_ID = 'freeform' const FREEFORM_DEFAULT_TITLE = '직접 티어표 만들기' function normalizePoolItem(item) { - if (!item || !['game', 'template'].includes(item.origin) || typeof item.src !== 'string') return item + if (!item || item.origin !== 'template' || typeof item.src !== 'string') return item if (item.src.startsWith('/uploads/')) return item try { @@ -83,7 +83,7 @@ const templateRequestSchema = z.object({ id: z.string().min(1), src: z.string().min(1), label: z.string().min(1).max(60), - origin: z.enum(['template', 'game', 'custom']).default('template'), + origin: z.enum(['template', 'custom']).default('template'), }) ), }) @@ -112,7 +112,7 @@ const tierListUpsertSchema = z.object({ id: z.string().min(1), src: z.string().min(1), label: z.string().min(1).max(60), - origin: z.enum(['template', 'game', 'custom']).default('template'), + origin: z.enum(['template', 'custom']).default('template'), }) ), }).superRefine((value, ctx) => { diff --git a/docs/history.md b/docs/history.md index 7545774..8e7be19 100644 --- a/docs/history.md +++ b/docs/history.md @@ -1,5 +1,9 @@ # 의사결정 이력 +## 2026-04-02 v1.4.31 +- 서비스가 아직 외부 공개 전이고 예전 북마크/예전 데이터베이스를 이어갈 필요가 없다는 전제가 확인되었으므로, 남겨둔 호환층을 유지하는 것보다 지금 마감 시점에 완전히 제거해 구조를 단순화하는 편이 맞다고 판단했다. +- 이 단계에서는 “기존 것도 읽어준다”보다 “현재 구조만 남긴다”가 더 중요한 목표가 되었으므로, redirect·legacy migration·`origin: 'game'` 허용까지 함께 정리해 실제 코드 검색에서 `game` 흔적을 0건으로 맞추는 방향으로 마감했다. + ## 2026-04-02 v1.4.30 - 로컬 MariaDB는 테스트용으로 새로 밀어도 된다는 전제가 확인되었으므로, 개발 환경에서는 기존 데이터를 끌고 가는 것보다 현재 스키마가 “빈 DB에서 바로 정상 부팅되는지”를 먼저 검증하는 편이 더 가치 있다고 판단했다. - `origin: 'game'` 호환층은 즉시 제거하기보다, `ensureData()`에서 저장 데이터와 요청 스냅샷을 자동 정규화하게 만들어 두고 새 DB에서도 legacy 값이 생기지 않는 상태를 먼저 확인한 뒤 다음 제거 판단으로 넘기는 편이 가장 안전하다고 정리했다. diff --git a/docs/todo.md b/docs/todo.md index eb509f6..d52b0cc 100644 --- a/docs/todo.md +++ b/docs/todo.md @@ -1,6 +1,8 @@ # 할 일 및 이슈 ## 단기 확인 +- `v1.4.31`에서 `/games` redirect와 legacy DB 마이그레이션까지 제거했으므로, 실제 QA에서는 오직 현재 주소(`/topics`, `/admin/templates`)와 새 DB 기준 흐름만 집중적으로 확인하면 된다. +- 현재 `backend/src`, `frontend/src` 기준 `game` 검색은 0건이므로, 이후 남는 확인 작업은 기능 QA와 운영 환경 배포 점검 쪽에만 집중한다. - `v1.4.30`에서 빈 로컬 MariaDB 재초기화 검증까지 통과했으므로, 다음 실제 QA에서는 “기존 데이터가 있는 환경”에서 `ensureData()`가 저장 티어표와 템플릿 요청 스냅샷의 legacy origin을 정상 정규화하는지만 추가 확인하면 된다. - 개발 환경 기준으로는 새 DB에서 `legacyTierItems=0`, `legacyRequestItems=0`가 확인됐으므로, 이후에는 `origin: 'game'` 호환 코드를 언제 완전히 제거할지 운영 데이터 기준으로만 판단하면 된다. - `v1.4.29`에서 새 티어표 데이터 기본 origin을 `template`로 바꿨으므로, 저장 후 다시 열기/복사/요청 생성/관리자 가져오기 흐름에서 예전 데이터와 새 데이터가 함께 섞여도 정상 동작하는지 한 번 더 확인한다. diff --git a/docs/update.md b/docs/update.md index eb7de51..2dd3a8d 100644 --- a/docs/update.md +++ b/docs/update.md @@ -1,5 +1,10 @@ # 업데이트 로그 +## 2026-04-02 v1.4.31 +- 서비스가 아직 공개 전이고 예전 링크/예전 DB를 이어갈 필요가 없다는 전제에 맞춰, `/games` redirect와 관리자 `/admin/games` redirect, DB 레거시 마이그레이션 코드, legacy origin 정규화 코드를 실제로 제거했다. +- 티어표 저장/request schema도 이제 `origin: 'template' | 'custom'`만 받도록 정리했고, 관리자 최근 최적화 작업 분류 fallback에 남아 있던 `games` 처리도 걷어냈다. +- seed 데이터 ID까지 `example-topic`, `another-topic` 기준으로 바꿔, 현재 `backend/src`와 `frontend/src` 코드 검색에서 `game` 흔적이 0건인 상태까지 정리했다. + ## 2026-04-02 v1.4.30 - `ensureData()` 단계에서 저장된 티어표 `pool_json`과 템플릿 요청 스냅샷(`items_json`, `board_items_json`) 안에 남아 있을 수 있는 `origin: 'game'` 값을 자동으로 `template`로 정리하도록 보강했다. - 로컬 MariaDB를 비운 뒤 현재 스키마로 다시 올리는 검증도 함께 진행했고, 새 DB 기준으로 `topics=3`, `tierlists=0`, `legacyTierItems=0`, `legacyRequestItems=0` 상태까지 확인했다. diff --git a/frontend/src/router/index.js b/frontend/src/router/index.js index 36302fd..995ad19 100644 --- a/frontend/src/router/index.js +++ b/frontend/src/router/index.js @@ -16,7 +16,6 @@ export function createRouter() { history: createWebHistory(), routes: [ { path: '/', name: 'home', component: HomeView }, - { path: '/games/:gameId', redirect: (to) => `/topics/${encodeURIComponent(String(to.params.gameId || ''))}` }, { path: '/topics/:topicId', name: 'topicHub', component: GameHubView }, { path: '/editor/:topicId/new', name: 'newEditor', component: TierEditorView }, { path: '/editor/:topicId/:tierListId', name: 'editEditor', component: TierEditorView }, @@ -26,7 +25,6 @@ export function createRouter() { { path: '/search', name: 'search', component: SearchResultsView }, { path: '/admin', redirect: '/admin/featured' }, { path: '/admin/featured', name: 'adminFeatured', component: AdminView }, - { path: '/admin/games', redirect: '/admin/templates' }, { path: '/admin/templates', name: 'adminTemplates', component: AdminView }, { path: '/admin/items', name: 'adminItems', component: AdminView }, { path: '/admin/tierlists', name: 'adminTierlists', component: AdminView }, diff --git a/frontend/src/views/AdminView.vue b/frontend/src/views/AdminView.vue index d303f7d..b28b87d 100644 --- a/frontend/src/views/AdminView.vue +++ b/frontend/src/views/AdminView.vue @@ -572,7 +572,6 @@ function formatImageJobSourceCategory(category) { case 'tierlists': return '티어표 썸네일' case 'topics': - case 'games': return '주제/템플릿 이미지' case 'avatars': return '프로필 아바타'