- block-editor.blade.php: 3패널 UI (Palette + Canvas + Properties) - Alpine.js blockEditor() 컴포넌트 (CRUD, Undo/Redo, SortableJS) - 기본 Block 6종: heading, paragraph, table, columns, divider, spacer - 폼 필드 Block 7종: text, number, date, select, checkbox, textarea, signature - BlockRendererService: JSON → HTML 렌더링 서비스 - 컨트롤러 분기: builder_type = 'block' → 블록 빌더 뷰 - 라우트 추가: block-create, block-edit - API store/update에 schema JSON 처리 추가 - index 페이지에 블록 빌더 진입 버튼 추가 - 목록에 builder_type 뱃지 표시
396 lines
14 KiB
PHP
396 lines
14 KiB
PHP
<?php
|
|
|
|
namespace App\Http\Controllers;
|
|
|
|
use App\Models\DocumentTemplate;
|
|
use App\Models\DocumentTemplateFieldPreset;
|
|
use App\Models\Products\CommonCode;
|
|
use App\Models\Tenants\Tenant;
|
|
use App\Models\User;
|
|
use Illuminate\Http\Request;
|
|
use Illuminate\Http\Response;
|
|
use Illuminate\Support\Facades\DB;
|
|
use Illuminate\View\View;
|
|
|
|
class DocumentTemplateController extends Controller
|
|
{
|
|
/**
|
|
* 문서양식 목록 페이지
|
|
*/
|
|
public function index(Request $request): View|Response
|
|
{
|
|
if ($request->header('HX-Request')) {
|
|
return response('', 200)->header('HX-Redirect', route('document-templates.index'));
|
|
}
|
|
|
|
return view('document-templates.index', [
|
|
'categories' => $this->getCategories(),
|
|
]);
|
|
}
|
|
|
|
/**
|
|
* 문서양식 생성 페이지
|
|
*/
|
|
public function create(): View
|
|
{
|
|
return view('document-templates.edit', [
|
|
'template' => null,
|
|
'templateData' => null,
|
|
'isCreate' => true,
|
|
'categories' => $this->getCategories(),
|
|
'tenant' => $this->getCurrentTenant(),
|
|
'presets' => DocumentTemplateFieldPreset::orderBy('sort_order')->get(),
|
|
'basicFieldKeys' => $this->getBasicFieldKeys(),
|
|
]);
|
|
}
|
|
|
|
/**
|
|
* 문서양식 수정 페이지
|
|
*/
|
|
public function edit(int $id): View
|
|
{
|
|
$template = DocumentTemplate::with([
|
|
'approvalLines',
|
|
'basicFields',
|
|
'sections.items',
|
|
'columns',
|
|
'sectionFields',
|
|
'links.linkValues',
|
|
])->findOrFail($id);
|
|
|
|
// 블록 빌더 타입이면 block-editor로 리다이렉트
|
|
if ($template->isBlockBuilder()) {
|
|
return $this->blockEdit($id);
|
|
}
|
|
|
|
// JavaScript용 데이터 변환
|
|
$templateData = $this->prepareTemplateData($template);
|
|
|
|
return view('document-templates.edit', [
|
|
'template' => $template,
|
|
'templateData' => $templateData,
|
|
'isCreate' => false,
|
|
'categories' => $this->getCategories(),
|
|
'tenant' => $this->getCurrentTenant(),
|
|
'presets' => DocumentTemplateFieldPreset::orderBy('sort_order')->get(),
|
|
'basicFieldKeys' => $this->getBasicFieldKeys(),
|
|
]);
|
|
}
|
|
|
|
/**
|
|
* 블록 빌더 - 새 양식 생성
|
|
*/
|
|
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,
|
|
]);
|
|
}
|
|
|
|
/**
|
|
* 현재 선택된 테넌트 조회
|
|
*/
|
|
private function getCurrentTenant(): ?Tenant
|
|
{
|
|
$tenantId = session('selected_tenant_id');
|
|
|
|
return $tenantId ? Tenant::find($tenantId) : null;
|
|
}
|
|
|
|
/**
|
|
* 기본필드 키 옵션 조회 (common_codes 기반)
|
|
*/
|
|
private function getBasicFieldKeys(): array
|
|
{
|
|
return CommonCode::query()
|
|
->where('code_group', 'doc_template_basic_field')
|
|
->where('is_active', true)
|
|
->orderBy('sort_order')
|
|
->get(['code', 'name'])
|
|
->toArray();
|
|
}
|
|
|
|
/**
|
|
* 문서분류 목록 조회 (common_codes 기반 + 기존 데이터 폴백)
|
|
*/
|
|
private function getCategories(): array
|
|
{
|
|
$tenantId = session('selected_tenant_id');
|
|
|
|
// 1. common_codes에서 document_category 조회 (테넌트 우선, 없으면 글로벌)
|
|
$commonCategories = DB::table('common_codes')
|
|
->where('code_group', 'document_category')
|
|
->where('is_active', true)
|
|
->where(function ($q) use ($tenantId) {
|
|
if ($tenantId) {
|
|
$q->where('tenant_id', $tenantId)
|
|
->orWhereNull('tenant_id');
|
|
} else {
|
|
$q->whereNull('tenant_id');
|
|
}
|
|
})
|
|
->orderBy('sort_order')
|
|
->get(['code', 'name', 'tenant_id'])
|
|
->unique('code') // code 기준 중복 제거 (tenant_id 있는 것이 우선)
|
|
->map(fn ($c) => [
|
|
'code' => $c->code,
|
|
'name' => $c->name,
|
|
])
|
|
->values()
|
|
->toArray();
|
|
|
|
// 2. 기존 템플릿에서 사용 중인 카테고리 (common_codes에 없는 것)
|
|
$usedCodes = array_column($commonCategories, 'code');
|
|
$usedNames = array_column($commonCategories, 'name');
|
|
|
|
$existingCategories = DocumentTemplate::query()
|
|
->where(function ($query) use ($tenantId) {
|
|
$query->whereNull('tenant_id');
|
|
if ($tenantId) {
|
|
$query->orWhere('tenant_id', $tenantId);
|
|
}
|
|
})
|
|
->whereNotNull('category')
|
|
->where('category', '!=', '')
|
|
->whereNotIn('category', $usedCodes) // code로 사용된 것 제외
|
|
->whereNotIn('category', $usedNames) // name으로 사용된 것 제외
|
|
->distinct()
|
|
->orderBy('category')
|
|
->pluck('category')
|
|
->map(fn ($c) => ['code' => $c, 'name' => $c])
|
|
->toArray();
|
|
|
|
return array_merge($commonCategories, $existingCategories);
|
|
}
|
|
|
|
/**
|
|
* JavaScript용 템플릿 데이터 준비
|
|
*/
|
|
private function prepareTemplateData(DocumentTemplate $template): array
|
|
{
|
|
return [
|
|
'name' => $template->name,
|
|
'category' => $template->category,
|
|
'title' => $template->title,
|
|
'company_name' => $template->company_name,
|
|
'footer_remark_label' => $template->footer_remark_label,
|
|
'footer_judgement_label' => $template->footer_judgement_label,
|
|
'footer_judgement_options' => $template->footer_judgement_options,
|
|
'is_active' => $template->is_active,
|
|
'linked_item_ids' => $template->linked_item_ids,
|
|
'linked_process_id' => $template->linked_process_id,
|
|
'approval_lines' => $template->approvalLines->map(function ($l) {
|
|
$userName = null;
|
|
if ($l->user_id) {
|
|
$userName = User::where('id', $l->user_id)->value('name');
|
|
}
|
|
|
|
return [
|
|
'id' => $l->id,
|
|
'name' => $l->name,
|
|
'dept' => $l->dept,
|
|
'role' => $l->role,
|
|
'user_id' => $l->user_id,
|
|
'user_name' => $userName,
|
|
];
|
|
})->toArray(),
|
|
'basic_fields' => $template->basicFields->map(function ($f) {
|
|
return [
|
|
'id' => $f->id,
|
|
'label' => $f->label,
|
|
'field_key' => $f->field_key,
|
|
'field_type' => $f->field_type,
|
|
'default_value' => $f->default_value,
|
|
];
|
|
})->toArray(),
|
|
'sections' => $template->sections->map(function ($s) {
|
|
return [
|
|
'id' => $s->id,
|
|
'title' => $s->title,
|
|
'image_path' => $s->image_path,
|
|
'items' => $s->items->map(function ($i) {
|
|
$fv = $i->field_values ?? [];
|
|
|
|
return [
|
|
'id' => $i->id,
|
|
'category' => $fv['category'] ?? $i->category,
|
|
'item' => $fv['item'] ?? $i->item,
|
|
'standard' => $fv['standard'] ?? $i->standard,
|
|
'tolerance' => $fv['tolerance'] ?? $i->tolerance,
|
|
'standard_criteria' => $fv['standard_criteria'] ?? $i->standard_criteria,
|
|
'method' => $fv['method'] ?? $i->method,
|
|
'measurement_type' => $fv['measurement_type'] ?? $i->measurement_type,
|
|
'frequency_n' => $fv['frequency_n'] ?? $i->frequency_n,
|
|
'frequency_c' => $fv['frequency_c'] ?? $i->frequency_c,
|
|
'frequency' => $fv['frequency'] ?? $i->frequency,
|
|
'regulation' => $fv['regulation'] ?? $i->regulation,
|
|
'field_values' => $fv,
|
|
];
|
|
})->toArray(),
|
|
];
|
|
})->toArray(),
|
|
'columns' => $template->columns->map(function ($c) {
|
|
return [
|
|
'id' => $c->id,
|
|
'label' => $c->label,
|
|
'width' => $c->width,
|
|
'column_type' => $c->column_type,
|
|
'group_name' => $c->group_name,
|
|
'sub_labels' => $c->sub_labels,
|
|
];
|
|
})->toArray(),
|
|
'section_fields' => $template->sectionFields->map(function ($f) {
|
|
return [
|
|
'id' => $f->id,
|
|
'field_key' => $f->field_key,
|
|
'label' => $f->label,
|
|
'field_type' => $f->field_type,
|
|
'options' => $f->options,
|
|
'width' => $f->width,
|
|
'is_required' => $f->is_required,
|
|
];
|
|
})->toArray(),
|
|
'template_links' => $this->buildTemplateLinks($template),
|
|
];
|
|
}
|
|
|
|
/**
|
|
* template_links 데이터 빌드 (linked_item_ids 레거시 호환)
|
|
*/
|
|
private function buildTemplateLinks(DocumentTemplate $template): array
|
|
{
|
|
// 신규 template_links가 있으면 그대로 사용
|
|
if ($template->links->isNotEmpty()) {
|
|
return $template->links->map(function ($l) {
|
|
$values = $l->linkValues->map(function ($v) use ($l) {
|
|
$displayText = $this->resolveDisplayText($l->source_table, $v->linkable_id, $l->display_fields);
|
|
|
|
return [
|
|
'id' => $v->id,
|
|
'linkable_id' => $v->linkable_id,
|
|
'display_text' => $displayText,
|
|
];
|
|
})->toArray();
|
|
|
|
return [
|
|
'id' => $l->id,
|
|
'link_key' => $l->link_key,
|
|
'label' => $l->label,
|
|
'link_type' => $l->link_type,
|
|
'source_table' => $l->source_table,
|
|
'search_params' => $l->search_params,
|
|
'display_fields' => $l->display_fields,
|
|
'is_required' => $l->is_required,
|
|
'values' => $values,
|
|
];
|
|
})->toArray();
|
|
}
|
|
|
|
// 레거시: linked_item_ids만 있고 template_links가 없는 경우 가상 엔트리 생성
|
|
$linkedItemIds = $template->linked_item_ids;
|
|
if (! empty($linkedItemIds) && is_array($linkedItemIds)) {
|
|
$displayFields = ['title' => 'name', 'subtitle' => 'code'];
|
|
$values = collect($linkedItemIds)->map(function ($itemId) use ($displayFields) {
|
|
return [
|
|
'id' => $itemId,
|
|
'linkable_id' => $itemId,
|
|
'display_text' => $this->resolveDisplayText('items', $itemId, $displayFields),
|
|
];
|
|
})->toArray();
|
|
|
|
return [[
|
|
'id' => 'legacy',
|
|
'link_key' => 'items',
|
|
'label' => '연결 품목',
|
|
'link_type' => 'multiple',
|
|
'source_table' => 'items',
|
|
'search_params' => null,
|
|
'display_fields' => $displayFields,
|
|
'is_required' => false,
|
|
'values' => $values,
|
|
]];
|
|
}
|
|
|
|
return [];
|
|
}
|
|
|
|
/**
|
|
* 소스 테이블에서 레코드의 표시 텍스트 조회
|
|
*/
|
|
private function resolveDisplayText(?string $sourceTable, int $linkableId, ?array $displayFields): string
|
|
{
|
|
if (! $sourceTable || ! $linkableId) {
|
|
return "ID: {$linkableId}";
|
|
}
|
|
|
|
$titleField = $displayFields['title'] ?? 'name';
|
|
$subtitleField = $displayFields['subtitle'] ?? null;
|
|
|
|
// 모델 매핑
|
|
$modelMap = [
|
|
'items' => \App\Models\Items\Item::class,
|
|
'processes' => \App\Models\Process::class,
|
|
'users' => \App\Models\User::class,
|
|
];
|
|
|
|
try {
|
|
if (isset($modelMap[$sourceTable])) {
|
|
$record = $modelMap[$sourceTable]::find($linkableId);
|
|
} else {
|
|
$record = DB::table($sourceTable)->find($linkableId);
|
|
}
|
|
|
|
if (! $record) {
|
|
return "ID: {$linkableId}";
|
|
}
|
|
|
|
$title = is_object($record) ? ($record->$titleField ?? '') : ($record->$titleField ?? '');
|
|
$subtitle = $subtitleField ? (is_object($record) ? ($record->$subtitleField ?? '') : ($record->$subtitleField ?? '')) : '';
|
|
|
|
return $title.($subtitle ? " ({$subtitle})" : '');
|
|
} catch (\Exception $e) {
|
|
return "ID: {$linkableId}";
|
|
}
|
|
}
|
|
}
|