diff --git a/app/Http/Controllers/Finance/DailyFundController.php b/app/Http/Controllers/Finance/DailyFundController.php index d76e02c0..060a02c5 100644 --- a/app/Http/Controllers/Finance/DailyFundController.php +++ b/app/Http/Controllers/Finance/DailyFundController.php @@ -7,6 +7,7 @@ use App\Models\Barobill\BankTransactionOverride; use App\Models\Finance\DailyFundMemo; use App\Models\Finance\DailyFundTransaction; +use App\Services\Barobill\BarobillBankSyncService; use Illuminate\Http\JsonResponse; use Illuminate\Http\Request; @@ -173,6 +174,13 @@ public function periodReport(Request $request): JsonResponse $startDateYmd = str_replace('-', '', $startDate); $endDateYmd = str_replace('-', '', $endDate); + // 바로빌 데이터 자동 동기화 (캐시가 오래되었으면 API에서 갱신) + try { + app(BarobillBankSyncService::class)->syncIfNeeded($tenantId, $startDateYmd, $endDateYmd); + } catch (\Throwable $e) { + \Illuminate\Support\Facades\Log::warning('[DailyFund] 바로빌 동기화 실패 (DB 캐시로 계속): '.$e->getMessage()); + } + // 기간 내 거래내역 조회 (중복 제거: balance 포함 고유키로 동일 거래만 제거) $transactions = BarobillBankTransaction::where('tenant_id', $tenantId) ->whereBetween('trans_date', [$startDateYmd, $endDateYmd]) diff --git a/app/Services/Barobill/BarobillBankSyncService.php b/app/Services/Barobill/BarobillBankSyncService.php new file mode 100644 index 00000000..97039f07 --- /dev/null +++ b/app/Services/Barobill/BarobillBankSyncService.php @@ -0,0 +1,375 @@ +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()]; + } + } +}