# 리팩토링 진행 기록 (Progress Log)

최근 갱신: 2026-04-18 (Phase 3b.6 추가)
브랜치 히스토리: `shot-more` → `refactor/phase-0-*` → `phase-1-*` → `phase-2-*` → `phase-3-image-domain` → `phase-4-legacy-convergence` → `phase-3b-image-service-split` (현재)

---

## 누적 정량 결과 (2026-04-18 기준)

| 파일 | 초안 (Phase 0 시작) | 현재 | Δ |
|---|---|---|---|
| `backend/app/api/v1/steps.py` | 1,518 | **376** | **-1,142 (-75%)** |
| `backend/app/api/v1/episodes.py` | 407 | **392** | -15 |
| `backend/app/core/steps/detail_steps.py` | 1,021 | **885** | -136 (Phase 3b.6에서 closure unpacking 제거 + dead code 제거) |
| `backend/app/core/steps/image_steps.py` | 746 | **730** | -16 |
| `backend/app/services/image_service.py` | 5,281 | **1,224** | **-4,057 (-77%)** (Phase 3b.1 + 3b.2 + 3b.3 + 3b.5 part1 + 3b.5b) |
| `backend/app/core/step_manifest.py` | 612 | 734 | +122 (필드 확장) |
| `backend/app/core/step_runner.py` | 343 | 354 | +11 |
| `backend/app/services/analysis_service.py` | 1,137 | **0** | **-1,137 (Phase 4.2 삭제)** |
| `backend/app/services/analysis_dispatch_service.py` | (없음) | **387** | +387 (Phase 4.1 신규) |
| `backend/app/services/prompt_service.py` | (없음) | **365** | +365 (Phase 3b.1 신규) |
| `backend/app/services/reference_image_service.py` | (없음) | **1,150** | +1,150 (Phase 3b.2 신규) |
| `backend/app/services/image_service_helpers.py` | (없음) | **162** | +162 (Phase 3b.2 신규) |
| `backend/app/services/scene_image_service.py` | (없음) | **2,929** | +2,929 (Phase 3b.3 신규) |
| `backend/app/services/fal_angle_helpers.py` | (없음) | **241** | +241 (Phase 3b.5 신규) |

**테스트**: 0 passed → **256 passed**
- Phase 0~1: 65 (manifest/catalog/applicability/checkpoint_io)
- Phase 2: 30 (sync services / shot_selection / snapshot / settings / api_endpoint decorator)
- Phase 3 부분: 8 (scene_analysis dto / loader)
- Phase 4.1: 26 (analysis_dispatch_service)
- Phase 4.3: 15 (outlook_dedup)
- Phase 4.4: 6 (relation delta sync)
- Phase 4.5: 4 (outlook delta sync)
- Phase 3b.1: 26 (prompt_service)
- Phase 3b.2: 34 (reference_image_service 23 + image_service_helpers 11) — Phase 3b.5b에서 wrapper delegation 7건 제거
- Phase 3b.3: 15 (scene_image_service body-level) — Phase 3b.5b에서 wrapper delegation 13건 제거
- Phase 3b.5b: 14 (signature 보존 테스트 — scene 9 + reference 5, Codex Important 반영)
- Phase 3b.6: 12 (scene_detail_analyze_one behavior 테스트 — 기본 구조 / fixed_elements / VE retry / outfit 정리 / 공백 정리 / legacy path)
- + 기타 (test_step_manifest_v3 재작성 22 등)

_참고: Phase 3b.4가 추가한 `test_image_service_phase_api.py` (5 tests)는 Phase 3b.2에서 `test_reference_image_service.py`로 대체·흡수되어 삭제됨._

**커밋 수**: 주요 17건 (Phase 0: 2, Phase 1: 1, Phase 2: 1, Phase 3 부분: 1, Phase 4: 5, Phase 3b: 5, 문서: 2).

---

## Phase 0 — 현행 계약 정리 (완료, 2026-04-17)

### 커밋
- `253e668` refactor(phase-0): 계약 정리 — test baseline 48단계 복구 + AnalysisService deprecated
- `051ff00` refactor(phase-0): baseline.md §5.2 pytest 수집 결과 반영

### 작업 내용
1. **test_step_manifest_v3.py baseline 복구**
   - 26단계 기대값 → 48단계로 갱신
   - scene_detail 의존성 7건 전체 재작성
   - `scene_director.label`을 "V/A/H" → "물리적 존재"로 수정
   - `if_set_design_enabled` known applicability에 추가
   - 결과: 11 failed / 9 passed → **22 passed / 0 failed**

2. **step_manifest.py docstring 확장**
   - "33단계" → "48단계" + category별 정확 분포 명시
   - analysis 41 (always 34, if_planning_doc 1, on_demand 3, disabled 3)
   - image 5 (always 4, if_has_outlooks 1)
   - auxiliary 2 (on_demand 2)

3. **docs/architecture/ 수치 갱신**
   - `00-overview.md`: Phase별 Active 합계 → 40, AnalysisService deprecated 노트
   - `06-data-contracts.md`: Active 34 → 40, step_type 4유형 섹션 추가

4. **AnalysisService deprecated 선언**
   - `/episodes/{id}/analyze`, `/reanalyze-scenes`에 `deprecated=True`
   - `logger.warning` 로그 (Phase 4 제거 예고)
   - OpenAPI `/docs`에 deprecated 표시

5. **baseline.md 신규 작성**
   - Phase별 추적용 지표 파일
   - 코드 크기 / 대형 함수 / Step 분포 / 결함 패턴 / 테스트 / Frontend hooks 섹션

### 리뷰 반영 (Codex P1 3건 + P2 1건 / Claude Important 3건 + Minor 1건)
- Codex P1-1 / Claude Important 1: deprecation URL에 `/projects/{project_id}` prefix
- Codex P1-2 / Claude Minor 4: `step_manifest.py` docstring 정확 수치
- Codex P1-3 / Claude Important 2: `run_all_analysis()` → `run_analysis()`, `reanalyze_scenes()`
- Codex P2-1: `_build_final_scene_prompt` 측정 496줄 → 211줄 (내부 중첩 함수 오측정)
- Claude Important 3: baseline §5.3에 deprecated 의존 테스트 3건 명시

---

## Phase 1 — Step Catalog 통합 (완료, 2026-04-17)

### 커밋
- `5cbf3e9` refactor(phase-1): Step Catalog 통합 + applicability validator + atomic write util

### 작업 내용

#### 1.1 `step_catalog.py` 신설
- `StepEntry` dataclass: manifest + class + 파생 필드 병합 View
- `STEP_CATALOG: Dict[str, StepEntry]` — 모듈 로드 시 `_build()` 실행
- 조회 헬퍼: `get_entry`, `get_depends_on`, `get_downstream_steps`, `get_all_downstream_recursive`, `get_ordered_entries`, `get_resume_sensitive_step_ids`, `get_active_entries`, `get_modifiers_of`

#### 1.2 `applicability.py` 신설
- `APPLICABILITY_VALIDATORS` 레지스트리
- `_if_planning_doc`: `ProjectRegistry.planning_doc_text` 조회 (Codex 지적: Episode → Project)
- `_if_has_outlooks`: `outlook_phase3` 체크포인트 조회
- `_if_set_design_enabled`: `SettingsRegistry` 경유
- `resolve_applicability(runner)` 공용 해석 함수

#### 1.3 하드코딩 downstream 치환 (4곳)
- `steps.py` `toggle_shot_selection`: hardcoded 10개 → `catalog_get_all_downstream_recursive("shot_selection")` (14개, composite_image_gen 오포함 제거 + 신규 5개)
- `steps.py` `_resume_sensitive`: hardcoded 3개 → `get_resume_sensitive_step_ids()`
- `steps.py` snapshot restore: 로컬 manifest import → catalog alias 통일
- `steps.py` `_needs_presync`: 튜플 상수화 + Phase 2 TODO 주석

#### 1.4 `checkpoint_io.py` 신설 (공통 유틸)
- `atomic_write_json(path, payload)`: uuid8 tmp suffix로 per-call unique
- `read_json_safe(path)`: 손상 시 None 수렴
- 소비자 교체 3곳: `steps.py` 로컬 `_atomic_write_json` 제거, `step_runner.save_checkpoint`, `t2i_review_step._save_checkpoint_data`

#### 1.5 `step_manifest.py` 필드 확장
- 48 step 모두에 `step_type`, `lifecycle`, `resume_sensitive` 필드 추가
- `step_type` 분포: transform 40 / editorial 2 / asset 6
- `lifecycle` 분포: active 41 / deprecated 6 / removed 1
- `modifies_checkpoints` (editorial만): t2i_review = [entity_t2i, scene_detail]
- `replaced_by` (deprecated 6건): scene_split, scene_cinematography, shot_cinematography, scene_dependency, outlook_extraction, project_summary
- `set_design.applicability`: `always` → `if_set_design_enabled`
- `project_summary.lifecycle`: `active` → `removed` (ProjectSummaryStep 클래스 부재)

#### 보조
- `StepRunner.check_applicability` → `resolve_applicability(self)` 호출
- `StepRunner.save_checkpoint` → `atomic_write_json` 사용
- `StepRunner.invalidate_downstream(target_step_id, delete_checkpoints)` 확장
- `StepRunner.run` 끝: `modifies_checkpoints` 소비 (editorial step downstream 자동 stale)
- `set_design_step.py`: runtime `settings.set_design_enabled` 체크 제거 (applicability로 이관)

### 테스트 44개 추가
- `test_checkpoint_io.py` (8): atomic write/read + tmp 고유성
- `test_manifest_fields.py` (16): step_type/lifecycle 제약 + editorial/asset/t2i_review
- `test_step_catalog.py` (13): catalog 계약 + downstream 일치
- `test_applicability.py` (6): resolve + registry 정합 + unknown rule ValueError

### 리뷰 반영 (Codex Critical 2 + P2 2 / Claude Important 3 + Minor 5)
- Codex C1: `_if_planning_doc` Episode → ProjectRegistry 수정
- Codex C2: `modifies_checkpoints` 소비 로직 추가 (editorial downstream 자동 무효화)
- Codex P1: `atomic_write_json` tmp에 uuid8 suffix
- Codex P4 / Claude M5: `read_json_safe or {}` → None skip + WARNING
- Claude I1: `steps.py:580` 로컬 manifest import → catalog 통일
- Claude I2: `project_summary` lifecycle=removed
- Claude M3/M4: set_design applicability 회귀 방어 + unknown rule 테스트
- Claude M1: `step_catalog` 순환 import 경고 docstring

---

## Phase 2 — Sync 분해 + Service 레이어 (완료, 2026-04-17)

### 커밋
- `ebf55a8` refactor(phase-2): CheckpointSync 5-way 분해 + Service 레이어

### 작업 내용

#### 2.1 `_sync_checkpoints_to_db` 722줄 → 5 Service + orchestrator
- `backend/app/services/checkpoint_sync/` 패키지 신설
- `_base.py` `BaseSyncService`: 공용 `_load_cp/_is_step_completed` + single `now` 주입
- `entity_sync_service.py`: EntityCanon + EntityEpisodeLink UPSERT (126줄)
- `relation_sync_service.py`: RelationFact visual_variant (96줄) — Phase 4 UPSERT 후보
- `scene_still_sync_service.py`: SceneStill + scene_summary + shot_type (398줄, 최대)
- `outlook_sync_service.py`: outlook canon + CharacterOutlook (161줄) — Phase 4 UPSERT 후보
- `episode_projection_service.py`: Episode status + t2i_appearance_count (96줄)
- `orchestrator.py`: 5-way 순차 + 단일 commit + 단일 now 공유
- `steps.py _sync_checkpoints_to_db`: 722줄 → 10줄 wrapper

#### 2.2 `ShotSelectionService` 신설
- `toggle_shot_selection` 로직을 `shot_selection_service.py`(183줄)로 이관
- 트랜잭션 순서 (DB 먼저 → 파일) + 원자 쓰기 + warnings 응답 보존
- API endpoint는 3줄 wrapper

#### 2.3 `SnapshotService` 신설
- `list_versions`, `save_snapshot`, `restore` 메서드
- 복원 시 전체 Service re-sync 호출
- status whitelist 복원 로직 보존 (not_applicable/skipped/error 포함)

#### 2.4 `SettingsRegistry` 도입
- `is_feature_enabled(feature, project_id, db)`: env fallback (DB override는 Phase 4+ 후속)
- `get_model_for_step(step_id, project_config)`: project override → manifest default → fallback
- `applicability._if_set_design_enabled`이 Registry 경유로 전환

#### 2.5 `@api_endpoint` 데코레이터
- `deps.py`에 추가: dict 응답 `warnings` ensure + AppError 재전파 + 예상 외 예외 → 500
- 시범 적용 4곳: `toggle_shot_selection`, `list_snapshots`, `create_snapshot`, `restore_snapshot`

#### 역의존 해소
- `image_steps.py:58`: `from app.api.v1.steps import _sync_checkpoints_to_db` → `from app.services.checkpoint_sync import orchestrate_full_sync`
- `entities.py:789`: `_sync_t2i_appearance_counts` 역의존 → `EpisodeProjectionService`

### 테스트 17건 추가
- `tests/services/test_checkpoint_sync_services.py` (10): 각 Service 계약
- `tests/services/test_shot_selection_service.py` (3): toggle AppError + 손상 전파
- `tests/services/test_snapshot_service.py` (5): list/save/restore 경계
- `tests/test_settings_registry.py` (6): is_feature_enabled + get_model_for_step
- `tests/test_api_endpoint_decorator.py` (6): dict warnings + AppError 재전파

### 리뷰 반영 (Codex 3 Issue + Claude 2 Important + 5 Minor)
- Codex Item 1: 단일 sync 내 `now` 공유 (orchestrator → Service 주입)
- Codex Item 2: `_load_cp` 예외 전파 복원 (baseline `json.loads` 동작)
- Codex Item 6: `ShotSelectionService._validate_shot_index` json.loads 복원 (raw JSONDecodeError)
- Codex Item 9 / Claude M5: `api_endpoint` 4 endpoint 시범 적용
- Claude I1 / M2: `EpisodeProjectionService.project()` → `sync_from_checkpoint()` 통일 + 반환값 버그 수정

### 이월 (Phase 4)
- UPSERT 전환: RelationFact, CharacterOutlook
- DB feature_flags 컬럼 + SettingsRegistry project-level override
- `api_endpoint` 전 endpoint 점진 적용 (Phase 5)

---

## Phase 3 부분 완료 (2026-04-17)

### 커밋
- `8277f2a` refactor(phase-3-partial): Image step 경계 + SceneAnalysisContext DTO

### 작업 내용

#### 3.1 Image step 경계 재정의 (플래그 기반, 메서드 분리는 3b)
- `image_service.py generate_reference_images_only`에 `skip_composite: bool = False` 파라미터 추가
- Phase 2(composite) 진입 직전 `if skip_composite: early return`
- `RefImageGenStep._execute`: `skip_composite=True` 호출 + `_mark_composite_done` 제거 (step 경계 붕괴 해소)
- `CompositeImageGenStep._execute`: 기본값 호출 (reference resume skip → composite만 진행)
- `RefImageGenStep` force SQL에 composite/outfit primary 제외 (`prompt_used LIKE '[outfit:%'` / `'[composite:%'`)

#### 3.6 SceneAnalysisContext DTO
- `core/dto/scene_analysis.py`: SceneAnalysisContext dataclass (20+ 필드)
- `core/steps/scene_context_loader.py`: `SceneContextLoader.load_all()` (238줄)
- `detail_steps._execute`: 기존 170줄 체크포인트 로드 → Loader 호출 + closure 변수 이름으로 unpack
- `_analyze_one` 400줄 본문은 변경 없음 (점진 전환, closure → ctx.field는 Phase 3b)

### 테스트 8건 추가
- `tests/core/test_scene_analysis_dto.py`: DTO 기본값 + Loader 체크포인트 경로 (shot_dependency 우선, outlook_phase3 우선, fixed_elements 빈 리스트 제외, shot_selection 필터링)

### 리뷰 반영 (Codex Critical 1 + 주의 2 + OK 6 / Claude Important 3 + Minor 6)
- **Codex #3 Critical**: RefImageGenStep force SQL composite/outfit 제외 필터 추가
- Claude I1/I3: 커밋 prefix "phase-3-partial" + baseline.md §진행 현황 요약 추가
- Codex #2/Claude I2: "부분 완료" 명시 (`_analyze_one` 본문 미전환)

### 이월 (Phase 3b)
- `generate_reference_images_only` → 두 메서드 분리 (`generate_base_references`, `generate_composites`)
- `_build_final_scene_prompt` 211줄 → 4 메서드 (PromptService)
- `ReferenceImageService` / `SceneImageService` 분할 (~3,000줄)
- `image_service.py` shim 후 제거
- `_analyze_one(ctx)` 시그니처 변경 + closure → ctx.field 전환

---

## Phase 4.1 — AnalysisService 호출 경로 내부 교체 (완료, 2026-04-17)

### 커밋
- (예정) `refactor(phase-4.1): /analyze·/reanalyze-scenes 내부 StepRunner로 교체 + 공용 dispatch 서비스`

### 작업 내용

#### 4.1.1 `analysis_dispatch_service.py` 신설 (384줄)
공식 dispatch 진입점. `steps.run_all_steps`의 `_run_all_bg` nested function + `episodes._run_analysis_in_background` + `episodes.reanalyze_scenes` 내부 로직을 단일 서비스로 수렴.

공개 API:
- `build_opik_context(db, project_id, episode_id)` — Opik thread 그룹핑 context (ORM 기반)
- `load_project_llm_config(db, project_id)` — ProjectSettings.llm_config_json 파싱
- `select_steps_for_category(db, project_id, category)` — analysis/image/all 카테고리 active step 목록
- `select_scene_reanalysis_steps(db, project_id)` — scene_save + downstream(active analysis)
- `get_step_runner(...)` — Step ID → StepRunner 인스턴스
- `run_steps_batch(...)` — background worker 실제 구현
- `dispatch_category_run(...)` — category run-all 공용 진입점
- `dispatch_scene_reanalysis(...)` — /reanalyze-scenes 공용 진입점

공용 상수:
- `RUN_ALL_CATEGORY_TOKENS = ("analysis", "image", "all", "reanalyze")` — `run_step`/dispatch 충돌 차단 공유

실패 복구:
- `_recover_episode_status_on_failure(db, episode_id, message)`: pipeline 실패 시 `Episode.status = "analyzing"` → `"error"` + `analysis_error` 기록 (Review Critical #1 반영)

#### 4.1.2 `api/v1/steps.py` 축소 (496 → 376줄, -120)
- `run_all_steps` → `dispatch_category_run` 위임 (내부 nested `_run_all_bg` 제거)
- `_build_opik_context`, `_get_step_runner`: dispatch_service wrapper (기존 테스트 호환)
- `_sync_checkpoints_to_db`, `_sync_t2i_appearance_counts`: Phase 2 wrapper 유지
- `run_step` 충돌 체크: `RUN_ALL_CATEGORY_TOKENS` 사용

#### 4.1.3 `api/v1/episodes.py` AnalysisService import 제거
- `from app.services.analysis_service import AnalysisService` 삭제
- `_run_analysis_in_background` nested function 제거
- `analyze_episode`: dispatch_category_run(category="analysis", mode="resume") 위임
- `reanalyze_scenes`: dispatch_scene_reanalysis 위임
- 양쪽 엔드포인트 `deprecated=True` 및 WARNING 로그는 유지 (호환 보존 안내)

### 테스트 20개 추가
- `tests/services/test_analysis_dispatch_service.py`
  - select_steps_for_category: category 필터 + planning_doc/all/invalid (4)
  - select_scene_reanalysis_steps: scene_save+downstream 포함 + order 정합 (2)
  - load_project_llm_config: 없음/정상/손상 (3)
  - dispatch_category_run: already_running/submit 성공/job_key 충돌 (3)
  - dispatch_scene_reanalysis: force mode 인자 검증 (inspect.bind) (1)
  - category ↔ reanalyze 양방향 차단 (1)
  - get_step_runner: valid/invalid step_id (2)
  - _recover_episode_status: analyzing→error / noop / 2000 chars / missing (4)

### 리뷰 반영

#### Claude 리뷰 (Critical 1 + Important 3 + Minor 3)
- **Critical #1**: `run_steps_batch` 실패 시 `episode.status = "analyzing"` 영구 잠금 위험
  → `_recover_episode_status_on_failure` 헬퍼 + outer try/except + all_ok=False 경로에서 호출. 4개 회귀 방지 테스트 추가
- **Important #2**: `reanalyze` job_key가 `run_step` 충돌 체크에 누락
  → `RUN_ALL_CATEGORY_TOKENS` 상수로 공유. 양방향 차단 테스트 추가
- **Important #3**: `select_scene_reanalysis_steps` 중복 필터 (`analysis_active` 변수 불필요)
  → `get_active_entries(category="analysis")` 결과를 직접 순회. downstream 교집합 + applicability만 체크
- **Important #4**: `build_opik_context` raw SQL (스타일 불일치)
  → `ProjectRegistry` / `Episode` ORM 쿼리로 전환
- **Minor #5**: `load_project_llm_config` bare `except Exception`
  → `json.JSONDecodeError`만 포착
- **Minor #6**: 테스트 `args_tuple[3]` positional-index 의존
  → `inspect.signature(target).bind(*args)`로 named argument 검증
- **Minor #7**: progress.md Phase 4.1 섹션 미기록
  → 본 문서 반영

#### Codex 리뷰 (Important 3 + Minor 1)
- **Important #1**: pre-dispatch 실패 시 worker-side recovery가 실행 안 됨 → episode.status가 "analyzing" 잠금
  → `analyze_episode` / `reanalyze_scenes`에 try/except 래핑. 실패 시 `prior_status`로 원복
- **Important #2**: `scene_save` 시작 시 `scene_segmentation` 체크포인트가 재계산 안 됨 (baseline은 `segments.json` 삭제)
  → `select_scene_reanalysis_steps` 루트를 `scene_save` → `scene_segmentation`으로 변경. scene_segmentation부터 force cascade
- **Important #3**: `scene_still_sync_service`가 stale 마킹만 하는데 `export_service.py:133,458`의 readers가 stale 필터 없음 → 삭제된 씬이 export에 leak
  → `export_service.py` 두 쿼리에 `SceneStill.status != "stale"` 필터 추가
- **Minor #1**: `run_steps_batch` 자체(gate.blocked skip, 실패 recovery, mid-sync, final-sync rollback)에 직접 테스트 부재
  → 6개 단위 테스트 추가 (test_run_steps_batch_step_failed/crashed/gate_blocked/mid_sync/final_sync_failure + reanalysis 시작점 검증)

### 회귀 범위
- Frontend: `/steps/run-all` 응답 format (ok/status/steps/job_key) 유지 → `EpisodeDetail.tsx:821`, `PipelineStepsPanel.tsx:125` 영향 없음
- Test: `tests/test_sync_v3.py`, `tests/test_pipeline_v3_e2e.py`가 `_get_step_runner`/`_sync_checkpoints_to_db` 사용 → wrapper 유지로 호환
- AnalysisService 파일 자체: 아직 삭제 전 (Phase 4.2 예정)

### 이월 (Phase 4.2~)
- `backend/app/services/analysis_service.py` 1,137줄 파일 삭제
- `backend/app/core/version_registry.py` `analysis_service` 엔트리 제거
- `docs/` 내 AnalysisService 참조 정리 (설계 문서 제외)

---

## Phase 4.2 — AnalysisService 파일 제거 (완료, 2026-04-17 @5f89b1e)

### 커밋
- `5f89b1e` refactor(phase-4.2): AnalysisService 파일 제거 (1,137줄)

### 작업 내용
- `backend/app/services/analysis_service.py` 파일 삭제 (−1,137줄)
- `backend/app/core/version_registry.py`에서 `analysis_service` 엔트리 제거

### 검증
- `grep -r "from app.services.analysis_service|import AnalysisService" backend/`
  → 0건 (episodes.py deprecation message 내 문자열 + dispatch_service docstring
     참조만 잔존 — 의도적 historical context)
- `tests/test_sync_v3.py` / `tests/test_pipeline_v3_e2e.py`가 AnalysisService를
  import하지 않음 (`_get_step_runner` / `_sync_checkpoints_to_db` wrapper만 사용)
- smoke: `from app.main import app` → OK
- 테스트: 129 passed (Phase 0~4.1 유지)

---

## Phase 4.3 — outlook_dedup UPSERT 이관 (완료, 2026-04-17)

### 커밋
- (예정) `refactor(phase-4.3): outlook_dedup DELETE → UPSERT 이관`

### 작업 내용

#### 4.3.1 `apply_outlook_merge` 리팩토링
기존 단일 150줄 함수를 8개 헬퍼로 분리:
- `_resolve_outlook_id(db, project_id, name)` — 이름 → EntityCanon.id 조회
- `_reassign_character_outlook(db, project_id, keep_id, remove_id)` (기존 유지)
- `_reassign_image_assets(db, project_id, keep_id, remove_id)` (asset_type='reference' 필터 추가)
- `_rewrite_scene_still_references(db, project_id, keep_name, remove_name)` (기존 유지, `[name]` marker 교체)
- `_migrate_entity_episode_links(db, project_id, keep_id, remove_id)` — **신규** UPSERT + count 합산
- `_migrate_entity_aliases(db, keep_id, remove_id)` — **신규** (canon_id, alias) unique 준수
- `_migrate_relation_participants(db, keep_id, remove_id)` — **신규** UPDATE + 중복 dedup
- `_merge_single_outlook(db, project_id, keep_name, remove_name)` — 통합 orchestrator

#### 4.3.2 DELETE-only → UPSERT 전환 (핵심)
기존 `apply_outlook_merge`에서 DELETE-only였던 3개 테이블을 이관(UPSERT)으로 전환:

| 테이블 | 기존 | 변경 후 | 데이터 유실 방지 |
|---|---|---|---|
| `entity_alias` | DELETE ALL where canon_id=remove | remove → keep UPDATE, 중복 시 DELETE | alias 유실 |
| `entity_episode_link` | DELETE ALL where canon_id=remove | remove → keep UPDATE, 같은 episode 중복 시 count 합산 후 DELETE | t2i_appearance_count 누적값 |
| `relation_participant` | DELETE ALL where canon_id=remove | remove → keep UPDATE + 동일 relation 내 중복 dedup | visual_variant 관계 |

### 테스트 15개 추가 (`tests/pipeline/test_outlook_dedup.py`)
- `_resolve_outlook_id`: 존재/미존재 (2)
- `_migrate_entity_aliases`: unique 이관 / 중복 skip / 빈 remove (3)
- `_migrate_relation_participants`: UPDATE + dedup (1)
- `_migrate_entity_episode_links`: 충돌 없음 / count 합산 (2)
- `_merge_single_outlook`: resolve 실패 skip / EntityCanon DELETE 마지막 (2)
- `apply_outlook_merge`: commit / rollback / counts (3)
- `_rewrite_scene_still_references`: marker 교체 / 이름 부재 skip (2)

### 리뷰 반영

#### Claude 리뷰 (Critical 1 + Important 4 + Minor 2)
- **Critical #1**: `_migrate_relation_participants` / `_migrate_entity_aliases`에 project_id 파라미터 부재 (실전 UUID 충돌 위험 낮음, 방어적 설계 결여)
  → docstring에 "호출자가 keep_id/remove_id를 project_id 스코프로 이미 검증함" 명시
- **Important #2**: `_migrate_entity_episode_links` keep_map 합산 후 갱신 누락 (unique constraint로 실전 미발생)
  → docstring에 "remove_rows에 동일 episode_id 중복 불가" 주석 추가
- **Important #3**: `test_merge_single_outlook_deletes_canon_last` 검증 로직 무효
  → `final_sqls[-1]`에 `DELETE FROM entity_canon` 명시 assert로 교체
- **Important #4**: FakeDB SQL fragment 부분매칭 취약 (짧은 키가 긴 키보다 먼저 매치)
  → 긴 키부터 정렬 매칭으로 변경
- **Important #5**: `_migrate_relation_participants` 후 동일 relation 내 중복 canon_id 가능 (downstream JOIN 카티시안 위험)
  → UPDATE 직후 `MIN(id) GROUP BY relation_id, canon_id` 기반 dedup DELETE 추가 + 테스트 갱신
- **Minor #11**: `find_duplicate_outlooks`의 `description[:80]` 슬라이싱 — CLAUDE.md 절대 규칙 ("LLM 데이터 자르지 않음") 위반 가능성
  → 슬라이싱 제거, description 전문 전달
- **Minor #12**: `progress.md` Phase 4.3 섹션 부재
  → 본 문서 반영

#### Codex 리뷰 (Critical 1 + Important 3 + Minor 2 — hotfix `b8c1313`로 반영)
- **Critical #1**: self-merge 가드 부재 — `keep_id == remove_id` 시 keep의 alias/image/relation 삭제 위험 (EntityCanon name uniqueness 없음)
  → `_merge_single_outlook` 시작 직후 가드 + warning log + False 반환
- **Important #2**: RelationParticipant dedup이 `(relation_id, canon_id)` 기준만 — 같은 relation에 base/variant 두 role이 모두 keep이면 한 role 유실 (self-collapsed relation 의미 파괴)
  → `GROUP BY relation_id, canon_id, participant_role`로 확장
- **Important #3**: `t2i_prompt_closeup` 미갱신 — image_service closeup 경로가 삭제된 outlook 이름을 계속 참조하는 stale prompt
  → SELECT/UPDATE에 `t2i_prompt_closeup` 포함
- **Important #5 (테스트)**: FakeDB는 PG unique 충돌/race 검증 불가
  → 통합 테스트는 향후 별도 작업. 단위 테스트 수준에서 role-aware dedup + closeup rewrite 회귀 방지 테스트 추가
- **Minor #7**: EntityEpisodeLink merge 시 `source` 컬럼 미병합 — 기능 영향 작음, 현재 유지
- **Minor #8**: 주석 "최신 1 row" vs SQL `MIN(id)` — Phase 4.4~5 문서 정리 시 함께 교정

### 회귀 범위
- outlook merge 경로만 영향. E2E에서 outlook_dedup step 호출 검증 필요
- `_reassign_image_assets`의 asset_type='reference' 필터 추가로 scene 이미지(still_id 기반)는 무관 확인
- SceneStill marker 교체는 `[name]` 경계 매칭으로 partial-match 방지

### 이월 (Phase 4.4~)
- (Phase 4.4 완료 예정) RelationFact delta sync (visual_variant 전체 DELETE→INSERT → UPSERT)
- (Phase 4.5 완료 예정) CharacterOutlook UPSERT 전환 (outlook_sync_service)

### Hotfix 커밋
- `b8c1313` fix(phase-4.3): Codex 리뷰 Critical/Important 반영 — self-merge 가드 + role-aware dedup + closeup rewrite

---

## Phase 4.4 — RelationFact delta sync (완료, 2026-04-17)

### 커밋
- (예정) `refactor(phase-4.4): RelationSyncService delta sync 전환`

### 작업 내용

#### 4.4.1 `RelationSyncService.sync_from_checkpoint` 재작성
기존: visual_variant 전체 DELETE → 체크포인트에서 전부 재INSERT.
변경: `(base_canon_id, variant_canon_id)` 키 기준 delta sync.

- 기대 상태: 체크포인트 `entity_relation.data.relations` (visual_similarity=True만)
- 현재 상태: DB의 `relation_fact WHERE relation_type='visual_variant'`
  + `relation_participant` JOIN으로 base/variant canon_id 추출
- Delta:
  - `to_insert = desired - existing` → RelationFact + 2 RelationParticipant INSERT
  - `to_update = 교집합 중 reason 다름` → continuity_reason UPDATE만
  - `to_delete = existing - desired` → participant + fact DELETE

#### 4.4.2 반환 계약 변경
- 기존: `{"relations": n, "removed_old": n, "skipped": n}`
- 변경: `{"relations": n, "inserted": n, "updated": n, "deleted": n, "skipped": n}`

### 테스트 6개 추가 (`tests/services/test_relation_delta_sync.py`)
- 빈 existing + desired 1건 → 신규 INSERT만
- visual_similarity=False만 → 기존 관계 DELETE
- 교집합 + reason 변경 → UPDATE만 (id 유지)
- 교집합 + reason 동일 → noop
- mixed 시나리오 (insert+update+delete 각 1건)
- unknown short_id skip
- existing에 participant 누락 → delta에서 skip

기존 `test_checkpoint_sync_services.py`의 relation 계약 테스트 2건도 새 계약에 맞게 갱신.

---

## Phase 4.5 — CharacterOutlook delta sync (완료, 2026-04-17)

### 커밋
- (예정) `refactor(phase-4.5): OutlookSyncService CharacterOutlook delta sync 전환`

### 작업 내용

#### 4.5.1 `OutlookSyncService._delta_sync_character_outlook` 신설
기존: 해당 에피소드 character의 character_outlook 전체 DELETE → 재INSERT.
변경: `(character_id, outlook_id)` 키 기준 delta sync.

- 기대 상태: 체크포인트 `outlook_phase3.data.scene_assignments`에서 파싱한 pair 집합
- 현재 상태: 이 episode character의 character_outlook (entity_episode_link JOIN)
  - 혹시 동일 키 중복 행이 있으면 최신 1개만 유지(dedup)
- Delta: to_insert / to_delete만 (UPSERT는 PK가 임의 uuid이므로 불필요)

#### 4.5.2 반환 계약 확장
- 기존: `{"outlooks": n, "links": n, "orphans_removed": n}`
- 변경: `{"outlooks": n, "links": n, "links_inserted": n, "links_deleted": n, "orphans_removed": n}`

### 테스트 4개 추가 (`tests/services/test_outlook_delta_sync.py`)
- insert-only 시나리오
- delete-only 시나리오 (scene_assignments 빈 경우)
- noop (체크포인트 = DB)
- 중복 행 dedup (동일 (cid, oid) 행 2개 → 최신 1개 유지)

기존 `test_checkpoint_sync_services.py`의 outlook 계약 테스트도 새 계약에 맞게 갱신.

### 회귀 범위 (Phase 4.4 + 4.5 공통)
- StepRunner 경로의 체크포인트→DB projection 안정성 향상
- `orchestrate_full_sync` 호출에서 RelationFact/CharacterOutlook의 변경이 incremental (기존 row id 유지)
- downstream의 `relation_fact.id` / `character_outlook.id` 참조는 여전히 안정

### 이월 (별도 작업)
- 실제 PostgreSQL 통합 테스트 (Codex Important #5)
- EntityEpisodeLink source 컬럼 merge 정책 (Codex Minor #7)
- outlook_dedup 주석 정확도 (Codex Minor #8)

---

## Phase 3b.1 — PromptService 분리 (완료, 2026-04-18)

### 커밋
- (예정) `refactor(phase-3b.1): _build_final_scene_prompt 211줄을 PromptService 4단계로 분리`

### 작업 내용

#### 3b.1.1 `backend/app/services/prompt_service.py` 신설
- `RefResolution` dataclass (ref_roles + ref_instructions + roles_text/instructions_text)
- `resolve_ref_roles(labeled_refs)` — 라벨 기반 Reference 지시 구성
- `replace_entity_ids(t2i_prompt, labeled_refs, entity_text_map)` — C##/C##O##/P##/L## 치환 + bracket/in O##/camera angle 제거
- `translate_if_korean(cleaned, ref_roles_text, ref_instructions_text, style_context, ...)` — 한국어 잔재 시 LLM 번역, 실패/파일 부재 시 원본 반환
- `build_scene_text(ref_roles_text, cleaned, ref_instructions_text)` — 최종 조립
- `build_final_scene_prompt(...)` — public entry (기존 계약 동일)

#### 3b.1.2 `image_service._build_final_scene_prompt` wrapper 축소
- 205줄 → 17줄 thin wrapper (prompt_service.build_final_scene_prompt 위임)
- image_service 내부 호출 2곳 (1627, 3381) 및 backend/scripts/experiment_*.py 7개 호환 유지

### 리뷰 반영

#### Claude 리뷰 (Critical 2 + Important 3)
- **Critical #1 (v2 template format key mismatch)**: v2 translate_prompt.md가 설계 변경으로 `{ref_instructions}` 제거하고 scene body만 반환. `str.format()`이 extra kwargs를 silently 무시하므로 v1/v2 모두 호환. 주석으로 설명.
- **Critical #2 (test 실 파일 의존)**: `translate_if_korean` 테스트가 prompts/ 디렉토리 mock 없이 실행 → CI 깨짐 위험. `_setup_translate_prompt_dir` helper + Path.resolve monkeypatch로 tmp_path 기반 격리. 2개 테스트 갱신 + prompt_dir 없음/버전 디렉토리 없음 edge case 테스트 2건 추가
- **Important #1 (_keep_part suffix 오염)**: `label[label.lower().find("keep"):]` 방식이 "Ignore..." 문장까지 포함 → 문장 단위 split로 교체
- **Important #2 (versions empty guard)**: `prompt_dir.iterdir()` FileNotFoundError + `versions[0]` IndexError 방어 추가, 원본 반환
- **Important #3 (중복 조건)**: `"ignore" in label_lower or "ignore" in label` — Important #1 수정으로 자연스럽게 제거됨

#### Codex 리뷰 (Important 3 + OK 6)
- **Important #1 (테스트 커버리지)**: mixed C##O##+C##, 단독 L## 치환, multi-reference numbering, composite fallback 테스트 4건 추가
- **Important #2 (wrapper-equivalence 취약)**: 고정 expected output characterization 테스트 추가 (deterministic snapshot)
- **Important #3 (baseline sandbox 확인 불가)**: 실행 환경 제약 — 지적만
- **Minor (naming)**: `replace_entity_ids`가 실제로 bracket/in O##/camera angle까지 처리 — 기존 동작 보존이라 name만 좁음. 범위 밖

### 테스트 26개 추가 (`tests/services/test_prompt_service.py`)
- resolve_ref_roles: 6 (빈/outfit/character/previous_same_room/previous_continuity/background)
- replace_entity_ids: 11 (composite/fallback/bracket L##/in O##/camera angle/prop/mixed/standalone L##/multi-ref/composite char fallback)
- translate_if_korean: 5 (영어 pass/LLM 호출/실패 원본/prompt_dir 없음/버전 없음)
- build_scene_text: 2 (정상 조립/중복 prefix 제거)
- entry point: 2 (smoke/characterization snapshot)

총 182 passed (Phase 0~4.5 156 + Phase 3b.1 26).

---

## Phase 3b.4 — generate_reference_images_only public API 분리 (완료, 2026-04-18)

### 커밋
- (예정) `refactor(phase-3b.4): ImageService reference/composite public API 분리`

### 작업 내용
- `ImageService.generate_base_references(episode_id, *, mode, ip)` 신설 — `skip_composite=True` 위임
- `ImageService.generate_composites(episode_id, *, mode, ip)` 신설 — `skip_composite=False` 위임
- `generate_reference_images_only` 유지 (api/v1/images.py + backend/scripts/experiment_* 호환)
- 호출자 전환:
  - `RefImageGenStep` → `svc.generate_base_references(...)`
  - `CompositeImageGenStep` → `svc.generate_composites(...)`

### 설계 의도
- 현 시점에서는 공통 orchestrator(`generate_reference_images_only`)의 thin wrapper.
- API 명확성 향상: 호출자가 어느 Phase를 원하는지 시그니처로 표현.
- Phase 3b.5(shim 제거)에서 `generate_reference_images_only`를 strictly internal로 강등 예정.

### 테스트
- Phase 3b.4 시점 5 tests (`tests/services/test_image_service_phase_api.py`) 추가.
- Phase 3b.2에서 동일 계약을 `test_reference_image_service.py`로 이관(추가 17건 포함) 후 원본 파일 삭제.

### 리뷰
- Phase 3b.4는 Phase 3b.5 batch 리뷰와 함께 진행 예정 (Phase 3b.1처럼 단독 리뷰 대신)

---

## Phase 3b.2 — ReferenceImageService 분리 (완료, 2026-04-18)

### 커밋
- (예정) `refactor(phase-3b.2): ImageService reference/composite 섹션을 ReferenceImageService로 분리 (~1,048줄)`

### 작업 내용
- `backend/app/services/reference_image_service.py` (신규 1,147줄) — ImageService의 참조/합성 관련 7개 메서드 이관.
  - `generate_base_references` / `generate_composites` (Phase 3b.4 public API 진입점)
  - `generate_reference_images_only` (본체 orchestrator ~704줄)
  - `generate_single_entity_image` / `generate_composite_image` / `regenerate_composites_for_entity`
  - `_validate_reference` (validator wrapper)
- `backend/app/services/image_service_helpers.py` (신규 162줄) — ImageService/ReferenceImageService/SceneImageService 공용 module-level helper 4개.
  - `image_to_dict` / `get_latest_world_guide` / `auto_set_primary` / `build_lineage_fields`
- `backend/app/services/image_service.py` (5,168 → 4,120줄, -1,048) — 위 메서드들을 thin wrapper로 전환. ImageService의 4개 shared helper는 module helper로 위임하는 shim으로 교체.
- 호출자 무변경: `api/v1/images.py`, `core/steps/image_steps.py`, `backend/scripts/experiment_*.py` 모두 ImageService 래퍼 시그니처 보존.

### 설계 의도
- 파일당 책임 축소 — 이미지 서비스가 `reference / composite / scene` 3개 도메인으로 자연 분해됨. 본 Phase에서 reference+composite 부분을 먼저 분리.
- 공유 helper의 `db, project_id, actor_id` 시그니처 통일 → Phase 3b.3 SceneImageService 신설 시 재사용.
- ReferenceImageService 안에 local helper 4개(`_get_episode`, `_get_project_dir`, `_get_style_context`, `_load_project_llm_config`)를 잠시 중복 복사. Phase 3b.5(또는 3b.3 병행)에서 공유 module로 승격 예정 — 본 Phase는 reference/composite 경계를 우선 고정.
- ImageService는 api/v1/images.py 및 backend/scripts/experiment_* 호환을 위해 thin wrapper로 유지. Phase 3b.5에서 제거 검토.

### 테스트 33 추가
- `tests/services/test_reference_image_service.py` (22 tests):
  - 위임 계약: `generate_base_references` → skip_composite=True / `generate_composites` → skip_composite=False
  - 시그니처 보존: `generate_reference_images_only`의 `skip_composite` keyword-only
  - regenerate_composites_for_entity 분기: character iter / O00 skip / outlook entity / unknown entity
  - `_validate_reference` 경로: 파일 없음 / 고득점 / 저득점 needs_fix / 예외
  - ImageService → ReferenceImageService 위임 검증 (7 wrapper 메서드 각각 @patch)
- `tests/services/test_image_service_helpers.py` (11 tests):
  - `image_to_dict`: 전 필드 포함 + null review_notes → "" + null reference_image_ids → "[]"
  - `get_latest_world_guide`: JSON 파싱 + null 반환 + JSONDecodeError graceful
  - `auto_set_primary`: composite/non-composite 분리 경쟁 + still_id 모든 경쟁
  - `build_lineage_fields`: refs 없음 / with refs / primary 없을 때 최신 fallback

### 리뷰 반영 (Claude + Codex hotfix 완료)
- Claude Important #1 (초기 커밋에 반영 → Codex 재지적으로 원복): `generate_composite_image` face_asset 필터에 일관성 목적으로 `[outlook_id:%]` 제외 추가했으나, **Codex Important #1이 behavior regression으로 재지적** — `pipeline_gate.py:157`에 레거시 `outlook_id:{outlook_id}` 키가 character composite primary로 문서화되어 있어 legacy migration 경로가 깨짐. **hotfix로 HEAD 동작(단일 경로는 `[composite:%]`만 제외) 복원**.
- Codex Important #2: body-level 테스트 부족 → `generate_composite_image` 3개 에러 경로 + `regenerate_composites_for_entity` outlook branch + ImageService 4개 shim passthrough, 총 8개 추가 (41 → 33+8).
- Codex Minor #1: `_get_style_context` 시그니처 `Optional[str]` → `str = None` 으로 HEAD와 일치 복원.

### 회귀 체크
- 전체 223 passing (기존 187 → +41 추가, 삭제된 5 보전 → 순증가 36).
- FastAPI app smoke: `from app.main import app` OK.
- api/v1/images.py 호출 경로 무변경 확인 (grep `generate_reference_images_only\(`).
- backend/scripts/experiment_*.py 무변경 확인 — 해당 스크립트들은 `generate_reference_images_only`를 사용하지 않음 (grep 결과 0건).

---

## Phase 3b.3 — SceneImageService 분리 (완료, 2026-04-18)

### 커밋
- (예정) `refactor(phase-3b.3): ImageService scene 섹션을 SceneImageService로 분리 (~2,471줄)` + Codex/Claude 리뷰 hotfix

### 작업 내용
- `backend/app/services/scene_image_service.py` (신규 ~2,929줄) — ImageService의 씬 이미지 관련 13개 메서드 + 2개 scene-private helper 이관
  - 본체: `generate_images` (1,342줄 orchestrator) / `generate_variations_only` (159줄) / `generate_single_scene_image` (284줄)
  - 변주: `generate_scene_with_variations` / `_generate_variation` / `_set_variant_primary` / `regenerate_variation` / `recommend_variations`
  - 선택: `select_variant` / `select_original`
  - 참조 도우미: `_build_scene_ref_image_map` / `_resolve_refs_for_prompt` / `_validate_scene`
  - scene-private: `_get_visible_entities` / `_get_reference_image_map` (ImageService.compose_prompts와 공유를 위해 ImageService에도 유지)
  - 런타임 필수 helper 복제: `_create_validator` / `get_image` / `_build_image_index` / `_rewrite_t2i_with_image_refs` — Claude Critical 지적으로 추가
  - `image_service.py` module-level 함수 재사용: `_select_and_recommend_angle` / `_apply_fal_angle` / `_select_final_best` (Phase 3b.5에서 독립 모듈화 예정)
- `backend/app/services/image_service.py` (4,120 → 1,649줄, -2,471) — 위 메서드들을 thin wrapper로 전환. scene-private helper 2개는 wrapper로 유지.
- 호출자 무변경: `backend/app/api/v1/images.py` (8곳), `backend/app/core/steps/image_steps.py:478` (1곳).

### 설계 의도
- ImageService 단일 파일 책임 분산: reference(Phase 3b.2) + scene(Phase 3b.3) 완료로 dict/조회/upload/compose만 남김.
- 자동 추출 스크립트 사용: Python으로 line range 추출 + `self._image_to_dict` → `image_to_dict` 등 일괄 치환.
- ImageService는 api/v1/images.py 및 core/steps/image_steps.py 호환을 위해 thin wrapper 유지.

### 테스트 28 추가 (`tests/services/test_scene_image_service.py`)
- ImageService → SceneImageService 위임 검증 13건 (13개 scene 메서드 각각 @patch)
- `_validate_scene` 경로별: 파일 없음 / 고득점 / 저득점 needs_fix / 예외
- `_get_visible_entities` 경로별: None / 빈 문자열 / invalid JSON / list of ids / list of dicts
- `_get_reference_image_map` 경로별: 빈 리스트 / primary 없을 때 fallback
- `select_variant` / `select_original` 에러 경로: still not found / image not found
- `recommend_variations` 에러 경로: openai_api_key 없음 / still not found

### 리뷰 반영 (Claude Critical 4건 + Codex Important 2건)

**Claude Critical (모두 런타임 폭발 버그 — 커밋 전 전수 수정)**
1. scene_image_service.py에 `OpenAIClient`, `WorldGuideGenerator`, `_build_final_scene_prompt`, `_select_and_recommend_angle`, `_apply_fal_angle`, `_select_final_best` import 누락 → NameError 방지 목적 추가.
2. `self._create_validator`, `self._build_image_index`, `self._rewrite_t2i_with_image_refs`, `self.get_image` 미정의 → SceneImageService에 복제 추가 (4개 모두).
3. `image_service.py`의 `_build_image_index`가 `@staticmethod` 없이 self도 없이 정의되어 있던 기존 버그 → @staticmethod 추가로 복구.
4. `regenerate_variation`의 `edit_type` 미정의 변수 → `"fal_angle"` 리터럴로 수정.

**Codex Important**
1. `ImageService.regenerate_variation` wrapper가 `*args, **kwargs`로 바뀌어 strict signature preservation이 깨짐 → 원래 시그니처 `(image_id, angle_json, color_prompt, ip)`로 복원.
2. 누락된 body-level 테스트 → `select_original` image.not_found, `recommend_variations` openai_key_missing + still_not_found 총 3건 추가.

### 회귀 체크
- 전체 251 passing (기존 223 → +28 순증가).
- FastAPI app smoke: `from app.main import app` OK.
- api/v1/images.py 8개 호출 경로 시그니처 일치 확인.
- `backend/app/core/steps/image_steps.py:478` `svc.generate_images(...)` 호출 보존.

---

## Phase 3b.5 (부분) — 역의존 제거 + scripts import 전환 (완료, 2026-04-18)

### 커밋
- `507d75d` `refactor(phase-3b.5-part1): scene→image_service 역의존 제거 + fal_angle helpers 모듈 분리`

### 작업 내용
- `backend/app/services/fal_angle_helpers.py` (신규 241줄): `_select_and_recommend_angle` / `_apply_fal_angle` / `_select_final_best` 3개 module-level helper를 독립 모듈로 이관 (이름 prefix 제거).
- `backend/app/services/scene_image_service.py`: `from app.services.image_service import _select_and_recommend_angle, ...` → `from app.services.fal_angle_helpers import ...` alias. **scene_image_service → image_service 역의존 완전 제거**.
- `backend/app/services/image_service.py`: 3개 module-level 함수를 fal_angle_helpers의 alias(backward compat)로 축소 — 1,649 → 1,445줄 (-204).
- `backend/scripts/experiment_set_{v3,v4,v5,v8,v9,regen,shots}.py` (7개): `from app.services.image_service import _build_final_scene_prompt` → `from app.services.prompt_service import build_final_scene_prompt as _build_final_scene_prompt`.

### 회귀 체크
- 251 passing 유지.
- `_build_final_scene_prompt` module-level wrapper (image_service.py)는 test_prompt_service의 wrapper match test를 위해 유지 (prompt_service.build_final_scene_prompt 호출).

---

## Phase 3b.5b — ImageService wrapper 20개 제거 + 호출자 전환 (완료, 2026-04-18)

### 커밋
- (예정) `refactor(phase-3b.5b): ImageService wrapper 20개 제거 + api/steps 호출자 전수 전환`

### 작업 내용

**제거한 wrapper 20개 (image_service.py)**
- reference 7: `generate_base_references` / `generate_composites` / `generate_reference_images_only` / `generate_single_entity_image` / `generate_composite_image` / `regenerate_composites_for_entity` / `_validate_reference`
- scene 13: `generate_images` / `generate_variations_only` / `generate_single_scene_image` / `recommend_variations` / `generate_scene_with_variations` / `_generate_variation` / `_set_variant_primary` / `regenerate_variation` / `select_variant` / `select_original` / `_build_scene_ref_image_map` / `_resolve_refs_for_prompt` / `_validate_scene`

**호출자 전수 전환**
- `backend/app/api/v1/images.py`: 신규 factory `_make_reference_service` / `_make_scene_service` 추가. 13개 호출 지점 전환 (generate_entity_image, _trigger_composite_regen bg, generate_composite_image, generate_still_image, _run_image_gen_in_background, _run_ref_gen bg, _run_var_gen bg, recommend_variations, generate_with_variations, edit_angle, edit_color, select_variant, select_original). `_make_service` 기존 factory는 ImageService 잔존 메서드용으로 유지.
- `backend/app/core/steps/image_steps.py`: 3곳 전환 — `RefImageGenStep` → ReferenceImageService / `CompositeImageGenStep` → ReferenceImageService / `SceneImagePipelineStep` → SceneImageService.

**Test 정리**
- `test_reference_image_service.py`: 7개 wrapper delegation 테스트 + 1개 signature parity 테스트 제거. body-level + 4개 helper shim passthrough 테스트 유지.
- `test_scene_image_service.py`: 13개 wrapper delegation 테스트 제거. 사용 안 되는 `patch`, `Path`, `ImageService` import 정리.

**image_service.py imports 정리**
wrapper 제거 후 unused가 된 25개 import 제거 — hashlib, ThreadPoolExecutor, as_completed, CharacterOutlook, RelationFact, RelationParticipant, WorldGuide, GeminiImageClient, ModerationError, gemini_key_count, get_next_key, OpenAIClient, PromptSanitizer, ProvenanceRecorder, build_visual_dependency_graph, topological_sort_entities, build_scene_dependency_graph, topological_sort_scenes, ReferenceImageGenerator, SceneImageGenerator, WorldGuideGenerator, get_module_info, ProgressTracker.

### 리뷰 반영 (Codex + Claude 병행)

**Claude Important 2건**
1. `reference_image_service.py` / `scene_image_service.py` 모듈 docstring의 "ImageService는 호환을 위해 각 메서드를 thin wrapper로 유지" 문구가 Phase 3b.5b 이후 사실이 아님 → Phase 3b.5b 결과 반영으로 갱신.
2. `image_service.py` 모듈 docstring "Phase 3b.2~5b를 거쳐" 표현을 "완료 후"로 확정형으로 교체.

**Codex Important 1건**
- Scene-side 테스트 커버리지 축소 (wrapper delegation 13건 제거 후 `generate_images`, `generate_variations_only` 등 7개 메서드 직접 테스트 없음) → signature 보존 테스트 14건 추가 (SceneImageService 9 + ReferenceImageService 5). api/v1/images.py 및 image_steps.py와 키워드 호출 계약 고정.

### 회귀 체크
- 244 passing (230 + signature 14).
- FastAPI app smoke: `from app.main import app` OK.
- `image_service.py`: 1,445 → **1,224 (-15%)** (wrapper 제거 + imports 정리).
- 누적 감소: **5,281 → 1,224 (-77%)** (Phase 3b.1 + 3b.2 + 3b.3 + 3b.5 part1 + 3b.5b).
- ImageService 잔존 책임: 공용 이미지 CRUD (list/get/update/regenerate/primary/upload) + scene 단위 유틸 (compose_prompts, _get_visible_entities, _build_image_index 등) + validator + generation trace 조회 + composer prompt 설정.
- fal_angle alias re-exports (`_apply_fal_angle`, `_select_and_recommend_angle`, `_select_final_best`) 및 `_build_final_scene_prompt` 모듈 wrapper는 backward compat를 위해 유지.

---

## Phase 3b.6 — `_analyze_one` DTO 전환 (완료, 2026-04-18)

### 커밋
- (예정) `refactor(phase-3b.6): SceneDetailStep._analyze_one을 class method로 승격 + closure → ctx 파라미터`

### 작업 내용

`SceneDetailStep._execute`의 400줄 nested function `_analyze_one`을 class method로 승격. 기존 closure 변수 13개를 `ctx: SceneAnalysisContext` 파라미터 접근(`ctx.X`)으로 교체.

**1. `_execute` 상단 단순화**
이전: `ctx = SceneContextLoader(self).load_all()` 후 18개 closure alias unpack.
이후: 3개만 로컬 alias 유지 (`segments` / `selected_map` / `shot_scenes_map` — `_execute` 본체에서만 쓰임).

**2. `_analyze_one` 승격**
이전:
```python
def _analyze_one(seg, shot_info=None):
    all_visible = scene_visible.get(si, [])  # closure
    ...
```
이후:
```python
def _analyze_one(self, seg, shot_info, ctx, system, schema):
    all_visible = ctx.scene_visible.get(si, [])
    ...
```

교체된 closure → `ctx.X` (13개): `scene_visible` / `summaries` / `shot_director_ve` / `dependencies` / `outlook_data` / `entities` / `scene_shots_map` / `beats_by_scene` / `fixed_elements_by_scene` / `staging_map` / `set_design_hints` / `shot_types_block` / `shot_director_vr`.

**3. 호출 시그니처 변경**
- `pool.submit(_analyze_one, seg, sh)` → `pool.submit(self._analyze_one, seg, sh, ctx, system, schema)` (x2, retry 포함)

**4. 신규 테스트** (`tests/core/test_scene_detail_analyze_one.py`, 12 tests)
- 기본 결과 구조 (scene_index / _shot_index / visible_entities)
- fixed_elements 주입 (applies_to_shots 일치/불일치)
- VE 위반 retry + retry 후에도 위반 시 강제 제거
- outfit_assignments 정리 + 자동 할당 + bare→composite 치환
- t2i_prompt 공백 정리
- 빈 엔티티 fallback
- legacy path (shot_info=None, Claude 리뷰 Important 반영)

### 리뷰 반영 (Claude + Codex 병행)

**Claude**
- **Critical**: L535 `shot_scenes_map.get(si, [])` — legacy path(shot_info=None)에서 bare name 잔존 → `ctx.shot_scenes_map`으로 교체. 테스트에서 커버되지 않아 놓쳤던 부분.
- **Important #1**: legacy 경로 테스트 추가 (`test_analyze_one_legacy_no_shot_info`).
- **Important #2**: dead 코드 `dep_info = next(...)` 제거 (단일 선언 후 미사용).

**Codex** (Claude와 병행 리뷰)
- **Important #1 중복 확인**: shot_info=None 경로 테스트 누락 (Claude와 동일 — 이미 반영).
- **Important #2**: `_mock_llm_result` helper가 실제 스키마 필수 필드(heading / beat_title / scene_type / variant_label / camera_effect) 누락 → 스키마 drift 시 false-positive 위험. mock에 최소 필수 필드 추가.
- Minor: `shot_director_vr` 치환 경로 + `still_frame_prompt` 정리 경로 테스트는 후속 이월 (별도 작업).

### 회귀 체크
- 256 passing (244 + 12).
- FastAPI app smoke: `from app.main import app` OK.
- `detail_steps.py`: 904 → **885 (-19)** (closure unpack + dead code 제거).

---

## 누적 파일 변경 (Phase 0~4.5 + Phase 3b.1/3b.2/3b.3/3b.4/3b.5 part1)

### 신규 파일 (23개)
- `backend/app/core/applicability.py` (Phase 1)
- `backend/app/core/checkpoint_io.py` (Phase 1)
- `backend/app/core/step_catalog.py` (Phase 1)
- `backend/app/core/settings_registry.py` (Phase 2)
- `backend/app/core/dto/__init__.py` (Phase 3)
- `backend/app/core/dto/scene_analysis.py` (Phase 3)
- `backend/app/core/steps/scene_context_loader.py` (Phase 3)
- `backend/app/services/checkpoint_sync/__init__.py` (Phase 2)
- `backend/app/services/checkpoint_sync/_base.py` (Phase 2)
- `backend/app/services/checkpoint_sync/entity_sync_service.py` (Phase 2)
- `backend/app/services/checkpoint_sync/relation_sync_service.py` (Phase 2)
- `backend/app/services/checkpoint_sync/scene_still_sync_service.py` (Phase 2)
- `backend/app/services/checkpoint_sync/outlook_sync_service.py` (Phase 2)
- `backend/app/services/checkpoint_sync/episode_projection_service.py` (Phase 2)
- `backend/app/services/checkpoint_sync/orchestrator.py` (Phase 2)
- `backend/app/services/shot_selection_service.py` (Phase 2)
- `backend/app/services/snapshot_service.py` (Phase 2)
- `backend/app/services/analysis_dispatch_service.py` (Phase 4.1)
- `backend/app/services/prompt_service.py` (Phase 3b.1)
- `backend/app/services/reference_image_service.py` (Phase 3b.2)
- `backend/app/services/image_service_helpers.py` (Phase 3b.2)
- `backend/app/services/scene_image_service.py` (Phase 3b.3)
- `backend/app/services/fal_angle_helpers.py` (Phase 3b.5 part1)

### 신규 테스트 (10개 파일, 89 tests)
- `tests/test_checkpoint_io.py` (8)
- `tests/test_manifest_fields.py` (16)
- `tests/test_step_catalog.py` (13)
- `tests/test_applicability.py` (6)
- `tests/test_settings_registry.py` (6)
- `tests/test_api_endpoint_decorator.py` (6)
- `tests/services/test_checkpoint_sync_services.py` (10)
- `tests/services/test_shot_selection_service.py` (3)
- `tests/services/test_snapshot_service.py` (5)
- `tests/core/test_scene_analysis_dto.py` (8)
- `tests/services/test_analysis_dispatch_service.py` (26) (Phase 4.1)
- `tests/pipeline/test_outlook_dedup.py` (15) (Phase 4.3)
- `tests/services/test_relation_delta_sync.py` (6) (Phase 4.4)
- `tests/services/test_outlook_delta_sync.py` (4) (Phase 4.5)
- `tests/services/test_prompt_service.py` (26) (Phase 3b.1)
- `tests/services/test_reference_image_service.py` (30) (Phase 3b.2, 3b.4 계약 흡수)
- `tests/services/test_image_service_helpers.py` (11) (Phase 3b.2)
- `tests/services/test_scene_image_service.py` (28) (Phase 3b.3)

### 기존 파일 수정 (주요)
- `backend/tests/test_step_manifest_v3.py`: 완전 재작성 (22 tests passing)
- `backend/app/core/step_manifest.py`: manifest 필드 확장 + docstring
- `backend/app/core/step_runner.py`: applicability/modifies_checkpoints/atomic_write 반영
- `backend/app/api/v1/steps.py`: 1,518 → 496줄
- `backend/app/api/v1/episodes.py`: AnalysisService 2 엔드포인트 deprecated
- `backend/app/api/v1/entities.py`: _sync_t2i_appearance_counts 역의존 해소
- `backend/app/core/steps/detail_steps.py`: Loader 호출로 170줄 감소
- `backend/app/core/steps/image_steps.py`: step 경계 재정의 + force SQL 필터
- `backend/app/core/steps/t2i_review_step.py`: checkpoint_io 사용 + 손상 skip
- `backend/app/core/steps/set_design_step.py`: runtime settings 체크 제거
- `backend/app/core/applicability.py`: SettingsRegistry 경유 (Phase 2)
- `backend/app/api/deps.py`: api_endpoint 데코레이터 추가
- `backend/app/services/image_service.py`: skip_composite 파라미터 추가

### 문서
- `docs/architecture-refactor-final/baseline.md`: 진행 현황 + 실측 수치
- `docs/architecture/00-overview.md`, `06-data-contracts.md`: 수치 갱신

---

## 2026-04-20~21 — Phase 5 Frontend + 세션 E (v0.5.25 → v0.6.0)

**브랜치**: `shot-more` (main 병합 안 함, 로컬 커밋)

**8 커밋 요약**:
| 커밋 | 버전 | 세션 | 핵심 |
|------|------|------|------|
| `8da8473` | v0.5.25 | A | React Query 설치 + 5개 useQuery 훅 (useEpisode/useStills/useEntities/useWorldGuide/useProgress) |
| `dd5e9f2` | v0.5.26 | B | useEntities/useWorldGuide + useShotToggle mutation (per-key reverse-patch) |
| `d893190` | v0.5.27 | B2 | useGenStatus/useScreenplay/useWorldRules + useProgress polling 통합 (undefined→null) |
| `89857d2` | v0.5.28 | C | EpisodeHeader/WorldGuide/ActionBar 3 컴포넌트 + PipelineStepsPanel onSettled + expectedSettlementRef (B2 H2 해소) |
| `5eab95e` | v0.5.29 | D | Dashboard/Episodes/ProjectDetail 전환 + Entities invalidate(B M1) + 9개 mutation 훅 |
| `66d4e4e` | v0.5.30 | D 후속 | Claude H 6건 (canManage/role rollback/조건부 polling/toast/useActivities overview/AbortController) |
| `4a88507` | v0.5.31 | C2 | Stills 분할 + pipeline_explained.html + architecture docs 최신화 |
| `631a082` | v0.6.0 | E | backend except:pass 38→0 + README 갱신 + **Phase 5 완결 tag** |

### 실측 LOC / useState

| 지표 | 시작 (v0.5.24) | 완료 (v0.6.0) | 변화 |
|------|----------------|---------------|------|
| EpisodeDetail LOC | 1,358 | **148** | **−89%** |
| EpisodeDetail useState | 34 | **1** | **−97%** |
| 페이지 LOC 합계 | 2,428 | 975 | −60% |
| React Query query 훅 | 0 | **16** | +16 |
| React Query mutation 훅 | 0 | **9 파일 / 27 mutation** | +27 |
| Backend `except: pass` | 38 | **0** | **−100%** |
| Frontend 컴포넌트 (episode/) | 0 | 4 (Header/WorldGuide/ActionBar/Stills) | +4 |

### 신규 파일

**Hooks (query)**: `useEpisode`, `useStills`, `useEntities`, `useWorldGuide`, `useProgress`, `useGenStatus`, `useScreenplay`, `useWorldRules`, `useProjectList`, `useEpisodesList`, `useEpisodesProgress`, `useProject`, `useMembers`, `useActivities`, `useProjectSummary`, `usePlanningDoc`, `useStillImages`

**Hooks (mutation)**: `useShotToggle`, `useCreateProject`, `useAnalyzeEpisode`, `useCreateEpisode`, `useMemberMutations`(3), `usePlanningDocMutations`(3), `useStillMutations`(15)

**Components**: `EpisodeHeader` (80), `EpisodeWorldGuide` (359), `EpisodeActionBar` (146), `EpisodeStills` (524)

**Types**: `types/project.ts`

**Docs**: `frontend/public/pipeline_explained.html` (1,010 LOC 비전문가용), `docs/architecture/00~07-*.md` 7개 최신화, `docs/product-roadmap-discussion.md`

### 듀얼 리뷰 반영 (각 세션)

- **A**: C1 mutation ref 패턴 (매 렌더 재생성 방지), C2 setState 비동기 가드
- **B**: C1 per-key reverse-patch (rollback), H2 stillsKey dynamic
- **B2**: Critical TanStack v5 undefined → null, H3 refetchOnMount 'always'
- **C**: H1 generatingImages 자동 해제, H2 hydratedRef, H(Codex) expectedSettlementRef
- **D**: H1 useEpisodesList refetchOnMount, H2 member 무효화 범위, H3 planningDoc 404-only null
- **D 후속**: H1 canManage my_role, H2 role rollback onSettled, H3 조건부 polling, H4 toast 이관, H5 useActivities overview 전용, H6 AbortController
- **C2**: C1 stillIds useMemo, C2 dead state 제거, C3 이중 refetch 제거, H1/H2 stillId variables

### 백엔드 수정 (세션 E)

21개 파일에 `except: pass` 제거:
- **LOG 25건**: `image_service.py`(4), `scene_image_service.py`(4), `images.py`(4), `entities.py`(2), `steps.py`(2), `entity_sync_service.py`(2), 그 외 1개씩
- **INTENTIONAL 13건**: `main.py`(헬스체크), `analysis_dispatch_service.py`(2 rollback), `checkpoint_io.py`(tmp cleanup), `scene_image_generator.py`(2), `pdf_validator.py`(2 fallback), `image_tracer.py`(opik optional), 그 외
- **로깅 없던 파일 5개**에 `logger` 추가: `export_service.py`, `image_service_helpers.py`, `project_service.py`, `entity_sync_service.py`, `images.py`
- 모든 파일 `py_compile` 통과 (21/21)
- `backend/app` + `backend/scripts` 내 `except: pass` 0건 달성

### 문서 최신화

- `docs/architecture/02-entity-extraction.md`: `entity_all_*` upstream `shot_extract` → `shot_validator` 정정
- `docs/architecture/03-shot-analysis.md`: `shot_validator` 섹션 + `shot_selection` v4 2중 캡
- `docs/architecture/04-scene-direction.md`: `scene_detail` v2→v9 + `zoom_in_detail` + `consumes_downstream` 2-pass
- `docs/architecture/06-data-contracts.md`: **Frontend Phase 5 React Query** 섹션 신설 (queryKey / mutation / polling)
- `README.md`: `0.0.0.0` 바인딩, v4 파이프라인 흐름, pipeline_explained.html 링크

### 생산성 지표

- 2~4주 예상 → **1일 집중 투입**으로 완료 (기존 Phase 0~4 기반 안정성 덕)
- 세션당 평균 커밋 1~2개, 모두 듀얼 리뷰(Claude + Codex) 통과
- E2E 테스트 별개 세션(#431)로 실측 검증 진행 중

### 이월 작업

- Task #398: 레거시 테스트 56건 정리 — 사용자 유보
- H7: Episode summary 필드 백엔드 계약 확인 — 백엔드 팀 소통 필요
- **제품 방향**: `docs/product-roadmap-discussion.md` 기반 영화 현장 인터뷰 → v0.7.0 로드맵
