where('tenant_id', $tenantId) ->first(); if (! $member || empty($member->barobill_id)) { return ['success' => false, 'error' => '바로빌 회원 정보 없음']; } $this->soapService->initForMember($member); $accounts = $this->getRegisteredAccounts($member); if (empty($accounts)) { return ['success' => true, 'message' => '등록된 계좌 없음', 'synced' => 0]; } $currentYearMonth = Carbon::now()->format('Ym'); $chunks = $this->splitDateRangeMonthly($startDateYmd, $endDateYmd); $totalSynced = 0; foreach ($accounts as $acc) { $accNum = $acc['bankAccountNum']; foreach ($chunks as $chunk) { $yearMonth = substr($chunk['start'], 0, 6); $isCurrentMonth = ($yearMonth === $currentYearMonth); $syncStatus = BarobillBankSyncStatus::withoutGlobalScopes() ->where('tenant_id', $tenantId) ->where('bank_account_num', $accNum) ->where('synced_year_month', $yearMonth) ->first(); if ($syncStatus) { if (! $isCurrentMonth) { continue; } if ($syncStatus->synced_at && $syncStatus->synced_at->diffInMinutes(now()) < 10) { continue; } } $count = $this->fetchAndCache( $tenantId, $member->barobill_id, $accNum, $acc['bankName'], $acc['bankCode'], $chunk['start'], $chunk['end'], $yearMonth ); $totalSynced += $count; } } return [ 'success' => true, 'synced' => $totalSynced, 'accounts' => count($accounts), ]; } /** * 바로빌 등록 계좌 목록 조회 (SOAP) */ public function getRegisteredAccounts(BarobillMember $member): array { $result = $this->soapService->getBankAccounts($member->biz_no, false); 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에 캐시 */ protected function fetchAndCache( int $tenantId, string $userId, string $accNum, string $bankName, string $bankCode, string $startDate, string $endDate, string $yearMonth ): int { $result = $this->soapService->call('bankaccount', 'GetPeriodBankAccountTransLog', [ 'ID' => $userId, 'BankAccountNum' => $accNum, 'StartDate' => $startDate, 'EndDate' => $endDate, 'TransDirection' => 1, 'CountPerPage' => 1000, 'CurrentPage' => 1, 'OrderDirection' => 2, ]); if (! $result['success']) { $errorCode = $result['error_code'] ?? 0; if (in_array($errorCode, [-25005, -25001])) { $this->updateSyncStatus($tenantId, $accNum, $yearMonth); } return 0; } $chunkData = $result['data']; if (is_numeric($chunkData) && $chunkData < 0) { if (in_array((int) $chunkData, [-25005, -25001])) { $this->updateSyncStatus($tenantId, $accNum, $yearMonth); } return 0; } $rawLogs = []; if (isset($chunkData->BankAccountLogList) && isset($chunkData->BankAccountLogList->BankAccountTransLog)) { $logs = $chunkData->BankAccountLogList->BankAccountTransLog; $rawLogs = is_array($logs) ? $logs : [$logs]; } $count = 0; if (! empty($rawLogs)) { $count = $this->cacheTransactions($tenantId, $accNum, $bankName, $bankCode, $rawLogs); Log::debug("[BankSync] 캐시 저장 ({$startDate}~{$endDate}): {$count}건"); } $this->updateSyncStatus($tenantId, $accNum, $yearMonth); return $count; } /** * API 응답을 DB에 배치 저장 */ protected function cacheTransactions(int $tenantId, string $accNum, string $bankName, string $bankCode, array $rawLogs): int { $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 = $this->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, ]; } $inserted = 0; foreach (array_chunk($rows, 100) as $batch) { $inserted += DB::table('barobill_bank_transactions')->insertOrIgnore($batch); } return $inserted; } protected function updateSyncStatus(int $tenantId, string $accNum, string $yearMonth): void { BarobillBankSyncStatus::withoutGlobalScopes()->updateOrCreate( [ 'tenant_id' => $tenantId, 'bank_account_num' => $accNum, 'synced_year_month' => $yearMonth, ], ['synced_at' => now()] ); } /** * 기간을 월별 청크로 분할 */ protected 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; } /** * 요약 정리 (중복 정보 제거) */ protected function cleanSummary(string $summary, string $remark): string { $summary = trim($summary); $remark = trim($remark); if (! empty($remark) && str_contains($summary, $remark)) { $summary = trim(str_replace($remark, '', $summary)); } return $summary; } }