feat(pop): v4 레이아웃 비율 스케일링 시스템 구현
- PopFlexRenderer에 BASE_VIEWPORT_WIDTH(1024px) 기준 스케일 계산 추가 - 컴포넌트 크기(fixedWidth/Height), gap, padding에 scale 적용 - 뷰어에서 viewportWidth 동적 감지 및 최대 1366px 제한 - 디자인 모드에서는 scale=1 유지, 뷰어에서만 비율 적용 - DndProvider 없는 환경에서 useDrag/useDrop 에러 방지 - v4 레이아웃 뷰어 렌더링 지원 (isPopLayoutV4 체크)
This commit is contained in:
@@ -20,15 +20,18 @@ import { ActiveTabProvider } from "@/contexts/ActiveTabContext";
|
||||
import { ScreenMultiLangProvider } from "@/contexts/ScreenMultiLangContext";
|
||||
import {
|
||||
PopLayoutDataV3,
|
||||
PopLayoutDataV4,
|
||||
PopLayoutModeKey,
|
||||
ensureV3Layout,
|
||||
isV3Layout,
|
||||
isV4Layout,
|
||||
} from "@/components/pop/designer/types/pop-layout";
|
||||
import {
|
||||
PopLayoutRenderer,
|
||||
hasBaseLayout,
|
||||
getEffectiveModeLayout,
|
||||
} from "@/components/pop/designer/renderers";
|
||||
import { PopFlexRenderer } from "@/components/pop/designer/renderers/PopFlexRenderer";
|
||||
import {
|
||||
useResponsiveMode,
|
||||
useResponsiveModeWithOverride,
|
||||
@@ -63,12 +66,18 @@ const isPopLayoutV3 = (layout: any): layout is PopLayoutDataV3 => {
|
||||
return layout && layout.version === "pop-3.0" && layout.layouts && layout.components;
|
||||
};
|
||||
|
||||
// v1/v2 레이아웃인지 확인 (마이그레이션 대상)
|
||||
// v4.0 레이아웃인지 확인
|
||||
const isPopLayoutV4 = (layout: any): layout is PopLayoutDataV4 => {
|
||||
return layout && layout.version === "pop-4.0" && layout.root && layout.components;
|
||||
};
|
||||
|
||||
// v1/v2/v3/v4 레이아웃인지 확인
|
||||
const isPopLayout = (layout: any): boolean => {
|
||||
return layout && (
|
||||
layout.version === "pop-1.0" ||
|
||||
layout.version === "pop-2.0" ||
|
||||
layout.version === "pop-3.0"
|
||||
layout.version === "pop-3.0" ||
|
||||
layout.version === "pop-4.0"
|
||||
);
|
||||
};
|
||||
|
||||
@@ -101,6 +110,7 @@ function PopScreenViewPage() {
|
||||
const [screen, setScreen] = useState<ScreenDefinition | null>(null);
|
||||
const [layout, setLayout] = useState<LayoutData | null>(null);
|
||||
const [popLayoutV3, setPopLayoutV3] = useState<PopLayoutDataV3 | null>(null);
|
||||
const [popLayoutV4, setPopLayoutV4] = useState<PopLayoutDataV4 | null>(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
@@ -108,6 +118,19 @@ function PopScreenViewPage() {
|
||||
const [selectedRowsData, setSelectedRowsData] = useState<any[]>([]);
|
||||
const [tableRefreshKey, setTableRefreshKey] = useState(0);
|
||||
|
||||
// 뷰포트 너비 (클라이언트 사이드에서만 계산, 최대 1366px)
|
||||
const [viewportWidth, setViewportWidth] = useState(1024); // 기본값: 태블릿 가로
|
||||
|
||||
useEffect(() => {
|
||||
const updateViewportWidth = () => {
|
||||
setViewportWidth(Math.min(window.innerWidth, 1366));
|
||||
};
|
||||
|
||||
updateViewportWidth();
|
||||
window.addEventListener("resize", updateViewportWidth);
|
||||
return () => window.removeEventListener("resize", updateViewportWidth);
|
||||
}, []);
|
||||
|
||||
// 컴포넌트 초기화
|
||||
useEffect(() => {
|
||||
const initComponents = async () => {
|
||||
@@ -133,10 +156,17 @@ function PopScreenViewPage() {
|
||||
try {
|
||||
const popLayout = await screenApi.getLayoutPop(screenId);
|
||||
|
||||
if (popLayout && isPopLayout(popLayout)) {
|
||||
if (popLayout && isPopLayoutV4(popLayout)) {
|
||||
// v4 레이아웃
|
||||
setPopLayoutV4(popLayout);
|
||||
setPopLayoutV3(null);
|
||||
const componentCount = Object.keys(popLayout.components).length;
|
||||
console.log(`[POP] v4 레이아웃 로드됨: ${componentCount}개 컴포넌트`);
|
||||
} else if (popLayout && isPopLayout(popLayout)) {
|
||||
// v1/v2/v3 → v3로 변환
|
||||
const v3Layout = ensureV3Layout(popLayout);
|
||||
setPopLayoutV3(v3Layout);
|
||||
setPopLayoutV4(null);
|
||||
|
||||
const componentCount = Object.keys(v3Layout.components).length;
|
||||
console.log(`[POP] v3 레이아웃 로드됨: ${componentCount}개 컴포넌트`);
|
||||
@@ -151,11 +181,13 @@ function PopScreenViewPage() {
|
||||
} else {
|
||||
console.log("[POP] 레이아웃 없음");
|
||||
setPopLayoutV3(null);
|
||||
setPopLayoutV4(null);
|
||||
setLayout(null);
|
||||
}
|
||||
} catch (layoutError) {
|
||||
console.warn("[POP] 레이아웃 로드 실패:", layoutError);
|
||||
setPopLayoutV3(null);
|
||||
setPopLayoutV4(null);
|
||||
setLayout(null);
|
||||
}
|
||||
} catch (error) {
|
||||
@@ -304,8 +336,20 @@ function PopScreenViewPage() {
|
||||
flexShrink: 0,
|
||||
} : undefined}
|
||||
>
|
||||
{/* POP 레이아웃 v3.0 렌더링 */}
|
||||
{popLayoutV3 ? (
|
||||
{/* POP 레이아웃 v4.0 렌더링 */}
|
||||
{popLayoutV4 ? (
|
||||
<div
|
||||
className="mx-auto h-full"
|
||||
style={{ maxWidth: 1366 }}
|
||||
>
|
||||
<PopFlexRenderer
|
||||
layout={popLayoutV4}
|
||||
viewportWidth={isPreviewMode ? currentDevice.width : viewportWidth}
|
||||
isDesignMode={false}
|
||||
/>
|
||||
</div>
|
||||
) : popLayoutV3 ? (
|
||||
/* POP 레이아웃 v3.0 렌더링 */
|
||||
<PopLayoutV3Renderer
|
||||
layout={popLayoutV3}
|
||||
modeKey={currentModeKey}
|
||||
|
||||
150
frontend/app/(pop)/pop/test-v4/page.tsx
Normal file
150
frontend/app/(pop)/pop/test-v4/page.tsx
Normal file
@@ -0,0 +1,150 @@
|
||||
"use client";
|
||||
|
||||
import { useState, useCallback } from "react";
|
||||
import { DndProvider } from "react-dnd";
|
||||
import { HTML5Backend } from "react-dnd-html5-backend";
|
||||
import {
|
||||
PopLayoutDataV4,
|
||||
createEmptyPopLayoutV4,
|
||||
addComponentToV4Layout,
|
||||
removeComponentFromV4Layout,
|
||||
updateComponentInV4Layout,
|
||||
updateContainerV4,
|
||||
findContainerV4,
|
||||
PopComponentType,
|
||||
PopComponentDefinitionV4,
|
||||
PopContainerV4,
|
||||
} from "@/components/pop/designer/types/pop-layout";
|
||||
import { PopCanvasV4 } from "@/components/pop/designer/PopCanvasV4";
|
||||
import { PopPanel } from "@/components/pop/designer/panels/PopPanel";
|
||||
import { ComponentEditorPanelV4 } from "@/components/pop/designer/panels/ComponentEditorPanelV4";
|
||||
|
||||
// ========================================
|
||||
// v4 테스트 페이지
|
||||
//
|
||||
// 목적: v4 렌더러, 캔버스, 속성 패널 테스트
|
||||
// 경로: /pop/test-v4
|
||||
// ========================================
|
||||
|
||||
export default function TestV4Page() {
|
||||
// 레이아웃 상태
|
||||
const [layout, setLayout] = useState<PopLayoutDataV4>(() => {
|
||||
// 초기 테스트 데이터
|
||||
const initial = createEmptyPopLayoutV4();
|
||||
return initial;
|
||||
});
|
||||
|
||||
// 선택 상태
|
||||
const [selectedComponentId, setSelectedComponentId] = useState<string | null>(null);
|
||||
const [selectedContainerId, setSelectedContainerId] = useState<string | null>(null);
|
||||
|
||||
// 컴포넌트 ID 카운터
|
||||
const [idCounter, setIdCounter] = useState(1);
|
||||
|
||||
// 선택된 컴포넌트/컨테이너 가져오기
|
||||
const selectedComponent = selectedComponentId
|
||||
? layout.components[selectedComponentId]
|
||||
: null;
|
||||
const selectedContainer = selectedContainerId
|
||||
? findContainerV4(layout.root, selectedContainerId)
|
||||
: null;
|
||||
|
||||
// 컴포넌트 드롭
|
||||
const handleDropComponent = useCallback(
|
||||
(type: PopComponentType, containerId: string) => {
|
||||
const componentId = `comp_${idCounter}`;
|
||||
setIdCounter((prev) => prev + 1);
|
||||
|
||||
setLayout((prev) =>
|
||||
addComponentToV4Layout(prev, componentId, type, containerId, `${type} ${idCounter}`)
|
||||
);
|
||||
setSelectedComponentId(componentId);
|
||||
setSelectedContainerId(null);
|
||||
},
|
||||
[idCounter]
|
||||
);
|
||||
|
||||
// 컴포넌트 삭제
|
||||
const handleDeleteComponent = useCallback((componentId: string) => {
|
||||
setLayout((prev) => removeComponentFromV4Layout(prev, componentId));
|
||||
setSelectedComponentId(null);
|
||||
}, []);
|
||||
|
||||
// 컴포넌트 업데이트
|
||||
const handleUpdateComponent = useCallback(
|
||||
(componentId: string, updates: Partial<PopComponentDefinitionV4>) => {
|
||||
setLayout((prev) => updateComponentInV4Layout(prev, componentId, updates));
|
||||
},
|
||||
[]
|
||||
);
|
||||
|
||||
// 컨테이너 업데이트
|
||||
const handleUpdateContainer = useCallback(
|
||||
(containerId: string, updates: Partial<PopContainerV4>) => {
|
||||
setLayout((prev) => ({
|
||||
...prev,
|
||||
root: updateContainerV4(prev.root, containerId, updates),
|
||||
}));
|
||||
},
|
||||
[]
|
||||
);
|
||||
|
||||
// 선택
|
||||
const handleSelectComponent = useCallback((id: string | null) => {
|
||||
setSelectedComponentId(id);
|
||||
if (id) setSelectedContainerId(null);
|
||||
}, []);
|
||||
|
||||
const handleSelectContainer = useCallback((id: string | null) => {
|
||||
setSelectedContainerId(id);
|
||||
if (id) setSelectedComponentId(null);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<DndProvider backend={HTML5Backend}>
|
||||
<div className="flex h-screen w-screen overflow-hidden bg-gray-100">
|
||||
{/* 왼쪽: 컴포넌트 팔레트 */}
|
||||
<div className="w-64 shrink-0 border-r bg-white">
|
||||
<div className="border-b px-4 py-3">
|
||||
<h2 className="text-sm font-semibold">v4 테스트</h2>
|
||||
<p className="text-xs text-muted-foreground">컴포넌트를 드래그하세요</p>
|
||||
</div>
|
||||
<PopPanel />
|
||||
</div>
|
||||
|
||||
{/* 중앙: 캔버스 */}
|
||||
<div className="flex-1 overflow-hidden">
|
||||
<PopCanvasV4
|
||||
layout={layout}
|
||||
selectedComponentId={selectedComponentId}
|
||||
selectedContainerId={selectedContainerId}
|
||||
onSelectComponent={handleSelectComponent}
|
||||
onSelectContainer={handleSelectContainer}
|
||||
onDropComponent={handleDropComponent}
|
||||
onUpdateComponent={handleUpdateComponent}
|
||||
onUpdateContainer={handleUpdateContainer}
|
||||
onDeleteComponent={handleDeleteComponent}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* 오른쪽: 속성 패널 */}
|
||||
<div className="w-72 shrink-0 border-l bg-white">
|
||||
<ComponentEditorPanelV4
|
||||
component={selectedComponent}
|
||||
container={selectedContainer}
|
||||
onUpdateComponent={
|
||||
selectedComponentId
|
||||
? (updates) => handleUpdateComponent(selectedComponentId, updates)
|
||||
: undefined
|
||||
}
|
||||
onUpdateContainer={
|
||||
selectedContainerId
|
||||
? (updates) => handleUpdateContainer(selectedContainerId, updates)
|
||||
: undefined
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</DndProvider>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user