feat: [planning-design] 좌표 기반 인쇄 기능 추가 + HTML 내보내기 블록 좌표 배치 개선

This commit is contained in:
김보곤
2026-03-08 00:51:12 +09:00
parent 8ff84e7f94
commit ac5ae6eb05

View File

@@ -1473,6 +1473,8 @@
<div style="margin-left:auto;"></div>
<button style="padding:4px 10px; border:1px solid #e2e8f0; border-radius:6px; font-size:11px; cursor:pointer; background:#fff; color:#374151;"
@click="sbEditMenu()">메뉴 편집</button>
<button style="padding:4px 10px; border:1px solid #e2e8f0; border-radius:6px; font-size:11px; cursor:pointer; background:#fff; color:#374151;"
@click="sbPrintPreview()">🖨 인쇄</button>
<button style="padding:4px 10px; border:1px solid var(--pc-indigo); border-radius:6px; font-size:11px; cursor:pointer; background:var(--pc-indigo); color:#fff;"
@click="sbExportHtml()">HTML 내보내기</button>
</div>
@@ -3607,7 +3609,15 @@ function planningCanvas() {
});
html += '</div><div class="content"><div class="wf">';
if (pg.blocks && pg.blocks.length > 0) {
pg.blocks.forEach(blk => { html += this.sbExportBlock(blk); });
// 캔버스 높이 계산
const maxBottom = Math.max(...pg.blocks.map(b => (b.y || 0) + (b.h || 40)), 400);
html += '<div style="position:relative;min-height:' + maxBottom + 'px;">';
pg.blocks.forEach(blk => {
html += '<div style="position:absolute;left:' + (blk.x||0) + 'px;top:' + (blk.y||0) + 'px;width:' + (blk.w||240) + 'px;min-height:' + (blk.h||40) + 'px;">';
html += this.sbExportBlock(blk);
html += '</div>';
});
html += '</div>';
} else if (pg.wireframeImage) {
html += '<img src="' + pg.wireframeImage + '">';
} else if (pg.wireframeContent) {
@@ -3637,6 +3647,76 @@ function planningCanvas() {
URL.revokeObjectURL(url);
},
sbPrintPreview() {
// HTML 내보내기와 동일하게 생성 후 새 창에서 인쇄
const origExport = this.sbExportHtml.bind(this);
let html = '<!DOCTYPE html><html><head><meta charset="utf-8"><title>' +
(this.sb.docInfo.projectName || 'Storyboard') + ' - 인쇄</title>' +
'<style>body{font-family:Pretendard,-apple-system,sans-serif;margin:0;padding:0;background:#fff;}' +
'.page{width:100%;background:#fff;overflow:hidden;page-break-after:always;border-bottom:1px solid #e2e8f0;}' +
'.page:last-child{border-bottom:none;}' +
'.hdr{display:grid;grid-template-columns:1fr auto auto auto auto auto;border-bottom:2px solid #1e293b;font-size:10px;}' +
'.hdr>div{padding:6px 10px;border-right:1px solid #cbd5e1;}.hdr>div:last-child{border-right:none;}' +
'.lbl{font-size:8px;color:#94a3b8;font-weight:600;}.val{font-size:11px;font-weight:700;color:#1e293b;}' +
'.body{display:flex;min-height:500px;}.menu{width:160px;border-right:1px solid #e2e8f0;padding:12px 0;background:#f8fafc;font-size:11px;}' +
'.menu-logo{padding:8px 12px;font-size:13px;font-weight:800;}.menu-sec{font-size:8px;font-weight:700;color:#94a3b8;padding:4px 12px;}' +
'.menu-item{padding:5px 12px 5px 16px;color:#64748b;}.menu-item.active{color:#4338ca;font-weight:700;background:#eef2ff;border-right:3px solid #4338ca;}' +
'.menu-child{padding-left:28px;font-size:10px;}.menu-child.active{color:#4338ca;font-weight:700;}' +
'.content{flex:1;display:flex;flex-direction:column;min-width:0;}.wf{flex:1;padding:16px;overflow:hidden;}' +
'.desc{border-top:2px solid #1e293b;padding:12px 16px;background:#fafbfc;}' +
'.desc-title{font-size:10px;font-weight:700;margin-bottom:8px;}.desc-item{display:flex;gap:10px;padding:6px 0;border-bottom:1px solid #f1f5f9;font-size:12px;line-height:1.6;}' +
'.desc-num{width:24px;height:24px;border-radius:50%;background:#1e293b;color:#fff;font-size:10px;font-weight:700;display:flex;align-items:center;justify-content:center;flex-shrink:0;}' +
'@media print{@page{size:A4 landscape;margin:8mm;} body{-webkit-print-color-adjust:exact;print-color-adjust:exact;}}</style></head><body>';
this.sb.pages.forEach((pg, idx) => {
html += '<div class="page"><div class="hdr">';
html += '<div><span class="lbl">단위업무명</span><br><span class="val">' + (this.sb.docInfo.unitTask || '-') + '</span></div>';
html += '<div><span class="lbl">버전</span><br><span class="val">' + (this.sb.docInfo.version || '-') + '</span></div>';
html += '<div><span class="lbl">Page</span><br><span class="val">' + (idx + 1) + '</span></div>';
html += '<div><span class="lbl">경로</span><br><span class="val">' + (pg.path || '-') + '</span></div>';
html += '<div><span class="lbl">화면명</span><br><span class="val">' + (pg.screenName || '-') + '</span></div>';
html += '<div><span class="lbl">화면 ID</span><br><span class="val">' + (pg.screenId || '-') + '</span></div>';
html += '</div><div class="body"><div class="menu">';
html += '<div class="menu-logo">' + (this.sb.docInfo.projectName || 'LOGO') + '</div>';
html += '<div class="menu-sec">ERP 메뉴</div>';
this.sb.menuTree.forEach(m => {
const isActive = pg.path && pg.path.startsWith(m.name);
html += '<div class="menu-item' + (isActive ? ' active' : '') + '">' + m.name + '</div>';
(m.children || []).forEach(c => {
const cActive = pg.path && pg.path.includes(c.name);
html += '<div class="menu-item menu-child' + (cActive ? ' active' : '') + '">- ' + c.name + '</div>';
});
});
html += '</div><div class="content"><div class="wf">';
if (pg.blocks && pg.blocks.length > 0) {
const maxBottom = Math.max(...pg.blocks.map(b => (b.y || 0) + (b.h || 40)), 400);
html += '<div style="position:relative;min-height:' + maxBottom + 'px;">';
pg.blocks.forEach(blk => {
html += '<div style="position:absolute;left:' + (blk.x||0) + 'px;top:' + (blk.y||0) + 'px;width:' + (blk.w||240) + 'px;min-height:' + (blk.h||40) + 'px;">';
html += this.sbExportBlock(blk);
html += '</div>';
});
html += '</div>';
}
html += '</div>';
if (pg.descriptions && pg.descriptions.length > 0) {
html += '<div class="desc"><div class="desc-title">Description</div>';
pg.descriptions.forEach((d, di) => {
html += '<div class="desc-item"><div class="desc-num">' + String(di + 1).padStart(2, '0') + '</div>';
html += '<div>' + (d.text || '').replace(/\n/g, '<br>') + '</div></div>';
});
html += '</div>';
}
html += '</div></div></div>';
});
html += '</body></html>';
const printWin = window.open('', '_blank');
printWin.document.write(html);
printWin.document.close();
printWin.onload = () => { printWin.print(); };
},
sbExportBlock(blk) {
const esc = (s) => (s || '').replace(/</g, '&lt;').replace(/>/g, '&gt;');
switch (blk.type) {