정렬 가이드라인 구현
This commit is contained in:
parent
ae23a4408e
commit
c5c6d9239c
|
|
@ -9,8 +9,15 @@ interface CanvasComponentProps {
|
||||||
}
|
}
|
||||||
|
|
||||||
export function CanvasComponent({ component }: CanvasComponentProps) {
|
export function CanvasComponent({ component }: CanvasComponentProps) {
|
||||||
const { selectedComponentId, selectComponent, updateComponent, getQueryResult, snapValueToGrid } =
|
const {
|
||||||
useReportDesigner();
|
selectedComponentId,
|
||||||
|
selectComponent,
|
||||||
|
updateComponent,
|
||||||
|
getQueryResult,
|
||||||
|
snapValueToGrid,
|
||||||
|
calculateAlignmentGuides,
|
||||||
|
clearAlignmentGuides,
|
||||||
|
} = useReportDesigner();
|
||||||
const [isDragging, setIsDragging] = useState(false);
|
const [isDragging, setIsDragging] = useState(false);
|
||||||
const [isResizing, setIsResizing] = useState(false);
|
const [isResizing, setIsResizing] = useState(false);
|
||||||
const [dragStart, setDragStart] = useState({ x: 0, y: 0 });
|
const [dragStart, setDragStart] = useState({ x: 0, y: 0 });
|
||||||
|
|
@ -54,10 +61,16 @@ export function CanvasComponent({ component }: CanvasComponentProps) {
|
||||||
if (isDragging) {
|
if (isDragging) {
|
||||||
const newX = Math.max(0, e.clientX - dragStart.x);
|
const newX = Math.max(0, e.clientX - dragStart.x);
|
||||||
const newY = Math.max(0, e.clientY - dragStart.y);
|
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 적용
|
// Grid Snap 적용
|
||||||
updateComponent(component.id, {
|
updateComponent(component.id, {
|
||||||
x: snapValueToGrid(newX),
|
x: snappedX,
|
||||||
y: snapValueToGrid(newY),
|
y: snappedY,
|
||||||
});
|
});
|
||||||
} else if (isResizing) {
|
} else if (isResizing) {
|
||||||
const deltaX = e.clientX - resizeStart.x;
|
const deltaX = e.clientX - resizeStart.x;
|
||||||
|
|
@ -75,6 +88,8 @@ export function CanvasComponent({ component }: CanvasComponentProps) {
|
||||||
const handleMouseUp = () => {
|
const handleMouseUp = () => {
|
||||||
setIsDragging(false);
|
setIsDragging(false);
|
||||||
setIsResizing(false);
|
setIsResizing(false);
|
||||||
|
// 가이드라인 초기화
|
||||||
|
clearAlignmentGuides();
|
||||||
};
|
};
|
||||||
|
|
||||||
document.addEventListener("mousemove", handleMouseMove);
|
document.addEventListener("mousemove", handleMouseMove);
|
||||||
|
|
@ -94,8 +109,12 @@ export function CanvasComponent({ component }: CanvasComponentProps) {
|
||||||
resizeStart.width,
|
resizeStart.width,
|
||||||
resizeStart.height,
|
resizeStart.height,
|
||||||
component.id,
|
component.id,
|
||||||
|
component.width,
|
||||||
|
component.height,
|
||||||
updateComponent,
|
updateComponent,
|
||||||
snapValueToGrid,
|
snapValueToGrid,
|
||||||
|
calculateAlignmentGuides,
|
||||||
|
clearAlignmentGuides,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
// 표시할 값 결정
|
// 표시할 값 결정
|
||||||
|
|
|
||||||
|
|
@ -20,6 +20,7 @@ export function ReportDesignerCanvas() {
|
||||||
showGrid,
|
showGrid,
|
||||||
gridSize,
|
gridSize,
|
||||||
snapValueToGrid,
|
snapValueToGrid,
|
||||||
|
alignmentGuides,
|
||||||
} = useReportDesigner();
|
} = useReportDesigner();
|
||||||
|
|
||||||
const [{ isOver }, drop] = useDrop(() => ({
|
const [{ isOver }, drop] = useDrop(() => ({
|
||||||
|
|
@ -109,6 +110,32 @@ export function ReportDesignerCanvas() {
|
||||||
}}
|
}}
|
||||||
onClick={handleCanvasClick}
|
onClick={handleCanvasClick}
|
||||||
>
|
>
|
||||||
|
{/* 정렬 가이드라인 렌더링 */}
|
||||||
|
{alignmentGuides.vertical.map((x, index) => (
|
||||||
|
<div
|
||||||
|
key={`v-${index}`}
|
||||||
|
className="pointer-events-none absolute top-0 bottom-0"
|
||||||
|
style={{
|
||||||
|
left: `${x}px`,
|
||||||
|
width: "1px",
|
||||||
|
backgroundColor: "#ef4444",
|
||||||
|
zIndex: 9999,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
{alignmentGuides.horizontal.map((y, index) => (
|
||||||
|
<div
|
||||||
|
key={`h-${index}`}
|
||||||
|
className="pointer-events-none absolute right-0 left-0"
|
||||||
|
style={{
|
||||||
|
top: `${y}px`,
|
||||||
|
height: "1px",
|
||||||
|
backgroundColor: "#ef4444",
|
||||||
|
zIndex: 9999,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
|
||||||
{/* 컴포넌트 렌더링 */}
|
{/* 컴포넌트 렌더링 */}
|
||||||
{components.map((component) => (
|
{components.map((component) => (
|
||||||
<CanvasComponent key={component.id} component={component} />
|
<CanvasComponent key={component.id} component={component} />
|
||||||
|
|
|
||||||
|
|
@ -316,6 +316,11 @@ interface ReportDesignerContextType {
|
||||||
snapToGrid: boolean;
|
snapToGrid: boolean;
|
||||||
setSnapToGrid: (snap: boolean) => void;
|
setSnapToGrid: (snap: boolean) => void;
|
||||||
snapValueToGrid: (value: number) => number;
|
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<ReportDesignerContextType | undefined>(undefined);
|
const ReportDesignerContext = createContext<ReportDesignerContextType | undefined>(undefined);
|
||||||
|
|
@ -336,6 +341,12 @@ export function ReportDesignerProvider({ reportId, children }: { reportId: strin
|
||||||
const [showGrid, setShowGrid] = useState(true); // Grid 표시 여부
|
const [showGrid, setShowGrid] = useState(true); // Grid 표시 여부
|
||||||
const [snapToGrid, setSnapToGrid] = useState(true); // Grid Snap 활성화
|
const [snapToGrid, setSnapToGrid] = useState(true); // Grid Snap 활성화
|
||||||
|
|
||||||
|
// 정렬 가이드라인
|
||||||
|
const [alignmentGuides, setAlignmentGuides] = useState<{
|
||||||
|
vertical: number[];
|
||||||
|
horizontal: number[];
|
||||||
|
}>({ vertical: [], horizontal: [] });
|
||||||
|
|
||||||
// Grid Snap 함수
|
// Grid Snap 함수
|
||||||
const snapValueToGrid = useCallback(
|
const snapValueToGrid = useCallback(
|
||||||
(value: number): number => {
|
(value: number): number => {
|
||||||
|
|
@ -345,6 +356,61 @@ export function ReportDesignerProvider({ reportId, children }: { reportId: strin
|
||||||
[snapToGrid, gridSize],
|
[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 [canvasWidth, setCanvasWidth] = useState(210);
|
||||||
const [canvasHeight, setCanvasHeight] = useState(297);
|
const [canvasHeight, setCanvasHeight] = useState(297);
|
||||||
|
|
@ -676,6 +742,10 @@ export function ReportDesignerProvider({ reportId, children }: { reportId: strin
|
||||||
snapToGrid,
|
snapToGrid,
|
||||||
setSnapToGrid,
|
setSnapToGrid,
|
||||||
snapValueToGrid,
|
snapValueToGrid,
|
||||||
|
// 정렬 가이드라인
|
||||||
|
alignmentGuides,
|
||||||
|
calculateAlignmentGuides,
|
||||||
|
clearAlignmentGuides,
|
||||||
};
|
};
|
||||||
|
|
||||||
return <ReportDesignerContext.Provider value={value}>{children}</ReportDesignerContext.Provider>;
|
return <ReportDesignerContext.Provider value={value}>{children}</ReportDesignerContext.Provider>;
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue