ERP-node/frontend/components/screen/ResponsiveDesignerContainer...

173 lines
5.3 KiB
TypeScript
Raw Permalink 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.

import React, { useState, useRef } from "react";
import { Button } from "@/components/ui/button";
import { Monitor, Maximize2, ZoomIn, ZoomOut } from "lucide-react";
import { useContainerSize } from "@/hooks/useViewportSize";
interface ResponsiveDesignerContainerProps {
children: React.ReactNode;
designWidth: number;
designHeight: number;
screenName?: string;
onScaleChange?: (scale: number) => void;
}
type DesignerViewMode = "fit" | "original" | "custom";
/**
* 화면 디자이너용 반응형 컨테이너
* 편집 작업을 위해 원본 크기 유지하면서도 뷰포트에 맞춰 조정 가능
*/
export const ResponsiveDesignerContainer: React.FC<ResponsiveDesignerContainerProps> = ({
children,
designWidth,
designHeight,
screenName,
onScaleChange,
}) => {
const containerRef = useRef<HTMLDivElement>(null);
const [viewMode, setViewMode] = useState<DesignerViewMode>("original");
const [customScale, setCustomScale] = useState(1);
const containerSize = useContainerSize(containerRef);
// 스케일 계산
const calculateScale = (): number => {
if (containerSize.width === 0 || containerSize.height === 0) return 1;
switch (viewMode) {
case "fit":
// 컨테이너에 맞춰 비율 유지하며 조정 (좌우 여백 16px씩 유지)
const scaleX = (containerSize.width - 32) / designWidth;
const scaleY = (containerSize.height - 64) / designHeight;
return Math.min(scaleX, scaleY, 2); // 최대 2배까지 허용
case "custom":
return customScale;
case "original":
default:
return 1;
}
};
const scale = calculateScale();
// 스케일 변경 시 콜백 호출
React.useEffect(() => {
onScaleChange?.(scale);
}, [scale, onScaleChange]);
const handleZoomIn = () => {
const newScale = Math.min(customScale * 1.1, 3);
setCustomScale(newScale);
setViewMode("custom");
};
const handleZoomOut = () => {
const newScale = Math.max(customScale * 0.9, 0.1);
setCustomScale(newScale);
setViewMode("custom");
};
const getViewModeInfo = (mode: DesignerViewMode) => {
switch (mode) {
case "fit":
return {
label: "화면 맞춤",
description: "뷰포트에 맞춰 자동 조정",
icon: <Monitor className="h-4 w-4" />,
};
case "original":
return {
label: "원본 크기",
description: "설계 해상도 100% 표시",
icon: <Maximize2 className="h-4 w-4" />,
};
case "custom":
return {
label: `사용자 정의 (${Math.round(customScale * 100)}%)`,
description: "사용자가 조정한 배율",
icon: <ZoomIn className="h-4 w-4" />,
};
}
};
const screenStyle = {
width: `${designWidth}px`,
height: `${designHeight}px`,
transform: `scale(${scale})`,
transformOrigin: "top left",
transition: "transform 0.3s ease-in-out",
};
const wrapperStyle = {
width: `${designWidth * scale}px`,
height: `${designHeight * scale}px`,
overflow: "hidden",
};
return (
<div className="flex h-full w-full flex-col bg-gray-100">
{/* 상단 컨트롤 바 */}
<div className="flex items-center justify-between border-b bg-white px-4 py-2 shadow-sm">
<div className="flex items-center space-x-2">
<span className="text-sm font-medium text-gray-700">
{screenName && `${screenName} - `}
{designWidth} × {designHeight}
</span>
<span className="text-xs text-gray-500">
(: {Math.round(scale * 100)}% | : {containerSize.width}×{containerSize.height})
</span>
</div>
<div className="flex items-center space-x-1">
{/* 줌 컨트롤 */}
<Button variant="ghost" size="sm" onClick={handleZoomOut} className="h-8 w-8 p-0" title="축소">
<ZoomOut className="h-4 w-4" />
</Button>
<span className="min-w-[60px] px-2 text-center text-xs text-muted-foreground">{Math.round(scale * 100)}%</span>
<Button variant="ghost" size="sm" onClick={handleZoomIn} className="h-8 w-8 p-0" title="확대">
<ZoomIn className="h-4 w-4" />
</Button>
{/* 뷰 모드 버튼 */}
{(["fit", "original"] as DesignerViewMode[]).map((mode) => {
const info = getViewModeInfo(mode);
return (
<Button
key={mode}
variant={viewMode === mode ? "default" : "ghost"}
size="sm"
onClick={() => setViewMode(mode)}
className="h-8 text-xs"
title={info.description}
>
{info.icon}
<span className="ml-1 hidden sm:inline">{info.label}</span>
</Button>
);
})}
</div>
</div>
{/* 디자인 영역 */}
<div
ref={containerRef}
className="flex-1 overflow-auto px-4 py-8"
style={{
justifyContent: "center",
alignItems: "flex-start",
display: "flex",
}}
>
<div style={wrapperStyle}>
<div style={screenStyle}>{children}</div>
</div>
</div>
</div>
);
};
export default ResponsiveDesignerContainer;