기획서 · 초안 v0.1 · 검토용

연속 출석 보너스

매일 접속으로 쌓이는 연속 출석일에 비례해 무료 럭키박스 팡 지급액을 최대 +20% 가산하는 리텐션 기능

작성일 2026-08-07 서비스 럭키박스 (주식회사 브라보식스) 수신 개발팀 / 디자인팀 상태 확정 대기
CONTENTS
  1. 목적 및 배경
  2. 핵심 규칙
  3. 팡 지급 계산식 변경
  4. 비용 영향 시뮬레이션
  5. UI 설계 — 배치 맵
  6. UI 설계 — 화면별 시안
  7. 디자인 스펙 (컬러·타이포·모션)
  8. 데이터 모델
  9. API 명세
  10. 서버 로직
  11. 관리자 페이지
  12. 엣지케이스 · 어뷰징
  13. QA 테스트 케이스
  14. 개발 범위 · 일정
  15. 확정 필요 사항

1. 목적 및 배경왜 만드는가

문제

해결

앱 접속만으로 자동 출석이 기록되고, 연속 출석일이 길수록 그날 무료박스의 팡 지급액에 보너스(+10 / +15 / +20%)가 붙는다. 하루라도 빠지면 리셋 → "연속을 끊고 싶지 않다"는 손실회피 심리로 습관을 만든다.

왜 이 방식이 안전한가

관점판단
사행성무관. 구매 유도 없음, 무료 행위(접속)에 대한 무료 보상(팡) 가산일 뿐. 확률 구조를 건드리지 않는다.
비용월 10만원 미만. DAU 1만 기준 추가 팡의 실비용은 일 2~3천원 수준 (4장 참조)
약관기존 이벤트 포인트 정책(30일 유효)에 그대로 편입 → 약관 개정 불필요, 이벤트 공지만으로 운영 가능
파트너매체사 정산 로직에 영향 없음. 팡은 브라보식스 내부 경제이므로 수익 분배(75:25) 무변동

성공 지표 (KPI)

지표현재목표(도입 8주 후)
D7 리텐션측정 필요+15%p
무료박스 일일 사용률 (DAU 대비)측정 필요60% 이상
평균 연속 출석일3.0일 이상
7일 연속 달성 유저 비중전체 활성 유저의 20%
유료박스 전환율측정 필요+10%(간접 효과)
선행 작업: 위 지표 중 "현재 값"이 비어 있는 항목은 기능 개발 전에 베이스라인 로깅부터 붙여야 한다. 붙이지 않으면 도입 효과를 증명할 수 없다.

2. 핵심 규칙확정 정책

2.1 출석의 정의 — 앱 접속 자동 출석

2.2 보너스 tier

1일차
+0%
기본 지급
2~3일 연속
+10%
팡 지급 가산
4~6일 연속
+15%
팡 지급 가산
7일 이상
+20%
상한 · 유지

2.3 리셋 규칙

2.4 적용 시점

무료박스를 플레이하는 그 시점의 연속 출석일로 tier를 결정한다. 앱 진입 시 자동 출석이 먼저 기록되므로 박스 플레이 시점에는 이미 "오늘 출석" 확정 상태다.

방어 로직 필수: 자동 출석 API 호출이 실패했거나 클라이언트가 우회한 상태에서 무료박스 플레이가 들어올 수 있다. 이때 서버가 그 자리에서 출석을 보정 기록한 뒤 보너스를 계산한다. 클라이언트가 보낸 보너스율은 절대 신뢰하지 않는다.

3. 팡 지급 계산식 변경기존 로직에 어떻게 얹히는가

3.1 기존 (AS-IS)

// 무료박스 팡 = 당첨 상품 판매가 × 레벨별 계수
divisor = 200 - (level - 1) * 10   // Lv1:200 … Lv10:110
pang    = floor(product_price / divisor)

3.2 변경 (TO-BE)

divisor    = 200 - (level - 1) * 10
basePang   = floor(product_price / divisor)          // 기존과 동일
bonusRate  = attendanceService.currentBonus(userId)  // 0 / 0.10 / 0.15 / 0.20
bonusPang  = floor(basePang * bonusRate)             // 가산분 (별도 산출)
totalPang  = basePang + bonusPang
구현 원칙 — 반올림은 basePang에 한 번, bonusPang에 한 번.
floor(base × (1+rate)) 가 아니라 base + floor(base × rate) 로 계산한다. 이유: 결과 화면에 "기본 15팡 + 보너스 1팡"을 정확히 분리 표기해야 하는데, 전자는 두 값의 합이 총액과 어긋나는 케이스가 생긴다.

3.3 예시 (평균 당첨가 3,000원 가정)

레벨divisor기본 팡+10%+15%+20%
Lv.12001515+1=1615+2=1715+3=18
Lv.31801616+1=1716+2=1816+3=19
Lv.51601818+1=1918+2=2018+3=21
Lv.101102727+2=2927+4=3127+5=32

3.4 팡 유형 및 유효기간

보너스가 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 Rate50%30일 만료 + 1,000팡 단위 소비
대상 DAU10,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 상한 방어

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일간 사용할 수 있어요.

7. 디자인 스펙컬러 · 타이포 · 모션 토큰

7.1 컬러

토큰HEX용도
brand/orange#FF7A00채움 도트, 보너스 배지, 강조 수치 (기존 앱 메인)
brand/orange-grad#FF9A2E → #FF6A00tier 승급 토스트, 진입 모달 배지
brand/orange-bg#FFF3E6위젯 배경, 하단 CTA 박스
brand/navy#1B2440제목 텍스트, 토스트 배경, Lv 칩 (기존 앱)
gray/dot-empty#EEF1F5 / border #E5E7EB미달성 도트
gray/sub#9AA3B2보조 문구

7.2 타이포

요소크기 / 굵기
위젯 타이틀 ("3일 연속 출석 중!")14px / 900, 연속일 숫자만 orange
보너스 배지11px / 900, 흰색, pill radius 999px, padding 3×9
도트 숫자10px / 800
다음 tier 안내11px / 500, gray/sub (숫자·% 만 800 + orange)
결과 총합 팡16px / 900, orange

7.3 컴포넌트 규격

7.4 모션

동작스펙
토스트 등장/퇴장slide-up 240ms ease-out / fade-out 200ms, 유지 3초 (승급 시 5초)
도트 채움오늘 도트 scale 0.8→1.0 + 컬러 전환 300ms cubic-bezier(.34,1.56,.64,1)
결과 팡 카운트업총합 0 → 최종값 600ms ease-out
tier 승급 축하위젯에 컨페티 300ms (승급 당일 1회만)
접근성prefers-reduced-motion 시 모든 애니메이션 생략, 최종 상태만 렌더

7.5 디자인 산출물 요청 목록 디자인

8. 데이터 모델 DB신규 마이그레이션 2 테이블 + 정책 시드

8.1 attendance_logs — 출석 원장 (append-only)

컬럼타입설명
idbigserial PK
user_iduuid / bigint FK → users
kst_datedate출석한 KST 날짜
streak_dayint기록 시점의 연속일 (사후 분석용 스냅샷)
bonus_ratenumeric(4,3)그날 적용된 보너스율 (0.000~0.200)
sourcevarchar(20)APP_ENTER / FREE_BOX_FALLBACK
created_attimestamptz
UNIQUE (user_id, kst_date) — 하루 1회 멱등 보장의 최종 방어선
INDEX (kst_date) — 일별 출석 통계 집계용

8.2 user_attendance_streaks — 연속 상태 캐시 (1인 1행)

컬럼타입설명
user_idPK FK → users
current_streakint default 0현재 연속일
best_streakint default 0최고 기록 (리셋 안 됨)
last_attend_kstdate마지막 출석 KST 날짜
total_attend_daysint default 0누적 출석일
updated_attimestamptz
왜 캐시 테이블을 두는가: current_streak은 attendance_logs에서 파생 가능하지만, 무료박스 오픈은 핫패스다. 매번 로그를 역순 스캔하면 유저당 수십~수백 행을 읽는다. 캐시 1행 조회로 O(1) 처리하고, 로그는 검증·통계·복구용 원장으로만 쓴다.

8.3 정책값 — reward_policies 시드

key   : "attendance_bonus"
value : {
  "enabled": true,
  "tiers": [
    { "days": 2, "rate": 0.10 },
    { "days": 4, "rate": 0.15 },
    { "days": 7, "rate": 0.20 }
  ],
  "maxRate": 0.20,        // 하드캡
  "maxBonusPang": 100,    // 1회 보너스 팡 절대 상한
  "pangType": "EVENT",   // 이벤트 팡 (30일)
  "applyTo": ["FREE_BOX"]
}
하드코딩 금지. tier·퍼센트·상한은 전부 정책 테이블에서 읽는다. 코드에 숫자를 박으면 이벤트 조정마다 배포가 필요해진다. enabled:false 만으로 기능 전체를 즉시 끌 수 있어야 한다(킬스위치).

8.4 팡 원장 확장

기존 팡 트랜잭션 테이블에 사유 코드 ATTENDANCE_BONUS 추가. 무료박스 1회당 최대 2건 기록:

reasonamounttypeexpire
FREE_BOX_REWARD15기존 정책기존 정책
ATTENDANCE_BONUS1EVENT+30일

9. API 명세 BE

9.1 POST /api/attendance/checkin — 자동 출석

인증 필요 · 멱등 · 클라이언트 앱 마운트 시 1회 호출

Response 200

{
  "isNew": true,            // 오늘 최초 기록이면 true (토스트 노출 조건)
  "isTierUp": false,        // 오늘 tier가 올라갔으면 true (강조 토스트)
  "currentStreak": 3,
  "bestStreak": 9,
  "bonusRate": 0.10,
  "bonusPercent": 10,       // UI 표기 편의
  "nextTier": { "days": 4, "rate": 0.15, "remainDays": 1 },
  "last7": [true,true,true,false,false,false,false],
  "todayAttended": true,
  "kstDate": "2026-08-07"
}

nextTiernull이면 최고 tier 도달(7일+) 상태.

9.2 GET /api/attendance/status — 위젯 조회

응답 스키마는 9.1과 동일(isNew, isTierUp 제외). 홈 위젯·내 정보 렌더용. 쓰기 없음.

9.3 무료박스 응답 확장 — POST /api/freebox/open

{
  "product": { ... },
  "pang": {
    "base": 15,
    "bonus": 1,
    "total": 16,
    "bonusRate": 0.10,
    "streakDay": 3,
    "nextTier": { "days": 4, "rate": 0.15, "remainDays": 1 }
  },
  "level": 1,
  "exp": 140
}
보안 원칙: 요청 바디에 보너스 관련 파라미터를 받지 않는다. 서버가 user_attendance_streaks를 직접 읽어 재계산한다. 클라이언트는 결과만 렌더한다.

9.4 에러 정책

상황처리
checkin 실패(네트워크/500)사용자에게 에러 노출 금지. 위젯을 이전 캐시 상태로 렌더, 무음 재시도 1회(2초 후)
비로그인 상태 호출401 대신 { guest: true } 200 반환 → 프론트는 잠금 위젯(mock 6) 렌더
정책 enabled:falsebonusRate: 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)

10.3 무료박스 지급부 수정

freeBoxService.open(userId):
  // ── 방어: 출석 누락 시 그 자리에서 보정 ──
  if streakRepo.find(userId)?.last_attend_kst != kstToday():
      attendanceService.checkin(userId, source="FREE_BOX_FALLBACK")

  winner   = drawFromPreset("DEFAULT")        // 무료박스는 항상 기본 프리셋
  divisor  = 200 - (user.level - 1) * 10
  basePang = floor(winner.price / divisor)

  rate      = attendanceService.currentBonus(userId)
  bonusPang = min(floor(basePang * rate), policy.maxBonusPang)

  tx:
     pangRepo.grant(userId, basePang,  "FREE_BOX_REWARD",   type=<기존정책>)
     if bonusPang > 0:
        pangRepo.grant(userId, bonusPang, "ATTENDANCE_BONUS", type="EVENT", expire=+30d)
     expRepo.add(userId, 10)                     // 무료박스 경험치 (기존)

  return { base:basePang, bonus:bonusPang, total:basePang+bonusPang, rate, streakDay }

10.4 KST 날짜 유틸

기존 무료박스의 kstToday()를 그대로 재사용한다. 새로 만들지 않는다. 두 개의 "오늘" 정의가 생기면 자정 근처에서 "출석은 됐는데 무료박스는 어제 것"이라는 CS가 반드시 발생한다. 서버 타임존이 UTC라면 date_trunc('day', now() AT TIME ZONE 'Asia/Seoul') 기준으로 통일.

11. 관리자 페이지 BE FE

11.1 정책 관리 (신규 메뉴: 이벤트 관리 > 출석 보너스)

11.2 대시보드 지표 추가

지표산출
일별 출석자 수 / DAU 대비 비율attendance_logs kst_date 집계
연속일 구간별 유저 분포user_attendance_streaks current_streak 버킷 (1 / 2-3 / 4-6 / 7+)
평균 연속 출석일avg(current_streak) — 활성 유저 한정
보너스 지급 팡 총액 / 일·월팡 원장 reason = ATTENDANCE_BONUS 합계
보너스 팡 소진율지급 대비 사용 (redemption 실측 → 4장 가정값 갱신용)
D1/D7 리텐션 — 연속일 구간별효과 검증 핵심 지표

11.3 유저 상세 화면

기존 사용자 관리 상세에 연속 출석 / 최고 기록 / 최근 30일 출석 캘린더 표시. CS 문의("출석했는데 보너스 안 붙었어요") 대응용.

12. 엣지케이스 · 어뷰징

12.1 엣지케이스

상황처리
KST 자정 직전 접속 → 직후 무료박스출석은 어제, 박스는 오늘 → 10.3의 fallback 체크인이 오늘 출석을 기록하며 연속 1일 시작. 정상 동작
하루 여러 번 접속첫 회만 기록. 이후 isNew:false → 토스트 미노출
자정을 걸친 세션(앱 계속 켜둠)날짜 변경 감지 시 클라이언트가 checkin 재호출. 미감지 시에도 무료박스 fallback이 커버
무료박스만 하고 checkin 미호출fallback 체크인으로 보정 (source=FREE_BOX_FALLBACK)
비회원 → 로그인 전환비회원 기간 소급 인정 없음. 전환 시점부터 1일차
보너스 팡이 0팡 (base 5팡 × 10%)트랜잭션 미생성, UI 보너스 행 숨김, "다음 단계에서 더 받아요" 문구
탈퇴 후 재가입신규 user_id → 1일차부터. 기존 기록 승계 없음
차단 계정(is_blocked)출석 기록/보너스 모두 스킵
테스트 계정(무제한 무료박스)출석 기록은 하되 통계 집계에서 제외 (지표 오염 방지)
기능 도중 OFF기존 연속일은 보존, 보너스만 0% 처리. 재개 시 그대로 이어감

12.2 어뷰징

시나리오영향대응
다계정으로 여러 개 7일 연속 유지낮음 — 계정당 일 최대 +3팡(1.8원)기존 다계정 스코어링·is_blocked 로직 그대로 적용. 별도 대응 불필요
클라이언트 조작으로 보너스율 위조차단됨서버 재계산 (9.3). 요청 파라미터로 보너스를 받지 않음
단말 시간 변경으로 연속일 조작차단됨날짜는 서버 KST만 사용. 클라이언트 시간 미참조
checkin 무한 호출낮음UNIQUE 제약 + advisory lock으로 멱등. 추가로 IP/유저 단위 rate limit 60회/분
고가 상품 당첨 + 보너스 폭증중간maxBonusPang 100팡 하드캡 (4.4)

12.3 컴플라이언스

13. QA 테스트 케이스

#케이스기대 결과
T-01최초 접속streak=1, rate=0, isNew=true, 토스트 노출
T-02같은 날 재접속streak 불변, isNew=false, 토스트 미노출
T-03연속 2일차streak=2, rate=0.10, isTierUp=true
T-04연속 3일차streak=3, rate=0.10, isTierUp=false
T-05연속 4일차 (경계)rate=0.15, isTierUp=true
T-06연속 7일차 (경계)rate=0.20, isTierUp=true
T-07연속 8·30일차rate=0.20 유지, isTierUp=false
T-081일 공백 후 접속streak=1, rate=0, best_streak 보존
T-0910일 공백 후 접속streak=1
T-10Lv1 · 3,000원 당첨 · 3일차base=15, bonus=1, total=16
T-11Lv10 · 3,000원 당첨 · 7일차base=27, bonus=5, total=32
T-12base 5팡 · 10% 보너스bonus=0, 트랜잭션 1건만 생성, UI 보너스 행 숨김
T-13150만원 당첨 · Lv10 · 20%bonus = min(2727, 100) = 100팡
T-14checkin 없이 무료박스 오픈fallback 체크인 후 보너스 정상 적용, source=FREE_BOX_FALLBACK
T-15동시 checkin 10회 병렬로그 1건만 생성, streak 1 증가
T-16클라이언트가 bonusRate=0.99 전송무시, 서버 계산값 적용
T-17KST 23:59 checkin → 00:01 박스fallback으로 새 날짜 출석 기록, 연속 정상 증가
T-18정책 enabled=falsebonusRate=0, 위젯·배지·보너스 행 전부 미노출
T-19비회원 접속guest 응답, 잠금 위젯 렌더, 기록 없음
T-20차단 계정출석/보너스 모두 스킵
T-21보너스 팡 만료지급 30일 후 자동 소멸, 이벤트 팡 선차감 규칙 적용
T-22reduced-motion 설정애니메이션 없이 최종 상태 즉시 렌더

14. 개발 범위 · 일정

구분작업담당예상
DB마이그레이션 2 테이블 + 인덱스 + reward_policies 시드 + 팡 사유코드 추가DB0.5d
BEattendanceService / Repository, rateOf, currentBonusBE1.5d
BEAPI 2종 (checkin / status) + 무료박스 응답 확장BE1d
BEfreeBoxService 지급 로직 수정 + fallback 체크인BE1d
BE관리자 정책 API + 대시보드 집계 쿼리BE1.5d
DE시안 7종 (7.5 목록)디자인2d
FE홈 위젯 + 내 정보 요약 + 자동 체크인 훅FE1.5d
FE토스트 (일반/승급) + 모션FE0.5d
FE무료박스 진입 배지 + 결과 팡 분리 표기 + 카운트업FE1d
FE규칙 바텀시트 + 비회원 잠금 위젯FE0.5d
FE관리자 정책 편집 화면FE1d
QAT-01~T-22 + 자정 경계 시나리오QA1.5d
합계약 14.5 MD

단계별 릴리즈

선행 조건: 1.KPI 베이스라인 로깅(현재 D7 리텐션·무료박스 사용률)을 기능 배포 최소 2주 전에 붙일 것. 지금 붙이지 않으면 이 기능의 효과를 영원히 증명할 수 없다.

15. 확정 필요 사항개발 착수 전 결론이 필요한 항목

#항목제안결정
11회 보너스 팡 절대 상한100팡 — 고가 상품 당첨 시 보너스 폭증 차단
2보너스 팡 유형이벤트 팡(30일) — 소진 유도 + 부채 관리
37일 초과 시 처리+20% 무기한 유지 (사이클 리셋 없음)
4홈 위젯 위치히어로 배너 직하단 고정
5비회원 노출잠금 위젯 + 로그인 CTA로 전환 소재 활용
67일 이상 유저의 트래커 표현완주 배지 + 연속일 카운터로 전환 (디자인 판단 필요)
7이벤트 페이지 개설 여부개설 권장 — 분쟁 방어 목적
8푸시/알림 연동범위 밖 — 현재 푸시 인프라 확인 후 2차

범위 밖 (2차 검토)