- 어드민 웹(fanit_admin) 지원: GET products/list(비노출 포함 전체 상품, AdminProductDto)·GET orders/list(전 유저 주문, createdAt desc 커서 페이지네이션) 추가 — Hosting rewrite·직접 호출 양쪽에서 일관되는 2세그먼트 라우트
- 지갑·원장 어드민 응답도 DTO로 감싸 Timestamp({_seconds}) 유출 제거
- admin 함수에 단일 origin CORS 헤지 추가(OPTIONS preflight를 requireAdmin보다 먼저 처리, 에러 응답에도 헤더 적용)
- scripts/set-admin-claim.ts로 운영자 admin 클레임 부여(부여 후 재로그인 필요)
- Storage 규칙: write만 admin 클레임으로 제한, read는 불변(앱 이미지 표시 계약 유지)
- 신규 조회·DTO vitest 테스트 추가
72 lines
3.1 KiB
TypeScript
72 lines
3.1 KiB
TypeScript
import { beforeEach, describe, expect, it } from "vitest";
|
|
import { Timestamp } from "firebase-admin/firestore";
|
|
import { firestore } from "../../src/firebase";
|
|
import { listAllOrders } from "../../src/repositories/orderRepository";
|
|
import type { OrderDoc } from "../../src/types/reward";
|
|
|
|
function makeOrder(uid: string, createdAt: Timestamp, overrides: Partial<OrderDoc> = {}): OrderDoc {
|
|
return {
|
|
uid,
|
|
items: [{ productId: "p1", qty: 1, pointPrice: 500, name: "굿즈" }],
|
|
totalPoints: 500,
|
|
recipient: { name: "홍길동", phone: "01000000000", address1: "서울", postalCode: "00000" },
|
|
status: "reserved",
|
|
clientIdempotencyKey: `key-${uid}-${createdAt.toMillis()}`,
|
|
reserveLedgerTxIds: [],
|
|
orderedAt: createdAt,
|
|
statusHistory: [{ status: "reserved", at: createdAt, actor: uid }],
|
|
createdAt,
|
|
updatedAt: createdAt,
|
|
...overrides,
|
|
};
|
|
}
|
|
|
|
describe("orderRepository.listAllOrders", () => {
|
|
beforeEach(async () => {
|
|
await firestore.recursiveDelete(firestore.collection("orders"));
|
|
});
|
|
|
|
it("uid 필터 없이 서로 다른 유저의 주문을 모두 createdAt desc로 반환한다", async () => {
|
|
const t1 = Timestamp.fromDate(new Date("2026-07-01T00:00:00.000Z"));
|
|
const t2 = Timestamp.fromDate(new Date("2026-07-02T00:00:00.000Z"));
|
|
const t3 = Timestamp.fromDate(new Date("2026-07-03T00:00:00.000Z"));
|
|
|
|
await firestore.doc("orders/o1").set(makeOrder("uid-a", t1));
|
|
await firestore.doc("orders/o2").set(makeOrder("uid-b", t2));
|
|
await firestore.doc("orders/o3").set(makeOrder("uid-a", t3));
|
|
|
|
const page = await listAllOrders(20);
|
|
expect(page.items.map((o) => o.id)).toEqual(["o3", "o2", "o1"]);
|
|
expect(page.items.map((o) => o.uid).sort()).toEqual(["uid-a", "uid-a", "uid-b"].sort());
|
|
});
|
|
|
|
it("cursor 기반 페이지네이션이 동작한다", async () => {
|
|
const t1 = Timestamp.fromDate(new Date("2026-07-01T00:00:00.000Z"));
|
|
const t2 = Timestamp.fromDate(new Date("2026-07-02T00:00:00.000Z"));
|
|
const t3 = Timestamp.fromDate(new Date("2026-07-03T00:00:00.000Z"));
|
|
|
|
await firestore.doc("orders/o1").set(makeOrder("uid-a", t1));
|
|
await firestore.doc("orders/o2").set(makeOrder("uid-b", t2));
|
|
await firestore.doc("orders/o3").set(makeOrder("uid-a", t3));
|
|
|
|
const firstPage = await listAllOrders(2);
|
|
expect(firstPage.items.map((o) => o.id)).toEqual(["o3", "o2"]);
|
|
expect(firstPage.cursor).toBe("o2");
|
|
|
|
const secondPage = await listAllOrders(2, firstPage.cursor ?? undefined);
|
|
expect(secondPage.items.map((o) => o.id)).toEqual(["o1"]);
|
|
expect(secondPage.cursor).toBe("o1");
|
|
});
|
|
|
|
it("status 필터를 걸면 해당 상태 주문만 반환한다 (기존 status+createdAt 인덱스 재사용)", async () => {
|
|
const t1 = Timestamp.fromDate(new Date("2026-07-01T00:00:00.000Z"));
|
|
const t2 = Timestamp.fromDate(new Date("2026-07-02T00:00:00.000Z"));
|
|
|
|
await firestore.doc("orders/o1").set(makeOrder("uid-a", t1, { status: "reserved" }));
|
|
await firestore.doc("orders/o2").set(makeOrder("uid-b", t2, { status: "confirmed" }));
|
|
|
|
const page = await listAllOrders(20, undefined, "confirmed");
|
|
expect(page.items.map((o) => o.id)).toEqual(["o2"]);
|
|
});
|
|
});
|