From 34788e854c84f4e255fd7aacd454415c7c70314c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B9=80=EB=B3=B4=EA=B3=A4?= Date: Tue, 10 Feb 2026 17:58:33 +0900 Subject: [PATCH] =?UTF-8?q?feat:=EC=9D=BC=EB=B0=98=EC=A0=84=ED=91=9C?= =?UTF-8?q?=EC=9E=85=EB=A0=A5=20=ED=8E=98=EC=9D=B4=EC=A7=80=20=EC=A0=84?= =?UTF-8?q?=EB=A9=B4=20=EA=B0=9C=ED=8E=B8=20-=20=EA=B3=84=EC=A2=8C?= =?UTF-8?q?=EC=9E=85=EC=B6=9C=EA=B8=88=20=EA=B8=B0=EB=B0=98=20=EB=B6=84?= =?UTF-8?q?=EA=B0=9C=20=EC=8B=9C=EC=8A=A4=ED=85=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 3탭 구조로 전면 재작성 (은행거래분개, 수동전표, 전표목록) - JournalEntryController에 bankTransactions, storeFromBank, bankJournals, deleteBankJournal API 추가 - JournalEntry 모델에 source_type, source_key fillable 및 헬퍼 메서드 추가 - 은행거래 목록에서 분개 모달로 복식부기 전표 생성 - 입금/출금에 따라 보통예금(103) 자동 세팅 - 분개 완료/미분개 상태 표시 및 필터링 - 기존 수동전표, 전표목록 기능 그대로 유지 Co-Authored-By: Claude Opus 4.6 --- .../Finance/JournalEntryController.php | 235 +++ app/Models/Finance/JournalEntry.php | 29 + .../views/finance/journal-entries.blade.php | 1655 ++++++++++++----- routes/web.php | 7 + 4 files changed, 1465 insertions(+), 461 deletions(-) diff --git a/app/Http/Controllers/Finance/JournalEntryController.php b/app/Http/Controllers/Finance/JournalEntryController.php index ca83dacf..3c55342e 100644 --- a/app/Http/Controllers/Finance/JournalEntryController.php +++ b/app/Http/Controllers/Finance/JournalEntryController.php @@ -3,6 +3,7 @@ namespace App\Http\Controllers\Finance; use App\Http\Controllers\Controller; +use App\Http\Controllers\Barobill\EaccountController; use App\Models\Finance\JournalEntry; use App\Models\Finance\JournalEntryLine; use App\Models\Finance\TradingPartner; @@ -10,6 +11,7 @@ use Illuminate\Http\JsonResponse; use Illuminate\Http\Request; use Illuminate\Support\Facades\DB; +use Illuminate\Support\Facades\Log; class JournalEntryController extends Controller { @@ -359,4 +361,237 @@ public function tradingPartners(Request $request): JsonResponse }), ]); } + + // ================================================================ + // 은행거래 기반 분개 API + // ================================================================ + + /** + * 은행거래 목록 조회 (EaccountController 재사용 + 분개상태 병합) + */ + public function bankTransactions(Request $request): JsonResponse + { + try { + $tenantId = session('selected_tenant_id', 1); + + // EaccountController의 transactions 메서드 호출하여 은행거래 조회 + $eaccountController = app(EaccountController::class); + $transResponse = $eaccountController->transactions($request); + $transData = json_decode($transResponse->getContent(), true); + + if (!($transData['success'] ?? false)) { + return response()->json($transData); + } + + $logs = $transData['data']['logs'] ?? []; + + // 각 거래의 uniqueKey 수집 + $uniqueKeys = array_column($logs, 'uniqueKey'); + + // 분개 완료된 source_key 조회 + $journaledKeys = JournalEntry::getJournaledSourceKeys($tenantId, 'bank_transaction', $uniqueKeys); + $journaledKeysMap = array_flip($journaledKeys); + + // 분개된 전표 ID도 조회 + $journalMap = []; + if (!empty($journaledKeys)) { + $journals = JournalEntry::where('tenant_id', $tenantId) + ->where('source_type', 'bank_transaction') + ->whereIn('source_key', $journaledKeys) + ->select('id', 'source_key', 'entry_no') + ->get(); + foreach ($journals as $j) { + $journalMap[$j->source_key] = ['id' => $j->id, 'entry_no' => $j->entry_no]; + } + } + + // 각 거래에 분개 상태 추가 + foreach ($logs as &$log) { + $key = $log['uniqueKey'] ?? ''; + $log['hasJournal'] = isset($journaledKeysMap[$key]); + $log['journalId'] = $journalMap[$key]['id'] ?? null; + $log['journalEntryNo'] = $journalMap[$key]['entry_no'] ?? null; + } + unset($log); + + // 분개 통계 + $journaledCount = count($journaledKeys); + $totalCount = count($logs); + + $transData['data']['logs'] = $logs; + $transData['data']['journalStats'] = [ + 'journaledCount' => $journaledCount, + 'unjournaledCount' => $totalCount - $journaledCount, + ]; + + return response()->json($transData); + } catch (\Throwable $e) { + Log::error('은행거래 목록 조회 오류: ' . $e->getMessage()); + return response()->json([ + 'success' => false, + 'message' => '은행거래 목록 조회 실패: ' . $e->getMessage(), + ], 500); + } + } + + /** + * 은행거래 기반 전표 생성 + */ + public function storeFromBank(Request $request): JsonResponse + { + $request->validate([ + 'source_key' => 'required|string|max:255', + 'entry_date' => 'required|date', + 'description' => 'nullable|string|max:500', + 'lines' => 'required|array|min:2', + 'lines.*.dc_type' => 'required|in:debit,credit', + 'lines.*.account_code' => 'required|string|max:10', + 'lines.*.account_name' => 'required|string|max:100', + 'lines.*.trading_partner_id' => 'nullable|integer', + 'lines.*.trading_partner_name' => 'nullable|string|max:100', + 'lines.*.debit_amount' => 'required|integer|min:0', + 'lines.*.credit_amount' => 'required|integer|min:0', + 'lines.*.description' => 'nullable|string|max:300', + ]); + + $tenantId = session('selected_tenant_id', 1); + $lines = $request->lines; + + $totalDebit = collect($lines)->sum('debit_amount'); + $totalCredit = collect($lines)->sum('credit_amount'); + + if ($totalDebit !== $totalCredit || $totalDebit === 0) { + return response()->json([ + 'success' => false, + 'message' => '차변합계와 대변합계가 일치해야 하며 0보다 커야 합니다.', + ], 422); + } + + // 중복 분개 체크 + $existing = JournalEntry::getJournalBySourceKey($tenantId, 'bank_transaction', $request->source_key); + if ($existing) { + return response()->json([ + 'success' => false, + 'message' => '이미 분개가 완료된 거래입니다. (전표번호: ' . $existing->entry_no . ')', + ], 422); + } + + try { + $entry = DB::transaction(function () use ($tenantId, $request, $lines, $totalDebit, $totalCredit) { + $entryNo = JournalEntry::generateEntryNo($tenantId, $request->entry_date); + + $entry = JournalEntry::create([ + 'tenant_id' => $tenantId, + 'entry_no' => $entryNo, + 'entry_date' => $request->entry_date, + 'description' => $request->description, + 'total_debit' => $totalDebit, + 'total_credit' => $totalCredit, + 'status' => 'draft', + 'source_type' => 'bank_transaction', + 'source_key' => $request->source_key, + 'created_by_name' => auth()->user()?->name ?? '시스템', + ]); + + foreach ($lines as $i => $line) { + JournalEntryLine::create([ + 'tenant_id' => $tenantId, + 'journal_entry_id' => $entry->id, + 'line_no' => $i + 1, + 'dc_type' => $line['dc_type'], + 'account_code' => $line['account_code'], + 'account_name' => $line['account_name'], + 'trading_partner_id' => $line['trading_partner_id'] ?? null, + 'trading_partner_name' => $line['trading_partner_name'] ?? null, + 'debit_amount' => $line['debit_amount'], + 'credit_amount' => $line['credit_amount'], + 'description' => $line['description'] ?? null, + ]); + } + + return $entry; + }); + + return response()->json([ + 'success' => true, + 'message' => '분개가 저장되었습니다.', + 'data' => ['id' => $entry->id, 'entry_no' => $entry->entry_no], + ]); + } catch (\Throwable $e) { + Log::error('은행거래 분개 저장 오류: ' . $e->getMessage()); + return response()->json([ + 'success' => false, + 'message' => '분개 저장 실패: ' . $e->getMessage(), + ], 500); + } + } + + /** + * 특정 은행거래의 기존 분개 조회 + */ + public function bankJournals(Request $request): JsonResponse + { + $tenantId = session('selected_tenant_id', 1); + $sourceKey = $request->get('source_key'); + + if (!$sourceKey) { + return response()->json(['success' => false, 'message' => 'source_key가 필요합니다.'], 422); + } + + $entry = JournalEntry::forTenant($tenantId) + ->where('source_type', 'bank_transaction') + ->where('source_key', $sourceKey) + ->with('lines') + ->first(); + + if (!$entry) { + return response()->json(['success' => true, 'data' => null]); + } + + return response()->json([ + 'success' => true, + 'data' => [ + 'id' => $entry->id, + 'entry_no' => $entry->entry_no, + 'entry_date' => $entry->entry_date->format('Y-m-d'), + 'description' => $entry->description, + 'total_debit' => $entry->total_debit, + 'total_credit' => $entry->total_credit, + 'status' => $entry->status, + 'lines' => $entry->lines->map(function ($line) { + return [ + 'id' => $line->id, + 'line_no' => $line->line_no, + 'dc_type' => $line->dc_type, + 'account_code' => $line->account_code, + 'account_name' => $line->account_name, + 'trading_partner_id' => $line->trading_partner_id, + 'trading_partner_name' => $line->trading_partner_name, + 'debit_amount' => $line->debit_amount, + 'credit_amount' => $line->credit_amount, + 'description' => $line->description, + ]; + }), + ], + ]); + } + + /** + * 은행거래 분개 삭제 (soft delete) + */ + public function deleteBankJournal(int $id): JsonResponse + { + $tenantId = session('selected_tenant_id', 1); + + $entry = JournalEntry::forTenant($tenantId) + ->where('source_type', 'bank_transaction') + ->findOrFail($id); + + $entry->delete(); + + return response()->json([ + 'success' => true, + 'message' => '분개가 삭제되었습니다.', + ]); + } } diff --git a/app/Models/Finance/JournalEntry.php b/app/Models/Finance/JournalEntry.php index 3821101e..c6367de9 100644 --- a/app/Models/Finance/JournalEntry.php +++ b/app/Models/Finance/JournalEntry.php @@ -21,6 +21,8 @@ class JournalEntry extends Model 'total_debit', 'total_credit', 'status', + 'source_type', + 'source_key', 'created_by_name', 'attachment_note', ]; @@ -41,6 +43,33 @@ public function scopeForTenant($query, $tenantId) return $query->where('tenant_id', $tenantId); } + /** + * 분개 완료된 source_key 일괄 조회 + */ + public static function getJournaledSourceKeys(int $tenantId, string $sourceType, array $sourceKeys): array + { + if (empty($sourceKeys)) { + return []; + } + + return static::where('tenant_id', $tenantId) + ->where('source_type', $sourceType) + ->whereIn('source_key', $sourceKeys) + ->pluck('source_key') + ->toArray(); + } + + /** + * source_key로 분개 전표 조회 (ID 포함) + */ + public static function getJournalBySourceKey(int $tenantId, string $sourceType, string $sourceKey) + { + return static::where('tenant_id', $tenantId) + ->where('source_type', $sourceType) + ->where('source_key', $sourceKey) + ->first(); + } + /** * 전표번호 자동채번: JE-YYYYMMDD-NNN */ diff --git a/resources/views/finance/journal-entries.blade.php b/resources/views/finance/journal-entries.blade.php index 9c056e8b..7affe0eb 100644 --- a/resources/views/finance/journal-entries.blade.php +++ b/resources/views/finance/journal-entries.blade.php @@ -22,6 +22,9 @@