diff --git a/frontend/components/admin/dashboard/CanvasElement.tsx b/frontend/components/admin/dashboard/CanvasElement.tsx index 55d45480..9350642e 100644 --- a/frontend/components/admin/dashboard/CanvasElement.tsx +++ b/frontend/components/admin/dashboard/CanvasElement.tsx @@ -2,7 +2,7 @@ import React, { useState, useCallback, useRef, useEffect } from "react"; import dynamic from "next/dynamic"; -import { DashboardElement, QueryResult } from "./types"; +import { DashboardElement, QueryResult, Position } from "./types"; import { ChartRenderer } from "./charts/ChartRenderer"; import { GRID_CONFIG } from "./gridUtils"; @@ -105,7 +105,7 @@ import { CalendarWidget } from "./widgets/CalendarWidget"; // 기사 관리 위젯 임포트 import { DriverManagementWidget } from "./widgets/DriverManagementWidget"; import { ListWidget } from "./widgets/ListWidget"; -import { MoreHorizontal, X } from "lucide-react"; +import { X } from "lucide-react"; import { Button } from "@/components/ui/button"; // 야드 관리 3D 위젯 @@ -137,12 +137,11 @@ interface CanvasElementProps { canvasWidth?: number; onUpdate: (id: string, updates: Partial) => void; onUpdateMultiple?: (updates: { id: string; updates: Partial }[]) => void; // 🔥 다중 업데이트 - onMultiDragStart?: (draggedId: string, otherOffsets: Record) => void; // 🔥 다중 드래그 시작 - onMultiDragMove?: (draggedElement: DashboardElement, tempPosition: { x: number; y: number }) => void; // 🔥 다중 드래그 중 - onMultiDragEnd?: () => void; // 🔥 다중 드래그 종료 + onMultiDragStart?: (draggedId: string, otherOffsets: Record) => void; + onMultiDragMove?: (draggedElement: DashboardElement, tempPosition: { x: number; y: number }) => void; + onMultiDragEnd?: () => void; onRemove: (id: string) => void; onSelect: (id: string | null) => void; - onConfigure?: (element: DashboardElement) => void; } /** @@ -167,7 +166,6 @@ export function CanvasElement({ onMultiDragEnd, onRemove, onSelect, - onConfigure, }: CanvasElementProps) { const [isDragging, setIsDragging] = useState(false); const [isResizing, setIsResizing] = useState(false); @@ -226,10 +224,10 @@ export function CanvasElement({ }; setDragStart(startPos); dragStartRef.current = startPos; // 🔥 ref에도 저장 - + // 🔥 드래그 시작 시 마우스 위치 초기화 (화면 중간) lastMouseYRef.current = window.innerHeight / 2; - + // 🔥 다중 선택된 경우, 다른 위젯들의 오프셋 계산 if (selectedElements.length > 1 && selectedElements.includes(element.id) && onMultiDragStart) { const offsets: Record = {}; @@ -246,10 +244,19 @@ export function CanvasElement({ }); onMultiDragStart(element.id, offsets); } - + e.preventDefault(); }, - [element.id, element.position.x, element.position.y, onSelect, isSelected, selectedElements, allElements, onMultiDragStart], + [ + element.id, + element.position.x, + element.position.y, + onSelect, + isSelected, + selectedElements, + allElements, + onMultiDragStart, + ], ); // 리사이즈 핸들 마우스다운 @@ -280,22 +287,23 @@ export function CanvasElement({ (e: MouseEvent) => { if (isDragging) { // 🔥 자동 스크롤: 다중 선택 시 첫 번째 위젯에서만 처리 - const isFirstSelectedElement = !selectedElements || selectedElements.length === 0 || selectedElements[0] === element.id; - + 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; // 🔥 스크롤 변화량 반영 @@ -312,7 +320,7 @@ export function CanvasElement({ const snappedY = Math.round(rawY / subGridSize) * subGridSize; setTempPosition({ x: snappedX, y: snappedY }); - + // 🔥 다중 드래그 중 - 다른 위젯들의 위치 업데이트 if (selectedElements.length > 1 && selectedElements.includes(element.id) && onMultiDragMove) { onMultiDragMove(element, { x: snappedX, y: snappedY }); @@ -410,11 +418,20 @@ export function CanvasElement({ .map((id) => { const targetElement = allElements.find((el) => el.id === id); if (!targetElement) return null; - + // 현재 요소와의 상대적 위치 유지 const relativeX = targetElement.position.x - dragStart.elementX; const relativeY = targetElement.position.y - dragStart.elementY; - + + const newPosition: Position = { + x: Math.max(0, Math.min(canvasWidth - targetElement.size.width, finalX + relativeX)), + y: Math.max(0, finalY + relativeY), + }; + + if (targetElement.position.z !== undefined) { + newPosition.z = targetElement.position.z; + } + return { id, updates: { @@ -425,8 +442,8 @@ export function CanvasElement({ }, }; }) - .filter((update): update is { id: string; updates: Partial } => update !== null); - + .filter((update): update is { id: string; updates: { position: Position } } => update !== null); + if (updates.length > 0) { // console.log("🔥 다중 선택 요소 함께 이동:", updates); onUpdateMultiple(updates); @@ -434,7 +451,7 @@ export function CanvasElement({ } setTempPosition(null); - + // 🔥 다중 드래그 종료 if (onMultiDragEnd) { onMultiDragEnd(); @@ -464,7 +481,7 @@ export function CanvasElement({ setIsDragging(false); setIsResizing(false); - + // 🔥 자동 스크롤 정리 autoScrollDirectionRef.current = null; if (autoScrollFrameRef.current) { @@ -501,32 +518,32 @@ export function CanvasElement({ 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; - } + + 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); @@ -671,10 +688,15 @@ export function CanvasElement({ // 드래그/리사이즈 중일 때는 임시 위치/크기 사용, 아니면 실제 값 사용 // 🔥 다중 드래그 중이면 multiDragOffset 적용 (단, 드래그 중인 위젯은 tempPosition 우선) - const displayPosition = tempPosition || (multiDragOffset && !isDragging ? { - x: element.position.x + multiDragOffset.x, - y: element.position.y + multiDragOffset.y, - } : element.position); + const displayPosition: Position = + tempPosition || + (multiDragOffset && !isDragging + ? { + x: element.position.x + multiDragOffset.x, + y: element.position.y + multiDragOffset.y, + ...(element.position.z !== undefined && { z: element.position.z }), + } + : element.position); const displaySize = tempSize || element.size; return ( @@ -696,18 +718,6 @@ export function CanvasElement({
{element.customTitle || element.title}
- {/* 설정 버튼 (기사관리 위젯만 자체 설정 UI 사용) */} - {onConfigure && !(element.type === "widget" && element.subtype === "driver-management") && ( - - )} {/* 삭제 버튼 */} + +
+
+ ); +} diff --git a/frontend/components/admin/dashboard/QueryEditor.tsx b/frontend/components/admin/dashboard/QueryEditor.tsx index 1335b243..a02cb9cf 100644 --- a/frontend/components/admin/dashboard/QueryEditor.tsx +++ b/frontend/components/admin/dashboard/QueryEditor.tsx @@ -12,7 +12,8 @@ import { Card } from "@/components/ui/card"; import { Badge } from "@/components/ui/badge"; import { Alert, AlertDescription } from "@/components/ui/alert"; import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table"; -import { Play, Loader2, Database, Code } from "lucide-react"; +import { Collapsible, CollapsibleContent, CollapsibleTrigger } from "@/components/ui/collapsible"; +import { Play, Loader2, Database, Code, ChevronDown, ChevronRight } from "lucide-react"; import { applyQueryFilters } from "./utils/queryHelpers"; interface QueryEditorProps { @@ -32,6 +33,7 @@ export function QueryEditor({ dataSource, onDataSourceChange, onQueryTest }: Que const [isExecuting, setIsExecuting] = useState(false); const [queryResult, setQueryResult] = useState(null); const [error, setError] = useState(null); + const [sampleQueryOpen, setSampleQueryOpen] = useState(false); // 쿼리 실행 const executeQuery = useCallback(async () => { @@ -155,55 +157,75 @@ ORDER BY 하위부서수 DESC`, }, []); return ( -
+
{/* 쿼리 에디터 헤더 */}
-
- -

SQL 쿼리 에디터

+
+ +

SQL 쿼리 에디터

-
- {/* 샘플 쿼리 버튼들 */} - -
- - - - - - -
-
+ {/* 샘플 쿼리 아코디언 */} + + + {sampleQueryOpen ? : } + 샘플 쿼리 + + +
+ + + + + +
+
+
{/* SQL 쿼리 입력 영역 */} -
- +
+