From 57340d43df635e24259840ac7fe73747e5f6ba3e Mon Sep 17 00:00:00 2001 From: ttipo Date: Sun, 8 Nov 2026 11:56:52 +0900 Subject: [PATCH] =?UTF-8?q?docs:=20=EC=95=B1=20=EC=95=8C=EB=A6=BC=20?= =?UTF-8?q?=EC=8B=A4=EA=B8=B0=EA=B8=B0=20=ED=99=95=EC=9D=B8=20=EA=B2=B0?= =?UTF-8?q?=EA=B3=BC=EC=99=80=20FID=20=EB=93=B1=EB=A1=9D=20=ED=91=9C?= =?UTF-8?q?=EC=8B=9C=20=C2=B7=20=EB=B4=A4=EC=9D=8C=201.5=EC=B4=88=EB=A5=BC?= =?UTF-8?q?=20=EA=B3=84=EC=95=BD=EC=97=90=20=EC=A0=81=EB=8A=94=EB=8B=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 2026-11-08 갤럭시 S23(Android 16)에서 FCM 보내기 · 표시 · 같은 tag 바꿔 쓰기 · 닫힌 앱 깨우기 · 누르면 받은편지함을 확인했다. 앱이 고친 두 가지(매니페스트의 firebase_messaging_installation_id_enabled · 보는 중 받은 알림 뒤의 봤음 1.5초)를 "클라이언트 — 앱"에, 같은 초를 세는 N 의 곁 효과를 N 줄에, 옛 Google Play 서비스 휴대폰의 등록 한계를 알려진 한계에 적었다. Co-Authored-By: Claude Opus 5.5 --- docs/mail/api-contract.md | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/docs/mail/api-contract.md b/docs/mail/api-contract.md index d644493..19261c4 100644 --- a/docs/mail/api-contract.md +++ b/docs/mail/api-contract.md @@ -191,6 +191,7 @@ 붙잡기에 실패하면 1 · 5 · 15 · 60분 뒤 다시(계정을 쓸 수 없음 — `AUTH_001` · 비밀번호 만료 · 잠김 · 엔진 연결이 끝남 — 은 60분부터). FCM 키가 없으면 아무도 붙잡지 않는다. 곁 효과: 알림을 켠 직원은 웹을 닫아도 14단계 검색 색인의 실시간 맞추기가 돈다. - **N**: 받은편지함의 안 읽은 메일(초안 빼고) 가운데 **이 휴대폰에서 마지막으로 앱을 본 뒤**(`seen_at` — 같은 초 포함) 받은 수 — 보낼 때 엔진에 센다(기기마다). PC 에서 읽은 메일은 빠진다. **0 이면 보내지 않는다.** + 같은 초를 넣으므로 그 메일과 같은 초에 `/unseen` 을 받으면 그 메일은 다음 알림에 다시 든다 — 앱은 앞에서 받은 알림 뒤의 `/unseen` 을 1.5초 늦춘다(아래 "클라이언트 — 앱"). 보내는 때는 웹 `new-mail` 이 있을 때뿐이다(안 읽음 표시 바꾸기 · 옛 메일 옮기기로는 보내지 않는다). - **간격**: 같은 직원은 30초에 한 번까지 — 첫 알림은 바로, 그사이 온 것은 30초 끝에 그때의 N 으로 한 번. 같은 직원의 보내기는 차례로 한다. - **따라잡기**: 붙잡을 때마다(서버를 다시 띄움 · 다시 붙잡음) 그 전 10분 안 · 마지막으로 보낸 뒤 · 본 뒤에 온 안 읽은 메일이 있으면 그 기기에 그때의 N 으로 한 번 — 이미 알린 · 이미 본 메일로는 다시 울리지 않는다. @@ -259,12 +260,17 @@ - **실기기 · 실제 키로 확인할 것**(첫 보내기 때): FCM 이 `message.fid` 와 메시지 칸(`priority "HIGH"` · `notification_priority` · `visibility` · `notification_count` · `ttl` 글)을 받는지, 오류 본문의 모양(`UNREGISTERED` · `SENDER_ID_MISMATCH` · 429 · 503 의 `Retry-After`) — **잘못된 FID 를 `message.fid` 가 아닌 칸(예: `message.token`)으로 알리면 지금은 그 줄을 지우지 않고 설정 오류(error — 한 시간에 한 번)로만 남긴다**(안전한 쪽 — 오류 본문을 보고 칸 이름을 맞춘다), 실기기의 FID 길이 · 글자(22자 base64url 로 본다), 같은 tag 로 바꿔 쓴 알림이 다시 울리는지 · `notification_count` 의 런처 배지(삼성). + **2026-11-08 갤럭시 S23(Android 16 · Google Play 서비스 26.42)에서 확인한 것**: FCM 이 `message.fid` 와 메시지 칸을 받았다(오류 없음 — 배달된 그 초에 보내 1초 안에 표시), FID 22자, + 같은 tag 로 바꿔 씀(알림 하나가 "새 메일 2통" · 개수 2로), 중요도 높음 · 잠금 화면 표시 · 강조색 · 작은 아이콘, 앱이 앞에 있으면 그리지 않음, + 앱을 닫아 프로세스가 없어도 휴대폰이 앱을 깨워 표시, 누르면 받은편지함(닫힌 앱도). 아직 보지 않은 것: 오류 본문의 모양 · 바꿔 쓴 알림이 다시 울리는지 · 런처 배지 · 절전(Doze) 중 도착. - **알려진 한계**: Google 은 기기마다 언제 몇 통(N)이 왔는지는 안다(내용 · 주소는 가지 않는다). 푸시는 최선을 다할 뿐이다(FCM 장애 · 강제 종료한 앱 · 배터리 제한). 웹에서 다 읽어도 이미 뜬 알림은 다음 알림 · 앱을 열 때까지 그대로다. 나에게 보낸 메일도 N 에 든다(그 메일이 알림의 방아쇠가 되지는 않는다). 서버가 10분 넘게 멈췄으면 그동안 온 메일은 다음 알림의 N 으로만 알린다. **인스턴스를 늘리면 알림이 겹친다**(붙잡기 · 간격이 프로세스 메모리 — nestjs 스킬의 Redis 목록). + Google Play 서비스가 26.12(versionCode 261200000)보다 옛 휴대폰은 SDK 가 FID 대신 옛 토큰 길로 돌아가 등록 소식의 값이 FID 모양이 아닐 수 있다 — `:` 가 섞이고 64자를 넘으면 + `PUSH_FID_PATTERN` 에 걸려 PUT 이 400 `VAL_001` 이다(스위치로 켜면 그 사전 문구, 저절로 보낸 PUT 은 앞으로 올 때마다 다시). 후속 거리: 앱이 FID 모양이 아닌 값을 받으면 "이 휴대폰에서는 받을 수 없음"으로 둔다. **클라이언트 — 앱**(Flutter — 앱 디자인 README "새 메일 알림 · 업데이트 · 앱 아이콘", 2026-11-07) -- 등록은 FCM 의 FID 로 한다(`register()` · `onRegistered` — 자동 초기화는 끈다). 보낼 조건 = 받을 수 있는 휴대폰 ∧ `pushAvailable` 이 거짓이 아님(모르면 보낼 수 있는 것으로) ∧ 메일을 쓸 수 있는 세션(제한 단계가 아님) ∧ 이 휴대폰에서 켬(처음 켜짐) ∧ 알림 권한 ∧ 새 메일 채널. +- 등록은 FCM 의 FID 로 한다(`register()` · `onRegistered` — 자동 초기화는 끈다). 안드로이드는 매니페스트에 `firebase_messaging_installation_id_enabled` = `true` 가 있어야 한다 — 없으면 SDK(firebase-messaging 26.0.1)가 `register()` · `unregister()` 를 곧바로 거절한다(2026-11-08 실기기). 보낼 조건 = 받을 수 있는 휴대폰 ∧ `pushAvailable` 이 거짓이 아님(모르면 보낼 수 있는 것으로) ∧ 메일을 쓸 수 있는 세션(제한 단계가 아님) ∧ 이 휴대폰에서 켬(처음 켜짐) ∧ 알림 권한 ∧ 새 메일 채널. `pushAvailable: false` 면 PUT 하지 않는다 — 그 전에 등록된 줄은 이어지고, 새 휴대폰은 앱이 앱 설정을 다시 받은 뒤(앞으로 올 때 — 1시간마다) 등록한다. - **PUT** 은 FID 를 받았을 때(등록 뒤 첫 소식 · FID 가 바뀜) · 세션이 열릴 때마다 한 번(앱을 띄울 때 — 지난 실행의 FID 로 바로) · 설정 스위치를 켤 때만 보낸다. `appVersion` 은 앱 판(모르면 `0.0.0`). 스위치로 켤 때만 실패를 알리고(그 코드의 사전 문구) 이 휴대폰의 값을 끔으로 둔다(처음 값 켬에서 허락하고 켰어도 — 스위치는 꺼진 채, 저절로 다시 하지 않는다). @@ -273,7 +279,9 @@ **로그아웃 · 세션 끝에는 부르지 않는다** — 앱은 토큰을 지운 뒤 서버 logout 과 함께 알림창을 지우고 FCM `unregister()` 로 FID 를 버린다(서로 기다리지 않는다). 풀지 못하면 다음 실행 · 로그아웃 상태로 앞으로 올 때 다시 푼다. 그사이 같은 휴대폰에서 다른 직원이 로그인하면 풀기 뒤에 다시 등록해 같은 FID 로 PUT 한다(서버가 같은 FID 의 앞 줄을 지운다). 그 밖의 앞 직원 줄은 서버가 지운다 — 끊은 세션은 보낼 때 바로 빠지고, 끝난 세션의 줄은 5분 정리가 지운다. -- **unseen** 은 앱이 앞으로 올 때와 앞에서 새 메일 알림을 받았을 때 보낸다 — 30초에 한 번(그사이 다시 오면 끝에 한 번 더), 조용히. 앱이 앞에 있을 때만 보내고, 끝을 기다리는 동안 뒤로 가면 그 자리에서 보낸다(N 을 "마지막으로 본 뒤"로 세게). +- **unseen** 은 앱이 앞으로 올 때와 앞에서 새 메일 알림을 받았을 때 보낸다 — 30초에 한 번(그사이 다시 오면 끝에 한 번 더), 조용히. 앱이 앞에 있을 때만 보내고, 기다리는 동안(30초 간격 끝 · 아래 1.5초) 뒤로 가면 그 자리에서 보낸다(N 을 "마지막으로 본 뒤"로 세게). + 앞에서 받은 알림 뒤의 unseen 은 **받은 때부터 1.5초가 지나기 전에는 보내지 않는다** — 서버가 `seen_at` 의 초부터 같은 초를 세므로, 그 알림을 일으킨 메일과 같은 초에 보내면 목록으로 보고 열지 않은 그 메일이 다음 알림에 다시 세어진다(2026-11-08 실기기 — 한 통이 "2통"이 됨). + 그 1.5초 안에 뒤로 가면 그 자리에서 보내므로, 그 좁은 경우에는 같은 초가 되어 그 메일이 다음 알림에 다시 들 수 있다(받아들였다). - 세 경로의 404 `RES_001`(그 경로가 없는 옛 서버)은 조용히 넘기고 등록 없이 지낸다. - **app-config** 는 앱을 띄우자마자 한 번 묻는다 — 인증 인터셉터 줄 밖이라 세션 되살리기(미리 갱신)를 기다리지 않는다. 앞으로 올 때 마지막으로 받은 지 1시간이 지났으면 다시(실패는 1분 간격으로 다시 — 알리지 않는다). 404 는 옛 서버(최소 판 없음 · `pushAvailable` 거짓). 이 앱의 판이 `minSupportedVersion.android` 보다 낮으면 어느 화면에서든(로그인 전 포함) 업데이트 화면이다(쓰던 메일은 임시보관함에 저장) — 모양이 틀리거나 받지 못했거나 이 앱의 판을 모르면 막지 않는다. 서버가 최소 판을 내리면 다음 확인에서 풀린다.