컴포넌트 배치문제 해결

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) => { const handleClick = (e: React.MouseEvent) => {
// 컴포넌트 영역 내에서만 클릭 이벤트 처리
e.stopPropagation(); e.stopPropagation();
onClick?.(e); 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 isDraggedComponentSelected = groupState.selectedComponents.includes(component.id);
const componentsToMove = isDraggedComponentSelected const componentsToMove = isDraggedComponentSelected
@ -1745,6 +1749,18 @@ export default function ScreenDesigner({ selectedScreen, onBackToList }: ScreenD
: [component]; : [component];
console.log("드래그 시작:", component.id, "이동할 컴포넌트 수:", componentsToMove.length); 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({ setDragState({
isDragging: true, isDragging: true,
@ -1761,8 +1777,8 @@ export default function ScreenDesigner({ selectedScreen, onBackToList }: ScreenD
z: (component.position as Position).z || 1, z: (component.position as Position).z || 1,
}, },
grabOffset: { grabOffset: {
x: event.clientX - rect.left - component.position.x, x: relativeMouseX - component.position.x,
y: event.clientY - rect.top - component.position.y, y: relativeMouseY - component.position.y,
}, },
justFinishedDrag: false, justFinishedDrag: false,
}); });
@ -1776,9 +1792,14 @@ export default function ScreenDesigner({ selectedScreen, onBackToList }: ScreenD
if (!dragState.isDragging || !dragState.draggedComponent || !canvasRef.current) return; if (!dragState.isDragging || !dragState.draggedComponent || !canvasRef.current) return;
const rect = canvasRef.current.getBoundingClientRect(); const rect = canvasRef.current.getBoundingClientRect();
// 캔버스 내부의 상대 좌표 계산 (스크롤 없는 고정 캔버스)
const relativeMouseX = event.clientX - rect.left;
const relativeMouseY = event.clientY - rect.top;
const newPosition = { const newPosition = {
x: event.clientX - rect.left - dragState.grabOffset.x, x: relativeMouseX - dragState.grabOffset.x,
y: event.clientY - rect.top - dragState.grabOffset.y, y: relativeMouseY - dragState.grabOffset.y,
z: (dragState.draggedComponent.position as Position).z || 1, z: (dragState.draggedComponent.position as Position).z || 1,
}; };
@ -2874,108 +2895,95 @@ export default function ScreenDesigner({ selectedScreen, onBackToList }: ScreenD
} }
return ( return (
<div <RealtimePreview
key={component.id} key={component.id}
className="absolute" component={displayComponent}
style={{ isSelected={
left: `${displayComponent.position.x}px`, selectedComponent?.id === component.id || groupState.selectedComponents.includes(component.id)
top: `${displayComponent.position.y}px`, }
width: displayComponent.style?.width || `${displayComponent.size.width}px`, onClick={(e) => handleComponentClick(component, e)}
height: displayComponent.style?.height || `${displayComponent.size.height}px`, onDragStart={(e) => startComponentDrag(component, e)}
zIndex: displayComponent.position.z || 1, onDragEnd={endDrag}
}}
> >
<RealtimePreview {/* 컨테이너, 그룹, 영역의 자식 컴포넌트들 렌더링 */}
component={displayComponent} {(component.type === "group" || component.type === "container" || component.type === "area") &&
isSelected={ layout.components
selectedComponent?.id === component.id || groupState.selectedComponents.includes(component.id) .filter((child) => child.parentId === component.id)
} .map((child) => {
onClick={(e) => handleComponentClick(component, e)} // 자식 컴포넌트에도 드래그 피드백 적용
onDragStart={(e) => startComponentDrag(component, e)} const isChildDraggingThis =
onDragEnd={endDrag} dragState.isDragging && dragState.draggedComponent?.id === child.id;
> const isChildBeingDragged =
{/* 컨테이너, 그룹, 영역의 자식 컴포넌트들 렌더링 */} dragState.isDragging &&
{(component.type === "group" || component.type === "container" || component.type === "area") && dragState.draggedComponents.some((dragComp) => dragComp.id === child.id);
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 = { displayChild = {
...child, ...child,
position: dragState.currentPosition, position: {
x: originalChildComponent.position.x + deltaX,
y: originalChildComponent.position.y + deltaY,
z: originalChildComponent.position.z || 1,
} as Position,
style: { style: {
...child.style, ...child.style,
opacity: 0.8, opacity: 0.8,
transform: "scale(1.02)",
transition: "none", 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 const relativeChildComponent = {
key={child.id} ...displayChild,
className="absolute" position: {
style={{ x: displayChild.position.x - component.position.x,
left: `${displayChild.position.x - component.position.x}px`, y: displayChild.position.y - component.position.y,
top: `${displayChild.position.y - component.position.y}px`, z: displayChild.position.z || 1,
width: `${displayChild.size.width}px`, },
height: `${displayChild.size.height}px`, // 순수 컴포넌트 높이만 사용 };
zIndex: displayChild.position.z || 1,
}} return (
> <RealtimePreview
<RealtimePreview key={child.id}
component={displayChild} component={relativeChildComponent}
isSelected={ isSelected={
selectedComponent?.id === child.id || selectedComponent?.id === child.id || groupState.selectedComponents.includes(child.id)
groupState.selectedComponents.includes(child.id) }
} onClick={(e) => handleComponentClick(child, e)}
onClick={(e) => handleComponentClick(child, e)} onDragStart={(e) => startComponentDrag(child, e)}
onDragStart={(e) => startComponentDrag(child, e)} onDragEnd={endDrag}
onDragEnd={endDrag} />
/> );
</div> })}
); </RealtimePreview>
})}
</RealtimePreview>
</div>
); );
})} })}

View File

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