Skip to content

Lazy Reveal 모델

AI 분석 결과 자동 노출 → 명시적 "보기" 버튼 reveal 로 전환 (2026-04-30 패널 합의).

Why

  • 사용자 신뢰: 상세 진입만으로 차감되는 기존 구조는 MAU 늘면 "왜 안 본 분석에 차감?" 컴플레인 폭발 위험
  • Quota 정합성: 캐시 무효화 의존 → 서버 측 user_revealed_disclosures 테이블로 단단한 SoT
  • 수익 모델 가치 인식: 사용자가 quota 소비 행위를 의식하면 무료 가치 인지 → IAP / 광고 전환 ↑

핵심 설계

단일 정책 결정 모듈 (internal/ai/reveal.go)

GET (analyze) 와 POST (reveal) 는 같은 정책 결정 (Classify) 의 두 표현. 한 결정이 4 outcome 중 하나로 분기:

Outcome조건analyze 응답reveal 응답
Pendingstatus != COMPLETED (PENDING/PROCESSING/FAILED 모두 포함)202 PENDING409 NOT_READY
FreeRevealis_short == true OR already_revealed == true200 + result 동봉200 + result, 차감 X
PaidReveal일반 공시 + 미 reveal + quota 남음200 + result null + 메타200 + result, 차감 +1
QuotaExhausted일반 공시 + 미 reveal + quota 0200 + can_watch_ad: true429 QUOTA_EXHAUSTED

FAILED status 도 사용자 시점에서 Pending 으로 통일 (DB 컬럼은 운영 모니터링용 유지).

API 분리

엔드포인트역할차감
GET /api/v1/ai/analyze/{rceptNo}메타 + (조건부) result. Inspect() 호출X
POST /api/v1/ai/analyze/{rceptNo}/reveal결과 + 차감. Reveal() 호출O (조건부)

Reveal 트랜잭션 — race-safe 재검증

POST /revealRevealService.Reveal():

  1. analysis fetch (read-only)
  2. status != COMPLETEDPending 반환
  3. quotaRepo.GetOrInit() (트랜잭션 밖) — quota row materialize + 주 reset
  4. 트랜잭션 시작
  5. INSERT user_revealed_disclosures ON CONFLICT DO NOTHINGnewlyRevealed 판정
  6. alreadyRevealed || isShortFreeReveal fast-path COMMIT (SELECT FOR UPDATE 스킵)
  7. SELECT FOR UPDATE quota row (race-safe 잠금)
  8. Classify 한 번 더 호출 (잠금 시점의 사실로 race-safe 재검증)
    • PaidReveal → UPDATE used_count+1 → COMMIT
    • QuotaExhausted → 롤백 (INSERT 도 무효)

3 원칙 (향후 quota 의사결정 시 유지)

  1. 명시적 사용자 의도 — 차감은 사용자 액션 (버튼 탭) 직후만
  2. 서버 SoTuser_revealed_disclosures 테이블이 진실. 클라이언트 캐시 의존 X
  3. 재진입 무료 — 같은 공시 두 번 보기는 추가 차감 없음

토론 우려 + 완화

Voice우려완화
DEVIL사용자가 카운터 깎이는 걸 체감 → 인지 부담 ↑무료 앱은 한도 체감이 본질 (PM 수용)
GROWTHReveal 버튼이 funnel 손실짧은 공시 자동 reveal + 첫 1회 온보딩 안내

도입 PR

  • 백엔드: feature/be-0501-lazy-reveal (PR #30)
  • 모바일: feature/mo-0501-quota-ui-v3 (PR #31)