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
-
-
-
-
-
+
+
+
+
+
-
-
-
-
-
-
@@ -201,6 +196,31 @@ class="w-full px-3 py-1.5 border border-gray-300 rounded-lg text-sm focus:outlin
+
+
+
+
+
+
검사 기준서에 포함될 필드를 설정합니다.
+
+
+
+
+
+
+
+
+
+
+
+
검사 기준서 섹션