ERP-node/frontend/components/screen/ResponsivePreviewModal.tsx

144 lines
4.9 KiB
TypeScript
Raw Normal View History

2025-10-17 10:12:41 +09:00
"use client";
import React, { useState, createContext, useContext } from "react";
2025-12-05 10:46:10 +09:00
import { Dialog, DialogContent, DialogHeader, DialogTitle } from "@/components/ui/dialog";
2025-10-17 10:12:41 +09:00
import { Button } from "@/components/ui/button";
import { Monitor, Tablet, Smartphone } from "lucide-react";
2025-10-17 10:12:41 +09:00
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">
2025-12-05 10:46:10 +09:00
<DialogTitle> </DialogTitle>
2025-10-17 10:12:41 +09:00
{/* 디바이스 선택 버튼들 */}
<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>
);
};