diff --git a/backend/src/db.js b/backend/src/db.js index 3ec2047..bc6d4e4 100644 --- a/backend/src/db.js +++ b/backend/src/db.js @@ -77,6 +77,15 @@ function mapTierListRow(row) { } } +function getUserDisplayName(row) { + if (!row) return '' + const nickname = (row.nickname || '').trim() + if (nickname) return nickname + const email = (row.email || '').trim() + if (!email) return '' + return email.split('@')[0] || email +} + async function createPool() { const rootConnection = await mysql.createConnection({ host: DB_HOST, @@ -537,7 +546,8 @@ async function listPublicTierLists(gameId) { t.updated_at, t.author_id, u.nickname, - u.email + u.email, + u.avatar_src FROM tierlists t INNER JOIN users u ON u.id = t.author_id ${whereClause} @@ -554,16 +564,27 @@ async function listPublicTierLists(gameId) { createdAt: Number(row.created_at), updatedAt: Number(row.updated_at), authorId: row.author_id, - authorName: row.nickname || row.email, + authorName: getUserDisplayName(row), + authorAvatarSrc: row.avatar_src || '', })) } async function listUserTierLists(userId) { const rows = await query( ` - SELECT id, game_id, title, created_at, updated_at, is_public - FROM tierlists - WHERE author_id = ? + SELECT + t.id, + t.game_id, + t.title, + t.created_at, + t.updated_at, + t.is_public, + u.nickname, + u.email, + u.avatar_src + FROM tierlists t + INNER JOIN users u ON u.id = t.author_id + WHERE t.author_id = ? ORDER BY updated_at DESC `, [userId] @@ -576,6 +597,8 @@ async function listUserTierLists(userId) { createdAt: Number(row.created_at), updatedAt: Number(row.updated_at), isPublic: !!row.is_public, + authorName: getUserDisplayName(row), + authorAvatarSrc: row.avatar_src || '', })) } diff --git a/docs/history.md b/docs/history.md index c6940e2..514c8c3 100644 --- a/docs/history.md +++ b/docs/history.md @@ -66,3 +66,9 @@ ## 2026-03-19 v0.1.17 - 작성자가 자기 티어표를 직접 삭제할 수 있어야 관리 흐름이 완결되므로, `내 티어표` 화면에 삭제 기능을 추가하기로 결정했다. - 사용자 커스텀 이미지는 서버 용량을 계속 차지하므로, 참조가 하나도 남지 않은 이미지(미사용 항목)를 식별하고 관리자 화면에서 개별/일괄 정리할 수 있도록 하기로 결정했다. + +## 2026-03-19 v0.1.19 +- 티어표 공개 여부는 운영 기준상 대부분 공개 공유가 목적이므로, 신규 작성 시 기본값을 `공개 ON`으로 두기로 결정했다. +- 에디터에서 저장 관련 행동은 좌우로 역할을 나눠 `다운로드/삭제`와 `공개/저장`으로 묶는 편이 더 빠르게 인지된다고 판단했다. +- 공개 목록과 내 목록에서 제목만으로는 식별이 어려우므로, 제목 옆에 작성자 아바타와 표시명을 함께 노출하기로 결정했다. +- 티어표 카드 메타 정보는 저장 시각과 업데이트 시각을 동시에 노출하는 대신, 최종 업데이트 시각만 남겨 단순화하기로 결정했다. diff --git a/docs/spec.md b/docs/spec.md index f95316a..839b6c0 100644 --- a/docs/spec.md +++ b/docs/spec.md @@ -105,7 +105,9 @@ - 비로그인 사용자는 공개된 티어표를 열람만 할 수 있고, 편집 UI와 저장 동작은 비활성화된다. - 커스텀 이미지 추가는 다중 파일 선택과 드래그 앤 드롭을 모두 지원한다. - 티어 행은 기본 5단으로 시작하지만, 사용자가 직접 추가하거나 제거할 수 있다. -- 작성자는 `내 티어표` 목록에서 저장한 티어표를 직접 삭제할 수 있다. +- 신규 티어표의 공개 여부 기본값은 `ON`이며, 기존 티어표는 편집 화면과 `내 티어표` 목록에서 직접 삭제할 수 있다. +- 공개 티어표 목록과 `내 티어표` 목록은 제목 옆에 작성자 아바타와 표시명을 함께 보여준다. +- 티어표 목록 메타 정보는 최종 업데이트 시각만 간략하게 표시한다. ## 운영 환경 변수 - 프런트엔드 diff --git a/docs/update.md b/docs/update.md index 5820b8d..56398a2 100644 --- a/docs/update.md +++ b/docs/update.md @@ -1,5 +1,11 @@ # 업데이트 로그 +## 2026-03-19 v0.1.19 +- **에디터 저장 영역 재정렬**: 공개 기본값을 `ON`으로 바꾸고, 액션 영역을 `이미지 다운로드 / 삭제 / 공개 ON·OFF / 저장` 흐름으로 재배치 +- **에디터 삭제 진입점 추가**: 기존 티어표는 편집 화면에서 바로 삭제할 수 있도록 버튼을 추가 +- **목록 작성자 표시 개선**: 공개 티어표와 내 티어표 목록의 제목 옆에 원형 아바타와 `by 닉네임(없으면 계정명)`을 표시 +- **목록 메타 단순화**: 티어표 카드 하단 정보는 게임 ID, 저장 시각, 라벨 문구를 제거하고 최종 업데이트 시각만 간략하게 노출 + ## 2026-03-19 v0.1.18 - **미사용 아이콘 필터 수정**: 관리자 아이템 관리의 `미사용 아이콘 보기` 체크 상태가 실제 API 요청의 `orphanOnly` 파라미터로 전달되도록 수정 - **삭제 활성화 흐름 정상화**: 미사용 아이콘만 조회했을 때 `usageCount = 0` 항목의 개별 삭제 버튼이 의도대로 활성화되도록 정리 diff --git a/frontend/src/views/GameHubView.vue b/frontend/src/views/GameHubView.vue index 9df5009..00696ef 100644 --- a/frontend/src/views/GameHubView.vue +++ b/frontend/src/views/GameHubView.vue @@ -2,6 +2,7 @@ import { computed, onMounted, ref } from 'vue' import { useRoute, useRouter } from 'vue-router' import { api } from '../lib/api' +import { toApiUrl } from '../lib/runtime' import { useAuthStore } from '../stores/auth' const route = useRoute() @@ -21,10 +22,21 @@ function fmt(ts) { day: '2-digit', hour: '2-digit', minute: '2-digit', - second: '2-digit', }) } +function displayNameOf(tierList) { + return tierList.authorName || '알 수 없음' +} + +function avatarSrcOf(tierList) { + return tierList.authorAvatarSrc ? toApiUrl(tierList.authorAvatarSrc) : '' +} + +function avatarFallbackOf(tierList) { + return displayNameOf(tierList).trim().charAt(0).toUpperCase() || '?' +} + onMounted(async () => { try { const [gameRes, listRes] = await Promise.all([api.getGame(gameId.value), api.listPublicTierLists(gameId.value)]) @@ -66,10 +78,15 @@ function openTierList(id) {