diff --git a/app/Http/Controllers/Barobill/EcardController.php b/app/Http/Controllers/Barobill/EcardController.php new file mode 100644 index 00000000..3f1692ef --- /dev/null +++ b/app/Http/Controllers/Barobill/EcardController.php @@ -0,0 +1,840 @@ +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, + ]); + } + + /** + * 등록된 카드 목록 조회 (GetCardEx) + */ + public function cards(Request $request): JsonResponse + { + try { + $availOnly = $request->input('availOnly', 1); + + // 현재 테넌트의 바로빌 회원 정보 조회 + $tenantId = session('selected_tenant_id', self::HEADQUARTERS_TENANT_ID); + $barobillMember = BarobillMember::where('tenant_id', $tenantId)->first(); + $userId = $barobillMember?->barobill_id ?? ''; + + $result = $this->callSoap('GetCardEx', [ + 'AvailOnly' => (int)$availOnly + ]); + + if (!$result['success']) { + return response()->json([ + 'success' => false, + 'error' => $result['error'], + 'error_code' => $result['error_code'] ?? null + ]); + } + + $cards = []; + $data = $result['data']; + + // Card 또는 CardEx에서 카드 목록 추출 + $cardList = []; + if (isset($data->Card)) { + $cardList = is_array($data->Card) ? $data->Card : [$data->Card]; + } elseif (isset($data->CardEx)) { + $cardList = is_array($data->CardEx) ? $data->CardEx : [$data->CardEx]; + } + + foreach ($cardList as $card) { + if (!is_object($card)) continue; + + $cardNum = $card->CardNum ?? ''; + if (empty($cardNum) || (is_numeric($cardNum) && $cardNum < 0)) { + continue; + } + + $cards[] = [ + 'cardNum' => $cardNum, + 'cardCompany' => $card->CardCompany ?? '', + 'cardBrand' => $this->getCardCompanyName($card->CardCompany ?? ''), + 'alias' => $card->Alias ?? '', + 'ownerName' => $card->OwnerName ?? '', + 'status' => isset($card->UseState) ? (int)$card->UseState : 1 + ]; + } + + 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() + ]); + } + } + + /** + * 카드 사용내역 조회 (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, [-25005, -25001])) { + return response()->json([ + 'success' => false, + 'error' => $this->getErrorMessage($errorCode), + 'error_code' => $errorCode + ]); + } + + // 데이터가 없는 경우 + if ($errorCode && in_array($errorCode, [-25005, -25001])) { + 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 + { + // 먼저 카드 목록 조회 + $cardResult = $this->callSoap('GetCardEx', ['AvailOnly' => 1]); + + if (!$cardResult['success']) { + return response()->json([ + 'success' => false, + 'error' => $cardResult['error'] + ]); + } + + $cardList = []; + $data = $cardResult['data']; + if (isset($data->Card)) { + $cardList = is_array($data->Card) ? $data->Card : [$data->Card]; + } elseif (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, [-25005, -25001])) { + $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->CardApprovalLogList) && isset($resultData->CardApprovalLogList->CardApprovalLog)) { + $rawLogs = is_array($resultData->CardApprovalLogList->CardApprovalLog) + ? $resultData->CardApprovalLogList->CardApprovalLog + : [$resultData->CardApprovalLogList->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). 사업자번호를 확인해주세요.', + -25001 => '등록된 카드가 없습니다 (-25001).', + -25005 => '조회된 데이터가 없습니다 (-25005).', + -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() + ]; + } + } +} diff --git a/app/Models/Barobill/CardTransaction.php b/app/Models/Barobill/CardTransaction.php new file mode 100644 index 00000000..bd4da981 --- /dev/null +++ b/app/Models/Barobill/CardTransaction.php @@ -0,0 +1,99 @@ + 'decimal:2', + 'tax' => 'decimal:2', + 'service_charge' => 'decimal:2', + ]; + + /** + * 테넌트 관계 + */ + public function tenant(): BelongsTo + { + return $this->belongsTo(Tenant::class); + } + + /** + * 거래 고유 키 생성 (매칭용) + */ + public function getUniqueKeyAttribute(): string + { + return implode('|', [ + $this->card_num, + $this->use_dt, + $this->approval_num, + (int) $this->approval_amount, + ]); + } + + /** + * 바로빌 로그 데이터로부터 고유 키 생성 (정적 메서드) + */ + public static function generateUniqueKey(array $log): string + { + return implode('|', [ + $log['cardNum'] ?? '', + $log['useDt'] ?? '', + $log['approvalNum'] ?? '', + (int) ($log['approvalAmount'] ?? 0), + ]); + } + + /** + * 테넌트별 거래 내역 조회 (기간별) + */ + public static function getByDateRange(int $tenantId, string $startDate, string $endDate, ?string $cardNum = null) + { + $query = self::where('tenant_id', $tenantId) + ->whereBetween('use_date', [$startDate, $endDate]) + ->orderBy('use_date', 'desc') + ->orderBy('use_time', 'desc'); + + if ($cardNum) { + $query->where('card_num', $cardNum); + } + + return $query->get()->keyBy(fn($item) => $item->unique_key); + } +} diff --git a/database/migrations/2026_01_23_150000_create_barobill_card_transactions_table.php b/database/migrations/2026_01_23_150000_create_barobill_card_transactions_table.php new file mode 100644 index 00000000..4993d4ac --- /dev/null +++ b/database/migrations/2026_01_23_150000_create_barobill_card_transactions_table.php @@ -0,0 +1,61 @@ +id(); + $table->foreignId('tenant_id')->constrained()->onDelete('cascade'); + $table->string('card_num', 50)->comment('카드번호'); + $table->string('card_company', 10)->nullable()->comment('카드사 코드'); + $table->string('card_company_name', 50)->nullable()->comment('카드사명'); + $table->string('use_dt', 20)->comment('사용일시 원본 (YYYYMMDDHHMMSS)'); + $table->string('use_date', 8)->comment('사용일 (YYYYMMDD)'); + $table->string('use_time', 6)->nullable()->comment('사용시간 (HHMMSS)'); + $table->string('approval_num', 50)->nullable()->comment('승인번호'); + $table->string('approval_type', 10)->nullable()->comment('승인유형 (1=승인, 2=취소)'); + $table->decimal('approval_amount', 18, 2)->default(0)->comment('승인금액'); + $table->decimal('tax', 18, 2)->default(0)->comment('부가세'); + $table->decimal('service_charge', 18, 2)->default(0)->comment('봉사료'); + $table->string('payment_plan', 10)->nullable()->comment('할부개월수'); + $table->string('currency_code', 10)->default('KRW')->comment('통화코드'); + $table->string('merchant_name', 255)->nullable()->comment('가맹점명'); + $table->string('merchant_biz_num', 20)->nullable()->comment('가맹점 사업자번호'); + $table->string('merchant_addr', 255)->nullable()->comment('가맹점 주소'); + $table->string('merchant_ceo', 100)->nullable()->comment('가맹점 대표자'); + $table->string('merchant_biz_type', 100)->nullable()->comment('가맹점 업종'); + $table->string('merchant_tel', 50)->nullable()->comment('가맹점 전화번호'); + $table->string('memo', 255)->nullable()->comment('메모'); + $table->string('use_key', 100)->nullable()->comment('사용키'); + $table->string('account_code', 50)->nullable()->comment('계정과목 코드'); + $table->string('account_name', 100)->nullable()->comment('계정과목 명'); + $table->timestamps(); + + // 복합 유니크 인덱스: 같은 거래는 중복 저장 방지 + $table->unique( + ['tenant_id', 'card_num', 'use_dt', 'approval_num', 'approval_amount'], + 'bb_card_trans_unique' + ); + + // 조회용 인덱스 + $table->index(['tenant_id', 'use_date'], 'bb_card_trans_tenant_date_idx'); + $table->index(['tenant_id', 'card_num', 'use_date'], 'bb_card_trans_tenant_card_date_idx'); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('barobill_card_transactions'); + } +}; diff --git a/database/seeders/MngMenuSeeder.php b/database/seeders/MngMenuSeeder.php index fdd2aaaa..c34699f3 100644 --- a/database/seeders/MngMenuSeeder.php +++ b/database/seeders/MngMenuSeeder.php @@ -592,11 +592,11 @@ protected function seedMainMenus(): void ]); $this->createMenu([ 'parent_id' => $barobillGroup->id, - 'name' => '카드사용내역', - 'url' => '/barobill/card-usage', + 'name' => '카드 사용내역', + 'url' => '/barobill/ecard', 'icon' => 'credit-card', 'sort_order' => $barobillSubOrder++, - 'options' => ['route_name' => 'barobill.card-usage.index', 'section' => 'main'], + 'options' => ['route_name' => 'barobill.ecard.index', 'section' => 'main'], ]); $this->createMenu([ 'parent_id' => $barobillGroup->id, diff --git a/resources/views/barobill/ecard/index.blade.php b/resources/views/barobill/ecard/index.blade.php new file mode 100644 index 00000000..0b60fc0e --- /dev/null +++ b/resources/views/barobill/ecard/index.blade.php @@ -0,0 +1,765 @@ +@extends('layouts.app') + +@section('title', '카드 사용내역') + +@section('content') + +@if($currentTenant) +
+
+
+
+ + + +
+
+
+ T-ID: {{ $currentTenant->id }} + @if($currentTenant->id == 1) + 파트너사 + @endif +
+

{{ $currentTenant->company_name }}

+
+
+ @if($barobillMember) +
+
+

사업자번호

+

{{ $barobillMember->biz_no }}

+
+
+

대표자

+

{{ $barobillMember->ceo_name ?? '-' }}

+
+
+

담당자

+

{{ $barobillMember->manager_name ?? '-' }}

+
+
+

바로빌 ID

+

{{ $barobillMember->barobill_id }}

+
+
+ @else +
+ + + + 바로빌 회원사 미연동 +
+ @endif +
+
+@endif + +
+@endsection + +@push('scripts') + + + + + +@endpush diff --git a/routes/web.php b/routes/web.php index db3211cb..8b04165b 100644 --- a/routes/web.php +++ b/routes/web.php @@ -300,6 +300,16 @@ Route::post('/save', [\App\Http\Controllers\Barobill\EaccountController::class, 'save'])->name('save'); Route::get('/export', [\App\Http\Controllers\Barobill\EaccountController::class, 'exportExcel'])->name('export'); }); + + // 카드 사용내역 (React 페이지) + Route::prefix('ecard')->name('ecard.')->group(function () { + Route::get('/', [\App\Http\Controllers\Barobill\EcardController::class, 'index'])->name('index'); + Route::get('/cards', [\App\Http\Controllers\Barobill\EcardController::class, 'cards'])->name('cards'); + Route::get('/transactions', [\App\Http\Controllers\Barobill\EcardController::class, 'transactions'])->name('transactions'); + Route::get('/account-codes', [\App\Http\Controllers\Barobill\EcardController::class, 'accountCodes'])->name('account-codes'); + Route::post('/save', [\App\Http\Controllers\Barobill\EcardController::class, 'save'])->name('save'); + Route::get('/export', [\App\Http\Controllers\Barobill\EcardController::class, 'exportExcel'])->name('export'); + }); }); /*