Spaces:
Sleeping
Sleeping
File size: 9,229 Bytes
7d46a91 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 | # ์ธํฐ์คํฐ์
ํ์คํ ๋ฆฌ + ์ธ์
์์ํ โ ๊ตฌํ ๊ณํ
> ์์ฑ์ผ: 2026-07-06 ยท ์ํ: **๊ตฌํ ์๋ฃ** (2026-07-06, feature/persistence)
> ๊ฒ์ฆ: pytest 26๊ฑด ํต๊ณผ(์ ๊ท 8๊ฑด ํฌํจ) + ์๋ฒ ์ฌ์์ e2e (E01-11 ์งํ โ kill/์ฌ๊ธฐ๋ โ
> GET ๋ณต์ ๋์ผ ์นด๋ โ advance ๊ณ์ ์งํ ยท ๋ฏธ์กด์ฌ ์ธ์
404)
> ํ์ ์์ (2026-07-06, ์ ์ ์คํ๋ ์ด ํผ๋๋ฐฑ 2ํ): ์ค๋ฒ๋ ์ด ๋ฐฉ์ ์์ฒด๋ฅผ ํ๊ธฐ โ
> ์ฐ์ถ ์ปท์ **์์ฒด ๋ฒํธ๋ฅผ ๊ฐ์ง ๋
๋ฆฝ ํ์ด์ง**๋ก (์: 6 ๋ณธ๋ฌธ โ 7 ์ปท โ 8 ๋ณธ๋ฌธ, total 66 = 61+5).
> ๋ฒํธ๋ ์๋ฒ(router `_page_index`)๊ฐ ๊ถ์, ์ปท ํ์ด์ง ๋ฒํธ = progress.index - 1.
> ํ๋ก ํธ์ ์ค๋ฒ๋ ์ด/pendingPage/ํด๋ฆญ๊ฐ๋ ๋ก์ง ์ ๋ถ ์ ๊ฑฐ โ ์ผ๋ฐ ํ์ด์ง ํ๋ฆ์ผ๋ก ๋จ์ํ.
> Node DOM ์คํ
์๋ฎฌ๋ ์ด์
์ผ๋ก ์ /์ญ๋ฐฉํฅยท์ขํด๋ฆญ ํต๊ณผ ๊ฒ์ฆ
> ์ ํ ๊ฒฐ์ (์ ์ ํ์ ):
> - ํฌ์ผ(์ฑํ
) ์ค๊ฐ ์ํ๋ ์์ํํ์ง ์๋๋ค โ ์๋ฒ ์ฌ์์/๋ณต์ ์ ํฌ์ผ์ ๋ฒ๋ฆฌ๊ณ ์ฝ๊ธฐ ํ๋ฉด์ผ๋ก
> - ๋ณต์ ๋ฒ์๋ "ํ์ฌ ํ์ด์ง๋ถํฐ ์ด์ด ์ฝ๊ธฐ"๋ง โ ๋ค๋ก๊ฐ๊ธฐ ํ์คํ ๋ฆฌ๋ ์๋ก๊ณ ์นจ ์ ํฌ๊ธฐ
> ๊ถ์ฅ์ ์ฑํ: SQLite + ์ค๋
์ท + ๋งค ๋ณ๊ฒฝ ์ฆ์ ์ฐ๊ธฐ(write-through) + localStorage ์ธ์
ID + ์ฝํ
์ธ ๋ฒ์ ์คํฌํ๋ก ๋ฌดํจํ
## ๋ฌธ์ ์ ์
1. **์ธํฐ์คํฐ์
๋ค๋ก๊ฐ๊ธฐ ๋ฒ๊ทธ**: ์ฐ์ถ ์ปท์ด ํ์คํ ๋ฆฌ์ ํ์ด์ง๊ฐ ์๋๋ผ ์ผํ์ฑ ์ค๋ฒ๋ ์ด๋ผ์,
๋ค๋ก ๋๊ธฐ๋ฉด ์ด๋ฏธ์ง๊ฐ ๊ฑด๋๋ฐ์ด์ง๊ณ ์ด์ ์คํฌ๋ฆฝํธ๊ฐ ๋์จ๋ค. ์๋๋ ์ ๋ฐฉํฅ๊ณผ ๋์ผํ๊ฒ
"์คํฌ๋ฆฝํธ โ ์ด๋ฏธ์ง ํ์ด์ง โ ์คํฌ๋ฆฝํธ"๊ฐ ์ญ๋ฐฉํฅ์์๋ ์ ์ง๋๋ ๊ฒ.
2. **์์ํ ๋ถ์ฌ**: ํ๋ ์ด ์ธ์
์ด 3์ธต ๋ชจ๋ ํ๋ฐ์ฑ โ
`router._sessions`(ํ์ด์ฌ dict) ยท ADK `InMemorySessionService` ยท ๋ธ๋ผ์ฐ์ `SID`(JS ๋ณ์).
์๋ก๊ณ ์นจ์ด๋ฉด ์ธ์
ID๋ฅผ ์๊ณ , ์๋ฒ ์ฌ์์์ด๋ฉด ์ ์ ์ ์งํ์ด ์ฌ๋ผ์ง๋ค.
CLAUDE.md ๊ถ์ ์์น("ํ์ฌ ์ ์ ์ ์์ฌ ์์น ๊ถ์: DB `play_session.current_state_id`")์ด ์์ ํ DB๊ฐ ์์ง ์๋ค.
## ์ค๊ณ ํต์ฌ
### ๋ณต์์ ํจ์ โ `_enter()` ๋ถ์ํจ๊ณผ
`StoryEngine._enter()`๋ ์ง์
์ ์ ์ฝ๋ ์ฆ๊ฐยท์ถ ์ ์ฉยท์ ํ๋ฆฝ์ ์ํํ๋ค.
๋ณต์์ `_enter(current_id)` ์ฌํธ์ถ๋ก ๊ตฌํํ๋ฉด **์ํ๊ฐ ์ด์ค ์ ์ฉ**๋๋ค.
๋ฐ๋ผ์ ๋ณต์์ ๋ถ์ํจ๊ณผ ์๋ ์ ์ฉ ๊ฒฝ๋ก๋ก: ํ๋๋ฅผ ์ง์ ๋ณต์ํ๊ณ ,
ํ์ฌ ์นด๋์ StepResult๋ ๋ ๋ ์ ์ฉ ๋ฉ์๋๋ก ์ฌ๊ตฌ์ฑํ๋ค (`_visible_narration` + `card.choices`๋ ์์ ์กฐํ).
### ์ค๋
์ท ํ์ด๋ก๋ (JSON ์ง๋ ฌํ โ ์ ํ๋ int/str/bool/list)
```json
{
"story": {"current_id": "E04-07", "visited": [...], "preset_branch": null, "ending_id": null},
"hidden": {"trust_score": 24, "doubt_score": 12, "frailty": 3,
"awareness_established": true, "axis_log": [...]}
}
```
- `ending_id`: ์๋ฉ ๋๋ฌ ์ธ์
๋ ๋ณต์ ๋์ (์๋ก๊ณ ์นจ ์ ์๋ฉ ํ๋ฉด ์ ์ง). ๋ณต์ ์ `chapter.ending_rules`์์ id๋ก ์ญ์กฐํ
- ํฌ์ผ ์งํ ์ค ์ ์ฅ๋ถ์ ์๋ค โ ํฌ์ผ ์์ ์ฆ๋ถ์ close ์ปค๋ฐ ํ์๋ง ์์ ์ํ์ ๋ฐ์ (๊ธฐ์กด ๋จ์ผ ๊ฒฝ๋ก ์ ์ง).
ํฌ์ผ ๋์ค ์๋ฒ๊ฐ ์ฃฝ์ผ๋ฉด ๊ทธ ๋ํ์ ์ ๋ขฐ์ถ ์ฆ๋ถ์ ์ ์ค๋๋ค (์น์ธ๋ ํธ๋ ์ด๋์คํ)
- ๊ธฐ์กด `HiddenState.to_session()`์ ํฌ์ผ ์ ์ฉ(๋ถ๋ถยทํค ์์ด)์ด๋ฏ๋ก ์ฌ์ฌ์ฉํ์ง ์๊ณ ์ค๋
์ท ๋ฉ์๋๋ฅผ ๋ณ๋ ์ถ๊ฐ
### ์ฝํ
์ธ ๋ฒ์ ์คํฌํ
- `ContentRepository`์ `content_version` ํ๋กํผํฐ: `chapter.id` + ์ ์ญ ์์์ ์นด๋ ID ๋ชฉ๋ก์ sha256 ํด์
- ์ ์ฅ ์ ์คํฌํ, ๋ณต์ ์ ๋ถ์ผ์น๋ฉด **๋ฌดํจํ**(404 โ ํ๋ก ํธ๊ฐ ์ ์ธ์
์์).
์นด๋ ๋ณธ๋ฌธ๋ง ๋ฐ๋ ๊ฒฝ์ฐ๋ ์ก์ง ๋ชปํ์ง๋ง(ID/์์ ๋์ผ) ๋ณต์์ด ๊นจ์ง์ง๋ ์์ผ๋ฏ๋ก ๋ฐ๋ชจ ์์ฉ
### ์ ์ฅ ์์ (write-through)
์์ ์ํ๊ฐ ๋ฐ๋๋ ๋ชจ๋ ์ง์ ์์ ์ฆ์ ์ ์ฅ โ `POST /session`(์์ฑ) ยท `advance` ยท `choose` ยท
`pocket/turn`(์๋ ด ์ปค๋ฐ ์) ยท `pocket/close`. `pocket/open`์ ์์ ์ํ ๋ฌด๋ณ๊ฒฝ์ด๋ผ ์ ์ฅ ์์.
### ๋์์ฑยท์๋ช
- uvicorn ์์ปค 1๊ฐ ์ ์ ์ ์ง (์ธ๋ฉ๋ชจ๋ฆฌ ์บ์ + ADK InMemory ์ธ์
๋๋ฌธ). SQLite๋ WAL ๋ชจ๋
- ๊ฐ์ ์ธ์
๋์ ์ ๊ทผ์ ๋ง์ง๋ง ์ฐ๊ธฐ ์น๋ฆฌ (๋ฐ๋ชจ ์์ฉ)
- ์๋ฒ ๊ธฐ๋ ์ 30์ผ ๋ฏธ๊ฐฑ์ ์ธ์
purge
## ๋ณ๊ฒฝ ํ์ผ ๋ชฉ๋ก
### A. ์ธํฐ์คํฐ์
ํ์คํ ๋ฆฌ (๋ฒ๊ทธ ์์ โ ํ๋ก ํธ ๋จ๋
)
| ํ์ผ | ๋ณ๊ฒฝ |
|---|---|
| `web/index.html` | ํ์คํ ๋ฆฌ ํญ๋ชฉ์ 2์ข
์ผ๋ก ํ์
ํ: `{kind:'page', html, progress, ep}` ยท `{kind:'inter', image, caption, progress, ep}`. โ `showStep()`: `step.image` ์์ผ๋ฉด inter ํญ๋ชฉ์ ๋จผ์ pushํ๊ณ ์ค๋ฒ๋ ์ด ํ์, ์ค๋ฒ๋ ์ด ํด๋ฆญ ์ page ํญ๋ชฉ push + ๋ ๋ โก `go()`: ์ปค์ ์ด๋์ ๊ณตํต `renderEntry(cursor)`๋ก ํต์ผ โ inter ํญ๋ชฉ์ด๋ฉด ์ค๋ฒ๋ ์ด ํ์, page ํญ๋ชฉ์ด๋ฉด HTML ๋ณต์ + ์ค๋ฒ๋ ์ด ๋ซ๊ธฐ โข ๋ธ๋ผ์ฐ์ง ์ค ์ค๋ฒ๋ ์ด ํด๋ฆญ = `go(+1)` (์ค๋ฒ๋ ์ด๊ฐ ๋ด๋น ์กด์ ๋ฎ์ผ๋ฏ๋ก; โ ํค๋ ๊ณ์ ๋์) โฃ `choose()`์ ์ค๋
์ท ๊ฐฑ์ (`history[cursor].html`)์ page ํญ๋ชฉ์๋ง ๋ฟ์ผ๋ฏ๋ก ๊ทธ๋๋ก |
### B. ์์ํ
| ํ์ผ | ๋ณ๊ฒฝ |
|---|---|
| `engine/services/state.py` | `HiddenState.snapshot() -> dict` / `HiddenState.from_snapshot(d)` ์ถ๊ฐ (์ ํ๋. `to_session()`๊ณผ ๋ณ๊ฐ โ ํฌ์ผ ๊ฒฝ๋ก ๋ถ๋ณ) |
| `engine/services/story.py` | โ `snapshot() -> dict` (story ํํธ) โก `StoryEngine.restore(repo, snap) -> StoryEngine` ํด๋์ค๋ฉ์๋ โ **๋ถ์ํจ๊ณผ ์์ด** ํ๋ ์ง์ ๋ณต์ โข `current_step() -> StepResult` โ ํ์ฌ ์นด๋/์๋ฉ์ StepResult๋ฅผ ๋ ๋ ์ ์ฉ์ผ๋ก ์ฌ๊ตฌ์ฑ |
| `engine/repositories/content.py` | `content_version` ํ๋กํผํฐ (sha256 ์คํฌํ) |
| `engine/repositories/play_session.py` | **์ ๊ท.** stdlib `sqlite3`, WAL. ํ
์ด๋ธ `play_session(sid TEXT PK, payload TEXT, content_version TEXT, updated_at TEXT)`. ๋ฉ์๋: `save(sid, payload, version)` / `load(sid, version) -> dict | None`(๋ฒ์ ๋ถ์ผ์น ์ None) / `purge(days=30)` |
| `engine/router.py` | โ `PlaySessionRepository(ROOT/"data"/"sessions.db")` ์์ฑ + ๊ธฐ๋ ์ `purge()` โก `_sessions` dict๋ ์บ์๋ก ์ ์ง, `_get()` ๋ฏธ์ค ์ ์ ์ฅ์์์ ๋ก๋ โ `StoryEngine.restore` + `current_step()`์ผ๋ก PlaySession ์ฌ๊ตฌ์ฑ (`pocket_sid=None` โ ํฌ์ผ ํ๊ธฐ) โข ์ ์ฅ ํฌํผ `_save(sid, ps)`๋ฅผ ์์ฑ/advance/choose/pocket ์๋ ด/close์ write-through |
| `web/index.html` | โ ๊ธฐ๋ ์ `localStorage.carmilla_sid` ์์ผ๋ฉด `GET /api/session/{sid}` ์๋ โ ์ฑ๊ณต ์ ํ์ง ๋ฒํผ์ด "์ด์ด ์ฝ๊ธฐ"๋ก ๋์(์ ์ธ์
์์ฑ ์๋ต, ํ์ฌ ํ์ด์ง๋ถํฐ), 404๋ฉด sid ํ๊ธฐ ํ ๊ธฐ์กด ํ๋ฆ โก ์ธ์
์์ฑ ์ sid๋ฅผ localStorage์ ์ ์ฅ โข ์๋ฉ "๋ค์, ์ฒ์์ ๋ฐค์ผ๋ก" ํด๋ฆญ ์ sid ์ ๊ฑฐ ํ reload |
| `.gitignore` | `data/` ์ถ๊ฐ |
| `tests/test_persistence.py` | **์ ๊ท.** โ ์ค๋
์ทโ๋ณต์ ๋ผ์ด๋ํธ๋ฆฝ: ๋ช ์นด๋ ์งํ ํ ๋ณต์ํ์ ๋ `current_id`/์จ์ ์ํ/์งํ๋ ๋์ผ โก **๋ณต์์ด ์ ์ฝ๋ยท์ถ์ ์ฌ์ ์ฉํ์ง ์์** (ํต์ฌ ํ๊ท) โข ์๋ฉ ์ธ์
๋ณต์ โฃ ์ ์ฅ์ save/load + ๋ฒ์ ๋ถ์ผ์น ์ None โค ๋ผ์ฐํฐ ๋ ๋ฒจ: `_sessions` ์บ์ ๋น์ด ๋ค(์ฌ์์ ๋ชจ์ฌ) GET์ผ๋ก ๋ณต์๋๊ณ ํฌ์ผ์ ๋ซํ ์์ |
| `docs/contract/demo_api.md` | `GET /api/session/{sid}`๊ฐ ๋ณต์ ์ง์
์ ์์ ๋ช
์ (404 = ๋ฏธ์กด์ฌ ๋๋ ์ฝํ
์ธ ๋ฒ์ ๋ถ์ผ์น โ ์ ์ธ์
์์ฑํ ๊ฒ). ํฌ์ผ ๋น์์ ๋ช
์ |
| `docs/plans/persistence.md` | ๋ณธ ๋ฌธ์ |
### ๋ณ๊ฒฝํ์ง ์๋ ๊ฒ
- `engine/app.py` โ ADK `InMemorySessionService` ์ ์ง (ํฌ์ผ ๋น์์ ๊ฒฐ์ ์ ๋ฐ๋ผ `SqliteSessionService` ๋ถํ์)
- `engine/services/pocket.py` โ ํฌ์ผ ์ปค๋ฐ ๋จ์ผ ๊ฒฝ๋ก ๋ถ๋ณ
- `content/` โ ๋ฌด๋ณ๊ฒฝ (์ฝํ
์ธ ์ปค๋ฐ ๊ธ์ง ๊ท์น)
## ์์
์์
1. **P1 โ ์ธํฐ์คํฐ์
ํ์คํ ๋ฆฌ** (`web/index.html` ๋จ๋
, ์๋ฒ ๋ฌด๊ด) โ ๋ธ๋ผ์ฐ์ ์์ ์ /์ญ๋ฐฉํฅ ์๋ ํ์ธ
2. **P2 โ ์ค๋
์ท/๋ณต์ ์์ง** (`state.py`, `story.py`) + ๋จ์ ํ
์คํธ (๋ถ์ํจ๊ณผ ๋ฏธ์ฌ์ ์ฉ ํ๊ท ํฌํจ)
3. **P3 โ SQLite ์ ์ฅ์ + ๋ผ์ฐํฐ write-through/๋ณต์** (`play_session.py`, `content.py`, `router.py`) + ๋ผ์ฐํฐ ํ
์คํธ
4. **P4 โ ํ๋ก ํธ ์ด์ด ์ฝ๊ธฐ** (`web/index.html` localStorage) โ ์๋ก๊ณ ์นจยท์๋ฒ ์ฌ์์ ํ ์ด์ด ์ฝ๊ธฐ ์๋ ํ์ธ
5. **P5 โ ๋ฌธ์ ๊ฐฑ์ ** (`demo_api.md`, ARCHITECTURE.md ์นํธ์ํธ 1์ค)
๊ฒ์ฆ: `pytest tests/ -q` ์ ๊ฑด ํต๊ณผ + ์๋ ์๋๋ฆฌ์ค 3์ข
(โ ์ด๋ฏธ์ง ํ์ด์ง ๋ค๋ก๊ฐ๊ธฐ/์์ผ๋ก๊ฐ๊ธฐ โก ์๋ก๊ณ ์นจ ํ ์ด์ด ์ฝ๊ธฐ โข uvicorn ์ฌ์์ ํ ์ด์ด ์ฝ๊ธฐ โ ํฌ์ผ ์ค์ด์๋ค๋ฉด ์ฝ๊ธฐ ํ๋ฉด์ผ๋ก).
## ๋ฆฌ์คํฌ / ํ๊ณ (์น์ธ๋ ํธ๋ ์ด๋์คํ ํฌํจ)
- ํฌ์ผ ๋์ค ์๋ฒ ์ฌ๋ง ์ ๊ทธ ๋ํ์ ์ ๋ขฐ์ถ ์ฆ๋ถ ์ ์ค (ํฌ์ผ ๋น์์ ๊ฒฐ์ ์ ๊ท๊ฒฐ)
- ์๋ก๊ณ ์นจ ์ ๋ค๋ก๊ฐ๊ธฐ ํ์คํ ๋ฆฌยท์ ํ ๊ฒฐ๊ณผ ์ฐ๋ฌธ์ ์์ค, ํ์ฌ ํ์ด์ง๋ถํฐ (๊ฒฐ์ โค)
- ์นด๋ ๋ณธ๋ฌธ๋ง ๋ฐ๋ ์ฝํ
์ธ ๊ฐฑ์ ์ ๋ฒ์ ์คํฌํ๊ฐ ๊ฐ์ง ๋ชปํจ (ID/์์ ๋์ผ ์) โ ๋ณต์์ ์ ๊นจ์ง
- ๋ฉํฐ ์์ปค/๋ฉํฐ ํ๋ก์ธ์ค๋ ์ฌ์ ํ ๋น์ง์ (ADK ์ธ์
ยท์บ์๊ฐ ์ธ๋ฉ๋ชจ๋ฆฌ)
|