diff --git a/database/seeders/IncomingInspectionTemplateSeeder.php b/database/seeders/IncomingInspectionTemplateSeeder.php index 6c3dfe5e..1d5e4cda 100644 --- a/database/seeders/IncomingInspectionTemplateSeeder.php +++ b/database/seeders/IncomingInspectionTemplateSeeder.php @@ -7,6 +7,7 @@ use App\Models\DocumentTemplateBasicField; use App\Models\DocumentTemplateColumn; use App\Models\DocumentTemplateSection; +use App\Models\DocumentTemplateSectionField; use App\Models\DocumentTemplateSectionItem; use Illuminate\Database\Seeder; @@ -36,6 +37,7 @@ public function run(): void $this->createApprovalLines($template->id); $this->createBasicFields($template->id); $this->createSection($template->id, $def['section_title'], $def['items']); + $this->createSectionFields($template->id); $this->createColumns($template->id); $this->command->info("✅ {$def['name']} (ID: {$template->id})"); @@ -247,6 +249,28 @@ private function createSection(int $templateId, string $title, array $items): vo } } + /** + * 검사 기준서 동적 필드 정의 (수입검사 공통) + */ + private function createSectionFields(int $templateId): void + { + $fields = [ + ['field_key' => 'category', 'label' => '분류', 'field_type' => 'text', 'width' => '80px', 'is_required' => false, 'sort_order' => 1], + ['field_key' => 'item', 'label' => '검사항목', 'field_type' => 'text', 'width' => '120px', 'is_required' => true, 'sort_order' => 2], + ['field_key' => 'standard', 'label' => '검사기준', 'field_type' => 'text', 'width' => '150px', 'is_required' => true, 'sort_order' => 3], + ['field_key' => 'method', 'label' => '검사방식', 'field_type' => 'select', 'width' => '100px', 'is_required' => false, 'sort_order' => 4], + ['field_key' => 'frequency', 'label' => '검사주기', 'field_type' => 'text', 'width' => '80px', 'is_required' => false, 'sort_order' => 5], + ['field_key' => 'regulation', 'label' => '관련규격', 'field_type' => 'text', 'width' => '100px', 'is_required' => false, 'sort_order' => 6], + ]; + + foreach ($fields as $field) { + DocumentTemplateSectionField::create(array_merge( + ['template_id' => $templateId], + $field, + )); + } + } + /** * 데이터 테이블 컬럼 (5130 공통 구조) */ @@ -288,6 +312,7 @@ private function cleanupExisting(string $name): void } DocumentTemplateColumn::where('template_id', $existing->id)->delete(); + DocumentTemplateSectionField::where('template_id', $existing->id)->delete(); $sections = DocumentTemplateSection::where('template_id', $existing->id)->get(); foreach ($sections as $section) { DocumentTemplateSectionItem::where('section_id', $section->id)->delete(); diff --git a/database/seeders/MidInspectionTemplateSeeder.php b/database/seeders/MidInspectionTemplateSeeder.php index 9536b7c9..3b33efa2 100644 --- a/database/seeders/MidInspectionTemplateSeeder.php +++ b/database/seeders/MidInspectionTemplateSeeder.php @@ -7,6 +7,7 @@ use App\Models\DocumentTemplateBasicField; use App\Models\DocumentTemplateColumn; use App\Models\DocumentTemplateSection; +use App\Models\DocumentTemplateSectionField; use App\Models\DocumentTemplateSectionItem; use Illuminate\Database\Seeder; @@ -40,6 +41,7 @@ public function run(): void $this->createSection($template->id, $sectionDef['title'], $sectionDef['items'], $i + 1); } + $this->createSectionFields($template->id); $this->createColumns($template->id, $def['columns']); $this->command->info("✅ {$def['name']} (ID: {$template->id})"); @@ -465,6 +467,27 @@ private function createSection(int $templateId, string $title, array $items, int } } + /** + * 검사 기준서 동적 필드 정의 (중간검사 공통) + */ + private function createSectionFields(int $templateId): void + { + $fields = [ + ['field_key' => 'category', 'label' => '분류', 'field_type' => 'text', 'width' => '80px', 'is_required' => false, 'sort_order' => 1], + ['field_key' => 'item', 'label' => '검사항목', 'field_type' => 'text', 'width' => '120px', 'is_required' => true, 'sort_order' => 2], + ['field_key' => 'standard', 'label' => '검사기준', 'field_type' => 'text', 'width' => '150px', 'is_required' => true, 'sort_order' => 3], + ['field_key' => 'method', 'label' => '검사방식', 'field_type' => 'select', 'width' => '100px', 'is_required' => false, 'sort_order' => 4], + ['field_key' => 'frequency', 'label' => '검사주기', 'field_type' => 'text', 'width' => '80px', 'is_required' => false, 'sort_order' => 5], + ]; + + foreach ($fields as $field) { + DocumentTemplateSectionField::create(array_merge( + ['template_id' => $templateId], + $field, + )); + } + } + /** * 데이터 테이블 컬럼 (양식별 다름) */ @@ -489,6 +512,7 @@ private function cleanupExisting(string $name): void } DocumentTemplateColumn::where('template_id', $existing->id)->delete(); + DocumentTemplateSectionField::where('template_id', $existing->id)->delete(); $sections = DocumentTemplateSection::where('template_id', $existing->id)->get(); foreach ($sections as $section) { DocumentTemplateSectionItem::where('section_id', $section->id)->delete(); diff --git a/resources/views/document-templates/edit.blade.php b/resources/views/document-templates/edit.blade.php index 26a2359a..641e81c2 100644 --- a/resources/views/document-templates/edit.blade.php +++ b/resources/views/document-templates/edit.blade.php @@ -251,18 +251,30 @@ class="w-full px-3 py-1.5 border border-gray-300 rounded-lg text-sm focus:outlin @@ -1006,6 +1018,148 @@ class="flex-1 px-3 py-2 border border-gray-300 rounded-lg text-sm font-medium"> `).join(''); } + // ===== 검사기준서 → 테이블컬럼 자동 파생 (Phase 5.0) ===== + + /** + * 모든 섹션의 items를 분석하여 columns를 자동 생성한다. + * 규칙: + * Step 1: 정적 컬럼 (NO, 검사항목, 검사기준) — 항상 포함 + * Step 2: 동적 컬럼 — items의 measurement_type/method에서 파생 + * - checkbox(육안) → check 컬럼 + * - numeric(체크/계측) → complex 컬럼 (sub_labels: n1~n{max(frequency_n)}) + * - single_value(밀시트/공인기관) → text 컬럼 (측정값) + * - text(기타) → text 컬럼 + * Step 3: 부가 컬럼 — 검사방식(다양하면), 비고, 판정(항상) + * + * @param {boolean} confirmOverwrite - true면 기존 columns 덮어쓰기 확인 건너뜀 + * @returns {Array} 생성된 columns 배열 + */ + function generateColumnsFromItems(confirmOverwrite = false) { + // 모든 섹션의 items 수집 + const allItems = templateState.sections.flatMap(s => s.items || []); + + if (allItems.length === 0) { + showToast('검사 기준서에 항목이 없습니다. 항목을 먼저 추가하세요.', 'warning'); + return null; + } + + // 기존 컬럼이 있고 사용자 확인 필요한 경우 + if (!confirmOverwrite && templateState.columns.length > 0) { + if (!confirm('기존 테이블 컬럼이 자동 생성된 컬럼으로 교체됩니다. 계속하시겠습니까?')) { + return null; + } + } + + const columns = []; + let sortOrder = 1; + + // --- Step 1: 정적 컬럼 --- + columns.push({ id: generateId(), label: 'NO', width: '40px', column_type: 'text', group_name: '', sub_labels: null, _auto: true }); + sortOrder++; + columns.push({ id: generateId(), label: '검사항목', width: '120px', column_type: 'text', group_name: '', sub_labels: null, _auto: true }); + sortOrder++; + columns.push({ id: generateId(), label: '검사기준', width: '150px', column_type: 'text', group_name: '', sub_labels: null, _auto: true }); + sortOrder++; + + // --- Step 2: 동적 컬럼 (measurement_type 분석) --- + const types = new Set(); + let maxFrequencyN = 3; // 기본 3회 측정 + const methods = new Set(); + + allItems.forEach(item => { + const mt = item.measurement_type || ''; + if (mt) types.add(mt); + if (item.method) methods.add(item.method); + // frequency_n에서 최대값 추출 + const fn = parseInt(item.frequency_n); + if (!isNaN(fn) && fn > maxFrequencyN) maxFrequencyN = fn; + // frequency 문자열에서 n= 값 추출 (예: "n=3, c=0") + if (item.frequency) { + const match = item.frequency.match(/n\s*=\s*(\d+)/i); + if (match) { + const nVal = parseInt(match[1]); + if (nVal > maxFrequencyN) maxFrequencyN = nVal; + } + } + }); + + // checkbox → check 컬럼 (OK/NG) + if (types.has('checkbox')) { + columns.push({ id: generateId(), label: 'OK/NG', width: '60px', column_type: 'check', group_name: '', sub_labels: null, _auto: true }); + sortOrder++; + } + + // numeric → complex 컬럼 (측정치 n1~nN) + if (types.has('numeric')) { + const subLabels = []; + for (let i = 1; i <= maxFrequencyN; i++) { + subLabels.push(`n${i}`); + } + columns.push({ id: generateId(), label: '측정치', width: `${Math.max(160, maxFrequencyN * 60)}px`, column_type: 'complex', group_name: '측정치', sub_labels: subLabels, _auto: true }); + sortOrder++; + } + + // single_value → text 컬럼 (측정값) + if (types.has('single_value')) { + columns.push({ id: generateId(), label: '측정값', width: '100px', column_type: 'text', group_name: '', sub_labels: null, _auto: true }); + sortOrder++; + } + + // substitute → text 컬럼 (성적서 번호) + if (types.has('substitute')) { + columns.push({ id: generateId(), label: '성적서 번호', width: '120px', column_type: 'text', group_name: '', sub_labels: null, _auto: true }); + sortOrder++; + } + + // text(기타) → text 컬럼 + if (types.has('text')) { + columns.push({ id: generateId(), label: '비고/기타', width: '100px', column_type: 'text', group_name: '', sub_labels: null, _auto: true }); + sortOrder++; + } + + // --- Step 3: 부가 컬럼 --- + + // 검사방식: 다양한 method가 있으면 포함 + if (methods.size > 1) { + columns.push({ id: generateId(), label: '검사방식', width: '100px', column_type: 'text', group_name: '', sub_labels: null, _auto: true }); + sortOrder++; + } + + // 판정 (항상 포함) + columns.push({ id: generateId(), label: '판정', width: '80px', column_type: 'select', group_name: '', sub_labels: null, _auto: true }); + + return columns; + } + + /** + * 자동 파생 실행 + 상태 반영 + 렌더링 + */ + function applyAutoColumns(confirmOverwrite = false) { + const columns = generateColumnsFromItems(confirmOverwrite); + if (columns) { + templateState.columns = columns; + renderColumns(); + showToast(`테이블 컬럼 ${columns.length}개가 자동 생성되었습니다.`, 'success'); + // 컬럼 탭에 자동생성 표시 업데이트 + updateAutoColumnsIndicator(); + } + } + + /** + * 자동생성 여부 표시 업데이트 + */ + function updateAutoColumnsIndicator() { + const indicator = document.getElementById('auto-columns-indicator'); + if (!indicator) return; + const hasAutoColumns = templateState.columns.some(c => c._auto); + if (hasAutoColumns) { + indicator.classList.remove('hidden'); + indicator.textContent = `자동 생성됨 (${templateState.columns.filter(c => c._auto).length}/${templateState.columns.length}개)`; + } else { + indicator.classList.add('hidden'); + } + } + // ===== 컬럼 관리 ===== function addColumn() { const column = { id: generateId(), label: '', width: '100px', column_type: 'text', group_name: '', sub_labels: null }; @@ -1030,6 +1184,7 @@ function renderColumns() { if (templateState.columns.length === 0) { container.innerHTML = ''; emptyMsg.classList.remove('hidden'); + updateAutoColumnsIndicator(); return; } @@ -1087,6 +1242,8 @@ class="w-16 px-1 py-0.5 border border-blue-200 rounded text-xs bg-white text-cen ` : ''} `).join(''); + + updateAutoColumnsIndicator(); } // ===== 컬럼 타입 변경 처리 ===== @@ -1170,7 +1327,7 @@ function saveTemplate() { approval_lines: templateState.approval_lines, basic_fields: templateState.basic_fields, sections: templateState.sections, - columns: templateState.columns, + columns: templateState.columns.map(c => { const { _auto, ...rest } = c; return rest; }), section_fields: templateState.section_fields, template_links: templateState.template_links.filter(l => l.source_table) };