반응형 미리보기 기능
This commit is contained in:
148
frontend/components/screen/ResponsivePreviewModal.tsx
Normal file
148
frontend/components/screen/ResponsivePreviewModal.tsx
Normal file
@@ -0,0 +1,148 @@
|
||||
"use client";
|
||||
|
||||
import React, { useState, createContext, useContext } from "react";
|
||||
import { Dialog, DialogContent, DialogHeader, DialogTitle } from "@/components/ui/dialog";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Monitor, Tablet, Smartphone, X } from "lucide-react";
|
||||
import { ComponentData } from "@/types/screen";
|
||||
import { ResponsiveLayoutEngine } from "./ResponsiveLayoutEngine";
|
||||
import { Breakpoint } from "@/types/responsive";
|
||||
|
||||
// 미리보기 모달용 브레이크포인트 Context
|
||||
const PreviewBreakpointContext = createContext<Breakpoint | null>(null);
|
||||
|
||||
// 미리보기 모달 내에서 브레이크포인트를 가져오는 훅
|
||||
export const usePreviewBreakpoint = (): Breakpoint | null => {
|
||||
return useContext(PreviewBreakpointContext);
|
||||
};
|
||||
|
||||
interface ResponsivePreviewModalProps {
|
||||
isOpen: boolean;
|
||||
onClose: () => void;
|
||||
components: ComponentData[];
|
||||
screenWidth: number;
|
||||
}
|
||||
|
||||
type DevicePreset = {
|
||||
name: string;
|
||||
width: number;
|
||||
height: number;
|
||||
icon: React.ReactNode;
|
||||
breakpoint: Breakpoint;
|
||||
};
|
||||
|
||||
const DEVICE_PRESETS: DevicePreset[] = [
|
||||
{
|
||||
name: "데스크톱",
|
||||
width: 1920,
|
||||
height: 1080,
|
||||
icon: <Monitor className="h-4 w-4" />,
|
||||
breakpoint: "desktop",
|
||||
},
|
||||
{
|
||||
name: "태블릿",
|
||||
width: 768,
|
||||
height: 1024,
|
||||
icon: <Tablet className="h-4 w-4" />,
|
||||
breakpoint: "tablet",
|
||||
},
|
||||
{
|
||||
name: "모바일",
|
||||
width: 375,
|
||||
height: 667,
|
||||
icon: <Smartphone className="h-4 w-4" />,
|
||||
breakpoint: "mobile",
|
||||
},
|
||||
];
|
||||
|
||||
export const ResponsivePreviewModal: React.FC<ResponsivePreviewModalProps> = ({
|
||||
isOpen,
|
||||
onClose,
|
||||
components,
|
||||
screenWidth,
|
||||
}) => {
|
||||
const [selectedDevice, setSelectedDevice] = useState<DevicePreset>(DEVICE_PRESETS[0]);
|
||||
const [scale, setScale] = useState(1);
|
||||
|
||||
// 스케일 계산: 모달 내에서 디바이스가 잘 보이도록
|
||||
React.useEffect(() => {
|
||||
// 모달 내부 너비를 1400px로 가정하고 여백 100px 제외
|
||||
const maxWidth = 1300;
|
||||
const calculatedScale = Math.min(1, maxWidth / selectedDevice.width);
|
||||
setScale(calculatedScale);
|
||||
}, [selectedDevice]);
|
||||
|
||||
return (
|
||||
<Dialog open={isOpen} onOpenChange={onClose}>
|
||||
<DialogContent className="max-h-[95vh] max-w-[95vw] p-0">
|
||||
<DialogHeader className="border-b px-6 pt-6 pb-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<DialogTitle>반응형 미리보기</DialogTitle>
|
||||
<Button variant="ghost" size="icon" onClick={onClose}>
|
||||
<X className="h-4 w-4" />
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{/* 디바이스 선택 버튼들 */}
|
||||
<div className="mt-4 flex gap-2">
|
||||
{DEVICE_PRESETS.map((device) => (
|
||||
<Button
|
||||
key={device.name}
|
||||
variant={selectedDevice.name === device.name ? "default" : "outline"}
|
||||
size="sm"
|
||||
onClick={() => setSelectedDevice(device)}
|
||||
className="gap-2"
|
||||
>
|
||||
{device.icon}
|
||||
<span>{device.name}</span>
|
||||
<span className="text-xs opacity-70">
|
||||
{device.width}×{device.height}
|
||||
</span>
|
||||
</Button>
|
||||
))}
|
||||
</div>
|
||||
</DialogHeader>
|
||||
|
||||
{/* 미리보기 영역 - Context Provider로 감싸서 브레이크포인트 전달 */}
|
||||
<PreviewBreakpointContext.Provider value={selectedDevice.breakpoint}>
|
||||
<div className="flex min-h-[600px] items-start justify-center overflow-auto bg-gray-50 p-6">
|
||||
<div
|
||||
className="relative border border-gray-300 bg-white shadow-2xl"
|
||||
style={{
|
||||
width: `${selectedDevice.width}px`,
|
||||
height: `${selectedDevice.height}px`,
|
||||
transform: `scale(${scale})`,
|
||||
transformOrigin: "top center",
|
||||
overflow: "auto",
|
||||
}}
|
||||
>
|
||||
{/* 디바이스 프레임 헤더 (선택사항) */}
|
||||
<div className="sticky top-0 z-10 flex items-center justify-between border-b border-gray-300 bg-gray-100 px-4 py-2">
|
||||
<div className="text-xs text-gray-600">
|
||||
{selectedDevice.name} - {selectedDevice.width}×{selectedDevice.height}
|
||||
</div>
|
||||
<div className="text-xs text-gray-500">스케일: {Math.round(scale * 100)}%</div>
|
||||
</div>
|
||||
|
||||
{/* 실제 컴포넌트 렌더링 */}
|
||||
<div className="p-4">
|
||||
<ResponsiveLayoutEngine
|
||||
components={components}
|
||||
breakpoint={selectedDevice.breakpoint}
|
||||
containerWidth={selectedDevice.width}
|
||||
screenWidth={screenWidth}
|
||||
preserveYPosition={selectedDevice.breakpoint === "desktop"}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</PreviewBreakpointContext.Provider>
|
||||
|
||||
{/* 푸터 정보 */}
|
||||
<div className="border-t bg-gray-50 px-6 py-3 text-xs text-gray-600">
|
||||
💡 Tip: 각 디바이스 버튼을 클릭하여 다양한 화면 크기에서 레이아웃을 확인할 수 있습니다.
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
};
|
||||
Reference in New Issue
Block a user