"use client"; import React from "react"; import { cn } from "@/lib/utils"; import { PopLayoutDataV3, PopLayoutModeKey, PopModeLayoutV3, GridPosition, MODE_RESOLUTIONS, PopComponentDefinition, } from "../types/pop-layout"; // ======================================== // Props 정의 // ======================================== interface PopLayoutRendererProps { /** 레이아웃 데이터 (v3.0) */ layout: PopLayoutDataV3; /** 현재 모드 키 (tablet_landscape, tablet_portrait 등) */ modeKey: PopLayoutModeKey; /** 디자인 모드 여부 (true: 편집 가능, false: 뷰어 모드) */ isDesignMode?: boolean; /** 선택된 컴포넌트 ID */ selectedComponentId?: string | null; /** 컴포넌트 클릭 시 호출 */ onComponentClick?: (componentId: string) => void; /** 배경 클릭 시 호출 (선택 해제용) */ onBackgroundClick?: () => void; /** 커스텀 모드 레이아웃 (fallback 등에서 변환된 레이아웃 사용 시) */ customModeLayout?: PopModeLayoutV3; /** 추가 className */ className?: string; /** 추가 style */ style?: React.CSSProperties; } // ======================================== // 컴포넌트 타입별 라벨 // ======================================== const COMPONENT_TYPE_LABELS: Record = { "pop-field": "필드", "pop-button": "버튼", "pop-list": "리스트", "pop-indicator": "인디케이터", "pop-scanner": "스캐너", "pop-numpad": "숫자패드", }; // ======================================== // POP 레이아웃 렌더러 (v3) // // 핵심 역할: // - 디자이너와 뷰어에서 **동일한** 렌더링 결과 보장 // - 컴포넌트가 캔버스에 직접 배치 (섹션 없음) // - CSS Grid + 1fr 비율 기반 // ======================================== export function PopLayoutRenderer({ layout, modeKey, isDesignMode = false, selectedComponentId, onComponentClick, onBackgroundClick, customModeLayout, className, style, }: PopLayoutRendererProps) { const { components, layouts, settings } = layout; const canvasGrid = settings.canvasGrid; // 현재 모드의 레이아웃 const modeLayout = customModeLayout || layouts[modeKey]; // 컴포넌트가 없으면 빈 상태 표시 if (!modeLayout || Object.keys(modeLayout.componentPositions).length === 0) { return (

레이아웃이 설정되지 않았습니다

{isDesignMode &&

컴포넌트를 추가해주세요

}
); } // 컴포넌트 ID 목록 const componentIds = Object.keys(modeLayout.componentPositions); return (
{ if (e.target === e.currentTarget) { onBackgroundClick?.(); } }} > {/* 컴포넌트들 직접 렌더링 */} {componentIds.map((componentId) => { const compDef = components[componentId]; const compPos = modeLayout.componentPositions[componentId]; if (!compDef || !compPos) return null; return ( onComponentClick?.(componentId)} /> ); })}
); } // ======================================== // 컴포넌트 렌더러 // ======================================== interface ComponentRendererProps { componentId: string; component: PopComponentDefinition; position: GridPosition; isDesignMode?: boolean; isSelected?: boolean; onComponentClick?: () => void; } function ComponentRenderer({ componentId, component, position, isDesignMode = false, isSelected = false, onComponentClick, }: ComponentRendererProps) { const typeLabel = COMPONENT_TYPE_LABELS[component.type] || component.type; return (
{ e.stopPropagation(); onComponentClick?.(); }} > {/* 컴포넌트 라벨 (디자인 모드에서만) */} {isDesignMode && (
{component.label || typeLabel}
)} {/* 컴포넌트 내용 */}
{renderComponentContent(component, isDesignMode)}
); } // ======================================== // 컴포넌트별 렌더링 // ======================================== function renderComponentContent( component: PopComponentDefinition, isDesignMode: boolean ): React.ReactNode { const typeLabel = COMPONENT_TYPE_LABELS[component.type] || component.type; // 디자인 모드에서는 플레이스홀더 표시 if (isDesignMode) { return (
{typeLabel}
); } // 뷰어 모드: 실제 컴포넌트 렌더링 switch (component.type) { case "pop-field": return ( ); case "pop-button": return ( ); case "pop-list": return (
리스트 (데이터 연결 필요)
); case "pop-indicator": return (
0
{component.label || "지표"}
); case "pop-scanner": return (
스캐너
탭하여 스캔
); case "pop-numpad": return (
{[1, 2, 3, 4, 5, 6, 7, 8, 9, "C", 0, "OK"].map((key) => ( ))}
); default: return (
{typeLabel}
); } } // ======================================== // 헬퍼 함수들 (export) // ======================================== /** * 특정 모드에 레이아웃이 설정되어 있는지 확인 */ export function hasModeLayout( layout: PopLayoutDataV3, modeKey: PopLayoutModeKey ): boolean { const modeLayout = layout.layouts[modeKey]; return modeLayout && Object.keys(modeLayout.componentPositions).length > 0; } /** * 태블릿 가로 모드(기준 모드)가 설정되어 있는지 확인 */ export function hasBaseLayout(layout: PopLayoutDataV3): boolean { return hasModeLayout(layout, "tablet_landscape"); } /** * 태블릿 가로 모드를 기준으로 다른 모드에 맞게 자동 변환 */ export function autoConvertLayout( layout: PopLayoutDataV3, targetModeKey: PopLayoutModeKey ): PopModeLayoutV3 { const sourceKey: PopLayoutModeKey = "tablet_landscape"; const sourceLayout = layout.layouts[sourceKey]; const sourceRes = MODE_RESOLUTIONS[sourceKey]; const targetRes = MODE_RESOLUTIONS[targetModeKey]; // 비율 계산 const widthRatio = targetRes.width / sourceRes.width; const heightRatio = targetRes.height / sourceRes.height; // 가로 → 세로 변환인지 확인 const isOrientationChange = sourceRes.width > sourceRes.height !== targetRes.width > targetRes.height; // 컴포넌트 위치 변환 const convertedPositions: Record = {}; let currentRow = 1; // 컴포넌트를 row, col 순으로 정렬 const sortedComponentIds = Object.keys(sourceLayout.componentPositions).sort( (a, b) => { const posA = sourceLayout.componentPositions[a]; const posB = sourceLayout.componentPositions[b]; if (posA.row !== posB.row) return posA.row - posB.row; return posA.col - posB.col; } ); for (const componentId of sortedComponentIds) { const sourcePos = sourceLayout.componentPositions[componentId]; if (isOrientationChange) { // 가로 → 세로: 세로 스택 방식 const canvasColumns = layout.settings.canvasGrid.columns; convertedPositions[componentId] = { col: 1, row: currentRow, colSpan: canvasColumns, rowSpan: Math.max(3, Math.round(sourcePos.rowSpan * 1.5)), }; currentRow += convertedPositions[componentId].rowSpan + 1; } else { // 같은 방향: 비율 변환 convertedPositions[componentId] = { col: Math.max(1, Math.round(sourcePos.col * widthRatio)), row: Math.max(1, Math.round(sourcePos.row * heightRatio)), colSpan: Math.max(1, Math.round(sourcePos.colSpan * widthRatio)), rowSpan: Math.max(1, Math.round(sourcePos.rowSpan * heightRatio)), }; } } return { componentPositions: convertedPositions, }; } /** * 현재 모드에 맞는 레이아웃 반환 (없으면 자동 변환) */ export function getEffectiveModeLayout( layout: PopLayoutDataV3, targetModeKey: PopLayoutModeKey ): { modeLayout: PopModeLayoutV3; isConverted: boolean; sourceModeKey: PopLayoutModeKey; } { // 해당 모드에 레이아웃이 있으면 그대로 사용 if (hasModeLayout(layout, targetModeKey)) { return { modeLayout: layout.layouts[targetModeKey], isConverted: false, sourceModeKey: targetModeKey, }; } // 없으면 태블릿 가로 모드를 기준으로 자동 변환 return { modeLayout: autoConvertLayout(layout, targetModeKey), isConverted: true, sourceModeKey: "tablet_landscape", }; } export default PopLayoutRenderer;