feat: [esign] 완료 알림톡 템플릿 2종 선택 및 버튼 URL 도메인 치환
- 발송 UI에 서명 요청 + 완료 알림톡 템플릿 각각 선택 가능 - 선택한 완료 템플릿명을 DB에 저장하여 서명 완료 시 사용 - 버튼 URL 도메인을 현재 환경의 app.url로 자동 치환 (개발/운영 환경 대응)
This commit is contained in:
@@ -827,11 +827,13 @@ 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');
|
||||
$completionTemplateName = $request->input('completion_template_name');
|
||||
|
||||
$contract->update([
|
||||
'status' => 'pending',
|
||||
'send_method' => $sendMethod,
|
||||
'sms_fallback' => $smsFallback,
|
||||
'completion_template_name' => $completionTemplateName,
|
||||
'updated_by' => auth()->id(),
|
||||
]);
|
||||
|
||||
@@ -1090,6 +1092,20 @@ private function sendAlimtalk(
|
||||
}
|
||||
unset($btn);
|
||||
|
||||
// 버튼 URL 도메인을 현재 환경의 도메인으로 치환
|
||||
$appHost = parse_url(config('app.url'), PHP_URL_HOST);
|
||||
foreach ($buttons as &$btn) {
|
||||
foreach (['Url1', 'Url2'] as $urlKey) {
|
||||
if (! empty($btn[$urlKey])) {
|
||||
$parsed = parse_url($btn[$urlKey]);
|
||||
if (isset($parsed['host']) && $parsed['host'] !== $appHost) {
|
||||
$btn[$urlKey] = str_replace($parsed['host'], $appHost, $btn[$urlKey]);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
unset($btn);
|
||||
|
||||
$receiverNum = preg_replace('/[^0-9]/', '', $signer->phone);
|
||||
|
||||
\Log::info('E-Sign 알림톡 발송 시도', [
|
||||
|
||||
@@ -754,7 +754,8 @@ private function sendCompletionAlimtalk(EsignContract $contract, EsignSigner $si
|
||||
return ['success' => false, 'channel' => 'alimtalk', 'error' => '등록된 카카오톡 채널이 없습니다'];
|
||||
}
|
||||
|
||||
$templateName = $this->resolveTemplateName('전자계약_완료');
|
||||
$templateName = $contract->completion_template_name
|
||||
?: $this->resolveTemplateName('전자계약_완료');
|
||||
$documentUrl = config('app.url').'/esign/sign/'.$signer->access_token.'/api/document';
|
||||
$signUrl = config('app.url').'/esign/sign/'.$signer->access_token;
|
||||
$completedAt = $contract->completed_at?->format('Y-m-d H:i') ?? now()->format('Y-m-d H:i');
|
||||
@@ -807,6 +808,20 @@ private function sendCompletionAlimtalk(EsignContract $contract, EsignSigner $si
|
||||
}
|
||||
unset($btn);
|
||||
|
||||
// 버튼 URL 도메인을 현재 환경의 도메인으로 치환
|
||||
$appHost = parse_url(config('app.url'), PHP_URL_HOST);
|
||||
foreach ($buttons as &$btn) {
|
||||
foreach (['Url1', 'Url2'] as $urlKey) {
|
||||
if (! empty($btn[$urlKey])) {
|
||||
$parsed = parse_url($btn[$urlKey]);
|
||||
if (isset($parsed['host']) && $parsed['host'] !== $appHost) {
|
||||
$btn[$urlKey] = str_replace($parsed['host'], $appHost, $btn[$urlKey]);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
unset($btn);
|
||||
|
||||
$receiverNum = preg_replace('/[^0-9]/', '', $signer->phone);
|
||||
|
||||
Log::info('E-Sign 완료 알림톡 발송 시도', [
|
||||
|
||||
@@ -27,6 +27,7 @@ class EsignContract extends Model
|
||||
'status',
|
||||
'send_method',
|
||||
'sms_fallback',
|
||||
'completion_template_name',
|
||||
'metadata',
|
||||
'expires_at',
|
||||
'completed_at',
|
||||
|
||||
@@ -39,6 +39,8 @@
|
||||
const [templateLoading, setTemplateLoading] = useState(false);
|
||||
const [templateError, setTemplateError] = useState('');
|
||||
const [templatePreview, setTemplatePreview] = useState(null);
|
||||
const [selectedCompletionTemplate, setSelectedCompletionTemplate] = useState('');
|
||||
const [completionTemplatePreview, setCompletionTemplatePreview] = useState(null);
|
||||
|
||||
const fetchContract = useCallback(async () => {
|
||||
try {
|
||||
@@ -57,6 +59,8 @@
|
||||
setTemplates([]);
|
||||
setSelectedTemplate('');
|
||||
setTemplatePreview(null);
|
||||
setSelectedCompletionTemplate('');
|
||||
setCompletionTemplatePreview(null);
|
||||
try {
|
||||
const res = await fetch('/esign/contracts/alimtalk-templates', { headers: getHeaders() });
|
||||
const json = await res.json();
|
||||
@@ -92,6 +96,7 @@
|
||||
send_method: sendMethod,
|
||||
sms_fallback: smsFallback,
|
||||
template_name: (sendMethod === 'alimtalk' || sendMethod === 'both') ? selectedTemplate : undefined,
|
||||
completion_template_name: (sendMethod === 'alimtalk' || sendMethod === 'both') ? selectedCompletionTemplate : undefined,
|
||||
}),
|
||||
});
|
||||
const json = await res.json();
|
||||
@@ -178,10 +183,10 @@ className="rounded text-blue-600 focus:ring-blue-500" />
|
||||
</label>
|
||||
)}
|
||||
|
||||
{/* 알림톡 템플릿 선택 */}
|
||||
{/* 서명 요청 알림톡 템플릿 선택 */}
|
||||
{needsAlimtalk && (
|
||||
<div className="mt-4 p-4 bg-gray-50 rounded-lg border border-gray-200">
|
||||
<label className="block text-sm font-medium text-gray-700 mb-2">알림톡 템플릿 선택</label>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-2">서명 요청 알림톡 템플릿</label>
|
||||
{templateLoading ? (
|
||||
<div className="text-sm text-gray-400 py-2">템플릿 목록 조회 중...</div>
|
||||
) : templateError ? (
|
||||
@@ -223,6 +228,42 @@ className="w-full px-3 py-2 border border-gray-300 rounded-lg text-sm focus:ring
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 완료 알림톡 템플릿 선택 */}
|
||||
{needsAlimtalk && !templateLoading && !templateError && templates.length > 0 && (
|
||||
<div className="mt-4 p-4 bg-gray-50 rounded-lg border border-gray-200">
|
||||
<label className="block text-sm font-medium text-gray-700 mb-2">완료 알림톡 템플릿</label>
|
||||
<p className="text-xs text-gray-500 mb-2">모든 서명자 서명 완료 시 발송될 알림톡 템플릿을 선택하세요.</p>
|
||||
<select
|
||||
value={selectedCompletionTemplate}
|
||||
onChange={e => {
|
||||
setSelectedCompletionTemplate(e.target.value);
|
||||
const tpl = templates.find(t => t.name === e.target.value);
|
||||
setCompletionTemplatePreview(tpl || null);
|
||||
}}
|
||||
className="w-full px-3 py-2 border border-gray-300 rounded-lg text-sm focus:ring-blue-500 focus:border-blue-500"
|
||||
>
|
||||
<option value="">-- 선택 안함 (기본 템플릿 사용) --</option>
|
||||
{templates.map(t => (
|
||||
<option key={t.name} value={t.name}>{t.name}</option>
|
||||
))}
|
||||
</select>
|
||||
|
||||
{completionTemplatePreview && (
|
||||
<div className="mt-3 p-3 bg-white border border-gray-200 rounded-lg">
|
||||
<p className="text-xs font-medium text-gray-500 mb-2">미리보기</p>
|
||||
<pre className="text-xs text-gray-700 whitespace-pre-wrap leading-relaxed">{completionTemplatePreview.content}</pre>
|
||||
{completionTemplatePreview.buttons?.length > 0 && (
|
||||
<div className="mt-2 pt-2 border-t border-gray-100">
|
||||
{completionTemplatePreview.buttons.map((btn, i) => (
|
||||
<span key={i} className="inline-block px-3 py-1 mr-1 text-xs bg-yellow-100 text-yellow-800 rounded">{btn.Name}</span>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* 서명자 연락처 확인 */}
|
||||
|
||||
Reference in New Issue
Block a user