Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 60cc5a72c5 | |||
| 8898ac24f9 |
@@ -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
|
||||
@@ -270,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,
|
||||
@@ -308,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,
|
||||
@@ -334,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,
|
||||
@@ -407,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,
|
||||
@@ -487,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) {
|
||||
@@ -522,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) {
|
||||
@@ -568,7 +527,7 @@ async function ensureSchema() {
|
||||
(?, ?, ?, ?),
|
||||
(?, ?, ?, ?)
|
||||
`,
|
||||
['example-game', '예시 게임', '', createdAt, 'another-game', '다른 예시 게임', '', createdAt]
|
||||
['example-topic', '예시 주제', '', createdAt, 'another-topic', '다른 예시 주제', '', createdAt]
|
||||
)
|
||||
|
||||
await query(
|
||||
@@ -580,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,
|
||||
|
||||
@@ -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) => {
|
||||
|
||||
@@ -1,5 +1,13 @@
|
||||
# 의사결정 이력
|
||||
|
||||
## 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 값이 생기지 않는 상태를 먼저 확인한 뒤 다음 제거 판단으로 넘기는 편이 가장 안전하다고 정리했다.
|
||||
|
||||
## 2026-04-02 v1.4.29
|
||||
- `origin: 'game'`는 이미 저장된 티어표 데이터와 직접 맞물리므로, 이 단계에서는 새 데이터 기본값만 `template`로 옮기고 예전 값도 계속 받아주는 점진 호환이 가장 안전하다고 판단했다.
|
||||
- 아이템 라이브러리의 `linkedGames`는 실제 의미가 템플릿 연결 정보이므로, 이 응답 키까지 `linkedTemplates`로 바꿔두는 편이 이후 관리자 유지보수에서 훨씬 덜 헷갈린다고 정리했다.
|
||||
|
||||
@@ -1,6 +1,10 @@
|
||||
# 할 일 및 이슈
|
||||
|
||||
## 단기 확인
|
||||
- `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`로 바꿨으므로, 저장 후 다시 열기/복사/요청 생성/관리자 가져오기 흐름에서 예전 데이터와 새 데이터가 함께 섞여도 정상 동작하는지 한 번 더 확인한다.
|
||||
- 관리자 아이템 라이브러리 응답 키가 `linkedTemplates`로 정리됐으므로, 사용자 업로드 이미지 삭제 차단과 템플릿 이동 모달이 그대로 정상 동작하는지 확인한다.
|
||||
- 현재 남아 있는 `game`는 레거시 redirect, DB 마이그레이션, 호환용 origin만 남겨둔 상태이므로, `v1.4` QA 후에는 이 레거시 층을 언제 제거할지 별도 마감 판단만 하면 된다.
|
||||
|
||||
@@ -1,5 +1,15 @@
|
||||
# 업데이트 로그
|
||||
|
||||
## 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` 상태까지 확인했다.
|
||||
- 즉 현재 개발 환경에서는 새로 생성되거나 다시 초기화한 데이터에 `game` 기반 origin이 남지 않으며, 남은 `game` 코드는 레거시 redirect·DB 마이그레이션 감지·과거 데이터 호환층만 담당하게 됐다.
|
||||
|
||||
## 2026-04-02 v1.4.29
|
||||
- 티어표 저장/request schema는 이제 새 데이터에서 `origin: 'template'`를 기본으로 쓰고, 예전 `origin: 'game'`도 계속 읽을 수 있게 호환 레이어를 남겼다.
|
||||
- 관리자 아이템 라이브러리의 템플릿 연결 정보도 `linkedTemplates` 기준으로 정리해, 내부 응답/프런트 상태에 남아 있던 `linkedGames` 흔적을 제거했다.
|
||||
|
||||
@@ -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 },
|
||||
|
||||
@@ -572,7 +572,6 @@ function formatImageJobSourceCategory(category) {
|
||||
case 'tierlists':
|
||||
return '티어표 썸네일'
|
||||
case 'topics':
|
||||
case 'games':
|
||||
return '주제/템플릿 이미지'
|
||||
case 'avatars':
|
||||
return '프로필 아바타'
|
||||
|
||||
Reference in New Issue
Block a user