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

@@ -4,6 +4,7 @@
use App\Http\Controllers\Controller;
use App\Mail\EsignRequestMail;
use App\Models\Barobill\BarobillMember;
use App\Models\ESign\EsignContract;
use App\Models\User;
use App\Services\ESign\DocxToPdfConverter;
@@ -13,6 +14,7 @@
use App\Models\ESign\EsignSignField;
use App\Models\ESign\EsignAuditLog;
use App\Models\Tenants\TenantSetting;
use App\Services\Barobill\BarobillService;
use App\Services\GoogleCloudStorageService;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
@@ -692,27 +694,27 @@ public function send(Request $request, int $id): JsonResponse
return response()->json(['success' => false, 'message' => '초안 상태에서만 발송할 수 있습니다.'], 422);
}
$sendMethod = $request->input('send_method', 'alimtalk');
$smsFallback = $request->boolean('sms_fallback', true);
$contract->update([
'status' => 'pending',
'send_method' => $sendMethod,
'sms_fallback' => $smsFallback,
'updated_by' => auth()->id(),
]);
// 서명 순서 유형에 따라 알림 발송
// 발송 대상 서명자 결정
if ($contract->sign_order_type === 'parallel') {
// 동시 서명: 모든 서명자에게 발송
foreach ($contract->signers as $s) {
$s->update(['status' => 'notified']);
Mail::to($s->email)->send(new EsignRequestMail($contract, $s));
}
$targetSigners = $contract->signers;
} else {
// 순차 서명: 첫 번째 서명자에게 발송
$firstSigner = $contract->signers()
->orderBy('sign_order')
->first();
if ($firstSigner) {
$firstSigner->update(['status' => 'notified']);
Mail::to($firstSigner->email)->send(new EsignRequestMail($contract, $firstSigner));
}
$first = $contract->signers()->orderBy('sign_order')->first();
$targetSigners = $first ? collect([$first]) : collect();
}
foreach ($targetSigners as $signer) {
$signer->update(['status' => 'notified']);
$this->dispatchNotification($contract, $signer, $sendMethod, $smsFallback);
}
EsignAuditLog::create([
@@ -721,7 +723,7 @@ public function send(Request $request, int $id): JsonResponse
'action' => 'sign_request_sent',
'ip_address' => $request->ip(),
'user_agent' => $request->userAgent(),
'metadata' => ['sent_by' => auth()->id()],
'metadata' => ['sent_by' => auth()->id(), 'send_method' => $sendMethod],
'created_at' => now(),
]);
@@ -748,7 +750,12 @@ public function remind(Request $request, int $id): JsonResponse
if ($nextSigner) {
$nextSigner->update(['status' => 'notified']);
Mail::to($nextSigner->email)->send(new EsignRequestMail($contract, $nextSigner, isReminder: true));
$this->dispatchNotification(
$contract, $nextSigner,
$contract->send_method ?? 'alimtalk',
$contract->sms_fallback ?? true,
isReminder: true,
);
}
EsignAuditLog::create([
@@ -772,6 +779,88 @@ public function remind(Request $request, int $id): JsonResponse
]);
}
/**
* 발송 방식에 따라 알림톡/이메일 분기 발송
*/
private function dispatchNotification(
EsignContract $contract,
EsignSigner $signer,
string $sendMethod,
bool $smsFallback,
bool $isReminder = false,
): void {
// 알림톡 발송
if (in_array($sendMethod, ['alimtalk', 'both']) && $signer->phone) {
$this->sendAlimtalk($contract, $signer, $smsFallback, $isReminder);
}
// 이메일 발송 (email/both 선택 시, 또는 알림톡인데 번호 없으면 폴백)
if (in_array($sendMethod, ['email', 'both']) || ($sendMethod === 'alimtalk' && !$signer->phone)) {
if ($signer->email) {
Mail::to($signer->email)->send(new EsignRequestMail($contract, $signer, $isReminder));
}
}
}
/**
* 알림톡 발송
*/
private function sendAlimtalk(
EsignContract $contract,
EsignSigner $signer,
bool $smsFallback = true,
bool $isReminder = false,
): 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;
$expires = $contract->expires_at?->format('Y-m-d H:i') ?? '없음';
$templateName = $isReminder ? '전자계약_리마인드' : '전자계약_서명요청';
$message = $isReminder
? "안녕하세요, {$signer->name}님.\n아직 서명이 완료되지 않은 전자계약이 있습니다.\n\n■ 계약명: {$contract->title}\n■ 서명 기한: {$expires}\n\n기한 내에 서명을 완료해 주세요."
: "안녕하세요, {$signer->name}님.\n전자계약 서명 요청이 도착했습니다.\n\n■ 계약명: {$contract->title}\n■ 서명 기한: {$expires}\n\n아래 버튼을 눌러 계약서를 확인하고 서명해 주세요.";
$smsMessage = $smsFallback
? "[SAM] {$signer->name}님, 전자계약 서명 요청이 도착했습니다. {$signUrl}"
: '';
$barobill->sendATKakaotalkEx(
corpNum: $member->corp_num,
certKey: $member->cert_key,
senderId: $member->kakaotalk_sender_id ?? '',
templateName: $templateName,
receiverName: $signer->name,
receiverNum: preg_replace('/[^0-9]/', '', $signer->phone),
title: $isReminder ? '전자계약 리마인드' : '전자계약 서명 요청',
message: $message,
buttons: [
[
'Name' => $isReminder ? '서명하기' : '계약서 확인하기',
'ButtonType' => 'WL',
'Url1' => $signUrl,
'Url2' => $signUrl,
],
],
smsMessage: $smsMessage,
);
} catch (\Throwable $e) {
\Log::warning('E-Sign 알림톡 발송 실패', [
'contract_id' => $contract->id,
'signer_id' => $signer->id,
'error' => $e->getMessage(),
]);
}
}
/**
* PDF 다운로드
*/