- Error Trigger: 임베드 형식으로 변경, 실패 노드/실행 ID 포함, fallback 처리 - Switch error: footer를 "봇 요약 처리 에러"로 구분 - 에러 유형 비교 테이블 추가 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
294 lines
8.9 KiB
Markdown
294 lines
8.9 KiB
Markdown
# n8n 워크플로우 구성 가이드
|
||
|
||
## 워크플로우 개요
|
||
|
||
이 워크플로우는 2개의 YouTube 채널(머니코믹스, 슈카월드)의 새 영상을 YouTube Data API v3로 감지하고, FastAPI `/api/news/summarize` 엔드포인트를 호출하여 요약한 뒤, 응답 결과에 따라 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. Schedule Trigger
|
||
|
||
| 설정 항목 | 값 |
|
||
|---|---|
|
||
| Type | Schedule Trigger |
|
||
| Rule | Every Hour |
|
||
| Minute | 0 |
|
||
|
||
### 2. YouTube API — playlistItems (채널별 각 1개)
|
||
|
||
| 설정 항목 | 값 |
|
||
|---|---|
|
||
| Type | HTTP Request |
|
||
| Method | GET |
|
||
| URL | `https://www.googleapis.com/youtube/v3/playlistItems` |
|
||
| Send Query Parameters | ON |
|
||
| Specify Query Parameters | Using Individual Fields |
|
||
|
||
**Query Parameters (Add Parameter로 추가):**
|
||
|
||
| 파라미터 | 값 |
|
||
|---|---|
|
||
| `part` | `snippet` |
|
||
| `playlistId` | 채널 업로드 목록 ID (아래 참고) |
|
||
| `maxResults` | `5` |
|
||
| `key` | YouTube Data API v3 키 |
|
||
|
||
**채널별 playlistId** (채널 ID의 `UC` → `UU`로 변환):
|
||
|
||
| 채널 | 채널 ID | playlistId (업로드 목록) |
|
||
|---|---|---|
|
||
| 머니코믹스 | `UCJo6G1u0e_-wS-JQn3T-zEw` | `UUJo6G1u0e_-wS-JQn3T-zEw` |
|
||
| 슈카월드 | `UCsJ6RuBiTVWRX156FVbeaGg` | `UUsJ6RuBiTVWRX156FVbeaGg` |
|
||
|
||
**응답 예시:**
|
||
|
||
```json
|
||
{
|
||
"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 |
|
||
| Mode | Run Once for All Items |
|
||
|
||
최근 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` |
|
||
| Send Body | ON |
|
||
| Body Content Type | JSON |
|
||
| Specify Body | Using JSON |
|
||
|
||
**Body (Expression 모드):**
|
||
|
||
```json
|
||
{
|
||
"video_url": "{{ $json.video_url }}",
|
||
"title": "{{ $json.title }}"
|
||
}
|
||
```
|
||
|
||
**Headers** (API_SECRET 사용 시):
|
||
|
||
| 헤더 | 값 |
|
||
|---|---|
|
||
| `X-Api-Secret` | 설정한 시크릿 값 |
|
||
|
||
**Options:**
|
||
- **Always Output Data**: ON (에러 시에도 다음 노드로 전달)
|
||
|
||
> **주의:** 영상 제목에 큰따옴표(`"`)가 포함된 경우 JSON이 깨질 수 있습니다. 반드시 **Expression 모드**를 사용하세요.
|
||
|
||
### 7. Switch (응답 분기)
|
||
|
||
| 설정 항목 | 값 |
|
||
|---|---|
|
||
| Type | Switch |
|
||
| Mode | Rules |
|
||
| Data Type | String |
|
||
| Value | `{{ $json.status }}` |
|
||
|
||
**Rules:**
|
||
|
||
| Rule | Operation | Value | Output |
|
||
|---|---|---|---|
|
||
| Rule 1 | Equal | `ok` | → Discord 요약 전송 |
|
||
| Rule 2 | Equal | `skipped` | → No Operation (무시) |
|
||
| Rule 3 | Equal | `error` | → Discord 에러 알림 |
|
||
|
||
### 8. Discord 요약 전송 (status=ok)
|
||
|
||
| 설정 항목 | 값 |
|
||
|---|---|
|
||
| Type | HTTP Request |
|
||
| Method | POST |
|
||
| URL | Discord 웹훅 URL |
|
||
| Send Body | ON |
|
||
| Body Content Type | JSON |
|
||
| Specify Body | Using JSON |
|
||
|
||
**Body (Expression 모드):**
|
||
|
||
```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 |
|
||
| Send Body | ON |
|
||
| Body Content Type | JSON |
|
||
| Specify Body | Using JSON |
|
||
|
||
**Body (Expression 모드):**
|
||
|
||
```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": "봇 요약 처리 에러 — FastAPI 응답"
|
||
}
|
||
}]
|
||
}
|
||
```
|
||
|
||
## 에러 처리
|
||
|
||
에러 알림은 두 종류로 구분됩니다:
|
||
|
||
| 구분 | 발생 위치 | 원인 예시 | 알림 제목 |
|
||
|---|---|---|---|
|
||
| **워크플로우 에러** | n8n 자체 | API 연결 불가, 노드 설정 오류, YouTube API 실패 | ⚠️ n8n 워크플로우 에러 |
|
||
| **요약 처리 에러** | FastAPI 봇 | 자막 없음, 쿠키 만료, Claude API 오류 | ❌ [채널명] 뉴스 요약 실패 |
|
||
|
||
### Error Trigger 워크플로우 (n8n 워크플로우 에러)
|
||
|
||
메인 워크플로우 자체가 실패하는 경우를 대비해 별도 에러 워크플로우를 만들 수 있습니다.
|
||
|
||
1. 새 워크플로우 생성 → **Error Trigger** 노드 추가
|
||
2. **HTTP Request** 노드로 Discord Webhook 호출:
|
||
|
||
| 설정 항목 | 값 |
|
||
|---|---|
|
||
| Type | HTTP Request |
|
||
| Method | POST |
|
||
| URL | Discord 웹훅 URL |
|
||
| Send Body | ON |
|
||
| Body Content Type | JSON |
|
||
| Specify Body | Using JSON |
|
||
|
||
**Body (Expression 모드):**
|
||
|
||
```json
|
||
{
|
||
"embeds": [{
|
||
"title": "⚠️ n8n 워크플로우 에러",
|
||
"color": 16753920,
|
||
"description": "n8n 워크플로우 실행 중 에러가 발생했습니다. 봇 요약 처리가 아닌 **워크플로우 자체 문제**입니다.",
|
||
"fields": [
|
||
{ "name": "워크플로우", "value": "{{ $json.workflow.name }}", "inline": true },
|
||
{ "name": "실패 노드", "value": "{{ $json.execution.lastNodeExecuted }}", "inline": true },
|
||
{ "name": "에러 내용", "value": "```\n{{ $json.execution.error.message || '상세 내용 없음' }}\n```", "inline": false },
|
||
{ "name": "실행 ID", "value": "`{{ $json.execution.id }}`", "inline": true }
|
||
],
|
||
"footer": {
|
||
"text": "n8n Error Trigger — 워크플로우 에러"
|
||
}
|
||
}]
|
||
}
|
||
```
|
||
|
||
3. 메인 워크플로우의 **Settings → Error Workflow**에서 이 에러 워크플로우를 지정
|
||
|
||
> **참고:** `$json.execution.error.message`가 빈 값일 수 있으므로 `|| '상세 내용 없음'`으로 fallback 처리합니다.
|