first(); if ($activeConfig) { $this->certKey = $activeConfig->cert_key; $this->corpNum = $activeConfig->corp_num; $this->isTestMode = $activeConfig->environment === 'test'; // 카드 조회는 CARD.asmx 사용 $baseUrl = $this->isTestMode ? 'https://testws.baroservice.com' : 'https://ws.baroservice.com'; $this->soapUrl = $baseUrl . '/CARD.asmx?WSDL'; } else { $this->isTestMode = config('services.barobill.test_mode', true); $this->certKey = $this->isTestMode ? config('services.barobill.cert_key_test', '') : config('services.barobill.cert_key_prod', ''); $this->corpNum = config('services.barobill.corp_num', ''); $this->soapUrl = $this->isTestMode ? 'https://testws.baroservice.com/CARD.asmx?WSDL' : 'https://ws.baroservice.com/CARD.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.ecard.index')); } // 현재 선택된 테넌트 정보 $tenantId = session('selected_tenant_id', self::HEADQUARTERS_TENANT_ID); $currentTenant = Tenant::find($tenantId); // 해당 테넌트의 바로빌 회원사 정보 $barobillMember = BarobillMember::where('tenant_id', $tenantId)->first(); return view('barobill.ecard.index', [ 'certKey' => $this->certKey, 'corpNum' => $this->corpNum, 'isTestMode' => $this->isTestMode, 'hasSoapClient' => $this->soapClient !== null, 'currentTenant' => $currentTenant, 'barobillMember' => $barobillMember, ]); } /** * 등록된 카드 목록 조회 (GetCardEx2) * 레퍼런스: https://dev.barobill.co.kr/docs/references/카드조회-API#GetCardEx2 */ public function cards(Request $request): JsonResponse { try { $availOnly = $request->input('availOnly', 0); $result = $this->callSoap('GetCardEx2', [ 'AvailOnly' => (int)$availOnly ]); if (!$result['success']) { return response()->json([ 'success' => false, 'error' => $result['error'], 'error_code' => $result['error_code'] ?? null ]); } $cards = []; $data = $result['data']; // GetCardEx2는 CardEx 배열을 반환 $cardList = []; if (isset($data->CardEx)) { $cardList = is_array($data->CardEx) ? $data->CardEx : [$data->CardEx]; } foreach ($cardList as $card) { if (!is_object($card)) continue; $cardNum = $card->CardNum ?? ''; // 에러 체크: CardNum이 음수면 에러 코드 if (empty($cardNum) || (is_numeric($cardNum) && $cardNum < 0)) { continue; } $cardCompanyCode = $card->CardCompanyCode ?? $card->CardCompany ?? ''; $cardCompanyName = !empty($card->CardCompanyName) ? $card->CardCompanyName : $this->getCardCompanyName($cardCompanyCode); $cards[] = [ 'cardNum' => $cardNum, 'cardCompany' => $cardCompanyCode, 'cardBrand' => $cardCompanyName, 'alias' => $card->Alias ?? '', 'cardType' => $card->CardType ?? '', 'cardTypeName' => ($card->CardType ?? '') === '2' ? '법인카드' : '개인카드', 'status' => $card->Status ?? '', 'statusName' => $this->getCardStatusName($card->Status ?? ''), 'webId' => $card->WebId ?? '', ]; } return response()->json([ 'success' => true, 'cards' => $cards, 'count' => count($cards) ]); } catch (\Throwable $e) { Log::error('카드 목록 조회 오류: ' . $e->getMessage()); return response()->json([ 'success' => false, 'error' => '서버 오류: ' . $e->getMessage() ]); } } /** * 카드 상태 코드 -> 이름 변환 */ private function getCardStatusName(string $status): string { $statuses = [ '0' => '대기중', '1' => '정상', '2' => '해지', '3' => '수집오류', '4' => '일시중지' ]; return $statuses[$status] ?? $status; } /** * 카드 사용내역 조회 (GetPeriodCardApprovalLog) */ public function transactions(Request $request): JsonResponse { try { $startDate = $request->input('startDate', date('Ymd')); $endDate = $request->input('endDate', date('Ymd')); $cardNum = str_replace('-', '', $request->input('cardNum', '')); $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 ?? ''; // DB에서 저장된 계정과목 데이터 조회 $savedData = CardTransaction::getByDateRange($tenantId, $startDate, $endDate, $cardNum ?: null); // 전체 카드 조회: 빈 값이면 모든 카드의 사용 내역 조회 if (empty($cardNum)) { return $this->getAllCardsTransactions($userId, $startDate, $endDate, $page, $limit, $savedData); } // 단일 카드 조회 $result = $this->callSoap('GetPeriodCardApprovalLog', [ 'ID' => $userId, 'CardNum' => $cardNum, 'StartDate' => $startDate, 'EndDate' => $endDate, '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, [-24005, -24001])) { return response()->json([ 'success' => false, 'error' => $this->getErrorMessage($errorCode), 'error_code' => $errorCode ]); } // 데이터가 없는 경우 if ($errorCode && in_array($errorCode, [-24005, -24001])) { return response()->json([ 'success' => true, 'data' => [ 'logs' => [], 'summary' => ['totalAmount' => 0, 'count' => 0, 'approvalCount' => 0, 'cancelCount' => 0], 'pagination' => ['currentPage' => 1, 'maxPageNum' => 1] ] ]); } // 데이터 파싱 (저장된 계정과목 병합) $logs = $this->parseTransactionLogs($resultData, $savedData); 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 getAllCardsTransactions(string $userId, string $startDate, string $endDate, int $page, int $limit, $savedData = null): JsonResponse { // 먼저 카드 목록 조회 (GetCardEx2 사용) $cardResult = $this->callSoap('GetCardEx2', ['AvailOnly' => 0]); if (!$cardResult['success']) { return response()->json([ 'success' => false, 'error' => $cardResult['error'] ]); } $cardList = []; $data = $cardResult['data']; if (isset($data->CardEx)) { $cardList = is_array($data->CardEx) ? $data->CardEx : [$data->CardEx]; } $allLogs = []; $totalAmount = 0; $approvalCount = 0; $cancelCount = 0; foreach ($cardList as $card) { if (!is_object($card)) continue; $cardNum = $card->CardNum ?? ''; if (empty($cardNum) || (is_numeric($cardNum) && $cardNum < 0)) continue; $cardResult = $this->callSoap('GetPeriodCardApprovalLog', [ 'ID' => $userId, 'CardNum' => $cardNum, 'StartDate' => $startDate, 'EndDate' => $endDate, 'CountPerPage' => 1000, 'CurrentPage' => 1, 'OrderDirection' => 2 ]); if ($cardResult['success']) { $cardData = $cardResult['data']; $errorCode = $this->checkErrorCode($cardData); if (!$errorCode || in_array($errorCode, [-24005, -24001])) { $parsed = $this->parseTransactionLogs($cardData, $savedData); foreach ($parsed['logs'] as $log) { $log['cardBrand'] = $this->getCardCompanyName($card->CardCompany ?? ''); $allLogs[] = $log; } $totalAmount += $parsed['summary']['totalAmount']; $approvalCount += $parsed['summary']['approvalCount']; $cancelCount += $parsed['summary']['cancelCount']; } } } // 날짜/시간 기준 정렬 (최신순) usort($allLogs, function ($a, $b) { return strcmp($b['useDt'] ?? '', $a['useDt'] ?? ''); }); // 페이지네이션 $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' => [ 'totalAmount' => $totalAmount, 'count' => $totalCount, 'approvalCount' => $approvalCount, 'cancelCount' => $cancelCount ] ] ]); } /** * 거래 내역 파싱 (저장된 계정과목 병합) */ private function parseTransactionLogs($resultData, $savedData = null): array { $logs = []; $totalAmount = 0; $approvalCount = 0; $cancelCount = 0; $rawLogs = []; if (isset($resultData->CardLogList) && isset($resultData->CardLogList->CardApprovalLog)) { $rawLogs = is_array($resultData->CardLogList->CardApprovalLog) ? $resultData->CardLogList->CardApprovalLog : [$resultData->CardLogList->CardApprovalLog]; } foreach ($rawLogs as $log) { $amount = floatval($log->ApprovalAmount ?? 0); $approvalType = $log->ApprovalType ?? '1'; if ($approvalType === '1') { $totalAmount += $amount; $approvalCount++; } else { $cancelCount++; } // 사용일시 파싱 $useDT = $log->UseDT ?? ''; $useDate = ''; $useTime = ''; $dateTime = ''; if (!empty($useDT) && strlen($useDT) >= 8) { $useDate = substr($useDT, 0, 8); if (strlen($useDT) >= 14) { $useTime = substr($useDT, 8, 6); $dateTime = substr($useDT, 0, 4) . '-' . substr($useDT, 4, 2) . '-' . substr($useDT, 6, 2) . ' ' . substr($useDT, 8, 2) . ':' . substr($useDT, 10, 2) . ':' . substr($useDT, 12, 2); } else { $dateTime = substr($useDT, 0, 4) . '-' . substr($useDT, 4, 2) . '-' . substr($useDT, 6, 2); } } $cardNum = $log->CardNum ?? ''; $approvalNum = $log->ApprovalNum ?? ''; // 고유 키 생성하여 저장된 데이터와 매칭 $uniqueKey = implode('|', [$cardNum, $useDT, $approvalNum, (int) $amount]); $savedItem = $savedData?->get($uniqueKey); $logItem = [ 'cardNum' => $cardNum, 'cardNumMasked' => $this->maskCardNumber($cardNum), 'cardCompany' => $log->CardCompany ?? '', 'cardBrand' => $this->getCardCompanyName($log->CardCompany ?? ''), 'useDt' => $useDT, 'useDate' => $useDate, 'useTime' => $useTime, 'useDateTime' => $dateTime, 'approvalNum' => $approvalNum, 'approvalType' => $approvalType, 'approvalTypeName' => $approvalType === '1' ? '승인' : '취소', 'approvalAmount' => $amount, 'approvalAmountFormatted' => number_format($amount), 'tax' => floatval($log->Tax ?? 0), 'taxFormatted' => number_format(floatval($log->Tax ?? 0)), 'serviceCharge' => floatval($log->ServiceCharge ?? 0), 'paymentPlan' => $log->PaymentPlan ?? '0', 'paymentPlanName' => $this->getPaymentPlanName($log->PaymentPlan ?? '0'), 'currencyCode' => $log->CurrencyCode ?? 'KRW', 'merchantName' => $log->UseStoreName ?? '', 'merchantBizNum' => $log->UseStoreCorpNum ?? '', 'merchantAddr' => $log->UseStoreAddr ?? '', 'merchantCeo' => $log->UseStoreCeo ?? '', 'merchantBizType' => $log->UseStoreBizType ?? '', 'merchantTel' => $log->UseStoreTel ?? '', 'memo' => $log->Memo ?? '', 'useKey' => $log->UseKey ?? '', // 저장된 계정과목 정보 병합 'accountCode' => $savedItem?->account_code ?? '', 'accountName' => $savedItem?->account_name ?? '', 'isSaved' => $savedItem !== null, ]; $logs[] = $logItem; } return [ 'logs' => $logs, 'summary' => [ 'totalAmount' => $totalAmount, 'count' => count($logs), 'approvalCount' => $approvalCount, 'cancelCount' => $cancelCount ] ]; } /** * 에러 코드 체크 */ private function checkErrorCode($data): ?int { if (isset($data->CurrentPage) && is_numeric($data->CurrentPage) && $data->CurrentPage < 0) { return (int)$data->CurrentPage; } if (isset($data->CardNum) && is_numeric($data->CardNum) && $data->CardNum < 0) { return (int)$data->CardNum; } return null; } /** * 에러 메시지 반환 */ private function getErrorMessage(int $errorCode): string { $messages = [ -10002 => '인증 실패 (-10002). CERTKEY가 올바르지 않거나 만료되었습니다.', -24005 => '사용자 정보 불일치 (-24005). 사업자번호를 확인해주세요.', -24001 => '등록된 카드가 없습니다 (-24001).', -24005 => '조회된 데이터가 없습니다 (-24005).', -25006 => '카드번호가 잘못되었습니다 (-25006).', -25007 => '조회 기간이 잘못되었습니다 (-25007).', ]; return $messages[$errorCode] ?? '바로빌 API 오류: ' . $errorCode; } /** * 카드번호 마스킹 */ private function maskCardNumber(string $cardNum): string { if (strlen($cardNum) < 8) return $cardNum; return substr($cardNum, 0, 4) . '-****-****-' . substr($cardNum, -4); } /** * 카드사 코드 -> 카드사명 변환 */ private function getCardCompanyName(string $code): string { $companies = [ '01' => '비씨', '02' => 'KB국민', '03' => '하나(외환)', '04' => '삼성', '06' => '신한', '07' => '현대', '08' => '롯데', '11' => 'NH농협', '12' => '수협', '13' => '씨티', '14' => '우리', '15' => '광주', '16' => '전북', '21' => '하나', '22' => '제주', '23' => 'SC제일', '25' => 'KDB산업', '26' => 'IBK기업', '27' => '새마을금고', '28' => '신협', '29' => '저축은행', '30' => '우체국', '31' => '카카오뱅크', '32' => 'K뱅크', '33' => '토스뱅크' ]; return $companies[$code] ?? $code; } /** * 할부 이름 반환 */ private function getPaymentPlanName(string $plan): string { if (empty($plan) || $plan === '0' || $plan === '00') { return '일시불'; } return $plan . '개월'; } /** * 계정과목 목록 조회 */ public function accountCodes(): JsonResponse { $tenantId = session('selected_tenant_id', self::HEADQUARTERS_TENANT_ID); $codes = AccountCode::getActiveByTenant($tenantId); return response()->json([ 'success' => true, 'data' => $codes->map(fn($c) => [ 'id' => $c->id, 'code' => $c->code, 'name' => $c->name, 'category' => $c->category, ]) ]); } /** * 카드 사용내역 저장 (계정과목 포함) */ public function save(Request $request): JsonResponse { try { $tenantId = session('selected_tenant_id', self::HEADQUARTERS_TENANT_ID); $transactions = $request->input('transactions', []); if (empty($transactions)) { return response()->json([ 'success' => false, 'error' => '저장할 데이터가 없습니다.' ]); } $saved = 0; $updated = 0; DB::beginTransaction(); foreach ($transactions as $trans) { $data = [ 'tenant_id' => $tenantId, 'card_num' => $trans['cardNum'] ?? '', 'card_company' => $trans['cardCompany'] ?? '', 'card_company_name' => $trans['cardBrand'] ?? '', 'use_dt' => $trans['useDt'] ?? '', 'use_date' => $trans['useDate'] ?? '', 'use_time' => $trans['useTime'] ?? '', 'approval_num' => $trans['approvalNum'] ?? '', 'approval_type' => $trans['approvalType'] ?? '', 'approval_amount' => floatval($trans['approvalAmount'] ?? 0), 'tax' => floatval($trans['tax'] ?? 0), 'service_charge' => floatval($trans['serviceCharge'] ?? 0), 'payment_plan' => $trans['paymentPlan'] ?? '', 'currency_code' => $trans['currencyCode'] ?? 'KRW', 'merchant_name' => $trans['merchantName'] ?? '', 'merchant_biz_num' => $trans['merchantBizNum'] ?? '', 'merchant_addr' => $trans['merchantAddr'] ?? '', 'merchant_ceo' => $trans['merchantCeo'] ?? '', 'merchant_biz_type' => $trans['merchantBizType'] ?? '', 'merchant_tel' => $trans['merchantTel'] ?? '', 'memo' => $trans['memo'] ?? '', 'use_key' => $trans['useKey'] ?? '', 'account_code' => $trans['accountCode'] ?? null, 'account_name' => $trans['accountName'] ?? null, ]; // Upsert: 있으면 업데이트, 없으면 생성 $existing = CardTransaction::where('tenant_id', $tenantId) ->where('card_num', $data['card_num']) ->where('use_dt', $data['use_dt']) ->where('approval_num', $data['approval_num']) ->where('approval_amount', $data['approval_amount']) ->first(); if ($existing) { // 계정과목만 업데이트 $existing->update([ 'account_code' => $data['account_code'], 'account_name' => $data['account_name'], ]); $updated++; } else { CardTransaction::create($data); $saved++; } } DB::commit(); return response()->json([ 'success' => true, 'message' => "저장 완료: 신규 {$saved}건, 수정 {$updated}건", 'saved' => $saved, 'updated' => $updated ]); } catch (\Throwable $e) { DB::rollBack(); Log::error('카드 사용내역 저장 오류: ' . $e->getMessage()); return response()->json([ 'success' => false, 'error' => '저장 오류: ' . $e->getMessage() ]); } } /** * 엑셀 다운로드 */ public function exportExcel(Request $request): StreamedResponse|JsonResponse { try { $tenantId = session('selected_tenant_id', self::HEADQUARTERS_TENANT_ID); $startDate = $request->input('startDate', date('Ymd')); $endDate = $request->input('endDate', date('Ymd')); $cardNum = $request->input('cardNum', ''); // DB에서 저장된 데이터 조회 $query = CardTransaction::where('tenant_id', $tenantId) ->whereBetween('use_date', [$startDate, $endDate]) ->orderBy('use_date', 'desc') ->orderBy('use_time', 'desc'); if (!empty($cardNum)) { $query->where('card_num', $cardNum); } $transactions = $query->get(); // 데이터가 없으면 안내 if ($transactions->isEmpty()) { return response()->json([ 'success' => false, 'error' => '저장된 데이터가 없습니다. 먼저 데이터를 조회하고 저장해주세요.' ]); } $filename = "카드사용내역_{$startDate}_{$endDate}.csv"; return response()->streamDownload(function () use ($transactions) { $handle = fopen('php://output', 'w'); // UTF-8 BOM for Excel fprintf($handle, chr(0xEF) . chr(0xBB) . chr(0xBF)); // 헤더 fputcsv($handle, [ '사용일시', '카드번호', '카드사', '가맹점명', '가맹점사업자번호', '승인금액', '부가세', '할부', '구분', '승인번호', '계정과목코드', '계정과목명' ]); // 데이터 foreach ($transactions as $trans) { $dateTime = ''; if ($trans->use_date) { $dateTime = substr($trans->use_date, 0, 4) . '-' . substr($trans->use_date, 4, 2) . '-' . substr($trans->use_date, 6, 2); if ($trans->use_time) { $dateTime .= ' ' . substr($trans->use_time, 0, 2) . ':' . substr($trans->use_time, 2, 2) . ':' . substr($trans->use_time, 4, 2); } } fputcsv($handle, [ $dateTime, $trans->card_num, $trans->card_company_name, $trans->merchant_name, $trans->merchant_biz_num, $trans->approval_amount, $trans->tax, $this->getPaymentPlanName($trans->payment_plan), $trans->approval_type === '1' ? '승인' : '취소', $trans->approval_num, $trans->account_code, $trans->account_name ]); } fclose($handle); }, $filename, [ 'Content-Type' => 'text/csv; charset=utf-8', 'Content-Disposition' => 'attachment; filename="' . $filename . '"', ]); } catch (\Throwable $e) { Log::error('엑셀 다운로드 오류: ' . $e->getMessage()); return response()->json([ 'success' => false, 'error' => '다운로드 오류: ' . $e->getMessage() ]); } } /** * 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() ]; } } }