From fc5c5e2b03e86a5dd53b6851e01095bf00c00424 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B6=8C=ED=98=81=EC=84=B1?= Date: Tue, 3 Feb 2026 15:00:01 +0900 Subject: [PATCH] =?UTF-8?q?refactor:=EB=AC=B8=EC=84=9C=EC=96=91=EC=8B=9D?= =?UTF-8?q?=20=EB=AF=B8=EB=A6=AC=EB=B3=B4=EA=B8=B0=20=EB=AA=A8=EB=8B=AC?= =?UTF-8?q?=EC=9D=84=20=EA=B3=B5=ED=86=B5=20partial=EB=A1=9C=20=ED=86=B5?= =?UTF-8?q?=ED=95=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit index/edit 페이지에 각각 중복 구현되어 있던 미리보기 렌더링 로직을 partials/preview-modal.blade.php로 통합하여 단일 buildDocumentPreviewHtml() 함수로 관리 Co-Authored-By: Claude Opus 4.5 --- .../views/document-templates/edit.blade.php | 498 ++++++------------ .../views/document-templates/index.blade.php | 254 +-------- .../partials/preview-modal.blade.php | 305 +++++++++++ 3 files changed, 465 insertions(+), 592 deletions(-) create mode 100644 resources/views/document-templates/partials/preview-modal.blade.php diff --git a/resources/views/document-templates/edit.blade.php b/resources/views/document-templates/edit.blade.php index bcb98247..70a8f9c9 100644 --- a/resources/views/document-templates/edit.blade.php +++ b/resources/views/document-templates/edit.blade.php @@ -2,6 +2,15 @@ @section('title', $isCreate ? '문서양식 등록' : '문서양식 편집') +@push('styles') + +@endpush + @section('content')
@@ -225,25 +234,8 @@ class="w-full px-3 py-1.5 border border-gray-300 rounded-lg text-sm focus:outlin
- - + +@include('document-templates.partials.preview-modal') @endsection @push('scripts') @@ -888,7 +880,7 @@ function addSectionItem(sectionId) { category: '', item: '', standard: '', - tolerance: '', + tolerance: null, standard_criteria: null, method: '', measurement_type: '', @@ -917,6 +909,112 @@ function updateSectionItem(sectionId, itemId, field, value) { } } + // ── 공차(Tolerance) 구조화 함수들 ── + + function updateToleranceProp(sectionId, itemId, prop, 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 (prop === 'type') { + // 타입 변경 시 초기화 + const defaults = { + symmetric: { type: 'symmetric', value: null }, + asymmetric: { type: 'asymmetric', plus: null, minus: null }, + range: { type: 'range', min: null, max: null }, + limit: { type: 'limit', op: 'lte', value: null }, + }; + item.tolerance = value ? (defaults[value] || null) : null; + } else { + if (!item.tolerance) return; + const numVal = value !== '' ? parseFloat(value) : null; + if (prop === 'op') { + item.tolerance.op = value; + } else { + item.tolerance[prop] = numVal; + } + } + renderSections(); + } + + function renderToleranceInput(sectionId, itemId, tol) { + const tolType = tol?.type || ''; + const selectHtml = ``; + + let fieldsHtml = ''; + if (tolType === 'symmetric') { + fieldsHtml = `
+ ± + +
`; + } else if (tolType === 'asymmetric') { + fieldsHtml = `
+ + + + - + +
`; + } else if (tolType === 'range') { + fieldsHtml = `
+ + ~ + +
`; + } else if (tolType === 'limit') { + fieldsHtml = `
+ + +
`; + } + + return selectHtml + fieldsHtml; + } + + function formatTolerance(tol) { + if (!tol || !tol.type) return '-'; + switch (tol.type) { + case 'symmetric': + return tol.value != null ? `\u00B1${tol.value}` : '-'; + case 'asymmetric': + return (tol.plus != null || tol.minus != null) + ? `+${tol.plus ?? 0} / -${tol.minus ?? 0}` : '-'; + case 'range': + return (tol.min != null || tol.max != null) + ? `${tol.min ?? ''} ~ ${tol.max ?? ''}` : '-'; + case 'limit': { + const opSymbol = { lte: '\u2264', lt: '<', gte: '\u2265', gt: '>' }; + return tol.value != null ? `${opSymbol[tol.op] || '\u2264'}${tol.value}` : '-'; + } + default: return '-'; + } + } + function updateStandardCriteria(sectionId, itemId, field, value) { const section = templateState.sections.find(s => s.id == sectionId); if (!section) return; @@ -993,15 +1091,15 @@ class="flex-1 px-3 py-2 border border-gray-300 rounded-lg text-sm font-medium">
${section.items.length > 0 ? `
- +
- + - - - - + + + + @@ -1020,34 +1118,32 @@ class="w-full px-2 py-1 border border-gray-200 rounded text-xs"> onchange="updateSectionItem('${section.id}', '${item.id}', 'item', this.value)" class="w-full px-2 py-1 border border-gray-200 rounded text-xs"> - - - - - ${f2 ? ` - ` : ''} - `); - } - return `
구분구분 검사항목검사기준공차/범위검사방식측정유형검사기준공차/범위검사방식측정유형 검사주기 관련규정 + -
+ class="w-full px-2 py-1 border border-gray-200 rounded text-xs mb-1"> +
+ class="px-1 py-0.5 border border-gray-200 rounded text-xs text-center" style="min-width:0;width:100%"> - ~ + ~ + class="px-1 py-0.5 border border-gray-200 rounded text-xs text-center" style="min-width:0;width:100%">
- + + ${renderToleranceInput(section.id, item.id, item.tolerance)} - ${templateState.approval_lines.map(l => ``).join('')} - ${templateState.approval_lines.map(l => ``).join('')} -
${escapeHtml(l.name || '-')}
${escapeHtml(l.dept || '')}
`; - }; - - // 기본필드 테이블 생성 - const renderBasicInfo = () => { - if (templateState.basic_fields.length === 0) { - return '

기본필드 미설정

'; - } - const fields = templateState.basic_fields; - const rows = []; - for (let i = 0; i < fields.length; i += 2) { - const f1 = fields[i]; - const f2 = fields[i + 1]; - rows.push(`
${escapeHtml(f1.label || '-')}${escapeHtml(f1.default_value || '(입력)')}${escapeHtml(f2.label || '-')}${escapeHtml(f2.default_value || '(입력)')}
${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) { - // 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('항목'); - const isStd = label.includes('검사기준') || label.includes('기준'); - // 검사항목: 구분+항목명 2칸, 검사기준: 기준+공차 2칸 - if (isItem || isStd) { - headerRow1 += `${escapeHtml(col.label)}`; - } else { - headerRow1 += `${escapeHtml(col.label)}`; - } - } - }); - if (hasComplex) { - return `${headerRow1}${headerRow2}`; - } - return `${headerRow1}`; - }; - - // 검사항목 행 생성 - const renderInspectionRows = () => { - const allItems = []; - templateState.sections.forEach(section => { - if (section.items) { - section.items.forEach(item => allItems.push(item)); - } - }); - if (allItems.length === 0) { - 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 `검사항목이 없습니다.`; - } - - const getMethodName = (code) => { - const m = inspectionMethods.find(im => im.code === code); - return m ? m.name : (code || '-'); - }; - - // 카테고리별 그룹핑 - const rows = []; - let i = 0; - while (i < allItems.length) { - const item = allItems[i]; - const cat = (item.category || '').trim(); - if (cat) { - const grouped = [item]; - while (i + 1 < allItems.length && (allItems[i + 1].category || '').trim() === cat) { - i++; - grouped.push(allItems[i]); - } - rows.push({ type: 'group', category: cat, items: grouped }); - } else { - rows.push({ type: 'single', item: item }); - } - i++; - } - - // 측정치 셀 렌더링 (rowspan 포함, 그룹용) - const renderMeasurementCellsWithRowspan = (col, mType, rowspanN, frequencyN) => { - const rs = ` rowspan="${rowspanN}"`; - 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 Array(cellCount).fill('').map(() => `☐OK ☐NG`).join('') - + (remainder > 0 ? `` : ''); - } else if (mType === 'numeric') { - return Array(cellCount).fill('').map(() => `___`).join('') - + (remainder > 0 ? `` : ''); - } else if (mType === 'single_value') { - return `( 입력 )`; - } else if (mType === 'substitute') { - return `성적서로 대체`; - } - return Array(totalCols).fill('').map(() => `-`).join(''); - }; - - // 측정치 셀 렌더링 헬퍼 (단일 항목용) - 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 Array(cellCount).fill('').map(() => '☐OK ☐NG').join('') - + (remainder > 0 ? `` : ''); - } else if (mType === 'numeric') { - return Array(cellCount).fill('').map(() => '___').join('') - + (remainder > 0 ? `` : ''); - } else if (mType === 'single_value') { - return `( 입력 )`; - } else if (mType === 'substitute') { - return `성적서로 대체`; - } - return Array(totalCols).fill('').map(() => '-').join(''); - }; - - let rowNum = 0; - return rows.map(row => { - rowNum++; - if (row.type === 'single') { - // 단일 항목: 검사항목 colspan=2, 검사기준 colspan=2 - 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 || '', item.frequency_n); - } - const label = (col.label || '').trim(); - if (label === 'NO' || label === 'no') { - return `${rowNum}`; - } else if (label.includes('검사항목') || label.includes('항목')) { - return `${escapeHtml(item.item || '-')}`; - } else if (label.includes('검사기준') || label.includes('기준')) { - return `${formatStandard(item)}`; - } else if (label.includes('검사방')) { - return `${escapeHtml(getMethodName(item.method))}`; - } else if (label.includes('주기')) { - return `${formatFrequency(item)}`; - } else if (label.includes('판정')) { - return `☐`; - } - return `-`; - }).join(''); - return `${cells}`; - } - - // 그룹 항목: 여러 행으로 렌더링 - const n = row.items.length; - return row.items.map((item, itemIdx) => { - let cells = ''; - cells += templateState.columns.map(col => { - if (col.column_type === 'complex' && col.sub_labels && col.sub_labels.length > 0) { - // 측정치: 각 행마다 개별 렌더링 (항목별 frequency_n 반영) - return renderMeasurementCells(col, item.measurement_type || '', item.frequency_n); - } - const label = (col.label || '').trim(); - const rs = itemIdx === 0 ? ` rowspan="${n}"` : ''; - - if (label === 'NO' || label === 'no') { - // NO: 첫 행만, rowspan - return itemIdx === 0 - ? `${rowNum}` - : ''; - } else if (label.includes('검사항목') || label.includes('항목')) { - // 구분(rowspan) + 항목명(각 행) - let catCell = itemIdx === 0 - ? `${escapeHtml(row.category)}` - : ''; - return catCell + `${escapeHtml(item.item || '-')}`; - } else if (label.includes('검사기준') || label.includes('기준')) { - // 기준 + 공차 (각 행 개별) - return `${formatStandard(item)}` - + `${escapeHtml(item.tolerance || '-')}`; - } else if (label.includes('검사방')) { - return `${escapeHtml(getMethodName(item.method))}`; - } else if (label.includes('주기')) { - return `${formatFrequency(item)}`; - } else if (label.includes('판정')) { - return `☐`; - } - return `-`; - }).join(''); - return `${cells}`; - }).join(''); - }).join(''); - }; - - return ` -
- -
-
-
KD
-
${escapeHtml(companyName)}
-
-
-

${escapeHtml(title)}

-
-
${renderApproval()}
-
- - - ${renderBasicInfo()} - - - ${templateState.sections.filter(s => s.image_path).map(s => ` -
-

${escapeHtml(s.title || '검사 기준')}

- ${escapeHtml(s.title)} -
- `).join('')} - - - ${templateState.columns.length > 0 ? ` - - ${renderColumnHeaders()} - ${renderInspectionRows()} -
- ` : '

테이블 컬럼이 설정되지 않았습니다.

'} - - -
-
- - - - - -
${escapeHtml(remarkLabel)}(내용 입력)
-
-
- - - - - - - -
${escapeHtml(judgementLabel)}
- ${judgementOptions.length > 0 ? judgementOptions.map(opt => `${escapeHtml(opt)}`).join('') : '옵션 미설정'} -
-
-
-
- `; - } - // ===== 이미지 업로드 ===== function uploadSectionImage(sectionId, input) { const file = input.files[0]; @@ -1787,7 +1600,8 @@ function formatStandard(item) { return parts.join(' ~ '); } let std = item.standard || '-'; - if (item.tolerance) std += ' (' + item.tolerance + ')'; + const tolStr = formatTolerance(item.tolerance); + if (tolStr !== '-') std += ' (' + tolStr + ')'; return escapeHtml(std); } diff --git a/resources/views/document-templates/index.blade.php b/resources/views/document-templates/index.blade.php index c0027174..a1a4b9bc 100644 --- a/resources/views/document-templates/index.blade.php +++ b/resources/views/document-templates/index.blade.php @@ -78,27 +78,8 @@ class="bg-white rounded-lg shadow-sm overflow-hidden">
- - + + @include('document-templates.partials.preview-modal') @endsection @push('scripts') @@ -249,7 +230,7 @@ function handleTrashedFilter() { }); }; - // 미리보기 + // 미리보기 (공통 buildDocumentPreviewHtml 사용) window.previewTemplate = function(id) { const modal = document.getElementById('preview-modal'); const content = document.getElementById('preview-content'); @@ -265,7 +246,7 @@ function handleTrashedFilter() { .then(response => response.json()) .then(data => { if (data.success) { - content.innerHTML = buildPreviewHtml(data.data); + content.innerHTML = buildDocumentPreviewHtml(data.data); } else { content.innerHTML = '

미리보기를 불러올 수 없습니다.

'; } @@ -276,233 +257,6 @@ function handleTrashedFilter() { }); }; - window.closePreviewModal = function() { - document.getElementById('preview-modal').classList.add('hidden'); - }; - - // ESC로 모달 닫기 - document.addEventListener('keydown', function(e) { - if (e.key === 'Escape') closePreviewModal(); - }); - - function esc(text) { - if (!text) return ''; - const div = document.createElement('div'); - div.textContent = text; - return div.innerHTML; - } - - function fmtStandard(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 esc(std); - } - - function fmtFrequency(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(esc(item.frequency)); - return parts.length > 0 ? parts.join(' / ') : '-'; - } - - function buildPreviewHtml(t) { - const title = t.title || '문서 양식'; - const companyName = t.company_name || ''; - const remarkLabel = t.footer_remark_label || '비고'; - const judgementLabel = t.footer_judgement_label || '종합판정'; - const judgementOptions = (t.footer_judgement_options || []).filter(o => (o || '').trim()); - const approvalLines = t.approval_lines || []; - const basicFields = t.basic_fields || []; - const sections = t.sections || []; - const columns = t.columns || []; - - // 결재란 - const approvalHtml = approvalLines.length === 0 - ? '결재라인 미설정' - : ` - ${approvalLines.map(l => ``).join('')} - ${approvalLines.map(l => ``).join('')} -
${esc(l.name || '-')}
${esc(l.dept || '')}
`; - - // 기본필드 - let basicHtml = ''; - if (basicFields.length > 0) { - const rows = []; - for (let i = 0; i < basicFields.length; i += 2) { - const f1 = basicFields[i], f2 = basicFields[i + 1]; - rows.push(` - ${esc(f1.label || '-')} - ${esc(f1.default_value || '(입력)')} - ${f2 ? `${esc(f2.label || '-')} - ${esc(f2.default_value || '(입력)')}` : ''} - `); - } - basicHtml = `${rows.join('')}
`; - } - - // 전체 항목에서 최대 frequency_n - let maxN = 0; - sections.forEach(s => (s.items || []).forEach(item => { - if (item.frequency_n && item.frequency_n > maxN) maxN = item.frequency_n; - })); - - // 컬럼 헤더 - const renderHeaders = () => { - if (columns.length === 0) return ''; - const hasComplex = columns.some(c => c.column_type === 'complex' && c.sub_labels && c.sub_labels.length > 0); - let h1 = '', h2 = ''; - columns.forEach(col => { - if (col.column_type === 'complex' && col.sub_labels && col.sub_labels.length > 0) { - const cs = Math.max(col.sub_labels.length, maxN); - h1 += `${esc(col.label)}`; - for (let ni = 1; ni <= cs; ni++) h2 += `n${ni}`; - } else { - const label = (col.label || '').trim(); - const isItem = label.includes('검사항목') || label.includes('항목'); - const isStd = label.includes('검사기준') || label.includes('기준'); - if (isItem || isStd) { - h1 += `${esc(col.label)}`; - } else { - h1 += `${esc(col.label)}`; - } - } - }); - return hasComplex ? `${h1}${h2}` : `${h1}`; - }; - - // 측정치 셀 - const mCell = (col, mType, fn) => { - const total = Math.max(col.sub_labels.length, maxN); - const cnt = fn ? fn : total; - const rem = total - cnt; - if (mType === 'checkbox') return Array(cnt).fill('').map(() => '☐OK ☐NG').join('') + (rem > 0 ? `` : ''); - if (mType === 'numeric') return Array(cnt).fill('').map(() => '___').join('') + (rem > 0 ? `` : ''); - if (mType === 'single_value') return `( 입력 )`; - if (mType === 'substitute') return `성적서로 대체`; - return Array(total).fill('').map(() => '-').join(''); - }; - - // 검사항목 행 - const renderRows = () => { - const allItems = []; - sections.forEach(s => (s.items || []).forEach(item => allItems.push(item))); - if (allItems.length === 0) { - const cs = columns.reduce((sum, c) => sum + (c.column_type === 'complex' && c.sub_labels ? Math.max(c.sub_labels.length, maxN) : 1), 0) || 1; - return `검사항목이 없습니다.`; - } - const groups = []; - let i = 0; - while (i < allItems.length) { - const item = allItems[i], cat = (item.category || '').trim(); - if (cat) { - const g = [item]; - while (i + 1 < allItems.length && (allItems[i + 1].category || '').trim() === cat) { i++; g.push(allItems[i]); } - groups.push({ type: 'group', category: cat, items: g }); - } else { - groups.push({ type: 'single', item }); - } - i++; - } - let rowNum = 0; - return groups.map(row => { - rowNum++; - if (row.type === 'single') { - const item = row.item; - return '' + columns.map(col => { - if (col.column_type === 'complex' && col.sub_labels && col.sub_labels.length > 0) return mCell(col, item.measurement_type || '', item.frequency_n); - const label = (col.label || '').trim(); - if (label === 'NO' || label === 'no') return `${rowNum}`; - if (label.includes('검사항목') || label.includes('항목')) return `${esc(item.item || '-')}`; - if (label.includes('검사기준') || label.includes('기준')) return `${fmtStandard(item)}`; - if (label.includes('검사방')) return `${esc(item.method || '-')}`; - if (label.includes('주기')) return `${fmtFrequency(item)}`; - if (label.includes('판정')) return '☐'; - return '-'; - }).join('') + ''; - } - const n = row.items.length; - return row.items.map((item, idx) => { - return '' + columns.map(col => { - if (col.column_type === 'complex' && col.sub_labels && col.sub_labels.length > 0) return mCell(col, item.measurement_type || '', item.frequency_n); - const label = (col.label || '').trim(); - const rs = idx === 0 ? ` rowspan="${n}"` : ''; - if (label === 'NO' || label === 'no') return idx === 0 ? `${rowNum}` : ''; - if (label.includes('검사항목') || label.includes('항목')) { - let catCell = idx === 0 ? `${esc(row.category)}` : ''; - return catCell + `${esc(item.item || '-')}`; - } - if (label.includes('검사기준') || label.includes('기준')) return `${fmtStandard(item)}${esc(item.tolerance || '-')}`; - if (label.includes('검사방')) return `${esc(item.method || '-')}`; - if (label.includes('주기')) return `${fmtFrequency(item)}`; - if (label.includes('판정')) return '☐'; - return '-'; - }).join('') + ''; - }).join(''); - }).join(''); - }; - - // 섹션 이미지 - const imagesHtml = sections.filter(s => s.image_path).map(s => ` -
-

${esc(s.title || '검사 기준')}

- ${esc(s.title)} -
- `).join(''); - - return ` -
-
-
-
KD
-
${esc(companyName)}
-
-
-

${esc(title)}

-
-
${approvalHtml}
-
- ${basicHtml} - ${imagesHtml} - ${columns.length > 0 ? ` - - ${renderHeaders()} - ${renderRows()} -
- ` : '

테이블 컬럼이 설정되지 않았습니다.

'} -
-
- - - - - -
${esc(remarkLabel)}(내용 입력)
-
-
- - - -
${esc(judgementLabel)}
- ${judgementOptions.length > 0 ? judgementOptions.map(o => `${esc(o)}`).join('') : '옵션 미설정'} -
-
-
-
- `; - } - // 활성 토글 window.toggleActive = function(id, buttonEl) { const btn = buttonEl || document.querySelector(`tr[data-template-id="${id}"] button[onclick*="toggleActive"]`); diff --git a/resources/views/document-templates/partials/preview-modal.blade.php b/resources/views/document-templates/partials/preview-modal.blade.php new file mode 100644 index 00000000..0fdb03ee --- /dev/null +++ b/resources/views/document-templates/partials/preview-modal.blade.php @@ -0,0 +1,305 @@ +{{-- 문서 미리보기 공통 모달 + 렌더링 함수 --}} +{{-- 사용법: @include('document-templates.partials.preview-modal') --}} +{{-- JS: buildDocumentPreviewHtml(data) 호출하여 HTML 생성 --}} + + + + +@push('scripts') + +@endpush \ No newline at end of file