feat: [planning-design] 좌표 기반 인쇄 기능 추가 + HTML 내보내기 블록 좌표 배치 개선
This commit is contained in:
@@ -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, '<').replace(/>/g, '>');
|
||||
switch (blk.type) {
|
||||
|
||||
Reference in New Issue
Block a user