diff --git a/app/Http/Controllers/Api/Admin/DocumentTemplateApiController.php b/app/Http/Controllers/Api/Admin/DocumentTemplateApiController.php index 33e947c9..d64be324 100644 --- a/app/Http/Controllers/Api/Admin/DocumentTemplateApiController.php +++ b/app/Http/Controllers/Api/Admin/DocumentTemplateApiController.php @@ -289,8 +289,11 @@ public function duplicate(Request $request, int $id): JsonResponse 'item' => $item->item, 'standard' => $item->standard, 'tolerance' => $item->tolerance, + 'standard_criteria' => $item->standard_criteria, 'method' => $item->method, 'measurement_type' => $item->measurement_type, + 'frequency_n' => $item->frequency_n, + 'frequency_c' => $item->frequency_c, 'frequency' => $item->frequency, 'regulation' => $item->regulation, 'sort_order' => $item->sort_order, @@ -428,8 +431,11 @@ private function saveRelations(DocumentTemplate $template, array $data, bool $de 'item' => $item['item'] ?? '', 'standard' => $item['standard'] ?? '', 'tolerance' => $item['tolerance'] ?? null, + 'standard_criteria' => $item['standard_criteria'] ?? null, 'method' => $item['method'] ?? '', 'measurement_type' => $item['measurement_type'] ?? null, + 'frequency_n' => $item['frequency_n'] ?? null, + 'frequency_c' => $item['frequency_c'] ?? null, 'frequency' => $item['frequency'] ?? '', 'regulation' => $item['regulation'] ?? '', 'sort_order' => $iIndex, diff --git a/app/Http/Controllers/DocumentTemplateController.php b/app/Http/Controllers/DocumentTemplateController.php index e99d1831..1e896368 100644 --- a/app/Http/Controllers/DocumentTemplateController.php +++ b/app/Http/Controllers/DocumentTemplateController.php @@ -131,8 +131,11 @@ private function prepareTemplateData(DocumentTemplate $template): array 'item' => $i->item, 'standard' => $i->standard, 'tolerance' => $i->tolerance, + 'standard_criteria' => $i->standard_criteria, 'method' => $i->method, 'measurement_type' => $i->measurement_type, + 'frequency_n' => $i->frequency_n, + 'frequency_c' => $i->frequency_c, 'frequency' => $i->frequency, 'regulation' => $i->regulation, ]; diff --git a/app/Models/DocumentTemplateSectionItem.php b/app/Models/DocumentTemplateSectionItem.php index 6442af5b..2dd61092 100644 --- a/app/Models/DocumentTemplateSectionItem.php +++ b/app/Models/DocumentTemplateSectionItem.php @@ -16,15 +16,21 @@ class DocumentTemplateSectionItem extends Model 'item', 'standard', 'tolerance', + 'standard_criteria', 'method', 'measurement_type', + 'frequency_n', + 'frequency_c', 'frequency', 'regulation', 'sort_order', ]; protected $casts = [ + 'standard_criteria' => 'array', 'sort_order' => 'integer', + 'frequency_n' => 'integer', + 'frequency_c' => 'integer', ]; public function section(): BelongsTo diff --git a/resources/views/document-templates/edit.blade.php b/resources/views/document-templates/edit.blade.php index 8c8b68e7..0deda018 100644 --- a/resources/views/document-templates/edit.blade.php +++ b/resources/views/document-templates/edit.blade.php @@ -345,12 +345,12 @@ function addBasicField() { } function removeBasicField(id) { - templateState.basic_fields = templateState.basic_fields.filter(f => f.id !== id); + templateState.basic_fields = templateState.basic_fields.filter(f => f.id !=id); renderBasicFields(); } function updateBasicField(id, field, value) { - const bf = templateState.basic_fields.find(f => f.id === id); + const bf = templateState.basic_fields.find(f => f.id ==id); if (bf) bf[field] = value; } @@ -419,12 +419,12 @@ function addApprovalLine() { } function removeApprovalLine(id) { - templateState.approval_lines = templateState.approval_lines.filter(l => l.id !== id); + templateState.approval_lines = templateState.approval_lines.filter(l => l.id !=id); renderApprovalLines(); } function updateApprovalLine(id, field, value) { - const line = templateState.approval_lines.find(l => l.id === id); + const line = templateState.approval_lines.find(l => l.id ==id); if (line) line[field] = value; } @@ -469,12 +469,12 @@ function addSection() { } function removeSection(id) { - templateState.sections = templateState.sections.filter(s => s.id !== id); + templateState.sections = templateState.sections.filter(s => s.id !=id); renderSections(); } function updateSection(id, field, value) { - const section = templateState.sections.find(s => s.id === id); + const section = templateState.sections.find(s => s.id ==id); if (section) section[field] = value; } @@ -543,7 +543,7 @@ function onMethodChange(sectionId, itemId, value) { } function addSectionItem(sectionId) { - const section = templateState.sections.find(s => s.id === sectionId); + const section = templateState.sections.find(s => s.id ==sectionId); if (section) { section.items.push({ id: generateId(), @@ -551,8 +551,11 @@ function addSectionItem(sectionId) { item: '', standard: '', tolerance: '', + standard_criteria: null, method: '', measurement_type: '', + frequency_n: null, + frequency_c: null, frequency: '', regulation: '' }); @@ -561,21 +564,42 @@ function addSectionItem(sectionId) { } function removeSectionItem(sectionId, itemId) { - const section = templateState.sections.find(s => s.id === sectionId); + const section = templateState.sections.find(s => s.id ==sectionId); if (section) { - section.items = section.items.filter(i => i.id !== itemId); + section.items = section.items.filter(i => i.id !=itemId); renderSections(); } } function updateSectionItem(sectionId, itemId, field, value) { - const section = templateState.sections.find(s => s.id === sectionId); + const section = templateState.sections.find(s => s.id ==sectionId); if (section) { - const item = section.items.find(i => i.id === itemId); + const item = section.items.find(i => i.id ==itemId); if (item) item[field] = value; } } + function updateStandardCriteria(sectionId, itemId, field, value) { + const section = templateState.sections.find(s => s.id == sectionId); + if (!section) return; + const item = section.items.find(i => i.id == itemId); + if (!item) return; + if (!item.standard_criteria) item.standard_criteria = {}; + if (field === 'min' || field === 'max') { + item.standard_criteria[field] = value !== '' ? parseFloat(value) : null; + // 기본 연산자 자동 설정 + if (field === 'min' && !item.standard_criteria.min_op) item.standard_criteria.min_op = 'gte'; + if (field === 'max' && !item.standard_criteria.max_op) item.standard_criteria.max_op = 'lte'; + } else { + item.standard_criteria[field] = value; + } + // min/max 둘 다 비어있으면 null로 초기화 + const c = item.standard_criteria; + if (c.min == null && c.max == null) { + item.standard_criteria = null; + } + } + function renderSections() { const container = document.getElementById('sections-container'); const emptyMsg = document.getElementById('sections-empty'); @@ -659,9 +683,28 @@ class="w-full px-2 py-1 border border-gray-200 rounded text-xs"> class="w-full px-2 py-1 border border-gray-200 rounded text-xs"> - + class="w-full px-2 py-1 border border-gray-200 rounded text-xs mb-1"> +
+ + + ~ + + +
- +
+ n= + + c= + + +
c.id !== id); + templateState.columns = templateState.columns.filter(c => c.id !=id); renderColumns(); } function updateColumn(id, field, value) { - const column = templateState.columns.find(c => c.id === id); + const column = templateState.columns.find(c => c.id ==id); if (column) column[field] = value; } @@ -797,7 +850,7 @@ class="w-16 px-1 py-0.5 border border-blue-200 rounded text-xs bg-white text-cen // ===== 컬럼 타입 변경 처리 ===== function handleColumnTypeChange(id, value) { - const column = templateState.columns.find(c => c.id === id); + const column = templateState.columns.find(c => c.id ==id); if (column) { column.column_type = value; if (value === 'complex' && !column.sub_labels) { @@ -811,7 +864,7 @@ function handleColumnTypeChange(id, value) { } function addSubLabel(colId) { - const column = templateState.columns.find(c => c.id === colId); + const column = templateState.columns.find(c => c.id ==colId); if (column) { if (!column.sub_labels) column.sub_labels = []; column.sub_labels.push(''); @@ -828,14 +881,14 @@ function addSubLabel(colId) { } function updateSubLabel(colId, idx, value) { - const column = templateState.columns.find(c => c.id === colId); + const column = templateState.columns.find(c => c.id ==colId); if (column && column.sub_labels) { column.sub_labels[idx] = value; } } function removeSubLabel(colId, idx) { - const column = templateState.columns.find(c => c.id === colId); + const column = templateState.columns.find(c => c.id ==colId); if (column && column.sub_labels) { column.sub_labels.splice(idx, 1); renderColumns(); @@ -949,18 +1002,36 @@ function generatePreviewHtml() { return `${rows.join('')}
`; }; + // 전체 항목에서 최대 frequency_n 계산 + const getMaxFrequencyN = () => { + let maxN = 0; + templateState.sections.forEach(section => { + if (section.items) { + section.items.forEach(item => { + if (item.frequency_n && item.frequency_n > maxN) { + maxN = item.frequency_n; + } + }); + } + }); + return maxN; + }; + // 검사 데이터 컬럼 헤더 생성 const renderColumnHeaders = () => { if (templateState.columns.length === 0) return ''; const hasComplex = templateState.columns.some(c => c.column_type === 'complex' && c.sub_labels && c.sub_labels.length > 0); + const maxN = getMaxFrequencyN(); let headerRow1 = ''; let headerRow2 = ''; templateState.columns.forEach(col => { if (col.column_type === 'complex' && col.sub_labels && col.sub_labels.length > 0) { - headerRow1 += `${escapeHtml(col.label)}`; - col.sub_labels.forEach(sl => { - headerRow2 += `${escapeHtml(sl)}`; - }); + // frequency_n 기반 동적 colspan (최소 sub_labels 수) + const colSpan = Math.max(col.sub_labels.length, maxN); + headerRow1 += `${escapeHtml(col.label)}`; + for (let ni = 1; ni <= colSpan; ni++) { + headerRow2 += `n${ni}`; + } } else { const label = (col.label || '').trim(); const isItem = label.includes('검사항목') || label.includes('항목'); @@ -988,7 +1059,8 @@ function generatePreviewHtml() { } }); if (allItems.length === 0) { - const colSpan = templateState.columns.reduce((sum, c) => sum + (c.column_type === 'complex' && c.sub_labels ? c.sub_labels.length : 1), 0) || 1; + const maxN = getMaxFrequencyN(); + const colSpan = templateState.columns.reduce((sum, c) => sum + (c.column_type === 'complex' && c.sub_labels ? Math.max(c.sub_labels.length, maxN) : 1), 0) || 1; return `검사항목이 없습니다.`; } @@ -1017,34 +1089,46 @@ function generatePreviewHtml() { } // 측정치 셀 렌더링 (rowspan 포함, 그룹용) - const renderMeasurementCellsWithRowspan = (col, mType, rowspanN) => { + const renderMeasurementCellsWithRowspan = (col, mType, rowspanN, frequencyN) => { const rs = ` rowspan="${rowspanN}"`; - const subCount = col.sub_labels.length; + const maxN = getMaxFrequencyN(); + const totalCols = Math.max(col.sub_labels.length, maxN); + const cellCount = frequencyN ? frequencyN : totalCols; + const remainder = totalCols - cellCount; + if (mType === 'checkbox') { - return col.sub_labels.map(() => `☐OK ☐NG`).join(''); + return Array(cellCount).fill('').map(() => `☐OK ☐NG`).join('') + + (remainder > 0 ? `` : ''); } else if (mType === 'numeric') { - return col.sub_labels.map(() => `___`).join(''); + return Array(cellCount).fill('').map(() => `___`).join('') + + (remainder > 0 ? `` : ''); } else if (mType === 'single_value') { - return `( 입력 )`; + return `( 입력 )`; } else if (mType === 'substitute') { - return `성적서로 대체`; + return `성적서로 대체`; } - return col.sub_labels.map(() => `-`).join(''); + return Array(totalCols).fill('').map(() => `-`).join(''); }; // 측정치 셀 렌더링 헬퍼 (단일 항목용) - const renderMeasurementCells = (col, mType) => { - const subCount = col.sub_labels.length; + const renderMeasurementCells = (col, mType, frequencyN) => { + const maxN = getMaxFrequencyN(); + const totalCols = Math.max(col.sub_labels.length, maxN); + const cellCount = frequencyN ? frequencyN : totalCols; + const remainder = totalCols - cellCount; + if (mType === 'checkbox') { - return col.sub_labels.map(() => '☐OK ☐NG').join(''); + return Array(cellCount).fill('').map(() => '☐OK ☐NG').join('') + + (remainder > 0 ? `` : ''); } else if (mType === 'numeric') { - return col.sub_labels.map(() => '___').join(''); + return Array(cellCount).fill('').map(() => '___').join('') + + (remainder > 0 ? `` : ''); } else if (mType === 'single_value') { - return `( 입력 )`; + return `( 입력 )`; } else if (mType === 'substitute') { - return `성적서로 대체`; + return `성적서로 대체`; } - return col.sub_labels.map(() => '-').join(''); + return Array(totalCols).fill('').map(() => '-').join(''); }; let rowNum = 0; @@ -1055,7 +1139,7 @@ function generatePreviewHtml() { const item = row.item; let cells = templateState.columns.map(col => { if (col.column_type === 'complex' && col.sub_labels && col.sub_labels.length > 0) { - return renderMeasurementCells(col, item.measurement_type || ''); + return renderMeasurementCells(col, item.measurement_type || '', item.frequency_n); } const label = (col.label || '').trim(); if (label === 'NO' || label === 'no') { @@ -1063,13 +1147,11 @@ function generatePreviewHtml() { } else if (label.includes('검사항목') || label.includes('항목')) { return `${escapeHtml(item.item || '-')}`; } else if (label.includes('검사기준') || label.includes('기준')) { - let std = item.standard || '-'; - if (item.tolerance) std += ' (' + item.tolerance + ')'; - return `${escapeHtml(std)}`; + return `${formatStandard(item)}`; } else if (label.includes('검사방')) { return `${escapeHtml(getMethodName(item.method))}`; } else if (label.includes('주기')) { - return `${escapeHtml(item.frequency || '-')}`; + return `${formatFrequency(item)}`; } else if (label.includes('판정')) { return `☐`; } @@ -1084,11 +1166,8 @@ function generatePreviewHtml() { let cells = ''; cells += templateState.columns.map(col => { if (col.column_type === 'complex' && col.sub_labels && col.sub_labels.length > 0) { - // 측정치: 첫 행만 rowspan - if (itemIdx === 0) { - return renderMeasurementCellsWithRowspan(col, row.items[0].measurement_type || '', n); - } - return ''; + // 측정치: 각 행마다 개별 렌더링 (항목별 frequency_n 반영) + return renderMeasurementCells(col, item.measurement_type || '', item.frequency_n); } const label = (col.label || '').trim(); const rs = itemIdx === 0 ? ` rowspan="${n}"` : ''; @@ -1106,21 +1185,14 @@ function generatePreviewHtml() { return catCell + `${escapeHtml(item.item || '-')}`; } else if (label.includes('검사기준') || label.includes('기준')) { // 기준 + 공차 (각 행 개별) - return `${escapeHtml(item.standard || '-')}` + return `${formatStandard(item)}` + `${escapeHtml(item.tolerance || '-')}`; } else if (label.includes('검사방')) { - return itemIdx === 0 - ? `${escapeHtml(getMethodName(item.method))}` - : ''; + return `${escapeHtml(getMethodName(item.method))}`; } else if (label.includes('주기')) { - return itemIdx === 0 - ? `${escapeHtml(item.frequency || '-')}` - : ''; + return `${formatFrequency(item)}`; } else if (label.includes('판정')) { - // 판정: 첫 행만 rowspan - return itemIdx === 0 - ? `☐` - : ''; + return `☐`; } return `-`; }).join(''); @@ -1210,7 +1282,7 @@ function uploadSectionImage(sectionId, input) { .then(response => response.json()) .then(result => { if (result.success) { - const section = templateState.sections.find(s => s.id === sectionId); + const section = templateState.sections.find(s => s.id ==sectionId); if (section) { section.image_path = result.path; renderSections(); @@ -1229,7 +1301,7 @@ function uploadSectionImage(sectionId, input) { } function removeSectionImage(sectionId) { - const section = templateState.sections.find(s => s.id === sectionId); + const section = templateState.sections.find(s => s.id ==sectionId); if (section) { section.image_path = null; renderSections(); @@ -1248,7 +1320,7 @@ function initSortable() { const newOrder = []; sectionsContainer.querySelectorAll('[data-section-id]').forEach((el, idx) => { const sectionId = el.dataset.sectionId; - const section = templateState.sections.find(s => s.id === sectionId); + const section = templateState.sections.find(s => s.id ==sectionId); if (section) newOrder.push(section); }); templateState.sections = newOrder; @@ -1265,7 +1337,7 @@ function initSortable() { const newOrder = []; approvalContainer.querySelectorAll('[data-id]').forEach((el) => { const id = el.dataset.id; - const line = templateState.approval_lines.find(l => l.id === id); + const line = templateState.approval_lines.find(l => l.id ==id); if (line) newOrder.push(line); }); templateState.approval_lines = newOrder; @@ -1282,7 +1354,7 @@ function initSortable() { const newOrder = []; basicFieldsContainer.querySelectorAll('[data-bf-id]').forEach((el) => { const id = el.dataset.bfId; - const field = templateState.basic_fields.find(f => f.id === id); + const field = templateState.basic_fields.find(f => f.id ==id); if (field) newOrder.push(field); }); templateState.basic_fields = newOrder; @@ -1299,7 +1371,7 @@ function initSortable() { const newOrder = []; columnsContainer.querySelectorAll('[data-column-id]').forEach((el) => { const id = el.dataset.columnId; - const col = templateState.columns.find(c => c.id === id); + const col = templateState.columns.find(c => c.id ==id); if (col) newOrder.push(col); }); templateState.columns = newOrder; @@ -1345,6 +1417,35 @@ function escapeHtml(text) { div.textContent = text; return div.innerHTML; } + + function formatStandard(item) { + const c = item.standard_criteria; + if (c && (c.min != null || c.max != null)) { + const opLabel = { gte: '이상', gt: '초과', lte: '이하', lt: '미만' }; + const parts = []; + if (c.min != null) parts.push(`${c.min} ${opLabel[c.min_op || 'gte']}`); + if (c.max != null) parts.push(`${c.max} ${opLabel[c.max_op || 'lte']}`); + return parts.join(' ~ '); + } + let std = item.standard || '-'; + if (item.tolerance) std += ' (' + item.tolerance + ')'; + return escapeHtml(std); + } + + function formatFrequency(item) { + const parts = []; + if (item.frequency_n != null && item.frequency_n !== '') { + let nc = `n=${item.frequency_n}`; + if (item.frequency_c != null && item.frequency_c !== '') { + nc += `, c=${item.frequency_c}`; + } + parts.push(nc); + } + if (item.frequency) { + parts.push(escapeHtml(item.frequency)); + } + return parts.length > 0 ? parts.join(' / ') : '-'; + } diff --git a/resources/views/documents/edit.blade.php b/resources/views/documents/edit.blade.php index 974549c1..d41d63f5 100644 --- a/resources/views/documents/edit.blade.php +++ b/resources/views/documents/edit.blade.php @@ -214,19 +214,41 @@ class="w-full text-center text-sm border-0 focus:ring-1 focus:ring-blue-400 roun class="rounded border-gray-300 text-blue-600 focus:ring-blue-500"> @elseif($col->column_type === 'measurement') - {{-- measurement: 수치 입력 --}} + {{-- measurement: 수치 입력 (frequency_n에 따라 다중 입력) --}} @php - $fieldKey = "s{$section->id}_r{$rowIndex}_c{$col->id}"; - $savedVal = $document?->data->where('field_key', $fieldKey)->first()?->field_value ?? ''; + $frequencyN = $item->frequency_n ?? 1; @endphp - + @if($frequencyN > 1) +
+ @for($nIdx = 1; $nIdx <= $frequencyN; $nIdx++) + @php + $fieldKey = "s{$section->id}_r{$rowIndex}_c{$col->id}_n{$nIdx}"; + $savedVal = $document?->data->where('field_key', $fieldKey)->first()?->field_value ?? ''; + @endphp + + @endfor +
+ @else + @php + $fieldKey = "s{$section->id}_r{$rowIndex}_c{$col->id}"; + $savedVal = $document?->data->where('field_key', $fieldKey)->first()?->field_value ?? ''; + @endphp + + @endif @else {{-- text: 정적 데이터 (항목정보) 또는 텍스트 입력 --}} @@ -237,7 +259,16 @@ class="w-full text-center text-sm border-0 focus:ring-1 focus:ring-blue-400 roun in_array($col->label, ['검사항목', '항목']) => $item->item, in_array($col->label, ['검사기준', '기준']) => $item->standard, in_array($col->label, ['검사방식', '방식', '검사방법']) => $item->method, - in_array($col->label, ['검사주기', '주기']) => $item->frequency, + in_array($col->label, ['검사주기', '주기']) => (function() use ($item) { + $parts = []; + if ($item->frequency_n) { + $nc = "n={$item->frequency_n}"; + if ($item->frequency_c !== null) $nc .= ", c={$item->frequency_c}"; + $parts[] = $nc; + } + if ($item->frequency) $parts[] = $item->frequency; + return $parts ? implode(' / ', $parts) : '-'; + })(), in_array($col->label, ['규격', '적용규격', '관련규정']) => $item->regulation, in_array($col->label, ['분류', '카테고리']) => $item->category, default => null,