auto: 일일 백업 2026-08-13 02:00

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
hyowons
2026-08-13 02:00:00 +09:00
parent c93d1ac385
commit 6428846506
127 changed files with 1822 additions and 1688 deletions
+68 -26
View File
@@ -32,6 +32,8 @@ NEWS_FEEDS = [
('국제', 'https://rss.edaily.co.kr/world_news.xml'),
('증시', 'https://rss.edaily.co.kr/stock_news.xml'),
]
# 토요일 오전(해외 전용 브리핑)에서 쓰는 축소 피드 — 국내 경제·증시 제외
OVERSEAS_NEWS_FEEDS = [f for f in NEWS_FEEDS if f[0] == '국제']
MARKET_FEEDS = [
('USD/KRW', 'https://query1.finance.yahoo.com/v8/finance/chart/KRW%3DX?range=2d&interval=1d'),
]
@@ -353,6 +355,21 @@ def recent_history_keys(history: dict, now: datetime, lookback_hours: int = NEWS
}
def recent_history_token_sets(history: dict, now: datetime,
lookback_hours: int = NEWS_HISTORY_LOOKBACK_HOURS) -> list:
"""최근 발송 제목의 토큰 집합 — 완전일치(key)로는 못 막는 '같은 사안 다른 제목'용."""
cutoff = now - timedelta(hours=lookback_hours)
out = []
for it in history.get('items', []):
title = (it.get('title') or '').strip()
if not title or _parse_iso(it.get('sent_at')) < cutoff:
continue
tokens = _title_tokens(title)
if tokens:
out.append(tokens)
return out
def mark_news_sent_from_pending(mode: str, today_key: str, now: datetime) -> int:
"""발송 성공 후 pending selection을 history로 옮기고 pending에서 제거. 반환: 추가된 항목 수."""
pending = load_json(PENDING_SELECTION_FILE, {})
@@ -387,17 +404,21 @@ def collect_news_articles(
per_category_cap: int = NEWS_PER_CATEGORY_CAP,
published_after: Optional[datetime] = None,
exclude_keys: Optional[set] = None,
exclude_token_sets: Optional[list] = None,
feeds: Optional[list] = None,
) -> list[dict]:
"""Return structured news articles passing cutoff + per-category cap + fuzzy title dedup.
published_after: 발행일이 이보다 이전인 항목 제외 (pub_dt 없으면 통과 — history에서 막힘)
exclude_keys: normalized title이 이 set에 있으면 제외 (최근 발송 이력)
exclude_keys: normalized title이 이 set에 있으면 제외 (최근 발송 이력, 완전일치)
exclude_token_sets: 최근 발송 이력 제목의 토큰 집합 — fuzzy 유사도로도 제외
feeds: 사용할 (category, url) 목록. 기본 NEWS_FEEDS, 토요일은 OVERSEAS_NEWS_FEEDS
"""
exclude_keys = exclude_keys or set()
seen_norm = set()
seen_token_sets = []
seen_token_sets = list(exclude_token_sets or [])
result = []
for category, url in NEWS_FEEDS:
for category, url in (feeds or NEWS_FEEDS):
try:
items = parse_google_news_feed(url)
except Exception as e:
@@ -762,32 +783,48 @@ def build_prepare_payload(mode: str, final: bool = False) -> dict:
raise SystemExit('mode must be morning or evening')
now = datetime.now(KST)
target = now if mode == 'morning' else (now + timedelta(days=1))
events = get_today_events(target)
ipo_changes = read_ipo_changes_for_brief()
tomorrow_listings = get_tomorrow_listings(now)
# 토요일 오전은 해외 전용 — 일정·IPO는 수집하지 않는다. gog 3콜이 빠지고,
# IPO seen을 선점하지 않아 금요일 sync 결과가 월요일 아침에 정상 노출된다.
overseas_only = (mode == 'morning' and now.weekday() == 5)
if overseas_only:
events, ipo_changes, tomorrow_listings = [], [], []
else:
events = get_today_events(target)
ipo_changes = read_ipo_changes_for_brief()
tomorrow_listings = get_tomorrow_listings(now)
cutoff = briefing_cutoff(mode, now)
history = load_news_history()
history = gc_news_history(history, now)
save_json(NEWS_HISTORY_FILE, history)
exclude_keys = recent_history_keys(history, now)
articles = collect_news_articles(published_after=cutoff, exclude_keys=exclude_keys)
exclude_token_sets = recent_history_token_sets(history, now)
articles = collect_news_articles(
published_after=cutoff,
exclude_keys=exclude_keys,
exclude_token_sets=exclude_token_sets,
feeds=OVERSEAS_NEWS_FEEDS if overseas_only else None,
)
today_key = now.strftime('%Y-%m-%d')
already_sent = was_already_sent(mode, today_key)
pending = load_json(PENDING_SELECTION_FILE, {})
pending[mode] = {
'date': today_key,
'articles': [
{
'key': _normalize_news_key(a['title']),
'title': a['title'],
'url': a.get('link', ''),
}
for a in articles
],
}
save_json(PENDING_SELECTION_FILE, pending)
# 이미 발송된 뒤(폴백이 스킵할 예정)에 pending을 덮어쓰면 발송본과 무관한 목록이 남아
# "같은 뉴스가 또 와?" 디버깅 때 잘못된 증거를 보게 된다.
if not already_sent:
pending = load_json(PENDING_SELECTION_FILE, {})
pending[mode] = {
'date': today_key,
'articles': [
{
'key': _normalize_news_key(a['title']),
'title': a['title'],
'url': a.get('link', ''),
}
for a in articles
],
}
save_json(PENDING_SELECTION_FILE, pending)
# 오전: @futuresnow(상세 미국) + 비하이브 '주식시황'(국내+미국) → LLM이 해외/국내로 재정리.
# 오후: 비하이브 '마감시황'(국내 장 마감 해설) → best-effort 마감시황 카드.
@@ -818,6 +855,7 @@ def build_prepare_payload(mode: str, final: bool = False) -> dict:
'ipo_changes': ipo_changes,
'tomorrow_listings': tomorrow_listings,
'articles': articles,
'overseas_only': overseas_only,
'news_window': {
'published_after': cutoff.isoformat(),
'history_lookback_hours': NEWS_HISTORY_LOOKBACK_HOURS,
@@ -828,7 +866,7 @@ def build_prepare_payload(mode: str, final: bool = False) -> dict:
'msci': get_msci_korea() if mode == 'morning' else [],
'indices': get_market_snapshot(include_domestic=(mode == 'evening')),
},
'already_sent': was_already_sent(mode, today_key),
'already_sent': already_sent,
'us_market': us_market,
'behive_market': behive_market,
'behive_close': behive_close,
@@ -1146,10 +1184,12 @@ def _morning_missing_summary_notes(data: dict) -> list[str]:
elif not data.get('overseas_summary'):
notes.append('오선 요약 없음')
if not behive_market.get('available_today'):
notes.append('비하이브 요약 없음')
elif not data.get('domestic_summary'):
notes.append('비하이브 요약 없음')
# 토요일 해외 전용 브리핑은 국내 시황을 애초에 넣지 않으므로 "없음" 각주가 노이즈다.
if not data.get('overseas_only'):
if not behive_market.get('available_today'):
notes.append('비하이브 요약 없음')
elif not data.get('domestic_summary'):
notes.append('비하이브 요약 없음')
return notes
@@ -1197,7 +1237,9 @@ def build_html_body(data: dict) -> str:
if notice:
out.append(notice)
# 섹션 순서: 오늘 일정(+내일 상장·공모일정) → 시장 체크 → 해외 → 국내 → 주요 뉴스
out.append(_list_card(today_title, events, today_empty))
# 토요일 해외 전용 브리핑은 일정 카드를 아예 뺀다 (events가 비어도 "일정 없습니다" 카드가 렌더되므로)
if not data.get('overseas_only'):
out.append(_list_card(today_title, events, today_empty))
if tomorrow_listings:
out.append(_list_card('내일 상장', tomorrow_listings))
if ipo: