fix: [esign] 알림톡 발송 실패 수정 (corp_num → biz_no)
- BarobillMember의 실제 컬럼명은 biz_no인데 corp_num으로 접근하여 항상 null - is_test_mode → server_mode로 수정 - kakaotalk_sender_id → 빈 문자열 (기본 발신프로필 사용)
This commit is contained in:
@@ -5,17 +5,17 @@
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Mail\EsignRequestMail;
|
||||
use App\Models\Barobill\BarobillMember;
|
||||
use App\Models\ESign\EsignAuditLog;
|
||||
use App\Models\ESign\EsignContract;
|
||||
use App\Models\User;
|
||||
use App\Services\ESign\DocxToPdfConverter;
|
||||
use App\Models\ESign\EsignFieldTemplate;
|
||||
use App\Models\ESign\EsignFieldTemplateItem;
|
||||
use App\Models\ESign\EsignSigner;
|
||||
use App\Models\ESign\EsignSignField;
|
||||
use App\Models\ESign\EsignAuditLog;
|
||||
use App\Models\Sales\TenantProspect;
|
||||
use App\Models\Tenants\TenantSetting;
|
||||
use App\Models\User;
|
||||
use App\Services\Barobill\BarobillService;
|
||||
use App\Services\ESign\DocxToPdfConverter;
|
||||
use App\Services\GoogleCloudStorageService;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\Request;
|
||||
@@ -38,7 +38,7 @@ public function searchPartners(Request $request): JsonResponse
|
||||
$query = User::where('is_active', true)
|
||||
->whereHas('userRoles', function ($w) use ($tenantId) {
|
||||
$w->where('tenant_id', $tenantId)
|
||||
->whereHas('role', fn($r) => $r->whereIn('name', ['sales', 'manager']));
|
||||
->whereHas('role', fn ($r) => $r->whereIn('name', ['sales', 'manager']));
|
||||
})
|
||||
->with(['salesPartner', 'userRoles' => function ($q) use ($tenantId) {
|
||||
$q->where('tenant_id', $tenantId)->with('role');
|
||||
@@ -47,8 +47,8 @@ public function searchPartners(Request $request): JsonResponse
|
||||
if ($q !== '') {
|
||||
$query->where(function ($w) use ($q) {
|
||||
$w->where('name', 'like', "%{$q}%")
|
||||
->orWhere('email', 'like', "%{$q}%")
|
||||
->orWhere('phone', 'like', "%{$q}%");
|
||||
->orWhere('email', 'like', "%{$q}%")
|
||||
->orWhere('phone', 'like', "%{$q}%");
|
||||
});
|
||||
}
|
||||
|
||||
@@ -58,7 +58,8 @@ public function searchPartners(Request $request): JsonResponse
|
||||
|
||||
$data = $users->map(function ($user) use ($roleLabels) {
|
||||
$sp = $user->salesPartner;
|
||||
$roles = $user->userRoles->map(fn($ur) => $roleLabels[$ur->role?->name] ?? null)->filter()->values();
|
||||
$roles = $user->userRoles->map(fn ($ur) => $roleLabels[$ur->role?->name] ?? null)->filter()->values();
|
||||
|
||||
return [
|
||||
'id' => $user->id,
|
||||
'name' => $user->name,
|
||||
@@ -86,15 +87,15 @@ public function searchTenants(Request $request): JsonResponse
|
||||
if ($q !== '') {
|
||||
$query->where(function ($w) use ($q) {
|
||||
$w->where('company_name', 'like', "%{$q}%")
|
||||
->orWhere('business_number', 'like', "%{$q}%")
|
||||
->orWhere('ceo_name', 'like', "%{$q}%")
|
||||
->orWhere('contact_phone', 'like', "%{$q}%");
|
||||
->orWhere('business_number', 'like', "%{$q}%")
|
||||
->orWhere('ceo_name', 'like', "%{$q}%")
|
||||
->orWhere('contact_phone', 'like', "%{$q}%");
|
||||
});
|
||||
}
|
||||
|
||||
$prospects = $query->orderBy('company_name')->limit(20)->get();
|
||||
|
||||
$data = $prospects->map(fn($p) => [
|
||||
$data = $prospects->map(fn ($p) => [
|
||||
'id' => $p->id,
|
||||
'company_name' => $p->company_name,
|
||||
'business_number' => $p->business_number,
|
||||
@@ -118,7 +119,7 @@ public function generateContractNumber(): JsonResponse
|
||||
|
||||
$lastContract = EsignContract::where('tenant_id', $tenantId)
|
||||
->where('contract_code', 'like', "{$prefix}%")
|
||||
->orderByRaw("CAST(SUBSTRING(contract_code, ?) AS UNSIGNED) DESC", [strlen($prefix) + 1])
|
||||
->orderByRaw('CAST(SUBSTRING(contract_code, ?) AS UNSIGNED) DESC', [strlen($prefix) + 1])
|
||||
->first();
|
||||
|
||||
$seq = 1;
|
||||
@@ -129,7 +130,7 @@ public function generateContractNumber(): JsonResponse
|
||||
|
||||
return response()->json([
|
||||
'success' => true,
|
||||
'data' => ['contract_number' => $prefix . $seq],
|
||||
'data' => ['contract_number' => $prefix.$seq],
|
||||
]);
|
||||
}
|
||||
|
||||
@@ -144,14 +145,14 @@ public function getStamp(): JsonResponse
|
||||
->where('setting_key', 'company_stamp')
|
||||
->first();
|
||||
|
||||
if (!$setting || empty($setting->setting_value['gcs_object'])) {
|
||||
if (! $setting || empty($setting->setting_value['gcs_object'])) {
|
||||
return response()->json(['success' => true, 'data' => null]);
|
||||
}
|
||||
|
||||
$gcs = app(GoogleCloudStorageService::class);
|
||||
$signedUrl = $gcs->getSignedUrl($setting->setting_value['gcs_object'], 60);
|
||||
|
||||
if (!$signedUrl) {
|
||||
if (! $signedUrl) {
|
||||
return response()->json(['success' => true, 'data' => null]);
|
||||
}
|
||||
|
||||
@@ -175,12 +176,12 @@ public function uploadStamp(Request $request): JsonResponse
|
||||
$tenantId = session('selected_tenant_id', 1);
|
||||
$imageData = base64_decode($request->input('stamp_image_data'));
|
||||
|
||||
if (!$imageData) {
|
||||
if (! $imageData) {
|
||||
return response()->json(['success' => false, 'message' => '이미지 데이터가 올바르지 않습니다.'], 422);
|
||||
}
|
||||
|
||||
$gcs = app(GoogleCloudStorageService::class);
|
||||
if (!$gcs->isAvailable()) {
|
||||
if (! $gcs->isAvailable()) {
|
||||
return response()->json(['success' => false, 'message' => 'GCS 설정이 되어 있지 않습니다.'], 500);
|
||||
}
|
||||
|
||||
@@ -189,7 +190,7 @@ public function uploadStamp(Request $request): JsonResponse
|
||||
->where('setting_group', 'esign')
|
||||
->where('setting_key', 'company_stamp')
|
||||
->first();
|
||||
if ($existing && !empty($existing->setting_value['gcs_object'])) {
|
||||
if ($existing && ! empty($existing->setting_value['gcs_object'])) {
|
||||
$gcs->delete($existing->setting_value['gcs_object']);
|
||||
}
|
||||
|
||||
@@ -201,7 +202,7 @@ public function uploadStamp(Request $request): JsonResponse
|
||||
$gcsUri = $gcs->upload($tmpFile, $gcsObject);
|
||||
unlink($tmpFile);
|
||||
|
||||
if (!$gcsUri) {
|
||||
if (! $gcsUri) {
|
||||
return response()->json(['success' => false, 'message' => 'GCS 업로드에 실패했습니다.'], 500);
|
||||
}
|
||||
|
||||
@@ -290,7 +291,7 @@ public function index(Request $request): JsonResponse
|
||||
if ($search = $request->input('search')) {
|
||||
$query->where(function ($q) use ($search) {
|
||||
$q->where('title', 'like', "%{$search}%")
|
||||
->orWhere('contract_code', 'like', "%{$search}%");
|
||||
->orWhere('contract_code', 'like', "%{$search}%");
|
||||
});
|
||||
}
|
||||
|
||||
@@ -307,7 +308,7 @@ public function show(int $id): JsonResponse
|
||||
{
|
||||
$tenantId = session('selected_tenant_id', 1);
|
||||
$contract = EsignContract::forTenant($tenantId)
|
||||
->with(['signers', 'signFields', 'auditLogs' => fn($q) => $q->orderBy('created_at', 'desc')])
|
||||
->with(['signers', 'signFields', 'auditLogs' => fn ($q) => $q->orderBy('created_at', 'desc')])
|
||||
->findOrFail($id);
|
||||
|
||||
return response()->json(['success' => true, 'data' => $contract]);
|
||||
@@ -339,7 +340,7 @@ public function store(Request $request): JsonResponse
|
||||
$userId = auth()->id();
|
||||
|
||||
// 계약 코드 생성
|
||||
$contractCode = 'ES-' . date('Ymd') . '-' . strtoupper(Str::random(6));
|
||||
$contractCode = 'ES-'.date('Ymd').'-'.strtoupper(Str::random(6));
|
||||
|
||||
// PDF 파일 처리
|
||||
$filePath = null;
|
||||
@@ -350,7 +351,7 @@ public function store(Request $request): JsonResponse
|
||||
if ($request->hasFile('file')) {
|
||||
// 사용자가 직접 업로드한 파일 우선 사용 (Word면 PDF 자동 변환)
|
||||
$file = $request->file('file');
|
||||
$converter = new DocxToPdfConverter();
|
||||
$converter = new DocxToPdfConverter;
|
||||
$result = $converter->convertAndStore($file, "esign/{$tenantId}/contracts");
|
||||
$filePath = $result['path'];
|
||||
$fileName = $result['name'];
|
||||
@@ -364,7 +365,7 @@ public function store(Request $request): JsonResponse
|
||||
|
||||
if ($template && $template->file_path && Storage::disk('local')->exists($template->file_path)) {
|
||||
$ext = pathinfo($template->file_path, PATHINFO_EXTENSION) ?: 'pdf';
|
||||
$newPath = "esign/{$tenantId}/contracts/" . Str::random(40) . ".{$ext}";
|
||||
$newPath = "esign/{$tenantId}/contracts/".Str::random(40).".{$ext}";
|
||||
Storage::disk('local')->copy($template->file_path, $newPath);
|
||||
|
||||
$filePath = $newPath;
|
||||
@@ -418,7 +419,7 @@ public function store(Request $request): JsonResponse
|
||||
->where('setting_key', 'company_stamp')
|
||||
->first();
|
||||
|
||||
if ($stampSetting && !empty($stampSetting->setting_value['gcs_object'])) {
|
||||
if ($stampSetting && ! empty($stampSetting->setting_value['gcs_object'])) {
|
||||
$creatorSigner = EsignSigner::withoutGlobalScopes()
|
||||
->where('contract_id', $contract->id)
|
||||
->where('role', 'creator')
|
||||
@@ -459,7 +460,9 @@ public function store(Request $request): JsonResponse
|
||||
|
||||
foreach ($template->items as $item) {
|
||||
$signerId = $signerMap[$item->signer_order] ?? null;
|
||||
if (!$signerId) continue;
|
||||
if (! $signerId) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$fieldValue = null;
|
||||
if ($item->field_variable && isset($variableValues[$item->field_variable])) {
|
||||
@@ -592,7 +595,7 @@ public function trashed(Request $request): JsonResponse
|
||||
if ($search = $request->input('search')) {
|
||||
$query->where(function ($q) use ($search) {
|
||||
$q->where('title', 'like', "%{$search}%")
|
||||
->orWhere('contract_code', 'like', "%{$search}%");
|
||||
->orWhere('contract_code', 'like', "%{$search}%");
|
||||
});
|
||||
}
|
||||
|
||||
@@ -807,15 +810,15 @@ public function send(Request $request, int $id): JsonResponse
|
||||
$failures = [];
|
||||
foreach ($notificationResults as $nr) {
|
||||
foreach ($nr['results'] as $r) {
|
||||
if (!$r['success']) {
|
||||
if (! $r['success']) {
|
||||
$failures[] = "{$nr['signer_name']}: {$r['channel']} 실패 ({$r['error']})";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$message = '서명 요청이 발송되었습니다.';
|
||||
if (!empty($failures)) {
|
||||
$message .= ' (일부 알림 실패: ' . implode(', ', $failures) . ')';
|
||||
if (! empty($failures)) {
|
||||
$message .= ' (일부 알림 실패: '.implode(', ', $failures).')';
|
||||
}
|
||||
|
||||
return response()->json([
|
||||
@@ -880,7 +883,7 @@ public function remind(Request $request, int $id): JsonResponse
|
||||
$failures = [];
|
||||
foreach ($notificationResults as $nr) {
|
||||
foreach ($nr['results'] as $r) {
|
||||
if (!$r['success']) {
|
||||
if (! $r['success']) {
|
||||
$failures[] = "{$r['channel']} 실패 ({$r['error']})";
|
||||
}
|
||||
}
|
||||
@@ -889,8 +892,8 @@ public function remind(Request $request, int $id): JsonResponse
|
||||
$message = $nextSigner
|
||||
? "{$nextSigner->name}에게 리마인더가 발송되었습니다."
|
||||
: '리마인더가 기록되었습니다.';
|
||||
if (!empty($failures)) {
|
||||
$message .= ' (일부 알림 실패: ' . implode(', ', $failures) . ')';
|
||||
if (! empty($failures)) {
|
||||
$message .= ' (일부 알림 실패: '.implode(', ', $failures).')';
|
||||
}
|
||||
|
||||
return response()->json([
|
||||
@@ -917,7 +920,7 @@ private function dispatchNotification(
|
||||
if (in_array($sendMethod, ['alimtalk', 'both']) && $signer->phone) {
|
||||
$alimtalkResult = $this->sendAlimtalk($contract, $signer, $smsFallback, $isReminder);
|
||||
$results[] = $alimtalkResult;
|
||||
$alimtalkFailed = !($alimtalkResult['success'] ?? false);
|
||||
$alimtalkFailed = ! ($alimtalkResult['success'] ?? false);
|
||||
}
|
||||
|
||||
// 이메일 발송 조건:
|
||||
@@ -925,7 +928,7 @@ private function dispatchNotification(
|
||||
// 2) alimtalk인데 번호 없으면 폴백
|
||||
// 3) alimtalk 발송 실패 시 이메일 자동 폴백
|
||||
$shouldSendEmail = in_array($sendMethod, ['email', 'both'])
|
||||
|| ($sendMethod === 'alimtalk' && !$signer->phone)
|
||||
|| ($sendMethod === 'alimtalk' && ! $signer->phone)
|
||||
|| ($sendMethod === 'alimtalk' && $alimtalkFailed);
|
||||
|
||||
if ($shouldSendEmail && $signer->email) {
|
||||
@@ -956,18 +959,18 @@ private function sendAlimtalk(
|
||||
): array {
|
||||
try {
|
||||
$member = BarobillMember::where('tenant_id', $contract->tenant_id)->first();
|
||||
if (!$member) {
|
||||
if (! $member) {
|
||||
return ['success' => false, 'channel' => 'alimtalk', 'error' => '바로빌 회원 미등록'];
|
||||
}
|
||||
|
||||
if (!$member->corp_num) {
|
||||
if (! $member->biz_no) {
|
||||
return ['success' => false, 'channel' => 'alimtalk', 'error' => '사업자번호 미설정 (알림톡 발송 불가)'];
|
||||
}
|
||||
|
||||
$barobill = app(BarobillService::class);
|
||||
$barobill->setServerMode($member->is_test_mode ? 'test' : 'production');
|
||||
$barobill->setServerMode($member->server_mode ?? 'production');
|
||||
|
||||
$signUrl = config('app.url') . '/esign/sign/' . $signer->access_token;
|
||||
$signUrl = config('app.url').'/esign/sign/'.$signer->access_token;
|
||||
$expires = $contract->expires_at?->format('Y-m-d H:i') ?? '없음';
|
||||
|
||||
$templateName = $isReminder ? '전자계약_리마인드' : '전자계약_서명요청';
|
||||
@@ -981,8 +984,8 @@ private function sendAlimtalk(
|
||||
: '';
|
||||
|
||||
$result = $barobill->sendATKakaotalkEx(
|
||||
corpNum: $member->corp_num,
|
||||
senderId: $member->kakaotalk_sender_id ?? '',
|
||||
corpNum: $member->biz_no,
|
||||
senderId: '',
|
||||
templateName: $templateName,
|
||||
receiverName: $signer->name,
|
||||
receiverNum: preg_replace('/[^0-9]/', '', $signer->phone),
|
||||
@@ -999,12 +1002,13 @@ private function sendAlimtalk(
|
||||
smsMessage: $smsMessage,
|
||||
);
|
||||
|
||||
if (!($result['success'] ?? false)) {
|
||||
if (! ($result['success'] ?? false)) {
|
||||
\Log::warning('E-Sign 알림톡 발송 실패', [
|
||||
'contract_id' => $contract->id,
|
||||
'signer_id' => $signer->id,
|
||||
'error' => $result['error'] ?? 'Unknown error',
|
||||
]);
|
||||
|
||||
return ['success' => false, 'channel' => 'alimtalk', 'error' => $result['error'] ?? 'API 호출 실패'];
|
||||
}
|
||||
|
||||
@@ -1015,6 +1019,7 @@ private function sendAlimtalk(
|
||||
'signer_id' => $signer->id,
|
||||
'error' => $e->getMessage(),
|
||||
]);
|
||||
|
||||
return ['success' => false, 'channel' => 'alimtalk', 'error' => $e->getMessage()];
|
||||
}
|
||||
}
|
||||
@@ -1056,7 +1061,7 @@ public function uploadPdf(Request $request, int $id): JsonResponse
|
||||
}
|
||||
|
||||
$file = $request->file('file');
|
||||
$converter = new DocxToPdfConverter();
|
||||
$converter = new DocxToPdfConverter;
|
||||
$result = $converter->convertAndStore($file, "esign/{$tenantId}/contracts");
|
||||
|
||||
$contract->update([
|
||||
@@ -1082,7 +1087,7 @@ public function downloadTemplatePdf(int $id)
|
||||
$tenantId = session('selected_tenant_id', 1);
|
||||
$template = EsignFieldTemplate::forTenant($tenantId)->findOrFail($id);
|
||||
|
||||
if (!$template->file_path || !Storage::disk('local')->exists($template->file_path)) {
|
||||
if (! $template->file_path || ! Storage::disk('local')->exists($template->file_path)) {
|
||||
abort(404, 'PDF 파일을 찾을 수 없습니다.');
|
||||
}
|
||||
|
||||
@@ -1161,7 +1166,7 @@ public function storeTemplate(Request $request): JsonResponse
|
||||
|
||||
if ($includePdf && $contractId) {
|
||||
$sourceContract = EsignContract::forTenant($tenantId)->find($contractId);
|
||||
if (!$sourceContract || !$sourceContract->original_file_path || !Storage::disk('local')->exists($sourceContract->original_file_path)) {
|
||||
if (! $sourceContract || ! $sourceContract->original_file_path || ! Storage::disk('local')->exists($sourceContract->original_file_path)) {
|
||||
$sourceContract = null;
|
||||
}
|
||||
}
|
||||
@@ -1293,7 +1298,7 @@ public function uploadTemplatePdf(Request $request, int $id): JsonResponse
|
||||
}
|
||||
|
||||
$file = $request->file('file');
|
||||
$converter = new DocxToPdfConverter();
|
||||
$converter = new DocxToPdfConverter;
|
||||
$result = $converter->convertAndStore($file, "esign/{$tenantId}/templates");
|
||||
|
||||
$template->update([
|
||||
@@ -1449,7 +1454,7 @@ public function duplicateTemplate(int $id): JsonResponse
|
||||
|
||||
$newTemplate = EsignFieldTemplate::create(array_merge([
|
||||
'tenant_id' => $tenantId,
|
||||
'name' => $template->name . ' (복사)',
|
||||
'name' => $template->name.' (복사)',
|
||||
'description' => $template->description,
|
||||
'category' => $template->category,
|
||||
'signer_count' => $template->signer_count,
|
||||
@@ -1540,7 +1545,9 @@ public function applyTemplate(Request $request, int $id): JsonResponse
|
||||
// 템플릿 아이템 → 필드 생성
|
||||
foreach ($template->items as $item) {
|
||||
$signerId = $signerMap[$item->signer_order] ?? null;
|
||||
if (!$signerId) continue;
|
||||
if (! $signerId) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// 변수가 바인딩된 필드는 자동 채움
|
||||
$fieldValue = null;
|
||||
@@ -1616,7 +1623,9 @@ public function copyFieldsFromContract(Request $request, int $id, int $sourceId)
|
||||
// 소스 signer_id → sign_order → 대상 signer_id
|
||||
$signOrder = $sourceSignerOrderMap[$field->signer_id] ?? null;
|
||||
$targetSignerId = $signOrder ? ($targetSignerMap[$signOrder] ?? null) : null;
|
||||
if (!$targetSignerId) continue;
|
||||
if (! $targetSignerId) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// field_variable이 있으면 대상 계약의 변수 맵에서 값 조회
|
||||
$fieldValue = null;
|
||||
@@ -1661,7 +1670,7 @@ private function buildVariableMap(EsignContract $contract, ?EsignFieldTemplate $
|
||||
$map = [];
|
||||
|
||||
// 시스템 변수: 서명자 정보 (역할 기반: 1=creator/갑/회사, 2=counterpart/을/파트너)
|
||||
$signers = $contract->signers->sortBy(fn($s) => $s->role === 'creator' ? 1 : 2);
|
||||
$signers = $contract->signers->sortBy(fn ($s) => $s->role === 'creator' ? 1 : 2);
|
||||
$idx = 1;
|
||||
foreach ($signers as $signer) {
|
||||
$map["signer{$idx}_name"] = $signer->name;
|
||||
|
||||
Reference in New Issue
Block a user