diff --git a/app/Http/Controllers/Api/Admin/DocumentTemplateApiController.php b/app/Http/Controllers/Api/Admin/DocumentTemplateApiController.php index a27e3d6a..e271d4d9 100644 --- a/app/Http/Controllers/Api/Admin/DocumentTemplateApiController.php +++ b/app/Http/Controllers/Api/Admin/DocumentTemplateApiController.php @@ -7,7 +7,10 @@ use App\Models\DocumentTemplateApprovalLine; use App\Models\DocumentTemplateBasicField; use App\Models\DocumentTemplateColumn; +use App\Models\DocumentTemplateLink; +use App\Models\DocumentTemplateLinkValue; use App\Models\DocumentTemplateSection; +use App\Models\DocumentTemplateSectionField; use App\Models\DocumentTemplateSectionItem; use Illuminate\Http\JsonResponse; use Illuminate\Http\Request; @@ -71,6 +74,8 @@ public function show(int $id): JsonResponse 'basicFields', 'sections.items', 'columns', + 'sectionFields', + 'links.linkValues', ])->findOrFail($id); return response()->json([ @@ -103,6 +108,8 @@ public function store(Request $request): JsonResponse 'basic_fields' => 'nullable|array', 'sections' => 'nullable|array', 'columns' => 'nullable|array', + 'section_fields' => 'nullable|array', + 'template_links' => 'nullable|array', ]); try { @@ -132,7 +139,7 @@ public function store(Request $request): JsonResponse return response()->json([ 'success' => true, 'message' => '문서양식이 생성되었습니다.', - 'data' => $template->load(['approvalLines', 'basicFields', 'sections.items', 'columns']), + 'data' => $template->load(['approvalLines', 'basicFields', 'sections.items', 'columns', 'sectionFields', 'links.linkValues']), ]); } catch (\Exception $e) { DB::rollBack(); @@ -170,6 +177,8 @@ public function update(Request $request, int $id): JsonResponse 'basic_fields' => 'nullable|array', 'sections' => 'nullable|array', 'columns' => 'nullable|array', + 'section_fields' => 'nullable|array', + 'template_links' => 'nullable|array', ]); try { @@ -198,7 +207,7 @@ public function update(Request $request, int $id): JsonResponse return response()->json([ 'success' => true, 'message' => '문서양식이 수정되었습니다.', - 'data' => $template->fresh(['approvalLines', 'basicFields', 'sections.items', 'columns']), + 'data' => $template->fresh(['approvalLines', 'basicFields', 'sections.items', 'columns', 'sectionFields', 'links.linkValues']), ]); } catch (\Exception $e) { DB::rollBack(); @@ -262,6 +271,8 @@ public function forceDestroy(int $id): JsonResponse $section->delete(); }); $template->columns()->delete(); + $template->sectionFields()->delete(); + $template->links()->delete(); // cascade로 linkValues도 삭제 $template->forceDelete(); return response()->json([ @@ -321,6 +332,8 @@ public function duplicate(Request $request, int $id): JsonResponse 'basicFields', 'sections.items', 'columns', + 'sectionFields', + 'links.linkValues', ])->findOrFail($id); $newName = $request->input('name', $source->name.' (복사)'); @@ -404,6 +417,45 @@ public function duplicate(Request $request, int $id): JsonResponse ]); } + // 검사 기준서 동적 필드 복제 + foreach ($source->sectionFields as $field) { + DocumentTemplateSectionField::create([ + 'template_id' => $newTemplate->id, + 'field_key' => $field->field_key, + 'label' => $field->label, + 'field_type' => $field->field_type, + 'options' => $field->options, + 'width' => $field->width, + 'is_required' => $field->is_required, + 'sort_order' => $field->sort_order, + ]); + } + + // 외부 키 매핑 복제 + foreach ($source->links as $link) { + $newLink = DocumentTemplateLink::create([ + 'template_id' => $newTemplate->id, + 'link_key' => $link->link_key, + 'label' => $link->label, + 'link_type' => $link->link_type, + 'source_table' => $link->source_table, + 'search_params' => $link->search_params, + 'display_fields' => $link->display_fields, + 'is_required' => $link->is_required, + 'sort_order' => $link->sort_order, + ]); + + foreach ($link->linkValues as $value) { + DocumentTemplateLinkValue::create([ + 'template_id' => $newTemplate->id, + 'link_id' => $newLink->id, + 'linkable_id' => $value->linkable_id, + 'sort_order' => $value->sort_order, + 'created_at' => now(), + ]); + } + } + DB::commit(); return response()->json([ @@ -476,6 +528,9 @@ private function saveRelations(DocumentTemplate $template, array $data, bool $de // sections는 cascade로 items도 함께 삭제됨 $template->sections()->delete(); $template->columns()->delete(); + $template->sectionFields()->delete(); + // links는 cascade로 linkValues도 함께 삭제됨 + $template->links()->delete(); } // 결재라인 @@ -551,5 +606,51 @@ private function saveRelations(DocumentTemplate $template, array $data, bool $de ]); } } + + // 검사 기준서 동적 필드 정의 + if (! empty($data['section_fields'])) { + foreach ($data['section_fields'] as $index => $field) { + DocumentTemplateSectionField::create([ + 'template_id' => $template->id, + 'field_key' => $field['field_key'] ?? '', + 'label' => $field['label'] ?? '', + 'field_type' => $field['field_type'] ?? 'text', + 'options' => $field['options'] ?? null, + 'width' => $field['width'] ?? '100px', + 'is_required' => $field['is_required'] ?? false, + 'sort_order' => $index, + ]); + } + } + + // 외부 키 매핑 + 연결 값 + if (! empty($data['template_links'])) { + foreach ($data['template_links'] as $index => $link) { + $newLink = DocumentTemplateLink::create([ + 'template_id' => $template->id, + 'link_key' => $link['link_key'] ?? '', + 'label' => $link['label'] ?? '', + 'link_type' => $link['link_type'] ?? 'single', + 'source_table' => $link['source_table'] ?? '', + 'search_params' => $link['search_params'] ?? null, + 'display_fields' => $link['display_fields'] ?? null, + 'is_required' => $link['is_required'] ?? false, + 'sort_order' => $index, + ]); + + // 연결 값 저장 + if (! empty($link['values'])) { + foreach ($link['values'] as $vIndex => $value) { + DocumentTemplateLinkValue::create([ + 'template_id' => $template->id, + 'link_id' => $newLink->id, + 'linkable_id' => $value['linkable_id'] ?? $value['id'] ?? $value, + 'sort_order' => $vIndex, + 'created_at' => now(), + ]); + } + } + } + } } -} \ No newline at end of file +} diff --git a/app/Http/Controllers/Api/Admin/SourceTableSearchController.php b/app/Http/Controllers/Api/Admin/SourceTableSearchController.php new file mode 100644 index 00000000..1e455524 --- /dev/null +++ b/app/Http/Controllers/Api/Admin/SourceTableSearchController.php @@ -0,0 +1,171 @@ + [ + 'model' => \App\Models\Items\Item::class, + 'search_columns' => ['name', 'code'], + 'select' => ['id', 'code', 'name', 'item_type', 'unit'], + 'title_field' => 'name', + 'subtitle_field' => 'code', + 'has_tenant' => true, + 'has_active' => true, + 'order_by' => 'name', + ], + 'processes' => [ + 'model' => \App\Models\Process::class, + 'search_columns' => ['process_name', 'process_code'], + 'select' => ['id', 'process_code', 'process_name'], + 'title_field' => 'process_name', + 'subtitle_field' => 'process_code', + 'has_tenant' => true, + 'has_active' => true, + 'order_by' => 'process_name', + ], + 'lots' => [ + 'table' => 'lots', + 'search_columns' => ['lot_number', 'specification'], + 'select' => ['id', 'lot_number', 'item_id', 'specification'], + 'title_field' => 'lot_number', + 'subtitle_field' => 'specification', + 'has_tenant' => true, + 'has_active' => false, + 'order_by' => 'lot_number', + ], + 'users' => [ + 'model' => \App\Models\User::class, + 'search_columns' => ['name', 'email'], + 'select' => ['id', 'name', 'email'], + 'title_field' => 'name', + 'subtitle_field' => 'email', + 'has_tenant' => false, + 'has_active' => false, + 'order_by' => 'name', + ], + ]; + } + + /** + * 소스 테이블 통합 검색 + * GET /api/admin/source-tables/{table}/search?q=xxx&item_type=RM,SM&ids=1,2,3 + */ + public function search(Request $request, string $table): JsonResponse + { + $config = $this->getTableConfig()[$table] ?? null; + + if (! $config) { + return response()->json([ + 'success' => false, + 'message' => "지원하지 않는 테이블: {$table}", + ], 400); + } + + $tenantId = session('selected_tenant_id'); + $query = $request->input('q', ''); + + // Eloquent 모델 또는 DB 쿼리빌더 + if (isset($config['model'])) { + $builder = $config['model']::query(); + } else { + $builder = DB::table($config['table'])->whereNull('deleted_at'); + } + + // 테넌트 필터 + if ($config['has_tenant'] && $tenantId) { + $builder->where('tenant_id', $tenantId); + } + + // 활성 필터 + if ($config['has_active']) { + $builder->where('is_active', true); + } + + // 검색어 필터 + if ($query) { + $builder->where(function ($q) use ($query, $config) { + foreach ($config['search_columns'] as $col) { + $q->orWhere($col, 'like', "%{$query}%"); + } + }); + } + + // 추가 파라미터 필터 (item_type 등) + if ($request->has('item_type')) { + $builder->whereIn('item_type', explode(',', $request->input('item_type'))); + } + + // ID 목록으로 조회 + if ($request->has('ids')) { + $builder->whereIn('id', explode(',', $request->input('ids'))); + } + + $results = $builder + ->orderBy($config['order_by']) + ->limit(30) + ->get($config['select']); + + return response()->json([ + 'success' => true, + 'data' => $results, + 'meta' => [ + 'title_field' => $config['title_field'], + 'subtitle_field' => $config['subtitle_field'], + ], + ]); + } + + /** + * 사용 가능한 소스 테이블 목록 + * GET /api/admin/source-tables + */ + public function tables(): JsonResponse + { + $config = $this->getTableConfig(); + + $tables = collect($config)->map(function ($cfg, $key) { + return [ + 'key' => $key, + 'title_field' => $cfg['title_field'], + 'subtitle_field' => $cfg['subtitle_field'], + ]; + })->values(); + + // system_field_definitions에서 라벨 가져오기 + $labels = DB::table('system_field_definitions') + ->select('source_table', 'source_table_label') + ->groupBy('source_table', 'source_table_label') + ->pluck('source_table_label', 'source_table') + ->toArray(); + + $tables = $tables->map(function ($t) use ($labels) { + $t['label'] = $labels[$t['key']] ?? ucfirst($t['key']); + + return $t; + }); + + return response()->json([ + 'success' => true, + 'data' => $tables, + ]); + } +} diff --git a/app/Http/Controllers/DocumentController.php b/app/Http/Controllers/DocumentController.php index 8afe8f5a..33cebf7a 100644 --- a/app/Http/Controllers/DocumentController.php +++ b/app/Http/Controllers/DocumentController.php @@ -55,7 +55,7 @@ public function create(Request $request): View|Response // 선택된 템플릿 $template = $templateId - ? DocumentTemplate::with(['approvalLines', 'basicFields', 'sections.items', 'columns'])->find($templateId) + ? DocumentTemplate::with(['approvalLines', 'basicFields', 'sections.items', 'columns', 'sectionFields', 'links.linkValues'])->find($templateId) : null; return view('documents.edit', [ @@ -82,6 +82,8 @@ public function edit(int $id): View|Response 'template.basicFields', 'template.sections.items', 'template.columns', + 'template.sectionFields', + 'template.links.linkValues', 'approvals.user', 'data', 'attachments.file', @@ -115,6 +117,8 @@ public function print(int $id): View 'template.basicFields', 'template.sections.items', 'template.columns', + 'template.sectionFields', + 'template.links.linkValues', 'approvals.user', 'data', 'creator', @@ -137,6 +141,8 @@ public function show(int $id): View 'template.basicFields', 'template.sections.items', 'template.columns', + 'template.sectionFields', + 'template.links.linkValues', 'approvals.user', 'data', 'attachments.file', diff --git a/app/Http/Controllers/DocumentTemplateController.php b/app/Http/Controllers/DocumentTemplateController.php index ec73da7e..91ab5999 100644 --- a/app/Http/Controllers/DocumentTemplateController.php +++ b/app/Http/Controllers/DocumentTemplateController.php @@ -3,9 +3,11 @@ namespace App\Http\Controllers; use App\Models\DocumentTemplate; +use App\Models\DocumentTemplateFieldPreset; use App\Models\Tenants\Tenant; use App\Models\User; use Illuminate\Http\Request; +use Illuminate\Support\Facades\DB; use Illuminate\View\View; class DocumentTemplateController extends Controller @@ -31,6 +33,7 @@ public function create(): View 'isCreate' => true, 'categories' => $this->getCategories(), 'tenant' => $this->getCurrentTenant(), + 'presets' => DocumentTemplateFieldPreset::orderBy('sort_order')->get(), ]); } @@ -44,6 +47,8 @@ public function edit(int $id): View 'basicFields', 'sections.items', 'columns', + 'sectionFields', + 'links.linkValues', ])->findOrFail($id); // JavaScript용 데이터 변환 @@ -55,6 +60,7 @@ public function edit(int $id): View 'isCreate' => false, 'categories' => $this->getCategories(), 'tenant' => $this->getCurrentTenant(), + 'presets' => DocumentTemplateFieldPreset::orderBy('sort_order')->get(), ]); } @@ -162,6 +168,79 @@ private function prepareTemplateData(DocumentTemplate $template): array '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' => $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(), ]; } -} \ No newline at end of file + + /** + * 소스 테이블에서 레코드의 표시 텍스트 조회 + */ + 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}"; + } + } +} diff --git a/app/Models/DocumentTemplate.php b/app/Models/DocumentTemplate.php index 77159b78..a4e517ac 100644 --- a/app/Models/DocumentTemplate.php +++ b/app/Models/DocumentTemplate.php @@ -69,4 +69,22 @@ public function columns(): HasMany return $this->hasMany(DocumentTemplateColumn::class, 'template_id') ->orderBy('sort_order'); } -} \ No newline at end of file + + /** + * 검사 기준서 동적 필드 정의 + */ + public function sectionFields(): HasMany + { + return $this->hasMany(DocumentTemplateSectionField::class, 'template_id') + ->orderBy('sort_order'); + } + + /** + * 외부 키 매핑 정의 + */ + public function links(): HasMany + { + return $this->hasMany(DocumentTemplateLink::class, 'template_id') + ->orderBy('sort_order'); + } +} diff --git a/app/Models/DocumentTemplateFieldPreset.php b/app/Models/DocumentTemplateFieldPreset.php new file mode 100644 index 00000000..25a95dc1 --- /dev/null +++ b/app/Models/DocumentTemplateFieldPreset.php @@ -0,0 +1,24 @@ + 'array', + 'links' => 'array', + 'sort_order' => 'integer', + ]; +} diff --git a/app/Models/DocumentTemplateLink.php b/app/Models/DocumentTemplateLink.php new file mode 100644 index 00000000..6d4b84b6 --- /dev/null +++ b/app/Models/DocumentTemplateLink.php @@ -0,0 +1,42 @@ + 'array', + 'display_fields' => 'array', + 'is_required' => 'boolean', + 'sort_order' => 'integer', + ]; + + public function template(): BelongsTo + { + return $this->belongsTo(DocumentTemplate::class, 'template_id'); + } + + public function linkValues(): HasMany + { + return $this->hasMany(DocumentTemplateLinkValue::class, 'link_id') + ->orderBy('sort_order'); + } +} diff --git a/app/Models/DocumentTemplateLinkValue.php b/app/Models/DocumentTemplateLinkValue.php new file mode 100644 index 00000000..6e8729ec --- /dev/null +++ b/app/Models/DocumentTemplateLinkValue.php @@ -0,0 +1,35 @@ + 'integer', + ]; + + public function template(): BelongsTo + { + return $this->belongsTo(DocumentTemplate::class, 'template_id'); + } + + public function link(): BelongsTo + { + return $this->belongsTo(DocumentTemplateLink::class, 'link_id'); + } +} diff --git a/app/Models/DocumentTemplateSectionField.php b/app/Models/DocumentTemplateSectionField.php new file mode 100644 index 00000000..48ee4c98 --- /dev/null +++ b/app/Models/DocumentTemplateSectionField.php @@ -0,0 +1,33 @@ + 'array', + 'is_required' => 'boolean', + 'sort_order' => 'integer', + ]; + + public function template(): BelongsTo + { + return $this->belongsTo(DocumentTemplate::class, 'template_id'); + } +} diff --git a/app/Models/DocumentTemplateSectionItem.php b/app/Models/DocumentTemplateSectionItem.php index 2dd61092..f2bb4413 100644 --- a/app/Models/DocumentTemplateSectionItem.php +++ b/app/Models/DocumentTemplateSectionItem.php @@ -23,18 +23,33 @@ class DocumentTemplateSectionItem extends Model 'frequency_c', 'frequency', 'regulation', + 'field_values', 'sort_order', ]; protected $casts = [ + 'tolerance' => 'array', 'standard_criteria' => 'array', + 'field_values' => 'array', 'sort_order' => 'integer', 'frequency_n' => 'integer', 'frequency_c' => 'integer', ]; + /** + * field_values 우선, 없으면 기존 컬럼 fallback + */ + public function getFieldValue(string $key): mixed + { + if (! empty($this->field_values) && array_key_exists($key, $this->field_values)) { + return $this->field_values[$key]; + } + + return $this->attributes[$key] ?? null; + } + public function section(): BelongsTo { return $this->belongsTo(DocumentTemplateSection::class, 'section_id'); } -} \ No newline at end of file +} diff --git a/app/Models/Items/Item.php b/app/Models/Items/Item.php new file mode 100644 index 00000000..8d352cd0 --- /dev/null +++ b/app/Models/Items/Item.php @@ -0,0 +1,25 @@ + 'boolean', + ]; +} diff --git a/resources/views/document-templates/edit.blade.php b/resources/views/document-templates/edit.blade.php index 70a8f9c9..c062dc48 100644 --- a/resources/views/document-templates/edit.blade.php +++ b/resources/views/document-templates/edit.blade.php @@ -113,26 +113,21 @@ class="w-full px-3 py-1.5 border border-gray-300 rounded-lg text-sm focus:outlin - -