# Python으로 Money Forward 스타일 재무 PDF 리포트 만들기 - Part 10 ## 완성 및 자동화 ### 최종 프로젝트 구조 ``` 20251207-make-pdf-report/ ├── pyproject.toml ├── .env ├── README.md │ ├── src/ │ ├── __init__.py │ ├── main.py # CLI 엔트리포인트 │ ├── config.py # 설정 │ ├── sheets_client.py # Google Sheets API │ ├── data_fetcher.py # 데이터 조회 │ ├── data_parser.py # 데이터 파싱 │ ├── data_models.py # 데이터 모델 │ │ │ ├── charts/ # 차트 모듈 │ │ ├── __init__.py │ │ ├── combo_chart.py │ │ ├── line_chart.py │ │ ├── radar_chart.py │ │ ├── waterfall_chart.py │ │ ├── donut_chart.py │ │ └── breakeven_chart.py │ │ │ └── pdf/ # PDF 모듈 │ ├── __init__.py │ ├── generator.py │ ├── styles.py │ ├── components/ │ │ ├── __init__.py │ │ ├── kpi_card.py │ │ ├── financial_table.py │ │ └── chart_renderer.py │ └── pages/ │ ├── __init__.py │ ├── dashboard.py │ ├── summary.py │ ├── pl_table.py │ ├── analysis.py │ ├── cashflow.py │ ├── variable_cost.py │ ├── financial_summary.py │ └── highlight.py │ ├── output/ # 생성된 PDF └── tests/ └── test_report.py ``` ### CLI 메인 엔트리포인트 `src/main.py`: ```python #!/usr/bin/env python3 """ Money Forward 스타일 재무 PDF 리포트 생성기 Usage: python -m src.main generate --month 2025-10 python -m src.main generate --output report.pdf python -m src.main preview # 미리보기 모드 """ import argparse from pathlib import Path from datetime import datetime from config import OUTPUT_DIR, COMPANY_INFO from sheets_client import sheets_client from data_fetcher import fetch_comparison_data from data_parser import build_financial_report from pdf.generator import ReportGenerator def generate_report( target_month: str = None, output_path: Path = None, open_after: bool = False, ) -> Path: """PDF 리포트 생성 Args: target_month: 대상 월 (예: '2025-10') output_path: 출력 경로 open_after: 생성 후 자동 열기 Returns: 생성된 PDF 경로 """ print("📊 Money Forward 스타일 재무 리포트 생성 시작...") # 1. 데이터 조회 print(" [1/4] Google Sheets에서 데이터 조회 중...") raw_data = fetch_comparison_data() if not raw_data: raise ValueError("스프레드시트에서 데이터를 찾을 수 없습니다.") print(f" → {len(raw_data)}개 행 조회 완료") # 2. 데이터 파싱 print(" [2/4] 데이터 파싱 중...") target = target_month or datetime.now().strftime("%Y年%m月") report_data = build_financial_report( raw_data=raw_data, company_name=COMPANY_INFO['name'], fiscal_year=COMPANY_INFO['fiscal_year'], period=COMPANY_INFO['period'], target_month=target, ) print(f" → KPI {len(report_data.annual_kpis)}개, P/L 항목 {len(report_data.pl_items)}개") # 3. PDF 생성 print(" [3/4] PDF 생성 중...") if output_path is None: timestamp = datetime.now().strftime("%Y%m%d_%H%M%S") output_path = OUTPUT_DIR / f"financial_report_{timestamp}.pdf" output_path.parent.mkdir(parents=True, exist_ok=True) generator = ReportGenerator(output_path, report_data) generator.generate() print(f" → {generator.total_pages}페이지 생성 완료") # 4. 완료 print(" [4/4] 저장 완료!") print(f"\n✅ PDF 생성 완료: {output_path}") print(f" 파일 크기: {output_path.stat().st_size / 1024:.1f} KB") # 자동 열기 if open_after: import subprocess import platform if platform.system() == 'Darwin': # macOS subprocess.run(['open', str(output_path)]) elif platform.system() == 'Windows': subprocess.run(['start', str(output_path)], shell=True) else: # Linux subprocess.run(['xdg-open', str(output_path)]) return output_path def list_sheets(): """스프레드시트 시트 목록 출력""" print("📋 스프레드시트 시트 목록:") sheets = sheets_client.get_all_sheets() for sheet in sheets: print(f" - {sheet['title']} (gid: {sheet['sheet_id']})") def main(): parser = argparse.ArgumentParser( description="Money Forward 스타일 재무 PDF 리포트 생성기" ) subparsers = parser.add_subparsers(dest='command', help='명령어') # generate 명령 gen_parser = subparsers.add_parser('generate', help='PDF 리포트 생성') gen_parser.add_argument( '--month', '-m', help='대상 월 (예: 2025-10)', default=None ) gen_parser.add_argument( '--output', '-o', type=Path, help='출력 파일 경로', default=None ) gen_parser.add_argument( '--open', action='store_true', help='생성 후 자동 열기' ) # list 명령 subparsers.add_parser('list', help='시트 목록 조회') # preview 명령 preview_parser = subparsers.add_parser('preview', help='미리보기 (샘플 데이터)') preview_parser.add_argument( '--open', action='store_true', help='생성 후 자동 열기' ) args = parser.parse_args() if args.command == 'generate': generate_report( target_month=args.month, output_path=args.output, open_after=args.open, ) elif args.command == 'list': list_sheets() elif args.command == 'preview': # 샘플 데이터로 미리보기 output_path = OUTPUT_DIR / "preview_report.pdf" generate_report( target_month="2025年10月", output_path=output_path, open_after=args.open, ) else: parser.print_help() if __name__ == "__main__": main() ``` ### 자동화 스크립트 `scripts/auto_generate.py`: ```python #!/usr/bin/env python3 """ 월간 자동 리포트 생성 스크립트 cron이나 Task Scheduler에서 실행 """ import sys from pathlib import Path from datetime import datetime import smtplib from email.mime.multipart import MIMEMultipart from email.mime.base import MIMEBase from email.mime.text import MIMEText from email import encoders # 프로젝트 루트를 path에 추가 sys.path.insert(0, str(Path(__file__).parent.parent / 'src')) from main import generate_report from config import OUTPUT_DIR def send_email_with_attachment( to_email: str, subject: str, body: str, attachment_path: Path, smtp_server: str = "smtp.gmail.com", smtp_port: int = 587, from_email: str = None, password: str = None, ): """이메일로 PDF 전송""" msg = MIMEMultipart() msg['From'] = from_email msg['To'] = to_email msg['Subject'] = subject msg.attach(MIMEText(body, 'plain')) # 첨부파일 with open(attachment_path, 'rb') as f: part = MIMEBase('application', 'octet-stream') part.set_payload(f.read()) encoders.encode_base64(part) part.add_header( 'Content-Disposition', f'attachment; filename="{attachment_path.name}"' ) msg.attach(part) # 전송 with smtplib.SMTP(smtp_server, smtp_port) as server: server.starttls() server.login(from_email, password) server.send_message(msg) def main(): """매월 1일 자동 실행""" now = datetime.now() # 전월 리포트 생성 if now.month == 1: target_year = now.year - 1 target_month = 12 else: target_year = now.year target_month = now.month - 1 target_str = f"{target_year}年{target_month:02d}月" print(f"📅 {target_str} 리포트 자동 생성 시작") try: # 리포트 생성 output_path = generate_report( target_month=target_str, output_path=OUTPUT_DIR / f"monthly_report_{target_year}{target_month:02d}.pdf" ) print(f"✅ 리포트 생성 완료: {output_path}") # 이메일 전송 (선택) # send_email_with_attachment( # to_email="cfo@example.com", # subject=f"[자동생성] {target_str} 재무 리포트", # body=f"{target_str} 재무 리포트가 첨부되어 있습니다.", # attachment_path=output_path, # ) except Exception as e: print(f"❌ 오류 발생: {e}") raise if __name__ == "__main__": main() ``` ### cron 설정 (Linux/macOS) ```bash # crontab -e # 매월 1일 오전 9시에 실행 0 9 1 * * cd /path/to/project && /path/to/venv/bin/python scripts/auto_generate.py >> /var/log/pdf_report.log 2>&1 ``` ### 테스트 `tests/test_report.py`: ```python """리포트 생성 테스트""" import pytest from pathlib import Path from decimal import Decimal from data_models import FinancialReport, KPICard, PLItem, MonthlyValue from pdf.generator import ReportGenerator @pytest.fixture def sample_report(): """테스트용 샘플 리포트""" return FinancialReport( company_name="テスト株式会社", fiscal_year="2025年度", period="4月1日〜3月31日", target_month="2025年10月", generated_at=datetime.now().date(), annual_kpis=[ KPICard(title="売上高", value=Decimal(100000000)), KPICard(title="営業利益", value=Decimal(10000000)), ], pl_items=[ PLItem( name="売上高", level=0, monthly_values=[ MonthlyValue(month="2025/04月", budget=Decimal(8000000), actual=Decimal(8500000)), ] ) ], ) def test_pdf_generation(sample_report, tmp_path): """PDF 생성 테스트""" output_path = tmp_path / "test_report.pdf" generator = ReportGenerator(output_path, sample_report) generator.generate() assert output_path.exists() assert output_path.stat().st_size > 0 def test_kpi_card_formatting(): """KPI 카드 포맷팅 테스트""" card = KPICard( title="売上高", value=Decimal(125947574), unit="円", ) assert card.formatted_value == "125,947,574円" def test_pl_item_totals(): """P/L 항목 합계 테스트""" item = PLItem( name="売上高", monthly_values=[ MonthlyValue(month="04", budget=Decimal(100), actual=Decimal(120)), MonthlyValue(month="05", budget=Decimal(200), actual=Decimal(180)), ] ) assert item.total_budget == Decimal(300) assert item.total_actual == Decimal(300) assert item.total_variance == Decimal(0) ``` ### 사용 예시 ```bash # 가상환경 활성화 source .venv/bin/activate # 기본 생성 python -m src.main generate # 특정 월 지정 python -m src.main generate --month 2025-10 # 파일명 지정 + 자동 열기 python -m src.main generate --output my_report.pdf --open # 시트 목록 확인 python -m src.main list # 미리보기 (샘플 데이터) python -m src.main preview --open ``` ### 결론 이 시리즈를 통해 다음을 구현했습니다: 1. **환경 설정**: uv + venv로 Python 환경 구성 2. **데이터 연동**: Google Sheets API로 실시간 데이터 조회 3. **데이터 모델**: dataclass로 타입 안전한 데이터 구조 4. **PDF 생성**: ReportLab으로 전문적인 PDF 레이아웃 5. **차트**: matplotlib으로 다양한 차트 생성 6. **자동화**: CLI와 cron으로 정기 리포트 생성 ### 향후 개선 가능 사항 - [ ] 다국어 지원 (한국어/영어 버전) - [ ] Web UI (Flask/Streamlit) - [ ] Excel 출력 옵션 - [ ] Slack/Teams 알림 연동 - [ ] Cloud Functions 배포 --- ## 시리즈 완료 **시리즈 네비게이션** - [x] Part 1: 프로젝트 소개 - [x] Part 2: 환경 설정 - [x] Part 3: Sheets API 연동 - [x] Part 4: 데이터 모델 - [x] Part 5: PDF 기본 레이아웃 - [x] Part 6: KPI 대시보드 - [x] Part 7: 테이블 구현 - [x] Part 8: 차트 생성 - [x] Part 9: 복합 차트 - [x] Part 10: 완성 (현재) --- **작성일**: 2025-12-07 **작성자**: Claude Code