"use client"; import { useCallback, useRef, useState, useEffect } from "react"; import { useDrop } from "react-dnd"; import { cn } from "@/lib/utils"; import { PopLayoutDataV4, PopContainerV4, PopComponentDefinitionV4, PopComponentType, PopSizeConstraintV4, } from "./types/pop-layout"; import { DND_ITEM_TYPES, DragItemComponent } from "./panels/PopPanel"; import { ZoomIn, ZoomOut, Maximize2, Smartphone, Tablet, RotateCcw, Lock } from "lucide-react"; import { Button } from "@/components/ui/button"; import { PopFlexRenderer } from "./renderers/PopFlexRenderer"; // ======================================== // 프리셋 해상도 (4개 모드) // ======================================== const VIEWPORT_PRESETS = [ { id: "mobile_portrait", label: "모바일 세로", shortLabel: "모바일↕", width: 375, height: 667, icon: Smartphone, isLandscape: false }, { id: "mobile_landscape", label: "모바일 가로", shortLabel: "모바일↔", width: 667, height: 375, icon: Smartphone, isLandscape: true }, { id: "tablet_portrait", label: "태블릿 세로", shortLabel: "태블릿↕", width: 768, height: 1024, icon: Tablet, isLandscape: false }, { id: "tablet_landscape", label: "태블릿 가로", shortLabel: "태블릿↔", width: 1024, height: 768, icon: Tablet, isLandscape: true }, ] as const; type ViewportPreset = (typeof VIEWPORT_PRESETS)[number]["id"]; // 기본 프리셋 (태블릿 가로) const DEFAULT_PRESET: ViewportPreset = "tablet_landscape"; // ======================================== // Props // ======================================== interface PopCanvasV4Props { layout: PopLayoutDataV4; selectedComponentId: string | null; selectedContainerId: string | null; currentMode: ViewportPreset; // 현재 모드 tempLayout?: PopContainerV4 | null; // 임시 레이아웃 (고정 전 미리보기) onModeChange: (mode: ViewportPreset) => void; // 모드 변경 onSelectComponent: (id: string | null) => void; onSelectContainer: (id: string | null) => void; onDropComponent: (type: PopComponentType, containerId: string) => void; onUpdateComponent: (componentId: string, updates: Partial) => void; onUpdateContainer: (containerId: string, updates: Partial) => void; onDeleteComponent: (componentId: string) => void; onResizeComponent?: (componentId: string, size: Partial) => void; onReorderComponent?: (containerId: string, fromIndex: number, toIndex: number) => void; onLockLayout?: () => void; // 배치 고정 onResetOverride?: (mode: ViewportPreset) => void; // 오버라이드 초기화 } // ======================================== // v4 캔버스 // // 핵심: 단일 캔버스 + 뷰포트 프리뷰 // - 가로/세로 모드 따로 없음 // - 다양한 뷰포트 크기로 미리보기 // ======================================== export function PopCanvasV4({ layout, selectedComponentId, selectedContainerId, currentMode, tempLayout, onModeChange, onSelectComponent, onSelectContainer, onDropComponent, onUpdateComponent, onUpdateContainer, onDeleteComponent, onResizeComponent, onReorderComponent, onLockLayout, onResetOverride, }: PopCanvasV4Props) { // 줌 상태 const [canvasScale, setCanvasScale] = useState(0.8); // 커스텀 뷰포트 크기 (슬라이더) const [customWidth, setCustomWidth] = useState(1024); const [customHeight, setCustomHeight] = useState(768); // 패닝 상태 const [isPanning, setIsPanning] = useState(false); const [panStart, setPanStart] = useState({ x: 0, y: 0 }); const [isSpacePressed, setIsSpacePressed] = useState(false); const containerRef = useRef(null); const dropRef = useRef(null); // 현재 뷰포트 해상도 const currentPreset = VIEWPORT_PRESETS.find((p) => p.id === currentMode)!; const viewportWidth = customWidth; const viewportHeight = customHeight; // 줌 컨트롤 const handleZoomIn = () => setCanvasScale((prev) => Math.min(1.5, prev + 0.1)); const handleZoomOut = () => setCanvasScale((prev) => Math.max(0.3, prev - 0.1)); const handleZoomFit = () => setCanvasScale(1.0); // 뷰포트 프리셋 변경 const handleViewportChange = (preset: ViewportPreset) => { onModeChange(preset); // 부모에게 알림 const presetData = VIEWPORT_PRESETS.find((p) => p.id === preset)!; setCustomWidth(presetData.width); setCustomHeight(presetData.height); }; // 슬라이더로 너비 변경 시 높이도 비율에 맞게 조정 const handleWidthChange = (newWidth: number) => { setCustomWidth(newWidth); // 현재 프리셋의 가로세로 비율 유지 const ratio = currentPreset.height / currentPreset.width; setCustomHeight(Math.round(newWidth * ratio)); }; // 패닝 const handlePanStart = (e: React.MouseEvent) => { const isMiddleButton = e.button === 1; const isScrollAreaClick = (e.target as HTMLElement).classList.contains("canvas-scroll-area"); if (isMiddleButton || isSpacePressed || isScrollAreaClick) { setIsPanning(true); setPanStart({ x: e.clientX, y: e.clientY }); e.preventDefault(); } }; const handlePanMove = (e: React.MouseEvent) => { if (!isPanning || !containerRef.current) return; const deltaX = e.clientX - panStart.x; const deltaY = e.clientY - panStart.y; containerRef.current.scrollLeft -= deltaX; containerRef.current.scrollTop -= deltaY; setPanStart({ x: e.clientX, y: e.clientY }); }; const handlePanEnd = () => setIsPanning(false); // 마우스 휠 줌 const handleWheel = useCallback((e: React.WheelEvent) => { if (e.ctrlKey || e.metaKey) { e.preventDefault(); const delta = e.deltaY > 0 ? -0.1 : 0.1; setCanvasScale((prev) => Math.max(0.3, Math.min(1.5, prev + delta))); } }, []); // Space 키 감지 (패닝용) // 참고: Delete/Backspace 키는 PopDesigner에서 처리 (히스토리 지원) useEffect(() => { const handleKeyDown = (e: KeyboardEvent) => { if (e.code === "Space" && !isSpacePressed) setIsSpacePressed(true); }; const handleKeyUp = (e: KeyboardEvent) => { if (e.code === "Space") setIsSpacePressed(false); }; window.addEventListener("keydown", handleKeyDown); window.addEventListener("keyup", handleKeyUp); return () => { window.removeEventListener("keydown", handleKeyDown); window.removeEventListener("keyup", handleKeyUp); }; }, [isSpacePressed]); // 컴포넌트 드롭 const [{ isOver, canDrop }, drop] = useDrop( () => ({ accept: DND_ITEM_TYPES.COMPONENT, drop: (item: DragItemComponent) => { // 루트 컨테이너에 추가 onDropComponent(item.componentType, "root"); }, collect: (monitor) => ({ isOver: monitor.isOver(), canDrop: monitor.canDrop(), }), }), [onDropComponent] ); drop(dropRef); // 오버라이드 상태 확인 const hasOverride = (mode: ViewportPreset): boolean => { if (mode === DEFAULT_PRESET) return false; // 기본 모드는 오버라이드 없음 const override = layout.overrides?.[mode as keyof typeof layout.overrides]; if (!override) return false; // 컴포넌트 또는 컨테이너 오버라이드가 있으면 true const hasComponentOverrides = override.components && Object.keys(override.components).length > 0; const hasContainerOverrides = override.containers && Object.keys(override.containers).length > 0; return !!(hasComponentOverrides || hasContainerOverrides); }; return (
{/* 툴바 */}
{/* 뷰포트 프리셋 (4개 모드) */}
미리보기: {VIEWPORT_PRESETS.map((preset) => { const Icon = preset.icon; const isActive = currentMode === preset.id; const isDefault = preset.id === DEFAULT_PRESET; const isEdited = hasOverride(preset.id); return ( ); })}
{/* 고정 버튼 (기본 모드가 아닐 때 표시) */} {currentMode !== DEFAULT_PRESET && onLockLayout && ( )} {/* 오버라이드 초기화 버튼 (편집된 모드에만 표시) */} {hasOverride(currentMode) && onResetOverride && ( )} {/* 줌 컨트롤 */}
{Math.round(canvasScale * 100)}%
{/* 뷰포트 크기 슬라이더 */}
너비: handleWidthChange(Number(e.target.value))} className="flex-1 h-1 bg-gray-300 rounded-lg appearance-none cursor-pointer" /> {customWidth} x {viewportHeight}
{/* 캔버스 영역 */}
{/* 디바이스 프레임 */}
{/* 뷰포트 라벨 */}
{currentPreset.label} ({viewportWidth}x{viewportHeight})
{/* Flexbox 렌더러 - 최소 높이는 뷰포트 높이, 컨텐츠에 따라 늘어남 */}
{ onSelectComponent(null); onSelectContainer(null); }} onComponentResize={onResizeComponent} onReorderComponent={onReorderComponent} />
{/* 드롭 안내 (빈 상태) */} {layout.root.children.length === 0 && (

{isOver && canDrop ? "여기에 놓으세요" : "컴포넌트를 드래그하세요"}

왼쪽 패널에서 컴포넌트를 드래그하여 추가

)}
); } export default PopCanvasV4;