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";
|
2025-10-21 17:48:24 +09:00
|
|
|
|
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>
|
|
|
|
|
|
);
|
|
|
|
|
|
};
|