From d5283099c4813cbf04afa90cdcac03f2665575ed 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 17:40:41 +0900 Subject: [PATCH] =?UTF-8?q?feat:=EC=83=88=20=ED=85=9C=ED=94=8C=EB=A6=BF=20?= =?UTF-8?q?=EC=83=9D=EC=84=B1=20=EA=B8=B0=EB=8A=A5=20=EC=B6=94=EA=B0=80=20?= =?UTF-8?q?(CreateTemplateModal)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - storeTemplate() API에서 items를 nullable로 변경하여 빈 템플릿 생성 허용 - signer_count/variables 파라미터 직접 지정 가능하도록 추가 - "새 템플릿" 버튼 클릭 시 CreateTemplateModal 표시 (이름/설명/카테고리/서명자수/PDF) - 생성 완료 후 필드 에디터(/esign/templates/{id}/fields)로 자동 이동 Co-Authored-By: Claude Opus 4.6 --- .../Controllers/ESign/EsignApiController.php | 12 +- resources/views/esign/templates.blade.php | 190 +++++++++++++++++- 2 files changed, 197 insertions(+), 5 deletions(-) diff --git a/app/Http/Controllers/ESign/EsignApiController.php b/app/Http/Controllers/ESign/EsignApiController.php index caee6ac0..fb0f732d 100644 --- a/app/Http/Controllers/ESign/EsignApiController.php +++ b/app/Http/Controllers/ESign/EsignApiController.php @@ -824,7 +824,9 @@ public function storeTemplate(Request $request): JsonResponse 'category' => 'nullable|string|max:50', 'include_pdf' => 'nullable|boolean', 'contract_id' => 'nullable|integer', - 'items' => 'required|array|min:1', + 'signer_count' => 'nullable|integer|min:1|max:6', + 'variables' => 'nullable|array', + 'items' => 'nullable|array', 'items.*.signer_order' => 'required|integer|min:1', 'items.*.page_number' => 'required|integer|min:1', 'items.*.position_x' => 'required|numeric', @@ -840,9 +842,10 @@ public function storeTemplate(Request $request): JsonResponse $tenantId = session('selected_tenant_id', 1); - // items에서 최대 signer_order를 추출하여 signer_count 결정 - $items = $request->input('items'); - $signerCount = max(array_column($items, 'signer_order')); + // signer_count 결정: 직접 지정 > items에서 추출 > 기본값 2 + $items = $request->input('items', []); + $signerCount = $request->input('signer_count') + ?: (count($items) > 0 ? max(array_column($items, 'signer_order')) : 2); // PDF 포함 여부 확인 $includePdf = $request->boolean('include_pdf'); @@ -879,6 +882,7 @@ public function storeTemplate(Request $request): JsonResponse 'description' => $request->input('description'), 'category' => $request->input('category'), 'signer_count' => $signerCount, + 'variables' => $request->input('variables', []), 'is_active' => true, 'created_by' => auth()->id(), ], $fileData)); diff --git a/resources/views/esign/templates.blade.php b/resources/views/esign/templates.blade.php index ba62f8d7..1256874c 100644 --- a/resources/views/esign/templates.blade.php +++ b/resources/views/esign/templates.blade.php @@ -424,6 +424,185 @@ className="block w-full text-center mt-2 px-3 py-1.5 bg-teal-50 text-teal-700 ro ); }; +// ─── CreateTemplateModal ─── +const CreateTemplateModal = ({ open, onClose, onCreated, showToast }) => { + const [name, setName] = useState(''); + const [description, setDescription] = useState(''); + const [category, setCategory] = useState(''); + const [customCategory, setCustomCategory] = useState(''); + const [useCustomCategory, setUseCustomCategory] = useState(false); + const [signerCount, setSignerCount] = useState(2); + const [pdfFile, setPdfFile] = useState(null); + const [creating, setCreating] = useState(false); + const fileInputRef = useRef(null); + + useEffect(() => { + if (open) { + setName(''); setDescription(''); setCategory(''); setCustomCategory(''); + setUseCustomCategory(false); setSignerCount(2); setPdfFile(null); + if (fileInputRef.current) fileInputRef.current.value = ''; + } + }, [open]); + + if (!open) return null; + + const finalCategory = useCustomCategory ? customCategory.trim() : category; + + const handleSubmit = async () => { + if (!name.trim()) { alert('템플릿 이름을 입력해주세요.'); return; } + setCreating(true); + try { + // 1. 빈 템플릿 생성 + const res = await fetch('/esign/contracts/templates', { + method: 'POST', + headers: { ...getHeaders(), 'Content-Type': 'application/json' }, + body: JSON.stringify({ + name: name.trim(), + description: description.trim() || null, + category: finalCategory || null, + signer_count: signerCount, + }), + }); + const json = await res.json(); + if (!json.success) { alert(json.message || '생성 실패'); setCreating(false); return; } + + const templateId = json.data.id; + + // 2. PDF 파일이 있으면 업로드 + if (pdfFile) { + const fd = new FormData(); + fd.append('file', pdfFile); + const uploadRes = await fetch(`/esign/contracts/templates/${templateId}/upload-pdf`, { + method: 'POST', + headers: { 'Accept': 'application/json', 'X-CSRF-TOKEN': csrfToken }, + body: fd, + }); + const uploadJson = await uploadRes.json(); + if (!uploadJson.success) { + showToast('템플릿은 생성되었으나 PDF 업로드에 실패했습니다.', 'error'); + } + } + + // 3. 필드 에디터로 이동 + showToast('템플릿이 생성되었습니다. 필드 에디터로 이동합니다.'); + setTimeout(() => { location.href = `/esign/templates/${templateId}/fields`; }, 500); + } catch (e) { + alert('서버 오류가 발생했습니다.'); + setCreating(false); + } + }; + + const handleFileChange = (e) => { + const file = e.target.files[0]; + if (file) setPdfFile(file); + }; + + return ( +
+
e.stopPropagation()}> + {/* 헤더 */} +
+
+

새 템플릿 생성

+ +
+

템플릿 기본 정보를 입력한 후 필드 에디터에서 서명 위치를 설정합니다.

+
+ + {/* 본문 */} +
+ {/* 이름 */} +
+ + setName(e.target.value)} + placeholder="예: 영업파트너 계약서 v2" + className="w-full border rounded-lg px-3 py-2 text-sm focus:ring-2 focus:ring-blue-200 focus:border-blue-400 outline-none" autoFocus /> +
+ + {/* 설명 */} +
+ +