Cloudwiki

Cloudwiki/설정/웹훅

# Discord 웹훅 알림 설정 가이드

CloudWiki 는 위키에서 발생한 주요 이벤트(가입 신청, 티켓 작성, 신규 토론, 공지 발행 등)를 [Discord 웹훅](https://discord.com/developers/docs/resources/webhook) 으로 발송할 수 있습니다. 운영자/관리자용 알림과 일반 사용자에게 보여줄 커뮤니티 알림을 **두 채널** 로 분리 발송하며, 이벤트 화이트리스트로 발송 종류를 세밀하게 제어합니다.

> 디스패처 구현체는 `src/utils/webhook/discord.ts` 에 있고, 이벤트 빌더는 `src/utils/webhook/events/*.ts` 에 모여 있습니다. 모든 발송은 `ctx.waitUntil` 로 비동기 처리되어 사용자 응답을 막지 않으며, 웹훅 실패는 `console.error` 로만 기록됩니다 (요청 핸들러로 에러 전파 ❌).

---

## 채널 구조

CloudWiki 의 웹훅은 **admin** 과 **community** 두 채널로 분리되어 있습니다. 호출부는 채널을 모르고 이벤트만 발행하며, 채널 라우팅은 이벤트 객체의 `channel` 필드로 결정됩니다.

| 채널 | 용도 | 환경 변수 (URL) | 환경 변수 (이벤트 화이트리스트) |
|---|---|---|---|
| `admin` | 운영자만 보는 비공개 알림 (가입 신청, 차단, 권한 변경, 티켓 등) | `DISCORD_ADMIN_WEBHOOK_URL` | `DISCORD_ADMIN_EVENTS` |
| `community` | 일반 멤버가 함께 보는 커뮤니티 알림 (신규 가입 환영, 신규 토론, 공지 발행) | `DISCORD_COMMUNITY_WEBHOOK_URL` | `DISCORD_COMMUNITY_EVENTS` |

URL 이 비어 있으면 해당 채널은 **자동 비활성**, 화이트리스트가 비어 있으면 모든 이벤트가 차단됩니다(긴급 음소거 용도).

웹훅의 표시 이름과 아바타는 `WIKI_NAME` / `WIKI_LOGO_URL` 을 재사용합니다. 로고 경로가 상대 경로(`/favicon.jpg` 등) 이면 `WIKI_PUBLIC_BASE_URL` 와 결합해 절대 URL 로 보정됩니다 (Discord 는 avatar 와 임베드 URL 에 절대 URL 을 요구).

---

## 지원 이벤트

이벤트 키는 `DISCORD_ADMIN_EVENTS` / `DISCORD_COMMUNITY_EVENTS` 의 화이트리스트 항목과 정확히 일치해야 발송됩니다.

### admin 채널

| 이벤트 키 | 트리거 | 빌더 위치 |
|---|---|---|
| `signup_pending` | 신규 가입 신청이 접수되었을 때 (승인 대기) | `src/utils/webhook/events/signup.ts` |
| `signup_rejected` | 관리자가 가입 신청을 거부했을 때 | `src/utils/webhook/events/signup.ts` |
| `ticket_create` | 사용자가 새 티켓을 등록했을 때 | `src/utils/webhook/events/ticket.ts` |
| `ticket_status` | 티켓 상태(open/closed 등) 가 바뀌었을 때 | `src/utils/webhook/events/ticket.ts` |
| `user_ban` | 사용자 차단 / 차단 해제 | `src/utils/webhook/events/user.ts` |
| `user_role_change` | 사용자 역할 변경 (예: `user → admin`) | `src/utils/webhook/events/user.ts` |
| `super_admin_action` | 다른 admin 이벤트로 잡히지 않는 super_admin 행위 (전역 설정 변경 등) | `src/utils/webhook/events/superAdmin.ts` |

### community 채널

| 이벤트 키 | 트리거 | 빌더 위치 |
|---|---|---|
| `user_joined` | 신규 사용자가 가입을 완료했을 때 (open 정책 즉시 가입 / approval 정책 승인 직후) | `src/utils/webhook/events/signup.ts` |
| `discussion_create` | 문서에 새 토론이 열렸을 때 (잠금 페이지 / R2 전용 네임스페이스 제외) | `src/utils/webhook/events/discussion.ts` |
| `announcement_publish` | `settings.announce_post` 가 새 게시물로 변경되었을 때 (동일 게시물의 메타 갱신은 제외) | `src/utils/webhook/events/blog.ts` |

> 신규 토론 알림은 잠금 페이지(`is_locked`) 와 R2 전용 네임스페이스(`isR2OnlyNamespace`) 에서는 호출부에서 발송이 차단됩니다.
> 공지 알림은 동일 `postId` + 동일 `title` 인 단순 메타 갱신(no-op) 일 때 호출부에서 차단됩니다.

---

## Discord 웹훅 URL 발급

각 채널마다 Discord 측에서 채널 단위로 웹훅 URL 을 따로 만들어야 합니다.

1. Discord 에서 알림을 받을 **서버** 를 선택.
2. 운영자 알림용 텍스트 채널(예: `#wiki-admin`) 을 우클릭 → **채널 편집** → 좌측 **연동** → **웹훅 보기** → **새 웹훅** 클릭.
3. 이름은 자유롭게 (예: `Wiki Admin`), 발송 시 표시 이름은 `WIKI_NAME` 으로 덮어쓰기 됩니다.
4. **웹훅 URL 복사** 버튼으로 URL 을 확보 → 이 값이 `DISCORD_ADMIN_WEBHOOK_URL` 입니다.
5. 커뮤니티 알림용 채널(예: `#wiki-feed`) 에서도 같은 절차로 새 웹훅을 만들어 URL 을 복사 → 이 값이 `DISCORD_COMMUNITY_WEBHOOK_URL` 입니다.

> 한 채널에 두 종류 알림을 모두 보내고 싶다면 같은 URL 을 두 변수에 똑같이 등록해도 동작합니다. 다만 화이트리스트는 채널별로 독립이므로 양쪽 모두 적절히 채워야 합니다.
> 웹훅 URL 자체에 인증 토큰이 포함되어 있으므로 절대 공개 저장소에 커밋하지 말고 반드시 **Secret** 으로 등록하세요.

---

## Cloudflare 에 등록

웹훅 설정값의 등록 위치는 두 곳입니다.

- **URL 두 개 (`DISCORD_ADMIN_WEBHOOK_URL`, `DISCORD_COMMUNITY_WEBHOOK_URL`)**: Cloudflare 대시보드의 **Secret** (또는 `wrangler secret put` CLI). 토큰 포함이므로 절대 평문 변수로 두면 안 됩니다.
- **이벤트 화이트리스트 두 개 (`DISCORD_ADMIN_EVENTS`, `DISCORD_COMMUNITY_EVENTS`)**: `wrangler.toml` 의 `[vars]` 섹션. 비밀이 아니며 PR 로 변경 이력을 남기는 편이 운영에 유리합니다.

### URL — Secret 등록

`wrangler` CLI 로:

```bash
wrangler secret put DISCORD_ADMIN_WEBHOOK_URL
wrangler secret put DISCORD_COMMUNITY_WEBHOOK_URL
```

또는 대시보드에서:

1. [Cloudflare 대시보드](https://dash.cloudflare.com/) → 좌측 **Workers & Pages** → 목록에서 `cloudwiki` Worker 클릭.
2. 상단 **Settings** → 좌측 **Variables and Secrets**.
3. **Add** 버튼으로 두 항목을 등록 (모두 **Secret / Encrypted** 타입).

   | Variable name | Type | Value |
   |---|---|---|
   | `DISCORD_ADMIN_WEBHOOK_URL` | **Secret** | 위에서 복사한 admin 채널 웹훅 URL |
   | `DISCORD_COMMUNITY_WEBHOOK_URL` | **Secret** | 위에서 복사한 community 채널 웹훅 URL |

4. **Save and deploy** 클릭.

URL 변수가 비어 있는 채널은 디스패처가 조용히 drop 합니다 (에러 없이 무시). 테스트 환경에서 한쪽만 등록해도 안전합니다.

### 이벤트 화이트리스트 — `wrangler.toml` 수정

`wrangler.toml` 의 `[vars]` 섹션에서 두 변수를 채웁니다. 기본값은 모든 정의된 이벤트를 활성화하는 형태입니다.

```toml
[vars]
# ... 기존 변수들 ...
DISCORD_ADMIN_EVENTS = "signup_pending,signup_rejected,ticket_create,ticket_status,user_ban,user_role_change,super_admin_action"
DISCORD_COMMUNITY_EVENTS = "user_joined,discussion_create,announcement_publish"
```

원치 않는 이벤트는 콤마 구분 목록에서 제거하면 됩니다. 예를 들어 community 채널에서 신규 가입 환영 메시지를 끄려면:

```toml
DISCORD_COMMUNITY_EVENTS = "discussion_create,announcement_publish"
```

긴급 음소거가 필요하면 값을 빈 문자열로 두면 해당 채널의 모든 이벤트가 차단됩니다.

```toml
DISCORD_ADMIN_EVENTS = ""
```

> ⚠️ `wrangler.toml` 에서 두 변수를 **삭제** 하면 자동 배포 시 빈 문자열로 간주되어 모든 이벤트가 차단됩니다. 의도한 동작이 아니라면 키는 두고 값만 조정하세요.

### 절대 URL 베이스 — `WIKI_PUBLIC_BASE_URL`

임베드의 링크(예: 토론이 열린 문서 링크, 티켓 링크, 가입 승인 패널 링크) 와 아바타 URL 은 모두 절대 URL 이어야 합니다. 워커가 알 수 없는 자기 자신의 공개 도메인을 알려주기 위해 `wrangler.toml` 에 다음을 설정합니다.

```toml
[vars]
WIKI_PUBLIC_BASE_URL = "https://wiki.example.com"
```

이 값이 비어 있으면 임베드 안의 상대 경로 링크(`/admin#signup-requests`, `/tickets/123`, `/w/<slug>` 등) 와 상대 경로 로고가 모두 누락된 채 발송됩니다. 메시지 자체는 도착하지만 링크가 동작하지 않으니 운영 도메인을 반드시 채우세요.

---

## 동작 확인

1. 두 URL Secret 과 두 EVENTS 변수가 모두 채워졌는지 확인 후 배포가 완료되었는지 점검합니다 (Workers → **Deployments** 에서 최신 빌드 success).
2. 가장 안전한 테스트는 **티켓 생성**: 일반 사용자 계정으로 새 티켓을 작성하면 admin 채널에 `🎫 #N <제목>` 임베드가 도착해야 합니다.
3. community 채널은 비공개 토론이 아닌 일반 문서에 새 토론을 열면 `💬 새 토론` 임베드로 확인할 수 있습니다.
4. 도착하지 않으면 Cloudflare 대시보드 → Worker → **Logs** (Real-time / Tail logs) 에서 다음 로그를 확인합니다.
   - `discord webhook non-2xx <channel> <event_type> <status>`: 웹훅 URL 은 살아있지만 Discord 가 거부 (예: 잘못된 임베드, 채널 삭제됨)
   - `discord webhook failed <channel> <event_type> <error>`: 네트워크 오류 또는 5초 타임아웃 (`AbortSignal.timeout(5000)`)

| 증상 | 점검 포인트 |
|---|---|
| 모든 알림이 안 옴 | URL Secret 미등록, 또는 EVENTS 가 빈 문자열 |
| 특정 이벤트만 안 옴 | 해당 채널의 EVENTS 화이트리스트에 키가 빠짐 |
| 임베드 안의 링크가 깨짐 | `WIKI_PUBLIC_BASE_URL` 미설정 |
| 아바타가 안 보임 | `WIKI_LOGO_URL` 이 상대경로인데 `WIKI_PUBLIC_BASE_URL` 미설정 |
| `signup_pending` 만 안 옴 | 회원가입 정책이 `open` 이라 승인 단계가 없을 수 있음 — [[Cloudwiki/설정/회원가입 정책]] 참고 |
| 신규 토론 알림이 안 옴 | 대상 페이지가 `is_locked` 이거나 R2 전용 네임스페이스 |

---

## 보안 / 운영 노트

- 웹훅 URL 은 그 자체로 **인증 토큰** 입니다. 유출되면 누구나 해당 채널에 메시지를 보낼 수 있으므로 반드시 Secret 으로 보관하고, 노출이 의심되면 Discord 채널 설정에서 웹훅을 삭제·재생성해 새 URL 을 다시 Secret 으로 등록합니다.
- 사용자 입력(가입 신청자 이름, 티켓 제목/본문, 토론 제목 등)은 임베드 description / field value 안으로 들어가기 전에 `escapeMd` 로 마크다운 메타문자(`*`, `_`, `~`, `|`, `` ` ``, `\`, `>`) 가 백슬래시로 이스케이프됩니다. 별도의 sanitize 작업을 호출부에서 추가할 필요는 없습니다.
- 긴 본문(`description`, 사유 등)은 `truncate` 로 100~500자 사이로 잘리고 `…` 가 붙습니다. Discord 임베드 한도(4096자) 를 의식할 필요 없이 임의 길이를 그대로 넘겨도 됩니다.
- 발송은 `ctx.waitUntil` 로 비동기 처리되므로, 웹훅 지연이 사용자 요청 응답에 영향을 주지 않습니다. 반대로 발송 실패도 사용자에게 보이지 않으므로 운영 로그 모니터링이 중요합니다.

[[Cloudwiki/설정]]