Files
news-summary-bot/docs/n8n-setup.md
sm4640 b34bc1f582
All checks were successful
news-summary-bot-cicd / build_push_deploy (push) Successful in 11m18s
Feat: [4.0.0] 뉴스 요약에 주식 시장 영향 분석 기능 추가
요약 프롬프트에 증권 애널리스트 역할을 통합하여 API 1회 호출로
시장 영향, 관련 섹터, 주목 종목, 전망을 함께 생성.
모든 필드는 단순 문자열로 유지하여 파싱 안정성 확보.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-28 15:30:22 +09:00

308 lines
9.9 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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 }}",
"channel_name": "{{ $json.channel_name }}"
}
```
**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:**
Specify Body를 **Using JSON**으로 선택 후, JSON 입력란 우측의 **Expression** 토글을 켜고 아래 코드를 입력합니다. `JSON.stringify`를 사용해야 요약 텍스트의 특수문자(따옴표, 줄바꿈 등)가 자동 이스케이프됩니다.
```javascript
={{ JSON.stringify({
embeds: [{
title: "📰 [" + $json.channel_name + "] " + $json.title,
url: $json.video_url,
description: "### 💡 " + $json.oneliner,
color: 2829105,
fields: [
{ name: "📋 주요 내용", value: $json.main_points, inline: false },
{ name: "🎯 결론 / 시사점", value: $json.conclusion, inline: false },
{ name: "📈 시장 영향", value: $json.market_impact, inline: false },
{ name: "🏭 관련 섹터", value: $json.sectors, inline: false },
{ name: "👀 주목 종목", value: $json.watchlist, inline: false },
{ name: "🔮 전망", value: $json.outlook, inline: false },
{ name: "🔗 원본 영상", value: $json.video_url, inline: false }
],
thumbnail: {
url: "https://img.youtube.com/vi/" + $json.video_url.split("v=")[1] + "/hqdefault.jpg"
},
footer: {
text: "⚠️ 투자 참고용 · 판단의 책임은 본인에게 있습니다 • " + $json.channel_name
},
timestamp: new Date().toISOString()
}]
}) }}
```
### 9. Discord 에러 알림 (status=error)
| 설정 항목 | 값 |
|---|---|
| Type | HTTP Request |
| Method | POST |
| URL | Discord 웹훅 URL |
| Send Body | ON |
| Body Content Type | JSON |
| Specify Body | Using JSON (Expression 토글 ON) |
**Body:**
```javascript
={{ JSON.stringify({
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 응답"
},
timestamp: new Date().toISOString()
}]
}) }}
```
## 에러 처리
에러 알림은 두 종류로 구분됩니다:
| 구분 | 발생 위치 | 원인 예시 | 알림 제목 |
|---|---|---|---|
| **워크플로우 에러** | 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 처리합니다.