generated from ttipo/project-base
docs: 앱 새 메일 알림 · 앱 설정을 계약 · 스킬 · 규칙에 적는다
계약 1절 끝의 옛 메모(/api/push-devices · { minVersion, latestVersion, storeUrl })를 확정한 경로 · 모양 · 규칙으로 바꾸고
(N · 간격 · 따라잡기 · 엔진 after 의 초 경계 · "알림이 멈췄습니다"를 보내는 까닭 표 · 실패 처리 · 실기기로 확인할 것),
"클라이언트 — 앱"에 앱이 실제로 부르는 때를 적었다. 2절 실시간 알림의 붙잡기와 6절 퇴사 마무리 목록도 더했다.
nestjs · docker 스킬과 env · 보안 · 로그 규칙에 FCM 키 자리 · 바깥 연결 · 가릴 값을 더했다.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
+117
-2
@@ -163,7 +163,120 @@
|
||||
- 로그아웃 · 다른 계정으로 로그인 · 세션 끝 뒤에 도착한 응답(로그인 · 코드 · 갱신 · me)은 쓰지 않는다 — 버린 로그인 결과에 토큰이 있으면 그 토큰으로 조용히 앱 logout 을 부른다(웹의 "로그아웃 뒤 늦은 갱신"과 같은 결).
|
||||
- 코드 단계 · 제한 단계에서 다시 쓸 비밀번호는 화면 메모리에만 10분 — 성공 · 다른 계정 · 떠남 · 제한이 풀릴 때 지운다(웹과 같다).
|
||||
|
||||
**이 절 다음(앱 첫 판 안 — 따로 차례)**: 푸시 기기 등록(`POST`·`DELETE /api/push-devices` — 앱 세션에 묶어 logout · 세션 끊기 · 비밀번호 바꾸기 · 퇴사 마무리 때 함께 지운다, 내용은 "새 메일 N통"만), 앱 최소 판 확인(공개 `GET /api/app-config` → `{ minVersion, latestVersion, storeUrl }`).
|
||||
### 앱 새 메일 알림 · 앱 설정 — 앱 1단계 (2026-11-07 정함 · 서버)
|
||||
|
||||
> 2026-11-07 사용자가 추천안대로 정했다(앞 메모의 `/api/push-devices` · `{ minVersion, latestVersion, storeUrl }` 를 이 절로 바꾼다).
|
||||
> 서버는 `modules/push`(등록 · 끝난 세션 정리 · 보내기 — 메일을 모른다) · `modules/mail/new-mail-push.service.ts`(새 메일 알아채기 · N · 간격 · 따라잡기) · `modules/app-config` 가 이 절대로 만들었다.
|
||||
> 보내는 곳은 FCM HTTP v1 하나 — 새 의존성 없이(node crypto 의 RS256 + 전역 fetch). 휴대폰 주소는 FCM 등록 토큰이 아니라 **FID**(Firebase 설치 id — v1 의 `token` 칸은 deprecated, `message.fid` 로 보낸다)다.
|
||||
|
||||
| 경로 | 본문 | 응답 |
|
||||
| --- | --- | --- |
|
||||
| `GET /api/app-config` (로그인 없음) | — | `{ minSupportedVersion: { android: "1.0.0", ios: null }, pushAvailable: boolean }` · `Cache-Control: no-store` |
|
||||
| `PUT /api/push-registrations/current` (앱 토큰만) | `{ fid, platform: "android", appVersion }` | 200 `{ updatedAt }` (ISO 8601 UTC · `Cache-Control: private, no-store`) |
|
||||
| `DELETE /api/push-registrations/current` (앱 토큰만) | — | 204 (등록이 없어도) |
|
||||
| `DELETE /api/push-registrations/current/unseen` (앱 토큰만) | — | 204 (등록이 없어도) |
|
||||
|
||||
- **app-config**: 최소 판은 서버 코드 상수(`modules/app-config/app-config.constants.ts`) — 옛 앱이 쓸 수 없게 서버 API 를 바꾸는 배포가 **같은 커밋에서** 올린다. `ios: null` 은 막지 않는다는 뜻(iOS 앱이 아직 없다).
|
||||
`pushAvailable` 은 서버가 FCM 키를 읽었는가(키가 막힌 것 — OAuth 거절 — 은 로그로만 알린다). 요청 수는 기본 한도(경로마다 IP 당 1분 100회 — 문의 연락처와 같다), 실패는 429 `REQ_001` 뿐이다.
|
||||
- **본문**: `fid` 는 영문 · 숫자 · `-` · `_` 64자 이하(Firebase 의 FID 는 22자), `platform` 은 `android` 만, `appVersion` 은 `숫자.숫자.숫자`(칸마다 1~4자리 · 앞자리 0 없이 — 앱의 `PackageInfo.version`). 모르는 칸은 400.
|
||||
- **실패**: 웹(쿠키) 세션 403 `AUTH_003`(`AppClientGuard` — 토큰은 맞지만 이 경로를 쓸 수 없는 클라이언트) · 제한된 세션(6절) 403 `AUTH_010` · 토큰 없음 · 그사이 끝난 · 끊긴 세션 401 `AUTH_001` · 본문 400 `VAL_001` · 요청 수 429 `REQ_001`. 새 오류 코드는 없다.
|
||||
- **등록 — 세션(sid)마다 한 줄**(표 `push_registrations` — 마이그레이션 `AddPushRegistrations`, 세션 표와 외래 키로 묶지 않는다). PUT 은 한 트랜잭션이다:
|
||||
그 세션이 살아 있는 · 제한 없는 앱 세션인지 보고(아니면 아무것도 바꾸지 않는다) → 같은 FID 의 다른 줄(다른 세션 · 다른 직원 — 같은 휴대폰에서 다른 직원이 로그인)을 지운다 →
|
||||
넣거나 바꾼다(FID · 플랫폼 · 판 · `updatedAt`) → 한 직원 10줄을 넘으면 가장 오래 바꾸지 않은 줄부터 지운다. 같은 FID 의 등록은 DB 잠금으로 차례가 된다(함께 등록해도 한 줄만 남는다).
|
||||
**새 줄이면 세는 시작(`seen_at`) = 지금**, 있던 줄은 세는 시작 · 마지막으로 보낸 때를 건드리지 않는다. FCM 키가 없어도(`pushAvailable: false`) 저장한다 — 키를 넣으면 다시 등록하지 않아도 알림이 시작된다.
|
||||
켬 · 끔은 휴대폰이 들고 있다 — 끄면 앱이 DELETE 한다(서버에 켜짐 칸은 없다).
|
||||
- **`/unseen`**: 그 줄의 `seen_at` = 지금(서버 시각) — "이 휴대폰에서 앱을 봤다". 다음 알림의 N 을 그때부터 센다.
|
||||
- **새 메일 알아채기**: 보낼 기기(살아 있는 · 제한 없는 앱 세션의 등록)가 있는 사내 직원은 SSE 연결이 없어도 서버가 그 직원의 엔진 연결을 **붙잡아 둔다**(2절 "실시간 알림"의 연결 — `MailEventsService.hold`, 직원마다 여전히 하나).
|
||||
판정은 웹의 `new-mail` 그대로다(받은편지함 · 초안 아님 · 읽지 않음 · 보낸 사람이 나 아님 · 본 때보다 10분 넘게 앞서 받은 메일 빼기). 붙잡을 직원은 서버가 뜰 때 · 5분마다 · 등록이 바뀐 직원은 그때 맞춘다(함께 여는 수 4).
|
||||
붙잡기에 실패하면 1 · 5 · 15 · 60분 뒤 다시(계정을 쓸 수 없음 — `AUTH_001` · 비밀번호 만료 · 잠김 · 엔진 연결이 끝남 — 은 60분부터). FCM 키가 없으면 아무도 붙잡지 않는다.
|
||||
곁 효과: 알림을 켠 직원은 웹을 닫아도 14단계 검색 색인의 실시간 맞추기가 돈다.
|
||||
- **N**: 받은편지함의 안 읽은 메일(초안 빼고) 가운데 **이 휴대폰에서 마지막으로 앱을 본 뒤**(`seen_at` — 같은 초 포함) 받은 수 — 보낼 때 엔진에 센다(기기마다). PC 에서 읽은 메일은 빠진다. **0 이면 보내지 않는다.**
|
||||
보내는 때는 웹 `new-mail` 이 있을 때뿐이다(안 읽음 표시 바꾸기 · 옛 메일 옮기기로는 보내지 않는다).
|
||||
- **간격**: 같은 직원은 30초에 한 번까지 — 첫 알림은 바로, 그사이 온 것은 30초 끝에 그때의 N 으로 한 번. 같은 직원의 보내기는 차례로 한다.
|
||||
- **따라잡기**: 붙잡을 때마다(서버를 다시 띄움 · 다시 붙잡음) 그 전 10분 안 · 마지막으로 보낸 뒤 · 본 뒤에 온 안 읽은 메일이 있으면 그 기기에 그때의 N 으로 한 번 — 이미 알린 · 이미 본 메일로는 다시 울리지 않는다.
|
||||
"보낸 뒤 · 본 뒤"는 `last_pushed_at`(그 알림의 N 을 센 때) · `seen_at` 의 **다음 초부터**다 — 받은 시각은 초라 그 초에 받은 메일은 그 알림에 이미 들었거나 이미 보았다고 본다(2026-11-07 리뷰). N 은 위처럼 `seen_at` 의 초부터 센다.
|
||||
그 초에 센 · 본 뒤 들어온 메일은 따로 새 메일 신호를 낸다 — 그 알림이 나가기 전(그 1초 안 · 30초 간격의 끝을 기다리는 사이)에 서버가 멈추면 놓친다. 따라잡기로 보내도 30초 간격에 든다.
|
||||
**엔진의 `after` 는 그 초를 넣지 않는다**("그 초보다 뒤" — RFC 8621 은 "같거나 뒤"라 적는다, 2026-11-08 개발 엔진 0.16.27 에서 확인) — 서버는 한 초 앞을 넘겨 같은 초를 센다(`jmap-mail.provider.ts`). 엔진 판을 올리면 다시 본다(같은 초를 넣게 바뀌면 따라잡기가 이미 알린 메일로 한 번 더 울린다).
|
||||
- **보내는 메시지** (FCM v1 `messages:send` 의 `message` — 서버 `modules/push/fcm/fcm-message.ts` 한 곳, 받는 것은 FID 와 개수뿐):
|
||||
```json
|
||||
{
|
||||
"fid": "<FID>",
|
||||
"notification": { "title": "새 메일 3통" },
|
||||
"data": { "module": "mail", "kind": "new-mail" },
|
||||
"android": {
|
||||
"collapse_key": "new-mail",
|
||||
"priority": "HIGH",
|
||||
"ttl": "86400s",
|
||||
"notification": {
|
||||
"tag": "new-mail",
|
||||
"channel_id": "new_mail",
|
||||
"notification_priority": "PRIORITY_HIGH",
|
||||
"visibility": "PUBLIC",
|
||||
"notification_count": 3
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
글은 제목 "새 메일 N통"뿐이다(천 단위 쉼표 — 원본은 서버 `modules/push/push-text.ts`). 주소 · 이름 · 보낸 사람 · 제목 · 메일 id · 세션 id 는 싣지 않는다 — 누르면 늘 메일 탭 받은편지함(`data.module`).
|
||||
꺼져 있던 휴대폰은 마지막 것 하나만 받고(`collapse_key`), 24시간이 지나면 버린다(`ttl`).
|
||||
- **그 휴대폰을 직원이 건드리지 않았는데 앱 세션이 끝나면** 5분 정리가 그 등록을 지운 뒤(`DELETE … RETURNING` — 한 번만) 그 휴대폰에 한 번 보낸다 — `kind: "session-ended"`, 같은 tag · 채널 · `collapse_key`(옛 "새 메일 N통"을 바꿔 쓴다), `priority: NORMAL`,
|
||||
글은 보드 `App1Push` ⑧(2026-11-07 확정) 그대로 — 제목 "새 메일 알림이 멈췄습니다" · 내용 "안전을 위해 로그아웃했습니다. 앱을 열어 다시 로그인해 주세요."(원본 `push-text.ts`).
|
||||
끝난 까닭(세션 표의 `revoked_reason`)마다 — 표는 서버 `modules/push/push.constants.ts` 의 `SESSION_ENDED_NOTICE` 한 곳(까닭을 더하면 거기서 정해야 컴파일된다):
|
||||
|
||||
| 끝난 까닭 | 값 | 알림 |
|
||||
| --- | --- | --- |
|
||||
| 저절로 끝남(미사용 14일 · 절대 30일) | 없음(끊기지 않았다) | 보낸다 |
|
||||
| 다른 곳에서 비밀번호를 바꿈 · 바꾸다 결과를 알 수 없게 됨 | `password-changed` · `password-change-uncertain` | 보낸다 |
|
||||
| 앱 갱신 유예(회전 응답을 잃었거나 토큰이 두 번 쓰였다) | `app-refresh-grace` | 보낸다 |
|
||||
| 리프레시 토큰 재사용 감지 | `refresh-token-reused` | 보낸다 |
|
||||
| 그 휴대폰에서 로그아웃 | `logout` | 보내지 않는다 |
|
||||
| 로그인된 기기에서 끊기 · 다른 곳 모두 로그아웃 | `ended-by-user` | 보내지 않는다 |
|
||||
| 운영 도구(재설정 · 잠금 · 이전용 비밀번호 다시 만들기 · 이전 마치기 · 퇴사 마무리) | `ended-by-operator` | 보내지 않는다 |
|
||||
| 계정을 쓸 수 없음(갱신 때) | `account-unavailable` | 보내지 않는다 |
|
||||
| 2단계를 켜 끊긴 세션(제한된 세션) | `two-factor-not-verified` | 보내지 않는다 |
|
||||
|
||||
제한이 붙은 세션 · 까닭이 없거나 표에 없는 값(다른 판이 남긴 줄) · 세션 줄이 없는 등록에는 보내지 않는다.
|
||||
보내기 직전 그 FID 를 다른 등록이 쓰고 있으면(그 휴대폰에서 다른 직원 · 같은 직원이 다시 로그인했다) 보내지 않는다.
|
||||
- **등록이 사라지는 길**: 보낼 때는 살아 있는 · 제한 없는 앱 세션의 줄만 고른다 — 끊은 세션(위 표의 까닭 모두)은 DB 의 `revoked_at` 으로 **바로** 빠진다(15초 기억과 상관없이).
|
||||
끝난 세션의 줄은 5분 정리(`modules/push/push-cleanup.service.ts`)가 지운다 — 위 표에서 "보낸다"인 세션의 휴대폰에만 지운 뒤 한 번 알린다. 제한이 붙은 세션의 줄은 남는다(보내지 않는다 — 풀리면 다시). 같은 휴대폰에서 다른 직원이 등록하면 앞 줄을 지운다(앞 직원의 붙잡기도 다시 고른다 — 알리지 않는다).
|
||||
**보내기 직전(다시 하기마다) 그 세션의 줄이 아직 그 FID 이고 세션이 살아 있는지 다시 본다** — 센 뒤 · 다시 하기를 기다리는 사이 그 휴대폰이 다른 직원에게 옮겨 갔으면 보내지 않는다.
|
||||
FCM 이 받지 않는 휴대폰(아래 "실패")은 그 줄을 지운다 — 그사이 PUT 이 FID 를 바꿨으면 둔다. 퇴사 마무리(6절)는 그 주소의 줄을 한 트랜잭션에 지운다("앱 알림 등록").
|
||||
- **FCM 의 실패**(`modules/push/fcm/fcm-sender.ts`): `UNREGISTERED`(404) · `SENDER_ID_MISMATCH`(403) · `message.fid` 칸의 `INVALID_ARGUMENT`(400) → 그 줄을 지운다 /
|
||||
401 → access token 을 버리고 새 토큰으로 한 번 더 / 429(`QUOTA_EXCEEDED`) → 바로 다시 보내지 않는다 — `Retry-After` 가 있고 남은 시간 안이면 그때 한 번, 아니면 이번 알림은 버린다(다음 새 메일이 그때의 N 으로 다시 간다 — 같은 직원은 30초에 한 번이라 한도에 닿을 일이 드물다) /
|
||||
5xx · 연결 오류 · 시간 초과 → 다시(1 · 4초 ±20% — 응답의 `Retry-After` 는 남은 시간 안에서만, 시도 3번 · 시도마다 10초 · 한 번 보내기 20초 안) /
|
||||
그 밖(400 · 403 `PERMISSION_DENIED` · 오류 코드 없는 404 · `THIRD_PARTY_AUTH_ERROR` · 3xx · OAuth 거절) → 지우지 않는다(프로젝트 id 가 틀린 404 로 모든 등록을 지우지 않게) · error. 서버 전체에서 함께 보내는 수 4.
|
||||
OAuth access token 은 만료 5분 전까지 기억한다(함께 부르면 교환은 한 번).
|
||||
- **로그**: FID(지문도) · 서비스 계정 키 · 개인 키 · JWT · access token · FCM · Google 의 오류 설명 글은 남기지 않는다 — 상태 · 오류 코드만, 주소는 가린다(`maskEmail`).
|
||||
보낸 것은 debug("앱 알림: user=a***@…, 기기 1, N 3"), 끝내 못 보냄은 warn · 설정 오류는 error(종류마다 한 시간에 한 번 — 묻힌 수를 다음 줄에), 붙잡기가 이어서 실패하면 warn 한 번 · 다시 되면 info 한 번.
|
||||
서버 종료로 끊긴 보내기는 로그를 남기지 않는다(배포마다 남는 잡음이 되지 않게).
|
||||
- **환경 변수**: `backend/.env.{env}` 의 `FCM_SERVICE_ACCOUNT_KEY` = 서비스 계정 JSON 키 파일을 통째로 base64 한 줄(JWT_SECRET 과 같은 자리 — compose 는 바꿀 것이 없다, 매일 백업에 함께 든다).
|
||||
비면 알림만 끈다(기동은 한다 — 운영 필수가 아니다, 기동 로그 warn). 값이 있으면 **모든 환경에서** 모양(base64 → JSON → `service_account` · `project_id` · `client_email` · `private_key_id` · PEM RSA 2048 이상 · `token_uri`)을 보고 틀리면 기동하지 않는다(어느 칸이 틀렸는지만 — 값은 싣지 않는다).
|
||||
켜지면 기동 로그 한 줄 "앱 새 메일 알림: 켬 (Firebase 프로젝트 <id>)". 서비스 계정은 알림 전용 하나 + 역할 "Firebase Cloud Messaging API Admin" 하나, 키는 개발 · 운영 따로.
|
||||
- **운영 확인**: backend 컨테이너 → `oauth2.googleapis.com:443` · `fcm.googleapis.com:443`(외부 이미지 중계와 같은 길 — 호스트 방화벽 · NCP ACG 의 나가는 규칙), 서버 시계(NTP — JWT 의 `iat`, 틀리면 토큰 교환이 거절된다).
|
||||
- **실기기 · 실제 키로 확인할 것**(첫 보내기 때): 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` 의 런처 배지(삼성).
|
||||
- **알려진 한계**: Google 은 기기마다 언제 몇 통(N)이 왔는지는 안다(내용 · 주소는 가지 않는다). 푸시는 최선을 다할 뿐이다(FCM 장애 · 강제 종료한 앱 · 배터리 제한). 웹에서 다 읽어도 이미 뜬 알림은 다음 알림 · 앱을 열 때까지 그대로다.
|
||||
나에게 보낸 메일도 N 에 든다(그 메일이 알림의 방아쇠가 되지는 않는다). 서버가 10분 넘게 멈췄으면 그동안 온 메일은 다음 알림의 N 으로만 알린다.
|
||||
**인스턴스를 늘리면 알림이 겹친다**(붙잡기 · 간격이 프로세스 메모리 — nestjs 스킬의 Redis 목록).
|
||||
|
||||
**클라이언트 — 앱**(Flutter — 앱 디자인 README "새 메일 알림 · 업데이트 · 앱 아이콘", 2026-11-07)
|
||||
- 등록은 FCM 의 FID 로 한다(`register()` · `onRegistered` — 자동 초기화는 끈다). 보낼 조건 = 받을 수 있는 휴대폰 ∧ `pushAvailable` 이 거짓이 아님(모르면 보낼 수 있는 것으로) ∧ 메일을 쓸 수 있는 세션(제한 단계가 아님) ∧ 이 휴대폰에서 켬(처음 켜짐) ∧ 알림 권한 ∧ 새 메일 채널.
|
||||
`pushAvailable: false` 면 PUT 하지 않는다 — 그 전에 등록된 줄은 이어지고, 새 휴대폰은 앱이 앱 설정을 다시 받은 뒤(앞으로 올 때 — 1시간마다) 등록한다.
|
||||
- **PUT** 은 FID 를 받았을 때(등록 뒤 첫 소식 · FID 가 바뀜) · 세션이 열릴 때마다 한 번(앱을 띄울 때 — 지난 실행의 FID 로 바로) · 설정 스위치를 켤 때만 보낸다. `appVersion` 은 앱 판(모르면 `0.0.0`).
|
||||
스위치로 켤 때만 실패를 알리고(그 코드의 사전 문구) 이 휴대폰의 값을 끔으로 둔다(처음 값 켬에서 허락하고 켰어도 — 스위치는 꺼진 채, 저절로 다시 하지 않는다).
|
||||
저절로 보낸 PUT 의 실패는 조용히 다음 기회(앞으로 옴 · FID 바뀜 · 다음 로그인)에 다시 한다 — 등록이 된 뒤로는 앞으로 와도 보내지 않는다.
|
||||
- **DELETE** 는 설정 스위치를 끌 때(지운 뒤 꺼진다 — 실패면 켬 그대로, 알린다)와 앞으로 올 때 권한 · 새 메일 채널이 꺼진 것을 알았을 때(조용히)만.
|
||||
**로그아웃 · 세션 끝에는 부르지 않는다** — 앱은 토큰을 지운 뒤 서버 logout 과 함께 알림창을 지우고 FCM `unregister()` 로 FID 를 버린다(서로 기다리지 않는다).
|
||||
풀지 못하면 다음 실행 · 로그아웃 상태로 앞으로 올 때 다시 푼다. 그사이 같은 휴대폰에서 다른 직원이 로그인하면 풀기 뒤에 다시 등록해 같은 FID 로 PUT 한다(서버가 같은 FID 의 앞 줄을 지운다).
|
||||
그 밖의 앞 직원 줄은 서버가 지운다 — 끊은 세션은 보낼 때 바로 빠지고, 끝난 세션의 줄은 5분 정리가 지운다.
|
||||
- **unseen** 은 앱이 앞으로 올 때와 앞에서 새 메일 알림을 받았을 때 보낸다 — 30초에 한 번(그사이 다시 오면 끝에 한 번 더), 조용히. 앱이 앞에 있을 때만 보내고, 끝을 기다리는 동안 뒤로 가면 그 자리에서 보낸다(N 을 "마지막으로 본 뒤"로 세게).
|
||||
- 세 경로의 404 `RES_001`(그 경로가 없는 옛 서버)은 조용히 넘기고 등록 없이 지낸다.
|
||||
- **app-config** 는 앱을 띄우자마자 한 번 묻는다 — 인증 인터셉터 줄 밖이라 세션 되살리기(미리 갱신)를 기다리지 않는다. 앞으로 올 때 마지막으로 받은 지 1시간이 지났으면 다시(실패는 1분 간격으로 다시 — 알리지 않는다).
|
||||
404 는 옛 서버(최소 판 없음 · `pushAvailable` 거짓). 이 앱의 판이 `minSupportedVersion.android` 보다 낮으면 어느 화면에서든(로그인 전 포함) 업데이트 화면이다(쓰던 메일은 임시보관함에 저장) — 모양이 틀리거나 받지 못했거나 이 앱의 판을 모르면 막지 않는다. 서버가 최소 판을 내리면 다음 확인에서 풀린다.
|
||||
- **받는 알림**: 앱이 앞에 있으면 휴대폰이 그리지 않는다 — 앱은 앞에서 받은 `new-mail` 로 새 메일을 바로 확인하고(2절 "새 메일 확인"의 클라이언트 — 앱), 앞으로 올 때 알림창의 `new-mail` 알림(tag)을 지운다.
|
||||
누르면 늘 메일 탭 받은편지함이다 — `kind` 로 가르지 않는다(`session-ended` 를 눌렀으면 받은편지함의 첫 요청이 세션 끝을 알아 로그인 화면으로 간다).
|
||||
작은 아이콘 · 색은 서버가 싣지 않는다 — 앱 매니페스트의 기본값(`ic_stat_ara` · #0E7C86)이고, 채널 `new_mail`(이름 "새 메일" · 중요도 높음)은 앱이 만든다.
|
||||
|
||||
---
|
||||
|
||||
@@ -1020,6 +1133,8 @@ data: {"reason":"token-expiring"}
|
||||
스트림이 열린 뒤 계정을 더 쓸 수 없게 되면(엔진이 대리 접근을 거절) `end` 없이 닫는다 — 클라이언트는 끊김으로 보고 다시 이어 401 → 갱신 실패 → 세션 만료로 간다.
|
||||
|
||||
- **서버**: 직원마다 엔진 연결 하나를 그 직원의 모든 연결에 나눠 쓴다(마지막 연결이 닫히고 30초 뒤 닫는다 — 새로 고침에 다시 열지 않게).
|
||||
**앱 새 메일 알림을 켠 직원은 연결이 없어도 엔진 연결을 붙잡는다**(앱 1단계 — 1절 "앱 새 메일 알림 · 앱 설정"): 같은 엔진 연결을 함께 쓰고(직원마다 여전히 하나) 연결 상한(5)에 세지 않는다.
|
||||
붙잡은 쪽은 새 메일 신호(같은 판정 — 보낸 사람 · 제목 없이)와 끝남만 받는다. 놓으면 남은 연결이 없을 때 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` 없이 — 엔진 한도 · 엔진이 내려간 동안은 세지 않는다).
|
||||
@@ -2743,7 +2858,7 @@ interface FileUploadCreate {
|
||||
만료 뒤에는 대리 접근 메서드가 막히고, 알림이 말하는 일(로그아웃 · 앱 비밀번호 지움)은 넣기 전에 끝나 있어야 하기 때문이다. 첫 세션 끊기가 실패하면 알림을 넣기 전에 멈추고,
|
||||
엔진 바꾸기가 실패하면 넣은 알림 메일을 지운다. 마지막 끊기는 그사이 옛 비밀번호로 새로 로그인한 세션을 위한 것이다.
|
||||
- 잠금: 받는 메일 넘기기 · 자동 응답(대리 접근으로 Sieve)을 먼저 걸고 → `authenticate` 빼기 → 세션 끊기. 뺀 뒤에는 대리 접근이 막혀 Sieve 를 새로 걸 수 없다(이미 건 Sieve 는 계속 돈다).
|
||||
- 퇴사 마무리: 엔진 계정을 지운 뒤 우리 DB 에서 그 주소의 행을 한 트랜잭션으로 지운다 — 로그인 기록 · 앱 비밀번호 사용 · 2단계 설정 · 서명 · 설정 · 사내 목록 기록 · 대용량 파일 · 링크,
|
||||
- 퇴사 마무리: 엔진 계정을 지운 뒤 우리 DB 에서 그 주소의 행을 한 트랜잭션으로 지운다 — 로그인 기록 · 앱 비밀번호 사용 · 2단계 설정 · 서명 · 설정 · 사내 목록 기록 · 대용량 파일 · 링크 · 검색 색인 · 앱 알림 등록(앱 1단계 — 휴대폰 주소),
|
||||
그리고 2단계 필수의 예외 주소 목록과 공용 주소(MailingList)의 구성원에서도 뺀다(실행 전 보여 주는 바뀔 것에 든다).
|
||||
세션 행은 바로 지우지 않는다 — 끊은 기록을 서버가 15초 동기화로 읽어야 하므로 끝난 세션 정리 cron 에 맡긴다(로그인된 기기 목록은 살아 있는 세션만 보인다).
|
||||
디스크 파일(대용량 · 서명 그림)은 그 뒤에 지우고 못 지운 것은 정리 cron 에 맡긴다. 실행 기록(`ops_audit_log`)은 남기고, 읽기만 하는 현황은 기록하지 않는다.
|
||||
|
||||
Reference in New Issue
Block a user