Files
news-summary-bot/CLAUDE.md
sm4640 302f892c5d
Some checks failed
news-summary-bot-cicd / build_push_deploy (push) Has been cancelled
Refactor: [3.0.0] Discord 전송을 n8n으로 이관, YouTube RSS → Data API 전환
- 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>
2026-03-25 15:05:36 +09:00

216 lines
7.6 KiB
Markdown
Raw 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.
# CLAUDE.md
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
## 프로젝트 개요
YouTube 뉴스/경제 채널의 새 영상을 감지하면 자막을 추출하고 Claude API로 요약하는 봇.
- 영상 감지: n8n (YouTube Data API v3 `playlistItems.list`)
- 요약 처리: FastAPI 앱 (이 레포)
- Discord 전송: n8n (FastAPI 응답을 받아서 Discord 웹훅으로 전송)
- 배포: Docker Hub → OCI 서버에서 docker-compose pull
## 빌드 & 실행
```bash
# 로컬 개발
pip install -r requirements.txt
uvicorn app.main:app --reload
# Docker
docker build -t nkey/news-summary-bot .
docker compose up
```
## 아키텍처
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/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` 필수. `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 필요 → 에러 발생 시 쿠키 갱신 확인
## n8n 워크플로우
### 전체 흐름
```
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 에러 알림
```
### 노드별 설정
#### 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
{
"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,
}
}));
});
```
#### 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/일)