# v10 기준 코드 리뷰

검토 기준 문서는 [docs/v10/00-architecture.md](/Users/manta/Documents/Projects/TheRoad-I1/docs/v10/00-architecture.md:1), [docs/v10/01-pipeline-steps.md](/Users/manta/Documents/Projects/TheRoad-I1/docs/v10/01-pipeline-steps.md:1), [docs/v10/02-prompts-and-schemas.md](/Users/manta/Documents/Projects/TheRoad-I1/docs/v10/02-prompts-and-schemas.md:1), [docs/v10/03-image-generation.md](/Users/manta/Documents/Projects/TheRoad-I1/docs/v10/03-image-generation.md:1), [docs/v10/04-infrastructure.md](/Users/manta/Documents/Projects/TheRoad-I1/docs/v10/04-infrastructure.md:1)이다. 실제 구현은 주로 [backend/app/core](/Users/manta/Documents/Projects/TheRoad-I1/backend/app/core), [backend/app/api/v1](/Users/manta/Documents/Projects/TheRoad-I1/backend/app/api/v1), [backend/app/modules](/Users/manta/Documents/Projects/TheRoad-I1/backend/app/modules), [backend/app/services](/Users/manta/Documents/Projects/TheRoad-I1/backend/app/services), [frontend/src](/Users/manta/Documents/Projects/TheRoad-I1/frontend/src) 기준으로 읽었다.

이번 리뷰는 정적 코드 분석 중심이다. 사용자의 요청대로 `docs/v10-c` 외 경로는 수정하지 않았고, 테스트 실행도 다른 폴더를 건드릴 가능성이 있어 의도적으로 하지 않았다.

## 총평

v10 문서가 설명하는 파이프라인의 큰 줄기는 실제 코드에 거의 반영되어 있다. 다만 데이터 동기화와 step orchestration 층에서 문서의 절대 규칙을 정면으로 깨는 코드가 남아 있고, 그 결과 멀티 에피소드 프로젝트에서 데이터가 사라지거나, 성공한 step이 실패로 기록되거나, UI가 실제보다 더 자주 막히는 문제가 생길 수 있다.

가장 큰 위험은 `checkpoint -> DB` 동기화 코드가 "에피소드 단위 결과"를 기준으로 "프로젝트 전역 canon/outlook"를 삭제 재구성한다는 점이다. 문서상 `entity_canon`은 프로젝트 전역 정의이고 `entity_episode_link`가 에피소드 연결인데, 현재 구현은 이 경계를 지키지 않는다.

## 주요 발견 사항

### 1. Critical: 에피소드 하나의 동기화가 프로젝트 전체 엔티티/아웃룩을 삭제할 수 있음

문서상 `entity_canon`은 프로젝트 전역 정의이고 `entity_episode_link`는 에피소드별 연결이다. 또한 절대 규칙으로 `scene_still sync는 UPSERT`, `DB/프로젝트 파일 삭제 금지`가 명시돼 있다. 근거: [docs/v10/00-architecture.md](/Users/manta/Documents/Projects/TheRoad-I1/docs/v10/00-architecture.md:98), [docs/v10/00-architecture.md](/Users/manta/Documents/Projects/TheRoad-I1/docs/v10/00-architecture.md:114).

하지만 실제 동기화 함수 [_sync_checkpoints_to_db](/Users/manta/Documents/Projects/TheRoad-I1/backend/app/api/v1/steps.py:694)는 현재 에피소드의 `entity_t2i` 체크포인트 이름 집합을 프로젝트 전체 `entity_canon` 이름 집합과 비교한 뒤, 다르면 곧바로 `character_outlook`, `entity_episode_link`, `entity_canon`을 `project_id` 기준으로 전부 삭제한다. 근거: [backend/app/api/v1/steps.py](/Users/manta/Documents/Projects/TheRoad-I1/backend/app/api/v1/steps.py:724), [backend/app/api/v1/steps.py](/Users/manta/Documents/Projects/TheRoad-I1/backend/app/api/v1/steps.py:741).

이 로직은 멀티 에피소드 프로젝트에서 거의 항상 위험하다. 예를 들어 episode A의 엔티티와 episode B의 엔티티 이름 집합이 다르면, B를 sync하는 순간 A가 쓰던 canon과 link, outlook 연결이 같이 날아간다. 뒤의 아웃룩 동기화도 동일하게 프로젝트 전역 `character_outlook` 삭제와 기존 outlook 삭제를 수행한다. 근거: [backend/app/api/v1/steps.py](/Users/manta/Documents/Projects/TheRoad-I1/backend/app/api/v1/steps.py:1193), [backend/app/api/v1/steps.py](/Users/manta/Documents/Projects/TheRoad-I1/backend/app/api/v1/steps.py:1237), [backend/app/api/v1/steps.py](/Users/manta/Documents/Projects/TheRoad-I1/backend/app/api/v1/steps.py:1293).

영향은 단순 UI 불일치가 아니다. 이미지 참조, relation participant, shot-level visible entity 해석, export 결과까지 모두 프로젝트 전역 canon을 전제로 움직이기 때문에, 특정 에피소드 sync가 다른 에피소드의 분석 산출물을 오염시킬 수 있다.

권고는 "프로젝트 전역 canon upsert + episode link upsert"로 구조를 분리하는 것이다. 삭제가 필요하더라도 `episode_link` 범위에서만 제한적으로 처리해야 한다.

### 2. High: `scene_still` 동기화가 여전히 삭제 기반이라 이미지 연결이 끊어짐

문서는 `scene_still sync는 UPSERT — DELETE→INSERT 금지`라고 못 박고 있다. 근거: [docs/v10/00-architecture.md](/Users/manta/Documents/Projects/TheRoad-I1/docs/v10/00-architecture.md:114).

실제 구현은 기존 still을 key로 찾아 업데이트하는 부분까지는 괜찮지만, 체크포인트에 없는 still은 `image_asset.still_id = NULL`로 끊은 뒤 `SceneStill` 자체를 삭제한다. 근거: [backend/app/api/v1/steps.py](/Users/manta/Documents/Projects/TheRoad-I1/backend/app/api/v1/steps.py:1091), [backend/app/api/v1/steps.py](/Users/manta/Documents/Projects/TheRoad-I1/backend/app/api/v1/steps.py:1100).

이건 선택 해제, shot 재분배, 일시적인 checkpoint 불일치가 생겼을 때 이미 생성된 scene asset을 손쉽게 orphan으로 만든다. 특히 `dependent_scene_id`와 이미지 이력은 continuity 파이프라인의 핵심인데, 현재 방식은 재분석 한 번에 그 연결성을 지워버린다.

권고는 hard delete 대신 `stale` 또는 `inactive` 플래그를 두고, 기존 `SceneStill.id`를 보존하는 방식으로 바꾸는 것이다. 이미지 자산이 이미 존재하는 시스템에서 FK를 끊고 row를 삭제하는 건 비용이 너무 크다.

### 3. High: `scene_consistency`의 GPT fallback 성공 후에도 실패 결과가 추가됨

`scene_consistency`는 1차 실패 후 sanitized retry, 그 다음 GPT fallback까지 가는 3단계 구조다. 그런데 GPT fallback이 성공해도 바로 아래 코드가 무조건 실패 placeholder를 append하고 `failed += 1`을 수행한다. 근거: [backend/app/core/steps/scene_consistency_step.py](/Users/manta/Documents/Projects/TheRoad-I1/backend/app/core/steps/scene_consistency_step.py:318), [backend/app/core/steps/scene_consistency_step.py](/Users/manta/Documents/Projects/TheRoad-I1/backend/app/core/steps/scene_consistency_step.py:340).

즉 한 scene에 대해 성공 결과와 `"분석 실패"` 결과가 동시에 들어갈 수 있고, step 전체 `failed_count`도 거짓으로 증가한다. 이 상태면 step status가 `partial`로 떨어질 수 있고, downstream 로그와 checkpoint가 실제보다 나쁘게 기록된다.

downstream에서 완전히 치명적인 데이터 손실로 이어지지는 않는 경우도 있지만, 같은 `scene_index`가 중복 저장되는 것 자체가 체크포인트 설계를 무너뜨린다. 이 문제는 단순 indentation 버그라서, 실패 placeholder append를 `except exc3` 블록 안으로 옮기면 해결된다.

### 4. High: runner는 `partial`을 허용하지만 API와 프런트는 `blocked`로 취급함

실행 게이트를 담당하는 [StepRunner.check_gate](/Users/manta/Documents/Projects/TheRoad-I1/backend/app/core/step_runner.py:64)는 선행 step 상태로 `completed`, `not_applicable`, `partial`을 모두 허용한다. 즉 partial upstream이 있어도 downstream은 실행 가능하다는 뜻이다.

그런데 step 상태 조회 API는 `completed`, `not_applicable`만 허용 상태로 본다. `partial`은 `can_run=false`가 되고, pending step은 `blocked`로 내려간다. 근거: [backend/app/api/v1/steps.py](/Users/manta/Documents/Projects/TheRoad-I1/backend/app/api/v1/steps.py:84). 프런트는 이 `can_run`을 그대로 버튼 활성화 조건에 사용한다. 근거: [frontend/src/components/shared/PipelineStepsPanel.tsx](/Users/manta/Documents/Projects/TheRoad-I1/frontend/src/components/shared/PipelineStepsPanel.tsx:182).

결과적으로 백엔드는 실행 가능한데 UI는 "막힘"으로 보여주고, 운영자가 수동 실행할 수 없는 상태가 생긴다. 이건 장애 복구나 부분 성공 이후의 후속 작업에서 특히 불편하다.

권고는 한 군데를 기준으로 semantics를 통일하는 것이다. 현재 구조라면 `get_all_steps`가 `partial`을 `can_run=true`로 보도록 고치는 편이 가장 안전하다.

### 5. Medium: checkpoint 저장 실패를 삼켜서 DB 상태와 파일 상태가 어긋날 수 있음

문서는 checkpoint 시스템을 원자적 저장과 아카이빙의 핵심 레이어로 설명한다. 근거: [docs/v10/00-architecture.md](/Users/manta/Documents/Projects/TheRoad-I1/docs/v10/00-architecture.md:72), [docs/v10/04-infrastructure.md](/Users/manta/Documents/Projects/TheRoad-I1/docs/v10/04-infrastructure.md:22).

하지만 실제 [StepRunner.save_checkpoint](/Users/manta/Documents/Projects/TheRoad-I1/backend/app/core/step_runner.py:127)는 write/replace 실패를 `logger.error`만 남기고 예외를 올리지 않는다. 근거: [backend/app/core/step_runner.py](/Users/manta/Documents/Projects/TheRoad-I1/backend/app/core/step_runner.py:138).

이 상태에서 `run()`은 이미 `step_run`을 completed/partial로 갱신한 뒤 계속 진행하므로, DB에는 성공으로 남았는데 checkpoint 파일은 이전 버전이거나 아예 없는 상태가 가능하다. 이후 `GET /result`, `_sync_checkpoints_to_db`, image pipeline, planning context 주입이 모두 checkpoint 파일을 신뢰하기 때문에, 실제 영향 범위가 넓다.

권고는 checkpoint 저장 실패를 step 실패로 승격하는 것이다. 최소한 `save_checkpoint()`가 실패하면 `run()`이 예외를 던져 `step_run.status='failed'`가 되게 해야 한다.

### 6. Medium: `applicability=if_*` 메타데이터가 실제로는 거의 동작하지 않음

문서는 `step_manifest.py`의 `applicability`를 정식 메타데이터로 정의한다. 근거: [docs/v10/01-pipeline-steps.md](/Users/manta/Documents/Projects/TheRoad-I1/docs/v10/01-pipeline-steps.md:5).

하지만 base runner의 [check_applicability](/Users/manta/Documents/Projects/TheRoad-I1/backend/app/core/step_runner.py:82)는 `disabled`, `always`, `on_demand`만 처리하고 나머지 `if_*`는 전부 `True`로 통과시킨다. 근거: [backend/app/core/step_runner.py](/Users/manta/Documents/Projects/TheRoad-I1/backend/app/core/step_runner.py:82). 실제 manifest에는 `planning_doc_analysis=if_planning_doc`, `composite_image_gen=if_has_outlooks`가 선언돼 있다. 근거: [backend/app/core/step_manifest.py](/Users/manta/Documents/Projects/TheRoad-I1/backend/app/core/step_manifest.py:18), [backend/app/core/step_manifest.py](/Users/manta/Documents/Projects/TheRoad-I1/backend/app/core/step_manifest.py:479).

현재는 `run-all`에서만 `if_planning_doc`를 별도 하드코딩으로 일부 보정하고 있고, 나머지는 step class가 직접 빈 결과를 반환하면서 사실상 우회한다. 이 구조는 manifest를 신뢰하는 UI/API와 실제 실행이 계속 벌어지게 만든다.

권고는 `if_planning_doc`, `if_has_outlooks`, `if_multi_char_scenes` 같은 rule을 base runner에서 공통 처리하거나, 최소한 해당 step class들에 명시적 override를 두는 것이다.

### 7. Medium: 모델 해석 소스가 이중화되어 있고 이미 v10 manifest와 어긋나 있음

문서와 코드 주석 모두 `step_manifest.py`를 SSOT처럼 설명하지만, 실제 모델 해석은 [llm_client.PIPELINE_STEPS](/Users/manta/Documents/Projects/TheRoad-I1/backend/app/modules/llm/llm_client.py:174)가 별도로 담당한다. 근거: [backend/app/core/step_manifest.py](/Users/manta/Documents/Projects/TheRoad-I1/backend/app/core/step_manifest.py:3), [docs/v10/04-infrastructure.md](/Users/manta/Documents/Projects/TheRoad-I1/docs/v10/04-infrastructure.md:56).

문제는 이 테이블이 이미 manifest와 어긋나 있다는 점이다. `planning_doc_analysis`와 `character_state_variant`는 `llm_client.PIPELINE_STEPS`에 아예 없고, 그 결과 `_resolve_model()`은 둘 다 fallback인 `gemini-pro`를 반환한다. 반면 manifest는 `planning_doc_analysis`를 flash-lite 계열, `character_state_variant`를 `gemini-image`로 정의한다. 근거: [backend/app/core/step_manifest.py](/Users/manta/Documents/Projects/TheRoad-I1/backend/app/core/step_manifest.py:22), [backend/app/core/step_manifest.py](/Users/manta/Documents/Projects/TheRoad-I1/backend/app/core/step_manifest.py:489), [backend/app/modules/llm/llm_client.py](/Users/manta/Documents/Projects/TheRoad-I1/backend/app/modules/llm/llm_client.py:175).

image step는 실제 생성 코드에서 별도 client를 쓰므로 즉시 오동작하지 않는 경우도 있지만, `resolved_model`, step list UI, 운영 로그, 디버깅 정보는 이미 틀린 값을 기록한다. `planning_doc_analysis`처럼 실제 `call_structured()`가 `llm_client`를 타는 step은 모델 라우팅 자체가 문서와 달라진다.

권고는 manifest의 alias만 쓰도록 구조를 정리하거나, 최소한 startup 시 두 매핑을 검증해 누락 step이 있으면 서버가 뜨지 않게 해야 한다.

### 8. Medium: `shot_dependency_t2i`가 다른 step의 checkpoint를 비원자적으로 덮어씀

문서상 checkpoint는 `tmp -> os.replace()`와 아카이빙을 전제로 관리된다. 근거: [docs/v10/00-architecture.md](/Users/manta/Documents/Projects/TheRoad-I1/docs/v10/00-architecture.md:72).

그런데 [ShotDependencyT2iStep](/Users/manta/Documents/Projects/TheRoad-I1/backend/app/core/steps/shot_dependency_t2i_step.py:239)는 자기 checkpoint만 저장하는 대신, 소비자가 읽는 `shot_dependency/manifest.json`을 직접 읽고 `write_text()`로 덮어쓴다. 그것도 target 파일이 이미 있을 때만 수행한다. 근거: [backend/app/core/steps/shot_dependency_t2i_step.py](/Users/manta/Documents/Projects/TheRoad-I1/backend/app/core/steps/shot_dependency_t2i_step.py:239).

이 구현은 두 가지 문제가 있다. 첫째, StepRunner의 원자적 저장/아카이빙을 우회한다. 둘째, target checkpoint가 없으면 `shot_dependency_t2i` 결과가 downstream 소비 경로로 전파되지 않는다. 실제 image service는 `shot_dependency_t2i`가 아니라 `shot_dependency` manifest를 읽는다. 근거: [backend/app/services/image_service.py](/Users/manta/Documents/Projects/TheRoad-I1/backend/app/services/image_service.py:1568).

권고는 downstream 소비 경로를 `shot_dependency_t2i` 자체로 바꾸거나, 공통 checkpoint writer를 통해 alias step을 원자적으로 갱신하는 것이다.

## 문서-구현 드리프트

v10 아키텍처 문서는 프런트를 Next.js로 기록하지만 실제 런타임은 Vite + React Router SPA다. 근거: [docs/v10/00-architecture.md](/Users/manta/Documents/Projects/TheRoad-I1/docs/v10/00-architecture.md:12), [frontend/package.json](/Users/manta/Documents/Projects/TheRoad-I1/frontend/package.json:6), [frontend/src/main.tsx](/Users/manta/Documents/Projects/TheRoad-I1/frontend/src/main.tsx:1), [frontend/src/App.tsx](/Users/manta/Documents/Projects/TheRoad-I1/frontend/src/App.tsx:1).

이건 런타임 버그는 아니지만, 배포 전략, SSR 기대치, 라우팅 방식, asset serving 문서를 모두 잘못 이끌 수 있다. 운영 문서와 온보딩 문서를 계속 v10 기반으로 확장할 계획이면 먼저 이 차이를 정리해야 한다.

## 우선순위 제안

1. `_sync_checkpoints_to_db()`의 project-wide delete를 즉시 중단하고, `entity_canon` / `character_outlook`를 UPSERT 중심으로 재설계할 것.
2. `scene_consistency`의 fallback indentation bug를 먼저 수정해 false partial 상태를 없앨 것.
3. `partial` 게이트 semantics를 runner, API, 프런트에서 동일하게 맞출 것.
4. checkpoint 저장 실패를 step failure로 승격하고, `shot_dependency_t2i`의 직접 write를 공통 저장 경로로 옮길 것.
5. `step_manifest`와 `llm_client.PIPELINE_STEPS`를 단일 소스로 통합할 것.
