ERD — 데이터 모델
9/04(Day 2) 18:00 ERD FREEZE 이후 변경은 마이그레이션 + 클라이언트 대응이 세트로 따라옵니다.
이 ERD의 성격이 다릅니다
일반 서비스라면 데이터는 기능을 뒷받침합니다. 여기서는 판정 로그 자체가 산출물입니다.
- 11.1 시스템 검증 12종이 전부 이 테이블들에서 나온다
- 11.2 실사용 데이터, 특히 침묵 권고 채택률이 여기서만 측정된다
- 8.8 인스펙터는 이 로그를 그대로 열어 보여주는 화면이다
기획서 11.2: 구현 비용은 로그 테이블 하나 수준이며, 초기 설계에 반드시 포함한다.
계정 테이블이 없습니다
로그인을 만들지 않습니다 (09-03 설계) 12.4가 로그인·설치 없이 즉시 진입을 요구합니다.
user·user_profile·refresh_token은 전부 삭제했습니다. 대신session이 익명 식별자(쿠키/로컬 저장) 하나로 플레이를 묶습니다.09-15~16에 과잉 사용 방지를 위해 구글 로그인을 도입했고, 판을 만든 사용자(
attempts.user_id)가 하루 판 수·전역 정원 집계의 기준이 됐습니다. 아래 본문은 09-03 설계입니다. 자세한 내용은 API 계약과 보안 및 운영 한도에 있습니다.
공통 컬럼
| 컬럼 | 타입 | 설명 |
|---|---|---|
id | BIGSERIAL PK | 내부 식별자 |
public_id | UUID UNIQUE | 외부 노출용 |
created_at | TIMESTAMPTZ | 생성 시각 (UTC 저장 · KST 표시) |
로그 성격 테이블은 updated_at·deleted_at을 두지 않습니다. 판정 기록은 수정하거나 지우지 않습니다.
테이블
세션 · 루프
| 테이블 | 역할 | 주요 컬럼 |
|---|---|---|
session | 익명 플레이 세션 | anon_key UNIQUE, domain(game·audit·career), started_at, ended_at |
loop | 루프 1회 | session_id FK, loop_no, utterance_budget, ended_reason, ending_type |
beat | 하루의 비트 (5개) | loop_id FK, beat_no, scene_key |
loop.ending_type은 7장의 4종 — repeat · isolation(고립) · hesitation(망설임) · handover(인계).
대화 · 판정
| 테이블 | 역할 | 주요 컬럼 |
|---|---|---|
utterance | 유저 발화 1건 | loop_id FK, beat_id FK, target_npc, text, delivery(전달량), exposure(노출량), budget_before/after |
npc_state | 루프 내 NPC 상태 | loop_id FK, npc_key(minseok·chaeyeon·eunsang), suspicion, trust, probe_budget, plan_json |
judgement_log | 판정 1건 = 인스펙터 1줄 | utterance_id FK, npc_key, kind, delta, accumulated, threshold, reason, raw_llm_json |
replan_event | 재계획 발동 | loop_id FK, npc_key, trigger, changed(bool), before_plan, after_plan |
judgement_log.kind — suspicion · trust · tool_call · tool_result · inconsistency · replan. 8.8 인스펙터 출력이 이 테이블의 한 루프 분량 그대로입니다.
도구
| 테이블 | 역할 | 주요 컬럼 |
|---|---|---|
tool_call | 도구 호출 | utterance_id FK, caller_npc, tool_name, arguments, result, side_effect_json, latency_ms, budget_after |
부작용을 컬럼으로 남긴다
ask_npc는 호출 자체가 상태를 바꿉니다 — 이름을 빌린 쪽의 의심도가 오릅니다.side_effect_json이 없으면 “왜 B가 갑자기 의심하지?”의 인과가 끊깁니다.
노트 · 손상
| 테이블 | 역할 | 주요 컬럼 |
|---|---|---|
note_entry | 루프 노트 (자동 기록) | session_id FK, loop_id FK, kind(death_sense·event·dialogue), text, embedding |
world_damage | 세계 손상 — 리셋되지 않는 유일한 상태 | session_id FK, depth_score, layer1_on, layer2_on, layer3_on, updated_at |
damage_event | 손상 발현 기록 | session_id FK, loop_id FK, layer, target, detail |
world_damage.depth_score는 루프 횟수가 아니라 개입 깊이에 비례합니다 (4.8). 별도 게이지를 만들지 않고 기존 판정 로그에서 산출합니다 — 발화 수 · 신뢰도 변화량 · 결정적 발언 여부.
침묵 권고 — 측정 ②
| 테이블 | 역할 | 주요 컬럼 |
|---|---|---|
advice_log | 권고 발동 1건 | loop_id FK, beat_id FK, fired(bool), recommendation, rationale, state_snapshot_json, rationale_shown(bool) |
| — | 유저 반응 | user_action(accepted·ignored·none), outcome_delta |
이 테이블 하나가 기획서 11.2의 핵심 지표 전부를 담당합니다
- 권고 수용률 →
user_action- 발동 빈도별 수용률 곡선 →
fired+ 시간 분포- 무시 vs 수용 그룹 결과 대조 →
outcome_delta- 근거 표시 유무별 수용률 (A/B) →
rationale_shown
rationale_shown을 처음부터 넣는 이유는, 나중에 붙이면 A/B 대조군이 없어져 11.2의 마지막 행을 측정할 수 없기 때문입니다.
하네스 계측
| 테이블 | 역할 | 주요 컬럼 |
|---|---|---|
llm_call | LLM 호출 1건 | loop_id FK, role(planner·agent·advisor·evaluator), model, schema_ok, retry_count, tokens_in/out, latency_ms, cost |
역할별 스키마 통과율 · 재시도율(9.4)이 여기서 나오고, 비용 관리도 겸합니다.
관계도
erDiagram
session ||--o{ loop : "1:N"
session ||--o{ note_entry : "루프를 넘어 누적"
session ||--|| world_damage : "리셋되지 않음"
session ||--o{ damage_event : "1:N"
loop ||--o{ beat : "1:N"
loop ||--o{ utterance : "1:N"
loop ||--o{ npc_state : "루프당 3행"
loop ||--o{ replan_event : "1:N"
loop ||--o{ advice_log : "1:N"
loop ||--o{ llm_call : "1:N"
utterance ||--o{ judgement_log : "1:N"
utterance ||--o{ tool_call : "1:N"
session {
bigint id PK
uuid public_id UK
text anon_key UK
text domain "game | audit | career"
timestamptz started_at
timestamptz ended_at
}
loop {
bigint id PK
bigint session_id FK
int loop_no
int utterance_budget
text ended_reason
text ending_type "repeat | isolation | hesitation | handover"
}
beat {
bigint id PK
bigint loop_id FK
int beat_no
text scene_key
}
utterance {
bigint id PK
bigint loop_id FK
bigint beat_id FK
text target_npc
text text
int delivery
int exposure
int budget_before
int budget_after
}
npc_state {
bigint id PK
bigint loop_id FK
text npc_key "minseok | chaeyeon | eunsang"
int suspicion
int trust
int probe_budget
jsonb plan_json
}
judgement_log {
bigint id PK
bigint utterance_id FK
text npc_key
text kind "suspicion | trust | tool_call | tool_result | inconsistency | replan"
int delta
int accumulated
int threshold
text reason
jsonb raw_llm_json
}
replan_event {
bigint id PK
bigint loop_id FK
text npc_key
text trigger
boolean changed
jsonb before_plan
jsonb after_plan
}
tool_call {
bigint id PK
bigint utterance_id FK
text caller_npc
text tool_name
jsonb arguments
jsonb result
jsonb side_effect_json
int latency_ms
int budget_after
}
note_entry {
bigint id PK
bigint session_id FK
bigint loop_id FK
text kind "death_sense | event | dialogue"
text text
vector embedding
}
world_damage {
bigint id PK
bigint session_id FK
int depth_score
boolean layer1_on
boolean layer2_on
boolean layer3_on
timestamptz updated_at
}
damage_event {
bigint id PK
bigint session_id FK
bigint loop_id FK
int layer
text target
text detail
}
advice_log {
bigint id PK
bigint loop_id FK
bigint beat_id FK
boolean fired
text recommendation
text rationale
jsonb state_snapshot_json
boolean rationale_shown
text user_action "accepted | ignored | none"
int outcome_delta
}
llm_call {
bigint id PK
bigint loop_id FK
text role "planner | agent | advisor | evaluator"
text model
boolean schema_ok
int retry_count
int tokens_in
int tokens_out
int latency_ms
numeric cost
}
session ──1:N── loop ──1:N── beat
│ ├──1:N── utterance ──1:N── judgement_log
│ │ └──1:N── tool_call
│ ├──1:N── npc_state (루프당 3행: 민석·채연·은상)
│ ├──1:N── replan_event
│ ├──1:N── advice_log
│ └──1:N── llm_call
├──1:N── note_entry ← 루프를 넘어 누적
├──1:1── world_damage ← 리셋되지 않음
└──1:N── damage_event
리셋 경계가 곧 테이블 경계입니다. loop 아래는 루프마다 새로 생기고, session 직속(note_entry·world_damage)은 루프를 넘어 살아남습니다. 8.4 메모리 비대칭이 스키마에 그대로 박힙니다.
민석 예외는 스키마가 아니라 정책
npc_state는 셋 다 같은 테이블을 씁니다. 차이는 루프 전이 시 무엇을 이월하는가뿐입니다.
| NPC | 이월 |
|---|---|
| 채연 · 은상 | 소거 (신뢰 5% 잔류) |
| 민석 | 유지 |
같은 NPC 인터페이스를 구현하되 메모리 정책만 다르게 주입합니다. 포트 설계가 제대로 돼 있으면 정책 주입만으로 처리됩니다 — 아키텍처가 서사를 지탱하는 지점입니다.
현행 구현과의 차이 2026-09-15 대화 정책
npc-dialogue-2부터는 네 NPC 모두 새 루프에서 당일 기억을 초기화하고 신뢰 5%만 남깁니다. 민석 예외는 v6 설계이며, 스키마는 그대로 두고 정책만 바뀌었습니다. AI 에이전트 설계 참고.
인덱스
| 테이블 | 인덱스 | 이유 |
|---|---|---|
loop | (session_id, loop_no) | 루프 조회 |
utterance | (loop_id, created_at) | 대화 복원 |
judgement_log | (utterance_id) | 인스펙터 개봉 |
note_entry | (session_id, loop_id) + embedding 벡터 인덱스 | 노트 축적 · search_notes |
advice_log | (user_action, rationale_shown) | 11.2 집계 |
llm_call | (role, model, schema_ok) | 9.4 하네스 지표 |
마이그레이션
- Alembic. 리비전 하나 = 변경 하나.
downgrade필수 작성 - Day 10(9/12)에
downgrade→upgrade왕복 검증 - 오픈 직전에는 추가만 하는 마이그레이션 (컬럼 삭제·이름 변경 금지)
결정 로그
| 날짜 | 결정 | 이유 |
|---|---|---|
| 2026-09-03 | 계정 테이블 전부 삭제, 익명 session 도입 | 12.4 로그인 없는 즉시 진입 |
| 2026-09-03 | advice_log.rationale_shown을 초기 설계에 포함 | 나중에 붙이면 A/B 대조군이 사라져 11.2 측정 불가 |
| 2026-09-03 | world_damage를 session 직속으로 (loop 아래 아님) | 손상은 리셋되지 않는 유일한 세계 상태 (4.8) |
| 2026-09-03 | 손상 게이지를 별도 관리하지 않고 판정 로그에서 산출 | 4.8 원문. 중복 상태를 만들면 어긋난다 |
| 2026-09-03 | tool_call.side_effect_json 필수 | 부작용이 안 남으면 발각의 인과가 끊긴다 |