# 토스페이먼츠 실전 적용 가이드 > 현재 플랫폼은 **테스트(모의 결제) 모드**로 동작합니다. > 사업자 등록 완료 후 아래 절차를 따라 실제 결제로 전환하세요. --- ## 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 ``` ### 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
; } ``` ### 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/건 별도 가이드 예정.