feat: [barobill] 바로빌 연동 관리 API 7개 엔드포인트 구현

- SOAP 기반 BarobillSoapService 생성 (MNG 코드 포팅)
- BarobillMember, BarobillConfig 모델 생성
- BarobillController 7개 메서드 (login, signup, status, URL 조회)
- FormRequest 검증 클래스 3개 생성
- 라우트 등록 (POST /barobill/login, /signup, GET /status 등)
- i18n 메시지 키 추가 (ko/en)
- config/services.php에 barobill 설정 추가
This commit is contained in:
김보곤
2026-02-20 22:39:04 +09:00
parent 1dd9057540
commit b576fe97e8
12 changed files with 899 additions and 0 deletions

View File

@@ -0,0 +1,73 @@
<?php
namespace App\Http\Controllers\Api\V1;
use App\Helpers\ApiResponse;
use App\Http\Controllers\Controller;
use App\Http\Requests\Barobill\BankServiceUrlRequest;
use App\Http\Requests\Barobill\BarobillLoginRequest;
use App\Http\Requests\Barobill\BarobillSignupRequest;
use App\Services\Barobill\BarobillSoapService;
class BarobillController extends Controller
{
public function __construct(
private BarobillSoapService $barobillSoapService
) {}
public function login(BarobillLoginRequest $request)
{
return ApiResponse::handle(
fn () => $this->barobillSoapService->registerLogin($request->validated()),
__('message.barobill.login_success')
);
}
public function signup(BarobillSignupRequest $request)
{
return ApiResponse::handle(
fn () => $this->barobillSoapService->registerSignup($request->validated()),
__('message.barobill.signup_success')
);
}
public function bankServiceUrl(BankServiceUrlRequest $request)
{
return ApiResponse::handle(
fn () => $this->barobillSoapService->getBankServiceRedirectUrl($request->validated()),
__('message.fetched')
);
}
public function status()
{
return ApiResponse::handle(
fn () => $this->barobillSoapService->getIntegrationStatus(),
__('message.fetched')
);
}
public function accountLinkUrl()
{
return ApiResponse::handle(
fn () => $this->barobillSoapService->getAccountLinkRedirectUrl(),
__('message.fetched')
);
}
public function cardLinkUrl()
{
return ApiResponse::handle(
fn () => $this->barobillSoapService->getCardLinkRedirectUrl(),
__('message.fetched')
);
}
public function certificateUrl()
{
return ApiResponse::handle(
fn () => $this->barobillSoapService->getCertificateRedirectUrl(),
__('message.fetched')
);
}
}

View File

@@ -0,0 +1,21 @@
<?php
namespace App\Http\Requests\Barobill;
use Illuminate\Foundation\Http\FormRequest;
class BankServiceUrlRequest extends FormRequest
{
public function authorize(): bool
{
return true;
}
public function rules(): array
{
return [
'bank_code' => ['required', 'string', 'max:10'],
'account_type' => ['required', 'string', 'max:10'],
];
}
}

View File

@@ -0,0 +1,21 @@
<?php
namespace App\Http\Requests\Barobill;
use Illuminate\Foundation\Http\FormRequest;
class BarobillLoginRequest extends FormRequest
{
public function authorize(): bool
{
return true;
}
public function rules(): array
{
return [
'barobill_id' => ['required', 'string', 'max:50'],
'password' => ['required', 'string', 'max:255'],
];
}
}

View File

@@ -0,0 +1,30 @@
<?php
namespace App\Http\Requests\Barobill;
use Illuminate\Foundation\Http\FormRequest;
class BarobillSignupRequest extends FormRequest
{
public function authorize(): bool
{
return true;
}
public function rules(): array
{
return [
'business_number' => ['required', 'string', 'max:20'],
'company_name' => ['required', 'string', 'max:100'],
'ceo_name' => ['required', 'string', 'max:50'],
'business_type' => ['nullable', 'string', 'max:50'],
'business_category' => ['nullable', 'string', 'max:50'],
'address' => ['nullable', 'string', 'max:255'],
'barobill_id' => ['required', 'string', 'max:50'],
'password' => ['required', 'string', 'max:255'],
'manager_name' => ['nullable', 'string', 'max:50'],
'manager_phone' => ['nullable', 'string', 'max:20'],
'manager_email' => ['nullable', 'email', 'max:100'],
];
}
}

View File

@@ -0,0 +1,40 @@
<?php
namespace App\Models\Tenants;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\SoftDeletes;
class BarobillConfig extends Model
{
use SoftDeletes;
protected $fillable = [
'name',
'environment',
'cert_key',
'corp_num',
'base_url',
'description',
'is_active',
];
protected $casts = [
'is_active' => 'boolean',
];
public static function getActiveTest(): ?self
{
return self::where('environment', 'test')->first();
}
public static function getActiveProduction(): ?self
{
return self::where('environment', 'production')->first();
}
public static function getActive(bool $isTestMode = false): ?self
{
return $isTestMode ? self::getActiveTest() : self::getActiveProduction();
}
}

View File

@@ -0,0 +1,66 @@
<?php
namespace App\Models\Tenants;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\SoftDeletes;
class BarobillMember extends Model
{
use SoftDeletes;
protected $table = 'barobill_members';
protected $fillable = [
'tenant_id',
'biz_no',
'corp_name',
'ceo_name',
'addr',
'biz_type',
'biz_class',
'barobill_id',
'barobill_pwd',
'manager_name',
'manager_email',
'manager_hp',
'status',
'server_mode',
'last_sales_fetch_at',
'last_purchases_fetch_at',
];
protected $casts = [
'created_at' => 'datetime',
'updated_at' => 'datetime',
'deleted_at' => 'datetime',
'barobill_pwd' => 'encrypted',
'last_sales_fetch_at' => 'datetime',
'last_purchases_fetch_at' => 'datetime',
];
protected $hidden = [
'barobill_pwd',
];
public function tenant(): BelongsTo
{
return $this->belongsTo(Tenant::class);
}
public function getFormattedBizNoAttribute(): string
{
$bizNo = preg_replace('/[^0-9]/', '', $this->biz_no);
if (strlen($bizNo) === 10) {
return substr($bizNo, 0, 3).'-'.substr($bizNo, 3, 2).'-'.substr($bizNo, 5);
}
return $this->biz_no;
}
public function isTestMode(): bool
{
return $this->server_mode !== 'production';
}
}

View File

@@ -0,0 +1,604 @@
<?php
namespace App\Services\Barobill;
use App\Models\Tenants\BarobillConfig;
use App\Models\Tenants\BarobillMember;
use App\Services\Service;
use Illuminate\Support\Facades\Log;
use SoapClient;
use SoapFault;
use Symfony\Component\HttpKernel\Exception\BadRequestHttpException;
use Throwable;
class BarobillSoapService extends Service
{
protected ?SoapClient $corpStateClient = null;
protected ?SoapClient $bankAccountClient = null;
protected ?SoapClient $cardClient = null;
protected string $certKey;
protected string $corpNum;
protected bool $isTestMode;
protected array $soapUrls;
protected array $errorMessages = [
-11101 => '사업자번호가 설정되지 않았거나 유효하지 않습니다.',
-11102 => 'CERTKEY가 유효하지 않습니다.',
-11103 => '인증서가 만료되었거나 유효하지 않습니다.',
-11104 => '해당 사업자가 등록되어 있지 않습니다.',
-11105 => '이미 등록된 사업자입니다.',
-11106 => '아이디가 이미 존재합니다.',
-11201 => '필수 파라미터가 누락되었습니다.',
-26001 => '공동인증서가 등록되어 있지 않습니다.',
-32000 => '알 수 없는 오류가 발생했습니다.',
-32001 => '사업자번호가 유효하지 않습니다.',
-32002 => '아이디가 유효하지 않습니다.',
-32003 => '비밀번호가 유효하지 않습니다.',
-32004 => '상호명이 유효하지 않습니다.',
-32005 => '대표자명이 유효하지 않습니다.',
-32006 => '이메일 형식이 유효하지 않습니다.',
-32010 => '이미 등록된 사업자번호입니다.',
-32011 => '이미 등록된 아이디입니다.',
-32012 => '이미 등록된 아이디입니다. 다른 아이디를 사용해주세요.',
-32013 => '비밀번호 형식이 유효하지 않습니다. (영문/숫자/특수문자 조합 8자리 이상)',
-32014 => '연락처 형식이 유효하지 않습니다.',
-32020 => '파트너 사업자번호가 유효하지 않습니다.',
-32021 => '파트너 인증키(CERTKEY)가 유효하지 않습니다.',
-99999 => '서버 내부 오류가 발생했습니다.',
];
public function __construct()
{
$this->isTestMode = config('services.barobill.test_mode', true);
$this->initializeConfig();
}
public function switchServerMode(bool $isTestMode): self
{
if ($this->isTestMode !== $isTestMode) {
$this->isTestMode = $isTestMode;
$this->corpStateClient = null;
$this->bankAccountClient = null;
$this->cardClient = null;
$this->initializeConfig();
}
return $this;
}
protected function initializeConfig(): void
{
$dbConfig = $this->loadConfigFromDatabase();
if ($dbConfig) {
$this->certKey = $dbConfig->cert_key;
$this->corpNum = $dbConfig->corp_num ?? '';
$this->soapUrls = $this->buildSoapUrls($dbConfig->base_url);
} else {
$this->certKey = $this->isTestMode
? config('services.barobill.cert_key_test', '')
: config('services.barobill.cert_key_prod', '');
$this->corpNum = config('services.barobill.corp_num', '');
$baseUrl = $this->isTestMode
? 'https://testws.baroservice.com'
: 'https://ws.baroservice.com';
$this->soapUrls = $this->buildSoapUrls($baseUrl);
}
}
protected function loadConfigFromDatabase(): ?BarobillConfig
{
try {
return BarobillConfig::getActive($this->isTestMode);
} catch (\Exception $e) {
Log::warning('바로빌 DB 설정 로드 실패', ['error' => $e->getMessage()]);
return null;
}
}
protected function buildSoapUrls(string $baseUrl): array
{
$baseUrl = rtrim($baseUrl, '/');
return [
'corpstate' => $baseUrl.'/CORPSTATE.asmx?WSDL',
'bankaccount' => $baseUrl.'/BANKACCOUNT.asmx?WSDL',
'card' => $baseUrl.'/CARD.asmx?WSDL',
];
}
protected function getCorpStateClient(): ?SoapClient
{
if ($this->corpStateClient === null) {
try {
$this->corpStateClient = new SoapClient($this->soapUrls['corpstate'], [
'trace' => true,
'encoding' => 'UTF-8',
'exceptions' => true,
'connection_timeout' => 30,
'cache_wsdl' => WSDL_CACHE_NONE,
]);
} catch (Throwable $e) {
Log::error('바로빌 CORPSTATE SOAP 클라이언트 생성 실패', [
'error' => $e->getMessage(),
'url' => $this->soapUrls['corpstate'],
]);
return null;
}
}
return $this->corpStateClient;
}
protected function getBankAccountClient(): ?SoapClient
{
if ($this->bankAccountClient === null) {
try {
$this->bankAccountClient = new SoapClient($this->soapUrls['bankaccount'], [
'trace' => true,
'encoding' => 'UTF-8',
'exceptions' => true,
'connection_timeout' => 30,
'cache_wsdl' => WSDL_CACHE_NONE,
]);
} catch (Throwable $e) {
Log::error('바로빌 BANKACCOUNT SOAP 클라이언트 생성 실패', [
'error' => $e->getMessage(),
'url' => $this->soapUrls['bankaccount'],
]);
return null;
}
}
return $this->bankAccountClient;
}
protected function getCardClient(): ?SoapClient
{
if ($this->cardClient === null) {
try {
$this->cardClient = new SoapClient($this->soapUrls['card'], [
'trace' => true,
'encoding' => 'UTF-8',
'exceptions' => true,
'connection_timeout' => 30,
'cache_wsdl' => WSDL_CACHE_NONE,
]);
} catch (Throwable $e) {
Log::error('바로빌 CARD SOAP 클라이언트 생성 실패', [
'error' => $e->getMessage(),
'url' => $this->soapUrls['card'],
]);
return null;
}
}
return $this->cardClient;
}
protected function call(string $service, string $method, array $params = []): array
{
$client = match ($service) {
'corpstate' => $this->getCorpStateClient(),
'bankaccount' => $this->getBankAccountClient(),
'card' => $this->getCardClient(),
default => null,
};
if (! $client) {
return [
'success' => false,
'error' => "SOAP 클라이언트가 초기화되지 않았습니다. ({$service})",
'error_code' => -1,
];
}
if (empty($this->certKey)) {
return [
'success' => false,
'error' => 'CERTKEY가 설정되지 않았습니다.',
'error_code' => -2,
];
}
if (! isset($params['CERTKEY'])) {
$params['CERTKEY'] = $this->certKey;
}
try {
Log::info('바로빌 SOAP API 호출', [
'service' => $service,
'method' => $method,
'test_mode' => $this->isTestMode,
]);
$result = $client->$method($params);
$resultProperty = $method.'Result';
if (isset($result->$resultProperty)) {
$resultData = $result->$resultProperty;
if (is_numeric($resultData) && $resultData < 0) {
$errorMessage = $this->errorMessages[$resultData] ?? "바로빌 API 오류 코드: {$resultData}";
Log::error('바로빌 SOAP API 오류', [
'method' => $method,
'error_code' => $resultData,
'error_message' => $errorMessage,
]);
return [
'success' => false,
'error' => $errorMessage,
'error_code' => $resultData,
];
}
return [
'success' => true,
'data' => $resultData,
];
}
return [
'success' => true,
'data' => $result,
];
} catch (SoapFault $e) {
Log::error('바로빌 SOAP 오류', [
'method' => $method,
'fault_code' => $e->faultcode ?? null,
'fault_string' => $e->faultstring ?? null,
]);
return [
'success' => false,
'error' => 'SOAP 오류: '.$e->getMessage(),
'error_code' => $e->getCode(),
];
} catch (Throwable $e) {
Log::error('바로빌 SOAP API 호출 오류', [
'method' => $method,
'error' => $e->getMessage(),
]);
return [
'success' => false,
'error' => 'API 호출 오류: '.$e->getMessage(),
'error_code' => -999,
];
}
}
protected function formatBizNo(string $bizNo): string
{
return preg_replace('/[^0-9]/', '', $bizNo);
}
// =========================================================================
// 회원사 조회
// =========================================================================
protected function getMember(): BarobillMember
{
$tenantId = $this->tenantId();
$member = BarobillMember::where('tenant_id', $tenantId)->first();
if (! $member) {
throw new BadRequestHttpException(__('error.barobill.member_not_found'));
}
$this->switchServerMode($member->isTestMode());
return $member;
}
// =========================================================================
// SOAP 비즈니스 메서드
// =========================================================================
public function registCorp(array $data): array
{
$params = [
'CorpNum' => $this->formatBizNo($data['biz_no']),
'CorpName' => $data['corp_name'],
'CEOName' => $data['ceo_name'],
'BizType' => $data['biz_type'] ?? '',
'BizClass' => $data['biz_class'] ?? '',
'PostNum' => $data['post_num'] ?? '',
'Addr1' => $data['addr'] ?? '',
'Addr2' => '',
'MemberName' => $data['manager_name'] ?? '',
'JuminNum' => '',
'ID' => $data['barobill_id'],
'PWD' => $data['barobill_pwd'],
'Grade' => '2',
'TEL' => $data['tel'] ?? '',
'HP' => $data['manager_hp'] ?? '',
'Email' => $data['manager_email'] ?? '',
];
return $this->call('corpstate', 'RegistCorp', $params);
}
public function getCorpState(string $corpNum): array
{
$params = [
'CorpNum' => $this->formatBizNo($corpNum),
];
return $this->call('corpstate', 'GetCorpState', $params);
}
public function getBankAccountScrapRequestUrl(string $corpNum, string $userId, string $userPwd): array
{
$params = [
'CorpNum' => $this->formatBizNo($corpNum),
'ID' => $userId,
'PWD' => $userPwd,
];
return $this->call('bankaccount', 'GetBankAccountScrapRequestURL', $params);
}
public function getBankAccountLogUrl(string $corpNum, string $userId, string $userPwd): array
{
$params = [
'CorpNum' => $this->formatBizNo($corpNum),
'ID' => $userId,
'PWD' => $userPwd,
];
return $this->call('bankaccount', 'GetBankAccountLogURL', $params);
}
public function getCertificateRegistUrl(string $corpNum, string $userId, string $userPwd): array
{
$params = [
'CorpNum' => $this->formatBizNo($corpNum),
'ID' => $userId,
'PWD' => $userPwd,
];
return $this->call('bankaccount', 'GetCertificateRegistURL', $params);
}
public function getBankAccounts(string $corpNum, bool $availOnly = true): array
{
$params = [
'CorpNum' => $this->formatBizNo($corpNum),
'AvailOnly' => $availOnly,
];
return $this->call('bankaccount', 'GetBankAccountEx', $params);
}
public function getCards(string $corpNum, bool $availOnly = true): array
{
$params = [
'CorpNum' => $this->formatBizNo($corpNum),
'AvailOnly' => $availOnly ? 1 : 0,
];
return $this->call('card', 'GetCardEx2', $params);
}
public function getCardScrapRequestUrl(string $corpNum, string $userId, string $userPwd): array
{
$params = [
'CorpNum' => $this->formatBizNo($corpNum),
'ID' => $userId,
'PWD' => $userPwd,
];
return $this->call('card', 'GetCardScrapRequestURL', $params);
}
// =========================================================================
// 컨트롤러 비즈니스 메서드
// =========================================================================
public function registerLogin(array $data): array
{
$tenantId = $this->tenantId();
$barobillId = $data['barobill_id'];
$password = $data['password'];
$member = BarobillMember::where('tenant_id', $tenantId)->first();
if ($member) {
$this->switchServerMode($member->isTestMode());
}
$result = $this->getCorpState($member?->biz_no ?? config('services.barobill.corp_num'));
if (! $result['success']) {
throw new BadRequestHttpException($result['error']);
}
$member = BarobillMember::updateOrCreate(
['tenant_id' => $tenantId],
[
'barobill_id' => $barobillId,
'barobill_pwd' => $password,
'status' => 'active',
]
);
return [
'id' => $member->id,
'barobill_id' => $member->barobill_id,
'status' => $member->status,
];
}
public function registerSignup(array $data): array
{
$tenantId = $this->tenantId();
$soapResult = $this->registCorp([
'biz_no' => $data['business_number'],
'corp_name' => $data['company_name'],
'ceo_name' => $data['ceo_name'],
'biz_type' => $data['business_type'] ?? '',
'biz_class' => $data['business_category'] ?? '',
'addr' => $data['address'] ?? '',
'barobill_id' => $data['barobill_id'],
'barobill_pwd' => $data['password'],
'manager_name' => $data['manager_name'] ?? '',
'manager_hp' => $data['manager_phone'] ?? '',
'manager_email' => $data['manager_email'] ?? '',
]);
if (! $soapResult['success']) {
throw new BadRequestHttpException($soapResult['error']);
}
$member = BarobillMember::updateOrCreate(
['tenant_id' => $tenantId],
[
'biz_no' => $this->formatBizNo($data['business_number']),
'corp_name' => $data['company_name'],
'ceo_name' => $data['ceo_name'],
'biz_type' => $data['business_type'] ?? '',
'biz_class' => $data['business_category'] ?? '',
'addr' => $data['address'] ?? '',
'barobill_id' => $data['barobill_id'],
'barobill_pwd' => $data['password'],
'manager_name' => $data['manager_name'] ?? '',
'manager_hp' => $data['manager_phone'] ?? '',
'manager_email' => $data['manager_email'] ?? '',
'status' => 'active',
'server_mode' => $this->isTestMode ? 'test' : 'production',
]
);
return [
'id' => $member->id,
'barobill_id' => $member->barobill_id,
'biz_no' => $member->formatted_biz_no,
'status' => $member->status,
];
}
public function getBankServiceRedirectUrl(array $params): array
{
$member = $this->getMember();
$result = $this->getBankAccountLogUrl(
$member->biz_no,
$member->barobill_id,
$member->barobill_pwd
);
if (! $result['success']) {
throw new BadRequestHttpException($result['error']);
}
return ['url' => $result['data']];
}
public function getIntegrationStatus(): array
{
$member = $this->getMember();
$bankResult = $this->getBankAccounts($member->biz_no);
$cardResult = $this->getCards($member->biz_no);
$bankCount = 0;
if ($bankResult['success'] && isset($bankResult['data'])) {
$data = $bankResult['data'];
if (is_object($data) && isset($data->BankAccountInfo)) {
$info = $data->BankAccountInfo;
$bankCount = is_array($info) ? count($info) : 1;
} elseif (is_array($data)) {
$bankCount = count($data);
}
}
$cardCount = 0;
if ($cardResult['success'] && isset($cardResult['data'])) {
$data = $cardResult['data'];
if (is_object($data) && isset($data->CardInfo)) {
$info = $data->CardInfo;
$cardCount = is_array($info) ? count($info) : 1;
} elseif (is_array($data)) {
$cardCount = count($data);
}
}
return [
'bank_account_count' => $bankCount,
'card_count' => $cardCount,
'member' => [
'barobill_id' => $member->barobill_id,
'biz_no' => $member->formatted_biz_no,
'status' => $member->status,
'server_mode' => $member->server_mode,
],
];
}
public function getAccountLinkRedirectUrl(): array
{
$member = $this->getMember();
$result = $this->getBankAccountScrapRequestUrl(
$member->biz_no,
$member->barobill_id,
$member->barobill_pwd
);
if (! $result['success']) {
throw new BadRequestHttpException($result['error']);
}
return ['url' => $result['data']];
}
public function getCardLinkRedirectUrl(): array
{
$member = $this->getMember();
$result = $this->getCardScrapRequestUrl(
$member->biz_no,
$member->barobill_id,
$member->barobill_pwd
);
if (! $result['success']) {
throw new BadRequestHttpException($result['error']);
}
return ['url' => $result['data']];
}
public function getCertificateRedirectUrl(): array
{
$member = $this->getMember();
$result = $this->getCertificateRegistUrl(
$member->biz_no,
$member->barobill_id,
$member->barobill_pwd
);
if (! $result['success']) {
throw new BadRequestHttpException($result['error']);
}
return ['url' => $result['data']];
}
}