first(); if ($activeConfig) { $this->certKey = $activeConfig->cert_key; $this->corpNum = $activeConfig->corp_num; $this->isTestMode = $activeConfig->environment === 'test'; // 홈택스 조회는 TI.asmx 사용 (세금계산서 서비스에 포함) $baseUrl = $this->isTestMode ? 'https://testws.baroservice.com' : 'https://ws.baroservice.com'; $this->soapUrl = $baseUrl . '/TI.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/TI.asmx?WSDL' : 'https://ws.baroservice.com/TI.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.hometax.index')); } // 현재 선택된 테넌트 정보 $tenantId = session('selected_tenant_id', self::HEADQUARTERS_TENANT_ID); $currentTenant = Tenant::find($tenantId); // 해당 테넌트의 바로빌 회원사 정보 $barobillMember = BarobillMember::where('tenant_id', $tenantId)->first(); return view('barobill.hometax.index', [ 'certKey' => $this->certKey, 'corpNum' => $this->corpNum, 'isTestMode' => $this->isTestMode, 'hasSoapClient' => $this->soapClient !== null, 'currentTenant' => $currentTenant, 'barobillMember' => $barobillMember, ]); } /** * 매출 세금계산서 목록 조회 (GetTaxInvoiceSalesListEx) */ public function sales(Request $request): JsonResponse { try { $startDate = $request->input('startDate', date('Ymd', strtotime('-1 month'))); $endDate = $request->input('endDate', date('Ymd')); $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 ?? ''; $result = $this->callSoap('GetTaxInvoiceSalesListEx', [ 'ID' => $userId, '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, [-60005, -60001])) { return response()->json([ 'success' => false, 'error' => $this->getErrorMessage($errorCode), 'error_code' => $errorCode ]); } // 데이터가 없는 경우 if ($errorCode && in_array($errorCode, [-60005, -60001])) { return response()->json([ 'success' => true, 'data' => [ 'invoices' => [], 'summary' => ['totalAmount' => 0, 'totalTax' => 0, 'count' => 0], 'pagination' => ['currentPage' => 1, 'maxPageNum' => 1] ] ]); } // 데이터 파싱 $parsed = $this->parseInvoices($resultData, 'sales'); return response()->json([ 'success' => true, 'data' => [ 'invoices' => $parsed['invoices'], 'pagination' => [ 'currentPage' => $resultData->CurrentPage ?? 1, 'countPerPage' => $resultData->CountPerPage ?? 50, 'maxPageNum' => $resultData->MaxPageNum ?? 1, 'maxIndex' => $resultData->MaxIndex ?? 0 ], 'summary' => $parsed['summary'] ] ]); } catch (\Throwable $e) { Log::error('홈택스 매출 조회 오류: ' . $e->getMessage()); return response()->json([ 'success' => false, 'error' => '서버 오류: ' . $e->getMessage() ]); } } /** * 매입 세금계산서 목록 조회 (GetTaxInvoicePurchaseListEx) */ public function purchases(Request $request): JsonResponse { try { $startDate = $request->input('startDate', date('Ymd', strtotime('-1 month'))); $endDate = $request->input('endDate', date('Ymd')); $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 ?? ''; $result = $this->callSoap('GetTaxInvoicePurchaseListEx', [ 'ID' => $userId, '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, [-60005, -60001])) { return response()->json([ 'success' => false, 'error' => $this->getErrorMessage($errorCode), 'error_code' => $errorCode ]); } // 데이터가 없는 경우 if ($errorCode && in_array($errorCode, [-60005, -60001])) { return response()->json([ 'success' => true, 'data' => [ 'invoices' => [], 'summary' => ['totalAmount' => 0, 'totalTax' => 0, 'count' => 0], 'pagination' => ['currentPage' => 1, 'maxPageNum' => 1] ] ]); } // 데이터 파싱 $parsed = $this->parseInvoices($resultData, 'purchase'); return response()->json([ 'success' => true, 'data' => [ 'invoices' => $parsed['invoices'], 'pagination' => [ 'currentPage' => $resultData->CurrentPage ?? 1, 'countPerPage' => $resultData->CountPerPage ?? 50, 'maxPageNum' => $resultData->MaxPageNum ?? 1, 'maxIndex' => $resultData->MaxIndex ?? 0 ], 'summary' => $parsed['summary'] ] ]); } catch (\Throwable $e) { Log::error('홈택스 매입 조회 오류: ' . $e->getMessage()); return response()->json([ 'success' => false, 'error' => '서버 오류: ' . $e->getMessage() ]); } } /** * 홈택스 수집 요청 * * 참고: 홈택스 수집 API는 별도 서비스 구독이 필요할 수 있습니다. * 현재는 바로빌에 등록된 세금계산서만 조회합니다. */ public function requestCollect(Request $request): JsonResponse { // 홈택스 수집 API는 별도 구독 필요 - 현재 미지원 안내 return response()->json([ 'success' => false, 'error' => '홈택스 수집 기능은 별도 서비스 구독이 필요합니다. 바로빌에 등록된 세금계산서는 매출/매입 탭에서 조회 가능합니다.' ]); } /** * 수집 상태 확인 * * 참고: 홈택스 수집 상태 API는 별도 서비스 구독이 필요할 수 있습니다. */ public function collectStatus(Request $request): JsonResponse { // 홈택스 수집 상태 API는 별도 구독 필요 - 현재 미지원 안내 return response()->json([ 'success' => true, 'data' => [ 'salesLastCollectDate' => '', 'purchaseLastCollectDate' => '', 'isCollecting' => false, 'collectStateText' => '미지원', 'message' => '홈택스 수집 기능은 별도 서비스 구독이 필요합니다.' ] ]); } /** * 세금계산서 파싱 */ private function parseInvoices($resultData, string $type = 'sales'): array { $invoices = []; $totalAmount = 0; $totalTax = 0; $rawList = []; // TI.asmx 응답 구조: TaxInvoiceSalesList / TaxInvoicePurchaseList $listProperty = $type === 'sales' ? 'TaxInvoiceSalesList' : 'TaxInvoicePurchaseList'; $itemProperty = $type === 'sales' ? 'TaxInvoiceSales' : 'TaxInvoicePurchase'; if (isset($resultData->$listProperty) && isset($resultData->$listProperty->$itemProperty)) { $rawList = is_array($resultData->$listProperty->$itemProperty) ? $resultData->$listProperty->$itemProperty : [$resultData->$listProperty->$itemProperty]; } foreach ($rawList as $item) { $supplyAmount = floatval($item->SupplyAmount ?? 0); $taxAmount = floatval($item->TaxAmount ?? 0); $totalAmount += $supplyAmount; $totalTax += $taxAmount; // 날짜 포맷팅 $writeDate = $item->WriteDate ?? ''; $formattedDate = ''; if (!empty($writeDate) && strlen($writeDate) >= 8) { $formattedDate = substr($writeDate, 0, 4) . '-' . substr($writeDate, 4, 2) . '-' . substr($writeDate, 6, 2); } $invoices[] = [ 'ntsConfirmNum' => $item->NTSConfirmNum ?? '', // 국세청 승인번호 'writeDate' => $writeDate, 'writeDateFormatted' => $formattedDate, 'invoicerCorpNum' => $item->InvoicerCorpNum ?? '', // 공급자 사업자번호 'invoicerCorpName' => $item->InvoicerCorpName ?? '', // 공급자 상호 'invoiceeCorpNum' => $item->InvoiceeCorpNum ?? '', // 공급받는자 사업자번호 'invoiceeCorpName' => $item->InvoiceeCorpName ?? '', // 공급받는자 상호 'supplyAmount' => $supplyAmount, 'supplyAmountFormatted' => number_format($supplyAmount), 'taxAmount' => $taxAmount, 'taxAmountFormatted' => number_format($taxAmount), 'totalAmount' => $supplyAmount + $taxAmount, 'totalAmountFormatted' => number_format($supplyAmount + $taxAmount), 'taxType' => $item->TaxType ?? '', // 과세유형 'taxTypeName' => $this->getTaxTypeName($item->TaxType ?? ''), 'issueType' => $item->IssueType ?? '', // 발급유형 'issueTypeName' => $this->getIssueTypeName($item->IssueType ?? ''), 'purposeType' => $item->PurposeType ?? '', // 영수/청구 'purposeTypeName' => $this->getPurposeTypeName($item->PurposeType ?? ''), 'modifyCode' => $item->ModifyCode ?? '', 'remark' => $item->Remark1 ?? '', 'collectDate' => $item->CollectDate ?? '', // 수집일 ]; } return [ 'invoices' => $invoices, 'summary' => [ 'totalAmount' => $totalAmount, 'totalTax' => $totalTax, 'totalSum' => $totalAmount + $totalTax, 'count' => count($invoices) ] ]; } /** * 에러 코드 체크 */ private function checkErrorCode($data): ?int { if (isset($data->CurrentPage) && is_numeric($data->CurrentPage) && $data->CurrentPage < 0) { return (int)$data->CurrentPage; } return null; } /** * 에러 메시지 반환 */ private function getErrorMessage(int $errorCode): string { $messages = [ -10002 => '인증 실패 (-10002). CERTKEY가 올바르지 않거나 만료되었습니다.', -60001 => '등록된 홈택스 정보가 없습니다 (-60001).', -60002 => '홈택스 인증서가 등록되지 않았습니다 (-60002).', -60003 => '홈택스 수집 서비스가 활성화되지 않았습니다 (-60003).', -60004 => '홈택스 부서사용자 ID가 등록되지 않았습니다 (-60004).', -60005 => '조회된 데이터가 없습니다 (-60005).', -60010 => '홈택스 로그인 실패 (-60010). 부서사용자 ID/비밀번호를 확인해주세요.', -60011 => '홈택스 데이터 수집 중입니다 (-60011). 잠시 후 다시 조회해주세요.', ]; return $messages[$errorCode] ?? '바로빌 API 오류: ' . $errorCode; } /** * 수집 유형 코드 반환 */ private function getCollectTypeCode(string $type): int { return match($type) { 'sales' => 1, 'purchase' => 2, default => 0 // all }; } /** * 과세유형 코드 -> 명칭 */ private function getTaxTypeName(string $code): string { return match($code) { '01' => '과세', '02' => '영세', '03' => '면세', default => $code }; } /** * 발급유형 코드 -> 명칭 */ private function getIssueTypeName(string $code): string { return match($code) { '01' => '정발행', '02' => '역발행', '03' => '위수탁', default => $code }; } /** * 영수/청구 코드 -> 명칭 */ private function getPurposeTypeName(string $code): string { return match($code) { '01' => '영수', '02' => '청구', default => $code }; } /** * 수집 상태 파싱 */ private function parseCollectState($data): array { return [ 'salesLastCollectDate' => $data->SalesLastCollectDate ?? '', 'purchaseLastCollectDate' => $data->PurchaseLastCollectDate ?? '', 'isCollecting' => ($data->CollectState ?? 0) == 1, 'collectStateText' => ($data->CollectState ?? 0) == 1 ? '수집 중' : '대기', ]; } /** * 엑셀 다운로드 */ public function exportExcel(Request $request): StreamedResponse|JsonResponse { try { $type = $request->input('type', 'sales'); // sales or purchase $invoices = $request->input('invoices', []); if (empty($invoices)) { return response()->json([ 'success' => false, 'error' => '저장할 데이터가 없습니다.' ]); } $typeName = $type === 'sales' ? '매출' : '매입'; $filename = "홈택스_{$typeName}_" . date('Ymd_His') . ".csv"; return response()->streamDownload(function () use ($invoices, $type) { $handle = fopen('php://output', 'w'); // UTF-8 BOM fprintf($handle, chr(0xEF) . chr(0xBB) . chr(0xBF)); // 헤더 if ($type === 'sales') { fputcsv($handle, [ '작성일', '국세청승인번호', '공급받는자 사업자번호', '공급받는자 상호', '공급가액', '세액', '합계', '과세유형', '발급유형', '영수/청구' ]); } else { fputcsv($handle, [ '작성일', '국세청승인번호', '공급자 사업자번호', '공급자 상호', '공급가액', '세액', '합계', '과세유형', '발급유형', '영수/청구' ]); } // 데이터 foreach ($invoices as $inv) { if ($type === 'sales') { fputcsv($handle, [ $inv['writeDateFormatted'] ?? '', $inv['ntsConfirmNum'] ?? '', $inv['invoiceeCorpNum'] ?? '', $inv['invoiceeCorpName'] ?? '', $inv['supplyAmount'] ?? 0, $inv['taxAmount'] ?? 0, $inv['totalAmount'] ?? 0, $inv['taxTypeName'] ?? '', $inv['issueTypeName'] ?? '', $inv['purposeTypeName'] ?? '' ]); } else { fputcsv($handle, [ $inv['writeDateFormatted'] ?? '', $inv['ntsConfirmNum'] ?? '', $inv['invoicerCorpNum'] ?? '', $inv['invoicerCorpName'] ?? '', $inv['supplyAmount'] ?? 0, $inv['taxAmount'] ?? 0, $inv['totalAmount'] ?? 0, $inv['taxTypeName'] ?? '', $inv['issueTypeName'] ?? '', $inv['purposeTypeName'] ?? '' ]); } } 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() ]; } } }