Spaces:
Sleeping
Sleeping
| # ์ธํฐ์คํฐ์ ํ์คํ ๋ฆฌ + ์ธ์ ์์ํ โ ๊ตฌํ ๊ณํ | |
| > ์์ฑ์ผ: 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 ์ธ์ ยท์บ์๊ฐ ์ธ๋ฉ๋ชจ๋ฆฌ) | |