Merge branch 'main' of http://39.117.244.52:3000/kjs/ERP-node into feat/dashboard

This commit is contained in:
dohyeons
2025-10-22 15:33:07 +09:00
40 changed files with 5490 additions and 3504 deletions

View File

@@ -170,6 +170,10 @@ export function CanvasElement({
const [isDragging, setIsDragging] = useState(false);
const [isResizing, setIsResizing] = useState(false);
const [dragStart, setDragStart] = useState({ x: 0, y: 0, elementX: 0, elementY: 0 });
const dragStartRef = useRef({ x: 0, y: 0, elementX: 0, elementY: 0, initialScrollY: 0 }); // 🔥 스크롤 조정용 ref
const autoScrollDirectionRef = useRef<"up" | "down" | null>(null); // 🔥 자동 스크롤 방향
const autoScrollFrameRef = useRef<number | null>(null); // 🔥 requestAnimationFrame ID
const lastMouseYRef = useRef<number>(window.innerHeight / 2); // 🔥 마지막 마우스 Y 위치 (초기값: 화면 중간)
const [resizeStart, setResizeStart] = useState({
x: 0,
y: 0,
@@ -211,12 +215,18 @@ export function CanvasElement({
}
setIsDragging(true);
setDragStart({
const startPos = {
x: e.clientX,
y: e.clientY,
elementX: element.position.x,
elementY: element.position.y,
});
initialScrollY: window.pageYOffset, // 🔥 드래그 시작 시점의 스크롤 위치
};
setDragStart(startPos);
dragStartRef.current = startPos; // 🔥 ref에도 저장
// 🔥 드래그 시작 시 마우스 위치 초기화 (화면 중간)
lastMouseYRef.current = window.innerHeight / 2;
// 🔥 다중 선택된 경우, 다른 위젯들의 오프셋 계산
if (selectedElements.length > 1 && selectedElements.includes(element.id) && onMultiDragStart) {
@@ -276,8 +286,26 @@ export function CanvasElement({
const handleMouseMove = useCallback(
(e: MouseEvent) => {
if (isDragging) {
const deltaX = e.clientX - dragStart.x;
const deltaY = e.clientY - dragStart.y;
// 🔥 자동 스크롤: 다중 선택 시 첫 번째 위젯에서만 처리
const isFirstSelectedElement =
!selectedElements || selectedElements.length === 0 || selectedElements[0] === element.id;
if (isFirstSelectedElement) {
const scrollThreshold = 100;
const viewportHeight = window.innerHeight;
const mouseY = e.clientY;
// 🔥 항상 마우스 위치 업데이트
lastMouseYRef.current = mouseY;
// console.log("🖱️ 마우스 위치 업데이트:", { mouseY, viewportHeight, top: scrollThreshold, bottom: viewportHeight - scrollThreshold });
}
// 🔥 현재 스크롤 위치를 고려한 deltaY 계산
const currentScrollY = window.pageYOffset;
const scrollDelta = currentScrollY - dragStartRef.current.initialScrollY;
const deltaX = e.clientX - dragStartRef.current.x;
const deltaY = e.clientY - dragStartRef.current.y + scrollDelta; // 🔥 스크롤 변화량 반영
// 임시 위치 계산
let rawX = Math.max(0, dragStart.elementX + deltaX);
@@ -363,6 +391,7 @@ export function CanvasElement({
allElements,
onUpdateMultiple,
onMultiDragMove,
// dragStartRef, autoScrollDirectionRef, autoScrollFrameRef는 ref라서 dependency 불필요
],
);
@@ -406,15 +435,18 @@ export function CanvasElement({
return {
id,
updates: {
position: newPosition,
position: {
x: Math.max(0, Math.min(canvasWidth - targetElement.size.width, finalX + relativeX)),
y: Math.max(0, finalY + relativeY),
},
},
};
})
.filter((update): update is { id: string; updates: { position: Position } } => update !== null);
if (updates.length > 0) {
console.log("🔥 다중 선택 요소 함께 이동:", updates);
onUpdateMultiple(updates as { id: string; updates: Partial<DashboardElement> }[]);
// console.log("🔥 다중 선택 요소 함께 이동:", updates);
onUpdateMultiple(updates);
}
}
@@ -449,6 +481,13 @@ export function CanvasElement({
setIsDragging(false);
setIsResizing(false);
// 🔥 자동 스크롤 정리
autoScrollDirectionRef.current = null;
if (autoScrollFrameRef.current) {
cancelAnimationFrame(autoScrollFrameRef.current);
autoScrollFrameRef.current = null;
}
}, [
isDragging,
isResizing,
@@ -467,6 +506,60 @@ export function CanvasElement({
dragStart.elementY,
]);
// 🔥 자동 스크롤 루프 (requestAnimationFrame 사용)
useEffect(() => {
if (!isDragging) return;
const scrollSpeed = 3; // 🔥 속도를 좀 더 부드럽게 (5 → 3)
const scrollThreshold = 100;
let animationFrameId: number;
let lastTime = performance.now();
const autoScrollLoop = (currentTime: number) => {
const viewportHeight = window.innerHeight;
const lastMouseY = lastMouseYRef.current;
// 🔥 스크롤 방향 결정
let shouldScroll = false;
let scrollDirection = 0;
if (lastMouseY < scrollThreshold) {
// 위쪽 영역
shouldScroll = true;
scrollDirection = -scrollSpeed;
// console.log("⬆️ 위로 스크롤 조건 만족:", { lastMouseY, scrollThreshold });
} else if (lastMouseY > viewportHeight - scrollThreshold) {
// 아래쪽 영역
shouldScroll = true;
scrollDirection = scrollSpeed;
// console.log("⬇️ 아래로 스크롤 조건 만족:", { lastMouseY, boundary: viewportHeight - scrollThreshold });
}
// 🔥 프레임 간격 계산
const deltaTime = currentTime - lastTime;
// 🔥 10ms 간격으로 스크롤
if (shouldScroll && deltaTime >= 10) {
window.scrollBy(0, scrollDirection);
// console.log("✅ 스크롤 실행:", { scrollDirection, deltaTime });
lastTime = currentTime;
}
// 계속 반복
animationFrameId = requestAnimationFrame(autoScrollLoop);
};
// 루프 시작
animationFrameId = requestAnimationFrame(autoScrollLoop);
autoScrollFrameRef.current = animationFrameId;
return () => {
if (animationFrameId) {
cancelAnimationFrame(animationFrameId);
}
};
}, [isDragging]);
// 전역 마우스 이벤트 등록
React.useEffect(() => {
if (isDragging || isResizing) {