Files
sam-manage/docs/05_PHASE5_REVENUE.md
hskwon 76c8a94e4f docs: MNG 프로젝트 문서 정비
- 개발 단계별 문서 추가 (00_OVERVIEW ~ 06_PHASE)
- 기술 표준 문서 추가 (99_TECHNICAL_STANDARDS)
- 개발 프로세스 및 패턴 문서 추가
  - API_FLOW_TESTER_DESIGN, DEV_PROCESS
  - HTMX_API_PATTERN, LAYOUT_PATTERN
  - SETUP_GUIDE, MNG_PROJECT_PLAN
- 프로젝트 관리 문서 추가 (project-management/)
- INDEX.md, MNG_CRITICAL_RULES.md 업데이트
2025-11-30 21:04:19 +09:00

447 lines
15 KiB
Markdown
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# Phase 5: 수익 관리
**기간:** 2주
**우선순위:** 높음 (SaaS 비즈니스 수익 관리)
**의존성:** Phase 1 (테넌트관리)
## 📋 Phase 개요
SaaS 비즈니스 모델의 수익 관리 시스템을 구축합니다.
**포함 기능:**
1. 구독관리 (Subscription Management)
2. 결제관리 (Payment Management)
3. 환불관리 (Refund Management)
---
## 1⃣ 구독관리 (Subscription Management)
### 기능 목록
#### 1.1 구독 플랜 관리
- **경로:** `/mng/subscriptions/plans`
- **기능:**
- 플랜 생성/수정/삭제 (Free, Basic, Pro, Enterprise)
- 가격, 기능 제한 설정
- 사용자 수, 저장 용량 한도
- 월간/연간 결제 옵션
- **권한:** `subscriptions.plans.manage`
#### 1.2 테넌트 구독 목록
- **경로:** `/mng/subscriptions`
- **기능:**
- 전체 테넌트 구독 현황
- 검색 (테넌트명, 플랜)
- 필터 (플랜, 상태, 만료 임박)
- 정렬 (만료일, 가입일)
- **권한:** `subscriptions.index`
#### 1.3 구독 상세 조회
- **경로:** `/mng/subscriptions/{id}`
- **기능:**
- 현재 플랜 정보
- 구독 이력 (업그레이드/다운그레이드)
- 사용량 통계 (사용자 수, 저장 용량)
- 다음 결제 예정일
- **권한:** `subscriptions.show`
#### 1.4 구독 플랜 변경
- **경로:** `/mng/subscriptions/{id}/change-plan`
- **기능:**
- 플랜 업그레이드/다운그레이드
- 즉시 적용 or 다음 결제일 적용
- 차액 정산 (프로레이션)
- **권한:** `subscriptions.change-plan`
#### 1.5 구독 해지
- **경로:** `/mng/subscriptions/{id}/cancel`
- **기능:**
- 즉시 해지 or 기간 만료 후 해지
- 해지 사유 기록
- 데이터 백업 안내
- **권한:** `subscriptions.cancel`
### DB 스키마
```sql
CREATE TABLE subscription_plans (
id BIGINT UNSIGNED PRIMARY KEY AUTO_INCREMENT,
name VARCHAR(100) NOT NULL COMMENT '플랜명 (Free, Basic, Pro, Enterprise)',
slug VARCHAR(50) UNIQUE NOT NULL,
description TEXT NULL,
price_monthly DECIMAL(10,2) NOT NULL COMMENT '월 가격',
price_yearly DECIMAL(10,2) NOT NULL COMMENT '연 가격',
max_users INT DEFAULT 10 COMMENT '최대 사용자 수',
storage_limit BIGINT DEFAULT 1073741824 COMMENT '저장 용량 (바이트)',
features JSON NULL COMMENT '플랜 기능 목록',
is_active BOOLEAN DEFAULT TRUE,
sort_order INT DEFAULT 0,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
INDEX idx_slug (slug),
INDEX idx_is_active (is_active)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
CREATE TABLE subscriptions (
id BIGINT UNSIGNED PRIMARY KEY AUTO_INCREMENT,
tenant_id BIGINT UNSIGNED UNIQUE NOT NULL,
plan_id BIGINT UNSIGNED NOT NULL,
status ENUM('active', 'past_due', 'cancelled', 'expired') DEFAULT 'active',
billing_cycle ENUM('monthly', 'yearly') DEFAULT 'monthly',
current_period_start DATE NOT NULL,
current_period_end DATE NOT NULL,
next_billing_date DATE NOT NULL,
cancelled_at TIMESTAMP NULL,
cancel_reason TEXT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
INDEX idx_tenant_id (tenant_id),
INDEX idx_plan_id (plan_id),
INDEX idx_status (status),
INDEX idx_next_billing_date (next_billing_date),
FOREIGN KEY (tenant_id) REFERENCES tenants(id) ON DELETE CASCADE,
FOREIGN KEY (plan_id) REFERENCES subscription_plans(id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
CREATE TABLE subscription_history (
id BIGINT UNSIGNED PRIMARY KEY AUTO_INCREMENT,
subscription_id BIGINT UNSIGNED NOT NULL,
old_plan_id BIGINT UNSIGNED NULL,
new_plan_id BIGINT UNSIGNED NOT NULL,
action ENUM('upgrade', 'downgrade', 'renew', 'cancel') NOT NULL,
reason TEXT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
INDEX idx_subscription_id (subscription_id),
FOREIGN KEY (subscription_id) REFERENCES subscriptions(id) ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
```
### API 엔드포인트
| Method | Endpoint | Description | FormRequest |
|--------|----------|-------------|-------------|
| GET | `/mng/subscriptions/plans` | 플랜 목록 | - |
| POST | `/mng/subscriptions/plans` | 플랜 생성 | `StorePlanRequest` |
| PUT | `/mng/subscriptions/plans/{id}` | 플랜 수정 | `UpdatePlanRequest` |
| GET | `/mng/subscriptions` | 구독 목록 | - |
| GET | `/mng/subscriptions/{id}` | 구독 상세 | - |
| POST | `/mng/subscriptions/{id}/change-plan` | 플랜 변경 | `ChangePlanRequest` |
| POST | `/mng/subscriptions/{id}/cancel` | 구독 해지 | `CancelSubscriptionRequest` |
### Service 클래스
```php
// app/Services/SubscriptionService.php
class SubscriptionService
{
public function listPlans(): Collection;
public function createPlan(array $data): SubscriptionPlan;
public function updatePlan(SubscriptionPlan $plan, array $data): SubscriptionPlan;
public function list(array $filters): LengthAwarePaginator;
public function find(int $id): Subscription;
public function create(int $tenantId, int $planId, string $billingCycle): Subscription;
public function changePlan(Subscription $subscription, int $newPlanId, bool $immediate = false): Subscription;
public function cancel(Subscription $subscription, string $reason, bool $immediate = false): bool;
public function renew(Subscription $subscription): bool;
public function getUsageStats(int $tenantId): array;
public function checkLimits(int $tenantId): array; // 사용자 수, 저장 용량 체크
public function calculateProration(Subscription $subscription, int $newPlanId): float;
}
```
### 개발 체크리스트
- [ ] `SubscriptionPlan`, `Subscription`, `SubscriptionHistory` 모델 작성
- [ ] `SubscriptionService` 클래스 작성
- [ ] 프로레이션 계산 로직 구현
- [ ] 사용량 체크 로직 (사용자 수, 저장 용량)
- [ ] 구독 만료 자동 처리 (스케줄러)
- [ ] FormRequest 작성
- [ ] `SubscriptionController` 작성
- [ ] Blade 템플릿 작성
- [ ] i18n 키 작성
- [ ] 테스트 작성
---
## 2⃣ 결제관리 (Payment Management)
### 기능 목록
#### 2.1 결제 내역 조회
- **경로:** `/mng/payments`
- **기능:**
- 전체 결제 내역 목록
- 검색 (테넌트명, 결제 번호)
- 필터 (상태, 날짜 범위, 플랜)
- 정렬 (결제일, 금액)
- 엑셀 내보내기
- **권한:** `payments.index`
#### 2.2 결제 상세 조회
- **경로:** `/mng/payments/{id}`
- **기능:**
- 결제 정보 (금액, 방법, 일시)
- 구독 정보
- 영수증 출력
- PG사 거래 번호
- **권한:** `payments.show`
#### 2.3 결제 처리
- **경로:** `/mng/payments/process`
- **기능:**
- PG 연동 (토스페이먼츠, 이니시스 등)
- 정기 결제 등록
- 일회성 결제
- 결제 실패 처리
- **권한:** `payments.process`
#### 2.4 결제 실패 관리
- **경로:** `/mng/payments/failed`
- **기능:**
- 실패 내역 조회
- 재시도
- 알림 발송
- **권한:** `payments.failed.manage`
### DB 스키마
```sql
CREATE TABLE payments (
id BIGINT UNSIGNED PRIMARY KEY AUTO_INCREMENT,
tenant_id BIGINT UNSIGNED NOT NULL,
subscription_id BIGINT UNSIGNED NOT NULL,
payment_number VARCHAR(50) UNIQUE NOT NULL COMMENT '결제 번호 (자동 생성)',
amount DECIMAL(10,2) NOT NULL COMMENT '결제 금액',
payment_method ENUM('card', 'bank_transfer', 'virtual_account', 'paypal') DEFAULT 'card',
status ENUM('pending', 'completed', 'failed', 'refunded') DEFAULT 'pending',
pg_provider VARCHAR(50) NULL COMMENT 'PG사 (toss, inicis 등)',
pg_transaction_id VARCHAR(255) NULL COMMENT 'PG사 거래 번호',
paid_at TIMESTAMP NULL COMMENT '결제 완료 일시',
failure_reason TEXT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
INDEX idx_tenant_id (tenant_id),
INDEX idx_subscription_id (subscription_id),
INDEX idx_payment_number (payment_number),
INDEX idx_status (status),
INDEX idx_paid_at (paid_at),
FOREIGN KEY (tenant_id) REFERENCES tenants(id) ON DELETE CASCADE,
FOREIGN KEY (subscription_id) REFERENCES subscriptions(id) ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
CREATE TABLE payment_cards (
id BIGINT UNSIGNED PRIMARY KEY AUTO_INCREMENT,
tenant_id BIGINT UNSIGNED NOT NULL,
card_number_masked VARCHAR(20) NOT NULL COMMENT '마스킹된 카드번호 (1234-****-****-5678)',
card_type VARCHAR(50) NULL COMMENT '카드사',
expiry_date VARCHAR(7) NOT NULL COMMENT 'MM/YYYY',
is_default BOOLEAN DEFAULT FALSE,
billing_key VARCHAR(255) NULL COMMENT 'PG사 빌링키 (정기결제)',
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
deleted_at TIMESTAMP NULL,
INDEX idx_tenant_id (tenant_id),
FOREIGN KEY (tenant_id) REFERENCES tenants(id) ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
```
### API 엔드포인트
| Method | Endpoint | Description | FormRequest |
|--------|----------|-------------|-------------|
| GET | `/mng/payments` | 결제 내역 목록 | - |
| GET | `/mng/payments/{id}` | 결제 상세 | - |
| POST | `/mng/payments/process` | 결제 처리 | `ProcessPaymentRequest` |
| POST | `/mng/payments/{id}/retry` | 결제 재시도 | - |
| GET | `/mng/payments/{id}/receipt` | 영수증 출력 | - |
| GET | `/mng/payments/failed` | 실패 내역 | - |
### Service 클래스
```php
// app/Services/PaymentService.php
class PaymentService
{
public function list(array $filters): LengthAwarePaginator;
public function find(int $id): Payment;
public function process(int $tenantId, int $subscriptionId, float $amount, string $method): Payment;
public function complete(Payment $payment, string $pgTransactionId): bool;
public function fail(Payment $payment, string $reason): bool;
public function retry(Payment $payment): Payment;
public function generateReceipt(Payment $payment): string; // PDF 경로
public function registerCard(int $tenantId, array $cardData): PaymentCard;
public function getCards(int $tenantId): Collection;
public function processRecurring(Subscription $subscription): Payment;
}
```
### 개발 체크리스트
- [ ] `Payment`, `PaymentCard` 모델 작성
- [ ] `PaymentService` 클래스 작성
- [ ] PG 연동 구현 (토스페이먼츠 우선)
- [ ] 정기 결제 스케줄러
- [ ] 결제 실패 알림 (이메일, SMS)
- [ ] 영수증 PDF 생성
- [ ] 결제 번호 자동 생성
- [ ] FormRequest 작성
- [ ] 테스트 작성 (Mock PG)
---
## 3⃣ 환불관리 (Refund Management)
### 기능 목록
#### 3.1 환불 요청 목록
- **경로:** `/mng/refunds`
- **기능:**
- 전체 환불 요청 목록
- 검색 (테넌트명, 결제 번호)
- 필터 (상태, 날짜)
- 정렬 (요청일, 금액)
- **권한:** `refunds.index`
#### 3.2 환불 요청 상세
- **경로:** `/mng/refunds/{id}`
- **기능:**
- 환불 요청 정보 (사유, 금액)
- 원 결제 정보
- 환불 처리 이력
- **권한:** `refunds.show`
#### 3.3 환불 승인/반려
- **경로:** `/mng/refunds/{id}/approve` 또는 `/reject`
- **기능:**
- 전액 환불 or 부분 환불
- 승인/반려 사유 기록
- PG 환불 API 호출
- 알림 발송
- **권한:** `refunds.approve`
#### 3.4 환불 처리
- **경로:** `/mng/refunds/{id}/process`
- **기능:**
- PG사 환불 API 연동
- 환불 완료 처리
- 구독 상태 업데이트
- **권한:** `refunds.process`
### DB 스키마
```sql
CREATE TABLE refunds (
id BIGINT UNSIGNED PRIMARY KEY AUTO_INCREMENT,
payment_id BIGINT UNSIGNED NOT NULL,
tenant_id BIGINT UNSIGNED NOT NULL,
refund_number VARCHAR(50) UNIQUE NOT NULL COMMENT '환불 번호',
refund_amount DECIMAL(10,2) NOT NULL COMMENT '환불 금액',
refund_type ENUM('full', 'partial') DEFAULT 'full',
reason TEXT NOT NULL COMMENT '환불 사유',
status ENUM('pending', 'approved', 'rejected', 'completed', 'failed') DEFAULT 'pending',
approved_by BIGINT UNSIGNED NULL COMMENT '승인자',
approved_at TIMESTAMP NULL,
rejection_reason TEXT NULL,
processed_at TIMESTAMP NULL COMMENT '환불 완료 일시',
pg_refund_transaction_id VARCHAR(255) NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
INDEX idx_payment_id (payment_id),
INDEX idx_tenant_id (tenant_id),
INDEX idx_status (status),
FOREIGN KEY (payment_id) REFERENCES payments(id) ON DELETE CASCADE,
FOREIGN KEY (tenant_id) REFERENCES tenants(id) ON DELETE CASCADE,
FOREIGN KEY (approved_by) REFERENCES users(id) ON DELETE SET NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
```
### API 엔드포인트
| Method | Endpoint | Description | FormRequest |
|--------|----------|-------------|-------------|
| GET | `/mng/refunds` | 환불 요청 목록 | - |
| GET | `/mng/refunds/{id}` | 환불 요청 상세 | - |
| POST | `/mng/refunds` | 환불 요청 생성 | `StoreRefundRequest` |
| POST | `/mng/refunds/{id}/approve` | 환불 승인 | `ApproveRefundRequest` |
| POST | `/mng/refunds/{id}/reject` | 환불 반려 | `RejectRefundRequest` |
| POST | `/mng/refunds/{id}/process` | 환불 처리 | - |
### Service 클래스
```php
// app/Services/RefundService.php
class RefundService
{
public function list(array $filters): LengthAwarePaginator;
public function find(int $id): Refund;
public function create(int $paymentId, float $amount, string $reason, string $type = 'full'): Refund;
public function approve(Refund $refund, int $approverId): bool;
public function reject(Refund $refund, string $reason): bool;
public function process(Refund $refund): bool; // PG 환불 API 호출
public function complete(Refund $refund, string $pgRefundTransactionId): bool;
public function fail(Refund $refund, string $reason): bool;
}
```
### 개발 체크리스트
- [ ] `Refund` 모델 작성
- [ ] `RefundService` 클래스 작성
- [ ] PG 환불 API 연동
- [ ] 부분 환불 로직 구현
- [ ] 환불 후 구독 상태 업데이트
- [ ] FormRequest 작성
- [ ] `RefundController` 작성
- [ ] 환불 알림 발송
- [ ] i18n 키 작성
- [ ] 테스트 작성
---
## 🎯 Phase 5 완료 조건
### 기능 완성도
- [ ] 구독 플랜 관리 완성
- [ ] 결제 처리 (PG 연동) 동작
- [ ] 환불 워크플로우 동작
- [ ] 정기 결제 스케줄러 동작
### 코드 품질
- [ ] Service-First, FormRequest 준수
- [ ] PG 연동 에러 처리
- [ ] i18n 키 사용
- [ ] Pint, PHPStan 통과
### 비즈니스 로직
- [ ] 프로레이션 계산 정확
- [ ] 사용량 제한 체크 동작
- [ ] 구독 자동 갱신/만료 처리
- [ ] 결제 실패 재시도 로직
### 보안
- [ ] PG 통신 암호화
- [ ] 카드 정보 마스킹
- [ ] 빌링키 안전 저장
- [ ] 결제 내역 접근 권한
### 테스트
- [ ] 구독 변경 시나리오 테스트
- [ ] 결제 처리 Mock 테스트
- [ ] 환불 워크플로우 테스트
---
**최종 업데이트:** 2025-11-21
**작성자:** Claude Code
**버전:** 1.0.0