- 상품 카탈로그(products/) 신설: 공개 필드(name·pointPrice·이미지)와 재고(total/reserved/safety)를 한 문서로 관리, 목록·상세는 60초 인메모리 캐시로 서빙 - 주문 생성 시 포인트 예약(order_reserve)과 재고 예약을 단일 트랜잭션으로 원자 처리, 주문 ID를 uid_클라이언트멱등키로 고정해 더블탭·재시도 중복 생성 차단 - 주문 상태 머신(reserved→confirmed→preparing→shipped→delivered / cancelled / refunded) 전이 검증과 전이별 capture·release·refund 역거래 원장 기록 - reward HTTP 함수 신설: 지갑·원장 조회, 상품 목록·상세, 교환 자격, 주문 생성·목록·상세·취소 API - 관리자 라우트 추가: 상품 등록·수정, 재고 조정(가용재고 음수 방지), 주문 상태 변경, 포인트 지급·회수 - Firestore 보안규칙(지갑 본인 read, 상품·주문은 API 전용)과 orders 복합 인덱스(uid+createdAt, status+createdAt) 추가 - 상품 4종 시드(seed:rewards)와 상세 이미지 업로드(upload:reward-assets) 스크립트 추가
38 lines
1.3 KiB
TypeScript
38 lines
1.3 KiB
TypeScript
import "./_bootstrap";
|
|
import { Timestamp } from "firebase-admin/firestore";
|
|
import { firestore } from "../src/firebase";
|
|
|
|
const products = [
|
|
{ id: "acrylic-keyring", name: "아크릴 키링", pointPrice: 3000, displayOrder: 1 },
|
|
{ id: "magsafe-tok", name: "맥세이프 톡", pointPrice: 6500, displayOrder: 2 },
|
|
{ id: "keyring-doll", name: "키링 인형", pointPrice: 11000, displayOrder: 3 },
|
|
{ id: "crossbag", name: "크로스백", pointPrice: 15000, displayOrder: 4 },
|
|
] as const;
|
|
|
|
async function main() {
|
|
const now = Timestamp.now();
|
|
for (const p of products) {
|
|
const product = firestore.doc(`products/${p.id}`);
|
|
const old = await product.get();
|
|
const existing = old.data();
|
|
const draft = old.exists ? {} : {
|
|
active: false,
|
|
redeemable: false,
|
|
mainImages: [],
|
|
detailImages: [],
|
|
};
|
|
await product.set({
|
|
name: p.name,
|
|
pointPrice: p.pointPrice,
|
|
displayOrder: p.displayOrder,
|
|
totalStock: existing?.totalStock ?? 0,
|
|
reservedStock: existing?.reservedStock ?? 0,
|
|
safetyStock: existing?.safetyStock ?? 0,
|
|
...draft,
|
|
createdAt: existing?.createdAt ?? now,
|
|
updatedAt: now,
|
|
}, { merge: true });
|
|
}
|
|
}
|
|
main().then(() => console.log(`seeded ${products.length} reward products`)).catch((e) => { console.error(e); process.exitCode = 1; });
|