캠페인 성공 알림 · 운영 퀘스트 참여 인원 · Music Shake 하루 1회 · 함수 배포 전 빌드

This commit is contained in:
Sn-Kinos
2026-10-08 16:01:12 +09:00
parent 93150a0d5b
commit f7075f0e6f
14 changed files with 243 additions and 18 deletions
@@ -113,10 +113,39 @@ export async function settleDueCampaigns(now: Timestamp = Timestamp.now()): Prom
if (success === null) continue;
settled += 1;
logger.info('[campaign] 정산', { id: doc.id, success });
if (success) await notifySuccess(doc.id);
}
return settled;
}
/**
* 성공 알림 — B-3 8-10 「캠페인 성공 Push」. 개설자 1건 · 후원자(고유 uid) 1건씩. 알림함에 쌓이면
* `fnPushInbox` 가 같은 문구로 Push 를 보낸다(`kind = campaign` → 앱 C-78 「결과」 단계).
* 문서 ID 가 캠페인마다 고정이라 다시 불러도 한 사람에게 한 건이다.
* ponytail: 판정 트랜잭션과 알림 사이에 함수가 죽으면 그 회차 알림은 빠진다(상태가 이미 success).
* 빠짐이 문제가 되면 `successNotifiedAt` 표식 + 재시도 패스로.
*/
export async function notifySuccess(campaignId: string): Promise<number> {
const ref = getFirestore().doc(`campaigns/${campaignId}`);
const [campaign, donations] = await Promise.all([ref.get(), ref.collection('donations').get()]);
const title = (campaign.get('title') as string | undefined) ?? '캠페인';
const creator = campaign.get('creatorUid') as string | undefined;
const donors = new Set(donations.docs
.map((d) => d.get('uid') as string | undefined)
.filter((u): u is string => !!u && u !== creator));
if (creator) {
await notifyOnce(creator, `campaignSuccess_${campaignId}`, {
title: `개설하신 「${title}」 캠페인이 목표를 달성했습니다`, campaignId,
});
}
for (const donor of donors) {
await notifyOnce(donor, `campaignSuccess_${campaignId}`, {
title: `후원하신 「${title}」 캠페인이 목표를 달성했습니다`, campaignId,
});
}
return donors.size + (creator ? 1 : 0);
}
/**
* 환불이 끝나지 않은 실패 캠페인을 돌린다 — 방금 실패로 판정된 건도 여기서 첫 환불을 받는다.
* 자동 시도 상한을 넘긴 건은 Admin 큐에 있으므로 건드리지 않는다. 실제로 돈 건수를 돌려준다.