활동 보상 시스템 (Activity Reward System)
문서 정보
- 작성일: 2026-02-27
- 최종 업데이트: 2026-02-27
- 버전: v1.0.0
사용자의 활동(콕 시청, 운동 컨디션 기록, 광고 시청)에 대해 Rules Engine이 DB에 저장된 규칙을 평가하여 보상(리워드 적립, 카메라 잠금 해제)을 자동 지급하는 시스템입니다. 규칙 변경 시 코드 수정 없이 DB만 수정하면 됩니다.
목차
- 핵심 개념
- 활동 유형과 보상
- Rules Engine 아키텍처
- 자격 조건 (Eligibility)
- 제한 설정 (Limits)
- MembershipRewardCounter
- 활동별 상세 플로우
- Google AdMob SSV 연동
- 카메라 잠금 해제
- 데이터 모델
- FAQ
핵심 개념
한 줄 요약
사용자가 활동을 하면, Rules Engine이 규칙을 평가하여 보상을 지급한다.
핵심 용어
| 용어 | 설명 |
|---|---|
| 활동 (Activity) | 사용자의 행동. 콕 시청, 운동 컨디션 기록, 광고 시청 등 |
| 규칙 (Rule) | "누가, 무엇을 하면, 어떤 보상을, 얼마나 받을 수 있는지" 정의한 설정 |
| Rules Engine | 규칙을 평가하고 보상 지급을 결정하는 핵심 엔진 |
| 리워드 | 활동 보상으로 적립되는 포인트 (서비스 내 재화) |
| 카메라 잠금 해제 | FREE 사용자가 광고 시청 후 PLUS 전용 카메라에 접근하는 것 |
| 멱등성 (Idempotency) | 같은 활동을 여러 번 요청해도 보상이 한 번만 지급되는 안전장치 |
설계 철학
- 분리된 구조: 콕, 운동 컨디션 등 핵심 서비스는 보상 로직을 모릅니다. 이벤트만 발행하고, 보상 시스템이 처리합니다.
- 유연한 규칙: 자격 조건, 보상 금액, 제한 등을 코드 수정 없이 DB 설정으로 변경할 수 있습니다.
- 감사 추적: 모든 활동과 보상 결과가 기록되어 추적이 가능합니다.
활동 유형과 보상
시스템이 지원하는 4가지 활동과 해당 보상입니다.
| 활동 유형 | 트리거 | 보상 | 대상 | 제한 |
|---|---|---|---|---|
| 콕 시청 (KOK_WATCH) | 콕 시청 완료 시 | 리워드 5 적립 | PLUS 구독자 | 50% 확률, 구독 주기당 240회 |
| 운동 컨디션 (WORKOUT_CONDITION) | 컨디션 기록 시 | 리워드 5 적립 | PLUS 패스 보유자 | 스케줄당 1회 |
| 광고 시청 (AD_WATCH) | 광고 시청 완료 시 | 카메라 잠금 해제 | FREE 사용자 | 제한 없음 |
| Google 광고 (GOOGLE_AD) | Google 리워드 광고 시청 시 | 리워드 5 적립 | PLUS 구독자 | 일 50회, 쿨다운 7분 |
콕 시청 보상의 50% 확률은 콕 생성 시점에 미리 결정됩니다 (isPointEligible 필드). 시청 시점이 아닌 생성 시점에 결정하여, 어떤 콕이 보상 대상인지 사전에 공정하게 배분합니다.
Rules Engine 아키텍처
Rules Engine은 사용자 활동이 발생할 때 다음 순서로 보상 여부를 판별합니다.
각 단계 설명
| 단계 | 이름 | 설명 |
|---|---|---|
| 1 | 규칙 매칭 | 활동 유형에 맞는 활성화 된 규칙을 DB에서 조회합니다 |
| 2 | 멱등성 체크 | 동일 활동에 대한 중복 보상을 방지합니다 (멱등성 키 기반) |
| 3 | 자격 검증 | 사용자의 멤버십, 패스 타입이 규칙의 자격 조건을 만족하는지 확인합니다 |
| 4 | 제한 확인 | 일별, 구독 주기별 등 보상 횟수 제한을 초과하지 않았는지 확인합니다 |
| 5 | 확률 검사 | 확률 기반 보상인 경우 랜덤 체크를 수행합니다 (기본값 100%) |
| 6 | 보상 지급 | 리워드 적립 또는 카메라 잠금 해제를 실행합니다 |
보상이 지급되든 거부되든, 모든 결과는 ActivityRecord에 기록됩니다. 이를 통해 "왜 보상을 못 받았는지" 추적이 가능합니다.
자격 조건 (Eligibility)
규칙마다 누가 보상을 받을 수 있는지 자격 조건이 JSON으로 정의되어 있습니다.
자격 조건 필드
| 필드 | 설명 | 예시 |
|---|---|---|
membershipTypes | 필요한 멤버십 타입 | ["PLUS"] |
passTypes | 허용되는 패스 타입 | ["PLUS"] 또는 ["FREE"] |
requiresActiveSubscription | 보상 청구 시점에 활성 구독 필요 여부 | true / false |
규칙별 자격 조건
| 규칙 | 멤버십 필요? | 패스 타입 | 설명 |
|---|---|---|---|
| 콕 시청 리워드 | PLUS 구독 필수 | PLUS | 구독 중인 PLUS 멤버십 사용자만 |
| 운동 컨디션 리워드 | 불필요 | PLUS | PLUS 패스(단건 구매 포함) 보유자 |
| 광고 카메라 해제 | 불필요 | FREE | FREE 사용자만 (PLUS는 이미 전체 접근 가능) |
| Google 광고 리워드 | PLUS 구독 필수 | - | PLUS 구독 중인 사용자만 |
패스 타입별 접근 권한 요약
| 기능 | FREE | PLUS 패스 (단건) | PLUS 멤버십 (구독) |
|---|---|---|---|
| 카메라 접근 | 일부만 (광고 시청으로 해제) | 전체 | 전체 |
| 콕 시청 리워드 | X | X | O (50%, 240회/주기) |
| 운동 컨디션 리워드 | X | O | O |
| Google 광고 리워드 | X | X | O (일 50회) |
제한 설정 (Limits)
보상의 과도한 지급을 막기 위해 규칙별로 제한이 설정됩니다.
제한 유형
| 제한 유형 | 설명 | 사용 예시 |
|---|---|---|
daily | 하루 최대 보상 횟수 | Google 광고: 일 50회 |
monthly | 월 최대 보상 횟수 | (현재 미사용) |
perMembershipCycle | 구독 주기당 최대 보상 횟수 | 콕 시청: 주기당 240회 |
perSchedule | 스케줄당 최대 보상 횟수 | 운동 컨디션: 스케줄당 1회 |
cooldownSeconds | 보상 간 최소 대기 시간(초) | Google 광고: 420초 (7분) |
현재 규칙별 제한 설정
| 규칙 | 제한 | 값 |
|---|---|---|
| 콕 시청 리워드 | 구독 주기당 | 240회 |
| 운동 컨디션 리워드 | 스케줄당 | 1회 (멱등성으로 보장) |
| 광고 카메라 해제 | 없음 | - |
| Google 광고 리워드 | 일일 + 쿨다운 | 50회/일 + 420초 간격 |
MembershipRewardCounter
구독 주기(Billing Cycle) 기반으로 보상 횟수를 추적하는 카운터입니다. 콕 시청 리워드의 "주기당 240회" 제한을 관리합니다.
동작 방식
카운터 주요 필드
| 필드 | 설명 |
|---|---|
currentCount | 현재 주기 내 보상 횟수 |
maxCount | 최대 허용 횟수 (240) |
accumulatedAmount | 현재 주기 내 누적 리워드 금액 |
cycleStartDate / cycleEndDate | 구독 주기 시작/종료일 |
subscriptionId | 연결된 구독 ID |
주기 관리 규칙
- 새 구독 주기 시작 시:
currentCount와accumulatedAmount가 0으로 초기화됩니다 - 구독 환불 시: 카운터가 삭제됩니다 (Hard Delete)
- 카운터는 기존 레코드를 업데이트: 주기별로 새 레코드를 만들지 않고, 기존 레코드의 주기 정보를 갱신합니다
콕 시청 리워드 응답에는 cycleSummary가 포함됩니다. 여기에는 estimatedPayment (예상 결제 금액)이 있으며, 이는 구독 가격 - 적립된 리워드 잔액으로 계산됩니다. 사용자에게 "리워드를 더 모으면 결제 금액이 줄어든다"는 동기부여를 제공합니다.
활동별 상세 플로우
콕 시청 (KOK_WATCH) 리워드
PLUS 구독자가 콕을 시청 완료하면, 50% 확률로 5 리워드가 적립됩니다.
콕 시청의 특수성:
- 확률(50%)은 시청 시점이 아닌 콕 생성 시점에
isPointEligible플래그로 미리 결정됩니다 - 멱등성 키 패턴:
KOK_WATCH_{kokId}_{timestampId}_{userId} - 응답에
cycleSummary(주기 내 누적 현황, 예상 결제 금액)를 포함합니다
운동 컨디션 (WORKOUT_CONDITION) 리워드
PLUS 패스 보유자(구독 또는 단건 구매)가 운동 컨디션을 기록하면, 5 리워드가 적립됩니다.
운동 컨디션의 특수성:
- 멤버십(구독) 없이도 PLUS 패스(단건 구매)만 있으면 보상 가능
- 스케줄당 1회 제한은 멱등성 키로 보장 (별도 카운터 불필요)
- 멱등성 키 패턴:
WORKOUT_CONDITION_{workoutConditionId}_{userId} - 확률: 100% (항상 지급)
Google AdMob SSV 연동
PLUS 구독자가 Google 리워드 광고를 시청하면, 5 리워드가 적립됩니다. 광고 시청 검증은 Google이 직접 서버로 콜백을 보내는 SSV (Server-Side Verification) 방식을 사용합니다.
전체 흐름
SSV 서명 검증 과정
| 단계 | 설명 |
|---|---|
| 1 | Google 공개키를 조회합니다 (24시간 캐싱) |
| 2 | 쿼리스트링에서 &signature= 앞부분을 서명 대상 메시지로 추출합니다 |
| 3 | ECDSA SHA256 알고리즘으로 서명을 검증합니다 |
| 4 | custom_data에서 userUuid를 추출하여 사용자를 식별합니다 |
보상 정책
| 항목 | 값 |
|---|---|
| 대상 | PLUS 구독자 |
| 보상 | 5 리워드 |
| 일일 제한 | 50회 |
| 쿨다운 | 420초 (7분) |
| 멱등성 | transaction_id 기반 |
Grant Status 코드
| 상태 | 설명 |
|---|---|
GRANTED | 보상 지급 성공 |
ALREADY_PROCESSED | 이미 처리된 transaction_id |
NOT_PLUS_MEMBERSHIP | PLUS 구독이 아님 |
COOLDOWN_ACTIVE | 쿨다운 대기 중 |
DAILY_LIMIT_EXCEEDED | 일일 한도 초과 |
SSV_VERIFICATION_FAILED | 서명 검증 실패 |
RULE_NOT_FOUND | 규칙 미설정 |
보상 결과는 SSE (Server-Sent Events)로 클라이언트에 실시간 전달됩니다. activity-reward.google-ad.granted (성공) 또는 activity-reward.google-ad.failed (실패) 이벤트가 발행됩니다.
카메라 잠금 해제
FREE 사용자는 일부 카메라(PLUS 티어)가 잠겨 있습니다. 광고를 시청하면 해당 타임슬롯의 PLUS 카메라 전체가 잠금 해제됩니다.
해제 흐름
잠금 해제 범위 (Scope)
| 범위 | 설명 | 현재 사용 |
|---|---|---|
| TIMESLOT | 광고를 시청한 타임슬롯의 PLUS 카메라만 해제 | O (기본값) |
| SCHEDULE | 해당 스케줄의 모든 타임슬롯에서 PLUS 카메라 해제 | X (미사용) |
비디오 조회 시 접근 권한 판별
사용자가 비디오 목록을 조회할 때, 카메라별 접근 가능 여부가 결정됩니다.
| 사용자 타입 | 판별 방식 |
|---|---|
| PLUS 패스 | 모든 카메라 접근 가능 (추가 조회 없음) |
| FREE 패스 | FREE 티어 카메라는 항상 접근 가능. PLUS 티어는 CameraUnlockRecord 존재 여부로 판별 |
유효 기간
expiresAt이null이면 스케줄 종료 시까지 유효합니다- 타임슬롯 기반이므로 동일 스케줄의 다른 타임슬롯에서는 별도로 광고를 시청해야 합니다
데이터 모델
ERD
주요 JSON 스키마
자격 조건 (eligibility)
{
"membershipTypes": ["PLUS"],
"passTypes": ["PLUS"],
"requiresActiveSubscription": false
}
보상 설정 (rewardConfig)
// 리워드 적립 (POINT)
{ "type": "POINT", "amount": 5, "probability": 0.5 }
// 카메라 잠금 해제 (CAMERA_UNLOCK)
{ "type": "CAMERA_UNLOCK", "durationInSeconds": null, "scope": "TIMESLOT" }