fix: [esign] 서명 요청/다음 서명자 알림에 역할 기반 분기 적용
- dispatchNotification: 상대방(counterpart)만 알림톡, 본사(creator)는 이메일 - 순차 서명 시 다음 서명자 알림도 동일 역할 기반 분기 적용 - 다음 서명자 알림에서 getKakaotalkChannelId/getTemplateData 헬퍼 활용 - 알림톡 실패 시 이메일 자동 폴백 로직 통일
This commit is contained in:
@@ -978,9 +978,10 @@ private function dispatchNotification(
|
||||
): array {
|
||||
$results = [];
|
||||
$alimtalkFailed = false;
|
||||
$isCounterpart = $signer->role === EsignSigner::ROLE_COUNTERPART;
|
||||
|
||||
// 알림톡 발송
|
||||
if (in_array($sendMethod, ['alimtalk', 'both']) && $signer->phone) {
|
||||
// 알림톡 발송: 상대방(counterpart)에게만 카카오톡 발송, 본사(creator)는 이메일
|
||||
if (in_array($sendMethod, ['alimtalk', 'both']) && $isCounterpart && $signer->phone) {
|
||||
$alimtalkResult = $this->sendAlimtalk($contract, $signer, $smsFallback, $isReminder, $templateName);
|
||||
$results[] = $alimtalkResult;
|
||||
$alimtalkFailed = ! ($alimtalkResult['success'] ?? false);
|
||||
@@ -988,11 +989,13 @@ private function dispatchNotification(
|
||||
|
||||
// 이메일 발송 조건:
|
||||
// 1) email/both 선택 시
|
||||
// 2) alimtalk인데 번호 없으면 폴백
|
||||
// 3) alimtalk 발송 실패 시 이메일 자동 폴백
|
||||
// 2) 본사(creator)는 항상 이메일
|
||||
// 3) 상대방이지만 전화번호 없으면 이메일 폴백
|
||||
// 4) 알림톡 발송 실패 시 이메일 자동 폴백
|
||||
$shouldSendEmail = in_array($sendMethod, ['email', 'both'])
|
||||
|| ($sendMethod === 'alimtalk' && ! $signer->phone)
|
||||
|| ($sendMethod === 'alimtalk' && $alimtalkFailed);
|
||||
|| ! $isCounterpart
|
||||
|| ($sendMethod === 'alimtalk' && $isCounterpart && ! $signer->phone)
|
||||
|| ($sendMethod === 'alimtalk' && $isCounterpart && $alimtalkFailed);
|
||||
|
||||
if ($shouldSendEmail && $signer->email) {
|
||||
try {
|
||||
|
||||
@@ -462,87 +462,102 @@ public function submitSignature(Request $request, string $token): JsonResponse
|
||||
$nextSigner->update(['status' => 'notified']);
|
||||
$nextSendMethod = $contract->send_method ?? 'alimtalk';
|
||||
$nextSmsFallback = $contract->sms_fallback ?? true;
|
||||
$nextIsCounterpart = $nextSigner->role === EsignSigner::ROLE_COUNTERPART;
|
||||
|
||||
$notificationResults = [];
|
||||
$alimtalkFailed = false;
|
||||
|
||||
// 알림톡 발송
|
||||
if (in_array($nextSendMethod, ['alimtalk', 'both']) && $nextSigner->phone) {
|
||||
// 알림톡 발송: 상대방(counterpart)에게만 카카오톡 발송
|
||||
if (in_array($nextSendMethod, ['alimtalk', 'both']) && $nextIsCounterpart && $nextSigner->phone) {
|
||||
try {
|
||||
$member = BarobillMember::where('tenant_id', $contract->tenant_id)->first();
|
||||
if ($member) {
|
||||
if ($member && $member->biz_no) {
|
||||
$barobill = app(BarobillService::class);
|
||||
$barobill->setServerMode($member->server_mode ?? 'production');
|
||||
$nextSignUrl = config('app.url').'/esign/sign/'.$nextSigner->access_token;
|
||||
$nextExpires = $contract->expires_at?->format('Y-m-d H:i') ?? '없음';
|
||||
|
||||
// 채널 ID 조회
|
||||
$channelResult = $barobill->getKakaotalkChannels($member->biz_no);
|
||||
$yellowId = '';
|
||||
if ($channelResult['success'] ?? false) {
|
||||
$chData = $channelResult['data'];
|
||||
if (is_object($chData) && isset($chData->KakaotalkChannel)) {
|
||||
$ch = is_array($chData->KakaotalkChannel) ? $chData->KakaotalkChannel[0] : $chData->KakaotalkChannel;
|
||||
$yellowId = $ch->ChannelId ?? '';
|
||||
}
|
||||
}
|
||||
$channelId = $this->getKakaotalkChannelId($barobill, $member->biz_no);
|
||||
|
||||
// 템플릿 본문 조회하여 변수 치환
|
||||
$tplResult = $barobill->getKakaotalkTemplates($member->biz_no, $yellowId);
|
||||
$tplMessage = null;
|
||||
if ($tplResult['success'] ?? false) {
|
||||
$tplData = $tplResult['data'];
|
||||
$tplItems = [];
|
||||
if (is_object($tplData) && isset($tplData->KakaotalkTemplate)) {
|
||||
$tplItems = is_array($tplData->KakaotalkTemplate) ? $tplData->KakaotalkTemplate : [$tplData->KakaotalkTemplate];
|
||||
}
|
||||
foreach ($tplItems as $t) {
|
||||
if (($t->TemplateName ?? '') === '전자계약_서명요청') {
|
||||
$tplMessage = str_replace(
|
||||
['#{이름}', '#{계약명}', '#{기한}'],
|
||||
[$nextSigner->name, $contract->title, $nextExpires],
|
||||
$t->TemplateContent
|
||||
);
|
||||
break;
|
||||
if ($channelId) {
|
||||
// 템플릿 본문 + 버튼 조회
|
||||
$tplData = $this->getTemplateData($barobill, $member->biz_no, $channelId, '전자계약_서명요청');
|
||||
$tplMessage = $tplData['content']
|
||||
? str_replace(
|
||||
['#{이름}', '#{계약명}', '#{기한}'],
|
||||
[$nextSigner->name, $contract->title, $nextExpires],
|
||||
$tplData['content']
|
||||
)
|
||||
: null;
|
||||
|
||||
// 버튼: 템플릿에서 가져온 URL의 #{토큰} 치환
|
||||
$buttons = ! empty($tplData['buttons']) ? $tplData['buttons'] : [
|
||||
['Name' => '계약서 확인하기', 'ButtonType' => 'WL', 'Url1' => $nextSignUrl, 'Url2' => $nextSignUrl],
|
||||
];
|
||||
foreach ($buttons as &$btn) {
|
||||
foreach (['Url1', 'Url2'] as $urlKey) {
|
||||
if (! empty($btn[$urlKey])) {
|
||||
$btn[$urlKey] = str_replace(
|
||||
['#{토큰}', '#{%ED%86%A0%ED%81%B0}'],
|
||||
[$nextSigner->access_token, $nextSigner->access_token],
|
||||
urldecode($btn[$urlKey])
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
unset($btn);
|
||||
|
||||
$atResult = $barobill->sendATKakaotalkEx(
|
||||
corpNum: $member->biz_no,
|
||||
senderId: $member->barobill_id,
|
||||
yellowId: $yellowId,
|
||||
templateName: '전자계약_서명요청',
|
||||
receiverName: $nextSigner->name,
|
||||
receiverNum: preg_replace('/[^0-9]/', '', $nextSigner->phone),
|
||||
title: '',
|
||||
message: $tplMessage ?? "안녕하세요, {$nextSigner->name}님.\n전자계약 서명 요청이 도착했습니다.\n\n■ 계약명: {$contract->title}\n■ 서명 기한: {$nextExpires}\n\n아래 버튼을 눌러 계약서를 확인하고 서명해 주세요.",
|
||||
buttons: [['Name' => '계약서 확인하기', 'ButtonType' => 'WL', 'Url1' => $nextSignUrl, 'Url2' => $nextSignUrl]],
|
||||
smsMessage: $nextSmsFallback ? "[SAM] {$nextSigner->name}님, 전자계약 서명 요청이 도착했습니다. {$nextSignUrl}" : '',
|
||||
);
|
||||
$notificationResults[] = [
|
||||
'success' => $atResult['success'] ?? false,
|
||||
'channel' => 'alimtalk',
|
||||
'error' => $atResult['error'] ?? null,
|
||||
];
|
||||
$atResult = $barobill->sendATKakaotalkEx(
|
||||
corpNum: $member->biz_no,
|
||||
senderId: $member->barobill_id,
|
||||
yellowId: $channelId,
|
||||
templateName: '전자계약_서명요청',
|
||||
receiverName: $nextSigner->name,
|
||||
receiverNum: preg_replace('/[^0-9]/', '', $nextSigner->phone),
|
||||
title: '',
|
||||
message: $tplMessage ?? "안녕하세요, {$nextSigner->name}님.\n전자계약 서명 요청이 도착했습니다.\n\n■ 계약명: {$contract->title}\n■ 서명 기한: {$nextExpires}\n\n아래 버튼을 눌러 계약서를 확인하고 서명해 주세요.",
|
||||
buttons: $buttons,
|
||||
smsMessage: $nextSmsFallback ? "[SAM] {$nextSigner->name}님, 전자계약 서명 요청이 도착했습니다. {$nextSignUrl}" : '',
|
||||
);
|
||||
$alimtalkFailed = ! ($atResult['success'] ?? false);
|
||||
$notificationResults[] = [
|
||||
'success' => $atResult['success'] ?? false,
|
||||
'channel' => 'alimtalk',
|
||||
'error' => $atResult['error'] ?? null,
|
||||
];
|
||||
} else {
|
||||
$alimtalkFailed = true;
|
||||
$notificationResults[] = ['success' => false, 'channel' => 'alimtalk', 'error' => '등록된 카카오톡 채널 없음'];
|
||||
}
|
||||
} else {
|
||||
$alimtalkFailed = true;
|
||||
$notificationResults[] = ['success' => false, 'channel' => 'alimtalk', 'error' => '바로빌 회원 미등록'];
|
||||
}
|
||||
} catch (\Throwable $e) {
|
||||
Log::warning('다음 서명자 알림톡 발송 실패', ['error' => $e->getMessage()]);
|
||||
$alimtalkFailed = true;
|
||||
$notificationResults[] = ['success' => false, 'channel' => 'alimtalk', 'error' => $e->getMessage()];
|
||||
}
|
||||
}
|
||||
|
||||
// 이메일 발송
|
||||
if (in_array($nextSendMethod, ['email', 'both']) || ($nextSendMethod === 'alimtalk' && ! $nextSigner->phone)) {
|
||||
if ($nextSigner->email) {
|
||||
try {
|
||||
Mail::to($nextSigner->email)->send(new EsignRequestMail($contract, $nextSigner));
|
||||
$notificationResults[] = ['success' => true, 'channel' => 'email', 'error' => null];
|
||||
} catch (\Throwable $e) {
|
||||
Log::warning('다음 서명자 이메일 발송 실패', ['error' => $e->getMessage()]);
|
||||
$notificationResults[] = ['success' => false, 'channel' => 'email', 'error' => $e->getMessage()];
|
||||
}
|
||||
// 이메일 발송 조건:
|
||||
// 1) email/both 선택 시
|
||||
// 2) 본사(creator)는 항상 이메일
|
||||
// 3) 상대방이지만 전화번호 없으면 이메일 폴백
|
||||
// 4) 알림톡 발송 실패 시 이메일 자동 폴백
|
||||
$shouldSendEmail = in_array($nextSendMethod, ['email', 'both'])
|
||||
|| ! $nextIsCounterpart
|
||||
|| ($nextSendMethod === 'alimtalk' && $nextIsCounterpart && ! $nextSigner->phone)
|
||||
|| ($nextSendMethod === 'alimtalk' && $nextIsCounterpart && $alimtalkFailed);
|
||||
|
||||
if ($shouldSendEmail && $nextSigner->email) {
|
||||
try {
|
||||
Mail::to($nextSigner->email)->send(new EsignRequestMail($contract, $nextSigner));
|
||||
$notificationResults[] = ['success' => true, 'channel' => 'email', 'error' => null];
|
||||
} catch (\Throwable $e) {
|
||||
Log::warning('다음 서명자 이메일 발송 실패', ['error' => $e->getMessage()]);
|
||||
$notificationResults[] = ['success' => false, 'channel' => 'email', 'error' => $e->getMessage()];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -555,6 +570,7 @@ public function submitSignature(Request $request, string $token): JsonResponse
|
||||
'user_agent' => $request->userAgent(),
|
||||
'metadata' => [
|
||||
'triggered_by' => 'auto_after_sign',
|
||||
'signer_role' => $nextSigner->role,
|
||||
'notification_results' => [[
|
||||
'signer_id' => $nextSigner->id,
|
||||
'signer_name' => $nextSigner->name,
|
||||
|
||||
Reference in New Issue
Block a user