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:
100
docs/03-realtime-protocol.md
Normal file
100
docs/03-realtime-protocol.md
Normal file
@@ -0,0 +1,100 @@
|
||||
# 03. 실시간 통신 프로토콜
|
||||
|
||||
## 1. 원칙
|
||||
- 연결은 사용자당 브라우저 탭 하나에 WebSocket 하나(`wss://<도메인>/ws`).
|
||||
- **서버가 유일한 진실**. 클라이언트는 행동을 "요청"만 하고, 화면은 서버가 보낸 상태로만 그린다.
|
||||
- 서버는 매번 **그 사람이 볼 수 있는 전체 화면 상태(view)** 를 보낸다(차이(diff) 전송 안 함). 보드게임 상태는 작아서(대부분 2~10KB) 전체 전송이 단순하고 안전하다. 메시지를 잃어버려도 다음 메시지 하나로 화면이 완전히 맞춰진다.
|
||||
- 애니메이션용 `events`는 덤이다. 놓쳐도 상태는 맞다.
|
||||
- 프로토콜 버전 `PROTOCOL = 1`. 서버와 버전이 다르면 클라이언트는 새로고침 안내를 띄운다(배포 직후 대비).
|
||||
|
||||
## 2. 연결과 인증
|
||||
1. 클라이언트가 `/ws`로 업그레이드 요청. 브라우저가 세션 쿠키(`sid`, httpOnly)를 같이 보낸다.
|
||||
2. 서버는 업그레이드 단계에서 세션을 확인하고, 없으면 401로 거절(클라이언트는 닉네임 화면으로).
|
||||
3. `Origin` 헤더가 우리 도메인이 아니면 거절(CSWSH 방지).
|
||||
4. 연결되면 서버가 `welcome`을 보낸다. 사용자가 이미 들어가 있는 방이 있으면 `welcome.activeRoom`에 코드를 넣어 준다(→ "진행 중인 게임으로 돌아가기").
|
||||
|
||||
## 3. 메시지 형식
|
||||
모든 메시지는 JSON 객체이며 `t`(type) 필드를 가진다. 모든 클라이언트 메시지는 `packages/shared`의 zod 스키마로 검사한다.
|
||||
|
||||
### 3.1 클라이언트 → 서버
|
||||
| t | 필드 | 설명 |
|
||||
|---|---|---|
|
||||
| `join` | `code`, `as: "player"\|"spectator"` | 방 입장. 이미 자리 있는 방이면 재입장 처리 |
|
||||
| `leave` | — | 방 나가기(게임 중이면 확인 후, 기권 처리 규칙은 6절) |
|
||||
| `seat` | `seat?: number` | 자리에 앉기/자리 바꾸기(대기실에서만) |
|
||||
| `unseat` | — | 관전자로 일어나기(대기실에서만) |
|
||||
| `ready` | `ready: boolean` | 준비 |
|
||||
| `config` | `gameId?`, `options?`, `maxPlayers?`, `visibility?`, `allowSpectators?`, `chatEnabled?`, `chatFilter?` | 방장: 게임/옵션/방 설정 변경(대기실·게임 끝난 뒤에만) |
|
||||
| `start` | — | 방장: 시작 |
|
||||
| `kick` | `userId` | 방장: 내보내기 |
|
||||
| `host` | `userId` | 방장 넘기기 |
|
||||
| `act` | `cs: number`, `a: Action` | 게임 행동. `cs`는 클라이언트 행동 번호(1부터 증가) |
|
||||
| `chat` | `text`(1~200자) | 채팅 |
|
||||
| `emote` | `id` | 빠른 반응(👍, 😂, "잘했어요!" 등 고정 목록) |
|
||||
| `rematch` | — | 게임 끝난 뒤 "한 판 더" 투표 |
|
||||
| `sync` | — | 전체 상태 다시 요청 |
|
||||
| `ping` | `ts` | 지연 측정(10초마다) |
|
||||
|
||||
### 3.2 서버 → 클라이언트
|
||||
| t | 필드 | 설명 |
|
||||
|---|---|---|
|
||||
| `welcome` | `me`, `serverTime`, `protocol`, `activeRoom?` | 연결 직후 |
|
||||
| `room` | `seq`, `room: RoomView` | 대기실/자리/옵션/접속 상태 전체 |
|
||||
| `state` | `seq`, `view`, `events`, `active: userId[]`, `deadline: number\|null`, `cs?` | 게임 상태. `cs`는 이 상태를 만든 행동이 내 것일 때만 포함(= 성공 응답) |
|
||||
| `reject` | `cs`, `reason` | 내 행동 거절(한국어 이유). 화면은 바꾸지 않고 안내만 표시 |
|
||||
| `chat` | `from`, `text`, `at`, `channel?` | 채팅(마피아 등은 채널 구분) |
|
||||
| `emote` | `from`, `id` | 반응 |
|
||||
| `result` | `seq`, `result` | 게임 결과 |
|
||||
| `notice` | `code`, `message` | "OO님이 연결이 끊겼어요" 같은 알림 |
|
||||
| `error` | `code`, `message` | 잘못된 요청, 방 없음 등 |
|
||||
| `pong` | `ts`, `serverTime` | 지연·시계 보정 |
|
||||
| `bye` | `reason` | 서버 종료 예정(`restart`), 다른 창에서 접속(`replaced`), 강퇴(`kicked`) |
|
||||
|
||||
## 4. 순서 보장 (seq)
|
||||
- 방마다 `seq`(정수)가 있고, 방에서 무엇이든 바뀌면 1 증가한다(자리, 옵션, 게임 행동 모두).
|
||||
- 클라이언트는 받은 `seq`가 가지고 있는 값보다 작거나 같으면 버린다(늦게 도착한 옛 메시지 무시).
|
||||
- `seq`가 건너뛰어도(중간 메시지 누락) 전체 상태이므로 그대로 적용한다. 단 `events` 애니메이션은 건너뛴다.
|
||||
|
||||
## 5. 행동 처리와 중복 방지
|
||||
- 클라이언트는 `act`를 보내면 해당 버튼을 잠그고 "처리 중" 표시(150ms 이상 걸릴 때만 보이게).
|
||||
- 서버는 `(userId, room, cs)`를 기억해 같은 `cs`가 다시 오면 무시한다(재연결 직후 재전송 대비). 재연결 시 클라이언트는 응답 못 받은 행동을 **다시 보내지 않고** `sync`로 상태를 받은 뒤 사용자가 다시 판단하게 한다(이미 처리됐을 수 있으므로).
|
||||
- 낙관적 업데이트는 하지 않는다(숨겨진 정보 게임에서 오히려 혼란). 예외: 루미큐브 타일 정렬, 그림 맞히기 붓질처럼 "내 화면 안에서만의 조작"은 즉시 그린다.
|
||||
- 2초 안에 응답이 없으면 "연결 확인 중" 표시 후 `ping`으로 연결 점검.
|
||||
|
||||
## 6. 연결 끊김과 재접속
|
||||
### 6.1 클라이언트
|
||||
- 끊기면 자동 재연결: 0.5s → 1s → 2s → 4s → 8s → 이후 10s 간격, 각 ±30% 무작위 지연(서버 재시작 시 몰림 방지).
|
||||
- 화면 위에 작은 띠로 "다시 연결하는 중…" 표시. 게임 화면은 그대로 두되 조작은 잠근다.
|
||||
- 탭이 다시 보이거나(`visibilitychange`) 네트워크가 돌아오면(`online`) 즉시 재시도.
|
||||
- 다시 연결되면 마지막 방에 자동 `join` → 서버가 `room` + `state` 전체를 보냄.
|
||||
|
||||
### 6.2 서버
|
||||
- 연결이 끊긴 플레이어는 **자리를 유지**하고 다른 사람에게 "연결 끊김" 표시.
|
||||
- 턴 타이머는 그대로 흐른다. 끊긴 사람의 차례에 시간이 다 되면 게임의 `onTimeout` 자동 행동(예: 포커 다이/체크, 오목 무작위 착수가 아니라 **시간패** 등 게임별 문서에 정의).
|
||||
- 끊긴 지 **60초**(방 옵션 30~300초)가 지나면 "자리 비움" 상태: 자기 차례가 오면 기다리지 않고 바로 자동 행동.
|
||||
- 자동 행동이 연속 3번이거나 끊긴 지 5분이 지나면 방장에게 "내보내기" 버튼을 보여 준다. 2인 대전 게임은 상대에게 "승리로 끝내기" 선택권을 준다.
|
||||
- 같은 사용자가 새 탭으로 같은 방에 들어오면 새 연결이 이기고, 옛 연결에는 `bye: replaced`.
|
||||
|
||||
## 7. 타이머와 시계
|
||||
- 모든 마감 시각은 서버 시각(epoch ms)으로 보낸다.
|
||||
- 클라이언트는 `pong.serverTime`과 왕복 시간으로 시계 차이를 추정(최근 5개 중간값)하고 남은 시간을 그린다.
|
||||
- 시간 판정은 서버만 한다. 서버 타이머는 방마다 하나(`setTimeout`)이며, 서버 재시작 후에는 저장된 `deadline`으로 다시 건다.
|
||||
|
||||
## 8. 하트비트·제한
|
||||
| 항목 | 값 |
|
||||
|---|---|
|
||||
| 서버 idle timeout | 40초 (Bun `idleTimeout`), 서버가 25초마다 ping 프레임 |
|
||||
| 클라이언트 ping | 10초마다, 5초 안에 pong이 없으면 연결을 끊고 재연결 |
|
||||
| 메시지 최대 크기 | 16KB (그림 맞히기 붓질 메시지만 32KB) |
|
||||
| 속도 제한 | 연결당 토큰 버킷 초당 20개, 최대 40개. 채팅은 0.5초에 1개. 넘으면 버리고 계속 넘으면 연결 종료 |
|
||||
| 압축 | permessage-deflate 사용(1KB 이상 메시지만 효과) |
|
||||
| 서버 송신 버퍼 | 연결당 1MB 넘게 밀리면(느린 클라이언트) 연결을 끊음 → 재연결 시 전체 상태로 회복 |
|
||||
|
||||
## 9. 실시간 경쟁 게임(할리갈리, 도블 등)
|
||||
- 먼저 누른 사람은 **서버 도착 순서**로 정한다. 같은 시점에 판정 창을 열고, 첫 도착을 처리한 뒤 30ms 동안 들어온 같은 종류 행동은 "늦음"으로 알려 준다.
|
||||
- 각자의 왕복 지연(RTT)을 화면 구석에 표시하고, 방 옵션으로 "지연 보정"(클라이언트가 누른 시각 − RTT/2로 순서 결정, 단 100ms 이내 차이만 보정)을 켤 수 있다. 기본은 꺼짐(조작 가능성 때문에). 세부는 각 게임 문서.
|
||||
|
||||
## 10. 서버 재시작 시
|
||||
1. 종료 신호(SIGTERM)를 받으면 새 연결을 받지 않고, 모든 연결에 `bye: restart`를 보낸 뒤 DB를 정리하고 종료(최대 10초).
|
||||
2. 클라이언트는 `restart`를 받으면 "잠시 후 자동으로 다시 연결돼요" 표시 후 1~3초 무작위 대기 후 재연결.
|
||||
3. 새 서버는 시작할 때 진행 중이던 방을 DB에서 복구(`04-stability-security.md` 3절)한 다음 연결을 받는다.
|
||||
Reference in New Issue
Block a user