장단콩 보드게임 & 미니어처 게임 클럽

트로피(업적) 시스템 설계

개요

마이페이지(mypage.js) “내 정보” 탭 하단에 트로피 그리드를 추가한다. 획득 조건은 Cloud Function이 stats/global 문서 변경을 트리거로 서버에서 판정하고, users/{uid}.trophies 배열에 기록한다. 신규 획득 트로피는 다음 페이지 로드 시 victory.js 스타일 팝업으로 1회 안내한다.

데이터 모델

users/{uid} 문서에 필드 추가:

trophies: [
  { id: "kongz-regular", earnedAt: Timestamp, seen: false },
  ...
]

트로피 정의 (functions/trophies.js)

ID, 이름, 설명, 이미지 경로, 조건 판정 함수를 코드 상수로 관리한다 (Firestore 설정 문서 없음, honor.md처럼 하드코딩이 이 프로젝트 관례).

id 이름 조건 이미지
kongz-regular 콩즈 죽돌이 누적 출석 10회 (attendCount >= 10) /assets/trophies/kongz-regular.png
kongz-veteran 콩즈 개근왕 누적 출석 30회 (attendCount >= 30) /assets/trophies/kongz-veteran.png
schedule-maker 일정 메이커 누적 게시글 등록 10개 (postCount >= 10) /assets/trophies/schedule-maker.png
full-house-king 만석 달성왕 본인이 등록한 이벤트 중 만석 5회 (attendees.length >= maxAttendees) /assets/trophies/full-house-king.png
game-2048-champion 2048 간판왕 game_scores에서 bestScore 전체 1위 /assets/trophies/game-2048-champion.png

각 트로피는 { id, name, description, image, check(ctx) => boolean } 형태의 객체로 정의. check는 판정에 필요한 데이터(memberStats, fullCount, isTopScorer 등)를 담은 ctx를 받는다.

판정 트리거 (functions/index.js)

stats/global 문서에 onDocumentWritten 트리거를 추가한다 (기존 notifyOnPendingSignup과 동일한 패턴).

트리거 실행 시:

  1. 변경된 members 맵을 순회하며 각 uid에 대해 kongz-regular, kongz-veteran, schedule-maker 조건을 attendCount/postCount로 판정한다.
  2. full-house-king 판정을 위해 해당 uidauthorUid인 이벤트 posts를 쿼리해 fullCount를 계산한다 (stats.jsrenderAwards 로직과 동일한 방식, 판정이 필요한 uid에 한해서만 쿼리).
  3. game-2048-championstats/global 트리거 범위 밖이므로, game_scores/{uid} 문서에 대한 별도 onDocumentWritten 트리거를 추가해 판정한다 (전체 컬렉션에서 자신이 1위인지 확인).
  4. 아직 보유하지 않은 트로피 중 조건을 만족하는 것이 있으면 users/{uid}.trophies{ id, earnedAt: serverTimestamp(), seen: false }를 추가한다 (이미 있는 트로피는 재판정하지 않음 — 배열에 해당 id가 없을 때만 추가).

팝업 (클라이언트)

마이페이지 UI

mypage.html#section-parties와 탈퇴 버튼 사이에 트로피 섹션 추가:

<div id="section-trophies" class="mb-4">
  <div style="...">보유 트로피</div>
  <div id="trophy-grid" style="display:grid;grid-template-columns:repeat(auto-fill,minmax(64px,1fr));gap:12px"></div>
</div>

범위 밖