화면 컴포넌트 위치문제 수정
This commit is contained in:
parent
03bce9d643
commit
5b79bfb19d
|
|
@ -1811,8 +1811,9 @@ export default function ScreenDesigner({ selectedScreen, onBackToList }: ScreenD
|
|||
const rect = canvasRef.current?.getBoundingClientRect();
|
||||
if (!rect) return;
|
||||
|
||||
const dropX = e.clientX - rect.left;
|
||||
const dropY = e.clientY - rect.top;
|
||||
// 🔥 중요: 줌 레벨을 고려한 마우스 위치 계산
|
||||
const dropX = (e.clientX - rect.left) / zoomLevel;
|
||||
const dropY = (e.clientY - rect.top) / zoomLevel;
|
||||
|
||||
// 현재 해상도에 맞는 격자 정보 계산
|
||||
const currentGridInfo = layout.gridSettings
|
||||
|
|
@ -1830,9 +1831,11 @@ export default function ScreenDesigner({ selectedScreen, onBackToList }: ScreenD
|
|||
? snapToGrid({ x: dropX, y: dropY, z: 1 }, currentGridInfo, layout.gridSettings as GridUtilSettings)
|
||||
: { x: dropX, y: dropY, z: 1 };
|
||||
|
||||
console.log("🏗️ 레이아웃 드롭:", {
|
||||
console.log("🏗️ 레이아웃 드롭 (줌 보정):", {
|
||||
zoomLevel,
|
||||
layoutType: layoutData.layoutType,
|
||||
zonesCount: layoutData.zones.length,
|
||||
mouseRaw: { x: e.clientX - rect.left, y: e.clientY - rect.top },
|
||||
dropPosition: { x: dropX, y: dropY },
|
||||
snappedPosition,
|
||||
});
|
||||
|
|
@ -1869,7 +1872,7 @@ export default function ScreenDesigner({ selectedScreen, onBackToList }: ScreenD
|
|||
|
||||
toast.success(`${layoutData.label} 레이아웃이 추가되었습니다.`);
|
||||
},
|
||||
[layout, gridInfo, screenResolution, snapToGrid, saveToHistory],
|
||||
[layout, gridInfo, screenResolution, snapToGrid, saveToHistory, zoomLevel],
|
||||
);
|
||||
|
||||
// handleZoneComponentDrop은 handleComponentDrop으로 대체됨
|
||||
|
|
@ -1954,32 +1957,47 @@ export default function ScreenDesigner({ selectedScreen, onBackToList }: ScreenD
|
|||
const componentWidth = component.defaultSize?.width || 120;
|
||||
const componentHeight = component.defaultSize?.height || 36;
|
||||
|
||||
// 방법 1: 마우스 포인터를 컴포넌트 중심으로 (현재 방식)
|
||||
const dropX_centered = e.clientX - rect.left - componentWidth / 2;
|
||||
const dropY_centered = e.clientY - rect.top - componentHeight / 2;
|
||||
// 🔥 중요: 줌 레벨과 transform-origin을 고려한 마우스 위치 계산
|
||||
// 1. 캔버스가 scale() 변환되어 있음 (transform-origin: top center)
|
||||
// 2. 캔버스가 justify-center로 중앙 정렬되어 있음
|
||||
|
||||
// 실제 캔버스 논리적 크기
|
||||
const canvasLogicalWidth = screenResolution.width;
|
||||
|
||||
// 화면상 캔버스 실제 크기 (스케일 적용 후)
|
||||
const canvasVisualWidth = canvasLogicalWidth * zoomLevel;
|
||||
|
||||
// 중앙 정렬로 인한 왼쪽 오프셋 계산
|
||||
// rect.left는 이미 중앙 정렬된 위치를 반영하고 있음
|
||||
|
||||
// 마우스의 캔버스 내 상대 위치 (스케일 보정)
|
||||
const mouseXInCanvas = (e.clientX - rect.left) / zoomLevel;
|
||||
const mouseYInCanvas = (e.clientY - rect.top) / zoomLevel;
|
||||
|
||||
// 방법 2: 마우스 포인터를 컴포넌트 좌상단으로 (사용자가 원할 수도 있는 방식)
|
||||
const dropX_topleft = e.clientX - rect.left;
|
||||
const dropY_topleft = e.clientY - rect.top;
|
||||
// 방법 1: 마우스 포인터를 컴포넌트 중심으로
|
||||
const dropX_centered = mouseXInCanvas - componentWidth / 2;
|
||||
const dropY_centered = mouseYInCanvas - componentHeight / 2;
|
||||
|
||||
// 방법 2: 마우스 포인터를 컴포넌트 좌상단으로
|
||||
const dropX_topleft = mouseXInCanvas;
|
||||
const dropY_topleft = mouseYInCanvas;
|
||||
|
||||
// 사용자가 원하는 방식으로 변경: 마우스 포인터가 좌상단에 오도록
|
||||
const dropX = dropX_topleft;
|
||||
const dropY = dropY_topleft;
|
||||
|
||||
console.log("🎯 위치 계산 디버깅:", {
|
||||
"1. 마우스 위치": { clientX: e.clientX, clientY: e.clientY },
|
||||
"2. 캔버스 위치": { left: rect.left, top: rect.top, width: rect.width, height: rect.height },
|
||||
"3. 캔버스 내 상대 위치": { x: e.clientX - rect.left, y: e.clientY - rect.top },
|
||||
"4. 컴포넌트 크기": { width: componentWidth, height: componentHeight },
|
||||
"5a. 중심 방식 좌상단": { x: dropX_centered, y: dropY_centered },
|
||||
"5b. 좌상단 방식": { x: dropX_topleft, y: dropY_topleft },
|
||||
"6. 선택된 방식": { dropX, dropY },
|
||||
"7. 예상 컴포넌트 중심": { x: dropX + componentWidth / 2, y: dropY + componentHeight / 2 },
|
||||
"8. 마우스와 중심 일치 확인": {
|
||||
match:
|
||||
Math.abs(dropX + componentWidth / 2 - (e.clientX - rect.left)) < 1 &&
|
||||
Math.abs(dropY + componentHeight / 2 - (e.clientY - rect.top)) < 1,
|
||||
},
|
||||
console.log("🎯 위치 계산 디버깅 (줌 레벨 + 중앙정렬 반영):", {
|
||||
"1. 줌 레벨": zoomLevel,
|
||||
"2. 마우스 위치 (화면)": { clientX: e.clientX, clientY: e.clientY },
|
||||
"3. 캔버스 위치 (rect)": { left: rect.left, top: rect.top, width: rect.width, height: rect.height },
|
||||
"4. 캔버스 논리적 크기": { width: canvasLogicalWidth, height: screenResolution.height },
|
||||
"5. 캔버스 시각적 크기": { width: canvasVisualWidth, height: screenResolution.height * zoomLevel },
|
||||
"6. 마우스 캔버스 내 상대위치 (줌 전)": { x: e.clientX - rect.left, y: e.clientY - rect.top },
|
||||
"7. 마우스 캔버스 내 상대위치 (줌 보정)": { x: mouseXInCanvas, y: mouseYInCanvas },
|
||||
"8. 컴포넌트 크기": { width: componentWidth, height: componentHeight },
|
||||
"9a. 중심 방식": { x: dropX_centered, y: dropY_centered },
|
||||
"9b. 좌상단 방식": { x: dropX_topleft, y: dropY_topleft },
|
||||
"10. 최종 선택": { dropX, dropY },
|
||||
});
|
||||
|
||||
// 현재 해상도에 맞는 격자 정보 계산
|
||||
|
|
@ -2826,7 +2844,7 @@ export default function ScreenDesigner({ selectedScreen, onBackToList }: ScreenD
|
|||
|
||||
// 컴포넌트 드래그 시작
|
||||
const startComponentDrag = useCallback(
|
||||
(component: ComponentData, event: React.MouseEvent) => {
|
||||
(component: ComponentData, event: React.MouseEvent | React.DragEvent) => {
|
||||
event.preventDefault();
|
||||
const rect = canvasRef.current?.getBoundingClientRect();
|
||||
if (!rect) return;
|
||||
|
|
@ -2839,9 +2857,10 @@ export default function ScreenDesigner({ selectedScreen, onBackToList }: ScreenD
|
|||
}));
|
||||
}
|
||||
|
||||
// 캔버스 내부의 상대 좌표 계산 (스크롤 없는 고정 캔버스)
|
||||
const relativeMouseX = event.clientX - rect.left;
|
||||
const relativeMouseY = event.clientY - rect.top;
|
||||
// 🔥 중요: 줌 레벨을 고려한 마우스 위치 계산
|
||||
// 캔버스가 scale() 변환되어 있기 때문에 마우스 위치도 역변환 필요
|
||||
const relativeMouseX = (event.clientX - rect.left) / zoomLevel;
|
||||
const relativeMouseY = (event.clientY - rect.top) / zoomLevel;
|
||||
|
||||
// 다중 선택된 컴포넌트들 확인
|
||||
const isDraggedComponentSelected = groupState.selectedComponents.includes(component.id);
|
||||
|
|
@ -2866,13 +2885,14 @@ export default function ScreenDesigner({ selectedScreen, onBackToList }: ScreenD
|
|||
}
|
||||
|
||||
// console.log("드래그 시작:", component.id, "이동할 컴포넌트 수:", componentsToMove.length);
|
||||
console.log("마우스 위치:", {
|
||||
console.log("마우스 위치 (줌 보정):", {
|
||||
zoomLevel,
|
||||
clientX: event.clientX,
|
||||
clientY: event.clientY,
|
||||
rectLeft: rect.left,
|
||||
rectTop: rect.top,
|
||||
relativeX: relativeMouseX,
|
||||
relativeY: relativeMouseY,
|
||||
mouseRaw: { x: event.clientX - rect.left, y: event.clientY - rect.top },
|
||||
mouseZoomCorrected: { x: relativeMouseX, y: relativeMouseY },
|
||||
componentX: component.position.x,
|
||||
componentY: component.position.y,
|
||||
grabOffsetX: relativeMouseX - component.position.x,
|
||||
|
|
@ -2906,7 +2926,7 @@ export default function ScreenDesigner({ selectedScreen, onBackToList }: ScreenD
|
|||
justFinishedDrag: false,
|
||||
});
|
||||
},
|
||||
[groupState.selectedComponents, layout.components, dragState.justFinishedDrag],
|
||||
[groupState.selectedComponents, layout.components, dragState.justFinishedDrag, zoomLevel],
|
||||
);
|
||||
|
||||
// 드래그 중 위치 업데이트 (성능 최적화 + 실시간 업데이트)
|
||||
|
|
@ -2916,9 +2936,10 @@ export default function ScreenDesigner({ selectedScreen, onBackToList }: ScreenD
|
|||
|
||||
const rect = canvasRef.current.getBoundingClientRect();
|
||||
|
||||
// 캔버스 내부의 상대 좌표 계산 (스크롤 없는 고정 캔버스)
|
||||
const relativeMouseX = event.clientX - rect.left;
|
||||
const relativeMouseY = event.clientY - rect.top;
|
||||
// 🔥 중요: 줌 레벨을 고려한 마우스 위치 계산
|
||||
// 캔버스가 scale() 변환되어 있기 때문에 마우스 위치도 역변환 필요
|
||||
const relativeMouseX = (event.clientX - rect.left) / zoomLevel;
|
||||
const relativeMouseY = (event.clientY - rect.top) / zoomLevel;
|
||||
|
||||
// 컴포넌트 크기 가져오기
|
||||
const draggedComp = layout.components.find((c) => c.id === dragState.draggedComponent.id);
|
||||
|
|
@ -2936,8 +2957,11 @@ export default function ScreenDesigner({ selectedScreen, onBackToList }: ScreenD
|
|||
};
|
||||
|
||||
// 드래그 상태 업데이트
|
||||
console.log("🔥 ScreenDesigner updateDragPosition:", {
|
||||
console.log("🔥 ScreenDesigner updateDragPosition (줌 보정):", {
|
||||
zoomLevel,
|
||||
draggedComponentId: dragState.draggedComponent.id,
|
||||
mouseRaw: { x: event.clientX - rect.left, y: event.clientY - rect.top },
|
||||
mouseZoomCorrected: { x: relativeMouseX, y: relativeMouseY },
|
||||
oldPosition: dragState.currentPosition,
|
||||
newPosition: newPosition,
|
||||
});
|
||||
|
|
@ -2961,7 +2985,7 @@ export default function ScreenDesigner({ selectedScreen, onBackToList }: ScreenD
|
|||
// 실제 레이아웃 업데이트는 endDrag에서 처리
|
||||
// 속성 패널에서는 dragState.currentPosition을 참조하여 실시간 표시
|
||||
},
|
||||
[dragState.isDragging, dragState.draggedComponent, dragState.grabOffset],
|
||||
[dragState.isDragging, dragState.draggedComponent, dragState.grabOffset, zoomLevel],
|
||||
);
|
||||
|
||||
// 드래그 종료
|
||||
|
|
@ -4416,7 +4440,7 @@ export default function ScreenDesigner({ selectedScreen, onBackToList }: ScreenD
|
|||
</div>
|
||||
);
|
||||
})()}
|
||||
{/* 🔥 줌 적용 시 스크롤 영역 확보를 위한 래퍼 */}
|
||||
{/* 🔥 줌 적용 시 스크롤 영역 확보를 위한 래퍼 - 중앙 정렬로 변경 */}
|
||||
<div
|
||||
className="flex justify-center"
|
||||
style={{
|
||||
|
|
@ -4435,7 +4459,7 @@ export default function ScreenDesigner({ selectedScreen, onBackToList }: ScreenD
|
|||
minHeight: `${screenResolution.height}px`,
|
||||
flexShrink: 0,
|
||||
transform: `scale(${zoomLevel})`,
|
||||
transformOrigin: "top center",
|
||||
transformOrigin: "top center", // 중앙 기준으로 스케일
|
||||
}}
|
||||
>
|
||||
<div
|
||||
|
|
|
|||
|
|
@ -33,6 +33,13 @@ export const GridPanel: React.FC<GridPanelProps> = ({
|
|||
});
|
||||
};
|
||||
|
||||
// 최대 컬럼 수 계산 (최소 컬럼 너비 30px 기준)
|
||||
const MIN_COLUMN_WIDTH = 30;
|
||||
const maxColumns = screenResolution
|
||||
? Math.floor((screenResolution.width - gridSettings.padding * 2 + gridSettings.gap) / (MIN_COLUMN_WIDTH + gridSettings.gap))
|
||||
: 24;
|
||||
const safeMaxColumns = Math.max(1, Math.min(maxColumns, 100)); // 최대 100개로 제한
|
||||
|
||||
// 실제 격자 정보 계산
|
||||
const actualGridInfo = screenResolution
|
||||
? calculateGridInfo(screenResolution.width, screenResolution.height, {
|
||||
|
|
@ -49,7 +56,7 @@ export const GridPanel: React.FC<GridPanelProps> = ({
|
|||
// 컬럼이 너무 작은지 확인
|
||||
const isColumnsTooSmall =
|
||||
screenResolution && actualGridInfo
|
||||
? actualGridInfo.columnWidth < 30 // 30px 미만이면 너무 작다고 판단
|
||||
? actualGridInfo.columnWidth < MIN_COLUMN_WIDTH
|
||||
: false;
|
||||
|
||||
return (
|
||||
|
|
@ -134,22 +141,22 @@ export const GridPanel: React.FC<GridPanelProps> = ({
|
|||
id="columns"
|
||||
type="number"
|
||||
min={1}
|
||||
max={24}
|
||||
max={safeMaxColumns}
|
||||
value={gridSettings.columns}
|
||||
onChange={(e) => {
|
||||
const value = parseInt(e.target.value, 10);
|
||||
if (!isNaN(value) && value >= 1 && value <= 24) {
|
||||
if (!isNaN(value) && value >= 1 && value <= safeMaxColumns) {
|
||||
updateSetting("columns", value);
|
||||
}
|
||||
}}
|
||||
className="h-8 text-xs"
|
||||
/>
|
||||
<span className="text-muted-foreground text-xs">/ 24</span>
|
||||
<span className="text-muted-foreground text-xs">/ {safeMaxColumns}</span>
|
||||
</div>
|
||||
<Slider
|
||||
id="columns-slider"
|
||||
min={1}
|
||||
max={24}
|
||||
max={safeMaxColumns}
|
||||
step={1}
|
||||
value={[gridSettings.columns]}
|
||||
onValueChange={([value]) => updateSetting("columns", value)}
|
||||
|
|
@ -157,8 +164,13 @@ export const GridPanel: React.FC<GridPanelProps> = ({
|
|||
/>
|
||||
<div className="text-muted-foreground flex justify-between text-xs">
|
||||
<span>1열</span>
|
||||
<span>24열</span>
|
||||
<span>{safeMaxColumns}열</span>
|
||||
</div>
|
||||
{isColumnsTooSmall && (
|
||||
<p className="text-xs text-amber-600">
|
||||
⚠️ 컬럼 너비가 너무 작습니다 (최소 {MIN_COLUMN_WIDTH}px 권장)
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
|
|
|
|||
|
|
@ -139,6 +139,13 @@ export const UnifiedPropertiesPanel: React.FC<UnifiedPropertiesPanelProps> = ({
|
|||
const renderGridSettings = () => {
|
||||
if (!gridSettings || !onGridSettingsChange) return null;
|
||||
|
||||
// 최대 컬럼 수 계산
|
||||
const MIN_COLUMN_WIDTH = 30;
|
||||
const maxColumns = currentResolution
|
||||
? Math.floor((currentResolution.width - gridSettings.padding * 2 + gridSettings.gap) / (MIN_COLUMN_WIDTH + gridSettings.gap))
|
||||
: 24;
|
||||
const safeMaxColumns = Math.max(1, Math.min(maxColumns, 100)); // 최대 100개로 제한
|
||||
|
||||
return (
|
||||
<div className="space-y-2">
|
||||
<div className="flex items-center gap-1.5">
|
||||
|
|
@ -190,21 +197,22 @@ export const UnifiedPropertiesPanel: React.FC<UnifiedPropertiesPanelProps> = ({
|
|||
id="columns"
|
||||
type="number"
|
||||
min={1}
|
||||
max={safeMaxColumns}
|
||||
step="1"
|
||||
value={gridSettings.columns}
|
||||
onChange={(e) => {
|
||||
const value = parseInt(e.target.value, 10);
|
||||
if (!isNaN(value) && value >= 1) {
|
||||
if (!isNaN(value) && value >= 1 && value <= safeMaxColumns) {
|
||||
updateGridSetting("columns", value);
|
||||
}
|
||||
}}
|
||||
className="h-6 px-2 py-0 text-xs"
|
||||
style={{ fontSize: "12px" }}
|
||||
placeholder="1 이상의 숫자"
|
||||
placeholder={`1~${safeMaxColumns}`}
|
||||
/>
|
||||
</div>
|
||||
<p className="text-muted-foreground text-[10px]">
|
||||
1 이상의 숫자를 입력하세요
|
||||
최대 {safeMaxColumns}개까지 설정 가능 (최소 컬럼 너비 {MIN_COLUMN_WIDTH}px)
|
||||
</p>
|
||||
</div>
|
||||
|
||||
|
|
|
|||
|
|
@ -15,17 +15,28 @@ export function calculateGridInfo(
|
|||
containerHeight: number,
|
||||
gridSettings: GridSettings,
|
||||
): GridInfo {
|
||||
const { columns, gap, padding } = gridSettings;
|
||||
const { gap, padding } = gridSettings;
|
||||
let { columns } = gridSettings;
|
||||
|
||||
// 사용 가능한 너비 계산 (패딩 제외)
|
||||
// 🔥 최소 컬럼 너비를 보장하기 위한 최대 컬럼 수 계산
|
||||
const MIN_COLUMN_WIDTH = 30; // 최소 컬럼 너비 30px
|
||||
const availableWidth = containerWidth - padding * 2;
|
||||
const maxPossibleColumns = Math.floor((availableWidth + gap) / (MIN_COLUMN_WIDTH + gap));
|
||||
|
||||
// 설정된 컬럼 수가 너무 많으면 자동으로 제한
|
||||
if (columns > maxPossibleColumns) {
|
||||
console.warn(
|
||||
`⚠️ 격자 컬럼 수가 너무 많습니다. ${columns}개 → ${maxPossibleColumns}개로 자동 조정됨 (최소 컬럼 너비: ${MIN_COLUMN_WIDTH}px)`,
|
||||
);
|
||||
columns = Math.max(1, maxPossibleColumns);
|
||||
}
|
||||
|
||||
// 격자 간격을 고려한 컬럼 너비 계산
|
||||
const totalGaps = (columns - 1) * gap;
|
||||
const columnWidth = (availableWidth - totalGaps) / columns;
|
||||
|
||||
return {
|
||||
columnWidth: Math.max(columnWidth, 20), // 최소 20px로 줄여서 더 많은 컬럼 표시
|
||||
columnWidth: Math.max(columnWidth, MIN_COLUMN_WIDTH),
|
||||
totalWidth: containerWidth,
|
||||
totalHeight: containerHeight,
|
||||
};
|
||||
|
|
|
|||
Loading…
Reference in New Issue