From ec43fe19916eebf555b2612c0c7609b1f8057ca9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B9=80=EB=B3=B4=EA=B3=A4?= Date: Thu, 12 Feb 2026 18:55:06 +0900 Subject: [PATCH] =?UTF-8?q?feat:E-Sign=20=ED=85=9C=ED=94=8C=EB=A6=BF=20?= =?UTF-8?q?=EA=B4=80=EB=A6=AC=20=EC=8B=9C=EC=8A=A4=ED=85=9C=20Phase=202=20?= =?UTF-8?q?=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 템플릿 관리 전용 페이지 (카드 그리드, 검색/필터, 편집/복제/삭제) - API: showTemplate, updateTemplate, duplicateTemplate 추가 - indexTemplates에 category/search 필터 추가 - 계약 생성 시 템플릿 선택 UI 추가 - 필드 에디터에서 URL 파라미터 template_id 자동 적용 - EsignFieldTemplate 모델에 category 필드 추가 Co-Authored-By: Claude Opus 4.6 --- .../Controllers/ESign/EsignApiController.php | 102 ++++- .../Controllers/ESign/EsignController.php | 9 + app/Models/ESign/EsignFieldTemplate.php | 6 + resources/views/esign/create.blade.php | 67 ++- resources/views/esign/fields.blade.php | 13 + resources/views/esign/templates.blade.php | 402 ++++++++++++++++++ routes/web.php | 4 + 7 files changed, 598 insertions(+), 5 deletions(-) create mode 100644 resources/views/esign/templates.blade.php diff --git a/app/Http/Controllers/ESign/EsignApiController.php b/app/Http/Controllers/ESign/EsignApiController.php index f31d142e..30722eb7 100644 --- a/app/Http/Controllers/ESign/EsignApiController.php +++ b/app/Http/Controllers/ESign/EsignApiController.php @@ -363,14 +363,19 @@ public function indexTemplates(Request $request): JsonResponse { $tenantId = session('selected_tenant_id', 1); $query = EsignFieldTemplate::forTenant($tenantId) - ->where('is_active', true) - ->with('items'); + ->where('is_active', true); + if ($category = $request->input('category')) { + $query->where('category', $category); + } + if ($search = $request->input('search')) { + $query->where('name', 'like', "%{$search}%"); + } if ($signerCount = $request->input('signer_count')) { $query->where('signer_count', $signerCount); } - $templates = $query->orderBy('created_at', 'desc')->get(); + $templates = $query->withCount('items')->with('creator:id,name')->latest()->get(); return response()->json(['success' => true, 'data' => $templates]); } @@ -383,6 +388,7 @@ public function storeTemplate(Request $request): JsonResponse $request->validate([ 'name' => 'required|string|max:100', 'description' => 'nullable|string', + 'category' => 'nullable|string|max:50', 'items' => 'required|array|min:1', 'items.*.signer_order' => 'required|integer|min:1', 'items.*.page_number' => 'required|integer|min:1', @@ -406,6 +412,7 @@ public function storeTemplate(Request $request): JsonResponse 'tenant_id' => $tenantId, 'name' => $request->input('name'), 'description' => $request->input('description'), + 'category' => $request->input('category'), 'signer_count' => $signerCount, 'is_active' => true, 'created_by' => auth()->id(), @@ -437,6 +444,95 @@ public function storeTemplate(Request $request): JsonResponse ]); } + /** + * 템플릿 단건 조회 + */ + public function showTemplate(int $id): JsonResponse + { + $tenantId = session('selected_tenant_id', 1); + $template = EsignFieldTemplate::forTenant($tenantId) + ->where('is_active', true) + ->with(['items', 'creator:id,name']) + ->findOrFail($id); + + return response()->json(['success' => true, 'data' => $template]); + } + + /** + * 템플릿 메타데이터 수정 + */ + public function updateTemplate(Request $request, int $id): JsonResponse + { + $request->validate([ + 'name' => 'required|string|max:100', + 'description' => 'nullable|string', + 'category' => 'nullable|string|max:50', + ]); + + $tenantId = session('selected_tenant_id', 1); + $template = EsignFieldTemplate::forTenant($tenantId)->findOrFail($id); + + $template->update([ + 'name' => $request->input('name'), + 'description' => $request->input('description'), + 'category' => $request->input('category'), + ]); + + return response()->json([ + 'success' => true, + 'message' => '템플릿이 수정되었습니다.', + 'data' => $template->fresh()->load('creator:id,name'), + ]); + } + + /** + * 템플릿 복제 + */ + public function duplicateTemplate(int $id): JsonResponse + { + $tenantId = session('selected_tenant_id', 1); + $template = EsignFieldTemplate::forTenant($tenantId) + ->where('is_active', true) + ->with('items') + ->findOrFail($id); + + $newTemplate = DB::transaction(function () use ($template, $tenantId) { + $newTemplate = EsignFieldTemplate::create([ + 'tenant_id' => $tenantId, + 'name' => $template->name . ' (복사)', + 'description' => $template->description, + 'category' => $template->category, + 'signer_count' => $template->signer_count, + 'is_active' => true, + 'created_by' => auth()->id(), + ]); + + foreach ($template->items as $item) { + EsignFieldTemplateItem::create([ + 'template_id' => $newTemplate->id, + 'signer_order' => $item->signer_order, + 'page_number' => $item->page_number, + 'position_x' => $item->position_x, + 'position_y' => $item->position_y, + 'width' => $item->width, + 'height' => $item->height, + 'field_type' => $item->field_type, + 'field_label' => $item->field_label, + 'is_required' => $item->is_required, + 'sort_order' => $item->sort_order, + ]); + } + + return $newTemplate; + }); + + return response()->json([ + 'success' => true, + 'message' => '템플릿이 복제되었습니다.', + 'data' => $newTemplate->load(['items', 'creator:id,name'])->loadCount('items'), + ]); + } + /** * 템플릿 삭제 (soft: is_active=false) */ diff --git a/app/Http/Controllers/ESign/EsignController.php b/app/Http/Controllers/ESign/EsignController.php index 2a3c6d9f..5dcc5e6c 100644 --- a/app/Http/Controllers/ESign/EsignController.php +++ b/app/Http/Controllers/ESign/EsignController.php @@ -54,6 +54,15 @@ public function send(Request $request, int $id): View|Response return view('esign.send', ['contractId' => $id]); } + public function templates(Request $request): View|Response + { + if ($request->header('HX-Request')) { + return response('', 200)->header('HX-Redirect', route('esign.templates')); + } + + return view('esign.templates'); + } + public function docs(Request $request): View|Response { if ($request->header('HX-Request')) { diff --git a/app/Models/ESign/EsignFieldTemplate.php b/app/Models/ESign/EsignFieldTemplate.php index 5c66a0f2..c496c21a 100644 --- a/app/Models/ESign/EsignFieldTemplate.php +++ b/app/Models/ESign/EsignFieldTemplate.php @@ -14,6 +14,7 @@ class EsignFieldTemplate extends Model 'tenant_id', 'name', 'description', + 'category', 'signer_count', 'is_active', 'created_by', @@ -29,6 +30,11 @@ public function scopeForTenant($query, $tenantId) return $query->where('tenant_id', $tenantId); } + public function scopeByCategory($query, $category) + { + return $query->where('category', $category); + } + public function items(): HasMany { return $this->hasMany(EsignFieldTemplateItem::class, 'template_id')->orderBy('sort_order'); diff --git a/resources/views/esign/create.blade.php b/resources/views/esign/create.blade.php index c71c8f69..b327013d 100644 --- a/resources/views/esign/create.blade.php +++ b/resources/views/esign/create.blade.php @@ -11,7 +11,7 @@ @include('partials.react-cdn') @verbatim +@endverbatim +@endpush diff --git a/routes/web.php b/routes/web.php index 0fb41964..a7ab9fc7 100644 --- a/routes/web.php +++ b/routes/web.php @@ -1398,6 +1398,7 @@ Route::get('/', [EsignController::class, 'dashboard'])->name('dashboard'); Route::get('/create', [EsignController::class, 'create'])->name('create'); Route::get('/docs', [EsignController::class, 'docs'])->name('docs'); + Route::get('/templates', [EsignController::class, 'templates'])->name('templates'); Route::get('/{id}', [EsignController::class, 'detail'])->whereNumber('id')->name('detail'); Route::get('/{id}/fields', [EsignController::class, 'fields'])->whereNumber('id')->name('fields'); Route::get('/{id}/send', [EsignController::class, 'send'])->whereNumber('id')->name('send'); @@ -1417,6 +1418,9 @@ // 필드 템플릿 Route::get('/templates', [EsignApiController::class, 'indexTemplates'])->name('templates.index'); Route::post('/templates', [EsignApiController::class, 'storeTemplate'])->name('templates.store'); + Route::get('/templates/{templateId}', [EsignApiController::class, 'showTemplate'])->whereNumber('templateId')->name('templates.show'); + Route::put('/templates/{templateId}', [EsignApiController::class, 'updateTemplate'])->whereNumber('templateId')->name('templates.update'); + Route::post('/templates/{templateId}/duplicate', [EsignApiController::class, 'duplicateTemplate'])->whereNumber('templateId')->name('templates.duplicate'); Route::delete('/templates/{templateId}', [EsignApiController::class, 'destroyTemplate'])->whereNumber('templateId')->name('templates.destroy'); // 템플릿 적용 / 필드 복사