# Python으로 Money Forward 스타일 재무 PDF 리포트 만들기 - Part 6 ## KPI 대시보드 페이지 구현 ### 대시보드 레이아웃 분석 Money Forward 스타일의 業績見通し 페이지는 다음과 같은 구조입니다: ``` ┌─────────────────────────────────────────────────────────────────┐ │ 業績見通し (単位:円) │ │ 組織名:株式会社GemEgg │ │ 会計期間:2025年度(4月1日〜3月31日) │ ├─────────────────────────────────────────────────────────────────┤ │ ┌────────┐ ┌────────┐ ┌──────────────────────────────────────┐│ │ │ 売上高 │ │売上総利益│ │ ││ │ │125,947,574円│ │72,841,825円│ │ 月별 추이 차트 ││ │ │ 着地見込 │ │ 着地見込 │ │ (매출/이익률 콤보차트) ││ │ └────────┘ └────────┘ │ ││ │ ┌────────┐ ┌────────┐ └──────────────────────────────────────┘│ │ │営業利益│ │当期純利益│ │ │ │51,104,333円│ │51,113,902円│ │ │ │ 着地見込 │ │ 着地見込 │ │ │ └────────┘ └────────┘ │ ├─────────────────────────────────────────────────────────────────┤ │ (하단 섹션: 월간 KPI + 월간 차트) │ └─────────────────────────────────────────────────────────────────┘ ``` ### KPI 카드 컴포넌트 `src/pdf/components/kpi_card.py`: ```python """KPI 카드 컴포넌트""" from reportlab.pdfgen.canvas import Canvas from reportlab.lib import colors from decimal import Decimal from typing import Optional from ..styles import COLORS, FONT_GOTHIC, KPI_CARD_STYLE class KPICardRenderer: """KPI 카드 렌더러""" def __init__(self, canvas: Canvas): self.canvas = canvas self.style = KPI_CARD_STYLE def draw( self, x: float, y: float, title: str, value: Decimal, subtitle: str = "", unit: str = "円", comparison_value: Optional[Decimal] = None, comparison_label: str = "", width: float = None, height: float = None, ): """KPI 카드 그리기 Args: x, y: 좌측 상단 좌표 title: 항목명 (売上高 등) value: 값 subtitle: 부제 (着地見込, 2025年10月 등) unit: 단위 (円, % 등) comparison_value: 비교값 (예산 등) comparison_label: 비교 레이블 (予算v2: 등) """ c = self.canvas w = width or self.style['width'] h = height or self.style['height'] padding = self.style['padding'] # 카드 배경 & 테두리 c.setStrokeColor(self.style['border_color']) c.setLineWidth(self.style['border_width']) c.setFillColor(self.style['background']) c.roundRect(x, y - h, w, h, 3, stroke=1, fill=1) # 제목 c.setFont(self.style['title_font'], self.style['title_size']) c.setFillColor(COLORS['text']) c.drawString(x + padding, y - padding - 10, title) # 비교 레이블 (우측 상단) if comparison_label: c.setFont(FONT_GOTHIC, 8) c.setFillColor(COLORS['text_light']) label_text = f"{comparison_label}" c.drawRightString(x + w - padding, y - padding - 10, label_text) # 메인 값 c.setFont(self.style['value_font'], self.style['value_size']) c.setFillColor(COLORS['text']) formatted_value = f"{int(value):,}" c.drawString(x + padding, y - h/2 - 5, formatted_value) # 단위 c.setFont(FONT_GOTHIC, 14) value_width = c.stringWidth(formatted_value, self.style['value_font'], self.style['value_size']) c.drawString(x + padding + value_width + 5, y - h/2 - 5, unit) # 차이 표시 (비교값이 있는 경우) if comparison_value is not None: variance = value - comparison_value variance_str = f"{'+' if variance >= 0 else ''}{int(variance):,}{unit}" # 차이 박스 box_x = x + w - padding - 80 box_y = y - h/2 - 15 box_w = 75 box_h = 20 # 색상 (양수=녹색, 음수=빨강) if variance >= 0: box_color = colors.HexColor('#E2F0D9') # 연한 녹색 text_color = colors.HexColor('#375623') # 진한 녹색 else: box_color = colors.HexColor('#FBE5D6') # 연한 빨강 text_color = colors.HexColor('#C00000') # 빨강 c.setFillColor(box_color) c.roundRect(box_x, box_y, box_w, box_h, 2, stroke=0, fill=1) c.setFont(FONT_GOTHIC, 9) c.setFillColor(text_color) c.drawCentredString(box_x + box_w/2, box_y + 6, variance_str) # 부제 c.setFont(self.style['subtitle_font'], self.style['subtitle_size']) c.setFillColor(COLORS['text_light']) c.drawString(x + padding, y - h + padding + 5, subtitle) def draw_rate_card( self, x: float, y: float, title: str, value: float, subtitle: str = "", comparison_value: Optional[float] = None, comparison_label: str = "", width: float = None, height: float = None, ): """비율 KPI 카드 (%, 배수 등)""" c = self.canvas w = width or self.style['width'] h = height or self.style['height'] padding = self.style['padding'] # 카드 배경 c.setStrokeColor(self.style['border_color']) c.setLineWidth(self.style['border_width']) c.setFillColor(self.style['background']) c.roundRect(x, y - h, w, h, 3, stroke=1, fill=1) # 제목 c.setFont(self.style['title_font'], self.style['title_size']) c.setFillColor(COLORS['text']) c.drawString(x + padding, y - padding - 10, title) # 비교 레이블 if comparison_label: c.setFont(FONT_GOTHIC, 8) c.setFillColor(COLORS['text_light']) c.drawRightString(x + w - padding, y - padding - 10, comparison_label) # 메인 값 c.setFont(self.style['value_font'], self.style['value_size']) c.setFillColor(COLORS['text']) formatted_value = f"{value:.2f}" c.drawString(x + padding, y - h/2 - 5, formatted_value) # 단위 (%) c.setFont(FONT_GOTHIC, 16) value_width = c.stringWidth(formatted_value, self.style['value_font'], self.style['value_size']) c.drawString(x + padding + value_width + 3, y - h/2 - 5, "%") # 차이 표시 if comparison_value is not None: variance = value - comparison_value variance_str = f"{'+' if variance >= 0 else ''}{variance:.2f}%" box_x = x + w - padding - 60 box_y = y - h/2 - 15 box_w = 55 box_h = 20 if variance >= 0: box_color = colors.HexColor('#E2F0D9') text_color = colors.HexColor('#375623') else: box_color = colors.HexColor('#FBE5D6') text_color = colors.HexColor('#C00000') c.setFillColor(box_color) c.roundRect(box_x, box_y, box_w, box_h, 2, stroke=0, fill=1) c.setFont(FONT_GOTHIC, 9) c.setFillColor(text_color) c.drawCentredString(box_x + box_w/2, box_y + 6, variance_str) # 부제 c.setFont(self.style['subtitle_font'], self.style['subtitle_size']) c.setFillColor(COLORS['text_light']) c.drawString(x + padding, y - h + padding + 5, subtitle) ``` ### 대시보드 페이지 생성기 `src/pdf/pages/dashboard.py`: ```python """대시보드 페이지""" from reportlab.pdfgen.canvas import Canvas from decimal import Decimal from ..styles import COLORS, FONT_GOTHIC, PAGE_STYLE from ..components.kpi_card import KPICardRenderer from data_models import FinancialReport class DashboardPage: """業績見通し 페이지 생성기""" def __init__(self, canvas: Canvas, report: FinancialReport): self.canvas = canvas self.report = report self.kpi_renderer = KPICardRenderer(canvas) def render(self, page_width: float, page_height: float): """페이지 렌더링""" c = self.canvas margin = PAGE_STYLE['margin_left'] # 제목 self._draw_header(page_width, page_height) # 콘텐츠 영역 시작점 content_top = page_height - 100 content_left = margin # 연간 KPI 섹션 (상단) self._draw_annual_section(content_left, content_top, page_width) # 월간 KPI 섹션 (하단) monthly_top = content_top - 230 self._draw_monthly_section(content_left, monthly_top, page_width) def _draw_header(self, page_width: float, page_height: float): """페이지 헤더""" c = self.canvas margin = PAGE_STYLE['margin_left'] top = page_height - PAGE_STYLE['margin_top'] # 제목 c.setFont(FONT_GOTHIC, 18) c.setFillColor(COLORS['text']) c.drawCentredString(page_width / 2, top, "業績見通し") # 회사 정보 c.setFont(FONT_GOTHIC, 10) c.drawString(margin, top - 30, f"組織名:{self.report.company_name}") c.drawString(margin, top - 44, f"会計期間:{self.report.fiscal_year}({self.report.period})") c.drawString(margin, top - 58, "全社") # 단위 c.setFont(FONT_GOTHIC, 9) c.setFillColor(COLORS['text_light']) right = page_width - PAGE_STYLE['margin_right'] c.drawRightString(right, top - 58, "(単位:円)") def _draw_annual_section(self, x: float, y: float, page_width: float): """연간 KPI 섹션""" card_width = 180 card_height = 80 gap = 15 # 2x3 그리드 (KPI 4개 + 비율 2개) # 첫 번째 행: 売上高, 売上総利益 self.kpi_renderer.draw( x, y, title="売上高", value=Decimal(125947574), subtitle="着地見込", width=card_width, height=card_height, ) self.kpi_renderer.draw( x + card_width + gap, y, title="売上総利益", value=Decimal(72841825), subtitle="着地見込", width=card_width, height=card_height, ) # 두 번째 행: 営業利益, 当期純利益 row2_y = y - card_height - gap self.kpi_renderer.draw( x, row2_y, title="営業利益", value=Decimal(51104333), subtitle="着地見込", width=card_width, height=card_height, ) self.kpi_renderer.draw( x + card_width + gap, row2_y, title="当期純利益", value=Decimal(51113902), subtitle="着地見込", width=card_width, height=card_height, ) # 세 번째 행: 売上総利益率, 営業利益率 row3_y = row2_y - card_height - gap self.kpi_renderer.draw_rate_card( x, row3_y, title="売上総利益率", value=57.84, subtitle="着地見込", width=card_width, height=card_height, ) self.kpi_renderer.draw_rate_card( x + card_width + gap, row3_y, title="営業利益率", value=40.58, subtitle="着地見込", width=card_width, height=card_height, ) # 차트 영역 표시 (Part 8에서 구현) chart_x = x + (card_width + gap) * 2 + 20 chart_width = page_width - chart_x - PAGE_STYLE['margin_right'] chart_height = card_height * 3 + gap * 2 c = self.canvas c.setStrokeColor(COLORS['border']) c.setDash(3, 3) c.rect(chart_x, y - chart_height, chart_width, chart_height) c.setDash() # 차트 플레이스홀더 텍스트 c.setFont(FONT_GOTHIC, 10) c.setFillColor(COLORS['text_light']) c.drawCentredString( chart_x + chart_width/2, y - chart_height/2, "年間推移チャート (Part 8で実装)" ) def _draw_monthly_section(self, x: float, y: float, page_width: float): """월간 KPI 섹션""" card_width = 180 card_height = 80 gap = 15 # 첫 번째 행 self.kpi_renderer.draw( x, y, title="売上高", value=Decimal(6100000), subtitle="2025年10月", width=card_width, height=card_height, ) self.kpi_renderer.draw( x + card_width + gap, y, title="売上総利益", value=Decimal(4898412), subtitle="2025年10月", width=card_width, height=card_height, ) # 두 번째 행 row2_y = y - card_height - gap self.kpi_renderer.draw( x, row2_y, title="営業利益", value=Decimal(2134436), subtitle="2025年10月", width=card_width, height=card_height, ) self.kpi_renderer.draw( x + card_width + gap, row2_y, title="当期純利益", value=Decimal(2135734), subtitle="2025年10月", width=card_width, height=card_height, ) # 세 번째 행: 비율 row3_y = row2_y - card_height - gap self.kpi_renderer.draw_rate_card( x, row3_y, title="売上総利益率", value=80.30, subtitle="2025年10月", width=card_width, height=card_height, ) self.kpi_renderer.draw_rate_card( x + card_width + gap, row3_y, title="営業利益率", value=34.99, subtitle="2025年10月", width=card_width, height=card_height, ) # 월간 차트 영역 chart_x = x + (card_width + gap) * 2 + 20 chart_width = page_width - chart_x - PAGE_STYLE['margin_right'] chart_height = card_height * 3 + gap * 2 c = self.canvas c.setStrokeColor(COLORS['border']) c.setDash(3, 3) c.rect(chart_x, y - chart_height, chart_width, chart_height) c.setDash() c.setFont(FONT_GOTHIC, 10) c.setFillColor(COLORS['text_light']) c.drawCentredString( chart_x + chart_width/2, y - chart_height/2, "月次推移チャート (Part 8で実装)" ) ``` ### 생성기 통합 `src/pdf/generator.py`에 대시보드 페이지 통합: ```python from .pages.dashboard import DashboardPage class ReportGenerator(PDFGenerator): # ... (기존 코드) def generate_dashboard_page(self): """業績見通し 페이지""" self.new_page() dashboard = DashboardPage(self.canvas, self.report) dashboard.render(self.width, self.height) self.draw_footer() ``` ### 결과 미리보기 KPI 카드가 2열 3행 그리드로 배치되고, 우측에 차트 영역이 예약됩니다. ### 다음 단계 Part 7에서는 損益計算書 테이블을 구현합니다. ReportLab의 Table 클래스를 사용한 복잡한 테이블 레이아웃을 다룹니다. --- **시리즈 네비게이션** - [x] Part 1: 프로젝트 소개 - [x] Part 2: 환경 설정 - [x] Part 3: Sheets API 연동 - [x] Part 4: 데이터 모델 - [x] Part 5: PDF 기본 레이아웃 - [x] Part 6: KPI 대시보드 (현재) - [ ] Part 7: 테이블 구현 - [ ] Part 8: 차트 생성 - [ ] Part 9: 복합 차트 - [ ] Part 10: 완성