# Phase 8 — Prompt Redesign Implementation Plan

> **For agentic workers:** Use subagent-driven-development (opus) — fresh implementer + spec reviewer + code quality reviewer per task. After all tasks complete, dispatch Codex + Claude dual code review.

**Goal:** spec `2026-04-30-phase8-prompt-redesign-spec.md`의 결정사항을 구현. T19에서 발견된 (a) 배경 top-down 모방, (b) 도면/배경 cultural fidelity 부재, (c) 도면-배경 시점 일관성 결함을 해결.

**Architecture:** floor_plan_prompt + background_prompt 두 prompt를 신규 버전으로 재설계 (덮어쓰기 X). schema에 `numbered_elements` + `camera_recommendations` 추가, system은 시나리오 의존 0 + cultural cues LLM derive + cinematic eye-level + source language 메타지시. background_render size default를 16:9(1536×864)로 변경. 그 외 코드 변경 0.

**Tech Stack:** Python 3.12, OpenAI Python SDK, gpt-5.5 (text), gpt-image-2 (image), pytest.

**Conventions enforced:**
- 시나리오 의존 단어 hardcode 절대 금지 (system prompt + 코드 모두)
- LLM 입력 데이터 무절단 (CLAUDE.md 절대 규칙)
- prompt 새 디렉토리 (`2.202604300800/`), 기존 `1.20260429*` 디렉토리 그대로
- subagent 모두 **opus** 강제 (사용자 지시)
- 듀얼 리뷰 (Codex + Claude) 마지막 단계

---

## Task 1: floor_plan_prompt v2 prompt 신설

**Files:**
- Create: `prompts/_base/floor_plan_prompt/2.202604300800/system.md`
- Create: `prompts/_base/floor_plan_prompt/2.202604300800/user_template.md`
- Create: `prompts/_base/floor_plan_prompt/2.202604300800/schema.json`
- Reference: `prompts/_base/floor_plan_prompt/1.202604292033/` (기존 — 변경 X)

**Steps:**

- [ ] **Step 1.1: schema.json 작성**

```json
{
  "type": "object",
  "properties": {
    "fp_id": {"type": "string", "pattern": "^[a-z0-9][a-z0-9_]*$"},
    "t2i_prompt": {"type": "string", "minLength": 30},
    "key_elements": {"type": "array", "items": {"type": "string"}},
    "numbered_elements": {
      "type": "array",
      "items": {
        "type": "object",
        "properties": {
          "number": {"type": "integer", "minimum": 1},
          "label": {"type": "string", "minLength": 2},
          "category": {"type": "string", "enum": ["furniture", "opening", "prop", "plot_device", "area"]},
          "position_hint": {"type": "string", "minLength": 5}
        },
        "required": ["number", "label", "category", "position_hint"],
        "additionalProperties": false
      }
    },
    "camera_recommendations": {
      "type": "array",
      "items": {
        "type": "object",
        "properties": {
          "bg_id": {"type": "string", "pattern": "^[a-z0-9][a-z0-9_]*$"},
          "sub_location": {"type": "string", "pattern": "^[a-z0-9][a-z0-9_]*$"},
          "camera_position": {"type": "string", "minLength": 10},
          "camera_height": {"type": "string", "minLength": 5},
          "lens_hint": {"type": "string", "minLength": 3},
          "framing_notes": {"type": "string"}
        },
        "required": ["bg_id", "sub_location", "camera_position", "camera_height", "lens_hint"],
        "additionalProperties": false
      }
    }
  },
  "required": ["fp_id", "t2i_prompt", "key_elements", "numbered_elements", "camera_recommendations"],
  "additionalProperties": false
}
```

- [ ] **Step 1.2: system.md 작성**

```markdown
# Floor Plan Prompt — System (v2)

You write t2i prompts for floor plan images (top-down architectural diagrams) AND output structured metadata used by downstream background prompts.

Given a floor plan spec from the master plan, plus the related scene segments (verbatim) and shots that will use this floor plan, produce a JSON object with: `fp_id`, English `t2i_prompt`, `key_elements[]`, `numbered_elements[]`, and `camera_recommendations[]`.

## Rules

1. **Top-down architectural diagram** — clean line work. NO photoreal furniture. Square 1024×1024 aspect.
2. **Numbered markers — minimal text inside the diagram**: every persistent prop, doorway, window, plot-critical visual device, and furniture item visible in downstream backgrounds MUST appear in the diagram as a small **number marker (1, 2, 3, ...)** — NEVER as a text label. Each number is unique and appears once on the diagram. Area labels (Living Room, Bedroom, Rooftop, Kitchen — short generic English words) MAY appear as larger text inside their area for area identification. No other text inside the diagram.
3. **English-only t2i_prompt body**. Korean/Hanja/kana = error. (식별자는 별도 ASCII 규칙으로 검증.)
4. **No proper nouns from the work**. Generic descriptors only.
5. **Cultural cues derive (no hardcoding)**: analyze `visual_world_rules` (era + region + description) + `scene_segments` (verbatim) and reflect period/region-specific architectural cues in `t2i_prompt` (window frame style, door type, floor material, lighting fixture, wall finish). The LLM must derive these — do NOT use specific work nouns or place names.
6. **numbered_elements output**: for every number you place on the diagram (objects + areas), emit one entry with `number`, `label` (short English phrase), `category` (`furniture` | `opening` | `prop` | `plot_device` | `area`), `position_hint` (e.g., "north wall, far end of living_room"). Numbers must be unique integers ≥ 1.
7. **camera_recommendations output**: for every `backgrounds[].bg_id` listed in the input that depends on this fp_id, emit one entry: `bg_id`, `sub_location`, `camera_position` (use number references like "near number 1 (entrance), facing diagonally toward number 2 (wardrobe) and number 3 (sliding window)"), `camera_height` (e.g., "eye-level standing ~1.6m"), `lens_hint` (e.g., "35mm wide angle"), `framing_notes` (optional — e.g., "include numbers 2 and 3 prominently, leave number 4 in foreground"). One entry per bg_id (same camera for all shots of that bg_id).

## Output

Strict JSON per schema. No prose outside JSON.
```

- [ ] **Step 1.3: user_template.md 작성**

```markdown
## Floor plan
fp_id: {fp_id}
sub_location: {sub_location}
scope: {scope}

## Backgrounds that will reference this floor plan
{backgrounds_block}

## All shots using this floor plan
{shots_block}

## Scene segments (verbatim — do not summarize)
{scene_segments_block}

## visual_world_rules
{visual_world_rules}
```

- [ ] **Step 1.4: 파일 검증**

```bash
python3 -c "import json; json.load(open('prompts/_base/floor_plan_prompt/2.202604300800/schema.json'))"
ls -la prompts/_base/floor_plan_prompt/2.202604300800/
```

Expected: schema.json validates, 3 files present.

- [ ] **Step 1.5: commit**

```bash
git add prompts/_base/floor_plan_prompt/2.202604300800/
git commit -m "feat(phase8): floor_plan_prompt v2 — numbered_elements + camera_recommendations + cultural cues derive"
```

---

## Task 2: background_prompt v2 prompt 신설

**Files:**
- Create: `prompts/_base/background_prompt/2.202604300800/system.md`
- Create: `prompts/_base/background_prompt/2.202604300800/user_template.md`
- Create: `prompts/_base/background_prompt/2.202604300800/schema.json`
- Reference: `prompts/_base/background_prompt/1.202604292053/` (기존 — 변경 X)

**Steps:**

- [ ] **Step 2.1: schema.json 작성** (기존과 동일 구조 — t2i_prompt minLength 50 유지)

```json
{
  "type": "object",
  "properties": {
    "bg_id": {"type": "string", "pattern": "^[a-z0-9][a-z0-9_]*$"},
    "t2i_prompt": {"type": "string", "minLength": 50},
    "ref_guide": {"type": "string"},
    "shot_guides": {
      "type": "array",
      "items": {
        "type": "object",
        "properties": {
          "shot_id": {"type": "string"},
          "guide_text": {"type": "string"}
        },
        "required": ["shot_id", "guide_text"],
        "additionalProperties": false
      }
    }
  },
  "required": ["bg_id", "t2i_prompt", "ref_guide", "shot_guides"],
  "additionalProperties": false
}
```

- [ ] **Step 2.2: system.md 작성**

```markdown
# Background Prompt — System (v2)

You write `t2i_prompt` for photoreal background images that will be rendered by gpt-image-2 with the floor plan PNG (and optional prior background) as image references.

Given a single background spec, produce: `t2i_prompt` (in source language), `ref_guide`, `shot_guides[]` (one per applies_to_shots).

## Rules

1. **Output language for t2i_prompt body**: write in the **SAME language as `scene_segments` and `visual_world_rules`** input. Identifiers (`bg_id`, `sub_location`, `state_label`, `shot_id`) and other JSON fields remain ASCII snake_case. Only the `t2i_prompt` body and `ref_guide`/`guide_text` follow the source language. (Trigger gpt-image-2 native web grounding in the source language.)
2. **No proper nouns from the work**. Use generic descriptors only.
3. **NO people, NO faces, NO blood-on-corpses depicted**. Background only — empty space, props, atmosphere.
4. **Plot-critical visual devices** mentioned in scene segments MUST be in t2i_prompt (drawn curtain, broken window, scattered debris, etc).
5. **Image purpose — cinematic eye-level photograph**: t2i_prompt MUST describe a film/drama still frame at standing human height (~1.6m), 35mm-class lens, wide composition framing furniture and walls from inside the room. **Strictly NOT a top-down view, NOT a floor plan, NOT an architectural diagram**. State this purpose explicitly.
6. **Reference role**: `floor_plan_path` is a **layout source ONLY** — use it to identify which furniture and openings exist and their relative positions. Do NOT instruct the model to "preserve the floor plan exactly" or replicate its top-down perspective. `prior_bg_paths` (when present) are for matching lighting/material/style consistency only.
7. **Camera derivation**: when input `camera_recommendations` for this bg_id is provided, translate its `camera_position` + `camera_height` + `lens_hint` + `framing_notes` into source-language natural prose in t2i_prompt. Convert numbered references (e.g., "number 2 (wardrobe)") into descriptive phrases using the input `numbered_elements` mapping (e.g., 한국어 "옷장"). When no recommendation is provided, derive viewpoint from scene_segments + applies_to_shots descriptions.
8. **Cultural/architectural cues derive (no hardcoding)**: analyze `visual_world_rules` (era + region + description) + `scene_segments` and weave period/region-specific reality cues into t2i_prompt (architectural style, materials, lighting fixture, window/door type, era-specific props). LLM must derive — do NOT hardcode work-specific nouns. These cues trigger gpt-image-2 native web grounding for accurate references.
9. **Aspect ratio cue**: end t2i_prompt with a "16:9 시네마틱 화면비" / "16:9 cinematic aspect ratio" hint in source language (actual size 1536×864 set by code).
10. **State variation**: state_label drives lighting/mood/decor (e.g., `day_normal`, `dusk_ransacked`, `night_blood_curtain_drawn`). Reflect vividly in source language.

## Output

Strict JSON per schema. No prose outside JSON.
```

- [ ] **Step 2.3: user_template.md 작성**

```markdown
## Background spec
bg_id: {bg_id}
loc_id: {loc_id}
sub_location: {sub_location}
state_label: {state_label}

## References
floor_plan: {floor_plan_path_block}
prior backgrounds: {prior_bg_paths_block}

## Floor plan numbered elements (from floor_plan_prompt)
{numbered_elements_block}

## Camera recommendation (from floor_plan_prompt)
{camera_recommendation_block}

## Applies to shots
{applies_to_shots_block}

## Scene segments (verbatim — do not summarize)
{scene_segments_block}

## visual_world_rules
{visual_world_rules}
```

- [ ] **Step 2.4: 파일 검증**

```bash
python3 -c "import json; json.load(open('prompts/_base/background_prompt/2.202604300800/schema.json'))"
```

- [ ] **Step 2.5: commit**

```bash
git add prompts/_base/background_prompt/2.202604300800/
git commit -m "feat(phase8): background_prompt v2 — source-language t2i + numbered_elements/camera ref + cinematic eye-level"
```

---

## Task 3: floor_plan_prompt 모듈 코드 갱신

**Files:**
- Modify: `backend/app/modules/pipeline/floor_plan_prompt.py`
- Test: `backend/tests/modules/pipeline/test_floor_plan_prompt.py`

**Steps:**

- [ ] **Step 3.1: 결과 보존 — numbered_elements + camera_recommendations**

`floor_plan_prompt.py`의 `run_floor_plan_prompt()` (또는 동등 entry func)가 LLM JSON 결과에서 `numbered_elements` + `camera_recommendations` 필드를 dict 그대로 보존해 반환하도록 수정. schema 기반 validation은 `call_structured`가 처리.

- [ ] **Step 3.2: 검증 helper 추가**

```python
def _validate_fp_prompt_extras(
    *,
    fp_result: dict,
    expected_bg_ids: set[str],
) -> None:
    """v2 신규 필드 검증.
    
    - numbered_elements[].number unique integer ≥1
    - camera_recommendations[].bg_id ⊆ expected_bg_ids (master_plan에서 이 fp를 ref하는 bg_ids)
    - camera_recommendations[].sub_location ASCII snake_case
    """
    nums = [int(e["number"]) for e in fp_result.get("numbered_elements", [])]
    if len(nums) != len(set(nums)):
        raise FloorPlanPromptError(f"duplicate numbered_elements.number: {nums}")
    if any(n < 1 for n in nums):
        raise FloorPlanPromptError(f"numbered_elements.number must be ≥1: {nums}")
    cam_bg = {c["bg_id"] for c in fp_result.get("camera_recommendations", [])}
    extra = cam_bg - expected_bg_ids
    if extra:
        raise FloorPlanPromptError(
            f"camera_recommendations references unknown bg_ids: {sorted(extra)}"
        )
```

호출부: run_floor_plan_prompt()이 LLM 결과 받은 직후 호출. expected_bg_ids는 caller(step)가 master_plan에서 이 fp_id를 depends_on_fp로 가진 bg 목록을 사전 추출해 전달.

- [ ] **Step 3.3: 단위 테스트**

```python
def test_floor_plan_prompt_v2_validates_numbered_elements_unique():
    bad = {"fp_id": "fp_x", "t2i_prompt": "..." * 10, "key_elements": [], 
           "numbered_elements": [{"number": 1, "label": "a", "category": "area", "position_hint": "x"*5},
                                 {"number": 1, "label": "b", "category": "area", "position_hint": "x"*5}],
           "camera_recommendations": []}
    with pytest.raises(FloorPlanPromptError, match="duplicate"):
        _validate_fp_prompt_extras(fp_result=bad, expected_bg_ids=set())

def test_floor_plan_prompt_v2_validates_camera_bg_ids_subset():
    bad = {"fp_id": "fp_x", "t2i_prompt": "..."*10, "key_elements": [],
           "numbered_elements": [],
           "camera_recommendations": [{"bg_id": "bg_unknown", "sub_location": "x", 
                                       "camera_position": "x"*10, "camera_height": "y"*5, 
                                       "lens_hint": "35mm"}]}
    with pytest.raises(FloorPlanPromptError, match="unknown bg_ids"):
        _validate_fp_prompt_extras(fp_result=bad, expected_bg_ids={"bg_known"})
```

- [ ] **Step 3.4: tests 실행**

```bash
.venv/bin/pytest backend/tests/modules/pipeline/test_floor_plan_prompt.py -v
```

Expected: 신규 2 tests PASS, 기존 회귀 0.

- [ ] **Step 3.5: commit**

```bash
git add backend/app/modules/pipeline/floor_plan_prompt.py backend/tests/modules/pipeline/test_floor_plan_prompt.py
git commit -m "feat(phase8): floor_plan_prompt module — v2 schema validation (numbered/camera)"
```

---

## Task 4: floor_plan_prompt_step 코드 갱신

**Files:**
- Modify: `backend/app/core/steps/floor_plan_prompt_step.py`
- Test: `backend/tests/core/steps/test_floor_plan_prompt_step.py`

**Steps:**

- [ ] **Step 4.1: PROMPT_VERSION 갱신**

`PROMPT_VERSION` 상수를 `"2.202604300800"`으로 변경. config_hash로 invalidate되어 기존 체크포인트 자동 재생성.

- [ ] **Step 4.2: master_plan에서 expected_bg_ids 사전 추출 + 모듈에 전달**

각 fp_id에 대해 master_plan의 `backgrounds[]` 중 `depends_on_fp`에 이 fp_id가 포함된 모든 `bg_id` 집합을 사전 추출해 `run_floor_plan_prompt`에 전달.

- [ ] **Step 4.3: 체크포인트 data 보존**

`data.fp_prompts[fp_id]` 구조에 `numbered_elements` + `camera_recommendations`가 dict 그대로 보존되는지 확인 (기존 dict shallow copy 패턴이면 자동 OK — 명시적 필드 화이트리스트가 있으면 추가).

- [ ] **Step 4.4: 단위 테스트 갱신**

```python
def test_fp_prompt_step_persists_v2_fields(tmp_project):
    # Mock LLM response with numbered + camera fields
    # Run step
    cp = load_checkpoint("floor_plan_prompt", project_id, episode_id)
    fp = cp["data"]["fp_prompts"]["fp_test"]
    assert "numbered_elements" in fp
    assert "camera_recommendations" in fp
    assert fp["numbered_elements"][0]["category"] in {"furniture", "opening", "prop", "plot_device", "area"}
```

- [ ] **Step 4.5: commit**

```bash
git add backend/app/core/steps/floor_plan_prompt_step.py backend/tests/core/steps/test_floor_plan_prompt_step.py
git commit -m "feat(phase8): floor_plan_prompt_step — bump PROMPT_VERSION + persist numbered/camera fields"
```

---

## Task 5: background_prompt 모듈 코드 갱신

**Files:**
- Modify: `backend/app/modules/pipeline/background_prompt.py`
- Test: `backend/tests/modules/pipeline/test_background_prompt.py`

**Steps:**

- [ ] **Step 5.1: input 인자 추가**

`run_background_prompt(...)` (또는 등가) 시그니처에 추가:
- `numbered_elements: Optional[List[Dict[str, Any]]] = None`  (전체 numbered_elements list — 도면의 dict)
- `camera_recommendation: Optional[Dict[str, Any]] = None`  (현재 bg_id에 매칭되는 단일 entry, 또는 None)

- [ ] **Step 5.2: user_template 직렬화 helper**

```python
def _format_numbered_elements_block(items: Optional[List[Dict[str, Any]]]) -> str:
    if not items:
        return "(none — derive from scene segments and shot descriptions)"
    lines = []
    for it in items:
        lines.append(f"{it['number']}. {it['label']} [{it['category']}] — {it['position_hint']}")
    return "\n".join(lines)

def _format_camera_recommendation_block(cam: Optional[Dict[str, Any]]) -> str:
    if not cam:
        return "(none — derive from scene segments and shot descriptions)"
    parts = [
        f"camera_position: {cam['camera_position']}",
        f"camera_height: {cam['camera_height']}",
        f"lens_hint: {cam['lens_hint']}",
    ]
    fn = cam.get("framing_notes", "")
    if fn:
        parts.append(f"framing_notes: {fn}")
    return "\n".join(parts)
```

user_template render 시 `{numbered_elements_block}`, `{camera_recommendation_block}` 키로 inject. `.replace()` 사용 (CLAUDE.md 절대 규칙 — `.format()` 금지, brace collision 방지).

- [ ] **Step 5.3: ASCII validation 제거 (식별자만 유지)**

기존 코드에 t2i_prompt 본문에 대한 ASCII safe / no-Korean 검증이 있다면 제거. fp_id/bg_id/sub_location/state_label/shot_id에 대한 ASCII 검증은 유지.

- [ ] **Step 5.4: 단위 테스트 갱신**

```python
def test_background_prompt_v2_inject_numbered_block():
    user_prompt = build_background_user_prompt(
        bg_id="bg_x", loc_id="L01", sub_location="living_room", state_label="day_normal",
        floor_plan_path=Path("/tmp/fp.png"), prior_bg_paths=[],
        applies_to_shots=["S1_Shot1"],
        scene_segments=[{"scene_index": 1, "heading": "EXT.", "text": "..."}],
        visual_world_rules="rules text",
        numbered_elements=[
            {"number": 1, "label": "wardrobe", "category": "furniture", "position_hint": "north wall"},
            {"number": 2, "label": "window", "category": "opening", "position_hint": "east wall"},
        ],
        camera_recommendation={
            "bg_id": "bg_x", "sub_location": "living_room",
            "camera_position": "near number 1, facing 2", "camera_height": "1.6m",
            "lens_hint": "35mm",
        },
    )
    assert "1. wardrobe [furniture] — north wall" in user_prompt
    assert "near number 1, facing 2" in user_prompt

def test_background_prompt_v2_blocks_blank_when_no_camera():
    up = build_background_user_prompt(
        bg_id="bg_x", loc_id="L01", sub_location="x", state_label="x",
        floor_plan_path=None, prior_bg_paths=[], applies_to_shots=[],
        scene_segments=[], visual_world_rules="",
        numbered_elements=None, camera_recommendation=None,
    )
    assert "(none" in up  # block 가이드 문자열

def test_background_prompt_v2_allows_korean_t2i_prompt():
    """ASCII-only 검증이 t2i_prompt 본문에 적용되지 않아야 함."""
    # 모듈에 ASCII validator가 있다면 t2i_prompt 한국어 검증을 통과해야 함
    ...
```

- [ ] **Step 5.5: 회귀 테스트**

```bash
.venv/bin/pytest backend/tests/modules/pipeline/test_background_prompt.py -v
```

Expected: 신규 3 tests PASS, 기존 회귀 0.

- [ ] **Step 5.6: commit**

```bash
git add backend/app/modules/pipeline/background_prompt.py backend/tests/modules/pipeline/test_background_prompt.py
git commit -m "feat(phase8): background_prompt module — accept numbered/camera + drop t2i ASCII guard"
```

---

## Task 6: background_prompt_step 코드 갱신

**Files:**
- Modify: `backend/app/core/steps/background_prompt_step.py`
- Test: `backend/tests/core/steps/test_background_prompt_step.py`

**Steps:**

- [ ] **Step 6.1: PROMPT_VERSION 갱신** → `"2.202604300800"`

- [ ] **Step 6.2: floor_plan_prompt 체크포인트 로드 + flat 매핑 구성**

```python
def _build_fp_lookup_for_bg(
    *,
    fp_prompt_cp: Dict[str, Any],
    master_plan_cp: Dict[str, Any],
) -> Tuple[Dict[str, List[dict]], Dict[str, dict]]:
    """bg_id -> (numbered_elements list, camera_recommendation entry).
    
    numbered_elements는 fp 단위로 fp_prompt_cp에 들어 있고, 각 bg는 depends_on_fp[0]을
    가진다 (master_plan invariant 4: 같은 sub_location은 같은 fp). camera는 fp 안 list
    에서 bg_id로 lookup.
    """
    fp_prompts = (fp_prompt_cp.get("data", {}) or {}).get("fp_prompts", {}) or {}
    plans = (master_plan_cp.get("data", {}) or {}).get("plans", {}) or {}
    bg_to_fp: Dict[str, str] = {}
    for _gid, gp in plans.items():
        plan = (gp or {}).get("plan") or {}
        for bg in plan.get("backgrounds", []) or []:
            dep = bg.get("depends_on_fp", []) or []
            if dep:
                bg_to_fp[bg["bg_id"]] = dep[0]
    
    numbered_lookup: Dict[str, List[dict]] = {}
    camera_lookup: Dict[str, dict] = {}
    for bg_id, fp_id in bg_to_fp.items():
        fp = fp_prompts.get(fp_id) or {}
        numbered_lookup[bg_id] = fp.get("numbered_elements", []) or []
        for cam in fp.get("camera_recommendations", []) or []:
            if cam.get("bg_id") == bg_id:
                camera_lookup[bg_id] = cam
                break
    return numbered_lookup, camera_lookup
```

- [ ] **Step 6.3: build/run 호출에 inject**

각 bg 처리 시 `numbered_lookup[bg_id]` + `camera_lookup.get(bg_id)`를 `run_background_prompt`에 전달.

- [ ] **Step 6.4: 단위 테스트**

```python
def test_bg_prompt_step_loads_v2_fp_fields(tmp_project, mock_llm):
    # given fp_prompt cp with numbered_elements + camera_recommendations for bg_x
    # given master_plan cp with bg_x depends_on_fp=[fp_y]
    # when bg_prompt step runs for bg_x
    # then build_user_prompt receives numbered_elements + camera entry
    ...
```

- [ ] **Step 6.5: commit**

```bash
git add backend/app/core/steps/background_prompt_step.py backend/tests/core/steps/test_background_prompt_step.py
git commit -m "feat(phase8): background_prompt_step — load numbered/camera from fp_prompt cp + bump version"
```

---

## Task 7: background_render size default 16:9

**Files:**
- Modify: `backend/app/modules/pipeline/background_render.py`
- Modify: `backend/app/core/steps/background_render_step.py` (필요 시)
- Test: `backend/tests/modules/pipeline/test_background_render.py`

**Steps:**

- [ ] **Step 7.1: 기본 size 변경**

`render_one_background()` 시그니처의 `size: str = "1024x1024"` → `size: str = "1536x864"`.

- [ ] **Step 7.2: render_step에서 명시 인자 전달 (선택)**

`background_render_step.py`가 `render_one_background` 호출 시 size를 명시 전달하지 않으면 default 적용. config/settings로 외부화 필요 시 추후 phase. 이번 phase는 default만 변경.

- [ ] **Step 7.3: 단위 테스트**

```python
def test_render_one_background_default_size_16_9(monkeypatch):
    captured = {}
    class FakeClient:
        class images:
            @staticmethod
            def edit(**kw):
                captured["size"] = kw["size"]
                # mock response
                ...
    info = render_one_background(
        openai_client=FakeClient,
        image_model="gpt-image-2",
        prompt="...",
        out_path=Path("/tmp/x.png"),
        fp_path=Path("/tmp/fp.png"),  # exists assumption — adapt as needed
        prior_bg_paths=[],
    )
    assert captured["size"] == "1536x864"
```

(실제 테스트는 OpenAI client mock 패턴에 맞춰 작성 — 기존 테스트 파일의 mock 패턴 따르기)

- [ ] **Step 7.4: commit**

```bash
git add backend/app/modules/pipeline/background_render.py backend/tests/modules/pipeline/test_background_render.py
git commit -m "feat(phase8): background_render — default size 1536x864 (16:9 cinematic)"
```

---

## Task 8: 통합 smoke 테스트

**Files:**
- Modify: `backend/tests/integration/test_phase7_e2e.py` (또는 `test_phase8_e2e.py` 신설 — Phase 7 패턴 mirror)

**Steps:**

- [ ] **Step 8.1: smoke 갱신/추가**

기존 Phase 7 통합 smoke가 6 step 순차 실행을 mock 기반으로 검증. v2 prompt + numbered/camera 필드까지 흐름이 통과하는지 확인.

```python
def test_phase8_floor_plan_to_background_flow_with_camera_recommendations():
    """Step 3 (fp_prompt) → Step 5 (bg_prompt) 데이터 흐름 검증.
    
    fp_prompt가 numbered_elements + camera_recommendations 산출 →
    bg_prompt_step이 로드해서 inject → user_prompt에 두 블록 포함 확인.
    """
    ...
```

- [ ] **Step 8.2: 회귀 — 전체 baseline 회귀 0**

```bash
.venv/bin/pytest backend/tests/ -v 2>&1 | tail -40
```

Expected: 1228 passed (Phase 7 baseline) → 1228 + 신규 tests passed, 회귀 0.

- [ ] **Step 8.3: commit**

```bash
git add backend/tests/integration/test_phase8_e2e.py
git commit -m "test(phase8): integration smoke — fp v2 → bg v2 data flow with numbered/camera"
```

---

## Task 9: 듀얼 코드 리뷰 (Codex + Claude)

**Steps:**

- [ ] **Step 9.1: Codex 리뷰 dispatch**

`/ultrareview` 또는 `codex:codex-rescue` agent를 통해 Codex review (변경된 파일 전부).

- [ ] **Step 9.2: Claude 리뷰 dispatch**

`feature-dev:code-reviewer` 또는 `superpowers:code-reviewer` agent로 Claude 리뷰 (병렬 가능).

- [ ] **Step 9.3: BLOCKING/IMPORTANT fix**

각 리뷰의 BLOCKING + IMPORTANT 항목 fix → re-review.

- [ ] **Step 9.4: 최종 push**

```bash
git push origin main
```

---

## Task 10: production E2E 재실행 준비 (사용자 실행)

**Steps:**

- [ ] **Step 10.1: 사용자 안내 문구**

"Phase 8 완료. PID c00bbe19 EP fe165e3a에서 floor_plan_prompt + background_prompt 새 버전으로 invalidate되어 Step 3+5+6이 자동 재실행됨. master_plan은 변경 없음 (Step 1+2 재사용). UI 또는 force 파라미터로 재실행."

- [ ] **Step 10.2: 결과 검증 항목 안내**

1. 도면 PNG 안 가구/문/창 = 번호만, 영역 라벨만 짧은 영어
2. 배경 PNG = 16:9 cinematic eye-level (top-down 0건)
3. 배경 t2i_prompt = 한국어 (PostgreSQL 저장 확인)
4. 도면/배경 cultural cues 반영 (옥탑방 한국 reality)
5. 같은 sub_location 안 카메라 위치가 도면 좌표계와 일관

---

## Self-review checklist

- [ ] 모든 task에 file paths exact + 코드 블록 + commit 메시지 포함
- [ ] 시나리오 의존 단어 hardcode 0 (system prompt + 코드 모두)
- [ ] LLM 입력 무절단 (모든 inject 블록이 verbatim)
- [ ] subagent 모두 opus 명시 (Task 9 리뷰는 Codex + Claude)
- [ ] PROMPT_VERSION 갱신 → 기존 체크포인트 자동 invalidate
- [ ] 별도 PR (variant_label varchar 확장) 분리 명시
