feat:바로빌 카카오톡(알림톡/친구톡) 서비스 구현
- BarobillService에 KAKAOTALK SOAP 클라이언트 추가 - 채널/템플릿 관리, 알림톡/친구톡 발송, 전송조회/예약취소 API - BarobillKakaotalkController (API) 생성: 15개 엔드포인트 - KakaotalkController (페이지) 생성: 5개 페이지 - 라우트 등록 (web.php, api.php) - Blade 뷰 5개 생성: 대시보드, 채널관리, 템플릿관리, 발송, 전송내역 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,435 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Controllers\Api\Admin\Barobill;
|
||||||
|
|
||||||
|
use App\Http\Controllers\Controller;
|
||||||
|
use App\Models\Barobill\BarobillMember;
|
||||||
|
use App\Services\Barobill\BarobillService;
|
||||||
|
use Illuminate\Http\JsonResponse;
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
use Illuminate\Support\Facades\Log;
|
||||||
|
|
||||||
|
class BarobillKakaotalkController extends Controller
|
||||||
|
{
|
||||||
|
public function __construct(
|
||||||
|
protected BarobillService $barobillService
|
||||||
|
) {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 회원사 조회 및 서버 모드 전환 헬퍼
|
||||||
|
*/
|
||||||
|
private function resolveMember(): BarobillMember|JsonResponse
|
||||||
|
{
|
||||||
|
$tenantId = session('selected_tenant_id', 1);
|
||||||
|
$member = BarobillMember::where('tenant_id', $tenantId)->first();
|
||||||
|
|
||||||
|
if (!$member) {
|
||||||
|
return response()->json([
|
||||||
|
'success' => false,
|
||||||
|
'message' => '바로빌 회원사가 등록되어 있지 않습니다.',
|
||||||
|
], 404);
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->barobillService->setServerMode($member->server_mode ?? 'test');
|
||||||
|
|
||||||
|
return $member;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==========================================
|
||||||
|
// 채널 관리
|
||||||
|
// ==========================================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 카카오톡 채널 목록 조회
|
||||||
|
*/
|
||||||
|
public function getChannels(): JsonResponse
|
||||||
|
{
|
||||||
|
$member = $this->resolveMember();
|
||||||
|
if ($member instanceof JsonResponse) return $member;
|
||||||
|
|
||||||
|
$result = $this->barobillService->getKakaotalkChannels($member->biz_no);
|
||||||
|
|
||||||
|
if (!$result['success']) {
|
||||||
|
return response()->json($result, 422);
|
||||||
|
}
|
||||||
|
|
||||||
|
return response()->json($result);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 카카오톡 채널 관리 URL 조회
|
||||||
|
*/
|
||||||
|
public function getChannelManagementUrl(): JsonResponse
|
||||||
|
{
|
||||||
|
$member = $this->resolveMember();
|
||||||
|
if ($member instanceof JsonResponse) return $member;
|
||||||
|
|
||||||
|
$result = $this->barobillService->getKakaotalkChannelManagementUrl(
|
||||||
|
$member->biz_no,
|
||||||
|
$member->barobill_id
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!$result['success']) {
|
||||||
|
return response()->json($result, 422);
|
||||||
|
}
|
||||||
|
|
||||||
|
return response()->json($result);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==========================================
|
||||||
|
// 템플릿 관리
|
||||||
|
// ==========================================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 카카오톡 템플릿 목록 조회
|
||||||
|
*/
|
||||||
|
public function getTemplates(Request $request): JsonResponse
|
||||||
|
{
|
||||||
|
$request->validate([
|
||||||
|
'channel_id' => 'required|string',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$member = $this->resolveMember();
|
||||||
|
if ($member instanceof JsonResponse) return $member;
|
||||||
|
|
||||||
|
$result = $this->barobillService->getKakaotalkTemplates(
|
||||||
|
$member->biz_no,
|
||||||
|
$request->input('channel_id')
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!$result['success']) {
|
||||||
|
return response()->json($result, 422);
|
||||||
|
}
|
||||||
|
|
||||||
|
return response()->json($result);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 카카오톡 템플릿 관리 URL 조회
|
||||||
|
*/
|
||||||
|
public function getTemplateManagementUrl(): JsonResponse
|
||||||
|
{
|
||||||
|
$member = $this->resolveMember();
|
||||||
|
if ($member instanceof JsonResponse) return $member;
|
||||||
|
|
||||||
|
$result = $this->barobillService->getKakaotalkTemplateManagementUrl(
|
||||||
|
$member->biz_no,
|
||||||
|
$member->barobill_id
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!$result['success']) {
|
||||||
|
return response()->json($result, 422);
|
||||||
|
}
|
||||||
|
|
||||||
|
return response()->json($result);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==========================================
|
||||||
|
// 알림톡 발송
|
||||||
|
// ==========================================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 알림톡 단건 발송
|
||||||
|
*/
|
||||||
|
public function sendAlimtalk(Request $request): JsonResponse
|
||||||
|
{
|
||||||
|
$validated = $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',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$member = $this->resolveMember();
|
||||||
|
if ($member instanceof JsonResponse) return $member;
|
||||||
|
|
||||||
|
$buttons = $validated['buttons'] ?? [];
|
||||||
|
|
||||||
|
// 버튼이 있으면 Ex 버전 사용
|
||||||
|
if (!empty($buttons)) {
|
||||||
|
$result = $this->barobillService->sendATKakaotalkEx(
|
||||||
|
$member->biz_no,
|
||||||
|
$validated['sender_id'],
|
||||||
|
$validated['template_name'],
|
||||||
|
$validated['receiver_name'],
|
||||||
|
$validated['receiver_num'],
|
||||||
|
$validated['title'] ?? '',
|
||||||
|
$validated['message'],
|
||||||
|
$buttons,
|
||||||
|
$validated['sms_message'] ?? '',
|
||||||
|
$validated['sms_subject'] ?? '',
|
||||||
|
$validated['reserve_dt'] ?? ''
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
$result = $this->barobillService->sendATKakaotalk(
|
||||||
|
$member->biz_no,
|
||||||
|
$validated['sender_id'],
|
||||||
|
$validated['template_name'],
|
||||||
|
$validated['receiver_name'],
|
||||||
|
$validated['receiver_num'],
|
||||||
|
$validated['title'] ?? '',
|
||||||
|
$validated['message'],
|
||||||
|
$validated['sms_message'] ?? '',
|
||||||
|
$validated['sms_subject'] ?? '',
|
||||||
|
$validated['reserve_dt'] ?? ''
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!$result['success']) {
|
||||||
|
return response()->json($result, 422);
|
||||||
|
}
|
||||||
|
|
||||||
|
return response()->json($result);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 알림톡 대량 발송
|
||||||
|
*/
|
||||||
|
public function sendAlimtalkBulk(Request $request): JsonResponse
|
||||||
|
{
|
||||||
|
$validated = $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',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$member = $this->resolveMember();
|
||||||
|
if ($member instanceof JsonResponse) return $member;
|
||||||
|
|
||||||
|
$result = $this->barobillService->sendATKakaotalks(
|
||||||
|
$member->biz_no,
|
||||||
|
$validated['sender_id'],
|
||||||
|
$validated['template_name'],
|
||||||
|
$validated['messages'],
|
||||||
|
$validated['reserve_dt'] ?? ''
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!$result['success']) {
|
||||||
|
return response()->json($result, 422);
|
||||||
|
}
|
||||||
|
|
||||||
|
return response()->json($result);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==========================================
|
||||||
|
// 친구톡 발송
|
||||||
|
// ==========================================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 친구톡 텍스트 발송
|
||||||
|
*/
|
||||||
|
public function sendFriendtalk(Request $request): JsonResponse
|
||||||
|
{
|
||||||
|
$validated = $request->validate([
|
||||||
|
'sender_id' => 'required|string',
|
||||||
|
'channel_id' => 'required|string',
|
||||||
|
'receiver_name' => 'required|string',
|
||||||
|
'receiver_num' => 'required|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',
|
||||||
|
'ad_yn' => 'nullable|boolean',
|
||||||
|
'reserve_dt' => 'nullable|string',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$member = $this->resolveMember();
|
||||||
|
if ($member instanceof JsonResponse) return $member;
|
||||||
|
|
||||||
|
$result = $this->barobillService->sendFTKakaotalk(
|
||||||
|
$member->biz_no,
|
||||||
|
$validated['sender_id'],
|
||||||
|
$validated['channel_id'],
|
||||||
|
$validated['receiver_name'],
|
||||||
|
$validated['receiver_num'],
|
||||||
|
$validated['message'],
|
||||||
|
$validated['buttons'] ?? [],
|
||||||
|
$validated['sms_message'] ?? '',
|
||||||
|
$validated['sms_subject'] ?? '',
|
||||||
|
$validated['ad_yn'] ?? false,
|
||||||
|
$validated['reserve_dt'] ?? ''
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!$result['success']) {
|
||||||
|
return response()->json($result, 422);
|
||||||
|
}
|
||||||
|
|
||||||
|
return response()->json($result);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 친구톡 이미지 발송
|
||||||
|
*/
|
||||||
|
public function sendFriendtalkImage(Request $request): JsonResponse
|
||||||
|
{
|
||||||
|
$validated = $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|url',
|
||||||
|
'buttons' => 'nullable|array',
|
||||||
|
'sms_message' => 'nullable|string',
|
||||||
|
'sms_subject' => 'nullable|string',
|
||||||
|
'ad_yn' => 'nullable|boolean',
|
||||||
|
'reserve_dt' => 'nullable|string',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$member = $this->resolveMember();
|
||||||
|
if ($member instanceof JsonResponse) return $member;
|
||||||
|
|
||||||
|
$result = $this->barobillService->sendFIKakaotalk(
|
||||||
|
$member->biz_no,
|
||||||
|
$validated['sender_id'],
|
||||||
|
$validated['channel_id'],
|
||||||
|
$validated['receiver_name'],
|
||||||
|
$validated['receiver_num'],
|
||||||
|
$validated['message'],
|
||||||
|
$validated['image_url'],
|
||||||
|
$validated['buttons'] ?? [],
|
||||||
|
$validated['sms_message'] ?? '',
|
||||||
|
$validated['sms_subject'] ?? '',
|
||||||
|
$validated['ad_yn'] ?? false,
|
||||||
|
$validated['reserve_dt'] ?? ''
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!$result['success']) {
|
||||||
|
return response()->json($result, 422);
|
||||||
|
}
|
||||||
|
|
||||||
|
return response()->json($result);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 친구톡 와이드 이미지 발송
|
||||||
|
*/
|
||||||
|
public function sendFriendtalkWide(Request $request): JsonResponse
|
||||||
|
{
|
||||||
|
$validated = $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|url',
|
||||||
|
'buttons' => 'nullable|array',
|
||||||
|
'sms_message' => 'nullable|string',
|
||||||
|
'sms_subject' => 'nullable|string',
|
||||||
|
'ad_yn' => 'nullable|boolean',
|
||||||
|
'reserve_dt' => 'nullable|string',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$member = $this->resolveMember();
|
||||||
|
if ($member instanceof JsonResponse) return $member;
|
||||||
|
|
||||||
|
$result = $this->barobillService->sendFWKakaotalk(
|
||||||
|
$member->biz_no,
|
||||||
|
$validated['sender_id'],
|
||||||
|
$validated['channel_id'],
|
||||||
|
$validated['receiver_name'],
|
||||||
|
$validated['receiver_num'],
|
||||||
|
$validated['message'],
|
||||||
|
$validated['image_url'],
|
||||||
|
$validated['buttons'] ?? [],
|
||||||
|
$validated['sms_message'] ?? '',
|
||||||
|
$validated['sms_subject'] ?? '',
|
||||||
|
$validated['ad_yn'] ?? false,
|
||||||
|
$validated['reserve_dt'] ?? ''
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!$result['success']) {
|
||||||
|
return response()->json($result, 422);
|
||||||
|
}
|
||||||
|
|
||||||
|
return response()->json($result);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==========================================
|
||||||
|
// 전송 결과 조회
|
||||||
|
// ==========================================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 전송 결과 조회 (단건)
|
||||||
|
*/
|
||||||
|
public function getSendResult(string $sendKey): JsonResponse
|
||||||
|
{
|
||||||
|
$member = $this->resolveMember();
|
||||||
|
if ($member instanceof JsonResponse) return $member;
|
||||||
|
|
||||||
|
$result = $this->barobillService->getSendKakaotalk($member->biz_no, $sendKey);
|
||||||
|
|
||||||
|
if (!$result['success']) {
|
||||||
|
return response()->json($result, 422);
|
||||||
|
}
|
||||||
|
|
||||||
|
return response()->json($result);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 전송 결과 조회 (다건)
|
||||||
|
*/
|
||||||
|
public function getSendResults(Request $request): JsonResponse
|
||||||
|
{
|
||||||
|
$validated = $request->validate([
|
||||||
|
'send_keys' => 'required|array|min:1',
|
||||||
|
'send_keys.*' => 'required|string',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$member = $this->resolveMember();
|
||||||
|
if ($member instanceof JsonResponse) return $member;
|
||||||
|
|
||||||
|
$result = $this->barobillService->getSendKakaotalks(
|
||||||
|
$member->biz_no,
|
||||||
|
$validated['send_keys']
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!$result['success']) {
|
||||||
|
return response()->json($result, 422);
|
||||||
|
}
|
||||||
|
|
||||||
|
return response()->json($result);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==========================================
|
||||||
|
// 예약 취소
|
||||||
|
// ==========================================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 예약 전송 취소
|
||||||
|
*/
|
||||||
|
public function cancelReserved(string $sendKey): JsonResponse
|
||||||
|
{
|
||||||
|
$member = $this->resolveMember();
|
||||||
|
if ($member instanceof JsonResponse) return $member;
|
||||||
|
|
||||||
|
$result = $this->barobillService->cancelReservedKakaotalk($member->biz_no, $sendKey);
|
||||||
|
|
||||||
|
if (!$result['success']) {
|
||||||
|
return response()->json($result, 422);
|
||||||
|
}
|
||||||
|
|
||||||
|
return response()->json($result);
|
||||||
|
}
|
||||||
|
}
|
||||||
93
app/Http/Controllers/Barobill/KakaotalkController.php
Normal file
93
app/Http/Controllers/Barobill/KakaotalkController.php
Normal file
@@ -0,0 +1,93 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Controllers\Barobill;
|
||||||
|
|
||||||
|
use App\Http\Controllers\Controller;
|
||||||
|
use App\Models\Barobill\BarobillMember;
|
||||||
|
use App\Models\Tenants\Tenant;
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
use Illuminate\Http\Response;
|
||||||
|
use Illuminate\View\View;
|
||||||
|
|
||||||
|
class KakaotalkController extends Controller
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* 카카오톡 메인 (대시보드)
|
||||||
|
*/
|
||||||
|
public function index(Request $request): View|Response
|
||||||
|
{
|
||||||
|
if ($request->header('HX-Request')) {
|
||||||
|
return response('', 200)->header('HX-Redirect', route('barobill.kakaotalk.index'));
|
||||||
|
}
|
||||||
|
|
||||||
|
$tenantId = session('selected_tenant_id', 1);
|
||||||
|
$currentTenant = Tenant::find($tenantId);
|
||||||
|
$barobillMember = BarobillMember::where('tenant_id', $tenantId)->first();
|
||||||
|
|
||||||
|
return view('barobill.kakaotalk.index', compact('currentTenant', 'barobillMember'));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 채널 관리
|
||||||
|
*/
|
||||||
|
public function channels(Request $request): View|Response
|
||||||
|
{
|
||||||
|
if ($request->header('HX-Request')) {
|
||||||
|
return response('', 200)->header('HX-Redirect', route('barobill.kakaotalk.channels'));
|
||||||
|
}
|
||||||
|
|
||||||
|
$tenantId = session('selected_tenant_id', 1);
|
||||||
|
$currentTenant = Tenant::find($tenantId);
|
||||||
|
$barobillMember = BarobillMember::where('tenant_id', $tenantId)->first();
|
||||||
|
|
||||||
|
return view('barobill.kakaotalk.channels.index', compact('currentTenant', 'barobillMember'));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 템플릿 관리
|
||||||
|
*/
|
||||||
|
public function templates(Request $request): View|Response
|
||||||
|
{
|
||||||
|
if ($request->header('HX-Request')) {
|
||||||
|
return response('', 200)->header('HX-Redirect', route('barobill.kakaotalk.templates'));
|
||||||
|
}
|
||||||
|
|
||||||
|
$tenantId = session('selected_tenant_id', 1);
|
||||||
|
$currentTenant = Tenant::find($tenantId);
|
||||||
|
$barobillMember = BarobillMember::where('tenant_id', $tenantId)->first();
|
||||||
|
|
||||||
|
return view('barobill.kakaotalk.templates.index', compact('currentTenant', 'barobillMember'));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 발송 (알림톡/친구톡)
|
||||||
|
*/
|
||||||
|
public function send(Request $request): View|Response
|
||||||
|
{
|
||||||
|
if ($request->header('HX-Request')) {
|
||||||
|
return response('', 200)->header('HX-Redirect', route('barobill.kakaotalk.send'));
|
||||||
|
}
|
||||||
|
|
||||||
|
$tenantId = session('selected_tenant_id', 1);
|
||||||
|
$currentTenant = Tenant::find($tenantId);
|
||||||
|
$barobillMember = BarobillMember::where('tenant_id', $tenantId)->first();
|
||||||
|
|
||||||
|
return view('barobill.kakaotalk.send.index', compact('currentTenant', 'barobillMember'));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 전송내역 조회
|
||||||
|
*/
|
||||||
|
public function history(Request $request): View|Response
|
||||||
|
{
|
||||||
|
if ($request->header('HX-Request')) {
|
||||||
|
return response('', 200)->header('HX-Redirect', route('barobill.kakaotalk.history'));
|
||||||
|
}
|
||||||
|
|
||||||
|
$tenantId = session('selected_tenant_id', 1);
|
||||||
|
$currentTenant = Tenant::find($tenantId);
|
||||||
|
$barobillMember = BarobillMember::where('tenant_id', $tenantId)->first();
|
||||||
|
|
||||||
|
return view('barobill.kakaotalk.history.index', compact('currentTenant', 'barobillMember'));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -18,6 +18,7 @@
|
|||||||
* @see https://ws.baroservice.com/TI.asmx (세금계산서)
|
* @see https://ws.baroservice.com/TI.asmx (세금계산서)
|
||||||
* @see https://ws.baroservice.com/BANKACCOUNT.asmx (계좌조회)
|
* @see https://ws.baroservice.com/BANKACCOUNT.asmx (계좌조회)
|
||||||
* @see https://ws.baroservice.com/CARD.asmx (카드조회)
|
* @see https://ws.baroservice.com/CARD.asmx (카드조회)
|
||||||
|
* @see https://ws.baroservice.com/KAKAOTALK.asmx (카카오톡)
|
||||||
*/
|
*/
|
||||||
class BarobillService
|
class BarobillService
|
||||||
{
|
{
|
||||||
@@ -41,6 +42,11 @@ class BarobillService
|
|||||||
*/
|
*/
|
||||||
protected ?SoapClient $cardClient = null;
|
protected ?SoapClient $cardClient = null;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* SOAP 클라이언트 (카카오톡용 - KAKAOTALK)
|
||||||
|
*/
|
||||||
|
protected ?SoapClient $kakaotalkClient = null;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* CERTKEY (인증키)
|
* CERTKEY (인증키)
|
||||||
*/
|
*/
|
||||||
@@ -115,6 +121,7 @@ public function switchServerMode(bool $isTestMode): self
|
|||||||
$this->tiClient = null;
|
$this->tiClient = null;
|
||||||
$this->bankAccountClient = null;
|
$this->bankAccountClient = null;
|
||||||
$this->cardClient = null;
|
$this->cardClient = null;
|
||||||
|
$this->kakaotalkClient = null;
|
||||||
// 설정 재로드
|
// 설정 재로드
|
||||||
$this->initializeConfig();
|
$this->initializeConfig();
|
||||||
}
|
}
|
||||||
@@ -167,6 +174,7 @@ protected function initializeConfig(): void
|
|||||||
'ti' => $baseUrl . '/TI.asmx?WSDL',
|
'ti' => $baseUrl . '/TI.asmx?WSDL',
|
||||||
'bankaccount' => $baseUrl . '/BANKACCOUNT.asmx?WSDL',
|
'bankaccount' => $baseUrl . '/BANKACCOUNT.asmx?WSDL',
|
||||||
'card' => $baseUrl . '/CARD.asmx?WSDL',
|
'card' => $baseUrl . '/CARD.asmx?WSDL',
|
||||||
|
'kakaotalk' => $baseUrl . '/KAKAOTALK.asmx?WSDL',
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -197,6 +205,7 @@ protected function buildSoapUrls(string $baseUrl): array
|
|||||||
'ti' => $baseUrl . '/TI.asmx?WSDL',
|
'ti' => $baseUrl . '/TI.asmx?WSDL',
|
||||||
'bankaccount' => $baseUrl . '/BANKACCOUNT.asmx?WSDL',
|
'bankaccount' => $baseUrl . '/BANKACCOUNT.asmx?WSDL',
|
||||||
'card' => $baseUrl . '/CARD.asmx?WSDL',
|
'card' => $baseUrl . '/CARD.asmx?WSDL',
|
||||||
|
'kakaotalk' => $baseUrl . '/KAKAOTALK.asmx?WSDL',
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -314,6 +323,7 @@ protected function call(string $service, string $method, array $params = []): ar
|
|||||||
'ti' => $this->getTiClient(),
|
'ti' => $this->getTiClient(),
|
||||||
'bankaccount' => $this->getBankAccountClient(),
|
'bankaccount' => $this->getBankAccountClient(),
|
||||||
'card' => $this->getCardClient(),
|
'card' => $this->getCardClient(),
|
||||||
|
'kakaotalk' => $this->getKakaotalkClient(),
|
||||||
default => null,
|
default => null,
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -1158,4 +1168,439 @@ public function getHomeTaxUrl(string $corpNum, string $userId, string $userPwd):
|
|||||||
|
|
||||||
return $this->call('ti', 'GetHomeTaxURL', $params);
|
return $this->call('ti', 'GetHomeTaxURL', $params);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ========================================
|
||||||
|
// 카카오톡 서비스 (KAKAOTALK)
|
||||||
|
// ========================================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* KAKAOTALK SOAP 클라이언트 가져오기
|
||||||
|
*/
|
||||||
|
protected function getKakaotalkClient(): ?SoapClient
|
||||||
|
{
|
||||||
|
if ($this->kakaotalkClient === null) {
|
||||||
|
try {
|
||||||
|
$this->kakaotalkClient = new SoapClient($this->soapUrls['kakaotalk'], [
|
||||||
|
'trace' => true,
|
||||||
|
'encoding' => 'UTF-8',
|
||||||
|
'exceptions' => true,
|
||||||
|
'connection_timeout' => 30,
|
||||||
|
'cache_wsdl' => WSDL_CACHE_NONE,
|
||||||
|
]);
|
||||||
|
} catch (Throwable $e) {
|
||||||
|
Log::error('바로빌 KAKAOTALK SOAP 클라이언트 생성 실패', [
|
||||||
|
'error' => $e->getMessage(),
|
||||||
|
'url' => $this->soapUrls['kakaotalk'],
|
||||||
|
]);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->kakaotalkClient;
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- 채널 관리 ---
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 카카오톡 채널 목록 조회
|
||||||
|
*
|
||||||
|
* @param string $corpNum 회원사 사업자번호
|
||||||
|
*/
|
||||||
|
public function getKakaotalkChannels(string $corpNum): array
|
||||||
|
{
|
||||||
|
$params = [
|
||||||
|
'CorpNum' => $this->formatBizNo($corpNum),
|
||||||
|
];
|
||||||
|
|
||||||
|
return $this->call('kakaotalk', 'GetKakaotalkChannels', $params);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 카카오톡 채널 관리 URL 조회
|
||||||
|
*
|
||||||
|
* @param string $corpNum 회원사 사업자번호
|
||||||
|
* @param string $id 회원사 사용자 ID
|
||||||
|
*/
|
||||||
|
public function getKakaotalkChannelManagementUrl(string $corpNum, string $id): array
|
||||||
|
{
|
||||||
|
$params = [
|
||||||
|
'CorpNum' => $this->formatBizNo($corpNum),
|
||||||
|
'ID' => $id,
|
||||||
|
];
|
||||||
|
|
||||||
|
return $this->call('kakaotalk', 'GetKakaotalkChannelManagementURL', $params);
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- 템플릿 관리 ---
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 카카오톡 템플릿 목록 조회
|
||||||
|
*
|
||||||
|
* @param string $corpNum 회원사 사업자번호
|
||||||
|
* @param string $channelId 카카오톡 채널 ID
|
||||||
|
*/
|
||||||
|
public function getKakaotalkTemplates(string $corpNum, string $channelId): array
|
||||||
|
{
|
||||||
|
$params = [
|
||||||
|
'CorpNum' => $this->formatBizNo($corpNum),
|
||||||
|
'ChannelId' => $channelId,
|
||||||
|
];
|
||||||
|
|
||||||
|
return $this->call('kakaotalk', 'GetKakaotalkTemplates', $params);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 카카오톡 템플릿 관리 URL 조회
|
||||||
|
*
|
||||||
|
* @param string $corpNum 회원사 사업자번호
|
||||||
|
* @param string $id 회원사 사용자 ID
|
||||||
|
*/
|
||||||
|
public function getKakaotalkTemplateManagementUrl(string $corpNum, string $id): array
|
||||||
|
{
|
||||||
|
$params = [
|
||||||
|
'CorpNum' => $this->formatBizNo($corpNum),
|
||||||
|
'ID' => $id,
|
||||||
|
];
|
||||||
|
|
||||||
|
return $this->call('kakaotalk', 'GetKakaotalkTemplateManagementURL', $params);
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- 알림톡 발송 ---
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 알림톡 단건 발송
|
||||||
|
*
|
||||||
|
* @param string $corpNum 회원사 사업자번호
|
||||||
|
* @param string $senderId 발신프로필 키 (채널 ID)
|
||||||
|
* @param string $templateName 템플릿 이름
|
||||||
|
* @param string $receiverName 수신자 이름
|
||||||
|
* @param string $receiverNum 수신자 전화번호
|
||||||
|
* @param string $title 알림톡 제목
|
||||||
|
* @param string $message 알림톡 본문
|
||||||
|
* @param string $smsMessage SMS 대체발송 메시지 (빈 값이면 대체발송 안 함)
|
||||||
|
* @param string $smsSubject SMS 대체발송 제목
|
||||||
|
* @param string $reserveDT 예약일시 (YYYYMMDDHHmmss, 빈 값이면 즉시발송)
|
||||||
|
*/
|
||||||
|
public function sendATKakaotalk(
|
||||||
|
string $corpNum,
|
||||||
|
string $senderId,
|
||||||
|
string $templateName,
|
||||||
|
string $receiverName,
|
||||||
|
string $receiverNum,
|
||||||
|
string $title,
|
||||||
|
string $message,
|
||||||
|
string $smsMessage = '',
|
||||||
|
string $smsSubject = '',
|
||||||
|
string $reserveDT = ''
|
||||||
|
): array {
|
||||||
|
$params = [
|
||||||
|
'CorpNum' => $this->formatBizNo($corpNum),
|
||||||
|
'SenderID' => $senderId,
|
||||||
|
'TemplateName' => $templateName,
|
||||||
|
'ReceiverName' => $receiverName,
|
||||||
|
'ReceiverNum' => $receiverNum,
|
||||||
|
'Title' => $title,
|
||||||
|
'Message' => $message,
|
||||||
|
'SmsMessage' => $smsMessage,
|
||||||
|
'SmsSubject' => $smsSubject,
|
||||||
|
'ReserveDT' => $reserveDT,
|
||||||
|
];
|
||||||
|
|
||||||
|
return $this->call('kakaotalk', 'SendATKakaotalk', $params);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 알림톡 단건 발송 (버튼 포함)
|
||||||
|
*
|
||||||
|
* @param string $corpNum 회원사 사업자번호
|
||||||
|
* @param string $senderId 발신프로필 키
|
||||||
|
* @param string $templateName 템플릿 이름
|
||||||
|
* @param string $receiverName 수신자 이름
|
||||||
|
* @param string $receiverNum 수신자 전화번호
|
||||||
|
* @param string $title 알림톡 제목
|
||||||
|
* @param string $message 알림톡 본문
|
||||||
|
* @param array $buttons 버튼 배열 [{Name, ButtonType, Url1, Url2}]
|
||||||
|
* @param string $smsMessage SMS 대체발송 메시지
|
||||||
|
* @param string $smsSubject SMS 대체발송 제목
|
||||||
|
* @param string $reserveDT 예약일시
|
||||||
|
*/
|
||||||
|
public function sendATKakaotalkEx(
|
||||||
|
string $corpNum,
|
||||||
|
string $senderId,
|
||||||
|
string $templateName,
|
||||||
|
string $receiverName,
|
||||||
|
string $receiverNum,
|
||||||
|
string $title,
|
||||||
|
string $message,
|
||||||
|
array $buttons = [],
|
||||||
|
string $smsMessage = '',
|
||||||
|
string $smsSubject = '',
|
||||||
|
string $reserveDT = ''
|
||||||
|
): array {
|
||||||
|
$params = [
|
||||||
|
'CorpNum' => $this->formatBizNo($corpNum),
|
||||||
|
'SenderID' => $senderId,
|
||||||
|
'TemplateName' => $templateName,
|
||||||
|
'ReceiverName' => $receiverName,
|
||||||
|
'ReceiverNum' => $receiverNum,
|
||||||
|
'Title' => $title,
|
||||||
|
'Message' => $message,
|
||||||
|
'Buttons' => $buttons,
|
||||||
|
'SmsMessage' => $smsMessage,
|
||||||
|
'SmsSubject' => $smsSubject,
|
||||||
|
'ReserveDT' => $reserveDT,
|
||||||
|
];
|
||||||
|
|
||||||
|
return $this->call('kakaotalk', 'SendATKakaotalkEx', $params);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 알림톡 대량 발송
|
||||||
|
*
|
||||||
|
* @param string $corpNum 회원사 사업자번호
|
||||||
|
* @param string $senderId 발신프로필 키
|
||||||
|
* @param string $templateName 템플릿 이름
|
||||||
|
* @param array $messages 메시지 배열 [{ReceiverName, ReceiverNum, Title, Message, SmsMessage, SmsSubject}]
|
||||||
|
* @param string $reserveDT 예약일시
|
||||||
|
*/
|
||||||
|
public function sendATKakaotalks(
|
||||||
|
string $corpNum,
|
||||||
|
string $senderId,
|
||||||
|
string $templateName,
|
||||||
|
array $messages,
|
||||||
|
string $reserveDT = ''
|
||||||
|
): array {
|
||||||
|
$params = [
|
||||||
|
'CorpNum' => $this->formatBizNo($corpNum),
|
||||||
|
'SenderID' => $senderId,
|
||||||
|
'TemplateName' => $templateName,
|
||||||
|
'Messages' => $messages,
|
||||||
|
'ReserveDT' => $reserveDT,
|
||||||
|
];
|
||||||
|
|
||||||
|
return $this->call('kakaotalk', 'SendATKakaotalks', $params);
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- 친구톡 발송 ---
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 친구톡 텍스트 단건 발송
|
||||||
|
*
|
||||||
|
* @param string $corpNum 회원사 사업자번호
|
||||||
|
* @param string $senderId 발신프로필 키
|
||||||
|
* @param string $channelId 채널 ID
|
||||||
|
* @param string $receiverName 수신자 이름
|
||||||
|
* @param string $receiverNum 수신자 전화번호
|
||||||
|
* @param string $message 메시지 내용
|
||||||
|
* @param array $buttons 버튼 배열
|
||||||
|
* @param string $smsMessage SMS 대체발송 메시지
|
||||||
|
* @param string $smsSubject SMS 대체발송 제목
|
||||||
|
* @param bool $adYn 광고 여부
|
||||||
|
* @param string $reserveDT 예약일시
|
||||||
|
*/
|
||||||
|
public function sendFTKakaotalk(
|
||||||
|
string $corpNum,
|
||||||
|
string $senderId,
|
||||||
|
string $channelId,
|
||||||
|
string $receiverName,
|
||||||
|
string $receiverNum,
|
||||||
|
string $message,
|
||||||
|
array $buttons = [],
|
||||||
|
string $smsMessage = '',
|
||||||
|
string $smsSubject = '',
|
||||||
|
bool $adYn = false,
|
||||||
|
string $reserveDT = ''
|
||||||
|
): array {
|
||||||
|
$params = [
|
||||||
|
'CorpNum' => $this->formatBizNo($corpNum),
|
||||||
|
'SenderID' => $senderId,
|
||||||
|
'ChannelId' => $channelId,
|
||||||
|
'ReceiverName' => $receiverName,
|
||||||
|
'ReceiverNum' => $receiverNum,
|
||||||
|
'Message' => $message,
|
||||||
|
'Buttons' => $buttons,
|
||||||
|
'SmsMessage' => $smsMessage,
|
||||||
|
'SmsSubject' => $smsSubject,
|
||||||
|
'AdYn' => $adYn ? 'Y' : 'N',
|
||||||
|
'ReserveDT' => $reserveDT,
|
||||||
|
];
|
||||||
|
|
||||||
|
return $this->call('kakaotalk', 'SendFTKakaotalk', $params);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 친구톡 대량 발송
|
||||||
|
*
|
||||||
|
* @param string $corpNum 회원사 사업자번호
|
||||||
|
* @param string $senderId 발신프로필 키
|
||||||
|
* @param string $channelId 채널 ID
|
||||||
|
* @param array $messages 메시지 배열
|
||||||
|
* @param bool $adYn 광고 여부
|
||||||
|
* @param string $reserveDT 예약일시
|
||||||
|
*/
|
||||||
|
public function sendFTKakaotalks(
|
||||||
|
string $corpNum,
|
||||||
|
string $senderId,
|
||||||
|
string $channelId,
|
||||||
|
array $messages,
|
||||||
|
bool $adYn = false,
|
||||||
|
string $reserveDT = ''
|
||||||
|
): array {
|
||||||
|
$params = [
|
||||||
|
'CorpNum' => $this->formatBizNo($corpNum),
|
||||||
|
'SenderID' => $senderId,
|
||||||
|
'ChannelId' => $channelId,
|
||||||
|
'Messages' => $messages,
|
||||||
|
'AdYn' => $adYn ? 'Y' : 'N',
|
||||||
|
'ReserveDT' => $reserveDT,
|
||||||
|
];
|
||||||
|
|
||||||
|
return $this->call('kakaotalk', 'SendFTKakaotalks', $params);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 친구톡 이미지 발송
|
||||||
|
*
|
||||||
|
* @param string $corpNum 회원사 사업자번호
|
||||||
|
* @param string $senderId 발신프로필 키
|
||||||
|
* @param string $channelId 채널 ID
|
||||||
|
* @param string $receiverName 수신자 이름
|
||||||
|
* @param string $receiverNum 수신자 전화번호
|
||||||
|
* @param string $message 메시지 내용
|
||||||
|
* @param string $imageUrl 이미지 URL
|
||||||
|
* @param array $buttons 버튼 배열
|
||||||
|
* @param string $smsMessage SMS 대체발송 메시지
|
||||||
|
* @param string $smsSubject SMS 대체발송 제목
|
||||||
|
* @param bool $adYn 광고 여부
|
||||||
|
* @param string $reserveDT 예약일시
|
||||||
|
*/
|
||||||
|
public function sendFIKakaotalk(
|
||||||
|
string $corpNum,
|
||||||
|
string $senderId,
|
||||||
|
string $channelId,
|
||||||
|
string $receiverName,
|
||||||
|
string $receiverNum,
|
||||||
|
string $message,
|
||||||
|
string $imageUrl,
|
||||||
|
array $buttons = [],
|
||||||
|
string $smsMessage = '',
|
||||||
|
string $smsSubject = '',
|
||||||
|
bool $adYn = false,
|
||||||
|
string $reserveDT = ''
|
||||||
|
): array {
|
||||||
|
$params = [
|
||||||
|
'CorpNum' => $this->formatBizNo($corpNum),
|
||||||
|
'SenderID' => $senderId,
|
||||||
|
'ChannelId' => $channelId,
|
||||||
|
'ReceiverName' => $receiverName,
|
||||||
|
'ReceiverNum' => $receiverNum,
|
||||||
|
'Message' => $message,
|
||||||
|
'ImageURL' => $imageUrl,
|
||||||
|
'Buttons' => $buttons,
|
||||||
|
'SmsMessage' => $smsMessage,
|
||||||
|
'SmsSubject' => $smsSubject,
|
||||||
|
'AdYn' => $adYn ? 'Y' : 'N',
|
||||||
|
'ReserveDT' => $reserveDT,
|
||||||
|
];
|
||||||
|
|
||||||
|
return $this->call('kakaotalk', 'SendFIKakaotalk', $params);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 친구톡 와이드 이미지 발송
|
||||||
|
*
|
||||||
|
* @param string $corpNum 회원사 사업자번호
|
||||||
|
* @param string $senderId 발신프로필 키
|
||||||
|
* @param string $channelId 채널 ID
|
||||||
|
* @param string $receiverName 수신자 이름
|
||||||
|
* @param string $receiverNum 수신자 전화번호
|
||||||
|
* @param string $message 메시지 내용
|
||||||
|
* @param string $imageUrl 와이드 이미지 URL
|
||||||
|
* @param array $buttons 버튼 배열
|
||||||
|
* @param string $smsMessage SMS 대체발송 메시지
|
||||||
|
* @param string $smsSubject SMS 대체발송 제목
|
||||||
|
* @param bool $adYn 광고 여부
|
||||||
|
* @param string $reserveDT 예약일시
|
||||||
|
*/
|
||||||
|
public function sendFWKakaotalk(
|
||||||
|
string $corpNum,
|
||||||
|
string $senderId,
|
||||||
|
string $channelId,
|
||||||
|
string $receiverName,
|
||||||
|
string $receiverNum,
|
||||||
|
string $message,
|
||||||
|
string $imageUrl,
|
||||||
|
array $buttons = [],
|
||||||
|
string $smsMessage = '',
|
||||||
|
string $smsSubject = '',
|
||||||
|
bool $adYn = false,
|
||||||
|
string $reserveDT = ''
|
||||||
|
): array {
|
||||||
|
$params = [
|
||||||
|
'CorpNum' => $this->formatBizNo($corpNum),
|
||||||
|
'SenderID' => $senderId,
|
||||||
|
'ChannelId' => $channelId,
|
||||||
|
'ReceiverName' => $receiverName,
|
||||||
|
'ReceiverNum' => $receiverNum,
|
||||||
|
'Message' => $message,
|
||||||
|
'ImageURL' => $imageUrl,
|
||||||
|
'Buttons' => $buttons,
|
||||||
|
'SmsMessage' => $smsMessage,
|
||||||
|
'SmsSubject' => $smsSubject,
|
||||||
|
'AdYn' => $adYn ? 'Y' : 'N',
|
||||||
|
'ReserveDT' => $reserveDT,
|
||||||
|
];
|
||||||
|
|
||||||
|
return $this->call('kakaotalk', 'SendFWKakaotalk', $params);
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- 전송 조회/관리 ---
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 전송 결과 조회 (단건)
|
||||||
|
*
|
||||||
|
* @param string $corpNum 회원사 사업자번호
|
||||||
|
* @param string $sendKey 전송키
|
||||||
|
*/
|
||||||
|
public function getSendKakaotalk(string $corpNum, string $sendKey): array
|
||||||
|
{
|
||||||
|
$params = [
|
||||||
|
'CorpNum' => $this->formatBizNo($corpNum),
|
||||||
|
'SendKey' => $sendKey,
|
||||||
|
];
|
||||||
|
|
||||||
|
return $this->call('kakaotalk', 'GetSendKakaotalk', $params);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 전송 결과 조회 (다건)
|
||||||
|
*
|
||||||
|
* @param string $corpNum 회원사 사업자번호
|
||||||
|
* @param array $sendKeyList 전송키 배열
|
||||||
|
*/
|
||||||
|
public function getSendKakaotalks(string $corpNum, array $sendKeyList): array
|
||||||
|
{
|
||||||
|
$params = [
|
||||||
|
'CorpNum' => $this->formatBizNo($corpNum),
|
||||||
|
'SendKeyList' => $sendKeyList,
|
||||||
|
];
|
||||||
|
|
||||||
|
return $this->call('kakaotalk', 'GetSendKakaotalks', $params);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 예약 전송 취소
|
||||||
|
*
|
||||||
|
* @param string $corpNum 회원사 사업자번호
|
||||||
|
* @param string $sendKey 전송키
|
||||||
|
*/
|
||||||
|
public function cancelReservedKakaotalk(string $corpNum, string $sendKey): array
|
||||||
|
{
|
||||||
|
$params = [
|
||||||
|
'CorpNum' => $this->formatBizNo($corpNum),
|
||||||
|
'SendKey' => $sendKey,
|
||||||
|
];
|
||||||
|
|
||||||
|
return $this->call('kakaotalk', 'CancelReservedKakaotalk', $params);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
103
resources/views/barobill/kakaotalk/channels/index.blade.php
Normal file
103
resources/views/barobill/kakaotalk/channels/index.blade.php
Normal file
@@ -0,0 +1,103 @@
|
|||||||
|
@extends('layouts.app')
|
||||||
|
|
||||||
|
@section('title', '카카오톡 채널 관리')
|
||||||
|
|
||||||
|
@section('content')
|
||||||
|
<div class="flex flex-col h-full">
|
||||||
|
<!-- 페이지 헤더 -->
|
||||||
|
<div class="flex flex-col sm:flex-row sm:justify-between sm:items-center gap-4 mb-6 flex-shrink-0">
|
||||||
|
<div>
|
||||||
|
<div class="flex items-center gap-2 text-sm text-gray-500 mb-1">
|
||||||
|
<a href="{{ route('barobill.kakaotalk.index') }}" class="hover:text-gray-700">카카오톡</a>
|
||||||
|
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7" /></svg>
|
||||||
|
<span class="text-gray-700">채널 관리</span>
|
||||||
|
</div>
|
||||||
|
<h1 class="text-2xl font-bold text-gray-800">채널 관리</h1>
|
||||||
|
<p class="text-sm text-gray-500 mt-1">카카오톡 채널 목록 조회 및 바로빌 채널 관리 페이지 연결</p>
|
||||||
|
</div>
|
||||||
|
<div class="flex gap-2">
|
||||||
|
<button type="button" onclick="loadChannels()" class="px-4 py-2 bg-gray-100 text-gray-700 rounded-lg hover:bg-gray-200 transition text-sm flex items-center gap-2">
|
||||||
|
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15" /></svg>
|
||||||
|
새로고침
|
||||||
|
</button>
|
||||||
|
<button type="button" onclick="openChannelManagement()" class="px-4 py-2 bg-yellow-500 text-white rounded-lg hover:bg-yellow-600 transition text-sm flex items-center gap-2">
|
||||||
|
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 6H6a2 2 0 00-2 2v10a2 2 0 002 2h10a2 2 0 002-2v-4M14 4h6m0 0v6m0-6L10 14" /></svg>
|
||||||
|
채널 관리 (바로빌)
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 채널 목록 -->
|
||||||
|
<div class="bg-white rounded-xl shadow-sm border border-gray-100">
|
||||||
|
<div id="channels-list" class="p-6">
|
||||||
|
<div class="text-center py-12 text-gray-400">
|
||||||
|
<svg class="w-10 h-10 mx-auto mb-3 animate-spin" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15" /></svg>
|
||||||
|
<p class="text-sm">채널 목록 로딩 중...</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
@endsection
|
||||||
|
|
||||||
|
@push('scripts')
|
||||||
|
<script>
|
||||||
|
function loadChannels() {
|
||||||
|
const container = document.getElementById('channels-list');
|
||||||
|
container.innerHTML = '<div class="text-center py-12 text-gray-400"><svg class="w-10 h-10 mx-auto mb-3 animate-spin" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15" /></svg><p class="text-sm">채널 목록 로딩 중...</p></div>';
|
||||||
|
|
||||||
|
fetch('/api/admin/barobill/kakaotalk/channels', {
|
||||||
|
headers: { 'Accept': 'application/json', 'X-Requested-With': 'XMLHttpRequest' }
|
||||||
|
})
|
||||||
|
.then(r => r.json())
|
||||||
|
.then(data => {
|
||||||
|
if (!data.success) {
|
||||||
|
container.innerHTML = '<div class="text-center py-12"><p class="text-sm text-red-500">' + (data.error || data.message) + '</p></div>';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const raw = data.data;
|
||||||
|
let items = [];
|
||||||
|
if (Array.isArray(raw)) items = raw;
|
||||||
|
else if (raw && raw.KakaotalkChannel) items = Array.isArray(raw.KakaotalkChannel) ? raw.KakaotalkChannel : [raw.KakaotalkChannel];
|
||||||
|
else if (raw) items = [raw];
|
||||||
|
|
||||||
|
if (items.length === 0) {
|
||||||
|
container.innerHTML = '<div class="text-center py-12 text-gray-400"><svg class="w-10 h-10 mx-auto mb-3" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M20 13V6a2 2 0 00-2-2H6a2 2 0 00-2 2v7m16 0v5a2 2 0 01-2 2H6a2 2 0 01-2-2v-5m16 0h-2.586a1 1 0 00-.707.293l-2.414 2.414a1 1 0 01-.707.293h-3.172a1 1 0 01-.707-.293l-2.414-2.414A1 1 0 006.586 13H4" /></svg><p class="text-sm">등록된 채널이 없습니다.</p><p class="text-xs mt-1">바로빌 채널 관리 페이지에서 채널을 등록해주세요.</p></div>';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let html = '<div class="overflow-x-auto"><table class="w-full text-sm"><thead><tr class="border-b border-gray-200"><th class="text-left py-3 px-4 font-medium text-gray-600">채널 ID</th><th class="text-left py-3 px-4 font-medium text-gray-600">채널명</th><th class="text-center py-3 px-4 font-medium text-gray-600">상태</th></tr></thead><tbody>';
|
||||||
|
items.forEach(ch => {
|
||||||
|
const statusColor = ch.Status === '정상' || ch.Status === 'Y' ? 'green' : 'gray';
|
||||||
|
html += '<tr class="border-b border-gray-50 hover:bg-gray-50">';
|
||||||
|
html += '<td class="py-3 px-4 font-mono text-xs">' + (ch.ChannelId || '-') + '</td>';
|
||||||
|
html += '<td class="py-3 px-4 font-medium">' + (ch.ChannelName || '-') + '</td>';
|
||||||
|
html += '<td class="py-3 px-4 text-center"><span class="px-2 py-0.5 rounded-full text-xs font-medium bg-' + statusColor + '-100 text-' + statusColor + '-700">' + (ch.Status || '-') + '</span></td>';
|
||||||
|
html += '</tr>';
|
||||||
|
});
|
||||||
|
html += '</tbody></table></div>';
|
||||||
|
container.innerHTML = html;
|
||||||
|
})
|
||||||
|
.catch(err => {
|
||||||
|
container.innerHTML = '<div class="text-center py-12"><p class="text-sm text-red-500">조회 오류: ' + err.message + '</p></div>';
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function openChannelManagement() {
|
||||||
|
fetch('/api/admin/barobill/kakaotalk/channels/management-url', {
|
||||||
|
headers: { 'Accept': 'application/json', 'X-Requested-With': 'XMLHttpRequest' }
|
||||||
|
})
|
||||||
|
.then(r => r.json())
|
||||||
|
.then(data => {
|
||||||
|
if (data.success && data.data) {
|
||||||
|
window.open(data.data, '_blank', 'width=1200,height=800');
|
||||||
|
} else {
|
||||||
|
alert(data.error || data.message || '관리 URL 조회 실패');
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(err => alert('오류: ' + err.message));
|
||||||
|
}
|
||||||
|
|
||||||
|
document.addEventListener('DOMContentLoaded', loadChannels);
|
||||||
|
</script>
|
||||||
|
@endpush
|
||||||
170
resources/views/barobill/kakaotalk/history/index.blade.php
Normal file
170
resources/views/barobill/kakaotalk/history/index.blade.php
Normal file
@@ -0,0 +1,170 @@
|
|||||||
|
@extends('layouts.app')
|
||||||
|
|
||||||
|
@section('title', '카카오톡 전송내역')
|
||||||
|
|
||||||
|
@section('content')
|
||||||
|
<div class="flex flex-col h-full">
|
||||||
|
<!-- 페이지 헤더 -->
|
||||||
|
<div class="flex flex-col sm:flex-row sm:justify-between sm:items-center gap-4 mb-6 flex-shrink-0">
|
||||||
|
<div>
|
||||||
|
<div class="flex items-center gap-2 text-sm text-gray-500 mb-1">
|
||||||
|
<a href="{{ route('barobill.kakaotalk.index') }}" class="hover:text-gray-700">카카오톡</a>
|
||||||
|
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7" /></svg>
|
||||||
|
<span class="text-gray-700">전송내역</span>
|
||||||
|
</div>
|
||||||
|
<h1 class="text-2xl font-bold text-gray-800">전송내역</h1>
|
||||||
|
<p class="text-sm text-gray-500 mt-1">카카오톡 발송 이력 조회 및 예약 관리</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
@if(!$barobillMember)
|
||||||
|
<div class="bg-yellow-50 border border-yellow-200 rounded-lg p-6 text-center">
|
||||||
|
<h3 class="text-lg font-semibold text-yellow-800 mb-1">바로빌 회원사 연동 필요</h3>
|
||||||
|
<p class="text-sm text-yellow-600">전송내역을 조회하려면 먼저 바로빌 회원사를 등록해주세요.</p>
|
||||||
|
</div>
|
||||||
|
@else
|
||||||
|
<!-- 조회 필터 -->
|
||||||
|
<div class="bg-white rounded-xl shadow-sm border border-gray-100 p-6 mb-6">
|
||||||
|
<h3 class="font-semibold text-gray-800 mb-4">전송키로 조회</h3>
|
||||||
|
<div class="flex gap-3">
|
||||||
|
<input type="text" id="search-send-key" placeholder="전송키를 입력하세요" class="flex-1 px-3 py-2 border border-gray-200 rounded-lg text-sm focus:ring-2 focus:ring-yellow-500">
|
||||||
|
<button type="button" onclick="searchBySendKey()" class="px-4 py-2 bg-yellow-500 text-white rounded-lg hover:bg-yellow-600 transition text-sm flex items-center gap-2">
|
||||||
|
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z" /></svg>
|
||||||
|
조회
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<p class="text-xs text-gray-500 mt-2">여러 건 조회 시 전송키를 쉼표(,)로 구분하세요.</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 조회 결과 -->
|
||||||
|
<div class="bg-white rounded-xl shadow-sm border border-gray-100">
|
||||||
|
<div id="history-result" class="p-6">
|
||||||
|
<div class="text-center py-12 text-gray-400">
|
||||||
|
<svg class="w-12 h-12 mx-auto mb-3" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2m-3 7h3m-3 4h3m-6-4h.01M9 16h.01" />
|
||||||
|
</svg>
|
||||||
|
<p class="text-sm">전송키를 입력하여 발송 결과를 조회하세요.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
@endif
|
||||||
|
</div>
|
||||||
|
@endsection
|
||||||
|
|
||||||
|
@push('scripts')
|
||||||
|
<script>
|
||||||
|
function searchBySendKey() {
|
||||||
|
const input = document.getElementById('search-send-key').value.trim();
|
||||||
|
const container = document.getElementById('history-result');
|
||||||
|
|
||||||
|
if (!input) {
|
||||||
|
alert('전송키를 입력해주세요.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
container.innerHTML = '<div class="text-center py-12 text-gray-400"><svg class="w-10 h-10 mx-auto mb-3 animate-spin" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15" /></svg><p class="text-sm">조회 중...</p></div>';
|
||||||
|
|
||||||
|
const keys = input.split(',').map(k => k.trim()).filter(k => k);
|
||||||
|
|
||||||
|
if (keys.length === 1) {
|
||||||
|
// 단건 조회
|
||||||
|
fetch('/api/admin/barobill/kakaotalk/send/' + encodeURIComponent(keys[0]), {
|
||||||
|
headers: { 'Accept': 'application/json', 'X-Requested-With': 'XMLHttpRequest' }
|
||||||
|
})
|
||||||
|
.then(r => r.json())
|
||||||
|
.then(data => renderResult(data, container))
|
||||||
|
.catch(err => { container.innerHTML = '<div class="text-center py-12"><p class="text-sm text-red-500">조회 오류: ' + err.message + '</p></div>'; });
|
||||||
|
} else {
|
||||||
|
// 다건 조회
|
||||||
|
fetch('/api/admin/barobill/kakaotalk/send/results', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json', 'Accept': 'application/json', 'X-Requested-With': 'XMLHttpRequest', 'X-CSRF-TOKEN': document.querySelector('meta[name="csrf-token"]')?.content },
|
||||||
|
body: JSON.stringify({ send_keys: keys }),
|
||||||
|
})
|
||||||
|
.then(r => r.json())
|
||||||
|
.then(data => renderResult(data, container))
|
||||||
|
.catch(err => { container.innerHTML = '<div class="text-center py-12"><p class="text-sm text-red-500">조회 오류: ' + err.message + '</p></div>'; });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderResult(data, container) {
|
||||||
|
if (!data.success) {
|
||||||
|
container.innerHTML = '<div class="text-center py-12"><p class="text-sm text-red-500">' + (data.error || data.message) + '</p></div>';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const raw = data.data;
|
||||||
|
let items = [];
|
||||||
|
|
||||||
|
// 단건 결과
|
||||||
|
if (raw && !Array.isArray(raw) && (raw.SendKey || raw.ReceiverNum)) {
|
||||||
|
items = [raw];
|
||||||
|
}
|
||||||
|
// 다건 결과
|
||||||
|
else if (Array.isArray(raw)) {
|
||||||
|
items = raw;
|
||||||
|
}
|
||||||
|
else if (raw && raw.KakaotalkSendResult) {
|
||||||
|
items = Array.isArray(raw.KakaotalkSendResult) ? raw.KakaotalkSendResult : [raw.KakaotalkSendResult];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (items.length === 0) {
|
||||||
|
container.innerHTML = '<div class="text-center py-12 text-gray-400"><p class="text-sm">조회된 결과가 없습니다.</p></div>';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let html = '<div class="overflow-x-auto"><table class="w-full text-sm"><thead><tr class="border-b border-gray-200">';
|
||||||
|
html += '<th class="text-left py-3 px-4 font-medium text-gray-600">전송키</th>';
|
||||||
|
html += '<th class="text-left py-3 px-4 font-medium text-gray-600">수신번호</th>';
|
||||||
|
html += '<th class="text-left py-3 px-4 font-medium text-gray-600">수신자</th>';
|
||||||
|
html += '<th class="text-center py-3 px-4 font-medium text-gray-600">상태</th>';
|
||||||
|
html += '<th class="text-center py-3 px-4 font-medium text-gray-600">발송일시</th>';
|
||||||
|
html += '<th class="text-center py-3 px-4 font-medium text-gray-600">작업</th>';
|
||||||
|
html += '</tr></thead><tbody>';
|
||||||
|
|
||||||
|
items.forEach(item => {
|
||||||
|
const statusMap = { '0': '대기', '1': '전송중', '2': '성공', '3': '실패', '-1': '취소' };
|
||||||
|
const statusLabel = statusMap[String(item.Status)] || item.Status || item.ResultCode || '-';
|
||||||
|
const statusColor = item.Status == 2 ? 'green' : (item.Status == 3 || item.Status == -1 ? 'red' : 'yellow');
|
||||||
|
|
||||||
|
html += '<tr class="border-b border-gray-50 hover:bg-gray-50">';
|
||||||
|
html += '<td class="py-3 px-4 font-mono text-xs">' + (item.SendKey || '-') + '</td>';
|
||||||
|
html += '<td class="py-3 px-4">' + (item.ReceiverNum || '-') + '</td>';
|
||||||
|
html += '<td class="py-3 px-4">' + (item.ReceiverName || '-') + '</td>';
|
||||||
|
html += '<td class="py-3 px-4 text-center"><span class="px-2 py-0.5 rounded-full text-xs font-medium bg-' + statusColor + '-100 text-' + statusColor + '-700">' + statusLabel + '</span></td>';
|
||||||
|
html += '<td class="py-3 px-4 text-center text-xs text-gray-500">' + (item.SendDT || item.ReserveDT || '-') + '</td>';
|
||||||
|
html += '<td class="py-3 px-4 text-center">';
|
||||||
|
if (item.Status == 0 || String(item.Status) === '대기') {
|
||||||
|
html += '<button onclick="cancelReserved(\'' + item.SendKey + '\')" class="text-xs text-red-600 hover:text-red-800">예약취소</button>';
|
||||||
|
}
|
||||||
|
html += '</td></tr>';
|
||||||
|
});
|
||||||
|
html += '</tbody></table></div>';
|
||||||
|
container.innerHTML = html;
|
||||||
|
}
|
||||||
|
|
||||||
|
function cancelReserved(sendKey) {
|
||||||
|
if (!confirm('전송키 ' + sendKey + '의 예약을 취소하시겠습니까?')) return;
|
||||||
|
|
||||||
|
fetch('/api/admin/barobill/kakaotalk/send/' + encodeURIComponent(sendKey) + '/cancel', {
|
||||||
|
method: 'DELETE',
|
||||||
|
headers: { 'Accept': 'application/json', 'X-Requested-With': 'XMLHttpRequest', 'X-CSRF-TOKEN': document.querySelector('meta[name="csrf-token"]')?.content },
|
||||||
|
})
|
||||||
|
.then(r => r.json())
|
||||||
|
.then(data => {
|
||||||
|
if (data.success) {
|
||||||
|
alert('예약이 취소되었습니다.');
|
||||||
|
searchBySendKey();
|
||||||
|
} else {
|
||||||
|
alert('취소 실패: ' + (data.error || data.message));
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(err => alert('오류: ' + err.message));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Enter 키로 조회
|
||||||
|
document.getElementById('search-send-key')?.addEventListener('keypress', function(e) {
|
||||||
|
if (e.key === 'Enter') searchBySendKey();
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
@endpush
|
||||||
194
resources/views/barobill/kakaotalk/index.blade.php
Normal file
194
resources/views/barobill/kakaotalk/index.blade.php
Normal file
@@ -0,0 +1,194 @@
|
|||||||
|
@extends('layouts.app')
|
||||||
|
|
||||||
|
@section('title', '카카오톡 서비스')
|
||||||
|
|
||||||
|
@section('content')
|
||||||
|
<!-- 현재 테넌트 정보 카드 -->
|
||||||
|
@if($currentTenant)
|
||||||
|
<div class="rounded-xl shadow-lg p-5 mb-6" style="background: linear-gradient(to right, #fee500, #f5c800); color: #3c1e1e;">
|
||||||
|
<div class="flex flex-col lg:flex-row lg:items-center lg:justify-between gap-4">
|
||||||
|
<div class="flex items-center gap-4">
|
||||||
|
<div class="p-3 rounded-xl" style="background: rgba(0,0,0,0.1);">
|
||||||
|
<svg class="w-7 h-7" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 12h.01M12 12h.01M16 12h.01M21 12c0 4.418-4.03 8-9 8a9.863 9.863 0 01-4.255-.949L3 20l1.395-3.72C3.512 15.042 3 13.574 3 12c0-4.418 4.03-8 9-8s9 3.582 9 8z" />
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<div class="flex items-center gap-2 mb-1">
|
||||||
|
<span class="px-2 py-0.5 rounded-full text-xs font-bold" style="background: rgba(0,0,0,0.1);">T-ID: {{ $currentTenant->id }}</span>
|
||||||
|
</div>
|
||||||
|
<h2 class="text-xl font-bold">{{ $currentTenant->company_name }} - 카카오톡 서비스</h2>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
@if($barobillMember)
|
||||||
|
<div class="grid grid-cols-2 gap-3 text-sm">
|
||||||
|
<div class="rounded-lg p-2" style="background: rgba(0,0,0,0.05);">
|
||||||
|
<p class="text-xs" style="color: rgba(60,30,30,0.6);">사업자번호</p>
|
||||||
|
<p class="font-medium">{{ $barobillMember->biz_no }}</p>
|
||||||
|
</div>
|
||||||
|
<div class="rounded-lg p-2" style="background: rgba(0,0,0,0.05);">
|
||||||
|
<p class="text-xs" style="color: rgba(60,30,30,0.6);">바로빌 ID</p>
|
||||||
|
<p class="font-medium">{{ $barobillMember->barobill_id }}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
@else
|
||||||
|
<div class="flex items-center gap-2" style="color: #7c2d12;">
|
||||||
|
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z" />
|
||||||
|
</svg>
|
||||||
|
<span class="text-sm font-medium">바로빌 회원사 미연동</span>
|
||||||
|
</div>
|
||||||
|
@endif
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
@endif
|
||||||
|
|
||||||
|
<div class="flex flex-col h-full">
|
||||||
|
<!-- 페이지 헤더 -->
|
||||||
|
<div class="flex flex-col sm:flex-row sm:justify-between sm:items-center gap-4 mb-6 flex-shrink-0">
|
||||||
|
<div>
|
||||||
|
<h1 class="text-2xl font-bold text-gray-800">카카오톡 서비스</h1>
|
||||||
|
<p class="text-sm text-gray-500 mt-1">알림톡/친구톡 발송 및 관리</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
@if(!$barobillMember)
|
||||||
|
<div class="bg-yellow-50 border border-yellow-200 rounded-lg p-6 text-center">
|
||||||
|
<svg class="w-12 h-12 text-yellow-400 mx-auto mb-3" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z" />
|
||||||
|
</svg>
|
||||||
|
<h3 class="text-lg font-semibold text-yellow-800 mb-1">바로빌 회원사 연동 필요</h3>
|
||||||
|
<p class="text-sm text-yellow-600">카카오톡 서비스를 이용하려면 먼저 바로빌 회원사를 등록해주세요.</p>
|
||||||
|
<a href="{{ route('barobill.members.index') }}" class="inline-block mt-4 px-4 py-2 bg-yellow-600 text-white rounded-lg hover:bg-yellow-700 transition text-sm">회원사 관리로 이동</a>
|
||||||
|
</div>
|
||||||
|
@else
|
||||||
|
<!-- 빠른 메뉴 카드 -->
|
||||||
|
<div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-6 mb-8">
|
||||||
|
<!-- 채널 관리 -->
|
||||||
|
<a href="{{ route('barobill.kakaotalk.channels') }}" class="bg-white rounded-xl shadow-sm p-6 hover:shadow-md transition group border border-gray-100">
|
||||||
|
<div class="w-12 h-12 bg-blue-100 rounded-xl flex items-center justify-center mb-4 group-hover:bg-blue-200 transition">
|
||||||
|
<svg class="w-6 h-6 text-blue-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 11H5m14 0a2 2 0 012 2v6a2 2 0 01-2 2H5a2 2 0 01-2-2v-6a2 2 0 012-2m14 0V9a2 2 0 00-2-2M5 11V9a2 2 0 012-2m0 0V5a2 2 0 012-2h6a2 2 0 012 2v2M7 7h10" />
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<h3 class="font-semibold text-gray-800 mb-1">채널 관리</h3>
|
||||||
|
<p class="text-xs text-gray-500">카카오톡 채널 목록 조회 및 관리</p>
|
||||||
|
</a>
|
||||||
|
|
||||||
|
<!-- 템플릿 관리 -->
|
||||||
|
<a href="{{ route('barobill.kakaotalk.templates') }}" class="bg-white rounded-xl shadow-sm p-6 hover:shadow-md transition group border border-gray-100">
|
||||||
|
<div class="w-12 h-12 bg-green-100 rounded-xl flex items-center justify-center mb-4 group-hover:bg-green-200 transition">
|
||||||
|
<svg class="w-6 h-6 text-green-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 5a1 1 0 011-1h14a1 1 0 011 1v2a1 1 0 01-1 1H5a1 1 0 01-1-1V5zM4 13a1 1 0 011-1h6a1 1 0 011 1v6a1 1 0 01-1 1H5a1 1 0 01-1-1v-6zM16 13a1 1 0 011-1h2a1 1 0 011 1v6a1 1 0 01-1 1h-2a1 1 0 01-1-1v-6z" />
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<h3 class="font-semibold text-gray-800 mb-1">템플릿 관리</h3>
|
||||||
|
<p class="text-xs text-gray-500">알림톡 템플릿 목록 조회 및 관리</p>
|
||||||
|
</a>
|
||||||
|
|
||||||
|
<!-- 발송 -->
|
||||||
|
<a href="{{ route('barobill.kakaotalk.send') }}" class="bg-white rounded-xl shadow-sm p-6 hover:shadow-md transition group border border-gray-100">
|
||||||
|
<div class="w-12 h-12 bg-purple-100 rounded-xl flex items-center justify-center mb-4 group-hover:bg-purple-200 transition">
|
||||||
|
<svg class="w-6 h-6 text-purple-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 19l9 2-9-18-9 18 9-2zm0 0v-8" />
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<h3 class="font-semibold text-gray-800 mb-1">메시지 발송</h3>
|
||||||
|
<p class="text-xs text-gray-500">알림톡/친구톡 메시지 발송</p>
|
||||||
|
</a>
|
||||||
|
|
||||||
|
<!-- 전송내역 -->
|
||||||
|
<a href="{{ route('barobill.kakaotalk.history') }}" class="bg-white rounded-xl shadow-sm p-6 hover:shadow-md transition group border border-gray-100">
|
||||||
|
<div class="w-12 h-12 bg-orange-100 rounded-xl flex items-center justify-center mb-4 group-hover:bg-orange-200 transition">
|
||||||
|
<svg class="w-6 h-6 text-orange-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2m-3 7h3m-3 4h3m-6-4h.01M9 16h.01" />
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<h3 class="font-semibold text-gray-800 mb-1">전송내역</h3>
|
||||||
|
<p class="text-xs text-gray-500">발송 이력 조회 및 예약 관리</p>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 채널 상태 -->
|
||||||
|
<div class="bg-white rounded-xl shadow-sm p-6 border border-gray-100">
|
||||||
|
<div class="flex items-center justify-between mb-4">
|
||||||
|
<div class="flex items-center gap-3">
|
||||||
|
<div class="w-10 h-10 bg-yellow-100 rounded-lg flex items-center justify-center">
|
||||||
|
<svg class="w-5 h-5 text-yellow-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 12h.01M12 12h.01M16 12h.01M21 12c0 4.418-4.03 8-9 8a9.863 9.863 0 01-4.255-.949L3 20l1.395-3.72C3.512 15.042 3 13.574 3 12c0-4.418 4.03-8 9-8s9 3.582 9 8z" />
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<h3 class="font-semibold text-gray-800">채널 현황</h3>
|
||||||
|
<p class="text-xs text-gray-500">등록된 카카오톡 채널 목록</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<button type="button" onclick="loadChannels()" class="px-3 py-1.5 bg-gray-100 text-gray-700 rounded-lg hover:bg-gray-200 transition text-sm flex items-center gap-1">
|
||||||
|
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15" />
|
||||||
|
</svg>
|
||||||
|
새로고침
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div id="channels-container">
|
||||||
|
<div class="text-center py-8 text-gray-400">
|
||||||
|
<svg class="w-8 h-8 mx-auto mb-2 animate-spin" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15" />
|
||||||
|
</svg>
|
||||||
|
<p class="text-sm">채널 정보 로딩 중...</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
@endif
|
||||||
|
</div>
|
||||||
|
@endsection
|
||||||
|
|
||||||
|
@push('scripts')
|
||||||
|
<script>
|
||||||
|
function loadChannels() {
|
||||||
|
const container = document.getElementById('channels-container');
|
||||||
|
if (!container) return;
|
||||||
|
|
||||||
|
container.innerHTML = '<div class="text-center py-8 text-gray-400"><svg class="w-8 h-8 mx-auto mb-2 animate-spin" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15" /></svg><p class="text-sm">채널 정보 로딩 중...</p></div>';
|
||||||
|
|
||||||
|
fetch('/api/admin/barobill/kakaotalk/channels', {
|
||||||
|
headers: { 'Accept': 'application/json', 'X-Requested-With': 'XMLHttpRequest' }
|
||||||
|
})
|
||||||
|
.then(r => r.json())
|
||||||
|
.then(data => {
|
||||||
|
if (!data.success) {
|
||||||
|
container.innerHTML = '<div class="text-center py-8 text-red-400"><p class="text-sm">' + (data.error || data.message || '채널 조회 실패') + '</p></div>';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const channels = data.data;
|
||||||
|
if (!channels || (Array.isArray(channels) && channels.length === 0)) {
|
||||||
|
container.innerHTML = '<div class="text-center py-8 text-gray-400"><p class="text-sm">등록된 채널이 없습니다.</p></div>';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let items = Array.isArray(channels) ? channels : (channels.KakaotalkChannel ? (Array.isArray(channels.KakaotalkChannel) ? channels.KakaotalkChannel : [channels.KakaotalkChannel]) : [channels]);
|
||||||
|
|
||||||
|
let html = '<div class="divide-y divide-gray-100">';
|
||||||
|
items.forEach(ch => {
|
||||||
|
const statusColor = ch.Status === '정상' || ch.Status === 'Y' ? 'green' : 'gray';
|
||||||
|
html += '<div class="flex items-center justify-between py-3">';
|
||||||
|
html += '<div class="flex items-center gap-3">';
|
||||||
|
html += '<div class="w-8 h-8 bg-yellow-100 rounded-full flex items-center justify-center"><svg class="w-4 h-4 text-yellow-600" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 12h.01M12 12h.01M16 12h.01M21 12c0 4.418-4.03 8-9 8a9.863 9.863 0 01-4.255-.949L3 20l1.395-3.72C3.512 15.042 3 13.574 3 12c0-4.418 4.03-8 9-8s9 3.582 9 8z" /></svg></div>';
|
||||||
|
html += '<div><p class="text-sm font-medium text-gray-800">' + (ch.ChannelName || ch.ChannelId || '-') + '</p>';
|
||||||
|
html += '<p class="text-xs text-gray-500">ID: ' + (ch.ChannelId || '-') + '</p></div>';
|
||||||
|
html += '</div>';
|
||||||
|
html += '<span class="px-2 py-0.5 rounded-full text-xs font-medium bg-' + statusColor + '-100 text-' + statusColor + '-700">' + (ch.Status || '-') + '</span>';
|
||||||
|
html += '</div>';
|
||||||
|
});
|
||||||
|
html += '</div>';
|
||||||
|
container.innerHTML = html;
|
||||||
|
})
|
||||||
|
.catch(err => {
|
||||||
|
container.innerHTML = '<div class="text-center py-8 text-red-400"><p class="text-sm">채널 조회 중 오류 발생: ' + err.message + '</p></div>';
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
document.addEventListener('DOMContentLoaded', loadChannels);
|
||||||
|
</script>
|
||||||
|
@endpush
|
||||||
421
resources/views/barobill/kakaotalk/send/index.blade.php
Normal file
421
resources/views/barobill/kakaotalk/send/index.blade.php
Normal file
@@ -0,0 +1,421 @@
|
|||||||
|
@extends('layouts.app')
|
||||||
|
|
||||||
|
@section('title', '카카오톡 메시지 발송')
|
||||||
|
|
||||||
|
@section('content')
|
||||||
|
<div class="flex flex-col h-full">
|
||||||
|
<!-- 페이지 헤더 -->
|
||||||
|
<div class="flex flex-col sm:flex-row sm:justify-between sm:items-center gap-4 mb-6 flex-shrink-0">
|
||||||
|
<div>
|
||||||
|
<div class="flex items-center gap-2 text-sm text-gray-500 mb-1">
|
||||||
|
<a href="{{ route('barobill.kakaotalk.index') }}" class="hover:text-gray-700">카카오톡</a>
|
||||||
|
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7" /></svg>
|
||||||
|
<span class="text-gray-700">메시지 발송</span>
|
||||||
|
</div>
|
||||||
|
<h1 class="text-2xl font-bold text-gray-800">메시지 발송</h1>
|
||||||
|
<p class="text-sm text-gray-500 mt-1">알림톡 또는 친구톡 메시지를 발송합니다</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
@if(!$barobillMember)
|
||||||
|
<div class="bg-yellow-50 border border-yellow-200 rounded-lg p-6 text-center">
|
||||||
|
<h3 class="text-lg font-semibold text-yellow-800 mb-1">바로빌 회원사 연동 필요</h3>
|
||||||
|
<p class="text-sm text-yellow-600">메시지 발송을 위해 먼저 바로빌 회원사를 등록해주세요.</p>
|
||||||
|
</div>
|
||||||
|
@else
|
||||||
|
<!-- 탭 네비게이션 -->
|
||||||
|
<div class="flex border-b border-gray-200 mb-6">
|
||||||
|
<button type="button" onclick="switchTab('alimtalk')" id="tab-alimtalk" class="px-6 py-3 text-sm font-medium border-b-2 border-yellow-500 text-yellow-700 bg-yellow-50/50">알림톡</button>
|
||||||
|
<button type="button" onclick="switchTab('friendtalk')" id="tab-friendtalk" class="px-6 py-3 text-sm font-medium border-b-2 border-transparent text-gray-500 hover:text-gray-700">친구톡</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 알림톡 발송 폼 -->
|
||||||
|
<div id="panel-alimtalk" class="space-y-6">
|
||||||
|
<div class="bg-white rounded-xl shadow-sm border border-gray-100 p-6">
|
||||||
|
<h3 class="font-semibold text-gray-800 mb-4">알림톡 발송</h3>
|
||||||
|
<form id="alimtalk-form" class="space-y-4">
|
||||||
|
<div class="grid grid-cols-1 lg:grid-cols-2 gap-4">
|
||||||
|
<!-- 채널/템플릿 선택 -->
|
||||||
|
<div>
|
||||||
|
<label class="block text-sm font-medium text-gray-600 mb-1">채널 (발신프로필)</label>
|
||||||
|
<select name="sender_id" id="at-sender-id" onchange="loadTemplateOptions('at')" class="w-full px-3 py-2 border border-gray-200 rounded-lg text-sm focus:ring-2 focus:ring-yellow-500">
|
||||||
|
<option value="">로딩 중...</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label class="block text-sm font-medium text-gray-600 mb-1">템플릿</label>
|
||||||
|
<select name="template_name" id="at-template" onchange="fillTemplateContent('at')" class="w-full px-3 py-2 border border-gray-200 rounded-lg text-sm focus:ring-2 focus:ring-yellow-500">
|
||||||
|
<option value="">채널을 먼저 선택하세요</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<!-- 수신자 -->
|
||||||
|
<div class="grid grid-cols-1 lg:grid-cols-2 gap-4">
|
||||||
|
<div>
|
||||||
|
<label class="block text-sm font-medium text-gray-600 mb-1">수신자명</label>
|
||||||
|
<input type="text" name="receiver_name" class="w-full px-3 py-2 border border-gray-200 rounded-lg text-sm" placeholder="홍길동">
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label class="block text-sm font-medium text-gray-600 mb-1">수신번호</label>
|
||||||
|
<input type="tel" name="receiver_num" class="w-full px-3 py-2 border border-gray-200 rounded-lg text-sm" placeholder="01012345678">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<!-- 제목 / 메시지 -->
|
||||||
|
<div>
|
||||||
|
<label class="block text-sm font-medium text-gray-600 mb-1">제목</label>
|
||||||
|
<input type="text" name="title" class="w-full px-3 py-2 border border-gray-200 rounded-lg text-sm" placeholder="알림톡 제목">
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label class="block text-sm font-medium text-gray-600 mb-1">메시지 내용</label>
|
||||||
|
<textarea name="message" id="at-message" rows="6" class="w-full px-3 py-2 border border-gray-200 rounded-lg text-sm" placeholder="템플릿 선택 시 자동 입력됩니다"></textarea>
|
||||||
|
</div>
|
||||||
|
<!-- SMS 대체발송 -->
|
||||||
|
<div class="bg-gray-50 rounded-lg p-4 space-y-3">
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<input type="checkbox" id="at-sms-toggle" onchange="toggleSmsSection('at')" class="w-4 h-4 text-yellow-600 rounded">
|
||||||
|
<label for="at-sms-toggle" class="text-sm font-medium text-gray-700">SMS 대체발송</label>
|
||||||
|
<span class="text-xs text-gray-500">(카카오톡 발송 실패 시 SMS로 발송)</span>
|
||||||
|
</div>
|
||||||
|
<div id="at-sms-fields" class="hidden space-y-3">
|
||||||
|
<input type="text" name="sms_subject" class="w-full px-3 py-2 border border-gray-200 rounded-lg text-sm" placeholder="SMS 제목 (LMS일 경우)">
|
||||||
|
<textarea name="sms_message" rows="3" class="w-full px-3 py-2 border border-gray-200 rounded-lg text-sm" placeholder="SMS 메시지 (90바이트 초과 시 LMS)"></textarea>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<!-- 예약 발송 -->
|
||||||
|
<div>
|
||||||
|
<label class="block text-sm font-medium text-gray-600 mb-1">예약 발송</label>
|
||||||
|
<input type="datetime-local" name="reserve_dt" class="w-full px-3 py-2 border border-gray-200 rounded-lg text-sm">
|
||||||
|
<p class="text-xs text-gray-500 mt-1">비워두면 즉시 발송됩니다.</p>
|
||||||
|
</div>
|
||||||
|
<div class="flex justify-end">
|
||||||
|
<button type="submit" class="px-6 py-2 bg-yellow-500 text-white rounded-lg hover:bg-yellow-600 transition font-medium flex items-center gap-2">
|
||||||
|
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 19l9 2-9-18-9 18 9-2zm0 0v-8" /></svg>
|
||||||
|
알림톡 발송
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 친구톡 발송 폼 -->
|
||||||
|
<div id="panel-friendtalk" class="space-y-6 hidden">
|
||||||
|
<div class="bg-white rounded-xl shadow-sm border border-gray-100 p-6">
|
||||||
|
<h3 class="font-semibold text-gray-800 mb-4">친구톡 발송</h3>
|
||||||
|
<form id="friendtalk-form" class="space-y-4">
|
||||||
|
<div class="grid grid-cols-1 lg:grid-cols-2 gap-4">
|
||||||
|
<div>
|
||||||
|
<label class="block text-sm font-medium text-gray-600 mb-1">채널 (발신프로필)</label>
|
||||||
|
<select name="sender_id" id="ft-sender-id" class="w-full px-3 py-2 border border-gray-200 rounded-lg text-sm focus:ring-2 focus:ring-yellow-500">
|
||||||
|
<option value="">로딩 중...</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label class="block text-sm font-medium text-gray-600 mb-1">발송 유형</label>
|
||||||
|
<select id="ft-type" onchange="toggleFtImageFields()" class="w-full px-3 py-2 border border-gray-200 rounded-lg text-sm">
|
||||||
|
<option value="text">텍스트</option>
|
||||||
|
<option value="image">이미지</option>
|
||||||
|
<option value="wide">와이드 이미지</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<!-- 수신자 -->
|
||||||
|
<div class="grid grid-cols-1 lg:grid-cols-2 gap-4">
|
||||||
|
<div>
|
||||||
|
<label class="block text-sm font-medium text-gray-600 mb-1">수신자명</label>
|
||||||
|
<input type="text" name="receiver_name" class="w-full px-3 py-2 border border-gray-200 rounded-lg text-sm" placeholder="홍길동">
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label class="block text-sm font-medium text-gray-600 mb-1">수신번호</label>
|
||||||
|
<input type="tel" name="receiver_num" class="w-full px-3 py-2 border border-gray-200 rounded-lg text-sm" placeholder="01012345678">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<!-- 메시지 -->
|
||||||
|
<div>
|
||||||
|
<label class="block text-sm font-medium text-gray-600 mb-1">메시지 내용</label>
|
||||||
|
<textarea name="message" rows="6" class="w-full px-3 py-2 border border-gray-200 rounded-lg text-sm" placeholder="친구톡 메시지 내용을 입력하세요"></textarea>
|
||||||
|
</div>
|
||||||
|
<!-- 이미지 URL -->
|
||||||
|
<div id="ft-image-fields" class="hidden">
|
||||||
|
<label class="block text-sm font-medium text-gray-600 mb-1">이미지 URL</label>
|
||||||
|
<input type="url" name="image_url" class="w-full px-3 py-2 border border-gray-200 rounded-lg text-sm" placeholder="https://example.com/image.jpg">
|
||||||
|
<p class="text-xs text-gray-500 mt-1">친구톡 이미지: 500x250px / 와이드 이미지: 800x400px</p>
|
||||||
|
</div>
|
||||||
|
<!-- 버튼 -->
|
||||||
|
<div class="bg-gray-50 rounded-lg p-4">
|
||||||
|
<div class="flex items-center justify-between mb-3">
|
||||||
|
<label class="text-sm font-medium text-gray-700">버튼 설정</label>
|
||||||
|
<button type="button" onclick="addButton('ft')" class="text-xs px-3 py-1 bg-gray-200 text-gray-700 rounded-lg hover:bg-gray-300">+ 버튼 추가</button>
|
||||||
|
</div>
|
||||||
|
<div id="ft-buttons-container" class="space-y-3"></div>
|
||||||
|
</div>
|
||||||
|
<!-- 광고 여부 -->
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<input type="checkbox" name="ad_yn" id="ft-ad-yn" class="w-4 h-4 text-yellow-600 rounded">
|
||||||
|
<label for="ft-ad-yn" class="text-sm text-gray-700">광고성 메시지</label>
|
||||||
|
<span class="text-xs text-gray-500">(광고 표시 포함)</span>
|
||||||
|
</div>
|
||||||
|
<!-- SMS 대체발송 -->
|
||||||
|
<div class="bg-gray-50 rounded-lg p-4 space-y-3">
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<input type="checkbox" id="ft-sms-toggle" onchange="toggleSmsSection('ft')" class="w-4 h-4 text-yellow-600 rounded">
|
||||||
|
<label for="ft-sms-toggle" class="text-sm font-medium text-gray-700">SMS 대체발송</label>
|
||||||
|
</div>
|
||||||
|
<div id="ft-sms-fields" class="hidden space-y-3">
|
||||||
|
<input type="text" name="sms_subject" class="w-full px-3 py-2 border border-gray-200 rounded-lg text-sm" placeholder="SMS 제목">
|
||||||
|
<textarea name="sms_message" rows="3" class="w-full px-3 py-2 border border-gray-200 rounded-lg text-sm" placeholder="SMS 메시지"></textarea>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<!-- 예약 발송 -->
|
||||||
|
<div>
|
||||||
|
<label class="block text-sm font-medium text-gray-600 mb-1">예약 발송</label>
|
||||||
|
<input type="datetime-local" name="reserve_dt" class="w-full px-3 py-2 border border-gray-200 rounded-lg text-sm">
|
||||||
|
</div>
|
||||||
|
<div class="flex justify-end">
|
||||||
|
<button type="submit" class="px-6 py-2 bg-yellow-500 text-white rounded-lg hover:bg-yellow-600 transition font-medium flex items-center gap-2">
|
||||||
|
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 19l9 2-9-18-9 18 9-2zm0 0v-8" /></svg>
|
||||||
|
친구톡 발송
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
@endif
|
||||||
|
</div>
|
||||||
|
@endsection
|
||||||
|
|
||||||
|
@push('scripts')
|
||||||
|
<script>
|
||||||
|
let channelList = [];
|
||||||
|
let templateCache = {};
|
||||||
|
|
||||||
|
function switchTab(tab) {
|
||||||
|
document.getElementById('panel-alimtalk').classList.toggle('hidden', tab !== 'alimtalk');
|
||||||
|
document.getElementById('panel-friendtalk').classList.toggle('hidden', tab !== 'friendtalk');
|
||||||
|
document.getElementById('tab-alimtalk').classList.toggle('border-yellow-500', tab === 'alimtalk');
|
||||||
|
document.getElementById('tab-alimtalk').classList.toggle('text-yellow-700', tab === 'alimtalk');
|
||||||
|
document.getElementById('tab-alimtalk').classList.toggle('bg-yellow-50/50', tab === 'alimtalk');
|
||||||
|
document.getElementById('tab-alimtalk').classList.toggle('border-transparent', tab !== 'alimtalk');
|
||||||
|
document.getElementById('tab-alimtalk').classList.toggle('text-gray-500', tab !== 'alimtalk');
|
||||||
|
document.getElementById('tab-friendtalk').classList.toggle('border-yellow-500', tab === 'friendtalk');
|
||||||
|
document.getElementById('tab-friendtalk').classList.toggle('text-yellow-700', tab === 'friendtalk');
|
||||||
|
document.getElementById('tab-friendtalk').classList.toggle('bg-yellow-50/50', tab === 'friendtalk');
|
||||||
|
document.getElementById('tab-friendtalk').classList.toggle('border-transparent', tab !== 'friendtalk');
|
||||||
|
document.getElementById('tab-friendtalk').classList.toggle('text-gray-500', tab !== 'friendtalk');
|
||||||
|
}
|
||||||
|
|
||||||
|
function toggleSmsSection(prefix) {
|
||||||
|
const checked = document.getElementById(prefix + '-sms-toggle').checked;
|
||||||
|
document.getElementById(prefix + '-sms-fields').classList.toggle('hidden', !checked);
|
||||||
|
}
|
||||||
|
|
||||||
|
function toggleFtImageFields() {
|
||||||
|
const type = document.getElementById('ft-type').value;
|
||||||
|
document.getElementById('ft-image-fields').classList.toggle('hidden', type === 'text');
|
||||||
|
}
|
||||||
|
|
||||||
|
function addButton(prefix) {
|
||||||
|
const container = document.getElementById(prefix + '-buttons-container');
|
||||||
|
const idx = container.children.length;
|
||||||
|
const html = '<div class="flex gap-2 items-start bg-white p-3 rounded-lg border"><div class="flex-1 grid grid-cols-2 gap-2"><input type="text" placeholder="버튼명" class="btn-name px-2 py-1 border rounded text-sm"><select class="btn-type px-2 py-1 border rounded text-sm"><option value="WL">웹링크</option><option value="AL">앱링크</option><option value="BK">봇키워드</option><option value="MD">메시지전달</option></select><input type="text" placeholder="URL1" class="btn-url1 px-2 py-1 border rounded text-sm"><input type="text" placeholder="URL2" class="btn-url2 px-2 py-1 border rounded text-sm"></div><button type="button" onclick="this.parentElement.remove()" class="text-red-400 hover:text-red-600 mt-1"><svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" /></svg></button></div>';
|
||||||
|
container.insertAdjacentHTML('beforeend', html);
|
||||||
|
}
|
||||||
|
|
||||||
|
function collectButtons(prefix) {
|
||||||
|
const container = document.getElementById(prefix + '-buttons-container');
|
||||||
|
const buttons = [];
|
||||||
|
container.querySelectorAll(':scope > div').forEach(row => {
|
||||||
|
const name = row.querySelector('.btn-name')?.value;
|
||||||
|
if (name) {
|
||||||
|
buttons.push({
|
||||||
|
Name: name,
|
||||||
|
ButtonType: row.querySelector('.btn-type')?.value || 'WL',
|
||||||
|
Url1: row.querySelector('.btn-url1')?.value || '',
|
||||||
|
Url2: row.querySelector('.btn-url2')?.value || '',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return buttons;
|
||||||
|
}
|
||||||
|
|
||||||
|
function loadChannelSelects() {
|
||||||
|
fetch('/api/admin/barobill/kakaotalk/channels', {
|
||||||
|
headers: { 'Accept': 'application/json', 'X-Requested-With': 'XMLHttpRequest' }
|
||||||
|
})
|
||||||
|
.then(r => r.json())
|
||||||
|
.then(data => {
|
||||||
|
if (!data.success) return;
|
||||||
|
const raw = data.data;
|
||||||
|
if (Array.isArray(raw)) channelList = raw;
|
||||||
|
else if (raw && raw.KakaotalkChannel) channelList = Array.isArray(raw.KakaotalkChannel) ? raw.KakaotalkChannel : [raw.KakaotalkChannel];
|
||||||
|
else if (raw) channelList = [raw];
|
||||||
|
|
||||||
|
['at-sender-id', 'ft-sender-id'].forEach(id => {
|
||||||
|
const sel = document.getElementById(id);
|
||||||
|
sel.innerHTML = '<option value="">-- 채널 선택 --</option>';
|
||||||
|
channelList.forEach(ch => {
|
||||||
|
const opt = document.createElement('option');
|
||||||
|
opt.value = ch.ChannelId;
|
||||||
|
opt.textContent = ch.ChannelName || ch.ChannelId;
|
||||||
|
sel.appendChild(opt);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function loadTemplateOptions(prefix) {
|
||||||
|
const channelId = document.getElementById(prefix + '-sender-id').value;
|
||||||
|
const tplSelect = document.getElementById(prefix + '-template');
|
||||||
|
if (!tplSelect) return;
|
||||||
|
|
||||||
|
if (!channelId) {
|
||||||
|
tplSelect.innerHTML = '<option value="">채널을 먼저 선택하세요</option>';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
tplSelect.innerHTML = '<option value="">로딩 중...</option>';
|
||||||
|
|
||||||
|
fetch('/api/admin/barobill/kakaotalk/templates?channel_id=' + encodeURIComponent(channelId), {
|
||||||
|
headers: { 'Accept': 'application/json', 'X-Requested-With': 'XMLHttpRequest' }
|
||||||
|
})
|
||||||
|
.then(r => r.json())
|
||||||
|
.then(data => {
|
||||||
|
tplSelect.innerHTML = '<option value="">-- 템플릿 선택 --</option>';
|
||||||
|
if (!data.success) return;
|
||||||
|
const raw = data.data;
|
||||||
|
let items = [];
|
||||||
|
if (Array.isArray(raw)) items = raw;
|
||||||
|
else if (raw && raw.KakaotalkTemplate) items = Array.isArray(raw.KakaotalkTemplate) ? raw.KakaotalkTemplate : [raw.KakaotalkTemplate];
|
||||||
|
|
||||||
|
templateCache[channelId] = items;
|
||||||
|
items.forEach(tpl => {
|
||||||
|
if (tpl.Status === 'R') { // 승인된 템플릿만
|
||||||
|
const opt = document.createElement('option');
|
||||||
|
opt.value = tpl.TemplateName;
|
||||||
|
opt.textContent = tpl.TemplateName;
|
||||||
|
tplSelect.appendChild(opt);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function fillTemplateContent(prefix) {
|
||||||
|
const channelId = document.getElementById(prefix + '-sender-id').value;
|
||||||
|
const tplName = document.getElementById(prefix + '-template').value;
|
||||||
|
if (!channelId || !tplName) return;
|
||||||
|
|
||||||
|
const items = templateCache[channelId] || [];
|
||||||
|
const tpl = items.find(t => t.TemplateName === tplName);
|
||||||
|
if (tpl) {
|
||||||
|
document.getElementById(prefix + '-message').value = tpl.TemplateContent || '';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatReserveDT(datetimeLocal) {
|
||||||
|
if (!datetimeLocal) return '';
|
||||||
|
return datetimeLocal.replace(/[-T:]/g, '').substring(0, 14);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 알림톡 발송
|
||||||
|
document.getElementById('alimtalk-form')?.addEventListener('submit', function(e) {
|
||||||
|
e.preventDefault();
|
||||||
|
const fd = new FormData(this);
|
||||||
|
const buttons = collectButtons('at');
|
||||||
|
|
||||||
|
const body = {
|
||||||
|
sender_id: fd.get('sender_id'),
|
||||||
|
template_name: fd.get('template_name'),
|
||||||
|
receiver_name: fd.get('receiver_name'),
|
||||||
|
receiver_num: fd.get('receiver_num'),
|
||||||
|
title: fd.get('title') || '',
|
||||||
|
message: fd.get('message'),
|
||||||
|
sms_message: fd.get('sms_message') || '',
|
||||||
|
sms_subject: fd.get('sms_subject') || '',
|
||||||
|
reserve_dt: formatReserveDT(fd.get('reserve_dt')),
|
||||||
|
};
|
||||||
|
if (buttons.length > 0) body.buttons = buttons;
|
||||||
|
|
||||||
|
const btn = this.querySelector('button[type="submit"]');
|
||||||
|
btn.disabled = true;
|
||||||
|
btn.textContent = '발송 중...';
|
||||||
|
|
||||||
|
fetch('/api/admin/barobill/kakaotalk/send/alimtalk', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json', 'Accept': 'application/json', 'X-Requested-With': 'XMLHttpRequest', 'X-CSRF-TOKEN': document.querySelector('meta[name="csrf-token"]')?.content },
|
||||||
|
body: JSON.stringify(body),
|
||||||
|
})
|
||||||
|
.then(r => r.json())
|
||||||
|
.then(data => {
|
||||||
|
btn.disabled = false;
|
||||||
|
btn.innerHTML = '<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 19l9 2-9-18-9 18 9-2zm0 0v-8" /></svg> 알림톡 발송';
|
||||||
|
if (data.success) {
|
||||||
|
alert('알림톡 발송 성공!\n전송키: ' + (data.data || ''));
|
||||||
|
} else {
|
||||||
|
alert('발송 실패: ' + (data.error || data.message));
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(err => {
|
||||||
|
btn.disabled = false;
|
||||||
|
btn.innerHTML = '<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 19l9 2-9-18-9 18 9-2zm0 0v-8" /></svg> 알림톡 발송';
|
||||||
|
alert('오류: ' + err.message);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// 친구톡 발송
|
||||||
|
document.getElementById('friendtalk-form')?.addEventListener('submit', function(e) {
|
||||||
|
e.preventDefault();
|
||||||
|
const fd = new FormData(this);
|
||||||
|
const ftType = document.getElementById('ft-type').value;
|
||||||
|
const buttons = collectButtons('ft');
|
||||||
|
const channelId = fd.get('sender_id');
|
||||||
|
|
||||||
|
const body = {
|
||||||
|
sender_id: channelId,
|
||||||
|
channel_id: channelId,
|
||||||
|
receiver_name: fd.get('receiver_name'),
|
||||||
|
receiver_num: fd.get('receiver_num'),
|
||||||
|
message: fd.get('message'),
|
||||||
|
sms_message: fd.get('sms_message') || '',
|
||||||
|
sms_subject: fd.get('sms_subject') || '',
|
||||||
|
ad_yn: document.getElementById('ft-ad-yn').checked,
|
||||||
|
reserve_dt: formatReserveDT(fd.get('reserve_dt')),
|
||||||
|
};
|
||||||
|
if (buttons.length > 0) body.buttons = buttons;
|
||||||
|
|
||||||
|
let endpoint = '/api/admin/barobill/kakaotalk/send/friendtalk';
|
||||||
|
if (ftType === 'image') {
|
||||||
|
endpoint = '/api/admin/barobill/kakaotalk/send/friendtalk-image';
|
||||||
|
body.image_url = fd.get('image_url');
|
||||||
|
} else if (ftType === 'wide') {
|
||||||
|
endpoint = '/api/admin/barobill/kakaotalk/send/friendtalk-wide';
|
||||||
|
body.image_url = fd.get('image_url');
|
||||||
|
}
|
||||||
|
|
||||||
|
const btn = this.querySelector('button[type="submit"]');
|
||||||
|
btn.disabled = true;
|
||||||
|
btn.textContent = '발송 중...';
|
||||||
|
|
||||||
|
fetch(endpoint, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json', 'Accept': 'application/json', 'X-Requested-With': 'XMLHttpRequest', 'X-CSRF-TOKEN': document.querySelector('meta[name="csrf-token"]')?.content },
|
||||||
|
body: JSON.stringify(body),
|
||||||
|
})
|
||||||
|
.then(r => r.json())
|
||||||
|
.then(data => {
|
||||||
|
btn.disabled = false;
|
||||||
|
btn.innerHTML = '<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 19l9 2-9-18-9 18 9-2zm0 0v-8" /></svg> 친구톡 발송';
|
||||||
|
if (data.success) {
|
||||||
|
alert('친구톡 발송 성공!\n전송키: ' + (data.data || ''));
|
||||||
|
} else {
|
||||||
|
alert('발송 실패: ' + (data.error || data.message));
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(err => {
|
||||||
|
btn.disabled = false;
|
||||||
|
btn.innerHTML = '<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 19l9 2-9-18-9 18 9-2zm0 0v-8" /></svg> 친구톡 발송';
|
||||||
|
alert('오류: ' + err.message);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
document.addEventListener('DOMContentLoaded', loadChannelSelects);
|
||||||
|
</script>
|
||||||
|
@endpush
|
||||||
191
resources/views/barobill/kakaotalk/templates/index.blade.php
Normal file
191
resources/views/barobill/kakaotalk/templates/index.blade.php
Normal file
@@ -0,0 +1,191 @@
|
|||||||
|
@extends('layouts.app')
|
||||||
|
|
||||||
|
@section('title', '카카오톡 템플릿 관리')
|
||||||
|
|
||||||
|
@section('content')
|
||||||
|
<div class="flex flex-col h-full">
|
||||||
|
<!-- 페이지 헤더 -->
|
||||||
|
<div class="flex flex-col sm:flex-row sm:justify-between sm:items-center gap-4 mb-6 flex-shrink-0">
|
||||||
|
<div>
|
||||||
|
<div class="flex items-center gap-2 text-sm text-gray-500 mb-1">
|
||||||
|
<a href="{{ route('barobill.kakaotalk.index') }}" class="hover:text-gray-700">카카오톡</a>
|
||||||
|
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7" /></svg>
|
||||||
|
<span class="text-gray-700">템플릿 관리</span>
|
||||||
|
</div>
|
||||||
|
<h1 class="text-2xl font-bold text-gray-800">템플릿 관리</h1>
|
||||||
|
<p class="text-sm text-gray-500 mt-1">알림톡 발송에 사용되는 템플릿 조회 및 관리</p>
|
||||||
|
</div>
|
||||||
|
<button type="button" onclick="openTemplateManagement()" class="px-4 py-2 bg-yellow-500 text-white rounded-lg hover:bg-yellow-600 transition text-sm flex items-center gap-2">
|
||||||
|
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 6H6a2 2 0 00-2 2v10a2 2 0 002 2h10a2 2 0 002-2v-4M14 4h6m0 0v6m0-6L10 14" /></svg>
|
||||||
|
템플릿 관리 (바로빌)
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 채널 선택 -->
|
||||||
|
<div class="bg-white rounded-xl shadow-sm border border-gray-100 p-6 mb-6">
|
||||||
|
<div class="flex items-center gap-4">
|
||||||
|
<label class="text-sm font-medium text-gray-700">채널 선택:</label>
|
||||||
|
<select id="channel-select" onchange="loadTemplates()" class="px-3 py-2 border border-gray-200 rounded-lg text-sm focus:ring-2 focus:ring-yellow-500 focus:border-yellow-500 min-w-[200px]">
|
||||||
|
<option value="">채널을 먼저 로딩합니다...</option>
|
||||||
|
</select>
|
||||||
|
<button type="button" onclick="loadTemplates()" class="px-3 py-1.5 bg-gray-100 text-gray-700 rounded-lg hover:bg-gray-200 transition text-sm">조회</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 템플릿 목록 -->
|
||||||
|
<div class="bg-white rounded-xl shadow-sm border border-gray-100">
|
||||||
|
<div id="templates-list" class="p-6">
|
||||||
|
<div class="text-center py-12 text-gray-400">
|
||||||
|
<p class="text-sm">채널을 선택하면 템플릿 목록이 표시됩니다.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 템플릿 상세 모달 -->
|
||||||
|
<div id="template-detail-modal" class="fixed inset-0 z-50 hidden">
|
||||||
|
<div class="fixed inset-0 bg-black/50" onclick="closeTemplateModal()"></div>
|
||||||
|
<div class="fixed inset-0 flex items-center justify-center p-4">
|
||||||
|
<div class="bg-white rounded-xl shadow-xl max-w-lg w-full max-h-[80vh] overflow-y-auto">
|
||||||
|
<div class="flex items-center justify-between p-6 border-b">
|
||||||
|
<h3 class="font-semibold text-gray-800" id="modal-template-name">템플릿 상세</h3>
|
||||||
|
<button onclick="closeTemplateModal()" class="text-gray-400 hover:text-gray-600">
|
||||||
|
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" /></svg>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div class="p-6 space-y-4" id="modal-template-content"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
@endsection
|
||||||
|
|
||||||
|
@push('scripts')
|
||||||
|
<script>
|
||||||
|
// 채널 목록 로드
|
||||||
|
function loadChannelOptions() {
|
||||||
|
fetch('/api/admin/barobill/kakaotalk/channels', {
|
||||||
|
headers: { 'Accept': 'application/json', 'X-Requested-With': 'XMLHttpRequest' }
|
||||||
|
})
|
||||||
|
.then(r => r.json())
|
||||||
|
.then(data => {
|
||||||
|
const select = document.getElementById('channel-select');
|
||||||
|
select.innerHTML = '<option value="">-- 채널 선택 --</option>';
|
||||||
|
|
||||||
|
if (!data.success) return;
|
||||||
|
|
||||||
|
const raw = data.data;
|
||||||
|
let items = [];
|
||||||
|
if (Array.isArray(raw)) items = raw;
|
||||||
|
else if (raw && raw.KakaotalkChannel) items = Array.isArray(raw.KakaotalkChannel) ? raw.KakaotalkChannel : [raw.KakaotalkChannel];
|
||||||
|
else if (raw) items = [raw];
|
||||||
|
|
||||||
|
items.forEach(ch => {
|
||||||
|
const opt = document.createElement('option');
|
||||||
|
opt.value = ch.ChannelId || '';
|
||||||
|
opt.textContent = (ch.ChannelName || ch.ChannelId || '-');
|
||||||
|
select.appendChild(opt);
|
||||||
|
});
|
||||||
|
|
||||||
|
if (items.length === 1) {
|
||||||
|
select.value = items[0].ChannelId;
|
||||||
|
loadTemplates();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function loadTemplates() {
|
||||||
|
const channelId = document.getElementById('channel-select').value;
|
||||||
|
const container = document.getElementById('templates-list');
|
||||||
|
|
||||||
|
if (!channelId) {
|
||||||
|
container.innerHTML = '<div class="text-center py-12 text-gray-400"><p class="text-sm">채널을 선택해주세요.</p></div>';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
container.innerHTML = '<div class="text-center py-12 text-gray-400"><svg class="w-10 h-10 mx-auto mb-3 animate-spin" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15" /></svg><p class="text-sm">템플릿 로딩 중...</p></div>';
|
||||||
|
|
||||||
|
fetch('/api/admin/barobill/kakaotalk/templates?channel_id=' + encodeURIComponent(channelId), {
|
||||||
|
headers: { 'Accept': 'application/json', 'X-Requested-With': 'XMLHttpRequest' }
|
||||||
|
})
|
||||||
|
.then(r => r.json())
|
||||||
|
.then(data => {
|
||||||
|
if (!data.success) {
|
||||||
|
container.innerHTML = '<div class="text-center py-12"><p class="text-sm text-red-500">' + (data.error || data.message) + '</p></div>';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const raw = data.data;
|
||||||
|
let items = [];
|
||||||
|
if (Array.isArray(raw)) items = raw;
|
||||||
|
else if (raw && raw.KakaotalkTemplate) items = Array.isArray(raw.KakaotalkTemplate) ? raw.KakaotalkTemplate : [raw.KakaotalkTemplate];
|
||||||
|
else if (raw) items = [raw];
|
||||||
|
|
||||||
|
if (items.length === 0) {
|
||||||
|
container.innerHTML = '<div class="text-center py-12 text-gray-400"><p class="text-sm">등록된 템플릿이 없습니다.</p></div>';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let html = '<div class="overflow-x-auto"><table class="w-full text-sm"><thead><tr class="border-b border-gray-200"><th class="text-left py-3 px-4 font-medium text-gray-600">템플릿명</th><th class="text-left py-3 px-4 font-medium text-gray-600">상태</th><th class="text-center py-3 px-4 font-medium text-gray-600">상세</th></tr></thead><tbody>';
|
||||||
|
items.forEach((tpl, idx) => {
|
||||||
|
const statusMap = { 'R': '승인', 'N': '반려', 'P': '심사중', 'S': '중단' };
|
||||||
|
const statusLabel = statusMap[tpl.Status] || tpl.Status || '-';
|
||||||
|
const statusColor = tpl.Status === 'R' ? 'green' : (tpl.Status === 'N' ? 'red' : 'yellow');
|
||||||
|
html += '<tr class="border-b border-gray-50 hover:bg-gray-50">';
|
||||||
|
html += '<td class="py-3 px-4 font-medium">' + (tpl.TemplateName || '-') + '</td>';
|
||||||
|
html += '<td class="py-3 px-4"><span class="px-2 py-0.5 rounded-full text-xs font-medium bg-' + statusColor + '-100 text-' + statusColor + '-700">' + statusLabel + '</span></td>';
|
||||||
|
html += '<td class="py-3 px-4 text-center"><button onclick=\'showTemplateDetail(' + JSON.stringify(tpl).replace(/'/g, "\\'") + ')\' class="text-blue-600 hover:text-blue-800 text-xs">상세보기</button></td>';
|
||||||
|
html += '</tr>';
|
||||||
|
});
|
||||||
|
html += '</tbody></table></div>';
|
||||||
|
container.innerHTML = html;
|
||||||
|
})
|
||||||
|
.catch(err => {
|
||||||
|
container.innerHTML = '<div class="text-center py-12"><p class="text-sm text-red-500">조회 오류: ' + err.message + '</p></div>';
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function showTemplateDetail(tpl) {
|
||||||
|
document.getElementById('modal-template-name').textContent = tpl.TemplateName || '템플릿 상세';
|
||||||
|
const content = document.getElementById('modal-template-content');
|
||||||
|
|
||||||
|
let html = '';
|
||||||
|
html += '<div><label class="text-xs text-gray-500 block mb-1">템플릿 내용</label><div class="bg-gray-50 rounded-lg p-4 text-sm whitespace-pre-wrap">' + (tpl.TemplateContent || '-') + '</div></div>';
|
||||||
|
if (tpl.TemplateExtra) {
|
||||||
|
html += '<div><label class="text-xs text-gray-500 block mb-1">부가 정보</label><div class="bg-gray-50 rounded-lg p-4 text-sm whitespace-pre-wrap">' + tpl.TemplateExtra + '</div></div>';
|
||||||
|
}
|
||||||
|
if (tpl.Buttons) {
|
||||||
|
html += '<div><label class="text-xs text-gray-500 block mb-1">버튼</label><div class="space-y-2">';
|
||||||
|
const btns = Array.isArray(tpl.Buttons.KakaotalkButton) ? tpl.Buttons.KakaotalkButton : (tpl.Buttons.KakaotalkButton ? [tpl.Buttons.KakaotalkButton] : []);
|
||||||
|
btns.forEach(btn => {
|
||||||
|
html += '<div class="bg-gray-50 rounded-lg p-3 text-sm"><span class="font-medium">' + (btn.Name || '-') + '</span> <span class="text-xs text-gray-500">(' + (btn.ButtonType || '-') + ')</span>';
|
||||||
|
if (btn.Url1) html += '<br><span class="text-xs text-blue-600">' + btn.Url1 + '</span>';
|
||||||
|
html += '</div>';
|
||||||
|
});
|
||||||
|
html += '</div></div>';
|
||||||
|
}
|
||||||
|
content.innerHTML = html;
|
||||||
|
document.getElementById('template-detail-modal').classList.remove('hidden');
|
||||||
|
}
|
||||||
|
|
||||||
|
function closeTemplateModal() {
|
||||||
|
document.getElementById('template-detail-modal').classList.add('hidden');
|
||||||
|
}
|
||||||
|
|
||||||
|
function openTemplateManagement() {
|
||||||
|
fetch('/api/admin/barobill/kakaotalk/templates/management-url', {
|
||||||
|
headers: { 'Accept': 'application/json', 'X-Requested-With': 'XMLHttpRequest' }
|
||||||
|
})
|
||||||
|
.then(r => r.json())
|
||||||
|
.then(data => {
|
||||||
|
if (data.success && data.data) {
|
||||||
|
window.open(data.data, '_blank', 'width=1200,height=800');
|
||||||
|
} else {
|
||||||
|
alert(data.error || data.message || '관리 URL 조회 실패');
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(err => alert('오류: ' + err.message));
|
||||||
|
}
|
||||||
|
|
||||||
|
document.addEventListener('DOMContentLoaded', loadChannelOptions);
|
||||||
|
</script>
|
||||||
|
@endpush
|
||||||
@@ -204,6 +204,33 @@
|
|||||||
Route::put('/pricing-policies/{id}', [\App\Http\Controllers\Api\Admin\Barobill\BarobillBillingController::class, 'updatePricingPolicy'])->name('pricing-policies.update');
|
Route::put('/pricing-policies/{id}', [\App\Http\Controllers\Api\Admin\Barobill\BarobillBillingController::class, 'updatePricingPolicy'])->name('pricing-policies.update');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// 바로빌 카카오톡 API
|
||||||
|
Route::prefix('barobill/kakaotalk')->name('barobill.kakaotalk.')->group(function () {
|
||||||
|
// 채널 관리
|
||||||
|
Route::get('/channels', [\App\Http\Controllers\Api\Admin\Barobill\BarobillKakaotalkController::class, 'getChannels'])->name('channels');
|
||||||
|
Route::get('/channels/management-url', [\App\Http\Controllers\Api\Admin\Barobill\BarobillKakaotalkController::class, 'getChannelManagementUrl'])->name('channels.management-url');
|
||||||
|
|
||||||
|
// 템플릿 관리
|
||||||
|
Route::get('/templates', [\App\Http\Controllers\Api\Admin\Barobill\BarobillKakaotalkController::class, 'getTemplates'])->name('templates');
|
||||||
|
Route::get('/templates/management-url', [\App\Http\Controllers\Api\Admin\Barobill\BarobillKakaotalkController::class, 'getTemplateManagementUrl'])->name('templates.management-url');
|
||||||
|
|
||||||
|
// 알림톡 발송
|
||||||
|
Route::post('/send/alimtalk', [\App\Http\Controllers\Api\Admin\Barobill\BarobillKakaotalkController::class, 'sendAlimtalk'])->name('send.alimtalk');
|
||||||
|
Route::post('/send/alimtalk-bulk', [\App\Http\Controllers\Api\Admin\Barobill\BarobillKakaotalkController::class, 'sendAlimtalkBulk'])->name('send.alimtalk-bulk');
|
||||||
|
|
||||||
|
// 친구톡 발송
|
||||||
|
Route::post('/send/friendtalk', [\App\Http\Controllers\Api\Admin\Barobill\BarobillKakaotalkController::class, 'sendFriendtalk'])->name('send.friendtalk');
|
||||||
|
Route::post('/send/friendtalk-image', [\App\Http\Controllers\Api\Admin\Barobill\BarobillKakaotalkController::class, 'sendFriendtalkImage'])->name('send.friendtalk-image');
|
||||||
|
Route::post('/send/friendtalk-wide', [\App\Http\Controllers\Api\Admin\Barobill\BarobillKakaotalkController::class, 'sendFriendtalkWide'])->name('send.friendtalk-wide');
|
||||||
|
|
||||||
|
// 전송 결과 조회
|
||||||
|
Route::get('/send/{sendKey}', [\App\Http\Controllers\Api\Admin\Barobill\BarobillKakaotalkController::class, 'getSendResult'])->name('send.result');
|
||||||
|
Route::post('/send/results', [\App\Http\Controllers\Api\Admin\Barobill\BarobillKakaotalkController::class, 'getSendResults'])->name('send.results');
|
||||||
|
|
||||||
|
// 예약 취소
|
||||||
|
Route::delete('/send/{sendKey}/cancel', [\App\Http\Controllers\Api\Admin\Barobill\BarobillKakaotalkController::class, 'cancelReserved'])->name('send.cancel');
|
||||||
|
});
|
||||||
|
|
||||||
// 테넌트 관리 API
|
// 테넌트 관리 API
|
||||||
Route::prefix('tenants')->name('tenants.')->group(function () {
|
Route::prefix('tenants')->name('tenants.')->group(function () {
|
||||||
// 고정 경로는 먼저 정의
|
// 고정 경로는 먼저 정의
|
||||||
|
|||||||
@@ -596,6 +596,15 @@
|
|||||||
// 카드내역 참조
|
// 카드내역 참조
|
||||||
Route::get('/card-transactions', [\App\Http\Controllers\Barobill\HometaxController::class, 'cardTransactions'])->name('card-transactions');
|
Route::get('/card-transactions', [\App\Http\Controllers\Barobill\HometaxController::class, 'cardTransactions'])->name('card-transactions');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// 카카오톡 (알림톡/친구톡)
|
||||||
|
Route::prefix('kakaotalk')->name('kakaotalk.')->group(function () {
|
||||||
|
Route::get('/', [\App\Http\Controllers\Barobill\KakaotalkController::class, 'index'])->name('index');
|
||||||
|
Route::get('/channels', [\App\Http\Controllers\Barobill\KakaotalkController::class, 'channels'])->name('channels');
|
||||||
|
Route::get('/templates', [\App\Http\Controllers\Barobill\KakaotalkController::class, 'templates'])->name('templates');
|
||||||
|
Route::get('/send', [\App\Http\Controllers\Barobill\KakaotalkController::class, 'send'])->name('send');
|
||||||
|
Route::get('/history', [\App\Http\Controllers\Barobill\KakaotalkController::class, 'history'])->name('history');
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
/*
|
/*
|
||||||
|
|||||||
Reference in New Issue
Block a user