"use client"; import React, { useMemo } from "react"; import { cn } from "@/lib/utils"; import { PopLayoutDataV5, PopComponentDefinitionV5, PopGridPosition, GridMode, GRID_BREAKPOINTS, detectGridMode, PopComponentType, } from "../types/pop-layout"; // ======================================== // Props // ======================================== interface PopRendererProps { /** v5 레이아웃 데이터 */ layout: PopLayoutDataV5; /** 현재 뷰포트 너비 */ viewportWidth: number; /** 현재 모드 (자동 감지 또는 수동 지정) */ currentMode?: GridMode; /** 디자인 모드 여부 */ isDesignMode?: boolean; /** 그리드 가이드 표시 여부 */ showGridGuide?: boolean; /** 선택된 컴포넌트 ID */ selectedComponentId?: string | null; /** 컴포넌트 클릭 */ onComponentClick?: (componentId: string) => void; /** 배경 클릭 */ onBackgroundClick?: () => void; /** 추가 className */ className?: string; } // ======================================== // 컴포넌트 타입별 라벨 // ======================================== const COMPONENT_TYPE_LABELS: Record = { "pop-sample": "샘플", }; // ======================================== // PopRenderer: v5 그리드 렌더러 // ======================================== export default function PopRenderer({ layout, viewportWidth, currentMode, isDesignMode = false, showGridGuide = true, selectedComponentId, onComponentClick, onBackgroundClick, className, }: PopRendererProps) { const { gridConfig, components, overrides } = layout; // 현재 모드 (자동 감지 또는 지정) const mode = currentMode || detectGridMode(viewportWidth); const breakpoint = GRID_BREAKPOINTS[mode]; // CSS Grid 스타일 const gridStyle = useMemo((): React.CSSProperties => ({ display: "grid", gridTemplateColumns: `repeat(${breakpoint.columns}, 1fr)`, gridAutoRows: `${breakpoint.rowHeight}px`, gap: `${breakpoint.gap}px`, padding: `${breakpoint.padding}px`, minHeight: "100%", backgroundColor: "#ffffff", position: "relative", }), [breakpoint]); // 그리드 가이드 셀 생성 const gridCells = useMemo(() => { if (!isDesignMode || !showGridGuide) return []; const cells = []; const rowCount = 20; // 충분한 행 수 for (let row = 1; row <= rowCount; row++) { for (let col = 1; col <= breakpoint.columns; col++) { cells.push({ id: `cell-${col}-${row}`, col, row }); } } return cells; }, [isDesignMode, showGridGuide, breakpoint.columns]); // visibility 체크 const isVisible = (comp: PopComponentDefinitionV5): boolean => { if (!comp.visibility) return true; const modeVisibility = comp.visibility[mode]; return modeVisibility !== false; }; // 위치 변환 (12칸 기준 → 현재 모드 칸 수) const convertPosition = (position: PopGridPosition): React.CSSProperties => { const sourceColumns = 12; // 항상 12칸 기준으로 저장 const targetColumns = breakpoint.columns; // 같은 칸 수면 그대로 사용 if (sourceColumns === targetColumns) { return { gridColumn: `${position.col} / span ${position.colSpan}`, gridRow: `${position.row} / span ${position.rowSpan}`, }; } // 비율 계산 (12칸 → 4칸, 6칸, 8칸) const ratio = targetColumns / sourceColumns; // 열 위치 변환 let newCol = Math.max(1, Math.ceil((position.col - 1) * ratio) + 1); let newColSpan = Math.max(1, Math.round(position.colSpan * ratio)); // 범위 초과 방지 if (newCol > targetColumns) { newCol = 1; } if (newCol + newColSpan - 1 > targetColumns) { newColSpan = targetColumns - newCol + 1; } return { gridColumn: `${newCol} / span ${Math.max(1, newColSpan)}`, gridRow: `${position.row} / span ${position.rowSpan}`, }; }; // 오버라이드 적용 const getEffectivePosition = (comp: PopComponentDefinitionV5): PopGridPosition => { const override = overrides?.[mode]?.positions?.[comp.id]; if (override) { return { ...comp.position, ...override }; } return comp.position; }; // 오버라이드 숨김 체크 const isHiddenByOverride = (comp: PopComponentDefinitionV5): boolean => { return overrides?.[mode]?.hidden?.includes(comp.id) ?? false; }; return (
{ if (e.target === e.currentTarget) { onBackgroundClick?.(); } }} > {/* 그리드 가이드 셀 (실제 DOM) */} {gridCells.map(cell => (
))} {/* 컴포넌트 렌더링 (z-index로 위에 표시) */} {Object.values(components).map((comp) => { // visibility 체크 if (!isVisible(comp)) return null; // 오버라이드 숨김 체크 if (isHiddenByOverride(comp)) return null; const position = getEffectivePosition(comp); const positionStyle = convertPosition(position); const isSelected = selectedComponentId === comp.id; return (
{ e.stopPropagation(); onComponentClick?.(comp.id); }} >
); })}
); } // ======================================== // 컴포넌트 내용 렌더링 // ======================================== interface ComponentContentProps { component: PopComponentDefinitionV5; isDesignMode: boolean; isSelected: boolean; } function ComponentContent({ component, isDesignMode, isSelected }: ComponentContentProps) { const typeLabel = COMPONENT_TYPE_LABELS[component.type] || component.type; // 디자인 모드: 플레이스홀더 표시 if (isDesignMode) { return (
{/* 헤더 */}
{component.label || typeLabel}
{/* 내용 */}
{typeLabel}
{/* 위치 정보 표시 */}
{component.position.col},{component.position.row} ({component.position.colSpan}×{component.position.rowSpan})
); } // 실제 모드: 컴포넌트 렌더링 return renderActualComponent(component); } // ======================================== // 실제 컴포넌트 렌더링 (뷰어 모드) // ======================================== function renderActualComponent(component: PopComponentDefinitionV5): React.ReactNode { const typeLabel = COMPONENT_TYPE_LABELS[component.type]; // 샘플 박스 렌더링 return (
{component.label || typeLabel}
); }