Webhook 처리
R2 업로드 이벤트와 Stream 트랜스코딩 완료 이벤트를 서버가 처리하는 과정
문서 정보
- 작성일: 2026-03-02
- 최종 업데이트: 2026-03-02
- 버전: v1.0.0
- 관련 코드:
src/modules/cloudflare/raw-video-webhook/
개요
체육관 R2에 영상이 업로드되면 두 가지 webhook이 Spoclip 서버로 전달됩니다.
| Webhook | 엔드포인트 | 트리거 |
|---|---|---|
| R2 Webhook | POST /cloudflare/raw-video-webhooks/:gymCode/r2 | R2 객체 생성/삭제 |
| Stream Webhook | POST /cloudflare/raw-video-webhooks/:gymCode/stream | Stream 트랜스코딩 완료/실패 |
두 엔드포인트 모두 @Public() (JWT 불필요)이며, HMAC-SHA256 서명으로 요청의 진위를 검증합니다.
R2 Webhook
요청 페이로드
{
"idempotencyKey": "uuid-v4",
"eventType": "object-create",
"object": {
"key": "COURT-A/2026-03-01/14/3/CAM-FRONT.mp4",
"size": 524288000,
"passthrough": {
"source": "R2",
"type": "RAW_VIDEO",
"gymCode": "GYM-001",
"courtCode": "COURT-A",
"cameraCode": "CAM-FRONT",
"date": "2026-03-01",
"hour": 14,
"timeSlot": 3
}
}
}
처리 흐름 (R2 Orchestrator)
관련 코드: src/modules/cloudflare/raw-video-webhook/orchestrators/r2/r2.orchestrator.ts
ObjectCreatedCommand 상세
- 해당 시간대(courtId + startTime + endTime)의 Video가 없으면 새로 생성
- Recording을 upsert (courtId + startTime + endTime 기준)
- 체육관 R2 크레덴셜로 presigned URL 생성 (72시간 TTL)
Video.metadata에 카메라별 R2 정보 추가:
videoResources[cameraCode] = {
r2: {
bucketName: 'raw-videos',
objectKey: 'COURT-A/2026-03-01/14/3/CAM-FRONT.mp4',
uploadStatus: 'UPLOADED',
size: 524288000,
uploadedAt: '2026-03-01T14:40:00Z',
playbackUrl: 'https://...' // presigned URL
}
}
관련 코드: src/modules/cloudflare/raw-video-webhook/orchestrators/r2/commands/object-created.command.ts
Stream Webhook
요청 페이로드 (Cloudflare 원본)
{
"uid": "stream-video-uid",
"readyToStream": true,
"status": { "state": "ready" },
"duration": 600,
"thumbnail": "https://...",
"playback": {
"hls": "https://customer-xxx.cloudflarestream.com/.../manifest/video.m3u8"
},
"meta": {
"source": "R2",
"type": "RAW_VIDEO",
"gymCode": "GYM-001",
"courtCode": "COURT-A",
"cameraCode": "CAM-FRONT",
"date": "2026-03-01",
"hour": "14",
"timeSlot": "3"
}
}
Interceptor 변환
Cloudflare Stream은 passthrough 필드를 meta 최상위에 flat하게 전송합니다. CloudflareStreamTransformInterceptor가 이를 서버 내부 형식으로 변환합니다.
| 변환 전 | 변환 후 |
|---|---|
meta.source | meta.passthrough.source |
meta.gymCode | meta.passthrough.gymCode |
meta['downloaded-from'] | meta.downloadedFrom |
관련 코드: src/modules/cloudflare/raw-video-webhook/shared/interceptors/cloudflare-stream-transform.interceptor.ts
처리 흐름 (Stream Orchestrator)
R2 Orchestrator와 동일한 전처리(파싱, 분산 락, Context, 서명 검증) 후:
| Stream 상태 | 처리 |
|---|---|
readyToStream === true | ReadyCommand - Stream 메타데이터 설정 |
status.state === 'error' | ErroredCommand - 실패 기록 |
| 그 외 | SKIPPED |
ReadyCommand 상세
R2 webhook이 먼저 처리되어야 합니다 (카메라 엔트리가 Video.metadata에 존재해야 함).
videoResources[cameraCode].stream = {
videoId: 'stream-video-uid',
uploadStatus: 'READY',
readyToStream: true,
encodedAt: '2026-03-01T14:45:00Z',
playbackUrl: 'https://customer-xxx.cloudflarestream.com/.../manifest/video.m3u8',
duration: 600,
thumbnail: 'https://...'
}
Stream의 HLS URL이 설정되면 사용자가 앱에서 영상을 시청할 수 있습니다.
관련 코드: src/modules/cloudflare/raw-video-webhook/orchestrators/stream/commands/ready.command.ts
Context 조회
Webhook 처리 시 체육관의 크레덴셜과 설정 정보를 조회합니다.
조회 경로
gyms → courts → court_media_configs → gym_media_configs
WHERE provider = 'CLOUDFLARE' AND status = 'ACTIVE'
반환 정보
| 필드 | 용도 |
|---|---|
gymId | 체육관 식별 |
courtId | 코트 식별 |
webhookSecret | HMAC 서명 검증 (복호화) |
accountId | CF Account ID (복호화) |
r2ApiKey, r2ApiSecret | R2 presigned URL 생성 (복호화) |
캐싱
동일 시간대에 여러 카메라 webhook이 연속으로 도착하므로, Context는 10분간 Redis에 캐싱됩니다.
관련 코드: src/modules/cloudflare/raw-video-webhook/shared/context-resolver.util.ts
분산 락
락 키 형식
video:{gymCode}:{courtCode}:{date}:{hour}:{timeSlot}
락 파라미터
| 파라미터 | 값 |
|---|---|
| TTL | 10초 |
| 최대 재시도 | 50회 |
| 재시도 간격 | 100ms |
동일 시간대의 멀티카메라 webhook이 동시에 도착해도, 분산 락으로 하나의 Video 엔티티에 대한 동시 쓰기를 방지합니다.
HMAC 서명 검증
모든 webhook 요청은 Webhook-Signature 헤더에 HMAC-SHA256 서명을 포함합니다. 서버는 gym_media_configs의 webhookSecret(복호화)으로 서명을 검증합니다.
관련 코드: src/modules/cloudflare/raw-video-webhook/core/signature.util.ts
멱등성
모든 webhook 요청에는 idempotencyKey가 포함됩니다. 처리 완료 시 webhook 로그를 생성하여 동일한 요청이 재전송되어도 중복 처리를 방지합니다.
변경 이력
| 버전 | 날짜 | 변경 내용 |
|---|---|---|
| v1.0.0 | 2026-03-02 | 초기 문서 작성 - R2/Stream Webhook 처리 흐름 - Context 조회 및 캐싱 - 분산 락, HMAC 서명, 멱등성 설명 - Interceptor 변환 로직 |