refactor:문서양식 미리보기 모달을 공통 partial로 통합
index/edit 페이지에 각각 중복 구현되어 있던 미리보기 렌더링 로직을 partials/preview-modal.blade.php로 통합하여 단일 buildDocumentPreviewHtml() 함수로 관리 Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -2,6 +2,15 @@
|
||||
|
||||
@section('title', $isCreate ? '문서양식 등록' : '문서양식 편집')
|
||||
|
||||
@push('styles')
|
||||
<style>
|
||||
/* number input 스피너 숨김 */
|
||||
input[type=number]::-webkit-inner-spin-button,
|
||||
input[type=number]::-webkit-outer-spin-button { -webkit-appearance: none; margin: 0; }
|
||||
input[type=number] { -moz-appearance: textfield; }
|
||||
</style>
|
||||
@endpush
|
||||
|
||||
@section('content')
|
||||
<div class="max-w-7xl mx-auto">
|
||||
<!-- 헤더 -->
|
||||
@@ -225,25 +234,8 @@ class="w-full px-3 py-1.5 border border-gray-300 rounded-lg text-sm focus:outlin
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 미리보기 모달 -->
|
||||
<div id="preview-modal" class="fixed inset-0 z-50 hidden overflow-y-auto">
|
||||
<div class="flex items-center justify-center min-h-screen p-4">
|
||||
<div class="fixed inset-0 bg-black bg-opacity-50" onclick="closePreviewModal()"></div>
|
||||
<div class="relative bg-white rounded-lg shadow-xl max-w-5xl w-full max-h-[90vh] overflow-hidden">
|
||||
<div class="flex justify-between items-center p-4 border-b">
|
||||
<h3 class="text-lg font-bold text-gray-800">문서 미리보기</h3>
|
||||
<button onclick="closePreviewModal()" class="text-gray-400 hover:text-gray-600">
|
||||
<svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
<div id="preview-content" class="p-4 overflow-auto max-h-[calc(90vh-120px)]">
|
||||
<!-- 미리보기 내용 -->
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- 미리보기 모달 (공통 partial) -->
|
||||
@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 = `<select onchange="updateToleranceProp('${sectionId}', '${itemId}', 'type', this.value)"
|
||||
class="w-full px-1 py-0.5 border border-gray-200 rounded text-xs mb-0.5">
|
||||
<option value="" ${!tolType ? 'selected' : ''}>없음</option>
|
||||
<option value="symmetric" ${tolType === 'symmetric' ? 'selected' : ''}>± 대칭</option>
|
||||
<option value="asymmetric" ${tolType === 'asymmetric' ? 'selected' : ''}>+/- 비대칭</option>
|
||||
<option value="range" ${tolType === 'range' ? 'selected' : ''}>~ 범위</option>
|
||||
<option value="limit" ${tolType === 'limit' ? 'selected' : ''}>한계값</option>
|
||||
</select>`;
|
||||
|
||||
let fieldsHtml = '';
|
||||
if (tolType === 'symmetric') {
|
||||
fieldsHtml = `<div class="flex items-center gap-0.5">
|
||||
<span class="text-xs text-gray-500">±</span>
|
||||
<input type="number" step="any" value="${tol.value ?? ''}" placeholder="0.10"
|
||||
onchange="updateToleranceProp('${sectionId}', '${itemId}', 'value', this.value)"
|
||||
class="flex-1 min-w-0 px-1 py-0.5 border border-gray-200 rounded text-xs">
|
||||
</div>`;
|
||||
} else if (tolType === 'asymmetric') {
|
||||
fieldsHtml = `<div class="flex items-center gap-0.5">
|
||||
<span class="text-xs text-gray-500">+</span>
|
||||
<input type="number" step="any" value="${tol.plus ?? ''}" placeholder="0.20"
|
||||
onchange="updateToleranceProp('${sectionId}', '${itemId}', 'plus', this.value)"
|
||||
class="flex-1 min-w-0 px-1 py-0.5 border border-gray-200 rounded text-xs">
|
||||
<span class="text-xs text-gray-500">-</span>
|
||||
<input type="number" step="any" value="${tol.minus ?? ''}" placeholder="0.10"
|
||||
onchange="updateToleranceProp('${sectionId}', '${itemId}', 'minus', this.value)"
|
||||
class="flex-1 min-w-0 px-1 py-0.5 border border-gray-200 rounded text-xs">
|
||||
</div>`;
|
||||
} else if (tolType === 'range') {
|
||||
fieldsHtml = `<div class="flex items-center gap-0.5">
|
||||
<input type="number" step="any" value="${tol.min ?? ''}" placeholder="min"
|
||||
onchange="updateToleranceProp('${sectionId}', '${itemId}', 'min', this.value)"
|
||||
class="flex-1 min-w-0 px-1 py-0.5 border border-gray-200 rounded text-xs">
|
||||
<span class="text-xs text-gray-400">~</span>
|
||||
<input type="number" step="any" value="${tol.max ?? ''}" placeholder="max"
|
||||
onchange="updateToleranceProp('${sectionId}', '${itemId}', 'max', this.value)"
|
||||
class="flex-1 min-w-0 px-1 py-0.5 border border-gray-200 rounded text-xs">
|
||||
</div>`;
|
||||
} else if (tolType === 'limit') {
|
||||
fieldsHtml = `<div class="flex items-center gap-0.5">
|
||||
<select onchange="updateToleranceProp('${sectionId}', '${itemId}', 'op', this.value)"
|
||||
class="px-0.5 py-0.5 border border-gray-200 rounded text-xs" style="width:2.5rem;flex-shrink:0">
|
||||
<option value="lte" ${tol.op === 'lte' ? 'selected' : ''}>≤</option>
|
||||
<option value="lt" ${tol.op === 'lt' ? 'selected' : ''}><</option>
|
||||
<option value="gte" ${tol.op === 'gte' ? 'selected' : ''}>≥</option>
|
||||
<option value="gt" ${tol.op === 'gt' ? 'selected' : ''}>></option>
|
||||
</select>
|
||||
<input type="number" step="any" value="${tol.value ?? ''}" placeholder="0.05"
|
||||
onchange="updateToleranceProp('${sectionId}', '${itemId}', 'value', this.value)"
|
||||
class="flex-1 min-w-0 px-1 py-0.5 border border-gray-200 rounded text-xs">
|
||||
</div>`;
|
||||
}
|
||||
|
||||
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">
|
||||
<div class="p-4">
|
||||
${section.items.length > 0 ? `
|
||||
<div class="overflow-x-auto">
|
||||
<table class="w-full text-sm" style="table-layout:fixed">
|
||||
<table class="text-sm" style="table-layout:fixed;width:1035px">
|
||||
<thead class="bg-gray-100">
|
||||
<tr>
|
||||
<th class="px-2 py-2 text-left text-xs font-medium text-gray-500" style="width:70px">구분</th>
|
||||
<th class="px-2 py-2 text-left text-xs font-medium text-gray-500" style="width:65px">구분</th>
|
||||
<th class="px-2 py-2 text-left text-xs font-medium text-gray-500" style="width:130px">검사항목</th>
|
||||
<th class="px-2 py-2 text-left text-xs font-medium text-gray-500" style="width:220px">검사기준</th>
|
||||
<th class="px-2 py-2 text-left text-xs font-medium text-gray-500" style="width:70px">공차/범위</th>
|
||||
<th class="px-2 py-2 text-left text-xs font-medium text-gray-500" style="width:100px">검사방식</th>
|
||||
<th class="px-2 py-2 text-left text-xs font-medium text-gray-500" style="width:90px">측정유형</th>
|
||||
<th class="px-2 py-2 text-left text-xs font-medium text-gray-500" style="width:280px">검사기준</th>
|
||||
<th class="px-2 py-2 text-left text-xs font-medium text-gray-500" style="width:120px">공차/범위</th>
|
||||
<th class="px-2 py-2 text-left text-xs font-medium text-gray-500" style="width:110px">검사방식</th>
|
||||
<th class="px-2 py-2 text-left text-xs font-medium text-gray-500" style="width:100px">측정유형</th>
|
||||
<th class="px-2 py-2 text-left text-xs font-medium text-gray-500" style="width:120px">검사주기</th>
|
||||
<th class="px-2 py-2 text-left text-xs font-medium text-gray-500" style="width:80px">관련규정</th>
|
||||
<th class="px-2 py-2" style="width:30px"></th>
|
||||
@@ -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">
|
||||
</td>
|
||||
<td class="px-1 py-1">
|
||||
<td class="px-1 py-1" style="overflow:hidden">
|
||||
<input type="text" value="${escapeHtml(item.standard)}" placeholder="기준값 (표시용)"
|
||||
onchange="updateSectionItem('${section.id}', '${item.id}', 'standard', this.value)"
|
||||
class="w-full px-2 py-1 border border-gray-200 rounded text-xs mb-1" style="width:15rem">
|
||||
<div class="flex items-center gap-0.5">
|
||||
class="w-full px-2 py-1 border border-gray-200 rounded text-xs mb-1">
|
||||
<div style="display:grid;grid-template-columns:1fr auto 8px 1fr auto;align-items:center;gap:2px">
|
||||
<input type="number" step="any" value="${item.standard_criteria?.min ?? ''}" placeholder="min"
|
||||
onchange="updateStandardCriteria('${section.id}', '${item.id}', 'min', this.value)"
|
||||
class="px-0.5 py-0.5 border border-gray-200 rounded text-xs text-center" style="width:3.5rem;padding:0.125rem;">
|
||||
class="px-1 py-0.5 border border-gray-200 rounded text-xs text-center" style="min-width:0;width:100%">
|
||||
<select onchange="updateStandardCriteria('${section.id}', '${item.id}', 'min_op', this.value)"
|
||||
class="px-0 py-0.5 border border-gray-200 rounded text-xs" style="width:3.5rem;padding:0.125rem;">
|
||||
class="px-0 py-0.5 border border-gray-200 rounded text-xs">
|
||||
<option value="gte" ${(item.standard_criteria?.min_op || 'gte') === 'gte' ? 'selected' : ''}>이상</option>
|
||||
<option value="gt" ${item.standard_criteria?.min_op === 'gt' ? 'selected' : ''}>초과</option>
|
||||
</select>
|
||||
<span class="text-xs text-gray-400">~</span>
|
||||
<span class="text-xs text-gray-400 text-center">~</span>
|
||||
<input type="number" step="any" value="${item.standard_criteria?.max ?? ''}" placeholder="max"
|
||||
onchange="updateStandardCriteria('${section.id}', '${item.id}', 'max', this.value)"
|
||||
class="px-0.5 py-0.5 border border-gray-200 rounded text-xs text-center" style="width:3.5rem;padding:0.125rem;">
|
||||
class="px-1 py-0.5 border border-gray-200 rounded text-xs text-center" style="min-width:0;width:100%">
|
||||
<select onchange="updateStandardCriteria('${section.id}', '${item.id}', 'max_op', this.value)"
|
||||
class="px-0 py-0.5 border border-gray-200 rounded text-xs" style="width:3.5rem;padding:0.125rem;">
|
||||
class="px-0 py-0.5 border border-gray-200 rounded text-xs">
|
||||
<option value="lte" ${(item.standard_criteria?.max_op || 'lte') === 'lte' ? 'selected' : ''}>이하</option>
|
||||
<option value="lt" ${item.standard_criteria?.max_op === 'lt' ? 'selected' : ''}>미만</option>
|
||||
</select>
|
||||
</div>
|
||||
</td>
|
||||
<td class="px-1 py-1">
|
||||
<input type="text" value="${escapeHtml(item.tolerance || '')}" placeholder="±0.10"
|
||||
onchange="updateSectionItem('${section.id}', '${item.id}', 'tolerance', this.value)"
|
||||
class="w-full px-2 py-1 border border-gray-200 rounded text-xs">
|
||||
<td class="px-1 py-1" style="overflow:hidden">
|
||||
${renderToleranceInput(section.id, item.id, item.tolerance)}
|
||||
</td>
|
||||
<td class="px-1 py-1">
|
||||
<select onchange="onMethodChange('${section.id}', '${item.id}', this.value)"
|
||||
@@ -1294,314 +1390,31 @@ function saveTemplate() {
|
||||
});
|
||||
}
|
||||
|
||||
// ===== 미리보기 =====
|
||||
// ===== 미리보기 (공통 buildDocumentPreviewHtml 사용) =====
|
||||
function openPreviewModal() {
|
||||
const getMethodName = (code) => {
|
||||
const m = inspectionMethods.find(im => im.code === code);
|
||||
return m ? m.name : (code || '-');
|
||||
};
|
||||
|
||||
const data = {
|
||||
title: document.getElementById('title').value,
|
||||
company_name: document.getElementById('company_name').value,
|
||||
footer_remark_label: document.getElementById('footer_remark_label').value,
|
||||
footer_judgement_label: document.getElementById('footer_judgement_label').value,
|
||||
footer_judgement_options: templateState.footer_judgement_options,
|
||||
approval_lines: templateState.approval_lines,
|
||||
basic_fields: templateState.basic_fields,
|
||||
sections: templateState.sections,
|
||||
columns: templateState.columns,
|
||||
methodResolver: getMethodName
|
||||
};
|
||||
|
||||
const content = document.getElementById('preview-content');
|
||||
content.innerHTML = generatePreviewHtml();
|
||||
content.innerHTML = buildDocumentPreviewHtml(data);
|
||||
document.getElementById('preview-modal').classList.remove('hidden');
|
||||
}
|
||||
|
||||
function closePreviewModal() {
|
||||
document.getElementById('preview-modal').classList.add('hidden');
|
||||
}
|
||||
|
||||
function generatePreviewHtml() {
|
||||
const title = document.getElementById('title').value || '문서 양식';
|
||||
const companyName = document.getElementById('company_name').value || '회사명';
|
||||
const remarkLabel = document.getElementById('footer_remark_label').value || '비고';
|
||||
const judgementLabel = document.getElementById('footer_judgement_label').value || '종합판정';
|
||||
const judgementOptions = templateState.footer_judgement_options.filter(o => o.trim() !== '');
|
||||
|
||||
// 결재란 생성
|
||||
const renderApproval = () => {
|
||||
if (templateState.approval_lines.length === 0) {
|
||||
return '<span class="text-xs text-gray-400">결재라인 미설정</span>';
|
||||
}
|
||||
return `<table class="border-collapse text-xs">
|
||||
<tr>${templateState.approval_lines.map(l => `<td class="border border-gray-400 px-3 py-1 bg-gray-100 text-center font-medium">${escapeHtml(l.name || '-')}</td>`).join('')}</tr>
|
||||
<tr>${templateState.approval_lines.map(l => `<td class="border border-gray-400 px-3 py-1 text-center"><div class="text-gray-400 text-xs">${escapeHtml(l.dept || '')}</div><div class="h-6"></div></td>`).join('')}</tr>
|
||||
</table>`;
|
||||
};
|
||||
|
||||
// 기본필드 테이블 생성
|
||||
const renderBasicInfo = () => {
|
||||
if (templateState.basic_fields.length === 0) {
|
||||
return '<p class="text-xs text-gray-400 mb-4">기본필드 미설정</p>';
|
||||
}
|
||||
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(`<tr>
|
||||
<td class="border border-gray-400 px-2 py-1.5 bg-gray-100 font-medium" style="width:100px">${escapeHtml(f1.label || '-')}</td>
|
||||
<td class="border border-gray-400 px-2 py-1.5 text-gray-400">${escapeHtml(f1.default_value || '(입력)')}</td>
|
||||
${f2 ? `<td class="border border-gray-400 px-2 py-1.5 bg-gray-100 font-medium" style="width:100px">${escapeHtml(f2.label || '-')}</td>
|
||||
<td class="border border-gray-400 px-2 py-1.5 text-gray-400">${escapeHtml(f2.default_value || '(입력)')}</td>` : '<td class="border border-gray-400 px-2 py-1.5" colspan="2"></td>'}
|
||||
</tr>`);
|
||||
}
|
||||
return `<table class="w-full border-collapse text-xs mb-4">${rows.join('')}</table>`;
|
||||
};
|
||||
|
||||
// 전체 항목에서 최대 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 += `<th class="border border-gray-400 px-2 py-1.5 bg-gray-100" colspan="${colSpan}">${escapeHtml(col.label)}</th>`;
|
||||
for (let ni = 1; ni <= colSpan; ni++) {
|
||||
headerRow2 += `<th class="border border-gray-400 px-1 py-1 bg-gray-100" style="width:40px">n${ni}</th>`;
|
||||
}
|
||||
} else {
|
||||
const label = (col.label || '').trim();
|
||||
const isItem = label.includes('검사항목') || label.includes('항목');
|
||||
const isStd = label.includes('검사기준') || label.includes('기준');
|
||||
// 검사항목: 구분+항목명 2칸, 검사기준: 기준+공차 2칸
|
||||
if (isItem || isStd) {
|
||||
headerRow1 += `<th class="border border-gray-400 px-2 py-1.5 bg-gray-100" colspan="2" ${hasComplex ? 'rowspan="2"' : ''}>${escapeHtml(col.label)}</th>`;
|
||||
} else {
|
||||
headerRow1 += `<th class="border border-gray-400 px-2 py-1.5 bg-gray-100" ${hasComplex ? 'rowspan="2"' : ''} style="width:${col.width || '80px'}">${escapeHtml(col.label)}</th>`;
|
||||
}
|
||||
}
|
||||
});
|
||||
if (hasComplex) {
|
||||
return `<tr>${headerRow1}</tr><tr>${headerRow2}</tr>`;
|
||||
}
|
||||
return `<tr>${headerRow1}</tr>`;
|
||||
};
|
||||
|
||||
// 검사항목 행 생성
|
||||
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 `<tr><td colspan="${colSpan}" class="text-center py-4 text-gray-400 border border-gray-400">검사항목이 없습니다.</td></tr>`;
|
||||
}
|
||||
|
||||
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(() => `<td class="border border-gray-400 px-1 py-1.5 text-center"${rs}><span style="font-size:11px">☐OK ☐NG</span></td>`).join('')
|
||||
+ (remainder > 0 ? `<td class="border border-gray-400 px-1 py-1.5"${rs} colspan="${remainder}"></td>` : '');
|
||||
} else if (mType === 'numeric') {
|
||||
return Array(cellCount).fill('').map(() => `<td class="border border-gray-400 px-1 py-1.5 text-center"${rs}><span style="color:#ccc;font-size:10px">___</span></td>`).join('')
|
||||
+ (remainder > 0 ? `<td class="border border-gray-400 px-1 py-1.5"${rs} colspan="${remainder}"></td>` : '');
|
||||
} else if (mType === 'single_value') {
|
||||
return `<td class="border border-gray-400 px-1 py-1.5 text-center" colspan="${totalCols}"${rs}><span style="color:#ccc;font-size:10px">( 입력 )</span></td>`;
|
||||
} else if (mType === 'substitute') {
|
||||
return `<td class="border border-gray-400 px-1 py-1.5 text-center text-gray-500" colspan="${totalCols}"${rs} style="font-size:10px">성적서로 대체</td>`;
|
||||
}
|
||||
return Array(totalCols).fill('').map(() => `<td class="border border-gray-400 px-1 py-1.5 text-center text-gray-300"${rs}>-</td>`).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(() => '<td class="border border-gray-400 px-1 py-1.5 text-center"><span style="font-size:11px">☐OK ☐NG</span></td>').join('')
|
||||
+ (remainder > 0 ? `<td class="border border-gray-400 px-1 py-1.5" colspan="${remainder}"></td>` : '');
|
||||
} else if (mType === 'numeric') {
|
||||
return Array(cellCount).fill('').map(() => '<td class="border border-gray-400 px-1 py-1.5 text-center"><span style="color:#ccc;font-size:10px">___</span></td>').join('')
|
||||
+ (remainder > 0 ? `<td class="border border-gray-400 px-1 py-1.5" colspan="${remainder}"></td>` : '');
|
||||
} else if (mType === 'single_value') {
|
||||
return `<td class="border border-gray-400 px-1 py-1.5 text-center" colspan="${totalCols}"><span style="color:#ccc;font-size:10px">( 입력 )</span></td>`;
|
||||
} else if (mType === 'substitute') {
|
||||
return `<td class="border border-gray-400 px-1 py-1.5 text-center text-gray-500" colspan="${totalCols}" style="font-size:10px">성적서로 대체</td>`;
|
||||
}
|
||||
return Array(totalCols).fill('').map(() => '<td class="border border-gray-400 px-1 py-1.5 text-center text-gray-300">-</td>').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 `<td class="border border-gray-400 px-2 py-1.5 text-center">${rowNum}</td>`;
|
||||
} else if (label.includes('검사항목') || label.includes('항목')) {
|
||||
return `<td class="border border-gray-400 px-2 py-1.5 text-center" colspan="2">${escapeHtml(item.item || '-')}</td>`;
|
||||
} else if (label.includes('검사기준') || label.includes('기준')) {
|
||||
return `<td class="border border-gray-400 px-2 py-1.5 text-center" colspan="2">${formatStandard(item)}</td>`;
|
||||
} else if (label.includes('검사방')) {
|
||||
return `<td class="border border-gray-400 px-2 py-1.5 text-center">${escapeHtml(getMethodName(item.method))}</td>`;
|
||||
} else if (label.includes('주기')) {
|
||||
return `<td class="border border-gray-400 px-2 py-1.5 text-center">${formatFrequency(item)}</td>`;
|
||||
} else if (label.includes('판정')) {
|
||||
return `<td class="border border-gray-400 px-2 py-1.5 text-center">☐</td>`;
|
||||
}
|
||||
return `<td class="border border-gray-400 px-2 py-1.5 text-center">-</td>`;
|
||||
}).join('');
|
||||
return `<tr>${cells}</tr>`;
|
||||
}
|
||||
|
||||
// 그룹 항목: 여러 행으로 렌더링
|
||||
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
|
||||
? `<td class="border border-gray-400 px-2 py-1.5 text-center"${rs}>${rowNum}</td>`
|
||||
: '';
|
||||
} else if (label.includes('검사항목') || label.includes('항목')) {
|
||||
// 구분(rowspan) + 항목명(각 행)
|
||||
let catCell = itemIdx === 0
|
||||
? `<td class="border border-gray-400 px-2 py-1.5 text-center font-medium"${rs}>${escapeHtml(row.category)}</td>`
|
||||
: '';
|
||||
return catCell + `<td class="border border-gray-400 px-2 py-1.5 text-center">${escapeHtml(item.item || '-')}</td>`;
|
||||
} else if (label.includes('검사기준') || label.includes('기준')) {
|
||||
// 기준 + 공차 (각 행 개별)
|
||||
return `<td class="border border-gray-400 px-2 py-1.5 text-center">${formatStandard(item)}</td>`
|
||||
+ `<td class="border border-gray-400 px-2 py-1.5 text-center">${escapeHtml(item.tolerance || '-')}</td>`;
|
||||
} else if (label.includes('검사방')) {
|
||||
return `<td class="border border-gray-400 px-2 py-1.5 text-center">${escapeHtml(getMethodName(item.method))}</td>`;
|
||||
} else if (label.includes('주기')) {
|
||||
return `<td class="border border-gray-400 px-2 py-1.5 text-center">${formatFrequency(item)}</td>`;
|
||||
} else if (label.includes('판정')) {
|
||||
return `<td class="border border-gray-400 px-2 py-1.5 text-center">☐</td>`;
|
||||
}
|
||||
return `<td class="border border-gray-400 px-2 py-1.5 text-center">-</td>`;
|
||||
}).join('');
|
||||
return `<tr>${cells}</tr>`;
|
||||
}).join('');
|
||||
}).join('');
|
||||
};
|
||||
|
||||
return `
|
||||
<div class="bg-white p-6" style="font-family: 'Malgun Gothic', sans-serif; font-size: 12px; max-width: 900px; margin: 0 auto;">
|
||||
<!-- 헤더: 로고 + 제목 + 결재란 -->
|
||||
<div class="flex justify-between items-start mb-4">
|
||||
<div class="text-center" style="width: 80px;">
|
||||
<div class="text-2xl font-bold">KD</div>
|
||||
<div class="text-xs">${escapeHtml(companyName)}</div>
|
||||
</div>
|
||||
<div class="flex-1 text-center">
|
||||
<h1 class="text-xl font-bold tracking-widest">${escapeHtml(title)}</h1>
|
||||
</div>
|
||||
<div>${renderApproval()}</div>
|
||||
</div>
|
||||
|
||||
<!-- 기본필드 정보 -->
|
||||
${renderBasicInfo()}
|
||||
|
||||
<!-- 검사 기준 이미지 -->
|
||||
${templateState.sections.filter(s => s.image_path).map(s => `
|
||||
<div class="mb-4 text-center">
|
||||
<p class="text-xs font-medium text-gray-600 mb-1">${escapeHtml(s.title || '검사 기준')}</p>
|
||||
<img src="/storage/${s.image_path}" alt="${escapeHtml(s.title)}" class="max-h-40 mx-auto border rounded">
|
||||
</div>
|
||||
`).join('')}
|
||||
|
||||
<!-- 검사 데이터 테이블 -->
|
||||
${templateState.columns.length > 0 ? `
|
||||
<table class="w-full border-collapse text-xs">
|
||||
<thead>${renderColumnHeaders()}</thead>
|
||||
<tbody>${renderInspectionRows()}</tbody>
|
||||
</table>
|
||||
` : '<p class="text-xs text-gray-400 text-center py-4">테이블 컬럼이 설정되지 않았습니다.</p>'}
|
||||
|
||||
<!-- Footer: 비고 + 종합판정 -->
|
||||
<div class="mt-4 flex gap-4">
|
||||
<div class="flex-1">
|
||||
<table class="w-full border-collapse text-xs">
|
||||
<tr>
|
||||
<td class="border border-gray-400 px-3 py-2 bg-gray-100 font-medium" style="width:100px">${escapeHtml(remarkLabel)}</td>
|
||||
<td class="border border-gray-400 px-3 py-2 text-gray-400">(내용 입력)</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
<div>
|
||||
<table class="border-collapse text-xs">
|
||||
<tr>
|
||||
<td class="border border-gray-400 px-4 py-2 bg-gray-100 font-medium">${escapeHtml(judgementLabel)}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="border border-gray-400 px-4 py-3 text-center">
|
||||
${judgementOptions.length > 0 ? judgementOptions.map(opt => `<span class="inline-block mx-1 px-2 py-0.5 border border-gray-300 rounded text-gray-500">${escapeHtml(opt)}</span>`).join('') : '<span class="text-gray-400">옵션 미설정</span>'}
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
// ===== 이미지 업로드 =====
|
||||
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);
|
||||
}
|
||||
|
||||
|
||||
@@ -78,27 +78,8 @@ class="bg-white rounded-lg shadow-sm overflow-hidden">
|
||||
<div class="animate-spin rounded-full h-12 w-12 border-b-2 border-blue-600"></div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- 미리보기 모달 -->
|
||||
<div id="preview-modal" class="fixed inset-0 z-50 hidden overflow-y-auto">
|
||||
<div class="flex items-center justify-center min-h-screen p-4">
|
||||
<div class="fixed inset-0 bg-black bg-opacity-50" onclick="closePreviewModal()"></div>
|
||||
<div class="relative bg-white rounded-lg shadow-xl max-w-5xl w-full max-h-[90vh] overflow-hidden">
|
||||
<div class="flex justify-between items-center p-4 border-b">
|
||||
<h3 class="text-lg font-bold text-gray-800">문서 미리보기</h3>
|
||||
<button onclick="closePreviewModal()" class="text-gray-400 hover:text-gray-600">
|
||||
<svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
<div id="preview-content" class="p-4 overflow-auto max-h-[calc(90vh-120px)]">
|
||||
<div class="flex justify-center items-center p-12">
|
||||
<div class="animate-spin rounded-full h-8 w-8 border-b-2 border-blue-600"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- 미리보기 모달 (공통 partial) -->
|
||||
@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 = '<p class="text-center text-red-500 py-8">미리보기를 불러올 수 없습니다.</p>';
|
||||
}
|
||||
@@ -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
|
||||
? '<span class="text-xs text-gray-400">결재라인 미설정</span>'
|
||||
: `<table class="border-collapse text-xs">
|
||||
<tr>${approvalLines.map(l => `<td class="border border-gray-400 px-3 py-1 bg-gray-100 text-center font-medium">${esc(l.name || '-')}</td>`).join('')}</tr>
|
||||
<tr>${approvalLines.map(l => `<td class="border border-gray-400 px-3 py-1 text-center"><div class="text-gray-400 text-xs">${esc(l.dept || '')}</div><div class="h-6"></div></td>`).join('')}</tr>
|
||||
</table>`;
|
||||
|
||||
// 기본필드
|
||||
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(`<tr>
|
||||
<td class="border border-gray-400 px-2 py-1.5 bg-gray-100 font-medium" style="width:100px">${esc(f1.label || '-')}</td>
|
||||
<td class="border border-gray-400 px-2 py-1.5 text-gray-400">${esc(f1.default_value || '(입력)')}</td>
|
||||
${f2 ? `<td class="border border-gray-400 px-2 py-1.5 bg-gray-100 font-medium" style="width:100px">${esc(f2.label || '-')}</td>
|
||||
<td class="border border-gray-400 px-2 py-1.5 text-gray-400">${esc(f2.default_value || '(입력)')}</td>` : '<td class="border border-gray-400 px-2 py-1.5" colspan="2"></td>'}
|
||||
</tr>`);
|
||||
}
|
||||
basicHtml = `<table class="w-full border-collapse text-xs mb-4">${rows.join('')}</table>`;
|
||||
}
|
||||
|
||||
// 전체 항목에서 최대 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 += `<th class="border border-gray-400 px-2 py-1.5 bg-gray-100" colspan="${cs}">${esc(col.label)}</th>`;
|
||||
for (let ni = 1; ni <= cs; ni++) h2 += `<th class="border border-gray-400 px-1 py-1 bg-gray-100" style="width:40px">n${ni}</th>`;
|
||||
} else {
|
||||
const label = (col.label || '').trim();
|
||||
const isItem = label.includes('검사항목') || label.includes('항목');
|
||||
const isStd = label.includes('검사기준') || label.includes('기준');
|
||||
if (isItem || isStd) {
|
||||
h1 += `<th class="border border-gray-400 px-2 py-1.5 bg-gray-100" colspan="2" ${hasComplex ? 'rowspan="2"' : ''}>${esc(col.label)}</th>`;
|
||||
} else {
|
||||
h1 += `<th class="border border-gray-400 px-2 py-1.5 bg-gray-100" ${hasComplex ? 'rowspan="2"' : ''} style="width:${col.width || '80px'}">${esc(col.label)}</th>`;
|
||||
}
|
||||
}
|
||||
});
|
||||
return hasComplex ? `<tr>${h1}</tr><tr>${h2}</tr>` : `<tr>${h1}</tr>`;
|
||||
};
|
||||
|
||||
// 측정치 셀
|
||||
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(() => '<td class="border border-gray-400 px-1 py-1.5 text-center"><span style="font-size:11px">☐OK ☐NG</span></td>').join('') + (rem > 0 ? `<td class="border border-gray-400 px-1 py-1.5" colspan="${rem}"></td>` : '');
|
||||
if (mType === 'numeric') return Array(cnt).fill('').map(() => '<td class="border border-gray-400 px-1 py-1.5 text-center"><span style="color:#ccc;font-size:10px">___</span></td>').join('') + (rem > 0 ? `<td class="border border-gray-400 px-1 py-1.5" colspan="${rem}"></td>` : '');
|
||||
if (mType === 'single_value') return `<td class="border border-gray-400 px-1 py-1.5 text-center" colspan="${total}"><span style="color:#ccc;font-size:10px">( 입력 )</span></td>`;
|
||||
if (mType === 'substitute') return `<td class="border border-gray-400 px-1 py-1.5 text-center text-gray-500" colspan="${total}" style="font-size:10px">성적서로 대체</td>`;
|
||||
return Array(total).fill('').map(() => '<td class="border border-gray-400 px-1 py-1.5 text-center text-gray-300">-</td>').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 `<tr><td colspan="${cs}" class="text-center py-4 text-gray-400 border border-gray-400">검사항목이 없습니다.</td></tr>`;
|
||||
}
|
||||
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 '<tr>' + 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 `<td class="border border-gray-400 px-2 py-1.5 text-center">${rowNum}</td>`;
|
||||
if (label.includes('검사항목') || label.includes('항목')) return `<td class="border border-gray-400 px-2 py-1.5 text-center" colspan="2">${esc(item.item || '-')}</td>`;
|
||||
if (label.includes('검사기준') || label.includes('기준')) return `<td class="border border-gray-400 px-2 py-1.5 text-center" colspan="2">${fmtStandard(item)}</td>`;
|
||||
if (label.includes('검사방')) return `<td class="border border-gray-400 px-2 py-1.5 text-center">${esc(item.method || '-')}</td>`;
|
||||
if (label.includes('주기')) return `<td class="border border-gray-400 px-2 py-1.5 text-center">${fmtFrequency(item)}</td>`;
|
||||
if (label.includes('판정')) return '<td class="border border-gray-400 px-2 py-1.5 text-center">☐</td>';
|
||||
return '<td class="border border-gray-400 px-2 py-1.5 text-center">-</td>';
|
||||
}).join('') + '</tr>';
|
||||
}
|
||||
const n = row.items.length;
|
||||
return row.items.map((item, idx) => {
|
||||
return '<tr>' + 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 ? `<td class="border border-gray-400 px-2 py-1.5 text-center"${rs}>${rowNum}</td>` : '';
|
||||
if (label.includes('검사항목') || label.includes('항목')) {
|
||||
let catCell = idx === 0 ? `<td class="border border-gray-400 px-2 py-1.5 text-center font-medium"${rs}>${esc(row.category)}</td>` : '';
|
||||
return catCell + `<td class="border border-gray-400 px-2 py-1.5 text-center">${esc(item.item || '-')}</td>`;
|
||||
}
|
||||
if (label.includes('검사기준') || label.includes('기준')) return `<td class="border border-gray-400 px-2 py-1.5 text-center">${fmtStandard(item)}</td><td class="border border-gray-400 px-2 py-1.5 text-center">${esc(item.tolerance || '-')}</td>`;
|
||||
if (label.includes('검사방')) return `<td class="border border-gray-400 px-2 py-1.5 text-center">${esc(item.method || '-')}</td>`;
|
||||
if (label.includes('주기')) return `<td class="border border-gray-400 px-2 py-1.5 text-center">${fmtFrequency(item)}</td>`;
|
||||
if (label.includes('판정')) return '<td class="border border-gray-400 px-2 py-1.5 text-center">☐</td>';
|
||||
return '<td class="border border-gray-400 px-2 py-1.5 text-center">-</td>';
|
||||
}).join('') + '</tr>';
|
||||
}).join('');
|
||||
}).join('');
|
||||
};
|
||||
|
||||
// 섹션 이미지
|
||||
const imagesHtml = sections.filter(s => s.image_path).map(s => `
|
||||
<div class="mb-4 text-center">
|
||||
<p class="text-xs font-medium text-gray-600 mb-1">${esc(s.title || '검사 기준')}</p>
|
||||
<img src="/storage/${s.image_path}" alt="${esc(s.title)}" class="max-h-40 mx-auto border rounded">
|
||||
</div>
|
||||
`).join('');
|
||||
|
||||
return `
|
||||
<div class="bg-white p-6" style="font-family: 'Malgun Gothic', sans-serif; font-size: 12px; max-width: 900px; margin: 0 auto;">
|
||||
<div class="flex justify-between items-start mb-4">
|
||||
<div class="text-center" style="width: 80px;">
|
||||
<div class="text-2xl font-bold">KD</div>
|
||||
<div class="text-xs">${esc(companyName)}</div>
|
||||
</div>
|
||||
<div class="flex-1 text-center">
|
||||
<h1 class="text-xl font-bold tracking-widest">${esc(title)}</h1>
|
||||
</div>
|
||||
<div>${approvalHtml}</div>
|
||||
</div>
|
||||
${basicHtml}
|
||||
${imagesHtml}
|
||||
${columns.length > 0 ? `
|
||||
<table class="w-full border-collapse text-xs">
|
||||
<thead>${renderHeaders()}</thead>
|
||||
<tbody>${renderRows()}</tbody>
|
||||
</table>
|
||||
` : '<p class="text-xs text-gray-400 text-center py-4">테이블 컬럼이 설정되지 않았습니다.</p>'}
|
||||
<div class="mt-4 flex gap-4">
|
||||
<div class="flex-1">
|
||||
<table class="w-full border-collapse text-xs">
|
||||
<tr>
|
||||
<td class="border border-gray-400 px-3 py-2 bg-gray-100 font-medium" style="width:100px">${esc(remarkLabel)}</td>
|
||||
<td class="border border-gray-400 px-3 py-2 text-gray-400">(내용 입력)</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
<div>
|
||||
<table class="border-collapse text-xs">
|
||||
<tr><td class="border border-gray-400 px-4 py-2 bg-gray-100 font-medium">${esc(judgementLabel)}</td></tr>
|
||||
<tr><td class="border border-gray-400 px-4 py-3 text-center">
|
||||
${judgementOptions.length > 0 ? judgementOptions.map(o => `<span class="inline-block mx-1 px-2 py-0.5 border border-gray-300 rounded text-gray-500">${esc(o)}</span>`).join('') : '<span class="text-gray-400">옵션 미설정</span>'}
|
||||
</td></tr>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
// 활성 토글
|
||||
window.toggleActive = function(id, buttonEl) {
|
||||
const btn = buttonEl || document.querySelector(`tr[data-template-id="${id}"] button[onclick*="toggleActive"]`);
|
||||
|
||||
@@ -0,0 +1,305 @@
|
||||
{{-- 문서 미리보기 공통 모달 + 렌더링 함수 --}}
|
||||
{{-- 사용법: @include('document-templates.partials.preview-modal') --}}
|
||||
{{-- JS: buildDocumentPreviewHtml(data) 호출하여 HTML 생성 --}}
|
||||
|
||||
<!-- 미리보기 모달 -->
|
||||
<div id="preview-modal" class="fixed inset-0 z-50 hidden overflow-y-auto">
|
||||
<div class="flex items-center justify-center min-h-screen p-4">
|
||||
<div class="fixed inset-0 bg-black bg-opacity-50" onclick="closePreviewModal()"></div>
|
||||
<div class="relative bg-white rounded-lg shadow-xl max-w-5xl w-full max-h-[90vh] overflow-hidden">
|
||||
<div class="flex justify-between items-center p-4 border-b">
|
||||
<h3 class="text-lg font-bold text-gray-800">문서 미리보기</h3>
|
||||
<button onclick="closePreviewModal()" class="text-gray-400 hover:text-gray-600">
|
||||
<svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
<div id="preview-content" class="p-4 overflow-auto max-h-[calc(90vh-120px)]">
|
||||
<!-- 미리보기 내용 -->
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@push('scripts')
|
||||
<script>
|
||||
// ===== 공통 미리보기 함수 =====
|
||||
|
||||
function closePreviewModal() {
|
||||
document.getElementById('preview-modal').classList.add('hidden');
|
||||
}
|
||||
|
||||
// ESC로 모달 닫기
|
||||
document.addEventListener('keydown', function(e) {
|
||||
if (e.key === 'Escape') closePreviewModal();
|
||||
});
|
||||
|
||||
// HTML 이스케이프
|
||||
function _previewEsc(text) {
|
||||
if (!text) return '';
|
||||
const div = document.createElement('div');
|
||||
div.textContent = text;
|
||||
return div.innerHTML;
|
||||
}
|
||||
|
||||
// 공차 포맷팅
|
||||
function _previewFormatTolerance(tol) {
|
||||
if (!tol) return '-';
|
||||
// 문자열인 경우 (Index API 응답)
|
||||
if (typeof tol === 'string') return tol || '-';
|
||||
// 객체인 경우 (Edit templateState)
|
||||
if (!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 _previewFormatStandard(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 || '-';
|
||||
const tolStr = _previewFormatTolerance(item.tolerance);
|
||||
if (tolStr !== '-') std += ' (' + tolStr + ')';
|
||||
return _previewEsc(std);
|
||||
}
|
||||
|
||||
// 주기 포맷팅
|
||||
function _previewFormatFrequency(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(_previewEsc(item.frequency));
|
||||
return parts.length > 0 ? parts.join(' / ') : '-';
|
||||
}
|
||||
|
||||
/**
|
||||
* 문서 미리보기 HTML 생성 (공통)
|
||||
*
|
||||
* @param {Object} data - 미리보기 데이터
|
||||
* @param {string} data.title - 문서 제목
|
||||
* @param {string} data.company_name - 회사명
|
||||
* @param {string} data.footer_remark_label - 비고 라벨
|
||||
* @param {string} data.footer_judgement_label - 종합판정 라벨
|
||||
* @param {Array} data.footer_judgement_options - 판정 옵션
|
||||
* @param {Array} data.approval_lines - 결재라인 [{name, dept}]
|
||||
* @param {Array} data.basic_fields - 기본필드 [{label, default_value}]
|
||||
* @param {Array} data.sections - 검사 섹션 [{title, image_path, items}]
|
||||
* @param {Array} data.columns - 테이블 컬럼
|
||||
* @param {Function} [data.methodResolver] - 검사방법 코드→이름 변환 함수 (선택)
|
||||
*/
|
||||
function buildDocumentPreviewHtml(data) {
|
||||
const title = data.title || '문서 양식';
|
||||
const companyName = data.company_name || '';
|
||||
const remarkLabel = data.footer_remark_label || '비고';
|
||||
const judgementLabel = data.footer_judgement_label || '종합판정';
|
||||
const judgementOptions = (data.footer_judgement_options || []).filter(o => (o || '').trim());
|
||||
const approvalLines = data.approval_lines || [];
|
||||
const basicFields = data.basic_fields || [];
|
||||
const sections = data.sections || [];
|
||||
const columns = data.columns || [];
|
||||
const methodResolver = data.methodResolver || function(code) { return code || '-'; };
|
||||
|
||||
// 결재란
|
||||
const approvalHtml = approvalLines.length === 0
|
||||
? '<span class="text-xs text-gray-400">결재라인 미설정</span>'
|
||||
: `<table class="border-collapse text-xs">
|
||||
<tr>${approvalLines.map(l => `<td class="border border-gray-400 px-3 py-1 bg-gray-100 text-center font-medium">${_previewEsc(l.name || '-')}</td>`).join('')}</tr>
|
||||
<tr>${approvalLines.map(l => `<td class="border border-gray-400 px-3 py-1 text-center"><div class="text-gray-400 text-xs">${_previewEsc(l.dept || '')}</div><div class="h-6"></div></td>`).join('')}</tr>
|
||||
</table>`;
|
||||
|
||||
// 기본필드
|
||||
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(`<tr>
|
||||
<td class="border border-gray-400 px-2 py-1.5 bg-gray-100 font-medium" style="width:100px">${_previewEsc(f1.label || '-')}</td>
|
||||
<td class="border border-gray-400 px-2 py-1.5 text-gray-400">${_previewEsc(f1.default_value || '(입력)')}</td>
|
||||
${f2 ? `<td class="border border-gray-400 px-2 py-1.5 bg-gray-100 font-medium" style="width:100px">${_previewEsc(f2.label || '-')}</td>
|
||||
<td class="border border-gray-400 px-2 py-1.5 text-gray-400">${_previewEsc(f2.default_value || '(입력)')}</td>` : '<td class="border border-gray-400 px-2 py-1.5" colspan="2"></td>'}
|
||||
</tr>`);
|
||||
}
|
||||
basicHtml = `<table class="w-full border-collapse text-xs mb-4">${rows.join('')}</table>`;
|
||||
}
|
||||
|
||||
// 전체 항목에서 최대 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 += `<th class="border border-gray-400 px-2 py-1.5 bg-gray-100" colspan="${cs}">${_previewEsc(col.label)}</th>`;
|
||||
for (let ni = 1; ni <= cs; ni++) h2 += `<th class="border border-gray-400 px-1 py-1 bg-gray-100" style="width:40px">n${ni}</th>`;
|
||||
} else {
|
||||
const label = (col.label || '').trim();
|
||||
const isItem = label.includes('검사항목') || label.includes('항목');
|
||||
const isStd = label.includes('검사기준') || label.includes('기준');
|
||||
if (isItem || isStd) {
|
||||
h1 += `<th class="border border-gray-400 px-2 py-1.5 bg-gray-100" colspan="2" ${hasComplex ? 'rowspan="2"' : ''}>${_previewEsc(col.label)}</th>`;
|
||||
} else {
|
||||
h1 += `<th class="border border-gray-400 px-2 py-1.5 bg-gray-100" ${hasComplex ? 'rowspan="2"' : ''} style="width:${col.width || '80px'}">${_previewEsc(col.label)}</th>`;
|
||||
}
|
||||
}
|
||||
});
|
||||
return hasComplex ? `<tr>${h1}</tr><tr>${h2}</tr>` : `<tr>${h1}</tr>`;
|
||||
};
|
||||
|
||||
// 측정치 셀
|
||||
const mCell = (col, mType, frequencyN) => {
|
||||
const total = Math.max(col.sub_labels.length, maxN);
|
||||
const cnt = frequencyN ? frequencyN : total;
|
||||
const rem = total - cnt;
|
||||
if (mType === 'checkbox') return Array(cnt).fill('').map(() => '<td class="border border-gray-400 px-1 py-1.5 text-center"><span style="font-size:11px">☐OK ☐NG</span></td>').join('') + (rem > 0 ? `<td class="border border-gray-400 px-1 py-1.5" colspan="${rem}"></td>` : '');
|
||||
if (mType === 'numeric') return Array(cnt).fill('').map(() => '<td class="border border-gray-400 px-1 py-1.5 text-center"><span style="color:#ccc;font-size:10px">___</span></td>').join('') + (rem > 0 ? `<td class="border border-gray-400 px-1 py-1.5" colspan="${rem}"></td>` : '');
|
||||
if (mType === 'single_value') return `<td class="border border-gray-400 px-1 py-1.5 text-center" colspan="${total}"><span style="color:#ccc;font-size:10px">( 입력 )</span></td>`;
|
||||
if (mType === 'substitute') return `<td class="border border-gray-400 px-1 py-1.5 text-center text-gray-500" colspan="${total}" style="font-size:10px">성적서로 대체</td>`;
|
||||
return Array(total).fill('').map(() => '<td class="border border-gray-400 px-1 py-1.5 text-center text-gray-300">-</td>').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 `<tr><td colspan="${cs}" class="text-center py-4 text-gray-400 border border-gray-400">검사항목이 없습니다.</td></tr>`;
|
||||
}
|
||||
|
||||
// 카테고리별 그룹핑
|
||||
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 '<tr>' + 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 `<td class="border border-gray-400 px-2 py-1.5 text-center">${rowNum}</td>`;
|
||||
if (label.includes('검사항목') || label.includes('항목')) return `<td class="border border-gray-400 px-2 py-1.5 text-center" colspan="2">${_previewEsc(item.item || '-')}</td>`;
|
||||
if (label.includes('검사기준') || label.includes('기준')) return `<td class="border border-gray-400 px-2 py-1.5 text-center" colspan="2">${_previewFormatStandard(item)}</td>`;
|
||||
if (label.includes('검사방')) return `<td class="border border-gray-400 px-2 py-1.5 text-center">${_previewEsc(methodResolver(item.method))}</td>`;
|
||||
if (label.includes('주기')) return `<td class="border border-gray-400 px-2 py-1.5 text-center">${_previewFormatFrequency(item)}</td>`;
|
||||
if (label.includes('판정')) return '<td class="border border-gray-400 px-2 py-1.5 text-center">☐</td>';
|
||||
return '<td class="border border-gray-400 px-2 py-1.5 text-center">-</td>';
|
||||
}).join('') + '</tr>';
|
||||
}
|
||||
|
||||
// 그룹 항목
|
||||
const n = row.items.length;
|
||||
return row.items.map((item, idx) => {
|
||||
return '<tr>' + 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 ? `<td class="border border-gray-400 px-2 py-1.5 text-center"${rs}>${rowNum}</td>` : '';
|
||||
if (label.includes('검사항목') || label.includes('항목')) {
|
||||
let catCell = idx === 0 ? `<td class="border border-gray-400 px-2 py-1.5 text-center font-medium"${rs}>${_previewEsc(row.category)}</td>` : '';
|
||||
return catCell + `<td class="border border-gray-400 px-2 py-1.5 text-center">${_previewEsc(item.item || '-')}</td>`;
|
||||
}
|
||||
if (label.includes('검사기준') || label.includes('기준')) return `<td class="border border-gray-400 px-2 py-1.5 text-center">${_previewFormatStandard(item)}</td><td class="border border-gray-400 px-2 py-1.5 text-center">${_previewEsc(_previewFormatTolerance(item.tolerance))}</td>`;
|
||||
if (label.includes('검사방')) return `<td class="border border-gray-400 px-2 py-1.5 text-center">${_previewEsc(methodResolver(item.method))}</td>`;
|
||||
if (label.includes('주기')) return `<td class="border border-gray-400 px-2 py-1.5 text-center">${_previewFormatFrequency(item)}</td>`;
|
||||
if (label.includes('판정')) return '<td class="border border-gray-400 px-2 py-1.5 text-center">☐</td>';
|
||||
return '<td class="border border-gray-400 px-2 py-1.5 text-center">-</td>';
|
||||
}).join('') + '</tr>';
|
||||
}).join('');
|
||||
}).join('');
|
||||
};
|
||||
|
||||
// 섹션 이미지
|
||||
const imagesHtml = sections.filter(s => s.image_path).map(s => `
|
||||
<div class="mb-4 text-center">
|
||||
<p class="text-xs font-medium text-gray-600 mb-1">${_previewEsc(s.title || '검사 기준')}</p>
|
||||
<img src="/storage/${s.image_path}" alt="${_previewEsc(s.title)}" class="max-h-40 mx-auto border rounded">
|
||||
</div>
|
||||
`).join('');
|
||||
|
||||
return `
|
||||
<div class="bg-white p-6" style="font-family: 'Malgun Gothic', sans-serif; font-size: 12px; max-width: 900px; margin: 0 auto;">
|
||||
<div class="flex justify-between items-start mb-4">
|
||||
<div class="text-center" style="width: 80px;">
|
||||
<div class="text-2xl font-bold">KD</div>
|
||||
<div class="text-xs">${_previewEsc(companyName)}</div>
|
||||
</div>
|
||||
<div class="flex-1 text-center">
|
||||
<h1 class="text-xl font-bold tracking-widest">${_previewEsc(title)}</h1>
|
||||
</div>
|
||||
<div>${approvalHtml}</div>
|
||||
</div>
|
||||
${basicHtml}
|
||||
${imagesHtml}
|
||||
${columns.length > 0 ? `
|
||||
<table class="w-full border-collapse text-xs">
|
||||
<thead>${renderHeaders()}</thead>
|
||||
<tbody>${renderRows()}</tbody>
|
||||
</table>
|
||||
` : '<p class="text-xs text-gray-400 text-center py-4">테이블 컬럼이 설정되지 않았습니다.</p>'}
|
||||
<div class="mt-4 flex gap-4">
|
||||
<div class="flex-1">
|
||||
<table class="w-full border-collapse text-xs">
|
||||
<tr>
|
||||
<td class="border border-gray-400 px-3 py-2 bg-gray-100 font-medium" style="width:100px">${_previewEsc(remarkLabel)}</td>
|
||||
<td class="border border-gray-400 px-3 py-2 text-gray-400">(내용 입력)</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
<div>
|
||||
<table class="border-collapse text-xs">
|
||||
<tr><td class="border border-gray-400 px-4 py-2 bg-gray-100 font-medium">${_previewEsc(judgementLabel)}</td></tr>
|
||||
<tr><td class="border border-gray-400 px-4 py-3 text-center">
|
||||
${judgementOptions.length > 0 ? judgementOptions.map(o => `<span class="inline-block mx-1 px-2 py-0.5 border border-gray-300 rounded text-gray-500">${_previewEsc(o)}</span>`).join('') : '<span class="text-gray-400">옵션 미설정</span>'}
|
||||
</td></tr>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
</script>
|
||||
@endpush
|
||||
Reference in New Issue
Block a user