diff --git a/app/Http/Controllers/ESign/EsignApiController.php b/app/Http/Controllers/ESign/EsignApiController.php index ae83635c..ab7a6d50 100644 --- a/app/Http/Controllers/ESign/EsignApiController.php +++ b/app/Http/Controllers/ESign/EsignApiController.php @@ -5,6 +5,7 @@ use App\Http\Controllers\Controller; use App\Mail\EsignRequestMail; use App\Models\ESign\EsignContract; +use App\Services\ESign\DocxToPdfConverter; use App\Models\ESign\EsignFieldTemplate; use App\Models\ESign\EsignFieldTemplateItem; use App\Models\ESign\EsignSigner; @@ -97,6 +98,7 @@ public function store(Request $request): JsonResponse 'signers.*.role' => 'required|in:creator,counterpart', 'metadata' => 'nullable|array', 'metadata.*' => 'nullable|string|max:500', + 'file' => 'nullable|file|mimes:pdf,doc,docx|max:20480', ]); $tenantId = session('selected_tenant_id', 1); @@ -112,12 +114,14 @@ public function store(Request $request): JsonResponse $fileSize = null; if ($request->hasFile('file')) { - // 사용자가 직접 업로드한 파일 우선 사용 + // 사용자가 직접 업로드한 파일 우선 사용 (Word면 PDF 자동 변환) $file = $request->file('file'); - $fileName = $file->getClientOriginalName(); - $fileSize = $file->getSize(); - $fileHash = hash_file('sha256', $file->getRealPath()); - $filePath = $file->store("esign/{$tenantId}/contracts", 'local'); + $converter = new DocxToPdfConverter(); + $result = $converter->convertAndStore($file, "esign/{$tenantId}/contracts"); + $filePath = $result['path']; + $fileName = $result['name']; + $fileHash = $result['hash']; + $fileSize = $result['size']; } elseif ($request->input('template_id')) { // 템플릿에 PDF가 있으면 복사 $template = EsignFieldTemplate::forTenant($tenantId) @@ -535,7 +539,7 @@ public function download(int $id) public function uploadPdf(Request $request, int $id): JsonResponse { $request->validate([ - 'file' => 'required|file|mimes:pdf|max:20480', + 'file' => 'required|file|mimes:pdf,doc,docx|max:20480', ]); $tenantId = session('selected_tenant_id', 1); @@ -546,20 +550,21 @@ public function uploadPdf(Request $request, int $id): JsonResponse } $file = $request->file('file'); - $filePath = $file->store("esign/{$tenantId}/contracts", 'local'); + $converter = new DocxToPdfConverter(); + $result = $converter->convertAndStore($file, "esign/{$tenantId}/contracts"); $contract->update([ - 'original_file_path' => $filePath, - 'original_file_name' => $file->getClientOriginalName(), - 'original_file_hash' => hash_file('sha256', $file->getRealPath()), - 'original_file_size' => $file->getSize(), + 'original_file_path' => $result['path'], + 'original_file_name' => $result['name'], + 'original_file_hash' => $result['hash'], + 'original_file_size' => $result['size'], 'updated_by' => auth()->id(), ]); return response()->json([ 'success' => true, 'message' => 'PDF 파일이 업로드되었습니다.', - 'data' => ['path' => $filePath, 'name' => $file->getClientOriginalName()], + 'data' => ['path' => $result['path'], 'name' => $result['name']], ]); } @@ -764,7 +769,7 @@ public function updateTemplate(Request $request, int $id): JsonResponse public function uploadTemplatePdf(Request $request, int $id): JsonResponse { $request->validate([ - 'file' => 'required|file|mimes:pdf|max:20480', + 'file' => 'required|file|mimes:pdf,doc,docx|max:20480', ]); $tenantId = session('selected_tenant_id', 1); @@ -776,13 +781,14 @@ public function uploadTemplatePdf(Request $request, int $id): JsonResponse } $file = $request->file('file'); - $filePath = $file->store("esign/{$tenantId}/templates", 'local'); + $converter = new DocxToPdfConverter(); + $result = $converter->convertAndStore($file, "esign/{$tenantId}/templates"); $template->update([ - 'file_path' => $filePath, - 'file_name' => $file->getClientOriginalName(), - 'file_hash' => hash_file('sha256', $file->getRealPath()), - 'file_size' => $file->getSize(), + 'file_path' => $result['path'], + 'file_name' => $result['name'], + 'file_hash' => $result['hash'], + 'file_size' => $result['size'], ]); return response()->json([ diff --git a/app/Services/ESign/DocxToPdfConverter.php b/app/Services/ESign/DocxToPdfConverter.php new file mode 100644 index 00000000..c43fc45f --- /dev/null +++ b/app/Services/ESign/DocxToPdfConverter.php @@ -0,0 +1,87 @@ +isWordFile($file)) { + // PDF는 그대로 저장 + $path = $file->store($storagePath, 'local'); + + return [ + 'path' => $path, + 'name' => $file->getClientOriginalName(), + 'hash' => hash_file('sha256', $file->getRealPath()), + 'size' => $file->getSize(), + ]; + } + + // Word → PDF 변환 + $originalName = $file->getClientOriginalName(); + $pdfName = preg_replace('/\.(docx?|DOCX?)$/', '.pdf', $originalName); + + $tmpDir = sys_get_temp_dir() . '/esign_convert_' . Str::random(16); + mkdir($tmpDir, 0755, true); + + try { + $tmpWordPath = $tmpDir . '/' . 'source.' . $file->getClientOriginalExtension(); + copy($file->getRealPath(), $tmpWordPath); + + $command = sprintf( + 'libreoffice --headless --convert-to pdf --outdir %s %s 2>&1', + escapeshellarg($tmpDir), + escapeshellarg($tmpWordPath) + ); + + exec($command, $output, $exitCode); + + if ($exitCode !== 0) { + throw new RuntimeException( + 'LibreOffice 변환 실패 (exit code: ' . $exitCode . '): ' . implode("\n", $output) + ); + } + + $tmpPdfPath = $tmpDir . '/source.pdf'; + if (!file_exists($tmpPdfPath)) { + throw new RuntimeException('변환된 PDF 파일을 찾을 수 없습니다.'); + } + + $hash = hash_file('sha256', $tmpPdfPath); + $size = filesize($tmpPdfPath); + + // 최종 저장 경로 + $finalPath = $storagePath . '/' . Str::random(40) . '.pdf'; + Storage::disk('local')->put($finalPath, file_get_contents($tmpPdfPath)); + + return [ + 'path' => $finalPath, + 'name' => $pdfName, + 'hash' => $hash, + 'size' => $size, + ]; + } finally { + // 임시 파일 정리 + array_map('unlink', glob($tmpDir . '/*')); + @rmdir($tmpDir); + } + } + + public function isWordFile(UploadedFile $file): bool + { + $ext = strtolower($file->getClientOriginalExtension()); + + return in_array($ext, ['doc', 'docx']); + } +} diff --git a/resources/views/esign/create.blade.php b/resources/views/esign/create.blade.php index f2e2a5d8..4d69948b 100644 --- a/resources/views/esign/create.blade.php +++ b/resources/views/esign/create.blade.php @@ -184,8 +184,8 @@ className="w-8 h-8 flex items-center justify-center rounded-lg text-amber-500 ho className="w-full border border-gray-300 rounded-md px-2.5 py-1.5 text-sm focus:ring-2 focus:ring-blue-500 focus:border-blue-500 outline-none transition-colors" />
{file.name} ({(file.size / 1024 / 1024).toFixed(2)} MB)
} {templateId && !file && (() => { diff --git a/resources/views/esign/fields.blade.php b/resources/views/esign/fields.blade.php index 65c893ab..7b040eb5 100644 --- a/resources/views/esign/fields.blade.php +++ b/resources/views/esign/fields.blade.php @@ -1161,7 +1161,7 @@ className="px-4 py-1.5 text-sm bg-blue-600 text-white rounded-lg hover:bg-blue-7PDF 파일은 이 템플릿으로 계약 생성 시 자동 사용됩니다. 최대 20MB.
)}