diff --git a/CLAUDE.md b/CLAUDE.md index d0fca5e..d8f0699 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -28,14 +28,14 @@ n8n(YouTube API로 새 영상 감지) → `POST /api/news/summarize` → 자막 - `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/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": "결론", "summary": "**한줄 요약**\n\n• 포인트1\n• 포인트2\n\n> 결론"} +{"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": "라이브/예정 영상은 요약 대상이 아닙니다"} @@ -44,7 +44,8 @@ n8n(YouTube API로 새 영상 감지) → `POST /api/news/summarize` → 자막 {"video_url": "...", "title": "...", "channel_name": "...", "status": "error", "error_type": "ValueError", "error_message": "..."} ``` -> `summary`는 `oneliner` + `main_points` + `conclusion`을 합친 Discord embed용 통합 필드. +> `summary`는 `oneliner` + `main_points` + `conclusion` + `market_impact` + `sectors` + `watchlist` + `outlook`을 합친 Discord embed용 통합 필드. +> 주식 분석 필드(`market_impact`, `sectors`, `watchlist`, `outlook`)는 뉴스와 무관 시 "해당 뉴스는 주식 시장과 직접적 연관이 없습니다"로 fallback. ## 환경변수 @@ -194,13 +195,17 @@ return $input.all().filter(item => { 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: "YouTube 뉴스 요약 봇 • " + $json.channel_name + text: "⚠️ 투자 참고용 · 판단의 책임은 본인에게 있습니다 • " + $json.channel_name }, timestamp: new Date().toISOString() }] diff --git a/app/main.py b/app/main.py index b245224..0648540 100644 --- a/app/main.py +++ b/app/main.py @@ -49,6 +49,14 @@ async def summarize_video( parts.append(f"\n📌 **주요 내용**\n{summary['main_points']}") if summary.get("conclusion"): parts.append(f"\n🎯 **결론**\n> {summary['conclusion']}") + if summary.get("market_impact"): + parts.append(f"\n📈 **시장 영향**\n{summary['market_impact']}") + if summary.get("sectors"): + parts.append(f"\n🏭 **관련 섹터**\n{summary['sectors']}") + if summary.get("watchlist"): + parts.append(f"\n👀 **주목 종목**\n{summary['watchlist']}") + if summary.get("outlook"): + parts.append(f"\n🔮 **전망**\n{summary['outlook']}") summary["summary"] = "\n".join(parts) return {**base, "status": "ok", **summary} diff --git a/app/summarizer.py b/app/summarizer.py index b17420a..49f6e7c 100644 --- a/app/summarizer.py +++ b/app/summarizer.py @@ -7,22 +7,36 @@ from app.config import settings client = anthropic.Anthropic(api_key=settings.anthropic_api_key) -SYSTEM_PROMPT = """너는 뉴스/경제 유튜브 영상을 시청하고 핵심을 전달하는 요약 전문가야. -영상을 직접 다 본 사람처럼, 구체적인 수치·사례·맥락을 포함해서 요약해줘. -읽는 사람이 영상을 안 봐도 내용을 충분히 파악할 수 있어야 해. +SYSTEM_PROMPT = """너는 뉴스/경제 유튜브 영상을 시청하고 핵심을 전달하는 요약 전문가이자, 주식 시장 영향을 분석하는 증권 애널리스트야. +영상을 직접 다 본 사람처럼, 구체적인 수치·사례·맥락을 포함해서 요약하고, 투자자 관점에서 시장 영향도 분석해줘. ## 출력 형식 반드시 아래 JSON만 출력해. 코드펜스(```)로 감싸지 마. -{"oneliner": "영상의 핵심 메시지를 구체적으로 한 문장", "main_points": "• 핵심 포인트 1\\n• 핵심 포인트 2\\n• 핵심 포인트 3", "conclusion": "영상이 전달하려는 메시지와 시청자가 취할 수 있는 액션"} +{"oneliner": "영상의 핵심 메시지를 구체적으로 한 문장", "main_points": "• 핵심 포인트 1\\n• 핵심 포인트 2\\n• 핵심 포인트 3", "conclusion": "영상이 전달하려는 메시지와 시청자가 취할 수 있는 액션", "market_impact": "이 뉴스가 시장 전체에 미치는 영향 요약 (1~2문장)", "sectors": "📈 섹터명 — 이유\\n📉 섹터명 — 이유", "watchlist": "• 종목명(종목코드) — 주목 이유\\n• 종목명(종목코드) — 주목 이유", "outlook": "향후 1~2주 전망 및 투자자 액션 포인트"} -## 규칙 -- 반드시 위 JSON 형식으로만 응답. 코드펜스, 설명 등 다른 텍스트 절대 금지 -- oneliner, main_points, conclusion 세 필드 모두 반드시 비어있지 않은 값으로 채울 것. 절대 빈 문자열("") 금지 +## 출력 형식 상세 규칙 +- 반드시 위 7개 필드를 가진 단일 JSON 객체만 출력. 다른 텍스트, 코드펜스, 설명 절대 금지 +- 7개 필드 모두 반드시 비어있지 않은 문자열로 채울 것. 절대 빈 문자열("") 금지 +- 모든 필드의 값은 문자열(string)이다. 배열([])이나 중첩 객체({})는 절대 사용 금지 +- 줄바꿈은 반드시 \\n 이스케이프로 표현. 실제 줄바꿈 문자 금지 +- 문자열 안에 큰따옴표(")가 필요하면 반드시 \\"로 이스케이프 + +## 요약 규칙 - 한국어로 작성 - main_points는 3~7개 불릿(•)으로 정리. 각 항목에 구체적인 수치, 종목명, 인물, 사건 등을 반드시 포함 - "~에 대해 이야기했다" 같은 메타 서술 금지. 내용 자체를 직접 전달 - 자막의 오타나 말더듬은 무시하고 의미 중심으로 정리 - 영상에서 언급된 구체적 수치(%, 금액, 날짜 등)가 있으면 반드시 포함 + +## 주식 분석 규칙 +- sectors는 2~4개 항목. 각 항목 앞에 📈(상승) 또는 📉(하락) 이모지로 방향 표시. 항목 사이는 \\n으로 구분 +- watchlist는 3~5개 종목. 한국 주식은 6자리 숫자(예: 005930), 미국 주식은 티커(예: AAPL). 형식: • 종목명(코드) — 이유 +- 한국 주식(KRX) 우선, 관련 있으면 미국 주식도 포함 +- 뉴스 내용과 직접 관련된 종목만 언급. 억지 연결 금지 +- 뉴스가 주식과 전혀 관련 없으면: market_impact, sectors, watchlist, outlook에 "해당 뉴스는 주식 시장과 직접적 연관이 없습니다"라고 작성 + +## 예시 출력 +{"oneliner": "미국 연준이 기준금리를 0.25%p 인하하며 3연속 인하 행진", "main_points": "• 연준 기준금리 5.25%→5.00%로 0.25%p 인하 결정\\n• 파월 의장, 인플레이션 2%대 안착 자신감 표명\\n• 고용 둔화 우려에 선제적 대응 성격\\n• 시장은 이미 반영했다는 분석과 추가 상승 여력 의견 공존", "conclusion": "금리 인하 사이클 진입이 확인되면서 성장주·부동산 등 금리 민감 섹터에 주목할 시점", "market_impact": "금리 인하로 유동성 확대 기대감이 커지며 성장주 중심의 반등 흐름이 예상된다", "sectors": "📈 반도체 — 성장주 밸류에이션 부담 완화로 수혜\\n📈 건설/부동산 — 금리 하락에 따른 주택 수요 회복 기대\\n📉 은행 — 예대마진 축소 우려", "watchlist": "• 삼성전자(005930) — 반도체 업황 회복 + 금리 인하 수혜\\n• SK하이닉스(000660) — HBM 수요 지속 + 유동성 장세 수혜\\n• 현대건설(000720) — 금리 인하 시 건설 경기 회복 수혜주\\n• SOFI(SOFI) — 미국 핀테크, 금리 인하로 대출 수요 증가 기대", "outlook": "3연속 인하로 완화 기조가 확실해진 만큼, 단기 차익 실현보다는 성장주 비중 확대가 유리한 구간. 다만 고용 지표 악화 시 경기침체 우려가 부각될 수 있으므로 다음 고용보고서 발표일 주시 필요"} """ @@ -43,10 +57,15 @@ def summarize(transcript: str, title: str) -> dict: parsed = _parse_json_response(raw) fallback = "(내용 없음)" + stock_fallback = "해당 뉴스는 주식 시장과 직접적 연관이 없습니다" return { "oneliner": parsed.get("oneliner") or fallback, "main_points": parsed.get("main_points") or fallback, "conclusion": parsed.get("conclusion") or fallback, + "market_impact": parsed.get("market_impact") or stock_fallback, + "sectors": parsed.get("sectors") or stock_fallback, + "watchlist": parsed.get("watchlist") or stock_fallback, + "outlook": parsed.get("outlook") or stock_fallback, } diff --git a/docs/n8n-setup.md b/docs/n8n-setup.md index 2d0bb29..1474a8c 100644 --- a/docs/n8n-setup.md +++ b/docs/n8n-setup.md @@ -205,27 +205,19 @@ Specify Body를 **Using JSON**으로 선택 후, JSON 입력란 우측의 **Expr description: "### 💡 " + $json.oneliner, color: 2829105, fields: [ - { - name: "📋 주요 내용", - value: $json.main_points, - inline: false - }, - { - name: "🎯 결론 / 시사점", - value: $json.conclusion, - inline: false - }, - { - name: "🔗 원본 영상", - value: $json.video_url, - inline: false - } + { 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: "YouTube 뉴스 요약 봇 • " + $json.channel_name + text: "⚠️ 투자 참고용 · 판단의 책임은 본인에게 있습니다 • " + $json.channel_name }, timestamp: new Date().toISOString() }]