도메인: Quota (주 AI 분석 한도)
룰
| 항목 | 값 |
|---|---|
| 주 무료 quota | 10 |
| 짧은 공시 임계값 | 1000자 미만 무료 노출 |
| Unavailable 임계 retry_count | 90 (FAILED 상태에서 도달 시 영구 부재) |
상수는 internal/ai/policy.go 의 DefaultRevealPolicy() 에 캡처. 이전 버전은 WEEKLY_FREE_QUOTA / SHORT_DOC_THRESHOLD 환경변수로 매 호출마다 hot-reload 가능했으나 prod 미사용으로 폐기 (2026-05-12). 정책 변형 필요 시 RevealPolicyFromEnv() 추가하거나 DefaultRevealPolicy() 의 값 수정.
가입 보너스는 V4 (2026-05-06) 에서 폐기됨. 첫 경험 절벽 (10 → 5) 거부감이 더 컸음.
정책 모듈 구조
RevealPolicy = QuotaPolicy + AnalysisPolicy. Classify 메서드가 분석 + quota 룰의 단일 진입점.
| 객체 | 책임 | 호출자 |
|---|---|---|
QuotaPolicy { WeeklyLimit } | 한도 + 주 경계 (CurrentWeekStart) + Remaining(Quota) | RevealService, QuotaRepository, admob.Handler |
AnalysisPolicy { ShortDocThreshold, UnavailableRetryThreshold } | IsShort(docLen), IsUnavailable(status, retryCount) | RevealService |
RevealPolicy { Quota, Analysis } | Classify(analysis, alreadyRevealed, quota) → Outcome | RevealService |
main.go 가 ai.DefaultRevealPolicy() 1회 생성 → 의존 모듈에 주입. admob 패키지는 revealPolicy.Quota 만 받아 SSV reward 시 quota_week_start 를 동일 룰로 set (이전엔 currentWeekStart 패키지 로컬 복제 — import cycle 회피용이었으나 deepening 후 ai → admob 단방향 import 로 해소).
비용 모델 (오해 방지)
LLM 호출은 공시당 1회 (스케줄러가 분석 시) — 사용자 reveal 은 analysis_cache 캐시 조회. 사용자 reveal 횟수는 LLM 비용과 무관. quota cap 의 목적은:
ad_free_passIAP funnel hook- 다중 계정 어뷰즈 방어 보조 (App Check + SSV 1차, cap 으로 ROI ↓)
- 인지 가치 — 무제한이면 "이 분석 가치 있나?" 신호 약화
상세 정책 결정 맥락: ../policies/monetization.md.
알고리즘
remaining = weekly_limit - used_count(weekly_limit = WEEKLY_FREE_QUOTA. used_count 는 음수 허용 — 광고 시청 시 -1.)
별도 ad_bonus 컬럼 없음. 광고 보상은 used_count -= 1 로 표현. ad_reward_transactions 테이블은 SSV idempotency (중복 콜백 차단) 전용 — quota 계산에 참조 안 함.
차감 결정은 internal/ai/reveal.go 의 Classify() 가 단일 진실:
analysis == nil또는status != COMPLETED→ Pendingis_short(doc_length < SHORT_DOC_THRESHOLD) → FreeRevealalready_revealed(user_revealed_disclosures에 row 있음) → FreeRevealremaining <= 0→ QuotaExhausted (429 QUOTA_EXHAUSTED+can_watch_ad: true)- 정상 → PaidReveal (
used_count +1+ reveal 기록, 단일 트랜잭션)
자세한 race-safe 트랜잭션 시퀀스: docs/policies/lazy-reveal.md.
Edge case
- 주 경계:
quota_week_start != current_week_start(월요일 KST) 이면used_count0 으로 리셋 (음수였어도 0) - 광고 시청:
used_count -= 1, 무제한.used_count=0인 상태에서 광고 보면 -1 되어 "선납" 처럼 작동하나, 주 reset 시 0 으로 초기화 — 주말에 광고 미리 봐두는 식 이월은 불가 (의도된 동작) - SSV 중복 콜백:
ad_reward_transactions.transaction_idPK 로 idempotency. 같은 transaction_id 두 번째 호출은 INSERT 충돌 → 보상 미지급 - 동시 reveal:
user_revealed_disclosuresINSERT ON CONFLICT + quota 차감 트랜잭션으로 중복 차감 방지
관련 테이블
dartbrief.user_analysis_quota— user_id, used_count, quota_week_startdartbrief.ad_reward_transactions— transaction_id (PK), user_id (idempotency 전용)dartbrief.user_revealed_disclosures— user_id, rcept_no, revealed_at
관련 API
GET /api/v1/quota→ quota.mdPOST /api/v1/ai/analyze/{rceptNo}/reveal→ ai-analyze-reveal.mdGET /api/v1/admob/ssv→ admob-ssv.md