feat:E-Sign 템플릿 변수 자동채움 시스템 구현

- 시스템 변수 (서명자명, 이메일, 계약제목, 날짜 등) 자동 해석
- 커스텀 변수 정의/관리 (템플릿별 계약금액, 기간 등)
- 템플릿 필드 에디터: 변수 관리 + 필드-변수 바인딩 UI
- 계약 생성 폼: 템플릿 변수 입력 섹션 추가
- 계약 필드 에디터: 변수 연결 정보 표시
- PdfSignatureService: font_size 반영 렌더링

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
김보곤
2026-02-13 07:44:45 +09:00
parent b206eeeb2d
commit 5ffabed6b4
9 changed files with 249 additions and 21 deletions

View File

@@ -95,6 +95,8 @@ public function store(Request $request): JsonResponse
'signers.*.email' => 'required|email|max:200',
'signers.*.phone' => 'nullable|string|max:20',
'signers.*.role' => 'required|in:creator,counterpart',
'metadata' => 'nullable|array',
'metadata.*' => 'nullable|string|max:500',
]);
$tenantId = session('selected_tenant_id', 1);
@@ -145,6 +147,7 @@ public function store(Request $request): JsonResponse
'original_file_hash' => $fileHash,
'original_file_size' => $fileSize,
'status' => 'draft',
'metadata' => $request->input('metadata'),
'expires_at' => $request->input('expires_at')
? \Carbon\Carbon::parse($request->input('expires_at'))
: now()->addDays($request->input('expires_days', 30)),
@@ -383,6 +386,7 @@ public function configureFields(Request $request, int $id): JsonResponse
'fields.*.height' => 'required|numeric',
'fields.*.field_type' => 'required|in:signature,stamp,text,date,checkbox',
'fields.*.field_label' => 'nullable|string|max:100',
'fields.*.field_variable' => 'nullable|string|max:50',
'fields.*.font_size' => 'nullable|integer|min:6|max:72',
'fields.*.is_required' => 'nullable|boolean',
]);
@@ -405,6 +409,7 @@ public function configureFields(Request $request, int $id): JsonResponse
'height' => $field['height'],
'field_type' => $field['field_type'],
'field_label' => $field['field_label'] ?? null,
'field_variable' => $field['field_variable'] ?? null,
'font_size' => $field['font_size'] ?? null,
'is_required' => $field['is_required'] ?? true,
'sort_order' => $i,
@@ -623,6 +628,7 @@ public function storeTemplate(Request $request): JsonResponse
'items.*.height' => 'required|numeric',
'items.*.field_type' => 'required|in:signature,stamp,text,date,checkbox',
'items.*.field_label' => 'nullable|string|max:100',
'items.*.field_variable' => 'nullable|string|max:50',
'items.*.font_size' => 'nullable|integer|min:6|max:72',
'items.*.is_required' => 'nullable|boolean',
]);
@@ -683,6 +689,7 @@ public function storeTemplate(Request $request): JsonResponse
'height' => $item['height'],
'field_type' => $item['field_type'],
'field_label' => $item['field_label'] ?? null,
'field_variable' => $item['field_variable'] ?? null,
'font_size' => $item['font_size'] ?? null,
'is_required' => $item['is_required'] ?? true,
'sort_order' => $i,
@@ -722,16 +729,27 @@ public function updateTemplate(Request $request, int $id): JsonResponse
'name' => 'required|string|max:100',
'description' => 'nullable|string',
'category' => 'nullable|string|max:50',
'variables' => 'nullable|array',
'variables.*.key' => 'required|string|max:50',
'variables.*.label' => 'required|string|max:100',
'variables.*.type' => 'nullable|in:text,number,date',
'variables.*.default' => 'nullable|string|max:500',
]);
$tenantId = session('selected_tenant_id', 1);
$template = EsignFieldTemplate::forTenant($tenantId)->findOrFail($id);
$template->update([
$updateData = [
'name' => $request->input('name'),
'description' => $request->input('description'),
'category' => $request->input('category'),
]);
];
if ($request->has('variables')) {
$updateData['variables'] = $request->input('variables');
}
$template->update($updateData);
return response()->json([
'success' => true,
@@ -837,6 +855,7 @@ public function updateTemplateItems(Request $request, int $templateId): JsonResp
'items.*.height' => 'required|numeric',
'items.*.field_type' => 'required|in:signature,stamp,text,date,checkbox',
'items.*.field_label' => 'nullable|string|max:100',
'items.*.field_variable' => 'nullable|string|max:50',
'items.*.font_size' => 'nullable|integer|min:6|max:72',
'items.*.is_required' => 'nullable|boolean',
]);
@@ -862,6 +881,7 @@ public function updateTemplateItems(Request $request, int $templateId): JsonResp
'height' => round($itemData['height'], 2),
'field_type' => $itemData['field_type'],
'field_label' => $itemData['field_label'] ?? '',
'field_variable' => $itemData['field_variable'] ?? null,
'font_size' => $itemData['font_size'] ?? null,
'is_required' => $itemData['is_required'] ?? true,
'sort_order' => $i,
@@ -913,6 +933,7 @@ public function duplicateTemplate(int $id): JsonResponse
'description' => $template->description,
'category' => $template->category,
'signer_count' => $template->signer_count,
'variables' => $template->variables,
'is_active' => true,
'created_by' => auth()->id(),
], $fileData));
@@ -928,6 +949,7 @@ public function duplicateTemplate(int $id): JsonResponse
'height' => $item->height,
'field_type' => $item->field_type,
'field_label' => $item->field_label,
'field_variable' => $item->field_variable,
'font_size' => $item->font_size,
'is_required' => $item->is_required,
'sort_order' => $item->sort_order,
@@ -988,7 +1010,10 @@ public function applyTemplate(Request $request, int $id): JsonResponse
$signerMap[$signer->sign_order] = $signer->id;
}
DB::transaction(function () use ($contract, $template, $tenantId, $signerMap) {
// 변수 해석용 맵 구성
$variableValues = $this->buildVariableMap($contract, $template);
DB::transaction(function () use ($contract, $template, $tenantId, $signerMap, $variableValues) {
// 기존 필드 삭제
EsignSignField::where('contract_id', $contract->id)->delete();
@@ -997,6 +1022,12 @@ public function applyTemplate(Request $request, int $id): JsonResponse
$signerId = $signerMap[$item->signer_order] ?? null;
if (!$signerId) continue;
// 변수가 바인딩된 필드는 자동 채움
$fieldValue = null;
if ($item->field_variable && isset($variableValues[$item->field_variable])) {
$fieldValue = $variableValues[$item->field_variable];
}
EsignSignField::create([
'tenant_id' => $tenantId,
'contract_id' => $contract->id,
@@ -1008,7 +1039,9 @@ public function applyTemplate(Request $request, int $id): JsonResponse
'height' => $item->height,
'field_type' => $item->field_type,
'field_label' => $item->field_label,
'field_variable' => $item->field_variable,
'font_size' => $item->font_size,
'field_value' => $fieldValue,
'is_required' => $item->is_required,
'sort_order' => $item->sort_order,
]);
@@ -1073,6 +1106,7 @@ public function copyFieldsFromContract(Request $request, int $id, int $sourceId)
'height' => $field->height,
'field_type' => $field->field_type,
'field_label' => $field->field_label,
'field_variable' => $field->field_variable,
'font_size' => $field->font_size,
'is_required' => $field->is_required,
'sort_order' => $field->sort_order,
@@ -1088,4 +1122,35 @@ public function copyFieldsFromContract(Request $request, int $id, int $sourceId)
'data' => $fields,
]);
}
/**
* 변수 해석 맵 구성 (시스템 변수 + 커스텀 변수)
*/
private function buildVariableMap(EsignContract $contract, EsignFieldTemplate $template): array
{
$map = [];
// 시스템 변수: 서명자 정보
$signers = $contract->signers->sortBy('sign_order');
$idx = 1;
foreach ($signers as $signer) {
$map["signer{$idx}_name"] = $signer->name;
$map["signer{$idx}_email"] = $signer->email;
$map["signer{$idx}_phone"] = $signer->phone ?? '';
$idx++;
}
// 시스템 변수: 계약 정보
$map['contract_title'] = $contract->title ?? '';
$map['current_date'] = now()->format('Y.m.d');
$map['expires_at'] = $contract->expires_at ? $contract->expires_at->format('Y.m.d') : '';
// 커스텀 변수: contract.metadata에서 조회
$metadata = $contract->metadata ?? [];
foreach ($metadata as $key => $value) {
$map[$key] = $value ?? '';
}
return $map;
}
}