Files
sam-manage/resources/views/barobill/kakaotalk/templates/index.blade.php
김보곤 d69f4ef5d3 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>
2026-02-14 14:11:15 +09:00

192 lines
9.9 KiB
PHP

@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