<!-- canonical: https://docs.axelabs.ai/ops/runbook/operator-broadcast -->
<!-- source: content/ops/runbook/operator-broadcast.mdx -->

---
title: 운영자 broadcast — Teams DM 으로 임직원 1:N 공지
description: Blueprint `/api/admin/broadcast-dm` REST 로 bot identity (ai@axellc.com) → AXE 임직원 1:N Teams DM. M365 Mail.Send 가 아닌 Teams 채널 — AXE 내부 broadcast 표준. vault 안내 / 운영 공지 / 시스템 변경 통보 등 use case.
playbook: true
---

# 운영자 broadcast (Teams DM)

## AI 요청 프롬프트

```
https://docs.axelabs.ai/ops/runbook/operator-broadcast 따라
임직원 [수신자 email list] 에게 다음 공지 broadcast 해줘:

[공지 본문 paste]

진행:
1. 본 페이지의 Prereq 확인 (Blueprint LIVE + CRON_SECRET 접근 가능)
2. Step 1..4 순서대로 — 매 step 결과 받고 다음
3. 함정 발생 시 페이지 "함정 정리" 표 따라 우회
4. Step 4 결과 (3 messageId) 받으면 종료 + Ship Log 한 줄 (선택)
```

본인 AI session = Claude Code / Cursor / ChatGPT 데스크탑 / Claude.app / 기타.

페이지 본문 = 사람이 직접 read 도 가능, AI 도 참고. AI 가 본 페이지 fetch 후 위 진행 순서대로 사용자와 step-by-step interactive 풀어나감.

## Prereq

- 운영자 자격 (AXE org Owner — 본 broadcast 가 정당 operational 공지인지 본인 판단)
- axe-macmini 에서 실행 (Blueprint LIVE 가 `http://blueprint.local:3100` 으로 접근 가능)
- `/Users/axe/blueprint/.env` 의 `CRON_SECRET` 환경변수 존재 (Blueprint deploy 시 자동 set)
- 수신자 email list (`@axellc.com` 도메인 — 운영자 자체 broadcast 가 cross-tenant 안 함)
- 본문 = plain text 또는 escaped html (default text — 권장)

## Step 1: 사전 확인 (Blueprint LIVE + CRON_SECRET + bot identity)

```bash
# (a) Blueprint health
curl -sf -o /dev/null -w 'blueprint http=%{http_code}\n' http://blueprint.local:3100/api/health

# (b) CRON_SECRET 길이 (값 출력 X)
SECRET=$(grep '^CRON_SECRET=' /Users/axe/blueprint/.env | cut -d= -f2- | tr -d '"' | tr -d "'")
echo "secret_len=${#SECRET}"

# (c) bot identity = ai@ 인지 self-DM 거부 응답으로 검증 (실제 메시지 발송 X)
curl -sf -X POST http://blueprint.local:3100/api/admin/broadcast-dm \
  -H "Authorization: Bearer $SECRET" \
  -H 'Content-Type: application/json' \
  -d '{"emails":["ai@axellc.com"],"text":"self-test","contentType":"text"}' \
  | jq '.results[0]'
# 기대: {email:"ai@axellc.com", status:"skipped", reason:"target AAD id matches bot — refusing self-DM"}
# → reason 안에 "matches bot" 보이면 bot = ai@ 정상
```

3개 다 통과해야 진행. 실패 시 함정 표 참조.

## Step 2: 본문 + payload JSON 작성

본문 = plain text (default contentType). 줄바꿈 `\n`, 특수문자 escape 불필요 (text mode 라 client 가 그대로 렌더). bullet 은 `-` 또는 `•` 등 plain.

```bash
# 본문을 한 변수에 — heredoc 이 가독성 ↑
BODY=$(cat <<'EOF'
[제목 1줄]

[본문 내용]
- bullet 1
- bullet 2

링크: https://...
EOF
)

# payload JSON 작성 (jq 가 \n + 특수문자 자동 escape)
EMAILS_JSON='["soohun.kang@axellc.com","taehun.kang@axellc.com","jinwoo.han@axellc.com"]'
jq -n --argjson emails "$EMAILS_JSON" --arg text "$BODY" \
  '{emails:$emails, contentType:"text", text:$text}' > /tmp/bcast.json

# 확인 (수신자 + 본문 길이만 — 본문 풀 출력 안 함)
jq '{emails, text_len: (.text | length)}' /tmp/bcast.json
```

## Step 3: POST 호출

```bash
SECRET=$(grep '^CRON_SECRET=' /Users/axe/blueprint/.env | cut -d= -f2- | tr -d '"' | tr -d "'")
curl -s -X POST http://blueprint.local:3100/api/admin/broadcast-dm \
  -H "Authorization: Bearer $SECRET" \
  -H 'Content-Type: application/json' \
  -d @/tmp/bcast.json \
  -w '\nHTTP_%{http_code}\n'
```

기대 응답:

```json
{
  "summary": {"total":3, "sent":3, "skipped":0, "error":0},
  "results": [
    {"email":"soohun.kang@axellc.com", "status":"sent", "chatId":"19:...@unq.gbl.spaces", "messageId":"1779..."},
    {"email":"taehun.kang@axellc.com", "status":"sent", "chatId":"19:...", "messageId":"1779..."},
    {"email":"jinwoo.han@axellc.com",  "status":"sent", "chatId":"19:...", "messageId":"1779..."}
  ]
}
```

`sent=3` + HTTP 200 = 정상. cleanup:

```bash
rm -f /tmp/bcast.json
```

## Step 4: 결과 검증 + Ship Log (선택)

각 수신자가 Teams 앱 (phone + desktop) 에서 ai@axellc.com 로부터 1:1 DM push 알림 받았는지 본인이 확인 (수신자 회신 또는 운영자 본인 Teams 의 sent items 확인).

운영 회고용 Ship Log 한 줄 (broadcast 가 의미 있는 일이면 — vault 공지 / 신규 customer launch 등):

```markdown
| YYYY-MM-DD HH:MM | broadcast (axe org) | (operator action) | **broadcast title** — 3 임직원 (soohun/taehun/jinwoo) Teams DM 발송. broadcast-dm REST + bot ai@. 본문 핵심: [한 줄 요약]. messageId: 1779..., 1779..., 1779... |
```

추가하면 `/Users/axe/axelabs-docs/content/ops/updates.mdx` Ship Log 표 최상단에 한 줄. `axe ship docs` 로 배포.

## 함정 정리

| # | 증상 | 원인 | 우회 |
|---|---|---|---|
| 1 | 외부 7cb41f76 MCP 의 노출 tool list 에 `send_mail` / `send_email` / `graph_send_email` 없음 | 의도된 분리 — Blueprint 의 32+ graph_* tool 중 send 계열은 `blueprint-graph` 내부 MCP 에만 등록, 외부 connector 는 read-only 격리 | 본 페이지의 broadcast-dm REST 사용 (admin 채널 별도) |
| 2 | `az rest --uri /me/sendMail` → `403 Forbidden ErrorAccessDenied` | az CLI 의 first-party app (04b07795) 이 Mail.Send scope 받을 권한 없음 (Microsoft 자사 앱 간 consent preauthorization 정책) | az CLI 경유 mail send 영구 불가 — broadcast-dm 또는 Outlook 수동 |
| 3 | `az account get-access-token --scope https://graph.microsoft.com/Mail.Send` → AADSTS65002 | (#2 와 동일 root cause) | (#2 와 동일) |
| 4 | `/api/admin/broadcast-dm` → `{status:"skipped", reason:"target AAD id matches bot — refusing self-DM"}` | bot identity = ai@axellc.com — 본인에게 DM 시도 거부 | 수신자 list 에서 ai@ 제거. 또는 self-test 용도 (Step 1 의 (c)) 일부러 의도적 |
| 5 | `{status:"skipped", reason:"no AAD object id for user"}` | 수신자가 Blueprint 에 한 번도 로그인한 적 없음 — NextAuth callback 의 `User.aadObjectId` populate 안 됨 | 수신자가 https://blueprint.axelabs.ai 1회 SSO 로그인 → 자동 fill → 재시도 |
| 6 | `401 Unauthorized` | `CRON_SECRET` 헤더 잘못 / .env 에 미설정 / Bearer prefix 빠짐 | `Authorization: Bearer $SECRET` 정확. .env 의 CRON_SECRET 값 직접 변수에 |
| 7 | `Connection refused http://blueprint.local:3100` | Blueprint 미실행 (PC 부팅 직후 / launchd 미가동) | `ps aux \| grep blueprint` 확인, 필요 시 `launchctl kickstart -k gui/$(id -u)/com.axe.blueprint` |
| 8 | 본문 줄바꿈 깨짐 (Teams 한 줄로 표시) | `contentType: "html"` 인데 plain text 넣음 — `<br>` 없으니 collapse | `contentType: "text"` 명시 (default 권장) |
| 9 | 본문 안 `*` 강조 등이 안 보임 | Teams 는 markdown 미해석 (text contentType) | bullet 은 `-` plain, 강조는 대문자 or `===` 줄 구분 |
| 10 | customer 직원 (`@truvia.co.kr` 등) 에게 보내려고 시도 → skip | broadcast-dm 의 bot = AXE tenant 의 ai@, cross-tenant chat 생성 불가 | customer 측은 본인 운영자 (Truvia 의 broadcast 채널) 자체 보냄 — sovereignty 원칙 |

## 관련 use case (참고)

- **vault 운영 공지** — KDF rotation / setup 안내 (D-ops-40, 2026-05-26 첫 실 사용)
- **신규 customer launch 안내** — 새 service 가 LIVE 됐을 때
- **시스템 변경 통보** — Blueprint major upgrade, frame schema migration 등
- **D-day 직전 사전 안내** — onboard step 차단 가능성 등 사전 경고

NOT for: 일반 잡담 (channel 사용), 1:1 코칭 (Teams 직접 chat), customer 측 공지 (customer 자체 채널).

## 매 작업 시 사전 작업

| 빈도 | 작업 | 자동화 |
|---|---|---|
| 매 broadcast | Step 1 사전 확인 (Blueprint LIVE + bot identity 검증) | ❌ (사람 판단) |
| `CRON_SECRET` 회전 시 | Blueprint .env + axe-macmini launchd 재시작 | [B-blueprint-secret-rotation](/ops/backlog) 참조 |

## 참조

- [D-ops-40](/ops/decisions) — vault axe.3 release (본 broadcast 의 첫 use case, 2026-05-26)
- [/architecture/vault-policies](/architecture/vault-policies) — vault 3 layer 정책 모델 (broadcast 본문 안 참조 patterns)
- [/services/blueprint](/services/blueprint) — Blueprint 서비스 개요
- `src/app/api/admin/broadcast-dm/route.ts` — REST 구현
- `src/lib/teams/graph-client.ts` — bot Graph client (`getSharedClient()` 가 ai@ 정합)
- [B-blueprint-broadcast-mail](/ops/backlog) — 실 SMTP email 발송 route 추가 (M5, 사용 빈도 보고 진행)
- [/ops/known-gaps](/ops/known-gaps) — Microsoft 첫 당사자 app consent policy (az CLI Mail.Send 영구 차단)
