diff --git a/app/Http/Controllers/Barobill/HometaxController.php b/app/Http/Controllers/Barobill/HometaxController.php new file mode 100644 index 00000000..4b648299 --- /dev/null +++ b/app/Http/Controllers/Barobill/HometaxController.php @@ -0,0 +1,680 @@ +first(); + + if ($activeConfig) { + $this->certKey = $activeConfig->cert_key; + $this->corpNum = $activeConfig->corp_num; + $this->isTestMode = $activeConfig->environment === 'test'; + // 홈택스 조회는 HOMETAX.asmx 사용 + $baseUrl = $this->isTestMode + ? 'https://testws.baroservice.com' + : 'https://ws.baroservice.com'; + $this->soapUrl = $baseUrl . '/HOMETAX.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/HOMETAX.asmx?WSDL' + : 'https://ws.baroservice.com/HOMETAX.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, + ]); + } + + /** + * 매출 세금계산서 목록 조회 (GetHomeTaxTIBySalesEx) + */ + 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('GetHomeTaxTIBySalesEx', [ + '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() + ]); + } + } + + /** + * 매입 세금계산서 목록 조회 (GetHomeTaxTIByPurchaseEx) + */ + 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('GetHomeTaxTIByPurchaseEx', [ + '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() + ]); + } + } + + /** + * 홈택스 수집 요청 (RequestHomeTaxTICollect) + */ + public function requestCollect(Request $request): JsonResponse + { + try { + $collectType = $request->input('type', 'all'); // all, sales, purchase + + $tenantId = session('selected_tenant_id', self::HEADQUARTERS_TENANT_ID); + $barobillMember = BarobillMember::where('tenant_id', $tenantId)->first(); + $userId = $barobillMember?->barobill_id ?? ''; + + $result = $this->callSoap('RequestHomeTaxTICollect', [ + 'ID' => $userId, + 'CollectType' => $this->getCollectTypeCode($collectType) + ]); + + if (!$result['success']) { + return response()->json([ + 'success' => false, + 'error' => $result['error'] + ]); + } + + return response()->json([ + 'success' => true, + 'message' => '홈택스 데이터 수집이 요청되었습니다. 잠시 후 다시 조회해주세요.', + 'data' => $result['data'] + ]); + } catch (\Throwable $e) { + Log::error('홈택스 수집 요청 오류: ' . $e->getMessage()); + return response()->json([ + 'success' => false, + 'error' => '서버 오류: ' . $e->getMessage() + ]); + } + } + + /** + * 수집 상태 확인 (GetCollectState) + */ + public function collectStatus(Request $request): JsonResponse + { + try { + $tenantId = session('selected_tenant_id', self::HEADQUARTERS_TENANT_ID); + $barobillMember = BarobillMember::where('tenant_id', $tenantId)->first(); + $userId = $barobillMember?->barobill_id ?? ''; + + $result = $this->callSoap('GetHomeTaxTICollectState', [ + 'ID' => $userId + ]); + + if (!$result['success']) { + return response()->json([ + 'success' => false, + 'error' => $result['error'] + ]); + } + + $data = $result['data']; + $state = $this->parseCollectState($data); + + return response()->json([ + 'success' => true, + 'data' => $state + ]); + } catch (\Throwable $e) { + Log::error('홈택스 수집 상태 조회 오류: ' . $e->getMessage()); + return response()->json([ + 'success' => false, + 'error' => '서버 오류: ' . $e->getMessage() + ]); + } + } + + /** + * 세금계산서 파싱 + */ + private function parseInvoices($resultData, string $type = 'sales'): array + { + $invoices = []; + $totalAmount = 0; + $totalTax = 0; + + $rawList = []; + $listProperty = $type === 'sales' ? 'HomeTaxTISalesList' : 'HomeTaxTIPurchaseList'; + $itemProperty = $type === 'sales' ? 'HomeTaxTISales' : 'HomeTaxTIPurchase'; + + 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() + ]; + } + } +} diff --git a/resources/views/barobill/hometax/index.blade.php b/resources/views/barobill/hometax/index.blade.php index aa9256df..cd9f7cc7 100644 --- a/resources/views/barobill/hometax/index.blade.php +++ b/resources/views/barobill/hometax/index.blade.php @@ -1,26 +1,668 @@ @extends('layouts.app') -@section('title', '홈텍스매입/매출') +@section('title', '홈택스 매입/매출') @section('content') -
홈텍스 매입/매출 데이터 조회
-