docs: 온라인 보드게임 사이트 계획서 (공통 14편 + 게임 26종)
- 개요, 조사(BGA·한국 인기 보드게임·UI/UX), 아키텍처, 실시간 프로토콜, 안정성·보안, 계정, 로비·방, UI/UX, 데이터 모델, 게임 엔진, 테스트, 배포·운영, 로드맵(M0~M9), 사용자 결정 항목 - 게임별 규칙·엔진 설계·UI·테스트 체크리스트 26종 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
68
docs/04-stability-security.md
Normal file
68
docs/04-stability-security.md
Normal file
@@ -0,0 +1,68 @@
|
||||
# 04. 안정성과 보안
|
||||
|
||||
## 1. 목표
|
||||
- 어떤 상황에서도 진행 중인 게임 상태를 잃지 않는다(프로세스 충돌, 배포, 서버 재부팅).
|
||||
- 한 방의 버그가 다른 방이나 서버 전체를 멈추지 않는다.
|
||||
- 클라이언트를 조작해도 규칙을 어기거나 남의 패를 볼 수 없다.
|
||||
|
||||
## 2. 상태 저장 방식
|
||||
- 방 상태는 메모리에 있고, **행동이 하나 처리될 때마다** 같은 동기 트랜잭션으로 SQLite에 기록한다.
|
||||
- `room_log`에 행동 한 줄 추가(누가, 무엇을, 서버 시각, seq) — 기록 보기·리플레이·디버깅용.
|
||||
- `room_state`에 최신 전체 상태(게임 상태 + RNG 상태 + 타이머 마감 + 자리 정보) 덮어쓰기.
|
||||
- 상태는 작으므로(대부분 수 KB) 매번 전체 저장이 가장 단순하고 확실하다. SQLite WAL + `synchronous=NORMAL`로 한 번 쓰기 1ms 미만 목표(테스트로 확인).
|
||||
- 저장이 끝난 다음에만 다른 사람에게 결과를 보낸다(보낸 상태 = 저장된 상태).
|
||||
- `synchronous=NORMAL`은 프로세스가 죽어도 데이터가 남는다. 정전 같은 OS 단위 장애에서는 마지막 몇 개 행동을 잃을 수 있고, 이는 허용한다.
|
||||
|
||||
## 3. 서버 시작 시 복구
|
||||
1. `rooms`에서 `status IN ('lobby','playing')`인 방을 읽는다.
|
||||
2. 각 방의 `room_state`를 불러 메모리 Room을 만든다. 모든 플레이어는 "연결 끊김" 상태로 시작.
|
||||
3. 저장된 `deadline`이 이미 지났다면 복구 직후 일괄 처리하지 않고, **복구 시각 + 30초**로 마감을 미뤄 준다(재접속할 시간).
|
||||
4. 복구 실패(스키마 불일치, 게임 코드 변경으로 상태가 안 맞음)한 방은 `status='broken'`으로 표시하고, 들어온 사람에게 "서버 업데이트로 이 게임을 이어갈 수 없어요. 새로 시작해 주세요"를 보여 준다.
|
||||
5. 게임 규칙 코드가 바뀌어 저장 상태 형식이 달라지면, 그 게임 모듈의 `stateVersion`을 올리고 `migrate(oldState)`를 제공하거나 위 4번으로 처리한다.
|
||||
|
||||
## 4. 장애 격리
|
||||
- 게임 모듈의 `apply/view` 호출은 try/catch로 감싼다. 예외가 나면:
|
||||
- 그 행동은 적용하지 않음(상태는 직전 그대로), 행동한 사람에게 `reject`("알 수 없는 오류가 발생했어요").
|
||||
- 오류 로그에 방 코드, 게임, seq, 행동, 스택을 남긴다.
|
||||
- 같은 방에서 오류가 3번 연속 나면 방을 "일시 정지"하고 방장에게 "게임 끝내기(무효)"를 제공.
|
||||
- `view()`에서 예외가 나면 그 사람에게만 오류 화면, 다른 사람에게는 정상 전송.
|
||||
- 프로세스 수준 `uncaughtException`/`unhandledRejection`은 로그 후 종료 → Docker `restart: unless-stopped`가 재시작 → 3절로 복구.
|
||||
- 무한 루프 방지: 게임 규칙 함수에 단위 테스트 + 무작위 대국 테스트로 처리 시간 상한(5ms) 확인.
|
||||
|
||||
## 5. 숨겨진 정보와 공정성
|
||||
- 클라이언트에는 `view(state, viewer)` 결과만 보낸다. 덱 순서, 남의 패, 마피아 직업, RNG 시드는 절대 보내지 않는다.
|
||||
- 게임마다 "정보 유출 테스트": 무작위 대국을 돌리며 매 순간 플레이어 A의 view(JSON 문자열)에 다른 사람만 알아야 하는 값(카드 ID 등)이 들어 있지 않은지 검사.
|
||||
- 셔플: 게임 시작 시 `crypto.getRandomValues`로 128비트 시드를 만들고, 상태 안의 시드 PRNG(sfc32)로 Fisher–Yates 셔플.
|
||||
- **공정성 확인(선택 표시)**: 게임 시작 때 시드의 SHA-256 해시를 모두에게 공개하고, 게임이 끝나면 시드를 공개한다. 결과 화면의 "공정성 확인"을 누르면 같은 시드로 덱을 다시 만들어 실제 배분과 같은지 브라우저가 확인한다.
|
||||
- 관전자에게는 공개 정보만. 포커·섯다처럼 관전이 정보 누설 통로가 될 수 있는 게임은 기본적으로 "관전 지연 표시 없음 + 공개 정보만"이고, 같은 사람이 플레이어와 관전자를 동시에 할 수 없다.
|
||||
|
||||
## 6. 사행성·법적 주의
|
||||
- 포커·섯다·고스톱 등은 **방 안에서만 쓰는 가상 칩/점수**를 쓴다. 방이 끝나면 사라지고, 계정에 쌓이지 않으며, 사고팔거나 바꿀 수 없다. 결제·유료 아이템·충전도 없다.
|
||||
- 기록에는 "몇 판 이겼는지"만 남기고 칩 잔액은 남기지 않는다.
|
||||
- 이 구조는 게임산업진흥법의 사행성 관련 위험을 피하기 위한 것이다(법률 자문 아님). 수익화를 하게 되면 그 전에 반드시 법률 검토를 받는다.
|
||||
- 상표: 루미큐브, 우노, 뱅!, 스플렌더, 할리갈리, 다빈치 코드, 모두의 마블 등은 상표다. 공개 서비스로 열기 전에 화면 표시 이름을 일반 명칭으로 바꿀지 사용자 결정을 받는다(`games/README.md` 4절). 카드·화투 그림은 직접 만든 SVG만 쓴다.
|
||||
|
||||
## 7. 웹 보안
|
||||
| 위협 | 대응 |
|
||||
|---|---|
|
||||
| 세션 탈취 | 세션 토큰은 32바이트 무작위, DB에는 SHA-256 해시만 저장. 쿠키 `HttpOnly; Secure; SameSite=Lax; Path=/` |
|
||||
| CSRF | 상태를 바꾸는 HTTP API는 POST + `Origin` 검사 + `SameSite=Lax` |
|
||||
| CSWSH | WS 업그레이드 때 `Origin` 검사 |
|
||||
| XSS | React 기본 이스케이프, `dangerouslySetInnerHTML` 금지(린트), 닉네임·채팅은 일반 텍스트. CSP: `default-src 'self'; img-src 'self' https://cdn.discordapp.com data:; connect-src 'self' wss://<도메인>; frame-ancestors 'none'` |
|
||||
| 메시지 폭주 | 3장 8절의 속도·크기 제한, 사용자당 동시 연결 5개, IP당 분당 방 생성 10개 |
|
||||
| 방 코드 무작위 대입 | 코드 입력 API는 IP당 분당 30회 제한, 6자리(100만 개) 중 사용 중인 코드는 수백 개 수준 |
|
||||
| 잘못된 입력 | 모든 메시지 zod 검사, 게임 `validate()`가 한 번 더 검사 |
|
||||
| 비밀값 | 디스코드 client secret, 세션 서명 키는 `.env`(저장소에 올리지 않음) |
|
||||
|
||||
## 8. 채팅 안전(어린이 사용자 고려)
|
||||
- 방 옵션: 채팅 켜기/끄기(기본 켜짐), 욕설 필터(기본 켜짐, 단어 목록 기반 `***` 치환).
|
||||
- 각 사용자는 특정 사람의 채팅을 "숨기기" 가능(내 화면에서만).
|
||||
- 빠른 반응(이모트)은 고정 목록이라 항상 안전.
|
||||
- 닉네임에도 같은 욕설 필터 적용.
|
||||
|
||||
## 9. 운영 안정성
|
||||
- 로그: JSON 한 줄 로그(시간, 레벨, 방, 사용자, 이벤트). 게임 행동 내용은 `room_log`에만.
|
||||
- 지표: `/internal/metrics`(외부 차단) — 연결 수, 방 수, 행동 처리 시간 히스토그램, 저장 시간, 오류 수, 메모리.
|
||||
- 백업: 매일 SQLite `VACUUM INTO`로 백업 파일 생성, 14일 보관. 복구 절차는 `11-deployment-ops.md`.
|
||||
- 정리: 아무도 없는 대기실 10분 후 삭제, 끝난 방 30분 후 메모리에서 내림(기록은 DB에 남음), 오래된 `room_log`는 90일 후 삭제.
|
||||
- 무중단에 가까운 배포: 새 버전 시작 전 옛 버전이 `bye: restart`로 안내 → 재시작(수 초) → 자동 재연결·복구.
|
||||
Reference in New Issue
Block a user