Skip to content

도메인: Quota (주 AI 분석 한도)

항목
주 무료 quota10
짧은 공시 임계값1000자 미만 무료 노출
Unavailable 임계 retry_count90 (FAILED 상태에서 도달 시 영구 부재)

주간 한도는 구독 티어로 결정 — 무료 10 / PRO 60 / MAX 200. WeeklyLimit 은 우회하는 게 아니라 weeklyLimit(activeTier) 매핑으로 상수만 바뀐다(used_count 는 티어 무관 기록). 티어/가격: domain-subscription. 한도는 종목당 ~1.3건/주 데이터로 책정(평상시 미도달, 피크·헤비유저 도달 → 상위 티어 유도).

상수는 internal/ai/policy.goDefaultRevealPolicy() 에 캡처. 이전 버전은 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 {}주 경계 (CurrentWeekStart) + Remaining(Quota, weeklyLimit) — 한도는 들고 있지 않고 인자로 받음RevealService, QuotaRepository, admob.Handler
AnalysisPolicy { ShortDocThreshold, UnavailableRetryThreshold }IsShort(docLen), IsUnavailable(status, retryCount)RevealService
RevealPolicy { Quota, Analysis }Classify(analysis, alreadyRevealed, quota, weeklyLimit) → OutcomeRevealService
entitlement.Tier티어 → 주간 한도(WeeklyLimit() 10/60/200) + 슬롯(SlotCapacity()) 매핑, ActiveTier(userID)RevealService, watchlist, ai.Handler

weekly_limit 은 정책 객체가 아니라 사용자 구독 티어로 결정 — RevealServiceentitlement.ActiveTier 로 resolve 해 Classify/Remaining 에 인자로 전달.

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 의 목적은:

  1. 구독 IAP funnel hook (주 한도 → 광고 또는 PRO/MAX 상위 티어)
  2. 다중 계정 어뷰즈 방어 보조 (App Check + SSV 1차, cap 으로 ROI ↓)
  3. 인지 가치 — 무제한이면 "이 분석 가치 있나?" 신호 약화. 티어 한도(10/60/200)도 평상시 미도달이나 천장이 존재

상세 정책 결정 맥락: ../policies/monetization.md.

알고리즘

remaining = weekly_limit - used_count

(weekly_limit = weeklyLimit(activeTier) — 무료 10 / PRO 60 / MAX 200. used_count 는 음수 허용 — 광고 시청 시 -1.)

별도 ad_bonus 컬럼 없음. 광고 보상은 used_count -= 1 로 표현. ad_reward_transactions 테이블은 SSV idempotency (중복 콜백 차단) 전용 — quota 계산에 참조 안 함.

차감 결정은 internal/ai/reveal.goClassify() 가 단일 진실:

  1. analysis == nil 또는 status != COMPLETEDPending
  2. is_short (doc_length < SHORT_DOC_THRESHOLD) → FreeReveal
  3. already_revealed (user_revealed_disclosures 에 row 있음) → FreeReveal
  4. remaining <= 0QuotaExhausted (429 QUOTA_EXHAUSTED + can_watch_ad: true)
  5. 정상 → PaidReveal (used_count +1 + reveal 기록, 단일 트랜잭션)

자세한 race-safe 트랜잭션 시퀀스: docs/policies/lazy-reveal.md.

Edge case

  • 주 경계: quota_week_start != current_week_start (월요일 KST) 이면 used_count 0 으로 리셋 (음수였어도 0)
  • 광고 시청: used_count -= 1, 무제한. used_count=0 인 상태에서 광고 보면 -1 되어 "선납" 처럼 작동하나, 주 reset 시 0 으로 초기화 — 주말에 광고 미리 봐두는 식 이월은 불가 (의도된 동작)
  • SSV 중복 콜백: ad_reward_transactions.transaction_id PK 로 idempotency. 같은 transaction_id 두 번째 호출은 INSERT 충돌 → 보상 미지급
  • 동시 reveal: user_revealed_disclosures INSERT ON CONFLICT + quota 차감 트랜잭션으로 중복 차감 방지

관련 테이블

  • dartbrief.user_analysis_quota — user_id, used_count, quota_week_start
  • dartbrief.ad_reward_transactions — transaction_id (PK), user_id (idempotency 전용)
  • dartbrief.user_revealed_disclosures — user_id, rcept_no, revealed_at

소진 시 funnel

주 한도 소진 후 분석 경로는 광고 시청(+1) 또는 상위 티어 구독 2갈래. 0회 소진 시 인디케이터 하단에 "구독하고 더 보기" CTA 버튼(quota-indicator) 노출 → 탭하면 페이월 하단 시트(source: quota_soldout). 광고 +1 은 티어 무관 항상 허용(can_watch_ad: true) — 소진 시 광고로 충전하거나 상위 티어로 한도 상향(무료→PRO/MAX, PRO→MAX) 또는 다음 주 reset. 티어 한도: domain-subscription.

관련 API