Docs: [3.0.0] 전체 문서 업데이트 — YouTube Data API + n8n Discord 전송 구조 반영
All checks were successful
news-summary-bot-cicd / build_push_deploy (push) Successful in 25m15s

- README: 아키텍처, 환경변수, API 응답 형식 업데이트
- n8n-setup: RSS → YouTube Data API playlistItems 전환, 노드별 상세 설정
- development: discord.py 제거 반영, API 응답 형식 추가
- operations: CI/CD 자동 배포 설명, DISCORD_WEBHOOK_URL 제거
- testing: DISCORD_WEBHOOK_URL 더미값 제거
- .env.example: DISCORD_WEBHOOK_URL 제거
- tests/test_discord.py 삭제 (모듈 삭제됨)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
sm4640
2026-03-25 15:18:39 +09:00
parent 302f892c5d
commit a2a82084ba
7 changed files with 260 additions and 273 deletions

View File

@@ -2,104 +2,249 @@
## 워크플로우 개요
이 워크플로우는 2개의 YouTube 채널(머니머니코믹스, 슈카월드)의 새 영상을 RSS Feed Trigger로 감지하고, FastAPI `/api/news/summarize` 엔드포인트를 호출하여 요약 후 Discord로 전송하는 자동화 파이프라인입니다.
이 워크플로우는 2개의 YouTube 채널(머니코믹스, 슈카월드)의 새 영상을 YouTube Data API v3로 감지하고, FastAPI `/api/news/summarize` 엔드포인트를 호출하여 요약한 뒤, 응답 결과에 따라 Discord로 요약 또는 에러 알림을 전송하는 자동화 파이프라인입니다.
**전체 흐름:**
```
RSS Feed Trigger (채널A) ──┐
├→ Merge → HTTP Request (FastAPI) → 완료
RSS Feed Trigger (채널B) ──┘ ↘ Error Trigger → Discord 알림
Schedule Trigger (매 정시)
├→ HTTP Request (머니코믹스 playlistItems) → Edit Fields (channel_name: 머니코믹스)
└→ HTTP Request (슈카월드 playlistItems) → Edit Fields (channel_name: 슈카월드)
Merge (Append)
Code (새 영상만 필터링)
HTTP Request (POST /api/news/summarize)
Switch (status 분기)
├→ ok → Discord 요약 전송
├→ skipped → No Operation
└→ error → Discord 에러 알림
```
## 사전 준비: YouTube Data API 키 발급
1. [Google Cloud Console](https://console.cloud.google.com/) → 프로젝트 생성/선택
2. "YouTube Data API v3" 검색 → **사용 설정**
3. **사용자 인증 정보 → API 키** 생성
4. (권장) 키 제한 설정:
- **API 제한**: YouTube Data API v3만 허용
- **애플리케이션 제한**: IP 주소 → n8n 서버 IP
5. 일일 쿼터: 10,000 units (`playlistItems.list`는 1 unit/요청 → 2채널 × 24회 = **48 units/일**)
## 노드 구성 상세
### 1. RSS Feed Trigger 노드 (2개)
RSS Feed Trigger는 **중복 방지가 내장**되어 있어 이전에 처리한 항목을 기억하고, 새 영상만 반환합니다. Poll Time으로 체크 주기를 설정합니다.
**노드 A - 머니머니코믹스:**
### 1. Schedule Trigger
| 설정 항목 | 값 |
|---|---|
| Feed URL | `https://www.youtube.com/feeds/videos.xml?channel_id=UCJo6G1u0e_-wS-JQn3T-zEw` |
| Poll Time | Every Day / Hour: 10, 14, 20 (하루 3회 권장) |
| Type | Schedule Trigger |
| Rule | Every Hour |
| Minute | 0 |
**노드 B - 슈카월드:**
### 2. YouTube API — playlistItems (채널별 각 1개)
| 설정 항목 | 값 |
|---|---|
| Feed URL | `https://www.youtube.com/feeds/videos.xml?channel_id=UCsJ6RuBiTVWRX156FVbeaGg` |
| Poll Time | Every Day / Hour: 10, 14, 20 (하루 3회 권장) |
| Type | HTTP Request |
| Method | GET |
| URL | `https://www.googleapis.com/youtube/v3/playlistItems` |
> ⚠️ **첫 실행 주의:** RSS Feed Trigger를 처음 활성화하면 피드에 있는 모든 영상(최대 15개)을 새 영상으로 인식합니다. 워크플로우 활성화 전에 수동으로 한 번 테스트 실행하여 기존 항목을 처리 완료 상태로 만드세요.
**Query Parameters:**
### 2. Merge 노드
- **타입:** Merge
- **모드:** Append
- 두 RSS Feed Trigger의 출력을 하나의 리스트로 합칩니다.
- 합쳐진 리스트의 각 항목마다 다음 노드(HTTP Request)가 실행됩니다.
### 3. HTTP Request 노드
FastAPI 서버의 `/api/news/summarize` 엔드포인트를 호출합니다.
| 설정 항목 | 값 |
| 파라미터 | 값 |
|---|---|
| Method | `POST` |
| URL | `https://nkeystudy.site/api/news/summarize` |
| Body Content Type | JSON |
| Body | 아래 참조 |
| `part` | `snippet` |
| `playlistId` | 채널 업로드 목록 ID (아래 참고) |
| `maxResults` | `5` |
| `key` | YouTube Data API v3 키 |
**Body 설정:**
**채널별 playlistId** (채널 ID의 `UC``UU`로 변환):
| 채널 | 채널 ID | playlistId (업로드 목록) |
|---|---|---|
| 머니코믹스 | `UCJo6G1u0e_-wS-JQn3T-zEw` | `UUJo6G1u0e_-wS-JQn3T-zEw` |
| 슈카월드 | `UCsJ6RuBiTVWRX156FVbeaGg` | `UUsJ6RuBiTVWRX156FVbeaGg` |
**응답 예시:**
- **Specify Body:** `Using JSON` (Expression 모드)
- **JSON:**
```json
{
"video_url": "{{ $json.link }}",
"items": [
{
"snippet": {
"publishedAt": "2026-03-25T06:00:00Z",
"title": "영상 제목",
"resourceId": { "videoId": "abc123" }
}
}
]
}
```
### 3. Edit Fields (채널별 각 1개)
| 설정 항목 | 값 |
|---|---|
| Type | Edit Fields (Set) |
| Mode | Manual Mapping |
| Fields to Set | `channel_name` (String) = `머니코믹스` 또는 `슈카월드` |
| Include Other Input Fields | All (기존 데이터 유지) |
### 4. Merge
| 설정 항목 | 값 |
|---|---|
| Type | Merge |
| Mode | Append |
### 5. Code (새 영상 필터링)
| 설정 항목 | 값 |
|---|---|
| Type | Code |
| Language | JavaScript |
최근 1시간 이내 발행된 영상만 필터링합니다:
```javascript
const oneHourAgo = new Date(Date.now() - 60 * 60 * 1000);
return $input.all().filter(item => {
const items = item.json.items || [];
return items.some(i => new Date(i.snippet.publishedAt) > oneHourAgo);
}).flatMap(item => {
const channelName = item.json.channel_name || '';
return (item.json.items || [])
.filter(i => new Date(i.snippet.publishedAt) > oneHourAgo)
.map(i => ({
json: {
video_url: `https://www.youtube.com/watch?v=${i.snippet.resourceId.videoId}`,
title: i.snippet.title,
channel_name: channelName,
}
}));
});
```
> **참고:** Schedule Trigger가 매 정시 실행되므로, 1시간 이내 영상을 필터링하면 새 영상만 처리됩니다. 영상이 없으면 빈 배열이 반환되어 이후 노드가 실행되지 않습니다.
### 6. HTTP Request (요약 API 호출)
| 설정 항목 | 값 |
|---|---|
| Type | HTTP Request |
| Method | POST |
| URL | `https://<서버주소>/api/news/summarize` |
| Body Content Type | JSON |
| Send Body | ON |
**Body (Expression 모드):**
```json
{
"video_url": "{{ $json.video_url }}",
"title": "{{ $json.title }}"
}
```
> **주의:** 영상 제목에 큰따옴표(`"`)가 포함된 경우 JSON이 깨질 수 있습니다. 반드시 **Expression 모드**를 사용하세요 (Fixed 모드에서는 특수문자 이스케이프가 안 됩니다).
**Headers:**
**Headers** (API_SECRET 사용 시):
| 헤더 | 값 |
|---|---|
| `Content-Type` | `application/json` |
| `X-Api-Secret` | 설정한 시크릿 값 (`.env``API_SECRET`과 동일) |
| `X-Api-Secret` | 설정한 시크릿 값 |
## 에러 처리
**Options:**
- **Always Output Data**: ON (에러 시에도 다음 노드로 전달)
### Error Trigger 워크플로우
> **주의:** 영상 제목에 큰따옴표(`"`)가 포함된 경우 JSON이 깨질 수 있습니다. 반드시 **Expression 모드**를 사용하세요.
메인 워크플로우와 별도로 에러 처리 워크플로우를 생성합니다.
1. **Error Trigger** 노드를 추가합니다.
2. **HTTP Request** 노드로 Discord Webhook을 호출합니다:
### 7. Switch (응답 분기)
| 설정 항목 | 값 |
|---|---|
| Method | `POST` |
| URL | Discord Webhook URL |
| Type | Switch |
| Field | `{{ $json.status }}` |
**Rules:**
| Rule | Value | Output |
|---|---|---|
| Rule 1 | `ok` | → Discord 요약 전송 |
| Rule 2 | `skipped` | → No Operation (무시) |
| Rule 3 | `error` | → Discord 에러 알림 |
### 8. Discord 요약 전송 (status=ok)
| 설정 항목 | 값 |
|---|---|
| Type | HTTP Request |
| Method | POST |
| URL | Discord 웹훅 URL |
| Body Content Type | JSON |
**Body:**
```json
{
"embeds": [{
"title": "📰 [{{ $json.channel_name }}] {{ $json.title }}",
"url": "{{ $json.video_url }}",
"description": "{{ $json.summary }}",
"color": 2829105,
"thumbnail": {
"url": "https://img.youtube.com/vi/{{ $json.video_url.split('v=')[1] }}/hqdefault.jpg"
},
"footer": {
"text": "YouTube 뉴스 요약 봇 • {{ $json.channel_name }}"
}
}]
}
```
### 9. Discord 에러 알림 (status=error)
| 설정 항목 | 값 |
|---|---|
| Type | HTTP Request |
| Method | POST |
| URL | Discord 웹훅 URL |
| Body Content Type | JSON |
**Body:**
```json
{
"embeds": [{
"title": "❌ [{{ $json.channel_name }}] 뉴스 요약 실패",
"color": 15548997,
"fields": [
{ "name": "영상 제목", "value": "{{ $json.title }}", "inline": false },
{ "name": "영상 URL", "value": "{{ $json.video_url }}", "inline": false },
{ "name": "에러 타입", "value": "`{{ $json.error_type }}`", "inline": true },
{ "name": "에러 내용", "value": "```\n{{ $json.error_message }}\n```", "inline": false }
],
"footer": {
"text": "YouTube 뉴스 요약 봇 - 에러 알림"
}
}]
}
```
## 에러 처리
### Error Trigger 워크플로우 (선택)
메인 워크플로우 자체가 실패하는 경우를 대비해 별도 에러 워크플로우를 만들 수 있습니다.
1. 새 워크플로우 생성 → **Error Trigger** 노드 추가
2. **HTTP Request** 노드로 Discord Webhook 호출:
```json
{
"content": "뉴스 요약 봇 에러 발생!\n워크플로우: {{ $json.workflow.name }}\n에러: {{ $json.execution.error.message }}"
}
```
3. 메인 워크플로우의 **Settings → Error Workflow**에서 이 에러 워크플로우를 지정합니다.
### HTTP Request 노드 자체 에러 처리
HTTP Request 노드 설정에서:
- **Continue On Fail:** `true`로 설정하면 하나의 영상 요약이 실패해도 나머지 영상은 계속 처리됩니다.
- **Retry On Fail:** `true`, **Max Retries:** `2`, **Wait Between Retries (ms):** `3000`
3. 메인 워크플로우의 **Settings → Error Workflow**에서 이 에러 워크플로우를 지정