Skip to content

도메인: AdMob (광고 보상)

V4 (2026-05-06) 변경: ad_bonus 컬럼 폐기. 광고 보상은 user_analysis_quota.used_count -= 1 (음수 허용) 로 표현. ad_reward_transactions 테이블은 SSV 중복 콜백 차단 (idempotency) 전용. 월 하드캡 등 ad_bonus 관련 항목 (이 문서 V3 시점) 은 모두 무효. 자세한 결정: docs/policies/quota-ad-v3.md §V4

개요

광고 시청 완료 후 AI 분석 capacity 를 +1 지급한다 (used_count -= 1). prod 환경에서는 Google AdMob Server-Side Verification(SSV) 콜백을 통해서만 보상이 이루어진다. legacy 클라이언트 claim 경로(POST /quota/ad-reward)는 local/dev 환경 전용이다.

데이터 모델 (핵심 테이블)

dartbrief.ad_reward_transactions

컬럼타입설명
transaction_idTEXT PKGoogle이 발급한 고유 트랜잭션 ID
user_idBIGINT보상 받은 사용자 ID
created_atTIMESTAMPTZ처리 시각

인덱스: user_id

PK(transaction_id)가 멱등성 보장의 핵심 — 같은 트랜잭션 재처리 시 INSERT ON CONFLICT DO NOTHING으로 skip.

dartbrief.user_analysis_quota.ad_bonus

컬럼설명
ad_bonus이번 달 광고 보상 누적 횟수 (월 초 reset)

동작 플로우

SSV 콜백 처리 (prod 경로)

mermaid
sequenceDiagram
    participant App as Flutter 앱
    participant AdMob as Google AdMob
    participant BE as 백엔드 /api/v1/admob/ssv

    App->>AdMob: 광고 요청 (ServerSideVerificationOptions.userId = userID)
    AdMob-->>App: 광고 노출

    App->>AdMob: 광고 시청 완료
    AdMob->>BE: GET /api/v1/admob/ssv?<br/>transaction_id=...&user_id=...&<br/>key_id=...&signature=...&...
    Note over BE: Rate limit 제외 경로<br/>(Google 공유 egress IP)

    BE->>BE: ECDSA-SHA256 서명 검증<br/>(signature, key_id 제거한 query string으로 signed content 구성)
    alt 서명 검증 실패
        BE-->>AdMob: 401 Unauthorized
    else user_id 파싱 실패
        BE-->>AdMob: 200 OK (Google 재시도 방지)
    end

    BE->>BE: tx 실행:<br/>1. INSERT ad_reward_transactions ON CONFLICT DO NOTHING<br/>2. RowsAffected==0 이면 rollback + 200 (중복)<br/>3. 신규면 ai.QuotaRepository.DecrementUsedTx (used_count -= 1)<br/>4. commit
    alt DB 에러
        BE-->>AdMob: 500 Internal Error (Google이 재시도)
    else 정상
        BE-->>AdMob: 200 OK
    end

서명 검증 상세

mermaid
flowchart TD
    A[rawQuery 수신] --> B[signature, key_id 파라미터 제거<br/>원래 순서 유지]
    B --> C[SHA-256 해시]
    C --> D[KeyCache.Get key_id<br/>Google 공개키 캐시 조회]
    D --> E[ECDSA DER 서명 디코딩<br/>base64url 또는 base64url-nopadding]
    E --> F{ecdsa.Verify?}
    F -- true --> G[검증 성공]
    F -- false --> H[401 반환]

dev/local 경로 (legacy)

mermaid
flowchart TD
    A[POST /api/v1/quota/ad-reward] --> B{APP_ENV?}
    B -- local 또는 dev --> C[AddAdBonusUpToCap userID, cap=200]
    B -- 그 외 빈 문자열 포함 --> D[404 Not Found]
    C --> E{ad_bonus >= 200?}
    E -- yes --> F[429 AD_LIMIT_EXCEEDED]
    E -- no --> G[ad_bonus +1 → quotaResponse 반환]

룰 / 정책

항목환경변수
월 ad_bonus 하드캡200AD_BONUS_MONTHLY_CAP
prod 보상 경로SSV 콜백만
dev/local 보상 경로POST /api/v1/quota/ad-rewardAPP_ENV=local 또는 dev
SSV Rate Limit제외 (Google 공유 IP)
Google 공개키 캐시KeyCache (만료 정책은 keycache.go 참조)
멱등성 키transaction_id (PK)

월 reset 연동

ad_bonus 컬럼은 user_analysis_quota 테이블에 있다. SSV 처리의 원자 CTE에서 quota_month != current_month이면 ad_bonus = 1로 reset 후 적립 (월 변경 감지).

adHardCap 동기화

SSV 핸들러의 adHardCap()과 ai 핸들러의 adBonusMonthlyHardCap()이 동일하게 AD_BONUS_MONTHLY_CAP 환경변수를 읽는다. 두 경로가 같은 정책 적용.

Edge case

케이스동작
Google SSV 재시도 (동일 transaction_id)ad_reward_transactions PK 충돌 → INSERT DO NOTHING → newly_recorded=0 → 200 반환 (보너스 지급 skip)
월 하드캡 도달add_bonus WHERE 조건 ad_bonus < $4 불충족 → UPDATE 0 rows → bonus_added=0 → 200 반환 (지급 없이 성공)
DB 에러 시 SSV500 반환 → Google이 일정 횟수 재시도 → 복구 후 transaction_id로 멱등 처리
user_id 파싱 불가200 반환 (Google 재시도 방지) — 이미 서명 검증은 통과한 상태
APP_ENV 미설정빈 문자열 → local/dev 조건 미충족 → /quota/ad-reward 404 (fail-closed)
동시 SSV 요청 (같은 user)두 요청이 다른 transaction_id 면 각자 tx 안에서 직렬 atomic 차감 — used_count = 누적 -2. 같은 transaction_id 면 한쪽만 INSERT 성공, 다른쪽 RowsAffected=0 → quota 미변경

관련 API

엔드포인트설명인증
GET /api/v1/admob/ssvGoogle SSV 콜백 (public, ECDSA 검증)불필요
POST /api/v1/quota/ad-rewardlegacy dev 경로 (local/dev only)필요 (JWT)
GET /api/v1/quotaquota 현황 조회필요 (JWT)

관련 문서