""" 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()