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:
@@ -7,7 +7,10 @@
|
|||||||
use App\Models\DocumentTemplateApprovalLine;
|
use App\Models\DocumentTemplateApprovalLine;
|
||||||
use App\Models\DocumentTemplateBasicField;
|
use App\Models\DocumentTemplateBasicField;
|
||||||
use App\Models\DocumentTemplateColumn;
|
use App\Models\DocumentTemplateColumn;
|
||||||
|
use App\Models\DocumentTemplateLink;
|
||||||
|
use App\Models\DocumentTemplateLinkValue;
|
||||||
use App\Models\DocumentTemplateSection;
|
use App\Models\DocumentTemplateSection;
|
||||||
|
use App\Models\DocumentTemplateSectionField;
|
||||||
use App\Models\DocumentTemplateSectionItem;
|
use App\Models\DocumentTemplateSectionItem;
|
||||||
use Illuminate\Http\JsonResponse;
|
use Illuminate\Http\JsonResponse;
|
||||||
use Illuminate\Http\Request;
|
use Illuminate\Http\Request;
|
||||||
@@ -71,6 +74,8 @@ public function show(int $id): JsonResponse
|
|||||||
'basicFields',
|
'basicFields',
|
||||||
'sections.items',
|
'sections.items',
|
||||||
'columns',
|
'columns',
|
||||||
|
'sectionFields',
|
||||||
|
'links.linkValues',
|
||||||
])->findOrFail($id);
|
])->findOrFail($id);
|
||||||
|
|
||||||
return response()->json([
|
return response()->json([
|
||||||
@@ -103,6 +108,8 @@ public function store(Request $request): JsonResponse
|
|||||||
'basic_fields' => 'nullable|array',
|
'basic_fields' => 'nullable|array',
|
||||||
'sections' => 'nullable|array',
|
'sections' => 'nullable|array',
|
||||||
'columns' => 'nullable|array',
|
'columns' => 'nullable|array',
|
||||||
|
'section_fields' => 'nullable|array',
|
||||||
|
'template_links' => 'nullable|array',
|
||||||
]);
|
]);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@@ -132,7 +139,7 @@ public function store(Request $request): JsonResponse
|
|||||||
return response()->json([
|
return response()->json([
|
||||||
'success' => true,
|
'success' => true,
|
||||||
'message' => '문서양식이 생성되었습니다.',
|
'message' => '문서양식이 생성되었습니다.',
|
||||||
'data' => $template->load(['approvalLines', 'basicFields', 'sections.items', 'columns']),
|
'data' => $template->load(['approvalLines', 'basicFields', 'sections.items', 'columns', 'sectionFields', 'links.linkValues']),
|
||||||
]);
|
]);
|
||||||
} catch (\Exception $e) {
|
} catch (\Exception $e) {
|
||||||
DB::rollBack();
|
DB::rollBack();
|
||||||
@@ -170,6 +177,8 @@ public function update(Request $request, int $id): JsonResponse
|
|||||||
'basic_fields' => 'nullable|array',
|
'basic_fields' => 'nullable|array',
|
||||||
'sections' => 'nullable|array',
|
'sections' => 'nullable|array',
|
||||||
'columns' => 'nullable|array',
|
'columns' => 'nullable|array',
|
||||||
|
'section_fields' => 'nullable|array',
|
||||||
|
'template_links' => 'nullable|array',
|
||||||
]);
|
]);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@@ -198,7 +207,7 @@ public function update(Request $request, int $id): JsonResponse
|
|||||||
return response()->json([
|
return response()->json([
|
||||||
'success' => true,
|
'success' => true,
|
||||||
'message' => '문서양식이 수정되었습니다.',
|
'message' => '문서양식이 수정되었습니다.',
|
||||||
'data' => $template->fresh(['approvalLines', 'basicFields', 'sections.items', 'columns']),
|
'data' => $template->fresh(['approvalLines', 'basicFields', 'sections.items', 'columns', 'sectionFields', 'links.linkValues']),
|
||||||
]);
|
]);
|
||||||
} catch (\Exception $e) {
|
} catch (\Exception $e) {
|
||||||
DB::rollBack();
|
DB::rollBack();
|
||||||
@@ -262,6 +271,8 @@ public function forceDestroy(int $id): JsonResponse
|
|||||||
$section->delete();
|
$section->delete();
|
||||||
});
|
});
|
||||||
$template->columns()->delete();
|
$template->columns()->delete();
|
||||||
|
$template->sectionFields()->delete();
|
||||||
|
$template->links()->delete(); // cascade로 linkValues도 삭제
|
||||||
$template->forceDelete();
|
$template->forceDelete();
|
||||||
|
|
||||||
return response()->json([
|
return response()->json([
|
||||||
@@ -321,6 +332,8 @@ public function duplicate(Request $request, int $id): JsonResponse
|
|||||||
'basicFields',
|
'basicFields',
|
||||||
'sections.items',
|
'sections.items',
|
||||||
'columns',
|
'columns',
|
||||||
|
'sectionFields',
|
||||||
|
'links.linkValues',
|
||||||
])->findOrFail($id);
|
])->findOrFail($id);
|
||||||
|
|
||||||
$newName = $request->input('name', $source->name.' (복사)');
|
$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();
|
DB::commit();
|
||||||
|
|
||||||
return response()->json([
|
return response()->json([
|
||||||
@@ -476,6 +528,9 @@ private function saveRelations(DocumentTemplate $template, array $data, bool $de
|
|||||||
// sections는 cascade로 items도 함께 삭제됨
|
// sections는 cascade로 items도 함께 삭제됨
|
||||||
$template->sections()->delete();
|
$template->sections()->delete();
|
||||||
$template->columns()->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(),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
171
app/Http/Controllers/Api/Admin/SourceTableSearchController.php
Normal file
171
app/Http/Controllers/Api/Admin/SourceTableSearchController.php
Normal file
@@ -0,0 +1,171 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Controllers\Api\Admin;
|
||||||
|
|
||||||
|
use App\Http\Controllers\Controller;
|
||||||
|
use Illuminate\Http\JsonResponse;
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
use Illuminate\Support\Facades\DB;
|
||||||
|
|
||||||
|
class SourceTableSearchController extends Controller
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* 소스 테이블별 검색 설정
|
||||||
|
* model: Eloquent 모델 클래스 (없으면 DB 쿼리빌더 사용)
|
||||||
|
* search_columns: 검색 대상 컬럼
|
||||||
|
* select: 반환할 컬럼
|
||||||
|
* title_field: 표시 제목 필드
|
||||||
|
* subtitle_field: 표시 부제목 필드
|
||||||
|
* has_tenant: tenant_id 필터 여부
|
||||||
|
* has_active: is_active 필터 여부
|
||||||
|
*/
|
||||||
|
private function getTableConfig(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'items' => [
|
||||||
|
'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,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -55,7 +55,7 @@ public function create(Request $request): View|Response
|
|||||||
|
|
||||||
// 선택된 템플릿
|
// 선택된 템플릿
|
||||||
$template = $templateId
|
$template = $templateId
|
||||||
? DocumentTemplate::with(['approvalLines', 'basicFields', 'sections.items', 'columns'])->find($templateId)
|
? DocumentTemplate::with(['approvalLines', 'basicFields', 'sections.items', 'columns', 'sectionFields', 'links.linkValues'])->find($templateId)
|
||||||
: null;
|
: null;
|
||||||
|
|
||||||
return view('documents.edit', [
|
return view('documents.edit', [
|
||||||
@@ -82,6 +82,8 @@ public function edit(int $id): View|Response
|
|||||||
'template.basicFields',
|
'template.basicFields',
|
||||||
'template.sections.items',
|
'template.sections.items',
|
||||||
'template.columns',
|
'template.columns',
|
||||||
|
'template.sectionFields',
|
||||||
|
'template.links.linkValues',
|
||||||
'approvals.user',
|
'approvals.user',
|
||||||
'data',
|
'data',
|
||||||
'attachments.file',
|
'attachments.file',
|
||||||
@@ -115,6 +117,8 @@ public function print(int $id): View
|
|||||||
'template.basicFields',
|
'template.basicFields',
|
||||||
'template.sections.items',
|
'template.sections.items',
|
||||||
'template.columns',
|
'template.columns',
|
||||||
|
'template.sectionFields',
|
||||||
|
'template.links.linkValues',
|
||||||
'approvals.user',
|
'approvals.user',
|
||||||
'data',
|
'data',
|
||||||
'creator',
|
'creator',
|
||||||
@@ -137,6 +141,8 @@ public function show(int $id): View
|
|||||||
'template.basicFields',
|
'template.basicFields',
|
||||||
'template.sections.items',
|
'template.sections.items',
|
||||||
'template.columns',
|
'template.columns',
|
||||||
|
'template.sectionFields',
|
||||||
|
'template.links.linkValues',
|
||||||
'approvals.user',
|
'approvals.user',
|
||||||
'data',
|
'data',
|
||||||
'attachments.file',
|
'attachments.file',
|
||||||
|
|||||||
@@ -3,9 +3,11 @@
|
|||||||
namespace App\Http\Controllers;
|
namespace App\Http\Controllers;
|
||||||
|
|
||||||
use App\Models\DocumentTemplate;
|
use App\Models\DocumentTemplate;
|
||||||
|
use App\Models\DocumentTemplateFieldPreset;
|
||||||
use App\Models\Tenants\Tenant;
|
use App\Models\Tenants\Tenant;
|
||||||
use App\Models\User;
|
use App\Models\User;
|
||||||
use Illuminate\Http\Request;
|
use Illuminate\Http\Request;
|
||||||
|
use Illuminate\Support\Facades\DB;
|
||||||
use Illuminate\View\View;
|
use Illuminate\View\View;
|
||||||
|
|
||||||
class DocumentTemplateController extends Controller
|
class DocumentTemplateController extends Controller
|
||||||
@@ -31,6 +33,7 @@ public function create(): View
|
|||||||
'isCreate' => true,
|
'isCreate' => true,
|
||||||
'categories' => $this->getCategories(),
|
'categories' => $this->getCategories(),
|
||||||
'tenant' => $this->getCurrentTenant(),
|
'tenant' => $this->getCurrentTenant(),
|
||||||
|
'presets' => DocumentTemplateFieldPreset::orderBy('sort_order')->get(),
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -44,6 +47,8 @@ public function edit(int $id): View
|
|||||||
'basicFields',
|
'basicFields',
|
||||||
'sections.items',
|
'sections.items',
|
||||||
'columns',
|
'columns',
|
||||||
|
'sectionFields',
|
||||||
|
'links.linkValues',
|
||||||
])->findOrFail($id);
|
])->findOrFail($id);
|
||||||
|
|
||||||
// JavaScript용 데이터 변환
|
// JavaScript용 데이터 변환
|
||||||
@@ -55,6 +60,7 @@ public function edit(int $id): View
|
|||||||
'isCreate' => false,
|
'isCreate' => false,
|
||||||
'categories' => $this->getCategories(),
|
'categories' => $this->getCategories(),
|
||||||
'tenant' => $this->getCurrentTenant(),
|
'tenant' => $this->getCurrentTenant(),
|
||||||
|
'presets' => DocumentTemplateFieldPreset::orderBy('sort_order')->get(),
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -162,6 +168,79 @@ private function prepareTemplateData(DocumentTemplate $template): array
|
|||||||
'sub_labels' => $c->sub_labels,
|
'sub_labels' => $c->sub_labels,
|
||||||
];
|
];
|
||||||
})->toArray(),
|
})->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(),
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
/**
|
||||||
|
* 소스 테이블에서 레코드의 표시 텍스트 조회
|
||||||
|
*/
|
||||||
|
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}";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -69,4 +69,22 @@ public function columns(): HasMany
|
|||||||
return $this->hasMany(DocumentTemplateColumn::class, 'template_id')
|
return $this->hasMany(DocumentTemplateColumn::class, 'template_id')
|
||||||
->orderBy('sort_order');
|
->orderBy('sort_order');
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
/**
|
||||||
|
* 검사 기준서 동적 필드 정의
|
||||||
|
*/
|
||||||
|
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');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
24
app/Models/DocumentTemplateFieldPreset.php
Normal file
24
app/Models/DocumentTemplateFieldPreset.php
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Models;
|
||||||
|
|
||||||
|
use Illuminate\Database\Eloquent\Model;
|
||||||
|
|
||||||
|
class DocumentTemplateFieldPreset extends Model
|
||||||
|
{
|
||||||
|
protected $table = 'document_template_field_presets';
|
||||||
|
|
||||||
|
protected $fillable = [
|
||||||
|
'name',
|
||||||
|
'category',
|
||||||
|
'fields',
|
||||||
|
'links',
|
||||||
|
'sort_order',
|
||||||
|
];
|
||||||
|
|
||||||
|
protected $casts = [
|
||||||
|
'fields' => 'array',
|
||||||
|
'links' => 'array',
|
||||||
|
'sort_order' => 'integer',
|
||||||
|
];
|
||||||
|
}
|
||||||
42
app/Models/DocumentTemplateLink.php
Normal file
42
app/Models/DocumentTemplateLink.php
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Models;
|
||||||
|
|
||||||
|
use Illuminate\Database\Eloquent\Model;
|
||||||
|
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||||
|
use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||||
|
|
||||||
|
class DocumentTemplateLink extends Model
|
||||||
|
{
|
||||||
|
protected $table = 'document_template_links';
|
||||||
|
|
||||||
|
protected $fillable = [
|
||||||
|
'template_id',
|
||||||
|
'link_key',
|
||||||
|
'label',
|
||||||
|
'link_type',
|
||||||
|
'source_table',
|
||||||
|
'search_params',
|
||||||
|
'display_fields',
|
||||||
|
'is_required',
|
||||||
|
'sort_order',
|
||||||
|
];
|
||||||
|
|
||||||
|
protected $casts = [
|
||||||
|
'search_params' => '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');
|
||||||
|
}
|
||||||
|
}
|
||||||
35
app/Models/DocumentTemplateLinkValue.php
Normal file
35
app/Models/DocumentTemplateLinkValue.php
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Models;
|
||||||
|
|
||||||
|
use Illuminate\Database\Eloquent\Model;
|
||||||
|
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||||
|
|
||||||
|
class DocumentTemplateLinkValue extends Model
|
||||||
|
{
|
||||||
|
protected $table = 'document_template_link_values';
|
||||||
|
|
||||||
|
public $timestamps = false;
|
||||||
|
|
||||||
|
protected $fillable = [
|
||||||
|
'template_id',
|
||||||
|
'link_id',
|
||||||
|
'linkable_id',
|
||||||
|
'sort_order',
|
||||||
|
'created_at',
|
||||||
|
];
|
||||||
|
|
||||||
|
protected $casts = [
|
||||||
|
'sort_order' => 'integer',
|
||||||
|
];
|
||||||
|
|
||||||
|
public function template(): BelongsTo
|
||||||
|
{
|
||||||
|
return $this->belongsTo(DocumentTemplate::class, 'template_id');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function link(): BelongsTo
|
||||||
|
{
|
||||||
|
return $this->belongsTo(DocumentTemplateLink::class, 'link_id');
|
||||||
|
}
|
||||||
|
}
|
||||||
33
app/Models/DocumentTemplateSectionField.php
Normal file
33
app/Models/DocumentTemplateSectionField.php
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Models;
|
||||||
|
|
||||||
|
use Illuminate\Database\Eloquent\Model;
|
||||||
|
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||||
|
|
||||||
|
class DocumentTemplateSectionField extends Model
|
||||||
|
{
|
||||||
|
protected $table = 'document_template_section_fields';
|
||||||
|
|
||||||
|
protected $fillable = [
|
||||||
|
'template_id',
|
||||||
|
'field_key',
|
||||||
|
'label',
|
||||||
|
'field_type',
|
||||||
|
'options',
|
||||||
|
'width',
|
||||||
|
'is_required',
|
||||||
|
'sort_order',
|
||||||
|
];
|
||||||
|
|
||||||
|
protected $casts = [
|
||||||
|
'options' => 'array',
|
||||||
|
'is_required' => 'boolean',
|
||||||
|
'sort_order' => 'integer',
|
||||||
|
];
|
||||||
|
|
||||||
|
public function template(): BelongsTo
|
||||||
|
{
|
||||||
|
return $this->belongsTo(DocumentTemplate::class, 'template_id');
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -23,18 +23,33 @@ class DocumentTemplateSectionItem extends Model
|
|||||||
'frequency_c',
|
'frequency_c',
|
||||||
'frequency',
|
'frequency',
|
||||||
'regulation',
|
'regulation',
|
||||||
|
'field_values',
|
||||||
'sort_order',
|
'sort_order',
|
||||||
];
|
];
|
||||||
|
|
||||||
protected $casts = [
|
protected $casts = [
|
||||||
|
'tolerance' => 'array',
|
||||||
'standard_criteria' => 'array',
|
'standard_criteria' => 'array',
|
||||||
|
'field_values' => 'array',
|
||||||
'sort_order' => 'integer',
|
'sort_order' => 'integer',
|
||||||
'frequency_n' => 'integer',
|
'frequency_n' => 'integer',
|
||||||
'frequency_c' => '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
|
public function section(): BelongsTo
|
||||||
{
|
{
|
||||||
return $this->belongsTo(DocumentTemplateSection::class, 'section_id');
|
return $this->belongsTo(DocumentTemplateSection::class, 'section_id');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
25
app/Models/Items/Item.php
Normal file
25
app/Models/Items/Item.php
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Models\Items;
|
||||||
|
|
||||||
|
use Illuminate\Database\Eloquent\Model;
|
||||||
|
use Illuminate\Database\Eloquent\SoftDeletes;
|
||||||
|
|
||||||
|
class Item extends Model
|
||||||
|
{
|
||||||
|
use SoftDeletes;
|
||||||
|
|
||||||
|
protected $fillable = [
|
||||||
|
'tenant_id',
|
||||||
|
'item_type',
|
||||||
|
'item_category',
|
||||||
|
'code',
|
||||||
|
'name',
|
||||||
|
'unit',
|
||||||
|
'is_active',
|
||||||
|
];
|
||||||
|
|
||||||
|
protected $casts = [
|
||||||
|
'is_active' => 'boolean',
|
||||||
|
];
|
||||||
|
}
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -97,6 +97,29 @@ class="w-full rounded-lg border-gray-300 text-sm focus:border-blue-500 focus:rin
|
|||||||
<input type="number" name="data[{{ $fieldKey }}]"
|
<input type="number" name="data[{{ $fieldKey }}]"
|
||||||
value="{{ $savedValue }}"
|
value="{{ $savedValue }}"
|
||||||
class="w-full rounded-lg border-gray-300 text-sm focus:border-blue-500 focus:ring-blue-500">
|
class="w-full rounded-lg border-gray-300 text-sm focus:border-blue-500 focus:ring-blue-500">
|
||||||
|
@elseif($field->field_type === 'item_search')
|
||||||
|
<div class="relative" x-data="itemSearch('{{ $fieldKey }}', '{{ $savedValue }}')" x-init="init()">
|
||||||
|
<input type="text" name="data[{{ $fieldKey }}]"
|
||||||
|
x-model="searchText"
|
||||||
|
@input.debounce.300ms="search()"
|
||||||
|
@focus="showResults = results.length > 0"
|
||||||
|
@click.away="showResults = false"
|
||||||
|
placeholder="품명 또는 코드로 검색"
|
||||||
|
autocomplete="off"
|
||||||
|
class="w-full rounded-lg border-gray-300 text-sm focus:border-blue-500 focus:ring-blue-500">
|
||||||
|
<div x-show="showResults" x-cloak
|
||||||
|
class="absolute z-50 w-full mt-1 bg-white border border-gray-200 rounded-lg shadow-lg max-h-60 overflow-y-auto">
|
||||||
|
<template x-for="item in results" :key="item.id">
|
||||||
|
<div @click="selectItem(item)"
|
||||||
|
class="px-3 py-2 cursor-pointer hover:bg-blue-50 flex justify-between items-center">
|
||||||
|
<span class="text-sm" x-text="item.name"></span>
|
||||||
|
<span class="text-xs text-gray-400" x-text="item.code"></span>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<div x-show="results.length === 0 && searchText.length > 0"
|
||||||
|
class="px-3 py-2 text-sm text-gray-400">검색 결과 없음</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
@else
|
@else
|
||||||
<input type="text" name="data[{{ $fieldKey }}]"
|
<input type="text" name="data[{{ $fieldKey }}]"
|
||||||
value="{{ $savedValue }}"
|
value="{{ $savedValue }}"
|
||||||
@@ -119,183 +142,514 @@ class="w-full rounded-lg border-gray-300 text-sm focus:border-blue-500 focus:rin
|
|||||||
|
|
||||||
{{-- 검사 데이터 테이블 --}}
|
{{-- 검사 데이터 테이블 --}}
|
||||||
@if($section->items->count() > 0 && $template->columns->count() > 0)
|
@if($section->items->count() > 0 && $template->columns->count() > 0)
|
||||||
|
@php
|
||||||
|
// 검사방식 코드→한글 매핑
|
||||||
|
$methodNames = [
|
||||||
|
'visual' => '육안검사',
|
||||||
|
'check' => '체크검사',
|
||||||
|
'mill_sheet' => '공급업체 밀시트',
|
||||||
|
'certified_agency' => '공인시험기관',
|
||||||
|
'substitute_cert' => '공급업체 성적서 대체',
|
||||||
|
'other' => '기타',
|
||||||
|
];
|
||||||
|
|
||||||
|
// 공차 포맷 함수
|
||||||
|
$formatTolerance = function($tol) {
|
||||||
|
if (!$tol || !is_array($tol) || !isset($tol['type'])) {
|
||||||
|
// 레거시 문자열 지원
|
||||||
|
return is_string($tol) && $tol !== '' ? $tol : '-';
|
||||||
|
}
|
||||||
|
switch ($tol['type']) {
|
||||||
|
case 'symmetric':
|
||||||
|
return isset($tol['value']) ? "±{$tol['value']}" : '-';
|
||||||
|
case 'asymmetric':
|
||||||
|
$p = $tol['plus'] ?? 0;
|
||||||
|
$m = $tol['minus'] ?? 0;
|
||||||
|
return ($p || $m) ? "+{$p} / -{$m}" : '-';
|
||||||
|
case 'range':
|
||||||
|
$min = $tol['min'] ?? '';
|
||||||
|
$max = $tol['max'] ?? '';
|
||||||
|
return ($min !== '' || $max !== '') ? "{$min} ~ {$max}" : '-';
|
||||||
|
case 'limit':
|
||||||
|
$opSymbol = ['lte' => '≤', 'lt' => '<', 'gte' => '≥', 'gt' => '>'];
|
||||||
|
$op = $opSymbol[$tol['op'] ?? 'lte'] ?? '≤';
|
||||||
|
return isset($tol['value']) ? "{$op}{$tol['value']}" : '-';
|
||||||
|
default:
|
||||||
|
return '-';
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// standard_criteria 포맷 함수 (getFieldValue 사용)
|
||||||
|
$formatStandard = function($item) use ($formatTolerance) {
|
||||||
|
$c = $item->getFieldValue('standard_criteria');
|
||||||
|
if ($c && is_array($c) && (isset($c['min']) || isset($c['max']))) {
|
||||||
|
$opLabel = ['gte' => '이상', 'gt' => '초과', 'lte' => '이하', 'lt' => '미만'];
|
||||||
|
$parts = [];
|
||||||
|
if (isset($c['min'])) $parts[] = $c['min'] . ' ' . ($opLabel[$c['min_op'] ?? 'gte'] ?? '이상');
|
||||||
|
if (isset($c['max'])) $parts[] = $c['max'] . ' ' . ($opLabel[$c['max_op'] ?? 'lte'] ?? '이하');
|
||||||
|
return implode(' ~ ', $parts);
|
||||||
|
}
|
||||||
|
$std = $item->getFieldValue('standard') ?: '-';
|
||||||
|
$tolStr = $formatTolerance($item->getFieldValue('tolerance'));
|
||||||
|
if ($tolStr !== '-') $std .= ' (' . $tolStr . ')';
|
||||||
|
return $std;
|
||||||
|
};
|
||||||
|
|
||||||
|
// 검사주기 포맷 함수 (getFieldValue 사용)
|
||||||
|
$formatFrequency = function($item) {
|
||||||
|
$parts = [];
|
||||||
|
$freqN = $item->getFieldValue('frequency_n');
|
||||||
|
$freqC = $item->getFieldValue('frequency_c');
|
||||||
|
$freq = $item->getFieldValue('frequency');
|
||||||
|
if ($freqN) {
|
||||||
|
$nc = "n={$freqN}";
|
||||||
|
if ($freqC !== null) $nc .= ", c={$freqC}";
|
||||||
|
$parts[] = $nc;
|
||||||
|
}
|
||||||
|
if ($freq) $parts[] = $freq;
|
||||||
|
return $parts ? implode(' / ', $parts) : '-';
|
||||||
|
};
|
||||||
|
|
||||||
|
// 카테고리별 그룹핑 (getFieldValue 사용)
|
||||||
|
$groupedRows = [];
|
||||||
|
$allItems = $section->items->values();
|
||||||
|
$idx = 0;
|
||||||
|
while ($idx < $allItems->count()) {
|
||||||
|
$item = $allItems[$idx];
|
||||||
|
$cat = trim($item->getFieldValue('category') ?? '');
|
||||||
|
if ($cat) {
|
||||||
|
$grouped = [$item];
|
||||||
|
while ($idx + 1 < $allItems->count() && trim($allItems[$idx + 1]->getFieldValue('category') ?? '') === $cat) {
|
||||||
|
$idx++;
|
||||||
|
$grouped[] = $allItems[$idx];
|
||||||
|
}
|
||||||
|
$groupedRows[] = ['type' => 'group', 'category' => $cat, 'items' => $grouped];
|
||||||
|
} else {
|
||||||
|
$groupedRows[] = ['type' => 'single', 'item' => $item];
|
||||||
|
}
|
||||||
|
$idx++;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 측정치 컬럼 정보
|
||||||
|
$hasComplex = $template->columns->contains(fn($c) => $c->column_type === 'complex' && $c->sub_labels);
|
||||||
|
$maxFreqN = $allItems->max(fn($i) => $i->getFieldValue('frequency_n')) ?: 0;
|
||||||
|
$complexCol = $template->columns->first(fn($c) => $c->column_type === 'complex' && $c->sub_labels);
|
||||||
|
$totalMeasCols = $complexCol ? max(count($complexCol->sub_labels), $maxFreqN) : 0;
|
||||||
|
@endphp
|
||||||
<div class="overflow-x-auto mt-4">
|
<div class="overflow-x-auto mt-4">
|
||||||
<table class="min-w-full border border-gray-300 text-sm" data-section-id="{{ $section->id }}">
|
<table class="min-w-full border border-gray-300 text-sm" data-section-id="{{ $section->id }}">
|
||||||
|
{{-- colgroup: 컬럼 너비 제어 --}}
|
||||||
|
<colgroup>
|
||||||
|
@foreach($template->columns as $col)
|
||||||
|
@php $colLabel = trim($col->label); @endphp
|
||||||
|
@if($col->column_type === 'complex' && $col->sub_labels)
|
||||||
|
@for($ci = 0; $ci < $totalMeasCols; $ci++)
|
||||||
|
<col style="width:60px">
|
||||||
|
@endfor
|
||||||
|
@elseif(str_contains(strtolower($colLabel), 'no') && strlen($colLabel) <= 4)
|
||||||
|
<col style="width:40px">
|
||||||
|
@elseif(in_array($colLabel, ['검사항목', '항목']))
|
||||||
|
<col style="width:60px">
|
||||||
|
<col style="width:100px">
|
||||||
|
@elseif(in_array($colLabel, ['검사기준', '기준']))
|
||||||
|
<col style="width:120px">
|
||||||
|
<col style="width:60px">
|
||||||
|
@elseif(str_contains($colLabel, '판정'))
|
||||||
|
<col style="width:40px">
|
||||||
|
@else
|
||||||
|
<col style="width:80px">
|
||||||
|
@endif
|
||||||
|
@endforeach
|
||||||
|
</colgroup>
|
||||||
{{-- 테이블 헤더 --}}
|
{{-- 테이블 헤더 --}}
|
||||||
<thead class="bg-gray-50">
|
<thead class="bg-gray-50">
|
||||||
<tr>
|
<tr>
|
||||||
@foreach($template->columns as $col)
|
@foreach($template->columns as $col)
|
||||||
@if($col->column_type === 'complex' && $col->sub_labels)
|
@if($col->column_type === 'complex' && $col->sub_labels)
|
||||||
<th colspan="{{ count($col->sub_labels) }}"
|
<th colspan="{{ $totalMeasCols }}"
|
||||||
class="px-2 py-2 text-center text-xs font-medium text-gray-600 uppercase border border-gray-300"
|
class="px-2 py-2 text-center text-xs font-medium text-gray-600 uppercase border border-gray-300">
|
||||||
style="min-width: {{ $col->width }}">
|
|
||||||
{{ $col->label }}
|
{{ $col->label }}
|
||||||
</th>
|
</th>
|
||||||
@else
|
@else
|
||||||
<th rowspan="{{ $template->columns->contains(fn($c) => $c->column_type === 'complex' && $c->sub_labels) ? 2 : 1 }}"
|
@php
|
||||||
class="px-2 py-2 text-center text-xs font-medium text-gray-600 uppercase border border-gray-300"
|
$colLabel = trim($col->label);
|
||||||
style="min-width: {{ $col->width }}">
|
$isItemOrStd = in_array($colLabel, ['검사항목', '항목']) || in_array($colLabel, ['검사기준', '기준']);
|
||||||
|
@endphp
|
||||||
|
<th {{ $isItemOrStd ? 'colspan=2' : '' }}
|
||||||
|
{{ $hasComplex ? 'rowspan=2' : '' }}
|
||||||
|
class="px-2 py-2 text-center text-xs font-medium text-gray-600 uppercase border border-gray-300">
|
||||||
{{ $col->label }}
|
{{ $col->label }}
|
||||||
</th>
|
</th>
|
||||||
@endif
|
@endif
|
||||||
@endforeach
|
@endforeach
|
||||||
</tr>
|
</tr>
|
||||||
{{-- 서브 라벨 행 (complex 컬럼이 있을 때만) --}}
|
@if($hasComplex)
|
||||||
@if($template->columns->contains(fn($c) => $c->column_type === 'complex' && $c->sub_labels))
|
|
||||||
<tr>
|
<tr>
|
||||||
@foreach($template->columns as $col)
|
@for($si = 1; $si <= $totalMeasCols; $si++)
|
||||||
@if($col->column_type === 'complex' && $col->sub_labels)
|
<th class="px-2 py-1 text-center text-xs font-medium text-gray-500 border border-gray-300 bg-gray-50">
|
||||||
@foreach($col->sub_labels as $subLabel)
|
n{{ $si }}
|
||||||
<th class="px-2 py-1 text-center text-xs font-medium text-gray-500 border border-gray-300 bg-gray-50">
|
</th>
|
||||||
{{ $subLabel }}
|
@endfor
|
||||||
</th>
|
|
||||||
@endforeach
|
|
||||||
@endif
|
|
||||||
@endforeach
|
|
||||||
</tr>
|
</tr>
|
||||||
@endif
|
@endif
|
||||||
</thead>
|
</thead>
|
||||||
{{-- 테이블 바디 --}}
|
{{-- 테이블 바디 --}}
|
||||||
<tbody>
|
<tbody>
|
||||||
@foreach($section->items as $rowIndex => $item)
|
@php $rowNum = 0; $globalRowIndex = 0; @endphp
|
||||||
<tr class="hover:bg-blue-50" data-row-index="{{ $rowIndex }}">
|
@foreach($groupedRows as $row)
|
||||||
@foreach($template->columns as $col)
|
@php $rowNum++; @endphp
|
||||||
@if($col->column_type === 'complex' && $col->sub_labels)
|
@if($row['type'] === 'single')
|
||||||
{{-- complex: 서브 라벨별 입력 필드 --}}
|
{{-- 단일 항목 --}}
|
||||||
@foreach($col->sub_labels as $subIndex => $subLabel)
|
@php $item = $row['item']; $rowIndex = $globalRowIndex; $globalRowIndex++; @endphp
|
||||||
@php
|
<tr class="hover:bg-blue-50" data-row-index="{{ $rowIndex }}">
|
||||||
$fieldKey = "s{$section->id}_r{$rowIndex}_c{$col->id}_sub{$subIndex}";
|
@foreach($template->columns as $col)
|
||||||
$savedVal = $document?->data->where('field_key', $fieldKey)->first()?->field_value ?? '';
|
@if($col->column_type === 'complex' && $col->sub_labels)
|
||||||
@endphp
|
{{-- 측정치: measurement_type에 따라 분기 (getFieldValue 사용) --}}
|
||||||
<td class="px-1 py-1 border border-gray-300 text-center">
|
@php
|
||||||
<input type="text"
|
$mType = $item->getFieldValue('measurement_type') ?? '';
|
||||||
name="section_data[{{ $fieldKey }}]"
|
$freqN = $item->getFieldValue('frequency_n') ?: $totalMeasCols;
|
||||||
value="{{ $savedVal }}"
|
$remainder = $totalMeasCols - $freqN;
|
||||||
data-section-id="{{ $section->id }}"
|
@endphp
|
||||||
data-column-id="{{ $col->id }}"
|
@if($mType === 'checkbox')
|
||||||
data-row-index="{{ $rowIndex }}"
|
@for($nIdx = 1; $nIdx <= $freqN; $nIdx++)
|
||||||
class="w-full text-center text-sm border-0 focus:ring-1 focus:ring-blue-400 rounded py-1"
|
@php
|
||||||
placeholder="{{ $subLabel }}">
|
$fieldKey = "s{$section->id}_r{$rowIndex}_c{$col->id}_n{$nIdx}";
|
||||||
</td>
|
$savedOK = $document?->data->where('field_key', $fieldKey . '_ok')->first()?->field_value ?? '';
|
||||||
@endforeach
|
$savedNG = $document?->data->where('field_key', $fieldKey . '_ng')->first()?->field_value ?? '';
|
||||||
@elseif($col->column_type === 'select')
|
@endphp
|
||||||
{{-- select: 판정 드롭다운 --}}
|
<td class="px-1 py-1 border border-gray-300 text-center">
|
||||||
@php
|
<label class="inline-flex items-center gap-0.5 text-xs">
|
||||||
$fieldKey = "s{$section->id}_r{$rowIndex}_c{$col->id}";
|
<input type="checkbox" name="section_data[{{ $fieldKey }}_ok]" value="OK" {{ $savedOK === 'OK' ? 'checked' : '' }}
|
||||||
$savedVal = $document?->data->where('field_key', $fieldKey)->first()?->field_value ?? '';
|
data-section-id="{{ $section->id }}" data-column-id="{{ $col->id }}" data-row-index="{{ $rowIndex }}"
|
||||||
$options = $template->footer_judgement_options ?? ['적합', '부적합'];
|
class="rounded border-gray-300 text-blue-600 focus:ring-blue-500 w-3 h-3">OK
|
||||||
@endphp
|
</label><br>
|
||||||
<td class="px-1 py-1 border border-gray-300 text-center">
|
<label class="inline-flex items-center gap-0.5 text-xs">
|
||||||
<select name="section_data[{{ $fieldKey }}]"
|
<input type="checkbox" name="section_data[{{ $fieldKey }}_ng]" value="NG" {{ $savedNG === 'NG' ? 'checked' : '' }}
|
||||||
data-section-id="{{ $section->id }}"
|
data-section-id="{{ $section->id }}" data-column-id="{{ $col->id }}" data-row-index="{{ $rowIndex }}"
|
||||||
data-column-id="{{ $col->id }}"
|
class="rounded border-gray-300 text-red-600 focus:ring-red-500 w-3 h-3">NG
|
||||||
data-row-index="{{ $rowIndex }}"
|
</label>
|
||||||
class="w-full text-center text-sm border-0 focus:ring-1 focus:ring-blue-400 rounded py-1">
|
</td>
|
||||||
<option value="">-</option>
|
@endfor
|
||||||
@foreach($options as $opt)
|
@if($remainder > 0)
|
||||||
<option value="{{ $opt }}" {{ $savedVal === $opt ? 'selected' : '' }}>{{ $opt }}</option>
|
<td class="border border-gray-300" colspan="{{ $remainder }}"></td>
|
||||||
@endforeach
|
@endif
|
||||||
</select>
|
@elseif($mType === 'numeric')
|
||||||
</td>
|
@for($nIdx = 1; $nIdx <= $freqN; $nIdx++)
|
||||||
@elseif($col->column_type === 'check')
|
|
||||||
{{-- check: 체크박스 --}}
|
|
||||||
@php
|
|
||||||
$fieldKey = "s{$section->id}_r{$rowIndex}_c{$col->id}";
|
|
||||||
$savedVal = $document?->data->where('field_key', $fieldKey)->first()?->field_value ?? '';
|
|
||||||
@endphp
|
|
||||||
<td class="px-1 py-1 border border-gray-300 text-center">
|
|
||||||
<input type="checkbox"
|
|
||||||
name="section_data[{{ $fieldKey }}]"
|
|
||||||
value="OK"
|
|
||||||
{{ $savedVal === 'OK' ? 'checked' : '' }}
|
|
||||||
data-section-id="{{ $section->id }}"
|
|
||||||
data-column-id="{{ $col->id }}"
|
|
||||||
data-row-index="{{ $rowIndex }}"
|
|
||||||
class="rounded border-gray-300 text-blue-600 focus:ring-blue-500">
|
|
||||||
</td>
|
|
||||||
@elseif($col->column_type === 'measurement')
|
|
||||||
{{-- measurement: 수치 입력 (frequency_n에 따라 다중 입력) --}}
|
|
||||||
@php
|
|
||||||
$frequencyN = $item->frequency_n ?? 1;
|
|
||||||
@endphp
|
|
||||||
<td class="px-1 py-1 border border-gray-300 text-center">
|
|
||||||
@if($frequencyN > 1)
|
|
||||||
<div class="flex gap-0.5">
|
|
||||||
@for($nIdx = 1; $nIdx <= $frequencyN; $nIdx++)
|
|
||||||
@php
|
@php
|
||||||
$fieldKey = "s{$section->id}_r{$rowIndex}_c{$col->id}_n{$nIdx}";
|
$fieldKey = "s{$section->id}_r{$rowIndex}_c{$col->id}_n{$nIdx}";
|
||||||
$savedVal = $document?->data->where('field_key', $fieldKey)->first()?->field_value ?? '';
|
$savedVal = $document?->data->where('field_key', $fieldKey)->first()?->field_value ?? '';
|
||||||
@endphp
|
@endphp
|
||||||
<input type="number" step="any"
|
<td class="px-1 py-1 border border-gray-300 text-center">
|
||||||
name="section_data[{{ $fieldKey }}]"
|
<input type="number" step="any" name="section_data[{{ $fieldKey }}]" value="{{ $savedVal }}"
|
||||||
value="{{ $savedVal }}"
|
data-section-id="{{ $section->id }}" data-column-id="{{ $col->id }}" data-row-index="{{ $rowIndex }}"
|
||||||
data-section-id="{{ $section->id }}"
|
class="w-full text-center text-sm border-0 focus:ring-1 focus:ring-blue-400 rounded py-1"
|
||||||
data-column-id="{{ $col->id }}"
|
placeholder="n{{ $nIdx }}">
|
||||||
data-row-index="{{ $rowIndex }}"
|
</td>
|
||||||
class="flex-1 min-w-0 text-center text-sm border border-gray-200 focus:ring-1 focus:ring-blue-400 rounded py-1"
|
|
||||||
placeholder="n{{ $nIdx }}" title="측정치 {{ $nIdx }}">
|
|
||||||
@endfor
|
@endfor
|
||||||
</div>
|
@if($remainder > 0)
|
||||||
@else
|
<td class="border border-gray-300" colspan="{{ $remainder }}"></td>
|
||||||
|
@endif
|
||||||
|
@elseif($mType === 'single_value')
|
||||||
|
@php
|
||||||
|
$fieldKey = "s{$section->id}_r{$rowIndex}_c{$col->id}";
|
||||||
|
$savedVal = $document?->data->where('field_key', $fieldKey)->first()?->field_value ?? '';
|
||||||
|
@endphp
|
||||||
|
<td class="px-1 py-1 border border-gray-300 text-center" colspan="{{ $totalMeasCols }}">
|
||||||
|
<input type="text" name="section_data[{{ $fieldKey }}]" value="{{ $savedVal }}"
|
||||||
|
data-section-id="{{ $section->id }}" data-column-id="{{ $col->id }}" data-row-index="{{ $rowIndex }}"
|
||||||
|
class="w-full text-center text-sm border-0 focus:ring-1 focus:ring-blue-400 rounded py-1"
|
||||||
|
placeholder="입력">
|
||||||
|
</td>
|
||||||
|
@elseif($mType === 'substitute')
|
||||||
|
@php
|
||||||
|
$fieldKey = "s{$section->id}_r{$rowIndex}_c{$col->id}";
|
||||||
|
$savedVal = $document?->data->where('field_key', $fieldKey)->first()?->field_value ?? '';
|
||||||
|
@endphp
|
||||||
|
<td class="px-1 py-1 border border-gray-300 text-center text-gray-400 text-xs" colspan="{{ $totalMeasCols }}">
|
||||||
|
<input type="text" name="section_data[{{ $fieldKey }}]" value="{{ $savedVal }}"
|
||||||
|
data-section-id="{{ $section->id }}" data-column-id="{{ $col->id }}" data-row-index="{{ $rowIndex }}"
|
||||||
|
class="w-full text-center text-sm border-0 focus:ring-1 focus:ring-blue-400 rounded py-1"
|
||||||
|
placeholder="(입력)">
|
||||||
|
</td>
|
||||||
|
@else
|
||||||
|
@for($nIdx = 1; $nIdx <= $totalMeasCols; $nIdx++)
|
||||||
|
@php
|
||||||
|
$fieldKey = "s{$section->id}_r{$rowIndex}_c{$col->id}_n{$nIdx}";
|
||||||
|
$savedVal = $document?->data->where('field_key', $fieldKey)->first()?->field_value ?? '';
|
||||||
|
@endphp
|
||||||
|
<td class="px-1 py-1 border border-gray-300 text-center">
|
||||||
|
<input type="text" name="section_data[{{ $fieldKey }}]" value="{{ $savedVal }}"
|
||||||
|
data-section-id="{{ $section->id }}" data-column-id="{{ $col->id }}" data-row-index="{{ $rowIndex }}"
|
||||||
|
class="w-full text-center text-sm border-0 focus:ring-1 focus:ring-blue-400 rounded py-1"
|
||||||
|
placeholder="n{{ $nIdx }}">
|
||||||
|
</td>
|
||||||
|
@endfor
|
||||||
|
@endif
|
||||||
|
@elseif($col->column_type === 'select')
|
||||||
|
@php
|
||||||
|
$fieldKey = "s{$section->id}_r{$rowIndex}_c{$col->id}";
|
||||||
|
$savedVal = $document?->data->where('field_key', $fieldKey)->first()?->field_value ?? '';
|
||||||
|
$options = $template->footer_judgement_options ?? ['적합', '부적합'];
|
||||||
|
@endphp
|
||||||
|
<td class="px-1 py-1 border border-gray-300 text-center">
|
||||||
|
<select name="section_data[{{ $fieldKey }}]"
|
||||||
|
data-section-id="{{ $section->id }}" data-column-id="{{ $col->id }}" data-row-index="{{ $rowIndex }}"
|
||||||
|
class="w-full text-center text-sm border-0 focus:ring-1 focus:ring-blue-400 rounded py-1">
|
||||||
|
<option value="">-</option>
|
||||||
|
@foreach($options as $opt)
|
||||||
|
<option value="{{ $opt }}" {{ $savedVal === $opt ? 'selected' : '' }}>{{ $opt }}</option>
|
||||||
|
@endforeach
|
||||||
|
</select>
|
||||||
|
</td>
|
||||||
|
@elseif($col->column_type === 'check')
|
||||||
@php
|
@php
|
||||||
$fieldKey = "s{$section->id}_r{$rowIndex}_c{$col->id}";
|
$fieldKey = "s{$section->id}_r{$rowIndex}_c{$col->id}";
|
||||||
$savedVal = $document?->data->where('field_key', $fieldKey)->first()?->field_value ?? '';
|
$savedVal = $document?->data->where('field_key', $fieldKey)->first()?->field_value ?? '';
|
||||||
@endphp
|
@endphp
|
||||||
<input type="number" step="any"
|
<td class="px-1 py-1 border border-gray-300 text-center">
|
||||||
name="section_data[{{ $fieldKey }}]"
|
<input type="checkbox" name="section_data[{{ $fieldKey }}]" value="OK" {{ $savedVal === 'OK' ? 'checked' : '' }}
|
||||||
value="{{ $savedVal }}"
|
data-section-id="{{ $section->id }}" data-column-id="{{ $col->id }}" data-row-index="{{ $rowIndex }}"
|
||||||
data-section-id="{{ $section->id }}"
|
class="rounded border-gray-300 text-blue-600 focus:ring-blue-500">
|
||||||
data-column-id="{{ $col->id }}"
|
</td>
|
||||||
data-row-index="{{ $rowIndex }}"
|
@else
|
||||||
class="w-full text-center text-sm border-0 focus:ring-1 focus:ring-blue-400 rounded py-1">
|
{{-- text: 정적 데이터 --}}
|
||||||
|
@php
|
||||||
|
$label = trim($col->label);
|
||||||
|
$isNoCol = str_contains(strtolower($label), 'no') && strlen($label) <= 4;
|
||||||
|
$isItemCol = in_array($label, ['검사항목', '항목']);
|
||||||
|
$isStdCol = in_array($label, ['검사기준', '기준']);
|
||||||
|
$isMethodCol = str_contains($label, '검사방') || in_array($label, ['방식', '검사방법']);
|
||||||
|
$isFreqCol = str_contains($label, '주기') || in_array($label, ['검사주기']);
|
||||||
|
$isJudgeCol = str_contains($label, '판정');
|
||||||
|
@endphp
|
||||||
|
@if($isNoCol)
|
||||||
|
<td class="px-2 py-2 border border-gray-300 text-sm text-center">{{ $rowNum }}</td>
|
||||||
|
@elseif($isItemCol)
|
||||||
|
<td class="px-2 py-2 border border-gray-300 text-sm text-center" colspan="2">{{ $item->getFieldValue('item') ?: '-' }}</td>
|
||||||
|
@elseif($isStdCol)
|
||||||
|
<td class="px-2 py-2 border border-gray-300 text-sm text-center" colspan="2">{{ $formatStandard($item) }}</td>
|
||||||
|
@elseif($isMethodCol)
|
||||||
|
@php $methodVal = $item->getFieldValue('method'); @endphp
|
||||||
|
<td class="px-2 py-2 border border-gray-300 text-sm text-center">{{ $methodNames[$methodVal] ?? ($methodVal ?: '-') }}</td>
|
||||||
|
@elseif($isFreqCol)
|
||||||
|
<td class="px-2 py-2 border border-gray-300 text-sm text-center">{{ $formatFrequency($item) }}</td>
|
||||||
|
@elseif($isJudgeCol)
|
||||||
|
@php
|
||||||
|
$fieldKey = "s{$section->id}_r{$rowIndex}_c{$col->id}";
|
||||||
|
$savedVal = $document?->data->where('field_key', $fieldKey)->first()?->field_value ?? '';
|
||||||
|
@endphp
|
||||||
|
<td class="px-1 py-1 border border-gray-300 text-center">
|
||||||
|
<input type="checkbox" name="section_data[{{ $fieldKey }}]" value="OK" {{ $savedVal === 'OK' ? 'checked' : '' }}
|
||||||
|
data-section-id="{{ $section->id }}" data-column-id="{{ $col->id }}" data-row-index="{{ $rowIndex }}"
|
||||||
|
class="rounded border-gray-300 text-blue-600 focus:ring-blue-500">
|
||||||
|
</td>
|
||||||
|
@else
|
||||||
|
@php
|
||||||
|
$staticValue = match(true) {
|
||||||
|
in_array($label, ['규격', '적용규격', '관련규정']) => $item->getFieldValue('regulation'),
|
||||||
|
in_array($label, ['분류', '카테고리']) => $item->getFieldValue('category'),
|
||||||
|
default => null,
|
||||||
|
};
|
||||||
|
@endphp
|
||||||
|
@if($staticValue !== null)
|
||||||
|
<td class="px-2 py-2 border border-gray-300 text-sm">{{ $staticValue }}</td>
|
||||||
|
@else
|
||||||
|
@php
|
||||||
|
$fieldKey = "s{$section->id}_r{$rowIndex}_c{$col->id}";
|
||||||
|
$savedVal = $document?->data->where('field_key', $fieldKey)->first()?->field_value ?? '';
|
||||||
|
@endphp
|
||||||
|
<td class="px-1 py-1 border border-gray-300 text-center">
|
||||||
|
<input type="text" name="section_data[{{ $fieldKey }}]" value="{{ $savedVal }}"
|
||||||
|
data-section-id="{{ $section->id }}" data-column-id="{{ $col->id }}" data-row-index="{{ $rowIndex }}"
|
||||||
|
class="w-full text-center text-sm border-0 focus:ring-1 focus:ring-blue-400 rounded py-1">
|
||||||
|
</td>
|
||||||
|
@endif
|
||||||
|
@endif
|
||||||
@endif
|
@endif
|
||||||
</td>
|
@endforeach
|
||||||
@else
|
</tr>
|
||||||
{{-- text: 정적 데이터 (항목정보) 또는 텍스트 입력 --}}
|
@else
|
||||||
@php
|
{{-- 그룹 항목 --}}
|
||||||
// 정적 컬럼 매핑: NO, 검사항목, 검사기준, 검사방식, 검사주기
|
@php $groupItems = $row['items']; $groupCount = count($groupItems); @endphp
|
||||||
$staticValue = match(true) {
|
@foreach($groupItems as $itemIdx => $item)
|
||||||
str_contains(strtolower($col->label), 'no') && strlen($col->label) <= 4 => $rowIndex + 1,
|
@php $rowIndex = $globalRowIndex; $globalRowIndex++; @endphp
|
||||||
in_array($col->label, ['검사항목', '항목']) => $item->item,
|
<tr class="hover:bg-blue-50" data-row-index="{{ $rowIndex }}">
|
||||||
in_array($col->label, ['검사기준', '기준']) => $item->standard,
|
@foreach($template->columns as $col)
|
||||||
in_array($col->label, ['검사방식', '방식', '검사방법']) => $item->method,
|
@if($col->column_type === 'complex' && $col->sub_labels)
|
||||||
in_array($col->label, ['검사주기', '주기']) => (function() use ($item) {
|
{{-- 측정치: 각 행 개별 렌더링 (getFieldValue 사용) --}}
|
||||||
$parts = [];
|
@php
|
||||||
if ($item->frequency_n) {
|
$mType = $item->getFieldValue('measurement_type') ?? '';
|
||||||
$nc = "n={$item->frequency_n}";
|
$freqN = $item->getFieldValue('frequency_n') ?: $totalMeasCols;
|
||||||
if ($item->frequency_c !== null) $nc .= ", c={$item->frequency_c}";
|
$remainder = $totalMeasCols - $freqN;
|
||||||
$parts[] = $nc;
|
@endphp
|
||||||
}
|
@if($mType === 'checkbox')
|
||||||
if ($item->frequency) $parts[] = $item->frequency;
|
@for($nIdx = 1; $nIdx <= $freqN; $nIdx++)
|
||||||
return $parts ? implode(' / ', $parts) : '-';
|
@php
|
||||||
})(),
|
$fieldKey = "s{$section->id}_r{$rowIndex}_c{$col->id}_n{$nIdx}";
|
||||||
in_array($col->label, ['규격', '적용규격', '관련규정']) => $item->regulation,
|
$savedOK = $document?->data->where('field_key', $fieldKey . '_ok')->first()?->field_value ?? '';
|
||||||
in_array($col->label, ['분류', '카테고리']) => $item->category,
|
$savedNG = $document?->data->where('field_key', $fieldKey . '_ng')->first()?->field_value ?? '';
|
||||||
default => null,
|
@endphp
|
||||||
};
|
<td class="px-1 py-1 border border-gray-300 text-center">
|
||||||
@endphp
|
<label class="inline-flex items-center gap-0.5 text-xs">
|
||||||
@if($staticValue !== null)
|
<input type="checkbox" name="section_data[{{ $fieldKey }}_ok]" value="OK" {{ $savedOK === 'OK' ? 'checked' : '' }}
|
||||||
<td class="px-2 py-2 border border-gray-300 text-sm text-gray-700 {{ is_numeric($staticValue) ? 'text-center' : '' }}">
|
data-section-id="{{ $section->id }}" data-column-id="{{ $col->id }}" data-row-index="{{ $rowIndex }}"
|
||||||
{{ $staticValue }}
|
class="rounded border-gray-300 text-blue-600 focus:ring-blue-500 w-3 h-3">OK
|
||||||
</td>
|
</label><br>
|
||||||
@else
|
<label class="inline-flex items-center gap-0.5 text-xs">
|
||||||
@php
|
<input type="checkbox" name="section_data[{{ $fieldKey }}_ng]" value="NG" {{ $savedNG === 'NG' ? 'checked' : '' }}
|
||||||
$fieldKey = "s{$section->id}_r{$rowIndex}_c{$col->id}";
|
data-section-id="{{ $section->id }}" data-column-id="{{ $col->id }}" data-row-index="{{ $rowIndex }}"
|
||||||
$savedVal = $document?->data->where('field_key', $fieldKey)->first()?->field_value ?? '';
|
class="rounded border-gray-300 text-red-600 focus:ring-red-500 w-3 h-3">NG
|
||||||
@endphp
|
</label>
|
||||||
<td class="px-1 py-1 border border-gray-300 text-center">
|
</td>
|
||||||
<input type="text"
|
@endfor
|
||||||
name="section_data[{{ $fieldKey }}]"
|
@if($remainder > 0)
|
||||||
value="{{ $savedVal }}"
|
<td class="border border-gray-300" colspan="{{ $remainder }}"></td>
|
||||||
data-section-id="{{ $section->id }}"
|
@endif
|
||||||
data-column-id="{{ $col->id }}"
|
@elseif($mType === 'numeric')
|
||||||
data-row-index="{{ $rowIndex }}"
|
@for($nIdx = 1; $nIdx <= $freqN; $nIdx++)
|
||||||
class="w-full text-center text-sm border-0 focus:ring-1 focus:ring-blue-400 rounded py-1">
|
@php
|
||||||
</td>
|
$fieldKey = "s{$section->id}_r{$rowIndex}_c{$col->id}_n{$nIdx}";
|
||||||
@endif
|
$savedVal = $document?->data->where('field_key', $fieldKey)->first()?->field_value ?? '';
|
||||||
@endif
|
@endphp
|
||||||
@endforeach
|
<td class="px-1 py-1 border border-gray-300 text-center">
|
||||||
</tr>
|
<input type="number" step="any" name="section_data[{{ $fieldKey }}]" value="{{ $savedVal }}"
|
||||||
|
data-section-id="{{ $section->id }}" data-column-id="{{ $col->id }}" data-row-index="{{ $rowIndex }}"
|
||||||
|
class="w-full text-center text-sm border-0 focus:ring-1 focus:ring-blue-400 rounded py-1"
|
||||||
|
placeholder="n{{ $nIdx }}">
|
||||||
|
</td>
|
||||||
|
@endfor
|
||||||
|
@if($remainder > 0)
|
||||||
|
<td class="border border-gray-300" colspan="{{ $remainder }}"></td>
|
||||||
|
@endif
|
||||||
|
@elseif($mType === 'single_value')
|
||||||
|
@php
|
||||||
|
$fieldKey = "s{$section->id}_r{$rowIndex}_c{$col->id}";
|
||||||
|
$savedVal = $document?->data->where('field_key', $fieldKey)->first()?->field_value ?? '';
|
||||||
|
@endphp
|
||||||
|
<td class="px-1 py-1 border border-gray-300 text-center" colspan="{{ $totalMeasCols }}">
|
||||||
|
<input type="text" name="section_data[{{ $fieldKey }}]" value="{{ $savedVal }}"
|
||||||
|
data-section-id="{{ $section->id }}" data-column-id="{{ $col->id }}" data-row-index="{{ $rowIndex }}"
|
||||||
|
class="w-full text-center text-sm border-0 focus:ring-1 focus:ring-blue-400 rounded py-1"
|
||||||
|
placeholder="입력">
|
||||||
|
</td>
|
||||||
|
@elseif($mType === 'substitute')
|
||||||
|
@php
|
||||||
|
$fieldKey = "s{$section->id}_r{$rowIndex}_c{$col->id}";
|
||||||
|
$savedVal = $document?->data->where('field_key', $fieldKey)->first()?->field_value ?? '';
|
||||||
|
@endphp
|
||||||
|
<td class="px-1 py-1 border border-gray-300 text-center text-gray-400 text-xs" colspan="{{ $totalMeasCols }}">
|
||||||
|
<input type="text" name="section_data[{{ $fieldKey }}]" value="{{ $savedVal }}"
|
||||||
|
data-section-id="{{ $section->id }}" data-column-id="{{ $col->id }}" data-row-index="{{ $rowIndex }}"
|
||||||
|
class="w-full text-center text-sm border-0 focus:ring-1 focus:ring-blue-400 rounded py-1"
|
||||||
|
placeholder="(입력)">
|
||||||
|
</td>
|
||||||
|
@else
|
||||||
|
@for($nIdx = 1; $nIdx <= $totalMeasCols; $nIdx++)
|
||||||
|
@php
|
||||||
|
$fieldKey = "s{$section->id}_r{$rowIndex}_c{$col->id}_n{$nIdx}";
|
||||||
|
$savedVal = $document?->data->where('field_key', $fieldKey)->first()?->field_value ?? '';
|
||||||
|
@endphp
|
||||||
|
<td class="px-1 py-1 border border-gray-300 text-center">
|
||||||
|
<input type="text" name="section_data[{{ $fieldKey }}]" value="{{ $savedVal }}"
|
||||||
|
data-section-id="{{ $section->id }}" data-column-id="{{ $col->id }}" data-row-index="{{ $rowIndex }}"
|
||||||
|
class="w-full text-center text-sm border-0 focus:ring-1 focus:ring-blue-400 rounded py-1"
|
||||||
|
placeholder="n{{ $nIdx }}">
|
||||||
|
</td>
|
||||||
|
@endfor
|
||||||
|
@endif
|
||||||
|
@elseif($col->column_type === 'select')
|
||||||
|
@php
|
||||||
|
$fieldKey = "s{$section->id}_r{$rowIndex}_c{$col->id}";
|
||||||
|
$savedVal = $document?->data->where('field_key', $fieldKey)->first()?->field_value ?? '';
|
||||||
|
$options = $template->footer_judgement_options ?? ['적합', '부적합'];
|
||||||
|
@endphp
|
||||||
|
<td class="px-1 py-1 border border-gray-300 text-center">
|
||||||
|
<select name="section_data[{{ $fieldKey }}]"
|
||||||
|
data-section-id="{{ $section->id }}" data-column-id="{{ $col->id }}" data-row-index="{{ $rowIndex }}"
|
||||||
|
class="w-full text-center text-sm border-0 focus:ring-1 focus:ring-blue-400 rounded py-1">
|
||||||
|
<option value="">-</option>
|
||||||
|
@foreach($options as $opt)
|
||||||
|
<option value="{{ $opt }}" {{ $savedVal === $opt ? 'selected' : '' }}>{{ $opt }}</option>
|
||||||
|
@endforeach
|
||||||
|
</select>
|
||||||
|
</td>
|
||||||
|
@elseif($col->column_type === 'check')
|
||||||
|
@php
|
||||||
|
$fieldKey = "s{$section->id}_r{$rowIndex}_c{$col->id}";
|
||||||
|
$savedVal = $document?->data->where('field_key', $fieldKey)->first()?->field_value ?? '';
|
||||||
|
@endphp
|
||||||
|
<td class="px-1 py-1 border border-gray-300 text-center">
|
||||||
|
<input type="checkbox" name="section_data[{{ $fieldKey }}]" value="OK" {{ $savedVal === 'OK' ? 'checked' : '' }}
|
||||||
|
data-section-id="{{ $section->id }}" data-column-id="{{ $col->id }}" data-row-index="{{ $rowIndex }}"
|
||||||
|
class="rounded border-gray-300 text-blue-600 focus:ring-blue-500">
|
||||||
|
</td>
|
||||||
|
@else
|
||||||
|
{{-- text: 정적 데이터 (그룹) --}}
|
||||||
|
@php
|
||||||
|
$label = trim($col->label);
|
||||||
|
$isNoCol = str_contains(strtolower($label), 'no') && strlen($label) <= 4;
|
||||||
|
$isItemCol = in_array($label, ['검사항목', '항목']);
|
||||||
|
$isStdCol = in_array($label, ['검사기준', '기준']);
|
||||||
|
$isMethodCol = str_contains($label, '검사방') || in_array($label, ['방식', '검사방법']);
|
||||||
|
$isFreqCol = str_contains($label, '주기') || in_array($label, ['검사주기']);
|
||||||
|
$isJudgeCol = str_contains($label, '판정');
|
||||||
|
@endphp
|
||||||
|
@if($isNoCol)
|
||||||
|
@if($itemIdx === 0)
|
||||||
|
<td class="px-2 py-2 border border-gray-300 text-sm text-center" rowspan="{{ $groupCount }}">{{ $rowNum }}</td>
|
||||||
|
@endif
|
||||||
|
@elseif($isItemCol)
|
||||||
|
{{-- 구분(rowspan) + 항목명(각 행) --}}
|
||||||
|
@if($itemIdx === 0)
|
||||||
|
<td class="px-2 py-2 border border-gray-300 text-sm text-center font-medium" rowspan="{{ $groupCount }}">{{ $row['category'] }}</td>
|
||||||
|
@endif
|
||||||
|
<td class="px-2 py-2 border border-gray-300 text-sm text-center">{{ $item->getFieldValue('item') ?: '-' }}</td>
|
||||||
|
@elseif($isStdCol)
|
||||||
|
{{-- 기준 + 공차 (각 행 개별) --}}
|
||||||
|
<td class="px-2 py-2 border border-gray-300 text-sm text-center">{{ $formatStandard($item) }}</td>
|
||||||
|
<td class="px-2 py-2 border border-gray-300 text-sm text-center">{{ $formatTolerance($item->getFieldValue('tolerance')) }}</td>
|
||||||
|
@elseif($isMethodCol)
|
||||||
|
@php $methodVal = $item->getFieldValue('method'); @endphp
|
||||||
|
<td class="px-2 py-2 border border-gray-300 text-sm text-center">{{ $methodNames[$methodVal] ?? ($methodVal ?: '-') }}</td>
|
||||||
|
@elseif($isFreqCol)
|
||||||
|
<td class="px-2 py-2 border border-gray-300 text-sm text-center">{{ $formatFrequency($item) }}</td>
|
||||||
|
@elseif($isJudgeCol)
|
||||||
|
@php
|
||||||
|
$fieldKey = "s{$section->id}_r{$rowIndex}_c{$col->id}";
|
||||||
|
$savedVal = $document?->data->where('field_key', $fieldKey)->first()?->field_value ?? '';
|
||||||
|
@endphp
|
||||||
|
<td class="px-1 py-1 border border-gray-300 text-center">
|
||||||
|
<input type="checkbox" name="section_data[{{ $fieldKey }}]" value="OK" {{ $savedVal === 'OK' ? 'checked' : '' }}
|
||||||
|
data-section-id="{{ $section->id }}" data-column-id="{{ $col->id }}" data-row-index="{{ $rowIndex }}"
|
||||||
|
class="rounded border-gray-300 text-blue-600 focus:ring-blue-500">
|
||||||
|
</td>
|
||||||
|
@else
|
||||||
|
@php
|
||||||
|
$staticValue = match(true) {
|
||||||
|
in_array($label, ['규격', '적용규격', '관련규정']) => $item->getFieldValue('regulation'),
|
||||||
|
in_array($label, ['분류', '카테고리']) => $item->getFieldValue('category'),
|
||||||
|
default => null,
|
||||||
|
};
|
||||||
|
@endphp
|
||||||
|
@if($staticValue !== null)
|
||||||
|
<td class="px-2 py-2 border border-gray-300 text-sm">{{ $staticValue }}</td>
|
||||||
|
@else
|
||||||
|
@php
|
||||||
|
$fieldKey = "s{$section->id}_r{$rowIndex}_c{$col->id}";
|
||||||
|
$savedVal = $document?->data->where('field_key', $fieldKey)->first()?->field_value ?? '';
|
||||||
|
@endphp
|
||||||
|
<td class="px-1 py-1 border border-gray-300 text-center">
|
||||||
|
<input type="text" name="section_data[{{ $fieldKey }}]" value="{{ $savedVal }}"
|
||||||
|
data-section-id="{{ $section->id }}" data-column-id="{{ $col->id }}" data-row-index="{{ $rowIndex }}"
|
||||||
|
class="w-full text-center text-sm border-0 focus:ring-1 focus:ring-blue-400 rounded py-1">
|
||||||
|
</td>
|
||||||
|
@endif
|
||||||
|
@endif
|
||||||
|
@endif
|
||||||
|
@endforeach
|
||||||
|
</tr>
|
||||||
|
@endforeach
|
||||||
|
@endif
|
||||||
@endforeach
|
@endforeach
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
@@ -363,6 +717,36 @@ class="px-6 py-2 bg-green-600 text-white text-sm font-medium rounded-lg hover:bg
|
|||||||
|
|
||||||
@push('scripts')
|
@push('scripts')
|
||||||
<script>
|
<script>
|
||||||
|
function itemSearch(fieldKey, savedValue) {
|
||||||
|
return {
|
||||||
|
searchText: savedValue,
|
||||||
|
results: [],
|
||||||
|
showResults: false,
|
||||||
|
init() {},
|
||||||
|
async search() {
|
||||||
|
if (this.searchText.length < 1) {
|
||||||
|
this.results = [];
|
||||||
|
this.showResults = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const res = await fetch(`/api/admin/items/search?q=${encodeURIComponent(this.searchText)}`, {
|
||||||
|
headers: { 'Accept': 'application/json' }
|
||||||
|
});
|
||||||
|
const json = await res.json();
|
||||||
|
this.results = json.data || [];
|
||||||
|
this.showResults = this.results.length > 0;
|
||||||
|
} catch (e) {
|
||||||
|
this.results = [];
|
||||||
|
}
|
||||||
|
},
|
||||||
|
selectItem(item) {
|
||||||
|
this.searchText = item.name;
|
||||||
|
this.showResults = false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
document.addEventListener('DOMContentLoaded', function() {
|
document.addEventListener('DOMContentLoaded', function() {
|
||||||
const form = document.getElementById('documentForm');
|
const form = document.getElementById('documentForm');
|
||||||
if (!form) return;
|
if (!form) return;
|
||||||
@@ -479,4 +863,4 @@ class="px-6 py-2 bg-green-600 text-white text-sm font-medium rounded-lg hover:bg
|
|||||||
};
|
};
|
||||||
@endif
|
@endif
|
||||||
</script>
|
</script>
|
||||||
@endpush
|
@endpush
|
||||||
|
|||||||
@@ -148,12 +148,12 @@ class="doc-th"
|
|||||||
@php
|
@php
|
||||||
$staticValue = match(true) {
|
$staticValue = match(true) {
|
||||||
str_contains(strtolower($col->label), 'no') && strlen($col->label) <= 4 => $rowIndex + 1,
|
str_contains(strtolower($col->label), 'no') && strlen($col->label) <= 4 => $rowIndex + 1,
|
||||||
in_array($col->label, ['검사항목', '항목']) => $item->item,
|
in_array($col->label, ['검사항목', '항목']) => $item->getFieldValue('item'),
|
||||||
in_array($col->label, ['검사기준', '기준']) => $item->standard,
|
in_array($col->label, ['검사기준', '기준']) => $item->getFieldValue('standard'),
|
||||||
in_array($col->label, ['검사방식', '방식', '검사방법']) => $item->method,
|
in_array($col->label, ['검사방식', '방식', '검사방법']) => $item->getFieldValue('method'),
|
||||||
in_array($col->label, ['검사주기', '주기']) => $item->frequency,
|
in_array($col->label, ['검사주기', '주기']) => $item->getFieldValue('frequency'),
|
||||||
in_array($col->label, ['규격', '적용규격', '관련규정']) => $item->regulation,
|
in_array($col->label, ['규격', '적용규격', '관련규정']) => $item->getFieldValue('regulation'),
|
||||||
in_array($col->label, ['분류', '카테고리']) => $item->category,
|
in_array($col->label, ['분류', '카테고리']) => $item->getFieldValue('category'),
|
||||||
default => null,
|
default => null,
|
||||||
};
|
};
|
||||||
@endphp
|
@endphp
|
||||||
@@ -247,4 +247,4 @@ class="doc-th"
|
|||||||
.doc-td { border-color: #666 !important; }
|
.doc-td { border-color: #666 !important; }
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
@endsection
|
@endsection
|
||||||
|
|||||||
@@ -232,12 +232,12 @@ class="px-2 py-2 text-center text-xs font-medium text-gray-600 uppercase border
|
|||||||
@php
|
@php
|
||||||
$staticValue = match(true) {
|
$staticValue = match(true) {
|
||||||
str_contains(strtolower($col->label), 'no') && strlen($col->label) <= 4 => $rowIndex + 1,
|
str_contains(strtolower($col->label), 'no') && strlen($col->label) <= 4 => $rowIndex + 1,
|
||||||
in_array($col->label, ['검사항목', '항목']) => $item->item,
|
in_array($col->label, ['검사항목', '항목']) => $item->getFieldValue('item'),
|
||||||
in_array($col->label, ['검사기준', '기준']) => $item->standard,
|
in_array($col->label, ['검사기준', '기준']) => $item->getFieldValue('standard'),
|
||||||
in_array($col->label, ['검사방식', '방식', '검사방법']) => $item->method,
|
in_array($col->label, ['검사방식', '방식', '검사방법']) => $item->getFieldValue('method'),
|
||||||
in_array($col->label, ['검사주기', '주기']) => $item->frequency,
|
in_array($col->label, ['검사주기', '주기']) => $item->getFieldValue('frequency'),
|
||||||
in_array($col->label, ['규격', '적용규격', '관련규정']) => $item->regulation,
|
in_array($col->label, ['규격', '적용규격', '관련규정']) => $item->getFieldValue('regulation'),
|
||||||
in_array($col->label, ['분류', '카테고리']) => $item->category,
|
in_array($col->label, ['분류', '카테고리']) => $item->getFieldValue('category'),
|
||||||
default => null,
|
default => null,
|
||||||
};
|
};
|
||||||
@endphp
|
@endphp
|
||||||
@@ -483,4 +483,4 @@ class="px-4 py-2 bg-red-600 text-white text-sm font-medium rounded-lg hover:bg-r
|
|||||||
};
|
};
|
||||||
@endif
|
@endif
|
||||||
</script>
|
</script>
|
||||||
@endpush
|
@endpush
|
||||||
|
|||||||
@@ -810,6 +810,16 @@
|
|||||||
Route::get('/{group}', [DocumentTemplateApiController::class, 'getCommonCodes'])->name('group');
|
Route::get('/{group}', [DocumentTemplateApiController::class, 'getCommonCodes'])->name('group');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| 소스 테이블 통합 검색 API (문서양식 연결 설정용)
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
*/
|
||||||
|
Route::middleware(['web', 'auth', 'hq.member'])->prefix('admin/source-tables')->name('api.admin.source-tables.')->group(function () {
|
||||||
|
Route::get('/', [\App\Http\Controllers\Api\Admin\SourceTableSearchController::class, 'tables'])->name('index');
|
||||||
|
Route::get('/{table}/search', [\App\Http\Controllers\Api\Admin\SourceTableSearchController::class, 'search'])->name('search');
|
||||||
|
});
|
||||||
|
|
||||||
/*
|
/*
|
||||||
|--------------------------------------------------------------------------
|
|--------------------------------------------------------------------------
|
||||||
| 품목 검색 API (문서 작성 시 품명 자동완성)
|
| 품목 검색 API (문서 작성 시 품명 자동완성)
|
||||||
|
|||||||
Reference in New Issue
Block a user