참고

데이터 모델 설계

저장할 값과 저장하지 말아야 할 값을 먼저 나눠봐요.

이 문서는 “이 스키마 그대로 만들라”는 정답지가 아니에요. Bootpay 결제 SDK를 붙일 때 가맹점 서버가 어떤 값을 확인하고, 어떤 값을 DB에 남겨야 하는지​​를 예시 코드와 테이블로 표현한 안내예요.

이미 주문 테이블이 있다면 그대로 쓰고, 아래 필드는 서비스 상태·정산·CS·취소 처리에 필요한 만큼만 매핑해요.

먼저 나눌 책임

결제 API는 결제 트랜잭션을 처리하고, 주문·상품·회원·구독 회차 같은 서비스 로직은 가맹점 서버가 관리해요.

구분 Bootpay가 제공하는 값/기능 가맹점 서버가 해야 할 일
단건 결제 receipt_id, 결제 상태, 금액, 결제수단, 취소 API, 웹훅 내부 주문과 매핑, 금액·상태 검증, 주문 상태 저장
자동결제 billing_key, 빌링키 조회, 빌링키 결제 요청 빌링키 저장, 회원/구독과 연결, 청구 시점 결정
예약결제 예약 등록·조회·취소 API, 실행 결과 웹훅 예약 ID 저장, 예약 상태 관리, 실행 결과 반영
카드 원본 정보 PG/카드사가 처리 카드번호 전체·CVC·비밀번호·유효기간을 저장하지 않음
커머스 SDK를 쓰는 경우

주문(Order)·구독(OrderSubscription) 같은 비즈니스 엔티티까지 Bootpay 쪽 모델로 관리하려면 결제 SDK가 아니라 커머스 SDK를 봐요 → 데이터 모델

가맹점 DB에 보통 남기는 값

Bootpay 값 가맹점 저장 위치 예시 저장 이유
receipt_id orders.bootpay_receipt_id 또는 order_payments.receipt_id 결제 조회·취소·웹훅 처리 기준
order_id orders.order_id Bootpay 결제 건과 내부 주문 매핑
price orders.paid_amount 결제 금액 기록과 정산/CS 확인. 원금이 아니라 남은 금액(원금 − 누적 취소금액) 이에요
status orders.payment_status 또는 원문 JSON Bootpay 결제 상태 확인용
method_symbol orders.payment_method 결제수단 표시·운영 분석
pg orders.pg PG별 장애·정산 확인. alias 가 아니라 PG 표시명​(예: 나이스페이먼츠)이에요
purchased_at orders.paid_at 결제 완료 시각 기록
cancelled_price orders.cancelled_amount 부분 취소 금액 추적
billing_key billing_keys.billing_key 자동결제·예약결제 요청 시 사용
billing_data.card_company billing_keys.card_company 마이페이지 표시용
billing_data.card_no billing_keys.masked_card_no 마스킹 카드번호 표시용
주문 상태와 결제 상태는 분리해요

Bootpay의 status는 결제 상태예요. 서비스의 주문 상태(pending, paid, cancelled, shipped 등)와 1:1로 복사하기보다, 원문 결제 상태를 참고해 가맹점 주문 상태를 별도로 전이​​시키는 편이 안전해요.

`pg` 값으로 코드 분기를 하지 않아요

pg 는 요청에 쓰는 alias(nicepay, kcp …)가 아니라 관리자에 등록된 PG 표시명​​이 그대로 내려와요. 표시명은 운영 중에 바뀔 수 있으니 화면 표기·정산 확인 용도로만 쓰고, 서버 분기 조건으로는 사용하지 않아요.

주문 테이블에 결제 필드를 더하는 예시

아래 SQL은 기존 주문 테이블에 결제 식별값을 붙이는 예시​​예요. 실제 컬럼명, 금액 타입, 인덱스, 상태값은 서비스의 주문 모델에 맞게 바꿔요.

-- 예시: 기존 orders 테이블에 결제 처리에 필요한 필드 추가
ALTER TABLE orders
    ADD COLUMN bootpay_receipt_id VARCHAR(100),   -- Bootpay 영수증 ID
    ADD COLUMN paid_amount        INTEGER,        -- 실제 결제 금액(KRW 기준 예시)
    ADD COLUMN payment_status     INTEGER,        -- Bootpay 원문 status 저장용
    ADD COLUMN payment_method     VARCHAR(30),    -- card, bank, vbank 등
    ADD COLUMN pg                 VARCHAR(50),
    ADD COLUMN paid_at            DATETIME,
    ADD INDEX idx_bootpay_receipt (bootpay_receipt_id);sql
주문 테이블이 아직 없다면 — 최소 예시
CREATE TABLE orders (
    id                 BIGINT PRIMARY KEY AUTO_INCREMENT,
    user_id            BIGINT NOT NULL,
    order_id           VARCHAR(100) NOT NULL,        -- 가맹점 주문번호
    order_name         VARCHAR(200),
    total_amount       INTEGER NOT NULL,             -- 가맹점이 계산한 주문 금액
    order_status       VARCHAR(30) DEFAULT 'pending',

    bootpay_receipt_id VARCHAR(100),
    paid_amount        INTEGER,
    payment_status     INTEGER,
    payment_method     VARCHAR(30),
    pg                 VARCHAR(50),
    paid_at            DATETIME,

    created_at         DATETIME DEFAULT CURRENT_TIMESTAMP,

    INDEX idx_user (user_id),
    UNIQUE INDEX idx_order_id (order_id),
    INDEX idx_bootpay_receipt (bootpay_receipt_id)
);sql

저장 시점​​은 결제 흐름에 따라 달라요.

흐름 저장 기준
클라이언트 승인 done 이후 서버에서 receipt_id로 결제 조회 → price, status 검증 후 저장
서버 승인 confirm 이후 서버에서 금액·상태를 확인하고 승인 API 호출 → 승인 성공 후 저장
가상계좌 status: 5 발급 시점에는 입금 대기 상태로 저장 → 입금 완료 웹훅(status: 1)에서 결제 완료로 전이
취소·환불 취소 API 응답 또는 웹훅 수신 후 취소 금액·주문 상태 업데이트
// 예시: 결제 조회/검증 성공 후 내부 주문 상태를 갱신
// price 는 잔액이므로, 최초 검증 시점의 원금은 price + cancelled_price 로 복원해요
const originalPrice = receipt.price + (receipt.cancelled_price ?? 0)

if (receipt.status === 1 && originalPrice === order.total_amount) {
    await db.orders.update({
        bootpay_receipt_id: receipt.receipt_id,
        paid_amount: originalPrice,          // 원금을 남기고 싶다면 복원값을 저장해요
        cancelled_amount: receipt.cancelled_price ?? 0,
        payment_status: receipt.status,
        payment_method: receipt.method_symbol,
        pg: receipt.pg,
        order_status: 'paid',
        paid_at: receipt.purchased_at ? new Date(receipt.purchased_at) : new Date(),
    }, { where: { order_id: order.order_id } })
}javascript
부분취소 이후에는 `price !== total_amount` 가 정상이에요

응답의 price원금 − 누적 취소금액​, 즉 남은 금액이에요. 부분취소가 한 번이라도 있었다면 주문 금액과 달라지는 게 정상이라, price === total_amount 로만 검증하면 취소 웹훅을 다시 처리할 때 실패해요.

paid_amount 에 무엇을 저장할지 먼저 정해요.

  • 원금을 남긴다price + cancelled_price 로 복원해 저장하고, 취소 금액은 별도 컬럼(cancelled_amount)에 둬요.
  • 잔액을 남긴다price 를 그대로 저장하되, 이 값은 취소가 생길 때마다 줄어든다는 점을 CS·정산 로직에서 감안해요.

결제 조회 응답에서 확인할 값

결제 조회 응답은 주문 확정의 근거로 사용해요. 모든 필드를 저장할 필요는 없고, 주문 처리·CS·취소에 필요한 값만 남겨요.

응답 필드 처리 기준 용도
receipt_id 저장 결제 조회, 취소, 웹훅 중복 처리 기준
order_id 저장/비교 내부 주문번호와 일치하는지 확인
price 비교 + 저장 남은 금액(원금 − 누적 취소금액). 주문 금액과 비교할 땐 price + cancelled_price 로 원금을 복원해요
status 비교 + 저장 아래 상태값 표를 참고해 흐름별로 판단
method_symbol 선택 결제수단 표시와 운영 분석
pg 선택 PG 표시명. 분기 조건으로 쓰지 않아요
purchased_at 선택 결제 완료 시각
cancelled_price 비교 + 저장 누적 취소 금액. 원금 복원과 부분취소 추적에 필요해요
tax_free 필요 시 면세 금액을 운영·회계에서 따로 봐야 할 때

자주 만나는 status

의미
0 결제대기
1 결제완료
2 입금/승인대기
3 결제승인중
4 결제진행중
5 가상계좌발급완료
7 결제승인지연중
11 빌링키발급완료
12 본인인증완료
20 결제취소완료
21 취소처리지연중
-1 결제실패
-2 결제승인실패
-3 가상계좌발급취소
-17 결제금액변조승인
-100 결제창닫힘

빌링키 테이블 예시

빌링키는 카드번호 원본이 아니라, 저장 결제수단으로 결제를 요청할 때 쓰는 식별값이에요. 가맹점은 billing_key와 표시용 마스킹 정보만 저장하고, 카드번호 전체·비밀번호·CVC·유효기간은 저장하지 않아요.

CREATE TABLE billing_keys (
    id                  BIGINT PRIMARY KEY AUTO_INCREMENT,
    user_id             BIGINT NOT NULL,
    billing_key         VARCHAR(100) NOT NULL,
    status              VARCHAR(20) DEFAULT 'active',
    card_company        VARCHAR(50),
    masked_card_no      VARCHAR(50),
    billing_expire_at   DATETIME,
    created_at          DATETIME DEFAULT CURRENT_TIMESTAMP,

    INDEX idx_user_status (user_id, status),
    UNIQUE INDEX idx_billing_key (billing_key)
);sql
Bootpay 빌링키 조회 필드 가맹점 컬럼 예시 저장 이유
billing_key billing_key 빌링키 결제 요청에 필요
billing_data.card_company card_company 표시용 카드사명
billing_data.card_no masked_card_no 마스킹 카드번호 표시
billing_expire_at billing_expire_at 만료 전 재등록 안내
status status 또는 원문 JSON 빌링키 유효 상태 확인. 1 사용가능, 0 사용불가
pg 선택 발급 PG 표시명 (분기 조건으로 쓰지 않아요)
method 선택 결제수단 표시명
method_symbol 선택 결제수단 alias
sandbox 선택 테스트 모드로 발급된 키인지 구분
version 선택 빌링키 발급 버전
published_at 선택 빌링키 발급 시각
revoked_at 선택 빌링키 해지 시각 (해지 전에는 null)
빌링키의 `method_symbol` 은 결제 조회와 다른 체계예요

빌링키 조회의 method_symbol결제수단 설정의 alias 이고, 결제 조회의 method_symbol결제수단 그룹 심볼이에요. 두 값을 같은 매핑 테이블로 처리하면 어긋나요.

자동결제 회차 관리

빌링키만 저장해도 “다시 결제 요청”은 할 수 있어요. 다만 매월 청구, 실패 재시도, 해지, 다음 결제일 관리는 별도 테이블이 필요해요. 예시는 데이터 저장 설계에서 확인해요.

저장하면 안 되는 것

카드 원본 정보 저장 금지

아래 값은 가맹점 DB에 저장하지 않아요.

  • 카드 번호 전체(full PAN)
  • CVV/CVC
  • 카드 비밀번호
  • 유효기간(월/년)
  • 고객이 빌링키 발급 화면에 입력한 카유생비 원문

마스킹된 카드번호와 카드사명처럼 응답으로 내려오는 표시용 값은 저장할 수 있어요.

데이터 흐름 요약

결제 API — Receipt → 주문 상태

단계 흐름 가맹점이 확인할 것
1 프론트엔드 또는 리다이렉트 결과에서 receipt_id 확보 내부 order_id와 함께 서버로 전달
2 서버가 결제 조회 API 호출 실제 price, status, order_id 확인
3 서버가 내부 주문과 비교 금액·주문 상태·중복 처리 확인
4 검증 성공 시 주문 상태 저장 paid 등 서비스 상태로 전이
5 이후 취소·입금 완료 웹훅 수신 receipt_id 기준으로 같은 주문 갱신

빌링 — BillingKey → 청구/예약

단계 흐름 가맹점이 확인할 것
1 빌링키 발급 후 receipt_id 또는 billing_key 확보 발급 방식에 따라 조회 API 호출
2 서버가 빌링키 조회 billing_key, 표시용 카드/계좌 정보 확인
3 가맹점 DB에 빌링키 저장 회원·구독·예약과 연결
4 필요한 시점에 빌링키 결제 또는 예약 등록 금액·주문번호·실행 시점은 가맹점이 결정
5 결제 결과 응답/웹훅 수신 주문·회차·예약 상태 업데이트