← 목록으로
2026-02-25plans

richbukae 결제→배송 플로우 완성 Implementation Plan

For Claude: REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task.

Goal: SALES_OS DB 구축 + Resend 이메일 검증 + E2E 결제→다운로드 플로우 완전 동작 확인

Architecture: 현재 코드는 거의 완성 상태. confirm API → DB 저장(non-blocking) + 이메일 발송(non-blocking) + 다운로드 토큰 반환. 미완성 부분은 (1) SALES_OS Turso DB 테이블 미생성, (2) Resend 실제 발송 미검증, (3) E2E 플로우 미테스트. 세 가지를 완성하면 CEO Toss 프로덕션 키 수령 즉시 실서비스 가능.

Tech Stack: Next.js 15, Turso (LibSQL), Resend SDK, Toss Payments v1 SDK (테스트 키), curl E2E 테스트

관련 태스크: fdaa3141


현황 스냅샷 (2026-02-25 기준)

컴포넌트상태비고
checkout/page.tsx✅ 완성TossPayment 컴포넌트, 이메일 입력
TossPayment.tsx✅ 완성Toss SDK v1, requestPayment
/api/payment/confirm✅ 완성Toss API 호출, 토큰 생성
/success 페이지✅ 완성다운로드 링크 3개 표시
/api/download✅ 완성토큰 검증, R2 URL 리다이렉트
SALES_OS DB❌ 테이블 미생성orders, crm_contacts 없음
Resend 이메일❓ 발송 미검증lib/resend.ts 구현 확인 필요
E2E 테스트❌ 없음전체 플로우 테스트 필요

CEO 블로킹 (이 플랜에서 제외 — Plan 2에서 처리)

  • TOSS_SECRET_KEY (프로덕션) → 현재 테스트 키 fallback으로 동작
  • PRODUCT_PDF/SKILLS/NOTION_URL → 현재 503 응답, 기능 테스트는 가능
  • RESEND_API_KEY → 없으면 이메일 발송 실패 (로그만)

Task 1: SALES_OS Turso DB 생성 및 테이블 마이그레이션

Files:

  • Create: projects/richbukae-store/scripts/migrate-sales-os.sh
  • Read: projects/richbukae-store/src/app/api/payment/confirm/route.ts:60-90 (orders INSERT 구조 참조)

Step 1: SALES_OS DB 존재 여부 확인

turso db list 2>&1 | grep -i sales

Expected: "sales-os" 또는 없음

Step 2: DB가 없으면 생성

turso db create sales-os --location nrt
turso db show sales-os

Expected: URL과 토큰 출력

Step 3: orders 테이블 생성

turso db shell sales-os "
CREATE TABLE IF NOT EXISTS orders (
  id INTEGER PRIMARY KEY AUTOINCREMENT,
  order_id TEXT NOT NULL UNIQUE,
  email TEXT NOT NULL DEFAULT '',
  product TEXT NOT NULL DEFAULT 'unknown',
  amount INTEGER NOT NULL,
  currency TEXT NOT NULL DEFAULT 'KRW',
  payment_key TEXT NOT NULL,
  payment_method TEXT,
  status TEXT NOT NULL DEFAULT 'completed',
  download_token TEXT,
  created_at INTEGER NOT NULL DEFAULT (unixepoch() * 1000)
);
"

Expected: 에러 없음

Step 4: crm_contacts 테이블 생성

turso db shell sales-os "
CREATE TABLE IF NOT EXISTS crm_contacts (
  id INTEGER PRIMARY KEY AUTOINCREMENT,
  email TEXT NOT NULL UNIQUE,
  source TEXT NOT NULL DEFAULT 'richbukae',
  total_purchases INTEGER NOT NULL DEFAULT 0,
  total_spent INTEGER NOT NULL DEFAULT 0,
  first_purchase_at INTEGER,
  last_purchase_at INTEGER,
  created_at INTEGER NOT NULL DEFAULT (unixepoch() * 1000)
);
"

Step 5: 테이블 생성 확인

turso db shell sales-os "SELECT name FROM sqlite_master WHERE type='table';"

Expected: orders, crm_contacts 출력

Step 6: 토큰 발급 및 Vercel env 설정

# 토큰 발급
turso db tokens create sales-os

# Vercel env 설정 (richbukae-store 프로젝트 기준)
cd projects/richbukae-store
vercel env add SALES_OS_DB_URL production
# 값: libsql://sales-os-migkjy.aws-ap-northeast-1.turso.io (실제 URL 입력)
vercel env add SALES_OS_DB_TOKEN production
# 값: 발급된 토큰 입력

Step 7: 커밋

cd /Users/nbs22/\(Claude\)/\(claude\).projects/business-builder
git add projects/richbukae-store/scripts/ 2>/dev/null || true
git commit -m "feat(richbukae): create sales-os Turso DB with orders + crm_contacts tables"

Task 2: Resend 이메일 배송 검증

Files:

  • Read: projects/richbukae-store/src/lib/resend.ts
  • Read: projects/richbukae-store/src/app/emails/ (이메일 템플릿 확인)

Step 1: resend.ts 구현 확인

cat projects/richbukae-store/src/lib/resend.ts

Expected: sendDeliveryEmail 함수 존재, RESEND_API_KEY 사용

Step 2: RESEND_API_KEY Vercel 설정 확인

cd projects/richbukae-store && vercel env ls | grep RESEND

Expected: RESEND_API_KEY 존재 여부 확인

Step 3: RESEND_API_KEY 없으면 루트 .env에서 복사

grep "RESEND" /Users/nbs22/\(Claude\)/\(claude\).projects/business-builder/.env
# 있으면:
cd projects/richbukae-store
vercel env add RESEND_API_KEY production

Step 4: 이메일 발송 curl 테스트 (confirm API 직접 호출)

# Toss 테스트 결제는 실제 paymentKey가 필요하므로 Resend만 단독 테스트
# resend.ts에 sendDeliveryEmail 직접 테스트 스크립트 작성
cat > /tmp/test-email.mjs << 'EOF'
import { Resend } from 'resend';
const resend = new Resend(process.env.RESEND_API_KEY);
const { data, error } = await resend.emails.send({
  from: 'noreply@richbukae.com',
  to: ['test@example.com'],
  subject: '[테스트] richbukae 배송 이메일',
  html: '<h1>테스트 이메일</h1><p>다운로드 링크: https://richbukae.com/api/download?type=pdf&token=test</p>',
});
console.log('Result:', { data, error });
EOF
RESEND_API_KEY=$(grep "^RESEND" /Users/nbs22/\(Claude\)/\(claude\).projects/business-builder/.env | cut -d= -f2) node /tmp/test-email.mjs

Expected: 이메일 ID 반환, 에러 없음

Step 5: resend.ts from 이메일 도메인 확인

grep -n "from" projects/richbukae-store/src/lib/resend.ts

Expected: noreply@richbukae.com 또는 onboarding@resend.dev (테스트용)

⚠️ richbukae.com 도메인이 Resend에 등록되지 않으면 발송 실패. 미등록이면 onboarding@resend.dev로 임시 변경.

Step 6: 커밋 (수정사항 있으면)

git add projects/richbukae-store/src/lib/resend.ts
git commit -m "fix(richbukae): update Resend sender domain for delivery emails"

Task 3: 다운로드 토큰 E2E 테스트

Files:

  • Read: projects/richbukae-store/src/lib/download.ts
  • Read: projects/richbukae-store/src/app/api/download/route.ts

Step 1: 토큰 생성 검증 스크립트 작성

cat > /tmp/test-download-token.mjs << 'EOF'
import crypto from 'crypto';
const DOWNLOAD_SECRET = 'richbukae-download-secret-2026';
const TOKEN_EXPIRY_MS = 7 * 24 * 60 * 60 * 1000;

function generateToken(orderId) {
  const expiry = Date.now() + TOKEN_EXPIRY_MS;
  const payload = `${orderId}:${expiry}`;
  const hmac = crypto.createHmac('sha256', DOWNLOAD_SECRET).update(payload).digest('hex');
  return Buffer.from(`${payload}:${hmac}`).toString('base64url');
}

function verifyToken(token) {
  try {
    const decoded = Buffer.from(token, 'base64url').toString('utf-8');
    const parts = decoded.split(':');
    if (parts.length !== 3) return { valid: false };
    const [orderId, expiryStr, providedHmac] = parts;
    if (Date.now() > parseInt(expiryStr)) return { valid: false };
    const payload = `${orderId}:${expiryStr}`;
    const expected = crypto.createHmac('sha256', DOWNLOAD_SECRET).update(payload).digest('hex');
    return { valid: providedHmac === expected, orderId };
  } catch { return { valid: false }; }
}

const token = generateToken('test-order-123');
console.log('Token:', token);
const result = verifyToken(token);
console.log('Verify:', result);
console.assert(result.valid === true, 'Token should be valid');
console.assert(result.orderId === 'test-order-123', 'OrderId should match');
console.log('✅ Token generation/verification OK');
EOF
node /tmp/test-download-token.mjs

Expected: ✅ Token generation/verification OK

Step 2: 다운로드 API 로컬 테스트 (dev 서버 필요)

cd projects/richbukae-store && npm run dev &
sleep 5

# 유효하지 않은 토큰 → 403 예상
curl -s -o /dev/null -w "%{http_code}" "http://localhost:3000/api/download?type=pdf&token=invalid"
# Expected: 403

# 유효한 토큰 생성 후 테스트
TOKEN=$(node -e "
import crypto from 'crypto';
const s='richbukae-download-secret-2026';
const exp=Date.now()+7*24*60*60*1000;
const p='order-test:'+exp;
const h=crypto.createHmac('sha256',s).update(p).digest('hex');
console.log(Buffer.from(p+':'+h).toString('base64url'));
" --input-type=module)
# PRODUCT_PDF_URL 미설정이면 503, 설정되면 302 예상
curl -s -o /dev/null -w "%{http_code}" "http://localhost:3000/api/download?type=pdf&token=$TOKEN"
# Expected: 503 (URL 미설정) or 302 (설정된 경우)

Step 3: dev 서버 종료

kill $(lsof -ti:3000) 2>/dev/null || true

Task 4: confirm API 통합 테스트 (Toss 테스트 결제 시뮬레이션)

Files:

  • Read: projects/richbukae-store/src/app/api/payment/confirm/route.ts

⚠️ 실제 Toss paymentKey는 결제 UI를 통해서만 발급됨. 여기서는 Toss API mock 응답으로 로직 검증.

Step 1: confirm API 로직 단독 검증

# TOSS_SECRET_KEY 없이 테스트 (fallback 키 사용)
# 로컬 서버 기동
cd projects/richbukae-store && npm run dev &
sleep 5

# 잘못된 paymentKey → Toss API 실패 응답 예상
curl -s -X POST "http://localhost:3000/api/payment/confirm" \
  -H "Content-Type: application/json" \
  -d '{"paymentKey":"invalid","orderId":"test-order-001","amount":67000,"email":"test@example.com"}' \
  | python3 -m json.tool
# Expected: {"error": "..."} with error message from Toss

Expected: 에러 JSON 반환, 200 아님

Step 2: 필수 파라미터 누락 테스트

curl -s -X POST "http://localhost:3000/api/payment/confirm" \
  -H "Content-Type: application/json" \
  -d '{"orderId":"test-order-001"}' \
  | python3 -m json.tool
# Expected: {"error": "paymentKey, orderId, amount are required"}, status 400

Step 3: SALES_OS DB 연결 확인 (env 설정 후)

# SALES_OS_DB_URL 설정 후 더미 DB 삽입 테스트
SALES_OS_DB_URL="libsql://sales-os-..." SALES_OS_DB_TOKEN="..." node -e "
import { createClient } from '@libsql/client';
const db = createClient({ url: process.env.SALES_OS_DB_URL, authToken: process.env.SALES_OS_DB_TOKEN });
const r = await db.execute('SELECT COUNT(*) as cnt FROM orders');
console.log('orders count:', r.rows[0].cnt);
" --input-type=module

Expected: orders count: 0 (빈 테이블)

Step 4: dev 서버 종료 + 커밋

kill $(lsof -ti:3000) 2>/dev/null || true
git add -A
git commit -m "test(richbukae): verify payment→download flow with test key"

Task 5: 빌드 검증 + Vercel 배포

Step 1: 프로덕션 빌드 확인

cd projects/richbukae-store && npm run build 2>&1 | tail -20

Expected: ✓ Compiled successfully

Step 2: Vercel Preview 배포

cd projects/richbukae-store && vercel 2>&1 | tail -10

Expected: Preview URL 출력

Step 3: Preview URL에서 E2E 스모크 테스트

PREVIEW_URL="https://richbukae-store-xxx.vercel.app"

# 체크아웃 페이지 로드
curl -s -o /dev/null -w "%{http_code}" "$PREVIEW_URL/checkout"
# Expected: 200

# 결제 확인 API
curl -s -X POST "$PREVIEW_URL/api/payment/confirm" \
  -H "Content-Type: application/json" \
  -d '{"paymentKey":"test","orderId":"smoke-test","amount":1000}' \
  | python3 -m json.tool
# Expected: 에러 응답 (Toss 테스트 paymentKey 미사용이므로) — 500/400 아닌 Toss 에러

Step 4: 커밋 (최종)

git add -A
git commit -m "feat(richbukae): payment-delivery flow complete — ready for production keys"

완료 체크리스트

  • SALES_OS DB (sales-os) Turso에 생성됨
  • orders, crm_contacts 테이블 생성됨
  • SALES_OS_DB_URL, SALES_OS_DB_TOKEN Vercel env 설정됨
  • Resend RESEND_API_KEY 설정됨
  • 이메일 발송 테스트 성공
  • 다운로드 토큰 생성/검증 테스트 PASS
  • confirm API 에러 처리 검증됨
  • 빌드 PASS
  • Vercel Preview 배포됨

CEO 블로킹 해소 후 즉시 처리 항목 (Plan 2)

  • TOSS_SECRET_KEY (프로덕션) → Vercel env add
  • NEXT_PUBLIC_TOSS_CLIENT_KEY (프로덕션) → Vercel env add
  • PRODUCT_PDF_URL, PRODUCT_SKILLS_URL, PRODUCT_NOTION_URL → Vercel env add
  • Vercel --prod 배포 (CEO 승인 후)
plans/2026/02/25/richbukae-payment-delivery-flow.md