auto: 일일 백업 2026-06-17 02:00
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -720,7 +720,25 @@ def send_mail(subject: str, body: str, html: bool = False) -> str:
|
||||
WEEKDAY_KR = ['월', '화', '수', '목', '금', '토', '일']
|
||||
|
||||
|
||||
def build_prepare_payload(mode: str) -> dict:
|
||||
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()
|
||||
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}',
|
||||
}
|
||||
|
||||
|
||||
def build_prepare_payload(mode: str, final: bool = False) -> dict:
|
||||
if mode not in {'morning', 'evening'}:
|
||||
raise SystemExit('mode must be morning or evening')
|
||||
now = datetime.now(KST)
|
||||
@@ -752,6 +770,17 @@ def build_prepare_payload(mode: str) -> dict:
|
||||
}
|
||||
save_json(PENDING_SELECTION_FILE, pending)
|
||||
|
||||
us_market = 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 재시도
|
||||
else:
|
||||
us_send_directive = 'proceed' # 주말 등 예정 없음 / 최종 발송 → US 없이 진행
|
||||
|
||||
return {
|
||||
'mode': mode,
|
||||
'date': today_key,
|
||||
@@ -772,11 +801,13 @@ def build_prepare_payload(mode: str) -> dict:
|
||||
'indices': get_market_snapshot(include_domestic=(mode == 'evening')),
|
||||
},
|
||||
'already_sent': was_already_sent(mode, today_key),
|
||||
'us_market': us_market,
|
||||
'us_send_directive': us_send_directive,
|
||||
}
|
||||
|
||||
|
||||
def cmd_prepare(mode: str) -> int:
|
||||
payload = build_prepare_payload(mode)
|
||||
def cmd_prepare(mode: str, final: bool = False) -> int:
|
||||
payload = build_prepare_payload(mode, final=final)
|
||||
print(json.dumps(payload, ensure_ascii=False, indent=2))
|
||||
return 0
|
||||
|
||||
@@ -888,6 +919,78 @@ def _market_card(title: str, lines: list) -> str:
|
||||
return _card(title, '\n'.join(rows))
|
||||
|
||||
|
||||
_US_GROUP_ICONS = [
|
||||
('지수', '📊'), ('특징주', '🏢'), ('섹터', '🏢'), ('테마', '🏢'),
|
||||
('연준', '🏦'), ('금리', '🏦'), ('일정', '🗓'), ('지표', '🗓'),
|
||||
('전문가', '💬'), ('시각', '💬'), ('전망', '💬'), ('이슈', '📰'),
|
||||
('기타', '🗒'), ('그 외', '🗒'),
|
||||
]
|
||||
|
||||
|
||||
def _us_group_icon(label: str) -> str:
|
||||
for key, icon in _US_GROUP_ICONS:
|
||||
if key in label:
|
||||
return icon
|
||||
return '📰'
|
||||
|
||||
|
||||
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>'
|
||||
|
||||
|
||||
def _us_card(summary, us_market: dict) -> str:
|
||||
"""미국증시 요약 카드.
|
||||
|
||||
summary: 카테고리 그룹 리스트 [{"label": "지수", "items": [...]}, ...] 또는
|
||||
하위호환용 평문 문자열(줄바꿈으로 불릿 구분).
|
||||
"""
|
||||
video = (us_market or {}).get('video') or {}
|
||||
blocks = []
|
||||
if isinstance(summary, list):
|
||||
first = True
|
||||
for g in summary:
|
||||
if not isinstance(g, dict):
|
||||
continue
|
||||
label = str(g.get('label', '')).strip()
|
||||
items = [x for x in (g.get('items') or []) if str(x).strip()]
|
||||
if not items:
|
||||
continue
|
||||
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>'
|
||||
)
|
||||
blocks.append(_us_items_html(items))
|
||||
first = False
|
||||
else:
|
||||
lines = [l for l in str(summary).split('\n') if l.strip()]
|
||||
blocks.append(_us_items_html(lines))
|
||||
|
||||
src = ''
|
||||
if video.get('url'):
|
||||
title = html_escape(video.get('title', '영상'))
|
||||
src = (
|
||||
'<div style="margin-top:11px;padding-top:9px;border-top:1px dashed #cfd8ea;'
|
||||
'color:#8a93a6;font-size:12px;">출처: '
|
||||
f'<a href="{html_escape(video["url"])}" style="color:#1565c0;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;'
|
||||
'padding:14px 16px 13px;">'
|
||||
'<div style="font-size:15px;font-weight:700;color:#0d47a1;margin-bottom:6px;'
|
||||
'letter-spacing:-0.2px;">🇺🇸 미국증시 요약</div>'
|
||||
f'{"".join(blocks)}'
|
||||
f'{src}</div>'
|
||||
)
|
||||
|
||||
|
||||
def _article_card(a: dict) -> str:
|
||||
S = HTML_STYLES
|
||||
title = html_escape(a.get('title', ''))
|
||||
@@ -950,6 +1053,9 @@ 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))
|
||||
@@ -1024,7 +1130,7 @@ def cmd_legacy(mode: str) -> int:
|
||||
|
||||
USAGE = (
|
||||
'usage:\n'
|
||||
' briefing_mail.py prepare {morning|evening}\n'
|
||||
' briefing_mail.py prepare {morning|evening} [--final] # --final: 08:30 최종 발송(US 미게시여도 진행)\n'
|
||||
' briefing_mail.py compose --input {-|path} # input = augmented prepare JSON with summaries\n'
|
||||
' briefing_mail.py send {morning|evening} --subject "..." --body-file {-|path} [--html]\n'
|
||||
' briefing_mail.py {morning|evening} # legacy single-shot\n'
|
||||
@@ -1039,7 +1145,8 @@ def main() -> int:
|
||||
if cmd == 'prepare':
|
||||
if len(args) < 2:
|
||||
raise SystemExit(USAGE)
|
||||
return cmd_prepare(args[1])
|
||||
final = '--final' in args[2:]
|
||||
return cmd_prepare(args[1], final=final)
|
||||
if cmd == 'compose':
|
||||
input_file = None
|
||||
i = 1
|
||||
|
||||
Reference in New Issue
Block a user