All checks were successful
news-summary-bot-cicd / build_push_deploy (push) Successful in 11m18s
요약 프롬프트에 증권 애널리스트 역할을 통합하여 API 1회 호출로 시장 영향, 관련 섹터, 주목 종목, 전망을 함께 생성. 모든 필드는 단순 문자열로 유지하여 파싱 안정성 확보. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
247 lines
9.6 KiB
Markdown
247 lines
9.6 KiB
Markdown
# 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
|
||
// 성공
|
||
{"video_url": "...", "title": "...", "channel_name": "...", "status": "ok", "oneliner": "한줄 요약", "main_points": "• 포인트1\n• 포인트2", "conclusion": "결론", "market_impact": "시장 영향 요약", "sectors": "📈 섹터명 — 이유\n📉 섹터명 — 이유", "watchlist": "• 종목명(코드) — 이유\n• 종목명(코드) — 이유", "outlook": "전망 및 액션 포인트", "summary": "통합 요약 텍스트"}
|
||
|
||
// 스킵 (라이브/쇼츠)
|
||
{"video_url": "...", "title": "...", "channel_name": "...", "status": "skipped", "reason": "라이브/예정 영상은 요약 대상이 아닙니다"}
|
||
|
||
// 에러
|
||
{"video_url": "...", "title": "...", "channel_name": "...", "status": "error", "error_type": "ValueError", "error_message": "..."}
|
||
```
|
||
|
||
> `summary`는 `oneliner` + `main_points` + `conclusion` + `market_impact` + `sectors` + `watchlist` + `outlook`을 합친 Discord embed용 통합 필드.
|
||
> 주식 분석 필드(`market_impact`, `sectors`, `watchlist`, `outlook`)는 뉴스와 무관 시 "해당 뉴스는 주식 시장과 직접적 연관이 없습니다"로 fallback.
|
||
|
||
## 환경변수
|
||
|
||
`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`
|
||
- **Send Query Parameters**: ON
|
||
- **Specify Query Parameters**: Using Individual Fields
|
||
- **Query Parameters (Add Parameter로 추가)**:
|
||
- `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
|
||
- **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,
|
||
}
|
||
}));
|
||
});
|
||
```
|
||
|
||
#### 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" 켜기 (에러 시에도 다음 노드로 전달)
|
||
|
||
#### 7. Switch (응답 분기)
|
||
- **Type**: Switch
|
||
- **Mode**: Rules
|
||
- **Data Type**: String
|
||
- **Value**: `{{ $json.status }}`
|
||
- **Rules**:
|
||
- 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 모드 (`=` 클릭)로 전환 후 아래 입력:
|
||
|
||
```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
|
||
- **Body**: Expression 모드 (`=` 클릭)로 전환 후 아래 입력:
|
||
|
||
```javascript
|
||
={{ JSON.stringify({
|
||
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: "봇 요약 처리 에러 — FastAPI 응답"
|
||
}
|
||
}]
|
||
}) }}
|
||
```
|
||
|
||
### 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/일)
|