Feat: [4.0.0] 뉴스 요약에 주식 시장 영향 분석 기능 추가
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>
This commit is contained in:
sm4640
2026-03-28 15:30:22 +09:00
parent 6ae4325994
commit b34bc1f582
4 changed files with 51 additions and 27 deletions

View File

@@ -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()
}]

View File

@@ -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}

View File

@@ -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,
}

View File

@@ -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()
}]