feat: [planning-design] 스토리보드 블록 Undo/Redo 기능 추가 (Ctrl+Z/Y)

This commit is contained in:
김보곤
2026-03-07 23:49:34 +09:00
parent abebf0e452
commit 3e3ea03139

View File

@@ -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;