📦 이 문서는 v1 이전 자료(deprecated)입니다 — 최신 정보는 문서 허브를 보세요
📋 PRD · 90일 챌린지 + 포인트 (v2 단순화)

90일 챌린지 · 포인트 — PRD

Lally 2010 습관 형성(66일) 근거 · 「90일 챌린지 → 포인트 적립 → 골고루 키트 구매」 단일 루프 · 마이페이지 포인트 표시

🎯 18 기능 요구 📐 6 비기능 요구 🗃 5 DB 테이블 🔌 9 API ⏰ 4 cron
━ 1. PRODUCT OVERVIEW ━

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끼니 입력 = 작은 행동 단위(매일 반복)
Skinner 가변비율 강화⛔ 룰렛 도파민 — 폐기
비트코인 반감기 (4단계)⛔ 반감기 적립 — 폐기(정액 적립으로 대체)
Cialdini 6 원칙⛔ 친구 초대 양방향 — 폐기

1-2. 핵심 원칙

━ 2. SYSTEM COMPONENTS ━

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
━ 3. FUNCTIONAL REQUIREMENTS ━

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. NON-FUNCTIONAL REQUIREMENTS ━

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. DATA MODEL ━

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 SPECIFICATIONS ━

6. API 명세 (9 엔드포인트)

MethodPathDescription인증
POST/api/mileage/earn끼니 입력 등 적립 (idempotency key 필수)JWT
GET/api/mileage/balance현재 잔액 + 누적 적립/차감JWT
GET/api/mileage/historyledger 내역 페이지네이션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/me90일 챌린지 현재 상태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. ALGORITHM SPECIFICATIONS ━

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 ━

8. Cron Jobs (4개)

이름스케줄역할
stage_transition매일 09:00 KSTstage 전환 사용자 식별 → 카톡 알림
completion_bonus매일 00:30 KST90일 완주자 +50,774 보너스 지급
inactive_reminder매일 19:00 KST3일 미입력 사용자 카톡 리마인드
balance_audit매일 03:00 KSTledger 합산 vs balance_cache 정합성 검증
━ 9. ANTI-FRAUD ━

9. Farming 방지

핵심 원칙

마일리지 = 우리 키트 매출만 차감 가능. 현금 환전 불가 → farming 동기 자체가 낮음. 다만 친구 초대 보너스 +3,000은 IP/디바이스 fingerprint로 엄격 차단.

위협방어
한 사용자가 하루 종일 끼니 입력일일 5건 한도 + chip 중복 차단 + 1시간 간격
여러 계정으로 셀프 초대동일 IP + 디바이스 fingerprint + 결제 카드 검증
봇이 끼니 입력 자동화Cloudflare Turnstile 가입 시 / 의심 패턴 시 재인증
리뷰 보너스 farming구매 검증 + 모더레이션 통과 후 적립 (T+1)
마일리지 부정 적립 (개발자 백도어)모든 적립 audit log + 운영자 수동 적립 사유 필수
━ 10. RELEASE PLAN ━

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 ━

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 응답