fix:E-Sign PDF 폰트 크기 2배 확대, 왼쪽 정렬 기본값, 서명자 매핑 수정

- PdfSignatureService: 자동 폰트 크기 공식 2배(h*0.7→h*1.4), 기본 정렬 C→L
- text_align 필드 추가 (L/C/R 정렬 선택 가능)
- store()/buildVariableMap(): sign_order→role 기반 매핑으로 변경
  (signer_order 1=creator/갑/회사, 2=counterpart/을/파트너)
- template-fields: 가로 정렬 버튼 UI 추가

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
김보곤
2026-02-13 18:07:31 +09:00
parent d5283099c4
commit 843e898f5f
5 changed files with 41 additions and 14 deletions

View File

@@ -337,9 +337,11 @@ public function store(Request $request): JsonResponse
if ($template && $template->items->isNotEmpty()) {
$contract->loadMissing('signers');
// 템플릿 signer_order를 역할(role)로 매핑: 1=creator(갑/회사), 2=counterpart(을/파트너)
$signerMap = [];
foreach ($contract->signers as $signer) {
$signerMap[$signer->sign_order] = $signer->id;
$templateOrder = $signer->role === 'creator' ? 1 : 2;
$signerMap[$templateOrder] = $signer->id;
}
$variableValues = $this->buildVariableMap($contract, $template);
@@ -366,6 +368,7 @@ public function store(Request $request): JsonResponse
'field_label' => $item->field_label,
'field_variable' => $item->field_variable,
'font_size' => $item->font_size,
'text_align' => $item->text_align ?? 'L',
'field_value' => $fieldValue,
'is_required' => $item->is_required,
'sort_order' => $item->sort_order,
@@ -837,6 +840,7 @@ public function storeTemplate(Request $request): JsonResponse
'items.*.field_label' => 'nullable|string|max:100',
'items.*.field_variable' => 'nullable|string|max:50',
'items.*.font_size' => 'nullable|integer|min:6|max:72',
'items.*.text_align' => 'nullable|string|in:L,C,R',
'items.*.is_required' => 'nullable|boolean',
]);
@@ -900,6 +904,7 @@ public function storeTemplate(Request $request): JsonResponse
'field_label' => $item['field_label'] ?? null,
'field_variable' => $item['field_variable'] ?? null,
'font_size' => $item['font_size'] ?? null,
'text_align' => $item['text_align'] ?? 'L',
'is_required' => $item['is_required'] ?? true,
'sort_order' => $i,
]);
@@ -1067,6 +1072,7 @@ public function updateTemplateItems(Request $request, int $templateId): JsonResp
'items.*.field_label' => 'nullable|string|max:100',
'items.*.field_variable' => 'nullable|string|max:50',
'items.*.font_size' => 'nullable|integer|min:6|max:72',
'items.*.text_align' => 'nullable|string|in:L,C,R',
'items.*.is_required' => 'nullable|boolean',
]);
@@ -1093,6 +1099,7 @@ public function updateTemplateItems(Request $request, int $templateId): JsonResp
'field_label' => $itemData['field_label'] ?? '',
'field_variable' => $itemData['field_variable'] ?? null,
'font_size' => $itemData['font_size'] ?? null,
'text_align' => $itemData['text_align'] ?? 'L',
'is_required' => $itemData['is_required'] ?? true,
'sort_order' => $i,
]);
@@ -1340,8 +1347,8 @@ private function buildVariableMap(EsignContract $contract, EsignFieldTemplate $t
{
$map = [];
// 시스템 변수: 서명자 정보
$signers = $contract->signers->sortBy('sign_order');
// 시스템 변수: 서명자 정보 (역할 기반: 1=creator/갑/회사, 2=counterpart/을/파트너)
$signers = $contract->signers->sortBy(fn($s) => $s->role === 'creator' ? 1 : 2);
$idx = 1;
foreach ($signers as $signer) {
$map["signer{$idx}_name"] = $signer->name;

View File

@@ -21,6 +21,7 @@ class EsignFieldTemplateItem extends Model
'field_label',
'field_variable',
'font_size',
'text_align',
'is_required',
'sort_order',
];

View File

@@ -22,6 +22,7 @@ class EsignSignField extends Model
'field_label',
'field_variable',
'font_size',
'text_align',
'field_value',
'is_required',
'sort_order',

View File

@@ -168,7 +168,7 @@ private function overlayDate(Fpdi $pdf, EsignSignField $field, float $x, float $
$dateText = now()->format('Y-m-d');
}
$this->renderText($pdf, $dateText, $x, $y, $w, $h, $field->font_size);
$this->renderText($pdf, $dateText, $x, $y, $w, $h, $field->font_size, $field->text_align ?? 'L');
}
/**
@@ -181,7 +181,7 @@ private function overlayText(Fpdi $pdf, EsignSignField $field, float $x, float $
return;
}
$this->renderText($pdf, $text, $x, $y, $w, $h, $field->font_size);
$this->renderText($pdf, $text, $x, $y, $w, $h, $field->font_size, $field->text_align ?? 'L');
}
/**
@@ -193,29 +193,30 @@ private function overlayCheckbox(Fpdi $pdf, EsignSignField $field, float $x, flo
return;
}
$this->renderText($pdf, "\xe2\x9c\x93", $x, $y, $w, $h, $field->font_size); // ✓ (UTF-8)
$this->renderText($pdf, "\xe2\x9c\x93", $x, $y, $w, $h, $field->font_size, 'C'); // ✓ (UTF-8)
}
/**
* 텍스트를 지정 영역에 렌더링하는 공통 메서드.
*/
private function renderText(Fpdi $pdf, string $text, float $x, float $y, float $w, float $h, ?int $fieldFontSize = null): void
private function renderText(Fpdi $pdf, string $text, float $x, float $y, float $w, float $h, ?int $fieldFontSize = null, string $textAlign = 'L'): void
{
// 필드에 지정된 폰트 크기 우선, 없으면 영역 높이 기반 자동 산출
// 필드에 지정된 폰트 크기 우선, 없으면 영역 높이 기반 자동 산출 (2배 확대)
if ($fieldFontSize && $fieldFontSize >= 4) {
$fontSize = $fieldFontSize;
} else {
$fontSize = min($h * 0.7, 12);
if ($fontSize < 4) {
$fontSize = 4;
$fontSize = min($h * 1.4, 24);
if ($fontSize < 6) {
$fontSize = 6;
}
}
$pdf->SetFont($this->getKoreanFont(), '', $fontSize);
$pdf->SetTextColor(0, 0, 0);
// 텍스트를 영역 중앙에 배치
// 텍스트를 지정된 정렬 방식으로 배치 (L=왼쪽, C=가운데, R=오른쪽)
$align = in_array($textAlign, ['L', 'C', 'R']) ? $textAlign : 'L';
$pdf->SetXY($x, $y);
$pdf->Cell($w, $h, $text, 0, 0, 'C', false, '', 0, false, 'T', 'M');
$pdf->Cell($w, $h, $text, 0, 0, $align, false, '', 0, false, 'T', 'M');
}
}

View File

@@ -461,6 +461,21 @@ className="w-full border rounded px-2 py-1 text-xs mt-0.5" />
className="w-full border rounded px-2 py-1 text-xs mt-0.5" />
</div>
</div>
{['text', 'date'].includes(selectedField.field_type) && (
<div>
<label className="text-[10px] text-gray-500">가로 정렬</label>
<div className="flex gap-1 mt-0.5">
{[{ value: 'L', label: '왼쪽', icon: '⫷' }, { value: 'C', label: '가운데', icon: '⫿' }, { value: 'R', label: '오른쪽', icon: '⫸' }].map(a => (
<button key={a.value}
onClick={() => onUpdateField(selectedFieldIndex, { text_align: a.value })}
className={`flex-1 px-2 py-1.5 border rounded text-[10px] font-medium transition-colors ${(selectedField.text_align || 'L') === a.value ? 'bg-blue-600 text-white border-blue-600' : 'text-gray-600 hover:bg-gray-50'}`}
title={a.label}>
{a.label}
</button>
))}
</div>
</div>
)}
<div className="flex items-center gap-2 pt-1">
<label className="flex items-center gap-1.5 text-xs cursor-pointer">
<input type="checkbox" checked={selectedField.is_required !== false}
@@ -592,6 +607,7 @@ className="text-gray-300 hover:text-red-500 flex-shrink-0 ml-1">&times;</button>
field_label: f.field_label || '',
field_variable: f.field_variable || null,
font_size: f.font_size || null,
text_align: f.text_align || 'L',
is_required: f.is_required !== false,
}));
setFields(loadedFields);
@@ -657,7 +673,7 @@ className="text-gray-300 hover:text-red-500 flex-shrink-0 ml-1">&times;</button>
const newField = {
signer_order: signerOrder, page_number: currentPage,
position_x: 25, position_y: 40, width: 20, height: 5,
field_type: fieldType, field_label: '', field_variable: null, font_size: null, is_required: true,
field_type: fieldType, field_label: '', field_variable: null, font_size: null, text_align: 'L', is_required: true,
};
const newFields = [...fields, newField];
setFields(newFields);
@@ -725,6 +741,7 @@ className="text-gray-300 hover:text-red-500 flex-shrink-0 ml-1">&times;</button>
field_label: f.field_label || '',
field_variable: f.field_variable || null,
font_size: f.font_size || null,
text_align: f.text_align || 'L',
is_required: f.is_required !== false,
})),
}),