feat: [esign] OTP 인증코드 SMS 발송 기능 추가

- send_method가 alimtalk/both일 때 SMS로 OTP 발송
- 바로빌 sendATKakaotalkEx SMS 대체발송 기능 활용
- SMS 실패 시 이메일 폴백
- auth.blade.php UI 메시지 SMS/이메일 분기 표시
This commit is contained in:
김보곤
2026-02-26 09:06:36 +09:00
parent daee3e3334
commit 59f68e272c
2 changed files with 113 additions and 9 deletions

View File

@@ -127,11 +127,13 @@ public function getContract(string $token): JsonResponse
'id' => $signer->id,
'name' => $signer->name,
'email' => $signer->email,
'phone' => $signer->phone,
'role' => $signer->role,
'status' => $signer->status,
'has_stamp' => (bool) $signer->signature_image_path,
'signed_at' => $signer->signed_at,
],
'send_method' => $contract->send_method ?? 'email',
'is_signable' => $isSignable,
'status_message' => $statusMessage,
],
@@ -161,10 +163,28 @@ public function sendOtp(string $token): JsonResponse
'otp_attempts' => 0,
]);
// OTP 이메일 발송
\Illuminate\Support\Facades\Mail::to($signer->email)->send(
new \App\Mail\EsignOtpMail($signer->name, $otpCode)
);
$sendMethod = $contract->send_method ?? 'email';
$channel = 'email';
// 알림톡/both 방식이고 전화번호가 있으면 SMS로 발송
if (in_array($sendMethod, ['alimtalk', 'both']) && $signer->phone) {
$smsSent = $this->sendOtpViaSms($contract, $signer, $otpCode);
if ($smsSent) {
$channel = 'sms';
} else {
// SMS 실패 시 이메일 폴백
if ($signer->email) {
Mail::to($signer->email)->send(new \App\Mail\EsignOtpMail($signer->name, $otpCode));
$channel = 'email';
} else {
return response()->json(['success' => false, 'message' => 'OTP 발송에 실패했습니다.'], 500);
}
}
} else {
// 이메일 방식 또는 전화번호 없음
Mail::to($signer->email)->send(new \App\Mail\EsignOtpMail($signer->name, $otpCode));
$channel = 'email';
}
EsignAuditLog::create([
'tenant_id' => $contract->tenant_id,
@@ -173,11 +193,19 @@ public function sendOtp(string $token): JsonResponse
'action' => 'otp_sent',
'ip_address' => request()->ip(),
'user_agent' => request()->userAgent(),
'metadata' => ['email' => $signer->email],
'metadata' => [
'channel' => $channel,
'email' => $channel === 'email' ? $signer->email : null,
'phone' => $channel === 'sms' ? $signer->phone : null,
],
'created_at' => now(),
]);
return response()->json(['success' => true, 'message' => '인증 코드가 발송되었습니다.']);
return response()->json([
'success' => true,
'message' => '인증 코드가 발송되었습니다.',
'channel' => $channel,
]);
}
/**
@@ -585,6 +613,74 @@ public function downloadDocument(string $token): StreamedResponse|JsonResponse
// ─── Private ───
/**
* SMS로 OTP 발송 (바로빌 알림톡 SMS 대체발송 기능 활용)
*/
private function sendOtpViaSms(EsignContract $contract, EsignSigner $signer, string $otpCode): bool
{
try {
$member = BarobillMember::where('tenant_id', $contract->tenant_id)->first();
if (! $member || ! $member->manager_hp) {
Log::warning('OTP SMS 발송 실패: 바로빌 회원 또는 발신번호 없음', [
'contract_id' => $contract->id,
'tenant_id' => $contract->tenant_id,
]);
return false;
}
$barobill = app(BarobillService::class);
$barobill->setServerMode($member->server_mode ?? 'production');
// 채널 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 ?? '';
}
}
$smsText = "[SAM] 전자계약 인증코드: {$otpCode} (5분 이내 입력)";
$result = $barobill->sendATKakaotalkEx(
corpNum: $member->biz_no,
senderId: $member->barobill_id,
yellowId: $yellowId,
templateName: '인증코드',
receiverName: $signer->name,
receiverNum: preg_replace('/[^0-9]/', '', $signer->phone),
title: '',
message: $smsText,
buttons: [],
smsMessage: $smsText,
smsSenderNum: preg_replace('/[^0-9]/', '', $member->manager_hp),
);
if ($result['success'] ?? false) {
return true;
}
Log::warning('OTP SMS 발송 API 실패', [
'contract_id' => $contract->id,
'signer_id' => $signer->id,
'error' => $result['error'] ?? 'Unknown',
]);
return false;
} catch (\Throwable $e) {
Log::error('OTP SMS 발송 예외', [
'contract_id' => $contract->id,
'signer_id' => $signer->id,
'error' => $e->getMessage(),
]);
return false;
}
}
private function sendCompletionAlimtalk(EsignContract $contract, EsignSigner $signer): array
{
try {