diff --git a/frontend/components/report/designer/CanvasComponent.tsx b/frontend/components/report/designer/CanvasComponent.tsx index 7f161860..f770b4f7 100644 --- a/frontend/components/report/designer/CanvasComponent.tsx +++ b/frontend/components/report/designer/CanvasComponent.tsx @@ -9,8 +9,15 @@ interface CanvasComponentProps { } export function CanvasComponent({ component }: CanvasComponentProps) { - const { selectedComponentId, selectComponent, updateComponent, getQueryResult, snapValueToGrid } = - useReportDesigner(); + const { + selectedComponentId, + selectComponent, + updateComponent, + getQueryResult, + snapValueToGrid, + calculateAlignmentGuides, + clearAlignmentGuides, + } = useReportDesigner(); const [isDragging, setIsDragging] = useState(false); const [isResizing, setIsResizing] = useState(false); const [dragStart, setDragStart] = useState({ x: 0, y: 0 }); @@ -54,10 +61,16 @@ export function CanvasComponent({ component }: CanvasComponentProps) { if (isDragging) { const newX = Math.max(0, e.clientX - dragStart.x); const newY = Math.max(0, e.clientY - dragStart.y); + const snappedX = snapValueToGrid(newX); + const snappedY = snapValueToGrid(newY); + + // 정렬 가이드라인 계산 + calculateAlignmentGuides(component.id, snappedX, snappedY, component.width, component.height); + // Grid Snap 적용 updateComponent(component.id, { - x: snapValueToGrid(newX), - y: snapValueToGrid(newY), + x: snappedX, + y: snappedY, }); } else if (isResizing) { const deltaX = e.clientX - resizeStart.x; @@ -75,6 +88,8 @@ export function CanvasComponent({ component }: CanvasComponentProps) { const handleMouseUp = () => { setIsDragging(false); setIsResizing(false); + // 가이드라인 초기화 + clearAlignmentGuides(); }; document.addEventListener("mousemove", handleMouseMove); @@ -94,8 +109,12 @@ export function CanvasComponent({ component }: CanvasComponentProps) { resizeStart.width, resizeStart.height, component.id, + component.width, + component.height, updateComponent, snapValueToGrid, + calculateAlignmentGuides, + clearAlignmentGuides, ]); // 표시할 값 결정 diff --git a/frontend/components/report/designer/ReportDesignerCanvas.tsx b/frontend/components/report/designer/ReportDesignerCanvas.tsx index 37e370fc..ad9f588e 100644 --- a/frontend/components/report/designer/ReportDesignerCanvas.tsx +++ b/frontend/components/report/designer/ReportDesignerCanvas.tsx @@ -20,6 +20,7 @@ export function ReportDesignerCanvas() { showGrid, gridSize, snapValueToGrid, + alignmentGuides, } = useReportDesigner(); const [{ isOver }, drop] = useDrop(() => ({ @@ -109,6 +110,32 @@ export function ReportDesignerCanvas() { }} onClick={handleCanvasClick} > + {/* 정렬 가이드라인 렌더링 */} + {alignmentGuides.vertical.map((x, index) => ( +
+ ))} + {alignmentGuides.horizontal.map((y, index) => ( +
+ ))} + {/* 컴포넌트 렌더링 */} {components.map((component) => ( diff --git a/frontend/contexts/ReportDesignerContext.tsx b/frontend/contexts/ReportDesignerContext.tsx index e8687dd3..ee8d0bfc 100644 --- a/frontend/contexts/ReportDesignerContext.tsx +++ b/frontend/contexts/ReportDesignerContext.tsx @@ -316,6 +316,11 @@ interface ReportDesignerContextType { snapToGrid: boolean; setSnapToGrid: (snap: boolean) => void; snapValueToGrid: (value: number) => number; + + // 정렬 가이드라인 + alignmentGuides: { vertical: number[]; horizontal: number[] }; + calculateAlignmentGuides: (draggingId: string, x: number, y: number, width: number, height: number) => void; + clearAlignmentGuides: () => void; } const ReportDesignerContext = createContext(undefined); @@ -336,6 +341,12 @@ export function ReportDesignerProvider({ reportId, children }: { reportId: strin const [showGrid, setShowGrid] = useState(true); // Grid 표시 여부 const [snapToGrid, setSnapToGrid] = useState(true); // Grid Snap 활성화 + // 정렬 가이드라인 + const [alignmentGuides, setAlignmentGuides] = useState<{ + vertical: number[]; + horizontal: number[]; + }>({ vertical: [], horizontal: [] }); + // Grid Snap 함수 const snapValueToGrid = useCallback( (value: number): number => { @@ -345,6 +356,61 @@ export function ReportDesignerProvider({ reportId, children }: { reportId: strin [snapToGrid, gridSize], ); + // 정렬 가이드라인 계산 (드래그 중인 컴포넌트 제외) + const calculateAlignmentGuides = useCallback( + (draggingId: string, x: number, y: number, width: number, height: number) => { + const threshold = 5; // 정렬 감지 임계값 (px) + const verticalLines: number[] = []; + const horizontalLines: number[] = []; + + // 드래그 중인 컴포넌트의 주요 위치 + const left = x; + const right = x + width; + const centerX = x + width / 2; + const top = y; + const bottom = y + height; + const centerY = y + height / 2; + + // 다른 컴포넌트들과 비교 + components.forEach((comp) => { + if (comp.id === draggingId) return; + + const compLeft = comp.x; + const compRight = comp.x + comp.width; + const compCenterX = comp.x + comp.width / 2; + const compTop = comp.y; + const compBottom = comp.y + comp.height; + const compCenterY = comp.y + comp.height / 2; + + // 세로 정렬 체크 (left, center, right) + if (Math.abs(left - compLeft) < threshold) verticalLines.push(compLeft); + if (Math.abs(left - compRight) < threshold) verticalLines.push(compRight); + if (Math.abs(right - compLeft) < threshold) verticalLines.push(compLeft); + if (Math.abs(right - compRight) < threshold) verticalLines.push(compRight); + if (Math.abs(centerX - compCenterX) < threshold) verticalLines.push(compCenterX); + + // 가로 정렬 체크 (top, center, bottom) + if (Math.abs(top - compTop) < threshold) horizontalLines.push(compTop); + if (Math.abs(top - compBottom) < threshold) horizontalLines.push(compBottom); + if (Math.abs(bottom - compTop) < threshold) horizontalLines.push(compTop); + if (Math.abs(bottom - compBottom) < threshold) horizontalLines.push(compBottom); + if (Math.abs(centerY - compCenterY) < threshold) horizontalLines.push(compCenterY); + }); + + // 중복 제거 + setAlignmentGuides({ + vertical: Array.from(new Set(verticalLines)), + horizontal: Array.from(new Set(horizontalLines)), + }); + }, + [components], + ); + + // 정렬 가이드라인 초기화 + const clearAlignmentGuides = useCallback(() => { + setAlignmentGuides({ vertical: [], horizontal: [] }); + }, []); + // 캔버스 설정 (기본값) const [canvasWidth, setCanvasWidth] = useState(210); const [canvasHeight, setCanvasHeight] = useState(297); @@ -676,6 +742,10 @@ export function ReportDesignerProvider({ reportId, children }: { reportId: strin snapToGrid, setSnapToGrid, snapValueToGrid, + // 정렬 가이드라인 + alignmentGuides, + calculateAlignmentGuides, + clearAlignmentGuides, }; return {children};