diff --git a/frontend/src/components/mail/ConversationListItem.vue b/frontend/src/components/mail/ConversationListItem.vue new file mode 100644 index 0000000..8dcdaed --- /dev/null +++ b/frontend/src/components/mail/ConversationListItem.vue @@ -0,0 +1,333 @@ + + + + + diff --git a/frontend/src/components/mail/ConversationTableRow.vue b/frontend/src/components/mail/ConversationTableRow.vue new file mode 100644 index 0000000..1c45e3c --- /dev/null +++ b/frontend/src/components/mail/ConversationTableRow.vue @@ -0,0 +1,287 @@ + + + + + diff --git a/frontend/src/components/mail/ListReadNav.vue b/frontend/src/components/mail/ListReadNav.vue index 599abe3..0ef43bb 100644 --- a/frontend/src/components/mail/ListReadNav.vue +++ b/frontend/src/components/mail/ListReadNav.vue @@ -2,6 +2,8 @@ // 목록 보기에서 연 메일의 이동 도구 — 목록으로 · 이전/다음 · 위치 "n / 전체" (디자인 원본 MainListRead) // 본문 툴바의 앞쪽에 들어간다 (MessageReader 의 toolbar-start 슬롯). // 목록은 메일함 또는 검색 결과다 — 검색 결과에서 연 메일이면 "목록으로"는 결과로, 이전/다음은 결과 안에서 옮긴다. +// 대화로 묶은 메일함(14단계)은 줄(대화) 사이를 옮긴다 — 이전/다음은 앞뒤 줄의 마지막 메일, 위치는 "n번째 대화 / 대화 수". +// 연 메일이 줄의 마지막 메일이 아니어도([이 메일만 열기]로 연 더 오래된 메일) 그 메일이 든 줄을 기준으로 한다 import { computed, ref } from 'vue' import { useRouter } from 'vue-router' @@ -32,16 +34,18 @@ const listHref = computed(() => { const source = mailStore.listSource return source ? router.resolve(listLocationOf(source)).href : router.resolve({ name: 'mail' }).href }) -/** 불러온 목록에서의 위치 — 목록 밖의 메일(주소로 바로 연 경우 등)이면 -1 */ -const index = computed(() => mailStore.messages.findIndex((message) => message.id === props.messageId)) +/** 불러온 목록에서의 위치(줄) — 목록 밖의 메일(주소로 바로 연 경우 등)이면 -1 */ +const index = computed(() => mailStore.rowIndexOfMessage(props.messageId)) /** * 전체 — 서버가 준 total, 없으면 메일함 개수(메일함 목록일 때만), 그것도 없으면 불러온 수. - * 검색 결과의 total 이 null 이면(1,000통 넘음 · 다 세지 못함 — 14단계) 모른다 — 위치 "n / 전체"를 보이지 않는다 + * 검색 결과의 total 이 null 이면(1,000통 넘음 · 다 세지 못함 — 14단계) 모른다 — 위치 "n / 전체"를 보이지 않는다. + * 대화 목록은 대화 수 — 모르면 다 받았을 때만 불러온 줄 수 (메일함의 통 수는 대화 수가 아니다) */ const total = computed(() => { const known = mailStore.pageInfo?.total ?? null if (known !== null) return known if (mailStore.search) return null + if (mailStore.isConversationList) return mailStore.hasMore ? null : mailStore.rowCount return mailStore.currentMailbox?.totalCount ?? mailStore.messages.length }) const position = computed(() => @@ -51,18 +55,17 @@ const hasPrev = computed(() => index.value > 0) /** 불러온 범위의 끝이어도 서버에 더 있거나(hasNext) 뒤에서 이어 받는 중이면 다음으로 갈 수 있다 */ const hasNext = computed( () => - index.value !== -1 && - (index.value < mailStore.messages.length - 1 || mailStore.hasMore || mailStore.isLoadingMore), + index.value !== -1 && (index.value < mailStore.rowCount - 1 || mailStore.hasMore || mailStore.isLoadingMore), ) /** 다음 메일을 받아야 하는데 목록을 보이는 채로 새로 고치는 중 — 끝날 때까지 [다음]을 막는다 (옛 커서로 붙이지 않게) */ const nextWaiting = computed( - () => index.value !== -1 && !mailStore.messages[index.value + 1] && mailStore.isRefreshing, + () => index.value !== -1 && mailStore.rowMessageIdAt(index.value + 1) === null && mailStore.isRefreshing, ) async function goPrev() { - const previous = mailStore.messages[index.value - 1] + const previous = mailStore.rowMessageIdAt(index.value - 1) const source = mailStore.listSource - if (previous && source) await showSiblingIn(source, previous.id) + if (previous && source) await showSiblingIn(source, previous) } /** @@ -72,19 +75,19 @@ async function goPrev() { async function goNext() { if (loadingNext.value || nextWaiting.value || index.value === -1) return const current = index.value - if (!mailStore.messages[current + 1] && (mailStore.hasMore || mailStore.isLoadingMore)) { + if (mailStore.rowMessageIdAt(current + 1) === null && (mailStore.hasMore || mailStore.isLoadingMore)) { loadingNext.value = true try { const waited = mailStore.isLoadingMore await mailStore.loadMore() - if (waited && !mailStore.messages[current + 1] && mailStore.hasMore) await mailStore.loadMore() + if (waited && mailStore.rowMessageIdAt(current + 1) === null && mailStore.hasMore) await mailStore.loadMore() } finally { loadingNext.value = false } } - const next = mailStore.messages[current + 1] + const next = mailStore.rowMessageIdAt(current + 1) const source = mailStore.listSource - if (next && source) await showSiblingIn(source, next.id) + if (next && source) await showSiblingIn(source, next) } /** 목록으로 — 불러온 페이지·스크롤 위치는 그대로, 초점은 보던 메일의 행으로 돌려준다 */ @@ -94,7 +97,8 @@ function onBackToList(event: MouseEvent) { event.preventDefault() const source = mailStore.listSource if (!source) return - viewStore.setReturnFocus(props.messageId) + // 대화 목록은 줄의 링크가 마지막 메일을 가리킨다 — 그 줄로 초점을 돌려준다 + viewStore.setReturnFocus(mailStore.rowMessageIdAt(index.value) ?? props.messageId) void backToSourceList(source) } diff --git a/frontend/src/components/mail/MailConversationToggle.vue b/frontend/src/components/mail/MailConversationToggle.vue new file mode 100644 index 0000000..8c17917 --- /dev/null +++ b/frontend/src/components/mail/MailConversationToggle.vue @@ -0,0 +1,80 @@ + + + + + diff --git a/frontend/src/components/mail/MailListFrame.vue b/frontend/src/components/mail/MailListFrame.vue index 024affb..1646e7b 100644 --- a/frontend/src/components/mail/MailListFrame.vue +++ b/frontend/src/components/mail/MailListFrame.vue @@ -8,6 +8,7 @@ // 결과 없음은 더 찾을 것이 없을 때만 — 이어 찾을 것이 남았는데 아직 하나도 못 찾았으면 [더 찾기](MailSearchMore) // - Esc 는 고른 것을 푼다 — 이 칸 안의 키만 받는다 (전역 단축키가 아니다 · 메뉴 · 대화상자 안의 Esc 는 그쪽 것이다) // - 목록 보기는 메일을 열면 이 칸이 사라진다 — 스크롤을 기억해 "목록으로" 돌아오면 되살리고 보던 메일로 초점을 돌려준다 +// - 대화로 묶은 메일함(14단계)도 같은 틀이다 — 줄 수 · 빈 상태는 메일 목록과 같은 규칙(줄이 대화일 뿐) import { computed, nextTick, ref, watch } from 'vue' import AppIcon from '@/components/AppIcon.vue' @@ -75,7 +76,7 @@ const retentionNotice = computed(() => { const role = mailbox.value?.role ?? null const text = role ? (MAILBOX_RETENTION_NOTICES[role] ?? null) : null if (text === null) return null - return mailStore.listStatus === 'ready' && mailStore.messages.length === 0 ? null : text + return mailStore.listStatus === 'ready' && mailStore.rowCount === 0 ? null : text }) /** 검색 조건 — 조건 줄은 걸렸을 때만 (14단계) */ const searchConditions = computed(() => @@ -97,15 +98,17 @@ const singleChar = computed( const loadingMore = computed(() => mailStore.isLoadingMore || mailStore.isRefreshing) /** 결과가 아직 없지만 이어 찾을 것이 남았다 — 결과 없음 대신 [더 찾기] */ const searchPending = computed( - () => Boolean(search.value) && mailStore.listStatus === 'ready' && mailStore.messages.length === 0 && mailStore.hasMore, + () => Boolean(search.value) && mailStore.listStatus === 'ready' && mailStore.rowCount === 0 && mailStore.hasMore, ) /** 없는 주소(RES_001)일 때 — 메일함은 받은편지함으로, 없는 메일함 안의 검색은 전체 메일함에서 */ const leaveLabel = computed(() => (search.value ? MAIL_SEARCH_TEXT.searchAll : '받은편지함으로')) function retry() { const current = search.value + const source = mailStore.listSource if (current) void mailStore.openSearch(current.q, current.scope, { force: true, conditions: current.conditions }) else if (scheduled.value) void mailStore.openScheduled() + else if (source?.kind === 'conversations') void mailStore.openConversations(source.mailboxId) else if (mailStore.currentMailboxId) void mailStore.openMailbox(mailStore.currentMailboxId) } @@ -240,7 +243,7 @@ watch( @leave="leave" /> -