feat: 구독/결제 API 확장 (Plan, Subscription, Payment)
- Plan/Subscription/Payment 모델에 상태 상수, 스코프, 헬퍼 메서드 추가
- PlanService, SubscriptionService, PaymentService 생성
- PlanController, SubscriptionController, PaymentController 생성
- FormRequest 9개 생성 (Plan 3개, Subscription 3개, Payment 3개)
- Swagger 문서 3개 생성 (PlanApi, SubscriptionApi, PaymentApi)
- API 라우트 22개 등록 (Plan 7개, Subscription 8개, Payment 7개)
- Pint 코드 스타일 정리
2025-12-18 16:20:29 +09:00
|
|
|
<?php
|
|
|
|
|
|
|
|
|
|
namespace App\Services;
|
|
|
|
|
|
2025-12-30 22:27:08 +09:00
|
|
|
use App\Models\ApiRequestLog;
|
2025-12-19 16:53:49 +09:00
|
|
|
use App\Models\Tenants\DataExport;
|
feat: 구독/결제 API 확장 (Plan, Subscription, Payment)
- Plan/Subscription/Payment 모델에 상태 상수, 스코프, 헬퍼 메서드 추가
- PlanService, SubscriptionService, PaymentService 생성
- PlanController, SubscriptionController, PaymentController 생성
- FormRequest 9개 생성 (Plan 3개, Subscription 3개, Payment 3개)
- Swagger 문서 3개 생성 (PlanApi, SubscriptionApi, PaymentApi)
- API 라우트 22개 등록 (Plan 7개, Subscription 8개, Payment 7개)
- Pint 코드 스타일 정리
2025-12-18 16:20:29 +09:00
|
|
|
use App\Models\Tenants\Payment;
|
|
|
|
|
use App\Models\Tenants\Plan;
|
|
|
|
|
use App\Models\Tenants\Subscription;
|
2025-12-19 16:53:49 +09:00
|
|
|
use App\Models\Tenants\Tenant;
|
feat: 구독/결제 API 확장 (Plan, Subscription, Payment)
- Plan/Subscription/Payment 모델에 상태 상수, 스코프, 헬퍼 메서드 추가
- PlanService, SubscriptionService, PaymentService 생성
- PlanController, SubscriptionController, PaymentController 생성
- FormRequest 9개 생성 (Plan 3개, Subscription 3개, Payment 3개)
- Swagger 문서 3개 생성 (PlanApi, SubscriptionApi, PaymentApi)
- API 라우트 22개 등록 (Plan 7개, Subscription 8개, Payment 7개)
- Pint 코드 스타일 정리
2025-12-18 16:20:29 +09:00
|
|
|
use Illuminate\Contracts\Pagination\LengthAwarePaginator;
|
|
|
|
|
use Illuminate\Support\Facades\DB;
|
|
|
|
|
use Symfony\Component\HttpKernel\Exception\BadRequestHttpException;
|
2025-12-19 16:53:49 +09:00
|
|
|
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
|
feat: 구독/결제 API 확장 (Plan, Subscription, Payment)
- Plan/Subscription/Payment 모델에 상태 상수, 스코프, 헬퍼 메서드 추가
- PlanService, SubscriptionService, PaymentService 생성
- PlanController, SubscriptionController, PaymentController 생성
- FormRequest 9개 생성 (Plan 3개, Subscription 3개, Payment 3개)
- Swagger 문서 3개 생성 (PlanApi, SubscriptionApi, PaymentApi)
- API 라우트 22개 등록 (Plan 7개, Subscription 8개, Payment 7개)
- Pint 코드 스타일 정리
2025-12-18 16:20:29 +09:00
|
|
|
|
|
|
|
|
class SubscriptionService extends Service
|
|
|
|
|
{
|
|
|
|
|
// =========================================================================
|
|
|
|
|
// 구독 목록/상세
|
|
|
|
|
// =========================================================================
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 구독 목록
|
|
|
|
|
*/
|
|
|
|
|
public function index(array $params): LengthAwarePaginator
|
|
|
|
|
{
|
|
|
|
|
$tenantId = $this->tenantId();
|
|
|
|
|
|
|
|
|
|
$query = Subscription::query()
|
|
|
|
|
->where('tenant_id', $tenantId)
|
|
|
|
|
->with(['plan:id,name,code,price,billing_cycle']);
|
|
|
|
|
|
|
|
|
|
// 상태 필터
|
|
|
|
|
if (! empty($params['status'])) {
|
|
|
|
|
$query->where('status', $params['status']);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 유효한 구독만
|
|
|
|
|
if (! empty($params['valid_only']) && $params['valid_only']) {
|
|
|
|
|
$query->valid();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 만료 예정 (N일 이내)
|
|
|
|
|
if (! empty($params['expiring_within'])) {
|
|
|
|
|
$query->expiringWithin((int) $params['expiring_within']);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 날짜 범위 필터
|
|
|
|
|
if (! empty($params['start_date'])) {
|
|
|
|
|
$query->where('started_at', '>=', $params['start_date']);
|
|
|
|
|
}
|
|
|
|
|
if (! empty($params['end_date'])) {
|
|
|
|
|
$query->where('started_at', '<=', $params['end_date']);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 정렬
|
|
|
|
|
$sortBy = $params['sort_by'] ?? 'started_at';
|
|
|
|
|
$sortDir = $params['sort_dir'] ?? 'desc';
|
|
|
|
|
$query->orderBy($sortBy, $sortDir);
|
|
|
|
|
|
|
|
|
|
$perPage = $params['per_page'] ?? 20;
|
|
|
|
|
|
|
|
|
|
return $query->paginate($perPage);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 현재 활성 구독
|
|
|
|
|
*/
|
|
|
|
|
public function current(): ?Subscription
|
|
|
|
|
{
|
|
|
|
|
$tenantId = $this->tenantId();
|
|
|
|
|
|
|
|
|
|
return Subscription::query()
|
|
|
|
|
->where('tenant_id', $tenantId)
|
|
|
|
|
->valid()
|
|
|
|
|
->with(['plan', 'payments' => function ($q) {
|
|
|
|
|
$q->completed()->orderBy('paid_at', 'desc')->limit(5);
|
|
|
|
|
}])
|
|
|
|
|
->orderBy('started_at', 'desc')
|
|
|
|
|
->first();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 구독 상세
|
|
|
|
|
*/
|
|
|
|
|
public function show(int $id): Subscription
|
|
|
|
|
{
|
|
|
|
|
$tenantId = $this->tenantId();
|
|
|
|
|
|
|
|
|
|
return Subscription::query()
|
|
|
|
|
->where('tenant_id', $tenantId)
|
|
|
|
|
->with([
|
|
|
|
|
'plan',
|
|
|
|
|
'payments' => function ($q) {
|
|
|
|
|
$q->orderBy('paid_at', 'desc');
|
|
|
|
|
},
|
|
|
|
|
])
|
|
|
|
|
->findOrFail($id);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// =========================================================================
|
|
|
|
|
// 구독 생성/취소
|
|
|
|
|
// =========================================================================
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 구독 생성 (결제 포함)
|
|
|
|
|
*/
|
|
|
|
|
public function store(array $data): Subscription
|
|
|
|
|
{
|
|
|
|
|
$tenantId = $this->tenantId();
|
|
|
|
|
$userId = $this->apiUserId();
|
|
|
|
|
|
|
|
|
|
// 요금제 확인
|
|
|
|
|
$plan = Plan::active()->findOrFail($data['plan_id']);
|
|
|
|
|
|
|
|
|
|
// 이미 활성 구독이 있는지 확인
|
|
|
|
|
$existingSubscription = Subscription::query()
|
|
|
|
|
->where('tenant_id', $tenantId)
|
|
|
|
|
->valid()
|
|
|
|
|
->first();
|
|
|
|
|
|
|
|
|
|
if ($existingSubscription) {
|
|
|
|
|
throw new BadRequestHttpException(__('error.subscription.already_active'));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return DB::transaction(function () use ($data, $plan, $tenantId, $userId) {
|
|
|
|
|
// 구독 생성
|
|
|
|
|
$subscription = Subscription::create([
|
|
|
|
|
'tenant_id' => $tenantId,
|
|
|
|
|
'plan_id' => $plan->id,
|
|
|
|
|
'started_at' => $data['started_at'] ?? now(),
|
|
|
|
|
'status' => Subscription::STATUS_PENDING,
|
|
|
|
|
'created_by' => $userId,
|
|
|
|
|
'updated_by' => $userId,
|
|
|
|
|
]);
|
|
|
|
|
|
|
|
|
|
// 결제 생성 (무료 요금제가 아닌 경우)
|
|
|
|
|
if ($plan->price > 0) {
|
|
|
|
|
$payment = Payment::create([
|
|
|
|
|
'subscription_id' => $subscription->id,
|
|
|
|
|
'amount' => $plan->price,
|
|
|
|
|
'payment_method' => $data['payment_method'] ?? Payment::METHOD_CARD,
|
|
|
|
|
'status' => Payment::STATUS_PENDING,
|
|
|
|
|
'created_by' => $userId,
|
|
|
|
|
'updated_by' => $userId,
|
|
|
|
|
]);
|
|
|
|
|
|
|
|
|
|
// 결제 완료 처리 (실제 PG 연동 시 수정 필요)
|
|
|
|
|
if (! empty($data['auto_complete']) && $data['auto_complete']) {
|
|
|
|
|
$payment->complete($data['transaction_id'] ?? null);
|
|
|
|
|
|
|
|
|
|
// 구독 활성화
|
|
|
|
|
$subscription->activate();
|
|
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
// 무료 요금제는 바로 활성화
|
|
|
|
|
Payment::create([
|
|
|
|
|
'subscription_id' => $subscription->id,
|
|
|
|
|
'amount' => 0,
|
|
|
|
|
'payment_method' => Payment::METHOD_FREE,
|
|
|
|
|
'status' => Payment::STATUS_COMPLETED,
|
|
|
|
|
'paid_at' => now(),
|
|
|
|
|
'created_by' => $userId,
|
|
|
|
|
'updated_by' => $userId,
|
|
|
|
|
]);
|
|
|
|
|
|
|
|
|
|
$subscription->activate();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return $subscription->fresh(['plan', 'payments']);
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 구독 취소
|
|
|
|
|
*/
|
|
|
|
|
public function cancel(int $id, ?string $reason = null): Subscription
|
|
|
|
|
{
|
|
|
|
|
$tenantId = $this->tenantId();
|
|
|
|
|
$userId = $this->apiUserId();
|
|
|
|
|
|
|
|
|
|
$subscription = Subscription::query()
|
|
|
|
|
->where('tenant_id', $tenantId)
|
|
|
|
|
->findOrFail($id);
|
|
|
|
|
|
|
|
|
|
if (! $subscription->isCancellable()) {
|
|
|
|
|
throw new BadRequestHttpException(__('error.subscription.not_cancellable'));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
$subscription->cancel($reason);
|
|
|
|
|
$subscription->updated_by = $userId;
|
|
|
|
|
$subscription->save();
|
|
|
|
|
|
|
|
|
|
return $subscription->fresh(['plan']);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 구독 갱신
|
|
|
|
|
*/
|
|
|
|
|
public function renew(int $id, array $data = []): Subscription
|
|
|
|
|
{
|
|
|
|
|
$tenantId = $this->tenantId();
|
|
|
|
|
$userId = $this->apiUserId();
|
|
|
|
|
|
|
|
|
|
$subscription = Subscription::query()
|
|
|
|
|
->where('tenant_id', $tenantId)
|
|
|
|
|
->findOrFail($id);
|
|
|
|
|
|
|
|
|
|
if ($subscription->status !== Subscription::STATUS_ACTIVE) {
|
|
|
|
|
throw new BadRequestHttpException(__('error.subscription.not_renewable'));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return DB::transaction(function () use ($subscription, $data, $userId) {
|
|
|
|
|
$plan = $subscription->plan;
|
|
|
|
|
|
|
|
|
|
// 결제 생성
|
|
|
|
|
if ($plan->price > 0) {
|
|
|
|
|
$payment = Payment::create([
|
|
|
|
|
'subscription_id' => $subscription->id,
|
|
|
|
|
'amount' => $plan->price,
|
|
|
|
|
'payment_method' => $data['payment_method'] ?? Payment::METHOD_CARD,
|
|
|
|
|
'status' => Payment::STATUS_PENDING,
|
|
|
|
|
'created_by' => $userId,
|
|
|
|
|
'updated_by' => $userId,
|
|
|
|
|
]);
|
|
|
|
|
|
|
|
|
|
// 결제 완료 처리
|
|
|
|
|
if (! empty($data['auto_complete']) && $data['auto_complete']) {
|
|
|
|
|
$payment->complete($data['transaction_id'] ?? null);
|
|
|
|
|
$subscription->renew();
|
|
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
// 무료 갱신
|
|
|
|
|
Payment::create([
|
|
|
|
|
'subscription_id' => $subscription->id,
|
|
|
|
|
'amount' => 0,
|
|
|
|
|
'payment_method' => Payment::METHOD_FREE,
|
|
|
|
|
'status' => Payment::STATUS_COMPLETED,
|
|
|
|
|
'paid_at' => now(),
|
|
|
|
|
'created_by' => $userId,
|
|
|
|
|
'updated_by' => $userId,
|
|
|
|
|
]);
|
|
|
|
|
|
|
|
|
|
$subscription->renew();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
$subscription->updated_by = $userId;
|
|
|
|
|
$subscription->save();
|
|
|
|
|
|
|
|
|
|
return $subscription->fresh(['plan', 'payments']);
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// =========================================================================
|
|
|
|
|
// 구독 상태 관리
|
|
|
|
|
// =========================================================================
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 구독 일시정지
|
|
|
|
|
*/
|
|
|
|
|
public function suspend(int $id): Subscription
|
|
|
|
|
{
|
|
|
|
|
$tenantId = $this->tenantId();
|
|
|
|
|
$userId = $this->apiUserId();
|
|
|
|
|
|
|
|
|
|
$subscription = Subscription::query()
|
|
|
|
|
->where('tenant_id', $tenantId)
|
|
|
|
|
->findOrFail($id);
|
|
|
|
|
|
|
|
|
|
if (! $subscription->suspend()) {
|
|
|
|
|
throw new BadRequestHttpException(__('error.subscription.not_suspendable'));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
$subscription->updated_by = $userId;
|
|
|
|
|
$subscription->save();
|
|
|
|
|
|
|
|
|
|
return $subscription->fresh(['plan']);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 구독 재개
|
|
|
|
|
*/
|
|
|
|
|
public function resume(int $id): Subscription
|
|
|
|
|
{
|
|
|
|
|
$tenantId = $this->tenantId();
|
|
|
|
|
$userId = $this->apiUserId();
|
|
|
|
|
|
|
|
|
|
$subscription = Subscription::query()
|
|
|
|
|
->where('tenant_id', $tenantId)
|
|
|
|
|
->findOrFail($id);
|
|
|
|
|
|
|
|
|
|
if (! $subscription->resume()) {
|
|
|
|
|
throw new BadRequestHttpException(__('error.subscription.not_resumable'));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
$subscription->updated_by = $userId;
|
|
|
|
|
$subscription->save();
|
|
|
|
|
|
|
|
|
|
return $subscription->fresh(['plan']);
|
|
|
|
|
}
|
2025-12-19 16:53:49 +09:00
|
|
|
|
|
|
|
|
// =========================================================================
|
|
|
|
|
// 사용량 조회
|
|
|
|
|
// =========================================================================
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 사용량 조회
|
|
|
|
|
*/
|
|
|
|
|
public function usage(): array
|
|
|
|
|
{
|
|
|
|
|
$tenantId = $this->tenantId();
|
|
|
|
|
|
|
|
|
|
$tenant = Tenant::with(['subscription.plan'])->findOrFail($tenantId);
|
|
|
|
|
|
|
|
|
|
// 사용자 수
|
|
|
|
|
$userCount = $tenant->users()->count();
|
|
|
|
|
$maxUsers = $tenant->max_users ?? 0;
|
|
|
|
|
|
|
|
|
|
// 저장공간
|
|
|
|
|
$storageUsed = $tenant->storage_used ?? 0;
|
|
|
|
|
$storageLimit = $tenant->storage_limit ?? 0;
|
|
|
|
|
|
2025-12-30 22:27:08 +09:00
|
|
|
// API 호출 수 (일간 - 로그는 1일 보관)
|
|
|
|
|
$apiCallsUsed = ApiRequestLog::where('tenant_id', $tenantId)
|
|
|
|
|
->whereDate('created_at', now()->toDateString())
|
|
|
|
|
->count();
|
|
|
|
|
// 기본 일간 API 호출 제한 (10,000회)
|
|
|
|
|
$apiCallsLimit = 10000;
|
|
|
|
|
|
2025-12-19 16:53:49 +09:00
|
|
|
// 구독 정보
|
|
|
|
|
$subscription = $tenant->subscription;
|
|
|
|
|
$remainingDays = null;
|
|
|
|
|
$planName = null;
|
|
|
|
|
|
|
|
|
|
if ($subscription && $subscription->is_valid) {
|
|
|
|
|
$remainingDays = $subscription->remaining_days;
|
|
|
|
|
$planName = $subscription->plan?->name;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return [
|
|
|
|
|
'users' => [
|
|
|
|
|
'used' => $userCount,
|
|
|
|
|
'limit' => $maxUsers,
|
|
|
|
|
'percentage' => $maxUsers > 0 ? round(($userCount / $maxUsers) * 100, 1) : 0,
|
|
|
|
|
],
|
|
|
|
|
'storage' => [
|
|
|
|
|
'used' => $storageUsed,
|
|
|
|
|
'used_formatted' => $tenant->getStorageUsedFormatted(),
|
|
|
|
|
'limit' => $storageLimit,
|
|
|
|
|
'limit_formatted' => $tenant->getStorageLimitFormatted(),
|
|
|
|
|
'percentage' => $storageLimit > 0 ? round(($storageUsed / $storageLimit) * 100, 1) : 0,
|
|
|
|
|
],
|
2025-12-30 22:27:08 +09:00
|
|
|
'api_calls' => [
|
|
|
|
|
'used' => $apiCallsUsed,
|
|
|
|
|
'limit' => $apiCallsLimit,
|
|
|
|
|
'percentage' => $apiCallsLimit > 0 ? round(($apiCallsUsed / $apiCallsLimit) * 100, 1) : 0,
|
|
|
|
|
],
|
2025-12-19 16:53:49 +09:00
|
|
|
'subscription' => [
|
|
|
|
|
'plan' => $planName,
|
|
|
|
|
'status' => $subscription?->status,
|
|
|
|
|
'remaining_days' => $remainingDays,
|
|
|
|
|
'started_at' => $subscription?->started_at?->toDateString(),
|
|
|
|
|
'ended_at' => $subscription?->ended_at?->toDateString(),
|
|
|
|
|
],
|
|
|
|
|
];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// =========================================================================
|
|
|
|
|
// 데이터 내보내기
|
|
|
|
|
// =========================================================================
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 내보내기 요청 생성
|
|
|
|
|
*/
|
|
|
|
|
public function createExport(array $data): DataExport
|
|
|
|
|
{
|
|
|
|
|
$tenantId = $this->tenantId();
|
|
|
|
|
$userId = $this->apiUserId();
|
|
|
|
|
|
|
|
|
|
// 진행 중인 내보내기가 있는지 확인
|
|
|
|
|
$pendingExport = DataExport::where('tenant_id', $tenantId)
|
|
|
|
|
->whereIn('status', [DataExport::STATUS_PENDING, DataExport::STATUS_PROCESSING])
|
|
|
|
|
->first();
|
|
|
|
|
|
|
|
|
|
if ($pendingExport) {
|
|
|
|
|
throw new BadRequestHttpException(__('error.export.already_in_progress'));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
$export = DataExport::create([
|
|
|
|
|
'tenant_id' => $tenantId,
|
|
|
|
|
'export_type' => $data['export_type'] ?? DataExport::TYPE_ALL,
|
|
|
|
|
'status' => DataExport::STATUS_PENDING,
|
|
|
|
|
'options' => $data['options'] ?? null,
|
|
|
|
|
'created_by' => $userId,
|
|
|
|
|
]);
|
|
|
|
|
|
|
|
|
|
// TODO: 비동기 Job 디스패치
|
|
|
|
|
// dispatch(new ProcessDataExport($export));
|
|
|
|
|
|
|
|
|
|
return $export;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 내보내기 상태 조회
|
|
|
|
|
*/
|
|
|
|
|
public function getExport(int $id): DataExport
|
|
|
|
|
{
|
|
|
|
|
$tenantId = $this->tenantId();
|
|
|
|
|
|
|
|
|
|
$export = DataExport::where('tenant_id', $tenantId)->find($id);
|
|
|
|
|
|
|
|
|
|
if (! $export) {
|
|
|
|
|
throw new NotFoundHttpException(__('error.export.not_found'));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return $export;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 내보내기 목록 조회
|
|
|
|
|
*/
|
|
|
|
|
public function getExports(array $params = []): LengthAwarePaginator
|
|
|
|
|
{
|
|
|
|
|
$tenantId = $this->tenantId();
|
|
|
|
|
|
|
|
|
|
$query = DataExport::where('tenant_id', $tenantId)
|
|
|
|
|
->with('creator:id,name,email');
|
|
|
|
|
|
|
|
|
|
// 상태 필터
|
|
|
|
|
if (! empty($params['status'])) {
|
|
|
|
|
$query->where('status', $params['status']);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 유형 필터
|
|
|
|
|
if (! empty($params['export_type'])) {
|
|
|
|
|
$query->where('export_type', $params['export_type']);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
$query->orderBy('created_at', 'desc');
|
|
|
|
|
|
|
|
|
|
$perPage = $params['per_page'] ?? 20;
|
|
|
|
|
|
|
|
|
|
return $query->paginate($perPage);
|
|
|
|
|
}
|
feat: 구독/결제 API 확장 (Plan, Subscription, Payment)
- Plan/Subscription/Payment 모델에 상태 상수, 스코프, 헬퍼 메서드 추가
- PlanService, SubscriptionService, PaymentService 생성
- PlanController, SubscriptionController, PaymentController 생성
- FormRequest 9개 생성 (Plan 3개, Subscription 3개, Payment 3개)
- Swagger 문서 3개 생성 (PlanApi, SubscriptionApi, PaymentApi)
- API 라우트 22개 등록 (Plan 7개, Subscription 8개, Payment 7개)
- Pint 코드 스타일 정리
2025-12-18 16:20:29 +09:00
|
|
|
}
|