From a74edaef6b3c9670131aeee0f6411d555e669a03 Mon Sep 17 00:00:00 2001 From: Sn-Kinos Date: Thu, 8 Oct 2026 18:51:18 +0900 Subject: [PATCH] =?UTF-8?q?=EC=8B=9C=EC=B2=AD=20=EA=B2=80=EC=A6=9D=20?= =?UTF-8?q?=EC=BD=9C=EB=B0=B1=EC=9D=B4=20=EC=97=86=EB=8A=94=20=ED=85=8C?= =?UTF-8?q?=EC=8A=A4=ED=8A=B8=20=EA=B4=91=EA=B3=A0=20=EB=8B=A8=EC=9C=84?= =?UTF-8?q?=EC=97=90=EC=84=9C=20=EA=B4=91=EA=B3=A0=20=EC=B6=94=EA=B0=80=20?= =?UTF-8?q?=EA=B8=B0=EC=97=AC=20POP=20=EC=A7=80=EA=B8=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/firebase/functions/src/quest/quest.ts | 18 +++++++++++++--- app/firebase/functions/test/quest_e2e.test.ts | 21 +++++++++++++++++++ 2 files changed, 36 insertions(+), 3 deletions(-) diff --git a/app/firebase/functions/src/quest/quest.ts b/app/firebase/functions/src/quest/quest.ts index ca02366..868356b 100644 --- a/app/firebase/functions/src/quest/quest.ts +++ b/app/firebase/functions/src/quest/quest.ts @@ -491,9 +491,21 @@ export const fnAdReward = onCall(async (req) => { // 시청 증거 — Google 이 서버로 보낸 SSV 콜백(서명 검증 통과분)만 인정한다. const ssv = await db.collection('adSsv') .where('uid', '==', uid).where('baseTxId', '==', baseTxId).limit(1).get(); - if (ssv.empty) fail(ERR.adNotVerified, '광고 시청이 아직 확인되지 않았습니다.'); - const impressionId = ssv.docs[0]!.id; - const network = (ssv.docs[0]!.get('adNetwork') as string | undefined) || 'admob'; + let impressionId: string; + let network: string; + if (!ssv.empty) { + impressionId = ssv.docs[0]!.id; + network = (ssv.docs[0]!.get('adNetwork') as string | undefined) || 'admob'; + } else if (process.env.AD_REWARD_TEST_UNIT === 'true') { + // Google 공개 **테스트 광고 단위**에는 우리 SSV URL 을 걸 수 없어 콜백이 오지 않는다(r11 3-6 — + // fn-ad-ssv 호출 0건). 앱은 끝까지 본 광고(onUserEarnedReward)에만 이 함수를 부르므로, 테스트 단위를 + // 쓰는 서버에서만 그 호출을 시청 완료로 받는다. 서버 .env(개발 서버만)로 켜며 앱이 바꿀 수 없다. + // ponytail: 발주사 AdMob 실 광고 단위 + SSV URL 등록 뒤 이 값을 끄면 SSV 만 인정한다. + impressionId = `test-${baseTxId}`; + network = 'admob-test'; + } else { + fail(ERR.adNotVerified, '광고 시청이 아직 확인되지 않았습니다.'); + } const logRef = db.doc(`adRewardLogs/${impressionId}`); const quest = (await db.doc(`quests/${questId}`).get()).data() ?? {}; diff --git a/app/firebase/functions/test/quest_e2e.test.ts b/app/firebase/functions/test/quest_e2e.test.ts index 3f66098..0b9d56b 100644 --- a/app/firebase/functions/test/quest_e2e.test.ts +++ b/app/firebase/functions/test/quest_e2e.test.ts @@ -370,6 +370,27 @@ describe('클라이언트가 고른 ID 로 한도를 다시 열 수 없다 (리 expect((await ad(baseTxId)).rewarded).toBe(10); }); + it('광고: 테스트 광고 단위(샌드박스)는 SSV 콜백이 오지 않으므로 끝까지 본 호출에 증분을 준다 (r11 3-6)', async () => { + // Google 공개 테스트 광고 단위에는 우리 SSV URL 을 걸 수 없어 fn-ad-ssv 가 한 번도 불리지 않는다. + // 개발 서버(.env.fanuzu-dev-b020d)만 켜는 값이다 — 꺼져 있으면 위 시험대로 E-AD-03. + process.env.AD_REWARD_TEST_UNIT = 'true'; + try { + const baseTxId = await baseReward(); + const reply = await ad(baseTxId); + expect(reply.rewarded).toBe(10); + const row = only(await adRows()); + expect(row.get('popType')).toBe('support'); + const log = await db().doc(`adRewardLogs/test-${baseTxId}`).get(); + expect(log.get('network')).toBe('admob-test'); + expect((await db().doc(`users/${UID}/questProgress/ad`).get()).get('count')).toBe(1); + // 같은 기본 보상에 두 번은 없다. + expect((await ad(baseTxId)).duplicate).toBe(true); + expect((await adRows()).length).toBe(1); + } finally { + delete process.env.AD_REWARD_TEST_UNIT; + } + }); + it('광고: 동시에 불러도 한 번만 지급하고, 재전송은 첫 결과를 돌려준다', async () => { const baseTxId = await baseReward(); await ssv(baseTxId, 'ssv-a');