Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 074d028f04 |
@@ -138,6 +138,7 @@ function mapTierListRow(row) {
|
|||||||
description: row.description || '',
|
description: row.description || '',
|
||||||
isPublic: !!row.is_public,
|
isPublic: !!row.is_public,
|
||||||
showCharacterNames: !!row.show_character_names,
|
showCharacterNames: !!row.show_character_names,
|
||||||
|
iconSize: Number(row.icon_size || 80),
|
||||||
sourceTierListId: row.source_tierlist_id || '',
|
sourceTierListId: row.source_tierlist_id || '',
|
||||||
sourceSnapshotTitle: row.source_snapshot_title || '',
|
sourceSnapshotTitle: row.source_snapshot_title || '',
|
||||||
sourceSnapshotAuthor: row.source_snapshot_author || '',
|
sourceSnapshotAuthor: row.source_snapshot_author || '',
|
||||||
@@ -314,6 +315,7 @@ async function ensureSchema() {
|
|||||||
description TEXT NOT NULL,
|
description TEXT NOT NULL,
|
||||||
is_public TINYINT(1) NOT NULL DEFAULT 0,
|
is_public TINYINT(1) NOT NULL DEFAULT 0,
|
||||||
show_character_names 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_tierlist_id VARCHAR(64) NULL DEFAULT NULL,
|
||||||
source_snapshot_title VARCHAR(120) NOT NULL DEFAULT '',
|
source_snapshot_title VARCHAR(120) NOT NULL DEFAULT '',
|
||||||
source_snapshot_author VARCHAR(120) NOT NULL DEFAULT '',
|
source_snapshot_author VARCHAR(120) NOT NULL DEFAULT '',
|
||||||
@@ -455,9 +457,13 @@ async function ensureSchema() {
|
|||||||
if (!tierListShowNamesColumns.length) {
|
if (!tierListShowNamesColumns.length) {
|
||||||
await query("ALTER TABLE tierlists ADD COLUMN show_character_names TINYINT(1) NOT NULL DEFAULT 0 AFTER is_public")
|
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'")
|
const tierListSourceIdColumns = await query("SHOW COLUMNS FROM tierlists LIKE 'source_tierlist_id'")
|
||||||
if (!tierListSourceIdColumns.length) {
|
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') {
|
} else if (tierListSourceIdColumns[0]?.Null !== 'YES') {
|
||||||
await query('ALTER TABLE tierlists MODIFY source_tierlist_id VARCHAR(64) NULL DEFAULT NULL')
|
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.description,
|
||||||
t.is_public,
|
t.is_public,
|
||||||
t.show_character_names,
|
t.show_character_names,
|
||||||
|
t.icon_size,
|
||||||
t.source_tierlist_id,
|
t.source_tierlist_id,
|
||||||
t.source_snapshot_title,
|
t.source_snapshot_title,
|
||||||
t.source_snapshot_author,
|
t.source_snapshot_author,
|
||||||
@@ -1990,6 +1997,7 @@ async function listAdminTierLists({ queryText = '', gameId = '', page = 1, limit
|
|||||||
t.description,
|
t.description,
|
||||||
t.is_public,
|
t.is_public,
|
||||||
t.show_character_names,
|
t.show_character_names,
|
||||||
|
t.icon_size,
|
||||||
t.source_tierlist_id,
|
t.source_tierlist_id,
|
||||||
t.source_snapshot_title,
|
t.source_snapshot_title,
|
||||||
t.source_snapshot_author,
|
t.source_snapshot_author,
|
||||||
@@ -2093,6 +2101,7 @@ async function findTierListById(id, currentUserId = '') {
|
|||||||
t.description,
|
t.description,
|
||||||
t.is_public,
|
t.is_public,
|
||||||
t.show_character_names,
|
t.show_character_names,
|
||||||
|
t.icon_size,
|
||||||
t.source_tierlist_id,
|
t.source_tierlist_id,
|
||||||
t.source_snapshot_title,
|
t.source_snapshot_title,
|
||||||
t.source_snapshot_author,
|
t.source_snapshot_author,
|
||||||
@@ -2346,6 +2355,7 @@ async function saveTierList({
|
|||||||
description,
|
description,
|
||||||
isPublic,
|
isPublic,
|
||||||
showCharacterNames = false,
|
showCharacterNames = false,
|
||||||
|
iconSize = 80,
|
||||||
sourceTierListId = '',
|
sourceTierListId = '',
|
||||||
sourceSnapshotTitle = '',
|
sourceSnapshotTitle = '',
|
||||||
sourceSnapshotAuthor = '',
|
sourceSnapshotAuthor = '',
|
||||||
@@ -2360,10 +2370,10 @@ async function saveTierList({
|
|||||||
await query(
|
await query(
|
||||||
`
|
`
|
||||||
UPDATE tierlists
|
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 = ?
|
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)
|
return findTierListById(existing.id, authorId)
|
||||||
}
|
}
|
||||||
@@ -2373,11 +2383,11 @@ async function saveTierList({
|
|||||||
await query(
|
await query(
|
||||||
`
|
`
|
||||||
INSERT INTO tierlists (
|
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)
|
return findTierListById(nextId, authorId)
|
||||||
}
|
}
|
||||||
@@ -2396,6 +2406,7 @@ async function duplicateTierListForUser({ tierList, targetUserId }) {
|
|||||||
description: tierList.description || '',
|
description: tierList.description || '',
|
||||||
isPublic: false,
|
isPublic: false,
|
||||||
showCharacterNames: !!tierList.showCharacterNames,
|
showCharacterNames: !!tierList.showCharacterNames,
|
||||||
|
iconSize: Number(tierList.iconSize || 80),
|
||||||
sourceTierListId: tierList.id,
|
sourceTierListId: tierList.id,
|
||||||
sourceSnapshotTitle: tierList.title || '',
|
sourceSnapshotTitle: tierList.title || '',
|
||||||
sourceSnapshotAuthor: tierList.authorName || tierList.authorAccountName || '',
|
sourceSnapshotAuthor: tierList.authorName || tierList.authorAccountName || '',
|
||||||
|
|||||||
@@ -92,6 +92,7 @@ const tierListUpsertSchema = z.object({
|
|||||||
description: z.string().max(1000).optional().default(''),
|
description: z.string().max(1000).optional().default(''),
|
||||||
isPublic: z.boolean().default(false),
|
isPublic: z.boolean().default(false),
|
||||||
showCharacterNames: z.boolean().optional().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(''),
|
sourceTierListId: z.string().max(64).optional().default(''),
|
||||||
sourceSnapshotTitle: z.string().max(120).optional().default(''),
|
sourceSnapshotTitle: z.string().max(120).optional().default(''),
|
||||||
sourceSnapshotAuthor: 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 || '',
|
description: payload.description || '',
|
||||||
isPublic: !!payload.isPublic,
|
isPublic: !!payload.isPublic,
|
||||||
showCharacterNames: !!payload.showCharacterNames,
|
showCharacterNames: !!payload.showCharacterNames,
|
||||||
|
iconSize: Number(payload.iconSize || 80),
|
||||||
sourceTierListId: payload.sourceTierListId || existing.sourceTierListId || '',
|
sourceTierListId: payload.sourceTierListId || existing.sourceTierListId || '',
|
||||||
sourceSnapshotTitle: payload.sourceSnapshotTitle || existing.sourceSnapshotTitle || '',
|
sourceSnapshotTitle: payload.sourceSnapshotTitle || existing.sourceSnapshotTitle || '',
|
||||||
sourceSnapshotAuthor: payload.sourceSnapshotAuthor || existing.sourceSnapshotAuthor || '',
|
sourceSnapshotAuthor: payload.sourceSnapshotAuthor || existing.sourceSnapshotAuthor || '',
|
||||||
@@ -307,6 +309,7 @@ router.post('/', requireAuth, async (req, res) => {
|
|||||||
description: payload.description || '',
|
description: payload.description || '',
|
||||||
isPublic: !!payload.isPublic,
|
isPublic: !!payload.isPublic,
|
||||||
showCharacterNames: !!payload.showCharacterNames,
|
showCharacterNames: !!payload.showCharacterNames,
|
||||||
|
iconSize: Number(payload.iconSize || 80),
|
||||||
sourceTierListId: payload.sourceTierListId || '',
|
sourceTierListId: payload.sourceTierListId || '',
|
||||||
sourceSnapshotTitle: payload.sourceSnapshotTitle || '',
|
sourceSnapshotTitle: payload.sourceSnapshotTitle || '',
|
||||||
sourceSnapshotAuthor: payload.sourceSnapshotAuthor || '',
|
sourceSnapshotAuthor: payload.sourceSnapshotAuthor || '',
|
||||||
|
|||||||
@@ -1,5 +1,8 @@
|
|||||||
# 의사결정 이력
|
# 의사결정 이력
|
||||||
|
|
||||||
|
## 2026-04-02 v1.3.86
|
||||||
|
- 아이콘 크기는 이미지 다운로드 결과에만 반영되고 저장본에는 남지 않으면 사용자가 체감상 “저장되지 않는 설정”으로 느끼게 되므로, 티어표 본문 설정으로 저장하는 편이 맞다고 정리했다.
|
||||||
|
|
||||||
## 2026-04-02 v1.3.83
|
## 2026-04-02 v1.3.83
|
||||||
- 모바일에서 열 헤더가 칸과 시각적으로 분리되는 문제는 전체 레이아웃을 다시 갈아엎기보다, 각 칸 안에 열 이름 배지를 같이 보여주는 편이 가장 적은 변경으로 효과를 낸다고 정리했다.
|
- 모바일에서 열 헤더가 칸과 시각적으로 분리되는 문제는 전체 레이아웃을 다시 갈아엎기보다, 각 칸 안에 열 이름 배지를 같이 보여주는 편이 가장 적은 변경으로 효과를 낸다고 정리했다.
|
||||||
- 배지를 쓰는 반응형 구간에서는 기존 상단 열 헤더까지 남겨두면 중복 정보가 되므로, 같은 브레이크포인트에서 헤더는 숨기고 칸 배지 하나만 남기는 편이 맞다고 정리했다.
|
- 배지를 쓰는 반응형 구간에서는 기존 상단 열 헤더까지 남겨두면 중복 정보가 되므로, 같은 브레이크포인트에서 헤더는 숨기고 칸 배지 하나만 남기는 편이 맞다고 정리했다.
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
# 할 일 및 이슈
|
# 할 일 및 이슈
|
||||||
|
|
||||||
## 단기 확인
|
## 단기 확인
|
||||||
|
- 티어표 `아이콘 크기`는 이제 저장 데이터로 승격됐으므로, 저장 후 재진입/프리뷰/복사본 생성에서 같은 크기가 유지되는지 한 번 더 QA한다.
|
||||||
- 티어표 편집/프리뷰 모바일 열 배지는 새로 붙였으므로, 실제 좁은 화면에서 칸 상단 배지와 아이템 썸네일이 겹치지 않고 열 구분이 자연스러운지 한 번 더 QA한다.
|
- 티어표 편집/프리뷰 모바일 열 배지는 새로 붙였으므로, 실제 좁은 화면에서 칸 상단 배지와 아이템 썸네일이 겹치지 않고 열 구분이 자연스러운지 한 번 더 QA한다.
|
||||||
- 모바일 열 배지는 같은 구간에서 상단 열 제목을 숨기도록 다시 맞췄으므로, 720px 안팎뿐 아니라 980px 이하 전 구간에서 중복 표기 없이 자연스러운지 한 번 더 QA한다.
|
- 모바일 열 배지는 같은 구간에서 상단 열 제목을 숨기도록 다시 맞췄으므로, 720px 안팎뿐 아니라 980px 이하 전 구간에서 중복 표기 없이 자연스러운지 한 번 더 QA한다.
|
||||||
- 모바일 티어표 편집 레이아웃은 행 라벨 폭을 다시 덮어쓰던 규칙을 걷어냈으므로, 실제 980px 이하 구간에서 행 라벨이 과하게 넓지 않고 칸 폭을 충분히 남기는지 한 번 더 QA한다.
|
- 모바일 티어표 편집 레이아웃은 행 라벨 폭을 다시 덮어쓰던 규칙을 걷어냈으므로, 실제 980px 이하 구간에서 행 라벨이 과하게 넓지 않고 칸 폭을 충분히 남기는지 한 번 더 QA한다.
|
||||||
|
|||||||
@@ -1,5 +1,9 @@
|
|||||||
# 업데이트 로그
|
# 업데이트 로그
|
||||||
|
|
||||||
|
## 2026-04-02 v1.3.86
|
||||||
|
- 티어표 편집의 `아이콘 크기`는 이제 임시 화면 상태가 아니라 저장 데이터에 함께 포함되며, 저장 후 다시 열기와 프리뷰 화면에서도 같은 크기로 복원되도록 정리함.
|
||||||
|
- 이를 위해 티어표 저장 payload, 서버 검증, DB 저장/조회에 `iconSize`를 추가하고 기존 데이터는 기본값 `80`으로 안전하게 보정되게 맞춤.
|
||||||
|
|
||||||
## 2026-04-02 v1.3.83
|
## 2026-04-02 v1.3.83
|
||||||
- 티어표 편집/프리뷰 화면에서 열을 여러 개 쓰는 경우, 모바일처럼 좁은 화면에서는 기존 상단 열 헤더만으로 각 칸의 의미를 읽기 어려웠으므로 각 칸 상단에 작은 열 이름 배지를 추가함.
|
- 티어표 편집/프리뷰 화면에서 열을 여러 개 쓰는 경우, 모바일처럼 좁은 화면에서는 기존 상단 열 헤더만으로 각 칸의 의미를 읽기 어려웠으므로 각 칸 상단에 작은 열 이름 배지를 추가함.
|
||||||
- 이 배지는 모바일 구간에서만 보이고 데스크톱 레이아웃은 그대로 유지되므로, 작은 화면에서는 `메인 / 밸런스 / 서포트` 같은 열 맥락을 스크롤 중에도 잃지 않게 정리함.
|
- 이 배지는 모바일 구간에서만 보이고 데스크톱 레이아웃은 그대로 유지되므로, 작은 화면에서는 `메인 / 밸런스 / 서포트` 같은 열 맥락을 스크롤 중에도 잃지 않게 정리함.
|
||||||
|
|||||||
@@ -678,6 +678,7 @@ function buildPayload(existingId) {
|
|||||||
description: (description.value || '').trim(),
|
description: (description.value || '').trim(),
|
||||||
isPublic: !!isPublic.value,
|
isPublic: !!isPublic.value,
|
||||||
showCharacterNames: !!showCharacterNames.value,
|
showCharacterNames: !!showCharacterNames.value,
|
||||||
|
iconSize: Number(iconSize.value || 80),
|
||||||
sourceTierListId: sourceTierListId.value || '',
|
sourceTierListId: sourceTierListId.value || '',
|
||||||
sourceSnapshotTitle: sourceSnapshotTitle.value || '',
|
sourceSnapshotTitle: sourceSnapshotTitle.value || '',
|
||||||
sourceSnapshotAuthor: sourceSnapshotAuthor.value || '',
|
sourceSnapshotAuthor: sourceSnapshotAuthor.value || '',
|
||||||
@@ -923,6 +924,7 @@ onMounted(() => {
|
|||||||
description.value = t.description || ''
|
description.value = t.description || ''
|
||||||
isPublic.value = !!t.isPublic
|
isPublic.value = !!t.isPublic
|
||||||
showCharacterNames.value = !!t.showCharacterNames
|
showCharacterNames.value = !!t.showCharacterNames
|
||||||
|
iconSize.value = Number(t.iconSize || 80)
|
||||||
authorName.value = t.authorName || ''
|
authorName.value = t.authorName || ''
|
||||||
authorAccountName.value = t.authorAccountName || ''
|
authorAccountName.value = t.authorAccountName || ''
|
||||||
updatedAt.value = Number(t.updatedAt || 0)
|
updatedAt.value = Number(t.updatedAt || 0)
|
||||||
@@ -1004,7 +1006,7 @@ onUnmounted(() => {
|
|||||||
<div v-if="isSaveModalOpen" class="modalOverlay" @click.self="closeSaveModal">
|
<div v-if="isSaveModalOpen" class="modalOverlay" @click.self="closeSaveModal">
|
||||||
<div class="modalCard" role="dialog" aria-modal="true" aria-labelledby="saveModalTitle">
|
<div class="modalCard" role="dialog" aria-modal="true" aria-labelledby="saveModalTitle">
|
||||||
<div id="saveModalTitle" class="modalCard__title">저장 완료</div>
|
<div id="saveModalTitle" class="modalCard__title">저장 완료</div>
|
||||||
<div class="modalCard__desc">티어표가 저장되었어요. 이어서 더 수정한 뒤 다시 저장할 수도 있어요.</div>
|
<div class="modalCard__desc">티어표가 저장되었어요.<br />이어서 더 수정한 뒤 다시 저장할 수도 있어요.</div>
|
||||||
<div class="modalCard__actions">
|
<div class="modalCard__actions">
|
||||||
<button class="btn btn--save" @click="closeSaveModal">확인</button>
|
<button class="btn btn--save" @click="closeSaveModal">확인</button>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user