@@ -1533,7 +1544,7 @@
@@ -1987,6 +1998,9 @@ function planningCanvas() {
_sbClipboard: null, // copied block data
sbMenuWidth: 160,
_sbMenuResize: null,
+ sbMultiSelected: [], // 다중 선택 블록 id 배열
+ _sbLasso: null, // { startX, startY, rx, ry, rw, rh }
+ _sbMultiDrag: null, // { startX, startY, origins: [{id, x, y}] }
_sbHistory: [],
_sbHistoryIdx: -1,
_sbHistoryPaused: false,
@@ -2789,8 +2803,8 @@ function planningCanvas() {
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
- if (this.viewMode === 'storyboard' && this.sbSelectedBlock) {
+ // 스토리보드 블록 Ctrl+C / Ctrl+V / Delete (단일 + 다중)
+ if (this.viewMode === 'storyboard' && (this.sbSelectedBlock || this.sbMultiSelected.length > 0)) {
if ((e.ctrlKey || e.metaKey) && (e.key === 'c' || e.key === 'C')) {
e.preventDefault();
this.sbCopyBlock();
@@ -2807,13 +2821,23 @@ function planningCanvas() {
return;
}
}
- if (this.viewMode === 'storyboard' && !this.sbSelectedBlock) {
+ if (this.viewMode === 'storyboard' && !this.sbSelectedBlock && this.sbMultiSelected.length === 0) {
if ((e.ctrlKey || e.metaKey) && (e.key === 'v' || e.key === 'V') && this._sbClipboard) {
e.preventDefault();
this.sbPasteBlock();
return;
}
}
+ // Ctrl+A 전체 선택
+ if (this.viewMode === 'storyboard' && (e.ctrlKey || e.metaKey) && (e.key === 'a' || e.key === 'A')) {
+ const page = this.sbCurrentPage;
+ if (page && page.blocks && page.blocks.length > 0) {
+ e.preventDefault();
+ this.sbSelectedBlock = null;
+ this.sbMultiSelected = page.blocks.map(b => b.id);
+ return;
+ }
+ }
if ((e.ctrlKey || e.metaKey) && e.key === 'f') {
if (this.viewMode === 'kanban' || this.viewMode === 'list') {
e.preventDefault();
@@ -3002,8 +3026,21 @@ function planningCanvas() {
sbBlockMouseDown(blk, bi, e) {
// 더블클릭 편집 모드에서는 드래그 안 함
if (this.sbEditingBlock === blk.id) return;
- this.sbSelectedBlock = blk.id;
this.sbPushHistory();
+ // 다중 선택된 블록 중 하나를 클릭하면 그룹 드래그
+ if (this.sbMultiSelected.includes(blk.id)) {
+ const page = this.sbCurrentPage;
+ const origins = this.sbMultiSelected.map(id => {
+ const b = page.blocks.find(x => x.id === id);
+ return b ? { id: b.id, x: b.x || 0, y: b.y || 0 } : null;
+ }).filter(Boolean);
+ this._sbMultiDrag = { startX: e.clientX, startY: e.clientY, origins };
+ e.preventDefault();
+ return;
+ }
+ // 단일 선택
+ this.sbMultiSelected = [];
+ this.sbSelectedBlock = blk.id;
this._sbDrag = {
blk,
startX: e.clientX,
@@ -3029,13 +3066,53 @@ function planningCanvas() {
},
sbCanvasMouseDown(e) {
- // 빈 캔버스 클릭 시 편집 모드 해제
- if (e.target === this.$refs.sbCanvas) {
+ // 빈 캔버스 클릭 시 올가미 시작
+ if (e.target === this.$refs.sbCanvas || e.target.classList.contains('sb-blk-empty-area')) {
this.sbEditingBlock = null;
+ const rect = this.$refs.sbCanvas.getBoundingClientRect();
+ const scrollL = this.$refs.sbCanvas.scrollLeft;
+ const scrollT = this.$refs.sbCanvas.scrollTop;
+ const sx = e.clientX - rect.left + scrollL;
+ const sy = e.clientY - rect.top + scrollT;
+ this._sbLasso = { startX: sx, startY: sy, rx: sx, ry: sy, rw: 0, rh: 0 };
+ this.sbMultiSelected = [];
+ this.sbSelectedBlock = null;
}
},
sbCanvasMouseMove(e) {
+ // 올가미 드래그
+ if (this._sbLasso) {
+ const rect = this.$refs.sbCanvas.getBoundingClientRect();
+ const scrollL = this.$refs.sbCanvas.scrollLeft;
+ const scrollT = this.$refs.sbCanvas.scrollTop;
+ const cx = e.clientX - rect.left + scrollL;
+ const cy = e.clientY - rect.top + scrollT;
+ const l = this._sbLasso;
+ l.rx = Math.min(l.startX, cx);
+ l.ry = Math.min(l.startY, cy);
+ l.rw = Math.abs(cx - l.startX);
+ l.rh = Math.abs(cy - l.startY);
+ return;
+ }
+ // 다중 블록 드래그
+ if (this._sbMultiDrag) {
+ const md = this._sbMultiDrag;
+ const dx = e.clientX - md.startX;
+ const dy = e.clientY - md.startY;
+ const page = this.sbCurrentPage;
+ if (page && page.blocks) {
+ md.origins.forEach(o => {
+ const blk = page.blocks.find(b => b.id === o.id);
+ if (blk) {
+ blk.x = Math.max(0, o.x + dx);
+ blk.y = Math.max(0, o.y + dy);
+ }
+ });
+ }
+ return;
+ }
+ // 단일 블록 드래그
if (this._sbDrag) {
const d = this._sbDrag;
const dx = e.clientX - d.startX;
@@ -3043,6 +3120,7 @@ function planningCanvas() {
d.blk.x = Math.max(0, d.origX + dx);
d.blk.y = Math.max(0, d.origY + dy);
}
+ // 리사이즈
if (this._sbResize) {
const r = this._sbResize;
const dx = e.clientX - r.startX;
@@ -3057,6 +3135,35 @@ function planningCanvas() {
},
sbCanvasMouseUp(e) {
+ // 올가미 완료 → 범위 내 블록 선택
+ if (this._sbLasso) {
+ const l = this._sbLasso;
+ if (l.rw > 5 && l.rh > 5) {
+ const page = this.sbCurrentPage;
+ if (page && page.blocks) {
+ this.sbMultiSelected = page.blocks.filter(blk => {
+ const bx = blk.x || 0, by = blk.y || 0;
+ const bw = blk.w || 240, bh = blk.h || 40;
+ // 블록이 올가미 사각형과 겹치는지 체크
+ return bx + bw > l.rx && bx < l.rx + l.rw &&
+ by + bh > l.ry && by < l.ry + l.rh;
+ }).map(b => b.id);
+ }
+ // 1개만 선택되면 단일 선택으로 전환
+ if (this.sbMultiSelected.length === 1) {
+ this.sbSelectedBlock = this.sbMultiSelected[0];
+ this.sbMultiSelected = [];
+ }
+ }
+ this._sbLasso = null;
+ return;
+ }
+ // 다중 드래그 완료
+ if (this._sbMultiDrag) {
+ this._sbMultiDrag = null;
+ this.autoSave();
+ return;
+ }
if (this._sbDrag) {
this._sbDrag = null;
this.autoSave();
@@ -3070,6 +3177,15 @@ function planningCanvas() {
sbCopyBlock() {
const page = this.sbCurrentPage;
if (!page || !page.blocks) return;
+ // 다중 선택 복사
+ if (this.sbMultiSelected.length > 0) {
+ this._sbClipboard = this.sbMultiSelected.map(id => {
+ const b = page.blocks.find(x => x.id === id);
+ return b ? JSON.parse(JSON.stringify(b)) : null;
+ }).filter(Boolean);
+ return;
+ }
+ // 단일 복사
const blk = page.blocks.find(b => b.id === this.sbSelectedBlock);
if (!blk) return;
this._sbClipboard = JSON.parse(JSON.stringify(blk));
@@ -3081,6 +3197,25 @@ function planningCanvas() {
if (!page) return;
if (!page.blocks) page.blocks = [];
this.sbPushHistory();
+ // 다중 붙여넣기
+ if (Array.isArray(this._sbClipboard)) {
+ const newIds = [];
+ const copies = this._sbClipboard.map(b => {
+ const copy = JSON.parse(JSON.stringify(b));
+ copy.id = 'blk_' + Date.now() + '_' + Math.random().toString(36).slice(2, 5);
+ copy.x = (copy.x || 0) + 24;
+ copy.y = (copy.y || 0) + 24;
+ newIds.push(copy.id);
+ return copy;
+ });
+ page.blocks.push(...copies);
+ this.sbSelectedBlock = null;
+ this.sbMultiSelected = newIds;
+ this._sbClipboard = copies; // 연속 오프셋 누적
+ this.autoSave();
+ return;
+ }
+ // 단일 붙여넣기
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;
@@ -3094,9 +3229,18 @@ function planningCanvas() {
sbDeleteSelectedBlock() {
const page = this.sbCurrentPage;
if (!page || !page.blocks) return;
+ this.sbPushHistory();
+ // 다중 삭제
+ if (this.sbMultiSelected.length > 0) {
+ page.blocks = page.blocks.filter(b => !this.sbMultiSelected.includes(b.id));
+ this.sbMultiSelected = [];
+ this.sbSelectedBlock = null;
+ this.autoSave();
+ 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();