Refactor: [3.0.0] Discord 전송을 n8n으로 이관, YouTube RSS → Data API 전환
Some checks failed
news-summary-bot-cicd / build_push_deploy (push) Has been cancelled

- FastAPI에서 Discord 직접 전송 제거, 요약 결과를 JSON으로 반환
- app/discord.py 삭제, DISCORD_WEBHOOK_URL 환경변수 제거
- 에러 시 500 대신 200 + status: error로 응답 (n8n에서 분기 처리)
- CLAUDE.md에 n8n 워크플로우 노드별 상세 설정 문서화

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
sm4640
2026-03-25 15:05:36 +09:00
parent 22949d0602
commit 302f892c5d
4 changed files with 176 additions and 169 deletions

184
CLAUDE.md
View File

@@ -4,9 +4,10 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co
## 프로젝트 개요
YouTube 뉴스/경제 채널의 새 영상을 감지하면 자막을 추출하고 Claude API로 요약한 뒤 Discord로 전송하는 봇.
- 영상 감지: n8n RSS 트리거 (외부)
YouTube 뉴스/경제 채널의 새 영상을 감지하면 자막을 추출하고 Claude API로 요약하는 봇.
- 영상 감지: n8n (YouTube Data API v3 `playlistItems.list`)
- 요약 처리: FastAPI 앱 (이 레포)
- Discord 전송: n8n (FastAPI 응답을 받아서 Discord 웹훅으로 전송)
- 배포: Docker Hub → OCI 서버에서 docker-compose pull
## 빌드 & 실행
@@ -23,39 +24,192 @@ docker compose up
## 아키텍처
n8n(RSS 감지) → `POST /api/news/summarize` → 자막 추출 → Claude 요약 → Discord 웹훅
n8n(YouTube API로 새 영상 감지) → `POST /api/news/summarize` → 자막 추출 → Claude 요약 → JSON 응답 → n8n이 Discord 웹훅 전송
- `app/main.py` — FastAPI 엔드포인트 (`/summarize`, `/health`) — Nginx가 `/api/news/` prefix를 strip
- `app/transcript.py` — YouTube 자막 추출 (`yt-dlp` + 쿠키 인증)
- `app/summarizer.py` — Claude Sonnet 4.6으로 요약 생성
- `app/discord.py` — Discord 웹훅 전송
- `app/config.py` — 환경변수 설정 (pydantic-settings)
### API 응답 형식
```json
// 성공
{"status": "ok", "title": "...", "summary": "요약 텍스트"}
// 스킵 (라이브/쇼츠)
{"status": "skipped", "title": "...", "reason": "라이브/예정 영상은 요약 대상이 아닙니다"}
// 에러
{"status": "error", "title": "...", "error_type": "ValueError", "error_message": "..."}
```
## 환경변수
`ANTHROPIC_API_KEY`, `DISCORD_WEBHOOK_URL` 필수. `API_SECRET`은 선택(n8n → FastAPI 인증용).
`ANTHROPIC_API_KEY` 필수. `API_SECRET`은 선택(n8n → FastAPI 인증용).
## 쿠키 인증 (YouTube 봇 감지 우회)
OCI 등 클라우드 서버에서 YouTube 자막 추출 시 봇 감지 차단을 우회하기 위해 쿠키 파일이 필요.
- 브라우저 확장(Get cookies.txt LOCALLY 등)으로 YouTube 쿠키를 `cookies.txt`로 export
- 서버의 `compose.apps.yml`에서 `./news-summary-bot/cookies.txt:/app/cookies.txt:ro`로 마운트
- 쿠키 만료 시(6개월~1년) 재export 필요 → 500 에러 발생 시 쿠키 갱신 확인
- 쿠키 만료 시(6개월~1년) 재export 필요 → 에러 발생 시 쿠키 갱신 확인
## n8n 워크플로우
### 전체 흐름
```
RSS Feed Trigger (채널A) ──┐
Merge → HTTP Request (POST /api/news/summarize)
RSS Feed Trigger (채널B) ──┘
Schedule Trigger (매 정시) ─→ 머니코믹스 playlistItems ─→ Edit Fields (channel_name: 머니코믹스) ──┐
슈카월드 playlistItems ─→ Edit Fields (channel_name: 슈카월드) ──┤
Merge (Append)
Code (새 영상만 필터링)
HTTP Request (POST /api/news/summarize)
Switch (status 분기)
├→ ok → Discord 요약 전송
├→ skipped → No Operation
└→ error → Discord 에러 알림
```
- **RSS Feed Trigger**: 채널별 RSS URL로 새 영상만 감지 (중복 방지 내장, Poll Time으로 주기 설정)
- **Merge**: 두 채널의 새 영상을 하나의 리스트로 합침
- **HTTP Request**: 각 영상마다 `POST <서버IP>/api/news/summarize` 호출
### 노드별 설정
요청 바디:
#### 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`
- **Query Parameters**:
- `part`: `snippet`
- `playlistId`: 채널 업로드 목록 ID (채널 ID의 `UC``UU`로 변환)
- `maxResults`: `5`
- `key`: YouTube Data API v3 키
- **채널별 playlistId**:
- 머니코믹스: `UUJo6G1u0e_-wS-JQn3T-zEw` (채널 ID: `UCJo6G1u0e_-wS-JQn3T-zEw`)
- 슈카월드: `UUsJ6RuBiTVWRX156FVbeaGg` (채널 ID: `UCsJ6RuBiTVWRX156FVbeaGg`)
- **응답 예시**:
```json
{"video_url": "https://youtu.be/xxx", "title": "영상 제목"}
{
"items": [
{
"snippet": {
"publishedAt": "2026-03-25T06:00:00Z",
"title": "영상 제목",
"resourceId": { "videoId": "abc123" }
}
}
]
}
```
`API_SECRET` 설정 시 헤더에 `X-Api-Secret` 포함.
#### 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,
}
}));
});
```
#### 6. HTTP Request (요약 API 호출)
- **Type**: HTTP Request
- **Method**: POST
- **URL**: `https://<서버주소>/api/news/summarize`
- **Body Content Type**: JSON
- **Body**:
```json
{
"video_url": "{{ $json.video_url }}",
"title": "{{ $json.title }}"
}
```
- **Headers** (API_SECRET 사용 시): `X-Api-Secret: <시크릿값>`
- **Options**: "Always Output Data" 켜기 (에러 시에도 다음 노드로 전달)
#### 7. Switch (응답 분기)
- **Type**: Switch
- **Field**: `{{ $json.status }}`
- **Rules**:
- `ok` → Discord 요약 전송
- `skipped` → No Operation (무시)
- `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": "에러 타입", "value": "`{{ $json.error_type }}`", "inline": true },
{ "name": "에러 내용", "value": "```\n{{ $json.error_message }}\n```", "inline": false }
],
"footer": { "text": "YouTube 뉴스 요약 봇 - 에러 알림" }
}]
}
```
### YouTube Data API 키 발급
1. [Google Cloud Console](https://console.cloud.google.com/) → 프로젝트 생성/선택
2. "YouTube Data API v3" 검색 → 사용 설정
3. 사용자 인증 정보 → API 키 생성
4. (선택) 키 제한: YouTube Data API v3만 허용, IP 제한 등
- 일일 쿼터: 10,000 units (`playlistItems.list`는 1 unit/요청 → 2채널 × 24회 = 48 units/일)