diff --git a/app/Http/Controllers/ESign/EsignApiController.php b/app/Http/Controllers/ESign/EsignApiController.php index ab7a6d50..a4a0bee9 100644 --- a/app/Http/Controllers/ESign/EsignApiController.php +++ b/app/Http/Controllers/ESign/EsignApiController.php @@ -99,6 +99,7 @@ public function store(Request $request): JsonResponse 'metadata' => 'nullable|array', 'metadata.*' => 'nullable|string|max:500', 'file' => 'nullable|file|mimes:pdf,doc,docx|max:20480', + 'creator_stamp_image' => 'nullable|string', ]); $tenantId = session('selected_tenant_id', 1); @@ -178,6 +179,21 @@ public function store(Request $request): JsonResponse ]); } + // 법인도장 이미지 처리 + if ($request->input('creator_stamp_image')) { + $creatorSigner = EsignSigner::withoutGlobalScopes() + ->where('contract_id', $contract->id) + ->where('role', 'creator') + ->first(); + + if ($creatorSigner) { + $imageData = base64_decode($request->input('creator_stamp_image')); + $imagePath = "esign/{$tenantId}/signatures/{$contract->id}_{$creatorSigner->id}_stamp.png"; + Storage::disk('local')->put($imagePath, $imageData); + $creatorSigner->update(['signature_image_path' => $imagePath]); + } + } + // 감사 로그 EsignAuditLog::create([ 'tenant_id' => $tenantId, @@ -440,16 +456,45 @@ public function send(Request $request, int $id): JsonResponse 'updated_by' => auth()->id(), ]); + // 법인도장이 있는 작성자 자동 서명 처리 + $creatorSigner = $contract->signers->firstWhere('role', 'creator'); + if ($creatorSigner && $creatorSigner->signature_image_path) { + $creatorSigner->update([ + 'status' => 'signed', + 'signed_at' => now(), + 'sign_ip_address' => $request->ip(), + 'sign_user_agent' => $request->userAgent(), + ]); + + EsignAuditLog::create([ + 'tenant_id' => $tenantId, + 'contract_id' => $contract->id, + 'signer_id' => $creatorSigner->id, + 'action' => 'signed', + 'ip_address' => $request->ip(), + 'user_agent' => $request->userAgent(), + 'metadata' => ['auto_stamp' => true, 'signer_name' => $creatorSigner->name], + 'created_at' => now(), + ]); + + // 계약 상태를 partially_signed로 변경 + $contract->update(['status' => 'partially_signed']); + } + // 서명 순서 유형에 따라 알림 발송 if ($contract->sign_order_type === 'parallel') { - // 동시 서명: 모든 서명자에게 발송 + // 동시 서명: 서명 안 한 서명자에게만 발송 foreach ($contract->signers as $s) { + if ($s->status === 'signed') continue; $s->update(['status' => 'notified']); Mail::to($s->email)->send(new EsignRequestMail($contract, $s)); } } else { - // 순차 서명: 첫 번째 서명자에게만 발송 - $nextSigner = $contract->signers()->orderBy('sign_order')->first(); + // 순차 서명: 다음 미서명 서명자에게 발송 + $nextSigner = $contract->signers() + ->where('status', '!=', 'signed') + ->orderBy('sign_order') + ->first(); if ($nextSigner) { $nextSigner->update(['status' => 'notified']); Mail::to($nextSigner->email)->send(new EsignRequestMail($contract, $nextSigner)); diff --git a/resources/views/esign/create.blade.php b/resources/views/esign/create.blade.php index 362f2435..c1f6ea7b 100644 --- a/resources/views/esign/create.blade.php +++ b/resources/views/esign/create.blade.php @@ -56,6 +56,8 @@ className="w-full border border-gray-300 rounded-md px-2.5 py-1.5 text-sm focus: counterpart_name: '', counterpart_email: '', counterpart_phone: '', }); const [file, setFile] = useState(null); + const [stampImage, setStampImage] = useState(null); + const [stampPreview, setStampPreview] = useState(null); const [submitting, setSubmitting] = useState(false); const [errors, setErrors] = useState({}); const [templates, setTemplates] = useState([]); @@ -64,6 +66,25 @@ className="w-full border border-gray-300 rounded-md px-2.5 py-1.5 text-sm focus: const [templateSearch, setTemplateSearch] = useState(''); const [metadata, setMetadata] = useState({}); const fileRef = useRef(null); + const stampFileRef = useRef(null); + + const handleStampSelect = (e) => { + const selected = e.target.files[0]; + if (!selected) return; + const reader = new FileReader(); + reader.onload = () => { + const base64 = reader.result.split(',')[1]; + setStampImage(base64); + setStampPreview(reader.result); + }; + reader.readAsDataURL(selected); + }; + + const removeStamp = () => { + setStampImage(null); + setStampPreview(null); + if (stampFileRef.current) stampFileRef.current.value = ''; + }; useEffect(() => { fetch('/esign/contracts/templates', { headers: { 'Accept': 'application/json' } }) @@ -132,6 +153,8 @@ className="w-full border border-gray-300 rounded-md px-2.5 py-1.5 text-sm focus: Object.entries(metadata).forEach(([k, v]) => { fd.append(`metadata[${k}]`, v || ''); }); } + if (stampImage) fd.append('creator_stamp_image', stampImage); + try { fd.append('_token', csrfToken); fd.append('signers[0][name]', form.creator_name); @@ -227,6 +250,39 @@ className="w-full border border-gray-300 rounded-md px-2.5 py-1.5 text-sm focus:
법인도장이 등록되었습니다.
+