1. 개요
⚠️ 2026-05-31 기획 v2 — 단일 루프로 단순화
복잡한 게이미피케이션(반감기 Stage·룰렛·친구초대 양방향 적립·댓글/리뷰 보상)을 전면 폐기하고 「90일 챌린지 → 포인트 적립 → 골고루 키트 구매」 단일 루프만 남긴다. 포인트는 식단 기록으로 정액 적립(반감기 없음), 골고루 키트 결제에 차감, 마이페이지에서 보유 포인트·적립/사용 내역을 본다. 아래 ⛔ DEPRECATED 표시 항목은 개발하지 않는다.
"90일 습관 챌린지 = 평생 식습관"
매일 식단을 기록하며 90일 습관을 완성(Lally 66일) → 기록마다 포인트 정액 적립 → 모인 포인트로 우리 아이 맞춤 골고루 키트를 구매. 기록할수록 키트가 저렴해지는 선순환. 마이페이지에서 포인트·진행률 확인.
1-1. 학술 근거
| 이론 | 적용 |
|---|---|
| Lally et al. 2010 (UCL) | 습관 형성 평균 66일 → 90일 챌린지 설계 근거 |
| BJ Fogg Tiny Habits | 끼니 입력 = 작은 행동 단위(매일 반복) |
| ⛔ 룰렛 도파민 — 폐기 | |
| ⛔ 반감기 적립 — 폐기(정액 적립으로 대체) | |
| ⛔ 친구 초대 양방향 — 폐기 |
1-2. 핵심 원칙
- 1 포인트 = 1원 골고루 키트 결제 차감 (현금 환전 X)
- 정액 적립 — 식단(끼니) 기록 1건 = +50P(반감기·Stage 없음, 1P=1원)
- 적립 단위 = 끼니 입력 1건 · 일일 한도 5끼(영유아 자연 한도·farming 방지) → 하루 최대 250P
- 사용처 = 골고루 키트 + 월 구독 결제 차감 — 마이페이지에서 잔액·적립/사용 내역 확인
- 인플레 한도 X — 많이 기록할수록 키트 매출로 환원
2. 시스템 컴포넌트
flowchart TB
USER[사용자 끼니 입력]
EARN[Earn Service
적립 검증·계산] STAGE[Stage Controller
반감기 4단계 결정] FRAUD[Anti-Fraud
한도·간격 체크] LEDGER[(Mileage Ledger
append-only)] BALANCE[(Balance Cache)] REDEEM[Redeem Service
키트 결제 차감] REFERRAL[Referral Tracker
친구 초대] ROULETTE[Roulette Engine
가변비율 강화] CHALLENGE[Challenge Service
90일 완주 트리거] KAKAO[KakaoTalk SENS
알림톡 발송] CRON[Cron Jobs] USER --> EARN --> FRAUD --> STAGE --> LEDGER --> BALANCE USER --> ROULETTE --> LEDGER REFERRAL --> LEDGER CHALLENGE --> LEDGER BALANCE --> REDEEM CRON --> CHALLENGE CRON --> KAKAO classDef core fill:#FFE8D1,stroke:#E89244,color:#1F2D3D classDef store fill:#E3F2FD,stroke:#3498DB,color:#1F2D3D classDef ext fill:#F3E5F5,stroke:#9C27B0,color:#1F2D3D class EARN,STAGE,FRAUD,REDEEM,ROULETTE,CHALLENGE core class LEDGER,BALANCE store class KAKAO,CRON ext
적립 검증·계산] STAGE[Stage Controller
반감기 4단계 결정] FRAUD[Anti-Fraud
한도·간격 체크] LEDGER[(Mileage Ledger
append-only)] BALANCE[(Balance Cache)] REDEEM[Redeem Service
키트 결제 차감] REFERRAL[Referral Tracker
친구 초대] ROULETTE[Roulette Engine
가변비율 강화] CHALLENGE[Challenge Service
90일 완주 트리거] KAKAO[KakaoTalk SENS
알림톡 발송] CRON[Cron Jobs] USER --> EARN --> FRAUD --> STAGE --> LEDGER --> BALANCE USER --> ROULETTE --> LEDGER REFERRAL --> LEDGER CHALLENGE --> LEDGER BALANCE --> REDEEM CRON --> CHALLENGE CRON --> KAKAO classDef core fill:#FFE8D1,stroke:#E89244,color:#1F2D3D classDef store fill:#E3F2FD,stroke:#3498DB,color:#1F2D3D classDef ext fill:#F3E5F5,stroke:#9C27B0,color:#1F2D3D class EARN,STAGE,FRAUD,REDEEM,ROULETTE,CHALLENGE core class LEDGER,BALANCE store class KAKAO,CRON ext
3. 기능 요구사항 (18개)
3-1. 포인트 적립 (정액) ✅ 구현 완료
FR-V01
끼니 입력 시 포인트 적립
90일 스트로크 챌린지 = 하루 최대 5끼 식단 입력. 각 끼니(식단) 입력마다 정액 적립(반감기·stage 없음).
승인: 끼니 입력 1건 = +50P (1P=1원 · 하루 최대 5끼 = 250P/일) / 토스트 즉시 표시 / ledger·balance 동시 갱신 (트랜잭션) —
earn_meal_point RPC 구현 완료(멱등·중복차단·위조방지)FR-V02
일일 한도 5끼 검증
하루 5건 초과 입력 시 기록은 저장하되 마일리지 X.
승인: 6번째 입력 시 "5끼 한도 도달, 기록만 저장" 안내 / 자정 KST 리셋
FR-V03
끼니 간격 1시간 검증
직전 끼니 입력 시각 + 60분 이내 입력은 마일리지 X.
승인: 검증 실패 시 "마지막 입력 후 1시간 지나야 적립" 메시지
FR-V04
chip 중복 차단
같은 chip (예: 오전간식) 같은 날 중복 입력 시 마일리지 X.
승인: 6 chip (아침·오전간식·점심·오후간식·저녁·야간) 각 1회만 적립
3-2. Stage Controller (반감기) ⛔ DEPRECATED (정액 적립으로 대체)
FR-V05
가입일 기준 stage 자동 결정
user.created_at 기준 일수로 stage 1-4 자동 결정.
승인: 1-21일 Stage 1 · 22-45일 Stage 2 · 46-66일 Stage 3 · 67-90일 Stage 4 · 91일+ Stage 4 유지
FR-V06
stage 전환 알림
stage 변경 시 카톡 알림톡 자동 발송.
승인: cron 매일 09:00 KST · "오늘부터 마일리지가 절반으로 줄어요. 90일 완주까지 X일 남았어요"
3-3. 90일 챌린지
FR-V07
90일 완주 보너스 적립
가입 91일째 자정 cron, 90일 중 60일 이상 입력 시 +50,774 보너스 자동 적립.
승인: ledger 별도 트랜잭션 (kind='challenge_completion') · 카톡 알림 "🎉 90일 완주! 골고루 키트 무료 결제하세요"
FR-V08
챌린지 진행률 표시
홈 화면 상단에 "X/90일 · Stage N · 누적 마일리지" 카드.
승인: 매 입력마다 즉시 갱신 · 진행 바 시각화 · 다음 stage 전환일 표시
3-4. 친구 초대 (양방향 적립) ⛔ DEPRECATED
FR-V09
고유 초대 링크 생성
사용자별 referral code (8자) 생성. /signup?ref=ABCD1234.
승인: URL 단축 X (gh-pages friendly) · 카톡 공유 시 OG 이미지 자동
FR-V10
초대 성공 시 양방향 적립
피초대자가 가입 + 첫 끼니 입력 완료 시 초대자 +3,000·피초대자 +1,000.
승인: 동일 IP·동일 디바이스 fingerprint 차단 / 월 5명 한도
3-5. 룰렛 (가변비율 강화) ⛔ DEPRECATED
FR-V11
일 1회 무료 룰렛
하루 첫 끼니 입력 후 룰렛 1회 무료. 보상 분포: 10원 30%·50원 40%·100원 20%·500원 8%·5000원 2%.
승인: 확률 서버 검증 (클라이언트 조작 차단) · 결과 ledger 기록 · 애니메이션 3초
3-6. 댓글·리뷰 보상 ⛔ DEPRECATED
FR-V12
도감 댓글 작성 시 보상
로그인 사용자가 도감 댓글 작성 시 +500. 모더레이션 통과 후 지급.
승인: 1 식재료당 1회 / 일 최대 5건 / 통과 후 적립 (T+1)
FR-V13
키트 리뷰 작성 시 보상
키트 구매 후 리뷰 작성 시 +5,000. 사진 포함 시 +5,000 추가.
승인: 구매 검증 / 1 키트당 1회 / 리뷰 모더레이션 통과 후 적립
3-7. 사용처 (Redeem)
FR-V14
키트 결제 시 마일리지 차감
결제 화면 "마일리지 사용" 입력 → 1마일리지 = 1원 차감.
승인: 보유 마일리지 표시 / 100% 차감 가능 (전액 무료 결제 OK) / 차감 후 결제 실패 시 자동 환원 (트랜잭션)
FR-V15
마일리지 내역 조회
사용자 마이페이지 → 적립·차감 전체 내역 (페이지네이션).
승인: 30일/3개월/1년/전체 필터 · CSV 다운로드 옵션
3-8. 카카오 알림톡 푸시
FR-V16
주요 이벤트 카톡 발송
stage 전환·90일 완주·친구 가입·룰렛 잭팟·1주 미입력 리마인드 자동 카톡.
승인: 네이버 클라우드 SENS 알림톡 / 템플릿 사전 심사 / 발송 실패 시 재시도 3회 / 건당 ₩8 비용 모니터링
3-9. 어드민·운영
FR-V17
운영자 수동 적립·차감
CS 이슈·이벤트 보상 시 운영자 수동 ledger 입력.
승인: 운영자 인증 / 사유 필수 입력 / 모든 변경 audit log 기록
FR-V18
실시간 대시보드
총 발행·차감·DAU·키트 결제 차감 비율 일/주/월 대시보드.
승인: Metabase·Retool 임베드 / 발행 vs 차감 비율 모니터링 (인플레 자체 추적)
4. 비기능 요구사항 (6개)
NFR-V01
적립 응답 시간 < 300ms
끼니 입력 → 마일리지 토스트까지 300ms 이내.
balance cache 활용 / append-only ledger / DB 트랜잭션 최적화
NFR-V02
적립 정확성 100%
중복·누락·오적립 0%.
idempotency key 적용 / append-only ledger / 자정 정합성 검증 cron
NFR-V03
fraud 차단율 ≥ 95%
동일 IP·동일 디바이스·짧은 간격 등 farming 95% 이상 차단.
디바이스 fingerprint + IP + 시간 간격 조합 검증 / 의심 계정 큐 자동 분류
NFR-V04
카톡 발송 ≥ 99%
알림톡 발송 성공률 99% 이상.
SENS 재시도 3회 / 실패 시 SMS fallback / 발송 로그 30일 보관
NFR-V05
비용 — 월 ₩100k 이내 (1만 MAU)
알림톡 + DB 호출 + cron 비용 합산.
알림톡 평균 사용자당 월 8건 × ₩8 = ₩640 × 1만 = ₩6.4M (보수) — 또는 push 우선 사용자 segmentation으로 ₩100k 이내 유지
NFR-V06
회계 감사 대비
전체 ledger 5년 이상 보관, 외부 회계 감사 시 추출 가능.
append-only / S3 백업 / 마일리지는 부채 인식 (1마일리지 = ₩1)
5. 데이터 모델 (Supabase Postgres)
-- 마일리지 ledger (append-only, 모든 변동 기록) CREATE TABLE mileage_ledger ( id BIGSERIAL PRIMARY KEY, user_id UUID NOT NULL REFERENCES users(id), kind TEXT NOT NULL, -- 'meal_input'·'referral_inviter'·'referral_invitee'·'roulette'·'comment'·'review'·'challenge_completion'·'redeem_kit'·'admin_adjust' amount INT NOT NULL, -- +/- (적립 양수, 차감 음수) meta JSONB, -- {meal_chip, stage, order_id, ref_user_id, ...} idempotency_key TEXT UNIQUE, -- 중복 적립 방지 balance_after INT NOT NULL, -- 거래 후 잔액 (감사용) created_at TIMESTAMPTZ DEFAULT NOW() ); CREATE INDEX idx_ledger_user_created ON mileage_ledger(user_id, created_at DESC); -- 잔액 캐시 (read 성능) CREATE TABLE mileage_balance ( user_id UUID PRIMARY KEY REFERENCES users(id), balance INT DEFAULT 0, total_earned INT DEFAULT 0, total_redeemed INT DEFAULT 0, updated_at TIMESTAMPTZ DEFAULT NOW() ); -- 친구 초대 트래킹 CREATE TABLE referrals ( id UUID PRIMARY KEY, inviter_id UUID REFERENCES users(id), invitee_id UUID REFERENCES users(id), referral_code TEXT, status TEXT, -- 'invited'·'signed_up'·'first_input'·'rewarded' inviter_ip INET, invitee_ip INET, invitee_device_fingerprint TEXT, rewarded_at TIMESTAMPTZ, created_at TIMESTAMPTZ DEFAULT NOW() ); -- 90일 챌린지 상태 CREATE TABLE challenges ( user_id UUID PRIMARY KEY REFERENCES users(id), started_at TIMESTAMPTZ DEFAULT NOW(), current_stage INT DEFAULT 1, days_input INT DEFAULT 0, -- 입력한 일수 completion_bonus_paid BOOLEAN DEFAULT FALSE, completed_at TIMESTAMPTZ ); -- 카톡 알림톡 발송 로그 CREATE TABLE kakao_messages ( id UUID PRIMARY KEY, user_id UUID REFERENCES users(id), template_id TEXT, -- 'stage_change'·'challenge_complete'·'reminder'·'jackpot' payload JSONB, status TEXT, -- 'sent'·'failed'·'retry' sens_message_id TEXT, sent_at TIMESTAMPTZ, cost_krw INT );
6. API 명세 (9 엔드포인트)
| Method | Path | Description | 인증 |
|---|---|---|---|
| POST | /api/mileage/earn | 끼니 입력 등 적립 (idempotency key 필수) | JWT |
| GET | /api/mileage/balance | 현재 잔액 + 누적 적립/차감 | JWT |
| GET | /api/mileage/history | ledger 내역 페이지네이션 | JWT |
| POST | /api/mileage/redeem | 키트 결제 차감 (atomic) | JWT |
| POST | /api/referrals/invite | 초대 링크 생성 | JWT |
| POST | /api/referrals/redeem | 피초대자 가입 처리 | JWT |
| POST | /api/roulette/spin | 룰렛 1회 실행 (서버 결정) | JWT |
| GET | /api/challenges/me | 90일 챌린지 현재 상태 | JWT |
| POST | /api/admin/mileage | 운영자 수동 입력 | admin |
6-1. POST /api/mileage/earn — 예시 페이로드
{
"kind": "meal_input",
"meta": {
"meal_chip": "점심",
"meal_id": "abc-123",
"input_time": "2026-05-25T12:30:00+09:00"
},
"idempotency_key": "meal_abc-123"
}
// 응답
{
"earned": 100,
"stage": 1,
"balance": 4200,
"daily_count": 3,
"daily_limit": 5,
"toast": "+100 마일리지 적립!"
}
// 한도 도달 응답
{
"earned": 0,
"reason": "daily_limit_reached",
"message": "5끼 한도 도달 — 기록만 저장"
}
7. 알고리즘
7-1. Stage 결정
function determineStage(user) { const daysSinceSignup = differenceInDays(now(), user.created_at); if (daysSinceSignup <= 21) return { stage: 1, reward: 100 }; if (daysSinceSignup <= 45) return { stage: 2, reward: 50 }; if (daysSinceSignup <= 66) return { stage: 3, reward: 25 }; return { stage: 4, reward: 12 }; // 67일+ }
7-2. 적립 검증 흐름
async function earnMileage(userId, payload) { // 1. Idempotency 체크 (중복 차단) if (await ledger.exists(payload.idempotency_key)) return previousResult; // 2. 일일 한도 (5끼) 체크 const todayCount = await ledger.countToday(userId, 'meal_input'); if (todayCount >= 5) return { earned: 0, reason: 'daily_limit_reached' }; // 3. 끼니 간격 1시간 체크 const lastMeal = await ledger.lastMeal(userId); if (lastMeal && minutesSince(lastMeal) < 60) return { earned: 0, reason: 'too_soon' }; // 4. chip 중복 체크 if (await ledger.todayChip(userId, payload.meta.meal_chip)) return { earned: 0, reason: 'chip_duplicate' }; // 5. Stage 결정 const { stage, reward } = determineStage(user); // 6. Append-only ledger 기록 + balance 갱신 (트랜잭션) await db.transaction(async (tx) => { await tx.ledger.insert({ user_id: userId, kind: 'meal_input', amount: reward, meta: { ...payload.meta, stage } }); await tx.balance.increment(userId, reward); }); return { earned: reward, stage, balance: newBalance }; }
7-3. 룰렛 분포 (서버 결정)
| 보상 | 확률 | 기대값 |
|---|---|---|
| 10원 | 30% | 3 |
| 50원 | 40% | 20 |
| 100원 | 20% | 20 |
| 500원 | 8% | 40 |
| 5000원 (잭팟) | 2% | 100 |
| 기대값/회 | 100% | 183원 |
90일 = 90회 룰렛 = 약 ₩16,500 평균 보상. 잭팟 2% × 90일 = 1.8회 발생 가능.
7-4. 90일 완주 판정 cron
async function processCompletionBonus() { const due = await challenges.findDue(); // signup +90일 + 미지급 for (const user of due) { if (user.days_input >= 60) { // 90일 중 60일 이상 입력 await ledger.insert({ user_id: user.id, kind: 'challenge_completion', amount: 50774 }); await challenges.markCompleted(user.id); await kakao.send(user.id, 'challenge_complete'); } } }
8. Cron Jobs (4개)
| 이름 | 스케줄 | 역할 |
|---|---|---|
| stage_transition | 매일 09:00 KST | stage 전환 사용자 식별 → 카톡 알림 |
| completion_bonus | 매일 00:30 KST | 90일 완주자 +50,774 보너스 지급 |
| inactive_reminder | 매일 19:00 KST | 3일 미입력 사용자 카톡 리마인드 |
| balance_audit | 매일 03:00 KST | ledger 합산 vs balance_cache 정합성 검증 |
9. Farming 방지
핵심 원칙
마일리지 = 우리 키트 매출만 차감 가능. 현금 환전 불가 → farming 동기 자체가 낮음. 다만 친구 초대 보너스 +3,000은 IP/디바이스 fingerprint로 엄격 차단.
| 위협 | 방어 |
|---|---|
| 한 사용자가 하루 종일 끼니 입력 | 일일 5건 한도 + chip 중복 차단 + 1시간 간격 |
| 여러 계정으로 셀프 초대 | 동일 IP + 디바이스 fingerprint + 결제 카드 검증 |
| 봇이 끼니 입력 자동화 | Cloudflare Turnstile 가입 시 / 의심 패턴 시 재인증 |
| 리뷰 보너스 farming | 구매 검증 + 모더레이션 통과 후 적립 (T+1) |
| 마일리지 부정 적립 (개발자 백도어) | 모든 적립 audit log + 운영자 수동 적립 사유 필수 |
10. 출시 단계 (3 phase)
v0 MVP (M4-M5) — 핵심 적립만
- FR-V01~V06 (끼니 입력·한도·stage·반감기)
- FR-V14 (키트 결제 차감)
- FR-V18 (운영자 대시보드)
- DB·API·cron 기본 구축
v1 (M6) — 바이럴 가속
- FR-V09~V11 (친구 초대·룰렛)
- FR-V07~V08 (90일 챌린지·완주 보너스)
- FR-V16 (카톡 알림톡 4 템플릿)
v2 (M7+) — 운영 고도화
- FR-V12~V13 (댓글·리뷰 보상)
- FR-V15 (마일리지 내역 + CSV)
- FR-V17 (운영자 수동 적립)
- Anti-fraud 디바이스 fingerprint 강화
11. KPI
| 지표 | 3개월 목표 | 측정 |
|---|---|---|
| 가입 → 첫 끼니 입력 전환율 | ≥ 70% | onboarding funnel |
| 90일 챌린지 시작률 | ≥ 50% | 가입 후 3일 내 끼니 입력 |
| 90일 챌린지 완주율 | ≥ 40% | 가입 90일 후 60일 이상 입력 |
| 평균 친구 초대 수 | ≥ 1.5명 | referrals 테이블 |
| 완주 보너스 → 키트 결제 차감 비율 | ≥ 80% | ledger 분석 |
| 일일 마일리지 적립 평균 | ~250 (2.5끼) | ledger 일별 평균 |
| fraud 차단율 | ≥ 95% | 의심 계정 자동 큐 |
| 카톡 발송 성공률 | ≥ 99% | SENS 응답 |