From 9676f0409efcde46b4cafebc663d75614aafacad Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B9=80=EB=B3=B4=EA=B3=A4?= Date: Thu, 26 Feb 2026 23:03:43 +0900 Subject: [PATCH] =?UTF-8?q?fix:=20[esign]=20=EC=84=9C=EB=AA=85=20=EC=9A=94?= =?UTF-8?q?=EC=B2=AD/=EB=8B=A4=EC=9D=8C=20=EC=84=9C=EB=AA=85=EC=9E=90=20?= =?UTF-8?q?=EC=95=8C=EB=A6=BC=EC=97=90=20=EC=97=AD=ED=95=A0=20=EA=B8=B0?= =?UTF-8?q?=EB=B0=98=20=EB=B6=84=EA=B8=B0=20=EC=A0=81=EC=9A=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - dispatchNotification: 상대방(counterpart)만 알림톡, 본사(creator)는 이메일 - 순차 서명 시 다음 서명자 알림도 동일 역할 기반 분기 적용 - 다음 서명자 알림에서 getKakaotalkChannelId/getTemplateData 헬퍼 활용 - 알림톡 실패 시 이메일 자동 폴백 로직 통일 --- .../Controllers/ESign/EsignApiController.php | 15 +- .../ESign/EsignPublicController.php | 130 ++++++++++-------- 2 files changed, 82 insertions(+), 63 deletions(-) diff --git a/app/Http/Controllers/ESign/EsignApiController.php b/app/Http/Controllers/ESign/EsignApiController.php index 45f20a7d..15089440 100644 --- a/app/Http/Controllers/ESign/EsignApiController.php +++ b/app/Http/Controllers/ESign/EsignApiController.php @@ -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 { diff --git a/app/Http/Controllers/ESign/EsignPublicController.php b/app/Http/Controllers/ESign/EsignPublicController.php index 255d54b5..61796a81 100644 --- a/app/Http/Controllers/ESign/EsignPublicController.php +++ b/app/Http/Controllers/ESign/EsignPublicController.php @@ -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,