This commit is contained in:
@@ -622,18 +622,19 @@ function LineGridView({
|
||||
[layout, blocks, canvas.baseHeight],
|
||||
);
|
||||
|
||||
// wrapper 실제 크기 측정 — ResizeObserver 로 반응형 추적
|
||||
// wrapper 실제 세로 크기 측정 — ResizeObserver 로 반응형 추적
|
||||
const wrapperRef = useRef<HTMLDivElement>(null);
|
||||
const [containerH, setContainerH] = useState<number | null>(null);
|
||||
const [containerW, setContainerW] = useState<number | null>(null);
|
||||
const [containerSize, setContainerSize] = useState<{
|
||||
width: number;
|
||||
height: number;
|
||||
} | null>(null);
|
||||
useLayoutEffect(() => {
|
||||
const el = wrapperRef.current;
|
||||
if (!el) return;
|
||||
const update = () => {
|
||||
const h = el.clientHeight;
|
||||
const w = el.clientWidth;
|
||||
if (h > 0) setContainerH(h);
|
||||
if (w > 0) setContainerW(w);
|
||||
const width = el.clientWidth;
|
||||
const height = el.clientHeight;
|
||||
if (width > 0 && height > 0) setContainerSize({ width, height });
|
||||
};
|
||||
update();
|
||||
const ro = new ResizeObserver(update);
|
||||
@@ -644,15 +645,40 @@ function LineGridView({
|
||||
// finalPx: containerH 측정 전이면 preferred 그대로(첫 프레임 fallback).
|
||||
// 측정된 뒤엔 available 에 정확히 맞도록 재분배.
|
||||
const finalPx = useMemo(() => {
|
||||
if (containerH == null) return plans.map((p) => p.preferredPx);
|
||||
return allocateRowHeightsPx(plans, containerH);
|
||||
}, [plans, containerH]);
|
||||
if (containerSize == null) return plans.map((p) => p.preferredPx);
|
||||
return allocateRowHeightsPx(plans, containerSize.height);
|
||||
}, [plans, containerSize]);
|
||||
|
||||
const gridTemplateRows =
|
||||
aspectPolicy === 'free'
|
||||
? layout.rows.map(() => 'auto').join(' ')
|
||||
: finalPx.map((px) => `${px}px`).join(' ');
|
||||
|
||||
const runtimeSizeById = useMemo(() => {
|
||||
const m = new Map<string, { width: number; height: number }>();
|
||||
const containerW = containerSize?.width ?? 0;
|
||||
if (containerW <= 0) return m;
|
||||
|
||||
const colWidthsPx = layout.cols.map((c) => c.weight * containerW);
|
||||
|
||||
for (const bl of layout.blocks) {
|
||||
if (bl.mode !== 'grid') continue;
|
||||
const colStart = Math.max(1, bl.colStart ?? 1) - 1;
|
||||
const colEnd = Math.max(colStart, (bl.colEnd ?? 1) - 1);
|
||||
const rowStart = Math.max(1, bl.rowStart ?? 1) - 1;
|
||||
const rowEnd = Math.max(rowStart, (bl.rowEnd ?? 1) - 1);
|
||||
|
||||
let width = 0;
|
||||
for (let i = colStart; i < colEnd; i++) width += colWidthsPx[i] ?? 0;
|
||||
|
||||
let height = 0;
|
||||
for (let i = rowStart; i < rowEnd; i++) height += finalPx[i] ?? 0;
|
||||
|
||||
m.set(bl.blockId, { width, height });
|
||||
}
|
||||
return m;
|
||||
}, [layout, containerSize, finalPx]);
|
||||
|
||||
// 각 블록 cell 의 final 높이가 preferred 대비 많이 줄어든 경우 compact 클래스.
|
||||
// 기준: finalH < preferredH * 0.9 (10% 이상 축소)
|
||||
const blockCompact = useMemo(() => {
|
||||
@@ -672,152 +698,6 @@ function LineGridView({
|
||||
return m;
|
||||
}, [layout, byId, finalPx, canvas.baseHeight]);
|
||||
|
||||
/**
|
||||
* tight-fit button compaction — 충돌 직전 박스 자체 축소.
|
||||
*
|
||||
* 1) row 단위 그룹 계산 (같은 rowStart 의 button 블록 묶음)
|
||||
* groupW = Σ colWeight (group bbox) × containerW
|
||||
* required = Σ designW + MIN_GAP*(n-1) + OUTER_INSET*2
|
||||
* groupScaleX = groupW / required (cap 1)
|
||||
* 2) 각 cell overflow 방지
|
||||
* eachScaleX = (cellW - 2*CELL_INSET) / designW
|
||||
* 3) text-aware floor — 라벨/아이콘/최소 padding 을 감당할 최소 px 까지 축소
|
||||
* minWidthPx = textW + iconW + 2*MIN_PAD_X + safety (하한 80px)
|
||||
* floorScaleX = minWidthPx / designW
|
||||
* scaleX = min(1, max(floorScaleX, min(groupScaleX, eachScaleX)))
|
||||
* 4) scaleY — row 의 final px 가 designH 보다 작을 때만
|
||||
*
|
||||
* 짧은 라벨("수정","저장")은 80px 근처까지, 긴 라벨은 라벨 길이에 따라
|
||||
* 더 큰 하한을 가진다. 결과는 BlockRenderer 가 실제 width/height/padding
|
||||
* px 로 반영.
|
||||
*/
|
||||
const buttonScales = useMemo(() => {
|
||||
const m = new Map<string, { x: number; y: number }>();
|
||||
if (containerW == null || containerW <= 0) return m;
|
||||
const MIN_GAP = 8;
|
||||
const OUTER_INSET = 6;
|
||||
const CELL_INSET = 4;
|
||||
const MIN_SCALE_Y = 0.8;
|
||||
|
||||
// tight 상태에서 버튼이 라벨을 감당할 최소 px 폭.
|
||||
// textW : 글자수 × fontSize × factor (한/영 평균 근사)
|
||||
// iconW : 아이콘 + gap (아이콘 있을 때)
|
||||
// padding-inline 최소 8px × 2 + safety 6px
|
||||
// 절대 floor 80px — 빈/극히 짧은 라벨도 최소 80 확보
|
||||
const computeMinWidthPx = (label: string, hasIcon: boolean): number => {
|
||||
const len = (label ?? '').toString().length;
|
||||
const textW = Math.max(12, len * 13 * 1.05);
|
||||
const iconW = hasIcon ? 16 + 6 : 0;
|
||||
const MIN_PAD_X = 8;
|
||||
const SAFETY = 6;
|
||||
return Math.max(80, textW + iconW + 2 * MIN_PAD_X + SAFETY);
|
||||
};
|
||||
|
||||
type ButtonInfo = {
|
||||
id: string;
|
||||
colStartIdx: number;
|
||||
colEndIdx: number;
|
||||
rowStartIdx: number;
|
||||
rowEndIdx: number;
|
||||
cellW: number;
|
||||
designW: number;
|
||||
designH: number;
|
||||
minWidthPx: number;
|
||||
};
|
||||
|
||||
// row 별 button 수집
|
||||
const byRowStart = new Map<number, ButtonInfo[]>();
|
||||
for (const bl of layout.blocks) {
|
||||
if (bl.mode !== 'grid') continue;
|
||||
const b = byId.get(bl.blockId);
|
||||
if (!b || b.componentId !== 'button') continue;
|
||||
const cs = (bl.colStart ?? 1) - 1;
|
||||
const ce = (bl.colEnd ?? 1) - 1;
|
||||
const rs = (bl.rowStart ?? 1) - 1;
|
||||
const re = (bl.rowEnd ?? 1) - 1;
|
||||
if (ce <= cs || re <= rs) continue;
|
||||
let cellWeight = 0;
|
||||
for (let i = cs; i < ce; i++) cellWeight += layout.cols[i]?.weight ?? 0;
|
||||
const cellW = containerW * cellWeight;
|
||||
const cfg = (b.config ?? {}) as Record<string, any>;
|
||||
const label = typeof cfg.text === 'string' ? cfg.text : '';
|
||||
const hasIcon = typeof cfg.icon === 'string' && cfg.icon.length > 0;
|
||||
const info: ButtonInfo = {
|
||||
id: bl.blockId,
|
||||
colStartIdx: cs,
|
||||
colEndIdx: ce,
|
||||
rowStartIdx: rs,
|
||||
rowEndIdx: re,
|
||||
cellW,
|
||||
designW: b.wPct * canvas.baseWidth,
|
||||
designH: b.hPct * canvas.baseHeight,
|
||||
minWidthPx: computeMinWidthPx(label, hasIcon),
|
||||
};
|
||||
const list = byRowStart.get(rs);
|
||||
if (list) list.push(info);
|
||||
else byRowStart.set(rs, [info]);
|
||||
}
|
||||
|
||||
for (const buttons of byRowStart.values()) {
|
||||
if (buttons.length === 0) continue;
|
||||
let minColStart = Number.POSITIVE_INFINITY;
|
||||
let maxColEnd = Number.NEGATIVE_INFINITY;
|
||||
for (const b of buttons) {
|
||||
if (b.colStartIdx < minColStart) minColStart = b.colStartIdx;
|
||||
if (b.colEndIdx > maxColEnd) maxColEnd = b.colEndIdx;
|
||||
}
|
||||
let groupWeightSum = 0;
|
||||
for (let i = minColStart; i < maxColEnd; i++) {
|
||||
groupWeightSum += layout.cols[i]?.weight ?? 0;
|
||||
}
|
||||
const groupW = containerW * groupWeightSum;
|
||||
|
||||
const n = buttons.length;
|
||||
const requiredW =
|
||||
buttons.reduce((s, b) => s + b.designW, 0) +
|
||||
MIN_GAP * Math.max(0, n - 1) +
|
||||
OUTER_INSET * 2;
|
||||
|
||||
const groupScaleX =
|
||||
groupW > 0 && requiredW > 0 ? Math.min(1, groupW / requiredW) : 1;
|
||||
|
||||
for (const b of buttons) {
|
||||
// 각 cell overflow 방지
|
||||
const eachScaleX =
|
||||
b.cellW > 0 && b.designW > 0
|
||||
? Math.min(1, (b.cellW - 2 * CELL_INSET) / b.designW)
|
||||
: 1;
|
||||
// text-aware floor — 버튼별 최소폭으로 clamp
|
||||
const floorX = b.designW > 0 ? b.minWidthPx / b.designW : 0;
|
||||
const rawScaleX = Math.min(groupScaleX, eachScaleX);
|
||||
let scaleX = Math.max(floorX, rawScaleX);
|
||||
scaleX = Math.min(1, scaleX);
|
||||
|
||||
// scaleY — row finalPx 합이 designH 보다 작아질 때만
|
||||
let cellH = 0;
|
||||
for (let i = b.rowStartIdx; i < b.rowEndIdx; i++) {
|
||||
cellH += finalPx[i] ?? 0;
|
||||
}
|
||||
let scaleY = 1;
|
||||
if (b.designH > 0 && cellH > 0 && cellH < b.designH) {
|
||||
scaleY = Math.max(MIN_SCALE_Y, cellH / b.designH);
|
||||
}
|
||||
|
||||
if (scaleX < 1 || scaleY < 1) {
|
||||
m.set(b.id, { x: scaleX, y: scaleY });
|
||||
}
|
||||
}
|
||||
}
|
||||
return m;
|
||||
}, [
|
||||
layout,
|
||||
byId,
|
||||
containerW,
|
||||
canvas.baseWidth,
|
||||
canvas.baseHeight,
|
||||
finalPx,
|
||||
]);
|
||||
|
||||
// 디버그 — row type / preferred / final / container / deficit·surplus
|
||||
useEffect(() => {
|
||||
if (typeof window === 'undefined' || blocks.length === 0) return;
|
||||
@@ -829,7 +709,7 @@ function LineGridView({
|
||||
if (!enabled) return;
|
||||
const totalPref = plans.reduce((s, p) => s + p.preferredPx, 0);
|
||||
const totalFinal = finalPx.reduce((s, v) => s + v, 0);
|
||||
const ch = containerH ?? 0;
|
||||
const ch = containerSize?.height ?? 0;
|
||||
const delta = ch > 0 ? ch - totalPref : 0;
|
||||
/* eslint-disable no-console */
|
||||
console.log(
|
||||
@@ -858,7 +738,7 @@ function LineGridView({
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
}, [plans, finalPx, containerH, gridTemplateRows, blocks.length]);
|
||||
}, [plans, finalPx, containerSize, gridTemplateRows, blocks.length]);
|
||||
|
||||
// 디버그 덤프 — 엔진/블록수/grid/overlay/cols/rows/tolerance + 블록별 box
|
||||
if (typeof window !== 'undefined' && blocks.length > 0) {
|
||||
@@ -916,11 +796,6 @@ function LineGridView({
|
||||
const block = byId.get(bl.blockId);
|
||||
if (!block) return null;
|
||||
const compact = blockCompact.get(block.id) ? ' compact' : '';
|
||||
const btnScale = buttonScales.get(block.id);
|
||||
const sx = btnScale?.x ?? 1;
|
||||
const sy = btnScale?.y ?? 1;
|
||||
const dataScale =
|
||||
btnScale ? `${sx.toFixed(3)}/${sy.toFixed(3)}` : undefined;
|
||||
if (bl.mode === 'grid') {
|
||||
return (
|
||||
<div
|
||||
@@ -929,7 +804,6 @@ function LineGridView({
|
||||
data-comp={block.componentId}
|
||||
data-col={`${bl.colStart}/${bl.colEnd}`}
|
||||
data-row={`${bl.rowStart}/${bl.rowEnd}`}
|
||||
data-btn-scale={dataScale}
|
||||
style={{
|
||||
gridColumn: `${bl.colStart} / ${bl.colEnd}`,
|
||||
gridRow: `${bl.rowStart} / ${bl.rowEnd}`,
|
||||
@@ -940,8 +814,7 @@ function LineGridView({
|
||||
context={context}
|
||||
view={view}
|
||||
canvas={canvas}
|
||||
compactScaleX={sx}
|
||||
compactScaleY={sy}
|
||||
runtimeSize={runtimeSizeById.get(block.id)}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
@@ -952,7 +825,6 @@ function LineGridView({
|
||||
className={`itpl-overlay role-${block.role} policy-${block.responsivePolicy}${compact}`}
|
||||
data-comp={block.componentId}
|
||||
data-overlay-reason={bl.overlayReason}
|
||||
data-btn-scale={dataScale}
|
||||
style={{
|
||||
left: `${(bl.leftPct ?? 0) * 100}%`,
|
||||
top: `${(bl.topPct ?? 0) * 100}%`,
|
||||
@@ -965,8 +837,6 @@ function LineGridView({
|
||||
context={context}
|
||||
view={view}
|
||||
canvas={canvas}
|
||||
compactScaleX={sx}
|
||||
compactScaleY={sy}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
@@ -1124,8 +994,7 @@ function BlockRenderer({
|
||||
context,
|
||||
view,
|
||||
canvas,
|
||||
compactScaleX,
|
||||
compactScaleY,
|
||||
runtimeSize,
|
||||
}: {
|
||||
block: BlockV2;
|
||||
context: TemplateRenderContext;
|
||||
@@ -1135,13 +1004,10 @@ function BlockRenderer({
|
||||
* line 경로에서만 넘어오고, band 경로는 기존 동작(size 0) 유지 — 회귀 방지.
|
||||
*/
|
||||
canvas?: CanvasV2;
|
||||
/**
|
||||
* tight-fit compaction — button 전용. 가로/세로 독립 scale.
|
||||
* 1 = 디자인 크기 그대로, < 1 이면 해당 축의 width/padding 을 실제 px 축소.
|
||||
* transform scale 은 사용하지 않는다 — 실제 box 가 줄어야 cell 안에 들어온다.
|
||||
*/
|
||||
compactScaleX?: number;
|
||||
compactScaleY?: number;
|
||||
runtimeSize?: {
|
||||
width: number;
|
||||
height: number;
|
||||
};
|
||||
}) {
|
||||
const def = ComponentRegistry.getComponent(block.componentId);
|
||||
if (!def?.component) {
|
||||
@@ -1160,39 +1026,32 @@ function BlockRenderer({
|
||||
|
||||
const bw = canvas?.baseWidth ?? 0;
|
||||
const bh = canvas?.baseHeight ?? 0;
|
||||
const sx =
|
||||
compactScaleX && compactScaleX > 0 && compactScaleX < 1 ? compactScaleX : 1;
|
||||
const sy =
|
||||
compactScaleY && compactScaleY > 0 && compactScaleY < 1 ? compactScaleY : 1;
|
||||
const position = {
|
||||
x: bw > 0 ? block.xPct * bw : 0,
|
||||
y: bh > 0 ? block.yPct * bh : 0,
|
||||
z: 1,
|
||||
};
|
||||
// 가로/세로 각 축 독립 scale. ButtonComponent 의 useDesignPx 가 이 값을
|
||||
// 그대로 width/height/minHeight 로 박아 실제 박스 크기로 반영한다.
|
||||
const size = {
|
||||
width: bw > 0 ? block.wPct * bw * sx : 0,
|
||||
height: bh > 0 ? block.hPct * bh * sy : 0,
|
||||
width: bw > 0 ? block.wPct * bw : 0,
|
||||
height: bh > 0 ? block.hPct * bh : 0,
|
||||
};
|
||||
|
||||
// button 의 padding 축소 — margin 은 사용하지 않는다. 박스 자체를 줄인다.
|
||||
// padding-inline : scaleX 비례
|
||||
// padding-block : scaleY 가 1 미만인 드문 경우에만 축소
|
||||
// font-size : 유지
|
||||
const componentStyle: React.CSSProperties = {};
|
||||
if (block.componentId === 'button') {
|
||||
if (sx < 1) {
|
||||
const padX = Math.max(4, Math.round(14 * sx));
|
||||
componentStyle.paddingLeft = `${padX}px`;
|
||||
componentStyle.paddingRight = `${padX}px`;
|
||||
}
|
||||
if (sy < 1) {
|
||||
const padY = Math.max(2, Math.round(6 * sy));
|
||||
componentStyle.paddingTop = `${padY}px`;
|
||||
componentStyle.paddingBottom = `${padY}px`;
|
||||
}
|
||||
}
|
||||
const isButtonLike =
|
||||
block.componentId === 'button' ||
|
||||
block.componentId === 'button-bar' ||
|
||||
block.componentId === 'pagination';
|
||||
const effectiveSize =
|
||||
isButtonLike && runtimeSize
|
||||
? {
|
||||
width:
|
||||
runtimeSize.width > 0
|
||||
? Math.min(size.width, Math.max(0, runtimeSize.width - 2))
|
||||
: size.width,
|
||||
height:
|
||||
runtimeSize.height > 0
|
||||
? Math.min(size.height, Math.max(0, runtimeSize.height - 2))
|
||||
: size.height,
|
||||
}
|
||||
: size;
|
||||
|
||||
return (
|
||||
<Cmp
|
||||
@@ -1200,7 +1059,7 @@ function BlockRenderer({
|
||||
id: block.id,
|
||||
componentType: block.componentId,
|
||||
position,
|
||||
size,
|
||||
size: effectiveSize,
|
||||
componentConfig: block.config,
|
||||
component_config: block.config,
|
||||
style: {},
|
||||
@@ -1214,7 +1073,6 @@ function BlockRenderer({
|
||||
context.onFormRowChange?.({ [fieldName]: value })
|
||||
}
|
||||
view={view}
|
||||
style={componentStyle}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user