# Phase 2 설계 스펙: 에피소드 관리 + 엔티티 분석 + LLM 모듈

**작성일**: 2026-03-15
**상태**: 승인됨
**범위**: Phase 2a/2b/2c 통합

---

## 1. 개요

Phase 2는 프로젝트에 시나리오 에피소드를 추가하고, LLM으로 엔티티(인물/배경/소품)와 씬 스틸을 추출하며, 에피소드 간 공유 엔티티 구조를 구현한다.

## 2. Phase 2a: 에피소드 관리 + 시나리오 업로드

### 2.1 프로젝트 DB 스키마 확장

```sql
-- projects/{id}/project.sqlite에 추가

episode (
    id            TEXT PRIMARY KEY,
    episode_number INTEGER NOT NULL,
    title         TEXT NOT NULL,
    source_filename TEXT NOT NULL,
    source_path   TEXT NOT NULL,       -- assets/screenplays/{filename}
    fulltext      TEXT,                -- PDF에서 추출한 전체 텍스트
    language      TEXT DEFAULT 'ko',   -- 'ko', 'en', 'ja'
    page_count    INTEGER,
    status        TEXT DEFAULT 'uploaded' CHECK (status IN ('uploaded', 'analyzing', 'analyzed', 'error')),
    analysis_error TEXT,
    created_at    TEXT NOT NULL,
    updated_at    TEXT NOT NULL
)
```

### 2.2 API

| 메서드 | 경로 | 설명 |
|--------|------|------|
| GET | `/api/v1/projects/{id}/episodes` | 에피소드 목록 |
| POST | `/api/v1/projects/{id}/episodes` | 에피소드 추가 (PDF 업로드) |
| GET | `/api/v1/projects/{id}/episodes/{ep_id}` | 에피소드 상세 |
| PATCH | `/api/v1/projects/{id}/episodes/{ep_id}` | 에피소드 수정 (제목, 순서) |
| DELETE | `/api/v1/projects/{id}/episodes/{ep_id}` | 에피소드 삭제 |
| POST | `/api/v1/projects/{id}/episodes/{ep_id}/analyze` | 분석 시작 (엔티티 + 씬 스틸) |

### 2.3 PDF 텍스트 추출

`backend/app/modules/pdf_parser.py` — pypdf 기반. PDF → 전문 텍스트 + 페이지 수 + 언어 감지. 독립 모듈.

### 2.4 프론트엔드

- 에피소드 목록 탭 (프로젝트 상세 페이지에 추가)
- PDF 업로드 모달 (에피소드 번호, 제목 입력)
- 에피소드 카드: 상태 뱃지 (uploaded/analyzing/analyzed/error)
- "분석 시작" 버튼

## 3. Phase 2b: LLM 분석 모듈 + 엔티티/씬 스틸 구조

### 3.1 프로젝트 DB 스키마 — 엔티티

```sql
entity_canon (
    id              TEXT PRIMARY KEY,
    entity_type     TEXT NOT NULL CHECK (entity_type IN ('character', 'location', 'prop')),
    name            TEXT NOT NULL,
    description     TEXT,
    stable_traits   TEXT DEFAULT '{}',  -- JSON
    status          TEXT DEFAULT 'active',
    created_at      TEXT NOT NULL,
    updated_at      TEXT NOT NULL
)

entity_alias (
    id              TEXT PRIMARY KEY,
    canon_id        TEXT NOT NULL REFERENCES entity_canon(id),
    alias           TEXT NOT NULL,
    UNIQUE(canon_id, alias)
)

relation_fact (
    id                  TEXT PRIMARY KEY,
    relation_family     TEXT NOT NULL,
    relation_type       TEXT NOT NULL,
    directionality      TEXT NOT NULL,
    temporal_scope      TEXT NOT NULL,
    continuity_priority TEXT NOT NULL,
    continuity_reason   TEXT,
    created_at          TEXT NOT NULL
)

relation_participant (
    id                TEXT PRIMARY KEY,
    relation_id       TEXT NOT NULL REFERENCES relation_fact(id),
    canon_id          TEXT NOT NULL REFERENCES entity_canon(id),
    participant_role  TEXT NOT NULL,
    participant_order INTEGER DEFAULT 1
)

scene_still (
    id                      TEXT PRIMARY KEY,
    episode_id              TEXT NOT NULL REFERENCES episode(id),
    still_index             INTEGER NOT NULL,
    screenplay_scene_heading TEXT,
    beat_title              TEXT,
    still_frame_prompt      TEXT,
    camera_json             TEXT DEFAULT '{}',
    lighting_json           TEXT DEFAULT '{}',
    visible_entities_json   TEXT DEFAULT '[]',
    status                  TEXT DEFAULT 'pending',
    created_at              TEXT NOT NULL
)

-- 엔티티-에피소드 연결 (어떤 에피소드에서 추출되었는지)
entity_episode_link (
    id              TEXT PRIMARY KEY,
    canon_id        TEXT NOT NULL REFERENCES entity_canon(id),
    episode_id      TEXT NOT NULL REFERENCES episode(id),
    source          TEXT DEFAULT 'extracted',
    UNIQUE(canon_id, episode_id)
)
```

### 3.2 LLM 분석 모듈 구조

```
backend/app/modules/
├── __init__.py
├── pdf_parser.py           # PDF → 텍스트 추출
├── llm/
│   ├── __init__.py
│   ├── base.py             # BaseLLMClient 인터페이스
│   ├── openai_client.py    # OpenAI API 호출 (GPT-5.4)
│   └── gemini_client.py    # Gemini API 호출 (향후)
├── entity_extractor.py     # 엔티티 추출 모듈
└── scene_still_extractor.py # 씬 스틸 추출 모듈
```

**핵심 원칙:**
- 각 모듈은 독립적으로 교체 가능
- LLM 호출은 `BaseLLMClient` 인터페이스 뒤에 숨김
- 프롬프트는 `prompts/_base/` 외부 파일에서 로드
- 분석 결과는 프로젝트 DB에 저장
- 기존 Old/screenplay/extract_entities.py의 스키마/로직 참고하되, 모듈형으로 재구성

### 3.3 엔티티 추출 흐름

1. 에피소드 fulltext + 이전 에피소드 메모리 (있으면) 로드
2. 프롬프트 로드 (`prompts/_base/entity_extraction/`)
3. LLM 호출 → 구조화된 JSON 응답
4. 응답 파싱 → entity_canon, entity_alias, relation_fact, relation_participant에 저장
5. 기존 canon과 이름/별칭 매칭 → 중복이면 연결, 신규면 생성
6. entity_episode_link에 연결 기록

### 3.4 씬 스틸 추출 흐름

1. 에피소드 fulltext + 씬 헤딩 카탈로그 로드
2. 프롬프트 로드 (`prompts/_base/scene_stills/`)
3. LLM 호출 → 씬 스틸 후보 JSON
4. 파싱 → scene_still에 저장
5. visible_entities를 entity_canon과 매칭

### 3.5 API

| 메서드 | 경로 | 설명 |
|--------|------|------|
| GET | `/api/v1/projects/{id}/entities` | 엔티티 목록 (필터: type) |
| GET | `/api/v1/projects/{id}/entities/{eid}` | 엔티티 상세 |
| PATCH | `/api/v1/projects/{id}/entities/{eid}` | 엔티티 수정 |
| GET | `/api/v1/projects/{id}/entities/{eid}/relations` | 엔티티 관계 |
| GET | `/api/v1/projects/{id}/episodes/{ep_id}/stills` | 에피소드 씬 스틸 목록 |
| GET | `/api/v1/projects/{id}/stills/{still_id}` | 씬 스틸 상세 |

### 3.6 분석 실행 서비스

`backend/app/services/analysis_service.py`:
- `run_analysis(project_id, episode_id)` → 비동기로 엔티티 추출 + 씬 스틸 추출
- 에피소드 status를 `analyzing` → `analyzed` 또는 `error`로 업데이트
- 스레드 풀에서 실행 (Phase 1의 파이프라인 실행 패턴 참고)
- 모든 단계에서 ActivityLogger 호출

## 4. Phase 2c: 프론트엔드 — 분석 결과 UI

### 4.1 에피소드 상세 페이지

- 에피소드 정보 + 상태
- 분석 시작 버튼 (analyzing 중에는 비활성)
- 분석 완료 시: 엔티티 탭 + 씬 스틸 탭

### 4.2 엔티티 관리 탭 (프로젝트 레벨)

- 전체 엔티티 카드 그리드 (character/location/prop 필터)
- 각 카드: 이름, 타입 뱃지, 설명, 연결된 에피소드 수
- 엔티티 상세 모달: 이름/설명 편집, 별칭 목록, 관계 목록
- 에피소드별 필터

### 4.3 씬 스틸 목록

- 에피소드별 씬 스틸 카드 목록
- 각 카드: 씬 헤딩, beat_title, 프롬프트 미리보기, visible entities 뱃지

## 5. 환경 변수

```
OPENAI_API_KEY=xxx           # 엔티티/씬 스틸 추출용
OPENAI_MODEL=gpt-5.4         # 기본 모델
```

## 6. 기술 스택 추가

| 항목 | 기술 |
|------|------|
| PDF 파싱 | pypdf (이미 requirements.txt에 있음) |
| LLM | OpenAI API (urllib 직접 호출, 기존 패턴 유지) |
| 비동기 분석 | threading (Phase 1 패턴 유지) |

## 7. 버전 관리

| 모듈 | 버전 | 프롬프트 의존성 |
|------|------|----------------|
| pdf_parser | 1.0.0 | 없음 |
| entity_extractor | 1.0.0 | entity_extraction/v5 |
| scene_still_extractor | 1.0.0 | scene_stills/v2 |
| analysis_service | 1.0.0 | 없음 |
