게임 전체 1위 트로피 수여 복구 설계
목표
2048와 콩드랍의 전체 최고 기록 보유자에게 각각 game-2048-champion, suika-master 업적을 영구 수여한다. 현재 최고 기록자인데 누락된 회원도 소급 수여한다. 트로피는 회수하지 않는다.
정책
- 전체 최고 기록과 같은 점수인 모든 회원은 공동 1위로 수여 대상이다.
- 점수 문서가 생성·수정될 때마다, 변경된 회원이 해당 게임의 현재 전체 1위인지 서버에서 판정한다.
- 수여는 멱등적이다. 같은 트로피가 이미 있으면 추가하지 않는다.
- 뒤에 다른 회원이 더 높은 점수를 내도 기존 수상자의 트로피는 유지한다.
- 두 게임 모두 0점은 수여 대상이 아니다.
서버 판정
functions/index.js의 onGameScoreUpdated와 onSuikaScoreUpdated는 각각 game_scores/{uid}와 suika_scores/{uid} 변경 후 전체 컬렉션을 조회한다. 2048은 bestScore, 콩드랍은 best를 같은 정규화 구조({ uid, bestScore })로 변환한 뒤 isTopScorer에 전달한다. 최고점이면 기존 awardTrophies를 통해 신규 트로피만 users/{uid}.trophies에 { id, earnedAt, seen: false }로 추가한다.
수정에서는 다음을 명확히 보강한다.
- 두 트리거가 최고점 갱신이 아닌 쓰기에서도 안전하게 동작하도록 공통 최고점 판정과 각 점수 스키마 변환을 순수 함수 테스트로 고정한다.
- 두 게임의 트리거가 동일한 영구 수여 정책을 따른다는 회귀 테스트를 추가한다.
- 트로피 문서 갱신 뒤 내 정보 페이지가 이미 사용하는
trophies배열과TROPHIES_META를 통해 해당 이미지를 잠금 없이 렌더링하는 경로는 변경하지 않는다.
소급 수여
관리자 통계 페이지의 기존 “트로피 소급 재계산”은 game_scores와 suika_scores 전체를 읽어 각 게임 최고점자에게 후보 트로피를 계산한다. 이 기능을 실행해 현재 각 게임의 누락된 전체 1위(공동 1위 포함)에게 두 트로피를 추가한다. 기존 보유자는 newlyEarnedTrophyIds로 제외한다.
일회성 수동 데이터 편집이나 트로피 회수는 수행하지 않는다.
검증
- 2048/콩드랍 각 점수 스키마가 올바른 점수로 정규화된다.
- 단독 1위와 공동 1위는 수여 대상이고, 0점·비최고점은 대상이 아니다.
- 이미 보유한 트로피는 다시 수여 후보에 포함되지 않는다.
- 소급 재계산 후보가 두 게임의 현재 최고점자에게 각각 만들어진다.
- 사용자 트로피 배열에 기록된
suika-master/game-2048-champion이 내 정보의 활성 트로피 렌더링으로 전달되는 기존 테스트를 유지한다.
범위 밖
- 1위에서 내려온 회원의 트로피 회수
- 점수 순위 UI의 변경
- 트로피 이미지 또는 팝업 디자인 변경