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()]; } } }