fix:SoapClient 대신 HTTP 직접 요청 방식으로 변경

- Laravel Http 파사드로 SOAP 요청 전송
- buildSoapRequest()로 XML 요청 생성
- parseSoapResponse()로 응답 XML 파싱
- SOAP 인코딩 오류 우회
This commit is contained in:
pro
2026-01-23 17:28:38 +09:00
parent b480f8e406
commit 5ccb31a99c

View File

@@ -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') . "</{$key}>";
}
return '<?xml version="1.0" encoding="utf-8"?>
<soap:Envelope xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/" xmlns:bar="https://www.baroservice.com/">
<soap:Body>
<bar:' . $method . '>
' . $paramsXml . '
</bar:' . $method . '>
</soap:Body>
</soap:Envelope>';
}
/**
* 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);