From a1f3de782f07d417e8e9e9444c8ac014a4c54001 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B9=80=EB=B3=B4=EA=B3=A4?= Date: Sun, 22 Mar 2026 19:55:23 +0900 Subject: [PATCH] =?UTF-8?q?feat:=20[barobill]=20=EC=B9=B4=EC=B9=B4?= =?UTF-8?q?=EC=98=A4=ED=86=A1/SMS=20=EB=B0=9C=EC=86=A1=20API=20=EC=9D=B4?= =?UTF-8?q?=EA=B4=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - BarobillKakaotalkController 생성 (12개 엔드포인트) - 채널/템플릿 조회, 알림톡/친구톡 발송, 결과 조회, 예약 취소 - BarobillSmsController 생성 (4개 엔드포인트) - SMS 발송, 발신번호 관리, 전송 상태 조회 - finance.php 라우트 등록 (barobill/kakaotalk/*, barobill/sms/*) - 기존 BarobillSoapService SOAP 메서드 활용 --- .../Api/V1/BarobillKakaotalkController.php | 379 ++++++++++++++++++ .../Api/V1/BarobillSmsController.php | 131 ++++++ routes/api/v1/finance.php | 26 ++ 3 files changed, 536 insertions(+) create mode 100644 app/Http/Controllers/Api/V1/BarobillKakaotalkController.php create mode 100644 app/Http/Controllers/Api/V1/BarobillSmsController.php diff --git a/app/Http/Controllers/Api/V1/BarobillKakaotalkController.php b/app/Http/Controllers/Api/V1/BarobillKakaotalkController.php new file mode 100644 index 00000000..1f4a9989 --- /dev/null +++ b/app/Http/Controllers/Api/V1/BarobillKakaotalkController.php @@ -0,0 +1,379 @@ +initSoap(); + + $result = $this->soapService->getKakaotalkChannels($corpNum); + + return $this->formatResult($result, 'channels'); + }, __('message.fetched')); + } + + /** + * 카카오톡 채널 관리 URL + */ + public function channelManagementUrl() + { + return ApiResponse::handle(function () { + [$member, $corpNum] = $this->initSoap(); + + $result = $this->soapService->getKakaotalkChannelManagementUrl($corpNum, $member->barobill_id); + + return $this->formatResult($result, 'url'); + }, __('message.fetched')); + } + + /** + * 카카오톡 템플릿 목록 + */ + public function templates(Request $request) + { + $data = $request->validate([ + 'channel_id' => 'required|string', + ]); + + return ApiResponse::handle(function () use ($data) { + [$member, $corpNum] = $this->initSoap(); + + $result = $this->soapService->getKakaotalkTemplates($corpNum, $data['channel_id']); + + return $this->formatResult($result, 'templates'); + }, __('message.fetched')); + } + + /** + * 카카오톡 템플릿 관리 URL + */ + public function templateManagementUrl() + { + return ApiResponse::handle(function () { + [$member, $corpNum] = $this->initSoap(); + + $result = $this->soapService->getKakaotalkTemplateManagementUrl($corpNum, $member->barobill_id); + + return $this->formatResult($result, 'url'); + }, __('message.fetched')); + } + + /** + * 알림톡 발송 + */ + public function sendAlimtalk(Request $request) + { + $data = $request->validate([ + 'sender_id' => 'required|string', + 'template_name' => 'required|string', + 'receiver_name' => 'required|string', + 'receiver_num' => 'required|string', + 'title' => 'nullable|string', + 'message' => 'required|string', + 'buttons' => 'nullable|array', + 'buttons.*.Name' => 'required_with:buttons|string', + 'buttons.*.ButtonType' => 'required_with:buttons|string', + 'buttons.*.Url1' => 'nullable|string', + 'buttons.*.Url2' => 'nullable|string', + 'sms_message' => 'nullable|string', + 'sms_subject' => 'nullable|string', + 'reserve_dt' => 'nullable|string', + ]); + + return ApiResponse::handle(function () use ($data) { + [$member, $corpNum] = $this->initSoap(); + + $buttons = $data['buttons'] ?? []; + + if (! empty($buttons)) { + $result = $this->soapService->sendATKakaotalkEx( + $corpNum, + $data['sender_id'], + $data['sender_id'], + $data['template_name'], + $data['receiver_name'], + $data['receiver_num'], + $data['title'] ?? '', + $data['message'], + $buttons, + $data['sms_message'] ?? '', + $data['sms_subject'] ?? '', + '', + $data['reserve_dt'] ?? '' + ); + } else { + $result = $this->soapService->sendATKakaotalk( + $corpNum, + $data['sender_id'], + $data['sender_id'], + $data['template_name'], + $data['receiver_name'], + $data['receiver_num'], + $data['title'] ?? '', + $data['message'], + $data['sms_message'] ?? '', + $data['sms_subject'] ?? '', + '', + $data['reserve_dt'] ?? '' + ); + } + + return $this->formatResult($result, 'send_key'); + }, __('message.created')); + } + + /** + * 알림톡 대량 발송 + */ + public function sendAlimtalkBulk(Request $request) + { + $data = $request->validate([ + 'sender_id' => 'required|string', + 'template_name' => 'required|string', + 'messages' => 'required|array|min:1', + 'messages.*.ReceiverName' => 'required|string', + 'messages.*.ReceiverNum' => 'required|string', + 'messages.*.Title' => 'nullable|string', + 'messages.*.Message' => 'required|string', + 'messages.*.SmsMessage' => 'nullable|string', + 'messages.*.SmsSubject' => 'nullable|string', + 'reserve_dt' => 'nullable|string', + ]); + + return ApiResponse::handle(function () use ($data) { + [$member, $corpNum] = $this->initSoap(); + + $result = $this->soapService->sendATKakaotalks( + $corpNum, + $data['sender_id'], + $data['sender_id'], + $data['template_name'], + $data['messages'], + $data['reserve_dt'] ?? '' + ); + + return $this->formatResult($result, 'send_key'); + }, __('message.created')); + } + + /** + * 친구톡 텍스트 발송 + */ + public function sendFriendtalk(Request $request) + { + $data = $request->validate([ + 'sender_id' => 'required|string', + 'channel_id' => 'required|string', + 'receiver_name' => 'required|string', + 'receiver_num' => 'required|string', + 'message' => 'required|string', + 'buttons' => 'nullable|array', + 'sms_message' => 'nullable|string', + 'sms_subject' => 'nullable|string', + 'ad_yn' => 'nullable|boolean', + 'reserve_dt' => 'nullable|string', + ]); + + return ApiResponse::handle(function () use ($data) { + [$member, $corpNum] = $this->initSoap(); + + $result = $this->soapService->sendFTKakaotalk( + $corpNum, + $data['sender_id'], + $data['channel_id'], + $data['receiver_name'], + $data['receiver_num'], + $data['message'], + $data['buttons'] ?? [], + $data['sms_message'] ?? '', + $data['sms_subject'] ?? '', + $data['ad_yn'] ?? false, + $data['reserve_dt'] ?? '' + ); + + return $this->formatResult($result, 'send_key'); + }, __('message.created')); + } + + /** + * 친구톡 이미지 발송 + */ + public function sendFriendtalkImage(Request $request) + { + $data = $request->validate([ + 'sender_id' => 'required|string', + 'channel_id' => 'required|string', + 'receiver_name' => 'required|string', + 'receiver_num' => 'required|string', + 'message' => 'required|string', + 'image_url' => 'required|string', + 'buttons' => 'nullable|array', + 'sms_message' => 'nullable|string', + 'sms_subject' => 'nullable|string', + 'ad_yn' => 'nullable|boolean', + 'reserve_dt' => 'nullable|string', + ]); + + return ApiResponse::handle(function () use ($data) { + [$member, $corpNum] = $this->initSoap(); + + $result = $this->soapService->sendFIKakaotalk( + $corpNum, + $data['sender_id'], + $data['channel_id'], + $data['receiver_name'], + $data['receiver_num'], + $data['message'], + $data['image_url'], + $data['buttons'] ?? [], + $data['sms_message'] ?? '', + $data['sms_subject'] ?? '', + $data['ad_yn'] ?? false, + $data['reserve_dt'] ?? '' + ); + + return $this->formatResult($result, 'send_key'); + }, __('message.created')); + } + + /** + * 친구톡 와이드 이미지 발송 + */ + public function sendFriendtalkWide(Request $request) + { + $data = $request->validate([ + 'sender_id' => 'required|string', + 'channel_id' => 'required|string', + 'receiver_name' => 'required|string', + 'receiver_num' => 'required|string', + 'message' => 'required|string', + 'image_url' => 'required|string', + 'buttons' => 'nullable|array', + 'sms_message' => 'nullable|string', + 'sms_subject' => 'nullable|string', + 'ad_yn' => 'nullable|boolean', + 'reserve_dt' => 'nullable|string', + ]); + + return ApiResponse::handle(function () use ($data) { + [$member, $corpNum] = $this->initSoap(); + + $result = $this->soapService->sendFWKakaotalk( + $corpNum, + $data['sender_id'], + $data['channel_id'], + $data['receiver_name'], + $data['receiver_num'], + $data['message'], + $data['image_url'], + $data['buttons'] ?? [], + $data['sms_message'] ?? '', + $data['sms_subject'] ?? '', + $data['ad_yn'] ?? false, + $data['reserve_dt'] ?? '' + ); + + return $this->formatResult($result, 'send_key'); + }, __('message.created')); + } + + /** + * 전송 결과 조회 (단건) + */ + public function getSendResult(string $sendKey) + { + return ApiResponse::handle(function () use ($sendKey) { + [$member, $corpNum] = $this->initSoap(); + + $result = $this->soapService->getSendKakaotalk($corpNum, $sendKey); + + return $this->formatResult($result, 'result'); + }, __('message.fetched')); + } + + /** + * 전송 결과 조회 (다건) + */ + public function getSendResults(Request $request) + { + $data = $request->validate([ + 'send_keys' => 'required|array|min:1', + 'send_keys.*' => 'required|string', + ]); + + return ApiResponse::handle(function () use ($data) { + [$member, $corpNum] = $this->initSoap(); + + $result = $this->soapService->getSendKakaotalks($corpNum, $data['send_keys']); + + return $this->formatResult($result, 'results'); + }, __('message.fetched')); + } + + /** + * 예약 발송 취소 + */ + public function cancelReserved(string $sendKey) + { + return ApiResponse::handle(function () use ($sendKey) { + [$member, $corpNum] = $this->initSoap(); + + $result = $this->soapService->cancelReservedKakaotalk($corpNum, $sendKey); + + return $this->formatResult($result, 'result'); + }, __('message.deleted')); + } + + /** + * 바로빌 회원 조회 및 SOAP 초기화 + */ + private function initSoap(): array + { + $member = $this->getMember(); + if (! $member) { + throw new \Symfony\Component\HttpKernel\Exception\NotFoundHttpException( + __('error.barobill.member_not_found', [], 'ko') ?: '바로빌 회원사가 등록되어 있지 않습니다.' + ); + } + + $this->soapService->initForMember($member); + + return [$member, $member->biz_no]; + } + + private function getMember(): ?BarobillMember + { + $tenantId = app('tenant_id'); + + return BarobillMember::withoutGlobalScopes() + ->where('tenant_id', $tenantId) + ->first(); + } + + private function formatResult(array $result, string $key): array + { + if (! ($result['success'] ?? false)) { + throw new \Symfony\Component\HttpKernel\Exception\UnprocessableEntityHttpException( + $result['error'] ?? '바로빌 API 호출 실패' + ); + } + + return [$key => $result['data'] ?? null]; + } +} diff --git a/app/Http/Controllers/Api/V1/BarobillSmsController.php b/app/Http/Controllers/Api/V1/BarobillSmsController.php new file mode 100644 index 00000000..4a5faa33 --- /dev/null +++ b/app/Http/Controllers/Api/V1/BarobillSmsController.php @@ -0,0 +1,131 @@ +validate([ + 'from_number' => 'required|string', + 'to_name' => 'required|string', + 'to_number' => 'required|string', + 'contents' => 'required|string', + 'send_dt' => 'nullable|string', + 'ref_key' => 'nullable|string', + ]); + + return ApiResponse::handle(function () use ($data) { + [$member, $corpNum] = $this->initSoap(); + + $result = $this->soapService->sendSMSMessage( + $corpNum, + $member->barobill_id, + $data['from_number'], + $data['to_name'], + $data['to_number'], + $data['contents'], + $data['send_dt'] ?? '', + $data['ref_key'] ?? '' + ); + + return $this->formatResult($result, 'send_key'); + }, __('message.created')); + } + + /** + * 발신번호 목록 + */ + public function fromNumbers() + { + return ApiResponse::handle(function () { + [$member, $corpNum] = $this->initSoap(); + + $result = $this->soapService->getSMSFromNumbers($corpNum); + + return $this->formatResult($result, 'from_numbers'); + }, __('message.fetched')); + } + + /** + * 발신번호 등록 여부 확인 + */ + public function checkFromNumber(Request $request) + { + $data = $request->validate([ + 'from_number' => 'required|string', + ]); + + return ApiResponse::handle(function () use ($data) { + [$member, $corpNum] = $this->initSoap(); + + $result = $this->soapService->checkSMSFromNumber($corpNum, $data['from_number']); + + return $this->formatResult($result, 'result'); + }, __('message.fetched')); + } + + /** + * SMS 전송 상태 조회 + */ + public function sendState(string $sendKey) + { + return ApiResponse::handle(function () use ($sendKey) { + [$member, $corpNum] = $this->initSoap(); + + $result = $this->soapService->getSMSSendState($corpNum, $sendKey); + + return $this->formatResult($result, 'state'); + }, __('message.fetched')); + } + + /** + * 바로빌 회원 조회 및 SOAP 초기화 + */ + private function initSoap(): array + { + $member = $this->getMember(); + if (! $member) { + throw new \Symfony\Component\HttpKernel\Exception\NotFoundHttpException( + __('error.barobill.member_not_found', [], 'ko') ?: '바로빌 회원사가 등록되어 있지 않습니다.' + ); + } + + $this->soapService->initForMember($member); + + return [$member, $member->biz_no]; + } + + private function getMember(): ?BarobillMember + { + $tenantId = app('tenant_id'); + + return BarobillMember::withoutGlobalScopes() + ->where('tenant_id', $tenantId) + ->first(); + } + + private function formatResult(array $result, string $key): array + { + if (! ($result['success'] ?? false)) { + throw new \Symfony\Component\HttpKernel\Exception\UnprocessableEntityHttpException( + $result['error'] ?? '바로빌 API 호출 실패' + ); + } + + return [$key => $result['data'] ?? null]; + } +} diff --git a/routes/api/v1/finance.php b/routes/api/v1/finance.php index 0e474b0e..fd66fe6b 100644 --- a/routes/api/v1/finance.php +++ b/routes/api/v1/finance.php @@ -20,7 +20,9 @@ use App\Http\Controllers\Api\V1\BarobillBankTransactionController; use App\Http\Controllers\Api\V1\BarobillCardTransactionController; use App\Http\Controllers\Api\V1\BarobillController; +use App\Http\Controllers\Api\V1\BarobillKakaotalkController; use App\Http\Controllers\Api\V1\BarobillSettingController; +use App\Http\Controllers\Api\V1\BarobillSmsController; use App\Http\Controllers\Api\V1\BarobillSyncController; use App\Http\Controllers\Api\V1\BillController; use App\Http\Controllers\Api\V1\CalendarController; @@ -397,6 +399,30 @@ Route::delete('/{id}/journal-entries', [TaxInvoiceController::class, 'deleteJournalEntries'])->whereNumber('id')->name('v1.tax-invoices.journal-entries.destroy'); }); +// Barobill Kakaotalk API (바로빌 카카오톡) +Route::prefix('barobill/kakaotalk')->group(function () { + Route::get('/channels', [BarobillKakaotalkController::class, 'channels'])->name('v1.barobill.kakaotalk.channels'); + Route::get('/channels/management-url', [BarobillKakaotalkController::class, 'channelManagementUrl'])->name('v1.barobill.kakaotalk.channels.management-url'); + Route::get('/templates', [BarobillKakaotalkController::class, 'templates'])->name('v1.barobill.kakaotalk.templates'); + Route::get('/templates/management-url', [BarobillKakaotalkController::class, 'templateManagementUrl'])->name('v1.barobill.kakaotalk.templates.management-url'); + Route::post('/send/alimtalk', [BarobillKakaotalkController::class, 'sendAlimtalk'])->name('v1.barobill.kakaotalk.send.alimtalk'); + Route::post('/send/alimtalk-bulk', [BarobillKakaotalkController::class, 'sendAlimtalkBulk'])->name('v1.barobill.kakaotalk.send.alimtalk-bulk'); + Route::post('/send/friendtalk', [BarobillKakaotalkController::class, 'sendFriendtalk'])->name('v1.barobill.kakaotalk.send.friendtalk'); + Route::post('/send/friendtalk-image', [BarobillKakaotalkController::class, 'sendFriendtalkImage'])->name('v1.barobill.kakaotalk.send.friendtalk-image'); + Route::post('/send/friendtalk-wide', [BarobillKakaotalkController::class, 'sendFriendtalkWide'])->name('v1.barobill.kakaotalk.send.friendtalk-wide'); + Route::get('/send/{sendKey}', [BarobillKakaotalkController::class, 'getSendResult'])->name('v1.barobill.kakaotalk.send.result'); + Route::post('/send/results', [BarobillKakaotalkController::class, 'getSendResults'])->name('v1.barobill.kakaotalk.send.results'); + Route::delete('/send/{sendKey}/cancel', [BarobillKakaotalkController::class, 'cancelReserved'])->name('v1.barobill.kakaotalk.send.cancel'); +}); + +// Barobill SMS API (바로빌 문자) +Route::prefix('barobill/sms')->group(function () { + Route::post('/send', [BarobillSmsController::class, 'send'])->name('v1.barobill.sms.send'); + Route::get('/from-numbers', [BarobillSmsController::class, 'fromNumbers'])->name('v1.barobill.sms.from-numbers'); + Route::post('/check-from-number', [BarobillSmsController::class, 'checkFromNumber'])->name('v1.barobill.sms.check-from-number'); + Route::get('/send-state/{sendKey}', [BarobillSmsController::class, 'sendState'])->name('v1.barobill.sms.send-state'); +}); + // Bad Debt API (악성채권 추심관리) Route::prefix('bad-debts')->group(function () { Route::get('', [BadDebtController::class, 'index'])->name('v1.bad-debts.index');