From 5a8efa51af2c3a222459a5c96f1286e02e8af6f0 Mon Sep 17 00:00:00 2001 From: kjs Date: Wed, 15 Oct 2025 10:44:05 +0900 Subject: [PATCH] =?UTF-8?q?=EC=BA=94=EB=B2=84=EC=8A=A4=20=EC=9D=B4?= =?UTF-8?q?=EB=8F=99=20=EB=B0=8F=20=EC=A4=8C=EA=B8=B0=EB=8A=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/app/(main)/admin/screenMng/page.tsx | 8 +- frontend/components/screen/ScreenDesigner.tsx | 152 ++++++++++++++++-- .../components/screen/toolbar/SlimToolbar.tsx | 19 ++- 3 files changed, 165 insertions(+), 14 deletions(-) diff --git a/frontend/app/(main)/admin/screenMng/page.tsx b/frontend/app/(main)/admin/screenMng/page.tsx index 94cb2dfa..ae622128 100644 --- a/frontend/app/(main)/admin/screenMng/page.tsx +++ b/frontend/app/(main)/admin/screenMng/page.tsx @@ -68,9 +68,13 @@ export default function ScreenManagementPage() { // 현재 단계가 마지막 단계인지 확인 const isLastStep = currentStep === "template"; - // 화면 설계 모드일 때는 레이아웃 없이 전체 화면 사용 + // 화면 설계 모드일 때는 레이아웃 없이 전체 화면 사용 (고정 높이) if (isDesignMode) { - return goToStep("list")} />; + return ( +
+ goToStep("list")} /> +
+ ); } return ( diff --git a/frontend/components/screen/ScreenDesigner.tsx b/frontend/components/screen/ScreenDesigner.tsx index 0061a4b2..7ddafd80 100644 --- a/frontend/components/screen/ScreenDesigner.tsx +++ b/frontend/components/screen/ScreenDesigner.tsx @@ -187,6 +187,22 @@ export default function ScreenDesigner({ selectedScreen, onBackToList }: ScreenD justFinishedDrag: false, // 드래그 종료 직후 클릭 방지용 }); + // Pan 모드 상태 (스페이스바 + 드래그) + const [isPanMode, setIsPanMode] = useState(false); + const [panState, setPanState] = useState({ + isPanning: false, + startX: 0, + startY: 0, + scrollLeft: 0, + scrollTop: 0, + }); + const canvasContainerRef = useRef(null); + + // Zoom 상태 + const [zoomLevel, setZoomLevel] = useState(1); // 1 = 100% + const MIN_ZOOM = 0.1; // 10% + const MAX_ZOOM = 3; // 300% + // 전역 파일 상태 변경 시 강제 리렌더링을 위한 상태 const [forceRenderTrigger, setForceRenderTrigger] = useState(0); @@ -973,6 +989,77 @@ export default function ScreenDesigner({ selectedScreen, onBackToList }: ScreenD } }, [selectedScreen?.screenId]); + // 스페이스바 키 이벤트 처리 (Pan 모드) + useEffect(() => { + const handleKeyDown = (e: KeyboardEvent) => { + // 입력 필드에서는 스페이스바 무시 + if (e.target instanceof HTMLInputElement || e.target instanceof HTMLTextAreaElement) { + return; + } + + if (e.code === "Space") { + e.preventDefault(); // 스페이스바 기본 스크롤 동작 차단 + if (!isPanMode) { + setIsPanMode(true); + // 커서 변경 + if (canvasContainerRef.current) { + canvasContainerRef.current.style.cursor = "grab"; + } + } + } + }; + + const handleKeyUp = (e: KeyboardEvent) => { + if (e.code === "Space") { + e.preventDefault(); // 스페이스바 기본 스크롤 동작 차단 + setIsPanMode(false); + setPanState((prev) => ({ ...prev, isPanning: false })); + // 커서 복원 + if (canvasContainerRef.current) { + canvasContainerRef.current.style.cursor = "default"; + } + } + }; + + window.addEventListener("keydown", handleKeyDown); + window.addEventListener("keyup", handleKeyUp); + + return () => { + window.removeEventListener("keydown", handleKeyDown); + window.removeEventListener("keyup", handleKeyUp); + }; + }, [isPanMode]); + + // 마우스 휠로 줌 제어 + useEffect(() => { + const handleWheel = (e: WheelEvent) => { + // 캔버스 컨테이너 내에서만 동작 + if (canvasContainerRef.current && canvasContainerRef.current.contains(e.target as Node)) { + // Shift 키를 누르지 않은 경우에만 줌 (Shift + 휠은 수평 스크롤용) + if (!e.shiftKey && !e.ctrlKey && !e.metaKey) { + // 기본 스크롤 동작 방지 + e.preventDefault(); + + const delta = e.deltaY; + const zoomFactor = 0.001; // 줌 속도 조절 + + setZoomLevel((prevZoom) => { + const newZoom = prevZoom - delta * zoomFactor; + return Math.max(MIN_ZOOM, Math.min(MAX_ZOOM, newZoom)); + }); + } + } + }; + + // passive: false로 설정하여 preventDefault() 가능하게 함 + canvasContainerRef.current?.addEventListener("wheel", handleWheel, { passive: false }); + + const containerRef = canvasContainerRef.current; + return () => { + containerRef?.removeEventListener("wheel", handleWheel); + }; + }, [MIN_ZOOM, MAX_ZOOM]); + // 격자 설정 업데이트 및 컴포넌트 자동 스냅 const updateGridSettings = useCallback( (newGridSettings: GridSettings) => { @@ -3538,11 +3625,12 @@ export default function ScreenDesigner({ selectedScreen, onBackToList }: ScreenD } return ( -
+
{/* 상단 슬림 툴바 */} - {/* 해상도 정보 표시 - 적당한 여백 */} -
-
- - {screenResolution.name} ({screenResolution.width} × {screenResolution.height}) - +
{ + if (isPanMode && canvasContainerRef.current) { + e.preventDefault(); + setPanState({ + isPanning: true, + startX: e.pageX, + startY: e.pageY, + scrollLeft: canvasContainerRef.current.scrollLeft, + scrollTop: canvasContainerRef.current.scrollTop, + }); + } + }} + onMouseMove={(e) => { + if (isPanMode && panState.isPanning && canvasContainerRef.current) { + e.preventDefault(); + const dx = e.pageX - panState.startX; + const dy = e.pageY - panState.startY; + canvasContainerRef.current.scrollLeft = panState.scrollLeft - dx; + canvasContainerRef.current.scrollTop = panState.scrollTop - dy; + } + }} + onMouseUp={() => { + if (isPanMode) { + setPanState((prev) => ({ ...prev, isPanning: false })); + } + }} + onMouseLeave={() => { + if (isPanMode) { + setPanState((prev) => ({ ...prev, isPanning: false })); + } + }} + > + {/* Pan 모드 안내 */} + {isPanMode && ( +
+ 🖐️ Pan 모드 활성화 - 드래그하여 캔버스 이동
+ )} + + {/* 줌 레벨 표시 */} +
+ 🔍 {Math.round(zoomLevel * 100)}%
- {/* 실제 작업 캔버스 (해상도 크기) - 반응형 개선 */} + {/* 실제 작업 캔버스 (해상도 크기) - 반응형 개선 + 줌 적용 */}
{ - if (e.target === e.currentTarget && !selectionDrag.wasSelecting) { + if (e.target === e.currentTarget && !selectionDrag.wasSelecting && !isPanMode) { setSelectedComponent(null); setGroupState((prev) => ({ ...prev, selectedComponents: [] })); } }} onMouseDown={(e) => { - if (e.target === e.currentTarget) { + // Pan 모드가 아닐 때만 다중 선택 시작 + if (e.target === e.currentTarget && !isPanMode) { startSelectionDrag(e); } }} diff --git a/frontend/components/screen/toolbar/SlimToolbar.tsx b/frontend/components/screen/toolbar/SlimToolbar.tsx index 31e0bc08..3cc2bc20 100644 --- a/frontend/components/screen/toolbar/SlimToolbar.tsx +++ b/frontend/components/screen/toolbar/SlimToolbar.tsx @@ -2,12 +2,14 @@ import React from "react"; import { Button } from "@/components/ui/button"; -import { Database, ArrowLeft, Undo, Redo, Play, Save } from "lucide-react"; +import { Database, ArrowLeft, Undo, Redo, Play, Save, Monitor } from "lucide-react"; import { cn } from "@/lib/utils"; +import { ScreenResolution } from "@/types/screen"; interface SlimToolbarProps { screenName?: string; tableName?: string; + screenResolution?: ScreenResolution; onBack: () => void; onSave: () => void; onUndo: () => void; @@ -21,6 +23,7 @@ interface SlimToolbarProps { export const SlimToolbar: React.FC = ({ screenName, tableName, + screenResolution, onBack, onSave, onUndo, @@ -52,6 +55,20 @@ export const SlimToolbar: React.FC = ({ )}
+ + {/* 해상도 정보 표시 */} + {screenResolution && ( + <> +
+
+ + {screenResolution.name} + + ({screenResolution.width} × {screenResolution.height}) + +
+ + )}
{/* 우측: 액션 버튼들 */}