diff --git a/app/Http/Controllers/Api/Admin/DocumentTemplateApiController.php b/app/Http/Controllers/Api/Admin/DocumentTemplateApiController.php index c417e28a..5d3e3dd2 100644 --- a/app/Http/Controllers/Api/Admin/DocumentTemplateApiController.php +++ b/app/Http/Controllers/Api/Admin/DocumentTemplateApiController.php @@ -127,6 +127,7 @@ public function store(Request $request): JsonResponse $validated = $request->validate([ 'name' => 'required|string|max:100', 'category' => 'nullable|string|max:50', + 'builder_type' => 'nullable|string|in:legacy,block', 'title' => 'nullable|string|max:200', 'company_name' => 'nullable|string|max:100', 'company_address' => 'nullable|string|max:255', @@ -134,6 +135,8 @@ public function store(Request $request): JsonResponse 'footer_remark_label' => 'nullable|string|max:50', 'footer_judgement_label' => 'nullable|string|max:50', 'footer_judgement_options' => 'nullable|array', + 'schema' => 'nullable|array', + 'page_config' => 'nullable|array', 'is_active' => 'boolean', 'linked_item_ids' => 'nullable|array', 'linked_item_ids.*' => 'integer', @@ -162,6 +165,7 @@ public function store(Request $request): JsonResponse 'tenant_id' => session('selected_tenant_id'), 'name' => $validated['name'], 'category' => $validated['category'] ?? null, + 'builder_type' => $validated['builder_type'] ?? 'legacy', 'title' => $validated['title'] ?? null, 'company_name' => $validated['company_name'] ?? '경동기업', 'company_address' => $validated['company_address'] ?? null, @@ -169,6 +173,8 @@ public function store(Request $request): JsonResponse 'footer_remark_label' => $validated['footer_remark_label'] ?? '부적합 내용', 'footer_judgement_label' => $validated['footer_judgement_label'] ?? '종합판정', 'footer_judgement_options' => $validated['footer_judgement_options'] ?? ['적합', '부적합'], + 'schema' => $validated['schema'] ?? null, + 'page_config' => $validated['page_config'] ?? null, 'is_active' => $validated['is_active'] ?? true, 'linked_item_ids' => $validated['linked_item_ids'] ?? null, 'linked_process_id' => $validated['linked_process_id'] ?? null, @@ -204,6 +210,7 @@ public function update(Request $request, int $id): JsonResponse $validated = $request->validate([ 'name' => 'required|string|max:100', 'category' => 'nullable|string|max:50', + 'builder_type' => 'nullable|string|in:legacy,block', 'title' => 'nullable|string|max:200', 'company_name' => 'nullable|string|max:100', 'company_address' => 'nullable|string|max:255', @@ -211,6 +218,8 @@ public function update(Request $request, int $id): JsonResponse 'footer_remark_label' => 'nullable|string|max:50', 'footer_judgement_label' => 'nullable|string|max:50', 'footer_judgement_options' => 'nullable|array', + 'schema' => 'nullable|array', + 'page_config' => 'nullable|array', 'is_active' => 'boolean', 'linked_item_ids' => 'nullable|array', 'linked_item_ids.*' => 'integer', @@ -235,7 +244,7 @@ public function update(Request $request, int $id): JsonResponse try { DB::beginTransaction(); - $template->update([ + $updateData = [ 'name' => $validated['name'], 'category' => $validated['category'] ?? null, 'title' => $validated['title'] ?? null, @@ -248,7 +257,20 @@ public function update(Request $request, int $id): JsonResponse 'is_active' => $validated['is_active'] ?? true, 'linked_item_ids' => $validated['linked_item_ids'] ?? null, 'linked_process_id' => $validated['linked_process_id'] ?? null, - ]); + ]; + + // 블록 빌더 전용 필드 + if (isset($validated['builder_type'])) { + $updateData['builder_type'] = $validated['builder_type']; + } + if (array_key_exists('schema', $validated)) { + $updateData['schema'] = $validated['schema']; + } + if (array_key_exists('page_config', $validated)) { + $updateData['page_config'] = $validated['page_config']; + } + + $template->update($updateData); // 관계 데이터 저장 (기존 데이터 삭제 후 재생성) $this->saveRelations($template, $validated, true); @@ -396,6 +418,7 @@ public function duplicate(Request $request, int $id): JsonResponse 'tenant_id' => $source->tenant_id, 'name' => $newName, 'category' => $source->category, + 'builder_type' => $source->builder_type ?? 'legacy', 'title' => $source->title, 'company_name' => $source->company_name, 'company_address' => $source->company_address, @@ -403,6 +426,8 @@ public function duplicate(Request $request, int $id): JsonResponse 'footer_remark_label' => $source->footer_remark_label, 'footer_judgement_label' => $source->footer_judgement_label, 'footer_judgement_options' => $source->footer_judgement_options, + 'schema' => $source->schema, + 'page_config' => $source->page_config, 'is_active' => false, 'linked_item_ids' => null, // 연결품목은 복사하지 않음 (중복 방지) 'linked_process_id' => null, diff --git a/app/Http/Controllers/DocumentTemplateController.php b/app/Http/Controllers/DocumentTemplateController.php index 4c7156d0..44688194 100644 --- a/app/Http/Controllers/DocumentTemplateController.php +++ b/app/Http/Controllers/DocumentTemplateController.php @@ -58,6 +58,11 @@ public function edit(int $id): View 'links.linkValues', ])->findOrFail($id); + // 블록 빌더 타입이면 block-editor로 리다이렉트 + if ($template->isBlockBuilder()) { + return $this->blockEdit($id); + } + // JavaScript용 데이터 변환 $templateData = $this->prepareTemplateData($template); @@ -72,6 +77,56 @@ public function edit(int $id): View ]); } + /** + * 블록 빌더 - 새 양식 생성 + */ + public function blockCreate(Request $request): View + { + if ($request->header('HX-Request')) { + return response('', 200)->header('HX-Redirect', route('document-templates.block-create')); + } + + return view('document-templates.block-editor', [ + 'template' => null, + 'templateId' => 0, + 'isCreate' => true, + 'categories' => $this->getCategories(), + 'initialSchema' => [ + '_name' => '새 문서양식', + '_category' => '', + 'version' => '1.0', + 'page' => ['size' => 'A4', 'orientation' => 'portrait', 'margin' => [20, 15, 20, 15]], + 'blocks' => [], + ], + ]); + } + + /** + * 블록 빌더 - 양식 수정 + */ + public function blockEdit(int $id): View + { + $template = DocumentTemplate::findOrFail($id); + + $schema = $template->schema ?? [ + 'version' => '1.0', + 'page' => $template->page_config ?? ['size' => 'A4', 'orientation' => 'portrait', 'margin' => [20, 15, 20, 15]], + 'blocks' => [], + ]; + + // 뷰에서 사용할 메타 정보 주입 + $schema['_name'] = $template->name; + $schema['_category'] = $template->category ?? ''; + + return view('document-templates.block-editor', [ + 'template' => $template, + 'templateId' => $template->id, + 'isCreate' => false, + 'categories' => $this->getCategories(), + 'initialSchema' => $schema, + ]); + } + /** * 현재 선택된 테넌트 조회 */ diff --git a/app/Models/DocumentTemplate.php b/app/Models/DocumentTemplate.php index 56645b60..f23eaddc 100644 --- a/app/Models/DocumentTemplate.php +++ b/app/Models/DocumentTemplate.php @@ -16,6 +16,7 @@ class DocumentTemplate extends Model 'tenant_id', 'name', 'category', + 'builder_type', 'title', 'company_name', 'company_address', @@ -23,6 +24,8 @@ class DocumentTemplate extends Model 'footer_remark_label', 'footer_judgement_label', 'footer_judgement_options', + 'schema', + 'page_config', 'is_active', 'linked_item_ids', 'linked_process_id', @@ -30,10 +33,28 @@ class DocumentTemplate extends Model protected $casts = [ 'footer_judgement_options' => 'array', + 'schema' => 'array', + 'page_config' => 'array', 'linked_item_ids' => 'array', 'is_active' => 'boolean', ]; + /** + * 블록 빌더 타입 여부 + */ + public function isBlockBuilder(): bool + { + return $this->builder_type === 'block'; + } + + /** + * 레거시 빌더 타입 여부 + */ + public function isLegacyBuilder(): bool + { + return $this->builder_type !== 'block'; + } + /** * 결재라인 */ diff --git a/app/Services/BlockRendererService.php b/app/Services/BlockRendererService.php new file mode 100644 index 00000000..9a4e6896 --- /dev/null +++ b/app/Services/BlockRendererService.php @@ -0,0 +1,235 @@ +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'] ?? []; + + return match ($type) { + 'heading' => $this->renderHeading($props, $data), + 'paragraph' => $this->renderParagraph($props, $data), + 'table' => $this->renderTable($props, $data), + 'columns' => $this->renderColumns($props, $data), + 'divider' => $this->renderDivider($props), + 'spacer' => $this->renderSpacer($props), + 'text_field' => $this->renderTextField($props, $data), + 'number_field' => $this->renderNumberField($props, $data), + 'date_field' => $this->renderDateField($props, $data), + 'select_field' => $this->renderSelectField($props, $data), + 'checkbox_field' => $this->renderCheckboxField($props, $data), + 'textarea_field' => $this->renderTextareaField($props, $data), + 'signature_field' => $this->renderSignatureField($props, $data), + default => "", + }; + } + + /** + * 데이터 바인딩 치환 + */ + 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'); + } + + return data_get($data, $key, $matches[0]); + }, $text); + } + + // ===== 렌더링 메서드 ===== + + protected function renderPageOpen(array $page): string + { + $size = $page['size'] ?? 'A4'; + $orientation = $page['orientation'] ?? 'portrait'; + + return '
{$text}
"; + } + + protected function renderTable(array $props, array $data): string + { + $headers = $props['headers'] ?? []; + $rows = $props['rows'] ?? []; + $showHeader = $props['showHeader'] ?? true; + + $html = '| '.e($header).' | '; + } + $html .= '
|---|
| '.e($value).' | '; + } + $html .= '
좌측 팔레트에서 블록을 추가하세요
+또는 블록을 클릭하여 시작
+| + + |
|---|
| + + |
블록을 선택하면
+속성을 편집할 수 있습니다
++ Phase 2에서 열 내부에 블록을 추가하는 기능이 구현됩니다. +
+