polarvo-layout 1.0.87 → 1.0.89

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.87",
3
+ "version": "1.0.89",
4
4
  "type": "module",
5
5
  "author": "unigence <unigencelab@gmail.com>",
6
6
  "repository": {
@@ -11,7 +11,7 @@
11
11
  <PolarLayout
12
12
  :id="preview ? `${key}Preview` : `${key}Layout`"
13
13
  :mode="section.mode"
14
- :style="getSectionStyle(section)"
14
+ :style="getSectionStyle(key, section)"
15
15
  :class="[getSectionClass(key, section), activeSection === key && preview ? 'text-blue-400 bg-blue-50' : 'text-gray-400']"
16
16
  @click.stop="emit('click:section', key)"
17
17
  >
@@ -19,7 +19,14 @@
19
19
  {{ key }}
20
20
  </div>
21
21
  <template v-if="section.mode === 'grid'">
22
- <GridLayout :polarvo="polarvo" :preview="preview" :sectionKey="key" :sectionData="section" :elementIds="section.elementIds" />
22
+ <GridLayout
23
+ :polarvo="polarvo"
24
+ :preview="preview"
25
+ :sectionKey="key"
26
+ :sectionData="section"
27
+ :elementIds="section.elementIds"
28
+ :extraHeight="getSectionExtraHeight(section)"
29
+ />
23
30
  </template>
24
31
 
25
32
  <template v-else>
@@ -37,11 +44,12 @@
37
44
  </template>
38
45
 
39
46
  <script setup>
40
- import { toRefs, computed, onMounted, onUnmounted, ref, inject } from 'vue';
47
+ import { toRefs, computed, onMounted, onUnmounted, ref, inject, unref } from 'vue';
41
48
 
42
49
  import PolarLayout from './PolarLayout.vue';
43
50
  import FreeLayout from './FreeLayout.vue';
44
51
  import GridLayout from './GridLayout.vue';
52
+ import { getGridRowMinPx, toGridTrackSizes, toGridRowSizes, isInLastLayoutRow } from '../../utils/grid-track.js';
45
53
 
46
54
  // 우클릭 방지
47
55
  // window.oncontextmenu = () => {
@@ -69,29 +77,53 @@ const layoutType = computed(() => {
69
77
  });
70
78
  const { layoutName, layoutData, activeSection, gapSize, gridNumber, gridRatio } = toRefs(props.polarvo.layout.state);
71
79
  const { activeDesign } = toRefs(props.polarvo.display.state);
80
+ const { elements: gridElements } = toRefs(props.polarvo.gridDrop.state);
72
81
  const selectedElement = inject('selectedElement');
73
82
  const activeEditMode = inject('activeEditMode');
74
83
  const activeLeaf = inject('activeLeaf');
84
+ const canvasExtraHeight = inject('canvasExtraHeight', 0); // CanvasContainer 확장 높이 (미리보기는 0)
85
+
86
+ const extraHeight = computed(() => (props.preview ? 0 : (unref(canvasExtraHeight) ?? 0)));
87
+
88
+ // 확장 높이는 마지막 행 섹션만 가져감
89
+ const getSectionExtraHeight = (section) => (isInLastLayoutRow(section, gridRatio.value?.row?.length) ? extraHeight.value : 0);
75
90
 
76
91
  const getBaseStyle = computed(() => {
77
92
  return {
78
93
  gridTemplateColumns: gridRatio.value?.column.map((ratio) => `${ratio}fr`).join(' '),
79
- gridTemplateRows: gridRatio.value?.row.map((ratio) => `${ratio}fr`).join(' '),
94
+ // 확장 높이는 마지막 행만 가져감 (min은 기존 fr과 동일하게 auto - 섹션 콘텐츠 최소 높이 유지)
95
+ gridTemplateRows: gridRatio.value?.row
96
+ ? toGridRowSizes(
97
+ gridRatio.value.row,
98
+ gridRatio.value.row.map(() => 'auto'),
99
+ gapSize.value ?? 0,
100
+ extraHeight.value,
101
+ )
102
+ : undefined,
80
103
  marginRight: layoutType.value === 'no-split' ? '0' : layoutType.value === 'three-column-split' ? '14px' : '8px',
81
104
  gap: `${gapSize.value}px`,
82
105
  };
83
106
  });
84
107
 
85
- const getSectionStyle = (section) => ({
86
- '--grid-columns': section.config?.gridColumns ?? 3,
87
- '--grid-rows': section.config?.gridRows ?? 3,
88
- '--grid-gap': `${section.config?.gridGap ?? 5}px`,
89
- '--grid-row-sizes': section.config?.gridRowRatio?.length ? section.config.gridRowRatio.map((r) => `${r}fr`).join(' ') : undefined, // 비어있으면 아예 안 넣어서 CSS 기본값으로 폴백
90
- '--grid-column-sizes': section.config?.gridColumnRatio?.length
91
- ? section.config.gridColumnRatio.map((r) => `${r}fr`).join(' ')
92
- : undefined,
93
- position: 'relative',
94
- });
108
+ const getSectionStyle = (key, section) => {
109
+ const gridRows = section.config?.gridRows ?? 3;
110
+ const rowRatio = section.config?.gridRowRatio?.length ? section.config.gridRowRatio : Array(gridRows).fill(1);
111
+ const sectionElements = gridElements.value.filter((el) => el.section === key && section.elementIds?.includes(el.id));
112
+
113
+ return {
114
+ '--grid-columns': section.config?.gridColumns ?? 3,
115
+ '--grid-rows': gridRows,
116
+ '--grid-gap': `${section.config?.gridGap ?? 5}px`,
117
+ '--grid-row-sizes': toGridRowSizes(
118
+ rowRatio,
119
+ getGridRowMinPx(sectionElements, gridRows),
120
+ section.config?.gridGap ?? 5,
121
+ getSectionExtraHeight(section),
122
+ ),
123
+ '--grid-column-sizes': section.config?.gridColumnRatio?.length ? toGridTrackSizes(section.config.gridColumnRatio) : undefined, // 비어있으면 아예 안 넣어서 CSS 기본값으로 폴백
124
+ position: 'relative',
125
+ };
126
+ };
95
127
 
96
128
  const getSectionClass = (key, section) => ({
97
129
  [key]: true,
@@ -24,8 +24,9 @@
24
24
  </template>
25
25
 
26
26
  <script setup>
27
- import { toRefs, ref, computed, watch } from 'vue';
27
+ import { toRefs, ref, computed, watch, provide, onMounted } from 'vue';
28
28
  import BaseLayout from './BaseLayout.vue';
29
+ import { isInLastLayoutRow, getGridLastRowRequiredPx } from '../../utils/grid-track.js';
29
30
 
30
31
  const props = defineProps({
31
32
  polarvo: {
@@ -43,7 +44,9 @@ const DEFAULT_DISPLAY_SIZE = {
43
44
 
44
45
  // height 관련 여유 공간 추가하여 계산함 (50, 200)
45
46
  const { displaySize, canvasSize } = toRefs(props.polarvo.display.state);
46
- const { elements, activeMode } = toRefs(props.polarvo.layout.state);
47
+ const { elements, activeMode, layoutData, gridRatio } = toRefs(props.polarvo.layout.state);
48
+ const { elements: gridElements } = toRefs(props.polarvo.gridDrop.state);
49
+ const { elements: freeElements } = toRefs(props.polarvo.freeDrop.state);
47
50
 
48
51
  const defaultHeight = computed(() => {
49
52
  const freeElementsBottom =
@@ -134,14 +137,59 @@ const containerStyle = computed(() => {
134
137
 
135
138
  defineExpose({ defaultHeight, additionalHeight, displayHeight });
136
139
 
140
+ // 확장 높이는 BaseLayout/GridLayout에서 마지막 행에만 붙임
141
+ provide('canvasExtraHeight', additionalHeight);
142
+
143
+ const RESIZE_STEP = 200;
144
+ const MIN_ROW_PX = 20; // GridLayout.vue의 MIN_ROW_PX와 동일한 값
145
+
137
146
  function expandContainer() {
138
- additionalHeight.value += 200;
147
+ additionalHeight.value += RESIZE_STEP;
148
+ }
149
+
150
+ // 확장 높이를 받는 마지막 행 섹션들이 요소 최소 크기를 유지하면서 줄어들 수 있는 높이(px)
151
+ // 확장 높이는 마지막 행만 가져가므로 마지막 행 섹션의 여유분이 곧 줄일 수 있는 최대치
152
+ function getShrinkableHeight() {
153
+ let slack = additionalHeight.value;
154
+
155
+ Object.entries(layoutData.value ?? {}).forEach(([key, section]) => {
156
+ if (!isInLastLayoutRow(section, gridRatio.value?.row?.length)) return;
157
+ const sectionEl = document.getElementById(`${key}Layout`);
158
+ if (!sectionEl) return;
159
+
160
+ const modeElements = (section.mode === 'grid' ? gridElements.value : freeElements.value) ?? [];
161
+ const sectionElements = modeElements.filter((el) => el.section === key && section.elementIds?.includes(el.id));
162
+
163
+ if (section.mode === 'grid') {
164
+ const gridRows = section.config?.gridRows ?? 3;
165
+ const trackPx = getComputedStyle(sectionEl)
166
+ .gridTemplateRows.split(' ')
167
+ .map((v) => parseFloat(v));
168
+ const lastTrackPx = trackPx[gridRows - 1] ?? 0;
169
+ const required = getGridLastRowRequiredPx(sectionElements, gridRows, trackPx, section.config?.gridGap ?? 5, MIN_ROW_PX);
170
+ slack = Math.min(slack, lastTrackPx - required);
171
+ } else {
172
+ const bottom = sectionElements.reduce((max, el) => Math.max(max, (el.position?.y || 0) + (el.size?.h || 0)), 0);
173
+ slack = Math.min(slack, sectionEl.clientHeight - bottom);
174
+ }
175
+ });
176
+
177
+ return Math.max(0, Math.floor(slack));
139
178
  }
140
179
 
141
- // displayHeight(=defaultHeight + additionalHeight)는 항상 defaultHeight 이상으로 유지
142
- const canShrink = computed(() => additionalHeight.value > 0);
180
+ // 요소 크기/배치·레이아웃·확장 높이가 바뀔 때마다 DOM 반영 후 다시 계산
181
+ const shrinkableHeight = ref(0);
182
+ const updateShrinkableHeight = () => {
183
+ shrinkableHeight.value = getShrinkableHeight();
184
+ };
185
+ watch([displayHeight, gridElements, freeElements, layoutData, gridRatio], updateShrinkableHeight, { deep: true, flush: 'post' });
186
+ onMounted(updateShrinkableHeight);
187
+
188
+ // displayHeight(=defaultHeight + additionalHeight)는 항상 defaultHeight 이상, 마지막 행 요소 최소 크기 이상으로 유지
189
+ const canShrink = computed(() => shrinkableHeight.value > 0);
143
190
  function shrinkContainer() {
144
- additionalHeight.value = Math.max(0, additionalHeight.value - 200);
191
+ const shrinkable = getShrinkableHeight();
192
+ additionalHeight.value -= Math.min(RESIZE_STEP, shrinkable);
145
193
  }
146
194
  </script>
147
195
 
@@ -74,6 +74,7 @@
74
74
 
75
75
  <script setup>
76
76
  import GridItem from './GridItem.vue';
77
+ import { getGridRowMinPx, toGridTrackSizes, toGridRowSizes } from '../../utils/grid-track.js';
77
78
 
78
79
  import { ref, toRefs, getCurrentInstance, onMounted, onUnmounted, inject, watch, computed } from 'vue';
79
80
 
@@ -98,6 +99,11 @@ const props = defineProps({
98
99
  type: Array,
99
100
  required: true,
100
101
  },
102
+ // 캔버스 확장 높이 - 마지막 행이 추가로 가져가는 px (BaseLayout에서 마지막 행 섹션에만 전달)
103
+ extraHeight: {
104
+ type: Number,
105
+ default: 0,
106
+ },
101
107
  });
102
108
 
103
109
  const { setActiveDesign, setActiveMenu } = props.polarvo.display;
@@ -133,6 +139,11 @@ const DRAG_CONFIG = {
133
139
  clientKey: 'clientY',
134
140
  positionKey: 'y',
135
141
  cssVar: '--grid-row-sizes',
142
+ posKey: 'row',
143
+ spanKey: 'rowSpan',
144
+ getMinPx: (el) => el.size?.h ?? MIN_ROW_PX, // GridDropEngine.getElementStyle의 minHeight와 동일
145
+ extraPx: () => props.extraHeight, // 마지막 행에 붙은 확장 높이 (비율 계산에서 제외)
146
+ toSizes: (ratio) => toGridRowSizes(ratio, getGridRowMinPx(filteredElements.value, ratio.length), gridGap.value, props.extraHeight), // BaseLayout과 동일한 형식
136
147
  },
137
148
  col: {
138
149
  ref: gridColumns,
@@ -142,6 +153,11 @@ const DRAG_CONFIG = {
142
153
  clientKey: 'clientX',
143
154
  positionKey: 'x',
144
155
  cssVar: '--grid-column-sizes',
156
+ posKey: 'col',
157
+ spanKey: 'colSpan',
158
+ getMinPx: () => MIN_COL_PX,
159
+ extraPx: () => 0,
160
+ toSizes: (ratio) => toGridTrackSizes(ratio), // BaseLayout과 동일한 형식
145
161
  },
146
162
  };
147
163
 
@@ -157,7 +173,8 @@ function handleMouseDown(event, axis, index) {
157
173
  event.preventDefault();
158
174
  event.stopPropagation();
159
175
 
160
- const { ref, minPx, ratioKey, gridTemplateKey, clientKey, positionKey, cssVar } = DRAG_CONFIG[axis];
176
+ const { ref, minPx, ratioKey, gridTemplateKey, clientKey, positionKey, cssVar, posKey, spanKey, getMinPx, extraPx, toSizes } =
177
+ DRAG_CONFIG[axis];
161
178
 
162
179
  dragState.value.axis = axis;
163
180
  dragState.value.activeIdx = index;
@@ -207,7 +224,7 @@ function handleMouseDown(event, axis, index) {
207
224
 
208
225
  const configured = props.sectionData?.config?.[ratioKey];
209
226
  const baselineRatio =
210
- (Array.isArray(configured) && configured.length === ref.value) || configured.some((v) => typeof v !== 'number' || !(v > 0))
227
+ Array.isArray(configured) && configured.length === ref.value && configured.every((v) => typeof v === 'number' && v > 0)
211
228
  ? [...configured]
212
229
  : Array(ref.value).fill(1);
213
230
 
@@ -215,24 +232,58 @@ function handleMouseDown(event, axis, index) {
215
232
  [gridTemplateKey].split(' ')
216
233
  .map((v) => parseFloat(v));
217
234
 
218
- // 총 Fr 계산
219
- const totalFr = baselineRatio.reduce((sum, v) => sum + v, 0);
220
- // 총 px 계산
221
- const totalPx = trackPx.reduce((sum, v) => sum + v, 0);
222
- // 1px당 Fr 계산
223
- const frPerPx = totalFr / totalPx;
235
+ const prevIdx = index - 1; // 드래그 행 위쪽 행 인덱스
236
+ const nextIdx = index; // 드래그 행 인덱스
237
+
238
+ // 1px당 Fr 계산 - 드래그로 바뀌는 두 트랙 기준
239
+ // 마지막 트랙의 확장 높이는 비율과 무관하게 붙는 값이라 제외
240
+ const ratioPx = [...trackPx];
241
+ ratioPx[ratioPx.length - 1] -= extraPx();
242
+ const frPerPx = (baselineRatio[prevIdx] + baselineRatio[nextIdx]) / (ratioPx[prevIdx] + ratioPx[nextIdx]);
243
+
244
+ // 요소 최소 크기 때문에 두 트랙이 가져야 하는 최소 px
245
+ const { prevMinPx, nextMinPx } = _getElementMinPx(trackPx, prevIdx, nextIdx);
224
246
 
225
247
  dragSnapShot = {
226
248
  container,
227
249
  baselineRatio,
228
250
  frPerPx,
229
- minFr: minPx * frPerPx, // 최소 Fr 계산
251
+ prevMinFr: Math.max(minPx, prevMinPx) * frPerPx,
252
+ // 마지막 트랙은 확장 높이가 이미 확보돼 있으므로 그만큼 비율 최소값에서 뺌
253
+ nextMinFr: Math.max(minPx, nextMinPx - (trackPx[nextIdx] - ratioPx[nextIdx])) * frPerPx,
230
254
  };
231
255
 
232
256
  document.addEventListener('mousemove', _startDragMove);
233
257
  document.addEventListener('mouseup', _stopDrag);
234
258
  }
235
259
 
260
+ // 드래그로 바뀌는 트랙(prev/next) 중 하나에만 걸친 요소 기준으로, 각 트랙이 가져야 하는 최소 px 계산
261
+ // (두 트랙 모두에 걸친 요소는 합이 그대로라 제약 없음)
262
+ function _getElementMinPx(trackPx, prevIdx, nextIdx) {
263
+ const gap = gridGap.value;
264
+ let prevMinPx = 0;
265
+ let nextMinPx = 0;
266
+
267
+ filteredElements.value.forEach((el) => {
268
+ const start = el.position[posKey] - 1; // 0-based 트랙 인덱스
269
+ const end = start + el.size[spanKey] - 1;
270
+ const coversPrev = start <= prevIdx && prevIdx <= end;
271
+ const coversNext = start <= nextIdx && nextIdx <= end;
272
+ if (coversPrev === coversNext) return;
273
+
274
+ const targetIdx = coversPrev ? prevIdx : nextIdx;
275
+ // 요소가 걸친 나머지 트랙 + gap 만큼은 이미 확보되어 있음
276
+ let otherPx = gap * (end - start);
277
+ for (let t = start; t <= end; t++) if (t !== targetIdx) otherPx += trackPx[t];
278
+
279
+ const required = getMinPx(el) - otherPx;
280
+ if (coversPrev) prevMinPx = Math.max(prevMinPx, required);
281
+ else nextMinPx = Math.max(nextMinPx, required);
282
+ });
283
+
284
+ return { prevMinPx, nextMinPx };
285
+ }
286
+
236
287
  function _startDragMove(e) {
237
288
  if (!dragSnapShot) return;
238
289
  pendingClient = e[clientKey];
@@ -243,16 +294,16 @@ function handleMouseDown(event, axis, index) {
243
294
  rafId = requestAnimationFrame(() => {
244
295
  rafId = null;
245
296
 
246
- const { baselineRatio, frPerPx, minFr, container } = dragSnapShot;
297
+ const { baselineRatio, frPerPx, prevMinFr, nextMinFr, container } = dragSnapShot;
247
298
  const prevIdx = index - 1; // 드래그 행 위쪽 행 인덱스
248
299
  const nextIdx = index; // 드래그 행 인덱스
249
300
 
250
301
  // 순수 이동량
251
302
  let delta = (pendingClient - startPos.value[positionKey]) * frPerPx;
252
- // 위쪽 행 작아지는 것 방지: 가장 커져도 minFr - baselineRatio[prevIdx] (음수값 = 위로 이동)
253
- delta = Math.max(minFr - baselineRatio[prevIdx], delta);
254
- // 아래쪽 행 작아지는 것 방지: 가장 작아져도 minFr (양수값 = 아래로 이동)
255
- delta = Math.min(baselineRatio[nextIdx] - minFr, delta);
303
+ // 위쪽 행 작아지는 것 방지: 가장 커져도 prevMinFr - baselineRatio[prevIdx] (음수값 = 위로 이동)
304
+ delta = Math.max(prevMinFr - baselineRatio[prevIdx], delta);
305
+ // 아래쪽 행 작아지는 것 방지: 가장 작아져도 nextMinFr (양수값 = 아래로 이동)
306
+ delta = Math.min(baselineRatio[nextIdx] - nextMinFr, delta);
256
307
 
257
308
  // 미리보기용
258
309
  const preview = [...baselineRatio];
@@ -262,7 +313,7 @@ function handleMouseDown(event, axis, index) {
262
313
  liveRatio = preview;
263
314
 
264
315
  // 화면에만 즉시 반영
265
- container.style.setProperty(cssVar, preview.map((v) => `${v}fr`).join(' '));
316
+ container.style.setProperty(cssVar, toSizes(preview));
266
317
  });
267
318
  }
268
319
  function _stopDrag() {
@@ -17,9 +17,9 @@ const props = defineProps({
17
17
  <style scoped>
18
18
  .polar-grid {
19
19
  display: grid;
20
- grid-template-columns: var(--grid-column-sizes, repeat(var(--grid-columns, 3), 1fr));
20
+ grid-template-columns: var(--grid-column-sizes, repeat(var(--grid-columns, 3), minmax(0, 1fr)));
21
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
+ grid-template-rows: var(--grid-row-sizes, repeat(var(--grid-rows, 3), minmax(0, 1fr)));
23
23
  gap: var(--grid-gap, 5px);
24
24
  width: 100%;
25
25
  height: 100%;
package/src/index.js CHANGED
@@ -12,7 +12,7 @@ import createPolarvoLayout from './library/index.js';
12
12
  // import LayoutFastMenu from './components/FastMenu/LayoutFastMenu.vue';
13
13
  // import DesignFastMenu from './components/FastMenu/DesignFastMenu.vue';
14
14
 
15
- import ElementSidebar from './components/SideBar/ElementSidebar.vue';
15
+ // import ElementSidebar from './components/SideBar/ElementSidebar.vue';
16
16
  import LayoutSideBar from './components/SideBar/LayoutSideBar.vue';
17
17
 
18
18
  import LayoutMiniMenu from './components/MiniMenu/LayoutMiniMenu.vue';
@@ -29,7 +29,7 @@ export {
29
29
  // ActionFastMenu,
30
30
  // LayoutFastMenu,
31
31
  // DesignFastMenu,
32
- ElementSidebar,
32
+ // ElementSidebar,
33
33
  LayoutSideBar,
34
34
  LayoutMiniMenu,
35
35
  CanvasContainer,
@@ -0,0 +1,52 @@
1
+ // 그리드 행 트랙별 최소 높이(px) - 해당 행 하나만 차지하는 요소의 size.h 중 최대값
2
+ // (여러 행에 걸친 요소는 GridLayout 행 크기 조절 시 별도로 제한)
3
+ export function getGridRowMinPx(elements, gridRows) {
4
+ const mins = Array(gridRows).fill(0);
5
+ elements.forEach((el) => {
6
+ const { row } = el.position;
7
+ if (el.size.rowSpan !== 1 || row < 1 || row > gridRows) return;
8
+ mins[row - 1] = Math.max(mins[row - 1], el.size.h ?? 0);
9
+ });
10
+ return mins;
11
+ }
12
+
13
+ // fr 비율 → grid-template 값
14
+ // min을 auto(콘텐츠 크기) 대신 명시값으로 고정 - 콘텐츠 크기가 트랙을 고정하면 남은 공간이 다른 트랙으로 재분배됨
15
+ export function toGridTrackSizes(ratio, minPx = []) {
16
+ return ratio.map((r, i) => `minmax(${minPx[i] ?? 0}px, ${r}fr)`).join(' ');
17
+ }
18
+
19
+ // fr 비율 → grid-template-rows 값 (마지막 행만 추가 높이를 가져가도록)
20
+ // 마지막 행을 제외한 행은 (컨테이너 높이 - 추가 높이 - gap 합) 기준 비율로 고정하고, 남는 높이는 마지막 행(1fr)이 전부 가져감
21
+ // → 캔버스 확장(CanvasContainer.additionalHeight) 시 전체 행에 나눠지지 않고 마지막 행만 늘어남
22
+ export function toGridRowSizes(ratio, minPx = [], gap = 0, extraPx = 0) {
23
+ const sum = ratio.reduce((acc, r) => acc + r, 0) || 1;
24
+ const fixedPx = extraPx + gap * (ratio.length - 1);
25
+ const toMin = (i) => (typeof minPx[i] === 'string' ? minPx[i] : `${minPx[i] ?? 0}px`);
26
+
27
+ return ratio
28
+ .map((r, i) => (i === ratio.length - 1 ? `minmax(${toMin(i)}, 1fr)` : `minmax(${toMin(i)}, calc((100% - ${fixedPx}px) * ${r / sum}))`))
29
+ .join(' ');
30
+ }
31
+
32
+ // 섹션이 BaseLayout의 마지막 행에 걸쳐 있는지 (layoutPosition.row는 0-based) - 캔버스 추가 높이를 받는 섹션
33
+ export function isInLastLayoutRow(section, layoutRowCount) {
34
+ const { row = 0, rowSpan = 1 } = section?.layoutPosition ?? {};
35
+ return row + rowSpan >= (layoutRowCount || 1);
36
+ }
37
+
38
+ // 그리드 섹션 마지막 행이 가져야 하는 최소 높이(px) - 빈 셀/요소 minHeight(GridDropEngine.getElementStyle) 기준
39
+ // 여러 행에 걸친 요소는 나머지 행(trackPx) + gap 만큼은 이미 확보된 것으로 봄
40
+ export function getGridLastRowRequiredPx(elements, gridRows, trackPx, gap, minRowPx) {
41
+ let required = minRowPx;
42
+ elements.forEach((el) => {
43
+ const { row } = el.position;
44
+ const { rowSpan } = el.size;
45
+ if (row + rowSpan - 1 !== gridRows) return;
46
+
47
+ let otherPx = gap * (rowSpan - 1);
48
+ for (let t = row - 1; t < gridRows - 1; t++) otherPx += trackPx[t] ?? 0;
49
+ required = Math.max(required, (el.size.h ?? minRowPx) - otherPx);
50
+ });
51
+ return required;
52
+ }