polarvo-layout 1.0.57 → 1.0.59
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 +4 -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 +253 -3
- package/src/components/Layout/PolarLayout.vue +3 -2
- package/src/core/engines/DisplayEngine.js +4 -2
- package/src/core/engines/FreeDropEngine.js +13 -15
- package/src/core/engines/GridDropEngine.js +2 -2
- package/src/core/managers/EngineManager.js +46 -11
- 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,10 @@ 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 기본값으로 폴백
|
|
94
|
+
'--grid-column-sizes': section.config?.gridColumnRatio?.length
|
|
95
|
+
? section.config.gridColumnRatio.map((r) => `${r}fr`).join(' ')
|
|
96
|
+
: undefined,
|
|
93
97
|
position: 'relative',
|
|
94
98
|
});
|
|
95
99
|
|
|
@@ -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,64 @@
|
|
|
8
8
|
:sectionKey="props.sectionKey"
|
|
9
9
|
></GridItem>
|
|
10
10
|
|
|
11
|
+
<!-- 행 크기 조절 -->
|
|
12
|
+
<div
|
|
13
|
+
v-for="r in gridRows - 1"
|
|
14
|
+
:key="`row-handle-${r}`"
|
|
15
|
+
:id="`row-handle-${r}`"
|
|
16
|
+
class="z-[60] relative self-end justify-self-stretch cursor-s-resize"
|
|
17
|
+
:style="{
|
|
18
|
+
gridRow: r,
|
|
19
|
+
gridColumn: '1 / -1',
|
|
20
|
+
height: `${gridGap}px`,
|
|
21
|
+
transform: 'translateY(100%)',
|
|
22
|
+
}"
|
|
23
|
+
@mousedown="handleMouseDown($event, 'row', r)"
|
|
24
|
+
>
|
|
25
|
+
<!-- 점선 -->
|
|
26
|
+
<div
|
|
27
|
+
class="pointer-events-none absolute left-0 right-0 top-1/2 -translate-y-1/2 border-t border-dashed border-blue-700"
|
|
28
|
+
:class="{
|
|
29
|
+
'opacity-100': dragState.axis === 'row' && dragState.activeIdx === r,
|
|
30
|
+
'opacity-0': !(dragState.axis === 'row' && dragState.activeIdx === r),
|
|
31
|
+
}"
|
|
32
|
+
></div>
|
|
33
|
+
|
|
34
|
+
<!-- 핸들 -->
|
|
35
|
+
<!-- <div
|
|
36
|
+
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"
|
|
37
|
+
></div> -->
|
|
38
|
+
</div>
|
|
39
|
+
|
|
40
|
+
<!-- 열 크기 조절 -->
|
|
41
|
+
<div
|
|
42
|
+
v-for="c in gridColumns - 1"
|
|
43
|
+
:key="`col-handle-${c}`"
|
|
44
|
+
:id="`col-handle-${c}`"
|
|
45
|
+
class="z-[60] relative self-stretch justify-self-end cursor-e-resize"
|
|
46
|
+
:style="{
|
|
47
|
+
gridRow: '1 / -1',
|
|
48
|
+
gridColumn: c,
|
|
49
|
+
width: `${gridGap}px`,
|
|
50
|
+
transform: 'translateX(100%)',
|
|
51
|
+
}"
|
|
52
|
+
@mousedown="handleMouseDown($event, 'col', c)"
|
|
53
|
+
>
|
|
54
|
+
<!-- 점선 -->
|
|
55
|
+
<div
|
|
56
|
+
class="pointer-events-none absolute top-0 bottom-0 left-1/2 -translate-x-1/2 border-l border-dashed border-blue-700"
|
|
57
|
+
:class="{
|
|
58
|
+
'opacity-100': dragState.axis === 'col' && dragState.activeIdx === c,
|
|
59
|
+
'opacity-0': !(dragState.axis === 'col' && dragState.activeIdx === c),
|
|
60
|
+
}"
|
|
61
|
+
></div>
|
|
62
|
+
|
|
63
|
+
<!-- 핸들 -->
|
|
64
|
+
<!-- <div
|
|
65
|
+
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"
|
|
66
|
+
></div> -->
|
|
67
|
+
</div>
|
|
68
|
+
|
|
11
69
|
<!-- 오버레이 -->
|
|
12
70
|
<div v-if="activeEditMode" class="absolute inset-0 bg-black opacity-50 z-30"></div>
|
|
13
71
|
</template>
|
|
@@ -41,9 +99,10 @@ const props = defineProps({
|
|
|
41
99
|
},
|
|
42
100
|
});
|
|
43
101
|
|
|
44
|
-
const { setActiveDesign } = props.polarvo.display;
|
|
45
|
-
const { updateActiveElement } = props.polarvo.layout;
|
|
102
|
+
const { setActiveDesign, setActiveMenu } = props.polarvo.display;
|
|
103
|
+
const { updateActiveElement, setSectionConfig } = props.polarvo.layout;
|
|
46
104
|
const { elements, activeElement } = toRefs(props.polarvo.gridDrop.state);
|
|
105
|
+
const { activeDesign } = toRefs(props.polarvo.display.state);
|
|
47
106
|
|
|
48
107
|
const selectedElement = inject('selectedElement');
|
|
49
108
|
const activeEditMode = inject('activeEditMode');
|
|
@@ -52,6 +111,172 @@ import { omit, cloneDeep, debounce } from 'lodash-es';
|
|
|
52
111
|
|
|
53
112
|
const gridColumns = ref(3); // 동적으로 계산
|
|
54
113
|
const gridRows = ref(1); // 동적으로 계산
|
|
114
|
+
const gridGap = computed(() => props.sectionData?.config?.gridGap ?? 5); // BaseLayout.vue --grid-gap과 동일한 소스/기본값
|
|
115
|
+
|
|
116
|
+
// row, col Resize
|
|
117
|
+
const MIN_ROW_PX = 20; // 행 최소 높이(px)
|
|
118
|
+
const MIN_COL_PX = 20; // 열 최소 너비(px)
|
|
119
|
+
|
|
120
|
+
const DRAG_CONFIG = {
|
|
121
|
+
row: {
|
|
122
|
+
ref: gridRows,
|
|
123
|
+
minPx: MIN_ROW_PX,
|
|
124
|
+
ratioKey: 'gridRowRatio',
|
|
125
|
+
gridTemplateKey: 'gridTemplateRows',
|
|
126
|
+
clientKey: 'clientY',
|
|
127
|
+
positionKey: 'y',
|
|
128
|
+
cssVar: '--grid-row-sizes',
|
|
129
|
+
},
|
|
130
|
+
col: {
|
|
131
|
+
ref: gridColumns,
|
|
132
|
+
minPx: MIN_COL_PX,
|
|
133
|
+
ratioKey: 'gridColumnRatio',
|
|
134
|
+
gridTemplateKey: 'gridTemplateColumns',
|
|
135
|
+
clientKey: 'clientX',
|
|
136
|
+
positionKey: 'x',
|
|
137
|
+
cssVar: '--grid-column-sizes',
|
|
138
|
+
},
|
|
139
|
+
};
|
|
140
|
+
|
|
141
|
+
const dragState = ref({
|
|
142
|
+
axis: null, // 'row' | 'col'
|
|
143
|
+
activeIdx: null,
|
|
144
|
+
isDragging: false,
|
|
145
|
+
});
|
|
146
|
+
const startPos = ref({ x: 0, y: 0 });
|
|
147
|
+
|
|
148
|
+
function handleMouseDown(event, axis, index) {
|
|
149
|
+
if (!index) return;
|
|
150
|
+
event.preventDefault();
|
|
151
|
+
event.stopPropagation();
|
|
152
|
+
|
|
153
|
+
const { ref, minPx, ratioKey, gridTemplateKey, clientKey, positionKey, cssVar } = DRAG_CONFIG[axis];
|
|
154
|
+
|
|
155
|
+
dragState.value.axis = axis;
|
|
156
|
+
dragState.value.activeIdx = index;
|
|
157
|
+
startPos.value = { x: event.clientX, y: event.clientY };
|
|
158
|
+
|
|
159
|
+
setActiveMenu(null); // 메뉴 닫기
|
|
160
|
+
setActiveDesign(null); // 디자인 닫기
|
|
161
|
+
|
|
162
|
+
let dragSnapShot = null; // 드래그 시작 시 스냅샷
|
|
163
|
+
let liveRatio = null; // 미리보기 중 계산된 최종 fr 배열 (stopDrag에서 커밋)
|
|
164
|
+
let pendingClient = null;
|
|
165
|
+
let rafId = null;
|
|
166
|
+
|
|
167
|
+
document.addEventListener('mousemove', _detectDragIntent);
|
|
168
|
+
document.addEventListener('mouseup', _cancelDragIntent);
|
|
169
|
+
|
|
170
|
+
function _detectDragIntent(e) {
|
|
171
|
+
if (dragState.value.axis !== axis || dragState.value.activeIdx !== index) return;
|
|
172
|
+
const deltaX = Math.abs(e.clientX - startPos.value.x);
|
|
173
|
+
const deltaY = Math.abs(e.clientY - startPos.value.y);
|
|
174
|
+
|
|
175
|
+
const isMoved = deltaX > 5 || deltaY > 5;
|
|
176
|
+
|
|
177
|
+
if (isMoved) {
|
|
178
|
+
// 의도 감지 리스너 제거
|
|
179
|
+
document.removeEventListener('mousemove', _detectDragIntent);
|
|
180
|
+
document.removeEventListener('mouseup', _cancelDragIntent);
|
|
181
|
+
|
|
182
|
+
// 드래그 시작
|
|
183
|
+
_startDrag();
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
function _cancelDragIntent() {
|
|
188
|
+
dragState.value.axis = null;
|
|
189
|
+
dragState.value.activeIdx = null;
|
|
190
|
+
|
|
191
|
+
document.removeEventListener('mousemove', _detectDragIntent);
|
|
192
|
+
document.removeEventListener('mouseup', _cancelDragIntent);
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
function _startDrag() {
|
|
196
|
+
dragState.value.isDragging = true;
|
|
197
|
+
|
|
198
|
+
const container = document.getElementById(`${props.sectionKey}Layout`);
|
|
199
|
+
if (!container) return _stopDrag();
|
|
200
|
+
|
|
201
|
+
const configured = props.sectionData?.config?.[ratioKey];
|
|
202
|
+
const baselineRatio =
|
|
203
|
+
(Array.isArray(configured) && configured.length === ref.value) || configured.some((v) => typeof v !== 'number' || !(v > 0))
|
|
204
|
+
? [...configured]
|
|
205
|
+
: Array(ref.value).fill(1);
|
|
206
|
+
|
|
207
|
+
const trackPx = getComputedStyle(container)
|
|
208
|
+
[gridTemplateKey].split(' ')
|
|
209
|
+
.map((v) => parseFloat(v));
|
|
210
|
+
|
|
211
|
+
// 총 Fr 계산
|
|
212
|
+
const totalFr = baselineRatio.reduce((sum, v) => sum + v, 0);
|
|
213
|
+
// 총 px 계산
|
|
214
|
+
const totalPx = trackPx.reduce((sum, v) => sum + v, 0);
|
|
215
|
+
// 1px당 Fr 계산
|
|
216
|
+
const frPerPx = totalFr / totalPx;
|
|
217
|
+
|
|
218
|
+
dragSnapShot = {
|
|
219
|
+
container,
|
|
220
|
+
baselineRatio,
|
|
221
|
+
frPerPx,
|
|
222
|
+
minFr: minPx * frPerPx, // 최소 Fr 계산
|
|
223
|
+
};
|
|
224
|
+
|
|
225
|
+
document.addEventListener('mousemove', _startDragMove);
|
|
226
|
+
document.addEventListener('mouseup', _stopDrag);
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
function _startDragMove(e) {
|
|
230
|
+
if (!dragSnapShot) return;
|
|
231
|
+
pendingClient = e[clientKey];
|
|
232
|
+
|
|
233
|
+
// rafId 중복 예약 방지
|
|
234
|
+
if (rafId) return;
|
|
235
|
+
|
|
236
|
+
rafId = requestAnimationFrame(() => {
|
|
237
|
+
rafId = null;
|
|
238
|
+
|
|
239
|
+
const { baselineRatio, frPerPx, minFr, container } = dragSnapShot;
|
|
240
|
+
const prevIdx = index - 1; // 드래그 행 위쪽 행 인덱스
|
|
241
|
+
const nextIdx = index; // 드래그 행 인덱스
|
|
242
|
+
|
|
243
|
+
// 순수 이동량
|
|
244
|
+
let delta = (pendingClient - startPos.value[positionKey]) * frPerPx;
|
|
245
|
+
// 위쪽 행 작아지는 것 방지: 가장 커져도 minFr - baselineRatio[prevIdx] (음수값 = 위로 이동)
|
|
246
|
+
delta = Math.max(minFr - baselineRatio[prevIdx], delta);
|
|
247
|
+
// 아래쪽 행 작아지는 것 방지: 가장 작아져도 minFr (양수값 = 아래로 이동)
|
|
248
|
+
delta = Math.min(baselineRatio[nextIdx] - minFr, delta);
|
|
249
|
+
|
|
250
|
+
// 미리보기용
|
|
251
|
+
const preview = [...baselineRatio];
|
|
252
|
+
preview[prevIdx] += delta;
|
|
253
|
+
preview[nextIdx] -= delta;
|
|
254
|
+
|
|
255
|
+
liveRatio = preview;
|
|
256
|
+
|
|
257
|
+
// 화면에만 즉시 반영
|
|
258
|
+
container.style.setProperty(cssVar, preview.map((v) => `${v}fr`).join(' '));
|
|
259
|
+
});
|
|
260
|
+
}
|
|
261
|
+
function _stopDrag() {
|
|
262
|
+
dragState.value.isDragging = false;
|
|
263
|
+
dragState.value.axis = null;
|
|
264
|
+
dragState.value.activeIdx = null;
|
|
265
|
+
|
|
266
|
+
if (liveRatio) {
|
|
267
|
+
setSectionConfig(ratioKey, liveRatio);
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
startPos.value = { x: 0, y: 0 };
|
|
271
|
+
dragSnapShot = null;
|
|
272
|
+
liveRatio = null;
|
|
273
|
+
pendingClient = null;
|
|
274
|
+
rafId = null;
|
|
275
|
+
|
|
276
|
+
document.removeEventListener('mousemove', _startDragMove);
|
|
277
|
+
document.removeEventListener('mouseup', _stopDrag);
|
|
278
|
+
}
|
|
279
|
+
}
|
|
55
280
|
|
|
56
281
|
function initData() {
|
|
57
282
|
gridColumns.value = props.sectionData?.config?.gridColumns || 3;
|
|
@@ -131,11 +356,22 @@ watch(
|
|
|
131
356
|
_isElementSwitching = false;
|
|
132
357
|
return;
|
|
133
358
|
}
|
|
134
|
-
|
|
359
|
+
updateActiveElement(selectedElement.value?.id, newElement);
|
|
135
360
|
},
|
|
136
361
|
{ deep: true },
|
|
137
362
|
);
|
|
138
363
|
|
|
364
|
+
watch(
|
|
365
|
+
() => activeDesign.value,
|
|
366
|
+
(newDesign, oldDesign) => {
|
|
367
|
+
if (newDesign != null || oldDesign == null) return; // null → null이 아닌 값으로 닫히는(값이 있다가 null 되는) 경우만
|
|
368
|
+
|
|
369
|
+
if (activeElement.value?.id && !activeElement.value.id.includes('inputForm')) {
|
|
370
|
+
updateActiveElement(activeElement.value.id, selectedElementForWatch.value, true);
|
|
371
|
+
}
|
|
372
|
+
},
|
|
373
|
+
);
|
|
374
|
+
|
|
139
375
|
onMounted(() => {
|
|
140
376
|
props.polarvo.components.register('sections', props.sectionKey, getCurrentInstance());
|
|
141
377
|
activeEditMode.value = false;
|
|
@@ -146,4 +382,18 @@ onMounted(() => {
|
|
|
146
382
|
onUnmounted(() => {
|
|
147
383
|
props.polarvo.components.unregister('sections', props.sectionKey);
|
|
148
384
|
});
|
|
385
|
+
|
|
386
|
+
//
|
|
149
387
|
</script>
|
|
388
|
+
|
|
389
|
+
<style lang="scss" scoped>
|
|
390
|
+
// .re-handle-line {
|
|
391
|
+
// position: absolute;
|
|
392
|
+
// z-index: 0;
|
|
393
|
+
// left: 0;
|
|
394
|
+
// right: 0;
|
|
395
|
+
// top: 50%;
|
|
396
|
+
// height: 1px;
|
|
397
|
+
// transform: translateY(-50%);
|
|
398
|
+
// }
|
|
399
|
+
</style>
|
|
@@ -17,8 +17,9 @@ const props = defineProps({
|
|
|
17
17
|
<style scoped>
|
|
18
18
|
.polar-grid {
|
|
19
19
|
display: grid;
|
|
20
|
-
grid-template-columns: repeat(var(--grid-columns, 3), 1fr);
|
|
21
|
-
grid-template-rows: repeat(var(--grid-rows, 3), 1fr);
|
|
20
|
+
grid-template-columns: var(--grid-column-sizes, repeat(var(--grid-columns, 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
|
|
|
@@ -835,7 +835,7 @@ class FreeDropEngine {
|
|
|
835
835
|
guides: this.guides,
|
|
836
836
|
timestamp: Date.now(),
|
|
837
837
|
});
|
|
838
|
-
this._resetDrag(true
|
|
838
|
+
this._resetDrag(true);
|
|
839
839
|
// }
|
|
840
840
|
|
|
841
841
|
return;
|
|
@@ -862,7 +862,7 @@ class FreeDropEngine {
|
|
|
862
862
|
* @param {boolean} emitEvent - 업데이트 이벤트 발송 여부
|
|
863
863
|
* @param {boolean} historyEvent - 히스토리 이벤트 기록 여부
|
|
864
864
|
*/
|
|
865
|
-
_resetDrag(emitEvent = false
|
|
865
|
+
_resetDrag(emitEvent = false) {
|
|
866
866
|
this._dragState.isDragging = false;
|
|
867
867
|
this.guides = { x: null, y: null, w: null, h: null };
|
|
868
868
|
|
|
@@ -871,17 +871,15 @@ class FreeDropEngine {
|
|
|
871
871
|
this._offsetPos = { x: 0, y: 0 };
|
|
872
872
|
this._dragOriginPos = { x: 0, y: 0 };
|
|
873
873
|
|
|
874
|
-
if (emitEvent) {
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
});
|
|
884
|
-
}
|
|
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
|
+
});
|
|
885
883
|
}
|
|
886
884
|
|
|
887
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', 'gridColumnRatio'].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,25 @@ 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 };
|
|
489
|
+
} else if (type === 'gridColumnRatio') {
|
|
490
|
+
// gridColumnRatio 배열은 gridColumns 길이와 일치해야 함
|
|
491
|
+
const gridColumns = this.activeData.config?.gridColumns || 1;
|
|
492
|
+
if (!Array.isArray(value) || value.length !== gridColumns || value.some((v) => typeof v !== 'number' || !(v > 0))) {
|
|
493
|
+
return { result: false, message: '잘못된 열 비율 값입니다.' };
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
newConfig = { gridColumnRatio: value };
|
|
470
497
|
} else {
|
|
498
|
+
const limit = CONFIG_LIMITS[type];
|
|
499
|
+
|
|
471
500
|
if (value < limit.min) {
|
|
472
501
|
result.result = false;
|
|
473
502
|
result.message = `설정 가능한 최소 ${limit.name}는 ${limit.min}입니다.`;
|
|
@@ -479,9 +508,11 @@ class EngineManager {
|
|
|
479
508
|
value = Math.min(Math.max(limit.min, value), limit.max);
|
|
480
509
|
|
|
481
510
|
if (type === 'column') {
|
|
482
|
-
|
|
511
|
+
// gridColumns 변경되면 gridColumnRatio 초기화
|
|
512
|
+
newConfig = { gridColumns: value, gridColumnRatio: [] };
|
|
483
513
|
} else if (type === 'row') {
|
|
484
|
-
|
|
514
|
+
// gridRows 변경되면 gridRowRatio 초기화
|
|
515
|
+
newConfig = { gridRows: value, gridRowRatio: [] };
|
|
485
516
|
} else if (type === 'gap') {
|
|
486
517
|
newConfig = { gridGap: value };
|
|
487
518
|
}
|
|
@@ -535,7 +566,11 @@ class EngineManager {
|
|
|
535
566
|
|
|
536
567
|
this.eventBus.emit('system:historyCheckpoint', {
|
|
537
568
|
domain: 'manager',
|
|
538
|
-
state:
|
|
569
|
+
state: {
|
|
570
|
+
manager: this.getState() || null,
|
|
571
|
+
layout: this.getEngine('layout')?.getState() || null,
|
|
572
|
+
display: this.getEngine('display')?.getState() || null,
|
|
573
|
+
},
|
|
539
574
|
});
|
|
540
575
|
});
|
|
541
576
|
|
|
@@ -957,7 +992,7 @@ class EngineManager {
|
|
|
957
992
|
setRatio: (type, index, ratio) => layoutEngine.setRatio(type, index, ratio),
|
|
958
993
|
// setUserLayoutData: (userData) => layoutEngine.setUserLayoutData(userData),
|
|
959
994
|
setActiveSection: (name) => this.setActiveSection(name),
|
|
960
|
-
updateActiveElement: (id, changes) => this.updateActiveElement(id, changes),
|
|
995
|
+
updateActiveElement: (id, changes, emitEvent) => this.updateActiveElement(id, changes, emitEvent),
|
|
961
996
|
|
|
962
997
|
setElements: (elements) => this.setElements(elements),
|
|
963
998
|
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
|
|