Trim redundant user reads/writes in userService (W5, R6)
W5: getMe synced photoUrl on every read whenever the token URL string differed, but providers (Google) rotate query params (=sN-c sizing) for the same image, causing write churn on a read path. Compare only the URL before '?' so the write fires only on a genuine photo change. R6: createMe/updateMe re-read the user with getUser right after writing. Synthesize the response instead — updateMe from the already-read doc plus the applied patch; createMe from the written inputs (createdAt approximated as now, since the stored value uses serverTimestamp and later reads reflect it). Removes one Firestore read per onboarding/profile edit.
This commit is contained in:
parent
38a2f78647
commit
92ce4f9153
@ -1,5 +1,5 @@
|
||||
import type { DecodedIdToken } from "firebase-admin/auth";
|
||||
import { FieldValue } from "firebase-admin/firestore";
|
||||
import { FieldValue, Timestamp } from "firebase-admin/firestore";
|
||||
import { HttpError } from "../middleware/errors";
|
||||
import { auth } from "../firebase";
|
||||
import {
|
||||
@ -86,11 +86,25 @@ function providerFromToken(token: DecodedIdToken): Provider {
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 두 프로필 사진 URL이 실질적으로 동일한지 판정한다.
|
||||
*
|
||||
* Google 등 일부 제공자는 같은 사진에도 쿼리스트링(예: `=s96-c` 크기 파라미터)을
|
||||
* 매번 바꿔 내려준다. 쿼리스트링만 다른 경우는 동일 사진으로 보아, `getMe`(읽기 경로)가
|
||||
* 매 호출마다 photoUrl write를 유발하는 것을 막는다.
|
||||
*/
|
||||
function samePhotoUrl(a: string | undefined, b: string | undefined): boolean {
|
||||
if (a === b) return true;
|
||||
if (!a || !b) return false;
|
||||
return a.split("?")[0] === b.split("?")[0];
|
||||
}
|
||||
|
||||
/**
|
||||
* 현재 로그인한 유저 문서를 조회한다. 없으면 404 + `USER_NOT_FOUND`.
|
||||
*
|
||||
* 토큰의 `picture`가 저장된 `photoUrl`과 다르면 자동 동기화한다
|
||||
* (프로필 사진 변경 반영). 응답은 동기화된 값으로 반환한다.
|
||||
* 토큰의 `picture`가 저장된 `photoUrl`과 **실질적으로** 다를 때만 자동 동기화한다
|
||||
* (쿼리스트링만 다른 경우는 write하지 않음 — 읽기 경로의 불필요한 write 방지).
|
||||
* 응답은 동기화된 값으로 반환한다.
|
||||
*/
|
||||
export async function getMe(token: DecodedIdToken): Promise<UserProfile> {
|
||||
const user = await getUser(token.uid);
|
||||
@ -99,7 +113,7 @@ export async function getMe(token: DecodedIdToken): Promise<UserProfile> {
|
||||
}
|
||||
|
||||
const tokenPhoto = token.picture;
|
||||
if (tokenPhoto && tokenPhoto !== user.photoUrl) {
|
||||
if (tokenPhoto && !samePhotoUrl(tokenPhoto, user.photoUrl)) {
|
||||
await updateUser(token.uid, { photoUrl: tokenPhoto });
|
||||
user.photoUrl = tokenPhoto;
|
||||
}
|
||||
@ -159,11 +173,18 @@ export async function createMe(
|
||||
|
||||
await deleteReservation(token.uid, displayName);
|
||||
|
||||
const created = await getUser(token.uid);
|
||||
if (!created) {
|
||||
throw new HttpError(500, "failed to read created user");
|
||||
}
|
||||
return toUserProfile(created);
|
||||
// 방금 쓴 값으로 응답을 합성한다(쓰기 후 2차 getUser 제거). `createdAt`은 저장본이
|
||||
// serverTimestamp로 기록되므로 응답에는 근사치(now)를 싣는다 — 이후 getMe가 저장본 반영.
|
||||
const profile: UserProfile = {
|
||||
displayName,
|
||||
email: token.email,
|
||||
provider,
|
||||
knowledgeLevel,
|
||||
createdAt: Timestamp.now(),
|
||||
};
|
||||
if (token.picture) profile.photoUrl = token.picture;
|
||||
if (favoriteTeamCode) profile.favoriteTeamCode = favoriteTeamCode;
|
||||
return profile;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -266,8 +287,22 @@ export async function updateMe(
|
||||
|
||||
await updateUser(token.uid, patch);
|
||||
|
||||
const updated = await getUser(token.uid);
|
||||
return toUserProfile(updated!);
|
||||
// 쓰기 후 2차 getUser 제거: 시작 시 읽은 user에 방금 적용한 변경만 반영해 합성한다.
|
||||
const merged: User = { ...user };
|
||||
if (typeof patch.displayName === "string") {
|
||||
merged.displayName = patch.displayName;
|
||||
}
|
||||
if (body.favoriteTeamCode !== undefined) {
|
||||
if (body.favoriteTeamCode === null) {
|
||||
delete merged.favoriteTeamCode;
|
||||
} else {
|
||||
merged.favoriteTeamCode = patch.favoriteTeamCode as TeamCode;
|
||||
}
|
||||
}
|
||||
if (typeof patch.knowledgeLevel === "string") {
|
||||
merged.knowledgeLevel = patch.knowledgeLevel as KnowledgeLevel;
|
||||
}
|
||||
return toUserProfile(merged);
|
||||
}
|
||||
|
||||
const NOTIFICATION_KEYS = new Set<string>(Object.values(NotificationKey));
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user