7. Membership — Membership·PointBalance·Payment·Refund
회원 멤버십 + 결제. 2B 멤버십, 2D 정책 정책 반영.
Membership
Purpose: 회원 멤버십. 결제 → 생성 → 활성화 → 만료/해지 라이프사이클. Related PRDs: 👤 멤버십·결제 · 🏢 멤버십 시스템
라이프사이클
[결제 성공] → 회원권 생성 (status=created, 유효기간 카운트 ❌)
↓
[첫 사용 (첫 세션 예약 또는 체크인)]
↓
[활성화] → status=active, activatedAt 기록, expiresAt 카운트 시작
↓
[활성 기간] — 회차 사용·일시정지·재개
↓
[종료 조건 충족]:
- 유효기간 만료 (expiresAt < NOW) → status=expired
- 회차 0 + 만료 임박 → status=expired
- 중도 해지 + 환불 → status=cancelled
- autoRenew=true → 다음 기간 새 Membership 자동 생성
↓
[연장] — 만료 임박/만료 후 N일 내 추가 결제로 기간·회차 연장 (status=active 유지/복원)
Fields
| Field | Type | Required | Default | Description |
|---|---|---|---|---|
| id | String | ✓ | cuid() | PK |
| memberId | String | ✓ | - | FK |
| type | MembershipType | ✓ | - | week1 / week2 |
| creditsRemaining | Int | ✓ | - | 주 1=4, 주 2=8 기준 |
| contractMonths | Int | ✓ | 1 | 1, 3, 6, 12 |
| discountRate | Decimal(3,2) | ✓ | 0 | 0, 0.05, 0.10, 0.15 |
| pricePerMonth | Int | ✓ | - | 정상가 |
| priceCharged | Int | ✓ | - | 실제 결제액 |
| paidAt | DateTime | ✓ | - | 결제 완료 시각 (Payment 연결) |
| activatedAt | DateTime? | - | null | 첫 사용 시점 (status=active 전환) |
| autoActivateBy | DateTime | ✓ | - | 결제 후 자동 활성화 한도 (기본 paidAt + 30일) |
| expiresAt | DateTime? | - | null | 활성화 시점 + contractMonths (활성화 전엔 null) |
| pausedAt | DateTime? | - | null | 일시정지 시작 |
| resumedAt | DateTime? | - | null | 재개 |
| pauseUsedDays | Int | ✓ | 0 | 누적 일시정지 일수 |
| autoRenew | Boolean | ✓ | true | 만료 시 자동 갱신 여부 |
| extensionCount | Int | ✓ | 0 | 수동 연장 횟수 (회원당 카운트) |
| status | MembershipStatus | ✓ | created | created/active/paused/expired/cancelled |
| createdAt | DateTime | ✓ | now() |
Validation
- 한 회원 = 1 active or created membership (status IN (‘active’, ‘created’))
- creditsRemaining: week1 = 0-4, week2 = 0-8 (확장 시 늘어남)
- pauseUsedDays ≤ contractMonths × 30 × 0.30
- expiresAt = activatedAt + contractMonths × 30일 + pauseUsedDays (활성화 후 산출)
- autoActivateBy = paidAt + 30일 (이 시점 미사용 시 시스템 자동 활성화)
- status=’active’ 전환 시 activatedAt, expiresAt 필수
- discountRate enum {0, 0.05, 0.10, 0.15}
State Transitions
stateDiagram-v2
[*] --> created: 결제 성공
created --> active: 첫 사용 또는 30일 후 자동
created --> cancelled: 청약철회 (7일 내, 미사용)
active --> paused: 일시정지
paused --> active: 재개
active --> extended: 수동 연장 (추가 결제)
extended --> active: 기간·회차 +
active --> cancelled: 중도 해지
active --> expired: 유효기간 만료 OR 회차 0 + 만료
paused --> expired: 만료 (정지 중)
expired --> active: 만료 후 N일 내 수동 연장 (재활성화)
연장 (수동) 룰
회원이 추가 결제로 기간·회차 연장 가능:
- 만료 전 연장: 기존 expiresAt + N개월 + 회차 추가 (잔여 + N)
- 만료 후 연장 (만료 후 30일 내): expired → active 재활성화 (새 기간 시작)
- 만료 후 30일 초과: 신규 가입과 동일 (Membership row 새로 생성)
- 가격 = 현재 PricingConfig 기준 (할인 적용 가능 — 연장 인센티브)
Indexes
[memberId, status]— 회원 활성 멤버십 조회[expiresAt]— 만료 임박 cron[autoRenew, expiresAt]— 갱신 cron
Common Queries
- 회원 활성 멤버십:
WHERE memberId=? AND status='active' - 갱신 임박 (D-7):
WHERE expiresAt BETWEEN NOW()+6d AND NOW()+7d AND autoRenew=true - 일시정지 한도 도달:
WHERE pauseUsedDays >= contractMonths * 30 * 0.30
Edge Cases
- 정지 중 약정 만료 → expired 상태 (회원 알림)
- creditsRemaining 0 + 다음 갱신 전 → 회원 다른 결제 권유 또는 대기
- 일시정지 한도 초과 신청 → UI에서 차단
- 자동 갱신 시 가격 변경 (본사가 정가 인상) → 7일 전 알림 + 동의
PointBalance
Purpose: 회원 포인트 잔액 (Pro 인증 멘토 예약 시 차감). Related PRDs: 👤 멤버십·결제 Lifecycle: 충전 → 차감 → 만료 (선택, Phase 1엔 무기한)
Fields
| Field | Type | Required | Default | Description |
|---|---|---|---|---|
| memberId | String | ✓ | - | PK (1:1 with Member) |
| balance | Int | ✓ | 0 | 포인트 (원 단위) |
| lifetimeCharged | Int | ✓ | 0 | 누적 충전액 |
| lifetimeSpent | Int | ✓ | 0 | 누적 사용액 |
| lastChargedAt | DateTime? | - | null | 마지막 충전 |
| lastSpentAt | DateTime? | - | null | 마지막 사용 |
Validation
- balance ≥ 0
- balance + spent = charged (정합성)
Indexes
memberId(PK)
Common Queries
- 회원 포인트 잔액:
WHERE memberId=? - 활발한 포인트 사용자:
ORDER BY lifetimeSpent DESC
Edge Cases
- Pro 인증 예약 시 잔액 부족 → 일반 멘토로 fallback
- 환불 시 미사용분 100% 환불 (현금 결제분), 보너스분 ❌
- 동시 차감 (race condition) → DB transaction + row lock
Payment
Purpose: 결제 트랜잭션 (멤버십·포인트·체험 등). Related PRDs: 🏢 멤버십 시스템 · 💳 결제 PG Lifecycle: pending → paid (또는 failed) → refunded (선택)
Fields
| Field | Type | Required | Default | Description |
|---|---|---|---|---|
| id | String | ✓ | cuid() | PK |
| memberId | String | ✓ | - | FK |
| type | PaymentType | ✓ | - | membership/point/trial/bonus |
| referenceId | String? | - | null | Membership·PointBalance 연결 |
| amount | Int | ✓ | - | 원화 |
| currency | String | ✓ | KRW | |
| status | PaymentStatus | ✓ | pending | pending/paid/failed/refunded |
| pgProvider | String? | - | null | “toss” / “portone” |
| pgTransactionId | String? | - | null | unique |
| pgRawResponse | Json? | - | null | 디버깅용 |
| paidAt | DateTime? | - | null | |
| refundedAt | DateTime? | - | null | |
| description | String? | - | null | “주 2회권 12개월 첫 결제” |
| createdAt | DateTime | ✓ | now() |
Validation
- amount > 0
- status=’paid’ 시 paidAt, pgTransactionId 필수
- (pgProvider, pgTransactionId) unique (중복 결제 방지)
State Transitions
stateDiagram-v2
[*] --> pending: PG 호출 직전
pending --> paid: success
pending --> failed: PG 거절
paid --> refunded: 환불
Indexes
[memberId, paidAt]— 회원 결제 내역pgTransactionId(unique)[status, createdAt]— pending 상태 5분+ → 알림
Common Queries
- 회원 결제 내역:
WHERE memberId=? ORDER BY paidAt DESC - 일별 매출:
SUM(amount) WHERE status='paid' AND DATE(paidAt) = ?
Edge Cases
- PG webhook 누락 → cron 5분 간격 동기화
- 결제 후 회원이 즉시 환불 신청 → 청약철회 룰 적용
- 부분 환불 → Refund.refundAmount < Payment.amount
Refund
Purpose: 환불 기록 (중도 해지·청약철회·노쇼 보상 등). Related PRDs: 💳 결제 흐름 Lifecycle: 신청 → 산출 → 처리 (PG API) → 완료
Fields
| Field | Type | Required | Default | Description |
|---|---|---|---|---|
| id | String | ✓ | cuid() | PK |
| paymentId | String | ✓ | - | FK (1:1 또는 N:1 부분환불) |
| usedCredits | Int | ✓ | 0 | 사용 회차 |
| refundAmount | Int | ✓ | - | 환불액 |
| fee | Int | ✓ | 0 | 위약금 (현재 0) |
| reason | String | ✓ | - | “중도해지” / “청약철회” / “본사노쇼보상” |
| processedAt | DateTime? | - | null | |
| pgRefundId | String? | - | null | PG 환불 트랜잭션 |
| status | String | ✓ | pending | pending/processed/failed |
Validation
- refundAmount + fee ≤ payment.amount
- reason enum
- paymentId당 누적 refund ≤ payment.amount
Indexes
paymentId— 결제별 환불[status, createdAt]— pending refund 추적
Common Queries
- 회원 환불 이력:
JOIN Payment WHERE Payment.memberId=? - 일별 환불액:
SUM(refundAmount) WHERE DATE(processedAt) = ?
Edge Cases
- PG 환불 실패 → 운영자 수동 처리 + 회원 알림
- 환불 후 멘토 정산 회수 → DistributionEntry 차감 (다음 격주)
📘 사용 PRD
👤 멤버십·결제 · 🏢 멤버십 시스템 · 🏢 정산 시스템 · 💳 결제 흐름
| 2026-05-13 | 초안 — Membership·PointBalance·Payment·Refund 상세 명세 |