From 9ba4bc1a4189fedc7f7d72f59c91db3d7bc9eb79 Mon Sep 17 00:00:00 2001 From: ttipo Date: Sat, 7 Nov 2026 01:15:43 +0900 Subject: [PATCH] =?UTF-8?q?fix(frontend):=2014=EB=8B=A8=EA=B3=84=20?= =?UTF-8?q?=ED=99=94=EB=A9=B4=EC=9D=84=20=EC=8B=A4=EC=82=AC=EC=9A=A9=20?= =?UTF-8?q?=EC=8B=9C=ED=97=98=20=C2=B7=20=EC=82=AC=EC=9A=A9=EC=9E=90=20?= =?UTF-8?q?=EA=B4=80=EC=A0=90=20=EC=A0=90=EA=B2=80=EB=8C=80=EB=A1=9C=20?= =?UTF-8?q?=EA=B3=A0=EC=B9=9C=EB=8B=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 분할 보기에서 대화 줄을 고르면 도구 줄이 두 줄로 넘치던 것, 목록 머리의 안 읽은 수가 1440 에서도 늘 잘리던 것 - 묶은 줄에 한 일의 알림은 "{메일함}에 있는 메일 N통을 …"(줄의 통 수는 대화 전체라서), 되돌리기 알림은 되돌린 메일함을 말한다 - 같은 대화 칸: 펼친 줄의 첨부 수 안내, 우리 원문 인용 머리부터 뺀 미리 보기, 보내기를 기다리는 메일은 줄 끝에 - 검색 준비 중 · 결과 없음 · 하루짜리 기간 · 내 이름 조건의 글, 예약 시각의 띄어쓰기, 이름 맞추기(대화로 묶기 · 내용에 든 말 · 표 머리 참여자) - 다른 탭 · PC 에서 바꾼 설정을 탭으로 돌아올 때 다시 받는다(60초 지났을 때) Co-Authored-By: Claude Opus 5.5 --- .../mail/ConversationMessagePanel.vue | 39 ++++- .../mail/MailConversationToggle.vue | 2 + .../src/components/mail/MailListFrame.vue | 1 + .../src/components/mail/MailListHeader.vue | 51 +++++-- .../src/components/mail/MailSearchBox.spec.ts | 7 + .../mail/MailSearchConditionLine.vue | 7 +- .../src/components/mail/MailSearchEmpty.vue | 9 +- .../mail/MailSelectionToolbar.spec.ts | 29 +++- .../components/mail/MailSelectionToolbar.vue | 18 ++- .../mail/MessageConversation.spec.ts | 70 +++++++++ .../src/components/mail/MessageReader.spec.ts | 2 +- .../mail/MessageReader.stage9.spec.ts | 4 +- frontend/src/components/mail/MessageTable.vue | 9 +- .../src/composables/useBulkMailActions.ts | 7 +- .../src/composables/useMailMoveFeedback.ts | 23 ++- .../useMailSettingsRefresh.spec.ts | 135 ++++++++++++++++++ .../src/composables/useMailSettingsRefresh.ts | 51 +++++++ .../pages/mail/ComposePage.sendlater.spec.ts | 4 +- frontend/src/pages/mail/MailLayout.vue | 8 +- .../pages/mail/MailboxPage.scheduled.spec.ts | 2 +- .../src/pages/mail/MailboxPage.search.spec.ts | 32 ++++- .../pages/mail/MailboxPage.threads.spec.ts | 96 ++++++++++++- .../pages/settings/MailSendReadPage.spec.ts | 6 +- .../src/pages/settings/MailSendReadPage.vue | 2 +- .../src/shared/constants/mail-conversation.ts | 15 ++ frontend/src/shared/constants/mail-search.ts | 16 ++- .../src/shared/constants/mail-send-later.ts | 8 +- .../src/shared/constants/mail-settings.ts | 16 ++- frontend/src/shared/constants/mail.ts | 23 ++- frontend/src/shared/utils/mail-bulk.spec.ts | 22 +++ frontend/src/shared/utils/mail-bulk.ts | 45 ++++-- .../utils/mail-conversation-list.spec.ts | 9 ++ .../shared/utils/mail-conversation-list.ts | 12 +- .../shared/utils/mail-conversation.spec.ts | 81 ++++++++++- .../src/shared/utils/mail-conversation.ts | 62 +++++++- frontend/src/shared/utils/mail-search.spec.ts | 22 +++ frontend/src/shared/utils/mail-search.ts | 38 ++++- .../src/stores/mail-settings.store.spec.ts | 90 +++++++++++- frontend/src/stores/mail-settings.store.ts | 44 ++++++ 39 files changed, 1024 insertions(+), 93 deletions(-) create mode 100644 frontend/src/composables/useMailSettingsRefresh.spec.ts create mode 100644 frontend/src/composables/useMailSettingsRefresh.ts diff --git a/frontend/src/components/mail/ConversationMessagePanel.vue b/frontend/src/components/mail/ConversationMessagePanel.vue index 35c1cf0..3fce3e9 100644 --- a/frontend/src/components/mail/ConversationMessagePanel.vue +++ b/frontend/src/components/mail/ConversationMessagePanel.vue @@ -2,7 +2,8 @@ // "같은 대화" 칸에서 펼친 줄의 안 (14단계 — 디자인 원본 Thread14 "하나 펼침") // 받는 사람 · 제목 한 줄씩, 본문(MailBodyFrame — 검색 쿼리 없이 받은 본문이라 강조가 없다), [답장] · [이 메일만 열기](36 — 밀도 예외 --size-notice-action 재사용). // - 본문이 다 보이지 않으면(서버의 isHtmlBodyOmitted · isBodyTruncated) 읽기 화면과 같은 안내 줄(MAIL_BODY_NOTICES)을 본문 위에 — 읽기 화면도 같은 본문이다. -// 외부 이미지 안내 · 첨부 목록은 두지 않는다 — [이 메일만 열기]로 연 읽기 화면에 있다 +// 외부 이미지 안내 · 첨부 목록은 두지 않는다 — [이 메일만 열기]로 연 읽기 화면에 있다. 첨부가 있으면 본문 아래 · [답장] 위에 +// "첨부 파일 N개는 [이 메일만 열기]를 누르면 볼 수 있습니다." (수는 읽기 화면의 "첨부 파일 N개"와 같다 — 본문 그림 빼고 · 대용량 첨부 더해) // - 본문을 받는 동안은 뼈대(aria-busy), 못 받으면 사전 문구 + [다시 시도]. 그사이 지워진 메일(RES_001)이면 [다시 시도] · [답장] · [이 메일만 열기] 없이 — // 스토어가 대화를 다시 받아 이 줄을 뺀다. 화면 낭독용 상태 영역은 늘 두고 글만 바꾼다 // - [답장]은 읽기 화면의 [답장]과 같은 쓰기 화면이고, 임시 저장본 · 예약한 메일에는 없다. [이 메일만 열기]는 그 메일을 읽기 화면으로 연다(mail-conversation.ts) — @@ -15,7 +16,9 @@ import MailBodyFrame from '@/components/mail/MailBodyFrame.vue' import { useMailPrint } from '@/composables/useMailPrint' import { MAIL_NO_SUBJECT } from '@/shared/constants/mail' import { MAIL_CONVERSATION_TEXT } from '@/shared/constants/mail-conversation' +import { formatCount } from '@/shared/utils/format' import { formatAddressList } from '@/shared/utils/mail-address' +import { listedAttachments } from '@/shared/utils/mail-attachment' import { mailBodyNotices } from '@/shared/utils/mail-body-notices' import type { ConversationRow } from '@/shared/utils/mail-conversation' import type { ConversationDetailState } from '@/stores/mail.store' @@ -47,6 +50,10 @@ const failure = computed(() => (props.detail?.status === 'error' ? props.detail. /** 그사이 지워진 메일 — 다시 해도 같고 그 메일로 갈 곳도 없다 */ const missing = computed(() => failure.value?.code === 'RES_001') const bodyNotices = computed(() => (ready.value ? mailBodyNotices(ready.value) : [])) +/** 읽기 화면에서 볼 수 있는 첨부 수 — 본문 그림(isInline)은 빼고 대용량 첨부는 더한다 (MessageReader 의 첨부 칸과 같은 수) */ +const attachmentCount = computed(() => + ready.value ? listedAttachments(ready.value.attachments).length + (ready.value.largeFiles?.length ?? 0) : 0, +) const bodyTitle = computed(() => MAIL_CONVERSATION_TEXT.bodyTitle(props.row.sender.name, props.row.time)) /** 본문 안에서 누른 키 — Ctrl+P(맥 ⌘P)면 브라우저 인쇄 대신 이 메일의 인쇄 화면 */ @@ -97,6 +104,18 @@ function onFrameKeydown(event: KeyboardEvent) { :title="bodyTitle" @frame-keydown="onFrameKeydown" /> +

+ + {{ MAIL_CONVERSATION_TEXT.attachmentsNote(formatCount(attachmentCount)) }} +

(), { +const props = withDefaults(defineProps(), { variant: 'split', }) @@ -37,18 +40,25 @@ const settingsStore = useMailSettingsStore() const scheduled = computed(() => isScheduledSource(mailStore.listSource)) const title = computed(() => (scheduled.value ? SEND_LATER_TEXT.mailbox : (mailStore.currentMailbox?.name ?? '메일함'))) +/** 메일함 목록의 개수 칸 — 묶었으면 대화 수(목록의 total), 아니면 메일함의 통 수. 예약됨 · 메일함을 모르면 null */ +const countInput = computed(() => { + const box = scheduled.value ? null : mailStore.currentMailbox + if (!box) return null + return mailStore.isConversationList + ? { grouped: true, total: mailStore.pageInfo?.total ?? null, unread: box.unreadCount } + : { grouped: false, total: box.totalCount, unread: box.unreadCount } +}) const countText = computed(() => { if (scheduled.value) { if (mailStore.listStatus !== 'ready') return '' return SEND_LATER_TEXT.listCount(formatCount(mailStore.pageInfo?.total ?? mailStore.messages.length)) } - const box = mailStore.currentMailbox - if (!box) return '' - if (mailStore.isConversationList) { - return listHeadCountText({ grouped: true, total: mailStore.pageInfo?.total ?? null, unread: box.unreadCount }) - } - return listHeadCountText({ grouped: false, total: box.totalCount, unread: box.unreadCount }) + return countInput.value ? listHeadCountText(countInput.value) : '' }) +/** 보이는 개수 — 분할 보기(좁은 머리)의 메일함 목록은 줄인 글(안 읽은 메일이 있으면 그 수만), 그 밖은 전체 글 */ +const shownCountText = computed(() => + props.variant === 'split' && countInput.value ? listHeadShortCountText(countInput.value) : countText.value, +) /** [대화로 묶기] — 메일함 목록에서만, 설정을 받았거나 받지 못했을 때 (14단계 — 받는 중 · 받기 전에는 숨긴다) */ const groupable = computed( () => @@ -72,11 +82,19 @@ const emptiable = computed(() => { > {{ title }} - + {{ countText }} + > + + +

{ padding: var(--space-4) var(--space-8) var(--space-4) var(--space-20); } +/* 분할 보기의 목록 칸(384) — 개수 글이 들 자리를 내려고 칸 사이를 조금 좁힌다 (머리말) */ +.list-header--split { + gap: var(--space-4); +} + +.list-header--split .list-header__heading { + gap: var(--space-6); +} + +.list-header--split .list-header__end { + gap: var(--space-4); +} + /* * 이름 · 개수 — 오른쪽 단추들이 남긴 자리를 쓴다. 좁으면 개수가 먼저 줄고(사라질 때까지) 그다음에 이름이 준다 * (14단계 — [대화로 묶기]가 더해져 분할 보기 칸에서 이름이 1px 모자라 "색인 …"으로 잘렸다) diff --git a/frontend/src/components/mail/MailSearchBox.spec.ts b/frontend/src/components/mail/MailSearchBox.spec.ts index 0ad4bb8..332f31d 100644 --- a/frontend/src/components/mail/MailSearchBox.spec.ts +++ b/frontend/src/components/mail/MailSearchBox.spec.ts @@ -248,6 +248,13 @@ describe('MailSearchBox — [조건] · 「조건으로 찾기」 (14단계 디 expect(panel.text()).toContain( '검색 칸의 말과 이 조건에 모두 맞는 메일을 찾습니다. 받는 사람에는 참조로 받은 사람도 들어갑니다.', ) + // 칸 안내 — 내용 칸은 칸 이름과 같은 말로 (2026-11-06 실사용 시험) + expect(['from', 'to', 'subject', 'body'].map((name) => field(wrapper, name).attributes('placeholder'))).toEqual([ + '이름이나 메일 주소', + '이름이나 메일 주소', + '제목에 든 말', + '내용에 든 말', + ]) expect(document.activeElement).toBe(field(wrapper, 'from').element) }) diff --git a/frontend/src/components/mail/MailSearchConditionLine.vue b/frontend/src/components/mail/MailSearchConditionLine.vue index a29ab51..1562e9e 100644 --- a/frontend/src/components/mail/MailSearchConditionLine.vue +++ b/frontend/src/components/mail/MailSearchConditionLine.vue @@ -1,7 +1,8 @@