Spaces:
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 + ์ฝํ ์ธ ๋ฒ์ ์คํฌํ๋ก ๋ฌดํจํ
๋ฌธ์ ์ ์
- ์ธํฐ์คํฐ์ ๋ค๋ก๊ฐ๊ธฐ ๋ฒ๊ทธ: ์ฐ์ถ ์ปท์ด ํ์คํ ๋ฆฌ์ ํ์ด์ง๊ฐ ์๋๋ผ ์ผํ์ฑ ์ค๋ฒ๋ ์ด๋ผ์, ๋ค๋ก ๋๊ธฐ๋ฉด ์ด๋ฏธ์ง๊ฐ ๊ฑด๋๋ฐ์ด์ง๊ณ ์ด์ ์คํฌ๋ฆฝํธ๊ฐ ๋์จ๋ค. ์๋๋ ์ ๋ฐฉํฅ๊ณผ ๋์ผํ๊ฒ "์คํฌ๋ฆฝํธ โ ์ด๋ฏธ์ง ํ์ด์ง โ ์คํฌ๋ฆฝํธ"๊ฐ ์ญ๋ฐฉํฅ์์๋ ์ ์ง๋๋ ๊ฒ.
- ์์ํ ๋ถ์ฌ: ํ๋ ์ด ์ธ์
์ด 3์ธต ๋ชจ๋ ํ๋ฐ์ฑ โ
router._sessions(ํ์ด์ฌ dict) ยท ADKInMemorySessionServiceยท ๋ธ๋ผ์ฐ์ SID(JS ๋ณ์). ์๋ก๊ณ ์นจ์ด๋ฉด ์ธ์ ID๋ฅผ ์๊ณ , ์๋ฒ ์ฌ์์์ด๋ฉด ์ ์ ์ ์งํ์ด ์ฌ๋ผ์ง๋ค. CLAUDE.md ๊ถ์ ์์น("ํ์ฌ ์ ์ ์ ์์ฌ ์์น ๊ถ์: DBplay_session.current_state_id")์ด ์์ ํ DB๊ฐ ์์ง ์๋ค.
์ค๊ณ ํต์ฌ
๋ณต์์ ํจ์ โ _enter() ๋ถ์ํจ๊ณผ
StoryEngine._enter()๋ ์ง์
์ ์ ์ฝ๋ ์ฆ๊ฐยท์ถ ์ ์ฉยท์ ํ๋ฆฝ์ ์ํํ๋ค.
๋ณต์์ _enter(current_id) ์ฌํธ์ถ๋ก ๊ตฌํํ๋ฉด ์ํ๊ฐ ์ด์ค ์ ์ฉ๋๋ค.
๋ฐ๋ผ์ ๋ณต์์ ๋ถ์ํจ๊ณผ ์๋ ์ ์ฉ ๊ฒฝ๋ก๋ก: ํ๋๋ฅผ ์ง์ ๋ณต์ํ๊ณ ,
ํ์ฌ ์นด๋์ StepResult๋ ๋ ๋ ์ ์ฉ ๋ฉ์๋๋ก ์ฌ๊ตฌ์ฑํ๋ค (_visible_narration + card.choices๋ ์์ ์กฐํ).
์ค๋ ์ท ํ์ด๋ก๋ (JSON ์ง๋ ฌํ โ ์ ํ๋ int/str/bool/list)
{
"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 |
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โ ADKInMemorySessionService์ ์ง (ํฌ์ผ ๋น์์ ๊ฒฐ์ ์ ๋ฐ๋ผSqliteSessionService๋ถํ์)engine/services/pocket.pyโ ํฌ์ผ ์ปค๋ฐ ๋จ์ผ ๊ฒฝ๋ก ๋ถ๋ณcontent/โ ๋ฌด๋ณ๊ฒฝ (์ฝํ ์ธ ์ปค๋ฐ ๊ธ์ง ๊ท์น)
์์ ์์
- P1 โ ์ธํฐ์คํฐ์
ํ์คํ ๋ฆฌ (
web/index.html๋จ๋ , ์๋ฒ ๋ฌด๊ด) โ ๋ธ๋ผ์ฐ์ ์์ ์ /์ญ๋ฐฉํฅ ์๋ ํ์ธ - P2 โ ์ค๋
์ท/๋ณต์ ์์ง (
state.py,story.py) + ๋จ์ ํ ์คํธ (๋ถ์ํจ๊ณผ ๋ฏธ์ฌ์ ์ฉ ํ๊ท ํฌํจ) - P3 โ SQLite ์ ์ฅ์ + ๋ผ์ฐํฐ write-through/๋ณต์ (
play_session.py,content.py,router.py) + ๋ผ์ฐํฐ ํ ์คํธ - P4 โ ํ๋ก ํธ ์ด์ด ์ฝ๊ธฐ (
web/index.htmllocalStorage) โ ์๋ก๊ณ ์นจยท์๋ฒ ์ฌ์์ ํ ์ด์ด ์ฝ๊ธฐ ์๋ ํ์ธ - P5 โ ๋ฌธ์ ๊ฐฑ์ (
demo_api.md, ARCHITECTURE.md ์นํธ์ํธ 1์ค)
๊ฒ์ฆ: pytest tests/ -q ์ ๊ฑด ํต๊ณผ + ์๋ ์๋๋ฆฌ์ค 3์ข
(โ ์ด๋ฏธ์ง ํ์ด์ง ๋ค๋ก๊ฐ๊ธฐ/์์ผ๋ก๊ฐ๊ธฐ โก ์๋ก๊ณ ์นจ ํ ์ด์ด ์ฝ๊ธฐ โข uvicorn ์ฌ์์ ํ ์ด์ด ์ฝ๊ธฐ โ ํฌ์ผ ์ค์ด์๋ค๋ฉด ์ฝ๊ธฐ ํ๋ฉด์ผ๋ก).
๋ฆฌ์คํฌ / ํ๊ณ (์น์ธ๋ ํธ๋ ์ด๋์คํ ํฌํจ)
- ํฌ์ผ ๋์ค ์๋ฒ ์ฌ๋ง ์ ๊ทธ ๋ํ์ ์ ๋ขฐ์ถ ์ฆ๋ถ ์ ์ค (ํฌ์ผ ๋น์์ ๊ฒฐ์ ์ ๊ท๊ฒฐ)
- ์๋ก๊ณ ์นจ ์ ๋ค๋ก๊ฐ๊ธฐ ํ์คํ ๋ฆฌยท์ ํ ๊ฒฐ๊ณผ ์ฐ๋ฌธ์ ์์ค, ํ์ฌ ํ์ด์ง๋ถํฐ (๊ฒฐ์ โค)
- ์นด๋ ๋ณธ๋ฌธ๋ง ๋ฐ๋ ์ฝํ ์ธ ๊ฐฑ์ ์ ๋ฒ์ ์คํฌํ๊ฐ ๊ฐ์ง ๋ชปํจ (ID/์์ ๋์ผ ์) โ ๋ณต์์ ์ ๊นจ์ง
- ๋ฉํฐ ์์ปค/๋ฉํฐ ํ๋ก์ธ์ค๋ ์ฌ์ ํ ๋น์ง์ (ADK ์ธ์ ยท์บ์๊ฐ ์ธ๋ฉ๋ชจ๋ฆฌ)