캔버스 이동 및 줌기능
This commit is contained in:
parent
4cb967fea6
commit
5a8efa51af
|
|
@ -68,9 +68,13 @@ export default function ScreenManagementPage() {
|
|||
// 현재 단계가 마지막 단계인지 확인
|
||||
const isLastStep = currentStep === "template";
|
||||
|
||||
// 화면 설계 모드일 때는 레이아웃 없이 전체 화면 사용
|
||||
// 화면 설계 모드일 때는 레이아웃 없이 전체 화면 사용 (고정 높이)
|
||||
if (isDesignMode) {
|
||||
return <ScreenDesigner selectedScreen={selectedScreen} onBackToList={() => goToStep("list")} />;
|
||||
return (
|
||||
<div className="fixed inset-0 z-50 bg-white">
|
||||
<ScreenDesigner selectedScreen={selectedScreen} onBackToList={() => goToStep("list")} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
|
|
|
|||
|
|
@ -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<HTMLDivElement>(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 (
|
||||
<div className="flex h-screen w-full flex-col bg-gradient-to-br from-gray-50 to-slate-100">
|
||||
<div className="flex h-full w-full flex-col bg-gradient-to-br from-gray-50 to-slate-100">
|
||||
{/* 상단 슬림 툴바 */}
|
||||
<SlimToolbar
|
||||
screenName={selectedScreen?.screenName}
|
||||
tableName={selectedScreen?.tableName}
|
||||
screenResolution={screenResolution}
|
||||
onBack={onBackToList}
|
||||
onSave={handleSave}
|
||||
onUndo={undo}
|
||||
|
|
@ -3666,36 +3754,78 @@ export default function ScreenDesigner({ selectedScreen, onBackToList }: ScreenD
|
|||
)}
|
||||
|
||||
{/* 메인 캔버스 영역 (스크롤 가능한 컨테이너) - 좌우 최소화, 위아래 넉넉한 여유 */}
|
||||
<div className="relative flex-1 overflow-auto bg-gradient-to-br from-gray-50 to-slate-100 px-2 py-6">
|
||||
{/* 해상도 정보 표시 - 적당한 여백 */}
|
||||
<div className="mb-4 flex items-center justify-center">
|
||||
<div className="rounded-lg border bg-white px-4 py-2 shadow-sm">
|
||||
<span className="text-sm font-medium text-gray-700">
|
||||
{screenResolution.name} ({screenResolution.width} × {screenResolution.height})
|
||||
</span>
|
||||
<div
|
||||
ref={canvasContainerRef}
|
||||
className="relative flex-1 overflow-auto bg-gradient-to-br from-gray-50 to-slate-100 px-2 py-6"
|
||||
style={{ cursor: isPanMode ? (panState.isPanning ? "grabbing" : "grab") : "default" }}
|
||||
onMouseDown={(e) => {
|
||||
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 && (
|
||||
<div className="pointer-events-none fixed top-20 left-1/2 z-50 -translate-x-1/2 transform rounded-lg bg-blue-600 px-4 py-2 text-sm font-medium text-white shadow-lg">
|
||||
🖐️ Pan 모드 활성화 - 드래그하여 캔버스 이동
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 줌 레벨 표시 */}
|
||||
<div className="pointer-events-none fixed right-6 bottom-6 z-50 rounded-lg bg-white px-4 py-2 text-sm font-medium text-gray-700 shadow-lg ring-1 ring-gray-200">
|
||||
🔍 {Math.round(zoomLevel * 100)}%
|
||||
</div>
|
||||
|
||||
{/* 실제 작업 캔버스 (해상도 크기) - 반응형 개선 */}
|
||||
{/* 실제 작업 캔버스 (해상도 크기) - 반응형 개선 + 줌 적용 */}
|
||||
<div
|
||||
className="mx-auto bg-white shadow-lg"
|
||||
style={{
|
||||
width: screenResolution.width,
|
||||
height: Math.max(screenResolution.height, 800), // 최소 높이 보장
|
||||
minHeight: screenResolution.height,
|
||||
transform: `scale(${zoomLevel})`, // 줌 레벨에 따라 시각적으로 확대/축소
|
||||
transformOrigin: "top center",
|
||||
transition: "transform 0.1s ease-out",
|
||||
}}
|
||||
>
|
||||
<div
|
||||
ref={canvasRef}
|
||||
className="relative h-full w-full overflow-auto bg-gradient-to-br from-slate-50/30 to-gray-100/20" // 미묘한 그라데이션 배경
|
||||
onClick={(e) => {
|
||||
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);
|
||||
}
|
||||
}}
|
||||
|
|
|
|||
|
|
@ -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<SlimToolbarProps> = ({
|
||||
screenName,
|
||||
tableName,
|
||||
screenResolution,
|
||||
onBack,
|
||||
onSave,
|
||||
onUndo,
|
||||
|
|
@ -52,6 +55,20 @@ export const SlimToolbar: React.FC<SlimToolbarProps> = ({
|
|||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 해상도 정보 표시 */}
|
||||
{screenResolution && (
|
||||
<>
|
||||
<div className="h-6 w-px bg-gray-300" />
|
||||
<div className="flex items-center space-x-2 rounded-md bg-blue-50 px-3 py-1.5">
|
||||
<Monitor className="h-4 w-4 text-blue-600" />
|
||||
<span className="text-sm font-medium text-blue-900">{screenResolution.name}</span>
|
||||
<span className="text-xs text-blue-600">
|
||||
({screenResolution.width} × {screenResolution.height})
|
||||
</span>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* 우측: 액션 버튼들 */}
|
||||
|
|
|
|||
Loading…
Reference in New Issue