Files
joke-app/docs/08-data-model.md
EJClaw f4d63e1544 docs: 온라인 보드게임 사이트 계획서 (공통 14편 + 게임 26종)
- 개요, 조사(BGA·한국 인기 보드게임·UI/UX), 아키텍처, 실시간 프로토콜,
  안정성·보안, 계정, 로비·방, UI/UX, 데이터 모델, 게임 엔진, 테스트,
  배포·운영, 로드맵(M0~M9), 사용자 결정 항목
- 게임별 규칙·엔진 설계·UI·테스트 체크리스트 26종

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-04 04:18:29 +09:00

5.3 KiB

08. 데이터 모델 (SQLite)

1. 설정

  • 파일: data/app.db (Docker 볼륨). PRAGMA journal_mode=WAL; PRAGMA synchronous=NORMAL; PRAGMA foreign_keys=ON; PRAGMA busy_timeout=5000;
  • 마이그레이션: apps/server/src/db/migrations/NNN_name.sql, 시작 시 schema_migrations 기준으로 순서대로 적용. 한 번 배포된 마이그레이션 파일은 수정 금지.
  • ID: 사용자·방은 ULID(시간 정렬 가능) 문자열.
  • 시각: epoch 밀리초 정수.

2. 테이블

CREATE TABLE users (
  id            TEXT PRIMARY KEY,           -- ULID
  kind          TEXT NOT NULL CHECK (kind IN ('guest','member')),
  nickname      TEXT NOT NULL,
  avatar_url    TEXT,                       -- 디스코드 아바타
  use_avatar    INTEGER NOT NULL DEFAULT 1,
  settings_json TEXT NOT NULL DEFAULT '{}', -- 글자 크기, 테마, 소리 등
  nick_changed_at INTEGER, nick_change_count INTEGER NOT NULL DEFAULT 0,
  created_at    INTEGER NOT NULL,
  last_seen_at  INTEGER NOT NULL,
  deleted_at    INTEGER
);

CREATE TABLE oauth_accounts (
  provider         TEXT NOT NULL,           -- 'discord'
  provider_user_id TEXT NOT NULL,
  user_id          TEXT NOT NULL REFERENCES users(id) ON DELETE CASCADE,
  username         TEXT,
  created_at       INTEGER NOT NULL,
  PRIMARY KEY (provider, provider_user_id)
);
CREATE INDEX oauth_accounts_user ON oauth_accounts(user_id);

CREATE TABLE sessions (
  token_hash  TEXT PRIMARY KEY,             -- sha256(token) hex
  user_id     TEXT NOT NULL REFERENCES users(id) ON DELETE CASCADE,
  created_at  INTEGER NOT NULL,
  expires_at  INTEGER NOT NULL,
  last_used_at INTEGER NOT NULL,
  user_agent  TEXT
);
CREATE INDEX sessions_user ON sessions(user_id);

CREATE TABLE rooms (
  id          TEXT PRIMARY KEY,             -- ULID
  code        TEXT NOT NULL,                -- 6자리
  host_id     TEXT NOT NULL,
  game_id     TEXT NOT NULL,
  status      TEXT NOT NULL CHECK (status IN ('lobby','playing','finished','paused','broken','closed')),
  visibility  TEXT NOT NULL,
  config_json TEXT NOT NULL,                -- 옵션, 인원, 턴 시간, 채팅 설정
  created_at  INTEGER NOT NULL,
  updated_at  INTEGER NOT NULL,
  closed_at   INTEGER
);
-- 열린 방끼리만 코드 중복 금지
CREATE UNIQUE INDEX rooms_open_code ON rooms(code) WHERE status <> 'closed';
CREATE INDEX rooms_status ON rooms(status);

CREATE TABLE room_state (
  room_id     TEXT PRIMARY KEY REFERENCES rooms(id) ON DELETE CASCADE,
  seq         INTEGER NOT NULL,
  state_json  TEXT NOT NULL,                -- 자리, 게임 상태, RNG 상태, deadline, stateVersion
  updated_at  INTEGER NOT NULL
);

CREATE TABLE room_log (
  room_id   TEXT NOT NULL,
  seq       INTEGER NOT NULL,
  game_no   INTEGER NOT NULL,               -- 방 안에서 몇 번째 게임인지
  actor_id  TEXT,                           -- null = 시스템(타이머 등)
  kind      TEXT NOT NULL,                  -- 'act' | 'timeout' | 'start' | 'seat' | ...
  data_json TEXT NOT NULL,
  at        INTEGER NOT NULL,
  PRIMARY KEY (room_id, seq)
);

CREATE TABLE games (
  id          TEXT PRIMARY KEY,             -- ULID, 한 판
  room_id     TEXT NOT NULL,
  game_no     INTEGER NOT NULL,
  game_id     TEXT NOT NULL,
  options_json TEXT NOT NULL,
  seed_hash   TEXT NOT NULL,
  seed        TEXT,                         -- 끝난 뒤에만 채움(공정성 확인)
  started_at  INTEGER NOT NULL,
  ended_at    INTEGER,
  end_reason  TEXT,                         -- 'normal' | 'resign' | 'abandoned' | 'void'
  result_json TEXT
);
CREATE INDEX games_room ON games(room_id);

CREATE TABLE game_players (
  game_pk   TEXT NOT NULL REFERENCES games(id) ON DELETE CASCADE,
  user_id   TEXT NOT NULL,
  seat      INTEGER NOT NULL,
  rank      INTEGER,                        -- 1 = 1등, 무승부는 같은 순위
  score     REAL,
  PRIMARY KEY (game_pk, user_id)
);
CREATE INDEX game_players_user ON game_players(user_id);

CREATE TABLE user_stats (
  user_id TEXT NOT NULL,
  game_id TEXT NOT NULL,
  played  INTEGER NOT NULL DEFAULT 0,
  wins    INTEGER NOT NULL DEFAULT 0,
  draws   INTEGER NOT NULL DEFAULT 0,
  PRIMARY KEY (user_id, game_id)
);

3. 쓰기 시점

사건 쓰기
방 생성 rooms insert, room_state insert
대기실 변경(자리, 옵션, 준비) room_state update(+ room_log)
게임 시작 games insert, room_state, room_log
게임 행동 / 시간 초과 room_log insert + room_state update (한 트랜잭션)
게임 끝 games update(결과, 시드 공개), game_players, user_stats upsert (한 트랜잭션)
방 닫힘 rooms.status='closed', room_state 삭제

4. 정리 작업(매시간)

  • 만료된 세션 삭제.
  • 90일 지난 room_log 삭제(결과는 games에 남음).
  • 1년 이상 접속 없는 게스트: 사용자·전적 삭제.
  • 닫힌 지 24시간이 지난 방의 코드는 자연히 재사용 가능(부분 유니크 인덱스).

5. 개인정보

  • 저장하는 개인정보: 닉네임, 디스코드 사용자 ID/이름/아바타 해시, 접속 시각, User-Agent(세션 관리용).
  • IP는 DB에 저장하지 않고 속도 제한용 메모리에만 둔다.
  • 계정 삭제 시 users.deleted_at 설정 + 닉네임을 "탈퇴한 사용자"로, OAuth 연결·세션 삭제. 다른 사람의 전적 표시가 깨지지 않게 행은 유지.