diff --git a/docs/history.md b/docs/history.md index 36ad2d8..1d7230f 100644 --- a/docs/history.md +++ b/docs/history.md @@ -1,5 +1,10 @@ # 의사결정 이력 +## 2026-04-07 v1.1.26 +- `주제 불러오는 중...` 같은 임시 문구는 스켈레톤보다 더 거슬리게 보일 수 있으므로, 짧은 로딩 구간에서는 텍스트 fallback 대신 빈 자리 + 스켈레톤으로 처리하는 편이 낫다고 정리했다. +- 저장용 랜덤 제목이나 내부 `tierListId`는 시스템 식별자 역할일 뿐 화면 표시용 카피가 아니므로, 사용자가 보는 제목 fallback 으로 직접 노출하지 않는 쪽이 맞다고 정리했다. +- 같은 컴포넌트 안에서 라우트 파라미터만 바뀌는 화면은 일반 watch 보다 더 이른 시점에 로딩 상태를 세우는 편이 깜빡임 완화에 유리하다고 판단했다. + ## 2026-04-07 v1.1.25 - 이번 로딩 문제는 “매우 짧은 대기시간 + 미완성 실제 화면 노출”이 핵심이므로, 풀스크린 로딩 화면을 계속 번쩍 띄우는 것보다 `초기 인증 게이트 + 페이지 스켈레톤` 조합이 더 맞다고 정리했다. - 로그인 상태가 아직 hydrate 되지 않았을 때는 비로그인 UI를 잠깐 먼저 보여주지 않고, 최소 시간 있는 부트 게이트로 가려 두는 편이 완성도가 높다고 판단했다. diff --git a/docs/map.md b/docs/map.md index d503dfc..55743e2 100644 --- a/docs/map.md +++ b/docs/map.md @@ -12,12 +12,12 @@ ## `/topics/:topicId` - 화면 파일: `frontend/src/views/TopicHubView.vue` -- 역할: 선택한 주제 slug 기준 정보 표시, 관리자 추천 티어표 상단 강조 섹션과 일반 공개 티어표 목록 분리 표시, 왼쪽 공통 검색창으로 해당 주제의 공개 티어표만 검색, 티어표별 `상단 썸네일 / 제목+좋아요 / 작성자+최종 수정일` 카드 표시, 새 티어표 작성은 우측 하단 CTA로 진입 +- 역할: 선택한 주제 slug 기준 정보 표시, 관리자 추천 티어표 상단 강조 섹션과 일반 공개 티어표 목록 분리 표시, 왼쪽 공통 검색창으로 해당 주제의 공개 티어표만 검색, 티어표별 `상단 썸네일 / 제목+좋아요 / 작성자+최종 수정일` 카드 표시, 새 티어표 작성은 우측 하단 CTA로 진입하며, 주제명은 데이터 준비 전 `주제 불러오는 중...` 같은 텍스트 대신 제목 영역 스켈레톤으로 대기 - 연동 API: `GET /api/topics/:topicId`, `GET /api/tierlists/public`, `POST /api/tierlists/:id/favorite`, `DELETE /api/tierlists/:id/favorite` ## `/editor/:topicId/new`, `/editor/:topicId/:tierListId` - 화면 파일: `frontend/src/views/TierEditorView.vue` -- 역할: 주제 slug 기반 에디터 진입, 티어 그룹 편집, 티어 행 추가/삭제, 보드 옆 아이템 풀에서 관리자 아이템/커스텀 아이템 다중 드래그 앤 드롭 업로드, 아이템 클릭 선택 후 셀/풀 재배치, 아이템 우클릭 메뉴 기반 복제본 생성, 상단 템플릿 제목 클릭 시 해당 주제 허브로 이동, 공통 오른쪽 레일 안에 직접 배치되는 우측 편집 섹션에서 티어표 제목/설명/대표 썸네일/공개 여부/저장 제어와 커스텀 아이템 이름 정리, 읽기 전용 상태의 즐겨찾기 단독 CTA, PNG 다운로드, 저장된 티어표 기준 템플릿 등록/업데이트 요청, 댓글 카드 표시, 데이터가 준비되기 전에는 편집 모드/프리뷰 모드 모두 전용 스켈레톤 레이아웃을 먼저 보여주고, `?preview=1` 진입 시 공통 앱 셸은 유지한 채 중앙 본문에서 완성본 프리뷰와 하단 댓글 카드를 렌더링하며, 우측 뷰어 카드(`공유 티어표 보기`)는 스폰서 카드 바로 아래에서 유지하고 즐겨찾기 CTA도 함께 노출 +- 역할: 주제 slug 기반 에디터 진입, 티어 그룹 편집, 티어 행 추가/삭제, 보드 옆 아이템 풀에서 관리자 아이템/커스텀 아이템 다중 드래그 앤 드롭 업로드, 아이템 클릭 선택 후 셀/풀 재배치, 아이템 우클릭 메뉴 기반 복제본 생성, 상단 템플릿 제목 클릭 시 해당 주제 허브로 이동, 공통 오른쪽 레일 안에 직접 배치되는 우측 편집 섹션에서 티어표 제목/설명/대표 썸네일/공개 여부/저장 제어와 커스텀 아이템 이름 정리, 읽기 전용 상태의 즐겨찾기 단독 CTA, PNG 다운로드, 저장된 티어표 기준 템플릿 등록/업데이트 요청, 댓글 카드 표시, 데이터가 준비되기 전에는 편집 모드/프리뷰 모드 모두 전용 스켈레톤 레이아웃을 먼저 보여주고, 저장용 랜덤 제목이나 내부 ID는 화면 표시용 제목으로 직접 노출하지 않으며, `?preview=1` 진입 시 공통 앱 셸은 유지한 채 중앙 본문에서 완성본 프리뷰와 하단 댓글 카드를 렌더링하며, 우측 뷰어 카드(`공유 티어표 보기`)는 스폰서 카드 바로 아래에서 유지하고 즐겨찾기 CTA도 함께 노출 - 연동 API: `GET /api/topics/:topicId`, `GET /api/tierlists/:id`, `GET /api/tierlists/:id/comments`, `POST /api/tierlists/:id/comments`, `DELETE /api/tierlists/:id/comments/:commentId`, `POST /api/tierlists/:id/favorite`, `DELETE /api/tierlists/:id/favorite`, `POST /api/tierlists/thumbnail`, `POST /api/tierlists/custom-items`, `POST /api/tierlists`, `POST /api/tierlists/template-request` ## `/comments` diff --git a/docs/spec.md b/docs/spec.md index ff8e0f1..5eb39a6 100644 --- a/docs/spec.md +++ b/docs/spec.md @@ -71,6 +71,7 @@ - 팔로우 피드: 팔로우한 작성자의 공개 티어표 - 즐겨찾기: 내가 즐겨찾기한 티어표 - 위 네 화면의 목록 데이터는 현재 페이지네이션이나 무한 스크롤 없이 조회 결과 전체를 한 번에 렌더링한다. +- 특정 주제 화면(`TopicHubView`)의 헤더는 데이터 준비 전 문자열 fallback 대신 제목/설명 스켈레톤만 보여준다. - 저장된 티어표에는 댓글 스레드가 붙을 수 있다. 작성자 본인 편집 화면에서는 `작업 팁` 아래, 작성자가 아닌 사용자의 보기 전용 화면에서는 `preview` 보드 아래에서 같은 댓글 카드를 사용한다. - 댓글 알림 메뉴는 좌측 사이드 `댓글 관리`로 노출하며, 읽지 않은 댓글이 하나라도 있으면 빨간 dot을 표시한다. - 댓글 관리(`/comments`)는 기본적으로 `안 읽은 댓글만 보기`를 켠 상태로 시작한다. @@ -95,6 +96,7 @@ - 공통 `workspaceBody` 카드 컨테이너를 벗기고, 중앙 보드 영역은 메인 컬럼에, 우측 `320px` 편집 패널은 공통 셸의 세 번째 컬럼 aside에 배치한다. - 공통 상단 토글 버튼은 Teleport로 이동한 로컬 편집 패널의 접힘/펼침 상태와도 연결되어, 우측 패널을 숨기면 중앙 보드 영역이 확장된다. - 편집 모드와 `preview=1` 뷰어 모드 모두 제목/보드/우측 패널 데이터가 준비되기 전까지는 실제 화면 대신 전용 스켈레톤 레이아웃을 먼저 보여준다. + - 저장용 랜덤 제목은 내부 저장/도배 방지용으로만 쓰고, 화면 표시용 제목 fallback 에서는 내부 `tierListId`나 랜덤 문자열을 직접 노출하지 않는다. - 제목, 설명, 대표 썸네일, 공개 여부, 저장/삭제/요청 액션을 우측 로컬 패널에 배치한다. 템플릿 등록/업데이트 요청 버튼은 저장된 티어표가 있을 때만 노출하며, 제목이 비어 있는 상태에서 저장하면 랜덤 고유 제목을 먼저 부여해 저장본을 만든다. - 상단 템플릿 제목은 해당 주제 허브로 이동하는 액션으로 사용하며, 미저장 변경이 있으면 이동 전에 확인 모달을 띄운다. - 보드 바로 옆에는 드래그용 아이템 풀을 별도 패널로 두고, 커스텀 아이템 이름 정리 목록은 우측 편집 패널 내부에서 관리한다. diff --git a/docs/todo.md b/docs/todo.md index 5e142f6..216a037 100644 --- a/docs/todo.md +++ b/docs/todo.md @@ -1,6 +1,9 @@ # 할 일 및 이슈 ## 단기 확인 +- `v1.1.26` 이후 템플릿 카드 클릭 시 `주제 불러오는 중...` 문구가 더 이상 보이지 않고, 주제명 헤더 스켈레톤 뒤에 실제 이름이 자연스럽게 붙는지 확인한다. +- `v1.1.26` 이후 템플릿 A에서 템플릿 B로 빠르게 연속 이동해도 이전 주제의 공개 티어표 카드가 잠깐 남지 않는지 확인한다. +- `v1.1.26` 이후 저장 제목이 없는 티어표를 열 때 내부 ID나 자동 생성 문자열 대신 주제명 기반 표시 제목만 보이는지 확인한다. - `v1.1.25` 이후 첫 진입 시 로그인 상태가 비로그인처럼 잠깐 보였다가 바뀌는 깜빡임이 실제로 줄었는지, 느린 네트워크/빠른 네트워크 양쪽에서 확인한다. - `v1.1.25` 이후 티어표 화면 진입 시 제목 자리에 내부 ID가 먼저 노출되지 않고, 편집 모드/프리뷰 모드 둘 다 스켈레톤 뒤에 실제 데이터가 자연스럽게 붙는지 확인한다. - 초기 인증 게이트는 최소 140ms를 기다리므로, 초고속 응답에서도 오히려 부트 스켈레톤이 과하게 눈에 띄지 않는지 확인한다. diff --git a/docs/update.md b/docs/update.md index dd16a73..986ec39 100644 --- a/docs/update.md +++ b/docs/update.md @@ -1,5 +1,12 @@ # 업데이트 로그 +## 2026-04-07 v1.1.26 +- 템플릿 주제 화면(`TopicHubView`)의 제목 fallback 문자열 `주제 불러오는 중...`을 제거했다. 이제 주제 데이터가 준비되기 전에는 실제 문구 대신 제목/설명 영역 스켈레톤을 보여준다. +- 주제 전환 시 이전 템플릿의 공개 티어표 목록이 잠깐 남아 보이지 않도록, `topicName`, `featuredTierLists`, `tierLists`, `brokenThumbnailIds`를 먼저 비우고 다시 불러오게 정리했다. +- 티어표 편집 화면은 저장용 랜덤 제목과 화면 표시용 제목을 분리했다. 이제 로딩 전후에 내부 `tierListId`나 자동 생성 문자열이 사용자 제목처럼 먼저 드러나지 않고, 표시용 제목은 `주제명 티어표` 또는 `제목 없는 티어표`로만 보여준다. +- 편집 화면 라우트 감시 watch 는 `flush: 'sync'`로 조정해, 같은 컴포넌트 안에서 파라미터가 바뀔 때도 로딩 상태가 더 이른 시점에 반영되도록 보강했다. +- 확인: `npm run build` + ## 2026-04-07 v1.1.25 - 앱 최초 진입 시 로그인 상태가 잠깐 비로그인처럼 보였다가 다시 로그인 상태로 바뀌는 깜빡임을 줄이기 위해, 공통 앱 셸에 `bootGate` 초기 게이트를 추가했다. `App.vue`는 `auth.refresh()`와 최소 140ms 대기 시간을 함께 기다린 뒤 셸을 렌더링해, 아주 짧은 인증 복원에서도 화면이 번쩍 바뀌지 않게 한다. - `TierEditorView`는 제목/보드/우측 아이템 풀이 준비되기 전까지 실제 편집 화면 대신 같은 서비스 톤의 스켈레톤 레이아웃을 먼저 보여주도록 바꿨다. 이로써 티어표 진입 직후 내부 ID나 빈 보드가 먼저 드러나는 현상을 줄였다. diff --git a/frontend/src/views/TierEditorView.vue b/frontend/src/views/TierEditorView.vue index 6f41dab..e03e49d 100644 --- a/frontend/src/views/TierEditorView.vue +++ b/frontend/src/views/TierEditorView.vue @@ -125,13 +125,19 @@ const effectiveAuthorName = computed(() => { return (authorAccountName.value || '').trim() || 'unknown' }) const autoGeneratedTitle = ref(createAutoTierListTitle()) -const effectiveTitle = computed(() => { +const saveTitle = computed(() => { const customTitle = (title.value || '').trim() if (customTitle) return customTitle - if (persistedTierListId.value) return persistedTierListId.value - if (tierListId.value && tierListId.value !== 'new') return tierListId.value return autoGeneratedTitle.value }) +const displayTitle = computed(() => { + const customTitle = (title.value || '').trim() + if (customTitle) return customTitle + const topicName = (templateName.value || '').trim() + if (topicName) return `${topicName} 티어표` + if (isEditorLoading.value) return '' + return '제목 없는 티어표' +}) const displayThumbnailUrl = computed(() => thumbnailPreviewUrl.value || (thumbnailSrc.value ? resolveItemSrc({ src: thumbnailSrc.value }) : '')) const untitledWarning = computed( () => @@ -916,7 +922,7 @@ async function downloadImage() { const url = URL.createObjectURL(blob) const a = document.createElement('a') a.href = url - a.download = `${effectiveTitle.value.trim()}.png` + a.download = `${(displayTitle.value || 'tier-maker').trim()}.png` document.body.appendChild(a) a.click() a.remove() @@ -980,7 +986,7 @@ async function uploadPendingThumbnail() { } function buildPayload(existingId) { - const finalTitle = effectiveTitle.value + const finalTitle = saveTitle.value return { id: existingId || undefined, topicId: templateId.value, @@ -1267,6 +1273,7 @@ function resetEditorStateForRoute() { groups.value = normalizeLoadedGroups([], columns.value) pool.value = [] itemsById.value = {} + templateName.value = '' title.value = '' persistedTierListId.value = '' thumbnailSrc.value = '' @@ -1416,7 +1423,7 @@ watch( () => { loadEditorState() }, - { immediate: true } + { immediate: true, flush: 'sync' } ) onMounted(() => { diff --git a/frontend/src/views/TopicHubView.vue b/frontend/src/views/TopicHubView.vue index 9cca868..5fe3e34 100644 --- a/frontend/src/views/TopicHubView.vue +++ b/frontend/src/views/TopicHubView.vue @@ -20,7 +20,7 @@ const error = ref('') const brokenThumbnailIds = ref({}) const isTopicLoading = ref(false) const isListView = computed(() => route.query.view === 'list') -const topicTitle = computed(() => topicName.value || (isTopicLoading.value ? '주제 불러오는 중...' : '')) +const topicTitle = computed(() => topicName.value || '') const publicTierLists = computed(() => tierLists.value.filter((tierList) => !tierList.isFeatured)) function fmt(ts) { @@ -88,6 +88,9 @@ watch( [topicId, query], () => { topicName.value = '' + featuredTierLists.value = [] + tierLists.value = [] + brokenThumbnailIds.value = {} error.value = '' loadTierLists() }, @@ -100,8 +103,16 @@ watch(
Collection
-

{{ topicTitle }}

-
이 주제의 공개 티어표를 같은 카드 레이아웃으로 살펴보고 이어서 새 티어표를 만들 수 있어요.
+ +
@@ -194,6 +205,39 @@ watch(