From 24c6927b56ce166d290f9ff35486138c45b22110 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B9=80=EB=B3=B4=EA=B3=A4?= Date: Fri, 13 Feb 2026 15:02:26 +0900 Subject: [PATCH] =?UTF-8?q?feat:E-Sign=20=EC=9E=91=EC=84=B1=EC=9E=90=20?= =?UTF-8?q?=EC=84=9C=EB=AA=85/=EB=8F=84=EC=9E=A5=20=EC=84=A0=ED=83=9D=20?= =?UTF-8?q?=EA=B8=B0=EB=8A=A5=20(=EC=9E=90=EB=8F=99=EC=84=9C=EB=AA=85=20?= =?UTF-8?q?=EC=A0=9C=EA=B1=B0)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 작성자 자동서명 로직 제거 → 양쪽 모두 서명 과정 필수 - 서명 화면에서 '직접 서명' / '법인도장' 선택 UI 추가 - 도장 선택 시 기존 stamp 이미지로 바로 제출 - getContract API에 has_stamp 필드 추가 - submitSignature에 use_stamp 플래그 처리 추가 Co-Authored-By: Claude Opus 4.6 --- .../Controllers/ESign/EsignApiController.php | 39 +++----------- .../ESign/EsignPublicController.php | 31 +++++++---- resources/views/esign/sign/sign.blade.php | 54 +++++++++++++++++-- 3 files changed, 79 insertions(+), 45 deletions(-) diff --git a/app/Http/Controllers/ESign/EsignApiController.php b/app/Http/Controllers/ESign/EsignApiController.php index a4a0bee9..5a655805 100644 --- a/app/Http/Controllers/ESign/EsignApiController.php +++ b/app/Http/Controllers/ESign/EsignApiController.php @@ -456,48 +456,21 @@ 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() - ->where('status', '!=', 'signed') + // 순차 서명: 첫 번째 서명자에게 발송 + $firstSigner = $contract->signers() ->orderBy('sign_order') ->first(); - if ($nextSigner) { - $nextSigner->update(['status' => 'notified']); - Mail::to($nextSigner->email)->send(new EsignRequestMail($contract, $nextSigner)); + if ($firstSigner) { + $firstSigner->update(['status' => 'notified']); + Mail::to($firstSigner->email)->send(new EsignRequestMail($contract, $firstSigner)); } } diff --git a/app/Http/Controllers/ESign/EsignPublicController.php b/app/Http/Controllers/ESign/EsignPublicController.php index 65f95b0c..c9c96b1f 100644 --- a/app/Http/Controllers/ESign/EsignPublicController.php +++ b/app/Http/Controllers/ESign/EsignPublicController.php @@ -84,6 +84,7 @@ public function getContract(string $token): JsonResponse 'email' => $signer->email, 'role' => $signer->role, 'status' => $signer->status, + 'has_stamp' => (bool) $signer->signature_image_path, ], ], ]); @@ -204,19 +205,31 @@ public function submitSignature(Request $request, string $token): JsonResponse return response()->json(['success' => false, 'message' => '서명할 수 없는 계약 상태입니다.'], 400); } - $signatureImage = $request->input('signature_image'); - if (! $signatureImage) { - return response()->json(['success' => false, 'message' => '서명 이미지가 필요합니다.'], 422); - } + $useStamp = $request->boolean('use_stamp'); - // 서명 이미지 저장 - $imageData = base64_decode($signatureImage); - $imagePath = "esign/{$contract->tenant_id}/signatures/{$contract->id}_{$signer->id}_" . time() . '.png'; - Storage::disk('local')->put($imagePath, $imageData); + if ($useStamp) { + // 도장 사용: 이미 저장된 stamp 이미지를 그대로 사용 + if (!$signer->signature_image_path) { + return response()->json(['success' => false, 'message' => '등록된 도장 이미지가 없습니다.'], 422); + } + // 기존 signature_image_path 유지, 새 이미지 저장 안 함 + } else { + // 직접 서명: signature_image 필수 + $signatureImage = $request->input('signature_image'); + if (!$signatureImage) { + return response()->json(['success' => false, 'message' => '서명 이미지가 필요합니다.'], 422); + } + + // 서명 이미지 저장 + $imageData = base64_decode($signatureImage); + $imagePath = "esign/{$contract->tenant_id}/signatures/{$contract->id}_{$signer->id}_" . time() . '.png'; + Storage::disk('local')->put($imagePath, $imageData); + + $signer->update(['signature_image_path' => $imagePath]); + } // 서명자 상태 업데이트 $signer->update([ - 'signature_image_path' => $imagePath, 'signed_at' => now(), 'consent_agreed_at' => now(), 'sign_ip_address' => $request->ip(), diff --git a/resources/views/esign/sign/sign.blade.php b/resources/views/esign/sign/sign.blade.php index dabb12e2..e65c6daf 100644 --- a/resources/views/esign/sign/sign.blade.php +++ b/resources/views/esign/sign/sign.blade.php @@ -36,6 +36,7 @@ const [submitting, setSubmitting] = useState(false); const [error, setError] = useState(''); const [signatureData, setSignatureData] = useState(null); + const [signMode, setSignMode] = useState('signature'); // 'signature' | 'stamp' const canvasRef = useRef(null); const padRef = useRef(null); @@ -77,7 +78,12 @@ const goToSign = () => { if (!consent) { alert('동의 항목을 체크해 주세요.'); return; } - setStep('sign'); + if (signMode === 'stamp') { + // 도장 모드: 서명 pad 건너뛰고 바로 제출 + submitStamp(); + } else { + setStep('sign'); + } }; const confirmSignature = () => { @@ -89,6 +95,25 @@ setStep('confirm'); }; + const submitStamp = async () => { + setSubmitting(true); + setError(''); + try { + const res = await fetch(`/esign/sign/${TOKEN}/api/submit`, { + method: 'POST', + headers: { 'Accept': 'application/json', 'Content-Type': 'application/json' }, + body: JSON.stringify({ use_stamp: true }), + }); + const json = await res.json(); + if (json.success) { + location.href = `/esign/sign/${TOKEN}/done`; + } else { + setError(json.message || '도장 제출에 실패했습니다.'); + } + } catch (e) { setError('서버 오류'); } + setSubmitting(false); + }; + const submitSignature = async () => { setSubmitting(true); setError(''); @@ -163,6 +188,29 @@ className="inline-flex items-center px-4 py-2 bg-blue-600 text-white rounded-lg 문서 열기 / 다운로드 + {signer?.has_stamp && ( +
+

서명 방법을 선택해 주세요.

+
+ + +
+
+ )}
- )}