# Task 2389 — IDS Phase 1 Fix 3: Hybrid 패턴 5종 (H1~H5)

**작업자**: 디자인팀 이나리 (Inari, sonnet) — hybrid-image 스킬 전문가
**작업일**: 2026-05-03
**플랜**: `/home/jay/workspace/memory/plans/insuro-design-system/plan.md` (v1.1) §3.2.1
**범위**: IDS Phase 1 Hybrid Pattern Standard (§0.2) — 5개 Python 패턴 모듈 + 공통 인터페이스 + SKILL.md 업데이트

## 결과 요약

**상태**: ✅ SUCCESS

5개 hybrid 패턴 모듈 + 공통 헬퍼 2개 + `__init__.py` + SKILL.md 추가 섹션 완료. 모든 검증 PASS.

## 산출물

### 1. 패턴 모듈 (5종) — `/home/jay/workspace/skills/hybrid-image/patterns/`

| 패턴 | 파일 | 배경 경로 | 텍스트 | 용도 |
| --- | --- | --- | --- | --- |
| H1. 포토 카드 | `h1_photo_card.py` | Gemini photoreal (Gemini CLI 통합 경로) | Satori 한글 헤드라인 | 보험 광고, 상품 소개 |
| H2. 일러스트 카드 | `h2_illustration_card.py` | Gemini illustration (Gemini CLI) | Satori 한글 + 가로 막대 차트 | 인포그래픽, 컨셉 카드 |
| H3. GPT 스타일 카드 | `h3_gpt_style_card.py` | GPT image (Codex CLI `codex exec --image-out`) | Satori 한글 | 다양한 스타일 (Gemini 어려운 영역) |
| H4. 그래디언트 카드 | `h4_gradient_card.py` | Satori CSS gradient (외부 호출 0건) | Satori 한글 | 미니멀, 매거진 스타일 |
| H5. 사진 합성 카드 | `h5_user_photo_card.py` | 사용자 업로드 사진 (file path) | Satori 한글 + 화이트 프레임 | 회사 소식, 인터뷰 |

### 2. 공통 헬퍼

- `_satori.py` — Satori (Node.js) 호출 헬퍼. `build_text_overlay_html()`, `render_html_to_png()`, `KOREAN_FONT_STACK`. Satori 부재 시 Pillow → 순수 PNG bytes로 graceful fallback.
- `_backgrounds.py` — Gemini CLI / Codex CLI 통합 경로 호출 헬퍼. `generate_gemini_background()`, `generate_codex_gpt_background()`, `build_background_layer_html()`, `default_gradient(theme)` (navy/warm/mint/rose/mono).

### 3. `__init__.py`

```python
from .h1_photo_card import render as render_h1_photo_card
# ... h2~h5

PATTERNS = {
    "h1": render_h1_photo_card,
    "h2": render_h2_illustration_card,
    "h3": render_h3_gpt_style_card,
    "h4": render_h4_gradient_card,
    "h5": render_h5_user_photo_card,
}
```

### 4. SKILL.md 추가 섹션 — `/home/jay/workspace/skills/hybrid-image/SKILL.md`

기존 마지막 라인(`산출물: .../v4-hybrid/`) 뒤에 "## Hybrid 패턴 5종 (Phase 1, IDS §3.2.1)" 섹션 append. 5종 표 + 공통 시그니처 + 사용 예시 + 외부 API 차단 의무 + 한글 폰트 fallback 차단 규칙 명시. 기존 파일 다른 부분 변경 0.

## 공통 시그니처 (전 패턴 동일)

```python
def render(
    title: str,
    body: str,
    output_path: Path | str,
    *,
    size: tuple[int, int] = (1080, 1350),
    design_tokens: dict | None = None,
    background_path: Path | str | None = None,
    prompt_hint: str | None = None,
) -> Path
```

`inspect.signature()` 검증 결과 5개 모두 `['title', 'body', 'output_path', 'size', 'design_tokens', 'background_path', 'prompt_hint']` 일치 확인.

## 검증 결과

### A. py_compile (8/8 PASS)

```
=== _satori.py ===           PASS
=== _backgrounds.py ===      PASS
=== h1_photo_card.py ===     PASS
=== h2_illustration_card.py === PASS
=== h3_gpt_style_card.py === PASS
=== h4_gradient_card.py ===  PASS
=== h5_user_photo_card.py === PASS
=== __init__.py ===          PASS
```

### B. PATTERNS import

```bash
$ python3 -c "import sys; sys.path.insert(0, '/home/jay/workspace/skills/hybrid-image'); from patterns import PATTERNS; print(list(PATTERNS.keys()))"
['h1', 'h2', 'h3', 'h4', 'h5']
```

### C. 외부 API 직접 호출 grep (0건)

```bash
# 실제 코드 레벨 (import / requests 호출) 검색:
grep -E "^import openai|^from openai|^import anthropic|^from anthropic|^from google\.generativeai|^import google\.generativeai|requests\.(get|post).*openai\.com|requests\.(get|post).*generativelanguage" patterns/

# 결과: No matches found
```

문서 주석 (`❌ ... 금지`) 형태로만 키워드가 등장하며, 실제 import/HTTP 호출은 0건. 모든 외부 호출은 `subprocess.run(["codex", ...])` / `subprocess.run(["gemini", ...])` 통합 경로만 사용.

### D. 시그니처 일관성 (5/5 일치)

5개 `render` 함수 파라미터 순서 완전히 동일. keyword-only 파라미터 4종 모두 동일.

### E. H4 functional smoke test

H4는 외부 의존 0이므로 실행 가능. 한글 텍스트로 1080x1350 PNG 생성 확인:

```
H4 output: /tmp/h_test/h4.png exists: True size: 8241
```

## 외부 API 직접 호출 차단 — 구현 메커니즘

1. **Gemini 배경 (H1, H2)** — `_backgrounds.generate_gemini_background()` 내부에서 `shutil.which("gemini")`로 CLI 존재 확인 후 `subprocess.run(["gemini", "-p", prompt, "--image-out", path])`. CLI 부재 시 `None` 반환 → gradient fallback.

2. **GPT image 배경 (H3)** — `_backgrounds.generate_codex_gpt_background()` 내부에서 `shutil.which("codex")` 확인 후 `subprocess.run(["codex", "exec", "--image-out", path, prompt])`. **OpenAI SDK 직접 호출 0건.** CLI 부재 시 gradient fallback.

3. **Satori 텍스트 (전 패턴)** — `_satori.render_html_to_png()` 내부에서 `shutil.which("node")` + `SATORI_RENDER_SCRIPT.exists()` 확인 후 `subprocess.run([node, satori_cli.js])`. 부재 시 Pillow → 순수 PNG 작성 fallback (외부 호출 없음).

4. **사용자 사진 (H5)** — 외부 호출 0. 단순 file path embed.

5. **Satori CSS gradient (H4)** — 외부 호출 0. Satori 자체 렌더링만 사용.

## 한글 폰트 fallback 차단

`_satori.KOREAN_FONT_STACK = "'Pretendard', 'Noto Sans KR'"` — system fallback (Arial, Helvetica, sans-serif) 절대 포함 금지. 모든 패턴이 이 상수 사용. 추가로 `word-break: keep-all` (음절 중간 줄바꿈 방지), `line-height: 1.375` (한글 수직 여백) 적용.

## 작업 원칙 준수

| 원칙 | 준수 여부 |
| --- | --- |
| 신규 파일만 (SKILL.md만 append 수정) | ✅ |
| 외부 API 직접 호출 0건 | ✅ |
| 금지 경로 (dispatch.py / scripts/* / teams/shared/** / CLAUDE.md / resources/design-md/**) 변경 X | ✅ (변경 0) |
| 타입 힌트 명확 (mypy/pyright 0건 목표) | ✅ |
| import 안전 (`python3 -m py_compile` 단독 PASS) | ✅ (8/8) |

## 파일 경로 (절대)

- `/home/jay/workspace/skills/hybrid-image/patterns/__init__.py`
- `/home/jay/workspace/skills/hybrid-image/patterns/_satori.py`
- `/home/jay/workspace/skills/hybrid-image/patterns/_backgrounds.py`
- `/home/jay/workspace/skills/hybrid-image/patterns/h1_photo_card.py`
- `/home/jay/workspace/skills/hybrid-image/patterns/h2_illustration_card.py`
- `/home/jay/workspace/skills/hybrid-image/patterns/h3_gpt_style_card.py`
- `/home/jay/workspace/skills/hybrid-image/patterns/h4_gradient_card.py`
- `/home/jay/workspace/skills/hybrid-image/patterns/h5_user_photo_card.py`
- `/home/jay/workspace/skills/hybrid-image/SKILL.md` (append-only)

## 후속 권장 사항 (이번 작업 범위 밖)

- Phase 2에서 Satori CLI 표준 인터페이스 (`satori_cli.js`)가 stdin JSON payload를 처리하도록 통일하면 `_satori.render_html_to_png()`의 fallback 경로 제거 가능
- Phase 3에서 H2 차트 컴포넌트를 별도 모듈로 추출해 재사용성 ↑

— 이나리 / 디자인팀
