From e27e9feb0bf1803e788397860227a4c2496f1b41 Mon Sep 17 00:00:00 2001 From: ttipo Date: Wed, 28 Oct 2026 13:31:38 +0900 Subject: [PATCH] =?UTF-8?q?docs:=205=EB=8B=A8=EA=B3=84(=EC=8B=A4=EC=8B=9C?= =?UTF-8?q?=EA=B0=84=20=EC=83=88=20=EB=A9=94=EC=9D=BC=20=C2=B7=20=EB=B0=94?= =?UTF-8?q?=ED=83=95=ED=99=94=EB=A9=B4=20=EC=95=8C=EB=A6=BC)=20=EA=B3=84?= =?UTF-8?q?=EC=95=BD=EA=B3=BC=20=EB=94=94=EC=9E=90=EC=9D=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 계약 2절 "실시간 알림": GET /api/mail-events(SSE) — state(+inboxUnread) · new-mail · ping(upstream) · end 이벤트, 직원마다 엔진 연결 하나 · 연결 5개 · 25초 ping · 토큰 만료 30초 전 end, 새 메일 판단(10분 넘게 앞선 메일 제외) · 5분 안전판, 클라이언트 규칙(브라우저마다 연결 하나 · fetch 로 읽기 · 다시 잇기 · 1분 확인과의 관계 · 바탕화면 알림 조건) - 엔진 확인 사실(Stalwart 0.16.27): EventSource 는 대리 접근으로 열린다, ping 은 30초보다 짧지 않다, EmailDelivery 는 초안에도 바뀐다, 스트림은 동시 요청 한도에 들지 않는다, 해석 못 하는 sinceState 는 400 notRequest - 디자인 캔버스 5단계 보드(SettingsNotify · MailRealtime)와 README "5단계" · "5단계 구현하며 정한 보완" Co-Authored-By: Claude Opus 5.5 --- docs/design/webmail/MailRealtime.dc.html | 213 ++++++++++++++++++++ docs/design/webmail/README.md | 68 ++++++- docs/design/webmail/SettingsNotify.dc.html | 222 +++++++++++++++++++++ docs/design/webmail/canvas.json | 9 +- docs/mail/api-contract.md | 89 ++++++++- 5 files changed, 592 insertions(+), 9 deletions(-) create mode 100644 docs/design/webmail/MailRealtime.dc.html create mode 100644 docs/design/webmail/SettingsNotify.dc.html diff --git a/docs/design/webmail/MailRealtime.dc.html b/docs/design/webmail/MailRealtime.dc.html new file mode 100644 index 0000000..566d701 --- /dev/null +++ b/docs/design/webmail/MailRealtime.dc.html @@ -0,0 +1,213 @@ + + + + +실시간 · 바탕화면 알림 상태 모음 + + + + + + + +
+ +
+

실시간 새 메일 · 바탕화면 알림

+

5단계에서 바뀌는 것들입니다. 메일 화면의 모양은 그대로이고, 새 메일이 1분을 기다리지 않고 바로 나타납니다. 바탕화면 알림은 설정 › 메일 › 알림에서 켜고, 「설정 · 알림」 보드에서 눌러 볼 수 있습니다.

+
+ +
+
+

목록 위에서 한 번 권하기 — 알림을 아직 고르지 않았을 때

+

새 메일 알림 줄과 같은 높이 40 (밀도 예외) · 닫으면 다시 보이지 않는다

+
+
+
+
+ 받은편지함 + 전체 128 · 안 읽음 3 +
+
+ + 바탕화면 알림을 켜 두면 다른 창을 보는 동안에도 새 메일을 알려 드립니다. + + +
+
+
+ 박서준오후 2:42 + 11월 납품분 견적서 보내 드립니다 +
+
+ 이도윤오후 1:05 + Re: 메일 서버 이전 일정 공유 +
+
+
+
    +
  • 알림을 켜지도 닫지도 않은 사람에게 메일 화면에서 한 번만 보입니다(브라우저 · 사용자마다).
  • +
  • [알림 켜기] → 브라우저가 묻습니다 → 허용하면 "바탕화면 알림을 켰습니다." 알림.
  • +
  • [닫기]를 누르면 다시 보이지 않습니다 — 설정 › 알림에서 언제든 켤 수 있습니다.
  • +
  • 브라우저가 이미 막았거나 지원하지 않으면 보이지 않습니다.
  • +
+
+
+ +
+ +
+
+

바탕화면 알림 — 한 통

+

제목: 보낸 사람 · 내용: 메일 제목

+
+
+
+ Microsoft Edge지금 +
+
+ +
+ 박서준 + 11월 납품분 견적서 보내 드립니다 + portal.arasystem.it.kr +
+
+
+

누르면 웹메일 탭으로 와서 그 메일을 엽니다. 이름이 없는 보낸 사람은 주소를 씁니다.

+
+ +
+
+

여러 통이 한꺼번에

+

한 번에 묶어 하나만

+
+
+
+ Microsoft Edge지금 +
+
+ +
+ 새 메일 3통 + 박서준 · 이도윤 외 1명 + portal.arasystem.it.kr +
+
+
+

누르면 받은편지함을 엽니다. 새 알림은 앞의 알림을 바꿔 써서 바탕화면에 쌓이지 않습니다.

+
+ +
+
+

메일 제목 숨김

+

설정에서 「메일 제목도 보이기」를 껐을 때

+
+
+
+ Microsoft Edge지금 +
+
+ +
+ 박서준 + 새 메일이 왔습니다 + portal.arasystem.it.kr +
+
+
+

여러 통이면 "새 메일 3통" · "박서준 · 이도윤 외 1명" 그대로입니다(제목만 뺍니다).

+
+
+ +
+ +
+

언제 알리나

+
    +
  • 받은편지함에 새로 배달된 안 읽은 메일만 — 내가 보낸 메일 · 임시 저장 · 다른 메일 프로그램이 다른 메일함으로 옮긴 메일은 알리지 않습니다.
  • +
  • 웹메일 화면을 보고 있으면 알리지 않습니다 — 목록 · 메일함 숫자 · 탭 제목 "(3)" 에 바로 보이므로. 다른 탭 · 다른 프로그램을 보고 있을 때만.
  • +
  • 웹메일 탭이 여러 개여도 한 번만 알립니다(브라우저마다 연결 하나).
  • +
  • 운영체제의 방해 금지 · 집중 모드에서는 운영체제가 숨깁니다. 소리도 운영체제 설정을 따릅니다.
  • +
  • 웹메일 탭을 모두 닫으면 알리지 않습니다 — 탭을 닫아도 오는 알림은 다음 단계입니다.
  • +
+
+ +
+

새 메일이 화면에 나타나는 때

+
+ 상대가 보냄 + + 1초 안 + + 목록 · 메일함 숫자 · 탭 제목 +
+

지금은 1분마다 확인해서 최대 1분 늦습니다. 화면 모양은 그대로이고, 목록을 내려 보고 있으면 지금처럼 "새 메일 N통 · 맨 위로" 줄이 뜹니다.

+

실시간 연결이 끊기면 저절로 1분 확인으로 돌아가고 다시 잇습니다 — 화면에는 알리지 않습니다(설정 › 알림에서 연결 상태를 볼 수 있습니다).

+
+ +
+
+

설정 › 알림 — 브라우저가 막았을 때 · 지원하지 않을 때

+

켜기 칸 아래의 안내가 바뀐다

+
+
+ +

+ + 브라우저가 이 사이트의 알림을 막고 있습니다. 주소창 왼쪽의 사이트 정보(자물쇠) › 알림을 「허용」으로 바꾼 뒤 이 화면을 새로 고쳐 주세요. +

+
+
+ +

이 브라우저는 바탕화면 알림을 지원하지 않습니다. 새 메일은 그래도 바로 목록에 나타납니다.

+
+
+ +
+
+

알림 문구

+

오른쪽 아래 · 화면 안 알림

+
+
+ + 바탕화면 알림을 켰습니다. +
+
+ + 브라우저가 알림을 막고 있습니다. 푸는 방법은 설정 › 알림에 있습니다. + 설정 열기 +
+

목록 위 [알림 켜기] · 설정의 켜기 칸에서 브라우저가 묻고 난 뒤의 결과입니다. 「차단」을 누른 사람에게 브라우저는 다시 묻지 않습니다.

+
+ +
+
+
+ + + diff --git a/docs/design/webmail/README.md b/docs/design/webmail/README.md index 921856e..bf40b9e 100644 --- a/docs/design/webmail/README.md +++ b/docs/design/webmail/README.md @@ -1,14 +1,15 @@ -# 웹메일 디자인 원본 (1~4단계) +# 웹메일 디자인 원본 (1~5단계) 확정된 디자인 캔버스의 원본이다. 화면 구현의 기준으로 쓴다. -- 캔버스: https://claude.ai/artifact/WJvVsKvXepxn3p75CGJAr2 (확정본: 2026-10-28, 버전 23) +- 캔버스: https://claude.ai/artifact/WJvVsKvXepxn3p75CGJAr2 (확정본: 2026-10-28, 버전 27) - 화면: `Login`(로그인) · `Main`(분할 보기: 메일함·목록·본문) · `MainList`(목록 보기: 게시판형 표) · `MainListRead`(목록 보기에서 연 메일) · `Compose`(메일 쓰기 — 서식 편집기) · `ComposeEditor`(메일 쓰기 · 편집기 상태: 포커스·툴바·메뉴·팝오버·인용·이미지 첨부) · `States`(오류·빈 상태·피드백 모음) · 2단계: `Settings`(설정 · 메일 서명) · `ComposeAssist`(메일 쓰기 · 임시 저장 · 받는 사람 자동 완성 · 서명) — 아래 "2단계" · 3단계: `Search`(메일 검색 · 분할 보기) · `BulkSelect`(여러 통 선택 · 목록 보기) · `ListStates`(새 메일 · 검색 · 여러 통 처리 상태) — 아래 "3단계" · 4단계: `SettingsAccount`(설정 · 비밀번호) · `SettingsVacation`(설정 · 부재중 자동 응답) · `MailboxManage`(메일함 관리 · 휴지통 비우기) · `MailActions4`(스팸 · 원문 · 오래 걸리는 처리) — 아래 "4단계" + · 5단계: `SettingsNotify`(설정 · 알림) · `MailRealtime`(실시간 새 메일 · 바탕화면 알림 상태) — 아래 "5단계" - 파일 형식은 캔버스 전용(`` 마크업 + `DCLogic` 클래스)이다. 그대로 쓰지 말고 **구조·간격·색·문구의 기준**으로만 본다. `Compose` 의 툴바·메뉴는 눌러 볼 수 있는 시안이다 — 버튼 상태만 바뀌고 본문 글자에 서식이 입혀지지는 않는다. @@ -309,6 +310,42 @@ API 와 서버 규칙은 계약 1절 "`PUT /api/auth/password`", 2절 "메일함 - 알림 문구: "메일 2통을 스팸메일함으로 옮겼습니다." [되돌리기] · "메일 1통을 받은편지함으로 옮겼습니다 (스팸 아님)." [되돌리기] · "‘협력사’ 메일함을 만들었습니다." · "‘거래처’ 메일함을 지웠습니다. 안의 메일 12통은 휴지통에 있습니다." · "휴지통을 비웠습니다 — 메일 21통." (비우기 · 지우기는 되돌리기 없음) +## 5단계 — 실시간 새 메일 · 바탕화면 알림 (2026-10-28 확정 — `SettingsNotify` · `MailRealtime`) + +API 와 서버 규칙은 계약 2절 "실시간 알림". + +### 설정 › 메일 › 알림 + +- 메뉴의 "메일" 묶음 끝에 「알림」(종 아이콘). +- [x] 새 메일이 오면 바탕화면 알림 보내기(44). 켜면 브라우저가 허락을 묻는다. 칸 아래 안내(12.5px): + 묻기 전 "켜면 브라우저가 알림을 허락할지 한 번 묻습니다 — 「허용」을 눌러 주세요." · 허용됨 "이 브라우저에서 알림이 허용되어 있습니다."(#067647, 체크 아이콘) · + 막힘(칸을 끄고 빨강 #B42318) "브라우저가 이 사이트의 알림을 막고 있습니다. 주소창 왼쪽의 사이트 정보(자물쇠) › 알림을 「허용」으로 바꾼 뒤 이 화면을 새로 고쳐 주세요." · + 지원하지 않음 "이 브라우저는 바탕화면 알림을 지원하지 않습니다. 새 메일은 그래도 바로 목록에 나타납니다." +- 알림에 보일 것: [x] 메일 제목도 보이기 — "끄면 보낸 사람 이름만 보입니다 — 화면을 다른 사람과 함께 보는 자리라면 꺼 두세요." (알림을 끄면 흐리게) +- [시험 알림 보내기](보조 단추 44 · 종 아이콘) — 알림 "ARA 웹메일" / "알림이 잘 옵니다. 새 메일이 오면 이렇게 알려 드립니다." +- 실시간 연결: ● 초록 "연결되어 있습니다 — 새 메일이 오면 바로 목록에 나타납니다." / 끊겼을 때 "연결할 수 없어 1분마다 확인합니다." (저절로 다시 잇는다) +- 아래 안내 상자(바탕 #F3F8F8): 이 브라우저에만 적용 · 웹메일 탭이 하나라도 열려 있어야 · 웹메일 화면을 보고 있을 때는 보내지 않음. + +### 목록 위에서 한 번 권하기 + +- 알림을 켜지도 닫지도 않은 사람에게 메일 화면의 목록 머리 아래에 40 줄(바탕 #F3F8F8 — 새 메일 알림 줄과 같은 높이, 밀도 예외): + 종 아이콘 + "바탕화면 알림을 켜 두면 다른 창을 보는 동안에도 새 메일을 알려 드립니다." [알림 켜기](강조 글자 굵게) [✕ 닫기]. +- [알림 켜기] → 브라우저가 묻는다 → 허용: 알림 "바탕화면 알림을 켰습니다." / 막힘: 오류 알림 "브라우저가 알림을 막고 있습니다. 푸는 방법은 설정 › 알림에 있습니다." [설정 열기]. +- [✕]는 다시 보이지 않는다(브라우저 · 사용자마다). 브라우저가 이미 막았거나 지원하지 않으면 보이지 않는다. + +### 바탕화면 알림 + +- 모양은 운영체제 · 브라우저가 그린다 — 정하는 것은 제목 · 내용 · 아이콘(ARA 마크, 바탕 #0B3640 둥근 사각)뿐이다. +- 한 통: 제목 = 보낸 사람(이름, 없으면 주소), 내용 = 메일 제목. 여러 통: "새 메일 3통" / "박서준 · 이도윤 외 1명". 제목 숨김: 내용 "새 메일이 왔습니다". +- 새 알림은 앞의 알림을 바꿔 쓴다(쌓이지 않는다). 누르면 웹메일 탭으로 와서 그 메일을 연다(여러 통이면 받은편지함). +- 받은편지함에 새로 배달된 안 읽은 메일만(내가 보낸 메일 · 임시 저장 · 다른 메일함으로 옮겨진 메일은 아님), 웹메일을 보고 있지 않을 때만(보이고 초점이 있는 탭이 없을 때), 탭이 여러 개여도 한 번. + +### 실시간 반영 + +- 화면 모양은 그대로다 — 새 메일이 1초 안에 목록 · 메일함 숫자 · 탭 제목 "(N)" 에 나타난다(지금은 최대 1분). 목록을 내려 보고 있으면 지금처럼 "새 메일 N통 · 맨 위로" 줄. +- 다른 탭을 보는 동안에도 탭 제목의 "(N)" 이 바로 바뀐다. +- 실시간 연결이 끊기면 화면에는 알리지 않고 1분 확인으로 돌아간다(설정 › 알림의 연결 상태로 볼 수 있다). + ## 구현하며 정한 보완 (2026-10-26 — 캔버스에는 아직 없다) - 찾지 못한 첨부(보관 시간이 지난 업로드·지워진 원본)는 실패로 표시하고, 전달 원본에는 "빼기"만 둔다(다시 올릴 수 없다). @@ -402,3 +439,30 @@ API 와 서버 규칙은 계약 1절 "`PUT /api/auth/password`", 2절 "메일함 - 목록 머리 「비우기」의 확인은 연 그 메일함만 비운다 — 확인 창이 열린 채 다른 메일함으로 가면(뒤로 가기 등) 창을 닫는다. - 서버가 처리 수 0 을 세 번 연달아 주면 멈추고 실패로 알린다(같은 요청을 끝없이 보내지 않게). - **스팸**: 「되돌리기」는 원래 메일함으로 옮기며 표시를 반대로 바꾼다 — 원래 아무 표시도 없던 메일은 "스팸 아님" 표시가 남는다(계약 2절 "스팸 · 원문"). + +### 5단계 구현하며 정한 보완 (2026-10-28 — 캔버스에는 아직 없다) + +- **알림을 막았을 때의 알림**은 보드의 어두운 바탕 대신 기존 오류 알림 모양(흰 카드)을 쓴다. [설정 열기]는 브랜드 색. +- **"웹메일을 보고 있음"** 은 같은 사용자로 로그인한 이 앱의 탭 가운데 보이고 초점이 있는 탭이 있다는 뜻이다 — 설정 화면을 보고 있어도 보내지 않는다. + 두 번째 모니터에 웹메일을 띄워 두고 다른 창에서 일하면 보낸다(권하기 문구 "다른 창을 보는 동안에도"와 맞게). +- **설정 › 알림** + - 허락은 했는데 알림을 꺼 둔 상태: 칸은 비우고 안내는 초록 "이 브라우저에서 알림이 허용되어 있습니다."(보드에 없는 상태). + - 실시간을 쓸 수 없는 브라우저(Web Locks · BroadcastChannel · 스트림 읽기 가운데 하나라도 없음)는 바탕화면 알림도 "지원하지 않습니다"로 본다 — 알림은 실시간으로만 온다. + - 연결 상태 글: 처음 잇는 중 "연결하는 중입니다…", 끊김 "연결할 수 없어 1분마다 확인합니다." 아래에 "저절로 다시 잇습니다. 회사 방화벽 · 프록시가 오래 열린 연결을 끊으면 이렇게 됩니다.", + 지원하지 않음 "이 브라우저는 실시간 연결을 지원하지 않습니다." + - 주소는 `/settings/mail/notify`, 탭 제목은 "메일 알림"(머리글은 "알림"). + - 켜기 칸에서 허락을 받은 결과는 칸 아래 안내로만 보인다(화면 알림은 목록 위 권하기 줄에서만 — 설정 화면에서 [설정 열기]는 어색하다). + - 연결이 끊긴 동안은 "연결할 수 없어 1분마다 확인합니다." 아래에 "연결될 때까지 바탕화면 알림은 오지 않습니다."를 더한다. + - 알림을 만들 수 없는 브라우저(휴대폰 Chrome 은 `new Notification()` 을 막는다)는 처음 실패할 때 "지원하지 않습니다"로 바꾼다. + - 한국어 안내 글은 낱말 가운데서 줄을 바꾸지 않는다(`word-break: keep-all`, 긴 주소는 `overflow-wrap: anywhere`). +- **목록 위 권하기 줄**은 메일함 목록에만 둔다(검색 결과에는 두지 않는다). 줄이 사라지면 초점을 목록 칸으로 옮긴다. + 분할 보기의 좁은 목록 칸(384)에서는 짧은 글 "바탕화면 알림으로 새 메일 받기"를 쓴다(긴 글은 툴팁) — 긴 글은 잘려 끝까지 읽을 수 없었다. +- **바탕화면 알림** + - "외 N명"은 서버가 보낸 새 메일(최대 5통)에서 겹치지 않는 보낸 사람(주소 기준)으로 센다. + - 시험 알림도 새 메일 알림과 같은 자리(tag)를 쓴다. 누르면 창만 앞으로 온다. + - 알림을 눌렀을 때 받은편지함을 아직 모르면 메일함을 받아 본 뒤 열고, 그래도 모르면 메일 첫 화면으로 간다. 누르기 전에 로그아웃 · 다른 계정 로그인이 있었으면 이동하지 않는다. + - 아이콘은 192px PNG(`frontend/src/assets/brand/notification-icon.png` — ARA 마크를 #0B3640 둥근 사각 가운데에). + - 사용자가 웹메일로 돌아오면(어느 탭이든 보이고 초점이 생기면) 띄운 알림을 닫는다 — 알림 센터에 지난 알림이 남지 않게. + - 허락을 묻는 동안 단추 · 칸은 눌러도 다시 묻지 않을 뿐 초점을 잃지 않는다(`aria-disabled`). 막힘 · 지원 안 함으로 칸이 꺼지면 초점을 안내 글로 옮긴다. +- **탭 제목 "(N)"** 은 지금처럼 메일 화면에서만 붙인다(메일 밖 화면에는 받은편지함 수의 기준이 없다). 메일 밖에 있는 동안 생긴 바뀜은 남겨 두었다가 메일 화면으로 돌아오면 맞춘다. +- **실시간 반영 시간**: 실시간이 부른 목록 맞추기는 1초 모아서 한 번 한다(한 통을 읽어도 바뀜이 오므로 — 연달은 바뀜에 목록을 여러 번 받지 않게). 그래서 새 메일은 "1초 안"이 아니라 2초 안팎에 목록에 나타난다. diff --git a/docs/design/webmail/SettingsNotify.dc.html b/docs/design/webmail/SettingsNotify.dc.html new file mode 100644 index 0000000..402f8b3 --- /dev/null +++ b/docs/design/webmail/SettingsNotify.dc.html @@ -0,0 +1,222 @@ + + + + +설정 · 알림 + + + + + + + +
+ + + +
+ +
+ 설정 +
+ + +
+
+ +
+ 김아라 + ara.kim@arasystem.it.kr +
+ + + 로그아웃 + +
+
+ +
+ + + +
+
+ +
+

알림

+

새 메일은 1분을 기다리지 않고 바로 목록에 나타납니다. 다른 창을 보는 동안에도 알고 싶으면 바탕화면 알림을 켜 두세요.

+
+ +
+ + +

+ + 이 브라우저에서 알림이 허용되어 있습니다. +

+
+ +

켜면 브라우저가 알림을 허락할지 한 번 묻습니다 — 「허용」을 눌러 주세요.

+
+
+ +
+ 알림에 보일 것 + +

끄면 보낸 사람 이름만 보입니다 — 화면을 다른 사람과 함께 보는 자리라면 꺼 두세요.

+
+ +
+ + 바탕화면 오른쪽 아래(맥은 오른쪽 위)에 알림이 뜨면 됩니다. +
+ +
+ +
+

실시간 연결

+

+ + 연결되어 있습니다 — 새 메일이 오면 바로 목록에 나타납니다. +

+

연결할 수 없을 때는 "연결할 수 없어 1분마다 확인합니다."로 바뀌고, 저절로 다시 잇습니다. 회사 방화벽 · 프록시가 오래 열린 연결을 끊으면 이렇게 됩니다.

+
+ +
+

알림 설정은 이 브라우저에만 적용됩니다. 다른 PC · 브라우저에서는 그곳에서 따로 켜 주세요.

+

웹메일 탭이 하나라도 열려 있어야 알림이 옵니다. 탭을 모두 닫으면 오지 않습니다.

+

웹메일 화면을 보고 있을 때는 바탕화면 알림을 보내지 않습니다 — 목록에 바로 보이므로.

+
+ +
+
+
+
+ + +
+
+ + Microsoft Edge + 지금 +
+
+ +
+ ARA 웹메일 + 알림이 잘 옵니다. 새 메일이 오면 이렇게 알려 드립니다. + portal.arasystem.it.kr +
+
+
+
+
+
+ + + diff --git a/docs/design/webmail/canvas.json b/docs/design/webmail/canvas.json index bf93b0d..b657ac8 100644 --- a/docs/design/webmail/canvas.json +++ b/docs/design/webmail/canvas.json @@ -20,14 +20,17 @@ "SettingsAccount.dc.html": { "x": 0, "y": 6640, "w": 1440, "h": 900, "title": "설정 · 비밀번호", "expand": "fill", "is_interactive": true }, "SettingsVacation.dc.html": { "x": 1520, "y": 6640, "w": 1440, "h": 1060, "title": "설정 · 부재중 자동 응답", "expand": "fill", "is_interactive": true }, "MailboxManage.dc.html": { "x": 3040, "y": 6640, "w": 1440, "h": 900, "title": "메일함 관리 · 휴지통 비우기", "expand": "fill", "is_interactive": true }, - "MailActions4.dc.html": { "x": 4560, "y": 6640, "w": 1440, "h": 1320, "title": "스팸 · 원문 · 오래 걸리는 처리 상태" } + "MailActions4.dc.html": { "x": 4560, "y": 6640, "w": 1440, "h": 1320, "title": "스팸 · 원문 · 오래 걸리는 처리 상태" }, + "SettingsNotify.dc.html": { "x": 0, "y": 8560, "w": 1440, "h": 900, "title": "설정 · 알림", "expand": "fill", "is_interactive": true }, + "MailRealtime.dc.html": { "x": 1520, "y": 8560, "w": 1440, "h": 1520, "title": "실시간 새 메일 · 바탕화면 알림 상태" } }, - "order": ["Main.dc.html", "Login.dc.html", "Compose.dc.html", "States.dc.html", "MainList.dc.html", "MainListRead.dc.html", "ComposeEditor.dc.html", "Settings.dc.html", "ComposeAssist.dc.html", "Search.dc.html", "BulkSelect.dc.html", "ListStates.dc.html", "SettingsAccount.dc.html", "SettingsVacation.dc.html", "MailboxManage.dc.html", "MailActions4.dc.html"], + "order": ["Main.dc.html", "Login.dc.html", "Compose.dc.html", "States.dc.html", "MainList.dc.html", "MainListRead.dc.html", "ComposeEditor.dc.html", "Settings.dc.html", "ComposeAssist.dc.html", "Search.dc.html", "BulkSelect.dc.html", "ListStates.dc.html", "SettingsAccount.dc.html", "SettingsVacation.dc.html", "MailboxManage.dc.html", "MailActions4.dc.html", "SettingsNotify.dc.html", "MailRealtime.dc.html"], "notes": { "flow": { "x": 0, "y": -300, "text": "arasystem 웹메일 — 1단계 화면 (로그인 → 메일함 → 쓰기)", "kind": "title1", "maxW": 4480 }, "phase2": { "x": 0, "y": 2180, "text": "2단계 화면 — 임시 저장 · 받는 사람 자동 완성 · 서명 (2026-10-27 확정)", "kind": "title1", "maxW": 2960 }, "phase3": { "x": 0, "y": 4180, "text": "3단계 화면 — 새 메일 · 검색 · 여러 통 처리 (2026-10-27 확정)", "kind": "title1", "maxW": 4480 }, - "phase4": { "x": 0, "y": 6340, "text": "4단계 화면 — 계정 · 메일 설정 (비밀번호 · 부재중 응답 · 메일함 관리 · 스팸 · 원문) — 2026-10-28 확정", "kind": "title1", "maxW": 6000 } + "phase4": { "x": 0, "y": 6340, "text": "4단계 화면 — 계정 · 메일 설정 (비밀번호 · 부재중 응답 · 메일함 관리 · 스팸 · 원문) — 2026-10-28 확정", "kind": "title1", "maxW": 6000 }, + "phase5": { "x": 0, "y": 8260, "text": "5단계 화면 — 실시간 새 메일 · 바탕화면 알림 — 2026-10-28 확정", "kind": "title1", "maxW": 2960 } }, "designSystems": [] } diff --git a/docs/mail/api-contract.md b/docs/mail/api-contract.md index 3426a33..44325fb 100644 --- a/docs/mail/api-contract.md +++ b/docs/mail/api-contract.md @@ -212,6 +212,7 @@ HTML 은 정화하기 **전에** 길이(HTML 부분이 여럿이면 이어 붙 | `PUT /api/mail-vacation` | `MailVacation` (전체 교체) | 200 `MailVacation` | 400 `VAL_001`, 503 `MAIL_001` | | `GET /api/mail-quota` | — | 200 `MailQuota` | `MAIL_001` | | `GET /api/mail-state` | — | 200 `MailState` (아래 "새 메일 확인") | 503 `MAIL_001` | +| `GET /api/mail-events` | — | `text/event-stream` (표준 래퍼 없음 — 아래 "실시간 알림", 5단계) | 401 `AUTH_001`, 429 `REQ_001`(한 직원의 연결 5개 초과), 503 `MAIL_001` | | `GET /api/messages` | `mailboxId` · `cursor` · `limit`(기본 20, 최대 100) · `q` — `mailboxId` 와 `q` 중 하나는 있어야 한다(`q` 만 있으면 휴지통·스팸메일함을 뺀 모든 메일함에서 찾는다 — 아래 "검색") | 200 `{ items: MessageSummary[], pageInfo }` — 받은 시각 내림차순, `total` 포함 | 400 `VAL_001`, `MAIL_001` | | `GET /api/messages/{id}` | — | 200 `MessageDetail` — **조회만으로 읽음 처리하지 않는다** | 404 `RES_001` | | `PATCH /api/messages/{id}` | `{ isRead?, isFlagged?, mailboxId?, isJunk? }` (하나 이상 — `isJunk` 는 4단계 "스팸 · 원문") | 200 `MessageSummary` | 404 `RES_001`, 400 `VAL_001` | @@ -491,9 +492,9 @@ interface MailState { - 받은편지함에 안 읽은 메일이 있으면 탭 제목 앞에 `(N) ` 을 붙인다 (디자인 README "3단계"). - 요청 수 한도(`THROTTLE_LIMIT`, 기본 1분 100회)는 경로마다 · IP 마다 센다 — 사무실 전체가 한 IP 로 나가면 보이는 탭이 100개를 넘을 때 1분 확인이 429 `REQ_001` 로 조용히 실패한다(다음 주기에 다시). 값이 바뀌었을 때의 맞추기(`GET /api/messages` · `/api/mailboxes` · `/api/mail-quota`)는 - 평소 사용과 같은 칸을 쓴다 — `GET /api/messages` 는 대략 "쓰고 있는 탭 수 × 1분에 1번 + 평소 목록 · 더 보기"다. 운영 전환 때 직원 수에 맞춰 정한다. -- 실시간 푸시는 이 단계에서 하지 않는다. 엔진은 JMAP EventSource 를 지원하므로(2026-10-27 확인) 1분이 모자라면 백엔드가 SSE 로 중계하는 방식으로 바꾼다 — - 그때는 nginx · 외부 프록시의 응답 버퍼링과 시간 제한도 함께 바꿔야 한다. + 평소 사용과 같은 칸을 쓴다 — `GET /api/messages` 는 대략 "쓰고 있는 탭 수 × 1분에 1번 + 평소 목록 · 더 보기"다(3단계). + 5단계부터는 실시간 연결이 살아 있는 동안 1분 확인 대신 "바뀔 때마다(1초 모아서) 보이는 탭 수 × 1번"이다 — 사무실이 바쁠수록 늘 수 있다. 운영 전환 때 직원 수에 맞춰 정한다. +- 5단계부터는 실시간 연결(아래 "실시간 알림")이 살아 있는 동안 1분 확인을 멈추고, 끊기면 1분 확인으로 돌아간다. 이 API 와 규칙은 그대로다. ### 검색 (`GET /api/messages` 의 `q`) — 3단계 @@ -644,8 +645,88 @@ interface MailVacation { - **원문 내려받기**: `GET /api/messages/{id}/source` — 엔진의 원본(blob)을 그대로 흘려보낸다(`message/rfc822`, `Cache-Control: private, no-store`). 파일 이름은 제목(없으면 "메일")에 `.eml` — 다운로드 헤더는 첨부와 같은 규칙(`shared/utils/content-disposition.ts`). 화면은 첨부 내려받기처럼 이동 전에 세션을 확인한다(`confirmSession()`). +### 실시간 알림 (`GET /api/mail-events`) — 5단계 + +서버가 메일 엔진(Stalwart)의 JMAP EventSource(RFC 8620 7.3)를 직원마다 하나 열고, 그 소식을 브라우저에 SSE(`text/event-stream`)로 흘려보낸다. 표준 응답 래퍼는 없다. + +```text +event: state +data: {"state":"","inboxUnread":3} + +event: new-mail +data: {"count":2,"messages":[{"id":"…","from":{"name":"박서준","email":"seojun.park@arasystem.it.kr"},"subject":"…","receivedAt":"2026-10-28T05:42:00.000Z"}]} + +event: ping +data: {"upstream":true} + +event: end +data: {"reason":"token-expiring"} +``` + +| 이벤트 | 언제 · 뜻 | +| --- | --- | +| `state` | 연결 직후 한 번, 그 뒤 메일함 · 메일이 바뀔 때마다(엔진 신호를 0.3초 모아서). `state` 는 `GET /api/mail-state` 와 같은 값, `inboxUnread` 는 받은편지함의 안 읽은 수 | +| `new-mail` | 받은편지함에 새로 배달된 메일이 있을 때 — `$draft` 아님 · `$seen` 아님 · 보낸 사람이 나 아님. 새것부터 최대 5통(`count` 는 전체 수). 같은 변화의 `state` 다음에 온다 | +| `ping` | 25초마다(직원마다 한 간격 — 새 연결의 첫 ping 은 더 빨리 올 수 있다), 그리고 `upstream` 이 바뀌면 그때 바로. `upstream: false` 면 서버와 엔진 사이가 끊겨 다시 잇는 중이다 — 그동안 클라이언트는 1분 확인으로 돌아간다. 다시 잇는 중에 새로 연결하면 마지막으로 알던 `state` 와 `ping {upstream:false}` 가 바로 온다 | +| `end` | 서버가 곧 닫는다 — `token-expiring`(액세스 토큰이 끝나기 30초 전 — 남은 시간이 30초 이하인 토큰(세션이 끝나기 30초 안에 갱신한 토큰)이면 토큰이 실제로 끝날 때) · `server-shutdown`(서버 종료 · 재시작) | + +| 연결 전 실패 (표준 실패 형식 JSON) | 코드 | +| --- | --- | +| 로그인 안 됨 · 토큰 만료 | 401 `AUTH_001` | +| 한 직원의 연결이 5개를 넘음(브라우저마다 하나라 기기 · 브라우저 5개) · 요청 수 한도(경로마다 IP 당) · 엔진 한도 | 429 `REQ_001` | +| 엔진에 연결할 수 없음(처음 연결 10초 상한) · 기다리는 사이 엔진 연결이 끝남 · 서버 종료 중 | 503 `MAIL_001` | + +스트림이 열린 뒤 계정을 더 쓸 수 없게 되면(엔진이 대리 접근을 거절) `end` 없이 닫는다 — 클라이언트는 끊김으로 보고 다시 이어 401 → 갱신 실패 → 세션 만료로 간다. + +- **서버**: 직원마다 엔진 연결 하나를 그 직원의 모든 연결에 나눠 쓴다(마지막 연결이 닫히고 30초 뒤 닫는다 — 새로 고침에 다시 열지 않게). + 엔진 연결은 대리 접근 자격 · `types=Mailbox,Email` · `ping=30` 이다 — 엔진은 30초보다 짧게 주지 않는다(10초를 달라고 해도 30초, 2026-10-28 확인). + 75초 동안 아무것도 오지 않으면 끊긴 것으로 보고 다시 잇는다(1 · 2 · 5 · 10 · 30초 간격에 ±20% — 엔진이 다시 뜰 때 모든 직원이 같은 순간에 붙지 않게). + 그동안 클라이언트에는 `ping` 의 `upstream: false` 로 알린다. 같은 기준으로 이어서 3번 묻지 못하면 기준을 버리고 처음 상태로 다시 잡는다(그 차례는 `new-mail` 없이 — 엔진 한도 · 엔진이 내려간 동안은 세지 않는다). + 엔진이 묻기를 한도(429)로 막으면 엔진 연결은 두고 5초(±20%) 뒤 다시 묻는다(`upstream` 은 true 그대로 — 다시 잇는 중이면 다시 잇기 간격대로). + **5분 안전판**: 엔진 신호가 없어도 마지막으로 물은 지 5분이 지나면 한 번 묻고, 달라졌으면 `state`(와 `new-mail`)를 보낸다 — 연결이 살아 있는 동안 클라이언트는 1분 확인을 멈추므로, 엔진이 신호를 빠뜨려도 낡은 값이 5분 넘게 남지 않게. +- **새 메일 판단**: 엔진의 `Email/changes`(지난 state 부터 생긴 메일) → `Email/get` 으로 한다. 엔진의 `EmailDelivery` 신호는 초안을 만들어도 바뀌므로 쓰지 않는다(2026-10-28 확인). + 배달에서 신호까지 0.5초 남짓이다(같은 날 개발 엔진). 다음 경우에는 `new-mail` 없이 `state` 만 보내고 기준을 다시 잡는다: + 바뀐 것을 셀 수 없음(`cannotCalculateChanges`), 엔진이 옛 기준을 거부함(Stalwart 0.16.27 은 해석하지 못하는 `sinceState` 에 HTTP 400 `notRequest` 로 요청 전체를 거부한다 — 2026-10-28 확인), + 한 번에 2,000건(500 × 4) 넘게 바뀜. + `Email/changes` 의 생성은 "배달"이 아니라 "만들어짐"이다 — 받은 시각(`receivedAt`)이 변화를 본 때보다 10분 넘게 앞선 메일은 알리지 않는다 + (메일 프로그램의 올리기 · mailcow 에서 옮길 때의 IMAP 가져오기(imapsync)가 옛 메일을 넣어도 알림이 쏟아지지 않게). 받은 시각을 읽지 못한 메일도 빼고, 엔진 시계가 앞서 미래 시각인 메일은 넣는다. + 그래서 서버와 엔진 사이가 10분 넘게 끊겼다 다시 이으면, 그동안 온 메일은 `state` 로만 알린다(목록에는 나타나고 바탕화면 알림은 없다). + "보낸 사람이 나"는 토큰의 주소와 발신 ID(별칭) 주소로 본다(대소문자 무시). +- 오래 열린 엔진 연결은 엔진의 동시 요청 한도(4)를 잡아먹지 않는다 — 한 직원의 스트림 8개를 연 채 그 직원 · 다른 직원의 보통 요청과 동시 요청 6개가 모두 됐다(2026-10-28 확인). +- 연결은 액세스 토큰이 끝날 때 닫는다(최대 1시간 — 그 전에 `end`). 로그아웃 · 비밀번호 변경으로 세션을 끊어도 다른 API 처럼 액세스 토큰이 끝날 때까지는 이어진다(1절 세션 규칙). + 서버가 종료될 때는 열린 연결을 `end` 로 먼저 닫는다 — 오래 열린 연결이 종료를 붙잡지 않게. + 알려진 한계: 잠자기 · VPN 재연결 · 망 바꿈으로 반쯤 열린 연결은 서버가 ping 을 쓰다 실패할 때(OS 기본 15분 안팎)까지 자리를 잡는다 — + 그사이 그 직원의 연결이 5개에 닿으면 새 연결은 429 이고 화면은 1분 확인으로 이어진다. +- 응답 머리글: `Content-Type: text/event-stream`, `Cache-Control: no-cache, no-transform`, `X-Accel-Buffering: no`. 서버는 스트림을 열기로 정하면 머리글을 바로 보낸다. + 앞단 nginx 는 이 경로만 버퍼링 · 캐시 · 압축을 끄고, 읽기 사이 시간 제한을 90초로 둔다(응답 전체가 아니라 두 번 읽는 사이 — ping 25초 · 클라이언트 감시 75초보다 길게, + 머리글이 오지 않으면 90초에 504). 외부 리버스 프록시도 버퍼링을 끄고, 응답 전체 시간에 상한이 있으면 1시간 넘게 둔다(운영 확인 항목 — docker 스킬). + 앱 로그에는 열고 닫는 것을 남기지 않는다(반복되는 정상 흐름) — 엔진 연결이 이어서 실패하면 warn. nginx 접근 로그에는 연결이 끝날 때 한 줄이 남는다(`rt=` 가 이어진 시간). +- **클라이언트** + - 브라우저마다 연결 하나 — 탭이 여러 개면 한 탭만 연결하고(Web Locks) 다른 탭에 소식을 나눈다(BroadcastChannel). 탭마다 열면 HTTP/1.1 의 주소당 연결 6개를 금방 다 쓴다. + 연결한 탭을 닫으면 다른 탭이 이어받는다. Web Locks 가 없는 브라우저는 실시간 없이 1분 확인만 한다. + 연결은 로그인한 동안 **앱 전체**에서 유지한다(설정 등 메일 밖 화면에서도 바탕화면 알림 · 연결 상태가 이어지게). 목록 맞추기 · 1분 확인 멈춤은 메일 화면이 있을 때만 한다. + - `fetch` 로 읽는다 — 브라우저의 `EventSource` 는 401 을 알 수 없어 세션을 갱신할 수 없다. 401 이면 세션을 한 번 갱신하고 다시 잇는다(갱신도 실패하면 지금의 세션 만료 흐름). + - 연결이 살아 있는 동안(75초 안에 `ping` · `state` 가 오고 `upstream: true`)은 1분 확인을 멈춘다. 75초는 요청을 시작한 때부터 센다(머리글이 오지 않는 연결도 끊게). + 끊기면(읽기 오류 포함 — 서버의 `end` 가 아닌 끝) 1분 확인으로 돌아가고 2 · 5 · 15 · 30 · 60초 간격으로 다시 잇는다. + 연결이 **자리 잡으면**(첫 `state` 뒤 `upstream: true` 인 ping 이 오거나 20초가 지나면) 처음 간격부터 다시 센다. 자리 잡은 연결의 `end` 는 바로 다시 잇는다 + (`token-expiring` 은 세션을 갱신한 뒤, `server-shutdown` 은 0~3초 흔들어 — 배포 때 모든 브라우저가 같은 순간에 붙지 않게). 그동안은 끊김으로 보지 않는다. + **자리 잡기 전에 온 `end` 는 까닭과 상관없이 실패처럼 간격을 둔다** — 열리자마자 끝나는 연결(세션 끝 직전의 짧은 토큰 · 응답을 짧게 끊는 프록시)에서 + 갱신 · 다시 잇기를 왕복 시간 속도로 되풀이하지 않게(갱신 한도는 IP 마다라 같은 사무실 직원까지 막는다). 다시 잇는 간격의 흔들림은 ±20%. + - 세션을 갱신한 **바로 뒤의 다시 잇기**가 또 401 이면 세션 만료로 본다(axios 의 401 재시도와 같다 — 그 시도가 다른 까닭으로 끝나면 이 표시는 지운다). + 갱신이 장애(5xx · 네트워크)로 실패하면 위 간격으로 기다렸다 다시 갱신해 본다. 한 이벤트가 1MB 를 넘으면 실패로 끊는다. + - 연결한 탭이 그만둘 때(닫힘 · 로그아웃 · `pagehide`)와 잠금을 이어받은 탭이 연결하기 전에 다른 탭에 끊김을 알린다 — 이어받은 탭의 연결이 열릴 때까지 모든 탭이 1분 확인으로 돌아간다 + (탭 닫기 · 탭 폐기처럼 그만두는 탭이 알리지 못한 경우도 덮는다). + 살아 있던 연결이 끊기면 1분 확인이 바로 한 번 묻고, 그 뒤 60초마다 묻는다. + - `state` 가 바뀌면 보이는 탭은 지금의 1분 확인이 하는 일(개수 · 보이는 목록 맞추기)을 그대로 하고, 숨은 탭은 `inboxUnread` 로 탭 제목만 맞춘 뒤 다시 보일 때 맞춘다. + 실시간이 부른 맞추기는 1초 모아서 한 번 한다(한 통을 읽어도 state 가 바뀐다 — 연달은 바뀜에 목록을 여러 번 받지 않게). + - **바탕화면 알림**(설정 › 메일 › 알림 — 브라우저 · 사용자마다 localStorage): 켜져 있고 브라우저가 허락했을 때, `new-mail` 을 받았는데 **웹메일을 보고 있는 탭**(보이고 초점이 있는 탭 — `visibilityState === 'visible' && document.hasFocus()`)이 하나도 없으면 알린다. + 두 번째 모니터에 웹메일을 띄워 두고 다른 창에서 일할 때도 알린다("다른 창을 보는 동안에도"). + 한 통이면 제목 = 보낸 사람(이름, 없으면 주소) · 내용 = 메일 제목(「메일 제목도 보이기」를 끄면 "새 메일이 왔습니다"), 여러 통이면 "새 메일 N통" · "박서준 · 이도윤 외 1명". + 같은 tag 로 앞 알림을 바꿔 쓴다(쌓이지 않는다). 누르면 그 탭으로 와서 메일을 연다(여러 통이면 받은편지함). 사용자가 웹메일로 돌아오면(어느 탭이든 보이고 초점이 생기면) 띄운 알림을 닫는다. + 허락은 사용자가 누른 뒤에만 묻는다(목록 위 한 번 권하기 · 설정의 켜기 칸). `new Notification()` 을 막는 브라우저(휴대폰 Chrome)는 처음 실패할 때 "지원하지 않음"으로 바꾼다. + ### 이 단계에서 하지 않는 것 -실시간 푸시(SSE · 데스크톱 알림 · 소리), 본문 안 이미지(인라인 cid — 붙여 넣은 이미지는 첨부로, 서명 그림만 예외), 외부 이미지 보기(이미지 중계), +탭을 모두 닫아도 오는 알림(웹 푸시 — 엔진은 `webpush-vapid` 를 지원한다), 알림 소리(운영체제 설정을 따른다), 휴대폰 푸시, 본문 안 이미지(인라인 cid — 붙여 넣은 이미지는 첨부로, 서명 그림만 예외), 외부 이미지 보기(이미지 중계), 여러 개의 서명, 엔진 계정 목록 기반의 사내 주소록, 자체 검색 색인(붙여 쓴 말 · 앞부분 · 제목 말머리로 찾기), 조건 검색(보낸 사람 · 기간 · 첨부 있음), 검색어 강조, 끌어다 놓아 옮기기, 메일함 옮기기(다른 상위로) · 하위 메일함까지 한꺼번에 지우기, 앱 비밀번호 · 2단계 인증(엔진은 지원한다), 부재중 응답의 서식 본문 · 사내/외부 구분, 인쇄.