144 lines
4.9 KiB
TypeScript
144 lines
4.9 KiB
TypeScript
"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>
|
||
);
|
||
};
|