Docs: [3.0.0] 전체 문서 업데이트 — YouTube Data API + n8n Discord 전송 구조 반영
All checks were successful
news-summary-bot-cicd / build_push_deploy (push) Successful in 25m15s
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:
@@ -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**에서 이 에러 워크플로우를 지정
|
||||
|
||||
Reference in New Issue
Block a user