fix: [esign] 서명 요청/다음 서명자 알림에 역할 기반 분기 적용

- dispatchNotification: 상대방(counterpart)만 알림톡, 본사(creator)는 이메일
- 순차 서명 시 다음 서명자 알림도 동일 역할 기반 분기 적용
- 다음 서명자 알림에서 getKakaotalkChannelId/getTemplateData 헬퍼 활용
- 알림톡 실패 시 이메일 자동 폴백 로직 통일
This commit is contained in:
김보곤
2026-02-26 23:03:43 +09:00
parent 50c43b52b0
commit 9676f0409e
2 changed files with 82 additions and 63 deletions

View File

@@ -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 {

View File

@@ -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,