- Firestore Timestamp가 res.json으로 그대로 나가 {_seconds,_nanoseconds}로 직렬화되던 문제 수정
- 앱의 포인트 내역(/reward/ledger)과 예측 기록(/stats/history) 크래시 원인 제거
- 클라 소비 7개 핸들러(reward/attendance/prediction/user/stats/kbo/chat)의 모든 응답에 명시적 DTO 타입 도입
- 날짜 와이어 형식을 순수 UTC ISO 8601(...Z)로 통일 — 채팅의 기존 KST(+09:00) 출력도 UTC로 전환
- DTO는 필드를 명시적으로 나열해 조립 (스프레드 덤프 제거) — 문서에 새 Timestamp 필드가 생겨도 다시 새지 않는다
- 재사용되는 주문 형태만 toOrderDto로 분리, 나머지는 응답 경계에서 직접 조립
- Firestore 저장 형식은 변경하지 않음. 출석 idempotent 재요청 경로는 저장된 Timestamp/문자열을 모두 처리
- DTO 직렬화 회귀 테스트 추가 (_seconds 부재, UTC ISO 형식, optional 키 생략, 미지 필드 차단)
79 lines
2.5 KiB
TypeScript
79 lines
2.5 KiB
TypeScript
import { onRequest } from "firebase-functions/https";
|
|
import { requireAuthToken } from "../middleware/auth";
|
|
import { sendError } from "../middleware/errors";
|
|
import {
|
|
checkNickname,
|
|
createMe,
|
|
deleteMe,
|
|
getMe,
|
|
updateMe,
|
|
updateNotifications,
|
|
} from "../services/userService";
|
|
import type {
|
|
MeResponseDto,
|
|
NotificationsResponseDto,
|
|
UserProfileDto,
|
|
} from "../types/dto/userDto";
|
|
|
|
/** `uid` + 프로필 DTO를 `MeResponseDto`로 조립한다. 필드를 하나하나 명시해 새 필드 누락을 방지한다. */
|
|
function toMeResponse(uid: string, profile: UserProfileDto): MeResponseDto {
|
|
return {
|
|
user: {
|
|
uid,
|
|
displayName: profile.displayName,
|
|
email: profile.email,
|
|
photoUrl: profile.photoUrl,
|
|
provider: profile.provider,
|
|
favoriteTeamCode: profile.favoriteTeamCode,
|
|
knowledgeLevel: profile.knowledgeLevel,
|
|
createdAt: profile.createdAt,
|
|
lastJudgedDate: profile.lastJudgedDate,
|
|
},
|
|
};
|
|
}
|
|
|
|
export const user = onRequest(async (req, res) => {
|
|
try {
|
|
if (req.method === "GET" && req.path === "/check-nickname") {
|
|
const token = await requireAuthToken(req);
|
|
const result = await checkNickname(token, req.query.displayName);
|
|
res.status(200).json(result);
|
|
return;
|
|
}
|
|
if (req.method === "GET" && req.path === "/") {
|
|
const token = await requireAuthToken(req);
|
|
const u = await getMe(token);
|
|
res.status(200).json(toMeResponse(token.uid, u));
|
|
return;
|
|
}
|
|
if (req.method === "POST" && req.path === "/") {
|
|
const token = await requireAuthToken(req);
|
|
const u = await createMe(token, req.body ?? {});
|
|
res.status(201).json(toMeResponse(token.uid, u));
|
|
return;
|
|
}
|
|
if (req.method === "PATCH" && req.path === "/") {
|
|
const token = await requireAuthToken(req);
|
|
const u = await updateMe(token, req.body ?? {});
|
|
res.status(200).json(toMeResponse(token.uid, u));
|
|
return;
|
|
}
|
|
if (req.method === "PATCH" && req.path === "/notifications") {
|
|
const token = await requireAuthToken(req);
|
|
const notifications = await updateNotifications(token, req.body ?? {});
|
|
const body: NotificationsResponseDto = { notifications };
|
|
res.status(200).json(body);
|
|
return;
|
|
}
|
|
if (req.method === "DELETE" && req.path === "/") {
|
|
const token = await requireAuthToken(req);
|
|
await deleteMe(token);
|
|
res.status(204).send();
|
|
return;
|
|
}
|
|
res.status(404).json({ error: "not found" });
|
|
} catch (err) {
|
|
sendError(res, err);
|
|
}
|
|
});
|