generated from ttipo/project-base
docs: 14단계 목록 대화로 묶어 보기의 규칙과 구현하며 정한 보완을 적는다
계약 2절 "대화 — 14단계"에 묶은 목록의 클라이언트 줄(첫 진입 짐작 · 맞추기 · 할 일 메일 · 줄의 별 · 옮기기 뒤 다시 받기 · 알려진 한계), 디자인 README 에 "14단계 구현하며 정한 보완 — 목록 대화로 묶어 보기", vue3 스킬에 골격 줄 · 밀도 예외 · 줄 기준 도우미 규칙을 적는다. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -37,7 +37,7 @@ description: Vue 3 프론트엔드(frontend/) 개발 규칙 — Composition API
|
||||
| 외부 HTML 격리 렌더링 | `src/components/mail/MailBodyFrame.vue` · `src/shared/utils/mail-html.ts` | 메일 본문 등 외부 HTML 은 이 컴포넌트로만 그린다 (sandbox·내부 CSP 근거는 파일 머리말). 보여 줄 HTML 고르기는 `displayedHtmlBody()` 하나다 — null 이거나 공백뿐이면 textBody (읽기 화면·인용 공용, 서버의 `isBodyTruncated` 와 같은 규칙) |
|
||||
| 메일 모듈 | `src/pages/mail/*` · `src/components/mail/*` | API 계약 `docs/mail/api-contract.md`, 디자인 원본 `docs/design/webmail/` |
|
||||
| 콘텐츠 열 토큰 | `src/styles/tokens.css` 의 `--content-*` · `--content-toolbar-start` | 읽기·쓰기 화면이 같은 최대 폭·여백·32px 시작선을 쓴다 (새 화면도 이 토큰으로) |
|
||||
| 밀도 예외 크기 | `src/styles/tokens.css` 의 `--size-compact-control` · `--size-segment-item` · `--size-segment-item-lg` · `--size-editor-tool` · `--size-editor-menu-item` · `--size-editor-swatch-cell` · `--size-editor-table-cell` · `--table-col-check` · `--size-row-check` · `--size-search-clear-width`/`-height` · `--size-notice-bar` · `--size-mailbox-more` · `--size-notice-action` · `--size-filter-control` · `--size-filter-remove` (주석에 "밀도 예외") | 클릭 영역 44px 기준의 의도된 예외(목록 표의 별·보기 전환, 설정 화면의 미리 보기 전환 36(`.segmented`)·서명 그림 크기 줄 36(작게·보통·원본·그림 빼기), 편집기 툴바 버튼 40×44·메뉴 항목 40·색 견본 40·표 고르기 칸 22, 3단계의 목록 표 고르기 칸 32·분할 행 고르기 칸 폭 36·검색 칸 ✕ 40×38·새 메일 알림 줄 40·검색 범위 전환 36·이동 메뉴 항목 40, 4단계의 메일함 행 ⋯ 32×40 · ⋯ 메뉴 항목 40, 5단계의 목록 위 알림 권하기 줄 40 · 줄 안의 단추 36, 6단계의 필터 조건 칸 40 · 조건 ✕ 36 · 필터 행 ▲▼ 32×40 · 켜기 스위치 누르는 곳 40×44 · 고치기 창 안 [다시 불러오기] 36, 8단계의 패널 그룹 줄 ⋯ 32×40 · 연락처 창의 줄 · 칸 40 · 줄 ✕ 36×40 · 구성원 넣기 창 전환 36 · 찾기 칸 40 — 새 토큰 없이 위 것을 다시 쓴다, 9단계의 읽기 화면 안내 줄 단추([이미지 보기] · [차단 풀기] · [고치기] · [예약 취소]) · 차단한 주소 줄의 [차단 풀기] 36(`--size-notice-action`) — 역시 새 토큰 없음, 14단계의 검색 칸 [조건] 32(`--size-compact-control`) · 조건 패널의 칸 · 단추 40(`--size-filter-control`) · 조건 줄 40 · 줄 안 [조건 지우기] 36 · 같은 대화 펼친 줄의 [답장] · [이 메일만 열기] 36(`--size-notice-action`) — 역시 새 토큰 없음, tokens.css 주석)에만 쓴다 — 사용자 확정(캔버스). 그 밖의 `--size-editor-*`(툴바 높이·글꼴 단추·본문 최소 높이 등)는 예외가 아니라 보통 크기다 |
|
||||
| 밀도 예외 크기 | `src/styles/tokens.css` 의 `--size-compact-control` · `--size-segment-item` · `--size-segment-item-lg` · `--size-editor-tool` · `--size-editor-menu-item` · `--size-editor-swatch-cell` · `--size-editor-table-cell` · `--table-col-check` · `--size-row-check` · `--size-search-clear-width`/`-height` · `--size-notice-bar` · `--size-mailbox-more` · `--size-notice-action` · `--size-filter-control` · `--size-filter-remove` (주석에 "밀도 예외") | 클릭 영역 44px 기준의 의도된 예외(목록 표의 별·보기 전환, 설정 화면의 미리 보기 전환 36(`.segmented`)·서명 그림 크기 줄 36(작게·보통·원본·그림 빼기), 편집기 툴바 버튼 40×44·메뉴 항목 40·색 견본 40·표 고르기 칸 22, 3단계의 목록 표 고르기 칸 32·분할 행 고르기 칸 폭 36·검색 칸 ✕ 40×38·새 메일 알림 줄 40·검색 범위 전환 36·이동 메뉴 항목 40, 4단계의 메일함 행 ⋯ 32×40 · ⋯ 메뉴 항목 40, 5단계의 목록 위 알림 권하기 줄 40 · 줄 안의 단추 36, 6단계의 필터 조건 칸 40 · 조건 ✕ 36 · 필터 행 ▲▼ 32×40 · 켜기 스위치 누르는 곳 40×44 · 고치기 창 안 [다시 불러오기] 36, 8단계의 패널 그룹 줄 ⋯ 32×40 · 연락처 창의 줄 · 칸 40 · 줄 ✕ 36×40 · 구성원 넣기 창 전환 36 · 찾기 칸 40 — 새 토큰 없이 위 것을 다시 쓴다, 9단계의 읽기 화면 안내 줄 단추([이미지 보기] · [차단 풀기] · [고치기] · [예약 취소]) · 차단한 주소 줄의 [차단 풀기] 36(`--size-notice-action`) — 역시 새 토큰 없음, 14단계의 검색 칸 [조건] 32(`--size-compact-control`) · 조건 패널의 칸 · 단추 40(`--size-filter-control`) · 조건 줄 40 · 줄 안 [조건 지우기] 36 · 같은 대화 펼친 줄의 [답장] · [이 메일만 열기] 36 · 목록 머리 [대화로 묶기] 36(`--size-notice-action`) — 역시 새 토큰 없음, tokens.css 주석)에만 쓴다 — 사용자 확정(캔버스). 그 밖의 `--size-editor-*`(툴바 높이·글꼴 단추·본문 최소 높이 등)는 예외가 아니라 보통 크기다 |
|
||||
| 사용자별 화면 설정 | `src/stores/mail-view.store.ts` | localStorage(키에 사용자 email, try/catch, 실패 시 기본값). 서버 설정 API 가 생기면 교체 |
|
||||
| 화면 이동 규칙 | `src/composables/useMailNavigation.ts` | 이전/다음은 replace, "목록으로"는 뒤로 가기 또는 replace (기록이 쌓이지 않게) |
|
||||
| 발송 키 규칙 | `src/shared/utils/idempotency-key.ts` 의 `shouldRenewSendKey()` | `Idempotency-Key` 는 성공과 `MAIL_005` 때만 바꾼다 — 그 밖의 실패 뒤 다시 보내기는 같은 키 (계약 2절 "발송 본문"). UUID 만들기는 `shared/utils/uuid.ts` 하나(필터 id 와 공용 — `crypto.randomUUID` 가 없으면 대체 경로) |
|
||||
@@ -46,7 +46,7 @@ description: Vue 3 프론트엔드(frontend/) 개발 규칙 — Composition API
|
||||
| 임시 저장 | `src/composables/useDraftSaving.ts` · `components/mail/ComposeDraftStatus.vue` · `composables/useComposeDraftLink.ts`(주소 맞추기 · 옛 id 기록 · 보내지 못한 메일을 저장본으로) · `composables/useDraftTrash.ts`(휴지통 · 되돌리기) | 3초 멈춤 · 쉬지 않고 쓰면 30초 · 10초 간격 · 한 번에 하나(계약 2절 "임시 저장", 상수 `shared/constants/mail.ts` 의 `DRAFT_*`). `MAIL_008`·PUT 404 는 막지 않고 POST 로 새로. 찾지 못한 첨부가 남았거나 본문이 한도를 넘는 동안은 저장하지 않고, 실을 수 없는 첨부가 든 남의 저장본은 첫 저장을 POST 로(원본 보존). 보내기·버리기 전에는 `suspend`+`settle` — 보내기는 settle 뒤 첨부 상태를 다시 본다. 저장할 때마다 주소를 `?draft=<새 id>` 로 맞추고(떠나기 전 저장 제외), 옛 id 로 돌아오면 `mail.store` 의 `latestDraftId()` 로 마지막 저장본을 연다. 쓰기 화면 휴지통은 DELETE 가 아니라 PATCH(휴지통으로 이동) — 이미 휴지통인 메일은 DELETE 가 영구 삭제한다 |
|
||||
| 받는 사람 자동 완성 | `src/composables/useRecipientSuggestions.ts` · `shared/utils/recipient-match.ts` · `components/mail/RecipientField.vue` · `RecipientSuggestionList.vue` | 검색어는 input · compositionupdate 의 `target.value`(한글 조합 중 글자 포함)로 따로 두고, 칩 확정은 v-model. Enter 등의 조합 판정은 `shared/utils/keyboard-shortcut.ts` 의 `isImeComposing()`(`isComposing` 또는 keyCode 229 — Safari · macOS) — 새 입력 칸의 Enter 처리도 이것을 쓴다. 쓰기 화면을 열 때 `limit=100` 한 번, 모자랄 때만 250ms 뒤 `q` 로(8단계부터 초성 · 조합 중 글자도 묻는다 — 서버의 q 가 같은 맞추기다: 짝 backend `modules/mail/recipient-match.ts`, 같은 표본 표 `test-utils/recipient-match-samples.ts` — 한쪽을 고치면 둘 다. 늦은 응답은 버린다). 초성 검색·맞은 글자 강조는 `recipient-match.ts`(강조는 `<strong>` 조각 — v-html 없음). combobox 접근성(aria-activedescendant · listbox · 조합 중 Enter 무시), 완전한 주소면 미리 고르지 않는다 |
|
||||
| 메일 서명 | `src/stores/mail-signature.store.ts` · `components/mail/editor/signature/` · `shared/utils/mail-signature.ts` · `pages/settings/` | 서명은 메일 화면에 들어올 때 받아 두고 쓰기 화면이 넣는다(새 메일은 끝, 답장·전달은 인용 위 — 이어 쓰기는 다시 넣지 않는다). 설정 화면(`SettingsLayout.vue` 왼쪽 메뉴 — 묶음 · 항목은 `shared/constants/settings-menu.ts` + `MailSignaturePage.vue`)은 들어올 때마다 새로 받는다. 미리 보기는 편집기 스키마의 `DOMSerializer` 로 그린다. 그림 3장은 서명 칸 안의 그림 수로(서버 보관 상한 10장은 서버가 센다) |
|
||||
| 메일 스토어 구성 | `src/stores/mail.store.ts`(조립 · 공개 API) + `src/stores/mail/` — `mailboxes.ts` · `list.ts` · `selection.ts` · `message.ts` · `bulk.ts` · `mailbox-admin.ts`(4단계 메일함 관리) · `conversation.ts`(14단계 같은 대화 칸) | Pinia 스토어는 `useMailStore` 하나이고 기능별 setup 모듈을 조립한다(스토어를 여러 개로 나누지 않는다 — 서로 부르는 고리가 생긴다). 목록 출처는 `shared/utils/mail-list-source.ts`(메일함 · 검색). **늦게 온 응답 버리기**: 목록 맞추기는 이 탭의 목록 변경 세대(`localGeneration` — 읽음 · 중요 · 빼기 · 옮기기마다 오른다)가 그사이 바뀌었으면 응답을 버리고 한 번 더 맞춘다. 여러 통 처리 · 한 통 삭제는 시작할 때의 출처 순번(`sourceSeq`)과 다르면 목록을 건드리지 않고 개수만 맞춘다 |
|
||||
| 메일 스토어 구성 | `src/stores/mail.store.ts`(조립 · 공개 API) + `src/stores/mail/` — `mailboxes.ts` · `list.ts` · `selection.ts` · `message.ts` · `bulk.ts` · `mailbox-admin.ts`(4단계 메일함 관리) · `conversation.ts`(14단계 같은 대화 칸) | Pinia 스토어는 `useMailStore` 하나이고 기능별 setup 모듈을 조립한다(스토어를 여러 개로 나누지 않는다 — 서로 부르는 고리가 생긴다). 목록 출처는 `shared/utils/mail-list-source.ts`(메일함 · 검색 · 대화로 묶은 메일함 `conversations` — 14단계). **목록에 든 메일인지 볼 때는 `messages` 대신 줄 기준 도우미**(`rowCount` · `rowIndexOfMessage` · `rowMessageIdAt` · `isListedMessage` · `listedSummaries`)를 쓴다 — 대화 목록에서는 `messages` 가 비어 있다. 목록 요청은 출처가 바뀌면 `listAbort`(AbortController)로 끊는다(끊긴 요청은 알리지 않는다). **늦게 온 응답 버리기**: 목록 맞추기는 이 탭의 목록 변경 세대(`localGeneration` — 읽음 · 중요 · 빼기 · 옮기기마다 오른다)가 그사이 바뀌었으면 응답을 버리고 한 번 더 맞춘다. 여러 통 처리 · 한 통 삭제는 시작할 때의 출처 순번(`sourceSeq`)과 다르면 목록을 건드리지 않고 개수만 맞춘다 |
|
||||
| 새 메일 확인 · 탭 제목 | `src/composables/useMailStatePolling.ts`(메일 화면 `MailLayout` 이 시작) · `useListArrivalNotice.ts` + `components/mail/MailNewNotice.vue` · `useMailTabTitle.ts` + `shared/utils/document-title.ts` | 화면이 보이는 동안 60초마다 `GET /mail-state`, 탭으로 돌아오면 10초 지났을 때 바로, 한 번에 하나, 실패는 `silent`. 바뀌면 `syncCounts()` + 보이는 목록 맞추기(불러온 만큼 · 최대 100통, 검색 결과는 건너뜀). 목록을 내려 본 채면 "새 메일 N통 · 맨 위로" 줄. 받은편지함 안 읽음이 있으면 탭 제목 앞 `(N) ` (메일 라우트에서만) — 계약 2절 "새 메일 확인" |
|
||||
| 실시간 새 메일 (5단계) | `src/composables/useMailRealtime.ts`(`App.vue` 가 시작 — 로그인한 동안 앱 전체) · `stores/mail-realtime.store.ts` · `shared/utils/sse-parser.ts` · `mail-events.ts` · `mail-realtime.ts` · `browser-support.ts` | **브라우저마다 연결 하나** — Web Locks 잠금(이름에 사용자 주소)을 받은 탭만 `GET /api/mail-events` 를 읽고 BroadcastChannel 로 나눈다(메시지에 사용자 주소, 다른 계정 것은 버린다). `fetch` 로 읽는다 — `EventSource` 는 401 을 몰라 쓰지 않는다. 75초 무소식 감시는 요청 시작부터, 401 → 세션 갱신 한 번(그 바로 뒤의 시도만 "갱신 직후"), 다시 잇기 2 · 5 · 15 · 30 · 60초(±20%) — 간격은 연결이 **자리 잡은 뒤**(정상 ping 또는 20초)에만 처음부터 다시 세고, 자리 잡기 전의 `end` 는 실패처럼 기다린다(열리자마자 끝나는 연결에서 갱신 폭주를 막는다). 메일 화면(`useMailStatePolling`)은 이 소식을 받아 연결이 살아 있는 동안 1분 확인을 멈추고, `state` 가 바뀌면 보이는 탭은 맞추기(1초 모아서 — `MAIL_REALTIME_SYNC_DELAY_MS`) · 숨은 탭은 `inboxUnread` 로 탭 제목만(`setInboxUnread`) — 계약 2절 "실시간 알림" |
|
||||
| 바탕화면 알림 (5단계) | `src/composables/useNewMailNotifier.ts` · `useDesktopNotification.ts` · `stores/mail-notify.store.ts`(브라우저 · 사용자마다 localStorage) · `shared/utils/desktop-notify.ts` · 목록 위 권하기 `components/mail/MailNotifyOffer.vue` · 설정 `pages/settings/MailNotifyPage.vue`(+ `components/settings/NotifySettingsForm.vue` · `RealtimeLinkStatus.vue`) · 아이콘 `assets/brand/notification-icon.png` · 문구 `shared/constants/mail-notify.ts` | 연결한 탭이 `new-mail` 을 받고 설정이 켜져 있고 허락됐고 **웹메일을 보고 있는 탭이 없을 때만**(보이고 초점이 있음 — `visibilityState === 'visible' && document.hasFocus()`, 다른 탭에 짧게 묻는다) 띄운다 — tag 하나로 앞 알림을 바꿔 쓰고, 사용자가 돌아오면 닫는다. 허락을 묻는 동안 단추는 `disabled` 가 아니라 `aria-disabled`(초점을 잃지 않게). 허락은 사용자가 누른 뒤에만(`Notification.requestPermission()`). 누르면 그 메일로(`useMailNavigation` 의 `openFromNotification` — 세션이 바뀌었으면 이동하지 않는다) |
|
||||
@@ -54,6 +54,7 @@ description: Vue 3 프론트엔드(frontend/) 개발 규칙 — Composition API
|
||||
| 메일 검색 | `src/components/mail/MailSearchBox.vue`(상단 바의 search 슬롯) · `composables/useMailSearchNavigation.ts` · `MailSearchHeader.vue` · `MailSearchScope.vue` · `MailSearchEmpty.vue` · 라우트 `/mail/search` · `/mail/search/:messageId` | Enter 로만 찾는다(`isImeComposing()` 이면 무시), ✕ · Esc 는 검색을 시작한 메일함으로. 주소 `?q=…&in=<메일함 id \| all>` — 검색 화면 안의 다시 찾기 · 범위 바꾸기는 replace. 서버와 같은 한도(200자 · 낱말 8개)를 먼저 본다. 조사 처리 · 한계는 서버 몫(계약 2절 "검색") |
|
||||
| 메일 검색 — 14단계 (조건 · 강조 · 준비 중) | `src/components/mail/MailSearchConditions.vue`(조건 패널 — 칸 `MailSearchConditionFields.vue` · 기간 `MailSearchPeriodField.vue` · [조건] `MailSearchConditionsToggle.vue`, 고치는 값 `composables/useSearchConditionsDraft.ts` · 열고 닫기 `useSearchConditionsPopup.ts`) · `MailSearchConditionLine.vue`(조건 줄) · `MailSearchNotices.vue`(준비 중 · 한 글자 안내) · `MailSearchMore.vue`([더 찾기]) · `HighlightedText.vue` · `shared/utils/mail-search.ts`(조건 ↔ 주소 ↔ API · 기간 · 조건 줄 글) · `search-highlight.ts` · `search-match.ts` · `shared/constants/mail-search.ts` | 조건 이름 · 화면 주소(`since` · `until` 은 브라우저 시간대 날짜 · 양 끝 포함, 첨부 `attachment=1`) · 이어 찾기(`MAIL_SEARCH_FILL_MAX_REQUESTS` 5번)는 계약 2절 "검색 — 14단계"의 클라이언트 줄. **`search-match.ts` 는 backend `shared/utils/search-query.ts` · `search-text.ts` · `subject-base.ts` 와 짝** — 같은 표본 표 `test-utils/search-text-samples.ts`(backend `test/search-text-samples.ts` 와 행 · 차례까지 같다, 보이지 않는 글자는 `\u` 로 적는다 — 한쪽을 고치면 둘 다). 맞은 말은 `HighlightedText` 가 글 조각 + `<mark>` 로 그린다(v-html 없음 — 위치가 글 밖 · 거꾸로 · 정수가 아니면 버리고, 서로게이트 쌍을 가르지 않는다). **강조가 든 응답은 보이기만 한다** — `openMessage(id, highlight)` 는 강조 쿼리가 다르면 다시 받고, 답장 · 전달 · 인쇄 · 예약 · 대용량 첨부 다시 받기는 강조 쿼리 없이 받는다. 손으로 고친 주소도 패널과 같은 검사(8낱말 넘으면 묻지 않고 "찾을 말이 너무 많습니다"). 강조 노랑은 `--color-search-mark` — 본문 틀(`mail-html.ts`)의 같은 값과 짝 |
|
||||
| 같은 대화 (14단계) | `src/components/mail/MessageConversation.vue`(읽기 화면 첨부 아래 칸) · `ConversationMessageRow.vue`(줄) · `ConversationMessagePanel.vue`(펼친 안) · `stores/mail/conversation.ts` · `composables/useConversationSync.ts`(언제 받을지 — 시계 주입) · `shared/utils/mail-conversation.ts`(이름 자리 · 쪽 맞추기 · 갈 곳) · `shared/constants/mail-conversation.ts` · `shared/utils/format.ts` 의 `formatConversationTime` | 받기 · 다시 받기는 계약 2절 "대화 — 14단계"의 클라이언트 줄(100통 · 이어 묻기 5번 · [더 보기], 실패는 칸 안 안내 — 404 면 칸 없음). **다시 받기 판정은 `mail.store` 의 `mailboxTotalsKey`**(메일함마다 `id:totalCount` — 바뀔 때만 `requestConversationRefresh()`, MailLayout 의 바뀜 소식에서는 부르지 않는다 — 읽음 · 중요 표시만 바뀐 소식으로 대화를 다시 받지 않게), 간격은 받기가 끝난 때(`conversationBusy` 가 꺼질 때)부터 10초. 펼친 본문은 `MailBodyFrame` · 강조 쿼리 없이, 안 읽은 메일은 `markRead`(`Promise<boolean>` — 같은 목록의 `pendingFlags` 에 같은 읽음이 있으면 건너뛴다)로. `closeMessage` 가 칸도 비운다. 초점은 누른 단추가 사라지면 줄의 펼침 단추 → 새 [다시 시도] → 칸 h3(`tabindex="-1"`) → 칸이 없어지면 읽기 화면에 `settled`. 상태 영역은 늘 두고 글만 바꾼다(다시 실패해도 읽게). [이 메일만 열기]는 분할 보기 push · 목록 보기 replace(layout prop) |
|
||||
| 대화로 묶어 보기 (14단계) | `src/components/mail/MailConversationToggle.vue`(목록 머리 [대화로 묶기]) · `ConversationListItem.vue`(분할 보기 줄) · `ConversationTableRow.vue`(목록 보기 표 줄) · `composables/useConversationRows.ts` · `shared/utils/mail-conversation-list.ts`(응답 맞추기 · 할 일 id · 참여자 · 머리 글 · 표시 적용 · 맞추기 병합 · 삭제 뒤 갈 곳) · `stores/mail/list.ts`(출처 `conversations`) · 설정 `stores/mail-settings.store.ts` 의 `groupByConversation`(+ 설정 › 보내기 · 읽기 한 줄) · 글 `shared/constants/mail-conversation.ts` 의 `MAIL_CONVERSATION_LIST_TEXT` | 받기 · 맞추기 · 줄에 하는 일은 계약 2절 "대화 — 14단계"의 클라이언트 줄(20줄 · 맞추기는 불러온 줄 수만큼 최대 100줄 `silent` · 할 일 = `messageIds` − 붙잡힌 메일 · 줄 전체 표시는 모두 바꿨을 때만 · 옮기기 · 삭제 뒤 한 번 다시 받기). **첫 진입 짐작**은 localStorage `groupByConversationHintKey(주소)` — 설정을 받기 전 · 받지 못했으면 이 값, 받으면 서버 값, 읽기 · 쓰기가 던져도 끔. 로그아웃 · 세션 만료에 설정 스토어를 비워도 화면이 보는 값은 그대로 둔다(뒤집히면 watcher 가 떠나는 화면의 목록을 다시 부른다 — 2026-11-06 리뷰). **목록을 부르는 watcher 는 `authStore.canUseApp` 이 false 면 부르지 않는다**(소스에도 넣는다). 붙잡힌 메일이 든 줄은 고르기 칸 · 별이 `aria-disabled` + 까닭(`SEND_LATER_TEXT.heldNotSelectable`). 목록 줄의 화면 낭독 글("안 읽음, " · "중요 표시" · "첨부 있음")은 `shared/constants/mail.ts` 의 `MAIL_LIST_ROW_TEXT` — 메일 줄은 아직 직접 적은 글이라 함께 고친다. 읽기 화면 [이전] · [다음] · 위치는 대화 단위(`ListReadNav.vue`), 한 통 삭제 뒤 갈 곳은 `conversationNeighborAfterRemoval` — 디자인 README "14단계 구현하며 정한 보완 — 목록 대화로 묶어 보기" |
|
||||
| 여러 통 처리 · 삭제 | `src/components/mail/MailCheckbox.vue` · `MailSelectionToolbar.vue`(+ `MailBulkButton` · `MailBulkMoveButton`) · `MailPurgeDialog.vue` · `MessageDeleteButton.vue` · `composables/useBulkMailActions.ts` · `useMessageRemoval.ts` · `useMailMoveFeedback.ts` · `shared/utils/mail-selection.ts` · `mail-bulk.ts` | 100통씩 차례로. 읽음 · 중요는 먼저 바꾸고 `failedIds` 만 되돌리고, 이동 · 삭제는 응답의 id 만 뺀다. **웹은 `DELETE /api/messages/{id}` 를 쓰지 않는다** — 휴지통 밖 삭제는 `PATCH` 이동(+ 되돌리기), 휴지통에서는 확인 대화상자(처음 초점 「취소」) 뒤 `POST /message-deletions`. 읽기 화면도 그 메일의 `mailboxIds` 로 같은 규칙을 탄다. 도구 줄은 role="toolbar" 로빙 tabindex(바쁜 · 꺼진 단추는 건너뛴다) — 계약 2절 "여러 통 한꺼번에" |
|
||||
| 설정 › 비밀번호 · 부재중 (4단계) | `src/pages/settings/AccountPasswordPage.vue` · `MailVacationPage.vue` · `components/settings/`(`SettingsPageFrame` 공용 틀 — 머리 오른쪽 단추는 `actions` 슬롯 · `PasswordChangeForm` · `PasswordField` · `Vacation*`) · `stores/mail-vacation.store.ts` · `shared/utils/password-change.ts` · `mail-vacation.ts` · 메뉴 `shared/constants/settings-menu.ts` | 메뉴는 묶음(계정 › 비밀번호 / 메일 › 서명 · 부재중 자동 응답 · 필터(6단계) · 알림(5단계)), 라우트 이름은 `SETTINGS_ROUTES` 하나. 비밀번호는 서버와 같은 규칙을 먼저 보고, 서버 거절(`AUTH_005` · `VAL_001`)은 칸 아래에만(알림 없음) — 성공하면 세 칸을 비운다. 부재중은 날짜만 고르고 브라우저 시간대의 시작일 0시 · 끝나는 날 다음 날 0시를 UTC 로 보낸다(변환은 `mail-vacation.ts` 한 곳 — 되읽을 때는 1ms 앞 날짜). 켤 때만 검사하고 꺼 둔 채로는 빈 값도 저장한다(길이 150 · 600 도 켤 때만 — 꺼 둔 채로는 4단계 한도까지, 끄기는 늘 저장되게). 부재중 상태는 메일 화면에 들어올 때 받고, 패널 안내 `components/mail/MailVacationNotice.vue` 가 1분마다 다시 계산한다(`useClock` · `VACATION_NOTICE_REFRESH_MS`). 저장 응답은 세션 세대(`authStore.generation`)가 바뀌었으면 쓰지 않는다. 저장한 값이 켜짐인데 엔진에서 규칙이 꺼져 있으면(응답의 `isActive: false` — 7단계) 머리 아래 노란 줄(`MailFilterNotice` 를 단추 없이 — `actionLabel` 을 주지 않는다). 칸을 고친 값이 아니라 저장한 값으로 보고, `isActive` 는 PUT 본문에 넣지 않는다(`useMail().saveVacation` 이 다섯 칸만) — 계약 1절 · 2절 "부재중 자동 응답" |
|
||||
| 메일함 관리 · 오래 걸리는 처리 (4단계) | `src/components/mail/MailboxTree.vue` · `MailboxPanelRow.vue` · `MailboxMoreButton.vue`(⋯ 메뉴) · `MailboxDialogs.vue`(+ `Mailbox*Dialog.vue`) · `MailboxEmptyButton.vue`(목록 머리 「비우기」) · `composables/useMailboxActions.ts` · `stores/mail/mailbox-admin.ts` · `shared/utils/mailbox-operation.ts` · 상수 `shared/constants/mailbox-manage.ts` | 만들기 · 이름 바꾸기는 칸 안 실패(`MAIL_009` · `VAL_001`)를 알림 없이 칸 아래에. 모두 읽음 · 비우기 · 지우기는 남은 수가 0(지우기는 `deleted: true`)이 될 때까지 다시 보내며 진행 알림을 고친다 — 진행은 응답의 `processed`, 세션이 바뀌면 멈춘다(`epoch` — 늦게 온 만들기 · 이름 바꾸기 응답도 버린다), 같은 메일함의 같은 처리는 하나, **휴지통 비우기와 메일함 지우기는 함께 돌리지 않는다**(비우기는 그사이 휴지통에 들어온 메일도 지운다 — 판정은 스토어의 `isMailboxOperationBlocked` 하나, 메뉴 · 단추 · 대화상자 확인이 모두 이것을 본다). 끝난 뒤 누른 단추가 사라져 초점을 잃었으면 패널의 지금 메일함 항목으로 옮긴다. ⋯ 메뉴는 보이는 영역 안에 다 들게 연다(`shared/utils/popup-placement.ts` — 아래가 모자라면 위로). 되돌릴 수 없는 일은 확인 대화상자(처음 초점 「취소」) 뒤에, 확인한 그 메일함만 — 계약 2절 "메일함 관리" |
|
||||
|
||||
@@ -1426,3 +1426,32 @@ API 와 서버 규칙은 계약 2절 "실시간 알림".
|
||||
- **초점**: 누른 단추가 사라지면(칸의 [다시 시도]가 성공 · [더 보기]로 끝까지 받음 · 다시 받아 줄이 빠짐) 그 줄의 펼침 단추 → 새 [다시 시도] → 칸 제목 차례로 옮기고, 칸이 통째로 사라지면 본문으로 옮긴다.
|
||||
[이 메일만 열기] 뒤에는 삭제 뒤 다음 메일로 갈 때처럼 따로 옮기지 않는다(두 길을 함께 고칠 거리).
|
||||
- **다시 받기**: 새 메일 · 지움 · 옮김으로 메일함 통 수가 바뀔 때와 쓰기 화면에서 돌아올 때만 다시 받는다 — 읽음 표시만 바뀐 소식으로는 받지 않는다(칸은 읽음을 그리지 않는다).
|
||||
|
||||
### 14단계 구현하며 정한 보완 — 목록 대화로 묶어 보기 (2026-11-06 — 캔버스에는 아직 없다)
|
||||
|
||||
받기 · 맞추기 · 줄에 하는 일의 규칙은 계약 2절 "대화 — 14단계"의 클라이언트 줄(목록을 대화로 묶어 보기)이 같은 내용이다.
|
||||
|
||||
- **크기 · 모양**
|
||||
- [대화로 묶기]는 36 — 안내 줄 단추의 밀도 예외(`--size-notice-action`)를 다시 쓴다. 새 크기 토큰은 없다.
|
||||
분할 보기는 아이콘만(36×36 — 휴지통 [비우기]와 같은 결, 이름은 title · 화면 낭독), 목록 보기는 이름까지 보인다.
|
||||
- 켠 모양은 옅은 청록 바탕 + 진한 청록 글자이고 테두리는 그대로다(보드의 #9FD3D7 테두리는 새 색 토큰이라 두지 않았다).
|
||||
- 설정 › 메일 › 보내기 · 읽기의 한 줄은 외부 이미지 아래 가는 선 뒤 — 끔 · 켬 전환(`.segmented--outline` — 보내기 취소와 같은 모양)과
|
||||
"답장과 전달로 이어진 메일을 메일함 목록에서 한 줄로 묶어 보여 줍니다. 목록 위의 [대화로 묶기]로도 켜고 끌 수 있습니다."
|
||||
- **줄**
|
||||
- 고르기 칸 · 별을 남겼다(보드의 줄에는 없다 — 검색 결과 줄과 같은 판단, 3단계부터 목록에서 여러 통을 고른다).
|
||||
메일이 모두 보내기를 기다리는 줄은 고르기 칸 · 별이 꺼짐(aria-disabled) + 까닭이고, 목록 보기에서 Ctrl · Shift + 클릭으로도 고르지 않는다.
|
||||
- 참여자: 다른 사람은 받은 차례대로, 나는 끝에 "나"(보드 "이도윤, 나" · "인사팀, 김민수, 나"). 보낸편지함 · 임시보관함에서 참여자가 나뿐이면 메일 목록처럼 "받는 사람: …"이다.
|
||||
임시보관함의 제목 없는 줄은 흐린 "(제목 없음)"이다(메일 줄과 같다).
|
||||
- 목록 보기(표)는 고르기 · 별 · 보낸 사람(참여자 + 회색 "N통") · 제목 · 날짜다 — **크기 칸은 뺐다**(대화의 크기는 마지막 메일의 크기가 아니다).
|
||||
표 제목은 "{메일함} 대화 목록", 머리 고르기는 "불러온 대화 모두 고르기", 고른 수는 "대화 N개 선택됨"이다.
|
||||
- 목록 머리가 좁으면 개수가 먼저 줄고 메일함 이름은 남는다(개수에 마우스를 올리면 다 보인다). **묶지 않았을 때의 머리 글도 보드 글로 바꿨다**("전체 9 · 안 읽음 2" → "메일 9통, 안 읽은 메일 2통").
|
||||
- "새 메일 N통 · 맨 위로"는 맨 앞에 새로 생긴 줄을 1통, 새 메일이 든 기존 줄을 새 메일 수로 센다.
|
||||
- **줄의 별**: 중요 표시가 하나라도 있으면 모두 풀고, 없으면 모두 표시한다. 보내기를 기다리는 메일만 중요 표시가 남으면
|
||||
"보내기를 기다리는 메일은 목록에서 중요 표시를 바꿀 수 없습니다. 메일을 열어 바꿔 주세요."
|
||||
- **읽기 화면**
|
||||
- [이전] · [다음]은 대화 사이를 오간다(앞뒤 줄의 마지막 메일). 위치는 "n / 대화 수"이고, 같은 대화 칸의 [이 메일만 열기]로 연 더 오래된 메일도 그 메일이 든 줄을 기준으로 한다. [목록으로]는 그 줄로 초점을 돌려준다.
|
||||
- [삭제](한 통) 뒤 열 메일(분할 보기 — 목록 보기는 지금처럼 목록으로): 그 줄에서 지운 메일의 다음 칸(더 오래된 메일), 없으면 앞 칸 →
|
||||
줄이 비면 다음 줄의 마지막 메일, 없으면 앞 줄의 마지막 메일. 아직 다시 받기 전이라 줄에 남아 있는 옛 마지막 메일이면 남은 메일의 첫 칸이다(메일 목록의 삭제 뒤와 같은 결).
|
||||
- **첫 진입**: 설정을 받기 전에는 이 브라우저가 그 직원에게서 마지막으로 안 값(처음이면 끔)으로 연다 — 서버 값이 다르면 받는 즉시 바꾼다.
|
||||
설정을 받지 못했으면 그 값대로 두고 [대화로 묶기]도 누를 수 있다(받는 동안에는 단추를 숨긴다). 로그아웃해도 그 값은 남고, 다른 직원이 로그인하면 그 직원의 값이다.
|
||||
- **알려진 한계**: 여러 통인 줄의 마지막 메일만 읽으면 다음 맞추기까지 줄이 굵은 채다. 마지막 메일이 아닌, 보내기를 기다리는 메일은 풀린 뒤에도 다음 맞추기까지 고를 수 없다(계약 2절).
|
||||
|
||||
@@ -778,6 +778,22 @@ interface ConversationSummary {
|
||||
- 500통이 넘는 대화는 `messageIds` 가 최신 500통이다 — 그 줄에 한 일은 그만큼에만 하고, 남은 메일은 다음에 다시 맞춘 목록의 같은 줄로 남는다(드문 경우).
|
||||
- **알려진 한계**: 쪽을 넘기는 사이 늦게 온 원본으로 두 대화가 합쳐지면 줄이 겹치거나 빠질 수 있다 — 처음부터 다시 받으면 맞는다.
|
||||
마지막 메일이 그 메일함을 떠났는데 색인이 아직 맞추지 않았으면 그 줄이 한 번 빠진다.
|
||||
- **클라이언트 — 목록을 대화로 묶어 보기**(14단계 웹 — 디자인 README "14단계 구현하며 정한 보완 — 목록 대화로 묶어 보기")
|
||||
- 켜고 끄기: 목록 머리 [대화로 묶기]와 설정 › 메일 › 보내기 · 읽기의 한 줄이 같은 `groupByConversation` 을 `PATCH` 로 저장한다 — 화면을 먼저 바꾸고 끝내 실패하면 되돌린다(칸마다 저장은 한 번에 하나 — 아래 "보내기 · 읽기 설정").
|
||||
메일함 목록만 묶는다 — 검색 결과 · 「예약됨」은 묶지 않는다.
|
||||
- 첫 진입: 설정을 받기 전 · 받지 못했으면 이 브라우저가 그 직원에게서 마지막으로 안 값(localStorage — 직원마다, 처음이거나 읽지 못하면 끔)으로 연다. 받은 값이 다르면 바로 그 값으로 다시 연다
|
||||
(짐작이 틀리면 버릴 첫 쪽 요청이 하나 나간다). 받지 못한 동안에도 [대화로 묶기]를 누를 수 있고, 저장 응답(설정 전체)을 받은 설정으로 삼는다 — 실패하면 누르기 전 짐작으로 돌아간다.
|
||||
로그아웃 · 세션 만료는 짐작을 바꾸지 않고, 세션이 끝나면(제한된 세션 포함) 목록을 다시 부르지 않는다.
|
||||
- 쪽은 20줄 · 받은 커서 그대로. 켜고 끔 · 메일함 바꿈 · 다른 목록으로 가면 앞 목록의 응답을 버리고 그 요청을 끊는다(`AbortSignal` — `engine` 기준은 한 요청이 15초까지). 끊긴 요청은 알리지 않는다.
|
||||
- 새 메일 확인 · 실시간: 메일 목록 대신 불러온 줄 수만큼(최대 100줄) `silent` 로 처음 쪽부터 다시 받는다 — 요청 수는 메일 목록과 같다.
|
||||
- 줄에 하는 일: 할 일 메일 = `messageIds` − `pendingSendIds` − (마지막 메일이 붙잡힌 메일이면) 그 메일. 100통씩 나눠 보낸다.
|
||||
- 화면을 먼저 바꾸는 것은 줄의 메일을 모두 바꿨을 때만 줄 전체(읽음 · 중요 해제)다 — 일부만이면 그대로 두고 다음 맞추기가 서버 값으로 고친다. 중요 해제는 붙잡힌 메일까지 바꿨을 때만이다(붙잡힌 메일의 중요 표시는 남는다).
|
||||
- 줄의 별: 중요 표시가 하나라도 있으면 모두 풀고, 없으면 할 일 메일을 모두 표시한다. 붙잡힌 메일이 든 줄을 풀면 줄을 미리 풀지 않고 묶은 목록을 한 번 다시 받는다 — 그래도 중요 표시가 남으면 알린다.
|
||||
- 읽기 화면의 별(한 통)이 실패하면 그 메일이 든 줄을 앞 값으로 되돌린다.
|
||||
- 옮기기 · 삭제 뒤에는 묶은 목록을 한 번 다시 받는다(남은 줄의 마지막 메일 · 통 수를 맞추려고). 서버가 바꾸지 못한 메일(`failedIds` · `skippedIds`)이 든 줄은 앞 값으로 되돌린 뒤 다시 받는다. 안 읽은 수는 끝난 뒤 메일함 목록으로 맞춘다.
|
||||
- 목록 머리의 대화 수는 `total` 이다 — `null` 이면 그 부분을 뺀다.
|
||||
- 알려진 한계: 여러 통인 줄의 마지막 메일만 읽으면 다음 맞추기(실시간이면 곧, 아니면 1분)까지 그 줄이 굵은 채다 — 읽을 때마다 다시 받지 않는다(경로마다 1분 100회).
|
||||
마지막 메일이 아닌 붙잡힌 메일은 보낼 시각을 몰라, 풀린 뒤에도 다음 맞추기까지 그 메일만 든 줄을 고를 수 없다.
|
||||
|
||||
#### 설정
|
||||
|
||||
|
||||
Reference in New Issue
Block a user