feat: [planning-design] 스토리보드 블록 Undo/Redo 기능 추가 (Ctrl+Z/Y)
This commit is contained in:
@@ -917,10 +917,10 @@
|
||||
<button class="pc-tb-btn" @click="toggleSidebar()" title="사이드바 토글">
|
||||
<svg fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 6h16M4 12h16M4 18h7"/></svg>
|
||||
</button>
|
||||
<button class="pc-tb-btn" @click="undoAction()" title="실행 취소 (Ctrl+Z)">
|
||||
<button class="pc-tb-btn" @click="viewMode === 'storyboard' ? sbUndo() : undoAction()" title="실행 취소 (Ctrl+Z)">
|
||||
<svg fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 10h10a8 8 0 018 8v2M3 10l6 6m-6-6l6-6"/></svg>
|
||||
</button>
|
||||
<button class="pc-tb-btn" @click="redoAction()" title="다시 실행 (Ctrl+Y)">
|
||||
<button class="pc-tb-btn" @click="viewMode === 'storyboard' ? sbRedo() : redoAction()" title="다시 실행 (Ctrl+Y)">
|
||||
<svg fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 10H11a8 8 0 00-8 8v2m18-10l-6 6m6-6l-6-6"/></svg>
|
||||
</button>
|
||||
<div class="pc-tb-sep"></div>
|
||||
@@ -1962,6 +1962,9 @@ function planningCanvas() {
|
||||
_sbDrag: null, // { blk, startX, startY, origX, origY }
|
||||
_sbResize: null, // { blk, dir, startX, startY, origW, origH }
|
||||
_sbClipboard: null, // copied block data
|
||||
_sbHistory: [],
|
||||
_sbHistoryIdx: -1,
|
||||
_sbHistoryPaused: false,
|
||||
sbTplOpen: false,
|
||||
sbTplTab: 'preset',
|
||||
sbTplSearch: '',
|
||||
@@ -2757,8 +2760,8 @@ function planningCanvas() {
|
||||
if (e.key === 'v' || e.key === 'V') this.tool = 'select';
|
||||
if (e.key === 'h' || e.key === 'H') this.tool = 'pan';
|
||||
if (e.key === 'c' || e.key === 'C') this.tool = 'connect';
|
||||
if ((e.ctrlKey || e.metaKey) && e.key === 'z') { e.preventDefault(); this.undoAction(); }
|
||||
if ((e.ctrlKey || e.metaKey) && e.key === 'y') { e.preventDefault(); this.redoAction(); }
|
||||
if ((e.ctrlKey || e.metaKey) && e.key === 'z') { e.preventDefault(); this.viewMode === 'storyboard' ? this.sbUndo() : this.undoAction(); return; }
|
||||
if ((e.ctrlKey || e.metaKey) && e.key === 'y') { e.preventDefault(); this.viewMode === 'storyboard' ? this.sbRedo() : this.redoAction(); return; }
|
||||
if ((e.ctrlKey || e.metaKey) && e.key === 's') { e.preventDefault(); this.saveProject(); }
|
||||
if ((e.ctrlKey || e.metaKey) && e.key === 'd') { e.preventDefault(); this.duplicateNode(); }
|
||||
// 스토리보드 블록 Ctrl+C / Ctrl+V / Delete
|
||||
@@ -2914,6 +2917,7 @@ function planningCanvas() {
|
||||
const page = this.sbCurrentPage;
|
||||
if (!page) return;
|
||||
if (!page.blocks) page.blocks = [];
|
||||
this.sbPushHistory();
|
||||
const blk = this.sbNewBlock(type);
|
||||
page.blocks.push(blk);
|
||||
this.sbSelectedBlock = blk.id;
|
||||
@@ -2923,6 +2927,7 @@ function planningCanvas() {
|
||||
sbAddBlockAfter(idx, type) {
|
||||
const page = this.sbCurrentPage;
|
||||
if (!page || !page.blocks) return;
|
||||
this.sbPushHistory();
|
||||
const blk = this.sbNewBlock(type);
|
||||
page.blocks.splice(idx + 1, 0, blk);
|
||||
this.sbSelectedBlock = blk.id;
|
||||
@@ -2932,6 +2937,7 @@ function planningCanvas() {
|
||||
sbRemoveBlock(idx) {
|
||||
const page = this.sbCurrentPage;
|
||||
if (!page || !page.blocks) return;
|
||||
this.sbPushHistory();
|
||||
page.blocks.splice(idx, 1);
|
||||
this.sbSelectedBlock = null;
|
||||
this.autoSave();
|
||||
@@ -2940,6 +2946,7 @@ function planningCanvas() {
|
||||
sbDuplicateBlock(idx) {
|
||||
const page = this.sbCurrentPage;
|
||||
if (!page || !page.blocks) return;
|
||||
this.sbPushHistory();
|
||||
const copy = JSON.parse(JSON.stringify(page.blocks[idx]));
|
||||
copy.id = 'blk_' + Date.now() + '_' + Math.random().toString(36).slice(2, 5);
|
||||
copy.x = (copy.x || 0) + 20;
|
||||
@@ -2954,6 +2961,7 @@ function planningCanvas() {
|
||||
// 더블클릭 편집 모드에서는 드래그 안 함
|
||||
if (this.sbEditingBlock === blk.id) return;
|
||||
this.sbSelectedBlock = blk.id;
|
||||
this.sbPushHistory();
|
||||
this._sbDrag = {
|
||||
blk,
|
||||
startX: e.clientX,
|
||||
@@ -2966,6 +2974,7 @@ function planningCanvas() {
|
||||
|
||||
sbResizeStart(blk, dir, e) {
|
||||
this.sbSelectedBlock = blk.id;
|
||||
this.sbPushHistory();
|
||||
this._sbResize = {
|
||||
blk,
|
||||
dir,
|
||||
@@ -3029,6 +3038,7 @@ function planningCanvas() {
|
||||
const page = this.sbCurrentPage;
|
||||
if (!page) return;
|
||||
if (!page.blocks) page.blocks = [];
|
||||
this.sbPushHistory();
|
||||
const copy = JSON.parse(JSON.stringify(this._sbClipboard));
|
||||
copy.id = 'blk_' + Date.now() + '_' + Math.random().toString(36).slice(2, 5);
|
||||
copy.x = (copy.x || 0) + 24;
|
||||
@@ -3044,11 +3054,62 @@ function planningCanvas() {
|
||||
if (!page || !page.blocks) return;
|
||||
const idx = page.blocks.findIndex(b => b.id === this.sbSelectedBlock);
|
||||
if (idx < 0) return;
|
||||
this.sbPushHistory();
|
||||
page.blocks.splice(idx, 1);
|
||||
this.sbSelectedBlock = null;
|
||||
this.autoSave();
|
||||
},
|
||||
|
||||
// ===== Storyboard Undo/Redo =====
|
||||
sbPushHistory() {
|
||||
if (this._sbHistoryPaused) return;
|
||||
const page = this.sbCurrentPage;
|
||||
if (!page || !page.blocks) return;
|
||||
const snap = JSON.parse(JSON.stringify(page.blocks));
|
||||
// 현재 위치 이후의 히스토리 삭제 (새 분기)
|
||||
if (this._sbHistoryIdx < this._sbHistory.length - 1) {
|
||||
this._sbHistory = this._sbHistory.slice(0, this._sbHistoryIdx + 1);
|
||||
}
|
||||
this._sbHistory.push(snap);
|
||||
// 최대 50개
|
||||
if (this._sbHistory.length > 50) {
|
||||
this._sbHistory = this._sbHistory.slice(this._sbHistory.length - 50);
|
||||
}
|
||||
this._sbHistoryIdx = this._sbHistory.length - 1;
|
||||
},
|
||||
|
||||
sbUndo() {
|
||||
if (this._sbHistoryIdx <= 0) return;
|
||||
const page = this.sbCurrentPage;
|
||||
if (!page) return;
|
||||
// 처음 undo 시 현재 상태 저장
|
||||
if (this._sbHistoryIdx === this._sbHistory.length - 1) {
|
||||
const snap = JSON.parse(JSON.stringify(page.blocks || []));
|
||||
if (this._sbHistory.length === 0 || JSON.stringify(this._sbHistory[this._sbHistoryIdx]) !== JSON.stringify(snap)) {
|
||||
this._sbHistory.push(snap);
|
||||
this._sbHistoryIdx = this._sbHistory.length - 1;
|
||||
}
|
||||
}
|
||||
this._sbHistoryIdx--;
|
||||
this._sbHistoryPaused = true;
|
||||
page.blocks = JSON.parse(JSON.stringify(this._sbHistory[this._sbHistoryIdx]));
|
||||
this.sbSelectedBlock = null;
|
||||
this._sbHistoryPaused = false;
|
||||
this.autoSave();
|
||||
},
|
||||
|
||||
sbRedo() {
|
||||
if (this._sbHistoryIdx >= this._sbHistory.length - 1) return;
|
||||
const page = this.sbCurrentPage;
|
||||
if (!page) return;
|
||||
this._sbHistoryIdx++;
|
||||
this._sbHistoryPaused = true;
|
||||
page.blocks = JSON.parse(JSON.stringify(this._sbHistory[this._sbHistoryIdx]));
|
||||
this.sbSelectedBlock = null;
|
||||
this._sbHistoryPaused = false;
|
||||
this.autoSave();
|
||||
},
|
||||
|
||||
sbTableAddRow(blk) {
|
||||
const colCount = blk.cols.length;
|
||||
blk.rows.push(Array(colCount).fill(''));
|
||||
@@ -3080,6 +3141,7 @@ function planningCanvas() {
|
||||
const page = this.sbCurrentPage;
|
||||
if (!page) return;
|
||||
if (!page.blocks) page.blocks = [];
|
||||
this.sbPushHistory();
|
||||
const blocks = JSON.parse(JSON.stringify(tpl.blocks));
|
||||
// 기존 블록 아래에 배치
|
||||
let curY = 16;
|
||||
|
||||
Reference in New Issue
Block a user