Files
sam-manage/app/Services/Barobill/BarobillBankSyncService.php
김보곤 ca36e8e54d fix: [daily-fund] 일일자금일보 바로빌 데이터 자동 동기화 추가
- periodReport에서 데이터 조회 전 바로빌 API 자동 동기화 트리거
- BarobillBankSyncService 서비스 클래스 생성 (EaccountController 로직 분리)
- 현재 월 캐시 10분 만료, 과거 월 영구 캐시 정책 동일 적용
- 동기화 실패 시 기존 DB 캐시로 폴백 (서비스 중단 방지)
2026-03-11 09:55:50 +09:00

376 lines
12 KiB
PHP

<?php
namespace App\Services\Barobill;
use App\Models\Barobill\BankSyncStatus;
use App\Models\Barobill\BankTransaction;
use App\Models\Barobill\BarobillConfig;
use App\Models\Barobill\BarobillMember;
use Carbon\Carbon;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Log;
/**
* 바로빌 계좌 거래내역 동기화 서비스
*
* EaccountController의 캐시/동기화 로직을 서비스로 분리하여
* DailyFundController 등 다른 곳에서도 동기화를 트리거할 수 있도록 함
*/
class BarobillBankSyncService
{
private ?string $certKey = null;
private ?string $corpNum = null;
private bool $isTestMode = false;
private ?string $soapUrl = null;
private ?\SoapClient $soapClient = null;
/**
* 지정 기간의 거래내역이 최신인지 확인하고, 필요 시 바로빌 API에서 동기화
*
* @param int $tenantId 테넌트 ID
* @param string $startDateYmd 시작일 (YYYYMMDD)
* @param string $endDateYmd 종료일 (YYYYMMDD)
*/
public function syncIfNeeded(int $tenantId, string $startDateYmd, string $endDateYmd): void
{
$member = BarobillMember::where('tenant_id', $tenantId)->first();
if (! $member || empty($member->barobill_id)) {
return;
}
$this->initFromConfig($member);
if (! $this->soapClient) {
Log::warning('[BankSync] SOAP 클라이언트 초기화 실패', ['tenantId' => $tenantId]);
return;
}
$accounts = $this->getRegisteredAccounts();
if (empty($accounts)) {
Log::debug('[BankSync] 등록된 계좌 없음', ['tenantId' => $tenantId]);
return;
}
$currentYearMonth = Carbon::now()->format('Ym');
$chunks = $this->splitDateRangeMonthly($startDateYmd, $endDateYmd);
foreach ($accounts as $acc) {
$accNum = $acc['bankAccountNum'];
foreach ($chunks as $chunk) {
$yearMonth = substr($chunk['start'], 0, 6);
$isCurrentMonth = ($yearMonth === $currentYearMonth);
// 캐시 판단
$syncStatus = BankSyncStatus::where('tenant_id', $tenantId)
->where('bank_account_num', $accNum)
->where('synced_year_month', $yearMonth)
->first();
if ($syncStatus) {
if (! $isCurrentMonth) {
continue; // 과거 월: 항상 캐시
}
if ($syncStatus->synced_at->diffInMinutes(now()) < 10) {
continue; // 현재 월: 10분 이내면 캐시
}
}
// API 호출 필요
$this->fetchAndCache(
$tenantId,
$member->barobill_id,
$accNum,
$acc['bankName'],
$acc['bankCode'],
$chunk['start'],
$chunk['end'],
$yearMonth
);
}
}
}
/**
* BarobillConfig + BarobillMember 설정으로 SOAP 초기화
*/
private function initFromConfig(BarobillMember $member): void
{
$memberTestMode = $member->isTestMode();
$targetEnv = $memberTestMode ? 'test' : 'production';
$config = BarobillConfig::where('environment', $targetEnv)->first();
if (! $config) {
$config = BarobillConfig::where('is_active', true)->first();
}
if ($config) {
$this->isTestMode = $memberTestMode;
$this->certKey = $config->cert_key;
$this->corpNum = $config->corp_num;
$baseUrl = $config->base_url ?: ($memberTestMode
? 'https://testws.baroservice.com'
: 'https://ws.baroservice.com');
$this->soapUrl = $baseUrl.'/BANKACCOUNT.asmx?WSDL';
}
$this->initSoapClient();
}
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_BOTH,
]);
} catch (\Throwable $e) {
Log::error('[BankSync] SOAP 클라이언트 생성 실패: '.$e->getMessage());
}
}
}
/**
* 바로빌 등록 계좌 목록 조회
*/
private function getRegisteredAccounts(): array
{
$result = $this->callSoap('GetBankAccountEx', ['AvailOnly' => 0]);
if (! $result['success']) {
return [];
}
$data = $result['data'];
$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];
}
$accounts = [];
foreach ($accountList as $acc) {
if (! is_object($acc)) {
continue;
}
$bankAccountNum = $acc->BankAccountNum ?? '';
if (empty($bankAccountNum) || (is_numeric($bankAccountNum) && $bankAccountNum < 0)) {
continue;
}
$accounts[] = [
'bankAccountNum' => $bankAccountNum,
'bankCode' => $acc->BankCode ?? '',
'bankName' => $acc->BankName ?? '',
];
}
return $accounts;
}
/**
* 바로빌 API에서 거래내역을 가져와 DB에 캐시
*/
private function fetchAndCache(
int $tenantId,
string $userId,
string $accNum,
string $bankName,
string $bankCode,
string $startDate,
string $endDate,
string $yearMonth
): void {
$result = $this->callSoap('GetPeriodBankAccountTransLog', [
'ID' => $userId,
'BankAccountNum' => $accNum,
'StartDate' => $startDate,
'EndDate' => $endDate,
'TransDirection' => 1,
'CountPerPage' => 1000,
'CurrentPage' => 1,
'OrderDirection' => 2,
]);
if (! $result['success']) {
return;
}
$chunkData = $result['data'];
// 에러 코드 체크
if (is_numeric($chunkData) && $chunkData < 0) {
$errorCode = (int) $chunkData;
// 데이터 없음이어도 sync 상태 기록 (빈 월 반복 호출 방지)
if (in_array($errorCode, [-25005, -25001])) {
$this->updateSyncStatus($tenantId, $accNum, $yearMonth);
}
return;
}
// 로그 추출
$rawLogs = [];
if (isset($chunkData->BankAccountLogList) && isset($chunkData->BankAccountLogList->BankAccountTransLog)) {
$logs = $chunkData->BankAccountLogList->BankAccountTransLog;
$rawLogs = is_array($logs) ? $logs : [$logs];
}
// DB에 캐시 저장
if (! empty($rawLogs)) {
$this->cacheTransactions($tenantId, $accNum, $bankName, $bankCode, $rawLogs);
Log::debug("[BankSync] 캐시 저장 ({$startDate}~{$endDate}): ".count($rawLogs).'건');
}
$this->updateSyncStatus($tenantId, $accNum, $yearMonth);
}
/**
* API 응답을 DB에 배치 저장
*/
private function cacheTransactions(int $tenantId, string $accNum, string $bankName, string $bankCode, array $rawLogs): void
{
$rows = [];
$now = now();
foreach ($rawLogs as $log) {
$transDT = $log->TransDT ?? '';
$transDate = strlen($transDT) >= 8 ? substr($transDT, 0, 8) : '';
$transTime = strlen($transDT) >= 14 ? substr($transDT, 8, 6) : '';
$deposit = floatval($log->Deposit ?? 0);
$withdraw = floatval($log->Withdraw ?? 0);
$balance = floatval($log->Balance ?? 0);
$summary = $log->TransRemark1 ?? $log->Summary ?? '';
$remark2 = $log->TransRemark2 ?? '';
$cleanSummary = BankTransaction::cleanSummary($summary, $remark2);
$rows[] = [
'tenant_id' => $tenantId,
'bank_account_num' => $log->BankAccountNum ?? $accNum,
'bank_code' => $log->BankCode ?? $bankCode,
'bank_name' => $log->BankName ?? $bankName,
'trans_date' => $transDate,
'trans_time' => $transTime,
'trans_dt' => $transDT,
'deposit' => $deposit,
'withdraw' => $withdraw,
'balance' => $balance,
'summary' => $cleanSummary,
'cast' => $remark2,
'memo' => $log->Memo ?? '',
'trans_office' => $log->TransOffice ?? '',
'is_manual' => false,
'created_at' => $now,
'updated_at' => $now,
];
}
foreach (array_chunk($rows, 100) as $batch) {
DB::table('barobill_bank_transactions')->insertOrIgnore($batch);
}
}
private function updateSyncStatus(int $tenantId, string $accNum, string $yearMonth): void
{
BankSyncStatus::updateOrCreate(
[
'tenant_id' => $tenantId,
'bank_account_num' => $accNum,
'synced_year_month' => $yearMonth,
],
['synced_at' => now()]
);
}
/**
* 기간을 월별 청크로 분할
*/
private function splitDateRangeMonthly(string $startDate, string $endDate): array
{
$start = Carbon::createFromFormat('Ymd', $startDate)->startOfDay();
$end = Carbon::createFromFormat('Ymd', $endDate)->endOfDay();
$chunks = [];
$cursor = $start->copy();
while ($cursor->lte($end)) {
$chunkStart = $cursor->copy();
$chunkEnd = $cursor->copy()->endOfMonth()->startOfDay();
if ($chunkEnd->gt($end)) {
$chunkEnd = $end->copy()->startOfDay();
}
$chunks[] = [
'start' => $chunkStart->format('Ymd'),
'end' => $chunkEnd->format('Ymd'),
];
$cursor = $chunkStart->copy()->addMonth()->startOfMonth();
}
return $chunks;
}
private function callSoap(string $method, array $params = []): array
{
if (! $this->soapClient) {
return ['success' => false, 'error' => 'SOAP 클라이언트 미초기화'];
}
if (! isset($params['CERTKEY'])) {
$params['CERTKEY'] = $this->certKey ?? '';
}
if (! isset($params['CorpNum'])) {
$params['CorpNum'] = $this->corpNum;
}
try {
$soapStartTime = microtime(true);
$result = $this->soapClient->$method($params);
$resultProperty = $method.'Result';
$elapsed = round((microtime(true) - $soapStartTime) * 1000);
Log::debug("[BankSync] SOAP {$method} 완료 ({$elapsed}ms)");
if (isset($result->$resultProperty)) {
$resultData = $result->$resultProperty;
if (is_numeric($resultData) && $resultData < 0) {
return [
'success' => false,
'error' => "바로빌 API 오류: {$resultData}",
'data' => $resultData,
];
}
return ['success' => true, 'data' => $resultData];
}
return ['success' => true, 'data' => $result];
} catch (\Throwable $e) {
Log::error('[BankSync] SOAP 오류: '.$e->getMessage());
return ['success' => false, 'error' => $e->getMessage()];
}
}
}