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

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

---

## 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/건

별도 가이드 예정.
