"""사용자 디자인 PNG 10장 → Claude Sonnet Vision → Jinja card.html.j2 자동 생성. CLI (이 phase 이후 추가): python -m app.design_importer """ import json import logging from pathlib import Path from typing import Dict, List from PIL import Image logger = logging.getLogger(__name__) __all__ = [ "_resolve_page_mapping", "_validate_images", ] # 페이지 1 (커버) 키워드 우선순위 — 먼저 매치된 키워드를 가진 첫 파일만 page 1 _COVER_KEYWORDS = ("cover", "start", "intro") # 페이지 10 (CTA) 키워드 우선순위 _CTA_KEYWORDS = ("cta", "outro", "finish", "end") # 인스타그램 카드 규격 (세로형 4:5 비율) _EXPECTED_SIZE = (1080, 1350) def _resolve_page_mapping(pages_dir: Path) -> Dict[str, int]: """templates//pages/ 안의 PNG 10장을 page 1~10에 매핑. 우선순위: 1. `_order.json` 있으면 그 매핑 그대로 사용 (검증 통과 시 반환) 2. 자동 매핑: - _COVER_KEYWORDS 우선순위 순서로 가장 앞 키워드를 가진 첫 PNG → page 1 - _CTA_KEYWORDS 우선순위 순서로 가장 앞 키워드를 가진 첫 PNG → page 10 - 남은 8장은 알파벳 정렬 → page 2~9 """ pages_dir = Path(pages_dir) pngs = sorted([p.name for p in pages_dir.glob("*.png")]) if len(pngs) != 10: raise ValueError( f"{pages_dir}에 PNG 10장 필요, 발견 {len(pngs)}장: {pngs}" ) order_path = pages_dir / "_order.json" if order_path.exists(): try: mapping = json.loads(order_path.read_text(encoding="utf-8")) except Exception as e: logger.warning("_order.json 파싱 실패, 자동 매핑으로 폴백: %s", e) else: if set(mapping.keys()) == set(pngs) and set(mapping.values()) == set(range(1, 11)): return {k: int(v) for k, v in mapping.items()} logger.warning( "_order.json 형식 오류 (파일 누락·page 중복), 자동 매핑으로 폴백" ) return _build_mapping(pngs) def _pick_by_keywords(names: List[str], keywords: tuple) -> str | None: """names 중 keywords의 우선순위에 따라 첫 매치 파일명 반환 (없으면 None).""" lower_names = [(n, n.lower()) for n in names] for kw in keywords: for orig, low in lower_names: if kw in low: return orig return None def _build_mapping(pngs: List[str]) -> Dict[str, int]: """자동 매핑 알고리즘 본체.""" mapping: Dict[str, int] = {} remaining = list(pngs) cover = _pick_by_keywords(remaining, _COVER_KEYWORDS) if cover: mapping[cover] = 1 remaining.remove(cover) cta = _pick_by_keywords(remaining, _CTA_KEYWORDS) if cta: mapping[cta] = 10 remaining.remove(cta) remaining_sorted = sorted(remaining) free_pages = sorted(set(range(1, 11)) - set(mapping.values())) for name, page in zip(remaining_sorted, free_pages): mapping[name] = page return mapping def _validate_images(pages_dir: Path) -> None: """모든 PNG가 정확히 1080×1350인지 검증. 다르면 ValueError. early-exit 하지 않고 전체 파일을 검사한 뒤 한 메시지에 모아 raise. """ pages_dir = Path(pages_dir) bad = [] for png_path in sorted(pages_dir.glob("*.png")): with Image.open(png_path) as img: if img.size != _EXPECTED_SIZE: bad.append((png_path.name, img.size)) if bad: msg = "; ".join(f"{n}: {s[0]}x{s[1]}" for n, s in bad) raise ValueError( f"모든 카드 디자인은 1080x1350이어야 함. 잘못된 파일: {msg}" )