From 68481c3ebf231be40911d8e01a59b03be4b246f6 Mon Sep 17 00:00:00 2001 From: zenn Date: Tue, 7 Apr 2026 14:24:16 +0900 Subject: [PATCH] =?UTF-8?q?=EB=8C=93=EA=B8=80=20=EC=9D=BD=EC=9D=8C?= =?UTF-8?q?=EA=B3=BC=20=EC=A6=90=EA=B2=A8=EC=B0=BE=EA=B8=B0=20=EC=A0=95?= =?UTF-8?q?=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/history.md | 5 ++++ docs/map.md | 4 +-- docs/spec.md | 3 ++ docs/todo.md | 3 ++ docs/update.md | 8 ++++++ frontend/src/views/CommentInboxView.vue | 37 +++++++++++++++++++++++-- frontend/src/views/TierEditorView.vue | 30 ++++++++++---------- 7 files changed, 70 insertions(+), 20 deletions(-) diff --git a/docs/history.md b/docs/history.md index 16bc3c9..bbe6436 100644 --- a/docs/history.md +++ b/docs/history.md @@ -1,5 +1,10 @@ # 의사결정 이력 +## 2026-04-07 v1.1.10 +- 댓글 관리함은 기본적으로 “안 읽은 것부터 처리하는 공간”이므로, 첫 진입 기본값을 전체 목록보다 `안 읽은 댓글만 보기 활성화`로 두는 편이 맞다고 정리했다. +- 댓글 관리 카드의 상단 배지는 정보 라벨보다 행동 버튼이 더 유용하다고 판단했다. `댓글/답글` 구분은 제목과 본문 구조만으로 충분히 이해되므로, 같은 자리는 `읽음 처리`처럼 즉시 처리 가능한 액션에 쓰는 쪽이 효율적이다. +- 티어표 즐겨찾기는 이미 API와 목록 화면이 있으므로 새 기능을 늘리기보다, 보기 화면 우측 레일에 단독 CTA로 명확히 드러내는 편이 더 중요하다고 정리했다. + ## 2026-04-07 v1.1.9 - 댓글 관리함은 단순 알림 문구보다 `어느 티어표에서 어떤 루트 댓글이 있었고 그 아래 어떤 새 댓글/답글이 달렸는지`를 카드 한 장 안에서 읽히게 하는 편이 더 중요하다고 정리했다. 그래서 좌측에는 대상 티어표 정보, 우측에는 댓글 흐름 자체를 배치하는 2열 구조를 기본으로 삼는다. - `commentInboxCard__lead`처럼 제목을 다시 설명하는 보조 문구는 상태 전달에 비해 공간만 차지하므로 제거하고, 대신 실제 댓글 작성자/시간/본문 정보를 바로 보여주는 방향이 낫다고 판단했다. diff --git a/docs/map.md b/docs/map.md index c76cf0f..849eabe 100644 --- a/docs/map.md +++ b/docs/map.md @@ -17,12 +17,12 @@ ## `/editor/:topicId/new`, `/editor/:topicId/:tierListId` - 화면 파일: `frontend/src/views/TierEditorView.vue` -- 역할: 주제 slug 기반 에디터 진입, 티어 그룹 편집, 티어 행 추가/삭제, 보드 옆 아이템 풀에서 관리자 아이템/커스텀 아이템 다중 드래그 앤 드롭 업로드, 아이템 클릭 선택 후 셀/풀 재배치, 아이템 우클릭 메뉴 기반 복제본 생성, 공통 오른쪽 레일 안에 직접 배치되는 우측 편집 섹션에서 티어표 제목/설명/대표 썸네일/공개 여부/저장 제어와 커스텀 아이템 이름 정리, 즐겨찾기 토글, PNG 다운로드, 저장된 티어표 기준 템플릿 등록/업데이트 요청, 댓글 카드 표시, `?preview=1` 진입 시 공통 앱 셸은 유지한 채 중앙 본문에서 완성본 프리뷰와 하단 댓글 카드를 렌더링하며, 우측 뷰어 카드(`공유 티어표 보기`)는 스폰서 카드 바로 아래에서 유지 +- 역할: 주제 slug 기반 에디터 진입, 티어 그룹 편집, 티어 행 추가/삭제, 보드 옆 아이템 풀에서 관리자 아이템/커스텀 아이템 다중 드래그 앤 드롭 업로드, 아이템 클릭 선택 후 셀/풀 재배치, 아이템 우클릭 메뉴 기반 복제본 생성, 공통 오른쪽 레일 안에 직접 배치되는 우측 편집 섹션에서 티어표 제목/설명/대표 썸네일/공개 여부/저장 제어와 커스텀 아이템 이름 정리, 읽기 전용 상태의 즐겨찾기 단독 CTA, PNG 다운로드, 저장된 티어표 기준 템플릿 등록/업데이트 요청, 댓글 카드 표시, `?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` - 화면 파일: `frontend/src/views/CommentInboxView.vue` -- 역할: 내 티어표에 달린 댓글과 내 댓글에 달린 답글을 시간순 카드로 확인, 안 읽은 댓글만 보기 필터, 모두 읽음 처리, 카드별 red dot 표시, 좌측 `썸네일/티어표 제목/템플릿 이름`과 우측 `루트 댓글/새 댓글 또는 답글` 비교 구조 제공, 카드 클릭 시 해당 티어표의 특정 댓글 위치로 이동 +- 역할: 내 티어표에 달린 댓글과 내 댓글에 달린 답글을 시간순 카드로 확인, 기본값이 켜진 `안 읽은 댓글만 보기` 필터, 모두 읽음 처리, 카드별 red dot 표시, 좌측 `썸네일/티어표 제목/템플릿 이름`과 우측 `루트 댓글/새 댓글 또는 답글` 비교 구조 제공, 카드 상단 `읽음 처리` 버튼, 카드 클릭 시 해당 티어표의 특정 댓글 위치로 이동 - 연동 API: `GET /api/comments/inbox`, `GET /api/comments/inbox/unread-count`, `POST /api/comments/inbox/read` ## `/login` diff --git a/docs/spec.md b/docs/spec.md index 34dcfc9..521c7c9 100644 --- a/docs/spec.md +++ b/docs/spec.md @@ -50,9 +50,12 @@ - `tierLists`: 추천 제외 최신 공개 티어표 - 저장된 티어표에는 댓글 스레드가 붙을 수 있다. 작성자 본인 편집 화면에서는 `작업 팁` 아래, 작성자가 아닌 사용자의 보기 전용 화면에서는 `preview` 보드 아래에서 같은 댓글 카드를 사용한다. - 댓글 알림 메뉴는 좌측 사이드 `댓글 관리`로 노출하며, 읽지 않은 댓글이 하나라도 있으면 빨간 dot을 표시한다. +- 댓글 관리(`/comments`)는 기본적으로 `안 읽은 댓글만 보기`를 켠 상태로 시작한다. - 댓글 정렬은 루트 댓글 최신순, 각 루트 내부의 답글은 오래된순을 기본 규칙으로 유지한다. - 댓글 표시 밀도 제어를 위해 기본 노출 개수는 루트 댓글 10개, 각 루트의 답글 3개로 제한하고 `더 보기` 버튼으로 추가 노출한다. - 댓글 관리 카드(`/comments`)는 좌측 `16:9 썸네일 + 티어표 제목 + 템플릿 이름`, 우측 `알림 제목 + 루트 댓글 정보 + 새 댓글/답글 정보`의 2열 구조를 사용한다. +- 댓글 관리 카드의 상단 우측 배지는 상태 라벨이 아니라 개별 `읽음 처리` 액션으로 사용한다. +- 티어표 즐겨찾기 API(`POST/DELETE /api/tierlists/:id/favorite`)는 이미 존재하며, 보기 화면 우측 레일에는 이를 직접 호출하는 단독 CTA를 노출한다. - 우측 패널 - 현재 화면 문맥에 맞는 설명, 빠른 액션, 계정 상태 같은 보조 정보를 배치한다. - 에디터/관리자 세부 옵션은 후속 단계에서 이 패널로 점진 이관한다. diff --git a/docs/todo.md b/docs/todo.md index fa25901..7be574b 100644 --- a/docs/todo.md +++ b/docs/todo.md @@ -1,6 +1,9 @@ # 할 일 및 이슈 ## 단기 확인 +- `v1.1.10` 이후 댓글 관리 화면이 기본적으로 안 읽은 댓글만 보이므로, 사용자가 처음 들어왔을 때 빈 화면처럼 느끼지 않는지와 `전체 보기`로 돌렸을 때도 자연스러운지 확인한다. +- 개별 `읽음 처리` 버튼을 눌렀을 때 카드가 즉시 사라지고 좌측 메뉴 unread dot도 함께 줄어드는지, 마지막 unread 카드까지 처리하면 dot이 사라지는지 확인한다. +- 티어표 보기 화면 우측 즐겨찾기 단독 CTA가 편집 라우트의 읽기 전용 상태와 `preview=1` 뷰어 모드 양쪽에서 모두 자연스럽게 보이는지 확인한다. - `v1.1.9` 이후 댓글 관리 카드에서 좌측 썸네일/티어표 정보와 우측 루트 댓글/새 댓글 정보가 실제로 한눈에 읽히는지, 특히 답글 알림에서 부모 댓글 작성자 정보가 자연스럽게 보이는지 확인한다. - `v1.1.9` 이후 `commentInboxCard__lead` 제거로 정보가 부족해지지 않았는지, 제목과 댓글 블록만으로 상태를 이해할 수 있는지 데스크톱/모바일에서 다시 확인한다. - `v1.1.8` 이후 댓글 더 보기 규칙(루트 10개, 답글 3개)과 남은 개수 표기가 실제 데이터에서 자연스럽게 동작하는지 확인한다. diff --git a/docs/update.md b/docs/update.md index 142d404..f178ebd 100644 --- a/docs/update.md +++ b/docs/update.md @@ -1,5 +1,13 @@ # 업데이트 로그 +## 2026-04-07 v1.1.10 +- 댓글 관리 화면은 기본 진입 시 `안 읽은 댓글만 보기`가 켜진 상태로 시작하도록 바꿨다. 처음 들어왔을 때 가장 중요한 미확인 알림만 먼저 보이게 하는 쪽이 관리 흐름에 더 자연스럽다. +- 댓글 관리 카드의 우측 배지는 더 이상 `댓글/답글` 구분용이 아니라 개별 `읽음 처리` 버튼으로 동작한다. 이제 해당 티어표 화면으로 들어가지 않아도 카드 단위로 바로 읽음 처리할 수 있다. +- 개별 읽음 처리는 `안 읽은 댓글만 보기`가 켜진 상태와 자연스럽게 연결되도록, 처리한 카드는 즉시 목록에서 빠지고 좌측 메뉴 unread dot도 함께 갱신되게 정리했다. +- 티어표 보기 화면의 즐겨찾기 액션을 더 명확하게 드러냈다. 일반 보기 화면 우측 사이드에는 `즐겨찾기에 추가하기 / 즐겨찾기 해제하기` 단독 버튼을 노출하고, 편집 라우트의 읽기 전용 상태에서도 같은 톤의 단일 CTA로 정리했다. +- 즐겨찾기 수는 버튼 안 숫자 대신 보조 문구로 분리해, 액션 자체를 더 또렷하게 읽히게 바꿨다. +- 확인: `npm run build` + ## 2026-04-07 v1.1.9 - 댓글 관리 화면의 패널과 카드 톤을 댓글 카드(`commentsCard`) 계열과 더 가깝게 다시 정리했다. 바깥 패널은 같은 배경/보더 문법을 쓰고, 개별 알림 카드는 장식성 그림자 대신 단정한 카드 레이어로 맞췄다. - 댓글 관리 카드의 정보 구조를 다시 설계했다. 왼쪽에는 `16:9 썸네일 / 티어표 제목 / 템플릿 이름`만 모으고, 오른쪽에는 `알림 제목 / 루트 댓글 / 새 댓글 또는 새 답글` 흐름으로 읽히게 정리했다. diff --git a/frontend/src/views/CommentInboxView.vue b/frontend/src/views/CommentInboxView.vue index fbb6629..b0c44cc 100644 --- a/frontend/src/views/CommentInboxView.vue +++ b/frontend/src/views/CommentInboxView.vue @@ -11,8 +11,9 @@ const router = useRouter() const toast = useToast() const notifications = ref([]) const isLoading = ref(false) -const unreadOnly = ref(false) +const unreadOnly = ref(true) const isMarkingAllRead = ref(false) +const markingNotificationId = ref('') const unreadCount = computed(() => notifications.value.filter((item) => !item.isRead).length) @@ -75,16 +76,30 @@ async function loadInbox() { async function markOneAsRead(notificationId) { const target = notifications.value.find((item) => item.id === notificationId) if (!target || target.isRead) return + const original = notifications.value.map((item) => ({ ...item })) target.isRead = true + if (unreadOnly.value) { + notifications.value = notifications.value.filter((item) => item.id !== notificationId) + } emitUnreadCount(unreadCount.value) try { await api.markCommentInboxRead({ notificationIds: [notificationId] }) } catch (error) { - target.isRead = false + notifications.value = original emitUnreadCount(unreadCount.value) } } +async function markNotificationButton(notificationId) { + if (!notificationId || markingNotificationId.value) return + markingNotificationId.value = notificationId + try { + await markOneAsRead(notificationId) + } finally { + markingNotificationId.value = '' + } +} + async function markAllAsRead() { if (!unreadCount.value) return isMarkingAllRead.value = true @@ -181,7 +196,15 @@ watch(unreadOnly, loadInbox)
{{ notificationTitle(notification) }}
- {{ notification.notificationType === 'comment_reply' ? '답글' : '댓글' }} +
@@ -373,13 +396,21 @@ watch(unreadOnly, loadInbox) .commentInboxCard__badge { display: inline-flex; align-items: center; + justify-content: center; min-height: 28px; padding: 0 12px; border-radius: 999px; + border: 1px solid color-mix(in srgb, var(--theme-accent) 22%, var(--theme-border)); background: color-mix(in srgb, var(--theme-accent) 18%, var(--theme-surface-soft)); color: var(--theme-text); font-size: 12px; font-weight: 800; + cursor: pointer; +} + +.commentInboxCard__badge:disabled { + cursor: default; + opacity: 0.7; } .commentInboxCard__thread { diff --git a/frontend/src/views/TierEditorView.vue b/frontend/src/views/TierEditorView.vue index 073ccd4..0871e6a 100644 --- a/frontend/src/views/TierEditorView.vue +++ b/frontend/src/views/TierEditorView.vue @@ -143,6 +143,7 @@ const canDuplicate = computed(() => !!auth.user && hasSavedTierList.value) const canSwitchToViewerMode = computed(() => isOwnTierList.value && hasSavedTierList.value && !previewMode.value) const canSwitchToEditMode = computed(() => isOwnTierList.value && hasSavedTierList.value && previewMode.value) const duplicateActionLabel = computed(() => (isOwnTierList.value ? '복사본 만들기' : '내 티어표로 복사')) +const favoriteActionLabel = computed(() => (isFavorited.value ? '즐겨찾기 해제하기' : '즐겨찾기에 추가하기')) const canOpenAuthorProfile = computed(() => !!ownerId.value && hasSavedTierList.value) const copiedFromLabel = computed(() => { if (!sourceTierListId.value) return '' @@ -1502,6 +1503,9 @@ onUnmounted(() => { + @@ -1944,10 +1948,10 @@ onUnmounted(() => {
- +
현재 {{ favoriteCount }}명이 이 티어표를 즐겨찾기했어요.
@@ -2996,19 +3000,15 @@ onUnmounted(() => { color: var(--theme-text-soft); word-break: break-word; } -.editorSidebar__favorite { - display: flex; - align-items: center; - justify-content: space-between; - gap: 10px; +.editorSidebar__favoriteButton { width: 100%; - padding: 11px 12px; - border-radius: 14px; - border: 1px solid var(--theme-border); - background: var(--theme-pill-bg); - color: var(--theme-text); - font-weight: 800; - cursor: pointer; +} + +.editorSidebar__favoriteMeta { + margin-top: 10px; + color: var(--theme-text-muted); + font-size: 13px; + line-height: 1.5; } .editorSidebar__section--footer { padding-top: 12px;