From f0b01a2dc0c1f257a316b9fb018f73c886a1356a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9C=A4=EC=A0=95=EB=AF=BC?= Date: Thu, 28 May 2026 17:34:16 +0900 Subject: [PATCH] Field-mask the scoreboard self-user read (R5) getScoreboard loaded the full user doc just to use 5 fields. Add getUserForScoreboard using getAll with a fieldMask so only displayName, photoUrl, tierPoints, favoriteTeamCode, and rankSnapshot are fetched, cutting per-request bandwidth. The short-lived me-rank cache (meRankCache, 30s) already covers the rank-count side of R5. --- src/repositories/userRepository.ts | 38 ++++++++++++++++++++++++++++++ src/services/scoreboardService.ts | 4 ++-- 2 files changed, 40 insertions(+), 2 deletions(-) diff --git a/src/repositories/userRepository.ts b/src/repositories/userRepository.ts index 35f6c89..25baae8 100644 --- a/src/repositories/userRepository.ts +++ b/src/repositories/userRepository.ts @@ -36,6 +36,44 @@ export async function getUser(uid: string): Promise { return snap.exists ? (snap.data() as User) : null; } +/** 스코어보드 "me" 계산에 필요한 최소 필드만 담는 형태. */ +export interface ScoreboardSelfUser { + displayName: string; + photoUrl?: string; + tierPoints?: number; + favoriteTeamCode?: TeamCode; + rankSnapshot?: RankSnapshot; +} + +/** + * 스코어보드 응답의 "me" 계산에 필요한 필드만 fieldMask로 읽어온다. + * 전체 user doc(streak/티켓/notifications 등) 대신 5개 필드만 전송받아 bandwidth를 줄인다. + * + * @returns 필요한 필드 부분집합. 문서가 없으면 `null`. + */ +export async function getUserForScoreboard( + uid: string +): Promise { + const ref = firestore.collection(COLLECTION).doc(uid); + const [snap] = await firestore.getAll(ref, { + fieldMask: [ + "displayName", + "photoUrl", + "tierPoints", + "favoriteTeamCode", + "rankSnapshot", + ], + }); + if (!snap.exists) return null; + const data = snap.data() as Partial; + const self: ScoreboardSelfUser = { displayName: data.displayName ?? "" }; + if (data.photoUrl) self.photoUrl = data.photoUrl; + if (data.tierPoints !== undefined) self.tierPoints = data.tierPoints; + if (data.favoriteTeamCode) self.favoriteTeamCode = data.favoriteTeamCode; + if (data.rankSnapshot) self.rankSnapshot = data.rankSnapshot; + return self; +} + export interface ScoreboardUserEntry { uid: string; displayName: string; diff --git a/src/services/scoreboardService.ts b/src/services/scoreboardService.ts index 68f5c36..61dcbb5 100644 --- a/src/services/scoreboardService.ts +++ b/src/services/scoreboardService.ts @@ -1,7 +1,7 @@ import { MemCache } from "../lib/memCache"; import { countUsersAboveTierPoints, - getUser, + getUserForScoreboard, } from "../repositories/userRepository"; import { readScope, @@ -51,7 +51,7 @@ export async function getScoreboard( uid: string, type: ScoreboardType ): Promise { - const user = await getUser(uid); + const user = await getUserForScoreboard(uid); if (!user) throw new HttpError(404, `user not found: ${uid}`); const teamCode = type === "team" ? user.favoriteTeamCode : undefined;