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>
9.6 KiB
9.6 KiB
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
빌드 & 실행
# 로컬 개발
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를 stripapp/transcript.py— YouTube 자막 추출 (yt-dlp+ 쿠키 인증)app/summarizer.py— Claude Sonnet 4.6으로 요약 + 주식 시장 영향 분석 생성app/config.py— 환경변수 설정 (pydantic-settings)
API 응답 형식
// 성공
{"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:snippetplaylistId: 채널 업로드 목록 ID (채널 ID의UC→UU로 변환)maxResults:5key: YouTube Data API v3 키
- 채널별 playlistId:
- 머니코믹스:
UUJo6G1u0e_-wS-JQn3T-zEw(채널 ID:UCJo6G1u0e_-wS-JQn3T-zEw) - 슈카월드:
UUsJ6RuBiTVWRX156FVbeaGg(채널 ID:UCsJ6RuBiTVWRX156FVbeaGg)
- 머니코믹스:
- 응답 예시:
{
"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시간 이내 발행된 영상만 필터링:
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 모드):
{
"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 에러 알림
- Rule 1: Equal
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 모드 (
=클릭)로 전환 후 아래 입력:
={{ 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 모드 (
=클릭)로 전환 후 아래 입력:
={{ 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 키 발급
- Google Cloud Console → 프로젝트 생성/선택
- "YouTube Data API v3" 검색 → 사용 설정
- 사용자 인증 정보 → API 키 생성
- (선택) 키 제한: YouTube Data API v3만 허용, IP 제한 등
- 일일 쿼터: 10,000 units (
playlistItems.list는 1 unit/요청 → 2채널 × 24회 = 48 units/일)