polarvo-layout 1.0.19 → 1.0.21

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (28) hide show
  1. package/package.json +1 -1
  2. package/src/components/FastMenu/DesignFastmenu.vue +2 -2
  3. package/src/components/FastMenu/DisplayFastMenu.vue +2 -2
  4. package/src/components/FastMenu/LayoutFastMenu.vue +18 -10
  5. package/src/components/Layout/CanvasContainer.vue +30 -6
  6. package/src/components/SideBar/ElementSideBar.vue +2 -2
  7. package/src/components/SideBar/LayoutSettingSideBar.vue +18 -10
  8. package/src/components/SideBar/LayoutSideBar.vue +19 -8
  9. package/src/configs/index.js +1 -0
  10. package/src/core/engines/DisplayEngine.js +72 -64
  11. package/src/core/engines/FreeDropEngine.js +40 -42
  12. package/src/core/engines/GridDropEngine.js +33 -43
  13. package/src/core/engines/HistoryEngine.js +55 -51
  14. package/src/core/engines/LayoutEngine.js +89 -68
  15. package/src/core/engines/originals/FreeDropEngine_0402.js +786 -0
  16. package/src/core/engines/originals/GridDropEngine_0402.js +557 -0
  17. package/src/core/managers/EngineManager.js +93 -131
  18. package/src/core/managers/originals/EngineManager_0402.js +826 -0
  19. package/src/library/DisplayLibrary.js +14 -17
  20. package/src/library/FreeDropLibrary.js +6 -4
  21. package/src/library/GridDropLibrary.js +3 -5
  22. package/src/library/LayoutLibrary.js +45 -48
  23. package/src/library/originals/DisplayLibrary_0402.js +137 -0
  24. package/src/library/originals/FreeDropLibrary_0402.js +185 -0
  25. package/src/library/originals/GridDropLibrary_0402.js +202 -0
  26. package/src/library/originals/HistoryLibrary_0402.js +98 -0
  27. package/src/library/originals/LayoutLibrary_0402.js +264 -0
  28. package/src/utils/index.js +8 -3
@@ -0,0 +1,786 @@
1
+ import { cloneDeep } from 'lodash-es';
2
+ import { nextTick } from 'vue';
3
+
4
+ // event 발행 시 'freeDrop'으로 시작하는 이름 사용
5
+ class FreeDropEngine {
6
+ constructor({ eventBus, options }) {
7
+ this.eventBus = eventBus;
8
+ this.resource = options.resource;
9
+
10
+ this._gridSize = null;
11
+ this._gridNumber = null;
12
+
13
+ this._elements = []; // 원본 요소 목록 (EngineManager과 동기화)
14
+ this._elementsUpdate = false; // 원본 요소 목록 변경 플래그
15
+ this._freeElements = []; // 로컬에서 사용하는 요소 목록
16
+
17
+ this._activeData = null;
18
+ this._activeSection = null;
19
+ this._activeMode = null;
20
+ this._activeConfig = null;
21
+
22
+ this._activeElement = null; // 드래그 중인 배치요소
23
+
24
+ this.guides = { x: null, y: null, w: null, h: null }; // 가이드 라인 위치
25
+
26
+ this._dragState = {
27
+ isDragging: false, // 드래그 상태
28
+ isResizing: false, // 리사이징 상태
29
+ direction: null, // 리사이징 방향
30
+ };
31
+
32
+ this._startPos = { x: 0, y: 0 }; // 드래그 시작 위치
33
+ this._startSize = { w: 0, h: 0 }; // 드래그 시작 크기
34
+ this._offsetPos = { x: 0, y: 0 }; // 마우스 오프셋 위치
35
+ this._alreadyExist = false; // 배치요소 기존재 여부
36
+
37
+ this._subscriptions = []; // 구독 해제 함수 저장용
38
+ this._setupSubscriptions();
39
+ }
40
+
41
+ /** ---------------------------------- 구독 관련 메소드 ---------------------------------- **/
42
+ /** [내부함수] 구독 및 unsubscribe 함수 저장
43
+ * @param {string} name - 이벤트 이름
44
+ * @param {function} handler - 이벤트 핸들러 함수
45
+ * @return {function} - unsubscribe 함수
46
+ */
47
+ _subscribe(name, handler) {
48
+ const unsubscribe = this.eventBus.subscribe(name, handler);
49
+ this._subscriptions.push(unsubscribe);
50
+ return unsubscribe;
51
+ }
52
+
53
+ /** [내부함수] 구독 설정 */
54
+ _setupSubscriptions() {
55
+ // 초기화 완료 이벤트 구독 - from DisplayEngine
56
+ this._subscribe('display:engineInitialized', ({ displaySize }) => {
57
+ const { gridSize } = displaySize;
58
+ this._gridSize = gridSize;
59
+
60
+ this._initialize();
61
+ });
62
+
63
+ this._subscribe('display:enginesReset', ({ displaySize }) => {
64
+ const { gridSize } = displaySize;
65
+ this._gridSize = gridSize;
66
+
67
+ this._initialize();
68
+ });
69
+
70
+ // displayMode 변경 감지 (displaySize가 함께 초기화됨)
71
+ this._subscribe('display:updateDisplayMode', ({ displaySize }) => {
72
+ const { gridSize } = displaySize;
73
+ this._gridSize = gridSize;
74
+ });
75
+
76
+ // layoutName 변경 감지 (gridNumber가 함께 초기화됨)
77
+ this._subscribe('layout:setLayoutName', ({ $gridNumber }) => {
78
+ this._gridNumber = $gridNumber;
79
+ });
80
+ this._subscribe('layout:updateLayoutName', ({ gridNumber }) => {
81
+ this._gridNumber = gridNumber;
82
+ });
83
+
84
+ // activeSection 변경 감지 (activeData가 함께 초기화됨)
85
+ this._subscribe('system:updateActiveSection', ({ activeSection, activeData }) => {
86
+ this._activeSection = activeSection;
87
+ this._activeData = activeData;
88
+ // this.setActiveElement(null);
89
+ });
90
+
91
+ this._subscribe('system:setElements', ({ elements }) => {
92
+ this._elements = elements;
93
+ this._elementsUpdate = true;
94
+
95
+ // 활성화 요소 초기화
96
+ this.setActiveElement(null);
97
+
98
+ this.eventBus.emit('freeDrop:setElements', {
99
+ elements: cloneDeep(this.freeElements),
100
+ timestamp: Date.now(),
101
+ });
102
+ });
103
+
104
+ this._subscribe('system:updateLayoutData', ({ $mode, $config }) => {
105
+ this._activeMode = $mode;
106
+ this._activeConfig = $config;
107
+ });
108
+
109
+ // sectionConfig 변경 감지 (free 모드인 경우에만 activeData 갱신)
110
+ // this._subscribe('system:updateActiveSectionConfig', ({ activeData }) => {
111
+ // if (activeData?.mode == 'free') {
112
+ // this._activeData = activeData;
113
+ // }
114
+ // });
115
+
116
+ this._subscribe('system:requestUpdateData', ({ elements, elementId, activeData }) => {
117
+ this._elements = elements;
118
+ this._elementsUpdate = true;
119
+
120
+ this.setActiveElement(this._elements.find((x) => x.id === elementId));
121
+
122
+ // if (activeData?.mode == 'free') {
123
+ // this._activeData = activeData;
124
+ // }
125
+
126
+ this.eventBus.emit('freeDrop:requestUpdateData', {
127
+ elements: cloneDeep(this.freeElements),
128
+ guides: this.guides,
129
+ timestamp: Date.now(),
130
+ });
131
+ });
132
+
133
+ this._subscribe('system:requestUpdateActiveElement', ({ elementId, element }) => {
134
+ const index = this._elements.findIndex((x) => x.id === elementId);
135
+
136
+ if (index !== -1) {
137
+ this._elements[index] = element; // 업데이트
138
+ this._elementsUpdate = true;
139
+ }
140
+
141
+ this.setActiveElement(element, 'update');
142
+
143
+ this.eventBus.emit('freeDrop:requestUpdateActiveElement', {
144
+ element: element,
145
+ timestamp: Date.now(),
146
+ });
147
+ });
148
+
149
+ this._subscribe('system:requestUpdateElements', ({ elements }) => {
150
+ this._elements = elements;
151
+ this._elementsUpdate = true;
152
+ });
153
+
154
+ this._subscribe('system:restoredState', ({ stateData }) => {
155
+ this._elements = stateData.elements;
156
+ this._elementsUpdate = true;
157
+ this.setActiveElement(null);
158
+
159
+ this.eventBus.emit('freeDrop:restoredState', {
160
+ elements: cloneDeep(this.freeElements),
161
+ timestamp: Date.now(),
162
+ });
163
+ });
164
+ }
165
+
166
+ /** 삭제 및 정리 */
167
+ destroy() {
168
+ this._subscriptions.forEach((unsubscribe) => {
169
+ if (typeof unsubscribe === 'function') {
170
+ unsubscribe();
171
+ }
172
+ });
173
+
174
+ this._initialize();
175
+ this._subscriptions = [];
176
+ this.eventBus = null;
177
+ this.resource = null;
178
+ console.log('[FreeDropEngine] 삭제 완료');
179
+ }
180
+
181
+ /** 리소스 가져오기
182
+ * @param {string} name - 리소스 이름
183
+ * @return {Array} - 리소스 배열
184
+ */
185
+ getResourceByName(name) {
186
+ if (!this.resource) {
187
+ console.error('[FreeDropEngine] 리소스가 설정되지 않았습니다.');
188
+ return [];
189
+ }
190
+ return this.resource[name] || [];
191
+ }
192
+
193
+ /** 전체 요소 중 자유 배치 요소만 반환
194
+ * @return {Array} - 자유배치 요소 배열
195
+ */
196
+ get freeElements() {
197
+ if (this._elementsUpdate) {
198
+ this._elementsUpdate = false;
199
+
200
+ const base = this._elements.filter((x) => x.mode === 'free');
201
+
202
+ if (this._activeElement) {
203
+ const exists = base.find((x) => x.id === this._activeElement?.id);
204
+
205
+ // 기존 배치요소인 경우
206
+ if (exists) {
207
+ // 기존 배치요소와 activeElement 교체
208
+ this._freeElements = base.map((element) => (element.id === this._activeElement.id ? this._activeElement : element));
209
+ } else {
210
+ this._freeElements = [...base, this._activeElement];
211
+ }
212
+ } else {
213
+ this._freeElements = base;
214
+ }
215
+ }
216
+
217
+ return this._freeElements;
218
+ }
219
+
220
+ /** ---------------------------------- 공통 메소드 ---------------------------------- **/
221
+ /** [내부함수] 내부 변수 초기화 */
222
+ _initialize() {
223
+ this._dragState = {
224
+ isDragging: false,
225
+ isResizing: false,
226
+ direction: null,
227
+ };
228
+
229
+ this.guides = { x: null, y: null, w: null, h: null };
230
+ this._startPos = { x: 0, y: 0 };
231
+ this._startSize = { w: 0, h: 0 };
232
+ this._offsetPos = { x: 0, y: 0 };
233
+
234
+ // this._activeData = null;
235
+ this._activeSection = null;
236
+ this._activeMode = null;
237
+ this._activeConfig = null;
238
+ this._activeElement = null;
239
+ this._alreadyExist = false;
240
+
241
+ this._elementsUpdate = false;
242
+ }
243
+
244
+ /** [내부함수] 그리드 스냅
245
+ * @param {number} value - 스냅할 값
246
+ * @returns {number} - 스냅된 값
247
+ */
248
+ _snapToGrid(value) {
249
+ return Math.round(value / this._gridSize) * this._gridSize;
250
+ }
251
+
252
+ /** [내부함수] layout 내부 진입 여부 감지
253
+ * @param {MouseEvent} event - 마우스 이벤트
254
+ * @param {Object} offsetPos - 오프셋 위치
255
+ * @return {boolean} - 레이아웃 내부 진입 여부
256
+ */
257
+ _detectLayoutEntry(event, offsetPos) {
258
+ const layoutEl = document.getElementById(`${this._activeSection}Layout`);
259
+ if (!layoutEl) {
260
+ console.error('[FreeDropEngine] 레이아웃 요소를 찾을 수 없습니다:', this._activeSection);
261
+ return false;
262
+ }
263
+
264
+ if (!this._activeElement) {
265
+ console.error('[FreeDropEngine] 활성화된 아이템 정보를 찾을 수 없습니다.');
266
+ return false;
267
+ }
268
+
269
+ const size = this._activeElement.size;
270
+ const layoutRect = layoutEl.getBoundingClientRect();
271
+
272
+ // 요소의 위치와 크기를 기준으로 경계 확인
273
+ const x = event.clientX - offsetPos.x + size.w;
274
+ const y = event.clientY - offsetPos.y + size.h;
275
+
276
+ const result = event.clientX >= layoutRect.left && event.clientY >= layoutRect.top && x <= layoutRect.right && y <= layoutRect.bottom;
277
+
278
+ return result;
279
+ }
280
+
281
+ /** [내부함수] 기존 배치요소와 충돌 여부 감지
282
+ * @return {boolean} - 충돌 여부
283
+ */
284
+ _detectElementCollision() {
285
+ if (!this._activeElement) {
286
+ console.error('[FreeDropEngine] 충돌 검사를 위한 아이템 정보를 찾을 수 없습니다:', this._activeElement.id);
287
+ return false;
288
+ }
289
+
290
+ const newElementRect = {
291
+ left: this._activeElement.position.x,
292
+ top: this._activeElement.position.y,
293
+ right: this._activeElement.position.x + this._activeElement.size.w,
294
+ bottom: this._activeElement.position.y + this._activeElement.size.h,
295
+ };
296
+
297
+ return this.freeElements
298
+ .filter((element) => element.section === this._activeElement.section)
299
+ .some((element) => {
300
+ if (element.id === this._activeElement.id) return false; // 자기 자신은 제외
301
+
302
+ const elementRect = {
303
+ left: element.position.x,
304
+ top: element.position.y,
305
+ right: element.position.x + element.size.w,
306
+ bottom: element.position.y + element.size.h,
307
+ };
308
+
309
+ return !(
310
+ newElementRect.right <= elementRect.left ||
311
+ newElementRect.left >= elementRect.right ||
312
+ newElementRect.bottom <= elementRect.top ||
313
+ newElementRect.top >= elementRect.bottom
314
+ );
315
+ });
316
+ }
317
+
318
+ /** [내부함수] 가이드라인 표시 설정 가져오기
319
+ * @return {boolean} - 가이드라인 표시 여부
320
+ */
321
+ _getGuideLineConfig() {
322
+ if (this._activeConfig) {
323
+ return this._activeConfig?.showGuideLine;
324
+ } else {
325
+ return false;
326
+ }
327
+ }
328
+
329
+ /** isLocked 값 변경 */
330
+ toggleLock() {
331
+ if (!this._activeElement) {
332
+ console.error('[FreeDropEngine] 활성화된 배치요소가 없습니다.');
333
+ return;
334
+ }
335
+ this._activeElement.isLocked = !this._activeElement.isLocked;
336
+ this._elementsUpdate = true;
337
+
338
+ this.eventBus.emit('freeDrop:toggleLock', {
339
+ elementId: this._activeElement.id,
340
+ timestamp: Date.now(),
341
+ });
342
+ }
343
+
344
+ /** ---------------------------------- 배치요소 관련 메소드 ---------------------------------- **/
345
+ /** 배치요소 신규 추가 - 메뉴에서 추가 아이템 선택
346
+ * @param {MouseEvent} event - 마우스 이벤트
347
+ * @param {string} elName - 추가할 아이템 이름
348
+ */
349
+ addElement(event, elName) {
350
+ // if (!this._activeData) {
351
+ // console.error('[FreeDropEngine] 활성화된 데이터가 없습니다.');
352
+ // return;
353
+ // }
354
+ if (this._activeMode !== 'free') return;
355
+
356
+ this._startPos = { x: event.clientX, y: event.clientY };
357
+
358
+ const elementSource = this.getResourceByName('elements');
359
+ const element = {
360
+ id: elName + '_' + Date.now().toString(36) + Math.random().toString(36).slice(2, 7),
361
+ type: elName,
362
+ mode: 'free',
363
+ position: this._startPos,
364
+ size: elementSource[elName] || { w: 100, h: 100 },
365
+ section: this._activeSection,
366
+ isLocked: false,
367
+ };
368
+
369
+ this.setActiveElement(element, 'add');
370
+
371
+ this._offsetPos.x = element.size.w / 2;
372
+ this._offsetPos.y = element.size.h / 2;
373
+
374
+ // this.startDrag(event, null);
375
+ // 신규 아이템 추가 시 바로 드래그 상태로 진입 (단, 기존 아이템과 달리 위치 기준이 마우스 포인터가 됨)
376
+ this.eventBus.emit('freeDrop:startDrag', { timestamp: Date.now() });
377
+
378
+ this._dragState.isDragging = true;
379
+ document.addEventListener('mousemove', this._startDragMove);
380
+ document.addEventListener('mouseup', this._stopDrag);
381
+ }
382
+
383
+ /** [내부함수] 배치요소 삭제
384
+ * @param {string} id - 삭제할 배치요소 ID
385
+ */
386
+ _deleteElement(id) {
387
+ const element = this.freeElements.find((el) => el.id === id);
388
+ if (!element) {
389
+ console.error('[FreeDropEngine] 삭제할 아이템을 찾을 수 없습니다:', id);
390
+ return;
391
+ }
392
+
393
+ if (confirm(`선택하신 ${element.type} 아이템을 삭제하시겠습니까?`)) {
394
+ // this.setActiveElement(null);
395
+
396
+ this.eventBus.emit('freeDrop:requestDeleteElement', {
397
+ elementId: id,
398
+ timestamp: Date.now(),
399
+ });
400
+ }
401
+ }
402
+
403
+ /** 활성화 배치요소 변경
404
+ * @param {object|null} element - 활성화할 배치요소 정보 (null인 경우 비활성화)
405
+ * @param {string|null} action - 액션 종류 (예: 'add')
406
+ */
407
+ setActiveElement(element, action = null) {
408
+ // 이미 활성화된 요소를 다시 활성화하려는 경우 무시
409
+ if (element && this._activeElement?.id === element?.id) return;
410
+
411
+ this._activeElement = element;
412
+ this._alreadyExist = action === 'add' ? false : true;
413
+ this._elementsUpdate = true;
414
+
415
+ this.eventBus.emit('freeDrop:setActiveElement', {
416
+ action: action,
417
+ activeElement: this._activeElement,
418
+ // elements: cloneDeep(this.freeElements),
419
+ timestamp: Date.now(),
420
+ });
421
+ }
422
+
423
+ /** 배치요소 스타일 가져오기
424
+ * @param {object} element - 배치요소 정보
425
+ * @returns {object} - 스타일 객체
426
+ */
427
+ getElementStyle(element) {
428
+ const { x, y } = element.position;
429
+ const { w, h } = element.size;
430
+
431
+ return {
432
+ left: `${x}px`,
433
+ top: `${y}px`,
434
+ width: `${w}px`,
435
+ height: `${h}px`,
436
+ };
437
+ }
438
+
439
+ /** [내부함수] 활성화 배치요소의 현재 위치 가져오기
440
+ * @param {MouseEvent} event - 마우스 이벤트
441
+ * @param {Object} offsetPos - 오프셋 위치 { x, y }
442
+ * @return {Object} - 활성화 배치요소의 그리드 스냅 위치 { x, y }
443
+ */
444
+ _getActiveElementPosition(event, offsetPos) {
445
+ const layoutEl = document.getElementById(`${this._activeSection}Layout`);
446
+ if (!layoutEl) {
447
+ console.error('[FreeDropEngine] 레이아웃 요소를 찾을 수 없습니다:', this._activeSection);
448
+ return { x: 0, y: 0 };
449
+ }
450
+
451
+ const layoutRect = layoutEl.getBoundingClientRect();
452
+ const rawX = event.clientX - layoutRect.left - offsetPos.x;
453
+ const rawY = event.clientY - layoutRect.top - offsetPos.y;
454
+
455
+ const gridX = this._snapToGrid(rawX);
456
+ const gridY = this._snapToGrid(rawY);
457
+
458
+ return { x: gridX, y: gridY };
459
+ }
460
+
461
+ /** [내부함수] 새로운 크기와 위치 계산
462
+ * @param {MouseEvent} event - 마우스 이벤트
463
+ * @param {string} direction - 리사이징 방향 (예: 'n', 'ne', 'e', 'se', 's', 'sw', 'w', 'nw')
464
+ * @returns {object} - 새로운 위치와 크기 { newX, newY, newW, newH }
465
+ */
466
+ _getNewSizeAndPosition(event, direction) {
467
+ const deltaX = event.clientX - this._offsetPos.x;
468
+ const deltaY = event.clientY - this._offsetPos.y;
469
+
470
+ let newX = this._startPos.x;
471
+ let newY = this._startPos.y;
472
+ let newW = this._startSize.w;
473
+ let newH = this._startSize.h;
474
+
475
+ // 방향에 따른 크기 및 위치 계산
476
+ // 위쪽 방향(north)
477
+ if (direction.includes('n')) {
478
+ const tmpHeight = this._snapToGrid(this._startSize.h - deltaY);
479
+ const offsetY = this._startSize.h - tmpHeight; // 높이 변화량
480
+
481
+ if (offsetY !== 0) {
482
+ newH = tmpHeight;
483
+ newY = this._startPos.y + offsetY;
484
+ }
485
+ }
486
+
487
+ // 아래쪽 방향(south)
488
+ if (direction.includes('s')) {
489
+ newH = this._snapToGrid(this._startSize.h + deltaY);
490
+ }
491
+
492
+ // 왼쪽 방향(west)
493
+ if (direction.includes('w')) {
494
+ const tempWidth = this._snapToGrid(this._startSize.w - deltaX);
495
+ const offsetX = this._startSize.w - tempWidth; // 너비 변화량
496
+
497
+ if (offsetX !== 0) {
498
+ newW = tempWidth;
499
+ newX = this._startPos.x + offsetX;
500
+ }
501
+ }
502
+
503
+ // 오른쪽 방향(east)
504
+ if (direction.includes('e')) {
505
+ newW = this._snapToGrid(this._startSize.w + deltaX);
506
+ }
507
+
508
+ // 최소 크기 제한
509
+ newW = Math.max(40, newW);
510
+ newH = Math.max(40, newH);
511
+
512
+ return { newX, newY, newW, newH };
513
+ }
514
+
515
+ /** [내부함수] 배치요소 위치 변경 */
516
+ _updateElementBounds = (() => {
517
+ let rafId = null;
518
+ return (position, size = null) => {
519
+ if (rafId) return;
520
+
521
+ rafId = nextTick(() => {
522
+ this._activeElement.position.x = position.x;
523
+ this._activeElement.position.y = position.y;
524
+
525
+ if (size) {
526
+ this._activeElement.size.w = size.w;
527
+ this._activeElement.size.h = size.h;
528
+ }
529
+
530
+ const showGuideLine = this._getGuideLineConfig();
531
+ if (showGuideLine) {
532
+ this.guides.x = position.x;
533
+ this.guides.y = position.y;
534
+ this.guides.w = this._activeElement.size.w;
535
+ this.guides.h = this._activeElement.size.h;
536
+ }
537
+
538
+ // 로컬에서 position, size 업데이트
539
+ this.eventBus.emit('freeDrop:requestUpdateLocalBounds', {
540
+ elementId: this._activeElement.id,
541
+ position: position,
542
+ size: size,
543
+ guides: this.guides,
544
+ timestamp: Date.now(),
545
+ });
546
+
547
+ rafId = null;
548
+ });
549
+ };
550
+ })();
551
+
552
+ /** ---------------------------------- 드래그 관련 메소드 ---------------------------------- **/
553
+ /** mousedown 실행
554
+ * @param {MouseEvent} event - 마우스 이벤트
555
+ * @param {string|null} id - 배치요소 ID (신규 아이템인 경우 null)
556
+ */
557
+ handleMouseDown(event, id = null) {
558
+ // 리사이징 중이면 드래그 불가
559
+ if (this._dragState.isResizing) return;
560
+
561
+ // 우클릭 시 해당 아이템 삭제
562
+ if (event.button === 2) {
563
+ this._deleteElement(id);
564
+ return false;
565
+ }
566
+
567
+ const element = this.freeElements.find((el) => el.id === id);
568
+ this.setActiveElement(element, 'click');
569
+
570
+ // 배치요소 잠겨 있으면 드래그/삭제 불가
571
+ if (element.isLocked) return;
572
+ this._startPos = { x: element.position.x, y: element.position.y };
573
+
574
+ const elementEl = document.getElementById(id);
575
+ if (!elementEl) {
576
+ console.error('[FreeDropEngine] 배치요소를 찾을 수 없습니다. ID:', id);
577
+ return false;
578
+ }
579
+
580
+ // 배치요소 위치 기준 오프셋 계산
581
+ const elementRect = elementEl.getBoundingClientRect();
582
+ if (elementRect) {
583
+ const { column, row } = this._gridNumber;
584
+
585
+ this._offsetPos.x = (event.clientX - elementRect.left) / column;
586
+ this._offsetPos.y = (event.clientY - elementRect.top) / row;
587
+ }
588
+
589
+ document.addEventListener('mousemove', this._detectDragIntent);
590
+ document.addEventListener('mouseup', this._cancelDragIntent);
591
+ }
592
+
593
+ // [내부함수] 드래그 의도 감지 - 최소 드래그 거리 이상으로 마우스가 이동한 경우
594
+ _detectDragIntent = (event) => {
595
+ const deltaX = Math.abs(event.clientX - this._startPos.x);
596
+ const deltaY = Math.abs(event.clientY - this._startPos.y);
597
+
598
+ // 최소 드래그 거리(gridSize) 이상 이동 여부
599
+ const isMoved = deltaX >= this._gridSize || deltaY >= this._gridSize;
600
+
601
+ if (isMoved) {
602
+ // 의도 감지 리스너 제거
603
+ document.removeEventListener('mousemove', this._detectDragIntent);
604
+ document.removeEventListener('mouseup', this._cancelDragIntent);
605
+
606
+ // 드래그 확정 후 실제 드래그 리스너 등록
607
+ this._dragState.isDragging = true;
608
+
609
+ this.eventBus.emit('freeDrop:startDrag', { timestamp: Date.now() });
610
+ document.addEventListener('mousemove', this._startDragMove);
611
+ document.addEventListener('mouseup', this._stopDrag);
612
+ // const position = this._getActiveElementPosition(event, this._offsetPos);
613
+ // this._updateElementBounds(position);
614
+ }
615
+ };
616
+
617
+ // [내부함수] 드래그 의도 취소 - 최소 드래그 거리 미만으로 마우스가 이동한 경우
618
+ _cancelDragIntent = (event) => {
619
+ document.removeEventListener('mousemove', this._detectDragIntent);
620
+ document.removeEventListener('mouseup', this._cancelDragIntent);
621
+ };
622
+
623
+ /** [내부함수] 드래그 진행
624
+ * @param {MouseEvent} event - 마우스 이벤트
625
+ */
626
+ _startDragMove = (event) => {
627
+ // 리사이징 중이면 드래그 불가
628
+ if (this._dragState.isResizing) return;
629
+
630
+ // 레이아웃 내부 진입 여부
631
+ const isInsideLayout = this._detectLayoutEntry(event, this._offsetPos);
632
+
633
+ if (isInsideLayout) {
634
+ const position = this._getActiveElementPosition(event, this._offsetPos);
635
+ this._updateElementBounds(position);
636
+ }
637
+ };
638
+
639
+ /** [내부함수] 드래그 종료 */
640
+ _stopDrag = () => {
641
+ // 정상적인 드래그 상태인 경우 리셋 후 종료 (단순 클릭, 레이아웃 외부에서 드래그 종료)
642
+ if (!this._dragState.isDragging) {
643
+ this._resetDrag();
644
+ return;
645
+ }
646
+
647
+ // 1) 기존 배치요소인 경우
648
+ if (this._alreadyExist) {
649
+ // 충돌 시 기존 위치로 복귀
650
+ if (this._detectElementCollision()) {
651
+ alert('아이템이 겹칩니다. 다른 위치로 드래그해주세요.');
652
+ this._activeElement.position.x = this._startPos.x;
653
+ this._activeElement.position.y = this._startPos.y;
654
+ this._resetDrag(true);
655
+ } else {
656
+ this._resetDrag(true, true);
657
+ }
658
+
659
+ return;
660
+ }
661
+ // 2) 신규 아이템인 경우
662
+ else {
663
+ // 충돌 시 추가 취소 (로컬에만 반영됨)
664
+ if (this._detectElementCollision()) {
665
+ alert('겹치는 위치에 배치할 수 없습니다.');
666
+ this.setActiveElement(null, 'add');
667
+ } else {
668
+ this.guides = { x: null, y: null, w: null, h: null };
669
+ this.eventBus.emit('freeDrop:requestAddElement', {
670
+ element: this._activeElement,
671
+ timestamp: Date.now(),
672
+ });
673
+ }
674
+ this._resetDrag();
675
+ return;
676
+ }
677
+ };
678
+
679
+ /** [내부함수] 드래그 관련 상태 초기화
680
+ * @param {boolean} emitEvent - 업데이트 이벤트 발송 여부
681
+ * @param {boolean} historyEvent - 히스토리 이벤트 기록 여부
682
+ */
683
+ _resetDrag(emitEvent = false, historyEvent = false) {
684
+ this._dragState.isDragging = false;
685
+ this.guides = { x: null, y: null, w: null, h: null };
686
+
687
+ this._startPos = { x: 0, y: 0 };
688
+ this._startSize = { w: 0, h: 0 };
689
+ this._offsetPos = { x: 0, y: 0 };
690
+
691
+ if (emitEvent) {
692
+ this.eventBus.emit('freeDrop:requestUpdateElement', {
693
+ historyEvent: historyEvent,
694
+ elementId: this._activeElement.id,
695
+ position: this._activeElement.position,
696
+ size: this._activeElement.size,
697
+ elements: cloneDeep(this.freeElements),
698
+ guides: this.guides,
699
+ timestamp: Date.now(),
700
+ });
701
+ }
702
+
703
+ document.removeEventListener('mousemove', this._startDragMove);
704
+ document.removeEventListener('mouseup', this._stopDrag);
705
+ }
706
+
707
+ /** ---------------------------------- 리사이징 관련 메소드 ---------------------------------- **/
708
+ /** 리사이징 시작
709
+ * @param {MouseEvent} event - 마우스 이벤트
710
+ * @param {string} direction - 리사이징 방향
711
+ */
712
+ startResize(event, direction) {
713
+ if (!this._activeElement) {
714
+ console.error('[FreeDropEngine] 활성화된 배치요소를 찾을 수 없습니다.');
715
+ return false;
716
+ }
717
+
718
+ // 배치요소 잠겨 있으면 드래그/삭제 불가
719
+ if (this._activeElement.isLocked) return;
720
+
721
+ this._dragState.isResizing = true;
722
+ this._dragState.direction = direction;
723
+ this._offsetPos = { x: event.clientX, y: event.clientY };
724
+
725
+ this._startPos = { x: this._activeElement.position.x, y: this._activeElement.position.y };
726
+ this._startSize = { w: this._activeElement.size.w, h: this._activeElement.size.h };
727
+
728
+ document.addEventListener('mousemove', this._startResizeMove);
729
+ document.addEventListener('mouseup', this._stopResize);
730
+ }
731
+
732
+ /** [내부함수] 리사이징 진행
733
+ * @param {MouseEvent} event - 마우스 이벤트
734
+ */
735
+ _startResizeMove = (event) => {
736
+ // 리사이징 중인 경우에만 드래그 가능
737
+ if (!this._dragState.isResizing) return;
738
+
739
+ const { newX, newY, newW, newH } = this._getNewSizeAndPosition(event, this._dragState.direction);
740
+ this._updateElementBounds({ x: newX, y: newY }, { w: newW, h: newH });
741
+ };
742
+
743
+ /** [내부함수] 리사이징 종료 */
744
+ _stopResize = () => {
745
+ // 충돌 시 기존 위치로 복귀
746
+ if (this._detectElementCollision()) {
747
+ alert('아이템이 겹칩니다. 사이즈 조정이 취소되었습니다.');
748
+ this._activeElement.position = this._startPos;
749
+ this._activeElement.size = this._startSize;
750
+ this._resetResize();
751
+ } else {
752
+ this._resetResize(true);
753
+ }
754
+
755
+ return;
756
+ };
757
+
758
+ /** [내부함수] 리사이즈 관련 상태 초기화
759
+ * @param {boolean} historyEvent - 히스토리 이벤트 기록 여부
760
+ */
761
+ _resetResize(historyEvent = false) {
762
+ this._dragState.isResizing = false;
763
+ this._dragState.direction = false;
764
+
765
+ this.guides = { x: null, y: null, w: null, h: null };
766
+
767
+ this._startPos = { x: 0, y: 0 };
768
+ this._startSize = { w: 0, h: 0 };
769
+ this._offsetPos = { x: 0, y: 0 };
770
+
771
+ this.eventBus.emit('freeDrop:requestUpdateElement', {
772
+ historyEvent: historyEvent,
773
+ elementId: this._activeElement.id,
774
+ position: this._activeElement.position,
775
+ size: this._activeElement.size,
776
+ activeElement: this._activeElement,
777
+ guides: this.guides,
778
+ timestamp: Date.now(),
779
+ });
780
+
781
+ document.removeEventListener('mousemove', this._startResizeMove);
782
+ document.removeEventListener('mouseup', this._stopResize);
783
+ }
784
+ }
785
+
786
+ export default FreeDropEngine;