diff --git a/backend/src/db.js b/backend/src/db.js index bc6d4e4..c91cc30 100644 --- a/backend/src/db.js +++ b/backend/src/db.js @@ -86,6 +86,13 @@ function getUserDisplayName(row) { return email.split('@')[0] || email } +function getUserAccountName(row) { + if (!row) return '' + const email = (row.email || '').trim() + if (!email) return '' + return email.split('@')[0] || email +} + async function createPool() { const rootConnection = await mysql.createConnection({ host: DB_HOST, @@ -565,6 +572,7 @@ async function listPublicTierLists(gameId) { updatedAt: Number(row.updated_at), authorId: row.author_id, authorName: getUserDisplayName(row), + authorAccountName: getUserAccountName(row), authorAvatarSrc: row.avatar_src || '', })) } @@ -598,6 +606,7 @@ async function listUserTierLists(userId) { updatedAt: Number(row.updated_at), isPublic: !!row.is_public, authorName: getUserDisplayName(row), + authorAccountName: getUserAccountName(row), authorAvatarSrc: row.avatar_src || '', })) } diff --git a/docs/history.md b/docs/history.md index 514c8c3..b98357e 100644 --- a/docs/history.md +++ b/docs/history.md @@ -72,3 +72,8 @@ - 에디터에서 저장 관련 행동은 좌우로 역할을 나눠 `다운로드/삭제`와 `공개/저장`으로 묶는 편이 더 빠르게 인지된다고 판단했다. - 공개 목록과 내 목록에서 제목만으로는 식별이 어려우므로, 제목 옆에 작성자 아바타와 표시명을 함께 노출하기로 결정했다. - 티어표 카드 메타 정보는 저장 시각과 업데이트 시각을 동시에 노출하는 대신, 최종 업데이트 시각만 남겨 단순화하기로 결정했다. + +## 2026-03-26 v0.1.21 +- 목록 썸네일 fallback 문자는 닉네임보다 계정 기준이 더 일관되므로, 아바타 이미지가 없을 때는 계정명 첫 글자를 사용하기로 결정했다. +- 저장 성공은 화면 이동보다 현 위치 유지가 더 중요하므로, 편집을 계속할 수 있는 확인형 모달로 피드백을 제공하기로 결정했다. +- PNG export는 가장자리 여백이 없는 결과보다 중앙 정렬된 카드형 결과물이 더 완성도 있게 보이므로, export 전용 보드에 충분한 바깥 패딩을 포함하기로 했다. diff --git a/docs/spec.md b/docs/spec.md index 839b6c0..994732e 100644 --- a/docs/spec.md +++ b/docs/spec.md @@ -107,7 +107,9 @@ - 티어 행은 기본 5단으로 시작하지만, 사용자가 직접 추가하거나 제거할 수 있다. - 신규 티어표의 공개 여부 기본값은 `ON`이며, 기존 티어표는 편집 화면과 `내 티어표` 목록에서 직접 삭제할 수 있다. - 공개 티어표 목록과 `내 티어표` 목록은 제목 옆에 작성자 아바타와 표시명을 함께 보여준다. +- 작성자 아바타 이미지가 없을 경우 목록 썸네일 fallback은 닉네임이 아니라 계정명 기준 첫 글자를 사용한다. - 티어표 목록 메타 정보는 최종 업데이트 시각만 간략하게 표시한다. +- 저장 성공 시에는 에디터 안에서 반투명 오버레이 기반 확인 모달을 띄우고, PNG export 이미지는 외곽 여백을 포함해 생성한다. ## 운영 환경 변수 - 프런트엔드 diff --git a/docs/update.md b/docs/update.md index b3f9163..9b7a2ec 100644 --- a/docs/update.md +++ b/docs/update.md @@ -1,5 +1,10 @@ # 업데이트 로그 +## 2026-03-26 v0.1.21 +- **아바타 fallback 기준 통일**: 티어표 목록에서 작성자 아바타 이미지가 없을 때 닉네임이 아니라 계정명 기준 첫 글자를 표시하도록 정리 +- **저장 완료 모달 추가**: 에디터에서 저장 성공 시 반투명 오버레이와 확인 버튼이 있는 피드백 모달을 표시하도록 추가 +- **다운로드 이미지 여백 보강**: PNG export 전용 보드에 외곽 패딩과 배경 여백을 넣어 콘텐츠가 가장자리에 붙어 보이지 않도록 조정 + ## 2026-03-19 v0.1.20 - **게임 선택 카드 순서 조정**: 홈 화면에서 일반 게임 카드를 먼저 보여주고 `직접 티어표 만들기` 카드는 마지막에 배치 - **게임 카드 3열 레이아웃**: PC 기준 게임 선택 화면 카드를 3열로 재구성하고, 썸네일을 16:9 비율로 통일 diff --git a/frontend/src/views/GameHubView.vue b/frontend/src/views/GameHubView.vue index 0393468..4a69c47 100644 --- a/frontend/src/views/GameHubView.vue +++ b/frontend/src/views/GameHubView.vue @@ -34,7 +34,7 @@ function avatarSrcOf(tierList) { } function avatarFallbackOf(tierList) { - return displayNameOf(tierList).trim().charAt(0).toUpperCase() || '?' + return (tierList.authorAccountName || 'u').trim().charAt(0).toUpperCase() || '?' } onMounted(async () => { diff --git a/frontend/src/views/MyTierListsView.vue b/frontend/src/views/MyTierListsView.vue index 82b95a2..5ed8b09 100644 --- a/frontend/src/views/MyTierListsView.vue +++ b/frontend/src/views/MyTierListsView.vue @@ -27,7 +27,7 @@ function avatarSrcOf(tierList) { } function avatarFallbackOf(tierList) { - return displayNameOf(tierList).trim().charAt(0).toUpperCase() || '?' + return (tierList.authorAccountName || 'u').trim().charAt(0).toUpperCase() || '?' } onMounted(async () => { diff --git a/frontend/src/views/TierEditorView.vue b/frontend/src/views/TierEditorView.vue index 6bb170f..a34331a 100644 --- a/frontend/src/views/TierEditorView.vue +++ b/frontend/src/views/TierEditorView.vue @@ -31,6 +31,7 @@ const isPublic = ref(true) const error = ref('') const isSaving = ref(false) const isExporting = ref(false) +const isSaveModalOpen = ref(false) const ownerId = ref('') const isDragActive = ref(false) @@ -292,6 +293,7 @@ async function save() { const payload = buildPayload(tierListId.value && tierListId.value !== 'new' ? tierListId.value : null) const res = await api.saveTierList(payload) if (tierListId.value === 'new') history.replaceState({}, '', `/editor/${gameId.value}/${res.tierList.id}`) + isSaveModalOpen.value = true } catch (e) { error.value = '저장 실패: 로그인 상태인지 확인해주세요.' } finally { @@ -299,6 +301,10 @@ async function save() { } } +function closeSaveModal() { + isSaveModalOpen.value = false +} + async function removeTierList() { if (!canEdit.value || isNewTierList.value) return error.value = '' @@ -407,6 +413,16 @@ onUnmounted(() => {
{{ error }}
+
+ +
+
@@ -609,6 +625,40 @@ onUnmounted(() => { padding: 20px; align-self: start; } +.modalOverlay { + position: fixed; + inset: 0; + z-index: 40; + display: grid; + place-items: center; + padding: 20px; + background: rgba(4, 8, 16, 0.68); + backdrop-filter: blur(4px); +} +.modalCard { + width: min(100%, 420px); + border-radius: 20px; + padding: 24px; + border: 1px solid rgba(255, 255, 255, 0.14); + background: linear-gradient(180deg, rgba(17, 24, 39, 0.96), rgba(11, 18, 32, 0.96)); + box-shadow: 0 24px 60px rgba(0, 0, 0, 0.38); + display: grid; + gap: 10px; +} +.modalCard__title { + font-size: 22px; + font-weight: 900; + letter-spacing: -0.02em; +} +.modalCard__desc { + line-height: 1.6; + opacity: 0.82; +} +.modalCard__actions { + display: flex; + justify-content: flex-end; + margin-top: 8px; +} .boardTools { display: flex; justify-content: flex-end; @@ -617,6 +667,11 @@ onUnmounted(() => { .exportBoard--active { display: grid; gap: 12px; + padding: 28px; + border-radius: 24px; + background: + radial-gradient(circle at top, rgba(96, 165, 250, 0.14), transparent 38%), + rgba(11, 18, 32, 0.98); } .exportBoard__title { font-size: 28px;