polarvo-layout 1.0.16 → 1.0.18

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