# review-codex-1 종합 수정 계획서 (통합판)

> **기준일**: 2026-04-21
> **입력**:
> - `docs/review-codex-1/01~10` (Codex 리뷰 10개 문서)
> - `docs/review-codex-1/11-fix-plan-codex.md` (Codex 보강 피드백)
> **범위**: 백엔드(175 파일) + 프런트(76 파일) + 테스트(76 파일) + 문서 전부
> **현재 상태**: v0.6.0 태그 (Phase 5 완결 선언) 기준
> **통합 이력**: v1 (원본 P0~P6 기반) → **v2 (현재, Wave/Gate/Lane 프레임 도입)**

---

## 0. Executive Summary

### 0.1 한 줄 진단

> **"리팩토링 방향성은 옳았고 상위 구조는 정리됐으나, 새 계약과 옛 계약이 동시에 살아 있어서 운영 신뢰성이 아직 배포 수준에 미달한다."**

### 0.2 실측 기준선 (2026-04-21)

| 지표 | 실측 결과 | 문서 주장 | Drift |
|------|----------|----------|-------|
| Backend `pytest backend/tests -q` | **637 passed / 56 failed / 2 errors / 1 skipped** | `256 passed` | ❌ 문서는 subset만 말함 |
| Frontend `npm run build` | ✅ 성공 | ✅ | ⚠️ 단일 chunk 987.79 kB 경고 |
| Frontend `npm run lint` | **77 errors / 3 warnings** | 언급 없음 | ❌ strict config 켜짐, 코드 미충족 |
| Frontend test/spec 파일 | **0개** | 언급 없음 | ❌ 자동화 테스트 lane 부재 |
| Step 총 개수 | **49** | 문서 `48`, 논의자료 `36` | ❌ 3-way mismatch |
| applicability 분포 | always 38 / on_demand 4 / disabled 4 / if_planning_doc 1 / if_set_design_enabled 1 / if_has_outlooks 1 | always 40 / on_demand 5 / disabled 3 | ❌ |
| `image_service.py` LOC | **1,228** (shim 선언) | `1,224`(완료 선언) | ⚠️ 방향 맞지만 크기 오해 |
| `except: pass` | **0** (v0.6.0에서 제거 완료 ✅) | 0 | ✅ 이 축은 유일하게 일치 |

### 0.3 핵심 판단 5가지 (Codex 보강 반영)

1. **완료 서술은 과장됐다**: `architecture-refactor-final/README.md`의 "256 passed / Phase 5 완결"은 **subset 결과**이지 저장소 전체 상태가 아니다.
2. **내부 수렴, 외부 이중화**: StepRunner + dispatch는 단일화됐지만, `/analyze`와 `/steps/run-all`이 공개 API로 둘 다 살아 있다.
3. **`P0 → P6` 구조는 유지하되, 실행은 `Wave 0 → 7` + `Gate G0 → G6`로 관리한다** (Codex 보강).
4. **`repo-wide pytest green`은 조기 목표가 아니라 후반부 gate**다. image/variation drift는 image 도메인 분해 후에야 안정적으로 닫힌다.
5. **Facade-first**: 대형 서비스 분해는 공식 진입점부터 고정하고 내부를 빼내는 순서여야 한다. Big-bang split은 회귀 해석 불가.

### 0.4 권장 작업 흐름 (Wave 기반)

```mermaid
flowchart LR
    W0[Wave 0<br/>사실표 lock] --> G0((G0))
    G0 --> W1[Wave 1<br/>공개 계약 수렴]
    W1 --> G1((G1))
    G1 --> W2[Wave 2<br/>startup/config 경계]
    W2 --> G2((G2))
    G2 --> W3[Wave 3<br/>테스트 lane + FE 안전망]
    W3 --> G3((G3))
    G3 --> W4[Wave 4<br/>projection 가시화]
    W4 --> G4((G4))
    G4 --> W5[Wave 5<br/>이미지 도메인 2차 분해]
    W5 --> G5((G5))
    G5 --> W6[Wave 6<br/>FE hotspot + 성능 마감]
    W6 --> G6((G6))
    G6 --> W7[Wave 7<br/>제품/운영 고도화]

    style G3 fill:#c8e6c9,stroke:#2e7d32
    style G5 fill:#c8e6c9,stroke:#2e7d32
    style G6 fill:#c8e6c9,stroke:#2e7d32
```

Gate의 제품적 의미:
- **G3 통과** = 내부 팀이 기능을 쓰면서 refactor를 계속할 수 있는 상태
- **G5 통과** = silent stale을 제거하고 image lane까지 green, 대형 backend 구조 변경 종료
- **G6 통과** = 파일럿 또는 외부 사용 검토가 가능한 상태

---

## 1. 전체 이슈 맵 (32건, 우선순위 v2)

### 1.1 Wave/Gate 재배치 표

| 번호 | 제목 | Wave (v2) | 원본 우선순위 | 영역 | 공수 | 리스크 | 병렬 |
|------|------|-----------|---------------|------|------|--------|------|
| **F01** | step 총량/분포 숫자 불일치 | W0 | P0 | 문서 | 0.5d | 낮음 | ✅ |
| **F02** | test 수치 2종 분리 미표기 | W0 | P0 | 문서 | 0.5d | 낮음 | ✅ |
| **F03** | `architecture-refactor-final` "완료" 과장 | W0 | P0 | 문서 | 0.5d | 낮음 | ✅ |
| **F04** | `06-data-contracts.md` 구시대 서술 | W0 | P0 | 문서 | 0.5d | 낮음 | ✅ |
| **F05** | line-level 증거 낡음 | W0 | P0 | 문서 | 0.5d | 낮음 | ✅ |
| **F06** | `product-roadmap-discussion.md` "36단계" | W0 | P0 | 문서 | 0.5d | 낮음 | ✅ |
| **F07** | `image_service.py` "완료" 과장 | W0 | P0 | 문서 | 0.5d | 낮음 | ✅ |
| **F08** | `/analyze` vs `/steps/run-all` 이중 계약 | W1 | P1 | 백+프 | 1.5d | 중 | 일부 |
| **F09** | `useAnalyzeEpisode.ts` legacy 경로 | W1 | P1 | 프런트 | 0.5d | 낮음 | ✅ |
| **F10** | `StepRunner`가 `STEP_MANIFEST` 직접 참조 | W1 | P1 | 백엔드 | 1d | 중 | ✅ |
| **F11** | `steps.py` orchestration hub | W1 | P1 | 백엔드 | 2d | 중 | 순차 |
| **F12** | `_needs_presync` route 하드코딩 | W1 | P1 | 백엔드 | 1d | 중 | 순차 |
| **F20** | `text_cleaner.clean_text` drift | W1 | P1/P2 | 백+테스트 | 0.3d | 낮음 | ✅ |
| **F21** | `VariationRecommender` 시그니처 drift | W1 | P1/P2 | 백+테스트 | 0.3d | 낮음 | ✅ |
| **F13** | `init_db()` import-time side effect | W2 | P2 | 백엔드 | 1d | **높** | 순차 |
| **F14** | `on_event("startup")` deprecated | W2 | P2 | 백엔드 | F13과 묶음 | 낮음 | - |
| **F15** | default-user bootstrap coupling | W2 | P2 | 백엔드 | 1d | 중 | ✅ |
| **F16** | `config.py` Pydantic v1 `class Config` | W2 | P2 | 백엔드 | 0.5d | 낮음 | ✅ |
| **F17** | insecure defaults 정책 | W2 | P2 | 백엔드+보안 | 1d | 중 | ✅ |
| **F18** | `test_pipeline_e2e` suite 실패 | W2 | P2 | 테스트 | F13 해결 시 자동 | - | - |
| **F19-A** | cluster A: step manifest drift | W3 | P2 | 테스트 | 1d | 낮음 | ✅ |
| **F19-B** | cluster B: scene/outlook/entity drift | W3 | P2 | 테스트 | 1.5d | 중 | ✅ |
| **F19-C** | cluster C: text/variation drift | W3 | P2 | 테스트 | 0.5d | 낮음 | F20/F21 후 |
| **F29** | `useEventSource.ts` 미사용 + lint | W3 | P5 | 프런트 | 0.3d | 낮음 | ✅ |
| **F30** | `ImageGalleryModal` set-state-in-effect | W3 | P5 | 프런트 | 0.3d | 낮음 | ✅ |
| **F32** | Vitest + RTL smoke lane | W3 | P5 | 프런트 | 2d | 낮음 | ✅ |
| **P3 전체** | projection/sync 신뢰성 | W4 | P3 | 백+프 | 3~4d | 중 | 일부 |
| **F25** | `image_steps.py` dead code | W5 | P4 | 백엔드 | 0.3d | 낮음 | ✅ |
| **F23** | `image_service.py` shim 축소 | W5 | P4 | 백엔드 | 3d | 중 | facade 후 |
| **F22** | `scene_image_service.py` 5-way split | W5 | P4 | 백엔드 | 5d | **높** | facade 후 |
| **F24** | `reference_image_service.py` 3-way | W5 | P4 | 백엔드 | 3d | 중 | ✅ |
| **F19-D** | cluster D: image API/response drift | W5 | P2 | 테스트 | 1d | 중 | image 분해 후 |
| **F26** | `SceneVariationCard.tsx` 1,606 LOC | W6 | P5 | 프런트 | 3d | 중 | harness 후 |
| **F27** | `Entities.tsx` any=5/catch=15 | W6 | P5 | 프런트 | 2d | 중 | ✅ |
| **F28** | `EpisodeStills.tsx` any=21/catch=17 | W6 | P5 | 프런트 | 2d | 중 | ✅ |
| **F31** | bundle 987 kB → code splitting | W6 | P5 | 프런트 | 1d | 낮음 | ✅ |
| **F32-확장** | hook/component 테스트 확장 | W6 | P5 | 프런트 | 1d | 낮음 | ✅ |

**총 추정 공수**: 35~47 개발일 (병렬 가능 영역 최대 활용 시 하한, 순차 진행 시 상한)

### 1.2 원본 v1 → v2 재배치 근거 (Codex 보강 반영)

| 이슈 | v1 배치 | v2 배치 | 이유 |
|------|---------|---------|------|
| F19 전체 | P2 (6.5d) | W3 A/B/C + W5 D | cluster D는 image 도메인 분해 후에만 안정 |
| F32 | P5 마지막 | W3 중간 | harness 없이 대형 컴포넌트 분해는 회귀 측정 불가 |
| F29/F30 | P5 | W3 | 저위험 lint blocker는 조기 병렬로 충분 |
| F22 5-way | "5-way split 한 번에" | "facade 고정 → 내부 추출 → shim 제거" 3단계 | Big-bang은 호출자/테스트 동시 흔들림 |
| F25 | P4 중간 | W5 첫 단계 | dead code 제거는 리스크 없고 facade scaffolding 기반 |

---

## 2. 실행 원칙 (신규, Codex 보강)

이 5개 원칙은 모든 Wave에 공통 적용된다.

### 2.1 Repair before redesign

먼저 현재 계약을 고정하고, 그 다음 구조를 더 예쁘게 만든다. **W0에서 문서를 현재 코드와 일치시키는 것**이 최우선인 이유.

### 2.2 One runtime boundary change per PR

한 PR 안에서 바꾸는 런타임 경계는 하나만 허용한다. 실패 원인을 분리할 수 있는 최소 단위.

**예**: `steps.py` route 축소와 `main.py` lifespan 전환을 같은 PR에 넣지 말 것.

### 2.3 Gate by lane, not by hope

phase 종료는 "느낌상 안정적"이 아니라 **명시된 lane green 조건**으로만 판단한다.

### 2.4 Facade first

대형 서비스 분해는 **facade(공식 진입점)를 먼저 고정**하고, 내부 구현을 빼내는 순서로 진행한다. Big-bang 분해 금지.

**예**: `scene_image_service.py` 5-way split → (1) facade method signature 고정 → (2) 내부 함수 파일 이동 → (3) 테스트 재고정 → (4) dead shim 제거.

### 2.5 Docs are part of done

계약을 바꿨다면 문서와 generated baseline도 **같은 PR에서 함께 갱신**한다. 문서가 뒤처지면 다음 wave에서 다시 기준선이 흔들린다.

### 2.6 각 Wave의 "완료" 정의 (공통)

각 wave 종료는 아래 네 조건을 **동시에** 만족해야 한다.

1. 코드 변경 완료
2. 해당 wave 전용 검증 lane green
3. 관련 문서 또는 generated baseline 갱신
4. rollback 포인트 명시

---

## 3. Gate 정의 (G0 ~ G6)

| Gate | 통과 조건 | 제품적 의미 |
|------|----------|-------------|
| **G0** | 문서 숫자와 generated manifest가 일치 | "현재 상태"를 한 목소리로 말할 수 있음 |
| **G1** | 프런트 분석 시작 단일 경로, StepRunner/steps API가 catalog 경유 | 런타임 계약 수렴 완료 |
| **G2** | import-time DB side effect 제거, lifespan 전환, prod insecure fail-fast | startup 경계가 통제 가능 |
| **G3** | backend core/startup lane green, frontend smoke lane 생성 | **대형 분해 전에 최소 안전망 확보** |
| **G4** | projection failure가 API/UI에서 보임, repair 경로 존재 | 운영 관측성 확보 |
| **G5** | image lane green, facade 정리 완료, **full backend regression green** | 대형 backend 구조 변경 종료 |
| **G6** | frontend lint green, smoke/hook tests green, bundle 경고 해석 가능 수준 | **파일럿/외부 사용 검토 가능** |

---

## 4. Lane 설계

### 4.1 Backend Lane

| Lane | 목적 | 검증 범위 | Wave 진입 | 최종 green |
|------|------|----------|-----------|------------|
| `B0-docs` | 문서와 generated baseline 정합 | manifest dump, drift grep | W0 | G0 |
| `B1-core` | manifest/catalog/step orchestration | `test_step_manifest_v3.py`, `test_step_catalog.py`, `test_applicability.py`, `test_step_runner_*.py`, `test_checkpoint_sync_services.py` | W1 | G1 |
| `B2-startup` | app lifecycle/fixture/bootstrap | `test_pipeline_e2e.py`, startup fixture | W2 | G2 |
| `B3-image` | variation/image API + response shape | `test_images_api.py`, `test_variation_pipeline.py`, `test_scene_image_service.py`, `test_reference_image_service.py` | W5 | G5 |
| `B4-full` | 저장소 전체 회귀 | `pytest backend/tests -q` → 0 failed | W5 종료 | **G5** (조기 목표 아님) |

### 4.2 Frontend Lane

| Lane | 목적 | 검증 범위 | Wave 진입 | 최종 green |
|------|------|----------|-----------|------------|
| `F0-static` | lint/ts 품질 | `npm run lint` | W3 (구조 위반) → W6 (전체) | G6 |
| `F1-smoke` | 주요 페이지 렌더/기본 상호작용 | Dashboard, Episodes, EpisodeDetail, EpisodeStills | W3 도입 | G3 |
| `F2-hooks` | mutation/query invalidation | `useStillMutations`, `useRunAllSteps` | W3 도입 | G6 확장 |
| `F3-build` | bundle/build 상태 | `npm run build` | 항시 | G6 |

### 4.3 v1 대비 변경점 (중요)

1. `B4-full` green은 원본 `P2 완료 조건`이었으나 **G5로 이동**. image cluster drift가 W5 전에는 닫히지 않음.
2. `F1-smoke`와 `F2-hooks`는 **W3에서 먼저 생성** (원본은 P5 마지막).
3. `F0-static`은 W3에서 구조 위반(set-state-in-effect 등)만 먼저 제거, 전체 lint green은 G6.

---

## 5. Wave 0 — 사실표 lock

### 5.1 목적

> **문서·manifest·테스트·UI가 같은 수치를 가리키게 만든다.** 이후 모든 작업의 기준선.

### 5.2 F01 — Step 총량/분포 숫자 교정

**근거**:
- `docs/architecture/00-overview.md:165-166` → `48단계 / active 40 / on_demand 5 / disabled 3`
- `docs/architecture/06-data-contracts.md:9` → `active 40 / 총 48`
- `docs/architecture-refactor-final/README.md:76-77` → "48단계"
- 실제 `backend/tests/test_step_manifest_v3.py:13-30` → `총 49 / analysis 42 / image 5 / auxiliary 2`
- applicability → `always 38 / on_demand 4 / disabled 4 / if_planning_doc 1 / if_set_design_enabled 1 / if_has_outlooks 1`

**작업**:
1. `backend/scripts/dump_step_manifest.py` 신설 — STEP_MANIFEST를 읽어 표 Markdown을 stdout으로 출력
2. `docs/architecture/_step_manifest.generated.md` 생성 (pre-commit hook으로 재생성)
3. `docs/architecture/00-overview.md:165-166` 교정
4. `docs/architecture/06-data-contracts.md:9` 교정
5. `docs/architecture-refactor-final/README.md:76-77` 교정
6. `docs/architecture/07-prompt-versioning-policy.md:99-115` 동반 재측정

**완료 기준**:
- `grep -rn "48단계\|total.*48\|active.*40" docs/` → 결과 없음 (또는 archive만)
- `python backend/scripts/dump_step_manifest.py | diff - docs/architecture/_step_manifest.generated.md` → 0

**같이 하지 말 것 (Codex 보강)**:
- step manifest 의미를 바꾸는 **런타임 변경** (W1으로 이월)
- `/analyze` 제거 같은 **API refactor** (W1)

---

### 5.3 F02 — test 수치 "subset vs repo" 분리

**근거**:
- `docs/architecture-refactor-final/test-plan.md:4` → `256 passed` (현행 상태로 서술)
- 같은 파일 `:41` → `375 tests / 8 collection errors` (다른 수치)
- 실제 repo → `637 passed / 56 failed / 2 errors / 1 skipped`

**작업**:
1. `test-plan.md`를 3개 섹션으로 재구성:
   - §1 Refactor subset green (256 passed)
   - §2 Repo 전체 현황 (637p/56f/2e/1s)
   - §3 Failure clusters (4종)
2. `README.md` 품질 뱃지 추가:
   ```markdown
   Status: refactor-subset ✅ (256p)  ·  repo-wide ⚠️ (637p / 56f — W3/W5에서 해소)
   ```
3. `docs/architecture/00-overview.md`에 "저장소 전체 품질 게이트" 섹션

**완료 기준**: 어떤 문서를 봐도 "256 passed"와 "repo 전체"를 혼동하지 않는다.

---

### 5.4 F03 — `architecture-refactor-final` "완료" 과장 조정

**근거**:
- `README.md:4-24` Phase 0~5 전체 완료 / 256 tests passed 강조
- `README.md:81-83` "프런트는 StepRunner만 쓴다" (실제 2-way)
- `README.md:14` "ImageService 5,281→1,224 분리 완료" (실제 1,228 shim 잔존)

**작업**:
1. 상단 배너 추가:
   ```markdown
   > ⚠️ **이 문서 세트는 리팩토링 기록(아카이브)입니다.**
   > 현재 저장소 상태는 `docs/review-codex-1/`와 `docs/architecture/`에서 확인하세요.
   > Wave 별 진행은 `docs/review-codex-1/11-fix-plan.md`에.
   ```
2. `:14` 옆에 `(shim 잔존, W5에서 facade-first 축소 예정)` 병기
3. `:81-83` → "EpisodeDetail은 StepRunner, Episodes는 legacy `/analyze` 병존 — W1에서 단일화"

---

### 5.5 F04 — `06-data-contracts.md` 구시대 서술

**근거**:
- `:13-24` `step_type` "Phase 1 도입 예정" (실제 이미 도입됨)
- `:28-32` `AnalysisService.run_analysis()` (실제 `analysis_service.py` 없음, `analysis_dispatch_service.py`)
- `:36` mutation 파일 수 9개 (실제 7개)

**작업**:
1. `step_type` 섹션: "도입 예정" → "현재 값 분포표"
2. `AnalysisService` → "deprecated 2026-04 (`analysis_dispatch_service`로 대체)"
3. mutation 파일 실제 리스트 (7개): `useAnalyzeEpisode`, `useCreateEpisode`, `useCreateProject`, `useMemberMutations`, `usePlanningDocMutations`, `useShotToggle`, `useStillMutations`
4. Query hook 17개 리스트 (`useActivities`, `useEntities`, `useEpisode`, `useEpisodesList`, `useEpisodesProgress`, `useGenStatus`, `useMembers`, `usePlanningDoc`, `useProgress`, `useProject`, `useProjectList`, `useProjectSummary`, `useScreenplay`, `useStillImages`, `useStills`, `useWorldGuide`, `useWorldRules`)

---

### 5.6 F05 — line-level 증거 자동 검증 또는 제거

**근거**:
- `README.md:81` → `EpisodeDetail.tsx:821` (실제 149 LOC)

**작업**: 옵션 A(권장) — 문서에서 line 숫자 제거, 파일 경로 + symbol name만 남김.

---

### 5.7 F06 — `product-roadmap-discussion.md` "36단계"

**근거**:
- `:11`, `:37` → "36단계" (실제 49)

**작업**:
- "36단계" → "분석 42단계 + 이미지 5단계 + 보조 2단계 (총 49)" 또는 비전문가용으로 "대본 → 씬 → 샷 → 참조 → 이미지" 흐름만

---

### 5.8 F07 — `image_service.py` "완료" 서술 정정

**근거**:
- `architecture-refactor-final/README.md:14` "5,281→1,224 3개 서비스 분리"
- 실제 1,228 shim + reference 1,150 + scene 2,936 = **5,314 LOC (분산 후 총합 유사)**

**작업**:
1. `README.md:14` 교체:
   ```markdown
   - ImageService: 5,281 → 3-way split 진행 중 (image 1,228 + reference 1,150 + scene 2,936 = 5,314)
     - shim 유지: 외부 호출자 호환 (W5 facade-first로 축소 예정)
   ```
2. `docs/architecture/05-image-generation.md`에 "현재 분할 상태" 표 추가

---

### 5.9 Wave 0 Exit (→ G0)

- [ ] 모든 문서 숫자가 `dump_step_manifest.py` 출력과 일치
- [ ] `test-plan.md`가 subset/repo 2종 수치를 분리
- [ ] `architecture-refactor-final`에 아카이브 배너 존재
- [ ] `product-roadmap-discussion.md`에서 "36" 제거
- [ ] `image_service.py`가 "완료"가 아니라 "shim 잔존"으로 서술됨
- [ ] line reference 제거 또는 자동 검증

**병렬 가능**: F01 (generator) + F02~F07 (문서 교정) 전체 병렬. **같이 하지 말 것**: 런타임 코드 변경 금지.

**소요**: 2~3일

---

## 6. Wave 1 — 공개 계약 수렴

### 6.1 목적

> **프런트 분석 시작을 StepRunner로 통합하고, `STEP_MANIFEST` 직접 참조를 `step_catalog` 경유로 모은다.**

### 6.2 권장 순서 (Codex 보강)

1. 새 공식 훅/서비스 추가
2. 호출자 전환
3. legacy shim 축소
4. manifest metadata 승격
5. `steps.py` route/service 분리

### 6.3 F08/F09 — `/analyze` vs `/steps/run-all` 이중 계약

**근거**:
- `frontend/src/pages/EpisodeDetail.tsx:81-85` → `/steps/run-all?category=analysis`
- `frontend/src/pages/Episodes.tsx:46-59` → `useAnalyzeEpisode`
- `frontend/src/hooks/api/mutations/useAnalyzeEpisode.ts:11-13` → legacy `/episodes/{id}/analyze`
- `backend/app/api/v1/episodes.py:135-207` `/analyze` live
- `backend/app/api/v1/episodes.py:210-260` `/reanalyze-scenes` live

**작업**:
1. **프런트 정리** (권장: `useRunAllSteps` 새 훅):
   - `frontend/src/hooks/api/mutations/useRunAllSteps.ts` 신설 (`/steps/run-all?category=analysis` 호출)
   - `EpisodeDetail.tsx`, `Episodes.tsx` 양쪽에서 공유
   - `useAnalyzeEpisode` 삭제
2. **백엔드 shim 축소**:
   - `/analyze` 본문 전부를 `dispatch_category_run(category="analysis")` 한 줄로 교체
   - `@deprecated` 데코레이터 + `Sunset` 헤더
   - `/reanalyze-scenes` 동일 패턴
3. **문서**:
   - `docs/architecture/00-overview.md` "공식 분석 진입점: `/steps/run-all`"
   - `docs/architecture/06-data-contracts.md` deprecated API 분리 표

**완료 기준**:
- `rg "/analyze|/reanalyze-scenes" frontend/src` → 0건
- `rg "useAnalyzeEpisode" frontend/src` → 0건
- 기존 E2E 유지

**소요**: 1.5d

---

### 6.4 F10 — `StepRunner`가 `STEP_MANIFEST` 직접 참조

**근거**:
- `backend/app/core/step_runner.py:21-23` → `from app.core.step_manifest import STEP_MANIFEST, get_depends_on, get_all_downstream_recursive`
- `:44-52` → `self.manifest = STEP_MANIFEST[step_id]`
- `backend/app/core/step_catalog.py:8-10` → "소비자는 STEP_CATALOG만 본다" 선언, 강제 없음

**작업**:
1. `step_catalog.py`에 accessor 추가:
   - `get_step_entry(step_id)` → CatalogEntry (manifest + runner_class)
   - `get_depends_on(step_id)` 프록시
   - `get_all_downstream_recursive(step_id)` 프록시
2. `step_runner.py` import 변경:
   ```python
   from app.core.step_catalog import get_step_entry, get_depends_on, get_all_downstream_recursive
   ```
3. `self.manifest = STEP_MANIFEST[step_id]` → `self.entry = get_step_entry(step_id)`
4. `_validate_step_registration`도 catalog 기반
5. 새 테스트: `test_step_runner_uses_catalog_only.py` (monkeypatch로 STEP_MANIFEST 비우고 동작 확인)

**순환 import 방지**: catalog → manifest 일방향. runner → catalog 일방향.

**소요**: 1d

---

### 6.5 F11 — `steps.py` orchestration hub 축소

**근거**:
- `backend/app/api/v1/steps.py:35-126` `get_all_steps()` → status/gate/blocked_by 직접 계산
- `:253-349` `run_step()` → nested `_run_in_background()` (session 재생성, gate, resume, post-sync)
- `:365-381` `_sync_checkpoints_to_db`, `_get_step_runner` legacy wrapper
- 총 383 LOC의 route 파일이 orchestration hub

**작업**:
1. **read-model 서비스 분리**:
   - `backend/app/services/step_readmodel_service.py` 신설
   - `get_all_steps_view(project_id, episode_id)` → status + gate + blocked_by 계산
2. **background orchestration 분리**:
   - `backend/app/services/step_execution_service.py` 신설
   - `run_single_step(...)`, `run_category(...)` — 기존 nested worker 본문
3. **legacy wrapper 이관**:
   - `_sync_checkpoints_to_db` → `services/checkpoint_sync/`
   - `_get_step_runner` → `step_execution_service`
4. `steps.py` 최종 책임: HTTP 파싱 + 서비스 호출 + response shaping

**완료 기준**: `wc -l steps.py` ≤ 200

**순차 강제 (Codex 보강)**:
- F12(presync metadata) 완료 후 F11 시작 → gate 계산 로직이 metadata 경유하도록

**소요**: 2d

---

### 6.6 F12 — `_needs_presync` 메타 승격

**근거**:
- `backend/app/api/v1/steps.py:307-324` → 하드코딩 tuple
- `step_manifest.py:21-30`에 projection 필요 메타 없음

**작업**:
1. `step_manifest.py` 엔트리에 `requires_projection_sync_before_run: bool` (default False) 필드 추가
2. 현재 7개 step 설정 (scene_director, outlook_extraction, outlook_phase1/2/3, scene_detail, scene_verify)
3. `step_execution_service.run_single_step()` 내부에서 entry 참조
4. `steps.py`에서 tuple 제거
5. `test_step_manifest_v3.py`에 분포 assertion 추가

**리스크 + 대응**:
- 누락된 step에 projection 의존성이 있으면 False로 설정 시 런타임 터짐
- **대응**: projection 의존성 정적 분석 가드 테스트 추가

**소요**: 1d

---

### 6.7 F20/F21 — 모듈 시그니처 drift (W1 묶음)

**F20**: `text_cleaner.clean_text` 제거됨 → `extract_text_from_pdf_llm`만 존재
**F21**: `VariationRecommender(llm_client=...)` → 현재 `project_llm_config`만

**작업**:
1. **F20 (권장: 테스트 갱신)**: `test_text_cleanup.py`를 `extract_text_from_pdf_llm`로 변경
2. **F21 (권장: DI path 복원)**: `VariationRecommender.__init__(self, project_llm_config, *, llm_client=None)` keyword optional 복원

**소요**: 0.6d

---

### 6.8 Wave 1 Exit (→ G1)

- [ ] 프런트에서 `/analyze`, `/reanalyze-scenes` 경로 0건
- [ ] `STEP_MANIFEST` 직접 import가 step_runner/steps.py에 0건
- [ ] `_needs_presync` tuple 제거, manifest 필드 승격
- [ ] `steps.py` ≤ 200 LOC
- [ ] `B1-core` lane green

**병렬 가능**:
- 프런트 call-site 전환 (F08/F09) + 백엔드 `step_catalog` accessor 보강 (F10) — write set 분리
- F20, F21은 언제든 병렬

**같이 하지 말 것**:
- `main.py` lifespan refactor (W2)
- image response shape 변경 (W5)

**소요**: 4~6d

---

## 7. Wave 2 — startup/config 경계 분리

### 7.1 목적 (수정)

> **import와 초기화, 개발 편의와 운영 정책을 분리한다.**
> **중요**: v1의 "repo-wide pytest green" 완료 조건은 **제거**. full green은 G5로 이동.

### 7.2 F13/F14 — `init_db()` + lifespan 전환

**근거**:
- `backend/app/main.py:39` 모듈 top-level `init_db()`
- `backend/app/main.py:45` `@app.on_event("startup")` (FastAPI deprecated)
- `backend/tests/test_pipeline_e2e.py:10-15` 전역 `app` import → `TestClient(app)` → startup 실행 → `user_account` 부재 에러

**작업**:
1. `main.py:39` `init_db()` 제거
2. `lifespan` 컨텍스트 매니저:
   ```python
   @asynccontextmanager
   async def lifespan(app: FastAPI):
       init_db()
       await _ensure_default_user()
       yield
   app = FastAPI(lifespan=lifespan)
   ```
3. `@app.on_event("startup")` 제거
4. 테스트 fixture가 명시적 DB 초기화를 수행하도록

**완료 기준**:
- `python -c "import app.main"` 후 DB 파일 생성 안 됨
- `test_pipeline_e2e.py` 전체 suite 통과

**소요**: 1d (F14 포함)

---

### 7.3 F15 — default-user bootstrap 분리

**작업**:
1. `_ensure_default_user()` 로직을 `backend/scripts/bootstrap_default_users.py` CLI로 이동
2. `main.py`에서 호출 제거
3. `README.md` "개발 환경 초기 설정" 섹션:
   ```bash
   python backend/scripts/bootstrap_default_users.py --dev
   ```
4. **프로덕션 가드**: `ENVIRONMENT=prod`일 때 bootstrap 실패

**소요**: 1d

---

### 7.4 F16 — `ConfigDict` 전환

**작업**: `class Config:` → `model_config = ConfigDict(...)` (Pydantic v2)

**소요**: 0.5d

---

### 7.5 F17 — insecure defaults 정책

**근거**:
- `backend/app/core/config.py:10` `secret_key = "dev-secret-key"`
- `:15-17` `admin123`, `creator123`

**작업**:
1. 기본값 제거 또는 validator로 강제:
   ```python
   @field_validator("secret_key")
   @classmethod
   def _validate_secret(cls, v, info):
       env = info.data.get("environment", "dev")
       if env == "prod" and v in ("", "dev-secret-key", None):
           raise ValueError("SECRET_KEY must be set in production")
       return v or "dev-secret-key-only-for-local"
   ```
2. `ADMIN_PASSWORD`, `CREATOR_PASSWORD` 동일 처리
3. `.env.example` 신설
4. `.gitignore`에 `.env` 확인

**순차 강제 (Codex 보강)**: prod fail-fast 병합 **전 CI/dev env 확인**. CI 설정을 먼저 `.env`로 이전하지 않으면 CI red.

**소요**: 1d

---

### 7.6 Wave 2 Exit (→ G2)

- [ ] `python -c "import app.main"`에서 DB 파일 생성 안 됨
- [ ] `on_event("startup")` 0건
- [ ] `bootstrap_default_users.py` CLI 스크립트 존재
- [ ] `config.py`가 ConfigDict 사용
- [ ] `ENVIRONMENT=prod`일 때 insecure defaults 거부
- [ ] **`B2-startup` lane green** (B4-full은 W5까지 이월)

**병렬 가능**: F16, bootstrap CLI 작성은 병렬.
**순차 강제**: lifespan 전환 후 fixture 정리.

**같이 하지 말 것**:
- `steps.py` refactor (W1)
- image 서비스 분해 (W5)

**소요**: 3~4d

---

## 8. Wave 3 — 테스트 lane 정규화 + 프런트 안전망 (신규 Wave)

### 8.1 목적 (Codex 보강 핵심)

> **테스트 실패를 해석 가능한 lane으로 나누고, 대형 refactor(W5/W6)를 지탱할 최소 안전망을 만든다.**

이 wave는 원본 v1에 없던 개념이다. v1에서는 F32(Vitest 도입)를 P5 마지막에 두었으나, **harness 없이 `SceneVariationCard.tsx`(1,606 LOC)나 `scene_image_service.py`(2,936 LOC)를 분해하면 회귀 측정이 불가능**하다는 Codex 판단에 따라 Wave 3를 신설.

### 8.2 F19 재분류 (cluster별)

| Cluster | 범위 | Wave | 예상 공수 |
|---------|------|------|-----------|
| **A** | step count/order/manifest drift (`test_step_manifest_v3`, `test_pipeline_v3_e2e`, `test_sync_v3`) | W3 | 1d |
| **B** | scene/outlook/entity legacy contract (`test_scene_director_v2`, `test_scene_verify_v2`, `test_outlook_v2`, `test_entity_extract_v4`, `test_entity_filter`, `test_entity_review_v4`, `test_scene_dependency_v2`, `test_scene_steps_v3`, `test_scene_detail_v3`) | W3 | 1.5d |
| **C** | text/variation constructor drift (`test_text_cleanup`, `test_variation_pipeline`) | W3 | 0.5d (F20/F21 후) |
| **D** | image API/response shape drift (`test_images_api`, `test_entities_api`, `test_variation_pipeline` 이미지 부분, `test_scene_image_service`, `test_reference_image_service`) | **W5로 이관** | 1d |

**접근법 (옵션 C, 원본 유지)**: 선택적 갱신 + 삭제. 각 테스트의 실제 책임 식별 → 현재 구조에 맞는 subset만 재작성, 나머지 삭제.

---

### 8.3 F29 — `useEventSource.ts` 정리

**근거**:
- 정의 있지만 호출자 없음 (미사용)
- `:7-20` set-state-in-effect 위반

**작업**: 삭제 (옵션 A, 권장). 필요 시 재작성이 쉬움.

**소요**: 0.3d

---

### 8.4 F30 — `ImageGalleryModal.tsx` set-state-in-effect

**근거**: `:88-94` 동일 lint rule 위반

**작업**:
- `useEffect` 분리: sync state는 `useMemo` 또는 event-driven으로 전환
- 또는 `useLayoutEffect` + flag로 초기화 1회만

**소요**: 0.3d

---

### 8.5 F32 — Vitest + RTL smoke lane 도입 (**W3으로 앞당김**)

**작업**:
1. `frontend/package.json` devDeps:
   - `vitest`, `@testing-library/react`, `@testing-library/jest-dom`, `jsdom`
2. `frontend/vitest.config.ts` (vite.config.ts 공유)
3. `frontend/src/test-setup.ts`
4. **W3 smoke test 4개** (F1-smoke):
   - `Dashboard.test.tsx`
   - `Episodes.test.tsx`
   - `EpisodeDetail.test.tsx`
   - `EpisodeStills.test.tsx`
5. **W3 hook test 2개** (F2-hooks):
   - `useStillMutations.test.ts`
   - `useRunAllSteps.test.ts` (W1에서 만든 훅)
6. `package.json`에 `"test": "vitest"` 추가
7. CI에 `npm run test` lane

**Wave 6 확장**: hook/component 테스트 확장 (별도 항목 F32-확장)

**소요**: 2d

---

### 8.6 Wave 3 Exit (→ G3)

- [x] **lane 분리** (`backend/pytest.ini` + auto-marking, W3-1, 2026-04-22) — core/startup/image/full/pg 5종. Baseline 647p/49f/1s.
- [x] `B1-core` lane green (W1에서 달성, W3-3 반영 후 262p/5s/0f)
- [x] `B2-startup` lane green (W2에서 달성)
- [x] backend F19 cluster A/B/C 정리 완료 (W3-2/3/4, cluster D는 W5 이관)
- [x] `F1-smoke`, `F2-hooks` lane 최소 smoke green (W3-5 Vitest + RTL — Dashboard 1 + useRunAllSteps + useStillMutations, 8 tests)
- [ ] `useEventSource.ts` 삭제 또는 통합
- [ ] `ImageGalleryModal.tsx` set-state-in-effect 0

**Gate G3의 의미**: **내부 팀이 기능을 계속 쓰면서 refactor를 이어갈 수 있는 상태.** 대형 W5/W6를 시작해도 됨.

**병렬 가능**:
- backend test cluster 정리 + frontend test harness 구축 (write set 완전 분리)
- F29/F30 저위험 lint blocker 제거는 언제든 병렬

**같이 하지 말 것**:
- 대형 frontend hotspot 분해 (W6)
- image service extraction (W5)

**소요**: 5~7d

---

## 9. Wave 4 — projection/sync 관측성

### 9.1 목적

> **checkpoint 성공과 DB projection 실패를 운영에서 구별 가능하게 만든다.**

### 9.2 작업 (원본 P3 전체)

**근거**:
- `backend/app/services/checkpoint_sync/scene_still_sync_service.py` 398 LOC — projection 병목
- 현재: sync 실패 시 로그만, UI 미반영

**작업**:
1. **P3-1. `SceneStillSyncService` 3-way 분해**:
   - `checkpoint_loader.py` — checkpoint manifest.json → in-memory
   - `scene_still_normalizer.py` — source of truth 결정
   - `scene_still_writer.py` — DB UPSERT (DELETE→INSERT 금지 제약 유지)
2. **P3-2. sync 실패 surface**:
   - `orchestrate_full_sync()` 실패 시 `StepRun.sync_status = 'failed'` + error detail 저장
   - 프런트 `StepRunStatusBadge` 컴포넌트
3. **P3-3. repair endpoint**:
   - `POST /api/v1/projects/{pid}/episodes/{eid}/repair-projection`
   - step별 sync 재실행 (checkpoint 보존, DB만 재UPSERT)
4. **P3-4. partial/projection 상태 UI**:
   - `PipelineStepsPanel.tsx`에 `sync_status` 뱃지

**완료 기준**:
- 사용자가 sync 실패를 UI로 인지 가능
- `scene_still_sync_service.py` ≤ 150 LOC (3-way)

**같이 하지 말 것**: image domain 대형 분해와 동일 PR (W5)

**소요**: 3~4d

---

## 10. Wave 5 — 이미지 도메인 2차 분해 (Facade-first)

### 10.1 목적 (Codex 보강 핵심)

> **이미지 도메인의 공식 진입점을 먼저 고정하고, 이후 내부 구현을 분리한다.**

### 10.2 v1 "5-way big-bang" → v2 "Facade-first 3단계"

원본 v1은 `scene_image_service.py`를 한 번에 5개 서비스로 쪼개려 했다. Codex 판단: **회귀 해석이 불가능**. 수정된 순서:

```
1. F25 dead code 제거 (무리스크)
2. facade 인터페이스 고정 (scene_image_service의 공식 메서드 시그니처 freeze)
3. variation / validation / persistence / provenance 내부 이동
4. image_service.py shim 호출자 정리
5. reference_image_service.py generation / edit / persistence 분리
6. image lane (B3) 테스트 재고정 + response shape 재freeze
7. F19 cluster D 정리
```

### 10.3 F25 — `image_steps.py` dead code 제거

**근거**:
- `backend/app/core/steps/image_steps.py:52-61` → `orchestrate_full_sync()` 호출 직후 `return`
- `:63` 이후 unreachable (엔티티/씬/아웃룩 sync 구현)

**작업**: `:63` 이후 블록 전체 삭제 (history 보존)

**소요**: 0.3d

---

### 10.4 F22 — `scene_image_service.py` 2,936 LOC 분해

**Facade-first 단계**:

**Phase A (facade 고정)**:
- `scene_image_service.py`의 공식 공개 메서드 리스트 확정
- 호출자 전수 검사: `rg "scene_image_service\.|SceneImageService\(" backend/ frontend/`
- 각 공식 메서드의 인터페이스 docstring 강화 + signature freeze

**Phase B (내부 이동)** — 5 모듈로 분리:
1. `scene_variation_service.py` — variation 생성/선택
2. `scene_validation_service.py` — T2I 검증, retry 전략
3. `scene_persistence_service.py` — DB UPSERT, still 동기화
4. `scene_provenance_service.py` — PNG metadata, 이력 추적
5. `scene_image_service.py` — facade 유지 (얇은 래퍼)

**Phase C (shim 제거 + 테스트 재고정)**:
- `image_service.py`의 shim 호출자가 모두 신규 모듈로 마이그레이션됐는지 확인
- F19 cluster D (image API shape) 동시 정리
- `B3-image` lane 재작성

**완료 기준**:
- 각 신규 서비스 ≤ 500 LOC
- `scene_image_service.py` ≤ 300 LOC (facade)
- 기존 API 호환 유지
- `B3-image` lane green

**리스크**: **높음**. 단계별 PR 필수. E2E (금월도 ep1) 각 Phase 후 실행.

**소요**: 5d

---

### 10.5 F23 — `image_service.py` shim 축소

**근거**:
- 1,228 LOC, 헤더 `:1-6` "helper shim" 선언
- 내부 `:199`, `:418`, `:576`, `:1223` "Phase 3b.2 shim" 주석

**작업**:
1. 메서드 책임 분류:
   - 관리/조회 → facade 유지
   - Phase 3b.2 shim → 호출자 업데이트 후 삭제
2. 호출자 전수 검사: `rg "ImageService\." backend/` + `rg "from.*image_service import"`
3. shim 삭제 가능한 메서드부터 제거
4. 최종 ≤ 400 LOC 목표

**순차 강제**: F22 facade 고정 후 시작 (shim과 신규 facade 중복 호출 가능성)

**소요**: 3d

---

### 10.6 F24 — `reference_image_service.py` 분해

**작업 — 3-way split**:
- `ref_generation_service.py` — 생성 로직
- `ref_editing_service.py` — angle/color/outfit 편집
- `ref_persistence_service.py` — DB 저장
- `reference_image_service.py` — facade

**병렬 가능**: F22와 write set 분리됨 (다른 파일).

**소요**: 3d

---

### 10.7 F19-D — image API/response shape drift

**작업**:
- `test_images_api.py`, `test_entities_api.py`, `test_variation_pipeline.py`의 이미지 부분을 W5 신규 facade에 맞게 재작성
- still/image response shape를 명시적 Pydantic model로 freeze

**소요**: 1d

---

### 10.8 Wave 5 Exit (→ G5)

- [ ] 이미지 도메인 각 파일 ≤ 500 LOC (facade 포함 총 8~9 파일)
- [ ] `image_steps.py` dead code 0
- [ ] 이미지 생성 E2E (금월도 ep1) 성공 — 96장 수준 유지
- [ ] `B3-image` lane green
- [ ] **`B4-full` lane green** (drift cluster D 포함 전체 0 failed)

**Gate G5의 의미**: **대형 backend 구조 변경 종료. silent stale 없는 상태.**

**병렬 가능**:
- F24 (reference 분리) + F22 Phase C의 image lane 테스트 갱신

**순차 강제**:
- facade 고정(F22 Phase A) 전에 호출자 대규모 정리 금지
- response shape freeze 전 frontend variation editor 변경 금지

**같이 하지 말 것**:
- `SceneVariationCard.tsx` 분해 (W6)
- `/images` API 응답 스키마 변경과 call-site refactor 동일 PR

**소요**: 12~15d

---

## 11. Wave 6 — 프런트 hotspot + 성능 마감

### 11.1 목적

> **test harness(W3)가 있는 상태에서 대형 파일과 정적 품질 부채를 마감한다.**

### 11.2 권장 순서 (Codex 보강)

1. **test harness가 있는 상태에서** hotspot 분해
2. 타입/에러 통합
3. route/component split
4. 최종 lint 정리

### 11.3 F26 — `SceneVariationCard.tsx` 1,606 LOC 분해

**작업 — 4-way split**:
1. `SceneVariationCard.tsx` — 컨테이너 + state 조율 (≤ 300 LOC)
2. `components/variation/VariationGallery.tsx`
3. `components/variation/VariationPromptEditor.tsx`
4. `components/variation/VariationDependencyEditor.tsx`

**완료 기준**:
- 각 ≤ 400 LOC
- Props 인터페이스 명시 (any 제거)
- Vitest + RTL golden path 커버

**순차 강제**: W3 harness 완료 후 시작.

**소요**: 3d

---

### 11.4 F27 — `Entities.tsx` 정리

**작업**:
- 3-way split: `EntitiesHeader`, `EntitiesTable`, `EntitiesActionBar`
- `useEntitiesActions` 커스텀 훅
- any 5 → 타입 정의
- catch 15 → `handleError(err, context)` helper

**소요**: 2d

---

### 11.5 F28 — `EpisodeStills.tsx` 정리

**작업**:
- any 21 → `types/episode.ts` 확장 + 타입 가드
- catch 17 → mutation 계층 통합 (`handler fetchStills` 기존 패턴 활용)

**소요**: 2d

---

### 11.6 F31 — code splitting

**작업**:
1. `App.tsx` 라우트 `React.lazy`
2. `SceneVariationCard`, `ImageGalleryModal` dynamic import
3. Vite `manualChunks` (vendor/react-query/editor 분리)

**완료 기준**: 초기 chunk ≤ 400 kB

**소요**: 1d

---

### 11.7 F32-확장 — hook/component 테스트 확장

**작업**: W3 smoke 6개 → hook/component 10+개로 확장

**소요**: 1d

---

### 11.8 Wave 6 Exit (→ G6)

- [ ] `SceneVariationCard.tsx` ≤ 400 LOC
- [ ] `Entities.tsx`, `EpisodeStills.tsx` any/catch hotspot 해소
- [ ] `npm run lint` → 0 errors
- [ ] 초기 bundle ≤ 400 kB
- [ ] Vitest test 10+개 green

**Gate G6의 의미**: **파일럿/외부 사용 검토가 가능한 상태.**

**병렬 가능**: F27 + F28 (컴포넌트 write set 분리) + F31 + F32-확장

**같이 하지 말 것**:
- backend image response shape 변경 (W5)
- startup/config 변경 (W2)

**소요**: 6~8d

---

## 12. Wave 7 — 제품/운영 고도화

### 12.1 목적

> **기술적 동작을 넘어 "현업이 신뢰하고 쓸 수 있는 도구"로.** 인터뷰 후 구체화.

### 12.2 성격 (Codex 보강)

- **delivery-critical path가 아님**
- 인터뷰/운영 피드백 기반 backlog

### 12.3 작업 후보 (예비)

1. **Provenance timeline**:
   - `backend/app/modules/provenance.py` (288 LOC) 이미 존재 → UI 연결
   - still별 prompt 변화, LLM model, retry count, moderation 차단 로그
2. **Snapshot/Restore UX**:
   - `snapshot_service.py` (202 LOC) 이미 존재 → UI 타임라인
3. **HiTL 작업자 중심 재설계**:
   - JSON 편집 → 이미지 위 드로잉, 자연어 지시
4. **다중 에피소드 연속성**:
   - 프로젝트 단위 `EntityCanon` 확장

**소요**: 인터뷰 결과 의존 (별도 로드맵)

---

## 13. 작업 의존성 그래프 (v2 Wave 기반)

```mermaid
flowchart TD
    subgraph W0["Wave 0 · 사실표 (2~3d)"]
        F01[F01 step 숫자]
        F02[F02 test 수치]
        F03[F03 완료 과장]
        F04[F04 data-contracts]
        F05[F05 line refs]
        F06[F06 36단계]
        F07[F07 image service]
    end

    subgraph W1["Wave 1 · 공개 계약 (4~6d)"]
        F08[F08 analyze 이중화]
        F09[F09 useAnalyzeEpisode]
        F10[F10 StepRunner catalog]
        F11[F11 steps.py route]
        F12[F12 presync metadata]
        F20[F20 clean_text]
        F21[F21 VariationRecommender]
    end

    subgraph W2["Wave 2 · startup (3~4d)"]
        F13[F13 init_db]
        F14[F14 lifespan]
        F15[F15 bootstrap]
        F16[F16 ConfigDict]
        F17[F17 insecure defaults]
    end

    subgraph W3["Wave 3 · 안전망 (5~7d, 신규)"]
        F19A[F19-A step manifest drift]
        F19B[F19-B scene/outlook/entity]
        F19C[F19-C text/variation]
        F29[F29 useEventSource]
        F30[F30 ImageGalleryModal]
        F32[F32 Vitest + RTL]
    end

    subgraph W4["Wave 4 · projection (3~4d)"]
        P3all[P3 전체]
    end

    subgraph W5["Wave 5 · image (12~15d)"]
        F25[F25 dead code]
        F22[F22 scene facade-first]
        F23[F23 image_service shim]
        F24[F24 reference 3-way]
        F19D[F19-D image API drift]
    end

    subgraph W6["Wave 6 · FE hotspot (6~8d)"]
        F26[F26 SceneVariationCard]
        F27[F27 Entities]
        F28[F28 EpisodeStills]
        F31[F31 code splitting]
        F32ext[F32 확장]
    end

    subgraph W7["Wave 7 · product (TBD)"]
        prov[Provenance]
        snap[Snapshot UX]
        hitl[HiTL 작업자]
        multi[다중 에피소드]
    end

    W0 -->|G0| W1
    W1 -->|G1| W2
    W2 -->|G2| W3
    W3 -->|G3| W4
    W3 -.->|G3| W5
    W4 -->|G4| W5
    W5 -->|G5| W6
    W6 -->|G6| W7

    F10 --> F11
    F12 --> F11
    F13 --> F14
    F13 --> F15
    F20 --> F19C
    F21 --> F19C
    F32 -.harness 필수.-> F26
    F25 --> F22
    F22 --> F23
    F22 --> F19D

    style W3 fill:#fff9c4,stroke:#f9a825
    style W5 fill:#ffcdd2,stroke:#c62828
```

---

## 14. PR 분할 권장안 (Codex 보강)

원본 v1은 Sprint 단위로 묶었으나, Codex 보강은 PR 14개로 세분화. 리뷰/회귀 확인 단위로 안전.

| PR | 범위 | Gate | Wave |
|----|------|------|------|
| **PR-01** | Wave 0 전체: manifest generator + 문서 교정 | G0 | W0 |
| **PR-02** | frontend 분석 시작 단일화 (`useRunAllSteps` + call-site 전환) | Wave 1 일부 | W1 |
| **PR-03** | `step_catalog` accessor 보강 + StepRunner direct import 제거 + F20/F21 | Wave 1 일부 | W1 |
| **PR-04** | `_needs_presync` metadata + `steps.py` service 분리 | **G1** | W1 |
| **PR-05** | lifespan 전환 + import-time side effect 제거 | Wave 2 일부 | W2 |
| **PR-06** | bootstrap CLI + `ConfigDict` + prod fail-fast | **G2** | W2 |
| **PR-07** | backend test lane 분리 + F19 cluster A/B/C 정리 | Wave 3 일부 | W3 |
| **PR-08** | frontend `vitest`/RTL + F29/F30 | **G3** | W3 |
| **PR-09** | projection sync status/repair API + UI 배지 | **G4** | W4 |
| **PR-10** | F25 dead code 제거 + image facade scaffolding | Wave 5 일부 | W5 |
| **PR-11** | `scene_image_service` 내부 추출 | Wave 5 일부 | W5 |
| **PR-12** | `reference_image_service` 내부 추출 + F19 cluster D | **G5** | W5 |
| **PR-13** | frontend hotspot 분해 (F26/F27/F28) | Wave 6 일부 | W6 |
| **PR-14** | code splitting + lint 마감 + F32 확장 | **G6** | W6 |

---

## 15. 병렬 실행 지침 (Codex 보강)

### 15.1 원칙

병렬 작업은 "기능적으로 독립"이 아니라 **write set 분리**로 판단.

### 15.2 권장 병렬 묶음

| 묶음 | Wave | 허용 이유 |
|------|------|-----------|
| 문서 교정 + manifest generator | W0 | 런타임 코드와 충돌 없음 |
| FE call-site 전환 + BE catalog accessor 보강 | W1 | 파일 셋 겹침 거의 없음 |
| config/bootstrap 정리 + startup fixture 정리 | W2 | 관련성 높지만 write set 분리 가능 |
| BE test cluster 정리 + FE test harness | W3 | repo 하위 트리 완전 분리 |
| BE sync 가시화 + FE badge | W4 | 계약 고정 후 병렬 가능 |
| reference 분리 + image lane 테스트 갱신 | W5 | facade 고정 이후 가능 |
| `Entities.tsx` + `EpisodeStills.tsx` | W6 | 컴포넌트 write set 분리 |

### 15.3 병렬 금지 묶음

| 금지 | 이유 |
|------|------|
| `steps.py` refactor + `main.py` lifecycle | 실패 원인 분리 불가 |
| image facade 변경 + FE variation editor 분해 | 응답 스키마와 UI 둘 다 흔들림 |
| startup fail-fast + CI 환경 미정 | red 원인 판별 어려움 |
| legacy shim 삭제 + 호출자 마이그레이션 미완료 | 즉시 회귀 |

---

## 16. 스프린트 구성 (Wave 기반 권장)

### Sprint 1 (1주) — W0 + W1 시작
- D1: PR-01 (Wave 0 전체)
- D2-D5: PR-02 (`useRunAllSteps` + 전환), PR-03 병렬

### Sprint 2 (1.5주) — W1 완료 + W2 시작
- D6-D8: PR-04 (`_needs_presync` + steps.py 분리)
- D9-D10: PR-05 (lifespan)
- D11-D12: PR-06 (bootstrap + ConfigDict + prod fail-fast)

### Sprint 3 (1.5주) — W3 전체
- D13-D15: PR-07 (test lane + F19 A/B/C) + PR-08 (vitest + F29/F30) **병렬**

### Sprint 4 (1주) — W4
- D16-D19: PR-09

### Sprint 5 (2주) — W5
- D20: PR-10 (dead code + facade scaffolding)
- D21-D27: PR-11 (scene 내부 추출)
- D28-D32: PR-12 (reference + cluster D)

### Sprint 6 (1.5주) — W6
- D33-D35: PR-13 (FE hotspot, F27+F28 병렬)
- D36-D37: PR-14 (code splitting + lint 마감)

### Sprint 7+ — W7 (인터뷰 기반)

**총 소요**: ~35~47d (실제 업무일 기준 7~10주)

---

## 17. 리스크 종합 + Rollback 기준

### 17.1 높은 리스크 작업 + Rollback

| 작업 | 리스크 | Rollback 기준 (Codex 보강) |
|------|--------|---------------------------|
| F13 (init_db 제거) | 테스트 fixture 전반 영향 | `test_pipeline_e2e` single run green 유지 시만 진행 |
| F22 (scene facade-first) | 이미지 파이프라인 regression | 각 Phase 후 금월도 E2E 통과 시만 다음 Phase |
| F17 (prod fail-fast) | CI red 유발 | CI env에 SECRET_KEY 사전 이전 전까지 병합 금지 |
| F19 cluster B | 30건 회귀 보호막 일시 감소 | cluster별 PR 분리, 각 PR 리뷰 |

### 17.2 일반 리스크 관리

1. **듀얼 리뷰**: 대형 리팩토링 PR (PR-04, PR-11, PR-12, PR-13)은 Claude + Codex 병행
2. **E2E 회귀 체크**: 각 Gate 통과 시점 금월도 full analysis + image generation
3. **문서 동기화**: 각 PR 완료 시 `docs/architecture/` 해당 섹션 업데이트
4. **보호 규칙**: `data/service.sqlite`, `projects/` 삭제 금지 (기존)

---

## 18. 검증 계획

### 18.1 각 Gate 자동 검증

```bash
# G0 (Wave 0)
python backend/scripts/dump_step_manifest.py > /tmp/manifest.md
diff /tmp/manifest.md docs/architecture/_step_manifest.generated.md
! rg "36단계|48단계|total.*48" docs/

# G1 (Wave 1)
! rg "/analyze\s*['\"\)]|/reanalyze-scenes" frontend/src
! rg "STEP_MANIFEST" backend/app/core/step_runner.py backend/app/api/v1/steps.py
! rg "_needs_presync" backend/
[ $(wc -l < backend/app/api/v1/steps.py) -le 200 ]
backend/.venv/bin/python -m pytest backend/tests/test_step_manifest_v3.py backend/tests/test_step_catalog.py -q  # B1-core

# G2 (Wave 2)
python -c "import app.main" && ! ls data/service.sqlite.tmp 2>/dev/null
backend/.venv/bin/python -m pytest backend/tests/test_pipeline_e2e.py -q  # B2-startup
! python -m uvicorn app.main:app --env-file prod.env 2>&1 | grep -q "started"  # prod fail-fast

# G3 (Wave 3)
backend/.venv/bin/python -m pytest backend/tests -q -m "core or startup" --no-cov  # lane
cd frontend && npm run test  # F1-smoke, F2-hooks
cd frontend && npm run lint | grep -c "set-state-in-effect" # → 0

# G4 (Wave 4)
# API 호출 테스트: sync 실패 시 sync_status='failed' 반영

# G5 (Wave 5)
for f in backend/app/services/scene_*.py backend/app/services/reference_*.py backend/app/services/image_service.py; do
  lines=$(wc -l < "$f"); echo "$lines $f"
  [ "$lines" -le 500 ] || echo "OVER: $f"
done
backend/.venv/bin/python -m pytest backend/tests -q  # B4-full → 0 failed

# G6 (Wave 6)
cd frontend && npm run lint  # 0 errors
cd frontend && npm run test  # 10+ green
cd frontend && npm run build  # 초기 chunk ≤ 400 kB
```

### 18.2 E2E 검증

금월도 프로젝트 (`9abb6c79-08a6-4777-9fed-99c0c3f410cc`):
- G1 이후: analysis force 재실행 → `/steps/run-all` 경유 확인
- G3 이후: frontend smoke로 모든 주요 페이지 렌더 확인
- G5 이후: 이미지 96장 재생성 (모더레이션 5회 차단 자동 복구 포함)

---

## 19. 최우선 착수 순서 (Codex 보강)

### 19.1 바로 시작할 6개 작업

1. **F01~F07 (W0)** — `G0` 달성 (2~3d)
2. **F08/F09** — frontend `/analyze` call-site를 `/steps/run-all`로 수렴
3. **F10/F12** — `StepRunner`/`steps.py`의 `STEP_MANIFEST` 직접 소비 축소 + `_needs_presync` metadata화
4. **F13/F14** — `main.py` import-time side effect 분리 + lifespan 전환
5. **backend test lane 분리** — `core/startup/image/full` 네 개
6. **F32 + F29/F30** — `vitest`/RTL 조기 도입 + 저위험 lint blocker 제거

### 19.2 아직 시작하면 안 되는 4개

1. **F22** `scene_image_service.py` 5-way split (G3 이전 금지)
2. **F26** `SceneVariationCard.tsx` 대형 분해 (G3 이전 금지 — harness 필수)
3. **image API response shape 재설계** (W5 facade 고정 후)
4. **W7 제품 UX 확장** (G6 이전 의미 없음)

각각 의미는 있지만 **G3 이전에 시작하면 회귀 비용이 커진다**.

---

## 20. 최종 권고 (Codex 보강 반영)

원본 v1의 F01~F32 문제 분류는 유지하되, 실제 실행은 **Wave 0~7 + Gate G0~G6** 프레임으로 관리.

핵심 5문장:
1. 원본의 F01~F32 분류는 유지한다.
2. 운영상 실제 실행은 P0~P6가 아니라 **Wave 0~7과 G0~G6**로 관리한다.
3. **`repo-wide pytest green`은 조기 목표가 아니라 G5에 배치**한다 (image cluster drift는 W5 전에 닫히지 않음).
4. **Frontend는 테스트 harness(W3)와 구조 위반 제거를 먼저 시작**해야 한다.
5. **이미지 도메인 분해는 facade-first 없이 크게 들어가면 실패 확률이 높다** (F22 3-Phase).

---

## 21. 문서 관리

### 21.1 이 문서의 위치

- 파일: `docs/review-codex-1/11-fix-plan.md`
- 버전: v2 (2026-04-21 통합판, Codex 보강 반영)
- 병행 자료: `docs/review-codex-1/11-fix-plan-codex.md` (Codex 리뷰 원본, 보존)

### 21.2 관련 문서

- **근거**: `docs/review-codex-1/01~10`
- **현재 아키텍처**: `docs/architecture/` (W0 완료 후 최신화)
- **리팩토링 기록**: `docs/architecture-refactor-final/` (W0-F03으로 아카이브 배너)
- **제품 논의**: `docs/product-roadmap-discussion.md` (W0-F06 적용 후)

### 21.3 작업 추적

각 F## 이슈는 별도 이슈/태스크로 관리. 본 문서는 전체 맥락 + 의존성 맵.

---

## 22. 참고: 언급되지 않았으나 관련된 이슈

| 항목 | 위치 | Wave 후보 |
|------|------|-----------|
| `entities.py` 970 LOC | `backend/app/api/v1/entities.py` | W5 후 검토 |
| `export_service.py` 925 LOC | `backend/app/services/export_service.py` | 별도 축 |
| `scene_extractor_v2.py` 954 LOC | `backend/app/modules/pipeline/` | 별도 축 |
| `detail_steps.py` 916 LOC | `backend/app/core/steps/` | W5 유사 분해 |
| `analysis_steps_legacy.py` 984 LOC (legacy) | — | 삭제 후보 |
| `entity_steps.py` 834 LOC | — | 별도 축 |

### 현재 프로젝트의 구조적 장점 (보존)

1. **Checkpoint canonical + DB projection** 분리
2. **Gemini + GPT 라우팅** (scene_director Gemini 필수)
3. **Prompt 버전 누적** + _archive 분리 정책
4. **저빈도 엔티티 skip** (DB 기반)
5. **Snapshot API** (복원 가능성)
6. **모더레이션 재시도 전략**

---

_이 계획서는 Codex 리뷰(2026-04-21) 10개 문서 + Codex 보강 피드백(`11-fix-plan-codex.md`)을 통합해 작성. 실측 수치는 2026-04-21 기준이며, 작업 시작 전 재측정 권장._
