diff --git a/app/Http/Controllers/ESign/EsignApiController.php b/app/Http/Controllers/ESign/EsignApiController.php index 2675681b..1b604efe 100644 --- a/app/Http/Controllers/ESign/EsignApiController.php +++ b/app/Http/Controllers/ESign/EsignApiController.php @@ -819,6 +819,7 @@ public function send(Request $request, int $id): JsonResponse $sendMethod = $request->input('send_method', 'email'); $smsFallback = $request->boolean('sms_fallback', true); + $templateName = $request->input('template_name'); $contract->update([ 'status' => 'pending', @@ -838,7 +839,7 @@ public function send(Request $request, int $id): JsonResponse $notificationResults = []; foreach ($targetSigners as $signer) { $signer->update(['status' => 'notified']); - $results = $this->dispatchNotification($contract, $signer, $sendMethod, $smsFallback); + $results = $this->dispatchNotification($contract, $signer, $sendMethod, $smsFallback, templateName: $templateName); $notificationResults[] = [ 'signer_id' => $signer->id, 'signer_name' => $signer->name, @@ -966,13 +967,14 @@ private function dispatchNotification( string $sendMethod, bool $smsFallback, bool $isReminder = false, + ?string $templateName = null, ): array { $results = []; $alimtalkFailed = false; // 알림톡 발송 if (in_array($sendMethod, ['alimtalk', 'both']) && $signer->phone) { - $alimtalkResult = $this->sendAlimtalk($contract, $signer, $smsFallback, $isReminder); + $alimtalkResult = $this->sendAlimtalk($contract, $signer, $smsFallback, $isReminder, $templateName); $results[] = $alimtalkResult; $alimtalkFailed = ! ($alimtalkResult['success'] ?? false); } @@ -1010,6 +1012,7 @@ private function sendAlimtalk( EsignSigner $signer, bool $smsFallback = true, bool $isReminder = false, + ?string $templateName = null, ): array { try { $member = BarobillMember::where('tenant_id', $contract->tenant_id)->first(); @@ -1033,7 +1036,9 @@ private function sendAlimtalk( $signUrl = config('app.url').'/esign/sign/'.$signer->access_token; $expires = $contract->expires_at?->format('Y-m-d H:i') ?? '없음'; - $templateName = $isReminder ? '전자계약_리마인드' : '전자계약_서명요청'; + if (! $templateName) { + $templateName = $isReminder ? '전자계약_리마인드' : '전자계약_서명요청'; + } // 등록된 템플릿 본문 + 버튼 정보 조회 (정확한 포맷 유지) $tplData = $this->getTemplateData($barobill, $member->biz_no, $channelId, $templateName); @@ -1228,6 +1233,99 @@ private function getTemplateData(BarobillService $barobill, string $bizNo, strin return $empty; } + /** + * 바로빌 등록 알림톡 템플릿 목록 조회 (승인 완료된 것만) + */ + public function getAlimtalkTemplates(): JsonResponse + { + try { + $tenantId = session('selected_tenant_id', 1); + $member = BarobillMember::where('tenant_id', $tenantId)->first(); + + if (! $member || ! $member->biz_no) { + return response()->json([ + 'success' => false, + 'message' => '바로빌 회원 정보 또는 사업자번호가 설정되지 않았습니다.', + ]); + } + + $barobill = app(BarobillService::class); + $barobill->setServerMode($member->server_mode ?? 'production'); + + $channelId = $this->getKakaotalkChannelId($barobill, $member->biz_no); + if (! $channelId) { + return response()->json([ + 'success' => false, + 'message' => '등록된 카카오톡 채널이 없습니다.', + ]); + } + + $result = $barobill->getKakaotalkTemplates($member->biz_no, $channelId); + if (! ($result['success'] ?? false) || empty($result['data'])) { + return response()->json([ + 'success' => false, + 'message' => '템플릿 목록을 조회할 수 없습니다.', + ]); + } + + $data = $result['data']; + $items = []; + if (is_object($data) && isset($data->KakaotalkTemplate)) { + $items = is_array($data->KakaotalkTemplate) + ? $data->KakaotalkTemplate + : [$data->KakaotalkTemplate]; + } + + // 승인(Status=3)된 템플릿만 필터링 + $templates = []; + foreach ($items as $tpl) { + $status = $tpl->Status ?? null; + if ($status != 3) { + continue; + } + + $buttons = []; + $btnData = $tpl->Buttons ?? null; + if ($btnData) { + $btnList = $btnData->KakaotalkButton ?? null; + if ($btnList) { + $btnList = is_array($btnList) ? $btnList : [$btnList]; + foreach ($btnList as $btn) { + $buttons[] = [ + 'Name' => $btn->Name ?? '', + 'ButtonType' => $btn->ButtonType ?? 'WL', + 'Url1' => $btn->Url1 ?? '', + 'Url2' => $btn->Url2 ?? '', + ]; + } + } + } + + $templates[] = [ + 'name' => $tpl->TemplateName ?? '', + 'content' => $tpl->TemplateContent ?? '', + 'status' => $status, + 'buttons' => $buttons, + ]; + } + + return response()->json([ + 'success' => true, + 'data' => [ + 'channel_id' => $channelId, + 'templates' => $templates, + ], + ]); + } catch (\Throwable $e) { + \Log::error('알림톡 템플릿 목록 조회 실패', ['error' => $e->getMessage()]); + + return response()->json([ + 'success' => false, + 'message' => '템플릿 목록 조회 중 오류: '.$e->getMessage(), + ]); + } + } + /** * PDF 다운로드 */ diff --git a/resources/views/esign/send.blade.php b/resources/views/esign/send.blade.php index 3c21cc0f..5be98c0e 100644 --- a/resources/views/esign/send.blade.php +++ b/resources/views/esign/send.blade.php @@ -34,6 +34,11 @@ const [sending, setSending] = useState(false); const [sendMethod, setSendMethod] = useState('email'); const [smsFallback, setSmsFallback] = useState(true); + const [templates, setTemplates] = useState([]); + const [selectedTemplate, setSelectedTemplate] = useState(''); + const [templateLoading, setTemplateLoading] = useState(false); + const [templateError, setTemplateError] = useState(''); + const [templatePreview, setTemplatePreview] = useState(null); const fetchContract = useCallback(async () => { try { @@ -46,6 +51,36 @@ useEffect(() => { fetchContract(); }, [fetchContract]); + const fetchTemplates = useCallback(async () => { + setTemplateLoading(true); + setTemplateError(''); + setTemplates([]); + setSelectedTemplate(''); + setTemplatePreview(null); + try { + const res = await fetch('/esign/contracts/alimtalk-templates', { headers: getHeaders() }); + const json = await res.json(); + if (json.success && json.data?.templates?.length > 0) { + setTemplates(json.data.templates); + if (json.data.templates.length === 1) { + setSelectedTemplate(json.data.templates[0].name); + setTemplatePreview(json.data.templates[0]); + } + } else { + setTemplateError(json.message || '승인된 알림톡 템플릿이 없습니다.'); + } + } catch (e) { + setTemplateError('템플릿 목록 조회 중 오류가 발생했습니다.'); + } + setTemplateLoading(false); + }, []); + + useEffect(() => { + if (sendMethod === 'alimtalk' || sendMethod === 'both') { + if (templates.length === 0 && !templateLoading) fetchTemplates(); + } + }, [sendMethod]); + const handleSend = async () => { const methodLabel = SEND_METHODS.find(m => m.value === sendMethod)?.label || sendMethod; if (!window.confirm(`서명 요청을 발송하시겠습니까?\n발송 방식: ${methodLabel}`)) return; @@ -53,7 +88,11 @@ try { const res = await fetch(`/esign/contracts/${CONTRACT_ID}/send`, { method: 'POST', headers: getHeaders(), - body: JSON.stringify({ send_method: sendMethod, sms_fallback: smsFallback }), + body: JSON.stringify({ + send_method: sendMethod, + sms_fallback: smsFallback, + template_name: (sendMethod === 'alimtalk' || sendMethod === 'both') ? selectedTemplate : undefined, + }), }); const json = await res.json(); if (json.success) { @@ -138,6 +177,52 @@ className="rounded text-blue-600 focus:ring-blue-500" /> (카카오톡 미사용자에게 SMS로 자동 전환) )} + + {/* 알림톡 템플릿 선택 */} + {needsAlimtalk && ( +
+ + {templateLoading ? ( +
템플릿 목록 조회 중...
+ ) : templateError ? ( +
+ {templateError} + +
+ ) : ( + <> + + + {templatePreview && ( +
+

미리보기

+
{templatePreview.content}
+ {templatePreview.buttons?.length > 0 && ( +
+ {templatePreview.buttons.map((btn, i) => ( + {btn.Name} + ))} +
+ )} +
+ )} + + )} +
+ )} {/* 서명자 연락처 확인 */} @@ -171,7 +256,7 @@ className="rounded text-blue-600 focus:ring-blue-500" /> {/* 발송 버튼 */}
돌아가기 - @@ -180,6 +265,9 @@ className="px-6 py-2 bg-green-600 text-white rounded-lg hover:bg-green-700 text- {fieldsCount === 0 && (

서명 필드를 먼저 설정해 주세요.

)} + {needsAlimtalk && !selectedTemplate && fieldsCount > 0 && ( +

알림톡 템플릿을 선택해 주세요.

+ )}
); }; diff --git a/routes/web.php b/routes/web.php index e3a2c4ea..fdeb3ebe 100644 --- a/routes/web.php +++ b/routes/web.php @@ -1520,6 +1520,7 @@ Route::get('/search-partners', [EsignApiController::class, 'searchPartners'])->name('search-partners'); Route::get('/search-tenants', [EsignApiController::class, 'searchTenants'])->name('search-tenants'); Route::get('/generate-contract-number', [EsignApiController::class, 'generateContractNumber'])->name('generate-contract-number'); + Route::get('/alimtalk-templates', [EsignApiController::class, 'getAlimtalkTemplates'])->name('alimtalk-templates'); Route::get('/stats', [EsignApiController::class, 'stats'])->name('stats'); Route::get('/list', [EsignApiController::class, 'index'])->name('list');