polarvo-layout 1.0.17 → 1.0.19

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.17",
3
+ "version": "1.0.19",
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,
@@ -328,9 +355,15 @@ class EngineManager {
328
355
  _setupFreeDropEngineConnections() {
329
356
  // 배치요소 추가 요청
330
357
  this._subscribe('freeDrop:requestAddElement', ({ element }) => {
358
+ if (this.state.elements.some((el) => el.id === element.id)) {
359
+ return;
360
+ }
361
+
331
362
  this.state.elements.push(element);
332
- this._updateActiveElementIds([...this.activeData.elementIds, element.id]);
363
+ this._updateActiveSectionConfig('elementIds', { elementIds: [...this.activeData.elementIds, element.id] });
364
+ // this._updateActiveElementIds([...this.activeData.elementIds, element.id]);
333
365
 
366
+ console.log('this.state.elements', this.state.elements)
334
367
  this.eventBus.emit('system:requestUpdateData', {
335
368
  action: 'add',
336
369
  elements: this.state.elements,
@@ -345,7 +378,8 @@ class EngineManager {
345
378
  // 배치요소 삭제 요청
346
379
  this._subscribe('freeDrop:requestDeleteElement', ({ elementId }) => {
347
380
  this.state.elements = this.state.elements.filter((el) => el.id !== elementId);
348
- this._updateActiveElementIds(this.activeData.elementIds.filter((elId) => elId !== elementId));
381
+ this._updateActiveSectionConfig('elementIds', { elementIds: this.activeData.elementIds.filter((elId) => elId !== elementId) });
382
+ // this._updateActiveElementIds(this.activeData.elementIds.filter((elId) => elId !== elementId));
349
383
 
350
384
  this.eventBus.emit('system:requestUpdateData', {
351
385
  action: 'delete',
@@ -381,7 +415,8 @@ class EngineManager {
381
415
  // 배치요소 추가 요청
382
416
  this._subscribe('gridDrop:requestAddElement', ({ element }) => {
383
417
  this.state.elements.push(element);
384
- this._updateActiveElementIds([...this.activeData.elementIds, element.id]);
418
+ this._updateActiveSectionConfig('elementIds', { elementIds: [...this.activeData.elementIds, element.id] });
419
+ // this._updateActiveElementIds([...this.activeData.elementIds, element.id]);
385
420
 
386
421
  this.eventBus.emit('system:requestUpdateData', {
387
422
  action: 'add',
@@ -397,7 +432,8 @@ class EngineManager {
397
432
  // 배치요소 삭제 요청
398
433
  this._subscribe('gridDrop:requestDeleteElement', ({ elementId }) => {
399
434
  this.state.elements = this.state.elements.filter((el) => el.id !== elementId);
400
- this._updateActiveElementIds(this.activeData.elementIds.filter((elId) => elId !== elementId));
435
+ this._updateActiveSectionConfig('elementIds', { elementIds: this.activeData.elementIds.filter((elId) => elId !== elementId) });
436
+ // this._updateActiveElementIds(this.activeData.elementIds.filter((elId) => elId !== elementId));
401
437
 
402
438
  this.eventBus.emit('system:requestUpdateData', {
403
439
  action: 'delete',
@@ -412,7 +448,6 @@ class EngineManager {
412
448
 
413
449
  // 배치요소 위치 및 사이즈 변경 요청
414
450
  this._subscribe('gridDrop:requestUpdateElement', ({ historyEvent, elementId, position, size }) => {
415
-
416
451
  const element = this.state.elements.find((el) => el.id === elementId);
417
452
  if (element) {
418
453
  element.position = position;
@@ -613,9 +648,7 @@ class EngineManager {
613
648
  this.engines.clear();
614
649
  this._initialized = false;
615
650
 
616
- this._dataUpdate = false;
617
651
  this._layoutData = null;
618
- this._activeData = null;
619
652
 
620
653
  this.state = {};
621
654
  this.eventBus = null;
@@ -630,9 +663,7 @@ class EngineManager {
630
663
  ...this.config.initialState,
631
664
  };
632
665
 
633
- this._dataUpdate = false;
634
666
  this._layoutData = null;
635
- this._activeData = null;
636
667
 
637
668
  this.eventBus.emit('system:enginesReset', {
638
669
  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;
@@ -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 }) => {