Add SEO skills 33-34 and fix bugs in skills 19-34
New skills: - Skill 33: Site migration planner with redirect mapping and monitoring - Skill 34: Reporting dashboard with HTML charts and Korean executive reports Bug fixes (Skill 34 - report_aggregator.py): - Add audit_type fallback for skill identification (was only using audit_id prefix) - Extract health scores from nested data dict (technical_score, onpage_score, etc.) - Support subdomain matching in domain filter (blog.ourdigital.org matches ourdigital.org) - Skip self-referencing DASH- aggregated reports Bug fixes (Skill 20 - naver_serp_analyzer.py): - Remove VIEW tab selectors (removed by Naver in 2026) - Add new section detectors: books (도서), shortform (숏폼), influencer (인플루언서) Improvements (Skill 34 - dashboard/executive report): - Add Korean category labels for Chart.js charts (기술 SEO, 온페이지, etc.) - Add Korean trend labels (개선 중 ↑, 안정 →, 하락 중 ↓) - Add English→Korean issue description translation layer (20 common patterns) Documentation improvements: - Add Korean triggers to 4 skill descriptions (19, 25, 28, 31) - Expand Skill 32 SKILL.md from 40→143 lines (was 6/10, added workflow, output format, limitations) - Add output format examples to Skills 27 and 28 SKILL.md - Add limitations sections to Skills 27 and 28 - Update README.md, CLAUDE.md, AGENTS.md for skills 33-34 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,622 @@
|
||||
"""
|
||||
Executive Report - Korean-language executive summary generation
|
||||
===============================================================
|
||||
Purpose: Generate stakeholder-ready executive summaries in Korean from
|
||||
aggregated SEO report data, with audience-specific detail levels
|
||||
for C-level, marketing, and technical teams.
|
||||
Python: 3.10+
|
||||
|
||||
Usage:
|
||||
python executive_report.py --report aggregated_report.json --audience c-level --output report.md
|
||||
python executive_report.py --report aggregated_report.json --audience marketing --output report.md
|
||||
python executive_report.py --report aggregated_report.json --audience technical --output report.md
|
||||
python executive_report.py --report aggregated_report.json --audience c-level --format notion
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import logging
|
||||
import sys
|
||||
from dataclasses import dataclass, field
|
||||
from datetime import datetime
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
logging.basicConfig(
|
||||
level=logging.INFO,
|
||||
format="%(asctime)s - %(levelname)s - %(message)s",
|
||||
)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Data classes
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
@dataclass
|
||||
class AudienceConfig:
|
||||
"""Configuration for report audience targeting."""
|
||||
level: str = "c-level" # c-level | marketing | technical
|
||||
detail_depth: str = "summary" # summary | moderate | detailed
|
||||
include_recommendations: bool = True
|
||||
include_technical_details: bool = False
|
||||
max_issues: int = 5
|
||||
max_recommendations: int = 5
|
||||
|
||||
@classmethod
|
||||
def from_level(cls, level: str) -> "AudienceConfig":
|
||||
"""Create config preset from audience level."""
|
||||
presets = {
|
||||
"c-level": cls(
|
||||
level="c-level",
|
||||
detail_depth="summary",
|
||||
include_recommendations=True,
|
||||
include_technical_details=False,
|
||||
max_issues=5,
|
||||
max_recommendations=3,
|
||||
),
|
||||
"marketing": cls(
|
||||
level="marketing",
|
||||
detail_depth="moderate",
|
||||
include_recommendations=True,
|
||||
include_technical_details=False,
|
||||
max_issues=10,
|
||||
max_recommendations=5,
|
||||
),
|
||||
"technical": cls(
|
||||
level="technical",
|
||||
detail_depth="detailed",
|
||||
include_recommendations=True,
|
||||
include_technical_details=True,
|
||||
max_issues=20,
|
||||
max_recommendations=10,
|
||||
),
|
||||
}
|
||||
return presets.get(level, presets["c-level"])
|
||||
|
||||
|
||||
@dataclass
|
||||
class ExecutiveSummary:
|
||||
"""Generated executive summary content."""
|
||||
title: str = ""
|
||||
domain: str = ""
|
||||
period: str = ""
|
||||
health_score: float = 0.0
|
||||
health_trend: str = "stable"
|
||||
key_wins: list[str] = field(default_factory=list)
|
||||
key_concerns: list[str] = field(default_factory=list)
|
||||
recommendations: list[str] = field(default_factory=list)
|
||||
narrative: str = ""
|
||||
audience: str = "c-level"
|
||||
category_summary: dict[str, str] = field(default_factory=dict)
|
||||
audit_id: str = ""
|
||||
timestamp: str = ""
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Korean text templates
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
HEALTH_LABELS_KR = {
|
||||
"excellent": "우수",
|
||||
"good": "양호",
|
||||
"average": "보통",
|
||||
"poor": "미흡",
|
||||
"critical": "위험",
|
||||
}
|
||||
|
||||
TREND_LABELS_KR = {
|
||||
"improving": "개선 중",
|
||||
"stable": "안정",
|
||||
"declining": "하락 중",
|
||||
}
|
||||
|
||||
CATEGORY_LABELS_KR = {
|
||||
"technical": "기술 SEO",
|
||||
"on_page": "온페이지 SEO",
|
||||
"performance": "성능 (Core Web Vitals)",
|
||||
"content": "콘텐츠 전략",
|
||||
"links": "링크 프로필",
|
||||
"local": "로컬 SEO",
|
||||
"keywords": "키워드 전략",
|
||||
"competitor": "경쟁 분석",
|
||||
"schema": "스키마/구조화 데이터",
|
||||
"kpi": "KPI 프레임워크",
|
||||
"search_console": "Search Console",
|
||||
"ecommerce": "이커머스 SEO",
|
||||
"international": "국제 SEO",
|
||||
"ai_search": "AI 검색 가시성",
|
||||
"entity_seo": "Knowledge Graph",
|
||||
}
|
||||
|
||||
# Common English issue descriptions -> Korean translations
|
||||
ISSUE_TRANSLATIONS_KR: dict[str, str] = {
|
||||
"missing meta description": "메타 설명(meta description) 누락",
|
||||
"missing title tag": "타이틀 태그 누락",
|
||||
"duplicate title": "중복 타이틀 태그",
|
||||
"duplicate meta description": "중복 메타 설명",
|
||||
"missing h1": "H1 태그 누락",
|
||||
"multiple h1 tags": "H1 태그 다수 사용",
|
||||
"missing alt text": "이미지 alt 텍스트 누락",
|
||||
"broken links": "깨진 링크 발견",
|
||||
"redirect chain": "리다이렉트 체인 발견",
|
||||
"mixed content": "Mixed Content (HTTP/HTTPS 혼합) 발견",
|
||||
"missing canonical": "Canonical 태그 누락",
|
||||
"noindex on important page": "중요 페이지에 noindex 설정됨",
|
||||
"slow page load": "페이지 로딩 속도 저하",
|
||||
"cls exceeds threshold": "CLS(누적 레이아웃 변경) 임계값 초과",
|
||||
"lcp exceeds threshold": "LCP(최대 콘텐츠풀 페인트) 임계값 초과",
|
||||
"missing sitemap": "사이트맵 누락",
|
||||
"robots.txt blocking important pages": "robots.txt에서 중요 페이지 차단 중",
|
||||
"missing schema markup": "스키마 마크업 누락",
|
||||
"missing hreflang": "hreflang 태그 누락",
|
||||
"thin content": "콘텐츠 부족 (Thin Content)",
|
||||
"orphan pages": "고아 페이지 발견 (내부 링크 없음)",
|
||||
}
|
||||
|
||||
|
||||
def _translate_description(desc: str) -> str:
|
||||
"""Translate common English issue descriptions to Korean."""
|
||||
desc_lower = desc.lower().strip()
|
||||
# Check exact match
|
||||
if desc_lower in ISSUE_TRANSLATIONS_KR:
|
||||
return ISSUE_TRANSLATIONS_KR[desc_lower]
|
||||
# Check partial match (case-insensitive replace)
|
||||
for eng, kor in ISSUE_TRANSLATIONS_KR.items():
|
||||
if eng in desc_lower:
|
||||
# Find the original-case substring and replace it
|
||||
idx = desc_lower.index(eng)
|
||||
return desc[:idx] + kor + desc[idx + len(eng):]
|
||||
return desc
|
||||
|
||||
|
||||
AUDIENCE_INTRO_KR = {
|
||||
"c-level": "본 보고서는 SEO 성과의 핵심 지표와 비즈니스 영향을 요약한 경영진용 보고서입니다.",
|
||||
"marketing": "본 보고서는 SEO 전략 실행 현황과 마케팅 성과를 분석한 마케팅팀 보고서입니다.",
|
||||
"technical": "본 보고서는 SEO 기술 진단 결과와 상세 개선 사항을 포함한 기술팀 보고서입니다.",
|
||||
}
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Generator
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
class ExecutiveReportGenerator:
|
||||
"""Generate Korean-language executive reports from aggregated SEO data."""
|
||||
|
||||
@staticmethod
|
||||
def _health_grade(score: float) -> str:
|
||||
"""Return health grade string."""
|
||||
if score >= 90:
|
||||
return "excellent"
|
||||
elif score >= 75:
|
||||
return "good"
|
||||
elif score >= 60:
|
||||
return "average"
|
||||
elif score >= 40:
|
||||
return "poor"
|
||||
else:
|
||||
return "critical"
|
||||
|
||||
def generate_narrative(
|
||||
self,
|
||||
report: dict[str, Any],
|
||||
audience: AudienceConfig,
|
||||
) -> str:
|
||||
"""Generate Korean narrative text for the executive summary."""
|
||||
domain = report.get("domain", "")
|
||||
health = report.get("overall_health", 0)
|
||||
trend = report.get("health_trend", "stable")
|
||||
grade = self._health_grade(health)
|
||||
grade_kr = HEALTH_LABELS_KR.get(grade, grade)
|
||||
trend_kr = TREND_LABELS_KR.get(trend, trend)
|
||||
|
||||
intro = AUDIENCE_INTRO_KR.get(audience.level, AUDIENCE_INTRO_KR["c-level"])
|
||||
|
||||
# Build narrative paragraphs
|
||||
paragraphs = []
|
||||
|
||||
# Opening
|
||||
paragraphs.append(intro)
|
||||
|
||||
# Health overview
|
||||
paragraphs.append(
|
||||
f"{domain}의 전체 SEO Health Score는 **{health}/100** ({grade_kr})이며, "
|
||||
f"현재 추세는 **{trend_kr}** 상태입니다."
|
||||
)
|
||||
|
||||
# Category highlights
|
||||
cat_scores = report.get("category_scores", {})
|
||||
if cat_scores:
|
||||
strong_cats = [
|
||||
CATEGORY_LABELS_KR.get(k, k)
|
||||
for k, v in cat_scores.items()
|
||||
if v >= 75
|
||||
]
|
||||
weak_cats = [
|
||||
CATEGORY_LABELS_KR.get(k, k)
|
||||
for k, v in cat_scores.items()
|
||||
if v < 50
|
||||
]
|
||||
|
||||
if strong_cats:
|
||||
paragraphs.append(
|
||||
f"강점 영역: {', '.join(strong_cats[:3])} 등이 양호한 성과를 보이고 있습니다."
|
||||
)
|
||||
if weak_cats:
|
||||
paragraphs.append(
|
||||
f"개선 필요 영역: {', '.join(weak_cats[:3])} 등에서 집중적인 개선이 필요합니다."
|
||||
)
|
||||
|
||||
# Skills coverage
|
||||
skills = report.get("skills_included", [])
|
||||
if skills:
|
||||
paragraphs.append(
|
||||
f"총 {len(skills)}개의 SEO 진단 도구를 통해 종합 분석을 수행하였습니다."
|
||||
)
|
||||
|
||||
# C-level specific: business impact focus
|
||||
if audience.level == "c-level":
|
||||
if trend == "improving":
|
||||
paragraphs.append(
|
||||
"전반적인 SEO 성과가 개선 추세에 있으며, 현재 전략을 유지하면서 "
|
||||
"핵심 약점 영역에 대한 집중 투자가 권장됩니다."
|
||||
)
|
||||
elif trend == "declining":
|
||||
paragraphs.append(
|
||||
"SEO 성과가 하락 추세를 보이고 있어, 원인 분석과 함께 "
|
||||
"긴급한 대응 조치가 필요합니다."
|
||||
)
|
||||
else:
|
||||
paragraphs.append(
|
||||
"SEO 성과가 안정적으로 유지되고 있으나, 경쟁 환경 변화에 대비하여 "
|
||||
"지속적인 모니터링과 선제적 대응이 필요합니다."
|
||||
)
|
||||
|
||||
# Marketing specific: channel and content focus
|
||||
elif audience.level == "marketing":
|
||||
top_issues = report.get("top_issues", [])
|
||||
content_issues = [
|
||||
i for i in top_issues if i.get("category") in ("content", "keywords")
|
||||
]
|
||||
if content_issues:
|
||||
paragraphs.append(
|
||||
f"콘텐츠/키워드 관련 이슈가 {len(content_issues)}건 발견되었으며, "
|
||||
f"콘텐츠 전략 수정이 권장됩니다."
|
||||
)
|
||||
|
||||
# Technical specific: detailed breakdown
|
||||
elif audience.level == "technical":
|
||||
for cat, score in sorted(
|
||||
cat_scores.items(), key=lambda x: x[1]
|
||||
):
|
||||
cat_kr = CATEGORY_LABELS_KR.get(cat, cat)
|
||||
paragraphs.append(f"- {cat_kr}: {score}/100")
|
||||
|
||||
return "\n\n".join(paragraphs)
|
||||
|
||||
def format_wins(self, report: dict[str, Any]) -> list[str]:
|
||||
"""Extract and format key wins in Korean."""
|
||||
wins = report.get("top_wins", [])
|
||||
formatted: list[str] = []
|
||||
|
||||
for win in wins:
|
||||
desc = _translate_description(win.get("description", ""))
|
||||
cat = win.get("category", "")
|
||||
cat_kr = CATEGORY_LABELS_KR.get(cat, cat)
|
||||
|
||||
if desc:
|
||||
formatted.append(f"[{cat_kr}] {desc}")
|
||||
|
||||
return formatted
|
||||
|
||||
def format_concerns(self, report: dict[str, Any]) -> list[str]:
|
||||
"""Extract and format key concerns in Korean."""
|
||||
issues = report.get("top_issues", [])
|
||||
formatted: list[str] = []
|
||||
|
||||
severity_kr = {
|
||||
"critical": "긴급",
|
||||
"high": "높음",
|
||||
"medium": "보통",
|
||||
"low": "낮음",
|
||||
}
|
||||
|
||||
for issue in issues:
|
||||
desc = _translate_description(issue.get("description", ""))
|
||||
severity = issue.get("severity", "medium")
|
||||
cat = issue.get("category", "")
|
||||
sev_kr = severity_kr.get(severity, severity)
|
||||
cat_kr = CATEGORY_LABELS_KR.get(cat, cat)
|
||||
|
||||
if desc:
|
||||
formatted.append(f"[{sev_kr}] [{cat_kr}] {desc}")
|
||||
|
||||
return formatted
|
||||
|
||||
def generate_recommendations(
|
||||
self,
|
||||
report: dict[str, Any],
|
||||
audience: AudienceConfig,
|
||||
) -> list[str]:
|
||||
"""Generate prioritized action items ranked by impact."""
|
||||
recommendations: list[str] = []
|
||||
cat_scores = report.get("category_scores", {})
|
||||
top_issues = report.get("top_issues", [])
|
||||
|
||||
# Priority 1: Critical issues
|
||||
critical = [i for i in top_issues if i.get("severity") == "critical"]
|
||||
for issue in critical[:3]:
|
||||
cat_kr = CATEGORY_LABELS_KR.get(issue.get("category", ""), "")
|
||||
desc = _translate_description(issue.get("description", ""))
|
||||
if audience.level == "c-level":
|
||||
recommendations.append(
|
||||
f"[긴급] {cat_kr} 영역 긴급 조치 필요 - {desc}"
|
||||
)
|
||||
else:
|
||||
recommendations.append(
|
||||
f"[긴급] {desc} (영역: {cat_kr})"
|
||||
)
|
||||
|
||||
# Priority 2: Weak categories
|
||||
weak_cats = sorted(
|
||||
[(k, v) for k, v in cat_scores.items() if v < 50],
|
||||
key=lambda x: x[1],
|
||||
)
|
||||
for cat, score in weak_cats[:3]:
|
||||
cat_kr = CATEGORY_LABELS_KR.get(cat, cat)
|
||||
if audience.level == "c-level":
|
||||
recommendations.append(
|
||||
f"[개선] {cat_kr} 점수 {score}/100 - 전략적 투자 권장"
|
||||
)
|
||||
elif audience.level == "marketing":
|
||||
recommendations.append(
|
||||
f"[개선] {cat_kr} ({score}/100) - 캠페인 전략 재검토 필요"
|
||||
)
|
||||
else:
|
||||
recommendations.append(
|
||||
f"[개선] {cat_kr} ({score}/100) - 상세 진단 및 기술적 개선 필요"
|
||||
)
|
||||
|
||||
# Priority 3: Maintenance for good categories
|
||||
strong_cats = [
|
||||
(k, v) for k, v in cat_scores.items() if v >= 75
|
||||
]
|
||||
if strong_cats:
|
||||
cats_kr = ", ".join(
|
||||
CATEGORY_LABELS_KR.get(k, k) for k, _ in strong_cats[:3]
|
||||
)
|
||||
recommendations.append(
|
||||
f"[유지] {cats_kr} - 현재 수준 유지 및 모니터링 지속"
|
||||
)
|
||||
|
||||
# Audience-specific recommendations
|
||||
if audience.level == "c-level":
|
||||
health = report.get("overall_health", 0)
|
||||
if health < 60:
|
||||
recommendations.append(
|
||||
"[전략] SEO 개선을 위한 전문 인력 또는 외부 에이전시 투입 검토"
|
||||
)
|
||||
elif audience.level == "marketing":
|
||||
recommendations.append(
|
||||
"[실행] 다음 분기 SEO 개선 로드맵 수립 및 KPI 설정"
|
||||
)
|
||||
elif audience.level == "technical":
|
||||
recommendations.append(
|
||||
"[실행] 기술 부채 해소 스프린트 계획 수립"
|
||||
)
|
||||
|
||||
return recommendations[:audience.max_recommendations]
|
||||
|
||||
def render_markdown(self, summary: ExecutiveSummary) -> str:
|
||||
"""Render executive summary as markdown document."""
|
||||
lines: list[str] = []
|
||||
|
||||
# Title
|
||||
lines.append(f"# {summary.title}")
|
||||
lines.append("")
|
||||
|
||||
# Meta
|
||||
audience_kr = {
|
||||
"c-level": "경영진",
|
||||
"marketing": "마케팅팀",
|
||||
"technical": "기술팀",
|
||||
}
|
||||
lines.append(f"**대상**: {audience_kr.get(summary.audience, summary.audience)}")
|
||||
lines.append(f"**도메인**: {summary.domain}")
|
||||
lines.append(f"**보고 일자**: {summary.period}")
|
||||
lines.append(f"**Audit ID**: {summary.audit_id}")
|
||||
lines.append("")
|
||||
|
||||
# Health Score
|
||||
grade = self._health_grade(summary.health_score)
|
||||
grade_kr = HEALTH_LABELS_KR.get(grade, grade)
|
||||
trend_kr = TREND_LABELS_KR.get(summary.health_trend, summary.health_trend)
|
||||
|
||||
lines.append("## Health Score")
|
||||
lines.append("")
|
||||
lines.append(f"| 지표 | 값 |")
|
||||
lines.append(f"|------|-----|")
|
||||
lines.append(f"| Overall Score | **{summary.health_score}/100** |")
|
||||
lines.append(f"| 등급 | {grade_kr} |")
|
||||
lines.append(f"| 추세 | {trend_kr} |")
|
||||
lines.append("")
|
||||
|
||||
# Category summary
|
||||
if summary.category_summary:
|
||||
lines.append("## 영역별 점수")
|
||||
lines.append("")
|
||||
lines.append("| 영역 | 점수 |")
|
||||
lines.append("|------|------|")
|
||||
for cat, score_str in summary.category_summary.items():
|
||||
cat_kr = CATEGORY_LABELS_KR.get(cat, cat)
|
||||
lines.append(f"| {cat_kr} | {score_str} |")
|
||||
lines.append("")
|
||||
|
||||
# Narrative
|
||||
lines.append("## 종합 분석")
|
||||
lines.append("")
|
||||
lines.append(summary.narrative)
|
||||
lines.append("")
|
||||
|
||||
# Key wins
|
||||
if summary.key_wins:
|
||||
lines.append("## 주요 성과")
|
||||
lines.append("")
|
||||
for win in summary.key_wins:
|
||||
lines.append(f"- {win}")
|
||||
lines.append("")
|
||||
|
||||
# Key concerns
|
||||
if summary.key_concerns:
|
||||
lines.append("## 주요 이슈")
|
||||
lines.append("")
|
||||
for concern in summary.key_concerns:
|
||||
lines.append(f"- {concern}")
|
||||
lines.append("")
|
||||
|
||||
# Recommendations
|
||||
if summary.recommendations:
|
||||
lines.append("## 권장 조치 사항")
|
||||
lines.append("")
|
||||
for i, rec in enumerate(summary.recommendations, 1):
|
||||
lines.append(f"{i}. {rec}")
|
||||
lines.append("")
|
||||
|
||||
# Footer
|
||||
lines.append("---")
|
||||
lines.append(
|
||||
f"*이 보고서는 SEO Reporting Dashboard (Skill 34)에 의해 "
|
||||
f"{summary.timestamp}에 자동 생성되었습니다.*"
|
||||
)
|
||||
|
||||
return "\n".join(lines)
|
||||
|
||||
def run(
|
||||
self,
|
||||
report_json: str,
|
||||
audience_level: str = "c-level",
|
||||
output_path: str | None = None,
|
||||
output_format: str = "markdown",
|
||||
) -> str:
|
||||
"""Orchestrate executive report generation."""
|
||||
# Load report
|
||||
report_path = Path(report_json)
|
||||
if not report_path.exists():
|
||||
raise FileNotFoundError(f"Report file not found: {report_json}")
|
||||
|
||||
report = json.loads(report_path.read_text(encoding="utf-8"))
|
||||
logger.info(f"Loaded report: {report.get('domain', 'unknown')}")
|
||||
|
||||
# Configure audience
|
||||
audience = AudienceConfig.from_level(audience_level)
|
||||
logger.info(f"Audience: {audience.level} (depth: {audience.detail_depth})")
|
||||
|
||||
# Build summary
|
||||
domain = report.get("domain", "")
|
||||
summary = ExecutiveSummary(
|
||||
title=f"SEO 성과 보고서 - {domain}",
|
||||
domain=domain,
|
||||
period=report.get("report_date", ""),
|
||||
health_score=report.get("overall_health", 0),
|
||||
health_trend=report.get("health_trend", "stable"),
|
||||
audit_id=report.get("audit_id", ""),
|
||||
audience=audience.level,
|
||||
timestamp=datetime.now().isoformat(),
|
||||
)
|
||||
|
||||
# Category summary
|
||||
cat_scores = report.get("category_scores", {})
|
||||
summary.category_summary = {
|
||||
cat: f"{score}/100"
|
||||
for cat, score in sorted(
|
||||
cat_scores.items(), key=lambda x: x[1], reverse=True
|
||||
)
|
||||
}
|
||||
|
||||
# Generate content
|
||||
summary.narrative = self.generate_narrative(report, audience)
|
||||
summary.key_wins = self.format_wins(report)[:audience.max_issues]
|
||||
summary.key_concerns = self.format_concerns(report)[:audience.max_issues]
|
||||
summary.recommendations = self.generate_recommendations(report, audience)
|
||||
|
||||
# Render
|
||||
if output_format == "markdown":
|
||||
content = self.render_markdown(summary)
|
||||
elif output_format == "notion":
|
||||
# For Notion, we output markdown that can be pasted into Notion
|
||||
content = self.render_markdown(summary)
|
||||
logger.info(
|
||||
"Notion format: use MCP tools to push this markdown to Notion "
|
||||
f"database {report.get('audit_id', 'DASH-YYYYMMDD-NNN')}"
|
||||
)
|
||||
else:
|
||||
content = self.render_markdown(summary)
|
||||
|
||||
# Save or print
|
||||
if output_path:
|
||||
Path(output_path).write_text(content, encoding="utf-8")
|
||||
logger.info(f"Executive report saved to {output_path}")
|
||||
else:
|
||||
print(content)
|
||||
|
||||
return content
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# CLI
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def parse_args(argv: list[str] | None = None) -> argparse.Namespace:
|
||||
parser = argparse.ArgumentParser(
|
||||
description="SEO Executive Report - Korean-language executive summary generator",
|
||||
formatter_class=argparse.RawDescriptionHelpFormatter,
|
||||
epilog="""\
|
||||
Examples:
|
||||
python executive_report.py --report aggregated_report.json --audience c-level --output report.md
|
||||
python executive_report.py --report aggregated_report.json --audience marketing --output report.md
|
||||
python executive_report.py --report aggregated_report.json --audience technical --format notion
|
||||
""",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--report",
|
||||
required=True,
|
||||
help="Path to aggregated report JSON file (from report_aggregator.py)",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--audience",
|
||||
choices=["c-level", "marketing", "technical"],
|
||||
default="c-level",
|
||||
help="Target audience level (default: c-level)",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--output",
|
||||
type=str,
|
||||
default=None,
|
||||
help="Output file path (prints to stdout if omitted)",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--format",
|
||||
choices=["markdown", "notion"],
|
||||
default="markdown",
|
||||
dest="output_format",
|
||||
help="Output format (default: markdown)",
|
||||
)
|
||||
return parser.parse_args(argv)
|
||||
|
||||
|
||||
def main() -> None:
|
||||
args = parse_args()
|
||||
|
||||
generator = ExecutiveReportGenerator()
|
||||
generator.run(
|
||||
report_json=args.report,
|
||||
audience_level=args.audience,
|
||||
output_path=args.output,
|
||||
output_format=args.output_format,
|
||||
)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in New Issue
Block a user