# 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 처리합니다.