Files
sam-manage/app/Http/Controllers/Barobill/EaccountController.php
pro 71080389c8 feat:계좌 입출금내역 조회 페이지 추가
- EaccountController.php: 바로빌 BANKACCOUNT.asmx SOAP API 연동
  - GetBankAccountEx: 등록된 계좌 목록 조회
  - GetPeriodBankAccountTransLog: 계좌 입출금내역 조회
- index.blade.php: React 기반 UI (전자세금계산서와 동일 구조)
  - 테넌트 정보 카드
  - 통계 카드 (입금/출금/계좌수/거래건수)
  - 계좌 선택 버튼
  - 기간 조회 필터 (이번달/지난달 버튼)
  - 입출금 내역 테이블 (스크롤)
- 라우트 추가: /barobill/eaccount
- 메뉴 시더 업데이트

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-23 10:13:28 +09:00

588 lines
21 KiB
PHP

<?php
namespace App\Http\Controllers\Barobill;
use App\Http\Controllers\Controller;
use App\Models\Barobill\BarobillConfig;
use App\Models\Barobill\BarobillMember;
use App\Models\Tenants\Tenant;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Illuminate\Http\Response;
use Illuminate\Support\Facades\Log;
use Illuminate\View\View;
/**
* 바로빌 계좌 입출금내역 조회 컨트롤러
*/
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
private const HEADQUARTERS_TENANT_ID = 1;
public function __construct()
{
// DB에서 활성화된 바로빌 설정 조회
$activeConfig = BarobillConfig::where('is_active', true)->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()
];
}
}
}