diff --git a/app/Http/Controllers/Barobill/HometaxController.php b/app/Http/Controllers/Barobill/HometaxController.php index 6ec35503..7d7293dd 100644 --- a/app/Http/Controllers/Barobill/HometaxController.php +++ b/app/Http/Controllers/Barobill/HometaxController.php @@ -9,6 +9,7 @@ use Illuminate\Http\JsonResponse; use Illuminate\Http\Request; use Illuminate\Http\Response; +use Illuminate\Support\Facades\Http; use Illuminate\Support\Facades\Log; use Illuminate\View\View; use Symfony\Component\HttpFoundation\StreamedResponse; @@ -16,20 +17,19 @@ /** * 바로빌 홈택스 매입/매출 조회 컨트롤러 * - * 홈택스에 신고된 세금계산서 및 현금영수증 내역을 조회합니다. + * 바로빌에서 발행/수신한 세금계산서 내역을 조회합니다. * - * @see https://dev.barobill.co.kr/services/hometax + * @see https://dev.barobill.co.kr/docs/taxinvoice */ class HometaxController extends Controller { /** - * 바로빌 SOAP 설정 + * 바로빌 설정 */ private ?string $certKey = null; private ?string $corpNum = null; private bool $isTestMode = false; - private ?string $soapUrl = null; - private ?\SoapClient $soapClient = null; + private string $baseUrl = ''; // 바로빌 파트너사 (본사) 테넌트 ID private const HEADQUARTERS_TENANT_ID = 1; @@ -43,51 +43,18 @@ public function __construct() $this->certKey = $activeConfig->cert_key; $this->corpNum = $activeConfig->corp_num; $this->isTestMode = $activeConfig->environment === 'test'; - // 홈택스 조회는 TI.asmx 사용 (세금계산서 서비스에 포함) - $baseUrl = $this->isTestMode + $this->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()); - } + $this->baseUrl = $this->isTestMode + ? 'https://testws.baroservice.com' + : 'https://ws.baroservice.com'; } } @@ -111,7 +78,7 @@ public function index(Request $request): View|Response 'certKey' => $this->certKey, 'corpNum' => $this->corpNum, 'isTestMode' => $this->isTestMode, - 'hasSoapClient' => $this->soapClient !== null, + 'hasSoapClient' => !empty($this->certKey) || $this->isTestMode, 'currentTenant' => $currentTenant, 'barobillMember' => $barobillMember, ]); @@ -591,17 +558,10 @@ public function exportExcel(Request $request): StreamedResponse|JsonResponse } /** - * SOAP 호출 (인코딩 오류 시 Raw XML 파싱으로 fallback) + * HTTP를 통한 SOAP 호출 (SoapClient 대신 직접 HTTP 요청) */ 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, @@ -625,48 +585,38 @@ private function callSoap(string $method, array $params = []): array } try { - Log::info("바로빌 홈택스 API 호출 - Method: {$method}, CorpNum: {$this->corpNum}"); + Log::info("바로빌 API 호출 (HTTP) - Method: {$method}, CorpNum: {$this->corpNum}"); - $result = $this->soapClient->$method($params); - $resultProperty = $method . 'Result'; + // SOAP 요청 XML 생성 + $soapXml = $this->buildSoapRequest($method, $params); - 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 - ]; - } + // HTTP 요청 + $response = Http::withOptions([ + 'verify' => false, + ]) + ->withHeaders([ + 'Content-Type' => 'text/xml; charset=utf-8', + 'SOAPAction' => 'https://www.baroservice.com/' . $method, + ]) + ->withBody($soapXml, 'text/xml') + ->timeout(30) + ->post($this->baseUrl . '/TI.asmx'); + if (!$response->successful()) { return [ - 'success' => true, - 'data' => $resultData + 'success' => false, + 'error' => 'HTTP 오류: ' . $response->status() ]; } - return [ - 'success' => true, - 'data' => $result - ]; - } catch (\SoapFault $e) { - // SOAP 인코딩 오류 시 Raw XML 파싱 시도 - if (str_contains($e->getMessage(), 'Encoding')) { - Log::warning('SOAP 인코딩 오류 - Raw XML 파싱 시도: ' . $e->getMessage()); - return $this->parseRawXmlResponse($method); - } + $xmlResponse = $response->body(); + Log::debug("SOAP 응답 길이: " . strlen($xmlResponse)); + + // XML 응답 파싱 + return $this->parseSoapResponse($xmlResponse, $method); - Log::error('바로빌 홈택스 SOAP 오류: ' . $e->getMessage()); - return [ - 'success' => false, - 'error' => 'SOAP 오류: ' . $e->getMessage(), - 'error_code' => $e->getCode() - ]; } catch (\Throwable $e) { - Log::error('바로빌 홈택스 API 호출 오류: ' . $e->getMessage()); + Log::error('바로빌 API 호출 오류: ' . $e->getMessage()); return [ 'success' => false, 'error' => 'API 호출 오류: ' . $e->getMessage() @@ -675,24 +625,33 @@ private function callSoap(string $method, array $params = []): array } /** - * Raw XML 응답 파싱 (SOAP 인코딩 오류 시 fallback) + * SOAP 요청 XML 생성 */ - private function parseRawXmlResponse(string $method): array + private function buildSoapRequest(string $method, array $params): string + { + $paramsXml = ''; + foreach ($params as $key => $value) { + $paramsXml .= "<{$key}>" . htmlspecialchars((string)$value, ENT_XML1, 'UTF-8') . ""; + } + + return ' + + + + ' . $paramsXml . ' + + +'; + } + + /** + * SOAP 응답 XML 파싱 + */ + private function parseSoapResponse(string $xmlResponse, string $method): array { try { - $rawResponse = $this->soapClient->__getLastResponse(); - - if (empty($rawResponse)) { - return [ - 'success' => false, - 'error' => 'Raw XML 응답이 비어있습니다.' - ]; - } - - Log::debug('Raw XML Response length: ' . strlen($rawResponse)); - // XML 파싱 - $xml = simplexml_load_string($rawResponse, 'SimpleXMLElement', LIBXML_NOCDATA); + $xml = simplexml_load_string($xmlResponse, 'SimpleXMLElement', LIBXML_NOCDATA); if ($xml === false) { return [ 'success' => false, @@ -700,37 +659,55 @@ private function parseRawXmlResponse(string $method): array ]; } - // 네임스페이스 처리 - $namespaces = $xml->getNamespaces(true); - $soapBody = $xml->children($namespaces['soap'] ?? 'http://schemas.xmlsoap.org/soap/envelope/'); + // 네임스페이스 등록 + $xml->registerXPathNamespace('soap', 'http://schemas.xmlsoap.org/soap/envelope/'); + $xml->registerXPathNamespace('bar', 'https://www.baroservice.com/'); - if (!isset($soapBody->Body)) { - return [ - 'success' => false, - 'error' => 'SOAP Body를 찾을 수 없습니다.' - ]; + // 결과 노드 찾기 + $resultNodes = $xml->xpath("//bar:{$method}Response/bar:{$method}Result"); + + if (empty($resultNodes)) { + // 네임스페이스 없이 다시 시도 + $resultNodes = $xml->xpath("//*[local-name()='{$method}Response']/*[local-name()='{$method}Result']"); } - $body = $soapBody->Body->children($namespaces[''] ?? 'https://www.baroservice.com/'); - $resultNode = $body->{$method . 'Response'}->{$method . 'Result'} ?? null; - - if ($resultNode === null) { + if (empty($resultNodes)) { + Log::warning("응답에서 {$method}Result를 찾을 수 없음"); return [ 'success' => false, 'error' => '응답 결과를 찾을 수 없습니다.' ]; } - // XML을 stdClass로 변환 + $resultNode = $resultNodes[0]; + + // 단순 숫자 응답인 경우 (에러 코드) + $textContent = trim((string)$resultNode); + if (is_numeric($textContent) && $resultNode->count() === 0) { + $code = (int)$textContent; + if ($code < 0) { + return [ + 'success' => false, + 'error' => $this->getErrorMessage($code), + 'error_code' => $code + ]; + } + return [ + 'success' => true, + 'data' => $code + ]; + } + + // 복잡한 객체 응답 파싱 $resultData = $this->xmlToObject($resultNode); return [ 'success' => true, - 'data' => $resultData, - 'parsed_from_raw' => true + 'data' => $resultData ]; + } catch (\Throwable $e) { - Log::error('Raw XML 파싱 오류: ' . $e->getMessage()); + Log::error('SOAP 응답 파싱 오류: ' . $e->getMessage()); return [ 'success' => false, 'error' => 'XML 파싱 오류: ' . $e->getMessage() @@ -751,15 +728,24 @@ private function xmlToObject(\SimpleXMLElement $xml): object } // 자식 요소 처리 - foreach ($xml->children() as $name => $child) { - $childCount = $xml->$name->count(); + $children = $xml->children(); + $childNames = []; - if ($childCount > 1) { + foreach ($children as $name => $child) { + $childNames[$name] = ($childNames[$name] ?? 0) + 1; + } + + foreach ($children as $name => $child) { + if ($childNames[$name] > 1) { // 여러 개의 동일 이름 요소 → 배열 if (!isset($result->$name)) { $result->$name = []; } - $result->$name[] = $this->xmlToObject($child); + if ($child->count() > 0) { + $result->{$name}[] = $this->xmlToObject($child); + } else { + $result->{$name}[] = (string)$child; + } } else if ($child->count() > 0) { // 자식이 있는 요소 → 재귀 호출 $result->$name = $this->xmlToObject($child);