polarvo-layout 1.0.7 → 1.0.9
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/FastMenu/ActionFastMenu.vue +1 -0
- package/src/components/FastMenu/DesignFastMenu.vue +17 -14
- package/src/components/FastMenu/DisplayFastMenu.vue +2 -2
- package/src/components/FastMenu/LayoutFastMenu.vue +4 -4
- package/src/components/IconBar/IconBar.vue +43 -22
- package/src/components/Layout/BaseLayout.vue +1 -3
- package/src/components/Layout/CanvasContainer.vue +6 -6
- package/src/components/Layout/FreeLayout.vue +31 -7
- package/src/components/Layout/GridLayout.vue +30 -6
- package/src/core/engines/DisplayEngine.js +4 -0
- package/src/core/engines/FreeDropEngine.js +2 -2
- package/src/core/managers/EngineManager.js +34 -1
- package/src/icons/action/EditIcon.vue +52 -0
- package/src/icons/action/LockIcon.vue +1 -1
- package/src/icons/action/UnlockIcon.vue +1 -1
- package/src/icons/iconHolder.vue +37 -0
package/package.json
CHANGED
|
@@ -41,6 +41,7 @@ async function saveSetting() {
|
|
|
41
41
|
const finalElements = elements.value.filter((el) => elementIdsTotal.includes(el.id));
|
|
42
42
|
|
|
43
43
|
const { width, height } = containerSize.value || { width: 1920, height: 1080 };
|
|
44
|
+
|
|
44
45
|
// 요소 데이터 정규화
|
|
45
46
|
const normalizedElements = dataConverter.normalize(finalElements, width, height);
|
|
46
47
|
|
|
@@ -47,17 +47,18 @@ const activeElement = computed(() => {
|
|
|
47
47
|
});
|
|
48
48
|
|
|
49
49
|
const selectedElement = inject('selectedElement');
|
|
50
|
-
const selectedChildProps = inject('selectedChildProps');
|
|
50
|
+
// const selectedChildProps = inject('selectedChildProps');
|
|
51
51
|
|
|
52
52
|
// const emits = defineEmits(['click:designBar']);
|
|
53
53
|
function clickDesignBar(designMenu) {
|
|
54
54
|
if (designMenu == 'option') {
|
|
55
|
+
// 테스트용 -> inputForm만 남기고 추후 삭제 예정
|
|
55
56
|
designMenu = activeElement.value?.type === 'inputForm' ? 'option-form' : 'option-default';
|
|
56
57
|
}
|
|
57
58
|
|
|
58
59
|
// selectedElement의 props 기본값 세팅
|
|
59
60
|
if (selectedElement.value) {
|
|
60
|
-
setComponentDefaults(selectedElement.value);
|
|
61
|
+
// setComponentDefaults(selectedElement.value);
|
|
61
62
|
}
|
|
62
63
|
|
|
63
64
|
setActiveDesign(designMenu);
|
|
@@ -65,19 +66,21 @@ function clickDesignBar(designMenu) {
|
|
|
65
66
|
|
|
66
67
|
|
|
67
68
|
function setComponentDefaults(element) {
|
|
68
|
-
const child = props.polarvo.components.getElement(activeSection.value, element.id);
|
|
69
|
-
if (!child) return;
|
|
70
|
-
selectedChildProps.value = Object.keys(child.$props);
|
|
69
|
+
// const child = props.polarvo.components.getElement(activeSection.value, element.id);
|
|
70
|
+
// if (!child) return;
|
|
71
|
+
// selectedChildProps.value = Object.keys(child.$props);
|
|
71
72
|
|
|
72
|
-
//click한 컴포넌트의 props 객체 조회
|
|
73
|
-
const childProps = child.$options.props;
|
|
73
|
+
// //click한 컴포넌트의 props 객체 조회
|
|
74
|
+
// const childProps = child.$options.props;
|
|
75
|
+
|
|
76
|
+
// // props에 default 항목이 있는 경우, 함수인지 아닌지 확인 후 default 값 설정
|
|
77
|
+
// Object.entries(childProps).forEach(([key, propInfo]) => {
|
|
78
|
+
// if (propInfo.default !== undefined && (element[key] === undefined || element[key] === null)) {
|
|
79
|
+
// // 함수인 경우 실행하여 기본값 설정
|
|
80
|
+
// selectedElement.value[key] = typeof propInfo.default === 'function' ? propInfo.default() : propInfo.default;
|
|
81
|
+
// }
|
|
82
|
+
// });
|
|
83
|
+
// selectedElement.value = element;
|
|
74
84
|
|
|
75
|
-
// props에 default 항목이 있는 경우, 함수인지 아닌지 확인 후 default 값 설정
|
|
76
|
-
Object.entries(childProps).forEach(([key, propInfo]) => {
|
|
77
|
-
if (propInfo.default !== undefined && (element[key] === undefined || element[key] === null)) {
|
|
78
|
-
// 함수인 경우 실행하여 기본값 설정
|
|
79
|
-
selectedElement.value[key] = typeof propInfo.default === 'function' ? propInfo.default() : propInfo.default;
|
|
80
|
-
}
|
|
81
|
-
});
|
|
82
85
|
}
|
|
83
86
|
</script>
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
<template>
|
|
2
2
|
<!-- editor - title left -->
|
|
3
3
|
<div id="fast-menu" class="flex items-center gap-4">
|
|
4
|
-
<div class="flex gap-2 text-gray-400">
|
|
4
|
+
<!-- <div class="flex gap-2 text-gray-400">
|
|
5
5
|
<font-awesome-icon
|
|
6
6
|
:icon="['fas', 'display']"
|
|
7
7
|
class="cursor-pointer icon hover:opacity-70 active:text-blue-600"
|
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
@click="setDisplayMode('mobile')"
|
|
16
16
|
/>
|
|
17
17
|
</div>
|
|
18
|
-
<div id="divider" class="w-px bg-gray-200 h-4"></div>
|
|
18
|
+
<div id="divider" class="w-px bg-gray-200 h-4"></div> -->
|
|
19
19
|
<div class="text-sm text-gray-400">
|
|
20
20
|
<input
|
|
21
21
|
type="number"
|
|
@@ -57,8 +57,8 @@
|
|
|
57
57
|
min="0"
|
|
58
58
|
max="12"
|
|
59
59
|
class="text-sm text-center w-6 border-b-2 border-white focus:outline-none focus:border-b-2 focus:border-blue-500"
|
|
60
|
-
:value="
|
|
61
|
-
@change="
|
|
60
|
+
:value="activeData?.config?.gridGap"
|
|
61
|
+
@change="setSectionConfig('gap', $event.target.value)"
|
|
62
62
|
/>
|
|
63
63
|
</div>
|
|
64
64
|
</div>
|
|
@@ -77,8 +77,8 @@ const props = defineProps({
|
|
|
77
77
|
},
|
|
78
78
|
});
|
|
79
79
|
|
|
80
|
-
const { activeSection, activeData
|
|
81
|
-
const {
|
|
80
|
+
const { activeSection, activeData } = toRefs(props.polarvo.layout.state);
|
|
81
|
+
const { setSectionMode, setSectionConfig } = props.polarvo.layout;
|
|
82
82
|
const sectionMode = computed(() => activeData.value?.mode);
|
|
83
83
|
</script>
|
|
84
84
|
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
<template>
|
|
2
2
|
<aside id="icon-bar" class="flex flex-col items-center justify-between w-16 h-fit bg-white border-r border-gray-200">
|
|
3
3
|
<div class="icon-wrapper logo border-b border-gray-200 text-2xl text-blue-800 p-0 w-full" @click="openIconBar = !openIconBar">
|
|
4
|
-
<font-awesome-icon :icon="['fas', 'p']" class="h-16" />
|
|
4
|
+
<font-awesome-icon :icon="['fas', 'p']" class="h-16 text-xs" />
|
|
5
5
|
</div>
|
|
6
6
|
|
|
7
7
|
<!-- 아이콘바 활성화 -->
|
|
@@ -10,12 +10,12 @@
|
|
|
10
10
|
<div class="top-side flex flex-col items-center gap-2 mt-4">
|
|
11
11
|
<div class="icon-group">
|
|
12
12
|
<div
|
|
13
|
-
class="icon-wrapper
|
|
13
|
+
class="icon-wrapper text-2xl text-gray-400 p-2"
|
|
14
14
|
title="프로젝트 관리"
|
|
15
15
|
:class="{ active: activeMenu === 'home' }"
|
|
16
16
|
@click="$router.push('/console/home')"
|
|
17
17
|
>
|
|
18
|
-
<font-awesome-icon :icon="['fas', 'folder']" />
|
|
18
|
+
<font-awesome-icon :icon="['fas', 'folder']" class="text-xl"/>
|
|
19
19
|
<div class="text-xs text-center col-span-1 w-max">프로젝트</div>
|
|
20
20
|
</div>
|
|
21
21
|
</div>
|
|
@@ -24,57 +24,77 @@
|
|
|
24
24
|
<template v-if="isEditorPage">
|
|
25
25
|
<div id="divider" class="h-px bg-gray-200 w-8"></div>
|
|
26
26
|
<div class="icon-group">
|
|
27
|
-
<!-- 👂
|
|
27
|
+
<!-- 👂 기본설정에 테마설정 포함 -->
|
|
28
28
|
<div
|
|
29
|
-
class="icon-wrapper text-2xl text-gray-400 p-2
|
|
30
|
-
title="
|
|
29
|
+
class="icon-wrapper text-2xl text-gray-400 p-2"
|
|
30
|
+
title="기본설정"
|
|
31
31
|
:class="{ active: activeMenu === 'projectSetting' }"
|
|
32
32
|
@click="polarvo.display.setActiveMenu('projectSetting')"
|
|
33
33
|
>
|
|
34
|
-
|
|
35
|
-
<div class="text-xs text-center col-span-1 w-max"
|
|
34
|
+
<font-awesome-icon :icon="['fas', 'screwdriver-wrench']" class="text-xl"/>
|
|
35
|
+
<div class="text-xs text-center col-span-1 w-max">기본설정</div>
|
|
36
36
|
</div>
|
|
37
37
|
<div
|
|
38
|
-
class="icon-wrapper text-2xl text-gray-400 p-2
|
|
38
|
+
class="icon-wrapper text-2xl text-gray-400 p-2"
|
|
39
|
+
title="정책설정"
|
|
40
|
+
:class="{ active: activeMenu === 'policySetting' }"
|
|
41
|
+
@click="polarvo.display.setActiveMenu('policySetting')"
|
|
42
|
+
>
|
|
43
|
+
<font-awesome-icon :icon="['fas', 'shield-halved']" class="text-xl"/>
|
|
44
|
+
<div class="text-xs text-center col-span-1 w-max">정책설정</div>
|
|
45
|
+
</div>
|
|
46
|
+
</div>
|
|
47
|
+
<div id="divider" class="h-px bg-gray-200 w-8"></div>
|
|
48
|
+
<div class="icon-group">
|
|
49
|
+
<div
|
|
50
|
+
class="icon-wrapper text-2xl text-gray-400 p-2"
|
|
39
51
|
title="DB관리"
|
|
40
52
|
:class="{ active: activeMenu === 'dbSetting' }"
|
|
41
53
|
@click="polarvo.display.setActiveMenu('dbSetting')"
|
|
42
54
|
>
|
|
43
|
-
<font-awesome-icon :icon="['fas', 'database']" />
|
|
55
|
+
<font-awesome-icon :icon="['fas', 'database']" class="text-xl"/>
|
|
44
56
|
<div class="text-xs text-center col-span-1 w-max">DB관리</div>
|
|
45
57
|
</div>
|
|
46
58
|
<div
|
|
47
|
-
class="icon-wrapper text-2xl text-gray-400 p-2
|
|
59
|
+
class="icon-wrapper text-2xl text-gray-400 p-2"
|
|
60
|
+
title="화면이동"
|
|
61
|
+
:class="{ active: activeMenu === 'screenMove' }"
|
|
62
|
+
@click="polarvo.display.setActiveMenu('screenMove')"
|
|
63
|
+
>
|
|
64
|
+
<font-awesome-icon :icon="['fas', 'boxes-packing']" class="text-xl"/>
|
|
65
|
+
<div class="text-xs text-center col-span-1 w-max">화면이동</div>
|
|
66
|
+
</div>
|
|
67
|
+
<div
|
|
68
|
+
class="icon-wrapper text-2xl text-gray-400 p-2"
|
|
48
69
|
title="화면관리"
|
|
49
70
|
:class="{ active: activeMenu === 'screenSetting' }"
|
|
50
71
|
@click="polarvo.display.setActiveMenu('screenSetting')"
|
|
51
72
|
>
|
|
52
|
-
<font-awesome-icon :icon="['
|
|
73
|
+
<font-awesome-icon :icon="['fas', 'computer']" class="text-xl"/>
|
|
53
74
|
<div class="text-xs text-center col-span-1 w-max">화면관리</div>
|
|
54
75
|
</div>
|
|
55
76
|
</div>
|
|
56
|
-
|
|
57
77
|
<!-- 화면 선택 시 아이콘 그룹 -->
|
|
58
78
|
<template v-if="isScreen">
|
|
59
79
|
<div id="divider" class="h-px bg-gray-200 w-8"></div>
|
|
60
80
|
<div class="icon-group">
|
|
61
81
|
<div
|
|
62
|
-
class="icon-wrapper text-2xl text-gray-400 p-2
|
|
82
|
+
class="icon-wrapper text-2xl text-gray-400 p-2"
|
|
63
83
|
title="레이아웃"
|
|
64
84
|
:class="{ active: activeMenu === 'layout' }"
|
|
65
85
|
@click="polarvo.display.setActiveMenu('layout')"
|
|
66
86
|
>
|
|
67
|
-
<font-awesome-icon :icon="['fas', 'table-columns']" />
|
|
87
|
+
<font-awesome-icon :icon="['fas', 'table-columns']" class="text-xl"/>
|
|
68
88
|
<div class="text-xs text-center col-span-1 w-max">레이아웃</div>
|
|
69
89
|
</div>
|
|
70
90
|
<div
|
|
71
91
|
v-if="activeSection"
|
|
72
|
-
class="icon-wrapper text-2xl text-gray-400 p-2
|
|
92
|
+
class="icon-wrapper text-2xl text-gray-400 p-2"
|
|
73
93
|
title="요소"
|
|
74
94
|
:class="{ active: activeMenu === 'element' }"
|
|
75
95
|
@click="polarvo.display.setActiveMenu('element')"
|
|
76
96
|
>
|
|
77
|
-
<font-awesome-icon :icon="['fas', 'shapes']" />
|
|
97
|
+
<font-awesome-icon :icon="['fas', 'shapes']" class="text-xl"/>
|
|
78
98
|
<div class="text-xs text-center col-span-1 w-max">요소</div>
|
|
79
99
|
</div>
|
|
80
100
|
</div>
|
|
@@ -90,7 +110,7 @@
|
|
|
90
110
|
:class="{ active: activeMenu === 'setting' }"
|
|
91
111
|
@click="$router.push('/console/setting')"
|
|
92
112
|
>
|
|
93
|
-
<font-awesome-icon :icon="['fas', 'gear']" />
|
|
113
|
+
<font-awesome-icon :icon="['fas', 'gear']" class="text-xl"/>
|
|
94
114
|
<div class="text-xs text-center col-span-1 w-max">환경설정</div>
|
|
95
115
|
</div>
|
|
96
116
|
<!-- 계정설정 -->
|
|
@@ -101,7 +121,7 @@
|
|
|
101
121
|
title="계정설정"
|
|
102
122
|
@click="toggleUser = !toggleUser"
|
|
103
123
|
>
|
|
104
|
-
<font-awesome-icon :icon="['fas', 'user']" />
|
|
124
|
+
<font-awesome-icon :icon="['fas', 'user']" class="text-xl"/>
|
|
105
125
|
</div>
|
|
106
126
|
|
|
107
127
|
<!-- 계정설정 토글메뉴 -->
|
|
@@ -165,6 +185,7 @@ async function logout() {
|
|
|
165
185
|
<style scoped lang="scss">
|
|
166
186
|
.icon-wrapper {
|
|
167
187
|
aspect-ratio: 1 / 1; /* aspect-square */
|
|
188
|
+
width: 100%;
|
|
168
189
|
display: grid; /* grid */
|
|
169
190
|
grid-template-rows: 1fr auto; /* grid-rows-[1fr_auto] */
|
|
170
191
|
place-items: center; /* place-items-center */
|
|
@@ -173,7 +194,7 @@ async function logout() {
|
|
|
173
194
|
cursor: pointer; /* cursor-pointer */
|
|
174
195
|
|
|
175
196
|
&:hover:not(.logo):not(.active) {
|
|
176
|
-
width: 100%;
|
|
197
|
+
// width: 100%;
|
|
177
198
|
background-color: #f3f4f6; /* hover:bg-gray-100 */
|
|
178
199
|
border-radius: 0.5rem; /* rounded-lg */
|
|
179
200
|
color: #4b5563; /* text-gray-600 */
|
|
@@ -199,8 +220,8 @@ async function logout() {
|
|
|
199
220
|
display: flex;
|
|
200
221
|
flex-direction: column;
|
|
201
222
|
align-items: center;
|
|
202
|
-
gap: 0.
|
|
203
|
-
padding-
|
|
223
|
+
gap: 0.25rem; /* gap-1 */
|
|
224
|
+
padding-block: 0.5rem; /* py-2 */
|
|
204
225
|
}
|
|
205
226
|
|
|
206
227
|
.bubble::before {
|
|
@@ -35,7 +35,7 @@
|
|
|
35
35
|
</template>
|
|
36
36
|
|
|
37
37
|
<script setup>
|
|
38
|
-
import { toRefs, computed
|
|
38
|
+
import { toRefs, computed } from 'vue';
|
|
39
39
|
|
|
40
40
|
import PolarLayout from './PolarLayout.vue';
|
|
41
41
|
import FreeLayout from './FreeLayout.vue';
|
|
@@ -89,8 +89,6 @@ const getSectionClass = (key, section) => ({
|
|
|
89
89
|
'section-active': activeSection.value === key,
|
|
90
90
|
disabled: activeSection.value != key && !props.preview,
|
|
91
91
|
});
|
|
92
|
-
|
|
93
|
-
|
|
94
92
|
</script>
|
|
95
93
|
|
|
96
94
|
<style scoped>
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
class="absolute bottom-4 left-4 right-4 bg-gray-50 flex items-center justify-center text-gray-400 rounded-lg hover:bg-gray-100 hover:text-gray-600 cursor-pointer"
|
|
7
7
|
@click="expandContainer()"
|
|
8
8
|
>
|
|
9
|
-
<font-awesome-icon :icon="['fas', 'plus']" class="h-8" />
|
|
9
|
+
<font-awesome-icon :icon="['fas', 'plus']" class="h-8 text-xs" />
|
|
10
10
|
</div>
|
|
11
11
|
</div>
|
|
12
12
|
</div>
|
|
@@ -47,6 +47,10 @@ watch(
|
|
|
47
47
|
const additionalHeight = ref(0);
|
|
48
48
|
const displayHeight = computed(() => defaultHeight.value + additionalHeight.value);
|
|
49
49
|
|
|
50
|
+
function expandContainer() {
|
|
51
|
+
additionalHeight.value += 200;
|
|
52
|
+
}
|
|
53
|
+
|
|
50
54
|
const containerStyle = computed(() => {
|
|
51
55
|
return {
|
|
52
56
|
aspectRatio: displaySize.value?.aspectRatio,
|
|
@@ -64,11 +68,7 @@ const containerStyle = computed(() => {
|
|
|
64
68
|
});
|
|
65
69
|
|
|
66
70
|
// 전체 elements
|
|
67
|
-
provide('elements', elements);
|
|
68
|
-
|
|
69
|
-
function expandContainer() {
|
|
70
|
-
additionalHeight.value += 200;
|
|
71
|
-
}
|
|
71
|
+
// provide('elements', elements);
|
|
72
72
|
</script>
|
|
73
73
|
|
|
74
74
|
<style scoped lang="scss">
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
class="absolute user-select-none"
|
|
8
8
|
:style="getElementStyle(item)"
|
|
9
9
|
:class="{ 'z-50': item.id === activeId, 'bg-white border-2 border-blue-400': item.id === activeId }"
|
|
10
|
-
@mousedown.stop="startDrag($event,
|
|
10
|
+
@mousedown.stop="startDrag($event, item.id)"
|
|
11
11
|
>
|
|
12
12
|
<!-- 리사이즈 핸들 -->
|
|
13
13
|
<div v-if="item.id === activeId" class="absolute inset-0 pointer-events-none">
|
|
@@ -17,9 +17,21 @@
|
|
|
17
17
|
:class="`handle absolute bg-blue-400 z-10 pointer-events-auto hover:bg-blue-700 ${handle.name}`"
|
|
18
18
|
@mousedown="startResize($event, handle.name)"
|
|
19
19
|
></div>
|
|
20
|
-
<div class="
|
|
21
|
-
|
|
22
|
-
<
|
|
20
|
+
<div class="flex flex-row m-1 gap-1">
|
|
21
|
+
<!-- 잠금 토글-->
|
|
22
|
+
<div class="z-10 pointer-events-auto cursor-pointer" @click="toggleLock()">
|
|
23
|
+
<LockIcon v-if="item.isLocked" />
|
|
24
|
+
<UnlockIcon v-else />
|
|
25
|
+
</div>
|
|
26
|
+
<!-- 편집 토글-->
|
|
27
|
+
<div class="z-10 pointer-events-auto cursor-pointer">
|
|
28
|
+
<div v-if="item.type === 'inputForm'" @click="toggleEditMode">
|
|
29
|
+
<div v-if="activeEditMode" class="bg-red-100 text-red-400 px-2 py-1 text-sm rounded-md hover:opacity-80 animate-pulse">
|
|
30
|
+
편집모드 on
|
|
31
|
+
</div>
|
|
32
|
+
<div v-else class="bg-gray-100 px-2 py-1 text-sm rounded-md hover:bg-gray-200">편집모드 off</div>
|
|
33
|
+
</div>
|
|
34
|
+
</div>
|
|
23
35
|
</div>
|
|
24
36
|
</div>
|
|
25
37
|
<component
|
|
@@ -27,10 +39,14 @@
|
|
|
27
39
|
:id="`${sectionKey}-${index}`"
|
|
28
40
|
:ref="(el) => setComponentRef(el, item.id)"
|
|
29
41
|
v-bind="item"
|
|
30
|
-
class="hover:opacity-80
|
|
42
|
+
class="hover:opacity-80"
|
|
43
|
+
:class="activeEditMode ? `pointer-events-auto` : `pointer-events-none`"
|
|
31
44
|
/>
|
|
32
45
|
</div>
|
|
33
|
-
|
|
46
|
+
|
|
47
|
+
<!-- 오버레이 -->
|
|
48
|
+
<div v-if="activeEditMode" class="absolute inset-0 bg-black opacity-50"></div>
|
|
49
|
+
|
|
34
50
|
<!-- 가이드라인 -->
|
|
35
51
|
<div v-if="isActive">
|
|
36
52
|
<div v-if="guides.x !== null" class="guide absolute z-10 pointer-events-none vertical" :style="{ left: `${guides.x}px` }"></div>
|
|
@@ -109,6 +125,11 @@ function setComponentRef(el, elementId) {
|
|
|
109
125
|
|
|
110
126
|
const selectedElement = inject('selectedElement');
|
|
111
127
|
|
|
128
|
+
const activeEditMode = inject('activeEditMode');
|
|
129
|
+
function toggleEditMode() {
|
|
130
|
+
activeEditMode.value = !activeEditMode.value;
|
|
131
|
+
}
|
|
132
|
+
|
|
112
133
|
import { omit, cloneDeep } from 'lodash-es';
|
|
113
134
|
watch(
|
|
114
135
|
() => activeElement.value?.id,
|
|
@@ -119,6 +140,9 @@ watch(
|
|
|
119
140
|
}
|
|
120
141
|
// selectedElement.value = structuredClone(toRaw(activeElement.value));
|
|
121
142
|
selectedElement.value = cloneDeep(activeElement.value);
|
|
143
|
+
|
|
144
|
+
// activeElement 바뀌면 편집 모드 해제
|
|
145
|
+
activeEditMode.value = false;
|
|
122
146
|
},
|
|
123
147
|
);
|
|
124
148
|
|
|
@@ -130,7 +154,7 @@ watch(
|
|
|
130
154
|
(newElement) => {
|
|
131
155
|
updateActiveElement(selectedElement.value?.id, newElement);
|
|
132
156
|
},
|
|
133
|
-
{ deep: true }
|
|
157
|
+
{ deep: true },
|
|
134
158
|
);
|
|
135
159
|
|
|
136
160
|
onMounted(() => {
|
|
@@ -15,9 +15,21 @@
|
|
|
15
15
|
<!-- 리사이즈 핸들 -->
|
|
16
16
|
<div v-if="activeElement?.id === item.id">
|
|
17
17
|
<div class="absolute w-4 h-4 bottom-0 right-0 bg-blue-400 z-10 cursor-se-resize" @mousedown="startResize($event, item.id)"></div>
|
|
18
|
-
<div class="
|
|
19
|
-
|
|
20
|
-
<
|
|
18
|
+
<div class="flex gap-1 m-1">
|
|
19
|
+
<!-- 잠금 토글-->
|
|
20
|
+
<div class="z-10 pointer-events-auto cursor-pointer" @click="toggleLock()">
|
|
21
|
+
<LockIcon v-if="item.isLocked" />
|
|
22
|
+
<UnlockIcon v-else />
|
|
23
|
+
</div>
|
|
24
|
+
<!-- 편집 토글-->
|
|
25
|
+
<div class="z-10 pointer-events-auto cursor-pointer">
|
|
26
|
+
<div v-if="item.type === 'inputForm'" @click="toggleEditMode">
|
|
27
|
+
<div v-if="activeEditMode" class="bg-red-100 text-red-400 px-2 py-1 text-sm rounded-md hover:opacity-80 animate-pulse">
|
|
28
|
+
편집모드 on
|
|
29
|
+
</div>
|
|
30
|
+
<div v-else class="bg-gray-100 px-2 py-1 text-sm rounded-md hover:bg-gray-200">편집모드 off</div>
|
|
31
|
+
</div>
|
|
32
|
+
</div>
|
|
21
33
|
</div>
|
|
22
34
|
</div>
|
|
23
35
|
|
|
@@ -26,10 +38,14 @@
|
|
|
26
38
|
:id="`${sectionKey}-${index}`"
|
|
27
39
|
:ref="(el) => setComponentRef(el, item.id)"
|
|
28
40
|
v-bind="item"
|
|
29
|
-
class="hover:opacity-80
|
|
41
|
+
class="hover:opacity-80"
|
|
42
|
+
:class="activeEditMode ? `pointer-events-auto` : `pointer-events-none`"
|
|
30
43
|
/>
|
|
31
44
|
</div>
|
|
32
45
|
|
|
46
|
+
<!-- 오버레이 -->
|
|
47
|
+
<div v-if="activeEditMode" class="absolute inset-0 bg-black opacity-50"></div>
|
|
48
|
+
|
|
33
49
|
<!-- 빈 그리드 설정 -->
|
|
34
50
|
<div
|
|
35
51
|
v-for="(cell, index) in emptyData[sectionKey]"
|
|
@@ -103,6 +119,11 @@ function setComponentRef(el, elementId) {
|
|
|
103
119
|
}
|
|
104
120
|
|
|
105
121
|
const selectedElement = inject('selectedElement');
|
|
122
|
+
const activeEditMode = inject('activeEditMode');
|
|
123
|
+
|
|
124
|
+
function toggleEditMode() {
|
|
125
|
+
activeEditMode.value = !activeEditMode.value;
|
|
126
|
+
}
|
|
106
127
|
|
|
107
128
|
import { omit, cloneDeep } from 'lodash-es';
|
|
108
129
|
watch(
|
|
@@ -114,8 +135,11 @@ watch(
|
|
|
114
135
|
}
|
|
115
136
|
// selectedElement.value = structuredClone(toRaw(activeElement.value));
|
|
116
137
|
selectedElement.value = cloneDeep(activeElement.value);
|
|
138
|
+
|
|
139
|
+
// activeElement 바뀌면 편집 모드 해제
|
|
140
|
+
activeEditMode.value = false;
|
|
117
141
|
},
|
|
118
|
-
{ immediate: true }
|
|
142
|
+
{ immediate: true },
|
|
119
143
|
);
|
|
120
144
|
|
|
121
145
|
// 특정 키 값을 제외하고 변경 감지
|
|
@@ -126,7 +150,7 @@ watch(
|
|
|
126
150
|
(newElement) => {
|
|
127
151
|
updateActiveElement(selectedElement.value?.id, newElement);
|
|
128
152
|
},
|
|
129
|
-
{ deep: true }
|
|
153
|
+
{ deep: true },
|
|
130
154
|
);
|
|
131
155
|
|
|
132
156
|
onMounted(() => {
|
|
@@ -163,12 +163,14 @@ class DisplayEngine {
|
|
|
163
163
|
if (this.displayMode === mode) return;
|
|
164
164
|
|
|
165
165
|
this.displayMode = mode;
|
|
166
|
+
const prevDisplaySize = { ...this.displaySize };
|
|
166
167
|
this._resetDisplaySizeByMode(mode);
|
|
167
168
|
|
|
168
169
|
// displayMode 변경 시, displaySize도 초기화 됨
|
|
169
170
|
this.eventBus.emit('display:updateDisplayMode', {
|
|
170
171
|
displayMode: this.displayMode,
|
|
171
172
|
displaySize: this.displaySize,
|
|
173
|
+
prev: { displaySize: prevDisplaySize },
|
|
172
174
|
timestamp: Date.now(),
|
|
173
175
|
});
|
|
174
176
|
}
|
|
@@ -213,10 +215,12 @@ class DisplayEngine {
|
|
|
213
215
|
alert(`설정 가능한 최대 ${typeLimit.name}는 ${typeLimit.max}${typeLimit.unit}입니다.`);
|
|
214
216
|
}
|
|
215
217
|
|
|
218
|
+
const prevDisplaySize = { ...this.displaySize };
|
|
216
219
|
this.displaySize[type] = Math.min(Math.max(0, numSize), typeLimit.max);
|
|
217
220
|
|
|
218
221
|
this.eventBus.emit('display:updateDisplaySize', {
|
|
219
222
|
displaySize: this.displaySize,
|
|
223
|
+
prev: { displaySize: prevDisplaySize },
|
|
220
224
|
timestamp: Date.now(),
|
|
221
225
|
});
|
|
222
226
|
}
|
|
@@ -71,9 +71,9 @@ class FreeDropEngine {
|
|
|
71
71
|
});
|
|
72
72
|
|
|
73
73
|
// layoutName 변경 감지 (gridNumber가 함께 초기화됨)
|
|
74
|
-
this._subscribe('layout:setLayoutName', ({gridNumber}) => {
|
|
74
|
+
this._subscribe('layout:setLayoutName', ({ gridNumber }) => {
|
|
75
75
|
this._gridNumber = gridNumber;
|
|
76
|
-
})
|
|
76
|
+
});
|
|
77
77
|
this._subscribe('layout:updateLayoutName', ({ gridNumber }) => {
|
|
78
78
|
this._gridNumber = gridNumber;
|
|
79
79
|
});
|
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import { cloneDeep } from 'lodash-es';
|
|
2
|
+
import { nextTick } from 'vue';
|
|
3
|
+
|
|
2
4
|
import EventBus from './EventBus.js';
|
|
3
5
|
import ApiManager from './ApiManager.js';
|
|
4
6
|
import utils from '../../utils/index.js';
|
|
@@ -180,7 +182,38 @@ class EngineManager {
|
|
|
180
182
|
|
|
181
183
|
/** ---------------------------------- display Engine ---------------------------------- **/
|
|
182
184
|
/** [내부함수] displayEngine 연결 설정 */
|
|
183
|
-
_setupDisplayEngineConnections() {
|
|
185
|
+
_setupDisplayEngineConnections() {
|
|
186
|
+
this._subscribe('display:updateDisplayMode', ({ displaySize, prev }) => {
|
|
187
|
+
this._updateElementsScale(prev.displaySize, displaySize);
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
this._subscribe('display:updateDisplaySize', ({ displaySize, prev }) => {
|
|
191
|
+
this._updateElementsScale(prev.displaySize, displaySize);
|
|
192
|
+
});
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
_updateElementsScale(oldValue, newValue) {
|
|
196
|
+
const oldWidth = oldValue.px;
|
|
197
|
+
const oldHeight = oldValue.px / oldValue.aspectRatio.split('/').map(Number).reduce((a, b) => a / b);
|
|
198
|
+
|
|
199
|
+
const newWidth = newValue.px;
|
|
200
|
+
const newHeight = newValue.px / newValue.aspectRatio.split('/').map(Number).reduce((a, b) => a / b);
|
|
201
|
+
|
|
202
|
+
nextTick(() => {
|
|
203
|
+
this.setContainerSize();
|
|
204
|
+
|
|
205
|
+
const normalizeElements = dataConverter.normalize(this.state.elements, oldWidth, oldHeight);
|
|
206
|
+
const denormalizedElements = dataConverter.denormalize(normalizeElements, newWidth, newHeight);
|
|
207
|
+
|
|
208
|
+
this.state.elements = cloneDeep(denormalizedElements);
|
|
209
|
+
|
|
210
|
+
this.eventBus.emit('system:requestUpdateElements', {
|
|
211
|
+
historyEvent: true,
|
|
212
|
+
elements: cloneDeep(this.state.elements),
|
|
213
|
+
timestamp: Date.now(),
|
|
214
|
+
});
|
|
215
|
+
});
|
|
216
|
+
}
|
|
184
217
|
|
|
185
218
|
/** ---------------------------------- layout Engine ---------------------------------- **/
|
|
186
219
|
/** [내부함수] layoutEngine 연결 설정 */
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<svg
|
|
3
|
+
width="24"
|
|
4
|
+
height="24"
|
|
5
|
+
viewBox="0 0 24 24"
|
|
6
|
+
fill="none"
|
|
7
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
8
|
+
class="edit-icon"
|
|
9
|
+
>
|
|
10
|
+
<!-- 종이/문서 -->
|
|
11
|
+
<path
|
|
12
|
+
d="M12 6H7C6.73478 6 6.48043 6.10536 6.29289 6.29289C6.10536 6.48043 6 6.73478 6 7V17C6 17.2652 6.10536 17.5196 6.29289 17.7071C6.48043 17.8946 6.73478 18 7 18H17C17.2652 18 17.5196 17.8946 17.7071 17.7071C17.8946 17.5196 18 17.2652 18 17V12"
|
|
13
|
+
stroke="currentColor"
|
|
14
|
+
stroke-width="2"
|
|
15
|
+
stroke-linecap="round"
|
|
16
|
+
stroke-linejoin="round"
|
|
17
|
+
/>
|
|
18
|
+
<!-- 연필 -->
|
|
19
|
+
<path
|
|
20
|
+
d="M17.5 4.5C17.6989 4.30109 17.9687 4.18934 18.25 4.18934C18.5313 4.18934 18.8011 4.30109 19 4.5C19.1989 4.69891 19.3107 4.96866 19.3107 5.25C19.3107 5.53134 19.1989 5.80109 19 6L13 12L11 13L12 11L17.5 4.5Z"
|
|
21
|
+
stroke="currentColor"
|
|
22
|
+
stroke-width="2"
|
|
23
|
+
stroke-linecap="round"
|
|
24
|
+
stroke-linejoin="round"
|
|
25
|
+
/>
|
|
26
|
+
</svg>
|
|
27
|
+
</template>
|
|
28
|
+
|
|
29
|
+
<style scoped lang="scss">
|
|
30
|
+
.edit-icon {
|
|
31
|
+
color: #6b7280; /* 미선택 상태를 나타내는 회색 */
|
|
32
|
+
background-color: rgba(107, 114, 128, 0.1); /* 연한 회색 배경 */
|
|
33
|
+
/* margin: 4px; */
|
|
34
|
+
padding: 2px;
|
|
35
|
+
border-radius: 4px;
|
|
36
|
+
|
|
37
|
+
&.active {
|
|
38
|
+
color: #2563eb; /* 선택 상태를 나타내는 파란색 */
|
|
39
|
+
background-color: rgba(37, 99, 235, 0.1);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
.edit-icon:hover {
|
|
44
|
+
color: #4b5563; /* 호버 시 더 진한 회색 */
|
|
45
|
+
background-color: rgba(107, 114, 128, 0.15); /* 호버 시 배경색 강화 */
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
.edit-icon:active {
|
|
49
|
+
color: #374151; /* 클릭 시 가장 진한 회색 */
|
|
50
|
+
background-color: rgba(107, 114, 128, 0.2);
|
|
51
|
+
}
|
|
52
|
+
</style>
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div class="icon-holder">
|
|
3
|
+
<slot></slot>
|
|
4
|
+
</div>
|
|
5
|
+
</template>
|
|
6
|
+
|
|
7
|
+
<style scoped lang="scss">
|
|
8
|
+
.icon-holder {
|
|
9
|
+
width: 24px;
|
|
10
|
+
height: 24px;
|
|
11
|
+
|
|
12
|
+
display: flex;
|
|
13
|
+
justify-content: center;
|
|
14
|
+
align-items: center;
|
|
15
|
+
|
|
16
|
+
color: #6b7280; /* 미선택 상태를 나타내는 회색 */
|
|
17
|
+
background-color: rgba(107, 114, 128, 0.1); /* 연한 회색 배경 */
|
|
18
|
+
/* margin: 4px; */
|
|
19
|
+
padding: 4px;
|
|
20
|
+
border-radius: 4px;
|
|
21
|
+
|
|
22
|
+
&.active {
|
|
23
|
+
color: #2563eb; /* 선택 상태를 나타내는 파란색 */
|
|
24
|
+
background-color: rgba(37, 99, 235, 0.1);
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
.icon-holder:hover {
|
|
29
|
+
color: #4b5563; /* 호버 시 더 진한 회색 */
|
|
30
|
+
background-color: rgba(107, 114, 128, 0.15); /* 호버 시 배경색 강화 */
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
.icon-holder:active {
|
|
34
|
+
color: #374151; /* 클릭 시 가장 진한 회색 */
|
|
35
|
+
background-color: rgba(107, 114, 128, 0.2);
|
|
36
|
+
}
|
|
37
|
+
</style>
|