261 lines
7.5 KiB
Markdown
261 lines
7.5 KiB
Markdown
|
|
# 구독료정산
|
|||
|
|
|
|||
|
|
## 개요
|
|||
|
|
|
|||
|
|
구독료정산은 고객사별 구독 플랜과 과금을 관리하는 기능입니다.
|
|||
|
|
플랜별 월정액 관리, 과금주기(월/연), MRR/ARR 집계, 사용자 수 추적을 지원합니다.
|
|||
|
|
|
|||
|
|
- **라우트**: `GET /finance/subscription`
|
|||
|
|
- **UI 기술**: React 18 + Babel (브라우저 트랜스파일링)
|
|||
|
|
|
|||
|
|
## 파일 구조
|
|||
|
|
|
|||
|
|
```
|
|||
|
|
mng/
|
|||
|
|
├── app/Http/Controllers/Finance/
|
|||
|
|
│ └── SubscriptionController.php # MNG 컨트롤러 (4개 메서드)
|
|||
|
|
├── app/Models/Finance/
|
|||
|
|
│ └── Subscription.php # MNG 구독 모델
|
|||
|
|
└── resources/views/finance/
|
|||
|
|
└── subscription.blade.php # React 기반 단일 페이지
|
|||
|
|
|
|||
|
|
api/
|
|||
|
|
├── app/Http/Controllers/Api/V1/
|
|||
|
|
│ └── SubscriptionController.php # API 컨트롤러 (REST)
|
|||
|
|
├── app/Services/
|
|||
|
|
│ └── SubscriptionService.php # 비즈니스 로직 서비스
|
|||
|
|
├── app/Models/Tenants/
|
|||
|
|
│ └── Subscription.php # API 구독 모델 (상세 구현)
|
|||
|
|
└── database/migrations/
|
|||
|
|
├── 2026_02_04_230008_create_subscriptions_table.php
|
|||
|
|
└── 2025_12_22_222722_add_cancel_columns_to_subscriptions_table.php
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
## 라우트
|
|||
|
|
|
|||
|
|
### MNG 라우트
|
|||
|
|
|
|||
|
|
```php
|
|||
|
|
// routes/web.php (finance prefix 그룹 내)
|
|||
|
|
GET /subscription → Blade 페이지 렌더링 (HX-Redirect)
|
|||
|
|
|
|||
|
|
// API 라우트 (subscriptions prefix)
|
|||
|
|
GET /subscriptions/list → index() 목록 + 통계 (JSON)
|
|||
|
|
POST /subscriptions/store → store() 등록
|
|||
|
|
PUT /subscriptions/{id} → update() 수정
|
|||
|
|
DELETE /subscriptions/{id} → destroy() 삭제
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### API 라우트
|
|||
|
|
|
|||
|
|
```php
|
|||
|
|
// routes/api.php (v1/subscriptions prefix)
|
|||
|
|
GET / → index() 목록 (페이지네이션)
|
|||
|
|
GET /{id} → show() 상세 조회
|
|||
|
|
POST / → store() 생성
|
|||
|
|
PUT /{id} → update() 수정
|
|||
|
|
DELETE /{id} → destroy() 삭제
|
|||
|
|
POST /{id}/cancel → cancel() 구독 취소
|
|||
|
|
POST /{id}/suspend → suspend() 일시정지
|
|||
|
|
POST /{id}/resume → resume() 재개
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
## 컨트롤러
|
|||
|
|
|
|||
|
|
### SubscriptionController (MNG)
|
|||
|
|
|
|||
|
|
| 메서드 | HTTP | 설명 |
|
|||
|
|
|--------|------|------|
|
|||
|
|
| `index()` | GET | 목록 + MRR/ARR 통계 (검색, 상태 필터) |
|
|||
|
|
| `store()` | POST | 구독 등록 |
|
|||
|
|
| `update()` | PUT | 구독 수정 |
|
|||
|
|
| `destroy()` | DELETE | 삭제 (Soft Delete) |
|
|||
|
|
|
|||
|
|
### index() 응답 구조
|
|||
|
|
|
|||
|
|
```json
|
|||
|
|
{
|
|||
|
|
"success": true,
|
|||
|
|
"data": [
|
|||
|
|
{
|
|||
|
|
"id": 1,
|
|||
|
|
"customer": "A사",
|
|||
|
|
"plan": "Business",
|
|||
|
|
"monthlyFee": 500000,
|
|||
|
|
"billingCycle": "monthly",
|
|||
|
|
"startDate": "2026-01-01",
|
|||
|
|
"nextBilling": "2026-03-01",
|
|||
|
|
"status": "active",
|
|||
|
|
"users": 10,
|
|||
|
|
"memo": ""
|
|||
|
|
}
|
|||
|
|
],
|
|||
|
|
"stats": {
|
|||
|
|
"activeCount": 15,
|
|||
|
|
"monthlyRecurring": 7500000,
|
|||
|
|
"yearlyRecurring": 90000000,
|
|||
|
|
"totalUsers": 150
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
## 핵심 로직
|
|||
|
|
|
|||
|
|
### MRR/ARR 계산
|
|||
|
|
|
|||
|
|
```
|
|||
|
|
MRR (Monthly Recurring Revenue)
|
|||
|
|
= SUM(active 구독의 monthly_fee) (billingCycle=monthly)
|
|||
|
|
+ SUM(active 구독의 monthly_fee / 12) (billingCycle=yearly를 월 환산)
|
|||
|
|
|
|||
|
|
ARR (Annual Recurring Revenue)
|
|||
|
|
= MRR × 12
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### 구독 상태 관리 (API 모델)
|
|||
|
|
|
|||
|
|
```
|
|||
|
|
pending (대기)
|
|||
|
|
↓ activate()
|
|||
|
|
active (활성)
|
|||
|
|
├── cancel(reason) → cancelled (취소)
|
|||
|
|
├── suspend() → suspended (일시정지)
|
|||
|
|
│ ↓ resume() → active (재활성)
|
|||
|
|
└── (만료) → expired
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### 구독 플랜
|
|||
|
|
|
|||
|
|
| 플랜 | 설명 |
|
|||
|
|
|------|------|
|
|||
|
|
| Starter | 소규모 기본 플랜 |
|
|||
|
|
| Business | 중규모 비즈니스 플랜 |
|
|||
|
|
| Enterprise | 대규모 기업 플랜 |
|
|||
|
|
|
|||
|
|
### 과금 주기
|
|||
|
|
|
|||
|
|
| 주기 | 설명 |
|
|||
|
|
|------|------|
|
|||
|
|
| monthly | 월간 과금 |
|
|||
|
|
| yearly | 연간 과금 |
|
|||
|
|
|
|||
|
|
## 모델
|
|||
|
|
|
|||
|
|
### Subscription (MNG)
|
|||
|
|
|
|||
|
|
**테이블**: `subscriptions`
|
|||
|
|
|
|||
|
|
| 필드 | 타입 | 설명 |
|
|||
|
|
|------|------|------|
|
|||
|
|
| `tenant_id` | bigint | 테넌트 ID |
|
|||
|
|
| `customer` | string | 고객사명 |
|
|||
|
|
| `plan` | string | 플랜명 (Starter/Business/Enterprise) |
|
|||
|
|
| `monthly_fee` | int | 월 정액 |
|
|||
|
|
| `billing_cycle` | string | monthly / yearly |
|
|||
|
|
| `start_date` | date | 시작일 |
|
|||
|
|
| `next_billing` | date | 다음 과금일 |
|
|||
|
|
| `status` | string | active / trial / cancelled |
|
|||
|
|
| `users` | int | 사용자 수 |
|
|||
|
|
| `memo` | text | 메모 |
|
|||
|
|
| `cancelled_at` | timestamp | 취소 일시 |
|
|||
|
|
| `cancel_reason` | string(500) | 취소 사유 |
|
|||
|
|
|
|||
|
|
- SoftDeletes 적용
|
|||
|
|
- Casts: monthly_fee, users → integer, start_date, next_billing → date
|
|||
|
|
- Scope: `forTenant($tenantId)`
|
|||
|
|
|
|||
|
|
### Subscription (API - 상세 구현)
|
|||
|
|
|
|||
|
|
API 프로젝트의 모델은 더 상세한 구독 라이프사이클을 지원합니다:
|
|||
|
|
|
|||
|
|
#### 상태 상수
|
|||
|
|
|
|||
|
|
```php
|
|||
|
|
const STATUS_ACTIVE = 'active';
|
|||
|
|
const STATUS_CANCELLED = 'cancelled';
|
|||
|
|
const STATUS_EXPIRED = 'expired';
|
|||
|
|
const STATUS_PENDING = 'pending';
|
|||
|
|
const STATUS_SUSPENDED = 'suspended';
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
#### Relationships
|
|||
|
|
|
|||
|
|
```php
|
|||
|
|
$subscription->tenant // BelongsTo Tenant
|
|||
|
|
$subscription->plan // BelongsTo Plan
|
|||
|
|
$subscription->payments // HasMany Payment
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
#### 주요 Accessor
|
|||
|
|
|
|||
|
|
```php
|
|||
|
|
$subscription->statusLabel // 상태 한글 라벨
|
|||
|
|
$subscription->isExpired // 만료 여부
|
|||
|
|
$subscription->remainingDays // 잔여 일수
|
|||
|
|
$subscription->isValid // 유효 여부 (active + 미만료)
|
|||
|
|
$subscription->totalPaid // 총 결제 금액
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
#### 주요 메서드
|
|||
|
|
|
|||
|
|
```php
|
|||
|
|
$subscription->activate() // 활성화
|
|||
|
|
$subscription->renew($newEndDate) // 갱신
|
|||
|
|
$subscription->cancel($reason) // 취소
|
|||
|
|
$subscription->suspend() // 일시정지
|
|||
|
|
$subscription->resume() // 재개
|
|||
|
|
$subscription->isCancellable() // 취소 가능 여부
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
## 서비스 클래스 (API)
|
|||
|
|
|
|||
|
|
### SubscriptionService
|
|||
|
|
|
|||
|
|
| 메서드 | 설명 |
|
|||
|
|
|--------|------|
|
|||
|
|
| `index(params)` | 페이지네이션 목록 (필터, 정렬) |
|
|||
|
|
| `current()` | 현재 활성 구독 조회 |
|
|||
|
|
| `show(id)` | 상세 (플랜, 최근 결제 포함) |
|
|||
|
|
| `store(data)` | 구독 생성 (결제 처리) |
|
|||
|
|
| `update(id, data)` | 구독 수정 |
|
|||
|
|
| `cancel(id, reason)` | 구독 취소 |
|
|||
|
|
| `suspend(id)` | 일시정지 |
|
|||
|
|
| `resume(id)` | 재개 |
|
|||
|
|
| `renew(id, newEndDate)` | 갱신 |
|
|||
|
|
|
|||
|
|
## 뷰 구성 (React)
|
|||
|
|
|
|||
|
|
### subscription.blade.php
|
|||
|
|
|
|||
|
|
```
|
|||
|
|
┌─ 페이지 헤더 ──────────────────────
|
|||
|
|
│ 제목: "구독료정산"
|
|||
|
|
│ [CSV 내보내기] [등록] 버튼
|
|||
|
|
│
|
|||
|
|
├─ 통계 카드 (4열) ──────────────────
|
|||
|
|
│ 활성 구독 | MRR (월간 반복 수익) | ARR (연간 반복 수익) | 총 사용자
|
|||
|
|
│
|
|||
|
|
├─ 필터 영역 ────────────────────────
|
|||
|
|
│ 검색 (고객사명) | 플랜 (Starter/Business/Enterprise) | 상태 (active/trial/cancelled)
|
|||
|
|
│
|
|||
|
|
├─ 구독 목록 테이블 ─────────────────
|
|||
|
|
│ 고객사 | 플랜 | 월정액 | 과금주기 | 다음과금일 | 사용자 | 상태 | 작업
|
|||
|
|
│ └─ 플랜: Starter(회색), Business(파랑), Enterprise(보라) 배지
|
|||
|
|
│ └─ 상태: active(초록), trial(파랑), cancelled(빨강) 배지
|
|||
|
|
│ └─ 작업: 수정/삭제 버튼
|
|||
|
|
│
|
|||
|
|
├─ 등록/수정 모달 ───────────────────
|
|||
|
|
│ 고객사명, 플랜 선택
|
|||
|
|
│ 월정액, 과금주기(월/연)
|
|||
|
|
│ 시작일, 다음 과금일
|
|||
|
|
│ 상태, 사용자 수, 메모
|
|||
|
|
│ [삭제] [취소] [등록/저장]
|
|||
|
|
│
|
|||
|
|
└─ 비어있을 때: 안내 메시지
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
## HTMX 호환성
|
|||
|
|
|
|||
|
|
- React 기반 페이지이므로 **HX-Redirect 필요**
|
|||
|
|
- `@push('scripts')` 블록에 React/Babel 스크립트 포함
|