feat:검사 기준서 동적화 + 소스 테이블 통합 검색

- 동적 필드/연결 모델 추가 (SectionField, Link, LinkValue, Preset)
- 통합 검색 API (SourceTableSearchController) - items/processes/lots/users
- 템플릿 편집 UI: 소스 테이블 드롭다운 + datalist 검색/선택
- 문서 작성/인쇄/상세 뷰: getFieldValue() 기반 동적 렌더링
- DocumentTemplateApiController: source_table 기반 저장/복제
- DocumentController: sectionFields/links eager loading 추가

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
2026-02-04 08:38:00 +09:00
parent fff3682ff7
commit cb097ad523
16 changed files with 1719 additions and 462 deletions

View File

@@ -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(),
]);
}
}
}
}
}
}
}