M9: 게임 기록 다시 보기

- 서버: 끝난 게임을 공개된 시드 + room_log 행동으로 다시 돌려 플레이어별 view 장면을 만든다
  (rooms/replay.ts, 장면은 view 차이만 전송: shared/view-patch.ts). 행동 로그의 at = 적용한 정확한 시각,
  start 로그에 stateVersion. GET /api/me/games, GET /api/replays/:gamePk(그 판 플레이어만, no-store),
  RoomView.lastGamePk. 로그가 지워졌거나 규칙 버전이 바뀌었거나 결과가 다르면 한국어 안내(410).
- 웹: 다시 보기 화면(/replay/:gamePk, 게임 Board/Side 읽기 전용, 처음/이전/재생·멈춤/다음/끝, 슬라이더),
  내 정보의 내 게임 기록 목록, 결과 화면의 [처음부터 다시 보기].
- 테스트: 모든 게임을 Room으로 끝까지 둔 뒤 다시 만든 상태·결과·장면이 같은지, 권한, view-patch, e2e/replay.e2e.ts.
- 문서: docs/09 §8, docs/07 §10.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
EJClaw
2026-10-06 13:20:05 +09:00
parent c27cb0a11a
commit c84c1ba95e
19 changed files with 1612 additions and 12 deletions

View File

@@ -152,6 +152,7 @@
- 게임 상세 + 공개 방 목록
- 대기실(방장/참가자/관전자, 인원 부족/가득 참)
- 게임 화면(내 차례/남의 차례/연결 끊김/자리 비움/일시 정지)
- 결과 화면(승/패/무승부, 한 판 더 투표 중)
- 계정(게스트/디스코드)
- 결과 화면(승/패/무승부, 한 판 더 투표 중, 처음부터 다시 보기)
- 계정(게스트/디스코드, 내 게임 기록 목록 + [다시 보기])
- 다시 보기(`/replay/:gamePk`: 판은 읽기 전용, 처음/이전/재생·멈춤/다음/끝 + 슬라이더. PC는 오른쪽 칸, 폰은 아래 고정 막대. 못 보는 게임은 안내 문구, docs/09 §8)
- 오류(방 없음, 방 가득 참, 서버 업데이트, 오프라인)

View File

@@ -120,3 +120,12 @@ export interface GameUI<V> {
- 게임별 불변식(카드 총수 보존, 칩 총합 보존 등)
3. **정보 유출 검사**: 매 단계 각 플레이어 view의 JSON에 남의 비공개 카드 ID가 없는지(게임이 `secretsOf(state, player)`를 테스트용으로 제공).
4. **시간 초과 테스트**: 모든 상태에서 `onTimeout`이 합법 행동을 돌려주는지.
## 8. 게임 기록 다시 보기 (`apps/server/src/rooms/replay.ts`, `apps/web/src/pages/ReplayPage.tsx`)
- **다시 만들기**: 끝난 게임을 공개된 시드 + `room_log`의 `act`/`auto` 기록으로 처음부터 다시 돌린다. `setup`(시작 시각)과 각 `apply`(그 행동을 적용한 정확한 `now`, 로그의 `at`)를 방과 똑같이 부르고 RNG도 이어서 쓰므로 결과가 방에서 끝난 상태와 한 글자도 다르지 않다. `start` 로그에 `stateVersion`을 남긴다.
- **보이는 것**: 그 판에 앉았던 플레이어만 볼 수 있고(관전자·남은 403), 각자 게임 중에 받던 자기 `view`(있으면 `syncView`)만 받는다. 시드와 원본 로그는 서버 밖으로 나가지 않는다. 응답은 `private, no-store`.
- **장면**: 스트리밍 행동(그림 맞히기 선)이 아닌 행동 하나가 한 장면. 첫 장면만 view 전체, 나머지는 앞 장면과의 차이(`packages/shared/src/view-patch.ts`)만 보낸다. 다시 만든 결과가 저장된 결과와 다르면 보여 주지 않는다.
- **못 보는 경우**(410, 한국어 안내): 90일이 지나 로그가 지워짐, 그 뒤로 게임 규칙 버전(`stateVersion`)이 바뀜, 다시 만든 결과가 다름.
- **API**: `GET /api/me/games?before=&limit=`(내가 끝까지 한 게임, 최신순), `GET /api/replays/:gamePk`. 다시 만든 결과는 잠깐(10분, 32개) 캐시하고, 한 번에 하나씩 조금씩 끊어 만들어 방 처리를 막지 않는다.
- **화면**: 게임의 `Board`/`Side`를 그대로 쓰되 읽기 전용(`act`는 아무것도 안 함, 판 안의 누르기·키 입력은 막음). 처음 / 이전 / 재생·멈춤 / 다음 / 끝 버튼, 슬라이더, "n번째 / 전체 n번", 장면 설명(누가 움직였는지, 자동 진행). 키보드 ←/→/Home/End. 들어가는 곳: 결과 화면 [처음부터 다시 보기](`RoomView.lastGamePk`), 내 정보 › 내 게임 기록 [다시 보기].
- **테스트**: 모든 게임을 실제 `Room`으로 끝까지(무작위 행동, 시간 초과, 나가기) 둔 뒤 다시 만든 상태·결과가 같은지, 모든 장면이 그 플레이어가 받은 view와 같은지(`replay.test.ts`), 권한(`replay-api.test.ts`), 브라우저(`e2e/replay.e2e.ts`).