diff --git a/app/Http/Controllers/Barobill/EaccountController.php b/app/Http/Controllers/Barobill/EaccountController.php index af358df9..45ca95b9 100644 --- a/app/Http/Controllers/Barobill/EaccountController.php +++ b/app/Http/Controllers/Barobill/EaccountController.php @@ -4,21 +4,21 @@ use App\Http\Controllers\Controller; use App\Models\Barobill\AccountCode; -use App\Models\Barobill\BarobillConfig; -use App\Models\Barobill\BarobillMember; use App\Models\Barobill\BankSyncStatus; use App\Models\Barobill\BankTransaction; use App\Models\Barobill\BankTransactionOverride; use App\Models\Barobill\BankTransactionSplit; +use App\Models\Barobill\BarobillConfig; +use App\Models\Barobill\BarobillMember; use App\Models\Finance\TradingPartner; use App\Models\Tenants\Tenant; +use Carbon\Carbon; use Illuminate\Http\JsonResponse; use Illuminate\Http\Request; use Illuminate\Http\Response; use Illuminate\Support\Facades\DB; use Illuminate\Support\Facades\Log; use Illuminate\View\View; -use Carbon\Carbon; use Symfony\Component\HttpFoundation\StreamedResponse; /** @@ -30,9 +30,13 @@ class EaccountController extends Controller * 바로빌 SOAP 설정 */ private ?string $certKey = null; + private ?string $corpNum = null; + private bool $isTestMode = false; + private ?string $soapUrl = null; + private ?\SoapClient $soapClient = null; // 바로빌 파트너사 (본사) 테넌트 ID @@ -51,7 +55,7 @@ public function __construct() $baseUrl = $this->isTestMode ? 'https://testws.baroservice.com' : 'https://ws.baroservice.com'; - $this->soapUrl = $baseUrl . '/BANKACCOUNT.asmx?WSDL'; + $this->soapUrl = $baseUrl.'/BANKACCOUNT.asmx?WSDL'; } else { $this->isTestMode = config('services.barobill.test_mode', true); // 테스트 모드에 따라 적절한 CERT_KEY 선택 @@ -72,14 +76,14 @@ public function __construct() */ private function initSoapClient(): void { - if (!empty($this->certKey) || $this->isTestMode) { + if (! empty($this->certKey) || $this->isTestMode) { try { $context = stream_context_create([ 'ssl' => [ 'verify_peer' => false, 'verify_peer_name' => false, - 'allow_self_signed' => true - ] + 'allow_self_signed' => true, + ], ]); $this->soapClient = new \SoapClient($this->soapUrl, [ @@ -88,10 +92,10 @@ private function initSoapClient(): void 'exceptions' => true, 'connection_timeout' => 30, 'stream_context' => $context, - 'cache_wsdl' => WSDL_CACHE_NONE + 'cache_wsdl' => WSDL_CACHE_NONE, ]); } catch (\Throwable $e) { - Log::error('바로빌 계좌 SOAP 클라이언트 생성 실패: ' . $e->getMessage()); + Log::error('바로빌 계좌 SOAP 클라이언트 생성 실패: '.$e->getMessage()); } } } @@ -148,14 +152,14 @@ private function applyMemberServerMode(BarobillMember $member): void $baseUrl = $config->base_url ?: ($memberTestMode ? 'https://testws.baroservice.com' : 'https://ws.baroservice.com'); - $this->soapUrl = $baseUrl . '/BANKACCOUNT.asmx?WSDL'; + $this->soapUrl = $baseUrl.'/BANKACCOUNT.asmx?WSDL'; // SOAP 클라이언트 재초기화 $this->initSoapClient(); Log::info('[Eaccount] 서버 모드 적용', [ 'targetEnv' => $targetEnv, - 'certKey' => substr($this->certKey ?? '', 0, 10) . '...', + 'certKey' => substr($this->certKey ?? '', 0, 10).'...', 'corpNum' => $this->corpNum, 'soapUrl' => $this->soapUrl, ]); @@ -185,14 +189,14 @@ public function accounts(Request $request): JsonResponse $userId = $barobillMember?->barobill_id ?? ''; $result = $this->callSoap('GetBankAccountEx', [ - 'AvailOnly' => (int)$availOnly + 'AvailOnly' => (int) $availOnly, ]); - if (!$result['success']) { + if (! $result['success']) { return response()->json([ 'success' => false, 'error' => $result['error'], - 'error_code' => $result['error_code'] ?? null + 'error_code' => $result['error_code'] ?? null, ]); } @@ -208,7 +212,9 @@ public function accounts(Request $request): JsonResponse } foreach ($accountList as $acc) { - if (!is_object($acc)) continue; + if (! is_object($acc)) { + continue; + } $bankAccountNum = $acc->BankAccountNum ?? ''; if (empty($bankAccountNum) || (is_numeric($bankAccountNum) && $bankAccountNum < 0)) { @@ -227,20 +233,21 @@ public function accounts(Request $request): JsonResponse 'currency' => $acc->Currency ?? 'KRW', 'issueDate' => $acc->IssueDate ?? '', 'balance' => $acc->Balance ?? 0, - 'status' => isset($acc->UseState) ? (int)$acc->UseState : 1 + 'status' => isset($acc->UseState) ? (int) $acc->UseState : 1, ]; } return response()->json([ 'success' => true, 'accounts' => $accounts, - 'count' => count($accounts) + 'count' => count($accounts), ]); } catch (\Throwable $e) { - Log::error('계좌 목록 조회 오류: ' . $e->getMessage()); + Log::error('계좌 목록 조회 오류: '.$e->getMessage()); + return response()->json([ 'success' => false, - 'error' => '서버 오류: ' . $e->getMessage() + 'error' => '서버 오류: '.$e->getMessage(), ]); } } @@ -267,7 +274,7 @@ public function latestBalances(Request $request): JsonResponse foreach ($allTransactions as $tx) { $accNum = $tx->bank_account_num; - if (!isset($accountBalances[$accNum])) { + if (! isset($accountBalances[$accNum])) { $accountBalances[$accNum] = [ 'bankAccountNum' => $accNum, 'bankName' => $tx->bank_name, @@ -278,7 +285,7 @@ public function latestBalances(Request $request): JsonResponse } $prev = $accountBalances[$accNum]['balance']; - if (!$tx->is_manual && (float) $tx->balance != 0) { + if (! $tx->is_manual && (float) $tx->balance != 0) { $accountBalances[$accNum]['balance'] = (float) $tx->balance; } else { $accountBalances[$accNum]['balance'] = $prev + (float) $tx->deposit - (float) $tx->withdraw; @@ -292,13 +299,14 @@ public function latestBalances(Request $request): JsonResponse return response()->json([ 'success' => true, 'balances' => $result, - 'count' => count($result) + 'count' => count($result), ]); } catch (\Throwable $e) { - Log::error('최신 잔액 조회 오류: ' . $e->getMessage()); + Log::error('최신 잔액 조회 오류: '.$e->getMessage()); + return response()->json([ 'success' => false, - 'error' => $e->getMessage() + 'error' => $e->getMessage(), ]); } } @@ -312,8 +320,8 @@ public function transactions(Request $request): JsonResponse $startDate = $request->input('startDate', date('Ymd')); $endDate = $request->input('endDate', date('Ymd')); $bankAccountNum = str_replace('-', '', $request->input('accountNum', '')); - $page = (int)$request->input('page', 1); - $limit = (int)$request->input('limit', 50); + $page = (int) $request->input('page', 1); + $limit = (int) $request->input('limit', 50); // 현재 테넌트의 바로빌 회원 정보 조회 $tenantId = session('selected_tenant_id', self::HEADQUARTERS_TENANT_ID); @@ -335,7 +343,7 @@ public function transactions(Request $request): JsonResponse $manualQuery = BankTransaction::where('tenant_id', $tenantId) ->where('is_manual', true) ->whereBetween('trans_date', [$startDate, $endDate]); - if (!empty($bankAccountNum)) { + if (! empty($bankAccountNum)) { $manualQuery->where('bank_account_num', $bankAccountNum); } $manualTransactions = $manualQuery->orderBy('trans_date', 'desc') @@ -356,19 +364,20 @@ public function transactions(Request $request): JsonResponse $manualLogs = $this->convertManualToLogs($manualTransactions); $baseBalance = $this->findBaseBalance($tenantId, $startDate, $bankAccountNum); $recalcLogs = $this->recalcManualBalances($manualLogs['logs'], $baseBalance); + return response()->json([ 'success' => true, 'data' => [ 'logs' => $recalcLogs, 'summary' => $manualLogs['summary'], - 'pagination' => ['currentPage' => 1, 'maxPageNum' => 1] - ] + 'pagination' => ['currentPage' => 1, 'maxPageNum' => 1], + ], ]); } // 월별 청크 결과를 합쳐서 파싱 - $fakeData = new \stdClass(); - $fakeData->BankAccountLogList = new \stdClass(); + $fakeData = new \stdClass; + $fakeData->BankAccountLogList = new \stdClass; $fakeData->BankAccountLogList->BankAccountTransLog = $fetched['logs']; $logs = $this->parseTransactionLogs($fakeData, '', $savedData, $tenantId); @@ -380,8 +389,9 @@ public function transactions(Request $request): JsonResponse // 날짜/시간 기준 정렬 (최신순) usort($mergedLogs, function ($a, $b) { - $dtA = ($a['transDate'] ?? '') . ($a['transTime'] ?? ''); - $dtB = ($b['transDate'] ?? '') . ($b['transTime'] ?? ''); + $dtA = ($a['transDate'] ?? '').($a['transTime'] ?? ''); + $dtB = ($b['transDate'] ?? '').($b['transTime'] ?? ''); + return strcmp($dtB, $dtA); }); @@ -398,7 +408,7 @@ public function transactions(Request $request): JsonResponse // 클라이언트 사이드 페이지네이션 $totalCount = count($mergedLogs); - $maxPageNum = (int)ceil($totalCount / $limit); + $maxPageNum = (int) ceil($totalCount / $limit); $startIndex = ($page - 1) * $limit; $paginatedLogs = array_slice($mergedLogs, $startIndex, $limit); @@ -410,16 +420,17 @@ public function transactions(Request $request): JsonResponse 'currentPage' => $page, 'countPerPage' => $limit, 'maxPageNum' => $maxPageNum, - 'maxIndex' => $totalCount + 'maxIndex' => $totalCount, ], - 'summary' => $mergedSummary - ] + 'summary' => $mergedSummary, + ], ]); } catch (\Throwable $e) { - Log::error('입출금내역 조회 오류: ' . $e->getMessage()); + Log::error('입출금내역 조회 오류: '.$e->getMessage()); + return response()->json([ 'success' => false, - 'error' => '서버 오류: ' . $e->getMessage() + 'error' => '서버 오류: '.$e->getMessage(), ]); } } @@ -432,10 +443,10 @@ private function getAllAccountsTransactions(string $userId, string $startDate, s // 먼저 계좌 목록 조회 $accountResult = $this->callSoap('GetBankAccountEx', ['AvailOnly' => 0]); - if (!$accountResult['success']) { + if (! $accountResult['success']) { return response()->json([ 'success' => false, - 'error' => $accountResult['error'] + 'error' => $accountResult['error'], ]); } @@ -452,18 +463,22 @@ private function getAllAccountsTransactions(string $userId, string $startDate, s $totalWithdraw = 0; foreach ($accountList as $acc) { - if (!is_object($acc)) continue; + if (! is_object($acc)) { + continue; + } $accNum = $acc->BankAccountNum ?? ''; - if (empty($accNum) || (is_numeric($accNum) && $accNum < 0)) continue; + if (empty($accNum) || (is_numeric($accNum) && $accNum < 0)) { + continue; + } // 기간을 월별로 분할하여 SOAP API 호출 (긴 기간 에러 방지) // 캐싱: 계좌별 bankName/bankCode 전달 $fetched = $this->fetchAccountTransactions($userId, $accNum, $startDate, $endDate, $tenantId, $acc->BankName ?? '', $acc->BankCode ?? ''); - if (!empty($fetched['logs'])) { - $fakeData = new \stdClass(); - $fakeData->BankAccountLogList = new \stdClass(); + if (! empty($fetched['logs'])) { + $fakeData = new \stdClass; + $fakeData->BankAccountLogList = new \stdClass; $fakeData->BankAccountLogList->BankAccountTransLog = $fetched['logs']; $parsed = $this->parseTransactionLogs($fakeData, $acc->BankName ?? '', $savedData, $tenantId); @@ -487,8 +502,9 @@ private function getAllAccountsTransactions(string $userId, string $startDate, s // 날짜/시간 기준 정렬 (최신순) usort($allLogs, function ($a, $b) { - $dateA = ($a['transDate'] ?? '') . ($a['transTime'] ?? ''); - $dateB = ($b['transDate'] ?? '') . ($b['transTime'] ?? ''); + $dateA = ($a['transDate'] ?? '').($a['transTime'] ?? ''); + $dateB = ($b['transDate'] ?? '').($b['transTime'] ?? ''); + return strcmp($dateB, $dateA); }); @@ -498,7 +514,7 @@ private function getAllAccountsTransactions(string $userId, string $startDate, s // 페이지네이션 $totalCount = count($allLogs); - $maxPageNum = (int)ceil($totalCount / $limit); + $maxPageNum = (int) ceil($totalCount / $limit); $startIndex = ($page - 1) * $limit; $paginatedLogs = array_slice($allLogs, $startIndex, $limit); @@ -510,14 +526,14 @@ private function getAllAccountsTransactions(string $userId, string $startDate, s 'currentPage' => $page, 'countPerPage' => $limit, 'maxPageNum' => $maxPageNum, - 'maxIndex' => $totalCount + 'maxIndex' => $totalCount, ], 'summary' => [ 'totalDeposit' => $totalDeposit, 'totalWithdraw' => $totalWithdraw, - 'count' => $totalCount - ] - ] + 'count' => $totalCount, + ], + ], ]); } @@ -545,8 +561,9 @@ private function parseTransactionLogs($resultData, string $defaultBankName = '', $deposit = (int) floatval($log->Deposit ?? 0); $withdraw = (int) floatval($log->Withdraw ?? 0); $balance = (int) floatval($log->Balance ?? 0); + $summary = $log->TransRemark1 ?? $log->Summary ?? ''; - $uniqueKey = implode('|', [$bankAccountNum, $transDT, $deposit, $withdraw, $balance]); + $uniqueKey = implode('|', [$bankAccountNum, $transDT, $deposit, $withdraw, $balance, $summary]); $uniqueKeys[] = $uniqueKey; } @@ -567,11 +584,11 @@ private function parseTransactionLogs($resultData, string $defaultBankName = '', $transTime = ''; $dateTime = ''; - if (!empty($transDT) && strlen($transDT) >= 14) { + if (! empty($transDT) && strlen($transDT) >= 14) { $transDate = substr($transDT, 0, 8); $transTime = substr($transDT, 8, 6); - $dateTime = substr($transDT, 0, 4) . '-' . substr($transDT, 4, 2) . '-' . substr($transDT, 6, 2) . ' ' . - substr($transDT, 8, 2) . ':' . substr($transDT, 10, 2) . ':' . substr($transDT, 12, 2); + $dateTime = substr($transDT, 0, 4).'-'.substr($transDT, 4, 2).'-'.substr($transDT, 6, 2).' '. + substr($transDT, 8, 2).':'.substr($transDT, 10, 2).':'.substr($transDT, 12, 2); } // 적요 파싱 (TransRemark1만 적요로, TransRemark2는 상대계좌예금주명으로 분리) @@ -582,7 +599,7 @@ private function parseTransactionLogs($resultData, string $defaultBankName = '', $bankAccountNum = $log->BankAccountNum ?? ''; // 고유 키 생성하여 저장된 데이터와 매칭 (숫자는 정수로 변환하여 형식 통일) - $uniqueKey = implode('|', [$bankAccountNum, $transDT, (int) $deposit, (int) $withdraw, (int) $balance]); + $uniqueKey = implode('|', [$bankAccountNum, $transDT, (int) $deposit, (int) $withdraw, (int) $balance, $summary]); $savedItem = $savedData?->get($uniqueKey); $override = $overrides->get($uniqueKey); @@ -631,8 +648,8 @@ private function parseTransactionLogs($resultData, string $defaultBankName = '', 'summary' => [ 'totalDeposit' => $totalDeposit, 'totalWithdraw' => $totalWithdraw, - 'count' => count($logs) - ] + 'count' => count($logs), + ], ]; } @@ -642,11 +659,12 @@ private function parseTransactionLogs($resultData, string $defaultBankName = '', private function checkErrorCode($data): ?int { if (isset($data->CurrentPage) && is_numeric($data->CurrentPage) && $data->CurrentPage < 0) { - return (int)$data->CurrentPage; + return (int) $data->CurrentPage; } if (isset($data->BankAccountNum) && is_numeric($data->BankAccountNum) && $data->BankAccountNum < 0) { - return (int)$data->BankAccountNum; + return (int) $data->BankAccountNum; } + return null; } @@ -664,7 +682,8 @@ private function getErrorMessage(int $errorCode): string -25006 => '계좌번호가 잘못되었습니다 (-25006).', -25007 => '조회 기간이 잘못되었습니다 (-25007).', ]; - return $messages[$errorCode] ?? '바로빌 API 오류: ' . $errorCode; + + return $messages[$errorCode] ?? '바로빌 API 오류: '.$errorCode; } /** @@ -729,7 +748,7 @@ private function fetchAccountTransactions(string $userId, string $accNum, string ->first(); if ($syncStatus) { - if (!$isCurrentMonth) { + if (! $isCurrentMonth) { $useCache = true; // 과거 월: 항상 캐시 } elseif ($syncStatus->synced_at->diffInMinutes(now()) < 10) { $useCache = true; // 현재 월: 10분 이내면 캐시 @@ -744,6 +763,7 @@ private function fetchAccountTransactions(string $userId, string $accNum, string $allRawLogs[] = $this->convertDbToRawLog($record); } Log::debug("바로빌 캐시 사용 ({$chunk['start']}~{$chunk['end']}): {$cachedRecords->count()}건"); + continue; } @@ -756,10 +776,10 @@ private function fetchAccountTransactions(string $userId, string $accNum, string 'TransDirection' => 1, 'CountPerPage' => 1000, 'CurrentPage' => 1, - 'OrderDirection' => 2 + 'OrderDirection' => 2, ]); - if (!$result['success']) { + if (! $result['success']) { continue; } @@ -767,8 +787,9 @@ private function fetchAccountTransactions(string $userId, string $accNum, string $errorCode = $this->checkErrorCode($chunkData); // 데이터 없음(-25005, -25001)은 건너뜀, 기타 에러도 건너뜀 (다른 월은 성공할 수 있음) - if ($errorCode && !in_array($errorCode, [-25005, -25001])) { + if ($errorCode && ! in_array($errorCode, [-25005, -25001])) { Log::debug("바로빌 API 기간 분할 - 에러 발생 ({$chunk['start']}~{$chunk['end']}): {$errorCode}"); + continue; } @@ -777,6 +798,7 @@ private function fetchAccountTransactions(string $userId, string $accNum, string if ($tenantId > 0) { $this->updateSyncStatus($tenantId, $accNum, $yearMonth); } + continue; } @@ -792,10 +814,10 @@ private function fetchAccountTransactions(string $userId, string $accNum, string } // 캐시 저장 (tenantId가 전달된 경우에만) - if ($tenantId > 0 && !empty($rawLogs)) { + if ($tenantId > 0 && ! empty($rawLogs)) { $this->cacheApiTransactions($tenantId, $accNum, $bankName, $bankCode, $rawLogs); $this->updateSyncStatus($tenantId, $accNum, $yearMonth); - Log::debug("바로빌 API 캐시 저장 ({$chunk['start']}~{$chunk['end']}): " . count($rawLogs) . "건"); + Log::debug("바로빌 API 캐시 저장 ({$chunk['start']}~{$chunk['end']}): ".count($rawLogs).'건'); } elseif ($tenantId > 0) { $this->updateSyncStatus($tenantId, $accNum, $yearMonth); } @@ -861,7 +883,7 @@ private function cacheApiTransactions(int $tenantId, string $accNum, string $ban */ private function convertDbToRawLog(BankTransaction $record): \stdClass { - $log = new \stdClass(); + $log = new \stdClass; $log->BankAccountNum = $record->bank_account_num; $log->TransDT = $record->trans_dt; $log->Deposit = $record->deposit; @@ -926,8 +948,9 @@ private function getBankName(string $code): string '088' => '신한은행', '089' => 'K뱅크', '090' => '카카오뱅크', - '092' => '토스뱅크' + '092' => '토스뱅크', ]; + return $banks[$code] ?? $code; } @@ -943,7 +966,7 @@ public function searchClients(Request $request): JsonResponse if (empty($keyword)) { return response()->json([ 'success' => true, - 'data' => [] + 'data' => [], ]); } @@ -953,7 +976,7 @@ public function searchClients(Request $request): JsonResponse if (is_numeric($keyword)) { $query->where(function ($q) use ($keyword) { $q->where('id', $keyword) - ->orWhere('name', 'like', "%{$keyword}%"); + ->orWhere('name', 'like', "%{$keyword}%"); }); } else { $query->where('name', 'like', "%{$keyword}%"); @@ -966,16 +989,17 @@ public function searchClients(Request $request): JsonResponse return response()->json([ 'success' => true, - 'data' => $clients->map(fn($c) => [ + 'data' => $clients->map(fn ($c) => [ 'code' => (string) $c->id, 'name' => $c->name, - ]) + ]), ]); } catch (\Throwable $e) { - Log::error('거래처 검색 오류: ' . $e->getMessage()); + Log::error('거래처 검색 오류: '.$e->getMessage()); + return response()->json([ 'success' => false, - 'error' => '검색 오류: ' . $e->getMessage() + 'error' => '검색 오류: '.$e->getMessage(), ]); } } @@ -989,12 +1013,12 @@ public function accountCodes(): JsonResponse return response()->json([ 'success' => true, - 'data' => $codes->map(fn($c) => [ + 'data' => $codes->map(fn ($c) => [ 'id' => $c->id, 'code' => $c->code, 'name' => $c->name, 'category' => $c->category, - ]) + ]), ]); } @@ -1007,7 +1031,7 @@ public function accountCodesAll(): JsonResponse return response()->json([ 'success' => true, - 'data' => $codes + 'data' => $codes, ]); } @@ -1029,7 +1053,7 @@ public function accountCodeStore(Request $request): JsonResponse if ($exists) { return response()->json([ 'success' => false, - 'error' => '이미 존재하는 계정과목 코드입니다.' + 'error' => '이미 존재하는 계정과목 코드입니다.', ], 422); } @@ -1047,12 +1071,12 @@ public function accountCodeStore(Request $request): JsonResponse return response()->json([ 'success' => true, 'message' => '계정과목이 추가되었습니다.', - 'data' => $accountCode + 'data' => $accountCode, ]); } catch (\Throwable $e) { return response()->json([ 'success' => false, - 'error' => '추가 실패: ' . $e->getMessage() + 'error' => '추가 실패: '.$e->getMessage(), ], 500); } } @@ -1065,10 +1089,10 @@ public function accountCodeUpdate(Request $request, int $id): JsonResponse try { $accountCode = AccountCode::find($id); - if (!$accountCode) { + if (! $accountCode) { return response()->json([ 'success' => false, - 'error' => '계정과목을 찾을 수 없습니다.' + 'error' => '계정과목을 찾을 수 없습니다.', ], 404); } @@ -1088,7 +1112,7 @@ public function accountCodeUpdate(Request $request, int $id): JsonResponse if ($exists) { return response()->json([ 'success' => false, - 'error' => '이미 존재하는 계정과목 코드입니다.' + 'error' => '이미 존재하는 계정과목 코드입니다.', ], 422); } } @@ -1098,12 +1122,12 @@ public function accountCodeUpdate(Request $request, int $id): JsonResponse return response()->json([ 'success' => true, 'message' => '계정과목이 수정되었습니다.', - 'data' => $accountCode + 'data' => $accountCode, ]); } catch (\Throwable $e) { return response()->json([ 'success' => false, - 'error' => '수정 실패: ' . $e->getMessage() + 'error' => '수정 실패: '.$e->getMessage(), ], 500); } } @@ -1116,10 +1140,10 @@ public function accountCodeDestroy(int $id): JsonResponse try { $accountCode = AccountCode::find($id); - if (!$accountCode) { + if (! $accountCode) { return response()->json([ 'success' => false, - 'error' => '계정과목을 찾을 수 없습니다.' + 'error' => '계정과목을 찾을 수 없습니다.', ], 404); } @@ -1127,12 +1151,12 @@ public function accountCodeDestroy(int $id): JsonResponse return response()->json([ 'success' => true, - 'message' => '계정과목이 삭제되었습니다.' + 'message' => '계정과목이 삭제되었습니다.', ]); } catch (\Throwable $e) { return response()->json([ 'success' => false, - 'error' => '삭제 실패: ' . $e->getMessage() + 'error' => '삭제 실패: '.$e->getMessage(), ], 500); } } @@ -1154,7 +1178,7 @@ public function save(Request $request): JsonResponse if (empty($transactions)) { return response()->json([ 'success' => false, - 'error' => '저장할 데이터가 없습니다.' + 'error' => '저장할 데이터가 없습니다.', ]); } @@ -1162,7 +1186,7 @@ public function save(Request $request): JsonResponse $manualCount = 0; $apiCount = 0; foreach ($transactions as $t) { - if (!empty($t['isManual'])) { + if (! empty($t['isManual'])) { $manualCount++; Log::info('[Eaccount Save] 수동 거래 감지', [ 'dbId' => $t['dbId'] ?? 'MISSING', @@ -1184,7 +1208,7 @@ public function save(Request $request): JsonResponse foreach ($transactions as $trans) { // 수동 입력 거래: dbId로 직접 찾아서 비-키 필드만 업데이트 // balance는 화면에서 재계산된 값이므로 composite key 매칭 불가 - if (!empty($trans['isManual']) && !empty($trans['dbId'])) { + if (! empty($trans['isManual']) && ! empty($trans['dbId'])) { $affectedRows = DB::table('barobill_bank_transactions') ->where('id', $trans['dbId']) ->where('tenant_id', $tenantId) @@ -1205,11 +1229,12 @@ public function save(Request $request): JsonResponse 'affected_rows' => $affectedRows, ]); $updated++; + continue; } // 거래일시 생성 - $transDt = ($trans['transDate'] ?? '') . ($trans['transTime'] ?? ''); + $transDt = ($trans['transDate'] ?? '').($trans['transTime'] ?? ''); $data = [ 'tenant_id' => $tenantId, @@ -1239,11 +1264,12 @@ public function save(Request $request): JsonResponse (int) $data['deposit'], (int) $data['withdraw'], (int) $data['balance'], + $data['summary'], ]); $savedUniqueKeys[] = $uniqueKey; // 순수 Query Builder로 Upsert (Eloquent 모델 우회) - // balance 포함하여 매칭 → DB unique 제약조건과 동일한 5개 컬럼으로 정확히 식별 + // balance + summary 포함하여 매칭 → 같은 금액·시간이라도 적요가 다르면 별도 거래로 식별 $existingIds = DB::table('barobill_bank_transactions') ->where('tenant_id', $tenantId) ->where('bank_account_num', $data['bank_account_num']) @@ -1251,6 +1277,7 @@ public function save(Request $request): JsonResponse ->where('deposit', $data['deposit']) ->where('withdraw', $data['withdraw']) ->where('balance', $data['balance']) + ->where('summary', $data['summary']) ->orderByDesc('id') ->pluck('id'); @@ -1288,14 +1315,14 @@ public function save(Request $request): JsonResponse // 오버라이드 동기화: 메인 테이블에 저장된 값이 최신이므로 // override의 modified_cast를 제거하여 충돌 방지 - if (!empty($savedUniqueKeys)) { + if (! empty($savedUniqueKeys)) { $overrides = BankTransactionOverride::forTenant($tenantId) ->whereIn('unique_key', $savedUniqueKeys) ->get(); foreach ($overrides as $override) { if ($override->modified_cast !== null) { - if (!empty($override->modified_summary)) { + if (! empty($override->modified_summary)) { // summary 오버라이드는 유지, cast 오버라이드만 제거 $override->update(['modified_cast' => null]); } else { @@ -1312,14 +1339,15 @@ public function save(Request $request): JsonResponse 'success' => true, 'message' => "저장 완료: 신규 {$saved}건, 수정 {$updated}건", 'saved' => $saved, - 'updated' => $updated + 'updated' => $updated, ]); } catch (\Throwable $e) { DB::rollBack(); - Log::error('입출금 내역 저장 오류: ' . $e->getMessage()); + Log::error('입출금 내역 저장 오류: '.$e->getMessage()); + return response()->json([ 'success' => false, - 'error' => '저장 오류: ' . $e->getMessage() + 'error' => '저장 오류: '.$e->getMessage(), ]); } } @@ -1341,7 +1369,7 @@ public function exportExcel(Request $request): StreamedResponse|JsonResponse ->orderBy('trans_date', 'desc') ->orderBy('trans_time', 'desc'); - if (!empty($accountNum)) { + if (! empty($accountNum)) { $query->where('bank_account_num', $accountNum); } @@ -1351,7 +1379,7 @@ public function exportExcel(Request $request): StreamedResponse|JsonResponse if ($transactions->isEmpty()) { return response()->json([ 'success' => false, - 'error' => '저장된 데이터가 없습니다. 먼저 데이터를 조회하고 저장해주세요.' + 'error' => '저장된 데이터가 없습니다. 먼저 데이터를 조회하고 저장해주세요.', ]); } @@ -1361,7 +1389,7 @@ public function exportExcel(Request $request): StreamedResponse|JsonResponse $handle = fopen('php://output', 'w'); // UTF-8 BOM for Excel - fprintf($handle, chr(0xEF) . chr(0xBB) . chr(0xBF)); + fprintf($handle, chr(0xEF).chr(0xBB).chr(0xBF)); // 헤더 fputcsv($handle, [ @@ -1375,19 +1403,19 @@ public function exportExcel(Request $request): StreamedResponse|JsonResponse '취급점', '상대계좌예금주명', '계정과목코드', - '계정과목명' + '계정과목명', ]); // 데이터 foreach ($transactions as $trans) { $dateTime = ''; if ($trans->trans_date) { - $dateTime = substr($trans->trans_date, 0, 4) . '-' . - substr($trans->trans_date, 4, 2) . '-' . + $dateTime = substr($trans->trans_date, 0, 4).'-'. + substr($trans->trans_date, 4, 2).'-'. substr($trans->trans_date, 6, 2); if ($trans->trans_time) { - $dateTime .= ' ' . substr($trans->trans_time, 0, 2) . ':' . - substr($trans->trans_time, 2, 2) . ':' . + $dateTime .= ' '.substr($trans->trans_time, 0, 2).':'. + substr($trans->trans_time, 2, 2).':'. substr($trans->trans_time, 4, 2); } } @@ -1403,20 +1431,21 @@ public function exportExcel(Request $request): StreamedResponse|JsonResponse $trans->trans_office, $trans->cast, $trans->account_code, - $trans->account_name + $trans->account_name, ]); } fclose($handle); }, $filename, [ 'Content-Type' => 'text/csv; charset=utf-8', - 'Content-Disposition' => 'attachment; filename="' . $filename . '"', + 'Content-Disposition' => 'attachment; filename="'.$filename.'"', ]); } catch (\Throwable $e) { - Log::error('엑셀 다운로드 오류: ' . $e->getMessage()); + Log::error('엑셀 다운로드 오류: '.$e->getMessage()); + return response()->json([ 'success' => false, - 'error' => '다운로드 오류: ' . $e->getMessage() + 'error' => '다운로드 오류: '.$e->getMessage(), ]); } } @@ -1446,7 +1475,7 @@ public function saveOverride(Request $request): JsonResponse return response()->json([ 'success' => true, 'message' => '오버라이드가 삭제되었습니다.', - 'deleted' => true + 'deleted' => true, ]); } @@ -1457,13 +1486,14 @@ public function saveOverride(Request $request): JsonResponse 'id' => $result->id, 'modifiedSummary' => $result->modified_summary, 'modifiedCast' => $result->modified_cast, - ] + ], ]); } catch (\Throwable $e) { - Log::error('오버라이드 저장 오류: ' . $e->getMessage()); + Log::error('오버라이드 저장 오류: '.$e->getMessage()); + return response()->json([ 'success' => false, - 'error' => '저장 오류: ' . $e->getMessage() + 'error' => '저장 오류: '.$e->getMessage(), ], 500); } } @@ -1494,7 +1524,7 @@ public function storeManual(Request $request): JsonResponse ]); $transTime = $validated['trans_time'] ?? '000000'; - $transDt = $validated['trans_date'] . $transTime; + $transDt = $validated['trans_date'].$transTime; $transaction = BankTransaction::create([ 'tenant_id' => $tenantId, @@ -1519,19 +1549,20 @@ public function storeManual(Request $request): JsonResponse return response()->json([ 'success' => true, 'message' => '수동 거래가 등록되었습니다.', - 'data' => ['id' => $transaction->id] + 'data' => ['id' => $transaction->id], ]); } catch (\Illuminate\Validation\ValidationException $e) { return response()->json([ 'success' => false, 'error' => '입력 데이터가 올바르지 않습니다.', - 'errors' => $e->errors() + 'errors' => $e->errors(), ], 422); } catch (\Throwable $e) { - Log::error('수동 거래 등록 오류: ' . $e->getMessage()); + Log::error('수동 거래 등록 오류: '.$e->getMessage()); + return response()->json([ 'success' => false, - 'error' => '등록 오류: ' . $e->getMessage() + 'error' => '등록 오류: '.$e->getMessage(), ]); } } @@ -1567,7 +1598,7 @@ public function updateManual(Request $request, int $id): JsonResponse ]); $transTime = $validated['trans_time'] ?? '000000'; - $transDt = $validated['trans_date'] . $transTime; + $transDt = $validated['trans_date'].$transTime; // 수동 거래 수정: unique key 컬럼(deposit/withdraw/balance)은 제외 // balance는 화면에서 재계산(recalcManualBalances)되므로 DB값 유지 필수 @@ -1591,19 +1622,20 @@ public function updateManual(Request $request, int $id): JsonResponse } catch (\Illuminate\Database\Eloquent\ModelNotFoundException $e) { return response()->json([ 'success' => false, - 'error' => '해당 거래를 찾을 수 없거나 수동 입력 건이 아닙니다.' + 'error' => '해당 거래를 찾을 수 없거나 수동 입력 건이 아닙니다.', ], 404); } catch (\Illuminate\Validation\ValidationException $e) { return response()->json([ 'success' => false, 'error' => '입력 데이터가 올바르지 않습니다.', - 'errors' => $e->errors() + 'errors' => $e->errors(), ], 422); } catch (\Throwable $e) { - Log::error('수동 거래 수정 오류: ' . $e->getMessage()); + Log::error('수동 거래 수정 오류: '.$e->getMessage()); + return response()->json([ 'success' => false, - 'error' => '수정 오류: ' . $e->getMessage() + 'error' => '수정 오류: '.$e->getMessage(), ]); } } @@ -1630,13 +1662,14 @@ public function destroyManual(int $id): JsonResponse } catch (\Illuminate\Database\Eloquent\ModelNotFoundException $e) { return response()->json([ 'success' => false, - 'error' => '해당 거래를 찾을 수 없거나 수동 입력 건이 아닙니다.' + 'error' => '해당 거래를 찾을 수 없거나 수동 입력 건이 아닙니다.', ], 404); } catch (\Throwable $e) { - Log::error('수동 거래 삭제 오류: ' . $e->getMessage()); + Log::error('수동 거래 삭제 오류: '.$e->getMessage()); + return response()->json([ 'success' => false, - 'error' => '삭제 오류: ' . $e->getMessage() + 'error' => '삭제 오류: '.$e->getMessage(), ]); } } @@ -1655,13 +1688,14 @@ public function splits(Request $request): JsonResponse return response()->json([ 'success' => true, - 'data' => $splits + 'data' => $splits, ]); } catch (\Throwable $e) { - Log::error('계좌 분개 내역 조회 오류: ' . $e->getMessage()); + Log::error('계좌 분개 내역 조회 오류: '.$e->getMessage()); + return response()->json([ 'success' => false, - 'error' => '조회 오류: ' . $e->getMessage() + 'error' => '조회 오류: '.$e->getMessage(), ]); } } @@ -1680,7 +1714,7 @@ public function saveSplits(Request $request): JsonResponse if (empty($uniqueKey)) { return response()->json([ 'success' => false, - 'error' => '고유키가 없습니다.' + 'error' => '고유키가 없습니다.', ]); } @@ -1693,7 +1727,7 @@ public function saveSplits(Request $request): JsonResponse if (abs($originalAmount - $splitTotal) > 0.01) { return response()->json([ 'success' => false, - 'error' => "분개 금액 합계({$splitTotal})가 원본 금액({$originalAmount})과 일치하지 않습니다." + 'error' => "분개 금액 합계({$splitTotal})가 원본 금액({$originalAmount})과 일치하지 않습니다.", ]); } @@ -1706,14 +1740,15 @@ public function saveSplits(Request $request): JsonResponse return response()->json([ 'success' => true, 'message' => '분개가 저장되었습니다.', - 'splitCount' => count($splits) + 'splitCount' => count($splits), ]); } catch (\Throwable $e) { DB::rollBack(); - Log::error('계좌 분개 저장 오류: ' . $e->getMessage()); + Log::error('계좌 분개 저장 오류: '.$e->getMessage()); + return response()->json([ 'success' => false, - 'error' => '저장 오류: ' . $e->getMessage() + 'error' => '저장 오류: '.$e->getMessage(), ]); } } @@ -1730,7 +1765,7 @@ public function deleteSplits(Request $request): JsonResponse if (empty($uniqueKey)) { return response()->json([ 'success' => false, - 'error' => '고유키가 없습니다.' + 'error' => '고유키가 없습니다.', ]); } @@ -1739,13 +1774,14 @@ public function deleteSplits(Request $request): JsonResponse return response()->json([ 'success' => true, 'message' => '분개가 삭제되었습니다.', - 'deleted' => $deleted + 'deleted' => $deleted, ]); } catch (\Throwable $e) { - Log::error('계좌 분개 삭제 오류: ' . $e->getMessage()); + Log::error('계좌 분개 삭제 오류: '.$e->getMessage()); + return response()->json([ 'success' => false, - 'error' => '삭제 오류: ' . $e->getMessage() + 'error' => '삭제 오류: '.$e->getMessage(), ]); } } @@ -1764,17 +1800,19 @@ private function findBaseBalance(int $tenantId, string $startDate, ?string $bank // 조회기간 이전의 모든 거래를 시간순(ASC)으로 조회 $prevTransactions = BankTransaction::where('tenant_id', $tenantId) ->where('trans_date', '<', $startDate) - ->when($bankAccountNum, fn($q) => $q->where('bank_account_num', $bankAccountNum)) + ->when($bankAccountNum, fn ($q) => $q->where('bank_account_num', $bankAccountNum)) ->orderBy('trans_date', 'asc') ->orderBy('trans_time', 'asc') ->get(); - if ($prevTransactions->isEmpty()) return null; + if ($prevTransactions->isEmpty()) { + return null; + } // 시간순으로 순회하며 잔액 추적 $balance = null; foreach ($prevTransactions as $tx) { - if (!$tx->is_manual && (float) $tx->balance != 0) { + if (! $tx->is_manual && (float) $tx->balance != 0) { // API 데이터: 바로빌이 제공한 정확한 잔액 사용 $balance = (float) $tx->balance; } else { @@ -1805,7 +1843,7 @@ private function mergeWithDedup(array $apiLogs, array $manualLogs): array foreach ($manualLogs as $mLog) { $key = implode('|', [ $mLog['bankAccountNum'] ?? '', - ($mLog['transDate'] ?? '') . ($mLog['transTime'] ?? ''), + ($mLog['transDate'] ?? '').($mLog['transTime'] ?? ''), (int) ($mLog['deposit'] ?? 0), (int) ($mLog['withdraw'] ?? 0), ]); @@ -1819,13 +1857,14 @@ private function mergeWithDedup(array $apiLogs, array $manualLogs): array foreach ($apiLogs as $aLog) { $key = implode('|', [ $aLog['bankAccountNum'] ?? '', - ($aLog['transDate'] ?? '') . ($aLog['transTime'] ?? ''), + ($aLog['transDate'] ?? '').($aLog['transTime'] ?? ''), (int) ($aLog['deposit'] ?? 0), (int) ($aLog['withdraw'] ?? 0), ]); if (isset($manualKeys[$key])) { $removedDeposit += (float) ($aLog['deposit'] ?? 0); $removedWithdraw += (float) ($aLog['withdraw'] ?? 0); + continue; // 수동 거래가 우선, API 거래 스킵 } $dedupedApiLogs[] = $aLog; @@ -1848,14 +1887,16 @@ private function mergeWithDedup(array $apiLogs, array $manualLogs): array private function recalcManualBalances(array $logs, ?float $baseBalance = null): array { - if (empty($logs)) return $logs; + if (empty($logs)) { + return $logs; + } // 시간순(ASC)으로 뒤집어서 순차 처리 $logs = array_reverse($logs); $prevBalance = $baseBalance; foreach ($logs as &$log) { - if (!empty($log['isManual'])) { + if (! empty($log['isManual'])) { $deposit = (float) ($log['deposit'] ?? 0); $withdraw = (float) ($log['withdraw'] ?? 0); $newBalance = ($prevBalance !== null ? $prevBalance : 0) + $deposit - $withdraw; @@ -1879,14 +1920,14 @@ private function convertManualToLogs($manualTransactions): array $totalDeposit = 0; $totalWithdraw = 0; - if (!$manualTransactions || $manualTransactions->isEmpty()) { + if (! $manualTransactions || $manualTransactions->isEmpty()) { return [ 'logs' => [], 'summary' => [ 'totalDeposit' => 0, 'totalWithdraw' => 0, 'count' => 0, - ] + ], ]; } @@ -1899,19 +1940,21 @@ private function convertManualToLogs($manualTransactions): array $transDt = $t->trans_dt ?? ''; $dateTime = ''; - if (!empty($transDt) && strlen($transDt) >= 8) { - $dateTime = substr($transDt, 0, 4) . '-' . substr($transDt, 4, 2) . '-' . substr($transDt, 6, 2); + if (! empty($transDt) && strlen($transDt) >= 8) { + $dateTime = substr($transDt, 0, 4).'-'.substr($transDt, 4, 2).'-'.substr($transDt, 6, 2); if (strlen($transDt) >= 14) { - $dateTime .= ' ' . substr($transDt, 8, 2) . ':' . substr($transDt, 10, 2) . ':' . substr($transDt, 12, 2); + $dateTime .= ' '.substr($transDt, 8, 2).':'.substr($transDt, 10, 2).':'.substr($transDt, 12, 2); } } + $summary = $t->summary ?? ''; $uniqueKey = implode('|', [ $t->bank_account_num, $transDt, (int) $deposit, (int) $withdraw, (int) $balance, + $summary, ]); $logs[] = [ @@ -1926,8 +1969,8 @@ private function convertManualToLogs($manualTransactions): array 'withdrawFormatted' => number_format($withdraw), 'balance' => $balance, 'balanceFormatted' => number_format($balance), - 'summary' => $t->summary ?? '', - 'originalSummary' => $t->summary ?? '', + 'summary' => $summary, + 'originalSummary' => $summary, 'cast' => $t->cast ?? '', 'originalCast' => $t->cast ?? '', 'memo' => $t->memo ?? '', @@ -1950,7 +1993,7 @@ private function convertManualToLogs($manualTransactions): array 'totalDeposit' => $totalDeposit, 'totalWithdraw' => $totalWithdraw, 'count' => count($logs), - ] + ], ]; } @@ -1959,32 +2002,32 @@ private function convertManualToLogs($manualTransactions): array */ private function callSoap(string $method, array $params = []): array { - if (!$this->soapClient) { + if (! $this->soapClient) { return [ 'success' => false, - 'error' => '바로빌 SOAP 클라이언트가 초기화되지 않았습니다.' + 'error' => '바로빌 SOAP 클라이언트가 초기화되지 않았습니다.', ]; } - if (empty($this->certKey) && !$this->isTestMode) { + if (empty($this->certKey) && ! $this->isTestMode) { return [ 'success' => false, - 'error' => 'CERTKEY가 설정되지 않았습니다.' + 'error' => 'CERTKEY가 설정되지 않았습니다.', ]; } if (empty($this->corpNum)) { return [ 'success' => false, - 'error' => '사업자번호가 설정되지 않았습니다.' + 'error' => '사업자번호가 설정되지 않았습니다.', ]; } // CERTKEY와 CorpNum 자동 추가 - if (!isset($params['CERTKEY'])) { + if (! isset($params['CERTKEY'])) { $params['CERTKEY'] = $this->certKey ?? ''; } - if (!isset($params['CorpNum'])) { + if (! isset($params['CorpNum'])) { $params['CorpNum'] = $this->corpNum; } @@ -1992,7 +2035,7 @@ private function callSoap(string $method, array $params = []): array Log::info("바로빌 계좌 API 호출 - Method: {$method}, CorpNum: {$this->corpNum}"); $result = $this->soapClient->$method($params); - $resultProperty = $method . 'Result'; + $resultProperty = $method.'Result'; if (isset($result->$resultProperty)) { $resultData = $result->$resultProperty; @@ -2001,33 +2044,35 @@ private function callSoap(string $method, array $params = []): array if (is_numeric($resultData) && $resultData < 0) { return [ 'success' => false, - 'error' => $this->getErrorMessage((int)$resultData), - 'error_code' => (int)$resultData + 'error' => $this->getErrorMessage((int) $resultData), + 'error_code' => (int) $resultData, ]; } return [ 'success' => true, - 'data' => $resultData + 'data' => $resultData, ]; } return [ 'success' => true, - 'data' => $result + 'data' => $result, ]; } catch (\SoapFault $e) { - Log::error('바로빌 SOAP 오류: ' . $e->getMessage()); + Log::error('바로빌 SOAP 오류: '.$e->getMessage()); + return [ 'success' => false, - 'error' => 'SOAP 오류: ' . $e->getMessage(), - 'error_code' => $e->getCode() + 'error' => 'SOAP 오류: '.$e->getMessage(), + 'error_code' => $e->getCode(), ]; } catch (\Throwable $e) { - Log::error('바로빌 API 호출 오류: ' . $e->getMessage()); + Log::error('바로빌 API 호출 오류: '.$e->getMessage()); + return [ 'success' => false, - 'error' => 'API 호출 오류: ' . $e->getMessage() + 'error' => 'API 호출 오류: '.$e->getMessage(), ]; } } diff --git a/app/Http/Controllers/Finance/JournalEntryController.php b/app/Http/Controllers/Finance/JournalEntryController.php index b30a5451..26cdfffc 100644 --- a/app/Http/Controllers/Finance/JournalEntryController.php +++ b/app/Http/Controllers/Finance/JournalEntryController.php @@ -3,11 +3,11 @@ namespace App\Http\Controllers\Finance; use App\Http\Controllers\Controller; +use App\Models\Barobill\AccountCode; use App\Models\Barobill\BankTransaction; use App\Models\Finance\JournalEntry; use App\Models\Finance\JournalEntryLine; use App\Models\Finance\TradingPartner; -use App\Models\Barobill\AccountCode; use Illuminate\Http\JsonResponse; use Illuminate\Http\Request; use Illuminate\Support\Facades\DB; @@ -224,7 +224,7 @@ public function store(Request $request): JsonResponse return response()->json([ 'success' => false, - 'message' => '전표 저장 실패: ' . $lastError->getMessage(), + 'message' => '전표 저장 실패: '.$lastError->getMessage(), ], 500); } @@ -399,7 +399,7 @@ public function bankTransactions(Request $request): JsonResponse $accountNum = $request->input('accountNum', ''); // barobill_bank_transactions 테이블에서 직접 조회 (중복 제거) - $normalizedAccNum = !empty($accountNum) ? str_replace('-', '', $accountNum) : null; + $normalizedAccNum = ! empty($accountNum) ? str_replace('-', '', $accountNum) : null; $dedupQuery = BankTransaction::where('tenant_id', $tenantId) ->whereBetween('trans_date', [$startDate, $endDate]); @@ -462,16 +462,29 @@ public function bankTransactions(Request $request): JsonResponse $journaledKeys = JournalEntry::getJournaledSourceKeys($tenantId, 'bank_transaction', $uniqueKeys); $journaledKeysMap = array_flip($journaledKeys); - // 분개된 전표 ID도 조회 + // 분개된 전표 ID도 조회 (레거시 키 포함) $journalMap = []; - if (!empty($journaledKeys)) { + if (! empty($journaledKeys)) { + // 새 키 + 레거시 키 모두 검색 + $allSearchKeys = $journaledKeys; + $legacyMap = []; + foreach ($journaledKeys as $key) { + $parts = explode('|', $key); + if (count($parts) === 6) { + $legacyKey = implode('|', array_slice($parts, 0, 5)); + $allSearchKeys[] = $legacyKey; + $legacyMap[$legacyKey] = $key; + } + } + $journals = JournalEntry::where('tenant_id', $tenantId) ->where('source_type', 'bank_transaction') - ->whereIn('source_key', $journaledKeys) + ->whereIn('source_key', array_unique($allSearchKeys)) ->select('id', 'source_key', 'entry_no') ->get(); foreach ($journals as $j) { - $journalMap[$j->source_key] = ['id' => $j->id, 'entry_no' => $j->entry_no]; + $mappedKey = $legacyMap[$j->source_key] ?? $j->source_key; + $journalMap[$mappedKey] = ['id' => $j->id, 'entry_no' => $j->entry_no]; } } @@ -514,10 +527,11 @@ public function bankTransactions(Request $request): JsonResponse ], ]); } catch (\Throwable $e) { - Log::error('은행거래 목록 조회 오류: ' . $e->getMessage()); + Log::error('은행거래 목록 조회 오류: '.$e->getMessage()); + return response()->json([ 'success' => false, - 'message' => '은행거래 목록 조회 실패: ' . $e->getMessage(), + 'message' => '은행거래 목록 조회 실패: '.$e->getMessage(), ], 500); } } @@ -560,7 +574,7 @@ public function storeFromBank(Request $request): JsonResponse if ($existing) { return response()->json([ 'success' => false, - 'message' => '이미 분개가 완료된 거래입니다. (전표번호: ' . $existing->entry_no . ')', + 'message' => '이미 분개가 완료된 거래입니다. (전표번호: '.$existing->entry_no.')', ], 422); } @@ -621,11 +635,11 @@ public function storeFromBank(Request $request): JsonResponse } } - Log::error('은행거래 분개 저장 오류: ' . $lastError->getMessage()); + Log::error('은행거래 분개 저장 오류: '.$lastError->getMessage()); return response()->json([ 'success' => false, - 'message' => '분개 저장 실패: ' . $lastError->getMessage(), + 'message' => '분개 저장 실패: '.$lastError->getMessage(), ], 500); } @@ -637,17 +651,24 @@ public function bankJournals(Request $request): JsonResponse $tenantId = session('selected_tenant_id', 1); $sourceKey = $request->get('source_key'); - if (!$sourceKey) { + if (! $sourceKey) { return response()->json(['success' => false, 'message' => 'source_key가 필요합니다.'], 422); } + // 레거시(summary 미포함) 형식과 신규(summary 포함) 형식 모두 매칭 + $keys = [$sourceKey]; + $parts = explode('|', $sourceKey); + if (count($parts) === 6) { + $keys[] = implode('|', array_slice($parts, 0, 5)); + } + $entry = JournalEntry::forTenant($tenantId) ->where('source_type', 'bank_transaction') - ->where('source_key', $sourceKey) + ->whereIn('source_key', $keys) ->with('lines') ->first(); - if (!$entry) { + if (! $entry) { return response()->json(['success' => true, 'data' => null]); } @@ -729,7 +750,7 @@ private function getPreviousBalances(int $tenantId, string $startDate, ?string $ $balances = []; foreach ($transactions as $tx) { $accNum = $tx->bank_account_num; - if (!$tx->is_manual && (float) $tx->balance != 0) { + if (! $tx->is_manual && (float) $tx->balance != 0) { // API 데이터: 바로빌이 제공한 잔액을 앵커로 사용 $balances[$accNum] = (float) $tx->balance; } else { @@ -797,7 +818,7 @@ public function accountCodeStore(Request $request): JsonResponse public function accountCodeUpdate(Request $request, int $id): JsonResponse { $accountCode = AccountCode::find($id); - if (!$accountCode) { + if (! $accountCode) { return response()->json(['success' => false, 'error' => '계정과목을 찾을 수 없습니다.'], 404); } @@ -829,7 +850,7 @@ public function accountCodeUpdate(Request $request, int $id): JsonResponse public function accountCodeDestroy(int $id): JsonResponse { $accountCode = AccountCode::find($id); - if (!$accountCode) { + if (! $accountCode) { return response()->json(['success' => false, 'error' => '계정과목을 찾을 수 없습니다.'], 404); } diff --git a/app/Models/Barobill/BankTransaction.php b/app/Models/Barobill/BankTransaction.php index c9833a5e..242491d3 100644 --- a/app/Models/Barobill/BankTransaction.php +++ b/app/Models/Barobill/BankTransaction.php @@ -2,9 +2,9 @@ namespace App\Models\Barobill; +use App\Models\Tenants\Tenant; use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Relations\BelongsTo; -use App\Models\Tenants\Tenant; /** * 바로빌 계좌 입출금 내역 모델 @@ -61,6 +61,7 @@ public function getUniqueKeyAttribute(): string (int) $this->deposit, (int) $this->withdraw, (int) $this->balance, + $this->summary ?? '', ]); } @@ -70,7 +71,7 @@ public function getUniqueKeyAttribute(): string public static function generateUniqueKey(array $log): string { // trans_dt 생성: transDate + transTime - $transDt = ($log['transDate'] ?? '') . ($log['transTime'] ?? ''); + $transDt = ($log['transDate'] ?? '').($log['transTime'] ?? ''); return implode('|', [ $log['bankAccountNum'] ?? '', @@ -78,6 +79,7 @@ public static function generateUniqueKey(array $log): string $log['deposit'] ?? 0, $log['withdraw'] ?? 0, $log['balance'] ?? 0, + $log['summary'] ?? '', ]); } @@ -110,6 +112,6 @@ public static function getByDateRange(int $tenantId, string $startDate, string $ $query->where('bank_account_num', $accountNum); } - return $query->get()->keyBy(fn($item) => $item->unique_key); + return $query->get()->keyBy(fn ($item) => $item->unique_key); } } diff --git a/app/Models/Barobill/BankTransactionOverride.php b/app/Models/Barobill/BankTransactionOverride.php index f370f4d3..2dd49a01 100644 --- a/app/Models/Barobill/BankTransactionOverride.php +++ b/app/Models/Barobill/BankTransactionOverride.php @@ -37,6 +37,8 @@ public function scopeByUniqueKey($query, string $uniqueKey) /** * 여러 고유키에 대한 오버라이드 조회 + * 레거시(summary 미포함) 형식과 신규(summary 포함) 형식 모두 매칭 + * * @return Collection key가 unique_key인 컬렉션 */ public static function getByUniqueKeys(int $tenantId, array $uniqueKeys): Collection @@ -45,10 +47,26 @@ public static function getByUniqueKeys(int $tenantId, array $uniqueKeys): Collec return collect(); } - return static::forTenant($tenantId) - ->whereIn('unique_key', $uniqueKeys) - ->get() - ->keyBy('unique_key'); + // 레거시 키도 함께 검색 + $allKeys = $uniqueKeys; + $legacyToNewMap = []; + foreach ($uniqueKeys as $key) { + $parts = explode('|', $key); + if (count($parts) === 6) { + $legacyKey = implode('|', array_slice($parts, 0, 5)); + $allKeys[] = $legacyKey; + $legacyToNewMap[$legacyKey] = $key; + } + } + + $results = static::forTenant($tenantId) + ->whereIn('unique_key', array_unique($allKeys)) + ->get(); + + // 레거시 키로 매칭된 것을 새 키로 리맵하여 keyBy + return $results->keyBy(function ($item) use ($legacyToNewMap) { + return $legacyToNewMap[$item->unique_key] ?? $item->unique_key; + }); } /** @@ -63,6 +81,7 @@ public static function saveOverride( // 둘 다 null이거나 빈 문자열이면 기존 레코드 삭제 if (empty($modifiedSummary) && empty($modifiedCast)) { static::forTenant($tenantId)->byUniqueKey($uniqueKey)->delete(); + return null; } diff --git a/app/Models/Finance/JournalEntry.php b/app/Models/Finance/JournalEntry.php index c6367de9..d60bffc6 100644 --- a/app/Models/Finance/JournalEntry.php +++ b/app/Models/Finance/JournalEntry.php @@ -45,6 +45,7 @@ public function scopeForTenant($query, $tenantId) /** * 분개 완료된 source_key 일괄 조회 + * 레거시(summary 미포함) 형식과 신규(summary 포함) 형식 모두 매칭 */ public static function getJournaledSourceKeys(int $tenantId, string $sourceType, array $sourceKeys): array { @@ -52,21 +53,52 @@ public static function getJournaledSourceKeys(int $tenantId, string $sourceType, return []; } - return static::where('tenant_id', $tenantId) + // 레거시 키도 함께 검색 (새 키에서 마지막 |summary 부분 제거) + $allKeys = $sourceKeys; + $legacyToNewMap = []; + foreach ($sourceKeys as $key) { + $parts = explode('|', $key); + if (count($parts) === 6) { + $legacyKey = implode('|', array_slice($parts, 0, 5)); + $allKeys[] = $legacyKey; + $legacyToNewMap[$legacyKey] = $key; + } + } + + $found = static::where('tenant_id', $tenantId) ->where('source_type', $sourceType) - ->whereIn('source_key', $sourceKeys) + ->whereIn('source_key', array_unique($allKeys)) ->pluck('source_key') ->toArray(); + + // 레거시 키로 매칭된 것을 새 키 형식으로 변환하여 반환 + $result = []; + foreach ($found as $key) { + if (in_array($key, $sourceKeys)) { + $result[] = $key; + } elseif (isset($legacyToNewMap[$key])) { + $result[] = $legacyToNewMap[$key]; + } + } + + return array_unique($result); } /** * source_key로 분개 전표 조회 (ID 포함) + * 레거시(summary 미포함) 형식과 신규(summary 포함) 형식 모두 매칭 */ public static function getJournalBySourceKey(int $tenantId, string $sourceType, string $sourceKey) { + $keys = [$sourceKey]; + $parts = explode('|', $sourceKey); + if (count($parts) === 6) { + $keys[] = implode('|', array_slice($parts, 0, 5)); + } + return static::where('tenant_id', $tenantId) ->where('source_type', $sourceType) - ->where('source_key', $sourceKey) + ->whereIn('source_key', $keys) ->first(); } @@ -81,7 +113,7 @@ public static function generateEntryNo($tenantId, $date) // withTrashed: soft-deleted 레코드도 포함하여 채번 (DB unique 제약 충돌 방지) $last = static::withTrashed() ->where('tenant_id', $tenantId) - ->where('entry_no', 'like', $prefix . '%') + ->where('entry_no', 'like', $prefix.'%') ->lockForUpdate() ->orderByDesc('entry_no') ->value('entry_no'); @@ -92,6 +124,6 @@ public static function generateEntryNo($tenantId, $date) $seq = 1; } - return $prefix . str_pad($seq, 3, '0', STR_PAD_LEFT); + return $prefix.str_pad($seq, 3, '0', STR_PAD_LEFT); } }