fix:SoapClient 대신 HTTP 직접 요청 방식으로 변경
- Laravel Http 파사드로 SOAP 요청 전송 - buildSoapRequest()로 XML 요청 생성 - parseSoapResponse()로 응답 XML 파싱 - SOAP 인코딩 오류 우회
This commit is contained in:
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user