feat:템플릿 편집 모달 확장 (PDF 관리 + 서식 필드 관리)
백엔드: - uploadTemplatePdf: 템플릿 PDF 업로드/교체 API - removeTemplatePdf: 템플릿 PDF 제거 API - destroyTemplateItem: 개별 필드 아이템 삭제 API (signer_count 자동 재계산) - updateTemplate 응답에 items 관계 포함 프론트엔드: - 모달 폭 420px → 680px 확장 - 3개 탭 구성: 기본 정보 / PDF 파일 / 서식 필드 - PDF 탭: 현재 파일 정보, 다운로드, 교체, 제거 기능 - 서식 필드 탭: 필드 목록 테이블 (유형/라벨/서명자/페이지/위치/필수), 개별 삭제 - 편집 시 상세 데이터(items 포함) 로드 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -732,7 +732,89 @@ public function updateTemplate(Request $request, int $id): JsonResponse
|
||||
return response()->json([
|
||||
'success' => true,
|
||||
'message' => '템플릿이 수정되었습니다.',
|
||||
'data' => $template->fresh()->load('creator:id,name'),
|
||||
'data' => $template->fresh()->load(['creator:id,name', 'items'])->loadCount('items'),
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* 템플릿 PDF 교체
|
||||
*/
|
||||
public function uploadTemplatePdf(Request $request, int $id): JsonResponse
|
||||
{
|
||||
$request->validate([
|
||||
'file' => 'required|file|mimes:pdf|max:20480',
|
||||
]);
|
||||
|
||||
$tenantId = session('selected_tenant_id', 1);
|
||||
$template = EsignFieldTemplate::forTenant($tenantId)->findOrFail($id);
|
||||
|
||||
// 기존 파일 삭제
|
||||
if ($template->file_path && Storage::disk('local')->exists($template->file_path)) {
|
||||
Storage::disk('local')->delete($template->file_path);
|
||||
}
|
||||
|
||||
$file = $request->file('file');
|
||||
$filePath = $file->store("esign/{$tenantId}/templates", 'local');
|
||||
|
||||
$template->update([
|
||||
'file_path' => $filePath,
|
||||
'file_name' => $file->getClientOriginalName(),
|
||||
'file_hash' => hash_file('sha256', $file->getRealPath()),
|
||||
'file_size' => $file->getSize(),
|
||||
]);
|
||||
|
||||
return response()->json([
|
||||
'success' => true,
|
||||
'message' => 'PDF가 교체되었습니다.',
|
||||
'data' => $template->fresh()->load(['creator:id,name', 'items'])->loadCount('items'),
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* 템플릿 PDF 제거
|
||||
*/
|
||||
public function removeTemplatePdf(int $id): JsonResponse
|
||||
{
|
||||
$tenantId = session('selected_tenant_id', 1);
|
||||
$template = EsignFieldTemplate::forTenant($tenantId)->findOrFail($id);
|
||||
|
||||
if ($template->file_path && Storage::disk('local')->exists($template->file_path)) {
|
||||
Storage::disk('local')->delete($template->file_path);
|
||||
}
|
||||
|
||||
$template->update([
|
||||
'file_path' => null,
|
||||
'file_name' => null,
|
||||
'file_hash' => null,
|
||||
'file_size' => null,
|
||||
]);
|
||||
|
||||
return response()->json([
|
||||
'success' => true,
|
||||
'message' => 'PDF가 제거되었습니다.',
|
||||
'data' => $template->fresh()->load(['creator:id,name', 'items'])->loadCount('items'),
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* 템플릿 필드 아이템 삭제
|
||||
*/
|
||||
public function destroyTemplateItem(int $templateId, int $itemId): JsonResponse
|
||||
{
|
||||
$tenantId = session('selected_tenant_id', 1);
|
||||
$template = EsignFieldTemplate::forTenant($tenantId)->findOrFail($templateId);
|
||||
$item = EsignFieldTemplateItem::where('template_id', $template->id)->findOrFail($itemId);
|
||||
|
||||
$item->delete();
|
||||
|
||||
// signer_count 재계산
|
||||
$maxOrder = EsignFieldTemplateItem::where('template_id', $template->id)->max('signer_order');
|
||||
$template->update(['signer_count' => $maxOrder ?: 0]);
|
||||
|
||||
return response()->json([
|
||||
'success' => true,
|
||||
'message' => '필드가 삭제되었습니다.',
|
||||
'data' => $template->fresh()->load(['creator:id,name', 'items'])->loadCount('items'),
|
||||
]);
|
||||
}
|
||||
|
||||
|
||||
@@ -38,62 +38,262 @@
|
||||
);
|
||||
};
|
||||
|
||||
const FIELD_TYPE_MAP = {
|
||||
signature: { label: '서명', color: 'bg-blue-100 text-blue-700' },
|
||||
stamp: { label: '도장', color: 'bg-purple-100 text-purple-700' },
|
||||
text: { label: '텍스트', color: 'bg-gray-100 text-gray-700' },
|
||||
date: { label: '날짜', color: 'bg-green-100 text-green-700' },
|
||||
checkbox: { label: '체크박스', color: 'bg-yellow-100 text-yellow-700' },
|
||||
};
|
||||
|
||||
// ─── EditTemplateModal ───
|
||||
const EditTemplateModal = ({ open, template, onClose, onSave }) => {
|
||||
const EditTemplateModal = ({ open, template, onClose, onUpdate }) => {
|
||||
const [tab, setTab] = useState('info');
|
||||
const [name, setName] = useState('');
|
||||
const [description, setDescription] = useState('');
|
||||
const [category, setCategory] = useState('');
|
||||
const [saving, setSaving] = useState(false);
|
||||
const [tpl, setTpl] = useState(null); // 로컬 상태 (PDF/필드 변경 반영)
|
||||
const [uploading, setUploading] = useState(false);
|
||||
const fileInputRef = useRef(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (template) {
|
||||
setName(template.name || '');
|
||||
setDescription(template.description || '');
|
||||
setCategory(template.category || '');
|
||||
setTpl(template);
|
||||
setTab('info');
|
||||
}
|
||||
}, [template]);
|
||||
|
||||
if (!open || !template) return null;
|
||||
if (!open || !tpl) return null;
|
||||
|
||||
const handleSave = async () => {
|
||||
if (!name.trim()) { alert('템플릿 이름을 입력해주세요.'); return; }
|
||||
setSaving(true);
|
||||
await onSave(template.id, { name: name.trim(), description: description.trim(), category: category || null });
|
||||
try {
|
||||
const res = await fetch(`/esign/contracts/templates/${tpl.id}`, {
|
||||
method: 'PUT', headers: { ...getHeaders(), 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ name: name.trim(), description: description.trim(), category: category || null }),
|
||||
});
|
||||
const json = await res.json();
|
||||
if (json.success) { onUpdate(json.data); onClose(); }
|
||||
else alert(json.message || '수정 실패');
|
||||
} catch (_) { alert('서버 오류'); }
|
||||
setSaving(false);
|
||||
};
|
||||
|
||||
const handleUploadPdf = async (e) => {
|
||||
const file = e.target.files[0];
|
||||
if (!file) return;
|
||||
setUploading(true);
|
||||
try {
|
||||
const fd = new FormData();
|
||||
fd.append('file', file);
|
||||
const res = await fetch(`/esign/contracts/templates/${tpl.id}/upload-pdf`, {
|
||||
method: 'POST',
|
||||
headers: { 'Accept': 'application/json', 'X-CSRF-TOKEN': csrfToken },
|
||||
body: fd,
|
||||
});
|
||||
const json = await res.json();
|
||||
if (json.success) { setTpl(json.data); onUpdate(json.data); }
|
||||
else alert(json.message || '업로드 실패');
|
||||
} catch (_) { alert('서버 오류'); }
|
||||
setUploading(false);
|
||||
if (fileInputRef.current) fileInputRef.current.value = '';
|
||||
};
|
||||
|
||||
const handleRemovePdf = async () => {
|
||||
if (!confirm('PDF 파일을 제거하시겠습니까?')) return;
|
||||
try {
|
||||
const res = await fetch(`/esign/contracts/templates/${tpl.id}/remove-pdf`, {
|
||||
method: 'DELETE', headers: getHeaders(),
|
||||
});
|
||||
const json = await res.json();
|
||||
if (json.success) { setTpl(json.data); onUpdate(json.data); }
|
||||
else alert(json.message || '제거 실패');
|
||||
} catch (_) { alert('서버 오류'); }
|
||||
};
|
||||
|
||||
const handleDeleteItem = async (itemId) => {
|
||||
if (!confirm('이 필드를 삭제하시겠습니까?')) return;
|
||||
try {
|
||||
const res = await fetch(`/esign/contracts/templates/${tpl.id}/items/${itemId}`, {
|
||||
method: 'DELETE', headers: getHeaders(),
|
||||
});
|
||||
const json = await res.json();
|
||||
if (json.success) { setTpl(json.data); onUpdate(json.data); }
|
||||
else alert(json.message || '삭제 실패');
|
||||
} catch (_) { alert('서버 오류'); }
|
||||
};
|
||||
|
||||
const tabs = [
|
||||
{ key: 'info', label: '기본 정보' },
|
||||
{ key: 'pdf', label: 'PDF 파일' },
|
||||
{ key: 'fields', label: `서식 필드 (${tpl.items?.length || 0})` },
|
||||
];
|
||||
|
||||
return (
|
||||
<div className="fixed inset-0 bg-black/40 flex items-center justify-center z-50" onClick={onClose}>
|
||||
<div className="bg-white rounded-xl shadow-2xl w-[420px] p-5" onClick={e => e.stopPropagation()}>
|
||||
<h3 className="text-base font-semibold mb-4">템플릿 편집</h3>
|
||||
<div className="space-y-3">
|
||||
<div>
|
||||
<label className="text-xs text-gray-500 block mb-1">템플릿 이름 *</label>
|
||||
<input type="text" value={name} onChange={e => setName(e.target.value)}
|
||||
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 />
|
||||
<div className="bg-white rounded-xl shadow-2xl w-[680px] max-h-[85vh] flex flex-col" onClick={e => e.stopPropagation()}>
|
||||
{/* 헤더 + 탭 */}
|
||||
<div className="px-6 pt-5 pb-0 flex-shrink-0">
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<h3 className="text-base font-semibold">템플릿 편집</h3>
|
||||
<button onClick={onClose} className="text-gray-400 hover:text-gray-600 text-lg leading-none">×</button>
|
||||
</div>
|
||||
<div>
|
||||
<label className="text-xs text-gray-500 block mb-1">설명</label>
|
||||
<textarea value={description} onChange={e => setDescription(e.target.value)}
|
||||
rows={2} 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 resize-none" />
|
||||
</div>
|
||||
<div>
|
||||
<label className="text-xs text-gray-500 block mb-1">카테고리</label>
|
||||
<div className="flex gap-2">
|
||||
<select value={category} onChange={e => setCategory(e.target.value)}
|
||||
className="flex-1 border rounded-lg px-3 py-2 text-sm focus:ring-2 focus:ring-blue-200 focus:border-blue-400 outline-none">
|
||||
<option value="">카테고리 없음</option>
|
||||
{CATEGORIES.map(c => <option key={c} value={c}>{c}</option>)}
|
||||
</select>
|
||||
</div>
|
||||
<div className="flex gap-1 border-b -mx-6 px-6">
|
||||
{tabs.map(t => (
|
||||
<button key={t.key} onClick={() => setTab(t.key)}
|
||||
className={`px-3 py-2 text-sm font-medium border-b-2 -mb-px transition-colors ${tab === t.key ? 'border-blue-600 text-blue-600' : 'border-transparent text-gray-500 hover:text-gray-700'}`}>
|
||||
{t.label}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex justify-end gap-2 mt-5">
|
||||
<button onClick={onClose} className="px-4 py-1.5 text-sm border rounded-lg hover:bg-gray-50">취소</button>
|
||||
<button onClick={handleSave} disabled={saving}
|
||||
className="px-4 py-1.5 text-sm bg-blue-600 text-white rounded-lg hover:bg-blue-700 disabled:opacity-50">
|
||||
{saving ? '저장 중...' : '저장'}
|
||||
</button>
|
||||
|
||||
{/* 탭 콘텐츠 */}
|
||||
<div className="px-6 py-5 flex-1 overflow-y-auto">
|
||||
{/* 기본 정보 탭 */}
|
||||
{tab === 'info' && (
|
||||
<div className="space-y-3">
|
||||
<div>
|
||||
<label className="text-xs text-gray-500 block mb-1">템플릿 이름 *</label>
|
||||
<input type="text" value={name} onChange={e => setName(e.target.value)}
|
||||
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 />
|
||||
</div>
|
||||
<div>
|
||||
<label className="text-xs text-gray-500 block mb-1">설명</label>
|
||||
<textarea value={description} onChange={e => setDescription(e.target.value)}
|
||||
rows={3} 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 resize-none" />
|
||||
</div>
|
||||
<div>
|
||||
<label className="text-xs text-gray-500 block mb-1">카테고리</label>
|
||||
<select value={category} onChange={e => setCategory(e.target.value)}
|
||||
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">
|
||||
<option value="">카테고리 없음</option>
|
||||
{CATEGORIES.map(c => <option key={c} value={c}>{c}</option>)}
|
||||
</select>
|
||||
</div>
|
||||
<div className="grid grid-cols-2 gap-3 pt-1">
|
||||
<div className="bg-gray-50 rounded-lg p-3">
|
||||
<p className="text-[11px] text-gray-400 mb-0.5">서명자 수</p>
|
||||
<p className="text-sm font-medium">{tpl.signer_count}명</p>
|
||||
</div>
|
||||
<div className="bg-gray-50 rounded-lg p-3">
|
||||
<p className="text-[11px] text-gray-400 mb-0.5">필드 수</p>
|
||||
<p className="text-sm font-medium">{tpl.items_count ?? tpl.items?.length ?? 0}개</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* PDF 파일 탭 */}
|
||||
{tab === 'pdf' && (
|
||||
<div className="space-y-4">
|
||||
{tpl.file_path ? (
|
||||
<div className="border rounded-lg p-4">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="w-10 h-10 bg-red-50 rounded-lg flex items-center justify-center flex-shrink-0">
|
||||
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="#EF4444" strokeWidth="2"><path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/><polyline points="14 2 14 8 20 8"/></svg>
|
||||
</div>
|
||||
<div className="flex-1 min-w-0">
|
||||
<p className="text-sm font-medium text-gray-900 truncate">{tpl.file_name || 'PDF 파일'}</p>
|
||||
<p className="text-xs text-gray-400">{tpl.file_size ? `${(tpl.file_size / 1024 / 1024).toFixed(2)} MB` : ''}</p>
|
||||
</div>
|
||||
<a href={`/esign/contracts/templates/${tpl.id}/download`} target="_blank"
|
||||
className="px-3 py-1.5 text-xs border rounded-lg text-gray-600 hover:bg-gray-50">다운로드</a>
|
||||
</div>
|
||||
<div className="flex gap-2 mt-3 pt-3 border-t">
|
||||
<button onClick={() => fileInputRef.current?.click()} disabled={uploading}
|
||||
className="px-3 py-1.5 text-xs bg-blue-600 text-white rounded-lg hover:bg-blue-700 disabled:opacity-50">
|
||||
{uploading ? '업로드 중...' : 'PDF 교체'}
|
||||
</button>
|
||||
<button onClick={handleRemovePdf}
|
||||
className="px-3 py-1.5 text-xs border border-red-300 text-red-600 rounded-lg hover:bg-red-50">
|
||||
PDF 제거
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className="border-2 border-dashed rounded-lg p-8 text-center">
|
||||
<svg className="mx-auto mb-3 text-gray-300" width="32" height="32" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5">
|
||||
<path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/><polyline points="14 2 14 8 20 8"/><line x1="12" y1="18" x2="12" y2="12"/><line x1="9" y1="15" x2="15" y2="15"/>
|
||||
</svg>
|
||||
<p className="text-sm text-gray-500 mb-3">연결된 PDF 파일이 없습니다</p>
|
||||
<button onClick={() => fileInputRef.current?.click()} disabled={uploading}
|
||||
className="px-4 py-2 text-sm bg-blue-600 text-white rounded-lg hover:bg-blue-700 disabled:opacity-50">
|
||||
{uploading ? '업로드 중...' : 'PDF 업로드'}
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
<input ref={fileInputRef} type="file" accept=".pdf" onChange={handleUploadPdf} className="hidden" />
|
||||
<p className="text-xs text-gray-400">PDF 파일은 이 템플릿으로 계약 생성 시 자동 사용됩니다. 최대 20MB.</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 서식 필드 탭 */}
|
||||
{tab === 'fields' && (
|
||||
<div>
|
||||
{(!tpl.items || tpl.items.length === 0) ? (
|
||||
<div className="text-center py-8 text-gray-400">
|
||||
<p className="text-sm mb-1">서식 필드가 없습니다.</p>
|
||||
<p className="text-xs">계약의 필드 에디터에서 "템플릿으로 저장" 시 필드가 포함됩니다.</p>
|
||||
</div>
|
||||
) : (
|
||||
<div className="border rounded-lg overflow-hidden">
|
||||
<table className="w-full">
|
||||
<thead className="bg-gray-50">
|
||||
<tr>
|
||||
<th className="px-3 py-2 text-left text-[11px] font-medium text-gray-500">유형</th>
|
||||
<th className="px-3 py-2 text-left text-[11px] font-medium text-gray-500">라벨</th>
|
||||
<th className="px-3 py-2 text-center text-[11px] font-medium text-gray-500">서명자</th>
|
||||
<th className="px-3 py-2 text-center text-[11px] font-medium text-gray-500">페이지</th>
|
||||
<th className="px-3 py-2 text-center text-[11px] font-medium text-gray-500">위치 (X, Y)</th>
|
||||
<th className="px-3 py-2 text-center text-[11px] font-medium text-gray-500">필수</th>
|
||||
<th className="px-3 py-2 w-10"></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="divide-y">
|
||||
{tpl.items.map(item => {
|
||||
const ft = FIELD_TYPE_MAP[item.field_type] || { label: item.field_type, color: 'bg-gray-100 text-gray-700' };
|
||||
return (
|
||||
<tr key={item.id} className="hover:bg-gray-50">
|
||||
<td className="px-3 py-2">
|
||||
<span className={`inline-block px-2 py-0.5 rounded text-[11px] font-medium ${ft.color}`}>{ft.label}</span>
|
||||
</td>
|
||||
<td className="px-3 py-2 text-sm text-gray-700">{item.field_label || '-'}</td>
|
||||
<td className="px-3 py-2 text-center text-xs text-gray-500">#{item.signer_order}</td>
|
||||
<td className="px-3 py-2 text-center text-xs text-gray-500">{item.page_number}</td>
|
||||
<td className="px-3 py-2 text-center text-xs text-gray-400 font-mono">{Number(item.position_x).toFixed(0)}, {Number(item.position_y).toFixed(0)}</td>
|
||||
<td className="px-3 py-2 text-center text-xs">{item.is_required ? <span className="text-green-600">✓</span> : <span className="text-gray-300">-</span>}</td>
|
||||
<td className="px-3 py-2 text-center">
|
||||
<button onClick={() => handleDeleteItem(item.id)} title="필드 삭제"
|
||||
className="text-gray-400 hover:text-red-500 transition-colors">
|
||||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2"><line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/></svg>
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
);
|
||||
})}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* 하단 버튼 */}
|
||||
<div className="px-6 py-4 border-t flex-shrink-0 flex justify-end gap-2">
|
||||
<button onClick={onClose} className="px-4 py-1.5 text-sm border rounded-lg hover:bg-gray-50">닫기</button>
|
||||
{tab === 'info' && (
|
||||
<button onClick={handleSave} disabled={saving}
|
||||
className="px-4 py-1.5 text-sm bg-blue-600 text-white rounded-lg hover:bg-blue-700 disabled:opacity-50">
|
||||
{saving ? '저장 중...' : '저장'}
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -243,24 +443,27 @@ className="w-full text-left px-3 py-1.5 text-sm text-red-600 hover:bg-red-50">
|
||||
fetchTemplates({ signerCount: val });
|
||||
};
|
||||
|
||||
const handleEdit = (template) => {
|
||||
setEditModal({ open: true, template });
|
||||
};
|
||||
|
||||
const handleSaveEdit = async (id, data) => {
|
||||
const handleEdit = async (template) => {
|
||||
// 상세 데이터(items 포함) 로드
|
||||
try {
|
||||
const res = await fetch(`/esign/contracts/templates/${id}`, {
|
||||
method: 'PUT', headers: getHeaders(), body: JSON.stringify(data),
|
||||
});
|
||||
const res = await fetch(`/esign/contracts/templates/${template.id}`, { headers: getHeaders() });
|
||||
const json = await res.json();
|
||||
if (json.success) {
|
||||
setTemplates(prev => prev.map(t => t.id === id ? { ...t, ...data } : t));
|
||||
setEditModal({ open: false, template: null });
|
||||
showToast('템플릿이 수정되었습니다.');
|
||||
setEditModal({ open: true, template: json.data });
|
||||
} else {
|
||||
alert(json.message || '수정 실패');
|
||||
setEditModal({ open: true, template });
|
||||
}
|
||||
} catch (_) { alert('서버 오류'); }
|
||||
} catch (_) {
|
||||
setEditModal({ open: true, template });
|
||||
}
|
||||
};
|
||||
|
||||
const handleUpdate = (updatedTemplate) => {
|
||||
setTemplates(prev => prev.map(t => t.id === updatedTemplate.id
|
||||
? { ...t, ...updatedTemplate, items_count: updatedTemplate.items?.length ?? t.items_count }
|
||||
: t
|
||||
));
|
||||
showToast('템플릿이 수정되었습니다.');
|
||||
};
|
||||
|
||||
const handleDuplicate = async (id) => {
|
||||
@@ -392,7 +595,7 @@ className="text-xs text-gray-500 hover:text-gray-700 underline">초기화</butto
|
||||
open={editModal.open}
|
||||
template={editModal.template}
|
||||
onClose={() => setEditModal({ open: false, template: null })}
|
||||
onSave={handleSaveEdit}
|
||||
onUpdate={handleUpdate}
|
||||
/>
|
||||
|
||||
<style>{`
|
||||
|
||||
@@ -1427,6 +1427,9 @@
|
||||
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');
|
||||
Route::post('/templates/{templateId}/upload-pdf', [EsignApiController::class, 'uploadTemplatePdf'])->whereNumber('templateId')->name('templates.upload-pdf');
|
||||
Route::delete('/templates/{templateId}/remove-pdf', [EsignApiController::class, 'removeTemplatePdf'])->whereNumber('templateId')->name('templates.remove-pdf');
|
||||
Route::delete('/templates/{templateId}/items/{itemId}', [EsignApiController::class, 'destroyTemplateItem'])->whereNumber('templateId')->whereNumber('itemId')->name('templates.items.destroy');
|
||||
Route::get('/templates/{templateId}/download', [EsignApiController::class, 'downloadTemplatePdf'])->whereNumber('templateId')->name('templates.download');
|
||||
|
||||
// 템플릿 적용 / 필드 복사
|
||||
|
||||
Reference in New Issue
Block a user