diff --git a/backend/src/db.js b/backend/src/db.js index 03ee6da..a308fbf 100644 --- a/backend/src/db.js +++ b/backend/src/db.js @@ -138,6 +138,7 @@ function mapTierListRow(row) { description: row.description || '', isPublic: !!row.is_public, showCharacterNames: !!row.show_character_names, + iconSize: Number(row.icon_size || 80), sourceTierListId: row.source_tierlist_id || '', sourceSnapshotTitle: row.source_snapshot_title || '', sourceSnapshotAuthor: row.source_snapshot_author || '', @@ -314,6 +315,7 @@ async function ensureSchema() { description TEXT NOT NULL, is_public TINYINT(1) NOT NULL DEFAULT 0, show_character_names TINYINT(1) NOT NULL DEFAULT 0, + icon_size INT NOT NULL DEFAULT 80, source_tierlist_id VARCHAR(64) NULL DEFAULT NULL, source_snapshot_title VARCHAR(120) NOT NULL DEFAULT '', source_snapshot_author VARCHAR(120) NOT NULL DEFAULT '', @@ -455,9 +457,13 @@ async function ensureSchema() { if (!tierListShowNamesColumns.length) { await query("ALTER TABLE tierlists ADD COLUMN show_character_names TINYINT(1) NOT NULL DEFAULT 0 AFTER is_public") } + const tierListIconSizeColumns = await query("SHOW COLUMNS FROM tierlists LIKE 'icon_size'") + if (!tierListIconSizeColumns.length) { + await query("ALTER TABLE tierlists ADD COLUMN icon_size INT NOT NULL DEFAULT 80 AFTER show_character_names") + } const tierListSourceIdColumns = await query("SHOW COLUMNS FROM tierlists LIKE 'source_tierlist_id'") if (!tierListSourceIdColumns.length) { - await query("ALTER TABLE tierlists ADD COLUMN source_tierlist_id VARCHAR(64) NULL DEFAULT NULL AFTER show_character_names") + await query("ALTER TABLE tierlists ADD COLUMN source_tierlist_id VARCHAR(64) NULL DEFAULT NULL AFTER icon_size") } else if (tierListSourceIdColumns[0]?.Null !== 'YES') { await query('ALTER TABLE tierlists MODIFY source_tierlist_id VARCHAR(64) NULL DEFAULT NULL') } @@ -1847,6 +1853,7 @@ async function listFavoriteTierLists(userId, { queryText = '', sort = 'favorited t.description, t.is_public, t.show_character_names, + t.icon_size, t.source_tierlist_id, t.source_snapshot_title, t.source_snapshot_author, @@ -1990,6 +1997,7 @@ async function listAdminTierLists({ queryText = '', gameId = '', page = 1, limit t.description, t.is_public, t.show_character_names, + t.icon_size, t.source_tierlist_id, t.source_snapshot_title, t.source_snapshot_author, @@ -2093,6 +2101,7 @@ async function findTierListById(id, currentUserId = '') { t.description, t.is_public, t.show_character_names, + t.icon_size, t.source_tierlist_id, t.source_snapshot_title, t.source_snapshot_author, @@ -2346,6 +2355,7 @@ async function saveTierList({ description, isPublic, showCharacterNames = false, + iconSize = 80, sourceTierListId = '', sourceSnapshotTitle = '', sourceSnapshotAuthor = '', @@ -2360,10 +2370,10 @@ async function saveTierList({ await query( ` UPDATE tierlists - SET title = ?, thumbnail_src = ?, description = ?, is_public = ?, show_character_names = ?, source_tierlist_id = ?, source_snapshot_title = ?, source_snapshot_author = ?, groups_json = ?, pool_json = ?, updated_at = ? + SET title = ?, thumbnail_src = ?, description = ?, is_public = ?, show_character_names = ?, icon_size = ?, source_tierlist_id = ?, source_snapshot_title = ?, source_snapshot_author = ?, groups_json = ?, pool_json = ?, updated_at = ? WHERE id = ? `, - [title, nextThumbnailSrc, description || '', isPublic ? 1 : 0, showCharacterNames ? 1 : 0, sourceTierListId || null, sourceSnapshotTitle || '', sourceSnapshotAuthor || '', serializeJson(groups), serializeJson(pool), now(), existing.id] + [title, nextThumbnailSrc, description || '', isPublic ? 1 : 0, showCharacterNames ? 1 : 0, Number(iconSize) || 80, sourceTierListId || null, sourceSnapshotTitle || '', sourceSnapshotAuthor || '', serializeJson(groups), serializeJson(pool), now(), existing.id] ) return findTierListById(existing.id, authorId) } @@ -2373,11 +2383,11 @@ async function saveTierList({ await query( ` INSERT INTO tierlists ( - id, author_id, game_id, title, thumbnail_src, description, is_public, show_character_names, source_tierlist_id, source_snapshot_title, source_snapshot_author, groups_json, pool_json, created_at, updated_at + id, author_id, game_id, title, thumbnail_src, description, is_public, show_character_names, icon_size, source_tierlist_id, source_snapshot_title, source_snapshot_author, groups_json, pool_json, created_at, updated_at ) - VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) `, - [nextId, authorId, gameId, title, nextThumbnailSrc, description || '', isPublic ? 1 : 0, showCharacterNames ? 1 : 0, sourceTierListId || null, sourceSnapshotTitle || '', sourceSnapshotAuthor || '', serializeJson(groups), serializeJson(pool), createdAt, createdAt] + [nextId, authorId, gameId, title, nextThumbnailSrc, description || '', isPublic ? 1 : 0, showCharacterNames ? 1 : 0, Number(iconSize) || 80, sourceTierListId || null, sourceSnapshotTitle || '', sourceSnapshotAuthor || '', serializeJson(groups), serializeJson(pool), createdAt, createdAt] ) return findTierListById(nextId, authorId) } @@ -2396,6 +2406,7 @@ async function duplicateTierListForUser({ tierList, targetUserId }) { description: tierList.description || '', isPublic: false, showCharacterNames: !!tierList.showCharacterNames, + iconSize: Number(tierList.iconSize || 80), sourceTierListId: tierList.id, sourceSnapshotTitle: tierList.title || '', sourceSnapshotAuthor: tierList.authorName || tierList.authorAccountName || '', diff --git a/backend/src/routes/tierlists.js b/backend/src/routes/tierlists.js index 14fa0f7..16387be 100644 --- a/backend/src/routes/tierlists.js +++ b/backend/src/routes/tierlists.js @@ -92,6 +92,7 @@ const tierListUpsertSchema = z.object({ description: z.string().max(1000).optional().default(''), isPublic: z.boolean().default(false), showCharacterNames: z.boolean().optional().default(false), + iconSize: z.number().int().min(48).max(112).optional().default(80), sourceTierListId: z.string().max(64).optional().default(''), sourceSnapshotTitle: z.string().max(120).optional().default(''), sourceSnapshotAuthor: z.string().max(120).optional().default(''), @@ -289,6 +290,7 @@ router.post('/', requireAuth, async (req, res) => { description: payload.description || '', isPublic: !!payload.isPublic, showCharacterNames: !!payload.showCharacterNames, + iconSize: Number(payload.iconSize || 80), sourceTierListId: payload.sourceTierListId || existing.sourceTierListId || '', sourceSnapshotTitle: payload.sourceSnapshotTitle || existing.sourceSnapshotTitle || '', sourceSnapshotAuthor: payload.sourceSnapshotAuthor || existing.sourceSnapshotAuthor || '', @@ -307,6 +309,7 @@ router.post('/', requireAuth, async (req, res) => { description: payload.description || '', isPublic: !!payload.isPublic, showCharacterNames: !!payload.showCharacterNames, + iconSize: Number(payload.iconSize || 80), sourceTierListId: payload.sourceTierListId || '', sourceSnapshotTitle: payload.sourceSnapshotTitle || '', sourceSnapshotAuthor: payload.sourceSnapshotAuthor || '', diff --git a/docs/history.md b/docs/history.md index b459f2d..15e08d2 100644 --- a/docs/history.md +++ b/docs/history.md @@ -1,5 +1,8 @@ # 의사결정 이력 +## 2026-04-02 v1.3.86 +- 아이콘 크기는 이미지 다운로드 결과에만 반영되고 저장본에는 남지 않으면 사용자가 체감상 “저장되지 않는 설정”으로 느끼게 되므로, 티어표 본문 설정으로 저장하는 편이 맞다고 정리했다. + ## 2026-04-02 v1.3.83 - 모바일에서 열 헤더가 칸과 시각적으로 분리되는 문제는 전체 레이아웃을 다시 갈아엎기보다, 각 칸 안에 열 이름 배지를 같이 보여주는 편이 가장 적은 변경으로 효과를 낸다고 정리했다. - 배지를 쓰는 반응형 구간에서는 기존 상단 열 헤더까지 남겨두면 중복 정보가 되므로, 같은 브레이크포인트에서 헤더는 숨기고 칸 배지 하나만 남기는 편이 맞다고 정리했다. diff --git a/docs/todo.md b/docs/todo.md index 87f3875..e59fcc9 100644 --- a/docs/todo.md +++ b/docs/todo.md @@ -1,6 +1,7 @@ # 할 일 및 이슈 ## 단기 확인 +- 티어표 `아이콘 크기`는 이제 저장 데이터로 승격됐으므로, 저장 후 재진입/프리뷰/복사본 생성에서 같은 크기가 유지되는지 한 번 더 QA한다. - 티어표 편집/프리뷰 모바일 열 배지는 새로 붙였으므로, 실제 좁은 화면에서 칸 상단 배지와 아이템 썸네일이 겹치지 않고 열 구분이 자연스러운지 한 번 더 QA한다. - 모바일 열 배지는 같은 구간에서 상단 열 제목을 숨기도록 다시 맞췄으므로, 720px 안팎뿐 아니라 980px 이하 전 구간에서 중복 표기 없이 자연스러운지 한 번 더 QA한다. - 모바일 티어표 편집 레이아웃은 행 라벨 폭을 다시 덮어쓰던 규칙을 걷어냈으므로, 실제 980px 이하 구간에서 행 라벨이 과하게 넓지 않고 칸 폭을 충분히 남기는지 한 번 더 QA한다. diff --git a/docs/update.md b/docs/update.md index f0a0f0e..331a8af 100644 --- a/docs/update.md +++ b/docs/update.md @@ -1,5 +1,9 @@ # 업데이트 로그 +## 2026-04-02 v1.3.86 +- 티어표 편집의 `아이콘 크기`는 이제 임시 화면 상태가 아니라 저장 데이터에 함께 포함되며, 저장 후 다시 열기와 프리뷰 화면에서도 같은 크기로 복원되도록 정리함. +- 이를 위해 티어표 저장 payload, 서버 검증, DB 저장/조회에 `iconSize`를 추가하고 기존 데이터는 기본값 `80`으로 안전하게 보정되게 맞춤. + ## 2026-04-02 v1.3.83 - 티어표 편집/프리뷰 화면에서 열을 여러 개 쓰는 경우, 모바일처럼 좁은 화면에서는 기존 상단 열 헤더만으로 각 칸의 의미를 읽기 어려웠으므로 각 칸 상단에 작은 열 이름 배지를 추가함. - 이 배지는 모바일 구간에서만 보이고 데스크톱 레이아웃은 그대로 유지되므로, 작은 화면에서는 `메인 / 밸런스 / 서포트` 같은 열 맥락을 스크롤 중에도 잃지 않게 정리함. diff --git a/frontend/src/views/TierEditorView.vue b/frontend/src/views/TierEditorView.vue index 6240c88..903bfc6 100644 --- a/frontend/src/views/TierEditorView.vue +++ b/frontend/src/views/TierEditorView.vue @@ -678,6 +678,7 @@ function buildPayload(existingId) { description: (description.value || '').trim(), isPublic: !!isPublic.value, showCharacterNames: !!showCharacterNames.value, + iconSize: Number(iconSize.value || 80), sourceTierListId: sourceTierListId.value || '', sourceSnapshotTitle: sourceSnapshotTitle.value || '', sourceSnapshotAuthor: sourceSnapshotAuthor.value || '', @@ -923,6 +924,7 @@ onMounted(() => { description.value = t.description || '' isPublic.value = !!t.isPublic showCharacterNames.value = !!t.showCharacterNames + iconSize.value = Number(t.iconSize || 80) authorName.value = t.authorName || '' authorAccountName.value = t.authorAccountName || '' updatedAt.value = Number(t.updatedAt || 0) @@ -1004,7 +1006,7 @@ onUnmounted(() => {