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 ``;
};
+ // 전체 항목에서 최대 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,