Files
esp32DIY_web/TOSS_PAYMENT_GUIDE.md
root 182782f271 feat: ESP32 DIY platform Phase 1 — marketplace with mock payment & flash token flow
- React+Vite frontend (dark theme, role-based routing: admin/seller/buyer)
- Express+Prisma+PostgreSQL backend with JWT auth and audit logging
- MinIO object storage with backend proxy for CORS-free firmware delivery
- Mock payment flow (order → mock-pay → FlashToken) for pre-business testing
- FlashToken lifecycle: issue → validate → esp-web-tools manifest → consume
- Admin approval workflow for project/product submissions
- Toss Payments integration guide (TOSS_PAYMENT_GUIDE.md) for live keys

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-20 06:43:08 +09:00

8.1 KiB

토스페이먼츠 실전 적용 가이드

현재 플랫폼은 테스트(모의 결제) 모드로 동작합니다.
사업자 등록 완료 후 아래 절차를 따라 실제 결제로 전환하세요.


1단계 — 사업자 등록

토스페이먼츠는 사업자 등록증이 필요합니다.

구분 내용
개인사업자 홈택스에서 사업자 등록 (즉시 발급 가능)
법인사업자 법인 설립 후 사업자 등록
필요 서류 사업자등록증, 통장 사본, 신분증

2단계 — 토스페이먼츠 계정 및 API 키 발급

2-1. 개발자 센터 가입

  1. https://developers.tosspayments.com 접속
  2. 회원가입 후 내 개발 정보API 키
  3. 테스트 키 (무료, 사업자 없어도 사용 가능):
    • test_ck_... — 클라이언트 키
    • test_sk_... — 시크릿 키

2-2. 실 결제 키 발급 (사업자 등록 후)

  1. 토스페이먼츠 파트너 신청: https://www.tosspayments.com/contact
  2. 심사 완료 후 라이브 키 발급:
    • live_ck_... — 클라이언트 키
    • live_sk_... — 시크릿 키
  3. 수수료: 카드 기준 약 2.2% (매출 규모에 따라 협의 가능)

3단계 — 플랫폼에 토스 SDK 적용

3-1. 환경 변수 업데이트

# platform/.env
TOSS_CLIENT_KEY=live_ck_YOUR_LIVE_KEY    # test_ck_ → live_ck_ 로 교체
TOSS_SECRET_KEY=live_sk_YOUR_LIVE_KEY    # test_sk_ → live_sk_ 로 교체
BASE_URL=https://your-domain.com         # 실제 도메인으로 변경

3-2. 프론트엔드 — 토스 SDK 로드 (platform/frontend/index.html)

<!-- 기존 -->
<!-- 없음 -->

<!-- 추가 -->
<script src="https://js.tosspayments.com/v2/standard"></script>

3-3. 프론트엔드 — 결제창 호출 (ProductDetail.jsx)

현재 handleBuy 함수의 mock-pay 부분을 아래로 교체하세요:

// ❌ 기존 모의 결제 코드
const payRes = await api.post(`/orders/${orderId}/mock-pay`);
navigate(`/flash/${payRes.data.flashToken}`);

// ✅ 토스 실결제 코드로 교체
const tossPayments = TossPayments(import.meta.env.VITE_TOSS_CLIENT_KEY);
const payment = tossPayments.payment({ customerKey: user.id });

await payment.requestPayment({
  method: 'CARD',
  amount: {
    currency: 'KRW',
    value: product.price,
  },
  orderId,                          // 서버에서 생성한 주문 ID
  orderName: product.project.title,
  successUrl: `${window.location.origin}/payment/success`,
  failUrl:    `${window.location.origin}/payment/fail`,
  customerEmail: user.email,
  customerName:  user.nickname,
});
// 위 코드 실행 시 토스 결제창으로 리다이렉트됨

3-4. Vite 환경변수 추가 (platform/frontend/.env)

VITE_TOSS_CLIENT_KEY=live_ck_YOUR_LIVE_KEY

3-5. 결제 성공 페이지 추가 (pages/PaymentSuccess.jsx)

// platform/frontend/src/pages/PaymentSuccess.jsx
import { useEffect } from 'react';
import { useNavigate, useSearchParams } from 'react-router-dom';
import api from '../api/client';

export default function PaymentSuccess() {
  const [params] = useSearchParams();
  const navigate = useNavigate();

  useEffect(() => {
    const paymentKey = params.get('paymentKey');
    const orderId    = params.get('orderId');
    const amount     = params.get('amount');

    // 서버에 결제 승인 요청
    api.post('/payments/toss/confirm', { paymentKey, orderId, amount })
      .then(r => navigate(`/flash/${r.data.flashToken}`))
      .catch(() => navigate('/payment/fail'));
  }, []);

  return <div className="spinner" />;
}

3-6. 백엔드 — 토스 결제 승인 라우트 추가 (routes/payments.js)

// platform/backend/src/routes/payments.js
const router = require('express').Router();
const prisma  = require('../config/db');
const { requireAuth } = require('../middleware/auth');
const { v4: uuidv4 }  = require('uuid');

// POST /api/payments/toss/confirm
router.post('/toss/confirm', requireAuth, async (req, res) => {
  const { paymentKey, orderId, amount } = req.body;

  // 1. DB에서 주문 확인
  const order = await prisma.order.findFirst({
    where: { tossOrderId: orderId, buyerId: req.user.id, status: 'pending' },
  });
  if (!order || order.amount !== parseInt(amount)) {
    return res.status(400).json({ error: '주문 정보가 일치하지 않습니다' });
  }

  // 2. 토스 서버에 결제 승인 요청
  const tossRes = await fetch('https://api.tosspayments.com/v1/payments/confirm', {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json',
      // 시크릿 키를 Base64로 인코딩
      Authorization: `Basic ${Buffer.from(process.env.TOSS_SECRET_KEY + ':').toString('base64')}`,
    },
    body: JSON.stringify({ paymentKey, orderId, amount }),
  });

  if (!tossRes.ok) {
    const err = await tossRes.json();
    return res.status(400).json({ error: err.message });
  }

  // 3. 주문 상태 업데이트 + FlashToken 발급
  const expiresAt = new Date(Date.now() + 30 * 24 * 60 * 60 * 1000);
  const [, flashToken] = await prisma.$transaction([
    prisma.order.update({
      where: { id: order.id },
      data: { status: 'paid', paidAt: new Date(), paymentKey },
    }),
    prisma.flashToken.create({
      data: { orderId: order.id, expiresAt },
    }),
    prisma.product.update({
      where: { id: order.productId },
      data: { totalSales: { increment: 1 } },
    }),
  ]);

  res.json({ flashToken: flashToken.token });
});

// POST /api/payments/toss/webhook — 토스 웹훅 (결제 상태 변경 알림)
router.post('/toss/webhook', async (req, res) => {
  // 웹훅 서명 검증 (토스 대시보드에서 시크릿 설정)
  const signature = req.headers['toss-payments-signature'];
  // TODO: HMAC-SHA256 서명 검증 구현

  const { eventType, data } = req.body;
  if (eventType === 'PAYMENT_STATUS_CHANGED' && data.status === 'CANCELED') {
    await prisma.order.updateMany({
      where: { paymentKey: data.paymentKey },
      data: { status: 'cancelled' },
    });
  }
  res.json({ success: true });
});

module.exports = router;

3-7. index.js에 payments 라우트 추가

// platform/backend/src/index.js 에 추가
app.use('/api/payments', require('./routes/payments'));

4단계 — 환불 처리 (토스 API)

현재 모의 환불은 DB만 변경합니다. 실결제 환불 시:

// routes/orders.js refund 엔드포인트에 추가
const tossRefundRes = await fetch(
  `https://api.tosspayments.com/v1/payments/${order.paymentKey}/cancel`,
  {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json',
      Authorization: `Basic ${Buffer.from(process.env.TOSS_SECRET_KEY + ':').toString('base64')}`,
    },
    body: JSON.stringify({ cancelReason: req.body.reason || '사용자 요청' }),
  }
);
if (!tossRefundRes.ok) throw new Error('토스 환불 API 실패');

5단계 — 토스 대시보드 설정

실키 발급 후 토스페이먼츠 대시보드에서:

설정 항목
결제 성공 URL https://your-domain.com/payment/success
결제 실패 URL https://your-domain.com/payment/fail
웹훅 URL https://your-domain.com/api/payments/toss/webhook
허용 도메인 your-domain.com

테스트 → 운영 전환 요약

항목 테스트 모드 (현재) 운영 모드 (전환 후)
결제 처리 mock-pay (즉시) 토스 결제창 → confirm API
API 키 없음 live_ck_, live_sk_
실제 청구 없음 있음
환불 DB만 변경 토스 cancel API 호출
코드 변경 - ProductDetail.jsx, payments.js 추가, index.js 라우트 등록

해외 결제 (Stripe) — 차후 적용

Stripe는 사업자 없이도 개인으로 가입 가능합니다.

npm install stripe

주요 차이점:

  • Stripe는 USD 기준 → 원화 환율 처리 필요
  • Payment Intent 방식 사용
  • 수수료: 약 2.9% + $0.30/건

별도 가이드 예정.