-
-
-
-
+
+
+
⠿
+
+
+
+
+
+
+ {{-- Text --}}
+
+
+
+
+ {{-- Heading --}}
+
+
+
+
+
+
+
+ {{-- Divider --}}
+
+
+
+
+ {{-- Callout --}}
+
+
+
+
+ {{-- Table --}}
+
+
+
+
+
+
+ |
+
+
+
+ |
+
+
+
+
+
+
+ |
+
+
+
+ |
+
+
+
+
+
+
+
+
+
+
+ {{-- Mock: Button --}}
+
+
+
+
+ {{-- Mock: Input --}}
+
+
+
+
+ {{-- Mock: Select --}}
+
+
+
+
+ {{-- Mock: Card --}}
+
+
+
+
+ {{-- Badges --}}
+
+
+
+
+ {{-- Todo --}}
+
+
+
+
+
+
{ $event.target.parentElement.nextElementSibling?.querySelector('[contenteditable]')?.focus(); })"
+ x-text="item.text">
+
+
+
+
+
+
+
+
+
+ {{-- Code --}}
+
+
+
+
+ {{-- Image --}}
+
+
+
+
+
![]()
+
+
+
+
+
+ 클릭하여 이미지 업로드
+
+
+
+
@@ -1503,6 +1815,9 @@ function planningCanvas() {
sbMenuEditorOpen: false,
sbMenuDraft: [],
_sbMenuDrag: null,
+ sbSelectedBlock: null,
+ sbBlockImageTarget: null,
+ _sbBlockDragIdx: null,
sb: {
docInfo: { projectName: '', unitTask: '', version: 'D1.0' },
menuTree: [
@@ -1598,6 +1913,13 @@ function planningCanvas() {
return this.sb.pages[this.sb.currentPageIndex] || null;
},
+ get sbPageBlocks() {
+ const page = this.sbCurrentPage;
+ if (!page) return [];
+ if (!page.blocks) page.blocks = [];
+ return page.blocks;
+ },
+
get allPaletteItems() {
return [
...this.paletteItems.planning,
@@ -2193,6 +2515,7 @@ function planningCanvas() {
screenId: '',
wireframeContent: '',
wireframeImage: '',
+ blocks: [],
descriptions: [],
};
},
@@ -2230,20 +2553,139 @@ function planningCanvas() {
this.autoSave();
},
- sbUploadImage(e) {
+ // ===== Block Editor =====
+ sbNewBlock(type) {
+ const id = 'blk_' + Date.now() + '_' + Math.random().toString(36).slice(2, 5);
+ const base = { id, type, content: '' };
+ switch (type) {
+ case 'heading': return { ...base };
+ case 'heading2': return { ...base };
+ case 'text': return { ...base };
+ case 'divider': return { ...base };
+ case 'callout': return { ...base, icon: '💡' };
+ case 'code': return { ...base };
+ case 'table': return { ...base, cols: ['항목', '내용', '비고'], rows: [['', '', ''], ['', '', '']] };
+ case 'button': return { ...base, content: '버튼', color: '#4338ca' };
+ case 'input': return { ...base, label: 'Label', placeholder: '값을 입력하세요' };
+ case 'select': return { ...base, label: 'Label', placeholder: '선택하세요' };
+ case 'card': return { ...base, title: '카드 제목', content: '카드 내용을 입력하세요' };
+ case 'badges': return { ...base, items: [
+ { text: '신규', color: '#dbeafe', textColor: '#2563eb' },
+ { text: '진행중', color: '#dcfce7', textColor: '#16a34a' },
+ { text: '완료', color: '#e0e7ff', textColor: '#4338ca' },
+ ]};
+ case 'todo': return { ...base, items: [
+ { text: '항목 1', checked: false },
+ { text: '항목 2', checked: false },
+ ]};
+ case 'image': return { ...base, src: '' };
+ default: return base;
+ }
+ },
+
+ sbAddBlock(type) {
+ const page = this.sbCurrentPage;
+ if (!page) return;
+ if (!page.blocks) page.blocks = [];
+ const blk = this.sbNewBlock(type);
+ page.blocks.push(blk);
+ this.sbSelectedBlock = blk.id;
+ this.autoSave();
+ },
+
+ sbAddBlockAfter(idx, type) {
+ const page = this.sbCurrentPage;
+ if (!page || !page.blocks) return;
+ const blk = this.sbNewBlock(type);
+ page.blocks.splice(idx + 1, 0, blk);
+ this.sbSelectedBlock = blk.id;
+ this.autoSave();
+ },
+
+ sbRemoveBlock(idx) {
+ const page = this.sbCurrentPage;
+ if (!page || !page.blocks) return;
+ page.blocks.splice(idx, 1);
+ this.sbSelectedBlock = null;
+ this.autoSave();
+ },
+
+ sbDuplicateBlock(idx) {
+ const page = this.sbCurrentPage;
+ if (!page || !page.blocks) return;
+ const copy = JSON.parse(JSON.stringify(page.blocks[idx]));
+ copy.id = 'blk_' + Date.now() + '_' + Math.random().toString(36).slice(2, 5);
+ page.blocks.splice(idx + 1, 0, copy);
+ this.sbSelectedBlock = copy.id;
+ this.autoSave();
+ },
+
+ sbMoveBlockUp(idx) {
+ const page = this.sbCurrentPage;
+ if (!page || idx <= 0) return;
+ const [item] = page.blocks.splice(idx, 1);
+ page.blocks.splice(idx - 1, 0, item);
+ this.autoSave();
+ },
+
+ sbMoveBlockDown(idx) {
+ const page = this.sbCurrentPage;
+ if (!page || idx >= page.blocks.length - 1) return;
+ const [item] = page.blocks.splice(idx, 1);
+ page.blocks.splice(idx + 1, 0, item);
+ this.autoSave();
+ },
+
+ sbBlockDragStart(idx, e) {
+ this._sbBlockDragIdx = idx;
+ e.dataTransfer.effectAllowed = 'move';
+ },
+
+ sbBlockDragOver(idx, e) {
+ if (this._sbBlockDragIdx === null) return;
+ e.dataTransfer.dropEffect = 'move';
+ },
+
+ sbBlockDrop(idx, e) {
+ if (this._sbBlockDragIdx === null) return;
+ const from = this._sbBlockDragIdx;
+ this._sbBlockDragIdx = null;
+ if (from === idx) return;
+ const page = this.sbCurrentPage;
+ if (!page) return;
+ const [item] = page.blocks.splice(from, 1);
+ page.blocks.splice(idx, 0, item);
+ this.autoSave();
+ },
+
+ sbBlockDragEnd() {
+ this._sbBlockDragIdx = null;
+ },
+
+ sbTableAddRow(blk) {
+ const colCount = blk.cols.length;
+ blk.rows.push(Array(colCount).fill(''));
+ this.autoSave();
+ },
+
+ sbTableAddCol(blk) {
+ blk.cols.push('컬럼');
+ blk.rows.forEach(row => row.push(''));
+ this.autoSave();
+ },
+
+ sbBlockUploadImage(e) {
const file = e.target.files[0];
- if (!file) return;
+ if (!file || !this.sbBlockImageTarget) return;
const reader = new FileReader();
+ const target = this.sbBlockImageTarget;
reader.onload = () => {
- const page = this.sbCurrentPage;
- if (page) {
- page.wireframeImage = reader.result;
- page.wireframeContent = '';
- this.autoSave();
- }
+ target.src = reader.result;
+ this.autoSave();
};
reader.readAsDataURL(file);
e.target.value = '';
+ this.sbBlockImageTarget = null;
},
sbEditMenu() {
@@ -2365,9 +2807,15 @@ function planningCanvas() {
});
});
html += '
';
- if (pg.wireframeImage) html += '

';
- else if (pg.wireframeContent) html += '
' + pg.wireframeContent + '
';
- else html += '
와이어프레임 영역
';
+ if (pg.blocks && pg.blocks.length > 0) {
+ pg.blocks.forEach(blk => { html += this.sbExportBlock(blk); });
+ } else if (pg.wireframeImage) {
+ html += '

';
+ } else if (pg.wireframeContent) {
+ html += '
' + pg.wireframeContent + '
';
+ } else {
+ html += '
와이어프레임 영역
';
+ }
html += '
';
if (pg.descriptions && pg.descriptions.length > 0) {
html += '
Description
';
@@ -2390,6 +2838,51 @@ function planningCanvas() {
URL.revokeObjectURL(url);
},
+ sbExportBlock(blk) {
+ const esc = (s) => (s || '').replace(//g, '>');
+ switch (blk.type) {
+ case 'heading': return '
' + esc(blk.content) + '
';
+ case 'heading2': return '
' + esc(blk.content) + '
';
+ case 'text': return '
' + esc(blk.content).replace(/\n/g, '
') + '
';
+ case 'divider': return '
';
+ case 'callout': return '
' + esc(blk.icon || '💡') + '' + esc(blk.content) + '
';
+ case 'code': return '
' + esc(blk.content) + '
';
+ case 'table': {
+ let t = '
';
+ (blk.cols || []).forEach(c => { t += '| ' + esc(c) + ' | '; });
+ t += '
';
+ (blk.rows || []).forEach(row => {
+ t += '';
+ row.forEach(cell => { t += '| ' + esc(cell) + ' | '; });
+ t += '
';
+ });
+ return t + '
';
+ }
+ case 'button': return '
' + esc(blk.content) + '
';
+ case 'input': return '
' + esc(blk.label) + '
' + esc(blk.placeholder) + '
';
+ case 'select': return '
' + esc(blk.label) + '
' + esc(blk.placeholder) + ' ▾
';
+ case 'card': return '
' + esc(blk.title) + '
' + esc(blk.content) + '
';
+ case 'badges': {
+ let b = '
';
+ (blk.items || []).forEach(badge => {
+ b += '' + esc(badge.text) + '';
+ });
+ return b + '
';
+ }
+ case 'todo': {
+ let t = '
';
+ (blk.items || []).forEach(item => {
+ t += '
';
+ t += item.checked ? '☑' : '☐';
+ t += ' ' + esc(item.text) + '
';
+ });
+ return t + '
';
+ }
+ case 'image': return blk.src ? '
' : '';
+ default: return '
' + esc(blk.content) + '
';
+ }
+ },
+
// ===== Context Menu =====
showContextMenu(e) {
this.contextMenuPos = { x: e.clientX, y: e.clientY };