polarvo-layout 1.0.56 → 1.0.58
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 +1 -1
- package/src/components/Layout/BaseLayout.vue +1 -0
- package/src/components/Layout/CanvasContainer.vue +4 -2
- package/src/components/Layout/FreeLayout.vue +13 -0
- package/src/components/Layout/GridItem.vue +2 -2
- package/src/components/Layout/GridLayout.vue +193 -3
- package/src/components/Layout/PolarLayout.vue +2 -1
- package/src/core/engines/DisplayEngine.js +4 -2
- package/src/core/engines/FreeDropEngine.js +15 -16
- package/src/core/engines/GridDropEngine.js +2 -2
- package/src/core/managers/EngineManager.js +36 -10
- package/src/library/DisplayLibrary.js +2 -2
- package/src/library/LayoutLibrary.js +2 -2
- package/src/utils/data-converter.js +6 -4
package/package.json
CHANGED
|
@@ -90,6 +90,7 @@ const getSectionStyle = (section) => ({
|
|
|
90
90
|
'--grid-columns': section.config?.gridColumns ?? 3,
|
|
91
91
|
'--grid-rows': section.config?.gridRows ?? 3,
|
|
92
92
|
'--grid-gap': `${section.config?.gridGap ?? 5}px`,
|
|
93
|
+
'--grid-row-sizes': section.config?.gridRowRatio?.length ? section.config.gridRowRatio.map((r) => `${r}fr`).join(' ') : undefined, // 비어있으면 아예 안 넣어서 CSS 기본값으로 폴백
|
|
93
94
|
position: 'relative',
|
|
94
95
|
});
|
|
95
96
|
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
<template>
|
|
2
|
-
<div class="canvas-wrapper w-full h-full max-h-[765px]
|
|
2
|
+
<div class="canvas-wrapper w-full h-full max-h-[765px] overflow-y-auto overflow-x-hidden">
|
|
3
3
|
<div id="canvas-container" class="border rounded-md w-full h-full bg-white" :style="containerStyle">
|
|
4
4
|
<BaseLayout :polarvo="polarvo" class="relative" />
|
|
5
5
|
<div
|
|
@@ -67,13 +67,14 @@ const containerStyle = computed(() => {
|
|
|
67
67
|
width: `${DEFAULT_DISPLAY_SIZE.px}px`,
|
|
68
68
|
height: `${displayHeight.value}px`,
|
|
69
69
|
margin: '0 auto',
|
|
70
|
-
transform: `scale(${DEFAULT_DISPLAY_SIZE.percent / 100})`,
|
|
71
70
|
transformOrigin: 'top center',
|
|
71
|
+
transform: `scale(${DEFAULT_DISPLAY_SIZE.percent / 100})`,
|
|
72
72
|
backgroundImage: `
|
|
73
73
|
linear-gradient(rgba(0,0,0,0.05) 1px, transparent 1px),
|
|
74
74
|
linear-gradient(90deg, rgba(0,0,0,0.05) 1px, transparent 1px)
|
|
75
75
|
`,
|
|
76
76
|
backgroundSize: `${DEFAULT_DISPLAY_SIZE.gridSize}px ${DEFAULT_DISPLAY_SIZE.gridSize}px`,
|
|
77
|
+
backgroundPosition: '8px 8px', // BaseLayout의 p-2(8px)와 맞춰 좌상단 기준으로 정렬
|
|
77
78
|
};
|
|
78
79
|
}
|
|
79
80
|
|
|
@@ -89,6 +90,7 @@ const containerStyle = computed(() => {
|
|
|
89
90
|
linear-gradient(90deg, rgba(0,0,0,0.05) 1px, transparent 1px)
|
|
90
91
|
`,
|
|
91
92
|
backgroundSize: `${displaySize.value.gridSize}px ${displaySize.value.gridSize}px`,
|
|
93
|
+
backgroundPosition: '8px 8px', // BaseLayout의 p-2(8px)와 맞춰 좌상단 기준으로 정렬
|
|
92
94
|
};
|
|
93
95
|
});
|
|
94
96
|
</script>
|
|
@@ -60,6 +60,7 @@ const props = defineProps({
|
|
|
60
60
|
const { setActiveDesign, setActiveMenu } = props.polarvo.display;
|
|
61
61
|
const { updateActiveElement } = props.polarvo.layout;
|
|
62
62
|
const { elements, guides, activeElement } = toRefs(props.polarvo.freeDrop.state);
|
|
63
|
+
const { activeDesign } = toRefs(props.polarvo.display.state);
|
|
63
64
|
const filteredElements = computed(() => {
|
|
64
65
|
return elements.value.filter((el) => props.elementIds?.includes(el.id) && el.section === props.sectionKey) ?? [];
|
|
65
66
|
});
|
|
@@ -74,6 +75,7 @@ watch(
|
|
|
74
75
|
() => activeElement.value?.id,
|
|
75
76
|
(newId, oldId) => {
|
|
76
77
|
if (newId === oldId) return;
|
|
78
|
+
|
|
77
79
|
if (!newId) {
|
|
78
80
|
setActiveDesign(null);
|
|
79
81
|
setActiveMenu(null);
|
|
@@ -102,6 +104,17 @@ watch(
|
|
|
102
104
|
{ deep: true },
|
|
103
105
|
);
|
|
104
106
|
|
|
107
|
+
watch(
|
|
108
|
+
() => activeDesign.value,
|
|
109
|
+
(newDesign, oldDesign) => {
|
|
110
|
+
if (newDesign != null || oldDesign == null) return; // null → null이 아닌 값으로 닫히는(값이 있다가 null 되는) 경우만
|
|
111
|
+
|
|
112
|
+
if (activeElement.value?.id && !activeElement.value.id.includes('inputForm')) {
|
|
113
|
+
updateActiveElement(activeElement.value.id, selectedElementForWatch.value, true);
|
|
114
|
+
}
|
|
115
|
+
},
|
|
116
|
+
);
|
|
117
|
+
|
|
105
118
|
onMounted(() => {
|
|
106
119
|
props.polarvo.components.register('sections', props.sectionKey, getCurrentInstance());
|
|
107
120
|
activeEditMode.value = false;
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
:class="
|
|
5
5
|
item.id
|
|
6
6
|
? ['relative bg-gray-200 user-select-none', { 'z-50 bg-white border-2 border-blue-400': item.id === activeId }]
|
|
7
|
-
: 'flex items-center justify-center bg-gray-100 border border-gray-300'
|
|
7
|
+
: 'flex items-center justify-center bg-gray-100 border border-gray-300 '
|
|
8
8
|
"
|
|
9
9
|
:style="itemStyle"
|
|
10
10
|
@mousedown.stop="activeEditMode ? null : handleMouseDown($event, item)"
|
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
<!-- 리사이즈 핸들 -->
|
|
15
15
|
<div v-if="item.id === activeId" class="absolute inset-0 pointer-events-none">
|
|
16
16
|
<div
|
|
17
|
-
class="absolute w-
|
|
17
|
+
class="absolute w-2 h-2 bottom-0 right-0 bg-blue-400 z-10 pointer-events-auto cursor-se-resize"
|
|
18
18
|
@mousedown="startResize($event, item.id)"
|
|
19
19
|
></div>
|
|
20
20
|
<div class="flex gap-1 m-1">
|
|
@@ -8,6 +8,32 @@
|
|
|
8
8
|
:sectionKey="props.sectionKey"
|
|
9
9
|
></GridItem>
|
|
10
10
|
|
|
11
|
+
<div
|
|
12
|
+
v-for="r in gridRows - 1"
|
|
13
|
+
:key="`row-handle-${r}`"
|
|
14
|
+
:id="`row-handle-${r}`"
|
|
15
|
+
class="z-[60] relative self-end justify-self-stretch cursor-s-resize "
|
|
16
|
+
:style="{
|
|
17
|
+
gridRow: r,
|
|
18
|
+
gridColumn: '1 / -1',
|
|
19
|
+
height: `${gridGap}px`,
|
|
20
|
+
transform: 'translateY(100%)',
|
|
21
|
+
}"
|
|
22
|
+
@mousedown="handleMouseDown($event, r)"
|
|
23
|
+
>
|
|
24
|
+
<!-- 점선 -->
|
|
25
|
+
<div
|
|
26
|
+
class="pointer-events-none absolute left-0 right-0 top-1/2 -translate-y-1/2 border-t border-dashed border-blue-700 "
|
|
27
|
+
:class="{ 'opacity-100': rowState.activeRow === r, 'opacity-0': rowState.activeRow !== r }"
|
|
28
|
+
></div>
|
|
29
|
+
|
|
30
|
+
<!-- 핸들 -->
|
|
31
|
+
<!-- <div
|
|
32
|
+
class="absolute left-1/2 top-1/2 -translate-x-1/2 -translate-y-1/2 w-3 h-3 cursor-s-resize rounded-full bg-violet-400 opacity-30 hover:opacity-100"
|
|
33
|
+
@mousedown="handleMouseDown($event, r)"
|
|
34
|
+
></div> -->
|
|
35
|
+
</div>
|
|
36
|
+
|
|
11
37
|
<!-- 오버레이 -->
|
|
12
38
|
<div v-if="activeEditMode" class="absolute inset-0 bg-black opacity-50 z-30"></div>
|
|
13
39
|
</template>
|
|
@@ -41,9 +67,10 @@ const props = defineProps({
|
|
|
41
67
|
},
|
|
42
68
|
});
|
|
43
69
|
|
|
44
|
-
const { setActiveDesign } = props.polarvo.display;
|
|
45
|
-
const { updateActiveElement } = props.polarvo.layout;
|
|
70
|
+
const { setActiveDesign, setActiveMenu } = props.polarvo.display;
|
|
71
|
+
const { updateActiveElement, setSectionConfig } = props.polarvo.layout;
|
|
46
72
|
const { elements, activeElement } = toRefs(props.polarvo.gridDrop.state);
|
|
73
|
+
const { activeDesign } = toRefs(props.polarvo.display.state);
|
|
47
74
|
|
|
48
75
|
const selectedElement = inject('selectedElement');
|
|
49
76
|
const activeEditMode = inject('activeEditMode');
|
|
@@ -52,6 +79,144 @@ import { omit, cloneDeep, debounce } from 'lodash-es';
|
|
|
52
79
|
|
|
53
80
|
const gridColumns = ref(3); // 동적으로 계산
|
|
54
81
|
const gridRows = ref(1); // 동적으로 계산
|
|
82
|
+
const gridGap = computed(() => props.sectionData?.config?.gridGap ?? 5); // BaseLayout.vue --grid-gap과 동일한 소스/기본값
|
|
83
|
+
|
|
84
|
+
// row Resize
|
|
85
|
+
const MIN_ROW_PX = 20; // 행 최소 높이(px)
|
|
86
|
+
|
|
87
|
+
const rowState = ref({
|
|
88
|
+
activeRow: null,
|
|
89
|
+
isDragging: false,
|
|
90
|
+
});
|
|
91
|
+
const startPos = ref({ x: 0, y: 0 });
|
|
92
|
+
|
|
93
|
+
function handleMouseDown(event, row) {
|
|
94
|
+
if (!row) return;
|
|
95
|
+
event.preventDefault();
|
|
96
|
+
event.stopPropagation();
|
|
97
|
+
|
|
98
|
+
rowState.value.activeRow = row;
|
|
99
|
+
startPos.value = { x: event.clientX, y: event.clientY };
|
|
100
|
+
|
|
101
|
+
setActiveMenu(null); // 메뉴 닫기
|
|
102
|
+
setActiveDesign(null); // 디자인 닫기
|
|
103
|
+
|
|
104
|
+
let dragSnapShot = null; // 드래그 시작 시 스냅샷
|
|
105
|
+
let liveRowRatio = null; // 미리보기 중 계산된 최종 fr 배열 (stopDrag에서 커밋)
|
|
106
|
+
let pendingClientY = null;
|
|
107
|
+
let rafId = null;
|
|
108
|
+
|
|
109
|
+
document.addEventListener('mousemove', _detectDragIntent);
|
|
110
|
+
document.addEventListener('mouseup', _cancelDragIntent);
|
|
111
|
+
|
|
112
|
+
function _detectDragIntent(e) {
|
|
113
|
+
if (rowState.value.activeRow !== row) return;
|
|
114
|
+
const deltaX = Math.abs(e.clientX - startPos.value.x);
|
|
115
|
+
const deltaY = Math.abs(e.clientY - startPos.value.y);
|
|
116
|
+
|
|
117
|
+
const isMoved = deltaX > 5 || deltaY > 5;
|
|
118
|
+
|
|
119
|
+
if (isMoved) {
|
|
120
|
+
// 의도 감지 리스너 제거
|
|
121
|
+
document.removeEventListener('mousemove', _detectDragIntent);
|
|
122
|
+
document.removeEventListener('mouseup', _cancelDragIntent);
|
|
123
|
+
|
|
124
|
+
// 드래그 시작
|
|
125
|
+
_startDrag();
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function _cancelDragIntent() {
|
|
130
|
+
rowState.value.activeRow = null;
|
|
131
|
+
|
|
132
|
+
document.removeEventListener('mousemove', _detectDragIntent);
|
|
133
|
+
document.removeEventListener('mouseup', _cancelDragIntent);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function _startDrag() {
|
|
137
|
+
rowState.value.isDragging = true;
|
|
138
|
+
|
|
139
|
+
const container = document.getElementById(`${props.sectionKey}Layout`);
|
|
140
|
+
if (!container) return _stopDrag();
|
|
141
|
+
|
|
142
|
+
const configured = props.sectionData?.config?.gridRowRatio;
|
|
143
|
+
const baselineRatio =
|
|
144
|
+
(Array.isArray(configured) && configured.length === gridRows.value) || configured.some((v) => typeof v !== 'number' || !(v > 0))
|
|
145
|
+
? [...configured]
|
|
146
|
+
: Array(gridRows.value).fill(1);
|
|
147
|
+
|
|
148
|
+
const trackPx = getComputedStyle(container)
|
|
149
|
+
.gridTemplateRows.split(' ')
|
|
150
|
+
.map((v) => parseFloat(v));
|
|
151
|
+
|
|
152
|
+
// 총 Fr 계산
|
|
153
|
+
const totalFr = baselineRatio.reduce((sum, v) => sum + v, 0);
|
|
154
|
+
// 총 px 계산
|
|
155
|
+
const totalPx = trackPx.reduce((sum, v) => sum + v, 0);
|
|
156
|
+
// 1px당 Fr 계산
|
|
157
|
+
const frPerPx = totalFr / totalPx;
|
|
158
|
+
|
|
159
|
+
dragSnapShot = {
|
|
160
|
+
container,
|
|
161
|
+
baselineRatio,
|
|
162
|
+
frPerPx,
|
|
163
|
+
minFr: MIN_ROW_PX * frPerPx, // 최소 Fr 계산
|
|
164
|
+
};
|
|
165
|
+
|
|
166
|
+
document.addEventListener('mousemove', _startDragMove);
|
|
167
|
+
document.addEventListener('mouseup', _stopDrag);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
function _startDragMove(e) {
|
|
171
|
+
if (!dragSnapShot) return;
|
|
172
|
+
pendingClientY = e.clientY;
|
|
173
|
+
|
|
174
|
+
// rafId 중복 예약 방지
|
|
175
|
+
if (rafId) return;
|
|
176
|
+
|
|
177
|
+
rafId = requestAnimationFrame(() => {
|
|
178
|
+
rafId = null;
|
|
179
|
+
|
|
180
|
+
const { baselineRatio, frPerPx, minFr, container } = dragSnapShot;
|
|
181
|
+
const topIdx = row - 1; // 드래그 행 위쪽 행 인덱스
|
|
182
|
+
const bottomIdx = row; // 드래그 행 인덱스
|
|
183
|
+
|
|
184
|
+
// 순수 이동량
|
|
185
|
+
let delta = (pendingClientY - startPos.value.y) * frPerPx;
|
|
186
|
+
// 위쪽 행 작아지는 것 방지: 가장 커져도 minFr - baselineRatio[topIdx] (음수값 = 위로 이동)
|
|
187
|
+
delta = Math.max(minFr - baselineRatio[topIdx], delta);
|
|
188
|
+
// 아래쪽 행 작아지는 것 방지: 가장 작아져도 minFr (양수값 = 아래로 이동)
|
|
189
|
+
delta = Math.min(baselineRatio[bottomIdx] - minFr, delta);
|
|
190
|
+
|
|
191
|
+
// 미리보기용
|
|
192
|
+
const preview = [...baselineRatio];
|
|
193
|
+
preview[topIdx] += delta;
|
|
194
|
+
preview[bottomIdx] -= delta;
|
|
195
|
+
|
|
196
|
+
liveRowRatio = preview;
|
|
197
|
+
|
|
198
|
+
// 화면에만 즉시 반영
|
|
199
|
+
container.style.setProperty('--grid-row-sizes', preview.map((v) => `${v}fr`).join(' '));
|
|
200
|
+
});
|
|
201
|
+
}
|
|
202
|
+
function _stopDrag() {
|
|
203
|
+
rowState.value.isDragging = false;
|
|
204
|
+
rowState.value.activeRow = null;
|
|
205
|
+
|
|
206
|
+
if (liveRowRatio) {
|
|
207
|
+
setSectionConfig('gridRowRatio', liveRowRatio);
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
startPos.value = { x: 0, y: 0 };
|
|
211
|
+
dragSnapShot = null;
|
|
212
|
+
liveRowRatio = null;
|
|
213
|
+
pendingClientY = null;
|
|
214
|
+
rafId = null;
|
|
215
|
+
|
|
216
|
+
document.removeEventListener('mousemove', _startDragMove);
|
|
217
|
+
document.removeEventListener('mouseup', _stopDrag);
|
|
218
|
+
}
|
|
219
|
+
}
|
|
55
220
|
|
|
56
221
|
function initData() {
|
|
57
222
|
gridColumns.value = props.sectionData?.config?.gridColumns || 3;
|
|
@@ -131,11 +296,22 @@ watch(
|
|
|
131
296
|
_isElementSwitching = false;
|
|
132
297
|
return;
|
|
133
298
|
}
|
|
134
|
-
|
|
299
|
+
updateActiveElement(selectedElement.value?.id, newElement);
|
|
135
300
|
},
|
|
136
301
|
{ deep: true },
|
|
137
302
|
);
|
|
138
303
|
|
|
304
|
+
watch(
|
|
305
|
+
() => activeDesign.value,
|
|
306
|
+
(newDesign, oldDesign) => {
|
|
307
|
+
if (newDesign != null || oldDesign == null) return; // null → null이 아닌 값으로 닫히는(값이 있다가 null 되는) 경우만
|
|
308
|
+
|
|
309
|
+
if (activeElement.value?.id && !activeElement.value.id.includes('inputForm')) {
|
|
310
|
+
updateActiveElement(activeElement.value.id, selectedElementForWatch.value, true);
|
|
311
|
+
}
|
|
312
|
+
},
|
|
313
|
+
);
|
|
314
|
+
|
|
139
315
|
onMounted(() => {
|
|
140
316
|
props.polarvo.components.register('sections', props.sectionKey, getCurrentInstance());
|
|
141
317
|
activeEditMode.value = false;
|
|
@@ -146,4 +322,18 @@ onMounted(() => {
|
|
|
146
322
|
onUnmounted(() => {
|
|
147
323
|
props.polarvo.components.unregister('sections', props.sectionKey);
|
|
148
324
|
});
|
|
325
|
+
|
|
326
|
+
//
|
|
149
327
|
</script>
|
|
328
|
+
|
|
329
|
+
<style lang="scss" scoped>
|
|
330
|
+
// .re-handle-line {
|
|
331
|
+
// position: absolute;
|
|
332
|
+
// z-index: 0;
|
|
333
|
+
// left: 0;
|
|
334
|
+
// right: 0;
|
|
335
|
+
// top: 50%;
|
|
336
|
+
// height: 1px;
|
|
337
|
+
// transform: translateY(-50%);
|
|
338
|
+
// }
|
|
339
|
+
</style>
|
|
@@ -18,7 +18,8 @@ const props = defineProps({
|
|
|
18
18
|
.polar-grid {
|
|
19
19
|
display: grid;
|
|
20
20
|
grid-template-columns: repeat(var(--grid-columns, 3), 1fr);
|
|
21
|
-
grid-template-rows: repeat(var(--grid-rows, 3), 1fr);
|
|
21
|
+
/* grid-template-rows: repeat(var(--grid-rows, 3), 1fr); */
|
|
22
|
+
grid-template-rows: var(--grid-row-sizes, repeat(var(--grid-rows, 3), 1fr));
|
|
22
23
|
gap: var(--grid-gap, 5px);
|
|
23
24
|
width: 100%;
|
|
24
25
|
height: 100%;
|
|
@@ -64,7 +64,7 @@ class DisplayEngine {
|
|
|
64
64
|
this._subscribe('freeDrop:setActiveElement', () => {
|
|
65
65
|
this.setActiveMenu(null);
|
|
66
66
|
this.setActiveDesign(null);
|
|
67
|
-
})
|
|
67
|
+
});
|
|
68
68
|
|
|
69
69
|
// freeDrop:setActiveElement가 항상 먼저 발행되어 아래 처리가 중복되므로 주석 처리
|
|
70
70
|
// this._subscribe('freeDrop:startDrag', () => {
|
|
@@ -151,10 +151,12 @@ class DisplayEngine {
|
|
|
151
151
|
*/
|
|
152
152
|
setActiveDesign(name) {
|
|
153
153
|
if (this.activeDesign === name) return;
|
|
154
|
+
const prevDesign = this.activeDesign;
|
|
154
155
|
this.activeDesign = name;
|
|
155
156
|
|
|
156
157
|
this.eventBus.emit('display:updateActiveDesign', {
|
|
157
|
-
$
|
|
158
|
+
$activeDesign: name,
|
|
159
|
+
prev: { activeDesign: prevDesign },
|
|
158
160
|
timestamp: Date.now(),
|
|
159
161
|
});
|
|
160
162
|
}
|
|
@@ -147,7 +147,7 @@ class FreeDropEngine {
|
|
|
147
147
|
|
|
148
148
|
this._subscribe('system:restoredState', ({ domain, state: snapShot }) => {
|
|
149
149
|
if (domain === 'all' || domain === 'draft') {
|
|
150
|
-
this._elements = snapShot.manager.elements || [];
|
|
150
|
+
this._elements = cloneDeep(snapShot.manager.elements) || [];
|
|
151
151
|
this._elementsUpdate = true;
|
|
152
152
|
this.setActiveElement(null);
|
|
153
153
|
|
|
@@ -161,7 +161,7 @@ class FreeDropEngine {
|
|
|
161
161
|
}
|
|
162
162
|
|
|
163
163
|
if (domain === 'manager') {
|
|
164
|
-
this._elements = snapShot.elements || [];
|
|
164
|
+
this._elements = cloneDeep(snapShot.elements) || [];
|
|
165
165
|
this._elementsUpdate = true;
|
|
166
166
|
this.setActiveElement(null);
|
|
167
167
|
|
|
@@ -666,7 +666,8 @@ class FreeDropEngine {
|
|
|
666
666
|
const layoutRect = this._getLayoutRect();
|
|
667
667
|
if (!layoutRect) return;
|
|
668
668
|
|
|
669
|
-
const step = this._gridSize || 1;
|
|
669
|
+
// const step = this._gridSize || 1;
|
|
670
|
+
const step = 1;
|
|
670
671
|
const rawX = this._activeElement.position.x + delta.x * step;
|
|
671
672
|
const rawY = this._activeElement.position.y + delta.y * step;
|
|
672
673
|
|
|
@@ -834,7 +835,7 @@ class FreeDropEngine {
|
|
|
834
835
|
guides: this.guides,
|
|
835
836
|
timestamp: Date.now(),
|
|
836
837
|
});
|
|
837
|
-
this._resetDrag(true
|
|
838
|
+
this._resetDrag(true);
|
|
838
839
|
// }
|
|
839
840
|
|
|
840
841
|
return;
|
|
@@ -861,7 +862,7 @@ class FreeDropEngine {
|
|
|
861
862
|
* @param {boolean} emitEvent - 업데이트 이벤트 발송 여부
|
|
862
863
|
* @param {boolean} historyEvent - 히스토리 이벤트 기록 여부
|
|
863
864
|
*/
|
|
864
|
-
_resetDrag(emitEvent = false
|
|
865
|
+
_resetDrag(emitEvent = false) {
|
|
865
866
|
this._dragState.isDragging = false;
|
|
866
867
|
this.guides = { x: null, y: null, w: null, h: null };
|
|
867
868
|
|
|
@@ -870,17 +871,15 @@ class FreeDropEngine {
|
|
|
870
871
|
this._offsetPos = { x: 0, y: 0 };
|
|
871
872
|
this._dragOriginPos = { x: 0, y: 0 };
|
|
872
873
|
|
|
873
|
-
if (emitEvent) {
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
});
|
|
883
|
-
}
|
|
874
|
+
if (emitEvent && this._activeElement) {
|
|
875
|
+
this.eventBus.emit('freeDrop:requestUpdateElement', {
|
|
876
|
+
historyEvent: true,
|
|
877
|
+
elementId: this._activeElement.id,
|
|
878
|
+
position: this._activeElement.position,
|
|
879
|
+
size: this._activeElement.size,
|
|
880
|
+
guides: this.guides,
|
|
881
|
+
timestamp: Date.now(),
|
|
882
|
+
});
|
|
884
883
|
}
|
|
885
884
|
|
|
886
885
|
document.removeEventListener('mousemove', this._startDragMove);
|
|
@@ -112,7 +112,7 @@ class GridDropEngine {
|
|
|
112
112
|
|
|
113
113
|
this._subscribe('system:restoredState', ({ domain, state: snapShot }) => {
|
|
114
114
|
if (domain === 'all' || domain === 'draft') {
|
|
115
|
-
this._elements = snapShot.manager.elements || [];
|
|
115
|
+
this._elements = cloneDeep(snapShot.manager.elements) || [];
|
|
116
116
|
this._elementsUpdate = true;
|
|
117
117
|
|
|
118
118
|
this.setActiveElement(null);
|
|
@@ -124,7 +124,7 @@ class GridDropEngine {
|
|
|
124
124
|
}
|
|
125
125
|
|
|
126
126
|
if (domain === 'manager') {
|
|
127
|
-
this._elements = snapShot.elements || [];
|
|
127
|
+
this._elements = cloneDeep(snapShot.elements) || [];
|
|
128
128
|
this._elementsUpdate = true;
|
|
129
129
|
|
|
130
130
|
this.setActiveElement(null);
|
|
@@ -69,7 +69,7 @@ class EngineManager {
|
|
|
69
69
|
* @param {object} state - 복원할 상태 객체
|
|
70
70
|
*/
|
|
71
71
|
setState(state) {
|
|
72
|
-
this.state.elements = state.elements;
|
|
72
|
+
this.state.elements = cloneDeep(state.elements);
|
|
73
73
|
this.setActiveSection(state.activeSection ?? null, true);
|
|
74
74
|
}
|
|
75
75
|
|
|
@@ -178,10 +178,14 @@ class EngineManager {
|
|
|
178
178
|
return false;
|
|
179
179
|
}
|
|
180
180
|
|
|
181
|
-
const { width
|
|
182
|
-
const
|
|
181
|
+
const { width } = containerEl.getBoundingClientRect();
|
|
182
|
+
const style = getComputedStyle(containerEl);
|
|
183
|
+
const paddingX = parseFloat(style.paddingLeft) + parseFloat(style.paddingRight);
|
|
184
|
+
const contentWidth = width - paddingX;
|
|
183
185
|
|
|
184
|
-
|
|
186
|
+
const newHeight = contentWidth / calculateAspectRatio(this._displaySize.aspectRatio);
|
|
187
|
+
|
|
188
|
+
this._containerWidth = contentWidth ?? this._containerWidth;
|
|
185
189
|
this._containerHeight = newHeight ?? this._containerHeight;
|
|
186
190
|
} catch (error) {
|
|
187
191
|
console.error('[EngineManager] 컨테이너 크기 설정 중 오류 발생: ', error);
|
|
@@ -228,8 +232,9 @@ class EngineManager {
|
|
|
228
232
|
/** activeElement 업데이트
|
|
229
233
|
* @param {string} id - 업데이트할 element의 ID
|
|
230
234
|
* @param {object} changes - 변경할 속성 객체
|
|
235
|
+
* @param {boolean} emitEvent - 이벤트 발생 여부
|
|
231
236
|
*/
|
|
232
|
-
updateActiveElement(id, changes) {
|
|
237
|
+
updateActiveElement(id, changes, emitEvent = false) {
|
|
233
238
|
if (!id || !changes) return false;
|
|
234
239
|
|
|
235
240
|
const index = this.state.elements.findIndex((x) => x.id === id);
|
|
@@ -253,6 +258,13 @@ class EngineManager {
|
|
|
253
258
|
element: updatedElement,
|
|
254
259
|
timestamp: Date.now(),
|
|
255
260
|
});
|
|
261
|
+
|
|
262
|
+
if (emitEvent) {
|
|
263
|
+
this.eventBus.emit('system:historyCheckpoint', {
|
|
264
|
+
domain: 'manager',
|
|
265
|
+
state: this.getState() || null,
|
|
266
|
+
});
|
|
267
|
+
}
|
|
256
268
|
}
|
|
257
269
|
|
|
258
270
|
/** ---------------------------------- display Engine ---------------------------------- **/
|
|
@@ -448,13 +460,12 @@ class EngineManager {
|
|
|
448
460
|
* @param {number|boolean} config - 새로운 설정 값
|
|
449
461
|
*/
|
|
450
462
|
setSectionConfig(type, config) {
|
|
451
|
-
if (!['column', 'row', 'gap', 'guideLine'].includes(type)) {
|
|
463
|
+
if (!['column', 'row', 'gap', 'guideLine', 'gridRowRatio'].includes(type)) {
|
|
452
464
|
return { result: false, message: '입력할 수 없는 config 타입입니다.' };
|
|
453
465
|
}
|
|
454
466
|
|
|
455
467
|
if (!this.activeData) return;
|
|
456
468
|
|
|
457
|
-
const limit = CONFIG_LIMITS[type];
|
|
458
469
|
const result = { result: true, message: 'config가 성공적으로 변경되었습니다.' };
|
|
459
470
|
// const limits = {
|
|
460
471
|
// column: { min: 1, max: 12, name: '열 개수' },
|
|
@@ -467,7 +478,17 @@ class EngineManager {
|
|
|
467
478
|
|
|
468
479
|
if (type === 'guideLine') {
|
|
469
480
|
newConfig = { showGuideLine: value };
|
|
481
|
+
} else if (type === 'gridRowRatio') {
|
|
482
|
+
// gridRowRatio 배열은 gridRows 길이와 일치해야 함
|
|
483
|
+
const gridRows = this.activeData.config?.gridRows || 1;
|
|
484
|
+
if (!Array.isArray(value) || value.length !== gridRows || value.some((v) => typeof v !== 'number' || !(v > 0))) {
|
|
485
|
+
return { result: false, message: '잘못된 행 비율 값입니다.' };
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
newConfig = { gridRowRatio: value };
|
|
470
489
|
} else {
|
|
490
|
+
const limit = CONFIG_LIMITS[type];
|
|
491
|
+
|
|
471
492
|
if (value < limit.min) {
|
|
472
493
|
result.result = false;
|
|
473
494
|
result.message = `설정 가능한 최소 ${limit.name}는 ${limit.min}입니다.`;
|
|
@@ -481,7 +502,8 @@ class EngineManager {
|
|
|
481
502
|
if (type === 'column') {
|
|
482
503
|
newConfig = { gridColumns: value };
|
|
483
504
|
} else if (type === 'row') {
|
|
484
|
-
|
|
505
|
+
// gridRows 변경되면 gridRowRatio 초기화
|
|
506
|
+
newConfig = { gridRows: value, gridRowRatio: [] };
|
|
485
507
|
} else if (type === 'gap') {
|
|
486
508
|
newConfig = { gridGap: value };
|
|
487
509
|
}
|
|
@@ -535,7 +557,11 @@ class EngineManager {
|
|
|
535
557
|
|
|
536
558
|
this.eventBus.emit('system:historyCheckpoint', {
|
|
537
559
|
domain: 'manager',
|
|
538
|
-
state:
|
|
560
|
+
state: {
|
|
561
|
+
manager: this.getState() || null,
|
|
562
|
+
layout: this.getEngine('layout')?.getState() || null,
|
|
563
|
+
display: this.getEngine('display')?.getState() || null,
|
|
564
|
+
},
|
|
539
565
|
});
|
|
540
566
|
});
|
|
541
567
|
|
|
@@ -957,7 +983,7 @@ class EngineManager {
|
|
|
957
983
|
setRatio: (type, index, ratio) => layoutEngine.setRatio(type, index, ratio),
|
|
958
984
|
// setUserLayoutData: (userData) => layoutEngine.setUserLayoutData(userData),
|
|
959
985
|
setActiveSection: (name) => this.setActiveSection(name),
|
|
960
|
-
updateActiveElement: (id, changes) => this.updateActiveElement(id, changes),
|
|
986
|
+
updateActiveElement: (id, changes, emitEvent) => this.updateActiveElement(id, changes, emitEvent),
|
|
961
987
|
|
|
962
988
|
setElements: (elements) => this.setElements(elements),
|
|
963
989
|
setSectionMode: (mode) => this.setSectionMode(mode),
|
|
@@ -48,8 +48,8 @@ function useDisplay(api) {
|
|
|
48
48
|
});
|
|
49
49
|
|
|
50
50
|
// activeDesign 변경 감지
|
|
51
|
-
_subscribe('display:updateActiveDesign', ({ $
|
|
52
|
-
state.activeDesign = $
|
|
51
|
+
_subscribe('display:updateActiveDesign', ({ $activeDesign }) => {
|
|
52
|
+
state.activeDesign = $activeDesign;
|
|
53
53
|
});
|
|
54
54
|
|
|
55
55
|
// displayMode 변경 감지 (displaySize가 함께 초기화됨)
|
|
@@ -228,9 +228,9 @@ function useLayout(api) {
|
|
|
228
228
|
}
|
|
229
229
|
};
|
|
230
230
|
|
|
231
|
-
const updateActiveElement = (id, changes) => {
|
|
231
|
+
const updateActiveElement = (id, changes, emitEvent) => {
|
|
232
232
|
try {
|
|
233
|
-
api.layout.updateActiveElement(id, changes);
|
|
233
|
+
api.layout.updateActiveElement(id, changes, emitEvent);
|
|
234
234
|
} catch (error) {
|
|
235
235
|
console.error('[useLayout] updateActiveElement 실행 중 오류 발생:', error);
|
|
236
236
|
}
|
|
@@ -4,6 +4,8 @@ const dataConverter = {
|
|
|
4
4
|
// 저장: 자유 배치인 경우, position, size 등을 뷰포트 대비 비율로 변환하여 저장
|
|
5
5
|
normalize(elements, containerWidth, containerHeight) {
|
|
6
6
|
const normalize = cloneDeep(elements);
|
|
7
|
+
if (!containerWidth || !containerHeight) return normalize;
|
|
8
|
+
|
|
7
9
|
normalize.map((el) => {
|
|
8
10
|
if (el.mode !== 'free') return;
|
|
9
11
|
el.position = {
|
|
@@ -25,12 +27,12 @@ const dataConverter = {
|
|
|
25
27
|
denormalize.map((el) => {
|
|
26
28
|
if (el.mode !== 'free') return;
|
|
27
29
|
el.position = {
|
|
28
|
-
x: el.position.x * containerWidth,
|
|
29
|
-
y: el.position.y * containerHeight,
|
|
30
|
+
x: Math.round(el.position.x * containerWidth),
|
|
31
|
+
y: Math.round(el.position.y * containerHeight),
|
|
30
32
|
};
|
|
31
33
|
el.size = {
|
|
32
|
-
w: el.size.w * containerWidth,
|
|
33
|
-
h: el.size.h * containerHeight,
|
|
34
|
+
w: Math.round(el.size.w * containerWidth),
|
|
35
|
+
h: Math.round(el.size.h * containerHeight),
|
|
34
36
|
};
|
|
35
37
|
});
|
|
36
38
|
|