---
template: policy-doc@1
doc_id: 04-messaging
title: 메시징·푸시 연동 — 인덱스가 보지 못하는 층
config: ../_meta/template-config.yaml
last_verified: 4d798f55
policies:
  - id: messaging:aligo:timeout
    status: conditional-unknown
    evidence: [A-vendor, S-constant]
    unknown_reason: 요청 만료 시점을 정하는 층이다. 만료 뒤 호출자가 어느 분기로 가는가는 동결 stg 4d798f55 의 코드로 확정되지만, 그 분기가 발효하려면 이 연동이 먼저 서야 한다 — 그 조건은 이 벤더 설정 블록의 환경 키 주입이고 키 값은 저장소 밖에 있다. 재유도된 게이트 종류는 `env` 이고 닫을 경로가 이 PRD 범위 밖이다. 만료 값 자체는 이 문서가 옮기지 않는다(30-contracts-limits 소유).
    anchors:
      - insuniverse-server/libs/aligo/src/aligo.service.ts:68
      - insuniverse-server/libs/aligo/src/aligo.service.ts:95
      - insuniverse-server/libs/aligo/src/aligo.service.ts:121
  - id: messaging:aligo:fallback
    status: conditional-unknown
    evidence: [A-vendor, S-constant]
    unknown_reason: 폴백이 없다는 것이 이 층의 내용이다 — 멀티 공급자 폴백은 진료수집 축 전용이고 이 벤더는 디스패처 풀에 없다. **관측된 부재이지 미조사가 아니다.** 그 부재가 운영에서 유지되는지는 이 연동이 실제로 서 있는가에 걸린다 — 그 조건은 이 벤더 설정 블록의 환경 키 주입이고 키 값은 저장소 밖에 있다. 재유도된 게이트 종류는 `env` 이고 닫을 경로가 이 PRD 범위 밖이다.
    anchors:
      - insuniverse-server/libs/aligo/src/aligo.service.ts:68
      - insuniverse-server/libs/aligo/src/aligo.service.ts:95
      - insuniverse-server/libs/aligo/src/aligo.service.ts:121
  - id: messaging:aligo:normalization
    status: conditional-unknown
    evidence: [A-vendor, S-constant]
    unknown_reason: 정규화 매퍼가 없다는 것이 이 층의 내용이다 — 응답을 그대로 응답 DTO 로 옮기거나 자기 형태를 정본으로 쓴다. **관측된 부재다.** 그 형태가 발효하려면 이 연동이 서야 한다 — 그 조건은 이 벤더 설정 블록의 환경 키 주입이고 키 값은 저장소 밖에 있다. 재유도된 게이트 종류는 `env` 이고 닫을 경로가 이 PRD 범위 밖이다.
    anchors:
      - insuniverse-server/libs/aligo/src/aligo.service.ts:68
      - insuniverse-server/libs/aligo/src/aligo.service.ts:95
      - insuniverse-server/libs/aligo/src/aligo.service.ts:121
  - id: messaging:aligo:exception
    status: conditional-unknown
    evidence: [A-vendor, S-constant]
    unknown_reason: 호출 실패를 어느 예외 형태로 감싸는가를 정하는 층이다. 예외 클래스 이름과 HTTP 상태는 코드로 확정되지만 그 예외가 던져지려면 이 연동이 서야 한다 — 그 조건은 이 벤더 설정 블록의 환경 키 주입이고 키 값은 저장소 밖에 있다. 재유도된 게이트 종류는 `none` 이고 닫을 경로가 이 PRD 범위 밖이다. 게이트 종류가 `none` 인 것은 이 층의 앵커 파일에서 DB 토큰도 env 토큰도 관측되지 않았다는 뜻이지 미명세가 아니다.
    anchors:
      - insuniverse-server/libs/aligo/src/aligo.exception.ts:3
  - id: messaging:aligo:env_gates
    status: conditional-unknown
    evidence: [A-vendor, S-constant]
    unknown_reason: 이 벤더 연동이 서려면 설정 블록의 환경 키가 주입돼야 한다는 것이 이 층의 내용이다. 키 **이름**은 런타임 인덱스가 열거하지만 값은 저장소 밖이고 이 문서도 이름을 나열하지 않는다(11-index-code · 12-index-runtime 소유). 재유도된 게이트 종류는 `env` 이고 닫을 경로가 이 PRD 범위 밖이다.
    anchors:
      - insuniverse-server/apps/api/src/config/configuration.ts:111
  - id: messaging:fcm:credentials
    status: conditional-unknown
    evidence: [S-constant, A-comment]
    unknown_reason: 푸시 발송에 필요한 자격증명 세 키가 설정 블록에서 환경 변수로 읽히고, 그 값으로 프로세스 전역 앱이 한 번만 초기화된다. 키 값이 저장소 밖이라 초기화가 실제로 성립하는지 확정되지 않는다. 닫을 경로가 이 PRD 범위 밖이다.
    limitations:
      - 이 연동에는 `timeout` · `normalization` · `fallback` 층이 코드에 없다. 전송을 벤더 SDK 가 소유하고 이 저장소는 초기화와 호출만 갖기 때문이며, **관측된 부재이지 미조사가 아니다.** 다른 여덟 벤더는 그 세 층을 런타임 인덱스가 지지만 이 연동은 인덱스 항목이 아니라 층을 승계할 자리도 없다
    anchors:
      - insuniverse-server/apps/api/src/config/configuration.ts:135
      - insuniverse-server/apps/api/src/modules/fcm/fcm.service.ts:28
  - id: messaging:fcm:token-lifecycle
    status: conditional-unknown
    evidence: [S-constant, A-comment]
    unknown_reason: 발송 응답이 미등록 토큰 오류를 내면 그 토큰을 저장소에서 지우고 예외를 위로 전파하지 않는다. 이 규칙은 코드로 확정되지만 발송 자체가 자격증명 주입에 걸려 있어 발효가 미판정이다. 닫을 경로가 이 PRD 범위 밖이다.
    anchors:
      - insuniverse-server/apps/api/src/modules/fcm/fcm.service.ts:11
      - insuniverse-server/apps/api/src/modules/fcm/fcm.service.ts:68
  - id: messaging:fcm:exception
    status: conditional-unknown
    evidence: [S-constant, A-comment]
    unknown_reason: 이 연동의 예외 형태가 다른 세 비-진료 벤더의 예외와 같은 모양이다 — 전용 예외 클래스 하나에 서버 내부 오류 상태를 실어 던진다. **인덱스가 이 벤더를 담지 않는데도 형태는 같다**는 것이 이 정책의 내용이다. 발효는 발송 경로가 서는가에 걸리고 그 조건이 저장소 밖이다. 게이트 종류가 `none` 인 것은 이 층의 앵커 파일에서 DB 토큰도 env 토큰도 관측되지 않았다는 뜻이지 미명세가 아니다. 닫을 경로가 이 PRD 범위 밖이다.
    anchors:
      - insuniverse-server/apps/api/src/modules/fcm/fcm.exception.ts:3
  - id: messaging:push.scheduler:cron
    status: conditional-unknown
    evidence: [S-constant, A-comment]
    unknown_reason: 예약 푸시 발송이 인프로세스 주기 선언으로 돈다. 주기와 시간대는 코드에 있고 **걸 게이트가 없다** — 배치표의 이 행은 `게이트 유무` 가 `없음` 이다. 그래서 미판정의 원인은 게이트 값 미상이 아니라 그 프로세스가 운영에 서 있다는 증거 부재이며, 그 증거는 저장소 밖에 있다. 닫을 경로가 이 PRD 범위 밖이다.
    anchors:
      - insuniverse-server/apps/api/src/modules/push/push.scheduler.ts:15
  - id: messaging:aligo:local-mock
    status: conditional-unknown
    evidence: [S-constant, A-comment]
    unknown_reason: 환경값이 로컬이면 실제 발송을 건너뛰고 성공 응답 형태를 만들어 반환한다. 그 환경값은 설정 블록에 키가 없어 프레임워크가 프로세스 환경에서 직접 읽는다 — **결제 축의 환경별 모드 차이와 같은 부류가 메시징 축에도 있다.** 값이 저장소 밖이라 어느 환경에서 어느 분기가 도는지 확정되지 않는다. 닫을 경로가 이 PRD 범위 밖이다.
    anchors:
      - insuniverse-server/libs/aligo/src/aligo.service.ts:38
  - id: messaging:__bridge__:platform
    status: conditional-unknown
    evidence: [S-component, A-comment]
    unknown_reason: 웹인가 웹뷰인가의 판정 자체가 브리지 이름 환경 변수에 걸려 있다. 그 이름이 주입돼 있고 실행 환경이 같은 이름의 객체를 노출할 때만 웹뷰로 판정한다 — **플랫폼 분기의 뿌리가 코드가 아니라 주입값이다.** 값이 저장소 밖이라 확정되지 않는다. 닫을 경로가 이 PRD 범위 밖이다.
    anchors:
      - insuniverse-site/front/src/entities/_global/model/store.ts:8
      - insuniverse-site/front/src/App.tsx:189
---

이 문서는 발송 축의 **연동 층**을 다룬다. 알림톡·문자 벤더의 다섯 층은 런타임 인덱스에서
승계하고, 그 위에 **인덱스가 담지 못한 것 여섯 건**을 코드에서 직접 유도해 더한다 — 앱 푸시
자격증명과 토큰 수명 · 예외 형태 · 예약 발송 스케줄러 · 로컬 모의 발송 · 웹/웹뷰 판정.

**이 문서의 축은 사각이다.** 벤더 인덱스는 키워드 여덟 종을 카디널리티 불변식으로 묶어 두었고,
그 집합 밖에 있는 연동은 **구조적으로 항목이 될 수 없다.** 앱 푸시는 그 대표 사례다 — 설정
블록도 있고 전용 통합 유닛도 있고 예외 클래스 형태까지 다른 세 비-진료 벤더와 같은데, 이름이
키워드 집합에 없어서 인덱스에 들어가지 못했다. 이 문서는 그 사각을 계산으로 드러내고 코드에서
층을 유도한다.

`push.scheduler` 를 이 문서가 수용하는 것도 같은 축이다. 다만 그 배정은 이 문서가 주장하지
않는다 — `05-schedules/04-runtime-topology.md` 의 배치표가 이미 정했고, 게이트가 그 조인을 돌린다.

---

## 정책 (기획)

**이 절에 정책 블록이 없다.** 본문 배치 자격을 갖는 상태 세 종은 전부 그 정책이 지금 어딘가에
도달해 활성이라는 주장을 담는데, 이 축에는 그 주장을 세울 근거가 저장소 안에 없다.

이 문서의 사유는 형제 문서 둘과 다르다. 진료 공급자 축의 걸림돌은 *풀에 들어 있는가* 이고
결제 축의 걸림돌은 *주입값 그 자체* 인데, 발송 축의 걸림돌은 **프로세스가 서 있는가와 브리지
이름이 무엇인가**다. 예약 발송은 인프로세스 주기 선언이라 걸 게이트가 아예 없고 — 배치표의 그
행은 `게이트 유무` 가 `없음` 이다 — 그 프로세스가 운영에 서 있다는 증거는 저장소 밖에 있다.
웹뷰 판정은 한 걸음 더 나가 **판정 자체가 주입값에 걸린다**. 무엇이 웹이고 무엇이 웹뷰인가를
코드가 아니라 환경 변수가 정한다.

부록의 격리는 판정 보류이지 부정이 아니다. 규칙 자체는 동결 stg `4d798f55` 의 코드로 확정된다.

## 연동 층 대조

첫 열은 정책 id 이고 나머지는 **게이트가 재유도한다**. `인덱스 값` 은 값이 아니라 **형태**만
옮긴다. 인덱스에서 승계한 다섯 층은 열거형을, 코드에서 유도한 여섯 건은 그 층이 정하는 것을
한 줄로 적는다.

| 정책 id | 벤더 | 층 | 인덱스 값 | 게이트 종류 | 미판정 종류 | 닫을 경로 |
|---|---|---|---|---|---|---|
| `messaging:aligo:timeout` | aligo | `timeout` | vendor_side_key: 없음 · dynamic_anchors 無 | `env` | `conditional-unknown` | 없음 — 범위 밖 |
| `messaging:aligo:fallback` | aligo | `fallback` | kind: none | `env` | `conditional-unknown` | 없음 — 범위 밖 |
| `messaging:aligo:normalization` | aligo | `normalization` | mapper 無 · target: none | `env` | `conditional-unknown` | 없음 — 범위 밖 |
| `messaging:aligo:exception` | aligo | `exception` | name: ALigoException · http_status: INTERNAL_SERVER_ERROR | `none` | `conditional-unknown` | 없음 — 범위 밖 |
| `messaging:aligo:env_gates` | aligo | `env_gates` | 키 이름만 (값 미이관) | `env` | `conditional-unknown` | 없음 — 범위 밖 |
| `messaging:fcm:credentials` | fcm | `credentials` | FCM 자격증명 3키와 프로세스 전역 1회 초기화 | `env` | `conditional-unknown` | 없음 — 범위 밖 |
| `messaging:fcm:token-lifecycle` | fcm | `token-lifecycle` | 미등록 토큰을 예외 전파 없이 삭제한다 | `env` | `conditional-unknown` | 없음 — 범위 밖 |
| `messaging:fcm:exception` | fcm | `exception` | FcmException 이 다른 3벤더 예외와 같은 형태다 | `none` | `conditional-unknown` | 없음 — 범위 밖 |
| `messaging:push.scheduler:cron` | push.scheduler | `cron` | 인프로세스 주기 선언 · 배치표가 이 문서에 배정 | `none` | `conditional-unknown` | 없음 — 범위 밖 |
| `messaging:aligo:local-mock` | aligo | `local-mock` | 환경값이 local 이면 실제 발송을 건너뛴다 | `env` | `conditional-unknown` | 없음 — 범위 밖 |
| `messaging:__bridge__:platform` | — | `platform` | 웹/웹뷰 판정이 브리지 이름 env 에 걸려 있다 | `env` | `conditional-unknown` | 없음 — 범위 밖 |

## 인덱스 사각 — FCM

벤더 인덱스는 벤더 키 여덟 종을 **카디널리티 불변식**으로 묶는다 — `vendors` 키 수가 규모
추출기의 벤더 키워드 원소 수와 같아야 하고, 그 불변식을 추출기가 fail-closed 로 강제한다.
앱 푸시(`fcm`)는 그 여덟 종에 없다.

**그런데 그 연동은 인덱스가 적용하는 구조 시험을 전부 통과한다.**

- 설정 블록이 있다 — 자격증명 세 키가 한 블록에 모여 있고 형태가 다른 벤더 블록과 같다
  (`insuniverse-server/apps/api/src/config/configuration.ts:135`).
- 전용 통합 유닛이 있다 — 모듈 디렉터리 하나가 예외·모듈·서비스·타입 네 파일을 갖는다.
- 예외 형태가 같다 — 전용 예외 클래스 하나에 서버 내부 오류 상태를 실어 던지며, 이는 발송·결제
  세 비-진료 벤더의 예외와 같은 모양이다
  (`insuniverse-server/apps/api/src/modules/fcm/fcm.exception.ts:3`).

**항목이 안 된 이유는 구조가 아니라 이름이다.** 키워드 집합이 여덟 개로 못박혀 있고 그 집합의
소유는 `12-index-runtime` 이다. 이 문서는 그 사실을 관측으로 적을 뿐 인덱스를 고치라고 요구하지
않는다 — 그것은 이 창의 범위 밖이며 이월로 올린다.

**사각은 앱 푸시 하나가 아니다.** 설정 블록 키 중 *코드 트리에 같은 이름의 전용 통합 유닛을
갖는* 것을 전수 열거하면 여덟 벤더 밖에 넷이 더 나온다. 게이트가 그 차집합을 매 실행 계산해
출력에 내고, 아래가 그 전건이다.

| 설정 블록 키 | 통합 유닛 | 이 문서가 여는가 |
|---|---|---|
| `fcm` | 앱 푸시 모듈 | **연다** — 여섯 정책 중 셋이 이 축이다 |
| `mailer` | 메일 발송 라이브러리 | 열지 않는다 — 발송 축이지만 이 창의 범위 밖이다. 이월로 올린다 |
| `s3` | 객체 저장소 라이브러리 | 열지 않는다 — 저장 축이며 이 PRD 소유가 아니다 |
| `user` | 사용자 모듈 | 열지 않는다 — 외부 벤더가 아니라 내부 서비스 기준 URL 이다 |

**이 열거의 사각도 적는다.** 판별은 *설정 블록 키와 같은 이름의 디렉터리가 있는가* 이므로,
이름이 어긋나는 외부 연동은 열거에서 빠진다 — 본인확인 · 챗봇 웹훅 · 동영상 API · CDN 이 그런
경우다. 넓히면 오탐이 늘고 좁히면 이 사각이 커진다. 어느 쪽도 닫히지 않으므로 사각을 적는다.

앱 푸시 연동에는 `timeout` · `normalization` · `fallback` 층이 코드에 없다. 전송을 벤더 SDK 가
소유하고 이 저장소는 초기화와 호출만 갖기 때문이며, **관측된 부재이지 미조사가 아니다.**

## push.scheduler 수용

예약 푸시 발송은 인프로세스 주기 선언으로 돈다
(`insuniverse-server/apps/api/src/modules/push/push.scheduler.ts:15`).
주기와 시간대는 데코레이터 인자로 코드에 있고, **걸 게이트가 없다.**

**이 문서가 그 스케줄러를 맡는다고 스스로 주장하지 않는다.** 배정의 근거는
`05-schedules/04-runtime-topology.md` 의 스케줄러 배치표이고, 그 표에서 `담당 문서` 가 이 문서인
행은 하나다. 게이트가 배치표를 다시 읽어 그 집합과 이 문서의 스케줄러 정책 집합을 조인하므로,
이 문서가 다른 스케줄러를 더하면 조인이 깨진다.

스케줄러가 부르는 도메인 로직은 이 문서가 열지 않는다 — 주기 선언까지만 적고 발송 내용은
`50-domain` 소유다.

## 플랫폼 차이 (웹 / 웹뷰)

**웹인가 웹뷰인가의 판정 자체가 주입값에 걸려 있다.** 전역 스토어가 브리지 이름 환경 변수로
웹뷰 여부를 정하고(`insuniverse-site/front/src/entities/_global/model/store.ts:8`), 앱 진입에서
같은 이름으로 브리지를 부른다(`insuniverse-site/front/src/App.tsx:189`). 이름이 없거나 실행
환경이 같은 이름의 객체를 노출하지 않으면 웹으로 판정한다 — **웹 판정이 폴백이다.**

아래 계수는 저자가 적지 않는다. 게이트가 동결 site 트리에서 다시 센다.

| 신호 | 소비 파일 수 | 소비 횟수 | 판정 주체 |
|---|---|---|---|
| BRIDGE_NAME | 20 | 32 | 주입 환경값 — 이름이 맞아야 브리지가 열린다 |
| postMessage | 5 | 9 | 실행 환경 — 브리지가 열린 뒤의 전달 수단이다 |

두 신호의 규모 차이가 이 축의 형태를 보여준다. 브리지 호출은 화면 스무 곳에 흩어져 있고
역방향 전달은 다섯 곳뿐이다 — 웹뷰 통합이 **넓게 얕게** 걸려 있다는 뜻이고, 그래서 브리지
이름 하나가 틀리면 스무 곳이 조용히 웹 경로로 떨어진다.

**같은 부류가 서버 축에도 있다.** 알림톡·문자 벤더의 발송 메서드는 환경값이 로컬이면 실제
발송을 건너뛰고 성공 응답 형태를 만들어 반환한다
(`insuniverse-server/libs/aligo/src/aligo.service.ts:38`). 그 환경값은 설정 블록에 키가 없어
프레임워크가 프로세스 환경에서 직접 읽는다 — 결제 축의 환경별 모드 차이와 같은 형태가 발송
축에도 있고, 둘 다 값이 저장소 밖이다.

## 강제 지점 (개발)

정책 내용을 다시 쓰지 않는다. 각 정책이 저장소 안 **어디에서** 강제되는가와 그 기제, 그리고
게이트 종류만 적는다. 배치가 부록인 정책도 이 절에 있어야 한다. 기준점은 전건 동결 stg
`4d798f55` 다.

### messaging:aligo:timeout

- 강제 방식: HTTP 클라이언트 호출 옵션. 만료가 지나면 호출이 끊기고 그 실패가 위층 분기로 간다
- 앵커: `insuniverse-server/libs/aligo/src/aligo.service.ts:68` · `insuniverse-server/libs/aligo/src/aligo.service.ts:95` · `insuniverse-server/libs/aligo/src/aligo.service.ts:121`
- 게이트: `env` — 주입 환경값이 정한다

### messaging:aligo:fallback

- 강제 방식: 디스패처 루프의 공급자 순회. 이 벤더가 그 순회의 원소인가로 강제된다
- 앵커: `insuniverse-server/libs/aligo/src/aligo.service.ts:68` · `insuniverse-server/libs/aligo/src/aligo.service.ts:95` · `insuniverse-server/libs/aligo/src/aligo.service.ts:121`
- 게이트: `env` — 주입 환경값이 정한다

### messaging:aligo:normalization

- 강제 방식: 응답 변환 지점. 매퍼가 있으면 그 파일이, 없으면 서비스가 응답 형태를 그대로 진다
- 앵커: `insuniverse-server/libs/aligo/src/aligo.service.ts:68` · `insuniverse-server/libs/aligo/src/aligo.service.ts:95` · `insuniverse-server/libs/aligo/src/aligo.service.ts:121`
- 게이트: `env` — 주입 환경값이 정한다

### messaging:aligo:exception

- 강제 방식: 전용 예외 클래스 생성 지점. 호출 실패가 그 형태로 감싸여 위로 간다
- 앵커: `insuniverse-server/libs/aligo/src/aligo.exception.ts:3`
- 게이트: `none` — 걸 게이트가 없다 — 앵커 파일에서 두 토큰이 모두 관측되지 않았다

### messaging:aligo:env_gates

- 강제 방식: 설정 블록 로드. 키가 비면 연동 진입이 성립하지 않는다
- 앵커: `insuniverse-server/apps/api/src/config/configuration.ts:111`
- 게이트: `env` — 주입 환경값이 정한다

### messaging:fcm:credentials

- 강제 방식: 설정 블록과 서비스 초기화. 모듈 전역에서 한 번만 앱을 세운다
- 앵커: `insuniverse-server/apps/api/src/config/configuration.ts:135` · `insuniverse-server/apps/api/src/modules/fcm/fcm.service.ts:28`
- 게이트: `env` — 주입 환경값이 정한다

### messaging:fcm:token-lifecycle

- 강제 방식: 발송 응답 오류 코드 분기. 미등록 토큰이면 저장 토큰을 비우고 예외를 삼킨다
- 앵커: `insuniverse-server/apps/api/src/modules/fcm/fcm.service.ts:11` · `insuniverse-server/apps/api/src/modules/fcm/fcm.service.ts:68`
- 게이트: `env` — 주입 환경값이 정한다

### messaging:fcm:exception

- 강제 방식: 전용 예외 클래스 생성 지점
- 앵커: `insuniverse-server/apps/api/src/modules/fcm/fcm.exception.ts:3`
- 게이트: `none` — 걸 게이트가 없다 — 앵커 파일에서 두 토큰이 모두 관측되지 않았다

### messaging:push.scheduler:cron

- 강제 방식: 데코레이터 주기 선언. 프로세스가 살아 있는 동안 스케줄러가 직접 건다
- 앵커: `insuniverse-server/apps/api/src/modules/push/push.scheduler.ts:15`
- 게이트: `none` — 걸 게이트가 없다 — 앵커 파일에서 두 토큰이 모두 관측되지 않았다

### messaging:aligo:local-mock

- 강제 방식: 발송 메서드 첫 분기. 환경값이 로컬이면 그 자리에서 성공 DTO 를 만들어 반환한다
- 앵커: `insuniverse-server/libs/aligo/src/aligo.service.ts:38`
- 게이트: `env` — 주입 환경값이 정한다

### messaging:__bridge__:platform

- 강제 방식: 전역 스토어 초기화와 앱 진입. 브리지 이름으로 웹뷰 여부를 정한다
- 앵커: `insuniverse-site/front/src/entities/_global/model/store.ts:8` · `insuniverse-site/front/src/App.tsx:189`
- 게이트: `env` — 주입 환경값이 정한다

## 적용 범위 (actor × 서비스)

`적용` 은 **그 정책의 앵커가 그 저장소에 있는 경우**에만 쓴다. 그 밖은 `—` 이며 빈 칸이 아니라
관측된 부재다. 이 규칙은 게이트가 다시 돌린다.

actor 다섯 열이 전건 `—` 인 이유는 형제 문서와 같다 — 연동 층은 로그인 주체를 보지 않는다.
푸시를 받는 사람이 어느 주체인가와 자격증명이 주입됐는가는 저장소 안에서 갈리지 않는다.

**서비스 축은 이 문서에서 갈린다.** 발송·푸시 열 건의 앵커는 server 에 있고, 플랫폼 판정 한
건의 앵커는 site 에 있다.

| 정책 id | 관리자 | 영업대리점 | 기업담당자 | 일반회원 | 기업회원 | server | site | admin |
|---|---|---|---|---|---|---|---|---|
| messaging:aligo:timeout | — | — | — | — | — | 적용 | — | — |
| messaging:aligo:fallback | — | — | — | — | — | 적용 | — | — |
| messaging:aligo:normalization | — | — | — | — | — | 적용 | — | — |
| messaging:aligo:exception | — | — | — | — | — | 적용 | — | — |
| messaging:aligo:env_gates | — | — | — | — | — | 적용 | — | — |
| messaging:fcm:credentials | — | — | — | — | — | 적용 | — | — |
| messaging:fcm:token-lifecycle | — | — | — | — | — | 적용 | — | — |
| messaging:fcm:exception | — | — | — | — | — | 적용 | — | — |
| messaging:push.scheduler:cron | — | — | — | — | — | 적용 | — | — |
| messaging:aligo:local-mock | — | — | — | — | — | 적용 | — | — |
| messaging:__bridge__:platform | — | — | — | — | — | — | 적용 | — |

## 예외·폴백

예외가 없는 정책도 행을 둔다 — 없다는 것도 확인의 결과이며 그 확인 근거를 함께 적는다.

| 정책 id | 예외 | 폴백 | 확인 근거 |
|---|---|---|---|
| messaging:aligo:timeout | 만료 자체가 예외 경로다 | 만료 뒤 폴백은 이 층이 정하지 않는다 — 폴백 층이 진다 | 런타임 인덱스의 이 벤더 층이 그 값을 담고, 그 층이 이 정책의 근거다 |
| messaging:aligo:fallback | 없음 | 없음 — 멀티 공급자 폴백은 진료수집 축 전용이고 이 벤더는 디스패처 풀에 없다 | 런타임 인덱스의 이 벤더 층이 그 값을 담고, 그 층이 이 정책의 근거다 |
| messaging:aligo:normalization | 없음 | 없음 — 변환 없이 응답 형태를 그대로 쓴다 | 런타임 인덱스의 이 벤더 층이 그 값을 담고, 그 층이 이 정책의 근거다 |
| messaging:aligo:exception | 이 층 자체가 예외 경로다 | 없음 — 예외를 대체하는 경로가 이 벤더에 없다 | 런타임 인덱스의 이 벤더 층이 그 값을 담고, 그 층이 이 정책의 근거다 |
| messaging:aligo:env_gates | 키가 비면 연동 진입이 성립하지 않는다 | 없음 — 키 부재를 메우는 기본값이 이 층에 없다 | 런타임 인덱스의 이 벤더 층이 그 값을 담고, 그 층이 이 정책의 근거다 |
| messaging:fcm:credentials | 키가 비면 앱 초기화가 성립하지 않는다 | 없음 — 초기화를 대체하는 경로가 없다 | 설정 블록과 서비스 초기화 지점 |
| messaging:fcm:token-lifecycle | 미등록 토큰 오류가 예외로 위로 가지 않는다 | 저장 토큰을 비우는 것이 폴백이다 — 다음 발송 대상에서 빠진다 | 발송 응답 오류 코드 분기 |
| messaging:fcm:exception | 이 층 자체가 예외 경로다 | 없음 — 예외를 대체하는 경로가 이 벤더에 없다 | 런타임 인덱스의 이 벤더 층이 그 값을 담고, 그 층이 이 정책의 근거다 |
| messaging:push.scheduler:cron | 주기 실행이 실패해도 다음 주기에 다시 돈다 | 없음 — 재시도나 보상 경로가 이 선언에 없다 | 데코레이터 주기 선언 지점 |
| messaging:aligo:local-mock | 로컬 분기는 벤더를 부르지 않으므로 벤더 실패가 없다 | 없음 — 이 분기 자체가 실 발송의 대체다 | 발송 메서드 첫 분기 |
| messaging:__bridge__:platform | 브리지 이름이 없거나 실행 환경이 그 객체를 노출하지 않으면 웹으로 판정한다 | 웹 판정이 폴백이다 — 웹뷰 전용 경로가 그때 열리지 않는다 | 전역 스토어 초기화 지점 |

## 부록

이 절의 정책은 **현행 발효 여부가 판정되지 않아** 본문에서 격리한다. 이 문서의 미판정은 전건
`게이트 미상` 이며 그 안에 두 형태가 섞여 있다 — 주입 환경값이 걸림돌인 것과, **걸 게이트가
아예 없어 도달 증거 부재가 걸림돌인 것**이다. 후자는 예약 발송 스케줄러와 예외 형태 두 건이며,
그 둘의 게이트 종류가 `none` 인 것은 앵커 파일에서 두 토큰이 모두 관측되지 않았다는 뜻이다.

**닫을 경로는 두 형태 모두 이 PRD 범위 밖이다.** 형제 문서 `01-treat-providers.md` 에는 닫을
경로가 이름을 갖는 `데이터 의존` 칸이 있지만 발송 축에는 그런 층이 없다.

**여기 실린 어떤 정책도 그 규칙이 틀렸다고 말하지 않는다.** 미판정은 부정이 아니다. 규칙 자체는
동결 stg `4d798f55` 의 코드로 확정되며, 판정되지 않은 것은 그 규칙이 지금 걸려 있는가다.

각 블록은 **그 층이 무엇을 정하고 그것이 발효하려면 무엇이 서야 하는가**를 적는다. 좌표는
프론트매터 `anchors` 와 `## 강제 지점 (개발)` 이 진다.

### messaging:aligo:timeout

요청 만료 시점을 정하는 층이다. 만료 뒤 호출자가 어느 분기로 가는가는 동결 stg 4d798f55 의 코드로 확정되지만, 그 분기가 발효하려면 이 연동이 먼저 서야 한다 — 그 조건은 이 벤더 설정 블록의 환경 키 주입이고 키 값은 저장소 밖에 있다. 이 층의 게이트 종류는 `env` 로 재유도됐고 닫을 경로가 이 PRD 범위 밖이다. 만료 값 자체는 이 문서가 옮기지 않는다(30-contracts-limits 소유).

### messaging:aligo:fallback

폴백이 없다는 것이 이 층의 내용이다 — 멀티 공급자 폴백은 진료수집 축 전용이고 이 벤더는 디스패처 풀에 없다. **관측된 부재이지 미조사가 아니다.** 그 부재가 운영에서 유지되는지는 이 연동이 실제로 서 있는가에 걸린다 — 그 조건은 이 벤더 설정 블록의 환경 키 주입이고 키 값은 저장소 밖에 있다. 이 층의 게이트 종류는 `env` 로 재유도됐고 닫을 경로가 이 PRD 범위 밖이다.

### messaging:aligo:normalization

정규화 매퍼가 없다는 것이 이 층의 내용이다 — 응답을 그대로 응답 DTO 로 옮기거나 자기 형태를 정본으로 쓴다. **관측된 부재다.** 그 형태가 발효하려면 이 연동이 서야 한다 — 그 조건은 이 벤더 설정 블록의 환경 키 주입이고 키 값은 저장소 밖에 있다. 이 층의 게이트 종류는 `env` 로 재유도됐고 닫을 경로가 이 PRD 범위 밖이다.

### messaging:aligo:exception

호출 실패를 어느 예외 형태로 감싸는가를 정하는 층이다. 예외 클래스 이름과 HTTP 상태는 코드로 확정되지만 그 예외가 던져지려면 이 연동이 서야 한다 — 그 조건은 이 벤더 설정 블록의 환경 키 주입이고 키 값은 저장소 밖에 있다. 이 층의 게이트 종류는 `none` 로 재유도됐고 닫을 경로가 이 PRD 범위 밖이다. 게이트 종류가 `none` 인 것은 이 층의 앵커 파일에서 DB 토큰도 env 토큰도 관측되지 않았다는 뜻이지 미명세가 아니다.

### messaging:aligo:env_gates

이 벤더 연동이 서려면 설정 블록의 환경 키가 주입돼야 한다는 것이 이 층의 내용이다. 키 **이름**은 런타임 인덱스가 열거하지만 값은 저장소 밖이고 이 문서도 이름을 나열하지 않는다(11-index-code · 12-index-runtime 소유). 이 층의 게이트 종류는 `env` 로 재유도됐고 닫을 경로가 이 PRD 범위 밖이다.

### messaging:fcm:credentials

푸시 발송에 필요한 자격증명 세 키가 설정 블록에서 환경 변수로 읽히고, 그 값으로 프로세스 전역 앱이 한 번만 초기화된다. 키 값이 저장소 밖이라 초기화가 실제로 성립하는지 확정되지 않는다. 닫을 경로가 이 PRD 범위 밖이다.

### messaging:fcm:token-lifecycle

발송 응답이 미등록 토큰 오류를 내면 그 토큰을 저장소에서 지우고 예외를 위로 전파하지 않는다. 이 규칙은 코드로 확정되지만 발송 자체가 자격증명 주입에 걸려 있어 발효가 미판정이다. 닫을 경로가 이 PRD 범위 밖이다.

### messaging:fcm:exception

이 연동의 예외 형태가 다른 세 비-진료 벤더의 예외와 같은 모양이다 — 전용 예외 클래스 하나에 서버 내부 오류 상태를 실어 던진다. **인덱스가 이 벤더를 담지 않는데도 형태는 같다**는 것이 이 정책의 내용이다. 발효는 발송 경로가 서는가에 걸리고 그 조건이 저장소 밖이다. 게이트 종류가 `none` 인 것은 이 층의 앵커 파일에서 DB 토큰도 env 토큰도 관측되지 않았다는 뜻이지 미명세가 아니다. 닫을 경로가 이 PRD 범위 밖이다.

### messaging:push.scheduler:cron

예약 푸시 발송이 인프로세스 주기 선언으로 돈다. 주기와 시간대는 코드에 있고 **걸 게이트가 없다** — 배치표의 이 행은 `게이트 유무` 가 `없음` 이다. 그래서 미판정의 원인은 게이트 값 미상이 아니라 그 프로세스가 운영에 서 있다는 증거 부재이며, 그 증거는 저장소 밖에 있다. 닫을 경로가 이 PRD 범위 밖이다.

### messaging:aligo:local-mock

환경값이 로컬이면 실제 발송을 건너뛰고 성공 응답 형태를 만들어 반환한다. 그 환경값은 설정 블록에 키가 없어 프레임워크가 프로세스 환경에서 직접 읽는다 — **결제 축의 환경별 모드 차이와 같은 부류가 메시징 축에도 있다.** 값이 저장소 밖이라 어느 환경에서 어느 분기가 도는지 확정되지 않는다. 닫을 경로가 이 PRD 범위 밖이다.

### messaging:__bridge__:platform

웹인가 웹뷰인가의 판정 자체가 브리지 이름 환경 변수에 걸려 있다. 그 이름이 주입돼 있고 실행 환경이 같은 이름의 객체를 노출할 때만 웹뷰로 판정한다 — **플랫폼 분기의 뿌리가 코드가 아니라 주입값이다.** 값이 저장소 밖이라 확정되지 않는다. 닫을 경로가 이 PRD 범위 밖이다.

## 연혁

릴리스 태그가 존재하지 않는다. 릴리스 열은 `—` 로 두고 커밋 SHA 로 추적한다. 행은
`docs/_meta/index/commits.server.jsonl` 이 이 문서의 앵커 경로 접촉으로 기록한 커밋 집합 안에서
**연동 조건이 바뀐 시점**만 골랐다.

**예약 발송 스케줄러는 행이 하나뿐이다.** 그 파일을 건드린 커밋이 인덱스 기준으로 한 건이고,
그것이 생성 시점이다. 이후 연동 조건이 바뀐 적이 없다는 관측이며 — 인덱스에 없는 커밋을 근거로
행을 만들지 않는다.

| 릴리스 | 날짜 | SHA | 변경 요약 |
|---|---|---|---|
| — | 2025-03-14 | `1e271f18` | 앱 푸시가 붙는다 — 자격증명 블록과 전역 초기화가 이때 생긴다 |
| — | 2025-07-14 | `c4136e53` | 앱 푸시 전용 예외 클래스 신설 — 예외 형태가 다른 벤더와 같아진 시점 |
| — | 2025-07-18 | `75a05d0c` | 미등록 토큰을 예외 전파 없이 삭제하는 규칙이 선다 — 토큰 수명 정책의 시작 |
| — | 2025-10-30 | `2ec78694` | 예약 발송 스케줄러 신설 — 인프로세스 주기 선언이 생긴 유일한 시점 |
| — | 2026-01-12 | `46efac63` | 발송 벤더 서비스 전반 리팩토링 — 로컬 모의 발송 분기가 현재 형태로 정착한다 |
