From 2aa1d78e6251c315c6bf7af74f75de973719e652 Mon Sep 17 00:00:00 2001 From: kent Date: Fri, 26 Dec 2025 15:47:03 +0900 Subject: [PATCH] =?UTF-8?q?feat:=20I-4=20=EA=B1=B0=EB=9E=98=ED=86=B5?= =?UTF-8?q?=EC=9E=A5=20=EC=A1=B0=ED=9A=8C=20API=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - BankTransactionController: 은행 거래내역 조회 API - BankTransactionService: 은행 거래 조회 로직 - Swagger 문서화 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- .../Api/V1/BankTransactionController.php | 66 +++++ app/Services/BankTransactionService.php | 229 ++++++++++++++++ app/Swagger/v1/BankTransactionApi.php | 248 ++++++++++++++++++ 3 files changed, 543 insertions(+) create mode 100644 app/Http/Controllers/Api/V1/BankTransactionController.php create mode 100644 app/Services/BankTransactionService.php create mode 100644 app/Swagger/v1/BankTransactionApi.php diff --git a/app/Http/Controllers/Api/V1/BankTransactionController.php b/app/Http/Controllers/Api/V1/BankTransactionController.php new file mode 100644 index 0000000..3109aa2 --- /dev/null +++ b/app/Http/Controllers/Api/V1/BankTransactionController.php @@ -0,0 +1,66 @@ +validate([ + 'start_date' => 'nullable|date', + 'end_date' => 'nullable|date|after_or_equal:start_date', + 'bank_account_id' => 'nullable|integer', + 'transaction_type' => 'nullable|string|max:50', + 'search' => 'nullable|string|max:100', + 'sort_by' => 'nullable|in:transaction_date,amount', + '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 accounts(): JsonResponse + { + return ApiResponse::handle(function () { + return $this->service->getAccountOptions(); + }, __('message.fetched')); + } +} diff --git a/app/Services/BankTransactionService.php b/app/Services/BankTransactionService.php new file mode 100644 index 0000000..9f84f5e --- /dev/null +++ b/app/Services/BankTransactionService.php @@ -0,0 +1,229 @@ +tenantId(); + + // 입금 쿼리 (payment_method = 'transfer') + $depositsQuery = DB::table('deposits') + ->join('bank_accounts', 'deposits.bank_account_id', '=', 'bank_accounts.id') + ->leftJoin('clients', 'deposits.client_id', '=', 'clients.id') + ->where('deposits.tenant_id', $tenantId) + ->where('deposits.payment_method', 'transfer') + ->whereNull('deposits.deleted_at') + ->select([ + 'deposits.id', + DB::raw("'deposit' as type"), + 'deposits.deposit_date as transaction_date', + 'bank_accounts.id as bank_account_id', + 'bank_accounts.bank_name', + 'bank_accounts.account_name', + 'deposits.description as note', + 'deposits.client_id as vendor_id', + DB::raw('COALESCE(clients.name, deposits.client_name) as vendor_name'), + 'deposits.client_name as depositor_name', + 'deposits.amount as deposit_amount', + DB::raw('0 as withdrawal_amount'), + 'deposits.account_code as transaction_type', + DB::raw("CONCAT('deposit-', deposits.id) as source_id"), + 'deposits.created_at', + 'deposits.updated_at', + ]); + + // 출금 쿼리 (payment_method = 'transfer') + $withdrawalsQuery = DB::table('withdrawals') + ->join('bank_accounts', 'withdrawals.bank_account_id', '=', 'bank_accounts.id') + ->leftJoin('clients', 'withdrawals.client_id', '=', 'clients.id') + ->where('withdrawals.tenant_id', $tenantId) + ->where('withdrawals.payment_method', 'transfer') + ->whereNull('withdrawals.deleted_at') + ->select([ + 'withdrawals.id', + DB::raw("'withdrawal' as type"), + 'withdrawals.withdrawal_date as transaction_date', + 'bank_accounts.id as bank_account_id', + 'bank_accounts.bank_name', + 'bank_accounts.account_name', + 'withdrawals.description as note', + 'withdrawals.client_id as vendor_id', + DB::raw('COALESCE(clients.name, withdrawals.client_name) as vendor_name'), + 'withdrawals.client_name as depositor_name', + DB::raw('0 as deposit_amount'), + 'withdrawals.amount as withdrawal_amount', + 'withdrawals.account_code as transaction_type', + DB::raw("CONCAT('withdrawal-', withdrawals.id) as source_id"), + 'withdrawals.created_at', + 'withdrawals.updated_at', + ]); + + // 필터 적용: 날짜 범위 + if ($startDate) { + $depositsQuery->whereDate('deposits.deposit_date', '>=', $startDate); + $withdrawalsQuery->whereDate('withdrawals.withdrawal_date', '>=', $startDate); + } + if ($endDate) { + $depositsQuery->whereDate('deposits.deposit_date', '<=', $endDate); + $withdrawalsQuery->whereDate('withdrawals.withdrawal_date', '<=', $endDate); + } + + // 필터 적용: 계좌 + if ($bankAccountId) { + $depositsQuery->where('deposits.bank_account_id', $bankAccountId); + $withdrawalsQuery->where('withdrawals.bank_account_id', $bankAccountId); + } + + // 필터 적용: 입출금유형 (account_code) + if ($transactionType) { + if ($transactionType === 'unset') { + $depositsQuery->whereNull('deposits.account_code'); + $withdrawalsQuery->whereNull('withdrawals.account_code'); + } else { + $depositsQuery->where('deposits.account_code', $transactionType); + $withdrawalsQuery->where('withdrawals.account_code', $transactionType); + } + } + + // 필터 적용: 검색어 + if ($search) { + $depositsQuery->where(function ($q) use ($search) { + $q->where('bank_accounts.bank_name', 'like', "%{$search}%") + ->orWhere('bank_accounts.account_name', 'like', "%{$search}%") + ->orWhere('deposits.client_name', 'like', "%{$search}%") + ->orWhere('clients.name', 'like', "%{$search}%") + ->orWhere('deposits.description', 'like', "%{$search}%"); + }); + + $withdrawalsQuery->where(function ($q) use ($search) { + $q->where('bank_accounts.bank_name', 'like', "%{$search}%") + ->orWhere('bank_accounts.account_name', 'like', "%{$search}%") + ->orWhere('withdrawals.client_name', 'like', "%{$search}%") + ->orWhere('clients.name', 'like', "%{$search}%") + ->orWhere('withdrawals.description', 'like', "%{$search}%"); + }); + } + + // UNION + $unionQuery = $depositsQuery->union($withdrawalsQuery); + + // 정렬 컬럼 매핑 + $sortColumn = match ($sortBy) { + 'transaction_date' => 'transaction_date', + 'deposit_amount', 'withdrawal_amount', 'amount' => DB::raw('(deposit_amount + withdrawal_amount)'), + default => 'transaction_date', + }; + + // 전체 조회하여 정렬 및 잔액 계산 + $allItems = DB::query() + ->fromSub($unionQuery, 'transactions') + ->orderBy($sortColumn, $sortDir) + ->get(); + + // 잔액 계산 (날짜순 정렬 기준) + $balance = 0; + $itemsWithBalance = $allItems->map(function ($item) use (&$balance) { + $balance += $item->deposit_amount - $item->withdrawal_amount; + $item->balance = $balance; + + return $item; + }); + + // 수동 페이지네이션 + $total = $itemsWithBalance->count(); + $offset = ($page - 1) * $perPage; + $paginatedItems = $itemsWithBalance->slice($offset, $perPage)->values(); + + return new LengthAwarePaginator( + $paginatedItems, + $total, + $perPage, + $page, + ['path' => request()->url()] + ); + } + + /** + * 입출금 요약 통계 + */ + public function summary(array $params): array + { + $startDate = $params['start_date'] ?? null; + $endDate = $params['end_date'] ?? null; + $tenantId = $this->tenantId(); + + // 입금 합계 (계좌이체) + $depositQuery = Deposit::where('tenant_id', $tenantId) + ->where('payment_method', 'transfer'); + if ($startDate) { + $depositQuery->whereDate('deposit_date', '>=', $startDate); + } + if ($endDate) { + $depositQuery->whereDate('deposit_date', '<=', $endDate); + } + $totalDeposit = $depositQuery->sum('amount'); + $depositUnsetCount = (clone $depositQuery)->whereNull('account_code')->count(); + + // 출금 합계 (계좌이체) + $withdrawalQuery = Withdrawal::where('tenant_id', $tenantId) + ->where('payment_method', 'transfer'); + if ($startDate) { + $withdrawalQuery->whereDate('withdrawal_date', '>=', $startDate); + } + if ($endDate) { + $withdrawalQuery->whereDate('withdrawal_date', '<=', $endDate); + } + $totalWithdrawal = $withdrawalQuery->sum('amount'); + $withdrawalUnsetCount = (clone $withdrawalQuery)->whereNull('account_code')->count(); + + return [ + 'total_deposit' => (float) $totalDeposit, + 'total_withdrawal' => (float) $totalWithdrawal, + 'deposit_unset_count' => (int) $depositUnsetCount, + 'withdrawal_unset_count' => (int) $withdrawalUnsetCount, + ]; + } + + /** + * 계좌 목록 조회 (필터용) + */ + public function getAccountOptions(): array + { + $tenantId = $this->tenantId(); + + return BankAccount::where('tenant_id', $tenantId) + ->where('status', 'active') + ->orderBy('bank_name') + ->orderBy('account_name') + ->get() + ->map(fn ($acc) => [ + 'id' => $acc->id, + 'label' => "{$acc->bank_name}|{$acc->account_name}", + ]) + ->toArray(); + } +} diff --git a/app/Swagger/v1/BankTransactionApi.php b/app/Swagger/v1/BankTransactionApi.php new file mode 100644 index 0000000..24f3747 --- /dev/null +++ b/app/Swagger/v1/BankTransactionApi.php @@ -0,0 +1,248 @@ +