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
- 프론트는
packageId만 보냅니다. - 결제 금액과 지급 Credits는 서버 상품 원장에서 결정합니다.
- Toss 승인 성공만으로 끝내지 않고 서버 order 상태와 Credits 지급까지 transaction으로 처리합니다.
orderId,paymentKey,idempotencyKey로 중복 지급을 방지합니다.- 결제 기능은 feature flag로 켤 수 있게 합니다.
- 실패한 생성에 대한 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 상품 등록을 진행합니다.
| SKU | Name | amountKrw | credits | bonusCredits | sortOrder |
|---|---|---|---|---|---|
topup_small_300 | Top-up Small | 7,900 | 300 | 0 | 10 |
topup_standard_1000 | Top-up Standard | 24,900 | 1,000 | 0 | 20 |
topup_large_2500 | Top-up Large | 59,900 | 2,500 | 0 | 30 |
가격 정책 기준:
- 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
상태 의미:
| Status | Meaning |
|---|---|
PENDING | 서버 주문 생성 완료, 결제 승인 전 |
CONFIRMED | Toss 승인 성공, Credits 지급 transaction 진입 전 또는 중간 상태 |
CREDITED | 결제 승인과 Credits 지급 모두 완료 |
FAILED | 승인 실패, 검증 실패, provider 오류 |
CANCELED | 사용자가 결제창 이탈 또는 취소 |
초기 구현에서는 CONFIRMED와 CREDITED를 같은 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 승인 시 검증:
orderId가 내 서버 주문인지 확인- 주문 소유자가 현재 사용자와 일치하는지 확인
- 주문 상태가
PENDING인지 확인 - 요청
amount가 서버 주문amount와 일치하는지 확인 - Toss 승인 API 호출
- Toss 응답 금액과 주문 금액 재확인
paymentKeyunique 저장- 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_ordersmigrationBillingModule- 상품 조회 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가 지급되지 않음
- 관리자 또는 운영자가
orderId와paymentKey로 결제 상태를 추적할 수 있음 - 일반 사용자 화면에서 crypto 결제가 기본 경로로 노출되지 않음
11. Follow-up Work
MVP 이후:
- Toss 정기결제 billing key
- Monthly Credits bucket
- 구독 갱신 시 Monthly Credits 리셋
- Top-up Credits와 Monthly Credits 차감 순서 구현
- 환불/부분 환불 운영 플로우
- 세금계산/영수증 정책
- provider별 원가 기반 Credits 차감량 자동 조정