feat:E-Sign 템플릿에 PDF 파일 포함 기능 추가

- 모델: EsignFieldTemplate fillable에 file 컬럼 추가
- storeTemplate: include_pdf + contract_id로 계약 PDF를 템플릿으로 복사
- store(계약 생성): template_id로 템플릿 PDF 자동 복사 (사용자 업로드 우선)
- duplicateTemplate: 복제 시 PDF 파일도 복사
- 템플릿 PDF 다운로드 엔드포인트 추가
- SaveTemplateModal: "현재 PDF 파일 포함" 체크박스 추가
- create: 템플릿 카드에 PDF 뱃지, PDF 자동 사용 안내
- templates: 템플릿 카드에 PDF 파일명 표시

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
김보곤
2026-02-12 20:16:56 +09:00
parent f326194c99
commit f050be52fe
6 changed files with 140 additions and 12 deletions

View File

@@ -14,6 +14,7 @@
use Illuminate\Http\Request;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Mail;
use Illuminate\Support\Facades\Storage;
use Illuminate\Support\Str;
class EsignApiController extends Controller
@@ -88,6 +89,7 @@ public function store(Request $request): JsonResponse
'sign_order_type' => 'required|in:counterpart_first,creator_first',
'expires_at' => 'nullable|date',
'expires_days' => 'nullable|integer|min:1|max:365',
'template_id' => 'nullable|integer',
'signers' => 'required|array|size:2',
'signers.*.name' => 'required|string|max:100',
'signers.*.email' => 'required|email|max:200',
@@ -108,11 +110,28 @@ public function store(Request $request): JsonResponse
$fileSize = null;
if ($request->hasFile('file')) {
// 사용자가 직접 업로드한 파일 우선 사용
$file = $request->file('file');
$fileName = $file->getClientOriginalName();
$fileSize = $file->getSize();
$fileHash = hash_file('sha256', $file->getRealPath());
$filePath = $file->store("esign/{$tenantId}/contracts", 'local');
} elseif ($request->input('template_id')) {
// 템플릿에 PDF가 있으면 복사
$template = EsignFieldTemplate::forTenant($tenantId)
->where('is_active', true)
->find($request->input('template_id'));
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}";
Storage::disk('local')->copy($template->file_path, $newPath);
$filePath = $newPath;
$fileName = $template->file_name;
$fileHash = $template->file_hash;
$fileSize = $template->file_size;
}
}
$contract = EsignContract::create([
@@ -388,6 +407,25 @@ public function uploadPdf(Request $request, int $id): JsonResponse
]);
}
/**
* 템플릿 PDF 다운로드
*/
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)) {
abort(404, 'PDF 파일을 찾을 수 없습니다.');
}
$fileName = $template->file_name ?: 'template.pdf';
return Storage::disk('local')->download($template->file_path, $fileName, [
'Content-Type' => 'application/pdf',
]);
}
// ─── 필드 템플릿 관련 메서드 ───
/**
@@ -423,6 +461,8 @@ public function storeTemplate(Request $request): JsonResponse
'name' => 'required|string|max:100',
'description' => 'nullable|string',
'category' => 'nullable|string|max:50',
'include_pdf' => 'nullable|boolean',
'contract_id' => 'nullable|integer',
'items' => 'required|array|min:1',
'items.*.signer_order' => 'required|integer|min:1',
'items.*.page_number' => 'required|integer|min:1',
@@ -441,8 +481,36 @@ public function storeTemplate(Request $request): JsonResponse
$items = $request->input('items');
$signerCount = max(array_column($items, 'signer_order'));
$template = DB::transaction(function () use ($tenantId, $request, $items, $signerCount) {
$template = EsignFieldTemplate::create([
// PDF 포함 여부 확인
$includePdf = $request->boolean('include_pdf');
$contractId = $request->input('contract_id');
$sourceContract = null;
if ($includePdf && $contractId) {
$sourceContract = EsignContract::forTenant($tenantId)->find($contractId);
if (!$sourceContract || !$sourceContract->original_file_path || !Storage::disk('local')->exists($sourceContract->original_file_path)) {
$sourceContract = null;
}
}
$template = DB::transaction(function () use ($tenantId, $request, $items, $signerCount, $sourceContract) {
$fileData = [];
if ($sourceContract) {
$timestamp = now()->format('YmdHis');
$ext = pathinfo($sourceContract->original_file_path, PATHINFO_EXTENSION) ?: 'pdf';
$newPath = "esign/{$tenantId}/templates/{$timestamp}.{$ext}";
Storage::disk('local')->copy($sourceContract->original_file_path, $newPath);
$fileData = [
'file_path' => $newPath,
'file_name' => $sourceContract->original_file_name,
'file_hash' => $sourceContract->original_file_hash,
'file_size' => $sourceContract->original_file_size,
];
}
$template = EsignFieldTemplate::create(array_merge([
'tenant_id' => $tenantId,
'name' => $request->input('name'),
'description' => $request->input('description'),
@@ -450,7 +518,7 @@ public function storeTemplate(Request $request): JsonResponse
'signer_count' => $signerCount,
'is_active' => true,
'created_by' => auth()->id(),
]);
], $fileData));
foreach ($items as $i => $item) {
EsignFieldTemplateItem::create([
@@ -531,7 +599,22 @@ public function duplicateTemplate(int $id): JsonResponse
->findOrFail($id);
$newTemplate = DB::transaction(function () use ($template, $tenantId) {
$newTemplate = EsignFieldTemplate::create([
$fileData = [];
if ($template->file_path && Storage::disk('local')->exists($template->file_path)) {
$timestamp = now()->format('YmdHis');
$ext = pathinfo($template->file_path, PATHINFO_EXTENSION) ?: 'pdf';
$newPath = "esign/{$tenantId}/templates/{$timestamp}_copy.{$ext}";
Storage::disk('local')->copy($template->file_path, $newPath);
$fileData = [
'file_path' => $newPath,
'file_name' => $template->file_name,
'file_hash' => $template->file_hash,
'file_size' => $template->file_size,
];
}
$newTemplate = EsignFieldTemplate::create(array_merge([
'tenant_id' => $tenantId,
'name' => $template->name . ' (복사)',
'description' => $template->description,
@@ -539,7 +622,7 @@ public function duplicateTemplate(int $id): JsonResponse
'signer_count' => $template->signer_count,
'is_active' => true,
'created_by' => auth()->id(),
]);
], $fileData));
foreach ($template->items as $item) {
EsignFieldTemplateItem::create([