diff --git a/frontend/components/screen/RealtimePreview.tsx b/frontend/components/screen/RealtimePreview.tsx index e3c4afd3..99e4bfe8 100644 --- a/frontend/components/screen/RealtimePreview.tsx +++ b/frontend/components/screen/RealtimePreview.tsx @@ -230,6 +230,7 @@ export const RealtimePreviewDynamic: React.FC = ({ : {}; const handleClick = (e: React.MouseEvent) => { + // 컴포넌트 영역 내에서만 클릭 이벤트 처리 e.stopPropagation(); onClick?.(e); }; diff --git a/frontend/components/screen/ScreenDesigner.tsx b/frontend/components/screen/ScreenDesigner.tsx index 94568037..f861d1ba 100644 --- a/frontend/components/screen/ScreenDesigner.tsx +++ b/frontend/components/screen/ScreenDesigner.tsx @@ -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 ( -
handleComponentClick(component, e)} + onDragStart={(e) => startComponentDrag(component, e)} + onDragEnd={endDrag} > - 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 ( -
- handleComponentClick(child, e)} - onDragStart={(e) => startComponentDrag(child, e)} - onDragEnd={endDrag} - /> -
- ); - })} -
-
+ // 자식 컴포넌트의 위치를 부모 기준 상대 좌표로 조정 + const relativeChildComponent = { + ...displayChild, + position: { + x: displayChild.position.x - component.position.x, + y: displayChild.position.y - component.position.y, + z: displayChild.position.z || 1, + }, + }; + + return ( + handleComponentClick(child, e)} + onDragStart={(e) => startComponentDrag(child, e)} + onDragEnd={endDrag} + /> + ); + })} + ); })} diff --git a/frontend/lib/utils/gridUtils.ts b/frontend/lib/utils/gridUtils.ts index 20cce383..29c1ffab 100644 --- a/frontend/lib/utils/gridUtils.ts +++ b/frontend/lib/utils/gridUtils.ts @@ -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); }