fix: [document-templates] 양식 디자이너 미리보기 렌더러 분기 처리

- builder_type=block 템플릿은 buildBlockPreviewHtml() 사용
- 레거시 템플릿은 기존 buildDocumentPreviewHtml() 유지
This commit is contained in:
김보곤
2026-02-28 20:16:37 +09:00
parent 18fb810f81
commit baf1fb5ddf
2 changed files with 155 additions and 1 deletions

View File

@@ -268,7 +268,12 @@ function handleTrashedFilter() {
.then(response => response.json())
.then(data => {
if (data.success) {
content.innerHTML = buildDocumentPreviewHtml(data.data);
// builder_type에 따라 적절한 렌더러 사용
if (data.data.builder_type === 'block') {
content.innerHTML = buildBlockPreviewHtml(data.data);
} else {
content.innerHTML = buildDocumentPreviewHtml(data.data);
}
} else {
content.innerHTML = '<p class="text-center text-red-500 py-8">미리보기를 불러올 수 없습니다.</p>';
}

View File

@@ -122,6 +122,155 @@ function _previewFormatFrequency(item) {
* @param {Array} data.columns - 테이블 컬럼
* @param {Function} [data.methodResolver] - 검사방법 코드→이름 변환 함수 (선택)
*/
/**
* 블록 기반(양식 디자이너) 미리보기 HTML 생성
*
* @param {Object} data - 템플릿 데이터 (builder_type === 'block')
* @param {Object} data.schema - 블록 스키마 { version, page, blocks[] }
*/
function buildBlockPreviewHtml(data) {
const schema = data.schema;
if (!schema || !schema.blocks || schema.blocks.length === 0) {
return '<div class="text-center text-gray-400 py-12">블록이 없습니다. 양식 디자이너에서 블록을 추가하세요.</div>';
}
const page = schema.page || {};
const orientation = page.orientation || 'portrait';
const blocks = schema.blocks;
function esc(text) {
if (!text) return '';
const div = document.createElement('div');
div.textContent = text;
return div.innerHTML;
}
function nl2br(text) {
return (text || '').replace(/\n/g, '<br>');
}
function renderBlock(block) {
const type = block.type || '';
const props = block.props || {};
switch (type) {
case 'heading': {
const level = Math.min(Math.max(parseInt(props.level || 2), 1), 6);
const text = esc(props.text || '');
const align = props.align || 'left';
const sizes = {1:'1.5em',2:'1.25em',3:'1.1em',4:'1em',5:'0.9em',6:'0.85em'};
return `<h${level} style="text-align:${align};font-size:${sizes[level]};font-weight:bold;margin:0.5em 0;">${text}</h${level}>`;
}
case 'paragraph': {
const text = nl2br(esc(props.text || ''));
const align = props.align || 'left';
return `<p style="text-align:${align};margin:0.3em 0;font-size:0.9em;line-height:1.6;">${text}</p>`;
}
case 'table': {
const headers = props.headers || [];
const rows = props.rows || [];
const showHeader = props.showHeader !== false;
let html = '<table style="width:100%;border-collapse:collapse;border:1px solid #333;margin:0.5em 0;font-size:0.85em;">';
if (showHeader && headers.length > 0) {
html += '<thead><tr>';
headers.forEach(h => { html += `<th style="border:1px solid #333;padding:4px 8px;background:#f5f5f5;font-weight:bold;text-align:center;">${esc(h)}</th>`; });
html += '</tr></thead>';
}
html += '<tbody>';
if (rows.length > 0) {
rows.forEach(row => {
html += '<tr>';
(row || []).forEach(cell => { html += `<td style="border:1px solid #333;padding:4px 8px;">${esc(cell || '')}</td>`; });
html += '</tr>';
});
} else {
const colCount = headers.length || 3;
html += '<tr>';
for (let i = 0; i < colCount; i++) html += '<td style="border:1px solid #333;padding:4px 8px;color:#ccc;text-align:center;">(입력)</td>';
html += '</tr>';
}
html += '</tbody></table>';
return html;
}
case 'columns': {
const count = parseInt(props.count || 2);
const children = props.children || [];
let html = '<div style="display:flex;gap:10px;margin:0.5em 0;">';
for (let i = 0; i < count; i++) {
html += '<div style="flex:1;">';
const colChildren = children[i] || [];
colChildren.forEach(child => { html += renderBlock(child); });
html += '</div>';
}
html += '</div>';
return html;
}
case 'divider': {
const style = (props.style || 'solid') === 'dashed' ? 'dashed' : 'solid';
return `<hr style="border:none;border-top:1px ${style} #ccc;margin:0.8em 0;">`;
}
case 'spacer': {
const height = Math.max(parseInt(props.height || 20), 0);
return `<div style="height:${height}px;"></div>`;
}
case 'text_field': {
const label = esc(props.label || '');
const req = props.required ? ' <span style="color:red;">*</span>' : '';
return `<div style="margin:0.3em 0;"><label style="font-size:0.8em;font-weight:bold;display:block;margin-bottom:2px;">${label}${req}</label><div style="border:1px solid #ccc;padding:4px 8px;min-height:24px;font-size:0.9em;color:#ccc;">(입력)</div></div>`;
}
case 'number_field': {
const label = esc(props.label || '');
const unit = props.unit ? ` (${esc(props.unit)})` : '';
return `<div style="margin:0.3em 0;"><label style="font-size:0.8em;font-weight:bold;display:block;margin-bottom:2px;">${label}${unit}</label><div style="border:1px solid #ccc;padding:4px 8px;min-height:24px;font-size:0.9em;color:#ccc;">(숫자)</div></div>`;
}
case 'date_field': {
const label = esc(props.label || '');
return `<div style="margin:0.3em 0;"><label style="font-size:0.8em;font-weight:bold;display:block;margin-bottom:2px;">${label}</label><div style="border:1px solid #ccc;padding:4px 8px;min-height:24px;font-size:0.9em;color:#ccc;">(날짜)</div></div>`;
}
case 'select_field': {
const label = esc(props.label || '');
const options = (props.options || []).map(o => esc(o)).join(' / ');
return `<div style="margin:0.3em 0;"><label style="font-size:0.8em;font-weight:bold;display:block;margin-bottom:2px;">${label}</label><div style="border:1px solid #ccc;padding:4px 8px;min-height:24px;font-size:0.9em;">${options || '<span style="color:#ccc;">(선택)</span>'}</div></div>`;
}
case 'checkbox_field': {
const label = esc(props.label || '');
const options = props.options || [];
let html = `<div style="margin:0.3em 0;"><label style="font-size:0.8em;font-weight:bold;display:block;margin-bottom:2px;">${label}</label><div style="display:flex;gap:12px;flex-wrap:wrap;">`;
options.forEach(opt => { html += `<label style="font-size:0.85em;display:flex;align-items:center;gap:4px;"><input type="checkbox" disabled> ${esc(opt)}</label>`; });
html += '</div></div>';
return html;
}
case 'textarea_field': {
const label = esc(props.label || '');
const rows = Math.max(parseInt(props.rows || 3), 1);
const height = rows * 24;
return `<div style="margin:0.3em 0;"><label style="font-size:0.8em;font-weight:bold;display:block;margin-bottom:2px;">${label}</label><div style="border:1px solid #ccc;padding:4px 8px;min-height:${height}px;font-size:0.9em;color:#ccc;">(장문 입력)</div></div>`;
}
case 'signature_field': {
const label = esc(props.label || '서명');
return `<div style="margin:0.5em 0;"><label style="font-size:0.8em;font-weight:bold;display:block;margin-bottom:2px;">${label}</label><div style="border:2px dashed #ccc;height:60px;display:flex;align-items:center;justify-content:center;font-size:0.8em;color:#999;">서명 영역</div></div>`;
}
default:
return `<div style="color:#999;font-size:0.8em;margin:0.3em 0;">[ ${esc(type)} 블록 ]</div>`;
}
}
let html = `<div style="font-family:'Noto Sans KR',sans-serif;max-width:210mm;margin:0 auto;padding:20mm 15mm;background:white;">`;
blocks.forEach(block => { html += renderBlock(block); });
html += '</div>';
return `
<div class="bg-white p-4" style="max-width:900px;margin:0 auto;">
<div class="flex items-center gap-2 mb-3 pb-2 border-b">
<span class="inline-flex items-center px-2 py-0.5 rounded text-xs font-medium bg-indigo-100 text-indigo-700">양식 디자이너</span>
<span class="text-sm font-medium text-gray-700">${esc(data.name || '')}</span>
<span class="text-xs text-gray-400 ml-auto">${blocks.length}개 블록</span>
</div>
<div class="border rounded-lg overflow-hidden">${html}</div>
</div>
`;
}
function buildDocumentPreviewHtml(data) {
const title = data.title || '문서 양식';
const companyName = data.company_name || '';