# TheRoad Scene Lab — Changelog

## v0.6.0 (2026.04.20) — Phase 5 완결 + 세션 E: 품질/문서 마무리

**Phase 5 Frontend 리팩토링 + Backend 관찰성 개선 마일스톤**.

### 배경
세션 A~D+C2로 Frontend React Query 전환 완료. 세션 E에서 백엔드 silent failure 제거 + 설치 문서 최신화로 v0.6.0 릴리스.

### 세션 E: Backend `except: pass` 정리 (38건)

**LOG 25건** — 조용히 삼키던 에러에 `logger.warning`/`logger.error` 추가:
- `image_service.py` (4곳): llm_config, angle_applied, visible_entities, validation_result
- `scene_image_service.py` (4곳): llm_config, shot_staging, labeled_refs, angle_json
- `reference_image_service.py`, `export_service.py`, `fal_angle_helpers.py`, `image_service_helpers.py`, `project_service.py`: 각 1곳
- `entity_sync_service.py` (2곳): short_id 형식 오류
- `api/v1/projects.py`, `api/v1/steps.py` (2곳), `api/v1/entities.py` (2곳): llm_config_json + set_design + t2i_variations_json
- `api/v1/images.py` (4곳): progress error 마킹 + reference/scene checkpoint
- `prompt_loader.py`, `image_tracer.py`, `analysis_steps_legacy.py`, `image_steps.py`: 각 1곳

**INTENTIONAL 13건** — 의도적 무시임을 주석으로 명시 (동작 변경 없음):
- `main.py` 헬스체크 DB probe (degraded 상태로 반영)
- `analysis_dispatch_service.py` (2곳) rollback best-effort
- `checkpoint_io.py` tmp 정리 (원본 예외 보존)
- `scene_image_generator.py` (2곳) stable_traits 파싱 실패 → 보조 설명만 생략
- `pdf_validator.py` (2곳) pdftoppm→sips fallback
- `image_tracer.py` opik optional import
- `entities.py` 체크포인트 마킹 (DB 업데이트 이미 완료)
- 기타 원본 응답 유지 목적의 정상 분기

**검증**:
- `backend/app/` + `backend/scripts/` 내 `except...pass` 패턴 **0건** 달성.
- 모든 수정 파일 py_compile 통과 (21/21).

### 문서 업데이트

- `README.md`: 
  - uvicorn 바인딩 `127.0.0.1` → `0.0.0.0` (LAN 접속용)
  - 파이프라인 흐름 v4 (beat→shot + scene_consistency) 반영
  - `/pipeline_explained.html` 링크 추가

### Phase 5 누적 요약 (v0.5.24 → v0.6.0)

| 지표 | 시작 (v0.5.24) | 완료 (v0.6.0) | 변화 |
|------|----------------|---------------|------|
| EpisodeDetail LOC | 1,358 | 148 | −89% |
| EpisodeDetail useState | 34 | 1 | −97% |
| 페이지 LOC 합계 | 2,428 | 2,085 | −14% |
| React Query 훅 (query) | 0 | 16 | +16 |
| React Query 훅 (mutation) | 0 | 9 파일 / 27개 mutation | +27 |
| Backend `except: pass` | 38 | 0 | −100% |

### 주요 산출물

**Frontend 리팩토링**:
- EpisodeDetail 쉘 컴포넌트화 (EpisodeHeader/WorldGuide/ActionBar/Stills)
- React Query 기반 데이터 페칭 전면 전환
- Shot toggle optimistic update + per-key rollback
- Still 이미지 병렬 조회(`useQueries`) + per-still invalidate

**문서화**:
- `docs/architecture/00~07-*.md` v0.5.x 반영 (shot_validator, scene_detail v9, zoom_in_detail)
- `frontend/public/pipeline_explained.html` — 비전문가용 7-stage 파이프라인 시각화
- Sidebar에서 `/pipeline_explained.html` 링크 접근

**Backend 관찰성**:
- 38건 silent failure 제거 → 로깅/주석 명시
- 디버깅/운영 관찰성 대폭 개선

### 이월

- Task #398: 레거시 테스트 56건 정리 (별도 세션)
- H7 Episode summary 필드 백엔드 계약 확인

---

## v0.5.31 (2026.04.20) — Phase 5 세션 C2: EpisodeDetail Stills 분할 + 일반인용 HTML 문서

### 배경
세션 D까지 완료 후 `EpisodeDetail.tsx`는 790 LOC / useState 12. 그중 약 600줄이 Scene Stills 섹션(15개 핸들러 + 9개 useState + 큰 JSX). 세션 C2 목표:
1. **Stills 전체 이관** — 독립 컴포넌트 + mutation 훅 묶음으로 분리
2. **일반인용 파이프라인 설명 페이지** — 비전문가(PM/감독/마케터) 대상 HTML
3. **아키텍처 문서 최신화** — `docs/architecture/` 7개 파일 v0.5.x 반영

### 신규 파일

**훅/컴포넌트**:
- `frontend/src/hooks/api/useStillImages.ts` — `useQueries`로 N개 still 이미지 병렬 조회 + `invalidate(stillId)` helper. 캐시 키: `['project', projectId, 'still', stillId, 'images']`.
- `frontend/src/hooks/api/mutations/useStillMutations.ts` — 15개 mutation 묶음 (recommend/generate/editAngle/editColor/applyAngleColor/regenerateWithPrompt/generateVariation/setRepresentative/selectVariant/selectOriginal/regenerateVariant/saveVariationPrompt/saveT2iVariation/saveT2i/patchStill).
- `frontend/src/components/episode/EpisodeStills.tsx` (524 LOC) — stills UI 전체 + 핸들러/state 이관.

**문서**:
- `frontend/public/pipeline_explained.html` (1,010줄) — 7-stage 구조(📄원본→🎬씬→👥요소→🎭연출→📷샷→🖼️이미지→✨편집). SVG 흐름도 + 예시 데이터 + dark theme + 반응형.
- `docs/architecture/00~07-*.md` — 7개 파일 v0.5.x 반영. 주요 변경:
  - `02-entity-extraction.md`: `entity_all_*` upstream을 `shot_validator`로 정정 (v0.5.4)
  - `03-shot-analysis.md`: `shot_validator` 전용 섹션 + `shot_selection` v4 2중 캡
  - `04-scene-direction.md`: `scene_detail` v2→v9 + `zoom_in_detail` + `consumes_downstream` 2-pass
  - `06-data-contracts.md`: **Frontend Phase 5 React Query** queryKey/mutation/polling 정리 (신규 섹션)

### EpisodeDetail 실측 (세션 A 시작 대비 누적)

| 지표 | 세션 A 시작 | 세션 C2 후 | 누적 감소 |
|------|-------------|-----------|----------|
| LOC | 1,358 | **148** | −1,210 (**−89%**) |
| useState | 34 | **1** | −33 (−97%) |
| useEffect | 7 | 1 | −6 |
| await api | 39 | 4 | −35 |

EpisodeDetail는 shell 컴포넌트로 축소: Header/WorldGuide/ActionBar/PipelineStepsPanel/Stills + ThreeStagePanel + running→terminal transition detection.

### Sidebar 링크 추가

- Dashboard/Project 사이드바 footer에 "파이프라인 설명" 링크 — `target="_blank"`로 `/pipeline_explained.html` 신규 탭에서 열기.

### 듀얼 리뷰 수용 (Claude Critical 3 + High 5)

**Critical**:
- **C1 stillIds 매 렌더 새 배열**: `useMemo(() => stills.map(s => s.id), [stills])` + 미사용 `invalidate*` void import 제거.
- **C2 t2iNeedsReconvert dead state**: setter만 쓰는 상태 + useEffect 전체 제거.
- **C3 이중 refetch**: `saveVariationPrompt`/`saveT2iVariation`/`saveT2i`/`patchStill` mutation의 `onSuccess: invalidateStills` 제거 — 호출측 핸들러가 `fetchStills(true)` preserveScroll 래퍼로 refetch 책임. dedup은 되지만 의도 이중화 제거.

**High**:
- **H1/H2 editAngle/editColor/applyAngleColor의 stillId 매칭**: 이전엔 prefix invalidate로 N개 쿼리 전체 재조회 + per-still pending 판정 불가. `variables`에 `stillId` 추가 → `invalidateStill(stillId)` + `isPendingFor(mutation, still.id)`로 해당 카드만 스피너.
- **H3 3중 invalidate 경쟁**: transition detection + onSettled + mutation onSuccess가 동일 stills 키 invalidate. 현재 TanStack dedup으로 실제 회귀 없음 — 주석만 남김.
- **H4 onSaveT2i promptB drop**: 원본 EpisodeDetail도 `promptA` 단일 인자만 사용 — 회귀 아님.
- **H5 (= C1)**: stillIds memoize.

### 이월

- 세션 E: backend `except: pass` 38건 + docs v0.6.0 tag

---

## v0.5.30 (2026.04.20) — Phase 5 세션 D 리뷰 후속 (Claude H1/H2/H3/H4/H5/H6)

### 배경
v0.5.29 커밋 후 Claude agent 리뷰에서 Critical 2건 + High 7건 추가 식별. Critical 2건은 이미 Codex 지적으로 v0.5.29 내 수정됨. High 6건 수용 (H7 유보 — 백엔드 계약 확인 필요).

### 수정

- **H1 canManage 오판 (ProjectDetail)**: `members`가 탭 활성 시에만 fetch → overview/activity 탭에서 `canManage=false` 오판. `project.my_role`(useProject 응답 포함)로 탭 독립 판정.
- **H2 role 변경 rollback (useChangeMemberRole)**: `onSuccess` → `onSettled`로 변경 → 실패 시에도 members 재조회로 select UI를 서버 기준 롤백.
- **H3 useEpisodesProgress 무조건 polling**: terminal 상태에서도 3초 polling 영구 지속 → `hasAnyRunning` 조건부 polling (useProgress와 동일 패턴).
- **H4 planningMsg unmount race (ProjectDetail)**: 120초 업로드 후 unmount 시 setState 경고 + 메시지 유실 → `planningMsg` state 제거, `addToast`로 이관 (전역 notification).
- **H5 recentActivities overview 전용**: overview 탭에서만 필요한데 모든 탭에서 fetch → `enabled: tab === 'overview'` 추가.
- **H6 useCreateEpisode timeout + 타입**: PDF 업로드 fetch 직접 호출로 `api<T>`의 30초 timeout 우회 → 120초 `AbortController` + `CreateEpisodeError` 타입.

### 유보

- **H7 Episode summary 필드 계약**: `/projects/{id}/episodes/` 응답에 summary 포함 여부 백엔드 확인 필요. 현재 `any[]` → typed 이관 시 표면화 가능성만 있고 실제 회귀는 없음.

---

## v0.5.29 (2026.04.20) — Phase 5 세션 D: 다른 페이지 React Query 전환

### 배경
세션 C 완료 후 `EpisodeDetail.tsx`는 훅/컴포넌트 패턴 확립. 같은 패턴을 `Dashboard`/`Episodes`/`ProjectDetail`로 전파하고, `Entities.tsx`에 Codex B M1 이월(저장 후 EpisodeDetail 캐시 미무효화) 해소.

### 신규 Query 훅 (`hooks/api/`)

- **`useProjectList`** — `['projects']`. Dashboard.
- **`useEpisodesList`** — `['project', id, 'episodes']`. analyzing 상태일 때 3초 polling + `refetchOnMount: 'always'`.
- **`useEpisodesProgress`** — `useQueries`로 분석 중 에피소드 병렬 polling. 각 쿼리 키는 `['episode', episodeId, 'progress']`로 `useProgress`와 공유.
- **`useProject`** — `['project', id]` exact.
- **`useMembers`** — `['project', id, 'members']`. enabled 플래그로 탭 활성 시에만 fetch.
- **`useActivities(projectId, perPage?)`** — `['project', id, 'activities', perPage]`. overview=10/activity 탭=전체 분리 캐싱.
- **`useProjectSummary`** — `['project', id, 'summary']`. style-rules의 world_summary.
- **`usePlanningDoc`** — `['project', id, 'planningDoc']`. 404만 null, 그 외 throw.

### 신규 Mutation 훅

- **`useCreateProject`** — Dashboard.
- **`useAnalyzeEpisode`**, **`useCreateEpisode`** — Episodes. `CreateEpisode`는 multipart/form-data.
- **`useAddMember`**, **`useRemoveMember`**, **`useChangeMemberRole`** — `members` + `['project', id]` + `['projects']` 3개 캐시 동시 무효화 (Codex D H2).
- **`useUploadPlanningDoc`**, **`useDeletePlanningDoc`**, **`useAnalyzePlanningDoc`** — 3종.

### 페이지 전환 실측

| 페이지 | LOC 전/후 | useState 전/후 |
|--------|----------|---------------|
| Dashboard.tsx | 227 → 196 (−14%) | 6 → 3 |
| Episodes.tsx | 360 → 268 (−26%) | 8 → 5 |
| ProjectDetail.tsx | 483 → 363 (−25%) | 16 → 2 |
| Entities.tsx | 1254 → 1258 (+4, invalidate 추가) | — |

**페이지 LOC 누적 2,324 → 2,085 (−239, −10%)**.

### Entities.tsx invalidate (Codex B M1 이월 해소)

`handleSave()` 성공 후 `queryClient.invalidateQueries(['project', id, 'entities'])` 한 줄 추가. EpisodeDetail의 `useEntities` 캐시가 자동 갱신되어 다른 탭에서 편집 시 반영.

### 듀얼 리뷰 수용

**Codex (Critical 0, High 3)**:
- **H1 useEpisodesList refetchOnMount 누락**: 5분 staleTime + cached `uploaded/error` 데이터 조합에서 analyze 직후 재진입 시 polling 시작 안 됨. `useProgress`와 동일하게 `refetchOnMount: 'always'` 추가.
- **H2 member mutation 무효화 범위**: `project.member_count` (헤더)와 Dashboard 카드(`member_count`, `my_role`)가 stale. `['project', id]` + `['projects']` 동시 무효화.
- **H3 usePlanningDoc catch-all**: 모든 예외를 null로 swallowing → 401/500/timeout도 "기획서 없음"으로 캐시. 404만 null, 나머지는 throw.

### 이월

- 세션 C2: `EpisodeDetail` Stills 분할 (SceneVariationCard 20+ props drilling 재설계).
- 세션 E: backend `except: pass` 38건 + docs v0.6.0.

---

## v0.5.28 (2026.04.20) — Phase 5 세션 C: EpisodeDetail 분할 + Codex B2 H2 해소

### 배경
세션 A/B/B2 완료 후 `EpisodeDetail.tsx` 1,208 LOC / useState 20. 세션 C 목표:
1. **컴포넌트 분할** — 독립적 UI 섹션을 `components/episode/` 하위로 추출
2. **이월 이슈 해소** — Codex B2 H2 (`PipelineStepsPanel onRefresh`가 POST started 직후 호출되어 stale UI 남음)

### 신규 컴포넌트 (`components/episode/`)

- **`EpisodeHeader.tsx` (80 lines)**: 에피소드 info bar + 요약 + `visual_world_rules` 표시.
- **`EpisodeWorldGuide.tsx` (332 lines)**: 월드가이드 편집(구조화/JSON 모드) + 씬 세그멘테이션 + 재분석 트리거.
  - `useWorldGuide` 내부 호출, `scene_split_threshold` 로컬 state 동기화, debounced preview fetch.
  - 저장 후 `invalidateQueries(['project', projectId, 'worldGuide'])`.
- **`EpisodeActionBar.tsx` (141 lines)**: `GenerationStatusPanel` + 생성/재시도/웹북 버튼.
  - `useGenStatus`/`useProgress` 내부 호출 (queryKey 공유 → 네트워크 1회).
  - `generatingImages`는 부모가 관리 (transition detection과 연동).

### PipelineStepsPanel 개선 (Codex B2 H2 해소)

**문제**: `onRefresh`가 `POST status: "started"` 직후 호출 → EpisodeDetail이 entities/worldGuide를 "옛 데이터"로 fresh 마킹 → 실제 완료 시 invalidate가 없어 5분 stale.

**해결**: `onSettled` prop 추가. `PipelineStepsPanel` 내부 5초 polling에서 running → terminal 전이 감지.

```ts
// prev 렌더의 running step_id 집합과 현재 steps 비교
const TERMINAL = new Set(['completed', 'partial', 'failed', 'cancelled'])
if (prevRunning.has(s.step_id) && TERMINAL.has(s.status)) someSettled = true
if (someSettled) onSettledRef.current()  // 세션 A C1 패턴: ref로 안정 참조
```

EpisodeDetail에서:
- `onRefresh`: started 시점 최소 invalidate (episode meta + progress + stills).
- `onSettled`: 완료 시점 결과 의존 캐시 일괄 invalidate (entities/worldGuide/worldRules/genStatus).

### EpisodeDetail 리팩터

- 제거된 import: `Badge`, `Button`, `GenerationStatusPanel`, `useNavigate`, `useWorldGuide`.
- 제거된 state/ref/callback: `showWorldGuide`, `editingWorldGuide`, `wgEditText`, `wgJsonMode`, `showSegmentation`, `splitThreshold`, `segmentPreview`, `reanalyzing`, `segmentDebounceRef`, `fetchSegmentPreview`, `fetchGenStatus`.
- JSX 섹션 제거: WorldGuide 편집 패널, 씬 세그멘테이션 패널, GenerationStatusPanel 직접 렌더, Action buttons inline 정의.

### 실측 감소

- EpisodeDetail LOC: 1,208 → **790** (−418, −35%)
- EpisodeDetail useState: 20 → **12** (−8)
- 세션 A 시작 대비 **누적**: LOC **1,358 → 790 (−568, −42%)**, useState **34 → 12 (−22, −65%)**

### 듀얼 리뷰 수용

**Claude (Critical 0, High 2, Medium 3)**:
- H1 ActionBar: `generatingImages`를 POST finally에서 바로 해제 → progress polling이 running을 반영하기 전 1렌더 동안 버튼 재활성. **progress가 running을 보이면 useEffect로 자동 해제**, 실패 시만 즉시 해제.
- H2 WorldGuide: `worldGuide` refetch 시 사용자 슬라이더 입력 덮어쓰기. **`hydratedRef` 패턴 — 최초 1회만 동기화, 저장 성공 시 재허용**.

**Codex (Critical 0, High 1, Medium 3, Low 1)**:
- **H (B2 H2 부분 미해소)**: 짧게 끝난 step은 첫 조회 시 이미 terminal → running을 못 본 경로는 `prevRunningStepsRef`로 감지 불가. **`expectedSettlementRef` 추가** — `handleRunStep`/`handleRunAll` POST 성공 시 기대 step 등록, 첫 terminal에서도 `onSettled()` 호출.
- M: `prevRunningStepsRef`가 projectId/episodeId 전환 시 미리셋 → false positive. **useEffect로 리셋**.
- M: ActionBar retry 버튼이 guard 미적용. **`startImageOp` helper로 통합** (retry 포함 동일 경로).
- Low: `saveThreshold` 예외 조용히 전파. **try/catch + error toast + 재throw**; 재분석 버튼은 실패 시 중단.

### 이월 (세션 C2)

- **Scene Stills 분할**: `SceneVariationCard`에 전달되는 prop 20+. 단순 JSX 이관만으로는 useState/콜백 소실 없음. 콜백+state 구조 재설계가 필요해 별도 세션으로 분리.

### 다음 세션 (Phase 5 잔여)

- **세션 C2**: Scene Stills 분할 — 콜백 묶음 훅(useStillImages/useStillMutations) + EpisodeStills 컴포넌트
- **세션 D**: ProjectDetail/Episodes/Dashboard 전환 + Entities.tsx invalidate(Codex B M1)
- **세션 E**: backend `except: pass` 38건 + docs v0.6.0

---

## v0.5.27 (2026.04.20) — Phase 5 세션 B2: 잔여 훅 4개 + polling 통합

### 배경
세션 B 완료 후 `EpisodeDetail.tsx` 1,263 LOC / useState 25. 세션 B2 목표:
1. **잔여 훅 4개** (genStatus/screenplay/worldRules/progress) — 마운트 effect 제거
2. **수동 setInterval polling 제거** — `useQuery` 내장 `refetchInterval`로 이관
3. **useState 20 목표 달성**

### 신규 훅

- **`hooks/api/useGenStatus.ts`**: `useQuery<GenStatus>`, 캐시 키 `['episode', episodeId, 'genStatus']`. progress transition + onRefresh에서 invalidate.
- **`hooks/api/useScreenplay.ts`**: `useQuery<ScreenplayText>` — fulltext + segment_context_chars. 변경 드물어 staleTime 기본 5분 충분.
- **`hooks/api/useWorldRules.ts`**: step 체크포인트 결과. `{ result: { data } }` envelope unwrap, step 미실행 시 data는 undefined. `retry: 1`.
- **`hooks/api/useProgress.ts`**: `refetchInterval: (query) => hasAnyRunning(query.state.data) ? 3000 : false` — running 중일 때만 3초 polling. `refetchIntervalInBackground: true` (기존 수동 setInterval 동작 재현). `hasAnyRunning` 헬퍼 export.

### 수정

- **types/episode.ts**: `GenStatus`, `ScreenplayText`, `WorldRules` 타입 추가.
- **EpisodeDetail.tsx**:
  - useState 5개 제거: `progress`, `genStatus`, `screenplayText`, `segmentContextChars`, `worldRules`.
  - useRef 1개 제거: `progressPollRef` (수동 setInterval 관리 폐기).
  - `fetchProgress`/`fetchScreenplayText`/`fetchGenStatus` async 본체 제거 → `fetchProgress`/`fetchGenStatus`는 `queryClient.invalidateQueries` 래퍼로 유지 (호출처 10+ 호환).
  - 마운트 effect 단순화: useQuery 자동 fetch만으로 충분 → `fetchProgress/fetchGenStatus/fetchScreenplayText()` 마운트 effect 삭제.
  - inline `visual_world_rules/result` useEffect → `useWorldRules` 훅.
  - polling useEffect: `setInterval/clearInterval` 관리 코드 제거, **transition 감지 + invalidate만** 담당.

### 실측 감소
- LOC: 1,263 → 1,208 (**-55**, 세션 A 시작 대비 **-150**)
- useState: 25 → 20 (**-5**, 세션 A 시작 대비 **-14**, 목표 달성)

### 듀얼 리뷰 수용

**Claude (Critical 0, High 2)**:
- H1: 전이 후 `['episode', episodeId]` prefix invalidate가 progress/screenplay까지 re-fetch → `exact: true` + 개별 키 invalidate (`genStatus`/`worldRules`만 명시).
- H2: `useWorldRules`의 `retry: 1`은 global default와 중복. 제거.

**Codex (Critical 0, High 3)**:
- **H1 (Critical급)**: 백엔드는 step 미실행 시 404가 아닌 `200 { status: "no_data", result: null }` 반환. 현재 queryFn이 `undefined` 반환 → TanStack Query v5가 에러로 처리. **`WorldRules | null` 타입으로 전환 + `status === 'no_data'` 명시 체크**.
- H3: `useProgress`/`useGenStatus`는 staleTime 5분 cached non-running → 재진입 시 polling 안 깨어남. **`refetchOnMount: 'always'` 추가**.
- (Low): 전이 invalidate 광역 → Claude H1과 동일 해소.

**이월 (세션 C)**: Codex H2 — `PipelineStepsPanel onRefresh`가 step 완료가 아닌 POST started 직후 호출됨. 실제 완료 감지하려면 PipelineStepsPanel 내부 polling에서 running→terminal 전이 감지 필요 (스코프 큰 구조 변경).

### 다음 세션 (Phase 5 잔여)
- **세션 C**: EpisodeDetail.tsx 분할 (5 sub-component, ~500 LOC 목표) — useState 기반 완성 후 분할.
- **세션 D**: ProjectDetail/Episodes/Dashboard 전환 + Entities.tsx invalidate(Codex M1 이월).
- **세션 E**: `except: pass` 38건 + docs v0.6.0.

---

## v0.5.26 (2026.04.20) — Phase 5 세션 B: useEntities/useWorldGuide + optimistic mutation

### 배경
세션 A 완료 후 EpisodeDetail.tsx 1,292 LOC / useState 28. 세션 B 목표:
1. **useEntities/useWorldGuide** — 남은 read-only 훅 전환
2. **useShotToggle mutation** — optimistic update + rollback 패턴 확립
3. **Codex H2 근본 해소** — `entities`를 projectId 기반 queryKey로 옮겨 수동 리셋 제거

### 변경
- **hooks/api/useEntities.ts (신규)**: `useQuery<Entity[]>`, 캐시 키 `['project', projectId, 'entities']` — projectId 전환 시 queryKey가 자동 분리되어 stale id 유출 불가능.
- **hooks/api/useWorldGuide.ts (신규)**: `useQuery<WorldGuide>`, 캐시 키 `['project', projectId, 'worldGuide']`. `style_rules?: Record<string, any>` 플렉시블 타입.
- **hooks/api/mutations/useShotToggle.ts (신규)**: `useMutation` + optimistic update.
  - `onMutate`: `cancelQueries` → `getQueryData` snapshot → `setQueryData`로 해당 scene의 shots_info.shots[idx].selected 토글 + selected_count 재계산.
  - `onError`: snapshot으로 rollback.
  - `onSettled`: `invalidateQueries`로 서버 기준 재동기화.
- **types/episode.ts**: `Entity` 타입 추가 (EpisodeDetail 인라인 선언 이관).
- **EpisodeDetail.tsx**:
  - `[entities, setEntities]` + `entitiesLoading` 3 useState → `useEntities` 훅.
  - `[worldGuide, setWorldGuide]` + `fetchWorldGuide` 함수 본체 → `useWorldGuide` 훅.
  - `handleToggleShot`: `await api + fetchStills(true)` → `shotToggle.mutate(..., { onError, onSettled })`.
  - progress 완료 transition에 `invalidateQueries(['project', id, 'entities'])` + `['project', id, 'worldGuide'])` 추가.
  - projectId 전환 useEffect: `setEntities([])` 제거 (queryKey 기반 자동 전환) — 세션 A Codex H2 근본 해소.
  - scene_split_threshold 동기화는 별도 useEffect로 분리.
  - worldGuide 저장 후 `fetchWorldGuide()` → `invalidateQueries(['project', id, 'worldGuide'])`.
  - `fetchEpisode` 래퍼 제거 (호출처 0건 + useQuery 자동 fetch + transition invalidate로 충분).
- **SceneVariationCard.tsx**: `onToggleShot` prop 시그니처 `Promise<void>` → `void | Promise<void>` (mutation의 `mutate()`는 void 반환).

### 실측 감소
- LOC: 1,292 → 1,263 (**-29**, 누적 -95) — 리뷰 수용 시 ref 패턴 추가로 일부 복구
- useState: 28 → 25 (**-3**, 누적 -9)

### 듀얼 리뷰 수용

**Claude (Critical 2 + High 3)**:
- **C1**: `shotToggle` 객체는 매 렌더 새 reference → `useCallback` 의존성에 직접 넣으면 `handleToggleShot`이 매 렌더 재생성 → SceneVariationCard 전체 재렌더. `shotToggleMutateRef` 패턴으로 mutate 안정 참조(세션 A의 `stillsRefetchRef` 패턴 재사용).
- **C2**: `setTogglingShots` 비동기 업데이트라 `skip` 변수는 동기 읽기 시점에 항상 false → double-click 가드 무력화. `togglingShotsRef`(useRef) 동기 가드 + setState는 UI 반영용 이중 관리.
- **H1 / Codex M3**: optimistic patch가 `still.scene_index === sceneIndex`만 비교해 legacy/partial row(`scene_index: null`, `still_index`로 fallback)는 갱신 안 됨. `matchesSceneKey()` 헬퍼에서 `still.scene_index ?? still.still_index` 통일.
- **H2**: `stillsKey`가 훅 생성 시점 캡처라 episodeId 교체 중간 window에서 stale key 사용 가능. `onMutate/onError/onSettled` 내부에서 매번 동적 생성으로 변경.
- **H3 / Codex M2**: `scene_split_threshold` 저장 2곳이 `/style-rules` PATCH 후 worldGuide invalidate 누락 → 페이지 재진입 시 5분 stale. 두 버튼 모두 `invalidateQueries(['project', id, 'worldGuide'])` 추가.

**Codex (High 2 + Medium 3)**:
- **H1 (Critical급)**: 전체 `stills` snapshot을 `context.previous`로 저장했다가 onError에서 통째로 복원 → 동시 mutation A 실패가 B의 최신 optimistic까지 덮어씀. **per-key reverse-patch**로 전환(같은 (scene,shot)에 토글을 한 번 더 적용 → 원상 복구). 다른 mutation 변경은 보존.
- **H2 (세션 B scope)**: `PipelineStepsPanel`에서 개별 step force는 `PipelineProgress` 갱신 없이 `step_run`만 갱신 → progress transition invalidate가 발동 안 함 → entity_extract/entity_merge/outlook_phase force해도 entities/worldGuide 5분 fresh 고착. `onRefresh` 핸들러에 entities/worldGuide invalidate 동반 추가.

**이월 (세션 D 범위)**:
- Codex M1: `Entities.tsx` 저장 시 project-scoped entity 캐시 invalidate 누락. 페이지 자체를 React Query 기반으로 전환하는 세션 D에서 근본 해소.

### 다음 세션 (Phase 5 잔여)
- **세션 B2 (선택)**: useProgress(SSE 통합) + useGenStatus + useScreenplay + useWorldRules — 추가 4~5 useState 제거
- **세션 C**: EpisodeDetail.tsx 분할 (5개 sub-component, ~500 LOC 목표)
- **세션 D**: ProjectDetail/Episodes/Dashboard 전환
- **세션 E**: `except: pass` 38건 + docs v0.6.0

---

## v0.5.25 (2026.04.20) — Phase 5 세션 A: React Query 기반 구축

### 배경
`EpisodeDetail.tsx` 1,358 LOC / useState 34 / `await api` 39 — useState+useEffect+fetch 3-tuple 패턴 반복. Phase 5 Frontend 리팩토링 시작. 세션 A는 **React Query 기반 확보**가 목표.

### 변경
- **신규 의존성**: `@tanstack/react-query@5.99.2` + `@tanstack/react-query-devtools`
- **main.tsx**: `QueryClientProvider` 추가 (staleTime 5분, retry 1). Devtools는 DEV 전용.
- **types/episode.ts (신규)**: `Episode`, `ResolvedEntity`, `SceneStill`, `StillsResponse` 공용 타입 추출.
- **hooks/api/useEpisode.ts (신규)**: `useQuery<Episode>`, 캐시 키 `['episode', episodeId]`.
- **hooks/api/useStills.ts (신규)**: `useQuery<StillsResponse>`, 배열/객체 2형태 응답 normalize, 캐시 키 `['episode', episodeId, 'stills']`.
- **EpisodeDetail.tsx**:
  - 인라인 `interface Episode/SceneStill/ResolvedEntity` 제거 → `types/episode`에서 import.
  - `episode/loading/stills/stillsLoading/availableComposites/availableOutfits/entitySidMap` 7 useState → useQuery.
  - `fetchEpisode/fetchStills` 함수 본체 → useQuery refetch 래퍼 (호출부 10+ 호환 유지).
  - preserveScroll 로직: imperative 래퍼에서 중첩 `requestAnimationFrame`으로 보존.

### 실측 감소
- LOC: 1,358 → 1,292 (**-66**, -4.9%)
- useState: 34 → 28 (**-6**)
- `await api` 직접 호출: 39 → 37

### 듀얼 리뷰 수용

**Claude (Critical 2 + Important 1)**:
- C1: `useCallback([episodeQuery/stillsQuery])` — query 객체는 매 렌더 새 reference. 무한 refetch 루프 위험. `useRef` 패턴으로 최신 refetch 캡처 + 의존성 빈 배열.
- C2: 단일 useEffect에 `[fetchStills, fetchEntities, entities.length, entitiesLoading]` → entities 로드 후 fetchStills 재실행. fetchStills useEffect 제거(useQuery가 마운트 fetch), fetchEntities만 분리.
- I3: 단일 rAF는 React DOM 커밋 전에 실행될 수 있어 스크롤 덮어쓰기 경쟁. 중첩 `rAF → rAF → scrollTo`로 다음 페인트 이후 실행.

**Codex (High 2 + Medium 1)**:
- H1: 분석 시작/완료/재분석 후 `episode.status`가 staleTime 5분 동안 고착. 3곳(progress 완료 transition, reanalyze 시작, PipelineStepsPanel onRefresh)에서 `queryClient.invalidateQueries(['episode', episodeId])` 추가.
- H2: `entities` 로컬 state가 projectId 전환 시 리셋 안 됨 → 이전 프로젝트 entity id가 새 still의 `visible_entities_json`에 섞일 위험. `id` 변경 useEffect로 `setEntities([])` + `setStillImages({})` 리셋. 세션 B에서 useEntities 훅 이관 예정.
- M1: useQuery가 이미 마운트 fetch → 마운트 effect의 `fetchEpisode()/fetchStills()` 호출은 중복 네트워크 + StrictMode 2회 호출 증폭. 초기 effect에서 제거.

### 남은 세션 (Phase 5 잔여)
- **세션 B**: 나머지 useQuery 훅(useEntities/useWorldGuide/useProgress) + optimistic update mutation
- **세션 C**: EpisodeDetail.tsx 1,292 → ~500 분할 (5개 sub-component)
- **세션 D**: ProjectDetail/Episodes/Dashboard 전환
- **세션 E**: `except: pass` 38건 분류 + docs 갱신

### 버전 업
- frontend/package.json 0.5.24 → 0.5.25

## v0.5.24 (2026.04.20) — 2-pass drift stale UI 구분 (B1, Claude v0.5.23 H3 후속)

### 배경
v0.5.23에서 `consumes_downstream` 1급 필드로 **보존 의미**는 명확해졌으나, `scene_detail` force 후 `shot_dependency_t2i` 수동 재실행을 사용자가 잊으면 `scene_context_loader`가 낡은 ref_usage를 소비하는 **silent drift**가 여전했다 (Claude H3 인정). 현재 UI는 이미 stale을 표시하지만 일반 stale과 drift stale이 시각적으로 구분 안 돼 주의력 유발에 실패.

### 변경
- **백엔드**:
  - `step_catalog.get_consumers_of(step_id)` 헬퍼 추가 (`get_consumes_downstream`의 역방향).
  - `/steps` API 응답 각 step에 `consumed_by: List[str]` 필드 추가. status=stale + consumed_by 비어있지 않음 = drift stale.
- **프론트 (PipelineStepsPanel.tsx)**:
  - `StepInfo` 인터페이스에 `consumed_by?: string[]` 추가.
  - `isDriftStale()` 헬퍼: drift stale step을 일반 stale과 구분.
  - 개별 step: drift stale은 ⚠️ 주황 + "재실행 권장" 라벨 (일반 stale은 🔄 회색 "무효화됨" 유지).
  - **패널 상단 경고 배너**: drift stale step이 1개 이상이면 Analysis/Image 헤더 위에 경고 배너 노출 — "상류 변경으로 N개 단계 재실행 권장" + 소비 주체 step명 + 대상 step명 목록. 실행 버튼은 배너에 없음(기존 step 재실행 경로 유지, 실수로 전체 재실행 방지).

### Silent drift 방지 효과
- Analysis 페이지 진입 시마다 반복 알림 → 세션 이탈 후 재방문에도 인지 가능.
- "모두 재실행" 버튼 의도적 배제 — shot_dependency_t2i만 돌리면 되는데 전체 재실행 시 LLM 비용 과다 방지.

### 한계 (Claude H1 + Codex M1 수용 — 배너 문구 현실화)
`isDriftStale`은 `status === 'stale' && consumed_by.length > 0`으로 판정. `consumed_by`는 manifest의 **정적 역참조 관계**이지 "현재 왜 stale인지"의 동적 이유는 아님.

이론적 false positive: `scene_detail`이 아닌 다른 상류(`scene_consistency`)의 force로 `shot_dependency_t2i`가 stale된 경우에도 `consumed_by=["scene_detail"]`이 그대로 반환돼 drift stale로 분류. "scene_detail이 소비하는 하류"라는 원문이 원인을 오해할 소지.

**대응**: 배너 문구를 "상류 변경으로"(원인 단정) → "역참조 소비 관계가 있는 stale"(사실 기반)로 완화. false positive라도 **재실행이 필요한 상태**라 사용자 행동은 동일.

**실질 빈도**: 현재 `consumes_downstream`을 선언한 step은 `scene_detail` 하나, 역참조 대상은 `shot_dependency_t2i` 하나. 근본 해소는 `step_run`에 `stale_reason` 칼럼(동적 stale 사유 기록) 추가가 필요하나 설계 대비 이득 낮음 — 별도 이슈로 이월.

### can_run 게이팅 (Codex M2 수용)
배너와 대상 목록은 `can_run=true`인 drift stale만 포함. 상류가 아직 running/blocked 상태면 `[재실행]` 시도 시 `gate.blocked`(400) 유도되므로 배너에서 제외. `!can_run`인 drift stale step은 개별 표시(⚠️ 아이콘)만 유지되고 배너는 조용함.

### 테스트
- `tests/core/test_consumes_downstream_integration.py`에 `test_get_consumers_of_reverse_lookup` 추가 (역방향 조회 일관성 검증).
- TypeScript 타입 체크 통과.

### 버전 업
- frontend/package.json 0.5.23 → 0.5.24

## v0.5.23 (2026.04.20) — P1 묶음 (consumes_downstream + entities.py 캐시 + 통합 테스트)

P1 3건 병합: ① `consumes_downstream` 1급 필드 승격, ② `entities.py list_stills` request-scoped 캐시, ③ 통합 테스트.

### ① `consumes_downstream` 1급 필드 (Claude v0.5.15 리뷰 #5 이월)

**배경**:
v0.5.15 도입한 `preserve_on_upstream_force: bool`은 **하류 step 입장**의 선언이었다 ("나는 어떤 상류가 force해도 살아남는다"). 의미가 모호했고 단일 플래그라 "누구 때문에 보존인지"가 암묵적. Claude v0.5.15 리뷰 #5 이월 항목을 수용.

**변경**:
- **상류 입장** 선언: "나는 이 하류들의 체크포인트를 역참조한다."
- `step_catalog.StepEntry`: `consumes_downstream: List[str] = field(default_factory=list)` 필드 추가, 기존 `preserve_on_upstream_force: bool` 제거.
- `step_manifest`: `shot_dependency_t2i`에서 플래그 제거, `scene_detail`에 `"consumes_downstream": ["shot_dependency_t2i"]` 선언.
- `step_runner.invalidate_downstream`: ref의 `consumes_downstream` 목록을 읽어 순회 중 `sid in consumes`면 파일 보존(DB stale). 로그 `Preserved stale checkpoint for X (consumed by Y)` — 소비 주체 명시.
- 헬퍼 `step_catalog.get_consumes_downstream(step_id)` 추가.

**의미론적 이점**:
- `scene_detail` force 시에만 `shot_dependency_t2i` 보존. 다른 상류(예: `scene_consistency`)가 force되면 일반 삭제 — 기존 플래그는 이 구분을 못 했다.
- 관계가 선언적으로 드러남 — 향후 `scene_detail`이 다른 하류를 역참조해도 목록에 추가만 하면 됨.

**한계 (Claude 리뷰 H3 수용 — 문서화)**:
이 변경은 **보존 의미를 명확히 할 뿐, 2-pass drift silent failure 자체를 해소하지는 않는다.** `scene_detail` force 이후 사용자가 `shot_dependency_t2i`를 수동 재실행하지 않으면, 다음 `scene_detail` 실행 시 `scene_context_loader`가 stale 체크포인트의 낡은 `ref_usage`를 그대로 소비한다. `scene_context_loader._load_dependencies()`의 stale warning은 로그에만 남을 뿐, UI는 결과가 정상인 것처럼 표시. 근본적 해소는 향후 자동 재실행(cascade re-run) 또는 UI 명시 알림이 필요하다 — 별도 이슈로 이월.

### ② `entities.py::list_stills` request-scoped 캐시 (Claude v0.5.16 Med #3 이월)

**문제**: `for still in stills:` 루프 안에서 매 iteration마다 `build_name_index()` 2회 호출 + `next((e for e in all_project_entities if e.short_id == sid), None)` 선형 탐색 3곳 → stills N개 × entities E개 시 **O(N·E)**.

**수정**: 루프 **전**에 `_char_name_idx`, `_outlook_name_idx`, `_short_id_map = {e.short_id: e for e in all_project_entities if e.short_id}` 사전 구축. 루프 내는 모두 O(1) dict 조회로 치환.

**파생 정리**: `entity_sid_map`도 `_short_id_map` 재사용으로 간소화.

### ③ 통합 테스트 (Claude v0.5.16 Med #5, v0.5.17 S2 이월)

**신규**: `backend/tests/core/test_consumes_downstream_integration.py` — 실 `STEP_MANIFEST` + `step_catalog` + `invalidate_downstream` 3 레이어 계약 검증 (5 tests, 모두 PASS).

- 데이터 레이어 4건: scene_detail 선언 fact-check, legacy flag 완전 부재, catalog 헬퍼 일치, consumes target이 실제 재귀 downstream에 포함 (오타·DAG 드리프트 방지).
- 실행 레이어 1건: 실 manifest + tmp path로 `scene_detail` force → `shot_dependency_t2i` 파일 보존 + `t2i_review` 삭제.

**기존 단위 테스트 이관**: `test_step_runner_preserve_flag.py` → `git mv test_step_runner_consumes_downstream.py` (히스토리 보존), 6 tests 재작성.

### 듀얼 리뷰 수용 (P1-1 1차 + P1 통합 2차)
- Claude H2 (v0.5.15 리뷰 Tuple unused import): 제거.
- Claude H3 (drift silent failure 한계 문서화): 위 "한계" 섹션.
- Codex Med (git mv 스테이징 분리): `git add` 양쪽 모두 staged.

### 버전 업
- frontend/package.json 0.5.22 → 0.5.23

## v0.5.22 (2026.04.20) — v0.5.19~21 사후 듀얼 리뷰 수용 수정

### 배경
v0.5.19/v0.5.20/v0.5.21 3 커밋에서 듀얼 리뷰를 누락. 사용자 지적으로 사후 리뷰 실행. Codex + Claude 일치 — Critical/High 0, Med 2 (수용) + Low (일부 수용).

### 수용 (Med — 401 재로그인 retry 카운트 소진)
- `migrate_v0513_reanalyze.check_prereq` 및 `migrate_shot_validator.check_prereq`에서 `resp.status_code == 401` 분기가 `continue`로 루프 넘어갈 때 `attempt`를 증가시켜 재시도 슬롯을 소진하던 이슈.
- 수정: 401 재로그인 후 **동일 iteration에서 즉시 재요청** (`resp = self.sess.get(url, timeout=10)`), attempt 미소진.

### 수용 (Low — archive_prompts 개선)
- `archive_prompts.py`: `argparse` 도입, `--keep N` CLI 인자, `--apply` 플래그 일관화.
- git mv 실패 카운트를 반환하고 1건이라도 실패하면 종료 코드 `2` (기존 0 반환 → 실패 은닉 방지).

### 이월 (Med — 다음 마이그레이션 스크립트 때 리팩토링)
- migrate 두 스크립트(`migrate_v0513_reanalyze.py` / `migrate_shot_validator.py`)의 `login`/`list_episodes`/`wait_step_done` 중복 ~170 LOC. 3번째 마이그레이션 스크립트가 추가되면 공통 `MigrationBase` 추출. 현재는 비용 대비 이득 낮음.

### 통과 (변경 없음)
- `/api/health` alias (stateless GET decorator 중첩, FastAPI 공식 패턴)
- `cleanup_checkpoint_archives.py` 정규식 (`^manifest_\d{8}_\d{6}\.json$`은 활성 `manifest.json` 미매칭, label/uuid 파일도 미매칭 — 안전)
- `test_step_runner_preserve_flag.py` 13 tests (monkeypatch 심볼 `app.core.step_runner.STEP_MANIFEST` 정확)
- Opik 동적 ID tag 잔존 0 (변경 없음)

### 버전 업
- frontend/package.json 0.5.21 → 0.5.22

## v0.5.21 (2026.04.20) — migrate 재시도 + checkpoint archive 정리 + shot_validator 일괄

### `migrate_v0513_reanalyze.py` HTTP 재시도 (P2 #10)
- `check_prereq`: HTTP 오류 3회 재시도 (지수 백오프 5/10초). 일시 네트워크 오류와 실 prereq 미충족 구분.
- 401 감지 시 재로그인.

### 신규: `cleanup_checkpoint_archives.py` (P2 #11)
- `step_runner`가 save/invalidate 시 생성하는 `manifest_YYYYMMDD_HHMMSS.json` auto-archive 누적 정리.
- **label/uuid 접미사 있는 스냅샷은 유지** (사용자 명시 스냅샷 보호).
- 자동 archive만 step당 최근 3개(기본) 남기고 삭제.
- **1회 실행으로 61 파일(4.4 MB) 제거** (금월도/기타 프로젝트 통합).

### 신규: `migrate_shot_validator.py` (P2 #8)
- 기존 프로젝트의 `shot_validator`를 v2 프롬프트 기준으로 일괄 재실행.
- prereq: `shot_extract` (completed/partial/stale 허용).
- 프로젝트 간 순차 실행 + HTTP 3회 재시도 + 401 재로그인.

### 버전 업
- frontend/package.json 0.5.20 → 0.5.21

## v0.5.20 (2026.04.20) — 프롬프트 아카이브 실행 + /api/health alias + Opik 점검

### 프롬프트 아카이브 (64 디렉토리, v0.5.14 정책 적용)
- 각 step의 `_version_sort_key` 내림차순 최신 **3개만 `_base` 유지**, 나머지는 `prompts/_archive/<step>/<version>/`로 `git mv` (히스토리 보존).
- 대상 step:
  - `scene_extractor_v2` (16), `scene_detail` (6), `outlook_extractor` (9), `shot_extract` (8), `entity_extract_v4` (5), `scene_director` (5), `shot_staging` (5), `entity_extractor_v2` (3), `ref_image_prompts` (2), `shot_dependency_t2i` (2), `scene_consistency` (1), `shot_selection` (1), `entity_all` (1)
- 로더 검증: `scene_detail/system v=9.202604201700`, `shot_extract/system v=11.202604201230` 등 최신 버전 정상 로드.

### 신규: `backend/scripts/archive_prompts.py`
- `_version_sort_key` 기반 자동화. dry-run 기본 + `--apply` 옵션.
- 향후 4번째 버전 생성 시 재실행만으로 유지보수 가능.

### `/api/health` alias (로그 스팸 정리)
- `app/main.py`의 `health_check` handler에 `@app.get("/api/health")` decorator 추가 (기존 `/api/v1/health` 유지).
- 외부 모니터링 툴의 `/api/health` 반복 404 제거.

### Opik 호출부 전수 점검 (잔존 0건)
- `extra_tags`는 모두 고정 문자열(retry/verify/t2i_review/visual_world_rules/scene_summary)
- 동적 ID(scene_index)는 v0.5.14에서 이미 `extra_metadata`로 이관됨
- `[f"attempt_{N}"]` (scene_steps.py:104)은 카디널리티 낮아 허용

### 버전 업
- frontend/package.json 0.5.19 → 0.5.20

## v0.5.19 (2026.04.20) — P1.5 단위 테스트 (Claude v0.5.15 이월 수용)

### 신규 (13 tests)
- `test_step_runner_preserve_flag.py` — 5건: `preserve_on_upstream_force` 플래그 경로, DB stale + 파일 보존, flag False, delete_checkpoints=False, target_step_id override
- `test_build_opik_metadata.py` — 8건: 기본 + extra_tags + extra_metadata 병합, **예약 키 보호** (session_id/trace_name/tags 덮어쓰기 차단)

### 기각
- character_state_variant 미등록명 필터 테스트: `name_matcher` 정규화 lookup 경로가 `test_name_matcher`에서 커버됨 (추가 가치 낮음)

### 버전 업
- frontend/package.json 0.5.18 → 0.5.19

## v0.5.18 (2026.04.20) — 2-pass drift 감지 (v0.5.15 Claude 리뷰 Critical #2 수용)

### 배경
v0.5.15 `preserve_on_upstream_force=True` 도입 후 `shot_dependency_t2i` 체크포인트 파일은 scene_detail force 시 보존되지만 DB step_run은 `stale`로 남음. 현재 `scene_context_loader`가 이 "낡은" 체크포인트를 그대로 소비해 이전 pass 기반의 ref_usage로 user_prompt를 구성 → **drift** 가능. 운영자가 이를 감지할 수 없는 silent 상태가 위험.

### 수정
`backend/app/core/steps/scene_context_loader.py::_load_dependencies` — shot_dependency_t2i 체크포인트 로드 성공 시 DB step_run status 조회, `stale`이면 warning 로그:

```
scene_detail: shot_dependency_t2i checkpoint is STALE — 이전 scene_detail 기반 ref_usage를 사용 중. drift 방지를 위해 scene_detail 완료 후 shot_dependency_t2i도 force 재실행 권장.
```

`_get_step_run`은 `Optional[tuple]`로 None short-circuit 안전. warning 레벨이 drift 심각도에 적합.

### 듀얼 리뷰 결과
- **Codex**: 통과
- **Claude**: 통과 + Suggestion 3건 (단위 테스트 공백 / 중복 warning 방지 / 메시지 개선)
  - 테스트 공백 → P1 이월
  - 중복 warning 방지 → scene_detail 1회 실행당 warning 1회이므로 실질 OK, 적용 안 함
  - 메시지 개선(API 명령 하드코딩) → DX vs 범용성 트레이드오프로 적용 안 함

### 버전 업
- frontend/package.json 0.5.17 → 0.5.18

## v0.5.17 (2026.04.20) — composite 저빈도 정합성 + low_freq_skip 유틸 + outlook_sync 정리

### 신규
- `backend/app/core/low_freq_skip.py` — `get_skip_file_path` / `load_low_freq_skip_ids` / `save_low_freq_skip_ids`.
- `backend/tests/core/test_low_freq_skip.py` — 7 unit tests (100% pass).

### CompositeImageGenStep 저빈도 정합성 (Breakout Sample partial 1/2 수정)
`ref_image_gen`이 저빈도 엔티티의 참조 이미지를 스킵하고 `ref_low_freq_skip.json`에 기록함에도, `CompositeImageGenStep._execute`는 `combo_total` 계산에서 이 스킵을 반영 안 해 `applicable_count`가 부풀려지고 `failed_count`도 왜곡(partial 1/2). 수정:
- `combo_total`: `c.outlook_id not in _o00_ids and c.character_id not in _low_freq_skip_ids`로 필터
- `existing_keys`: 과거 생성분이라도 분모와 동일 조건(O00/저빈도 제외)을 분자에도 적용 — **Codex 리뷰 Medium 수용**. 분모와 분자 불일치 방지

### low_freq_skip 읽기/쓰기 유틸로 통일 (Claude S1 수용, 4곳 중복 제거)
- `pipeline_gate.py:89-98` — `load_low_freq_skip_ids` 경유
- `reference_image_service.py:377-379` — `save_low_freq_skip_ids` 경유
- `api/v1/images.py:776-783` — `len(load_low_freq_skip_ids(...))` 경유
- `image_steps.py` CompositeImageGenStep — 유틸 경유

파싱 실패 시 유틸이 `warning` 로그 emit + empty set 반환. 운영자가 JSON 손상을 감지 가능.

### outlook_sync_service out-param 제거 (v0.5.16 Claude High #2 이월 수용)
- `_delta_sync_character_outlook(self, outlooks, ol_data, char_name_id)` → `(self, outlooks, ol_data)` (out-param 제거)
- caller(`sync_from_checkpoint`)의 raw `char_name_id = {e.name: e.id for ...}` dead code 제거
- method 내부에서 `char_name_id = build_name_index(char_entries, ...)` **local** 구축. v0.5.16 정규화 정책 적용
- 외부 caller 영향 없음 (`_delta_sync_character_outlook`은 single call-site)

### 듀얼 리뷰 결과
- **Codex**: Medium 1건 (existing_keys 분자 불일치) — 수용
- **Claude**: Suggestion [S1] 유틸 추출 — 수용, [S2] 회귀 테스트 — 유틸 7 단위 테스트로 부분 수용 (composite 자체 통합 테스트는 이월)
- Critical / Important: 0

### 버전 업
- frontend/package.json 0.5.16 → 0.5.17

## v0.5.16 (2026.04.20) — 이름 매칭 드리프트 방지 유틸 (name_matcher)

### 배경
LLM 출력 이름(`character_angles[].character`, scene_consistency `fixed_elements` 등)과 DB 고정 이름(`EntityCanon.name`) 사이에 표기 드리프트 발생:
- "이도령" vs "이도령(혼)" (괄호 접미사 variant)
- "민숙" vs "민숙 " (전후 공백)
- 전각 괄호(（ 【 〈 《 「 『)

exact-match 전제의 dict lookup이 정상 엔티티를 "미등록"으로 분류하는 silent bug. 실측으로 `character_state_variant`가 이름 드리프트로 참조 이미지 생성 스킵되는 케이스 있었음.

### 신규
- `backend/app/core/name_matcher.py` — `normalize_name` / `_collapse_whitespace` / `build_name_index` / `lookup_name`
- `backend/tests/core/test_name_matcher.py` — 21개 단위 테스트 (100% pass)

### 정규화 정책 (variant 보호)
실측 `cfb87551` 프로젝트에서 `서현`(base) + `서현 (5세)` / `(10대)` / `(20대)` / `(30대)` 5 변형 동시 존재 발견. 단순 bare key 등록은 variant가 base와 silent 충돌하는 위험. 따라서:

- **raw** (원본 이름) 등록
- **collapsed** (공백만 정리) 등록 — raw와 다를 때
- **bare** (괄호 제거 정규화) — **raw에 괄호가 없을 때만**. variant는 bare 등록 제외 (base 자리 보호)
- 충돌 시 첫 등록 우선 + `logger.warning("name_index collision: ...")` (드리프트 감지)

`lookup_name`은 query 쪽을 raw → collapsed → bare 단계적 완화. variant index에 bare key 없으므로 bare query는 base 있을 때만 매칭 (엄격 전제).

**의도적 제외**: 조사 제거("민숙이"→"민숙") / 대소문자 통일.

### 수정 5 매칭 지점 (exact match → normalized)
- `image_steps.py` — `CharacterStateVariantStep`의 `name_to_uuid`/`name_to_desc` + `CompositeImageGenStep`의 `ol_name_map`
- `director_steps.py` — scene_director `name_to_short`
- `shot_dependency_step.py` — `name_to_sid`
- `outlook_sync_service.py` — `ol_name_to_id` + `char_name_id` (정규화 별칭 병합)
- `entities.py` — T2I 파싱 C##O## 해상화 (linear search → indexed)

`entity_steps.py`는 `entity_queue`에서 name·short_id가 같은 소스라 드리프트 불가 판단으로 스킵.

### 듀얼 리뷰 (Claude 완료, Codex 환경 제약으로 부분)
**수용**
- Claude High #1 (silent variant drift): 충돌 시 warning log + variant bare key 등록 제외 설계 전환
- Claude Low #7: entity_steps.py 스킵 판단 근거 주석

**이월 (P1)**
- Claude High #2: `outlook_sync_service._delta_sync_character_outlook`이 `char_name_id` out-param 주입에 정규화 별칭 병합 — caller 오염 위험. 다음 커밋에서 local 분리
- Claude Med #3: `entities.py` request-scoped 재구축 캐싱
- Claude Med #5: 5개 지점 통합 테스트

### 버전 업
- frontend/package.json 0.5.15 → 0.5.16

## v0.5.15 (2026.04.20) — 2-pass 의존성 체크포인트 보존 플래그

### 배경
v0.5.13에서 `scene_detail`이 `scene_context_loader`를 통해 하류 `shot_dependency_t2i`의 refined `ref_usage`(zoom_in_detail 등)를 역참조 소비하도록 수정(9956c27). 그러나 step_manifest의 depends_on은 단방향 DAG이라, `scene_detail` force 시 step_runner의 `invalidate_downstream`이 `shot_dependency_t2i/manifest.json`을 archive+unlink → 실행 중 fallback으로 `shot_dependency`(1차, ref_usage=NONE)를 읽어 9956c27의 zoom 블록이 여전히 dead code 상태로 잔존. 오늘 금월도 실측으로 확인.

### 수정
- `step_catalog.StepEntry`: `preserve_on_upstream_force: bool = False` 필드 추가.
- `step_manifest.shot_dependency_t2i`: `preserve_on_upstream_force=True` 설정.
- `step_runner.invalidate_downstream`: 플래그=True인 step은 **파일 보존**(DB는 stale 유지) + INFO 로그로 "drift 방지를 위해 해당 step 재실행 권장" 안내.

### 실측 검증 (금월도 PID 2aba79e5, 3-pass 절차)
1. `shot_dependency_t2i` force (1차) — zoom_in_detail 분류 6건 생성
2. `scene_detail` force — 플래그로 shot_dependency_t2i 체크포인트 보존, zoom 블록 user_prompt 주입 성공
3. `shot_dependency_t2i` force (drift 방지) — 새 scene_detail 기반 ref_usage 재분류

t2i_prompt 변화 (6 zoom shots 중 5건 반영):
- S28/shot4: `C03O04 in a worn work jacket` → `a middle-aged Korean man` (C## 완전 제거)
- S24/shot3: 보통명사 선행 + `on the same bicycle in the same storm`
- S23/shot2: `in the same dim internet cafe booth` ("same" 명시)
- S13/shot6, S5/shot11: "Focus on [부위]" 보통명사 구조 유지
- S28/shot3: 유일한 예외 (C03O04 4회 잔존) — LLM 준수 실패 사례

로그 실측: `Preserved stale checkpoint for shot_dependency_t2i (preserve_on_upstream_force=True) — 파일 보존, DB stale. drift 방지를 위해 scene_detail force 완료 후 shot_dependency_t2i도 재실행 권장` INFO 출력 확인.

### 듀얼 리뷰 후 수정 (Codex 1건 + Claude 3건 수용)
- `step_runner.py`: logger.debug → logger.info 승격 (Codex #4 + Claude #3: 드리프트 감지를 위한 운영 가시성).
- 주석 확장: 3파일 모두에 "2-pass 의존성(two-pass dep)" 용어 정의 + 운영 가이드 (Claude #6).

### 기각 (Codex 관점5)
FORCE_STEPS 순서를 `scene_consistency → scene_detail → shot_dependency_t2i → t2i_review`로 바꾸자는 제안은 잘못. scene_detail이 먼저 돌면 zoom 블록 주입을 위한 ref_usage 재료가 없으므로 현재 순서(`... → shot_dependency_t2i → scene_detail → ...`)가 맞다.

### P1 이월
- 단위 테스트 (Claude #4): `preserve_on_upstream_force` 경로 회귀 방지 0건.
- drift 감지 (Claude #2): checkpoint payload에 상류 `run_id` 태깅 후 load 시점 비교.
- `consumes_downstream` 1급 필드 (Claude #5 Low/Design): 2-pass 관계를 graph에 선언적으로 표현.

### 버전 업
- frontend/package.json 0.5.14 → 0.5.15.

## v0.5.14 (2026.04.20) — 기술부채 정리: 병렬화 + Opik 카디널리티 + schema 정리 + 마이그레이션

### P1.1 scene_consistency 병렬화 (성능)
- `scene_consistency_step.py`: 씬별 순차 for 루프 → `ThreadPoolExecutor(max_workers=env SCENE_CONSISTENCY_WORKERS, default=5)`.
- `_process_one_scene` 메서드로 3-tier fallback(Gemini → sanitize → GPT) 추출.
- 기대 성능: 30 씬 기준 10분+ → 1~2분.
- 안전 필터 fallback 로직은 worker 내부 보존.

### P1.2 Opik `scene_{si}` tag → metadata (카디널리티)
- `step_runner.py`: `build_opik_metadata(extra_tags, extra_metadata)` 파라미터 확장. dict 값은 meta에 merge (기존 키 보호).
- 6개 step caller의 동적 ID 태그를 metadata로 이동:
  - `[f"scene_{si}"]` → `extra_metadata={"scene_index": si}`
  - `[f"location_{sid}"]` → `extra_metadata={"location_id": sid}`
  - `[f"verify_{si}"]` → `extra_tags=["verify"] + extra_metadata={"scene_index": si}`
- 대상: detail_steps, scene_consistency_step, scene_camera_flow_step, shot_validator_step, location_consistency_step.
- Opik 인덱스 카디널리티 폭발(수백~수천 고유 태그) → 일반 태그만.

### P1.3 scene_detail schema 정리
- `detail_steps.py`: `representative_moment` 쓰기 단일화, `still_frame_prompt` 키 pop (레거시 제거).
- 읽기는 레거시 체크포인트(`still_frame_prompt`) fallback 유지 (하위 호환).
- DB 컬럼 `still_frame_prompt`은 변경 없음 (경계 sync에서 변환).

### P2.1 마이그레이션 스크립트 — `backend/scripts/migrate_v0513_reanalyze.py`
- 프로젝트 일괄 재분석 (requests 기반).
- 스냅샷 자동 저장 → 4단계(scene_consistency → shot_dependency_t2i → scene_detail → t2i_review) force 순차 → 폴링 완료 대기.
  (순서: `scene_consistency`가 체인 선행 의존이므로 가장 먼저 실행)
- 프로젝트 간 병렬 금지 (rate-limit 준수).
- `--dry-run` 모드 지원.

### P2.2 프롬프트 아카이브 정책
- `docs/architecture/07-prompt-versioning-policy.md` 신규.
- 각 step 최신 3개 버전만 `prompts/_base/`에 유지, 나머지 `prompts/_archive/`로 이동 (로더 자동 제외).

### 듀얼 리뷰 후 수정 (Codex P0 1건 + 양쪽 P1 5건)
- `migrate`: snapshot 실패 시 에피소드 스킵 + 복구 명령 안내.
- `migrate`: `wait_step_done`이 `partial` 상태도 정상 완료로 인정 (StepRunner의 정상 상태).
- `migrate`: 401 세션 만료 감지 → 자동 재로그인 (저장된 자격증명 기반).
- `llm_client`: `_init_opik` / `_get_router` 전역 lazy init에 `RLock` 추가 — ThreadPool worker 동시 호출 race 방지.
- `scene_consistency`: 병렬 진입 전 로그를 `already_done` / `single_shot_skipped`로 분리 (혼란스러운 표현 제거).
- `07-prompt-versioning-policy.md`: 디렉토리 구조도 일관성 수정 (`prompts/_archive`는 `_base`와 같은 레벨).

### 버전 업
- package.json 0.5.13 → 0.5.14

### 후속 패치 — migrate 스크립트 사전 체크
- `migrate_v0513_reanalyze.py`: 실행 전 `REQUIRED_PREREQ_STEPS`(shot_validator, shot_staging, shot_director, scene_consistency, scene_detail, shot_dependency_t2i) 상태 검사. 미완료/blocked면 에피소드 스킵.
- 허용 상태: `completed` / `partial` / `stale` (stale은 하류 force로 invalidate됐지만 재분석 대상이므로 허용).
- 구(old) 파이프라인(shot_extract + shot_selection + 구 scene_detail만)으로만 완료된 프로젝트 자동 보호.
- 실측: 요괴전 v4-test(b7212a0a) → SKIP 사유 출력, d168f244 → 적격 판정.

---

## v0.5.13 (2026.04.20) — 이미지 중복 렌더링 3종 해결 (scene_detail v9 + scene_consistency v4 + shot_dependency_t2i v5)

### 배경
v0.5.12 E2E 실측에서 3종 새 이슈 발견:
- **S12/19**: 민숙(사망 인물)의 팔이 화면에 2개 중복 렌더링 — 전신 참조 + 확대 손목 이중 keep
- **S13/3**: 경찰차 문이 2개 렌더링 — shot_dependency의 ignore_elements가 조건부("if not visible") 모호 표현
- **S29/5**: 인물 상반신 탈의 — C##O## 복합 ID에 옷 텍스트 없어 T2I가 노출 해석

### scene_detail v8 → v9 (1.8.0 → 1.9.0)
- **인물 복장 간단 언급 필수 룰 추가**: 인물 상반신/전신이 프레임 내 보이면 C##O## 뒤에 옷 1~3단어 표현 필수 (예: `in a worn jumper`, `in a hooded jacket`). 과잉 묘사 금지 — 참조 이미지가 세부 담당
- **앞쪽 참조 샷 처리 섹션 신설**: ref_usage 유형(`zoom_in_detail` / `exact_background` / `atmosphere_reference`)별 t2i_prompt 작성 방식 명시
- **zoom_in_detail 특수 규칙**: 같은 시공간 · 카메라 확대/이동만 — 새 요소 추가 금지, 초점 영역만 구체화

### scene_consistency v3 → v4 (2.1.0 → 2.2.0)
- **같은 인물 전신/확대 프레이밍 중복 keep 금지 룰 추가**: 샷마다 프레이밍 규모(전신형 vs 확대형)가 다르면 character_state를 별도 항목으로 분리, `applies_to_shots`를 겹치지 않게 분리. 한 샷에 전신 + 확대 부위 keep이 동시에 들어가면 T2I가 팔/몸을 중복 렌더링하는 문제 해결
- **프레이밍 판별 체크리스트**: shot description / staging의 camera_direction·character_angles 기반

### shot_dependency_t2i v4 → v5 (1.1.0 → 1.2.0)
- **`zoom_in_detail` ref_usage 신설**: 같은 순간·같은 공간·같은 피사체에서 카메라만 확대/이동한 경우. 판정 체크리스트(4개 조건 모두 만족) + 사용 방식 명시
- **ignore_elements 조건부 표현 금지 룰**: `if ... is not visible`, `if applicable`, `only if ...`, `when ...` 같은 조건부는 T2I가 해석 못해 원본 유지 → 중복 렌더링 유발. 확정적 지시만 허용. 무시할 게 없으면 빈 문자열("")
- **schema enum 확장**: `["zoom_in_detail", "exact_background", "atmosphere_reference"]`

### 코드 통합
- `detail_steps.py`: 앞쪽 연관 Shot 블록에 ref_usage 라벨 표시 + `zoom_in_detail` 감지 시 전용 안내 블록("새 요소 금지, 확대 영역만 구체화") 주입
- `scene_image_service.py`: `_ref_usage == "zoom_in_detail"` branch 추가 → label `previous shot at same location (SAME FRAME ZOOMED)` 생성
- `prompt_service.py`: `same frame zoomed` 레이블에 대한 ref_role/instructions 분기 추가 (same room 분기 앞에 배치)

### 버전 업
- `scene_detail_composer` 1.8.0 → 1.9.0 (prompt v8 → v9)
- `scene_consistency` 2.1.0 → 2.2.0 (prompt v3 → v4)
- `shot_dependency_t2i` 1.1.0 → 1.2.0 (prompt v4 → v5)
- `image_service` 1.8.1 → 1.9.0 (zoom_in_detail label 지원)
- package.json 0.5.12 → 0.5.13

### 기대 효과
- S12/19 팔 중복: scene_consistency가 전신/확대를 샷별로 분리 → 각 샷 프롬프트에 한쪽 묘사만 들어감
- S13/3 문 중복: ignore가 확정적 표현만 → T2I가 원본 문을 제대로 제거
- S29/5 상반신 탈의: C##O## 뒤에 옷 텍스트 필수 → T2I가 의상 유지

---

## v0.5.12 (2026.04.20) — scene_detail v8 과잉 중립화 수정 + location_consistency manifest 제거

### scene_detail v8 (1.7.0 → 1.8.0)
v7에서 공격 어휘 팔레트를 과하게 축소해서 씬 원문이 폭력을 묘사해도 LLM이 쓸 수 있는 어휘가 부족 → T2I 친밀 bias 회피 기능 상실 우려. v8에서 팔레트 복원:

- **역할 대비 어휘 복원**: attacker / assailant / aggressor / predator / victim / prey / target
- **강도 형용사 복원**: forceful, firm, aggressive, violent, brutal, savage, feral, predatory, vicious
- **물리적 결과 어휘 복원**: tearing flesh, ripped skin, gaping wound, jagged gash, blood spray, spurting blood, fresh blood soaking, bruising, split lip
- **권력 불균형 자세 규칙 유지** (친밀 해석 차단 핵심)
- **조건 유지**: "씬 원문이 그 수준의 폭력을 명시한 경우만 사용. 장르 편향으로 임의 추가 금지"
- **추가 원칙**: "안전 필터 우려로 스스로 검열 금지 — 이미지 생성 단계의 3단계 회피 루틴(PromptSanitizer의 film previs/movie poster/aftermath)이 moderation block 자동 처리"

### location_consistency manifest 제거
v0.5.10에서 scene_detail 주입만 막고 step은 보존했는데, manifest에 등록되어 있어 run-all이 계속 실행 (LLM 비용 낭비). manifest entry 삭제 → run-all에서 완전 제외.
- step 코드/프롬프트/version_registry는 **보존** (향후 재도입 가능)
- STEP_MANIFEST: 50 → 49, analysis: 43 → 42

### 버전 업
- scene_detail_composer 1.7.0 → 1.8.0 (prompt v7 → v8)
- package.json 0.5.11 → 0.5.12

### 검증
- 30/30 pass (manifest count 테스트 갱신)
- E2E 재실행으로 폭력 어휘 복원 효과 검증 예정

---

## v0.5.11 (2026.04.20) — 시나리오 의존 표현 전수 범용화 (7개 프롬프트)

### 배경
사용자 지적: 현재 프롬프트에 특정 시나리오(금월도·괴수물)의 구체 예시와 "한국·근미래" 스타일이 하드코딩되어, 어떤 시나리오에도 범용 적용되어야 한다는 원칙(CLAUDE.md·feedback_no_scenario_names)과 충돌. 전수 조사 결과 7개 프롬프트에서 편향 확인.

### 수정 대상 (모두 새 버전 디렉토리)

1. **prototype_prompts/v5 → v6** (`scene_image_en/ko.md`, `entity_reference_en/ko.md`)
   - "contemporary / near-future Korea" 하드코딩 제거 → `world_guide_block`의 era/region 존중
   - "historical/fantasy/medieval/retro styling 금지" → "세계관 가이드와 어긋나는 스타일 금지"로 일반화
   - `Korean baseline clothing` → `clothing consistent with the world guide's era/region`

2. **scene_detail v6 → v7 (1.7.0)**
   - Focus on 금지 예시의 `thigh/hiking/bite wound` → `trembling hand/exposed forearm/patch of discolored skin`
   - `silhouette suspended mid-leap + dusk forest haze` → `backlit figure + dim ambient fill`
   - 공격 섹션 완전 중립화: "조건부 선택적 가이드"로 변경, 장르 키워드(`feral/predatory/blood spray/gaping wound/jagged gash`) 제거, "원문 이상의 훼손 금지" 명시
   - `mouth pressed into neck + utility pole + bite wound spraying blood` → `one figure pinning the other, weight pressing forward` (완전 중립)

3. **location_consistency v1 → v2 (1.1.0)**
   - "approximately 8 meters long" 등 **실 수치 제거** → 상대 크기만 (`wide enough for two people to pass side by side`)
   - `fishing boat` 예시 → 거주 공간 + 야외 공공 공간 2종 범용 예시
   - `traditional Korean fishing boat/village` → "세계관 가이드 시대·지역과 일치"

4. **shot_staging v7 → v8 (2.2.0)**
   - `one foot on pedal` → `one foot on a step, one hand on a doorframe`
   - `바닥 혈흔 질감` → `바닥 패턴 질감, 표면 얼룩`
   - `자전거 바퀴` 예시 → `가구 부속`

5. **scene_consistency v2 → v3 (2.1.0)**
   - 시체 예시(`lying face-down, blood pool, bruised skin`) → 수면 예시(`curled on sofa, blanket`) + 부상 예시 병행
   - "원문에 명시된 상태만. 장르 편향으로 상처·혈흔 임의 추가 금지" 원칙 추가

6. **shot_extract v10 → v11 (2.1.0)**
   - `피묻은 손으로 칼을 쥐고 있는` → `젖은 손으로 물건을 쥐고 있는`
   - 변형 예시 `빙의로 얼굴이 바뀜, 인간→동물/비인간형` → `외모가 근본적으로 바뀜, 종·형상 자체 변화 (원문 명시 시만)`
   - 일시적 상태 예시 `부상, 출혈, 피멍, 화상` → `부상, 얼룩, 멍, 화상 등` (출혈 제거, 중립화)

7. **shot_validator v1 → v2 (1.1.0)**
   - `피묻은 손으로 칼을 쥐고 있는` → `젖은 손으로 물건을 쥐고 있는`
   - `총을 겨누며/겨눈 채` → `손을 뻗으며/뻗은 채`

### 버전 업
- prototype_prompts v6: scene_image_generator 1.4.0, reference_image_generator 1.3.0, world_guide_generator 1.1.0
- scene_detail_composer 1.6.0 → 1.7.0 (prompt v6 → v7)
- scene_consistency 2.0.0 → 2.1.0 (prompt v2 → v3)
- location_consistency 1.0.0 → 1.1.0 (prompt v1 → v2)
- shot_staging 2.1.0 → 2.2.0 (prompt v7 → v8)
- shot_extractor 2.0.0 → 2.1.0 (prompt v10 → v11)
- shot_validator 1.0.0 → 1.1.0 (prompt v1 → v2)
- package.json 0.5.10 → 0.5.11

### 보존 사항
- 각 v5/v6/v1/v7/v2/v10/v1 구버전 프롬프트 **모두 보존** (rollback 가능)
- 프롬프트 로더는 `_version_sort_key` 기반 최신 자동 선택

### 검증
- 54 tests pass (회귀 없음)
- 실제 LLM 생성 결과는 다음 E2E 테스트로 검증 예정

---

## v0.5.10 (2026.04.20) — location_consistency 롤백 + scene_detail v6

### 배경 (v0.5.9 E2E 관찰)
사용자 실측 피드백으로 v0.5.9의 location_consistency 주입이 역효과 확인:
- 얼굴만 분리 (S14/8, S15/3, S21/6) — v5 규칙 "얼굴 부위 C## 허용"이 극단 클로즈업에서 몸과 분리된 얼굴 합성 유발
- 사진 크기 비율 오류 (S29/3, S29/5, S29/8) — "approximately X inches" 같은 실 수치를 T2I가 해석 못함, P04 참조 이미지가 무시되는 현상
- **t2i 프롬프트 평균 단어 수 v4 149 → v5 209 (+40%)** → T2I 모델 혼란
- **이미 `shot_dependency_t2i`가 이전 샷 참조 이미지를 `exact_background`/`atmosphere_reference`로 주입 중** (60개 중 43개 = 72%에서 ignore/keep 지정)

결론: location_consistency 텍스트 주입은 이미 작동 중인 참조 이미지 방식과 **중복** + **오히려 프롬프트 비대화로 품질 저하**.

### location_consistency 주입 롤백 (step 자체는 보존)
- `scene_detail.depends_on`에서 `location_consistency` 제거
- `scene_context_loader._load_location_visuals` 반환값 빈 dict 강제
- `detail_steps._analyze_one`의 location 외형 주입 블록 제거
- `location_consistency` step, 프롬프트, version_registry 엔트리는 **보존** (향후 다른 방식 검토 여지)

### scene_detail v6 (1.6.0)
- **`[L##]` 블록 v4 형식 복귀**: v5의 "<고정 외형> + <씬별 조명>" 통합 규칙 롤백, "공간 묘사 + 조명 + 분위기 20~30단어"로 간결화
- **극단 클로즈업 표현 완화**: "face filling the entire frame" 같은 표현 금지. "tight framing, upper body in frame"처럼 얼굴+몸 일부 함께 유지
- **C##O## 복합 ID 직접 사용**: t2i_prompt 안에 `C01O02` 같은 복합 ID 직접 쓰기 (기존 "bare ID만" 정책 반전). outfit_assignments는 schema 유지. 이미지 생성 단계의 `_build_image_index`가 자동 치환
- **신체 부위 Focus 포커싱 C##O## 완전 금지**: 얼굴·비얼굴 가리지 않고 "Focus on <부위>"에서는 보통명사 + 식별 정보만 (인상 참조 포기, 자연스러운 구도 우선)
- **실 수치 금지**: "approximately X meters/inches" 등 실 수치는 T2I가 해석 못함. "smaller than her palm", "half the TV's height" 등 상대 비율로만

### 버전 업
- scene_detail_composer 1.5.0 → 1.6.0
- package.json 0.5.9 → 0.5.10

### 검증
- 기존 테스트 37/37 pass (location_consistency 8건 유지, manifest count 변경 없음 — step은 보존)
- E2E 재검증: 동일 프로젝트(8427c2c0) scene_detail + 문제 샷 이미지 재생성 예정

---

## v0.5.9 (2026.04.19) — 씬 간 location 외형 일관성 (location_consistency)

### 배경 (금월도 v4 E2E 관찰)
- 이미지 품질 이슈 5번: 같은 location(L##)이 씬마다 다르게 그려짐 (예: 씬 26 대형 탱커 vs 씬 30 작은 어선)
- 원인: scene_image_pipeline이 **location 참조 이미지는 명시적으로 제외**하고 (인물/소품만 주입), scene_detail LLM이 씬마다 `[L##: 설명]` 블록을 달리 해석
- 결과: 씬 이미지 생성은 텍스트 블록만으로 location을 그림 → 씬 간 불일치

### location_consistency step 신설 (order 19.85)
- **목적**: 모든 L##에 대해 씬 간 동일한 외형 문장을 사전 확정. scene_detail이 T2I 프롬프트에 강제 삽입
- **외형만**: 크기·형태·재질·색상·고정 소품. 환경 상태(날씨/시간/조명/인물/감정) 절대 금지
- **영어 3~5 문장, 40~80 단어**, T2I 직접 삽입용
- **scene_director present_entity_ids 기반 매핑** (ID 기반) + fallback 이름 매칭 (heading 단어 경계, 2자 이상)
- **3단계 fallback**: gemini-pro → GPT → entity_detail description (실패 표시)

### scene_detail v5 (1.5.0)
- `[L##: ...]` 블록 규칙 통합: location_consistency 고정 외형 + 씬별 조명/분위기 10~15단어
- 고정 외형 문장은 "단어를 바꾸지 말고 그대로" 블록 앞부분에 포함 지시

### 듀얼 리뷰 Critical 반영
- **CLAUDE.md 절대 규칙 위반**: `text[:800]` 제거, 씬 전문 LLM 전달
- **fallback failed 집계**: fallback의 한국어/분위기 포함 entity_detail description 영구 주입 방지. `analysis_summary="실패"` 항목은 scene_detail 로드에서 제외되어 next resume 시 재시도
- **fut.result() 방어**: 내부 미처리 예외 catch
- **entity_detail key fallback**: `{name}:location` → `name` → name prefix 3단계 lookup
- **이름 substring 오염 방지**: "강" vs "강가" → scene_director ID 기반 우선, substring fallback은 heading + 2자 이상

### 변경 파일
- `prompts/_base/location_consistency/1.202604191800/` (신규, system.md + schema)
- `prompts/_base/scene_detail/5.202604192000/` (신규, v4 기반 [L##] 규칙 통합)
- `backend/app/core/steps/location_consistency_step.py` (신규)
- `backend/app/core/step_manifest.py` (order 19.85 등록)
- `backend/app/core/steps/__init__.py` (STEP_CLASSES)
- `backend/app/modules/llm/llm_client.py` (PIPELINE_STEPS)
- `backend/app/core/dto/scene_analysis.py` (location_visuals_by_id)
- `backend/app/core/steps/scene_context_loader.py` (_load_location_visuals, 실패 마커 스킵)
- `backend/app/core/steps/detail_steps.py` (주입 블록)
- `backend/app/core/version_registry.py` (location_consistency 1.0.0, scene_detail 1.5.0)
- `backend/tests/core/test_location_consistency.py` (신규, 8건)
- `backend/tests/test_step_manifest_v3.py` (카운트 49→50, 42→43)

### 버전 업
- location_consistency 1.0.0 (신규)
- scene_detail_composer 1.4.0 → 1.5.0 (prompt v4 → v5)
- package.json 0.5.8 → 0.5.9

---

## v0.5.8 (2026.04.19) — 이미지 품질 v4 + shot_selection 2중 캡 + toggle 동시성

### 배경 (금월도 v4 E2E 관찰)
- v3에서 이미지 품질 이슈 4건 확인:
  1. "Focus on C##'s <비얼굴 부위>" 패턴으로 얼굴이 허벅지/손목에 합성
  2. 사진·포스터·화면 속 C##으로 2D 매체 위에 실물 얼굴 크게 합성
  3. 실루엣이 "completely black, featureless"로 평면 cutout
  4. 공격·폭력이 친밀 bias(키스/포옹)로 해석
- shot_selection 40% 여전히 목표(15~30%) 초과 — 작은 씬(1~5샷)이 max_n=3 상한에 걸려 100% 선택되는 구조

### scene_detail v4 (이미지 품질 4대 강화)
- **"Focus on C##'s <비얼굴 신체 부위>" 절대 금지** — 얼굴 부위(눈/이마/입/턱)는 C## 허용, 그 외는 보통명사 강제
- **사진/포스터/화면/거울 속 인물 C## 금지** — 2D 매체 속 얼굴은 보통명사 ("a printed portrait of a Korean woman")
- **실루엣 깊이 가이드** — "completely black, featureless" 금지, rim-lit/body volume preserved 필수
- **공격/폭력 4요소** — 역할 명시(attacker/victim) + 공격성 형용사 + 훼손 증거 + 권력 불균형 자세

### shot_selection v4 (2중 캡 + reason 필수)
- **절대 상한 + 비율 상한** 병행: max_n=3 AND `⌊total/2⌋`
- 작은 씬 전량 선택 원천 차단 (예: 3샷 씬 → 최대 1개)
- schema `selected_shots: [{shot_index, reason}]` 필수화
- reason은 "서사 High/시각 High — <근거>" 형식
- Low 서사 샷만 있는 예외적 씬은 0개 허용 (코드 fallback)

### Critical 버그 수정 (듀얼 리뷰)
- `_apply_episode_max_shots`: selected_shots single-source로 통일 (pop/slice 순서 불일치 위험 제거)
- toggle 동시성: `pg_advisory_xact_lock` 추가 — 같은 episode 동시 요청 직렬화
- toggle v3 호환: `selected_shots` 없으면 `selected_shot_indices`로 backfill (기존 선택 유실 방지)
- toggle EPISODE_MAX_SHOTS 초과 방지: add 시 400 거절 (자동 비례 삭감 채택 안 함)

### E2E 실측 (baedcb5c v4 프로젝트)
- shot_selection 40.0% → **33.0%** (200 → 66 shots, 14 shots 감소)
- scene_detail 평면 실루엣: **0건** (이전 1건+)
- scene_detail 사진 속 C##: **0건** (이전 1건+)
- Focus on C## 비얼굴: 2건 잔존 (S13/2 foot, S24/2 grip) — 모두 3인칭 전신 구도라 얼굴 합성 없음 (이미지 실측)
- 알려진 롱테일: S13/2 경찰차 문 위치 불자연 (참조 이미지 해석 한계, T2I 모델 범위)

### 테스트
- 신규 4건 추가 (advisory lock / EPISODE_MAX_SHOTS cap / deselect 우회 / v3 backfill)
- 기존 3건 회귀 없음 (7/7 pass)

### 버전 업
- shot_selector 3.0.0 → 4.0.0 (prompt v3 → v4)
- scene_detail_composer 1.3.0 → 1.4.0 (prompt v3 → v4)
- package.json 0.5.7 → 0.5.8

---

## v0.5.7 (2026.04.18) — ROI 기반 shot_selection + 복잡 장면 대안 전략 (scene_detail v3)

### 배경 (금월도 v3 E2E 관찰)
- shot_selection이 중요도 낮은 샷 과다 선택 (84 shots = 37.7%). 자전거 페달 밟기 등 연결 순간 단독 선택
- 원작 "자전거를 타고 달린다"를 shot_extract가 "페달에 발을 올린 측면"으로 쪼개 정지화 → T2I 해석 오류(seated)
- 사용자 지침: "ROI 높은 것만, T2I 어려운 건 회피/단순화/부분포커싱"

### shot_selection v3 (ROI 2축 평가)
- **A축 서사적 중요도** (High/Medium/Low) + **B축 시각 표현 가치** (High/Medium/Low) 매트릭스
- **Low 서사 전량 선택 금지** (시각 가치 상관없이)
- 고서사×저시각 → scene_detail에서 부분 포커싱/회피 전략 위임
- **선택 비율 15~30%**로 하향 (v2 30~50%)
- 연결 순간/일상 이동 단독 선택 금지 (범용 원칙)
- 시나리오 고유명사 0 (사용자 엄수 요구)

### scene_detail v3 (복잡 장면 3-전략)
- **A. 단순화**: 인물/소품/배경 수 축소, 핵심 1요소만 깊게
- **B. 부분 포커싱**: 전신·다수 구도 포기, 부위/소품 클로즈업 (C## 금지, 보통명사)
- **C. 회피**: 동적 연속 동작 → 정적 결과 또는 반응 샷으로 re-frame
- 선택 힌트: "무엇이" → A, "어떤 디테일" → B, "어떤 감정/결과 + 복잡" → C

### 버전 업
- shot_selector 2.0.0 → 3.0.0 (prompt v2 → v3)
- scene_detail_composer 1.2.0 → 1.3.0 (prompt v2 → v3)
- package.json 0.5.6 → 0.5.7

---

## v0.5.6 (2026.04.18) — shot_validator gpt fallback + shot_staging v7 영화적 다양성

### shot_validator 3-tier retry (Gemini content_filter 우회)
- 금월도 v2 E2E 실측: 씬 19 `finish_reason=content_filter` deterministic 차단
- 3-tier attempt: `primary (gemini-pro)` → `retry_same` → `fallback_gpt (GPT-5.4)`
- `content_filter`/`safety`/`recitation`/`empty response` 키워드 감지 시 `retry_same` 자동 스킵 (결정적 차단에서 동일 재시도 무의미)
- `project_config` 깊은 병합으로 다른 step 설정 보존
- attempt_label을 schema_name 접미사 + opik tag로 기록

### shot_staging v7 (범용 영화적 연출 강화)
- **영화적 shot type 필수**: ECU/CU/MCU/MS/MWS/WS/EWS 중 1개 명시
- **앵글 다양화**: low/high/Dutch/overhead/worm's eye, 씬 내 shot끼리 반복 회피
- **연출 기법**: OTS, POV, silhouette, reflection, rack focus, handheld 등
- **body_pose "standing" 남용 금지** + 구체적 자세 카테고리 (mid-stride paused, leaning, crouching 등)
- **정당한 standing 허용 예외**: 의례/제식/정렬 등 맥락 명시 (법정, 장례, 점호 등 범용 예)
- **facing_camera 기본값 회피**: three-quarter 선호, 의미 있을 때만 정면
- **씬 내 다양성 체크**: shot type/앵글/방향 상호 다양화 (shot 1개 씬은 자연 적용 안 됨)
- **시나리오 고유명사 0** (특정 작품 의존 금지 규칙 엄수)

### 근거 데이터
금월도 v1 T2I 79 프롬프트 실측:
- facing camera 41.8% / standing 25.3% / full-body 5.1%
- low angle / high angle / Dutch angle / POV / wide shot 표준 용어: **0%**
- 연출 단조성 확인 → v7 설계 방향 결정

### 마이그레이션
기존 프로젝트 — shot_staging v7 효과 보려면 `shot_staging` force 재실행 + 다운스트림(scene_detail, shot_dependency_t2i, t2i_review, scene_image_pipeline) 순차 재실행 필요

### 테스트
- `test_shot_validator.py` 17 tests (신규 5: skips_retry_on_empty, retries_same_on_transient, skips_retry_same_when_safety_keyword, preserves_shot_validator_config_keys_in_fallback, preserves_original_when_all_three_fail)
- baseline 281 passed

### 리뷰 반영
Codex + Claude 병행:
- Claude Important 1: schema.json `body_pose`의 `standing` 예시 → 구체적 자세 + 금지 명시로 교체 (system.md와 일치)
- Claude Important 2 / Codex Important 1: deterministic 차단 시 `retry_same` 스킵 로직 추가
- Codex Important 2: v7에 정당한 `standing` carve-out 추가 (의례/제식 맥락)
- Codex Question: `project_config` 깊은 병합으로 기존 shot_validator 키 보존
- Minor (보류): opik `scene_{si}` tag를 metadata로 이동, opik tagging 테스트 추가

---

## v0.5.5 (2026.04.18) — editorial step 1-pass 정책 적용

### 배경
`t2i_review` 같은 editorial step이 타 체크포인트를 수정하면 step_runner가 다운스트림을 자동 `stale` 마킹. 이 cascade가 `run-all resume` 재실행 시 editorial을 또 발동 → 매번 새 fixes 발견 → **무한 수렴 실패**. 금월도 v1 E2E에서 1st pass(53 fixes) / 2nd pass(48 fixes)로 수렴 안 되는 현상 목격.

### 변경
- `step_runner.py:294-309` editorial cascade **기본 비활성**. manifest에 `invalidate_downstream_on_edit: True` 명시한 경우에만 cascade
- `StepEntry` dataclass에 `invalidate_downstream_on_edit: bool = False` 필드 추가 (STEP_CATALOG만 보는 소비자 원칙 준수)
- cascade-off 경로에 `logger.debug` 추가 — 관찰 가능성 유지
- 계약 테스트 (`test_editorial_cascade_disabled_by_default`) + StepCatalog 노출 테스트 + StepRunner 런타임 테스트 4건 추가

### 정책 Tradeoff (중요)
- editorial이 수정한 체크포인트 파일은 즉시 업데이트됨
- 그러나 resume 모드는 `completed` 상태 step을 스킵 → **다운스트림이 이미 생성한 결과는 editorial 수정 전 버전 기반**
- editorial fix를 다운스트림 최종 output에 반영하려면 해당 step을 **force 재실행** 필요
- 이 tradeoff는 1-pass 정책의 명시적 대가. 수렴 안전성을 위해 "editorial 수정이 모든 다운스트림에 전파"라는 강한 보장을 포기

### 영향
- 기존 editorial step (`t2i_review`, deprecated `scene_verify`) 모두 1-pass 동작
- `run-all resume` 후 stale cascade로 UI에 혼란스러운 stale 표기 사라짐
- 이미 stale/blocked 상태인 step_run row는 이 커밋만으로 자동 복구되지 않음 — 필요 시 해당 step force 재실행 또는 snapshot 복원

---

## v0.5.4 (2026.04.18) — shot_validator 신설 (한 찰나 원칙 후처리 검증)

### 신규 step: `shot_validator` (order 7.25)
- shot_extract 결과 각 shot의 description을 LLM으로 검증·재작성
- 시간 연결어("~하며", "~하고", "~한 뒤") 및 연속 동작 섞임 검출 시 단일 정지 순간으로 재작성
- 원본은 `original_description` 필드로 백업 (감사·원복 가능)
- 실측 근거: 기존 shot_extract v10(1,206 shots, 15 프로젝트)에서 시간 연결어 2.5% 잔존 — 주 패턴 "~하며 서 있는/바라보는"
- 프롬프트 `shot_validator/1.202604181200`, gemini-pro 모델, 씬 단위 병렬

### 다운스트림 전환
- 14개 step의 `_load_prev_checkpoint("shot_extract")` → `_load_prev_checkpoint("shot_validator")`
- manifest depends_on 10곳 교체 (`entity_all_*`, `shot_selection`, `shot_director`, `scene_camera_flow`, `shot_staging`, `scene_detail`, `t2i_review`, `set_design`)
- 체크포인트 구조 동일 → 다운스트림 로직 변경 없음

### 기존 프로젝트 마이그레이션
- 기존 프로젝트는 `shot_validator` force 실행 필요 (체크포인트 없으면 다운스트림 실패)

### 테스트
- `tests/core/test_shot_validator.py` 9 tests (정상·재작성·빈 revision·LLM 실패·shot_index 누락·정렬 등)
- baseline 260 → 269 passed

---

## v0.5.3 (2026.04.17) — 코드리뷰 Critical/Important 수정 + Codex/Claude 2차 리뷰 반영

### 2차 리뷰 반영
- **entity_episode_link outlook link 보존** (`steps.py`): UPSERT sweep이 C/L/P만 추적하므로 기존 outlook link가 실수 삭제되는 버그 수정 — `_existing_links`를 C/L/P canon ID로 한정
- **restore_snapshot whitelist 확장** (`steps.py`): `not_applicable`/`skipped`/`error` 누락 → 이전 상태의 강등 방지
- **멤버 제거/역할변경 unhandled rejection** (`ProjectDetail.tsx`): `throw err` 제거 — MemberList가 JSX에서 직접 호출하여 catch 없음
- **toggle_shot_selection 응답 warnings** (`steps.py`): 파일 쓰기 실패 시 silent 성공 응답 → response에 `warnings` 필드 포함
- **Shot 토글 race 프론트 가드** (`EpisodeDetail.tsx`, `SceneVariationCard.tsx`): `togglingShots` Set + `handleToggleShot` 공통 함수 + 버튼 `disabled` — pending 요청 중 중복 클릭 차단 (쓰기 권한이 있는 단일 writer 전제, 백엔드 advisory lock 불요)

### Backend
- **보안 경고** (`main.py`): dev-secret-key, admin123, creator123 기본값 사용 시 startup WARNING 로그
- **toggle_shot_selection 트랜잭션 순서** (`steps.py`): DB commit 선행 → 원자 파일 쓰기(tmp+rename). 파일/DB 불일치 방지
- **entity_episode_link UPSERT 전환** (`steps.py`): DELETE→INSERT 패턴 제거, (canon_id, episode_id) 기준 UPSERT로 `t2i_appearance_count` 등 메타데이터 보존
- **restore_snapshot status 복원** (`steps.py`): status 강제 completed → 아카이브 실제 status 값 사용
- **pipeline_gate N+1 쿼리 제거** (`pipeline_gate.py`): missing_combos 루프의 entity 조회 → `IN` 배치 쿼리
- **번역 실패 fallback** (`image_service.py`): translation 실패 + 한국어 잔존 시 `error` 레벨 로그 (Gemini 안전필터 대응)
- **scene_consistency selected_map 누락 방어** (`scene_consistency_step.py`): shot_selection에 씬 없을 때 전체 샷 포함 → 빈 리스트 + 경고
- **applies_to_shots int 캐스팅** (`detail_steps.py`): LLM이 string 반환해도 match 성공

### Frontend
- **Sidebar 버전 자동 동기화** (`vite.config.ts`, `Sidebar.tsx`): `__APP_VERSION__`/`__BUILD_DATE__`/`__BUILD_TAG__` 주입. 하드코딩 제거
- **ProjectDetail 멤버 핸들러** try/catch + `addToast`: 추가/제거/권한변경 실패 시 사용자 피드백
- **fetchAllStillImages** (`EpisodeDetail.tsx`): `for ... not awaited` → `Promise.allSettled`
- **alert() → addToast 전면 교체**: 6건 (Episodes, PromptManager, PipelineStepsPanel ×3, LLMConfigPanel)

## v0.5.2 (2026.04.17) — 시점 혼합 수정 + 전체 코드리뷰

### 씬 이미지 품질 (4건 수정)
- **scene_detail v2 프롬프트 규칙 추가**
  - 단일 시점 규칙: 1인칭 클로즈업 + 3인칭 전신 혼합 금지
  - 클로즈업 구도에서 C## 사용 금지 (손/팔/소품 포커스 시 보통명사만)
  - 소품 방향 단일 명시 (카메라/인물 양방향 동시 지정 금지)
  - 복잡 구도 단순화 (2인+소품 클로즈업+배경 → 핵심만)

### image_service.py (v1.8.1)
- `"keep the face, hair, and identity from image N"` → `"use image N as character appearance reference — match the person's identity where visible in the scene"`
- `CRITICAL` 라인 중립화: `"If only a body part is shown, do NOT add the face"`
- bare C## → C##O## 치환 시 composite form 매칭 추가 (`\bC##\b(?:O\d{2,3})?`)
- 중복 `check_scene_images_ready` 호출 제거

### detail_steps.py
- `_check_prompts` 복합 ID (C##O##) 위반 감지 추가
- `_ve_pattern` 2자리 → 3자리 지원 (`[CLP]\d{2,3}(?:O\d{2,3})?`)

### 시나리오 의존 예시 제거 (CLAUDE.md 규칙)
- `shot_staging/4.../system.md` — "수리영", "인우" → "해당 인물의 이름(원어)"
- `shot_staging/5.../system.md` — 동일
- `t2i_review/1.../scene_system.md` — "옥탑방", "PC방" → "특정 문화권의 고유 공간"
- `visual_world_rules/3.../system.md` — "옥탑방, 항구, 마트, 경찰서" → "특정 장소 유형"

### version_registry.py
- 중복 정의 제거 (image_service 1.7.0)
- shot-more 신규 모듈 8개 등록 (scene_consistency, character_state_variant, shot_dependency_t2i, scene_camera_flow, shot_extractor, shot_selector, shot_staging, scene_detail_composer)

### docs/architecture/
- 7개 문서 1,853줄 → 2,283줄 (신규 step 3개 반영 + v0.5.1 변경사항 + Active/Legacy 구분표)

## v0.5.1 (2026.04.16) — 참조 이미지 매칭 수정

### image_service.py
- T2I 텍스트 regex skip 전면 삭제 (back_to_camera/over_shoulder/closed 오탐 제거)
- state_variant ref 없을 때 skip → 일반 composite ref fallback으로 변경
- exact_background + fixed_char → state_variant 강제 스킵 최적화 삭제
- prop ref 주입: t2i_prompt에 P## 언급된 소품만 (word-boundary regex 체크)
- `skip_char_sids` 파라미터/dead code 완전 제거

### scene_detail v2 프롬프트
- C## 사용 규칙: 얼굴 식별 가능 → C##, 뒷모습/실루엣 → 보통명사
- 고정 요소 통합 규칙: C## 매핑 인물의 이중 묘사(보통명사+C##) 금지
- 길이 제한 제거 (Gemini LLM 기반 — 모순/중복 방지가 핵심)

### detail_steps.py
- 고정 요소 주입 지시문 하드코딩 제거 → system.md로 이동

## v0.5.0 (2026.04.15) — shot-more 재설계

### Pipeline
- **shot_extract v10**: "한 샷 = 한 찰나" 극단화. 수 제한 제거, 연속 동작 분해(before/during/after) 강제, 시간 연결어 금지
- **shot_selection v2**: "이미지 생성 가치" 기준 재정의. 비선택 샷은 버리지 않고 맥락/플로우 설계 자료로 보존
- **scene_camera_flow 신규 step**(order 17.05, gemini-pro): 씬 관통 카메라 연속 이동 경로 설계 + 선택된 샷을 플로우 위에 배정
- **shot_staging v6**: 독립 카메라 결정 금지 → scene_camera_flow에서 파생. "앞 샷과 달라진 축 하나만 강조" 규칙
- **scene_detail 프롬프트 전용 디렉토리**(`prompts/_base/scene_detail/1.202604151200/`): scene_extractor_v2 fallback 탈피, 단일 순간 규칙 강화
- **shot_cinematography deprecate**: 의존성 제거, 체크포인트/코드는 보존 (롤백 가능)

### DB / API
- `scene_still` 테이블: `is_selected`, `image_generated` 컬럼 추가 (기존 데이터 백필)
- `_sync_checkpoints_to_db` 확장: 비선택 샷도 scene_still row 생성 (맥락 보존)
- `PATCH /shot_selection/toggle` 개선: 즉시 DB `is_selected` 반영, scene_camera_flow/shot_staging/scene_consistency 하위 무효화 추가
- `SceneStillResponse`: `is_selected`, `image_generated` 필드 노출

### UI
- `SceneVariationCard`: 비선택 샷 간소 카드 (흐림 / dashed 보더 / 읽기 전용 / "+선택" 토글 버튼)
- Sidebar 버전 v0.5.0 표시

### Docs
- `docs/shot-more/00-design.md`: 설계 문서 (12 섹션)

## v0.3 (2026.03.27)

### Pipeline
- 변형 캐릭터 자동 감지 + scene_detail 프롬프트 주입 (이름 기반 base name 그룹핑)
- 서브씬 처리 (몽타주/회상/인터컷 등 한 시공간만 선택)
- T2I 단일 스틸컷 원칙: scene_director 전체 인물을 한 컷에 강제하지 않음
- scene_detail 실패 씬 1회 자동 retry
- entity_t2i 인간형 인물 국적/인종 명기 규칙 (v5 프롬프트)

### Image Generation
- 개별/배치 이미지 생성 동일 경로 통합 (ref_image_pipeline)
- aspect ratio 통일: character=3:4, outlook=3:4, prop=1:1, composite=16:9
- 합성 이미지 개별 생성 API (POST /composites/generate)
- 인물/아웃룩 변경 시 합성 재생성 확인 다이얼로그
- face/composite primary 분리 관리 (_auto_set_primary, set_primary_image)
- face 로드 시 composite 이미지 제외 (배치+개별 모두)
- generation-status: location 제외 + distinct entity 카운트

### UI
- 캐릭터 상세: 얼굴(별표/대표/확대) + 합성(대표 primary만 확대)
- 인물+아웃룩 모달: 합성 이미지 별표/대표/확대 + 1개일 때 자동 대표
- 이미지 확대 모달 fullscreen (90vw x 85vh)
- 엔티티 타입별 업로드 aspect ratio 힌트 (3:4, 1:1, 16:9)
- Dashboard per_page=100

### Bug Fixes
- stable_traits list 타입 처리 (reference_image_generator)
- custom_prompt regression 수정 (t2i_prompt보다 우선)
- 합성 업로드 face 버킷 오류 → 업로드 제거 (생성만 유지)

## v0.2 (2026.03.26)

### Pipeline
- short_id 조기 생성 (entity_all → 전 파이프라인 전파)
- outlook 3단계 분할 (phase1/2/3 독립 step)
- outlook phase3 리팩토링 (LLM은 병합 판별만, 코드에서 정리)
- entity_t2i short_description(50자) 추가

### Bug Fixes
- entity_t2i 이름 보존 (괄호 소실 방지)
- outlook orphan retry (phase1 직후 0개 캐릭터 감지)
- outlook phase2 character_id 소유자 명시
- _sync_checkpoints_to_db 카운터 충돌 방지

## v0.1 (2026.03.25)

- Pipeline v3 초기 구현 (26단계)
- FastAPI + React UI
- Gemini/GPT 멀티 LLM 파이프라인
- 씬 세그먼테이션 (정규식 + LLM)
- scene_director Gemini Pro
- 이미지 생성 파이프라인 (참조 + 합성 + 씬)
