드래그 영역 선택(Marquee Selection) 기능 추가
This commit is contained in:
parent
aa283d11da
commit
352f9f441f
|
|
@ -1,6 +1,6 @@
|
|||
"use client";
|
||||
|
||||
import { useRef, useEffect } from "react";
|
||||
import { useRef, useEffect, useState } from "react";
|
||||
import { useDrop } from "react-dnd";
|
||||
import { useReportDesigner } from "@/contexts/ReportDesignerContext";
|
||||
import { ComponentConfig, WatermarkConfig } from "@/types/report";
|
||||
|
|
@ -201,6 +201,7 @@ export function ReportDesignerCanvas() {
|
|||
canvasHeight,
|
||||
margins,
|
||||
selectComponent,
|
||||
selectMultipleComponents,
|
||||
selectedComponentId,
|
||||
selectedComponentIds,
|
||||
removeComponent,
|
||||
|
|
@ -216,6 +217,22 @@ export function ReportDesignerCanvas() {
|
|||
layoutConfig,
|
||||
} = useReportDesigner();
|
||||
|
||||
// 드래그 영역 선택 (Marquee Selection) 상태
|
||||
const [isMarqueeSelecting, setIsMarqueeSelecting] = useState(false);
|
||||
const [marqueeStart, setMarqueeStart] = useState({ x: 0, y: 0 });
|
||||
const [marqueeEnd, setMarqueeEnd] = useState({ x: 0, y: 0 });
|
||||
// 클로저 문제 해결을 위한 refs (동기적으로 업데이트)
|
||||
const marqueeStartRef = useRef({ x: 0, y: 0 });
|
||||
const marqueeEndRef = useRef({ x: 0, y: 0 });
|
||||
const componentsRef = useRef(components);
|
||||
const selectMultipleRef = useRef(selectMultipleComponents);
|
||||
// 마퀴 선택 직후 click 이벤트 무시를 위한 플래그
|
||||
const justFinishedMarqueeRef = useRef(false);
|
||||
|
||||
// refs 동기적 업데이트 (useEffect 대신 직접 할당)
|
||||
componentsRef.current = components;
|
||||
selectMultipleRef.current = selectMultipleComponents;
|
||||
|
||||
const [{ isOver }, drop] = useDrop(() => ({
|
||||
accept: "component",
|
||||
drop: (item: { componentType: string }, monitor) => {
|
||||
|
|
@ -420,12 +437,127 @@ export function ReportDesignerCanvas() {
|
|||
}),
|
||||
}));
|
||||
|
||||
// 캔버스 클릭 시 선택 해제 (드래그 선택이 아닐 때만)
|
||||
const handleCanvasClick = (e: React.MouseEvent<HTMLDivElement>) => {
|
||||
if (e.target === e.currentTarget) {
|
||||
// 마퀴 선택 직후의 click 이벤트는 무시
|
||||
if (justFinishedMarqueeRef.current) {
|
||||
justFinishedMarqueeRef.current = false;
|
||||
return;
|
||||
}
|
||||
if (e.target === e.currentTarget && !isMarqueeSelecting) {
|
||||
selectComponent(null);
|
||||
}
|
||||
};
|
||||
|
||||
// 드래그 영역 선택 시작
|
||||
const handleCanvasMouseDown = (e: React.MouseEvent<HTMLDivElement>) => {
|
||||
// 캔버스 자체를 클릭했을 때만 (컴포넌트 클릭 시 제외)
|
||||
if (e.target !== e.currentTarget) return;
|
||||
if (!canvasRef.current) return;
|
||||
|
||||
const rect = canvasRef.current.getBoundingClientRect();
|
||||
const x = e.clientX - rect.left;
|
||||
const y = e.clientY - rect.top;
|
||||
|
||||
// state와 ref 모두 설정
|
||||
setIsMarqueeSelecting(true);
|
||||
setMarqueeStart({ x, y });
|
||||
setMarqueeEnd({ x, y });
|
||||
marqueeStartRef.current = { x, y };
|
||||
marqueeEndRef.current = { x, y };
|
||||
|
||||
// Ctrl/Cmd 키가 눌리지 않았으면 기존 선택 해제
|
||||
if (!e.ctrlKey && !e.metaKey) {
|
||||
selectComponent(null);
|
||||
}
|
||||
};
|
||||
|
||||
// 드래그 영역 선택 중
|
||||
useEffect(() => {
|
||||
if (!isMarqueeSelecting) return;
|
||||
|
||||
const handleMouseMove = (e: MouseEvent) => {
|
||||
if (!canvasRef.current) return;
|
||||
|
||||
const rect = canvasRef.current.getBoundingClientRect();
|
||||
const x = Math.max(0, Math.min(e.clientX - rect.left, canvasWidth * MM_TO_PX));
|
||||
const y = Math.max(0, Math.min(e.clientY - rect.top, canvasHeight * MM_TO_PX));
|
||||
|
||||
// state와 ref 둘 다 업데이트
|
||||
setMarqueeEnd({ x, y });
|
||||
marqueeEndRef.current = { x, y };
|
||||
};
|
||||
|
||||
const handleMouseUp = () => {
|
||||
// ref에서 최신 값 가져오기 (클로저 문제 해결)
|
||||
const currentStart = marqueeStartRef.current;
|
||||
const currentEnd = marqueeEndRef.current;
|
||||
const currentComponents = componentsRef.current;
|
||||
const currentSelectMultiple = selectMultipleRef.current;
|
||||
|
||||
// 선택 영역 계산
|
||||
const selectionRect = {
|
||||
left: Math.min(currentStart.x, currentEnd.x),
|
||||
top: Math.min(currentStart.y, currentEnd.y),
|
||||
right: Math.max(currentStart.x, currentEnd.x),
|
||||
bottom: Math.max(currentStart.y, currentEnd.y),
|
||||
};
|
||||
|
||||
// 최소 드래그 거리 체크 (5px 이상이어야 선택으로 인식)
|
||||
const dragDistance = Math.sqrt(
|
||||
Math.pow(currentEnd.x - currentStart.x, 2) + Math.pow(currentEnd.y - currentStart.y, 2)
|
||||
);
|
||||
|
||||
if (dragDistance > 5) {
|
||||
// 선택 영역과 교차하는 컴포넌트 찾기
|
||||
const intersectingComponents = currentComponents.filter((comp) => {
|
||||
const compRect = {
|
||||
left: comp.x,
|
||||
top: comp.y,
|
||||
right: comp.x + comp.width,
|
||||
bottom: comp.y + comp.height,
|
||||
};
|
||||
|
||||
// 두 사각형이 교차하는지 확인
|
||||
return !(
|
||||
compRect.right < selectionRect.left ||
|
||||
compRect.left > selectionRect.right ||
|
||||
compRect.bottom < selectionRect.top ||
|
||||
compRect.top > selectionRect.bottom
|
||||
);
|
||||
});
|
||||
|
||||
// 교차하는 컴포넌트들 한번에 선택
|
||||
if (intersectingComponents.length > 0) {
|
||||
const ids = intersectingComponents.map((comp) => comp.id);
|
||||
currentSelectMultiple(ids);
|
||||
// click 이벤트가 선택을 해제하지 않도록 플래그 설정
|
||||
justFinishedMarqueeRef.current = true;
|
||||
}
|
||||
}
|
||||
|
||||
setIsMarqueeSelecting(false);
|
||||
};
|
||||
|
||||
document.addEventListener("mousemove", handleMouseMove);
|
||||
document.addEventListener("mouseup", handleMouseUp);
|
||||
|
||||
return () => {
|
||||
document.removeEventListener("mousemove", handleMouseMove);
|
||||
document.removeEventListener("mouseup", handleMouseUp);
|
||||
};
|
||||
}, [isMarqueeSelecting, canvasWidth, canvasHeight]);
|
||||
|
||||
// 선택 영역 사각형 계산
|
||||
const getMarqueeRect = () => {
|
||||
return {
|
||||
left: Math.min(marqueeStart.x, marqueeEnd.x),
|
||||
top: Math.min(marqueeStart.y, marqueeEnd.y),
|
||||
width: Math.abs(marqueeEnd.x - marqueeStart.x),
|
||||
height: Math.abs(marqueeEnd.y - marqueeStart.y),
|
||||
};
|
||||
};
|
||||
|
||||
// 키보드 단축키 (Delete, Ctrl+C, Ctrl+V, 화살표 이동)
|
||||
useEffect(() => {
|
||||
const handleKeyDown = (e: KeyboardEvent) => {
|
||||
|
|
@ -592,8 +724,10 @@ export function ReportDesignerCanvas() {
|
|||
`
|
||||
: undefined,
|
||||
backgroundSize: showGrid ? `${gridSize}px ${gridSize}px` : undefined,
|
||||
cursor: isMarqueeSelecting ? "crosshair" : "default",
|
||||
}}
|
||||
onClick={handleCanvasClick}
|
||||
onMouseDown={handleCanvasMouseDown}
|
||||
>
|
||||
{/* 페이지 여백 가이드 */}
|
||||
{currentPage && (
|
||||
|
|
@ -648,6 +782,20 @@ export function ReportDesignerCanvas() {
|
|||
<CanvasComponent key={component.id} component={component} />
|
||||
))}
|
||||
|
||||
{/* 드래그 영역 선택 사각형 */}
|
||||
{isMarqueeSelecting && (
|
||||
<div
|
||||
className="pointer-events-none absolute border-2 border-blue-500 bg-blue-500/10"
|
||||
style={{
|
||||
left: `${getMarqueeRect().left}px`,
|
||||
top: `${getMarqueeRect().top}px`,
|
||||
width: `${getMarqueeRect().width}px`,
|
||||
height: `${getMarqueeRect().height}px`,
|
||||
zIndex: 10000,
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* 빈 캔버스 안내 */}
|
||||
{components.length === 0 && (
|
||||
<div className="absolute inset-0 flex items-center justify-center text-gray-400">
|
||||
|
|
|
|||
|
|
@ -63,6 +63,7 @@ interface ReportDesignerContextType {
|
|||
updateComponent: (id: string, updates: Partial<ComponentConfig>) => void;
|
||||
removeComponent: (id: string) => void;
|
||||
selectComponent: (id: string | null, isMultiSelect?: boolean) => void;
|
||||
selectMultipleComponents: (ids: string[]) => void; // 여러 컴포넌트 한번에 선택
|
||||
|
||||
// 레이아웃 관리
|
||||
updateLayout: (updates: Partial<ReportLayout>) => void;
|
||||
|
|
@ -1344,6 +1345,17 @@ export function ReportDesignerProvider({ reportId, children }: { reportId: strin
|
|||
}
|
||||
}, []);
|
||||
|
||||
// 여러 컴포넌트 한번에 선택 (마퀴 선택용)
|
||||
const selectMultipleComponents = useCallback((ids: string[]) => {
|
||||
if (ids.length === 0) {
|
||||
setSelectedComponentId(null);
|
||||
setSelectedComponentIds([]);
|
||||
return;
|
||||
}
|
||||
setSelectedComponentId(ids[0]);
|
||||
setSelectedComponentIds(ids);
|
||||
}, []);
|
||||
|
||||
// 레이아웃 업데이트
|
||||
const updateLayout = useCallback(
|
||||
(updates: Partial<ReportLayout>) => {
|
||||
|
|
@ -1639,6 +1651,7 @@ export function ReportDesignerProvider({ reportId, children }: { reportId: strin
|
|||
updateComponent,
|
||||
removeComponent,
|
||||
selectComponent,
|
||||
selectMultipleComponents,
|
||||
updateLayout,
|
||||
saveLayout,
|
||||
loadLayout,
|
||||
|
|
|
|||
Loading…
Reference in New Issue