Skip to main content

Billing Implementation Plan

이 문서는 Payment & Credit Policy를 실제 제품에 반영하기 위한 구현 계획입니다. 초기 범위는 국내 일반 결제 기반 Top-up Credits 단건 결제입니다. 월 구독, Monthly Credits, 환불 자동화는 후속 단계로 둡니다.

1. Implementation Scope

In Scope

  • 사용자-facing 명칭을 Credits로 통일
  • /credits 페이지에서 일반 결제 기반 Credits 상품 판매
  • Toss Payments 결제창 연동
  • 서버 기준 상품/주문/결제 승인/중복 지급 방지
  • 결제 성공 시 Credits 지급
  • 결제 및 Credits ledger 감사 가능성 확보

Out of Scope

  • Toss 정기결제 billing key
  • Monthly Credits 리셋/소멸 자동화
  • 자동 환불 API
  • 관리자 환불 처리 UI
  • crypto 결제 개선
  • 여러 PG provider 추상화

기존 crypto 결제는 삭제하지 않고 일반 사용자 화면에서 숨깁니다.

2. Implementation Principles

  1. 프론트는 packageId만 보냅니다.
  2. 결제 금액과 지급 Credits는 서버 상품 원장에서 결정합니다.
  3. Toss 승인 성공만으로 끝내지 않고 서버 order 상태와 Credits 지급까지 transaction으로 처리합니다.
  4. orderId, paymentKey, idempotencyKey로 중복 지급을 방지합니다.
  5. 결제 기능은 feature flag로 켤 수 있게 합니다.
  6. 실패한 생성에 대한 Credits 환불 구조는 기존 CreditsService를 유지합니다.

3. Phase 0: Naming and Compatibility

목표는 결제 구현 전에 사용자-facing 용어를 정리하는 것입니다.

작업:

  • UI의 VT, Vivid Token, Token 표현을 Credits로 교체
  • 알림 문구, 결제 페이지, 관리자 크레딧 문구도 Credits 기준으로 정리
  • 기존 DB 컬럼명은 즉시 바꾸지 않아도 됨
  • 기존 Wallet, CreditTransaction 도메인은 내부 구현명으로 유지 가능

완료 기준:

  • 일반 사용자 화면에서 VT가 보이지 않음
  • 생성 비용 표시가 생성 -N Credits 형태로 통일됨

4. Phase 1: Product and Order Foundation

Toss 연동 전에 서버 기준 상품과 주문 원장을 먼저 만듭니다.

4.1. Credit Product

Credits 상품은 서버에서 관리합니다.

예상 테이블:

billing_products
- id uuid
- sku varchar unique
- name varchar
- description text nullable
- type enum: TOPUP
- amountKrw integer
- credits integer
- bonusCredits integer default 0
- currency varchar default 'KRW'
- status enum: ACTIVE, INACTIVE, ARCHIVED
- sortOrder integer
- metadata jsonb nullable
- createdAt
- updatedAt

초기 top-up 상품은 다음 값으로 seed 또는 admin 상품 등록을 진행합니다.

SKUNameamountKrwcreditsbonusCreditssortOrder
topup_small_300Top-up Small7,900300010
topup_standard_1000Top-up Standard24,9001,000020
topup_large_2500Top-up Large59,9002,500030

가격 정책 기준:

  • Persona image generation: 10 Credits / image
  • Top-up target: 약 250원 / image
  • Top-up Credits: 구독 갱신과 무관하게 보존
  • Subscription Credits: 후속 단계에서 top-up보다 낮은 장당 단가로 제공

중요한 것은 프론트 하드코딩을 제거하고, 상품 금액과 지급 Credits를 서버 원장 기준으로 결정하는 것입니다.

4.2. Payment Order

결제 요청마다 서버 주문을 생성합니다.

예상 테이블:

payment_orders
- id uuid
- userId uuid
- provider enum: TOSS
- productId uuid
- orderId varchar unique
- orderName varchar
- amount integer
- currency varchar default 'KRW'
- credits integer
- bonusCredits integer default 0
- status enum: PENDING, CONFIRMED, CREDITED, FAILED, CANCELED
- paymentKey varchar unique nullable
- failureCode varchar nullable
- failureMessage text nullable
- requestedAt timestamptz
- approvedAt timestamptz nullable
- creditedAt timestamptz nullable
- metadata jsonb nullable
- createdAt
- updatedAt

상태 의미:

StatusMeaning
PENDING서버 주문 생성 완료, 결제 승인 전
CONFIRMEDToss 승인 성공, Credits 지급 transaction 진입 전 또는 중간 상태
CREDITED결제 승인과 Credits 지급 모두 완료
FAILED승인 실패, 검증 실패, provider 오류
CANCELED사용자가 결제창 이탈 또는 취소

초기 구현에서는 CONFIRMEDCREDITED를 같은 transaction 안에서 처리해 중간 상태 노출을 최소화합니다.

5. Phase 2: Backend Billing Module

새 backend module은 기존 payments crypto module과 분리합니다.

권장 구조:

src/billing/
- billing.module.ts
- billing.controller.ts
- billing.service.ts
- toss-payments.client.ts
- dto/
- entities/
- enums/

기존 crypto payments module은 유지하되, 일반 결제 신규 구현은 billing module에서 시작합니다.

5.1. API

상품 조회:

GET /v1/billing/products
auth: optional or required by policy
response:
{
products: [
{
id,
sku,
name,
amountKrw,
credits,
bonusCredits,
currency
}
]
}

주문 생성:

POST /v1/billing/orders
auth: required
body: { productId }
response:
{
orderId,
orderName,
amount,
currency,
customerKey,
successUrl,
failUrl
}

Toss 승인:

POST /v1/billing/toss/confirm
auth: required
body: {
paymentKey,
orderId,
amount
}
response:
{
orderId,
status: "CREDITED",
creditedAmount,
balance
}

내 결제 내역:

GET /v1/billing/orders
auth: required
query: { limit, offset }

5.2. Toss Client

환경 변수:

TOSS_PAYMENTS_SECRET_KEY=
TOSS_PAYMENTS_CLIENT_KEY=
PUBLIC_FRONTEND_URL=

서버는 secret key로 Toss 승인 API를 호출합니다. 프론트에는 client key만 노출합니다.

Toss 승인 시 검증:

  1. orderId가 내 서버 주문인지 확인
  2. 주문 소유자가 현재 사용자와 일치하는지 확인
  3. 주문 상태가 PENDING인지 확인
  4. 요청 amount가 서버 주문 amount와 일치하는지 확인
  5. Toss 승인 API 호출
  6. Toss 응답 금액과 주문 금액 재확인
  7. paymentKey unique 저장
  8. Credits 지급

5.3. Credits Grant

Credits 지급은 기존 CreditsService.addCredit()를 사용하되, idempotency를 반드시 추가합니다.

권장 metadata:

{
"source": "billing",
"provider": "TOSS",
"orderId": "ORDER_...",
"paymentKey": "payment_...",
"productId": "...",
"sku": "TOPUP_..."
}

권장 idempotency key:

billing:toss:credit:{paymentKey}

결제 승인과 Credits 지급은 하나의 DB transaction에서 처리합니다.

6. Phase 3: Frontend Credits Page

/credits 페이지를 일반 결제 중심으로 교체합니다.

작업:

  • 지갑 연결, USDT, BNB 가스비 UI 숨김
  • GET /v1/billing/products로 상품 조회
  • 상품 카드에 원화 금액과 지급 Credits 표시
  • 구매 클릭 시 POST /v1/billing/orders
  • Toss Payments 결제창 호출
  • 성공 callback에서 /v1/billing/toss/confirm 호출
  • 성공 후 Credits 잔액 refetch
  • 실패 callback에서 order 실패 상태를 표시

프론트 route:

/credits
/credits/success
/credits/fail

성공 callback은 결제 성공 화면이 아니라 서버 confirm 결과를 기다린 뒤 최종 성공을 보여줍니다.

7. Phase 4: Admin and Operations

초기에는 최소 운영 조회만 구현합니다.

Admin 기능:

  • 결제 주문 목록
  • orderId, paymentKey, user 검색
  • 주문 상태 필터
  • 지급 Credits 확인
  • 실패 사유 확인

초기에는 관리자 환불 버튼을 만들지 않습니다. 환불은 운영 절차를 먼저 정한 뒤 별도 단계로 구현합니다.

8. Test Plan

Backend Unit Tests

  • 비활성 상품으로 주문 생성 불가
  • 서버 상품 금액으로 주문 생성
  • 프론트가 보낸 amount/credits를 사용하지 않음
  • amount mismatch 시 Toss 승인 호출 전 거절
  • 이미 CREDITED인 주문 confirm 재요청 시 중복 지급 방지
  • 같은 paymentKey 중복 저장 방지
  • Credits 지급 실패 시 order가 잘못 완료 처리되지 않음

Backend Integration Tests

  • 주문 생성 -> Toss confirm mock -> Credits 증가
  • confirm 실패 -> order FAILED
  • transaction rollback 검증
  • credit transaction metadata/idempotency 검증

Frontend Tests

  • 상품 목록 로딩
  • 결제 버튼 클릭 시 주문 생성 호출
  • 결제 성공 callback에서 confirm 호출
  • confirm 성공 후 balance refetch
  • 실패 callback 메시지 표시

Manual Sandbox Checklist

  • Toss sandbox 결제 성공
  • 결제창 이탈
  • 금액 변조 시 실패
  • confirm API 중복 호출
  • 네트워크 실패 후 재시도
  • Credits 잔액 반영

9. Rollout Plan

Step 1: Backend Foundation

  • billing_products, payment_orders migration
  • BillingModule
  • 상품 조회 API
  • 주문 생성 API
  • Toss client mock 기반 tests

Step 2: Toss Confirm

  • Toss secret key 환경 변수
  • confirm API
  • Credits 지급 transaction
  • 중복 지급 방지

Step 3: Frontend Switch

  • /credits 페이지 일반 결제 UI 전환
  • crypto UI feature flag 처리
  • Toss client key 주입
  • success/fail route 추가

Step 4: Admin Read Model

  • 결제 내역 조회 API
  • 관리자 결제 목록 UI
  • 실패/중복 지급 조사에 필요한 필드 노출

Step 5: Production Rollout

  • Toss sandbox E2E 확인
  • prod secret 등록
  • 소액 실결제 확인
  • CloudWatch/log monitoring 확인
  • crypto 결제 UI 숨김 유지

10. Acceptance Criteria

MVP 결제 구현 완료 기준:

  • 사용자가 /credits에서 일반 결제로 Credits를 구매할 수 있음
  • 결제 금액과 지급 Credits가 서버 상품 원장 기준으로 결정됨
  • 결제 성공 후 Credits 잔액이 즉시 증가함
  • 동일 결제 건이 여러 번 confirm되어도 Credits가 중복 지급되지 않음
  • 결제 실패/취소 시 Credits가 지급되지 않음
  • 관리자 또는 운영자가 orderIdpaymentKey로 결제 상태를 추적할 수 있음
  • 일반 사용자 화면에서 crypto 결제가 기본 경로로 노출되지 않음

11. Follow-up Work

MVP 이후:

  • Toss 정기결제 billing key
  • Monthly Credits bucket
  • 구독 갱신 시 Monthly Credits 리셋
  • Top-up Credits와 Monthly Credits 차감 순서 구현
  • 환불/부분 환불 운영 플로우
  • 세금계산/영수증 정책
  • provider별 원가 기반 Credits 차감량 자동 조정