auto: 일일 백업 2026-06-18 02:00
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -720,22 +720,41 @@ def send_mail(subject: str, body: str, html: bool = False) -> str:
|
||||
WEEKDAY_KR = ['월', '화', '수', '목', '금', '토', '일']
|
||||
|
||||
|
||||
def _empty_market_block(name: str, err: str) -> dict:
|
||||
return {
|
||||
'channel': name, 'recap_expected_today': False, 'available_today': False,
|
||||
'video': None, 'transcript': '', 'transcript_status': 'error', 'error': err,
|
||||
}
|
||||
|
||||
|
||||
def get_us_market_block() -> dict:
|
||||
"""오전 브리핑용 미국증시 요약 블록. 실패해도 브리핑이 죽지 않도록 예외를 삼킨다."""
|
||||
try:
|
||||
sys.path.insert(0, str(Path(__file__).resolve().parent))
|
||||
import us_market_digest # type: ignore
|
||||
return us_market_digest.get_us_summary()
|
||||
import youtube_briefing_digest # type: ignore
|
||||
return youtube_briefing_digest.get_us_summary()
|
||||
except Exception as e:
|
||||
return {
|
||||
'channel': '미국증시 요약',
|
||||
'recap_expected_today': False,
|
||||
'available_today': False,
|
||||
'video': None,
|
||||
'transcript': '',
|
||||
'transcript_status': 'error',
|
||||
'error': f'{type(e).__name__}: {e}',
|
||||
}
|
||||
return _empty_market_block('미국증시 요약', f'{type(e).__name__}: {e}')
|
||||
|
||||
|
||||
def get_behive_market_block() -> dict:
|
||||
"""오전 브리핑용 국내 증시 시황 블록(비하이브 '주식시황'). best-effort — 없어도 브리핑 진행."""
|
||||
try:
|
||||
sys.path.insert(0, str(Path(__file__).resolve().parent))
|
||||
import youtube_briefing_digest # type: ignore
|
||||
return youtube_briefing_digest.get_behive_market_summary()
|
||||
except Exception as e:
|
||||
return _empty_market_block('국내 증시 시황', f'{type(e).__name__}: {e}')
|
||||
|
||||
|
||||
def get_behive_close_block() -> dict:
|
||||
"""오후 브리핑용 마감시황 블록(비하이브 '마감시황'). best-effort — 없어도 브리핑 진행."""
|
||||
try:
|
||||
sys.path.insert(0, str(Path(__file__).resolve().parent))
|
||||
import youtube_briefing_digest # type: ignore
|
||||
return youtube_briefing_digest.get_behive_close_summary()
|
||||
except Exception as e:
|
||||
return _empty_market_block('마감시황', f'{type(e).__name__}: {e}')
|
||||
|
||||
|
||||
def build_prepare_payload(mode: str, final: bool = False) -> dict:
|
||||
@@ -770,16 +789,25 @@ def build_prepare_payload(mode: str, final: bool = False) -> dict:
|
||||
}
|
||||
save_json(PENDING_SELECTION_FILE, pending)
|
||||
|
||||
# 오전: @futuresnow(상세 미국) + 비하이브 '주식시황'(국내+미국) → LLM이 해외/국내로 재정리.
|
||||
# 오후: 비하이브 '마감시황'(국내 장 마감 해설) → best-effort 마감시황 카드.
|
||||
us_market = None
|
||||
behive_market = None
|
||||
behive_close = None
|
||||
us_send_directive = 'proceed'
|
||||
if mode == 'morning':
|
||||
us_market = get_us_market_block()
|
||||
if us_market.get('available_today'):
|
||||
us_send_directive = 'proceed' # 오늘 요약 영상 있음 → 포함 발송
|
||||
elif us_market.get('recap_expected_today') and not final:
|
||||
us_send_directive = 'wait' # 예정일인데 아직 미게시 → 08:30 재시도
|
||||
behive_market = get_behive_market_block()
|
||||
any_available = us_market.get('available_today') or behive_market.get('available_today')
|
||||
any_expected = us_market.get('recap_expected_today') or behive_market.get('recap_expected_today')
|
||||
if any_available:
|
||||
us_send_directive = 'proceed' # 해외/국내 소스 중 하나라도 게시 → 진행
|
||||
elif any_expected and not final:
|
||||
us_send_directive = 'wait' # 평일인데 둘 다 미게시 → 다음 폴백 재시도
|
||||
else:
|
||||
us_send_directive = 'proceed' # 주말 등 예정 없음 / 최종 발송 → US 없이 진행
|
||||
us_send_directive = 'proceed' # 주말 등 예정 없음 / 최종 발송
|
||||
elif mode == 'evening':
|
||||
behive_close = get_behive_close_block() # best-effort, 발송 게이트 없음
|
||||
|
||||
return {
|
||||
'mode': mode,
|
||||
@@ -802,6 +830,8 @@ def build_prepare_payload(mode: str, final: bool = False) -> dict:
|
||||
},
|
||||
'already_sent': was_already_sent(mode, today_key),
|
||||
'us_market': us_market,
|
||||
'behive_market': behive_market,
|
||||
'behive_close': behive_close,
|
||||
'us_send_directive': us_send_directive,
|
||||
}
|
||||
|
||||
@@ -909,6 +939,11 @@ def _market_card(title: str, lines: list) -> str:
|
||||
f'{html_escape(value)} '
|
||||
f'<span style="{color}">({html_escape(change)})</span>'
|
||||
)
|
||||
else:
|
||||
# 괄호 없이 값 자체에 ▲/▼가 있으면(예: 비하이브 야간선물 "▼ 1.15%") 값에 색 적용
|
||||
color = _market_change_color(value)
|
||||
if color != HTML_STYLES['flat']:
|
||||
right = f'<span style="{color}">{html_escape(value)}</span>'
|
||||
rows.append(
|
||||
f'<div style="{S["market_line"]}">'
|
||||
f'<span style="{S["market_label"]}">{html_escape(label)}</span>'
|
||||
@@ -934,22 +969,62 @@ def _us_group_icon(label: str) -> str:
|
||||
return '📰'
|
||||
|
||||
|
||||
def _emphasize(text: str) -> str:
|
||||
"""`**강조**` 마크다운을 굵게+형광펜 하이라이트 <span>으로 변환. 나머지는 html_escape."""
|
||||
parts = re.split(r'\*\*(.+?)\*\*', str(text))
|
||||
out = []
|
||||
for i, p in enumerate(parts):
|
||||
if i % 2 == 1: # ** ** 안쪽 — 배경 없이 글자만 굵게 + 진한 강조색
|
||||
out.append(
|
||||
f'<span style="font-weight:700;color:#c62828;">{html_escape(p)}</span>'
|
||||
)
|
||||
else:
|
||||
out.append(html_escape(p))
|
||||
return ''.join(out)
|
||||
|
||||
|
||||
def _emphasize_multiline(text: str) -> str:
|
||||
return '<br>'.join(_emphasize(line) for line in str(text).split('\n'))
|
||||
|
||||
|
||||
def _us_items_html(items: list) -> str:
|
||||
lis = '\n'.join(
|
||||
f'<li style="margin:6px 0;padding-left:3px;line-height:1.7;color:#1a1a1a;'
|
||||
f'font-size:13.5px;">{html_escape(_strip_bullet(str(it).strip()))}</li>'
|
||||
for it in items if str(it).strip()
|
||||
)
|
||||
return f'<ul style="margin:2px 0 0;padding-left:19px;">{lis}</ul>'
|
||||
lis = []
|
||||
for it in items:
|
||||
s = _strip_bullet(str(it).strip())
|
||||
if not s:
|
||||
continue
|
||||
# 불릿 앞 '🔥' = 국내 증시 영향 큰 항목 → 강조 배경 밴드
|
||||
if s.startswith('🔥'):
|
||||
s = s[1:].strip()
|
||||
lis.append(
|
||||
'<li style="margin:6px 0;padding:4px 7px;line-height:1.7;color:#1a1a1a;'
|
||||
'font-size:13.5px;background:#fff7f6;border-radius:4px;">'
|
||||
f'🔥 {_emphasize(s)}</li>'
|
||||
)
|
||||
else:
|
||||
lis.append(
|
||||
'<li style="margin:6px 0;padding-left:3px;line-height:1.7;color:#1a1a1a;'
|
||||
f'font-size:13.5px;">{_emphasize(s)}</li>'
|
||||
)
|
||||
return f'<ul style="margin:2px 0 0;padding-left:19px;">{chr(10).join(lis)}</ul>'
|
||||
|
||||
|
||||
def _us_card(summary, us_market: dict) -> str:
|
||||
"""미국증시 요약 카드.
|
||||
_US_PALETTE = {'header': '#0d47a1', 'group': '#1565c0', 'accent': '#1565c0',
|
||||
'bg': '#eef4ff', 'border': '#c5d4f0', 'rule': '#cfd8ea'}
|
||||
_BEHIVE_PALETTE = {'header': '#1b5e20', 'group': '#2e7d32', 'accent': '#2e7d32',
|
||||
'bg': '#eef7ee', 'border': '#c5e0c5', 'rule': '#cfe3cf'}
|
||||
_CLOSE_PALETTE = {'header': '#5d4037', 'group': '#8d6e63', 'accent': '#8d6e63',
|
||||
'bg': '#f7f1ec', 'border': '#e0d3c8', 'rule': '#e6dbd1'}
|
||||
|
||||
|
||||
def _video_summary_card(heading: str, summary, market: dict, pal: dict) -> str:
|
||||
"""시황 영상 요약 카드 (미국증시·국내증시 공용).
|
||||
|
||||
summary: 카테고리 그룹 리스트 [{"label": "지수", "items": [...]}, ...] 또는
|
||||
하위호환용 평문 문자열(줄바꿈으로 불릿 구분).
|
||||
pal: 색상 팔레트(_US_PALETTE / _BEHIVE_PALETTE).
|
||||
"""
|
||||
video = (us_market or {}).get('video') or {}
|
||||
video = (market or {}).get('video') or {}
|
||||
blocks = []
|
||||
if isinstance(summary, list):
|
||||
first = True
|
||||
@@ -963,7 +1038,7 @@ def _us_card(summary, us_market: dict) -> str:
|
||||
mt = '0' if first else '12px'
|
||||
blocks.append(
|
||||
f'<div style="margin:{mt} 0 1px;font-size:12.5px;font-weight:700;'
|
||||
f'color:#1565c0;letter-spacing:-0.2px;">{_us_group_icon(label)} {html_escape(label)}</div>'
|
||||
f'color:{pal["group"]};letter-spacing:-0.2px;">{_us_group_icon(label)} {html_escape(label)}</div>'
|
||||
)
|
||||
blocks.append(_us_items_html(items))
|
||||
first = False
|
||||
@@ -975,17 +1050,17 @@ def _us_card(summary, us_market: dict) -> str:
|
||||
if video.get('url'):
|
||||
title = html_escape(video.get('title', '영상'))
|
||||
src = (
|
||||
'<div style="margin-top:11px;padding-top:9px;border-top:1px dashed #cfd8ea;'
|
||||
f'<div style="margin-top:11px;padding-top:9px;border-top:1px dashed {pal["rule"]};'
|
||||
'color:#8a93a6;font-size:12px;">출처: '
|
||||
f'<a href="{html_escape(video["url"])}" style="color:#1565c0;text-decoration:none;" '
|
||||
f'<a href="{html_escape(video["url"])}" style="color:{pal["accent"]};text-decoration:none;" '
|
||||
f'target="_blank">{title} ↗</a></div>'
|
||||
)
|
||||
return (
|
||||
'<div style="margin-bottom:20px;border:1px solid #c5d4f0;border-left:4px solid #1565c0;'
|
||||
'border-radius:8px;background-color:#eef4ff;'
|
||||
f'<div style="margin-bottom:20px;border:1px solid {pal["border"]};'
|
||||
f'border-left:4px solid {pal["accent"]};border-radius:8px;background-color:{pal["bg"]};'
|
||||
'padding:14px 16px 13px;">'
|
||||
'<div style="font-size:15px;font-weight:700;color:#0d47a1;margin-bottom:6px;'
|
||||
'letter-spacing:-0.2px;">🇺🇸 미국증시 요약</div>'
|
||||
f'<div style="font-size:15px;font-weight:700;color:{pal["header"]};margin-bottom:6px;'
|
||||
f'letter-spacing:-0.2px;">{heading}</div>'
|
||||
f'{"".join(blocks)}'
|
||||
f'{src}</div>'
|
||||
)
|
||||
@@ -1006,15 +1081,26 @@ def _article_card(a: dict) -> str:
|
||||
f' | <a href="{html_escape(link)}" style="{S["link"]}" target="_blank">원문 ↗</a>'
|
||||
if link else ''
|
||||
)
|
||||
parts = [f'<div style="{S["article"]}">']
|
||||
# 국내 증시에 큰 영향을 줄 뉴스: 빨강 강조 테두리 + 🔥 배지
|
||||
impact = bool(a.get('kr_impact'))
|
||||
article_style = S['article']
|
||||
badge = ''
|
||||
if impact:
|
||||
article_style += 'border-left:4px solid #c62828;background:#fff7f6;'
|
||||
badge = (
|
||||
'<span style="display:inline-block;background:#fdecea;color:#c62828;'
|
||||
'font-size:11px;font-weight:700;padding:2px 8px;border-radius:10px;'
|
||||
'margin-right:6px;vertical-align:middle;">🔥 국내증시 영향</span>'
|
||||
)
|
||||
parts = [f'<div style="{article_style}">']
|
||||
parts.append(
|
||||
f'<div><span style="{S["category"]}">{cat}</span>'
|
||||
f'<div>{badge}<span style="{S["category"]}">{cat}</span>'
|
||||
f'<span style="{S["title"]}">{title}</span></div>'
|
||||
)
|
||||
parts.append(f'<div style="{S["source"]}">출처: {source}{link_html}</div>')
|
||||
if summary:
|
||||
parts.append(
|
||||
f'<div style="{S["summary"]}">{html_escape(summary).replace(chr(10), "<br>")}</div>'
|
||||
f'<div style="{S["summary"]}">{_emphasize_multiline(summary)}</div>'
|
||||
)
|
||||
parts.append('</div>')
|
||||
return '\n'.join(parts)
|
||||
@@ -1033,6 +1119,11 @@ def build_html_body(data: dict) -> str:
|
||||
indices = market.get('indices', []) or []
|
||||
futures = market.get('futures', []) or []
|
||||
msci = market.get('msci', []) or []
|
||||
# 비하이브 장전 시황이 말한 값으로 교체 (스크랩값은 전날 것일 수 있음). 없으면 스크랩 유지.
|
||||
if data.get('kospi_futures_override'):
|
||||
futures = [data['kospi_futures_override']]
|
||||
if data.get('msci_override'):
|
||||
msci = [data['msci_override']]
|
||||
|
||||
if mode == 'morning':
|
||||
greet_line1 = '관리자님, 좋은 아침입니다.'
|
||||
@@ -1053,15 +1144,30 @@ def build_html_body(data: dict) -> str:
|
||||
out.append(
|
||||
f'<div style="{S["greet"]}">{html_escape(greet_line1)}<br>{html_escape(greet_line2)}</div>'
|
||||
)
|
||||
us_summary = data.get('us_market_summary')
|
||||
if mode == 'morning' and us_summary:
|
||||
out.append(_us_card(us_summary, data.get('us_market') or {}))
|
||||
# 섹션 순서: 오늘 일정(+내일 상장·공모일정) → 시장 체크 → 해외 → 국내 → 주요 뉴스
|
||||
out.append(_list_card(today_title, events, today_empty))
|
||||
if tomorrow_listings:
|
||||
out.append(_list_card('내일 상장', tomorrow_listings))
|
||||
if ipo:
|
||||
out.append(_list_card('공모일정 변경', ipo))
|
||||
out.append(_market_card(market_title, market_lines))
|
||||
if mode == 'morning':
|
||||
overseas = data.get('overseas_summary')
|
||||
if overseas:
|
||||
# 해외 카드 출처: @futuresnow 우선, 없으면 비하이브
|
||||
ov_market = data.get('us_market') or {}
|
||||
if not (ov_market.get('video')):
|
||||
ov_market = data.get('behive_market') or {}
|
||||
out.append(_video_summary_card('🌎 해외 증시·이슈', overseas, ov_market, _US_PALETTE))
|
||||
domestic = data.get('domestic_summary')
|
||||
if domestic:
|
||||
out.append(_video_summary_card('🇰🇷 국내 증시·이슈', domestic,
|
||||
data.get('behive_market') or {}, _BEHIVE_PALETTE))
|
||||
elif mode == 'evening':
|
||||
close_summary = data.get('close_summary')
|
||||
if close_summary:
|
||||
out.append(_video_summary_card('📕 마감시황', close_summary,
|
||||
data.get('behive_close') or {}, _CLOSE_PALETTE))
|
||||
if articles:
|
||||
out.append(f'<div style="{S["news_section"]}">주요 뉴스</div>')
|
||||
for a in articles:
|
||||
|
||||
Reference in New Issue
Block a user