컴포넌트 배치문제 해결

This commit is contained in:
kjs 2025-09-09 18:02:07 +09:00
parent db782eb9c9
commit 4736dd87b6
3 changed files with 125 additions and 112 deletions

View File

@ -230,6 +230,7 @@ export const RealtimePreviewDynamic: React.FC<RealtimePreviewProps> = ({
: {};
const handleClick = (e: React.MouseEvent) => {
// 컴포넌트 영역 내에서만 클릭 이벤트 처리
e.stopPropagation();
onClick?.(e);
};

View File

@ -1738,6 +1738,10 @@ export default function ScreenDesigner({ selectedScreen, onBackToList }: ScreenD
}));
}
// 캔버스 내부의 상대 좌표 계산 (스크롤 없는 고정 캔버스)
const relativeMouseX = event.clientX - rect.left;
const relativeMouseY = event.clientY - rect.top;
// 다중 선택된 컴포넌트들 확인
const isDraggedComponentSelected = groupState.selectedComponents.includes(component.id);
const componentsToMove = isDraggedComponentSelected
@ -1745,6 +1749,18 @@ export default function ScreenDesigner({ selectedScreen, onBackToList }: ScreenD
: [component];
console.log("드래그 시작:", component.id, "이동할 컴포넌트 수:", componentsToMove.length);
console.log("마우스 위치:", {
clientX: event.clientX,
clientY: event.clientY,
rectLeft: rect.left,
rectTop: rect.top,
relativeX: relativeMouseX,
relativeY: relativeMouseY,
componentX: component.position.x,
componentY: component.position.y,
grabOffsetX: relativeMouseX - component.position.x,
grabOffsetY: relativeMouseY - component.position.y,
});
setDragState({
isDragging: true,
@ -1761,8 +1777,8 @@ export default function ScreenDesigner({ selectedScreen, onBackToList }: ScreenD
z: (component.position as Position).z || 1,
},
grabOffset: {
x: event.clientX - rect.left - component.position.x,
y: event.clientY - rect.top - component.position.y,
x: relativeMouseX - component.position.x,
y: relativeMouseY - component.position.y,
},
justFinishedDrag: false,
});
@ -1776,9 +1792,14 @@ export default function ScreenDesigner({ selectedScreen, onBackToList }: ScreenD
if (!dragState.isDragging || !dragState.draggedComponent || !canvasRef.current) return;
const rect = canvasRef.current.getBoundingClientRect();
// 캔버스 내부의 상대 좌표 계산 (스크롤 없는 고정 캔버스)
const relativeMouseX = event.clientX - rect.left;
const relativeMouseY = event.clientY - rect.top;
const newPosition = {
x: event.clientX - rect.left - dragState.grabOffset.x,
y: event.clientY - rect.top - dragState.grabOffset.y,
x: relativeMouseX - dragState.grabOffset.x,
y: relativeMouseY - dragState.grabOffset.y,
z: (dragState.draggedComponent.position as Position).z || 1,
};
@ -2874,108 +2895,95 @@ export default function ScreenDesigner({ selectedScreen, onBackToList }: ScreenD
}
return (
<div
<RealtimePreview
key={component.id}
className="absolute"
style={{
left: `${displayComponent.position.x}px`,
top: `${displayComponent.position.y}px`,
width: displayComponent.style?.width || `${displayComponent.size.width}px`,
height: displayComponent.style?.height || `${displayComponent.size.height}px`,
zIndex: displayComponent.position.z || 1,
}}
component={displayComponent}
isSelected={
selectedComponent?.id === component.id || groupState.selectedComponents.includes(component.id)
}
onClick={(e) => handleComponentClick(component, e)}
onDragStart={(e) => startComponentDrag(component, e)}
onDragEnd={endDrag}
>
<RealtimePreview
component={displayComponent}
isSelected={
selectedComponent?.id === component.id || groupState.selectedComponents.includes(component.id)
}
onClick={(e) => handleComponentClick(component, e)}
onDragStart={(e) => startComponentDrag(component, e)}
onDragEnd={endDrag}
>
{/* 컨테이너, 그룹, 영역의 자식 컴포넌트들 렌더링 */}
{(component.type === "group" || component.type === "container" || component.type === "area") &&
layout.components
.filter((child) => child.parentId === component.id)
.map((child) => {
// 자식 컴포넌트에도 드래그 피드백 적용
const isChildDraggingThis =
dragState.isDragging && dragState.draggedComponent?.id === child.id;
const isChildBeingDragged =
dragState.isDragging &&
dragState.draggedComponents.some((dragComp) => dragComp.id === child.id);
{/* 컨테이너, 그룹, 영역의 자식 컴포넌트들 렌더링 */}
{(component.type === "group" || component.type === "container" || component.type === "area") &&
layout.components
.filter((child) => child.parentId === component.id)
.map((child) => {
// 자식 컴포넌트에도 드래그 피드백 적용
const isChildDraggingThis =
dragState.isDragging && dragState.draggedComponent?.id === child.id;
const isChildBeingDragged =
dragState.isDragging &&
dragState.draggedComponents.some((dragComp) => dragComp.id === child.id);
let displayChild = child;
let displayChild = child;
if (isChildBeingDragged) {
if (isChildDraggingThis) {
// 주 드래그 자식 컴포넌트
displayChild = {
...child,
position: dragState.currentPosition,
style: {
...child.style,
opacity: 0.8,
transform: "scale(1.02)",
transition: "none",
zIndex: 9999,
},
};
} else {
// 다른 선택된 자식 컴포넌트들
const originalChildComponent = dragState.draggedComponents.find(
(dragComp) => dragComp.id === child.id,
);
if (originalChildComponent) {
const deltaX = dragState.currentPosition.x - dragState.originalPosition.x;
const deltaY = dragState.currentPosition.y - dragState.originalPosition.y;
if (isChildBeingDragged) {
if (isChildDraggingThis) {
// 주 드래그 자식 컴포넌트
displayChild = {
...child,
position: dragState.currentPosition,
position: {
x: originalChildComponent.position.x + deltaX,
y: originalChildComponent.position.y + deltaY,
z: originalChildComponent.position.z || 1,
} as Position,
style: {
...child.style,
opacity: 0.8,
transform: "scale(1.02)",
transition: "none",
zIndex: 9999,
zIndex: 8888,
},
};
} else {
// 다른 선택된 자식 컴포넌트들
const originalChildComponent = dragState.draggedComponents.find(
(dragComp) => dragComp.id === child.id,
);
if (originalChildComponent) {
const deltaX = dragState.currentPosition.x - dragState.originalPosition.x;
const deltaY = dragState.currentPosition.y - dragState.originalPosition.y;
displayChild = {
...child,
position: {
x: originalChildComponent.position.x + deltaX,
y: originalChildComponent.position.y + deltaY,
z: originalChildComponent.position.z || 1,
} as Position,
style: {
...child.style,
opacity: 0.8,
transition: "none",
zIndex: 8888,
},
};
}
}
}
}
return (
<div
key={child.id}
className="absolute"
style={{
left: `${displayChild.position.x - component.position.x}px`,
top: `${displayChild.position.y - component.position.y}px`,
width: `${displayChild.size.width}px`,
height: `${displayChild.size.height}px`, // 순수 컴포넌트 높이만 사용
zIndex: displayChild.position.z || 1,
}}
>
<RealtimePreview
component={displayChild}
isSelected={
selectedComponent?.id === child.id ||
groupState.selectedComponents.includes(child.id)
}
onClick={(e) => handleComponentClick(child, e)}
onDragStart={(e) => startComponentDrag(child, e)}
onDragEnd={endDrag}
/>
</div>
);
})}
</RealtimePreview>
</div>
// 자식 컴포넌트의 위치를 부모 기준 상대 좌표로 조정
const relativeChildComponent = {
...displayChild,
position: {
x: displayChild.position.x - component.position.x,
y: displayChild.position.y - component.position.y,
z: displayChild.position.z || 1,
},
};
return (
<RealtimePreview
key={child.id}
component={relativeChildComponent}
isSelected={
selectedComponent?.id === child.id || groupState.selectedComponents.includes(child.id)
}
onClick={(e) => handleComponentClick(child, e)}
onDragStart={(e) => startComponentDrag(child, e)}
onDragEnd={endDrag}
/>
);
})}
</RealtimePreview>
);
})}

View File

@ -48,14 +48,21 @@ export function snapToGrid(position: Position, gridInfo: GridInfo, gridSettings:
const { columnWidth } = gridInfo;
const { gap, padding } = gridSettings;
// 격자 기준으로 위치 계산
const gridX = Math.round((position.x - padding) / (columnWidth + gap));
const rowHeight = Math.max(20, gap); // gap과 최소 20px 중 큰 값으로 행 높이 설정
const gridY = Math.round((position.y - padding) / rowHeight); // 동적 행 높이로 세로 스냅
// 격자 셀 크기 (컬럼 너비 + 간격을 하나의 격자 단위로 계산)
const cellWidth = columnWidth + gap;
const cellHeight = Math.max(40, gap * 2); // 행 높이를 더 크게 설정
// 패딩을 제외한 상대 위치
const relativeX = position.x - padding;
const relativeY = position.y - padding;
// 격자 기준으로 위치 계산 (가장 가까운 격자점으로 스냅)
const gridX = Math.round(relativeX / cellWidth);
const gridY = Math.round(relativeY / cellHeight);
// 실제 픽셀 위치로 변환
const snappedX = Math.max(padding, padding + gridX * (columnWidth + gap));
const snappedY = Math.max(padding, padding + gridY * rowHeight);
const snappedX = Math.max(padding, padding + gridX * cellWidth);
const snappedY = Math.max(padding, padding + gridY * cellHeight);
return {
x: snappedX,
@ -172,25 +179,22 @@ export function generateGridLines(
const gridInfo = calculateGridInfo(containerWidth, containerHeight, gridSettings);
const { columnWidth } = gridInfo;
// 세로 격자선 (컬럼 경계)
// 격자 셀 크기 (스냅 로직과 동일하게)
const cellWidth = columnWidth + gap;
const cellHeight = Math.max(40, gap * 2);
// 세로 격자선
const verticalLines: number[] = [];
// 좌측 경계선
verticalLines.push(padding);
// 각 컬럼의 오른쪽 경계선들 (컬럼 사이의 격자선)
for (let i = 1; i < columns; i++) {
const x = padding + i * columnWidth + i * gap;
verticalLines.push(x);
for (let i = 0; i <= columns; i++) {
const x = padding + i * cellWidth;
if (x <= containerWidth) {
verticalLines.push(x);
}
}
// 우측 경계선
verticalLines.push(containerWidth - padding);
// 가로 격자선 (동적 행 높이 단위)
const rowHeight = Math.max(20, gap);
// 가로 격자선
const horizontalLines: number[] = [];
for (let y = padding; y < containerHeight; y += rowHeight) {
for (let y = padding; y < containerHeight; y += cellHeight) {
horizontalLines.push(y);
}