feat: [planning-design] 스토리보드 뷰 통합 완성
- loadProject()에서 sb 데이터 복원 추가 - newProject()에서 sb 초기화 추가 - init()에서 sbInitPages() 호출 추가
This commit is contained in:
@@ -490,6 +490,137 @@
|
||||
}
|
||||
.pc-filter-clear:hover { text-decoration: underline; }
|
||||
|
||||
/* ===== Storyboard View ===== */
|
||||
.sb-wrap { flex: 1; display: flex; flex-direction: column; overflow: hidden; background: #e5e7eb; }
|
||||
.sb-topbar {
|
||||
display: flex; align-items: center; gap: 8px; padding: 8px 16px;
|
||||
background: #fff; border-bottom: 1px solid #e2e8f0; flex-shrink: 0;
|
||||
}
|
||||
.sb-topbar label { font-size: 10px; font-weight: 600; color: #94a3b8; }
|
||||
.sb-topbar input, .sb-topbar select {
|
||||
padding: 4px 8px; border: 1px solid #e2e8f0; border-radius: 6px;
|
||||
font-size: 12px; outline: none;
|
||||
}
|
||||
.sb-topbar input:focus { border-color: var(--pc-indigo); }
|
||||
.sb-topbar-sep { width: 1px; height: 24px; background: #e2e8f0; }
|
||||
.sb-pages-nav {
|
||||
display: flex; align-items: center; gap: 4px;
|
||||
font-size: 12px; font-weight: 600; color: #374151;
|
||||
}
|
||||
.sb-pages-nav button {
|
||||
width: 28px; height: 28px; border-radius: 6px; border: 1px solid #e2e8f0;
|
||||
background: #fff; cursor: pointer; display: flex; align-items: center; justify-content: center;
|
||||
font-size: 14px; color: #64748b;
|
||||
}
|
||||
.sb-pages-nav button:hover { background: #f1f5f9; }
|
||||
.sb-body { flex: 1; display: flex; overflow: hidden; }
|
||||
.sb-page-list {
|
||||
width: 140px; flex-shrink: 0; background: #f1f5f9; border-right: 1px solid #e2e8f0;
|
||||
overflow-y: auto; padding: 8px;
|
||||
}
|
||||
.sb-page-thumb {
|
||||
padding: 6px 8px; border-radius: 6px; cursor: pointer; margin-bottom: 4px;
|
||||
font-size: 10px; color: #64748b; border: 2px solid transparent;
|
||||
background: #fff; transition: all 0.15s;
|
||||
}
|
||||
.sb-page-thumb:hover { border-color: #cbd5e1; }
|
||||
.sb-page-thumb.active { border-color: var(--pc-indigo); background: #eef2ff; color: #4338ca; }
|
||||
.sb-page-thumb-num { font-weight: 700; font-size: 11px; color: #374151; }
|
||||
.sb-editor { flex: 1; overflow: auto; padding: 24px; display: flex; justify-content: center; }
|
||||
|
||||
/* Storyboard Page (A4-like) */
|
||||
.sb-page {
|
||||
width: 1100px; min-height: 750px; background: #fff; border-radius: 4px;
|
||||
box-shadow: 0 2px 12px rgba(0,0,0,0.1); display: flex; flex-direction: column;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.sb-page-header {
|
||||
display: grid; grid-template-columns: 1fr auto auto auto auto auto;
|
||||
border-bottom: 2px solid #1e293b; font-size: 10px;
|
||||
}
|
||||
.sb-page-header > div {
|
||||
padding: 6px 10px; border-right: 1px solid #cbd5e1;
|
||||
display: flex; flex-direction: column; justify-content: center;
|
||||
}
|
||||
.sb-page-header > div:last-child { border-right: none; }
|
||||
.sb-page-header .label { font-size: 8px; color: #94a3b8; font-weight: 600; text-transform: uppercase; }
|
||||
.sb-page-header .value { font-size: 11px; font-weight: 700; color: #1e293b; }
|
||||
.sb-page-header .value input {
|
||||
border: none; outline: none; font-size: 11px; font-weight: 700; color: #1e293b;
|
||||
width: 100%; background: transparent;
|
||||
}
|
||||
.sb-page-header .value input:focus { border-bottom: 1px solid var(--pc-indigo); }
|
||||
.sb-page-body { display: flex; flex: 1; min-height: 0; }
|
||||
|
||||
/* Left Menu Panel */
|
||||
.sb-menu-panel {
|
||||
width: 160px; flex-shrink: 0; border-right: 1px solid #e2e8f0;
|
||||
padding: 12px 0; font-size: 11px; overflow-y: auto; background: #f8fafc;
|
||||
}
|
||||
.sb-menu-section { font-size: 8px; font-weight: 700; color: #94a3b8; padding: 4px 12px; text-transform: uppercase; }
|
||||
.sb-menu-item {
|
||||
padding: 5px 12px 5px 16px; color: #64748b; cursor: default;
|
||||
font-size: 11px; line-height: 1.4;
|
||||
}
|
||||
.sb-menu-item.active { color: #4338ca; font-weight: 700; background: #eef2ff; border-right: 3px solid #4338ca; }
|
||||
.sb-menu-child { padding-left: 28px; font-size: 10px; }
|
||||
.sb-menu-child.active { color: #4338ca; font-weight: 700; }
|
||||
.sb-menu-logo { padding: 8px 12px; font-size: 13px; font-weight: 800; color: #1e293b; letter-spacing: -0.5px; }
|
||||
|
||||
/* Main Content + Description */
|
||||
.sb-content-area { flex: 1; display: flex; flex-direction: column; min-width: 0; }
|
||||
.sb-wireframe {
|
||||
flex: 1; padding: 16px; min-height: 300px; position: relative;
|
||||
}
|
||||
.sb-wireframe-placeholder {
|
||||
border: 2px dashed #d1d5db; border-radius: 8px; height: 100%; min-height: 280px;
|
||||
display: flex; align-items: center; justify-content: center; flex-direction: column;
|
||||
color: #94a3b8; font-size: 12px; gap: 8px; cursor: pointer; transition: all 0.15s;
|
||||
}
|
||||
.sb-wireframe-placeholder:hover { border-color: var(--pc-indigo); background: #fafafe; }
|
||||
.sb-wireframe-content {
|
||||
width: 100%; min-height: 280px; border: 1px solid #e2e8f0; border-radius: 8px;
|
||||
padding: 16px; font-size: 13px; line-height: 1.7; color: #374151;
|
||||
outline: none; overflow: auto;
|
||||
}
|
||||
.sb-wireframe-content:focus { border-color: var(--pc-indigo); }
|
||||
.sb-wireframe-img { max-width: 100%; border-radius: 8px; }
|
||||
.sb-desc-panel {
|
||||
border-top: 2px solid #1e293b; padding: 12px 16px; background: #fafbfc;
|
||||
max-height: 260px; overflow-y: auto;
|
||||
}
|
||||
.sb-desc-title {
|
||||
font-size: 10px; font-weight: 700; color: #1e293b; margin-bottom: 8px;
|
||||
text-transform: uppercase; letter-spacing: 0.5px;
|
||||
}
|
||||
.sb-desc-item {
|
||||
display: flex; gap: 10px; padding: 6px 0; border-bottom: 1px solid #f1f5f9;
|
||||
font-size: 12px; line-height: 1.6;
|
||||
}
|
||||
.sb-desc-item:last-child { border-bottom: none; }
|
||||
.sb-desc-num {
|
||||
width: 24px; height: 24px; border-radius: 50%; flex-shrink: 0;
|
||||
background: #1e293b; color: #fff; font-size: 10px; font-weight: 700;
|
||||
display: flex; align-items: center; justify-content: center;
|
||||
}
|
||||
.sb-desc-text { flex: 1; color: #374151; }
|
||||
.sb-desc-text textarea {
|
||||
width: 100%; border: none; outline: none; font-size: 12px; line-height: 1.6;
|
||||
color: #374151; resize: none; background: transparent; min-height: 20px;
|
||||
}
|
||||
.sb-desc-text textarea:focus { background: #fff; border-radius: 4px; }
|
||||
.sb-desc-add {
|
||||
display: flex; align-items: center; gap: 6px; padding: 8px 0;
|
||||
font-size: 11px; color: var(--pc-indigo); cursor: pointer; font-weight: 500;
|
||||
}
|
||||
.sb-desc-add:hover { color: #4f46e5; }
|
||||
.sb-desc-remove {
|
||||
width: 20px; height: 20px; border: none; background: transparent; color: #cbd5e1;
|
||||
cursor: pointer; font-size: 14px; border-radius: 4px; display: flex;
|
||||
align-items: center; justify-content: center; flex-shrink: 0;
|
||||
}
|
||||
.sb-desc-remove:hover { color: #ef4444; background: #fef2f2; }
|
||||
|
||||
/* Phase Swimlane (Timeline View) */
|
||||
.pc-swimlane {
|
||||
position: absolute; top: 0;
|
||||
@@ -533,6 +664,7 @@
|
||||
<button class="pc-view-tab" :class="{ active: viewMode === 'flow' }" @click="switchView('flow')">플로우</button>
|
||||
<button class="pc-view-tab" :class="{ active: viewMode === 'kanban' }" @click="switchView('kanban')">칸반</button>
|
||||
<button class="pc-view-tab" :class="{ active: viewMode === 'list' }" @click="switchView('list')">리스트</button>
|
||||
<button class="pc-view-tab" :class="{ active: viewMode === 'storyboard' }" @click="switchView('storyboard')" style="color:#f59e0b;">스토리보드</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -947,6 +1079,157 @@
|
||||
<div x-show="filteredNodes.length === 0" class="text-center text-gray-400 text-sm py-16">노드가 없습니다</div>
|
||||
</div>
|
||||
|
||||
{{-- Storyboard View --}}
|
||||
<div class="sb-wrap" x-show="viewMode === 'storyboard'">
|
||||
{{-- Storyboard Top Bar --}}
|
||||
<div class="sb-topbar">
|
||||
<label>프로젝트</label>
|
||||
<input type="text" x-model="sb.docInfo.projectName" placeholder="프로젝트명" style="width:200px;" @input="autoSave()">
|
||||
<div class="sb-topbar-sep"></div>
|
||||
<label>단위업무</label>
|
||||
<input type="text" x-model="sb.docInfo.unitTask" placeholder="단위업무명" style="width:120px;" @input="autoSave()">
|
||||
<div class="sb-topbar-sep"></div>
|
||||
<label>버전</label>
|
||||
<input type="text" x-model="sb.docInfo.version" placeholder="D1.0" style="width:60px;" @input="autoSave()">
|
||||
<div class="sb-topbar-sep"></div>
|
||||
<div class="sb-pages-nav">
|
||||
<button @click="sbPrevPage()" title="이전 페이지"><</button>
|
||||
<span x-text="(sb.currentPageIndex + 1) + ' / ' + sb.pages.length"></span>
|
||||
<button @click="sbNextPage()" title="다음 페이지">></button>
|
||||
</div>
|
||||
<div class="sb-topbar-sep"></div>
|
||||
<button style="padding:4px 10px; border:1px solid #e2e8f0; border-radius:6px; font-size:11px; cursor:pointer; background:#fff; color:#374151;"
|
||||
@click="sbAddPage()">+ 페이지 추가</button>
|
||||
<button style="padding:4px 10px; border:1px solid #e2e8f0; border-radius:6px; font-size:11px; cursor:pointer; background:#fff; color:#ef4444;"
|
||||
x-show="sb.pages.length > 1"
|
||||
@click="sbDeletePage()">페이지 삭제</button>
|
||||
<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 var(--pc-indigo); border-radius:6px; font-size:11px; cursor:pointer; background:var(--pc-indigo); color:#fff;"
|
||||
@click="sbExportHtml()">HTML 내보내기</button>
|
||||
</div>
|
||||
|
||||
<div class="sb-body">
|
||||
{{-- Page List (thumbnail) --}}
|
||||
<div class="sb-page-list">
|
||||
<template x-for="(pg, idx) in sb.pages" :key="pg.id">
|
||||
<div class="sb-page-thumb" :class="{ active: sb.currentPageIndex === idx }" @click="sb.currentPageIndex = idx">
|
||||
<div class="sb-page-thumb-num" x-text="(idx + 1) + '.'"></div>
|
||||
<div x-text="pg.screenName || '(화면명)'" style="overflow:hidden; text-overflow:ellipsis; white-space:nowrap;"></div>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
|
||||
{{-- Page Editor --}}
|
||||
<div class="sb-editor">
|
||||
<template x-if="sbCurrentPage">
|
||||
<div class="sb-page">
|
||||
{{-- Page Header --}}
|
||||
<div class="sb-page-header">
|
||||
<div>
|
||||
<span class="label">단위업무명</span>
|
||||
<span class="value" x-text="sb.docInfo.unitTask || '-'"></span>
|
||||
</div>
|
||||
<div>
|
||||
<span class="label">버전</span>
|
||||
<span class="value" x-text="sb.docInfo.version || '-'"></span>
|
||||
</div>
|
||||
<div>
|
||||
<span class="label">Page</span>
|
||||
<span class="value" x-text="sb.currentPageIndex + 1"></span>
|
||||
</div>
|
||||
<div>
|
||||
<span class="label">경로</span>
|
||||
<span class="value"><input type="text" x-model="sbCurrentPage.path" placeholder="품질관리 > 제품검사관리" @input="autoSave()"></span>
|
||||
</div>
|
||||
<div>
|
||||
<span class="label">화면명</span>
|
||||
<span class="value"><input type="text" x-model="sbCurrentPage.screenName" placeholder="제품검사 목록" @input="autoSave()"></span>
|
||||
</div>
|
||||
<div>
|
||||
<span class="label">화면 ID</span>
|
||||
<span class="value"><input type="text" x-model="sbCurrentPage.screenId" placeholder="QM-001" @input="autoSave()"></span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="sb-page-body">
|
||||
{{-- Left Menu --}}
|
||||
<div class="sb-menu-panel">
|
||||
<div class="sb-menu-logo" x-text="sb.docInfo.projectName || 'LOGO'"></div>
|
||||
<div class="sb-menu-section">ERP 메뉴</div>
|
||||
<template x-for="menu in sb.menuTree" :key="menu.name">
|
||||
<div>
|
||||
<div class="sb-menu-item" :class="{ active: sbCurrentPage.path && sbCurrentPage.path.startsWith(menu.name) }" x-text="menu.name"></div>
|
||||
<template x-for="child in (menu.children || [])" :key="child.name">
|
||||
<div class="sb-menu-item sb-menu-child"
|
||||
:class="{ active: sbCurrentPage.path && sbCurrentPage.path.includes(child.name) }"
|
||||
x-text="'- ' + child.name"></div>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
|
||||
{{-- Content + Description --}}
|
||||
<div class="sb-content-area">
|
||||
{{-- Wireframe Area --}}
|
||||
<div class="sb-wireframe">
|
||||
<template x-if="!sbCurrentPage.wireframeContent && !sbCurrentPage.wireframeImage">
|
||||
<div class="sb-wireframe-placeholder" @click="$refs.sbImageInput?.click()">
|
||||
<svg style="width:32px;height:32px;color:#cbd5e1;" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M4 16l4.586-4.586a2 2 0 012.828 0L16 16m-2-2l1.586-1.586a2 2 0 012.828 0L20 14m-6-6h.01M6 20h12a2 2 0 002-2V6a2 2 0 00-2-2H6a2 2 0 00-2 2v12a2 2 0 002 2z"/></svg>
|
||||
<span>이미지를 업로드하거나 클릭하여 텍스트 입력</span>
|
||||
<div style="display:flex; gap:8px; margin-top:8px;">
|
||||
<button style="padding:4px 12px; border:1px solid var(--pc-indigo); border-radius:6px; font-size:11px; color:var(--pc-indigo); background:#fff; cursor:pointer;"
|
||||
@click.stop="sbCurrentPage.wireframeContent = ' '">텍스트로 작성</button>
|
||||
<button style="padding:4px 12px; border:1px solid var(--pc-indigo); border-radius:6px; font-size:11px; color:#fff; background:var(--pc-indigo); cursor:pointer;"
|
||||
@click.stop="$refs.sbImageInput.click()">이미지 업로드</button>
|
||||
</div>
|
||||
<input type="file" accept="image/*" x-ref="sbImageInput" style="display:none;" @change="sbUploadImage($event)">
|
||||
</div>
|
||||
</template>
|
||||
<template x-if="sbCurrentPage.wireframeImage">
|
||||
<div style="position:relative;">
|
||||
<img class="sb-wireframe-img" :src="sbCurrentPage.wireframeImage" alt="wireframe">
|
||||
<button style="position:absolute; top:4px; right:4px; width:24px; height:24px; border-radius:50%; border:none; background:rgba(0,0,0,0.5); color:#fff; cursor:pointer; font-size:12px;"
|
||||
@click="sbCurrentPage.wireframeImage = ''; autoSave();">×</button>
|
||||
</div>
|
||||
</template>
|
||||
<template x-if="sbCurrentPage.wireframeContent && !sbCurrentPage.wireframeImage">
|
||||
<div>
|
||||
<div class="sb-wireframe-content" contenteditable="true"
|
||||
@blur="sbCurrentPage.wireframeContent = $event.target.innerHTML; autoSave();"
|
||||
x-html="sbCurrentPage.wireframeContent"></div>
|
||||
<div style="display:flex; gap:4px; margin-top:4px;">
|
||||
<button style="font-size:10px; color:#94a3b8; cursor:pointer; border:none; background:none;"
|
||||
@click="$refs.sbImageInput2?.click()">이미지로 교체</button>
|
||||
<input type="file" accept="image/*" x-ref="sbImageInput2" style="display:none;" @change="sbUploadImage($event)">
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
|
||||
{{-- Description Panel --}}
|
||||
<div class="sb-desc-panel">
|
||||
<div class="sb-desc-title">Description</div>
|
||||
<template x-for="(desc, idx) in (sbCurrentPage.descriptions || [])" :key="idx">
|
||||
<div class="sb-desc-item">
|
||||
<div class="sb-desc-num" x-text="String(idx + 1).padStart(2, '0')"></div>
|
||||
<div class="sb-desc-text">
|
||||
<textarea x-model="desc.text" rows="2" placeholder="기능 설명을 입력하세요..." @input="autoSave()"></textarea>
|
||||
</div>
|
||||
<button class="sb-desc-remove" @click="sbCurrentPage.descriptions.splice(idx, 1); autoSave();">×</button>
|
||||
</div>
|
||||
</template>
|
||||
<div class="sb-desc-add" @click="sbAddDescription()">+ Description 항목 추가</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>{{-- /Main Content Area --}}
|
||||
</div>
|
||||
|
||||
@@ -1110,6 +1393,26 @@ function planningCanvas() {
|
||||
_kanbanDragNodeId: null,
|
||||
_connTick: 0,
|
||||
|
||||
// Storyboard
|
||||
sb: {
|
||||
docInfo: { projectName: '', unitTask: '', version: 'D1.0' },
|
||||
menuTree: [
|
||||
{ name: '대시보드', children: [] },
|
||||
{ name: '판매관리', children: [] },
|
||||
{ name: '생산관리', children: [] },
|
||||
{ name: '출고관리', children: [] },
|
||||
{ name: '품질관리', children: [
|
||||
{ name: '제품검사관리' },
|
||||
{ name: '실적신고관리' },
|
||||
{ name: '품질인정심사' },
|
||||
]},
|
||||
{ name: '자재관리', children: [] },
|
||||
{ name: '기준정보', children: [] },
|
||||
],
|
||||
pages: [],
|
||||
currentPageIndex: 0,
|
||||
},
|
||||
|
||||
// Drag State
|
||||
dragging: false,
|
||||
dragNode: null,
|
||||
@@ -1182,6 +1485,10 @@ function planningCanvas() {
|
||||
],
|
||||
},
|
||||
|
||||
get sbCurrentPage() {
|
||||
return this.sb.pages[this.sb.currentPageIndex] || null;
|
||||
},
|
||||
|
||||
get allPaletteItems() {
|
||||
return [
|
||||
...this.paletteItems.planning,
|
||||
@@ -1231,6 +1538,7 @@ function planningCanvas() {
|
||||
} else {
|
||||
this.newProject();
|
||||
}
|
||||
this.sbInitPages();
|
||||
},
|
||||
|
||||
// ===== Project Management =====
|
||||
@@ -1246,6 +1554,11 @@ function planningCanvas() {
|
||||
this.panX = 0;
|
||||
this.panY = 0;
|
||||
this.zoom = 1;
|
||||
// 스토리보드 초기화
|
||||
this.sb.docInfo = { projectName: '', unitTask: '', version: 'D1.0' };
|
||||
this.sb.pages = [];
|
||||
this.sb.currentPageIndex = 0;
|
||||
this.sbInitPages();
|
||||
localStorage.setItem(CURRENT_KEY, this.currentProjectId);
|
||||
this.pushHistory();
|
||||
},
|
||||
@@ -1256,6 +1569,7 @@ function planningCanvas() {
|
||||
title: this.projectTitle,
|
||||
nodes: JSON.parse(JSON.stringify(this.nodes)),
|
||||
connections: JSON.parse(JSON.stringify(this.connections)),
|
||||
sb: JSON.parse(JSON.stringify(this.sb)),
|
||||
viewMode: this.viewMode,
|
||||
nodeCount: this.nodes.length,
|
||||
updatedAt: new Date().toISOString(),
|
||||
@@ -1287,6 +1601,15 @@ function planningCanvas() {
|
||||
this.selectedConnection = null;
|
||||
this.history = [];
|
||||
this.historyIndex = -1;
|
||||
// 스토리보드 데이터 복원
|
||||
if (proj.sb) {
|
||||
this.sb = JSON.parse(JSON.stringify(proj.sb));
|
||||
} else {
|
||||
this.sb.docInfo = { projectName: '', unitTask: '', version: 'D1.0' };
|
||||
this.sb.pages = [];
|
||||
this.sb.currentPageIndex = 0;
|
||||
}
|
||||
this.sbInitPages();
|
||||
localStorage.setItem(CURRENT_KEY, id);
|
||||
this.pushHistory();
|
||||
idCounter = Math.max(0, ...this.nodes.map(n => parseInt(n.id?.split('_')[1]) || 0)) + 1;
|
||||
@@ -1745,6 +2068,151 @@ function planningCanvas() {
|
||||
this.autoSave();
|
||||
},
|
||||
|
||||
// ===== Storyboard =====
|
||||
sbInitPages() {
|
||||
if (!this.sb.pages || this.sb.pages.length === 0) {
|
||||
this.sb.pages = [this.sbNewPageData()];
|
||||
this.sb.currentPageIndex = 0;
|
||||
}
|
||||
},
|
||||
|
||||
sbNewPageData() {
|
||||
return {
|
||||
id: 'sp_' + Date.now() + '_' + Math.random().toString(36).slice(2, 6),
|
||||
path: '',
|
||||
screenName: '',
|
||||
screenId: '',
|
||||
wireframeContent: '',
|
||||
wireframeImage: '',
|
||||
descriptions: [],
|
||||
};
|
||||
},
|
||||
|
||||
sbAddPage() {
|
||||
const newPage = this.sbNewPageData();
|
||||
this.sb.pages.push(newPage);
|
||||
this.sb.currentPageIndex = this.sb.pages.length - 1;
|
||||
this.autoSave();
|
||||
},
|
||||
|
||||
sbDeletePage() {
|
||||
if (this.sb.pages.length <= 1) return;
|
||||
if (!confirm('이 페이지를 삭제하시겠습니까?')) return;
|
||||
this.sb.pages.splice(this.sb.currentPageIndex, 1);
|
||||
if (this.sb.currentPageIndex >= this.sb.pages.length) {
|
||||
this.sb.currentPageIndex = this.sb.pages.length - 1;
|
||||
}
|
||||
this.autoSave();
|
||||
},
|
||||
|
||||
sbPrevPage() {
|
||||
if (this.sb.currentPageIndex > 0) this.sb.currentPageIndex--;
|
||||
},
|
||||
|
||||
sbNextPage() {
|
||||
if (this.sb.currentPageIndex < this.sb.pages.length - 1) this.sb.currentPageIndex++;
|
||||
},
|
||||
|
||||
sbAddDescription() {
|
||||
const page = this.sbCurrentPage;
|
||||
if (!page) return;
|
||||
if (!page.descriptions) page.descriptions = [];
|
||||
page.descriptions.push({ text: '' });
|
||||
this.autoSave();
|
||||
},
|
||||
|
||||
sbUploadImage(e) {
|
||||
const file = e.target.files[0];
|
||||
if (!file) return;
|
||||
const reader = new FileReader();
|
||||
reader.onload = () => {
|
||||
const page = this.sbCurrentPage;
|
||||
if (page) {
|
||||
page.wireframeImage = reader.result;
|
||||
page.wireframeContent = '';
|
||||
this.autoSave();
|
||||
}
|
||||
};
|
||||
reader.readAsDataURL(file);
|
||||
e.target.value = '';
|
||||
},
|
||||
|
||||
sbEditMenu() {
|
||||
const json = JSON.stringify(this.sb.menuTree, null, 2);
|
||||
const input = prompt('메뉴 트리 JSON을 편집하세요:\n(취소하면 변경 없음)', json);
|
||||
if (input === null) return;
|
||||
try {
|
||||
this.sb.menuTree = JSON.parse(input);
|
||||
this.autoSave();
|
||||
} catch (err) {
|
||||
alert('JSON 형식이 올바르지 않습니다.');
|
||||
}
|
||||
},
|
||||
|
||||
sbExportHtml() {
|
||||
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:20px;background:#e5e7eb;}' +
|
||||
'.page{width:1100px;margin:0 auto 24px;background:#fff;border-radius:4px;box-shadow:0 2px 8px rgba(0,0,0,0.1);overflow:hidden;page-break-after:always;}' +
|
||||
'.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;}.wf{flex:1;padding:16px;}' +
|
||||
'.wf img{max-width:100%;border-radius:4px;}.wf-text{border:1px solid #e2e8f0;border-radius:4px;padding:12px;min-height:200px;font-size:13px;line-height:1.7;}' +
|
||||
'.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{body{background:#fff;padding:0;}.page{box-shadow:none;margin:0;border-radius:0;}}</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.wireframeImage) html += '<img src="' + pg.wireframeImage + '">';
|
||||
else if (pg.wireframeContent) html += '<div class="wf-text">' + pg.wireframeContent + '</div>';
|
||||
else html += '<div class="wf-text" style="color:#94a3b8;">와이어프레임 영역</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 blob = new Blob([html], { type: 'text/html' });
|
||||
const url = URL.createObjectURL(blob);
|
||||
const a = document.createElement('a');
|
||||
a.href = url;
|
||||
a.download = (this.sb.docInfo.projectName || 'storyboard') + '.html';
|
||||
a.click();
|
||||
URL.revokeObjectURL(url);
|
||||
},
|
||||
|
||||
// ===== Context Menu =====
|
||||
showContextMenu(e) {
|
||||
this.contextMenuPos = { x: e.clientX, y: e.clientY };
|
||||
|
||||
Reference in New Issue
Block a user