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

144 lines
4.9 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"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 } 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">
<DialogTitle> </DialogTitle>
{/* 디바이스 선택 버튼들 */}
<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>
);
};