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,826 @@
1
+ import { cloneDeep } from 'lodash-es';
2
+ import { nextTick } from 'vue';
3
+
4
+ import EventBus from './EventBus.js';
5
+ import ApiManager from './ApiManager.js';
6
+ import utils from '../../utils/index.js';
7
+ const { dataConverter } = utils;
8
+
9
+ import createDefaultConfig from '../../configs/index.js';
10
+ const { initialState, resource } = createDefaultConfig();
11
+
12
+ // event 발행 시 'system'으로 시작하는 이름 사용
13
+ class EngineManager {
14
+ constructor(config = {}) {
15
+ this.eventBus = new EventBus();
16
+ this.engines = new Map();
17
+ this.config = { ...config };
18
+
19
+ // API 인스턴스
20
+ if (config.apiClient) {
21
+ this.apiManager = new ApiManager(config.apiClient, config.apiEndpoints);
22
+ }
23
+
24
+ this.state = {
25
+ ...initialState,
26
+ ...config.initialState,
27
+ };
28
+
29
+ // (캐시) layoutData 및 activeSection 변경 감지용 -> activeData getter에서 사용
30
+ this._layoutData = null;
31
+
32
+ // (캐시) dataConverter에서 사용하는 현재 컨테이너 크기
33
+ this._containerWidth = 1920;
34
+ this._containerHeight = 1080;
35
+
36
+ this._subscriptions = [];
37
+ this._initialized = false;
38
+ }
39
+
40
+ /** activeSection 데이터 반환
41
+ * @return {object} - layoutData 중 activeSection 데이터 객체
42
+ */
43
+ get activeData() {
44
+ if (!this._layoutData || !this.state.activeSection) {
45
+ return null;
46
+ }
47
+ return this._layoutData[this.state.activeSection];
48
+ }
49
+
50
+ /** 상태 정보 반환
51
+ * @return {object} - 현재 상태 객체
52
+ */
53
+ getState() {
54
+ return {
55
+ elements: this.state.elements,
56
+ layoutData: cloneDeep(this._layoutData),
57
+ activeData: cloneDeep(this.activeData),
58
+ activeSection: this.state.activeSection,
59
+ };
60
+ }
61
+
62
+ /** 상태 정보 설정
63
+ * @param {object} state - 복원할 상태 객체
64
+ */
65
+ setState(state) {
66
+ this.state.elements = state.elements;
67
+ this._layoutData = state.layoutData;
68
+ this.setActiveSection(state.activeSection);
69
+
70
+ this.eventBus.emit('system:restoredState', {
71
+ stateData: state,
72
+ timestamp: Date.now(),
73
+ });
74
+ }
75
+
76
+ /** [내부함수] activeSection의 설정값 업데이트
77
+ * @param {string} type - 설정 타입 (mode, config)
78
+ * @param {object} updates - 업데이트할 설정 값 객체
79
+ */
80
+ _updateActiveSectionConfig(type, updates) {
81
+ if (!this._layoutData || !this.state.activeSection) {
82
+ console.error('[EngineManager] activeData를 업데이트할 수 없습니다.');
83
+ return;
84
+ }
85
+
86
+ Object.keys(updates).forEach((key) => {
87
+ if (!(key in this._layoutData[this.state.activeSection])) {
88
+ if (key == 'elementIds') {
89
+ this._layoutData[this.state.activeSection][key] = [];
90
+ } else {
91
+ this._layoutData[this.state.activeSection][key] = null;
92
+ }
93
+ }
94
+ });
95
+
96
+
97
+ // 원본 데이터 업데이트
98
+ this._layoutData[this.state.activeSection] = {
99
+ ...this._layoutData[this.state.activeSection],
100
+ ...updates,
101
+ };
102
+
103
+
104
+ this.eventBus.emit('system:updateLayoutData', {
105
+ section: this.state.activeSection,
106
+ changes: {...updates},
107
+ $mode: this.activeData?.mode,
108
+ $config: this.activeData?.config,
109
+ timestamp: Date.now(),
110
+ })
111
+
112
+ // // 이벤트 발행
113
+ // this.eventBus.emit('system:updateActiveSectionConfig', {
114
+ // type: type,
115
+ // layoutData: cloneDeep(this._layoutData),
116
+ // activeData: cloneDeep(this.activeData),
117
+ // timestamp: Date.now(),
118
+ // });
119
+ }
120
+
121
+ // /** [내부함수] activeSection의 elementIds 업데이트
122
+ // * @param {Array} elementIds - 새로운 elementIds 배열
123
+ // */
124
+ // _updateActiveElementIds(elementIds) {
125
+ // if (!this._layoutData || !this.state.activeSection) {
126
+ // console.error('[EngineManager] activeData를 업데이트할 수 없습니다.');
127
+ // return;
128
+ // }
129
+
130
+ // if (!('elementIds' in this._layoutData[this.state.activeSection])) {
131
+ // this._layoutData[this.state.activeSection].elementIds = [];
132
+ // }
133
+
134
+ // this._layoutData[this.state.activeSection].elementIds = elementIds;
135
+ // }
136
+
137
+ /** 현재 컨테이너 크기 설정 */
138
+ setContainerSize() {
139
+ try {
140
+ const containerEl = document.getElementById('baseLayout');
141
+ if (!containerEl) {
142
+ console.warn('[EngineManager] baseLayout 요소를 찾을 수 없습니다.');
143
+ return false;
144
+ }
145
+ const { width, height } = containerEl.getBoundingClientRect();
146
+
147
+ this._containerWidth = width ?? this._containerWidth;
148
+ this._containerHeight = height ?? this._containerHeight;
149
+ } catch (error) {
150
+ console.error('[EngineManager] 컨테이너 크기 설정 중 오류 발생: ', error);
151
+ return false;
152
+ }
153
+ }
154
+
155
+ getContainerSize() {
156
+ return {
157
+ width: this._containerWidth,
158
+ height: this._containerHeight,
159
+ };
160
+ }
161
+
162
+ /** elements 초기화
163
+ * @param {Array} elements - 새로운 elements 배열
164
+ */
165
+ setElements(elements) {
166
+ if (!elements || !Array.isArray(elements)) {
167
+ console.error('[EngineManager] 유효하지 않은 elements:', elements);
168
+ return false;
169
+ }
170
+
171
+ const denormalizedElements = dataConverter.denormalize(elements, this._containerWidth, this._containerHeight);
172
+
173
+ // section 정보가 없는 경우 section1으로 간주
174
+ this.state.elements = denormalizedElements.map((x) => (x.section ? { ...x } : { ...x, section: 'section1' }));
175
+
176
+ this.eventBus.emit('system:setElements', {
177
+ layoutData: cloneDeep(this._layoutData),
178
+ elements: this.state.elements,
179
+ timestamp: Date.now(),
180
+ });
181
+ }
182
+
183
+ /** activeElement 업데이트
184
+ * @param {string} id - 업데이트할 element의 ID
185
+ * @param {object} changes - 변경할 속성 객체
186
+ */
187
+ updateActiveElement(id, changes) {
188
+ if (!id || !changes) return false;
189
+
190
+ const index = this.state.elements.findIndex((x) => x.id === id);
191
+ if (index === -1) return false;
192
+
193
+ // 보호필드 (id, mode, section)은 직접 업데이트 불가 - 레이아웃 엔진에서 일괄 관리
194
+ const { id: _, mode: __, section: ___, ...safeChanges } = changes;
195
+ if (Object.keys(safeChanges).length === 0) return false;
196
+
197
+ const updatedElement = {
198
+ ...this.state.elements[index],
199
+ ...safeChanges,
200
+ };
201
+
202
+ this.state.elements = [...this.state.elements.slice(0, index), updatedElement, ...this.state.elements.slice(index + 1)];
203
+
204
+ // 변경된 element만 전달
205
+ this.eventBus.emit('system:requestUpdateActiveElement', {
206
+ // elements: this.state.elements,
207
+ elementId: id,
208
+ element: updatedElement,
209
+ timestamp: Date.now(),
210
+ });
211
+ }
212
+
213
+ /** ---------------------------------- display Engine ---------------------------------- **/
214
+ /** [내부함수] displayEngine 연결 설정 */
215
+ _setupDisplayEngineConnections() {
216
+ this._subscribe('display:updateDisplayMode', ({ displaySize, prev }) => {
217
+ this._updateElementsScale(prev.displaySize, displaySize);
218
+ });
219
+
220
+ this._subscribe('display:updateDisplaySize', ({ displaySize, prev }) => {
221
+ this._updateElementsScale(prev.displaySize, displaySize);
222
+ });
223
+ }
224
+
225
+ _updateElementsScale(oldValue, newValue) {
226
+ const oldWidth = oldValue.px;
227
+ const oldHeight =
228
+ oldValue.px /
229
+ oldValue.aspectRatio
230
+ .split('/')
231
+ .map(Number)
232
+ .reduce((a, b) => a / b);
233
+
234
+ const newWidth = newValue.px;
235
+ const newHeight =
236
+ newValue.px /
237
+ newValue.aspectRatio
238
+ .split('/')
239
+ .map(Number)
240
+ .reduce((a, b) => a / b);
241
+
242
+ nextTick(() => {
243
+ this.setContainerSize();
244
+
245
+ const normalizeElements = dataConverter.normalize(this.state.elements, oldWidth, oldHeight);
246
+ const denormalizedElements = dataConverter.denormalize(normalizeElements, newWidth, newHeight);
247
+
248
+ this.state.elements = cloneDeep(denormalizedElements);
249
+
250
+ this.eventBus.emit('system:requestUpdateElements', {
251
+ historyEvent: true,
252
+ elements: cloneDeep(this.state.elements),
253
+ timestamp: Date.now(),
254
+ });
255
+ });
256
+ }
257
+
258
+ /** ---------------------------------- layout Engine ---------------------------------- **/
259
+ /** [내부함수] layoutEngine 연결 설정 */
260
+ _setupLayoutEngineConnections() {
261
+ // layoutName 변경에 따른 activeSection 초기화
262
+ this._subscribe('layout:setLayoutName', ({ $layoutData }) => {
263
+ this._layoutData = $layoutData;
264
+ this.setActiveSection('section1', true);
265
+
266
+ this.eventBus.emit('system:updateLayoutData', {
267
+ activeData: cloneDeep(this.activeData),
268
+ timestamp: Date.now(),
269
+ });
270
+ });
271
+ this._subscribe('layout:updateLayoutName', ({ layoutData }) => {
272
+ this._layoutData = layoutData;
273
+ this.setActiveSection(null, true);
274
+
275
+ this.eventBus.emit('system:updateLayoutData', {
276
+ activeData: cloneDeep(this.activeData),
277
+ timestamp: Date.now(),
278
+ });
279
+ });
280
+ }
281
+
282
+ /** activeSection 변경
283
+ * @param {string} name - 섹션 이름
284
+ * @param {boolean} setting - 설정모드 여부
285
+ */
286
+ setActiveSection(name, setting = false) {
287
+ if (!setting && this.state.activeSection === name) return;
288
+
289
+ this.state.activeSection = name;
290
+
291
+ this.eventBus.emit('system:updateActiveSection', {
292
+ activeSection: name,
293
+ activeData: cloneDeep(this.activeData),
294
+ timestamp: Date.now(),
295
+ });
296
+ }
297
+
298
+ /** sectionMode 변경
299
+ * @param {string} mode - 섹션 모드('free' | 'grid')
300
+ */
301
+ setSectionMode(mode) {
302
+ if (!['free', 'grid'].includes(mode)) {
303
+ console.error('[EngineManager] 잘못된 섹션 모드:', mode);
304
+ return;
305
+ }
306
+
307
+ if (!this.activeData || this.activeData.mode === mode) return;
308
+ const updates = {
309
+ mode: mode,
310
+ elementIds: this.state.elements.filter((x) => x.mode === mode).map((x) => x.id),
311
+ };
312
+
313
+ this._updateActiveSectionConfig('mode', updates);
314
+ }
315
+
316
+ /** sectionConfig 변경
317
+ * @param {string} type - config 타입 (column, row, gap, guideLine)
318
+ * @param {number|boolean} config - 새로운 설정 값
319
+ */
320
+ setSectionConfig(type, config) {
321
+ if (!['column', 'row', 'gap', 'guideLine'].includes(type)) {
322
+ console.error('[EngineManager] 잘못된 config 타입:', type);
323
+ return;
324
+ }
325
+
326
+ if (!this.activeData) return;
327
+
328
+ const limits = {
329
+ column: { min: 1, max: 12, name: '열 개수' },
330
+ row: { min: 1, max: 50, name: '행 개수' },
331
+ gap: { min: 0, max: 12, name: '갭 크기' },
332
+ };
333
+
334
+ let newConfig = {};
335
+ let value = config;
336
+
337
+ if (type === 'guideLine') {
338
+ newConfig = { showGuideLine: value };
339
+ } else {
340
+ if (value < limits[type].min) {
341
+ alert(`설정 가능한 최소 ${limits[type].name}는 ${limits[type].min}입니다.`);
342
+ }
343
+ if (value > limits[type].max) {
344
+ alert(`설정 가능한 최대 ${limits[type].name}는 ${limits[type].max}입니다.`);
345
+ }
346
+ value = Math.min(Math.max(limits[type].min, value), limits[type].max);
347
+
348
+ if (type === 'column') {
349
+ newConfig = { gridColumns: value };
350
+ } else if (type === 'row') {
351
+ newConfig = { gridRows: value };
352
+ } else if (type === 'gap') {
353
+ newConfig = { gridGap: value };
354
+ }
355
+ }
356
+
357
+ const updates = { config: { ...this.activeData.config, ...newConfig } };
358
+ this._updateActiveSectionConfig('config', updates);
359
+ }
360
+
361
+ /** ---------------------------------- freeDrop Engine ---------------------------------- **/
362
+ /** [내부함수] freeDropEngine 연결 설정 */
363
+ _setupFreeDropEngineConnections() {
364
+ // 배치요소 추가 요청
365
+ this._subscribe('freeDrop:requestAddElement', ({ element }) => {
366
+ if (this.state.elements.some((el) => el.id === element.id)) {
367
+ return;
368
+ }
369
+
370
+ this.state.elements.push(element);
371
+ this._updateActiveSectionConfig('elementIds', { elementIds: [...this.activeData.elementIds, element.id] });
372
+ // this._updateActiveElementIds([...this.activeData.elementIds, element.id]);
373
+
374
+ console.log('this.state.elements', this.state.elements)
375
+ this.eventBus.emit('system:requestUpdateData', {
376
+ action: 'add',
377
+ elements: this.state.elements,
378
+ elementId: element.id,
379
+ layoutData: cloneDeep(this._layoutData),
380
+ activeData: cloneDeep(this.activeData),
381
+ activeSection: this.state.activeSection,
382
+ timestamp: Date.now(),
383
+ });
384
+ });
385
+
386
+ // 배치요소 삭제 요청
387
+ this._subscribe('freeDrop:requestDeleteElement', ({ elementId }) => {
388
+ this.state.elements = this.state.elements.filter((el) => el.id !== elementId);
389
+ this._updateActiveSectionConfig('elementIds', { elementIds: this.activeData.elementIds.filter((elId) => elId !== elementId) });
390
+ // this._updateActiveElementIds(this.activeData.elementIds.filter((elId) => elId !== elementId));
391
+
392
+ this.eventBus.emit('system:requestUpdateData', {
393
+ action: 'delete',
394
+ elements: this.state.elements,
395
+ elementId: null,
396
+ layoutData: cloneDeep(this._layoutData),
397
+ activeData: cloneDeep(this.activeData),
398
+ activeSection: this.state.activeSection,
399
+ timestamp: Date.now(),
400
+ });
401
+ });
402
+
403
+ // 배치요소 위치 및 사이즈 변경 반영 요청
404
+ this._subscribe('freeDrop:requestUpdateElement', ({ historyEvent, elementId, position, size, guides }) => {
405
+ const element = this.state.elements.find((el) => el.id === elementId);
406
+ if (element) {
407
+ element.position = position;
408
+ element.size = size;
409
+ }
410
+
411
+ this.eventBus.emit('system:requestUpdateElements', {
412
+ historyEvent: historyEvent,
413
+ elements: cloneDeep(this.state.elements),
414
+ guides: guides,
415
+ timestamp: Date.now(),
416
+ });
417
+ });
418
+ }
419
+
420
+ /** ---------------------------------- gridDrop Engine ---------------------------------- **/
421
+ /** [내부함수] gridDropEngine 연결 설정 */
422
+ _setupGridDropEngineConnections() {
423
+ // 배치요소 추가 요청
424
+ this._subscribe('gridDrop:requestAddElement', ({ element }) => {
425
+ this.state.elements.push(element);
426
+ this._updateActiveSectionConfig('elementIds', { elementIds: [...this.activeData.elementIds, element.id] });
427
+ // this._updateActiveElementIds([...this.activeData.elementIds, element.id]);
428
+
429
+ this.eventBus.emit('system:requestUpdateData', {
430
+ action: 'add',
431
+ elements: this.state.elements,
432
+ elementId: element.id,
433
+ layoutData: cloneDeep(this._layoutData),
434
+ activeData: cloneDeep(this.activeData),
435
+ activeSection: this.state.activeSection,
436
+ timestamp: Date.now(),
437
+ });
438
+ });
439
+
440
+ // 배치요소 삭제 요청
441
+ this._subscribe('gridDrop:requestDeleteElement', ({ elementId }) => {
442
+ this.state.elements = this.state.elements.filter((el) => el.id !== elementId);
443
+ this._updateActiveSectionConfig('elementIds', { elementIds: this.activeData.elementIds.filter((elId) => elId !== elementId) });
444
+ // this._updateActiveElementIds(this.activeData.elementIds.filter((elId) => elId !== elementId));
445
+
446
+ this.eventBus.emit('system:requestUpdateData', {
447
+ action: 'delete',
448
+ elements: this.state.elements,
449
+ elementId: null,
450
+ layoutData: cloneDeep(this._layoutData),
451
+ activeData: cloneDeep(this.activeData),
452
+ activeSection: this.state.activeSection,
453
+ timestamp: Date.now(),
454
+ });
455
+ });
456
+
457
+ // 배치요소 위치 및 사이즈 변경 요청
458
+ this._subscribe('gridDrop:requestUpdateElement', ({ historyEvent, elementId, position, size }) => {
459
+ const element = this.state.elements.find((el) => el.id === elementId);
460
+ if (element) {
461
+ element.position = position;
462
+ if (size) {
463
+ element.size = size;
464
+ }
465
+ }
466
+
467
+ this.eventBus.emit('system:requestUpdateElements', {
468
+ historyEvent: historyEvent,
469
+ elements: cloneDeep(this.state.elements),
470
+ timestamp: Date.now(),
471
+ });
472
+ });
473
+ }
474
+
475
+ /** ---------------------------------- history Engine ---------------------------------- **/
476
+ /** [내부함수] historyEngine 연결 설정 */
477
+ _setupHistoryEngineConnections() {}
478
+
479
+ /** ---------------------------------- 구독 관련 메소드 ---------------------------------- **/
480
+ /** [내부함수] 구독 및 unsubscribe 함수 저장
481
+ * @param {string} name - 이벤트 이름
482
+ * @param {function} handler - 이벤트 핸들러 함수
483
+ * @return {function} - unsubscribe 함수
484
+ */
485
+ _subscribe(name, handler) {
486
+ const unsubscribe = this.eventBus.subscribe(name, handler);
487
+ this._subscriptions.push(unsubscribe);
488
+ return unsubscribe;
489
+ }
490
+
491
+ /** ---------------------------------- 엔진 초기화 메서드 ---------------------------------- **/
492
+ /** [내부함수] 엔진 초기화 */
493
+ async initialize() {
494
+ // 초기화 되지 않은 경우에만 초기화 수행
495
+ if (!this._initialized) {
496
+ await this._createEngines();
497
+ await this._setupEngineConnections();
498
+ this._initialized = true;
499
+ }
500
+ }
501
+
502
+ /** [내부함수] 엔진 인스턴스 생성 */
503
+ async _createEngines() {
504
+ try {
505
+ console.log('[EngineManager] 엔진 생성 시작');
506
+
507
+ const [
508
+ { default: DisplayEngine },
509
+ { default: LayoutEngine },
510
+ { default: FreeDropEngine },
511
+ { default: GridDropEngine },
512
+ { default: HistoryEngine },
513
+ ] = await Promise.all([
514
+ import('../engines/DisplayEngine.js'),
515
+ import('../engines/LayoutEngine.js'),
516
+ import('../engines/FreeDropEngine.js'),
517
+ import('../engines/GridDropEngine.js'),
518
+ import('../engines/HistoryEngine.js'),
519
+ ]);
520
+ this.addEngine('display', DisplayEngine, {
521
+ eventBus: this.eventBus,
522
+ options: {
523
+ resource: resource.display,
524
+ },
525
+ });
526
+ this.addEngine('layout', LayoutEngine, {
527
+ eventBus: this.eventBus,
528
+ options: {
529
+ resource: resource.layout,
530
+ },
531
+ });
532
+ this.addEngine('freeDrop', FreeDropEngine, {
533
+ eventBus: this.eventBus,
534
+ options: {
535
+ gridSize: this.state.gridSize,
536
+ resource: resource.freedrop,
537
+ },
538
+ });
539
+ this.addEngine('gridDrop', GridDropEngine, {
540
+ eventBus: this.eventBus,
541
+ options: {
542
+ resource: resource.griddrop,
543
+ },
544
+ });
545
+ this.addEngine('history', HistoryEngine, {
546
+ eventBus: this.eventBus,
547
+ options: {
548
+ engines: this.engines,
549
+ manager: this,
550
+ },
551
+ });
552
+ console.log('[EngineManager] 엔진 생성 완료');
553
+ } catch (error) {
554
+ console.error('[EngineManager] 엔진 생성 중 오류 발생: ', error);
555
+ throw error;
556
+ }
557
+ }
558
+
559
+ /** [내부함수] 엔진 인스턴스 간 연결 설정 */
560
+ async _setupEngineConnections() {
561
+ this._setupDisplayEngineConnections();
562
+ this._setupLayoutEngineConnections();
563
+ this._setupFreeDropEngineConnections();
564
+ this._setupGridDropEngineConnections();
565
+ this._setupHistoryEngineConnections();
566
+
567
+ // 모든 연결 설정 완료 후 초기화 완료 이벤트 발행
568
+ this.eventBus.emit('system:engineInitialized', {
569
+ state: this.state,
570
+ timestamp: Date.now(),
571
+ });
572
+ }
573
+
574
+ /** ---------------------------------- 엔진 관리 메소드 ---------------------------------- **/
575
+ /** 엔진 정보 불러오기
576
+ * @param {string} name - 엔진 이름
577
+ * @return {object} - 엔진 인스턴스
578
+ */
579
+ getEngine(name) {
580
+ return this.engines.get(name);
581
+ }
582
+
583
+ /** 엔진 추가
584
+ * @param {string} name - 엔진 이름
585
+ * @param {class} engineClass - 엔진 클래스
586
+ * @param {object} config - 엔진 생성 옵션
587
+ * @return {boolean} - 추가 성공 여부
588
+ */
589
+ addEngine(name, engineClass, config = {}) {
590
+ if (this.engines.has(name)) {
591
+ console.warn(`[EngineManager] 이미 존재하는 엔진 이름입니다: ${name}`);
592
+ return false;
593
+ }
594
+
595
+ try {
596
+ const engine = new engineClass({
597
+ eventBus: this.eventBus,
598
+ ...config,
599
+ });
600
+
601
+ this.engines.set(name, engine);
602
+ return true;
603
+ } catch (error) {
604
+ console.error(`[EngineManager] ${name} 엔진 추가 중 오류 발생`, error);
605
+ return false;
606
+ }
607
+ }
608
+
609
+ /** 엔진 제거
610
+ * @param {string} name - 엔진 이름
611
+ * @return {boolean} - 제거 성공 여부
612
+ */
613
+ removeEngine(name) {
614
+ if (!this.engines.has(name)) {
615
+ console.warn(`[EngineManager] 존재하지 않는 엔진 이름입니다: ${name}`);
616
+ return false;
617
+ }
618
+
619
+ try {
620
+ const engine = this.engines.get(name);
621
+
622
+ if (typeof engine.destroy === 'function') {
623
+ engine.destroy();
624
+ }
625
+
626
+ this.engines.delete(name);
627
+ console.log(`[EngineManager] ${name} 엔진이 제거되었습니다`);
628
+ return true;
629
+ } catch (error) {
630
+ console.error(`[EngineManager] ${name} 엔진 제거 중 오류 발생`, error);
631
+ return false;
632
+ }
633
+ }
634
+
635
+ /** 엔진 매니저 삭제 */
636
+ destroy() {
637
+ console.log('[EngineManager] 엔진 매니저 삭제 시작');
638
+
639
+ // 모든 구독 해제
640
+ this._subscriptions.forEach((unsubscribe) => {
641
+ if (typeof unsubscribe === 'function') {
642
+ unsubscribe();
643
+ }
644
+ });
645
+
646
+ this._subscriptions = [];
647
+
648
+ for (const [name, engine] of this.engines) {
649
+ try {
650
+ this.removeEngine(name);
651
+ } catch (error) {
652
+ console.error(`[EngineManager] ${name} 엔진 삭제 중 오류 발생`, error);
653
+ }
654
+ }
655
+
656
+ this.engines.clear();
657
+ this._initialized = false;
658
+
659
+ this._layoutData = null;
660
+
661
+ this.state = {};
662
+ this.eventBus = null;
663
+
664
+ console.log('[EngineManager] 엔진 매니저 삭제 완료');
665
+ }
666
+
667
+ /** 엔진 초기화 */
668
+ resetEngines() {
669
+ this.state = {
670
+ ...initialState,
671
+ ...this.config.initialState,
672
+ };
673
+
674
+ this._layoutData = null;
675
+
676
+ this.eventBus.emit('system:enginesReset', {
677
+ timestamp: Date.now(),
678
+ });
679
+ }
680
+
681
+ /** ---------------------------------- 캡슐화 API 메소드 ---------------------------------- **/
682
+ /** 외부 접근용 API 반환
683
+ * @return {object} - 외부용 API 객체
684
+ */
685
+ getAPI() {
686
+ const displayEngine = this.getEngine('display');
687
+ const layoutEngine = this.getEngine('layout');
688
+ const freeDropEngine = this.getEngine('freeDrop');
689
+ const gridDropEngine = this.getEngine('gridDrop');
690
+ const historyEngine = this.getEngine('history');
691
+
692
+ const api = {
693
+ eventBus: this.eventBus,
694
+ resetEngines: () => this.resetEngines(),
695
+
696
+ // 엔진별 상태 조회 메서드
697
+ getDisplayState: () => ({
698
+ activeMenu: displayEngine.activeMenu,
699
+ activeDesign: displayEngine.activeDesign,
700
+ displayMode: displayEngine.displayMode,
701
+ displaySize: cloneDeep(displayEngine.displaySize),
702
+ }),
703
+
704
+ getLayoutState: () => ({
705
+ layoutName: layoutEngine.layoutName,
706
+ layoutSource: layoutEngine.layoutSource,
707
+ elements: this.state.elements,
708
+ layoutData: layoutEngine.layoutData,
709
+ activeSection: this.state.activeSection,
710
+ activeData: this.activeData,
711
+ gapSize: layoutEngine.gapSize,
712
+ gridNumber: cloneDeep(layoutEngine.gridNumber),
713
+ gridRatio: cloneDeep(layoutEngine.gridRatio),
714
+ }),
715
+
716
+ getFreeDropState: () => ({
717
+ handles: freeDropEngine.getResourceByName('handles'),
718
+ }),
719
+ getGridDropState: () => ({}),
720
+ getHistoryState: () => ({}),
721
+
722
+ display: {
723
+ setActiveMenu: (name) => displayEngine.setActiveMenu(name),
724
+ setActiveDesign: (name) => displayEngine.setActiveDesign(name),
725
+ setDisplayMode: (mode) => displayEngine.setDisplayMode(mode),
726
+ setDisplaySize: (type, size) => displayEngine.setDisplaySize(type, size),
727
+ },
728
+
729
+ layout: {
730
+ // getLayoutName: () => layoutEngine.layoutName,
731
+ // getActiveData: () => this.activeData,
732
+ // getLayoutData: () => layoutEngine.layoutData,
733
+ getContainerSize: () => this.getContainerSize(),
734
+ setContainerSize: () => this.setContainerSize(),
735
+
736
+ setLayoutName: (name, userData = null, setting = false) => layoutEngine.setLayoutName(name, userData, setting),
737
+ setGapSize: (size) => layoutEngine.setGapSize(size),
738
+ setRatio: (type, index, ratio) => layoutEngine.setRatio(type, index, ratio),
739
+ // setUserLayoutData: (userData) => layoutEngine.setUserLayoutData(userData),
740
+ setActiveSection: (name) => this.setActiveSection(name),
741
+ updateActiveElement: (id, changes) => this.updateActiveElement(id, changes),
742
+
743
+ setElements: (elements) => this.setElements(elements),
744
+ setSectionMode: (mode) => this.setSectionMode(mode),
745
+ setSectionConfig: (type, config) => this.setSectionConfig(type, config),
746
+ },
747
+
748
+ freeDrop: {
749
+ // getFreeElements: () => freeDropEngine.freeElements,
750
+ getElementStyle: (element) => freeDropEngine.getElementStyle(element),
751
+ // getGuides: () => freeDropEngine.guides,
752
+ setActiveElement: (element, type) => freeDropEngine.setActiveElement(element, type),
753
+
754
+ addElement: (event, elName) => freeDropEngine.addElement(event, elName),
755
+ toggleLock: () => freeDropEngine.toggleLock(),
756
+ handleMouseDown: (event, id) => freeDropEngine.handleMouseDown(event, id),
757
+ startResize: (event, direction) => freeDropEngine.startResize(event, direction),
758
+ },
759
+
760
+ gridDrop: {
761
+ // getGridElements: () => gridDropEngine.gridElements,
762
+ getElementStyle: (type, cell) => gridDropEngine.getElementStyle(type, cell),
763
+
764
+ // setActiveCell: (type, cell) => gridDropEngine.setActiveCell(type, cell),
765
+ setActiveElement: (element, type) => gridDropEngine.setActiveElement(element, type),
766
+ detectHoverCell: (cell) => gridDropEngine.detectHoverCell(cell),
767
+
768
+ addElement: (elName) => gridDropEngine.addElement(elName),
769
+ toggleLock: () => gridDropEngine.toggleLock(),
770
+ startDrag: (event, id) => gridDropEngine.startDrag(event, id),
771
+ handleMouseDown: (event, id) => gridDropEngine.handleMouseDown(event, id),
772
+ startResize: (event, id) => gridDropEngine.startResize(event, id),
773
+ },
774
+
775
+ history: {
776
+ getHistoryState: () => historyEngine.historyState,
777
+ undo: () => historyEngine.undo(),
778
+ redo: () => historyEngine.redo(),
779
+ },
780
+ };
781
+
782
+ if (this.apiManager) {
783
+ api.userApi = {
784
+ logout: async () => this.apiManager.logout(),
785
+ save: async (data) => this.apiManager.save(data),
786
+ };
787
+ }
788
+
789
+ // proxy 생성 함수
790
+ const createProxy = (api, path = []) => {
791
+ return new Proxy(api, {
792
+ get: (target, prop) => {
793
+ if (prop === 'isReady' && path.length === 0) {
794
+ return () => this._initialized;
795
+ }
796
+
797
+ // 초기화 확인
798
+ if (path.length === 0 && !this._initialized) {
799
+ console.error('[EngineManager] 엔진이 초기화되지 않았습니다.');
800
+ }
801
+
802
+ const value = target[prop];
803
+
804
+ // 객체면 재귀적 proxy
805
+ if (typeof value === 'object' && value !== null) {
806
+ // EventBus는 직접 반환 (Proxy 적용하지 않음)
807
+ if (prop === 'eventBus') {
808
+ return value;
809
+ }
810
+ return createProxy(value, [...path, prop]);
811
+ }
812
+
813
+ if (typeof value === 'function') {
814
+ // 다른 메서드들은 EngineManager에 바인딩
815
+ return value.bind(this);
816
+ }
817
+ return value;
818
+ },
819
+ });
820
+ };
821
+
822
+ return createProxy(api);
823
+ }
824
+ }
825
+
826
+ export default EngineManager;