feat: [esign] 고객(counterpart) 도장 업로드/선택 기능 추가

- 백엔드: submitSignature에 stamp_image(base64) 파라미터 처리 추가
- 프론트: counterpart 역할에 도장 선택 UI 항상 표시
- 프론트: 도장 이미지 업로드(PNG/JPG, 5MB 제한) 및 미리보기 기능
- 기존 Creator 법인도장 흐름은 변경 없음
This commit is contained in:
김보곤
2026-02-23 14:48:30 +09:00
parent 3e294f4a1f
commit a7aef552c3
2 changed files with 109 additions and 26 deletions

View File

@@ -3,18 +3,18 @@
namespace App\Http\Controllers\ESign;
use App\Http\Controllers\Controller;
use App\Mail\EsignCompletedMail;
use App\Mail\EsignRequestMail;
use App\Models\Barobill\BarobillMember;
use App\Models\ESign\EsignAuditLog;
use App\Models\ESign\EsignContract;
use App\Models\ESign\EsignSigner;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use App\Mail\EsignCompletedMail;
use App\Mail\EsignRequestMail;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Mail;
use App\Services\Barobill\BarobillService;
use App\Services\ESign\PdfSignatureService;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Mail;
use Illuminate\Support\Facades\Storage;
use Illuminate\Support\Str;
use Illuminate\View\View;
@@ -232,21 +232,29 @@ public function submitSignature(Request $request, string $token): JsonResponse
$useStamp = $request->boolean('use_stamp');
if ($useStamp) {
// 도장 사용: 이미 저장된 stamp 이미지를 그대로 사용
if (!$signer->signature_image_path) {
$stampImage = $request->input('stamp_image');
if ($stampImage) {
// 고객 업로드 도장: base64 디코딩 후 저장
$imageData = base64_decode($stampImage);
if (strlen($imageData) > 5 * 1024 * 1024) {
return response()->json(['success' => false, 'message' => '도장 이미지는 5MB 이하만 가능합니다.'], 422);
}
$imagePath = "esign/{$contract->tenant_id}/signatures/{$contract->id}_{$signer->id}_stamp_".time().'.png';
Storage::disk('local')->put($imagePath, $imageData);
$signer->update(['signature_image_path' => $imagePath]);
} elseif (! $signer->signature_image_path) {
return response()->json(['success' => false, 'message' => '등록된 도장 이미지가 없습니다.'], 422);
}
// 기존 signature_image_path 유지, 새 이미지 저장 안 함
} else {
// 직접 서명: signature_image 필수
$signatureImage = $request->input('signature_image');
if (!$signatureImage) {
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';
$imagePath = "esign/{$contract->tenant_id}/signatures/{$contract->id}_{$signer->id}_".time().'.png';
Storage::disk('local')->put($imagePath, $imageData);
$signer->update(['signature_image_path' => $imagePath]);
@@ -286,7 +294,7 @@ public function submitSignature(Request $request, string $token): JsonResponse
// PDF에 서명 이미지/텍스트 합성
try {
$pdfService = new PdfSignatureService();
$pdfService = new PdfSignatureService;
$pdfService->mergeSignatures($contract);
} catch (\Throwable $e) {
Log::error('PDF 서명 합성 실패', [
@@ -313,7 +321,7 @@ public function submitSignature(Request $request, string $token): JsonResponse
$completionResults = [];
try {
// 이메일 발송
if (in_array($sendMethod, ['email', 'both']) || !$completedSigner->phone) {
if (in_array($sendMethod, ['email', 'both']) || ! $completedSigner->phone) {
if ($completedSigner->email) {
try {
Mail::to($completedSigner->email)->send(
@@ -380,7 +388,7 @@ public function submitSignature(Request $request, string $token): JsonResponse
if ($member) {
$barobill = app(BarobillService::class);
$barobill->setServerMode($member->is_test_mode ? 'test' : 'production');
$nextSignUrl = config('app.url') . '/esign/sign/' . $nextSigner->access_token;
$nextSignUrl = config('app.url').'/esign/sign/'.$nextSigner->access_token;
$nextExpires = $contract->expires_at?->format('Y-m-d H:i') ?? '없음';
$atResult = $barobill->sendATKakaotalkEx(
corpNum: $member->corp_num,
@@ -408,7 +416,7 @@ public function submitSignature(Request $request, string $token): JsonResponse
}
// 이메일 발송
if (in_array($nextSendMethod, ['email', 'both']) || ($nextSendMethod === 'alimtalk' && !$nextSigner->phone)) {
if (in_array($nextSendMethod, ['email', 'both']) || ($nextSendMethod === 'alimtalk' && ! $nextSigner->phone)) {
if ($nextSigner->email) {
try {
Mail::to($nextSigner->email)->send(new EsignRequestMail($contract, $nextSigner));
@@ -498,7 +506,7 @@ public function downloadDocument(string $token): StreamedResponse|JsonResponse
} else {
// 서명 전: 텍스트/날짜/체크박스 필드가 합성된 미리보기 PDF 생성
try {
$pdfService = new PdfSignatureService();
$pdfService = new PdfSignatureService;
$filePath = $pdfService->generatePreview($contract);
} catch (\Throwable $e) {
Log::warning('미리보기 PDF 생성 실패, 원본 제공', ['error' => $e->getMessage()]);
@@ -510,7 +518,7 @@ public function downloadDocument(string $token): StreamedResponse|JsonResponse
return response()->json(['success' => false, 'message' => '문서 파일이 존재하지 않습니다.'], 404);
}
$fileName = $contract->original_file_name ?: ($contract->title . '.pdf');
$fileName = $contract->original_file_name ?: ($contract->title.'.pdf');
return Storage::disk('local')->download($filePath, $fileName, [
'Content-Type' => 'application/pdf',
@@ -523,14 +531,14 @@ private function sendCompletionAlimtalk(EsignContract $contract, EsignSigner $si
{
try {
$member = BarobillMember::where('tenant_id', $contract->tenant_id)->first();
if (!$member) {
if (! $member) {
return ['success' => false, 'channel' => 'alimtalk', 'error' => '바로빌 회원 미등록'];
}
$barobill = app(BarobillService::class);
$barobill->setServerMode($member->is_test_mode ? 'test' : 'production');
$signUrl = config('app.url') . '/esign/sign/' . $signer->access_token;
$signUrl = config('app.url').'/esign/sign/'.$signer->access_token;
$completedAt = $contract->completed_at?->format('Y-m-d H:i') ?? now()->format('Y-m-d H:i');
$result = $barobill->sendATKakaotalkEx(
@@ -554,12 +562,13 @@ private function sendCompletionAlimtalk(EsignContract $contract, EsignSigner $si
: '',
);
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 호출 실패'];
}
@@ -570,6 +579,7 @@ private function sendCompletionAlimtalk(EsignContract $contract, EsignSigner $si
'signer_id' => $signer->id,
'error' => $e->getMessage(),
]);
return ['success' => false, 'channel' => 'alimtalk', 'error' => $e->getMessage()];
}
}