참고

주문 흐름

생성부터 취소·환불까지 주문 상태 전이를 시퀀스로 고정해요.

주문 생성부터 결제, 검증, 웹훅 처리까지 전체 시퀀스를 이해해요.

개별 API 사용법은 주문서 요청, 결제 검증에서 확인해요. 이 문서는 전체 흐름을 한눈에 보여줘요.

개념·정책 배경 (먼저 정해야 할 것)

주문 구현 전에 블로그 가이드를 봐요.

결제만 연동하려면

자체 주문 시스템이 있고 결제만 붙이려면 결제 흐름을 참고해요. 결제 과정은 동일하고, 이 문서는 Bootpay가 주문·이행·배송까지 관리하는 커머스 API 흐름이에요.

전체 시퀀스

참여자: 고객 | 가맹점 프론트 | 가맹점 서버 | Bootpay | PG사

단계 흐름 유형 비고
1 고객 -> 가맹점 프론트: 상품 선택 실선
2 가맹점 프론트 -> 가맹점 서버: 주문 생성 요청 실선
3 가맹점 서버 -> 가맹점 프론트: order_id 반환 점선
4 가맹점 프론트 -> Bootpay: 결제위젯/체크아웃 호출 실선
5 Bootpay -> PG사: 결제 요청 실선
6 PG사 -> Bootpay: 결제 결과 점선
7 Bootpay -> 가맹점 프론트: receipt_id 반환 점선
8 가맹점 프론트 -> 가맹점 서버: receipt_id 전달 실선
9 가맹점 서버 -> Bootpay: 결제 검증 (GET) 실선
10 Bootpay -> 가맹점 서버: 검증 결과 점선
11 가맹점 서버: DB 저장 + 상태 변경 실선 DB 저장 + 상태 변경
12 가맹점 서버 -> 가맹점 프론트: 결과 반환 점선
13 가맹점 프론트 -> 고객: 완료 페이지 점선
14 Bootpay -> 가맹점 서버: 웹훅 (order.done) 실선
15 가맹점 서버: 후처리 실선 후처리 (이메일 등)

Bootpay 내부 주문 3단계

Bootpay는 내부적으로 주문을 3단계 모델​​로 관리해요. API 응답과 웹훅 데이터를 이해하는 데 도움이 돼요.

흐름 OrderPre / 결제 전 임시 -> Order / 결제 확정 : 결제 완료 Order / 결제 확정 -> OrderPurchase / 이행/배송 : 판매자별 분리

노드

ID 라벨 위치 타입
A OrderPre / 결제 전 임시 col 0, row 0
B Order / 결제 확정 col 1, row 0 success
C OrderPurchase / 이행/배송 col 2, row 0
  • OrderPre: 결제위젯이 열리기 전 생성되는 임시 주문. 제한 시간(기본 31분) 내에 결제하지 않으면 자동 만료돼요.
  • Order: 결제가 완료되면 OrderPre가 Order로 확정돼요. 이 시점부터 order_id, order_number가 발급돼요.
  • OrderPurchase: 판매자(셀러)별 이행 단위예요. 멀티셀러 쇼핑몰이면 하나의 Order에 여러 OrderPurchase가 생길 수 있어요.

Bootpay 주문 상태코드 참조

가맹점은 아래 상태코드를 직접 관리할 필요 없지만, API 응답의 status 필드가 이 값을 따라요.

Bootpay 상태 코드 가맹점 매핑 예시 설명
READY 0 pending 주문 생성됨 (결제 대기)
DEPOSIT_WAIT 1 pending 무통장입금 대기
PAY_DONE 2 paid 결제 완료
BUY_CONFIRM 8 completed 구매 확정
CANCEL_DONE 10 refunded 전체 취소 완료
RETURN_DONE 11 returned 반품 완료
EXCHANGE_DONE 12 exchanged 교환 완료
CLOSED -1 expired 미결제 만료

단계별 상세

1단계: 주문 예비 생성 (가맹점 서버)

결제 전에 가맹점 DB에 주문을 먼저 만들어요.

// POST /api/orders — 가맹점 자체 API
app.post('/api/orders', async (req, res) => {
    const { user_id, products } = req.body

    // 가격은 반드시 백엔드에서 계산 (프론트엔드 금액을 믿지 않음)
    const totalPrice = await calculatePrice(products)

    const order = await db.orders.create({
        user_id,
        total_amount: totalPrice,
        status: 'pending',    // 아직 결제 전
        items: products
    })

    res.json({
        order_id: order.id,
        price: totalPrice,
        order_name: formatOrderName(products)
    })
})javascript
가격은 서버에서 계산

프론트엔드에서 보낸 금액을 그대로 사용하면 금액 변조 위험이 있어요. 반드시 백엔드에서 상품 가격을 조회하여 계산해요.

2단계: 결제 요청 (프론트엔드)

서버에서 받은 정보로 결제위젯 또는 체크아웃을 호출해요.

// 결제위젯 방식
Bootpay.requestPayment({
    client_key: 'APPLICATION_ID',
    price: orderData.price,           // 서버에서 받은 금액
    order_name: orderData.order_name,
    order_id: String(orderData.order_id),  // 가맹점 주문 ID
    pg: '나이스페이',
    method: '카드',
    user: { id: userId, username: userName }
})
.then(response => {
    // 3단계: 서버로 검증 요청
    verifyPayment(response.receipt_id, orderData.order_id)
})
.catch(error => {
    console.error('결제 실패:', error)
})javascript

3단계: 결제 검증 (가맹점 서버)

가장 중요한 단계​​예요. 프론트엔드 결과만 믿으면 안 돼요.

app.post('/api/orders/verify', async (req, res) => {
    const { receipt_id, order_id } = req.body

    // 가맹점 DB에서 주문 조회
    const order = await db.orders.findByPk(order_id)
    if (!order) return res.status(404).json({ error: '주문을 찾을 수 없음' })

    // Bootpay에서 결제 정보 조회
    const receipt = await Bootpay.receiptPayment(receipt_id)

    // 검증: 상태 + 금액
    if (receipt.status === 1 && receipt.price === order.total_amount) {
        // ✅ 검증 성공 → DB 업데이트
        await db.orders.update(
            {
                status: 'paid',
                bootpay_receipt_id: receipt_id,
                paid_at: new Date()
            },
            { where: { id: order_id } }
        )
        res.json({ success: true })
    } else {
        // ❌ 검증 실패 → 결제 취소
        await Bootpay.cancelPayment({
            receipt_id,
            cancel_price: receipt.price,
            cancel_reason: '금액 위변조 의심'
        })
        res.json({ success: false, message: '검증 실패' })
    }
})javascript

4단계: 웹훅 수신 (비동기 후처리)

결제 검증과 별개로, Bootpay가 웹훅을 발송해요. 후처리 로직에 활용해요.

// 웹훅 수신 후 처리 (상세: /webhook/guide)
app.post('/webhooks/bootpay', async (req, res) => {
    res.status(200).json({ success: true })  // 즉시 200 반환

    const { event, data } = req.body
    if (event === 'order.done') {
        // 이메일 발송, 재고 차감, 포인트 적립 등
        await sendOrderConfirmEmail(data.order_number)
        await decrementStock(data.order_number)
    }
})javascript

주문 상태 관리

가맹점 DB의 주문 상태는 아래 흐름을 따른다:

상태

ID 라벨 위치 타입
start col 0, row 1 initial
pending pending col 1, row 1
paid paid col 2, row 1 active
confirmed confirmed col 3, row 1
completed completed col 4, row 1 active
refunded refunded col 3, row 2 end
expired expired col 1, row 2 end

전이 -> pending pending -> paid paid -> confirmed confirmed -> completed paid -> refunded : 환불 pending -> expired : 결제 안 함

상태 설명 변경 시점
pending 주문 생성됨, 결제 대기 주문 예비 생성 시
paid 결제 완료, 검증 성공 서버 검증 성공 후
confirmed 주문 확인 (출고 준비) 가맹점이 수동 또는 자동으로
completed 배송/서비스 완료 배송 완료 확인 후
refunded 환불 완료 웹훅 order.cancelled 수신 시
expired 미결제로 만료 일정 시간 후 자동 (정책에 따라)

디지털 상품이면 paidcompleted를 바로 처리해도 돼요. 물리적 배송이 있으면 confirmed(출고) 단계를 거쳐요.

취소/환불 흐름

주문 취소는 전체 취소​​와 부분 취소​(금액 또는 상품 단위)로 나뉘어요.

전체 취소 vs 부분 취소

유형 설명 사용 시점
전체 취소 주문 전액 환불 주문 전체를 취소할 때
부분 취소 (금액) 지정 금액만 환불 쿠폰 할인 차감 등 금액 조정
부분 취소 (상품) 특정 상품만 취소 여러 상품 중 일부만 환불

취소 요청 → 승인 → 완료 흐름

참여자: 가맹점 서버 | Bootpay

단계 흐름 유형 비고
1 가맹점 서버 -> Bootpay: 취소 요청 (POST) 실선
2 Bootpay: 취소 금액 계산 · 자동승인 확인 실선 취소 금액 계산 · 자동승인 확인
3 Bootpay -> 가맹점 서버: 승인 결과 점선
4 Bootpay: PG사 환불 · 상태 변경 실선 PG사 환불 · 상태 변경
5 Bootpay -> 가맹점 서버: 웹훅 (order.cancelled) 실선
6 가맹점 서버: 후처리: 재고 복구 · 고객 안내 실선 재고 복구 · 서비스 비활성화 · 안내 이메일

취소 완료 웹훅 처리

// 웹훅 수신: order.cancelled
async function handleOrderCancelled(data) {
    const { order_number, cancel_price, remain_price } = data

    // 1. 주문 상태 업데이트
    if (remain_price === 0) {
        // 전체 취소
        await db.orders.update(
            { status: 'refunded' },
            { where: { bootpay_order_number: order_number } }
        )
    } else {
        // 부분 취소 — 잔액이 남아있으면 주문은 유지
        await db.orders.update(
            { total_amount: remain_price, status: 'partially_refunded' },
            { where: { bootpay_order_number: order_number } }
        )
    }

    // 2. 재고 복구 (전체 취소 시)
    // 3. 환불 안내 이메일
}javascript
부분 취소 시 주의

부분 취소가 발생하면 주문 자체는 유효해요. statusrefunded로 바꾸면 안 되고, 잔여 금액을 기준으로 서비스를 유지해야 해요.

이행(Fulfillment) 흐름

물리적 배송이 있는 커머스에서는 결제 완료 후 이행 단계​​를 관리해야 해요.

이행 상태 흐름

흐름 ready / (발주대기) -> confirmed / (발주확인) confirmed / (발주확인) -> shipped / (배송중) shipped / (배송중) -> delivered / (배송완료)

노드

ID 라벨 위치 타입
A ready / (발주대기) col 0, row 0
B confirmed / (발주확인) col 1, row 0
C shipped / (배송중) col 2, row 0 process
D delivered / (배송완료) col 3, row 0 success
이행 상태 Bootpay 코드 가맹점 액션
ready 0~1 발주 접수. 상품 준비 시작
confirmed 2 발주 확인 완료
shipped 4 배송 시작. 운송장 번호 등록
delivered 5 배송 완료. 구매 확정 대기

배송 정보 연동 패턴

// 발주 확인 + 배송 등록 (가맹점 서버에서)
async function shipOrder(orderNumber, trackingInfo) {
    // Bootpay에 배송 정보 전달
    await Bootpay.updateOrderPurchase(orderNumber, {
        delivery_company_code: trackingInfo.company_code,  // 택배사 코드
        tracking_number: trackingInfo.tracking_number       // 운송장 번호
    })

    // 가맹점 DB 업데이트
    await db.order_fulfillments.update(
        {
            progress_status: 'shipped',
            shipped_at: new Date(),
            delivery_company: trackingInfo.company_name,
            tracking_number: trackingInfo.tracking_number
        },
        { where: { bootpay_order_number: orderNumber } }
    )
}javascript
디지털 상품

배송이 없는 디지털 상품이라면 이행 단계를 건너뛰고, 결제 완료 시 즉시 서비스를 활성화하면 돼요.

흔한 실수

1검증 없이 프론트엔드 결과만 믿기

// ❌ 절대 하면 안 됨
Bootpay.requestPayment({ ... })
.then(response => {
    // 프론트엔드 결과만으로 주문 완료 처리
    db.orders.update({ status: 'paid' })  // 위험!
})javascript

프론트엔드 응답은 위변조가 가능해요. 반드시 백엔드에서 결제 검증을 수행해요.

2웹훅에만 의존하기

// ❌ 위험 — 웹훅이 지연되면 고객이 결제했는데 서비스를 못 씀
// 웹훅만으로 주문 완료 처리javascript

동기 검증(3단계)​​이 메인 흐름이고, 웹훅은 보조 수단​(이메일, 재고 등 후처리)예요.

3가격을 프론트엔드에서 계산

// ❌ 위험 — 프론트엔드에서 할인 적용
const price = product.price * 0.8  // 20% 할인
Bootpay.requestPayment({ price })  // 변조 가능javascript

가격 계산은 반드시 서버에서 수행하고, 결제 검증 시 서버의 가격과 비교해요.