value) */ public function render(array $schema, string $mode = 'view', array $data = []): string { $this->mode = $mode; $blocks = $schema['blocks'] ?? []; $page = $schema['page'] ?? []; $html = $this->renderPageOpen($page); foreach ($blocks as $block) { $html .= $this->renderBlock($block, $data); } $html .= $this->renderPageClose(); return $html; } /** * 개별 블록 렌더링 */ public function renderBlock(array $block, array $data = []): string { $type = $block['type'] ?? ''; $props = $block['props'] ?? []; $blockId = $block['id'] ?? ''; return match ($type) { 'heading' => $this->renderHeading($props, $data), 'paragraph' => $this->renderParagraph($props, $data), 'table' => $this->renderTable($props, $blockId, $data), 'columns' => $this->renderColumns($props, $data), 'divider' => $this->renderDivider($props), 'spacer' => $this->renderSpacer($props), 'text_field' => $this->renderTextField($props, $blockId, $data), 'number_field' => $this->renderNumberField($props, $blockId, $data), 'date_field' => $this->renderDateField($props, $blockId, $data), 'select_field' => $this->renderSelectField($props, $blockId, $data), 'checkbox_field' => $this->renderCheckboxField($props, $blockId, $data), 'textarea_field' => $this->renderTextareaField($props, $blockId, $data), 'signature_field' => $this->renderSignatureField($props, $blockId, $data), default => "", }; } /** * 블록 스키마에서 폼 필드의 binding 목록 추출 * edit.blade.php에서 JavaScript 데이터 수집 시 사용 */ public function extractBindings(array $schema): array { $bindings = []; $formTypes = ['text_field', 'number_field', 'date_field', 'select_field', 'checkbox_field', 'textarea_field', 'signature_field']; foreach ($schema['blocks'] ?? [] as $block) { $type = $block['type'] ?? ''; if (in_array($type, $formTypes)) { $binding = $block['props']['binding'] ?? ''; $blockId = $block['id'] ?? ''; $fieldKey = $binding ?: 'block_'.$blockId; $bindings[] = [ 'block_id' => $blockId, 'type' => $type, 'field_key' => $fieldKey, 'label' => $block['props']['label'] ?? '', 'required' => ! empty($block['props']['required']), ]; } // columns 내부 블록도 탐색 if ($type === 'columns') { foreach ($block['props']['children'] ?? [] as $children) { foreach ($children as $child) { $childType = $child['type'] ?? ''; if (in_array($childType, $formTypes)) { $binding = $child['props']['binding'] ?? ''; $childId = $child['id'] ?? ''; $fieldKey = $binding ?: 'block_'.$childId; $bindings[] = [ 'block_id' => $childId, 'type' => $childType, 'field_key' => $fieldKey, 'label' => $child['props']['label'] ?? '', 'required' => ! empty($child['props']['required']), ]; } } } } } return $bindings; } /** * 데이터 바인딩 치환 (변수 해석) */ protected function resolveBinding(string $text, array $data): string { return preg_replace_callback('/\{\{(.+?)\}\}/', function ($matches) use ($data) { $key = trim($matches[1]); if ($key === 'today') { return now()->format('Y-m-d'); } if ($key === 'now') { return now()->format('Y-m-d H:i'); } return data_get($data, $key, $matches[0]); }, $text); } /** * 폼 필드의 field_key 결정: binding이 있으면 binding, 없으면 block_{id} */ protected function getFieldKey(array $props, string $blockId): string { $binding = $props['binding'] ?? ''; return $binding !== '' ? $binding : 'block_'.$blockId; } /** * EAV 데이터에서 값 조회 */ protected function getFieldValue(string $fieldKey, array $data, string $default = ''): string { return (string) ($data[$fieldKey] ?? $default); } protected function isEditMode(): bool { return $this->mode === 'edit'; } protected function isViewMode(): bool { return $this->mode === 'view'; } protected function isPrintMode(): bool { return $this->mode === 'print'; } // ===== 레이아웃 블록 렌더링 ===== protected function renderPageOpen(array $page): string { $size = $page['size'] ?? 'A4'; $orientation = $page['orientation'] ?? 'portrait'; $printStyle = $this->isPrintMode() ? 'max-width: 210mm; margin: 0 auto; padding: 20mm 15mm;' : ''; return '
'; } protected function renderPageClose(): string { return '
'; } protected function renderHeading(array $props, array $data): string { $level = min(max(intval($props['level'] ?? 2), 1), 6); $text = e($this->resolveBinding($props['text'] ?? '', $data)); $align = e($props['align'] ?? 'left'); $sizes = [1 => '1.5em', 2 => '1.25em', 3 => '1.1em', 4 => '1em', 5 => '0.9em', 6 => '0.85em']; $size = $sizes[$level]; return "{$text}"; } protected function renderParagraph(array $props, array $data): string { $text = e($this->resolveBinding($props['text'] ?? '', $data)); $text = nl2br($text); $align = e($props['align'] ?? 'left'); return "

{$text}

"; } protected function renderTable(array $props, string $blockId, array $data): string { $headers = $props['headers'] ?? []; $rows = $props['rows'] ?? []; $showHeader = $props['showHeader'] ?? true; $html = ''; if ($showHeader && ! empty($headers)) { $html .= ''; foreach ($headers as $header) { $html .= ''; } $html .= ''; } $html .= ''; foreach ($rows as $rIdx => $row) { $html .= ''; foreach ($row as $cIdx => $cell) { $cellKey = "tbl_{$blockId}_r{$rIdx}_c{$cIdx}"; $savedValue = $this->getFieldValue($cellKey, $data, ''); $cellText = $cell ?? ''; if ($this->isEditMode()) { $displayValue = $savedValue !== '' ? $savedValue : $cellText; $html .= ''; } else { $displayValue = $savedValue !== '' ? $savedValue : $cellText; $value = $this->resolveBinding($displayValue, $data); $html .= ''; } } $html .= ''; } $html .= '
'.e($header).'
'; $html .= ''; $html .= ''.e($value).'
'; return $html; } protected function renderColumns(array $props, array $data): string { $count = intval($props['count'] ?? 2); $children = $props['children'] ?? []; $html = '
'; for ($i = 0; $i < $count; $i++) { $html .= '
'; $colChildren = $children[$i] ?? []; foreach ($colChildren as $child) { $html .= $this->renderBlock($child, $data); } $html .= '
'; } $html .= '
'; return $html; } protected function renderDivider(array $props): string { $style = ($props['style'] ?? 'solid') === 'dashed' ? 'dashed' : 'solid'; return "
"; } protected function renderSpacer(array $props): string { $height = max(intval($props['height'] ?? 20), 0); return "
"; } // ===== 폼 필드 블록 렌더링 ===== protected function renderTextField(array $props, string $blockId, array $data): string { $label = e($props['label'] ?? ''); $fieldKey = $this->getFieldKey($props, $blockId); $value = $this->getFieldValue($fieldKey, $data); $required = ! empty($props['required']); $requiredMark = $required ? ' *' : ''; $placeholder = e($props['placeholder'] ?? ''); if ($this->isEditMode()) { $requiredAttr = $required ? 'required' : ''; return '
' .'' .'' .'
'; } // view / print $displayValue = $value !== '' ? e($value) : '-'; return '
'.$displayValue.'
'; } protected function renderNumberField(array $props, string $blockId, array $data): string { $label = e($props['label'] ?? ''); $fieldKey = $this->getFieldKey($props, $blockId); $value = $this->getFieldValue($fieldKey, $data); $unit = e($props['unit'] ?? ''); $unitDisplay = $unit ? " ({$unit})" : ''; $min = $props['min'] ?? null; $max = $props['max'] ?? null; $decimal = intval($props['decimal'] ?? 0); if ($this->isEditMode()) { $attrs = ''; if ($min !== null) { $attrs .= ' min="'.e($min).'"'; } if ($max !== null) { $attrs .= ' max="'.e($max).'"'; } if ($decimal > 0) { $attrs .= ' step="'.e(1 / pow(10, $decimal)).'"'; } return '
' .'' .'' .'
'; } $displayValue = $value !== '' ? e($value).$unitDisplay : '-'; return '
'.$displayValue.'
'; } protected function renderDateField(array $props, string $blockId, array $data): string { $label = e($props['label'] ?? ''); $fieldKey = $this->getFieldKey($props, $blockId); $value = $this->getFieldValue($fieldKey, $data); if ($this->isEditMode()) { $defaultValue = $value !== '' ? $value : ($this->resolveBinding($props['default'] ?? '', $data) ?: ''); return '
' .'' .'' .'
'; } $displayValue = $value !== '' ? e($value) : '-'; return '
'.$displayValue.'
'; } protected function renderSelectField(array $props, string $blockId, array $data): string { $label = e($props['label'] ?? ''); $fieldKey = $this->getFieldKey($props, $blockId); $value = $this->getFieldValue($fieldKey, $data); $options = $props['options'] ?? []; if ($this->isEditMode()) { $html = '
' .'' .'
'; return $html; } $displayValue = $value !== '' ? e($value) : '-'; return '
'.$displayValue.'
'; } protected function renderCheckboxField(array $props, string $blockId, array $data): string { $label = e($props['label'] ?? ''); $fieldKey = $this->getFieldKey($props, $blockId); $savedValues = array_filter(explode(',', $this->getFieldValue($fieldKey, $data))); $options = $props['options'] ?? []; if ($this->isEditMode()) { $html = '
' .'' .'
'; foreach ($options as $idx => $opt) { $checked = in_array($opt, $savedValues) ? ' checked' : ''; $html .= ''; } $html .= '
'; return $html; } // view / print $html = '
'; foreach ($options as $opt) { $checked = in_array($opt, $savedValues) ? '☑' : '☐'; $html .= ''.$checked.' '.e($opt).''; } $html .= '
'; return $html; } protected function renderTextareaField(array $props, string $blockId, array $data): string { $label = e($props['label'] ?? ''); $fieldKey = $this->getFieldKey($props, $blockId); $value = $this->getFieldValue($fieldKey, $data); $rows = max(intval($props['rows'] ?? 3), 1); if ($this->isEditMode()) { return '
' .'' .'' .'
'; } $displayValue = $value !== '' ? nl2br(e($value)) : '-'; $height = $rows * 24; return '
'.$displayValue.'
'; } protected function renderSignatureField(array $props, string $blockId, array $data): string { $label = e($props['label'] ?? '서명'); $fieldKey = $this->getFieldKey($props, $blockId); $value = $this->getFieldValue($fieldKey, $data); if ($this->isEditMode()) { return '
' .'' .'
' .'클릭하여 서명' .'
'; } if ($value) { return '
서명
'; } return '
서명 영역
'; } }