From e7862ed6e6123a550a0f44870da30f2783132864 Mon Sep 17 00:00:00 2001 From: kent Date: Fri, 26 Dec 2025 15:46:48 +0900 Subject: [PATCH] =?UTF-8?q?feat:=20I-3=20=EB=B2=95=EC=9D=B8=EC=B9=B4?= =?UTF-8?q?=EB=93=9C=20=EC=82=AC=EC=9A=A9=EB=82=B4=EC=97=AD=20API=20?= =?UTF-8?q?=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - CardTransactionController: 카드 거래내역 조회 API - CardTransactionService: 카드 거래 조회 로직 - Withdrawal 모델 카드 필드 확장 - Swagger 문서화 - withdrawals 테이블 카드 필드 마이그레이션 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- .../Api/V1/CardTransactionController.php | 92 ++++++ app/Models/Tenants/Withdrawal.php | 13 + app/Services/CardTransactionService.php | 183 +++++++++++ app/Swagger/v1/CardTransactionApi.php | 301 ++++++++++++++++++ ...3_add_card_fields_to_withdrawals_table.php | 35 ++ 5 files changed, 624 insertions(+) create mode 100644 app/Http/Controllers/Api/V1/CardTransactionController.php create mode 100644 app/Services/CardTransactionService.php create mode 100644 app/Swagger/v1/CardTransactionApi.php create mode 100644 database/migrations/2025_12_26_133933_add_card_fields_to_withdrawals_table.php diff --git a/app/Http/Controllers/Api/V1/CardTransactionController.php b/app/Http/Controllers/Api/V1/CardTransactionController.php new file mode 100644 index 0000000..13c9b37 --- /dev/null +++ b/app/Http/Controllers/Api/V1/CardTransactionController.php @@ -0,0 +1,92 @@ +validate([ + 'start_date' => 'nullable|date', + 'end_date' => 'nullable|date|after_or_equal:start_date', + 'card_id' => 'nullable|integer', + 'search' => 'nullable|string|max:100', + 'sort_by' => 'nullable|in:used_at,amount,merchant_name,created_at', + 'sort_dir' => 'nullable|in:asc,desc', + 'per_page' => 'nullable|integer|min:1|max:100', + 'page' => 'nullable|integer|min:1', + ]); + + return $this->service->index($params); + }, __('message.fetched')); + } + + /** + * 카드 거래 요약 통계 + */ + public function summary(Request $request): JsonResponse + { + return ApiResponse::handle(function () use ($request) { + $params = $request->validate([ + 'start_date' => 'nullable|date', + 'end_date' => 'nullable|date|after_or_equal:start_date', + ]); + + return $this->service->summary($params); + }, __('message.fetched')); + } + + /** + * 계정과목 일괄 수정 + */ + public function bulkUpdateAccountCode(Request $request): JsonResponse + { + return ApiResponse::handle(function () use ($request) { + $validated = $request->validate([ + 'ids' => 'required|array|min:1', + 'ids.*' => 'required|integer', + 'account_code' => 'required|string|max:20', + ]); + + $updatedCount = $this->service->bulkUpdateAccountCode( + $validated['ids'], + $validated['account_code'] + ); + + return ['updated_count' => $updatedCount]; + }, __('message.updated')); + } + + /** + * 단일 카드 거래 조회 + */ + public function show(int $id): JsonResponse + { + return ApiResponse::handle(function () use ($id) { + $transaction = $this->service->show($id); + + if (! $transaction) { + throw new \Illuminate\Database\Eloquent\ModelNotFoundException; + } + + return $transaction; + }, __('message.fetched')); + } +} diff --git a/app/Models/Tenants/Withdrawal.php b/app/Models/Tenants/Withdrawal.php index 9b3a84a..082ef1a 100644 --- a/app/Models/Tenants/Withdrawal.php +++ b/app/Models/Tenants/Withdrawal.php @@ -14,9 +14,12 @@ class Withdrawal extends Model protected $fillable = [ 'tenant_id', 'withdrawal_date', + 'used_at', 'client_id', 'client_name', + 'merchant_name', 'bank_account_id', + 'card_id', 'amount', 'payment_method', 'account_code', @@ -30,9 +33,11 @@ class Withdrawal extends Model protected $casts = [ 'withdrawal_date' => 'date', + 'used_at' => 'datetime', 'amount' => 'decimal:2', 'client_id' => 'integer', 'bank_account_id' => 'integer', + 'card_id' => 'integer', 'reference_id' => 'integer', ]; @@ -62,6 +67,14 @@ public function bankAccount(): BelongsTo return $this->belongsTo(BankAccount::class); } + /** + * 카드 관계 + */ + public function card(): BelongsTo + { + return $this->belongsTo(Card::class); + } + /** * 생성자 관계 */ diff --git a/app/Services/CardTransactionService.php b/app/Services/CardTransactionService.php new file mode 100644 index 0000000..f1c64bd --- /dev/null +++ b/app/Services/CardTransactionService.php @@ -0,0 +1,183 @@ +where('payment_method', 'card') + ->with(['card', 'card.assignedUser', 'creator']); + + // 날짜 필터 + if (! empty($params['start_date'])) { + $query->where(function ($q) use ($params) { + $q->whereDate('used_at', '>=', $params['start_date']) + ->orWhere(function ($q2) use ($params) { + $q2->whereNull('used_at') + ->whereDate('withdrawal_date', '>=', $params['start_date']); + }); + }); + } + + if (! empty($params['end_date'])) { + $query->where(function ($q) use ($params) { + $q->whereDate('used_at', '<=', $params['end_date']) + ->orWhere(function ($q2) use ($params) { + $q2->whereNull('used_at') + ->whereDate('withdrawal_date', '<=', $params['end_date']); + }); + }); + } + + // 카드 필터 + if (! empty($params['card_id'])) { + $query->where('card_id', $params['card_id']); + } + + // 검색 (카드번호, 가맹점명, 적요) + if (! empty($params['search'])) { + $search = $params['search']; + $query->where(function ($q) use ($search) { + $q->where('merchant_name', 'like', "%{$search}%") + ->orWhere('description', 'like', "%{$search}%") + ->orWhereHas('card', function ($cardQ) use ($search) { + $cardQ->where('card_name', 'like', "%{$search}%") + ->orWhere('card_company', 'like', "%{$search}%"); + }); + }); + } + + // 정렬 + $sortBy = $params['sort_by'] ?? 'used_at'; + $sortDir = $params['sort_dir'] ?? 'desc'; + + // 정렬 필드 매핑 + $sortableFields = [ + 'used_at' => DB::raw('COALESCE(used_at, withdrawal_date)'), + 'amount' => 'amount', + 'merchant_name' => 'merchant_name', + 'created_at' => 'created_at', + ]; + + if (array_key_exists($sortBy, $sortableFields)) { + $query->orderBy($sortableFields[$sortBy], $sortDir); + } else { + $query->orderBy(DB::raw('COALESCE(used_at, withdrawal_date)'), 'desc'); + } + + $perPage = $params['per_page'] ?? 20; + + return $query->paginate($perPage); + } + + /** + * 카드 거래 요약 통계 + * + * @param array{start_date?: string, end_date?: string} $params + * @return array{ + * previous_month_total: float, + * current_month_total: float, + * total_count: int, + * total_amount: float + * } + */ + public function summary(array $params = []): array + { + // 전월 기준일 계산 + $currentDate = now(); + $currentMonthStart = $currentDate->copy()->startOfMonth()->toDateString(); + $currentMonthEnd = $currentDate->copy()->endOfMonth()->toDateString(); + $previousMonthStart = $currentDate->copy()->subMonth()->startOfMonth()->toDateString(); + $previousMonthEnd = $currentDate->copy()->subMonth()->endOfMonth()->toDateString(); + + // 전월 카드 사용액 + $previousMonthTotal = Withdrawal::query() + ->where('payment_method', 'card') + ->where(function ($q) use ($previousMonthStart, $previousMonthEnd) { + $q->whereBetween(DB::raw('DATE(COALESCE(used_at, withdrawal_date))'), [$previousMonthStart, $previousMonthEnd]); + }) + ->sum('amount'); + + // 당월 카드 사용액 + $currentMonthTotal = Withdrawal::query() + ->where('payment_method', 'card') + ->where(function ($q) use ($currentMonthStart, $currentMonthEnd) { + $q->whereBetween(DB::raw('DATE(COALESCE(used_at, withdrawal_date))'), [$currentMonthStart, $currentMonthEnd]); + }) + ->sum('amount'); + + // 조회 기간 내 총 건수/금액 (옵션) + $periodQuery = Withdrawal::query()->where('payment_method', 'card'); + + if (! empty($params['start_date'])) { + $periodQuery->where(DB::raw('DATE(COALESCE(used_at, withdrawal_date))'), '>=', $params['start_date']); + } + if (! empty($params['end_date'])) { + $periodQuery->where(DB::raw('DATE(COALESCE(used_at, withdrawal_date))'), '<=', $params['end_date']); + } + + $totalCount = $periodQuery->count(); + $totalAmount = (clone $periodQuery)->sum('amount'); + + return [ + 'previous_month_total' => (float) $previousMonthTotal, + 'current_month_total' => (float) $currentMonthTotal, + 'total_count' => $totalCount, + 'total_amount' => (float) $totalAmount, + ]; + } + + /** + * 계정과목 일괄 수정 + * + * @param array $ids 거래 ID 배열 + * @param string $accountCode 계정과목 코드 + * @return int 수정된 건수 + */ + public function bulkUpdateAccountCode(array $ids, string $accountCode): int + { + return Withdrawal::query() + ->whereIn('id', $ids) + ->where('payment_method', 'card') + ->update([ + 'account_code' => $accountCode, + 'updated_by' => $this->apiUserId(), + ]); + } + + /** + * 단일 카드 거래 조회 + */ + public function show(int $id): ?Withdrawal + { + return Withdrawal::query() + ->where('payment_method', 'card') + ->with(['card', 'card.assignedUser', 'creator']) + ->find($id); + } +} diff --git a/app/Swagger/v1/CardTransactionApi.php b/app/Swagger/v1/CardTransactionApi.php new file mode 100644 index 0000000..04c1db4 --- /dev/null +++ b/app/Swagger/v1/CardTransactionApi.php @@ -0,0 +1,301 @@ +unsignedBigInteger('card_id')->nullable()->after('bank_account_id')->comment('카드 ID'); + $table->string('merchant_name', 100)->nullable()->after('client_name')->comment('가맹점명 (카드 결제시)'); + $table->timestamp('used_at')->nullable()->after('withdrawal_date')->comment('사용일시 (카드 결제시)'); + + // 인덱스 + $table->index('card_id', 'idx_card'); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::table('withdrawals', function (Blueprint $table) { + $table->dropIndex('idx_card'); + $table->dropColumn(['card_id', 'merchant_name', 'used_at']); + }); + } +};