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 응답 |
|---|---|---|---|
| Pending | status != COMPLETED (PENDING/PROCESSING/FAILED 모두 포함) | 202 PENDING | 409 NOT_READY |
| FreeReveal | is_short == true OR already_revealed == true | 200 + result 동봉 | 200 + result, 차감 X |
| PaidReveal | 일반 공시 + 미 reveal + quota 남음 | 200 + result null + 메타 | 200 + result, 차감 +1 |
| QuotaExhausted | 일반 공시 + 미 reveal + quota 0 | 200 + can_watch_ad: true | 429 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 /reveal 의 RevealService.Reveal():
analysisfetch (read-only)status != COMPLETED→Pending반환quotaRepo.GetOrInit()(트랜잭션 밖) — quota row materialize + 주 reset- 트랜잭션 시작
INSERT user_revealed_disclosures ON CONFLICT DO NOTHING→newlyRevealed판정alreadyRevealed || isShort→ FreeReveal fast-path COMMIT (SELECT FOR UPDATE 스킵)SELECT FOR UPDATEquota row (race-safe 잠금)Classify한 번 더 호출 (잠금 시점의 사실로 race-safe 재검증)PaidReveal→ UPDATE used_count+1 → COMMITQuotaExhausted→ 롤백 (INSERT 도 무효)
3 원칙 (향후 quota 의사결정 시 유지)
- 명시적 사용자 의도 — 차감은 사용자 액션 (버튼 탭) 직후만
- 서버 SoT —
user_revealed_disclosures테이블이 진실. 클라이언트 캐시 의존 X - 재진입 무료 — 같은 공시 두 번 보기는 추가 차감 없음
토론 우려 + 완화
| Voice | 우려 | 완화 |
|---|---|---|
| DEVIL | 사용자가 카운터 깎이는 걸 체감 → 인지 부담 ↑ | 무료 앱은 한도 체감이 본질 (PM 수용) |
| GROWTH | Reveal 버튼이 funnel 손실 | 짧은 공시 자동 reveal + 첫 1회 온보딩 안내 |
도입 PR
- 백엔드:
feature/be-0501-lazy-reveal(PR #30) - 모바일:
feature/mo-0501-quota-ui-v3(PR #31)