feat:전자계약 알림톡 발송 방식 선택 기능 추가

- EsignApiController: send_method/sms_fallback 저장, dispatchNotification/sendAlimtalk 메서드 추가
- EsignPublicController: 완료/다음서명자 알림에 알림톡 지원 추가
- EsignContract 모델: send_method, sms_fallback fillable/casts 추가
- send.blade.php: 발송방식 선택 UI (알림톡/이메일/동시), SMS 대체발송, 연락처 확인

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
김보곤
2026-02-14 16:12:41 +09:00
parent 33871e3240
commit bea7bd5987
4 changed files with 301 additions and 52 deletions

View File

@@ -3,6 +3,7 @@
namespace App\Http\Controllers\ESign;
use App\Http\Controllers\Controller;
use App\Models\Barobill\BarobillMember;
use App\Models\ESign\EsignAuditLog;
use App\Models\ESign\EsignContract;
use App\Models\ESign\EsignSigner;
@@ -12,6 +13,7 @@
use App\Mail\EsignRequestMail;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Mail;
use App\Services\Barobill\BarobillService;
use App\Services\ESign\PdfSignatureService;
use Illuminate\Support\Facades\Storage;
use Illuminate\Support\Str;
@@ -305,25 +307,36 @@ public function submitSignature(Request $request, string $token): JsonResponse
'created_at' => now(),
]);
// 모든 서명자에게 완료 이메일 발송
// 모든 서명자에게 완료 알림 발송
$sendMethod = $contract->send_method ?? 'alimtalk';
foreach ($allSigners as $completedSigner) {
try {
Mail::to($completedSigner->email)->send(
new EsignCompletedMail($contract, $completedSigner, $allSigners)
);
// 이메일 발송
if (in_array($sendMethod, ['email', 'both']) || !$completedSigner->phone) {
if ($completedSigner->email) {
Mail::to($completedSigner->email)->send(
new EsignCompletedMail($contract, $completedSigner, $allSigners)
);
}
}
// 알림톡 발송
if (in_array($sendMethod, ['alimtalk', 'both']) && $completedSigner->phone) {
$this->sendCompletionAlimtalk($contract, $completedSigner);
}
EsignAuditLog::create([
'tenant_id' => $contract->tenant_id,
'contract_id' => $contract->id,
'signer_id' => $completedSigner->id,
'action' => 'completion_email_sent',
'action' => 'completion_notification_sent',
'ip_address' => $request->ip(),
'user_agent' => $request->userAgent(),
'metadata' => ['email' => $completedSigner->email],
'metadata' => ['send_method' => $sendMethod],
'created_at' => now(),
]);
} catch (\Throwable $e) {
Log::error('계약 완료 이메일 발송 실패', [
Log::error('계약 완료 알림 발송 실패', [
'contract_id' => $contract->id,
'signer_id' => $completedSigner->id,
'error' => $e->getMessage(),
@@ -342,7 +355,42 @@ public function submitSignature(Request $request, string $token): JsonResponse
if ($nextSigner) {
$nextSigner->update(['status' => 'notified']);
Mail::to($nextSigner->email)->send(new EsignRequestMail($contract, $nextSigner));
$nextSendMethod = $contract->send_method ?? 'alimtalk';
$nextSmsFallback = $contract->sms_fallback ?? true;
// 알림톡 발송
if (in_array($nextSendMethod, ['alimtalk', 'both']) && $nextSigner->phone) {
try {
$member = BarobillMember::where('tenant_id', $contract->tenant_id)->first();
if ($member) {
$barobill = app(BarobillService::class);
$barobill->setServerMode($member->is_test_mode ? 'test' : 'production');
$nextSignUrl = config('app.url') . '/esign/sign/' . $nextSigner->access_token;
$nextExpires = $contract->expires_at?->format('Y-m-d H:i') ?? '없음';
$barobill->sendATKakaotalkEx(
corpNum: $member->corp_num,
certKey: $member->cert_key,
senderId: $member->kakaotalk_sender_id ?? '',
templateName: '전자계약_서명요청',
receiverName: $nextSigner->name,
receiverNum: preg_replace('/[^0-9]/', '', $nextSigner->phone),
title: '전자계약 서명 요청',
message: "안녕하세요, {$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}" : '',
);
}
} catch (\Throwable $e) {
Log::warning('다음 서명자 알림톡 발송 실패', ['error' => $e->getMessage()]);
}
}
// 이메일 발송
if (in_array($nextSendMethod, ['email', 'both']) || ($nextSendMethod === 'alimtalk' && !$nextSigner->phone)) {
if ($nextSigner->email) {
Mail::to($nextSigner->email)->send(new EsignRequestMail($contract, $nextSigner));
}
}
EsignAuditLog::create([
'tenant_id' => $contract->tenant_id,
@@ -436,6 +484,50 @@ public function downloadDocument(string $token): StreamedResponse|JsonResponse
// ─── Private ───
private function sendCompletionAlimtalk(EsignContract $contract, EsignSigner $signer): void
{
try {
$member = BarobillMember::where('tenant_id', $contract->tenant_id)->first();
if (!$member) {
return;
}
$barobill = app(BarobillService::class);
$barobill->setServerMode($member->is_test_mode ? 'test' : 'production');
$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');
$barobill->sendATKakaotalkEx(
corpNum: $member->corp_num,
certKey: $member->cert_key,
senderId: $member->kakaotalk_sender_id ?? '',
templateName: '전자계약_완료',
receiverName: $signer->name,
receiverNum: preg_replace('/[^0-9]/', '', $signer->phone),
title: '전자계약 완료',
message: "안녕하세요, {$signer->name}님.\n전자계약이 모든 서명자의 서명 완료로 확정되었습니다.\n\n■ 계약명: {$contract->title}\n■ 완료일: {$completedAt}\n\n아래 버튼에서 서명 완료된 계약서를 확인할 수 있습니다.",
buttons: [
[
'Name' => '계약서 확인하기',
'ButtonType' => 'WL',
'Url1' => $signUrl,
'Url2' => $signUrl,
],
],
smsMessage: ($contract->sms_fallback ?? true)
? "[SAM] {$signer->name}님, 전자계약이 완료되었습니다. {$signUrl}"
: '',
);
} catch (\Throwable $e) {
Log::warning('E-Sign 완료 알림톡 발송 실패', [
'contract_id' => $contract->id,
'signer_id' => $signer->id,
'error' => $e->getMessage(),
]);
}
}
private function findSigner(string $token): ?EsignSigner
{
$signer = EsignSigner::withoutGlobalScopes()