구현 원칙 — 반올림은 basePang에 한 번, bonusPang에 한 번. floor(base × (1+rate)) 가 아니라 base + floor(base × rate) 로 계산한다. 이유: 결과 화면에 "기본 15팡 + 보너스 1팡"을 정확히 분리 표기해야 하는데, 전자는 두 값의 합이 총액과 어긋나는 케이스가 생긴다.
3.3 예시 (평균 당첨가 3,000원 가정)
레벨
divisor
기본 팡
+10%
+15%
+20%
Lv.1
200
15
15+1=16
15+2=17
15+3=18
Lv.3
180
16
16+1=17
16+2=18
16+3=19
Lv.5
160
18
18+1=19
18+2=20
18+3=21
Lv.10
110
27
27+2=29
27+4=31
27+5=32
3.4 팡 유형 및 유효기간
보너스 팡은 이벤트 팡으로 적립 → 유효기간 30일, 사용 시 이벤트 팡 우선 차감.
기본 팡의 기존 유형 정책은 변경하지 않는다.
팡 원장에는 기본분과 보너스분을 별도 트랜잭션으로 2건 기록한다 → 정산·통계에서 보너스 비용을 독립 집계 가능. reason: FREE_BOX_REWARD / reason: ATTENDANCE_BONUS
보너스가 0팡인 경우 트랜잭션 미생성. 예: basePang이 5팡인데 +10% → floor(0.5)=0. 이때 0팡 적립 레코드를 만들지 말고, UI에서도 보너스 라인을 숨긴다(또는 "다음 단계에서 더 받아요" 문구로 대체).
4. 비용 영향 시뮬레이션얼마나 드는가
4.1 전제
변수
값
근거
무료박스 1회 평균 팡
20팡
운영 실데이터(174회/3,480팡)
팡 1단위 실원가
0.6원
박스 평균 상품원가 600원 ÷ 1,000팡
Redemption Rate
50%
30일 만료 + 1,000팡 단위 소비
대상 DAU
10,000
매체사 5개사 합산 준비 물량
무료박스 참여율
60%
보수적 가정 → 6,000회/일
4.2 연속일 분포 가정과 가중평균 보너스율
구간
유저 비중
보너스율
기여
1일차
40%
0%
0.000
2~3일
25%
10%
0.025
4~6일
15%
15%
0.023
7일 이상
20%
20%
0.040
가중평균
100%
—
약 8.8%
4.3 결과
항목
일
월(30일)
추가 지급 팡 (6,000회 × 20팡 × 8.8%)
약 10,600팡
약 317,000팡
장부 원가 (×0.6원)
약 6,300원
약 190,000원
실비용 (redemption 50% 반영)
약 3,200원
약 95,000원
결론: DAU 1만 기준 월 약 9.5만원. 무료박스 사용률이 10%p만 올라도 회수되는 수준. 리텐션 실험 중 가장 비용 효율이 높은 축에 속한다. 단, 위 값은 redemption 50% 가정에 민감하므로 도입 1개월 후 실측으로 갱신할 것.
💡 30일 소멸은 위 실비용에 이미 반영되어 있다. 보너스 팡은 이벤트 팡(유효기간 30일 · 사용 시 우선 차감, 3.4)이므로, 실비용 = 장부원가 × redemption 50%의 그 50%가 곧 "30일 안에 실제로 쓰인 분"이고 나머지 ≈ 소멸·미사용분이다.
더 나아가 순수 무료 이용자는 하루 약 20팡 × 30일 ≈ 600팡 수준에서 소멸이 돌아 유료박스 문턱 1,000팡을 혼자서는 넘지 못한다. 즉 보너스 팡은 유료 구매·캐시백 등 다른 팡 유입이 있는 유저에서만 우선 차감되어 소진되고, 그 외에는 소멸한다. 따라서 실제 redemption은 50%보다 낮을 가능성이 크며, 위 실비용(월 약 9.5만원)은 보수적 상한으로 보는 것이 타당하다. 도입 후 보너스 팡 소멸률·소진률을 실측해 redemption을 재설정한다.
4.4 상한 방어
보너스율 상한 +20% 하드캡 — 정책값이 잘못 입력돼도 코드에서 min(rate, 0.20) 로 이중 방어.
1회 보너스 팡 절대 상한 100팡 — 고가 상품 당첨(150만원 × Lv10 = 13,636팡) 시 보너스만 2,727팡이 나가는 것을 차단. 확정 필요
5. UI 설계 — 배치 맵어느 화면 어디에 넣는가
#
화면
위치
컴포넌트
노출 조건
우선순위
1
홈
메인 히어로 배너 바로 아래, "New!! 새로운 상품" 위
연속 출석 위젯 (7칸 트래커)
로그인 유저 상시
P0
2
전 화면
화면 하단 (bottom nav 위 80px)
자동 출석 토스트
그날 첫 접속 1회
P0
3
무료박스 오픈 결과
팡 지급 표기 영역
기본/보너스 분리 표기
보너스 > 0일 때
P0
4
무료박스 진입 모달
모달 상단
"+10% 적용 중" 배지
보너스 > 0일 때
P1
5
내 정보
EXP 바 아래, 보유 팡 카드 위
연속 출석 요약 행
로그인 유저 상시
P1
6
홈 (비회원)
1번 위젯 자리
로그인 유도 위젯
쿠키 세션 비회원
P1
7
이벤트
이벤트 목록 상단 고정
규칙 안내 페이지
상시
P1
배치 근거 — 왜 홈 히어로 배너 "아래"인가
현재 홈은 상단 히어로 배너(금 한돈 등 프로모션) → New 상품 → 상품목록 순서다. 히어로 배너는 신규 유저 후킹용이라 교체가 잦으므로 위젯을 그 위에 두면 프로모션 임팩트를 죽인다. 반대로 상품목록 아래로 내리면 스크롤 없이 안 보인다. 히어로 직하단이 "재방문 유저가 앱 열자마자 0.5초 안에 보는" 유일한 고정 슬롯이다.
화면 흐름
앱 진입
│
├─▶ [자동] POST /api/attendance/checkin
│ │
│ ├─ isNew = true ──▶ ② 토스트 노출 (3초) ──▶ ① 홈 위젯 갱신(카운트업 애니메이션)
│ └─ isNew = false ──▶ ① 홈 위젯만 현재 상태로 렌더
│
└─▶ 무료 럭키박스 진입
│
├─▶ ④ 진입 모달 상단 "연속 3일 · 팡 +10% 적용 중" 배지
│
└─▶ 박스 오픈 (서버가 보너스 재계산)
│
└─▶ ③ 결과 화면
기본 15팡
+ 보너스 1팡 (연속 3일 +10%) ← 오렌지 강조 + 카운트업
─────────────
총 16팡 획득!
"하루 더 오면 +15%!" CTA
6. UI 설계 — 화면별 시안디자인팀 전달용 · 실제 앱 스타일 기준
1 홈 — 연속 출석 위젯 (P0)
‹
120팡Lv.1🔔👤
럭키박스 열고 금 한돈 받기히어로 배너 (기존)
🔥 3일 연속 출석 중!
팡 +10%
1
2
3
4
5
6
7
내일도 오면 4일차 · 팡 +15% 로 올라가요
New!! 새로운 상품
홈
전당
LUCKY!
룰렛
보관함
구성 ① 좌: 불꽃 이모지 + 연속일 강조 ② 우: 현재 보너스 배지(오렌지 pill) ③ 7칸 도트 트래커 ④ 다음 tier 안내 1줄 도트 상태 채움(오렌지) / 오늘(오렌지 점선 테두리) / 미래(그레이) 7일 초과 시 트래커를 "7/7 완주" 배지 + 연속일 카운터(예: 🔥 23일)로 전환, 도트는 전부 채움 유지 탭 시 규칙 안내 바텀시트 오픈
2 자동 출석 토스트 (P0)
‹
120팡Lv.1🔔👤
럭키박스 열고 금 한돈 받기히어로 배너 (기존)
홈 콘텐츠 …
🔥
3일 연속 출석! 오늘 무료 럭키박스 팡 +10%
홈
전당
LUCKY!
룰렛
보관함
노출 그날 첫 접속(isNew=true) 1회만. 재접속 시 미노출 위치 bottom nav 상단 12px, 좌우 12px 마진 모션 아래에서 슬라이드업(240ms, ease-out) → 3초 유지 → 페이드아웃(200ms) tier 승급일에는 강조 변형 — "🎉 4일 연속 달성! 팡 +15%로 상승" (배경 오렌지 그라데이션, 5초 유지) 주의 알럿/모달 금지. 앱 진입 첫인상을 막지 않는다
3 무료박스 결과 — 팡 분리 표기 (P0)
‹
120팡Lv.1🔔👤
무료 럭키박스 결과
🎧
무선 블루투스 이어폰
무료박스는 상품 대신 팡으로 지급됩니다
기본 지급15 팡
🔥 연속 3일 보너스 +10%+1 팡
총 획득16 팡
내일도 오면 +15% · 연속을 지켜주세요!
홈
전당
LUCKY!
룰렛
보관함
핵심 보너스가 "얼마나 이득인지"를 유일하게 숫자로 증명하는 화면. 반드시 기본/보너스/총합 3행 분리 모션 기본 → 보너스 → 총합 순으로 순차 등장(각 200ms 딜레이), 총합은 0부터 카운트업(600ms) 보너스 0일 때 보너스 행 숨김 + 하단 CTA를 "내일 또 오면 팡 +10%!"로 교체 tier 승급 직전(3일차·6일차)에는 하단 CTA를 강조 컬러로 반전
4 무료박스 진입 모달 배지 (P1)
‹
120팡Lv.1🔔👤
🔥 연속 3일 · 팡 +10% 적용 중
오늘의 무료 럭키박스
1일 1회 · 팡으로 지급됩니다
📦
무료로 열기
홈
전당
LUCKY!
룰렛
보관함
배지 모달 최상단 중앙. 보너스 0%면 배지 대신 "오늘 출석 완료 · 내일부터 팡 +10%" 회색 텍스트 목적 오픈 직전 기대감 세팅. 결과 화면 보너스 표기와 같은 문구·같은 컬러를 써서 인지 연결
5 내 정보 — 연속 출석 요약 (P1)
‹내 정보‹
LEVEL 1
안녕하세요! user6님
130 / 1,000 EXP
🔥 연속 출석 3일
팡 +10%
1
2
3
4
5
6
7
최고 기록 9일 · 이번 달 출석 14일
보유 팡
환급 팡 + 이벤트 팡
120팡
홈
전당
LUCKY!
룰렛
보관함
배치 EXP 바 아래 / 보유 팡 카드 위. 레벨·EXP와 시각적으로 한 덩어리로 묶어 "내 성장 지표" 그룹을 만든다 추가 정보 최고 기록 · 이번 달 누적 출석일 (홈 위젯에는 없는 정보 → 내 정보 방문 동기) 탭 시 출석 달력 뷰(범위 밖, 2차)
6 비회원 — 로그인 유도 (P1)
‹
0팡게스트🔔👤
럭키박스 열고 금 한돈 받기히어로 배너 (기존)
🔒 연속 출석 보너스
팡 +최대 20%
1
2
3
4
5
6
7
3초 로그인하고 출석 시작하기
홈
전당
LUCKY!
룰렛
보관함
목적 배너 유입 비회원(쿠키 세션)의 회원 전환 소재로 재활용. 잠금 상태 위젯 + 카카오/네이버 로그인 CTA 주의 로그인 후 그 시점부터 1일차 시작. 비회원 기간을 소급 인정하지 않는다(계정 파밍 방지)
6.7 규칙 안내 바텀시트 (위젯 탭 시)
연속 출석
무료 럭키박스 팡 보너스
1일차
기본 지급
2~3일 연속
+10%
4~6일 연속
+15%
7일 이상 연속
+20% (최대)
하단 안내 문구(카피 확정안):
· 앱에 접속만 해도 자동으로 출석이 기록돼요.
· 출석 기준일은 매일 자정(한국 시간)에 바뀌어요.
· 하루라도 접속하지 않으면 연속 출석은 1일차부터 다시 시작돼요.
· 보너스는 무료 럭키박스로 받는 팡에만 적용돼요.
· 보너스로 받은 팡은 이벤트 팡으로, 지급일로부터 30일간 사용할 수 있어요.
bonusRate: 0 + featureOff: true → 위젯 및 모든 보너스 UI 미노출
10. 서버 로직 BE
10.1 체크인 (유저 단위 락 필수)
checkin(userId):
lock("attendance:" + userId) // advisory lock — 동시 진입 중복 방지
today = kstToday()
s = streakRepo.findOrCreate(userId)
prevRate = rateOf(s.current_streak)
if s.last_attend_kst == today:
return { isNew:false, ...snapshot(s) }
if s.last_attend_kst == today.minus(1 day):
s.current_streak += 1 // 연속 이어짐else:
s.current_streak = 1 // 최초 또는 공백 → 리셋
s.last_attend_kst = today
s.best_streak = max(s.best_streak, s.current_streak)
s.total_attend_days += 1
streakRepo.save(s)
newRate = rateOf(s.current_streak)
logRepo.upsertIgnoreConflict(userId, today, s.current_streak, newRate, "APP_ENTER")
return { isNew:true, isTierUp: newRate > prevRate, ...snapshot(s) }
10.2 보너스율 계산
rateOf(streak):
p = policy("attendance_bonus")
if !p.enabled: return 0
r = max(t.rate for t in p.tiers if t.days <= streak, default=0)
return min(r, p.maxRate) // 하드캡 이중 방어
currentBonus(userId):
s = streakRepo.find(userId)
// 오늘 출석이 확정된 경우에만 보너스 인정if s == null or s.last_attend_kst != kstToday(): return 0
return rateOf(s.current_streak)
기존 무료박스의 kstToday()를 그대로 재사용한다. 새로 만들지 않는다. 두 개의 "오늘" 정의가 생기면 자정 근처에서 "출석은 됐는데 무료박스는 어제 것"이라는 CS가 반드시 발생한다. 서버 타임존이 UTC라면 date_trunc('day', now() AT TIME ZONE 'Asia/Seoul') 기준으로 통일.