Files
webflash/platform/TOSS_PAYMENT_GUIDE.md
root 9a9967bed8 feat: mock payment flow + flash token page (test mode)
- Mock purchase: order create → mock-pay → FlashToken issued instantly (no real billing)
- Flash page (/flash/:token): esp-web-tools integration, token state display, consume on complete
- Orders route: create/mock-pay/me/refund with full audit logging
- Flash route: GET validate, GET manifest (esp-web-tools compatible), POST consume
- MinIO file proxy (/api/files/*): browser CORS solved, firmware served through backend
- Schema: chipFamily on Project, flashOffset on ProjectFile
- ProjectNew: chipFamily selector + firmware flash offset option
- MyOrders: real order list with flash token status and buttons
- Dockerfile: prisma db push (no migration files needed for dev)
- TOSS_PAYMENT_GUIDE.md: step-by-step guide for real payment after business registration

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

268 lines
8.1 KiB
Markdown

# 토스페이먼츠 실전 적용 가이드
> 현재 플랫폼은 **테스트(모의 결제) 모드**로 동작합니다.
> 사업자 등록 완료 후 아래 절차를 따라 실제 결제로 전환하세요.
---
## 1단계 — 사업자 등록
토스페이먼츠는 사업자 등록증이 필요합니다.
| 구분 | 내용 |
|------|------|
| 개인사업자 | 홈택스에서 사업자 등록 (즉시 발급 가능) |
| 법인사업자 | 법인 설립 후 사업자 등록 |
| 필요 서류 | 사업자등록증, 통장 사본, 신분증 |
---
## 2단계 — 토스페이먼츠 계정 및 API 키 발급
### 2-1. 개발자 센터 가입
1. [https://developers.tosspayments.com](https://developers.tosspayments.com) 접속
2. 회원가입 후 **내 개발 정보****API 키**
3. **테스트 키** (무료, 사업자 없어도 사용 가능):
- `test_ck_...` — 클라이언트 키
- `test_sk_...` — 시크릿 키
### 2-2. 실 결제 키 발급 (사업자 등록 후)
1. 토스페이먼츠 파트너 신청: [https://www.tosspayments.com/contact](https://www.tosspayments.com/contact)
2. 심사 완료 후 **라이브 키** 발급:
- `live_ck_...` — 클라이언트 키
- `live_sk_...` — 시크릿 키
3. 수수료: 카드 기준 약 **2.2%** (매출 규모에 따라 협의 가능)
---
## 3단계 — 플랫폼에 토스 SDK 적용
### 3-1. 환경 변수 업데이트
```env
# 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`)
```html
<!-- 기존 -->
<!-- 없음 -->
<!-- 추가 -->
<script src="https://js.tosspayments.com/v2/standard"></script>
```
### 3-3. 프론트엔드 — 결제창 호출 (`ProductDetail.jsx`)
현재 `handleBuy` 함수의 `mock-pay` 부분을 아래로 교체하세요:
```javascript
// ❌ 기존 모의 결제 코드
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`)
```env
VITE_TOSS_CLIENT_KEY=live_ck_YOUR_LIVE_KEY
```
### 3-5. 결제 성공 페이지 추가 (`pages/PaymentSuccess.jsx`)
```javascript
// 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`)
```javascript
// 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 라우트 추가
```javascript
// platform/backend/src/index.js 에 추가
app.use('/api/payments', require('./routes/payments'));
```
---
## 4단계 — 환불 처리 (토스 API)
현재 모의 환불은 DB만 변경합니다. 실결제 환불 시:
```javascript
// 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단계 — 토스 대시보드 설정
실키 발급 후 [토스페이먼츠 대시보드](https://app.tosspayments.com)에서:
| 설정 항목 | 값 |
|----------|-----|
| 결제 성공 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는 사업자 없이도 개인으로 가입 가능합니다.
```bash
npm install stripe
```
주요 차이점:
- Stripe는 USD 기준 → 원화 환율 처리 필요
- Payment Intent 방식 사용
- 수수료: 약 2.9% + $0.30/건
별도 가이드 예정.