# 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/일)