"""DA-11: 슬라이드 조합 렌더러.
블록 배치 명세(JSON)를 받아 Jinja2로 HTML을 생성한다.
다중 페이지 지원: pages 배열의 각 페이지를 .slide div로 렌더링.
"""
from __future__ import annotations
import logging
from pathlib import Path
from typing import Any
from jinja2 import Environment, FileSystemLoader
logger = logging.getLogger(__name__)
TEMPLATES_DIR = Path(__file__).parent.parent / "templates"
STATIC_DIR = Path(__file__).parent.parent / "static"
def create_jinja_env() -> Environment:
"""Jinja2 환경 생성. templates/ 폴더를 로더로 사용."""
return Environment(
loader=FileSystemLoader(str(TEMPLATES_DIR)),
autoescape=False,
)
def render_multi_page(layout_concept: dict[str, Any]) -> str:
"""다중 페이지 레이아웃 컨셉으로 완성 HTML을 생성한다.
Args:
layout_concept: 디자인 팀장 + 텍스트 편집자가 완성한 구조:
{
"title": "슬라이드 제목",
"pages": [
{
"grid_areas": "...",
"grid_columns": "...",
"grid_rows": "...",
"blocks": [{"area": "...", "type": "...", "data": {...}}]
},
...
]
}
Returns:
완성된 HTML 문자열 (다중 페이지 시 .slide div 여러 개).
"""
env = create_jinja_env()
title = layout_concept.get("title", "슬라이드")
pages = layout_concept.get("pages", [])
if not pages:
logger.warning("페이지가 없습니다. 빈 HTML 반환.")
return "
페이지가 없습니다.
"
# 각 페이지의 블록을 개별 렌더링
pages_rendered = []
for page_idx, page in enumerate(pages):
blocks_rendered = []
for block in page.get("blocks", []):
block_type = block.get("type", "")
block_data = block.get("data", {})
template_path = f"blocks/{block_type}.html"
try:
block_template = env.get_template(template_path)
rendered_html = block_template.render(**block_data)
except Exception as e:
logger.warning(f"블록 렌더링 실패 ({block_type}): {e}")
rendered_html = f'블록 렌더링 실패: {block_type}
'
blocks_rendered.append({
"area": block.get("area", "main"),
"html": rendered_html,
})
pages_rendered.append({
"grid_areas": page.get("grid_areas", "'main'"),
"grid_columns": page.get("grid_columns", "1fr"),
"grid_rows": page.get("grid_rows", "auto"),
"blocks": blocks_rendered,
"page_number": page_idx + 1,
})
# base 템플릿 렌더링
base_template = env.get_template("slide-base.html")
html = base_template.render(
slide_title=title,
pages=pages_rendered,
total_pages=len(pages_rendered),
)
# CSS를 인라인으로 삽입
tokens_css = (STATIC_DIR / "tokens.css").read_text(encoding="utf-8")
base_css = (STATIC_DIR / "base.css").read_text(encoding="utf-8")
base_css = base_css.replace("@import url('./tokens.css');", "")
inline_css = f""
html = html.replace(
'',
inline_css,
)
logger.info(f"슬라이드 렌더링 완료: {title}, {len(pages_rendered)}페이지")
return html
# 하위 호환: 기존 render_slide도 유지
def render_slide(layout: dict[str, Any]) -> str:
"""기존 단일 페이지 렌더링 (하위 호환).
pages 구조가 있으면 render_multi_page로 위임.
없으면 기존 방식으로 단일 페이지 렌더링.
"""
if "pages" in layout:
return render_multi_page(layout)
# 기존 단일 페이지 로직
env = create_jinja_env()
blocks_rendered = []
for block in layout.get("blocks", []):
block_type = block["type"]
block_data = block.get("data", {})
template_path = f"blocks/{block_type}.html"
try:
block_template = env.get_template(template_path)
rendered_html = block_template.render(**block_data)
except Exception as e:
logger.warning(f"블록 렌더링 실패 ({block_type}): {e}")
rendered_html = f'블록 렌더링 실패: {block_type}
'
blocks_rendered.append({
"area": block["area"],
"html": rendered_html,
})
base_template = env.get_template("slide-base.html")
html = base_template.render(
slide_title=layout.get("title", ""),
pages=[{
"grid_areas": layout.get("grid_areas", "'header' 'main' 'footer'"),
"grid_columns": layout.get("grid_columns", "1fr"),
"grid_rows": layout.get("grid_rows", "auto 1fr auto"),
"blocks": blocks_rendered,
"page_number": 1,
}],
total_pages=1,
)
tokens_css = (STATIC_DIR / "tokens.css").read_text(encoding="utf-8")
base_css = (STATIC_DIR / "base.css").read_text(encoding="utf-8")
base_css = base_css.replace("@import url('./tokens.css');", "")
inline_css = f""
html = html.replace(
'',
inline_css,
)
logger.info(f"슬라이드 렌더링 완료: {layout.get('title', 'untitled')}")
return html
def render_standalone_block(block_type: str, data: dict[str, Any]) -> str:
"""단일 블록을 독립 HTML로 렌더링 (테스트/미리보기용)."""
env = create_jinja_env()
template = env.get_template(f"blocks/{block_type}.html")
return template.render(**data)