# Python으로 Money Forward 스타일 재무 PDF 리포트 만들기 - Part 5 ## PDF 기본 레이아웃 (ReportLab) ### ReportLab 소개 ReportLab은 Python에서 PDF를 생성하는 가장 강력한 라이브러리입니다. 두 가지 레벨의 API를 제공합니다: | API | 설명 | 용도 | |-----|------|------| | **Canvas (저수준)** | 픽셀 단위 제어 | 복잡한 레이아웃, 차트 | | **Platypus (고수준)** | 문서 흐름 자동화 | 보고서, 문서 | Money Forward 스타일 리포트에는 **두 API를 조합**하여 사용합니다. ### 기본 개념 ```python from reportlab.lib.pagesizes import A4, landscape from reportlab.pdfgen import canvas # 페이지 크기 width, height = landscape(A4) # (841.89, 595.28) points # 1 point = 1/72 inch ≈ 0.35mm # 좌표계: 좌측 하단이 (0, 0) ``` ### 스타일 정의 `src/pdf/styles.py`: ```python """PDF 스타일 정의""" from reportlab.lib import colors from reportlab.lib.styles import ParagraphStyle from reportlab.pdfbase import pdfmetrics from reportlab.pdfbase.cidfonts import UnicodeCIDFont # 일본어 폰트 등록 pdfmetrics.registerFont(UnicodeCIDFont('HeiseiKakuGo-W5')) pdfmetrics.registerFont(UnicodeCIDFont('HeiseiMin-W3')) # 폰트 상수 FONT_GOTHIC = 'HeiseiKakuGo-W5' # 고딕체 (제목, 강조) FONT_MINCHO = 'HeiseiMin-W3' # 명조체 (본문) # 색상 팔레트 COLORS = { 'primary': colors.HexColor('#1F4E79'), # 진한 파랑 'secondary': colors.HexColor('#4472C4'), # 파랑 'accent': colors.HexColor('#70AD47'), # 녹색 'warning': colors.HexColor('#C00000'), # 빨강 'text': colors.HexColor('#333333'), # 본문 'text_light': colors.HexColor('#666666'), # 보조 텍스트 'border': colors.HexColor('#D9D9D9'), # 테두리 'background': colors.HexColor('#F2F2F2'), # 배경 'white': colors.white, 'black': colors.black, } # KPI 카드 스타일 KPI_CARD_STYLE = { 'width': 180, 'height': 80, 'padding': 10, 'border_width': 1, 'border_color': COLORS['border'], 'background': COLORS['white'], 'title_font': FONT_GOTHIC, 'title_size': 10, 'value_font': FONT_GOTHIC, 'value_size': 24, 'subtitle_font': FONT_GOTHIC, 'subtitle_size': 8, } # 테이블 스타일 TABLE_STYLE = { 'header_background': COLORS['primary'], 'header_text': COLORS['white'], 'header_font': FONT_GOTHIC, 'header_size': 9, 'body_font': FONT_MINCHO, 'body_size': 8, 'row_height': 18, 'header_height': 24, 'border_color': COLORS['border'], 'alt_row_color': COLORS['background'], } # 페이지 스타일 PAGE_STYLE = { 'margin_top': 40, 'margin_bottom': 30, 'margin_left': 40, 'margin_right': 40, 'header_height': 50, 'footer_height': 20, } # Paragraph 스타일 PARAGRAPH_STYLES = { 'title': ParagraphStyle( 'Title', fontName=FONT_GOTHIC, fontSize=18, textColor=COLORS['text'], spaceAfter=12, ), 'heading1': ParagraphStyle( 'Heading1', fontName=FONT_GOTHIC, fontSize=14, textColor=COLORS['primary'], spaceAfter=8, ), 'body': ParagraphStyle( 'Body', fontName=FONT_MINCHO, fontSize=10, textColor=COLORS['text'], leading=14, ), } ``` ### PDF 생성기 기본 클래스 `src/pdf/generator.py`: ```python """PDF 생성기""" from pathlib import Path from datetime import datetime from reportlab.lib.pagesizes import A4, landscape from reportlab.pdfgen import canvas from reportlab.lib.units import mm from .styles import COLORS, FONT_GOTHIC, PAGE_STYLE class PDFGenerator: """PDF 생성기 기본 클래스""" def __init__(self, output_path: Path, title: str = "財務レポート"): self.output_path = output_path self.title = title self.page_size = landscape(A4) self.width, self.height = self.page_size self.canvas = None self.page_number = 0 self.total_pages = 0 def create(self): """PDF 생성 시작""" self.canvas = canvas.Canvas( str(self.output_path), pagesize=self.page_size ) self.canvas.setTitle(self.title) def save(self): """PDF 저장""" self.canvas.save() def new_page(self): """새 페이지 추가""" if self.page_number > 0: self.canvas.showPage() self.page_number += 1 def draw_header(self, title: str, subtitle: str = ""): """페이지 헤더 그리기""" c = self.canvas margin = PAGE_STYLE['margin_left'] top = self.height - PAGE_STYLE['margin_top'] # 제목 c.setFont(FONT_GOTHIC, 18) c.setFillColor(COLORS['text']) c.drawString(margin, top, title) # 부제 if subtitle: c.setFont(FONT_GOTHIC, 10) c.setFillColor(COLORS['text_light']) c.drawString(margin, top - 20, subtitle) def draw_footer(self): """페이지 푸터 그리기""" c = self.canvas margin = PAGE_STYLE['margin_left'] bottom = PAGE_STYLE['margin_bottom'] # 페이지 번호 c.setFont(FONT_GOTHIC, 9) c.setFillColor(COLORS['text_light']) page_text = f"{self.page_number} / {self.total_pages}" text_width = c.stringWidth(page_text, FONT_GOTHIC, 9) c.drawString( (self.width - text_width) / 2, bottom, page_text ) def draw_company_info(self, company_name: str, fiscal_year: str, period: str): """회사 정보 그리기""" c = self.canvas margin = PAGE_STYLE['margin_left'] top = self.height - PAGE_STYLE['margin_top'] - 30 c.setFont(FONT_GOTHIC, 10) c.setFillColor(COLORS['text']) c.drawString(margin, top, f"組織名:{company_name}") c.drawString(margin, top - 14, f"会計期間:{fiscal_year}({period})") def draw_unit_label(self): """단위 레이블""" c = self.canvas right = self.width - PAGE_STYLE['margin_right'] top = self.height - PAGE_STYLE['margin_top'] - 30 c.setFont(FONT_GOTHIC, 9) c.setFillColor(COLORS['text_light']) c.drawRightString(right, top, "(単位:円)") def get_content_area(self) -> tuple[float, float, float, float]: """콘텐츠 영역 좌표 반환 (x, y, width, height)""" x = PAGE_STYLE['margin_left'] y = PAGE_STYLE['margin_bottom'] + PAGE_STYLE['footer_height'] w = self.width - PAGE_STYLE['margin_left'] - PAGE_STYLE['margin_right'] h = self.height - PAGE_STYLE['margin_top'] - PAGE_STYLE['header_height'] - PAGE_STYLE['margin_bottom'] - PAGE_STYLE['footer_height'] return x, y, w, h class ReportGenerator(PDFGenerator): """재무 리포트 전용 생성기""" def __init__(self, output_path: Path, report_data): super().__init__(output_path) self.report = report_data self.total_pages = 17 # 전체 페이지 수 def generate(self): """전체 리포트 생성""" self.create() # 1. 業績見通し (1페이지) self.generate_dashboard_page() # 2. サマリ (1페이지) self.generate_summary_page() # 3-4. 損益計算書 (2페이지) self.generate_pl_pages() # 5-7. 業績分析表 (3페이지) self.generate_analysis_pages() # 8-9. キャッシュフロー計算書 (2페이지) self.generate_cashflow_pages() # 10-11. 変動損益計算書 (2페이지) self.generate_variable_cost_pages() # 12-15. 財務サマリ (4페이지) self.generate_financial_summary_pages() # 16-17. 財務ハイライト (2페이지) self.generate_highlight_pages() self.save() def generate_dashboard_page(self): """業績見通し 페이지""" self.new_page() self.draw_header("業績見通し") self.draw_company_info( self.report.company_name, self.report.fiscal_year, self.report.period ) self.draw_unit_label() self.draw_footer() # KPI 카드 그리기 (Part 6에서 구현) # 차트 그리기 (Part 8에서 구현) def generate_summary_page(self): """サマリ 페이지""" self.new_page() self.draw_footer() # 내용은 Part 6에서 구현 def generate_pl_pages(self): """損益計算書 페이지들""" for _ in range(2): self.new_page() self.draw_footer() # 내용은 Part 7에서 구현 def generate_analysis_pages(self): """業績分析表 페이지들""" for _ in range(3): self.new_page() self.draw_footer() def generate_cashflow_pages(self): """キャッシュフロー計算書 페이지들""" for _ in range(2): self.new_page() self.draw_footer() def generate_variable_cost_pages(self): """変動損益計算書 페이지들""" for _ in range(2): self.new_page() self.draw_footer() def generate_financial_summary_pages(self): """財務サマリ 페이지들""" for _ in range(4): self.new_page() self.draw_footer() def generate_highlight_pages(self): """財務ハイライト 페이지들""" for _ in range(2): self.new_page() self.draw_footer() ``` ### 테스트 실행 `src/test_pdf.py`: ```python """PDF 생성 테스트""" from pathlib import Path from datetime import date from data_models import FinancialReport, KPICard from pdf.generator import ReportGenerator from decimal import Decimal def create_test_report(): """테스트용 리포트 데이터""" return FinancialReport( company_name="株式会社GemEgg", fiscal_year="2025年度", period="4月1日〜3月31日", target_month="2025年10月", generated_at=date.today(), annual_kpis=[ KPICard(title="売上高", value=Decimal(125947574), subtitle="着地見込"), KPICard(title="売上総利益", value=Decimal(72841825), subtitle="着地見込"), KPICard(title="営業利益", value=Decimal(51104333), subtitle="着地見込"), KPICard(title="当期純利益", value=Decimal(51113902), subtitle="着地見込"), ], ) def main(): output_path = Path("output/test_report.pdf") output_path.parent.mkdir(exist_ok=True) report_data = create_test_report() generator = ReportGenerator(output_path, report_data) generator.generate() print(f"✓ PDF 생성 완료: {output_path}") if __name__ == "__main__": main() ``` ### 실행 ```bash python src/test_pdf.py ``` ### 출력 결과 17페이지짜리 기본 구조를 가진 PDF가 생성됩니다. 각 페이지에는: - 헤더 (타이틀, 회사 정보) - 푸터 (페이지 번호) - 빈 콘텐츠 영역 이 준비가 되어 있습니다. ### 다음 단계 Part 6에서는 첫 번째 페이지인 KPI 대시보드를 구현합니다. KPI 카드 레이아웃과 스타일링을 다룹니다. --- **시리즈 네비게이션** - [x] Part 1: 프로젝트 소개 - [x] Part 2: 환경 설정 - [x] Part 3: Sheets API 연동 - [x] Part 4: 데이터 모델 - [x] Part 5: PDF 기본 레이아웃 (현재) - [ ] Part 6: KPI 대시보드 - [ ] Part 7: 테이블 구현 - [ ] Part 8: 차트 생성 - [ ] Part 9: 복합 차트 - [ ] Part 10: 완성