From 71080389c8c778cfea2f9235f2f37ffe42270101 Mon Sep 17 00:00:00 2001 From: pro Date: Fri, 23 Jan 2026 10:13:28 +0900 Subject: [PATCH] =?UTF-8?q?feat:=EA=B3=84=EC=A2=8C=20=EC=9E=85=EC=B6=9C?= =?UTF-8?q?=EA=B8=88=EB=82=B4=EC=97=AD=20=EC=A1=B0=ED=9A=8C=20=ED=8E=98?= =?UTF-8?q?=EC=9D=B4=EC=A7=80=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - EaccountController.php: 바로빌 BANKACCOUNT.asmx SOAP API 연동 - GetBankAccountEx: 등록된 계좌 목록 조회 - GetPeriodBankAccountTransLog: 계좌 입출금내역 조회 - index.blade.php: React 기반 UI (전자세금계산서와 동일 구조) - 테넌트 정보 카드 - 통계 카드 (입금/출금/계좌수/거래건수) - 계좌 선택 버튼 - 기간 조회 필터 (이번달/지난달 버튼) - 입출금 내역 테이블 (스크롤) - 라우트 추가: /barobill/eaccount - 메뉴 시더 업데이트 Co-Authored-By: Claude Opus 4.5 --- .../Barobill/EaccountController.php | 587 ++++++++++++++++++ database/seeders/MngMenuSeeder.php | 6 +- .../views/barobill/eaccount/index.blade.php | 476 ++++++++++++++ routes/web.php | 7 + 4 files changed, 1073 insertions(+), 3 deletions(-) create mode 100644 app/Http/Controllers/Barobill/EaccountController.php create mode 100644 resources/views/barobill/eaccount/index.blade.php diff --git a/app/Http/Controllers/Barobill/EaccountController.php b/app/Http/Controllers/Barobill/EaccountController.php new file mode 100644 index 00000000..b81e7422 --- /dev/null +++ b/app/Http/Controllers/Barobill/EaccountController.php @@ -0,0 +1,587 @@ +first(); + + if ($activeConfig) { + $this->certKey = $activeConfig->cert_key; + $this->corpNum = $activeConfig->corp_num; + $this->isTestMode = $activeConfig->environment === 'test'; + // 계좌 조회는 BANKACCOUNT.asmx 사용 + $baseUrl = $this->isTestMode + ? 'https://testws.baroservice.com' + : 'https://ws.baroservice.com'; + $this->soapUrl = $baseUrl . '/BANKACCOUNT.asmx?WSDL'; + } else { + $this->certKey = config('services.barobill.cert_key', ''); + $this->corpNum = config('services.barobill.corp_num', ''); + $this->isTestMode = config('services.barobill.test_mode', true); + $this->soapUrl = $this->isTestMode + ? 'https://testws.baroservice.com/BANKACCOUNT.asmx?WSDL' + : 'https://ws.baroservice.com/BANKACCOUNT.asmx?WSDL'; + } + + $this->initSoapClient(); + } + + /** + * SOAP 클라이언트 초기화 + */ + private function initSoapClient(): void + { + if (!empty($this->certKey) || $this->isTestMode) { + try { + $context = stream_context_create([ + 'ssl' => [ + 'verify_peer' => false, + 'verify_peer_name' => false, + 'allow_self_signed' => true + ] + ]); + + $this->soapClient = new \SoapClient($this->soapUrl, [ + 'trace' => true, + 'encoding' => 'UTF-8', + 'exceptions' => true, + 'connection_timeout' => 30, + 'stream_context' => $context, + 'cache_wsdl' => WSDL_CACHE_NONE + ]); + } catch (\Throwable $e) { + Log::error('바로빌 계좌 SOAP 클라이언트 생성 실패: ' . $e->getMessage()); + } + } + } + + /** + * 계좌 입출금내역 메인 페이지 + */ + public function index(Request $request): View|Response + { + if ($request->header('HX-Request')) { + return response('', 200)->header('HX-Redirect', route('barobill.eaccount.index')); + } + + // 현재 선택된 테넌트 정보 + $tenantId = session('selected_tenant_id', self::HEADQUARTERS_TENANT_ID); + $currentTenant = Tenant::find($tenantId); + + // 해당 테넌트의 바로빌 회원사 정보 + $barobillMember = BarobillMember::where('tenant_id', $tenantId)->first(); + + return view('barobill.eaccount.index', [ + 'certKey' => $this->certKey, + 'corpNum' => $this->corpNum, + 'isTestMode' => $this->isTestMode, + 'hasSoapClient' => $this->soapClient !== null, + 'currentTenant' => $currentTenant, + 'barobillMember' => $barobillMember, + ]); + } + + /** + * 등록된 계좌 목록 조회 (GetBankAccountEx) + */ + public function accounts(Request $request): JsonResponse + { + try { + $availOnly = $request->input('availOnly', 0); + + // 현재 테넌트의 바로빌 회원 정보 조회 + $tenantId = session('selected_tenant_id', self::HEADQUARTERS_TENANT_ID); + $barobillMember = BarobillMember::where('tenant_id', $tenantId)->first(); + + // 바로빌 사용자 ID 결정 + $userId = $barobillMember?->barobill_id ?? ''; + + $result = $this->callSoap('GetBankAccountEx', [ + 'AvailOnly' => (int)$availOnly + ]); + + if (!$result['success']) { + return response()->json([ + 'success' => false, + 'error' => $result['error'], + 'error_code' => $result['error_code'] ?? null + ]); + } + + $accounts = []; + $data = $result['data']; + + // BankAccount 또는 BankAccountEx에서 계좌 목록 추출 + $accountList = []; + if (isset($data->BankAccount)) { + $accountList = is_array($data->BankAccount) ? $data->BankAccount : [$data->BankAccount]; + } elseif (isset($data->BankAccountEx)) { + $accountList = is_array($data->BankAccountEx) ? $data->BankAccountEx : [$data->BankAccountEx]; + } + + foreach ($accountList as $acc) { + if (!is_object($acc)) continue; + + $bankAccountNum = $acc->BankAccountNum ?? ''; + if (empty($bankAccountNum) || (is_numeric($bankAccountNum) && $bankAccountNum < 0)) { + continue; + } + + $bankCode = $acc->BankCode ?? ''; + $bankName = $acc->BankName ?? $this->getBankName($bankCode); + + $accounts[] = [ + 'bankAccountNum' => $bankAccountNum, + 'bankCode' => $bankCode, + 'bankName' => $bankName, + 'accountName' => $acc->AccountName ?? '', + 'accountType' => $acc->AccountType ?? '', + 'currency' => $acc->Currency ?? 'KRW', + 'issueDate' => $acc->IssueDate ?? '', + 'balance' => $acc->Balance ?? 0, + 'status' => isset($acc->UseState) ? (int)$acc->UseState : 1 + ]; + } + + return response()->json([ + 'success' => true, + 'accounts' => $accounts, + 'count' => count($accounts) + ]); + } catch (\Throwable $e) { + Log::error('계좌 목록 조회 오류: ' . $e->getMessage()); + return response()->json([ + 'success' => false, + 'error' => '서버 오류: ' . $e->getMessage() + ]); + } + } + + /** + * 계좌 입출금내역 조회 (GetPeriodBankAccountTransLog) + */ + public function transactions(Request $request): JsonResponse + { + try { + $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); + + // 현재 테넌트의 바로빌 회원 정보 조회 + $tenantId = session('selected_tenant_id', self::HEADQUARTERS_TENANT_ID); + $barobillMember = BarobillMember::where('tenant_id', $tenantId)->first(); + $userId = $barobillMember?->barobill_id ?? ''; + + // 전체 계좌 조회: 빈 값이면 모든 계좌의 거래 내역 조회 + if (empty($bankAccountNum)) { + return $this->getAllAccountsTransactions($userId, $startDate, $endDate, $page, $limit); + } + + // 단일 계좌 조회 + $result = $this->callSoap('GetPeriodBankAccountTransLog', [ + 'ID' => $userId, + 'BankAccountNum' => $bankAccountNum, + 'StartDate' => $startDate, + 'EndDate' => $endDate, + 'TransDirection' => 1, // 1:전체 + 'CountPerPage' => $limit, + 'CurrentPage' => $page, + 'OrderDirection' => 2 // 2:내림차순 + ]); + + if (!$result['success']) { + return response()->json([ + 'success' => false, + 'error' => $result['error'], + 'error_code' => $result['error_code'] ?? null + ]); + } + + $resultData = $result['data']; + + // 에러 코드 체크 + $errorCode = $this->checkErrorCode($resultData); + if ($errorCode && !in_array($errorCode, [-25005, -25001])) { + return response()->json([ + 'success' => false, + 'error' => $this->getErrorMessage($errorCode), + 'error_code' => $errorCode + ]); + } + + // 데이터가 없는 경우 + if ($errorCode && in_array($errorCode, [-25005, -25001])) { + return response()->json([ + 'success' => true, + 'data' => [ + 'logs' => [], + 'summary' => ['totalDeposit' => 0, 'totalWithdraw' => 0, 'count' => 0], + 'pagination' => ['currentPage' => 1, 'maxPageNum' => 1] + ] + ]); + } + + // 데이터 파싱 + $logs = $this->parseTransactionLogs($resultData); + + return response()->json([ + 'success' => true, + 'data' => [ + 'logs' => $logs['logs'], + 'pagination' => [ + 'currentPage' => $resultData->CurrentPage ?? 1, + 'countPerPage' => $resultData->CountPerPage ?? 50, + 'maxPageNum' => $resultData->MaxPageNum ?? 1, + 'maxIndex' => $resultData->MaxIndex ?? 0 + ], + 'summary' => $logs['summary'] + ] + ]); + } catch (\Throwable $e) { + Log::error('입출금내역 조회 오류: ' . $e->getMessage()); + return response()->json([ + 'success' => false, + 'error' => '서버 오류: ' . $e->getMessage() + ]); + } + } + + /** + * 전체 계좌의 거래 내역 조회 + */ + private function getAllAccountsTransactions(string $userId, string $startDate, string $endDate, int $page, int $limit): JsonResponse + { + // 먼저 계좌 목록 조회 + $accountResult = $this->callSoap('GetBankAccountEx', ['AvailOnly' => 0]); + + if (!$accountResult['success']) { + return response()->json([ + 'success' => false, + 'error' => $accountResult['error'] + ]); + } + + $accountList = []; + $data = $accountResult['data']; + if (isset($data->BankAccount)) { + $accountList = is_array($data->BankAccount) ? $data->BankAccount : [$data->BankAccount]; + } elseif (isset($data->BankAccountEx)) { + $accountList = is_array($data->BankAccountEx) ? $data->BankAccountEx : [$data->BankAccountEx]; + } + + $allLogs = []; + $totalDeposit = 0; + $totalWithdraw = 0; + + foreach ($accountList as $acc) { + if (!is_object($acc)) continue; + + $accNum = $acc->BankAccountNum ?? ''; + if (empty($accNum) || (is_numeric($accNum) && $accNum < 0)) continue; + + $accResult = $this->callSoap('GetPeriodBankAccountTransLog', [ + 'ID' => $userId, + 'BankAccountNum' => $accNum, + 'StartDate' => $startDate, + 'EndDate' => $endDate, + 'TransDirection' => 1, + 'CountPerPage' => 1000, + 'CurrentPage' => 1, + 'OrderDirection' => 2 + ]); + + if ($accResult['success']) { + $accData = $accResult['data']; + $errorCode = $this->checkErrorCode($accData); + + if (!$errorCode || in_array($errorCode, [-25005, -25001])) { + $parsed = $this->parseTransactionLogs($accData, $acc->BankName ?? ''); + foreach ($parsed['logs'] as $log) { + $log['bankName'] = $acc->BankName ?? $this->getBankName($acc->BankCode ?? ''); + $allLogs[] = $log; + } + $totalDeposit += $parsed['summary']['totalDeposit']; + $totalWithdraw += $parsed['summary']['totalWithdraw']; + } + } + } + + // 날짜/시간 기준 정렬 (최신순) + usort($allLogs, function ($a, $b) { + $dateA = ($a['transDate'] ?? '') . ($a['transTime'] ?? ''); + $dateB = ($b['transDate'] ?? '') . ($b['transTime'] ?? ''); + return strcmp($dateB, $dateA); + }); + + // 페이지네이션 + $totalCount = count($allLogs); + $maxPageNum = (int)ceil($totalCount / $limit); + $startIndex = ($page - 1) * $limit; + $paginatedLogs = array_slice($allLogs, $startIndex, $limit); + + return response()->json([ + 'success' => true, + 'data' => [ + 'logs' => $paginatedLogs, + 'pagination' => [ + 'currentPage' => $page, + 'countPerPage' => $limit, + 'maxPageNum' => $maxPageNum, + 'maxIndex' => $totalCount + ], + 'summary' => [ + 'totalDeposit' => $totalDeposit, + 'totalWithdraw' => $totalWithdraw, + 'count' => $totalCount + ] + ] + ]); + } + + /** + * 거래 내역 파싱 + */ + private function parseTransactionLogs($resultData, string $defaultBankName = ''): array + { + $logs = []; + $totalDeposit = 0; + $totalWithdraw = 0; + + $rawLogs = []; + if (isset($resultData->BankAccountLogList) && isset($resultData->BankAccountLogList->BankAccountTransLog)) { + $rawLogs = is_array($resultData->BankAccountLogList->BankAccountTransLog) + ? $resultData->BankAccountLogList->BankAccountTransLog + : [$resultData->BankAccountLogList->BankAccountTransLog]; + } + + foreach ($rawLogs as $log) { + $deposit = floatval($log->Deposit ?? 0); + $withdraw = floatval($log->Withdraw ?? 0); + $totalDeposit += $deposit; + $totalWithdraw += $withdraw; + + // 거래일시 파싱 + $transDT = $log->TransDT ?? ''; + $transDate = ''; + $transTime = ''; + $dateTime = ''; + + 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); + } + + // 적요 파싱 + $summary = $log->TransRemark1 ?? $log->Summary ?? ''; + $remark2 = $log->TransRemark2 ?? ''; + $transType = $log->TransType ?? ''; + $fullSummary = $summary; + if (!empty($remark2)) { + $fullSummary = $fullSummary ? $fullSummary . ' ' . $remark2 : $remark2; + } + + $logs[] = [ + 'transDate' => $transDate, + 'transTime' => $transTime, + 'transDateTime' => $dateTime, + 'bankAccountNum' => $log->BankAccountNum ?? '', + 'bankName' => $log->BankName ?? $defaultBankName, + 'deposit' => $deposit, + 'withdraw' => $withdraw, + 'depositFormatted' => number_format($deposit), + 'withdrawFormatted' => number_format($withdraw), + 'balance' => floatval($log->Balance ?? 0), + 'balanceFormatted' => number_format(floatval($log->Balance ?? 0)), + 'summary' => $fullSummary, + 'cast' => $log->Cast ?? '', + 'memo' => $log->Memo ?? '', + 'transOffice' => $log->TransOffice ?? '' + ]; + } + + return [ + 'logs' => $logs, + 'summary' => [ + 'totalDeposit' => $totalDeposit, + 'totalWithdraw' => $totalWithdraw, + 'count' => count($logs) + ] + ]; + } + + /** + * 에러 코드 체크 + */ + private function checkErrorCode($data): ?int + { + if (isset($data->CurrentPage) && is_numeric($data->CurrentPage) && $data->CurrentPage < 0) { + return (int)$data->CurrentPage; + } + if (isset($data->BankAccountNum) && is_numeric($data->BankAccountNum) && $data->BankAccountNum < 0) { + return (int)$data->BankAccountNum; + } + return null; + } + + /** + * 에러 메시지 반환 + */ + private function getErrorMessage(int $errorCode): string + { + $messages = [ + -10002 => '인증 실패 (-10002). CERTKEY가 올바르지 않거나 만료되었습니다.', + -50214 => '은행 로그인 실패 (-50214). 바로빌 사이트에서 계좌 비밀번호/인증서를 점검해주세요.', + -24005 => '사용자 정보 불일치 (-24005). 사업자번호를 확인해주세요.', + -25001 => '등록된 계좌가 없습니다 (-25001).', + -25005 => '조회된 데이터가 없습니다 (-25005).', + -25006 => '계좌번호가 잘못되었습니다 (-25006).', + -25007 => '조회 기간이 잘못되었습니다 (-25007).', + ]; + return $messages[$errorCode] ?? '바로빌 API 오류: ' . $errorCode; + } + + /** + * 은행 코드 -> 은행명 변환 + */ + private function getBankName(string $code): string + { + $banks = [ + '002' => 'KDB산업은행', + '003' => 'IBK기업은행', + '004' => 'KB국민은행', + '007' => '수협은행', + '011' => 'NH농협은행', + '020' => '우리은행', + '023' => 'SC제일은행', + '027' => '한국씨티은행', + '031' => '대구은행', + '032' => '부산은행', + '034' => '광주은행', + '035' => '제주은행', + '037' => '전북은행', + '039' => '경남은행', + '045' => '새마을금고', + '048' => '신협', + '050' => '저축은행', + '064' => '산림조합', + '071' => '우체국', + '081' => '하나은행', + '088' => '신한은행', + '089' => 'K뱅크', + '090' => '카카오뱅크', + '092' => '토스뱅크' + ]; + return $banks[$code] ?? $code; + } + + /** + * SOAP 호출 + */ + private function callSoap(string $method, array $params = []): array + { + if (!$this->soapClient) { + return [ + 'success' => false, + 'error' => '바로빌 SOAP 클라이언트가 초기화되지 않았습니다.' + ]; + } + + if (empty($this->certKey) && !$this->isTestMode) { + return [ + 'success' => false, + 'error' => 'CERTKEY가 설정되지 않았습니다.' + ]; + } + + if (empty($this->corpNum)) { + return [ + 'success' => false, + 'error' => '사업자번호가 설정되지 않았습니다.' + ]; + } + + // CERTKEY와 CorpNum 자동 추가 + if (!isset($params['CERTKEY'])) { + $params['CERTKEY'] = $this->certKey ?? ''; + } + if (!isset($params['CorpNum'])) { + $params['CorpNum'] = $this->corpNum; + } + + try { + Log::info("바로빌 계좌 API 호출 - Method: {$method}, CorpNum: {$this->corpNum}"); + + $result = $this->soapClient->$method($params); + $resultProperty = $method . 'Result'; + + if (isset($result->$resultProperty)) { + $resultData = $result->$resultProperty; + + // 에러 코드 체크 + if (is_numeric($resultData) && $resultData < 0) { + return [ + 'success' => false, + 'error' => $this->getErrorMessage((int)$resultData), + 'error_code' => (int)$resultData + ]; + } + + return [ + 'success' => true, + 'data' => $resultData + ]; + } + + return [ + 'success' => true, + 'data' => $result + ]; + } catch (\SoapFault $e) { + Log::error('바로빌 SOAP 오류: ' . $e->getMessage()); + return [ + 'success' => false, + 'error' => 'SOAP 오류: ' . $e->getMessage(), + 'error_code' => $e->getCode() + ]; + } catch (\Throwable $e) { + Log::error('바로빌 API 호출 오류: ' . $e->getMessage()); + return [ + 'success' => false, + 'error' => 'API 호출 오류: ' . $e->getMessage() + ]; + } + } +} diff --git a/database/seeders/MngMenuSeeder.php b/database/seeders/MngMenuSeeder.php index f332e2ca..fdd2aaaa 100644 --- a/database/seeders/MngMenuSeeder.php +++ b/database/seeders/MngMenuSeeder.php @@ -584,11 +584,11 @@ protected function seedMainMenus(): void ]); $this->createMenu([ 'parent_id' => $barobillGroup->id, - 'name' => '계좌조회', - 'url' => '/barobill/bank-account', + 'name' => '계좌 입출금내역', + 'url' => '/barobill/eaccount', 'icon' => 'credit-card', 'sort_order' => $barobillSubOrder++, - 'options' => ['route_name' => 'barobill.bank-account.index', 'section' => 'main'], + 'options' => ['route_name' => 'barobill.eaccount.index', 'section' => 'main'], ]); $this->createMenu([ 'parent_id' => $barobillGroup->id, diff --git a/resources/views/barobill/eaccount/index.blade.php b/resources/views/barobill/eaccount/index.blade.php new file mode 100644 index 00000000..3ffaf3b3 --- /dev/null +++ b/resources/views/barobill/eaccount/index.blade.php @@ -0,0 +1,476 @@ +@extends('layouts.app') + +@section('title', '계좌 입출금내역') + +@section('content') + +@if($currentTenant) +
+
+
+
+ + + +
+
+
+ T-ID: {{ $currentTenant->id }} + @if($currentTenant->id == 1) + 파트너사 + @endif +
+

{{ $currentTenant->company_name }}

+
+
+ @if($barobillMember) +
+
+

사업자번호

+

{{ $barobillMember->biz_no }}

+
+
+

대표자

+

{{ $barobillMember->ceo_name ?? '-' }}

+
+
+

담당자

+

{{ $barobillMember->manager_name ?? '-' }}

+
+
+

바로빌 ID

+

{{ $barobillMember->barobill_id }}

+
+
+ @else +
+ + + + 바로빌 회원사 미연동 +
+ @endif +
+
+@endif + +
+@endsection + +@push('scripts') + + + + + +@endpush diff --git a/routes/web.php b/routes/web.php index 3de31351..f79eff87 100644 --- a/routes/web.php +++ b/routes/web.php @@ -286,6 +286,13 @@ Route::post('/send-to-nts', [\App\Http\Controllers\Barobill\EtaxController::class, 'sendToNts'])->name('send-to-nts'); Route::post('/delete', [\App\Http\Controllers\Barobill\EtaxController::class, 'delete'])->name('delete'); }); + + // 계좌 입출금내역 (React 페이지) + Route::prefix('eaccount')->name('eaccount.')->group(function () { + Route::get('/', [\App\Http\Controllers\Barobill\EaccountController::class, 'index'])->name('index'); + Route::get('/accounts', [\App\Http\Controllers\Barobill\EaccountController::class, 'accounts'])->name('accounts'); + Route::get('/transactions', [\App\Http\Controllers\Barobill\EaccountController::class, 'transactions'])->name('transactions'); + }); }); /*