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

@@ -957,3 +957,80 @@ label.field {
.schema-group > legend {
grid-column: 1 / -1;
}
/* Replay (docs/09 §8): read-only board, step controls in the side panel (PC) or a bottom bar (phone/tablet) */
.replay-readonly {
display: contents;
}
.replay-readonly button,
.replay-readonly [role='button'],
.replay-readonly svg {
cursor: default;
}
.replay-controls {
display: flex;
flex-direction: column;
gap: 8px;
width: 100%;
background: var(--surface);
border-radius: var(--radius-lg);
box-shadow: var(--shadow);
padding: 12px;
}
.bottom-bar .replay-controls {
box-shadow: none;
background: none;
padding: 0;
}
.replay-count {
font-size: 1.1rem;
font-variant-numeric: tabular-nums;
}
.replay-desc {
margin: 0;
min-height: 1.4em;
}
.replay-slider {
width: 100%;
height: 36px;
margin: 0;
accent-color: var(--primary);
}
.replay-buttons {
display: grid;
grid-template-columns: repeat(5, minmax(0, 1fr));
gap: 6px;
}
.replay-buttons .btn {
min-width: 0;
min-height: 60px;
padding: 0 4px;
flex-direction: column;
gap: 2px;
font-size: 0.95rem;
line-height: 1.1;
}
.replay-buttons .btn span {
font-size: 1.2rem;
}
.game-layout.replay-narrow {
padding-bottom: calc(240px + env(safe-area-inset-bottom));
}
.my-games {
list-style: none;
margin: 0;
padding: 0;
display: flex;
flex-direction: column;
}
.my-game {
display: flex;
align-items: center;
gap: 12px;
padding: 12px 0;
border-top: 1px solid var(--border);
}
.my-game:first-child {
border-top: 0;
padding-top: 0;
}