From 320100c4e2581e79dd69c622e2c5196e8560e713 Mon Sep 17 00:00:00 2001 From: SeongHyun Kim Date: Fri, 13 Mar 2026 16:32:20 +0900 Subject: [PATCH] =?UTF-8?q?refactor:=20POP=20=EA=B7=B8=EB=A6=AC=EB=93=9C?= =?UTF-8?q?=20=EC=8B=9C=EC=8A=A4=ED=85=9C=20=EB=AA=85=EC=B9=AD=20=ED=86=B5?= =?UTF-8?q?=EC=9D=BC=20+=20Dead=20Code=20=EC=A0=9C=EA=B1=B0=20V5=E2=86=92V?= =?UTF-8?q?6=20=EC=A0=84=ED=99=98=20=EA=B3=BC=EC=A0=95=EC=97=90=EC=84=9C?= =?UTF-8?q?=20=EB=88=84=EC=A0=81=EB=90=9C=20=EB=B2=84=EC=A0=84=20=EC=A0=91?= =?UTF-8?q?=EB=AF=B8=EC=82=AC,=20=EB=AF=B8=EC=82=AC=EC=9A=A9=20=ED=95=A8?= =?UTF-8?q?=EC=88=98,=20=EB=A0=88=EA=B1=B0=EC=8B=9C=20=EC=9E=94=EC=9E=AC?= =?UTF-8?q?=EB=A5=BC=20=EC=A0=95=EB=A6=AC=ED=95=98=EC=97=AC=20=EC=BD=94?= =?UTF-8?q?=EB=93=9C=20=EA=B4=80=EB=A6=AC=EC=84=B1=EC=9D=84=20=ED=99=95?= =?UTF-8?q?=EB=B3=B4=ED=95=9C=EB=8B=A4.=2014=EA=B0=9C=20=ED=8C=8C=EC=9D=BC?= =?UTF-8?q?=20=EC=88=98=EC=A0=95,=20365=EC=A4=84=20=EC=88=9C=EA=B0=90.=20[?= =?UTF-8?q?=ED=83=80=EC=9E=85=20=EB=A6=AC=EB=84=A4=EC=9D=B4=EB=B0=8D]=20(1?= =?UTF-8?q?4=EA=B0=9C=20=ED=8C=8C=EC=9D=BC)=20-=20PopLayoutDataV5=20?= =?UTF-8?q?=E2=86=92=20PopLayoutData=20-=20PopComponentDefinitionV5=20?= =?UTF-8?q?=E2=86=92=20PopComponentDefinition=20-=20PopGlobalSettingsV5=20?= =?UTF-8?q?=E2=86=92=20PopGlobalSettings=20-=20PopModeOverrideV5=20?= =?UTF-8?q?=E2=86=92=20PopModeOverride=20-=20createEmptyPopLayoutV5=20?= =?UTF-8?q?=E2=86=92=20createEmptyLayout=20-=20isV5Layout=20=E2=86=92=20is?= =?UTF-8?q?PopLayout=20-=20addComponentToV5Layout=20=E2=86=92=20addCompone?= =?UTF-8?q?ntToLayout=20-=20createComponentDefinitionV5=20=E2=86=92=20crea?= =?UTF-8?q?teComponentDefinition=20-=20=EA=B5=AC=20=EC=9D=B4=EB=A6=84?= =?UTF-8?q?=EC=9D=80=20deprecated=20=EB=B3=84=EC=B9=AD=EC=9C=BC=EB=A1=9C?= =?UTF-8?q?=20=EC=9C=A0=EC=A7=80=20(=ED=95=98=EC=9C=84=20=ED=98=B8?= =?UTF-8?q?=ED=99=98)=20[Dead=20Code=20=EC=82=AD=EC=A0=9C]=20(gridUtils.ts?= =?UTF-8?q?=20-350=EC=A4=84)=20-=20getAdjustedBreakpoint,=20convertPositio?= =?UTF-8?q?nToMode,=20isOutOfBounds,=20=20=20mouseToGridPosition,=20gridTo?= =?UTF-8?q?PixelPosition,=20isValidPosition,=20=20=20clampPosition,=20auto?= =?UTF-8?q?LayoutComponents=20(=EC=A0=84=EB=B6=80=20=EC=99=B8=EB=B6=80=20?= =?UTF-8?q?=EC=82=AC=EC=9A=A9=200=EA=B1=B4)=20-=20needsReview=20+=20Review?= =?UTF-8?q?Panel/ReviewItem=20(=ED=95=AD=EC=83=81=20false,=20=EB=AF=B8?= =?UTF-8?q?=EC=82=AC=EC=9A=A9)=20-=20getEffectiveComponentPosition=20expor?= =?UTF-8?q?t=20=E2=86=92=20=EB=82=B4=EB=B6=80=20=ED=95=A8=EC=88=98?= =?UTF-8?q?=EB=A1=9C=20=EC=A0=84=ED=99=98=20[=EB=A0=88=EA=B1=B0=EC=8B=9C?= =?UTF-8?q?=20=EB=A1=9C=EB=8D=94=20=EB=B6=84=EB=A6=AC]=20(=EC=8B=A0?= =?UTF-8?q?=EA=B7=9C=20legacyLoader.ts)=20-=20convertV5LayoutToV6=20?= =?UTF-8?q?=E2=86=92=20loadLegacyLayout=20(legacyLoader.ts)=20-=20V5=20?= =?UTF-8?q?=EB=B3=80=ED=99=98=20=EC=83=81=EC=88=98/=ED=95=A8=EC=88=98?= =?UTF-8?q?=EB=A5=BC=20gridUtils=EC=97=90=EC=84=9C=20=EB=B6=84=EB=A6=AC=20?= =?UTF-8?q?[=EC=A3=BC=EC=84=9D=20=EC=A0=95=EB=A6=AC]=20-=20"v5=20=EA=B7=B8?= =?UTF-8?q?=EB=A6=AC=EB=93=9C"=20=E2=86=92=20"POP=20=EB=B8=94=EB=A1=9D=20?= =?UTF-8?q?=EA=B7=B8=EB=A6=AC=EB=93=9C"=20-=20"=ED=95=98=EC=9C=84=20?= =?UTF-8?q?=ED=98=B8=ED=99=98=EC=9A=A9"=20=E2=86=92=20"=EB=B7=B0=ED=8F=AC?= =?UTF-8?q?=ED=8A=B8=20=ED=94=84=EB=A6=AC=EC=85=8B"=20/=20"=EB=A0=88?= =?UTF-8?q?=EC=9D=B4=EC=95=84=EC=9B=83=20=EC=84=A4=EC=A0=95=EC=9A=A9"=20-?= =?UTF-8?q?=20=ED=8C=8C=EC=9D=BC=20=ED=97=A4=EB=8D=94,=20=EC=84=B9?= =?UTF-8?q?=EC=85=98=20=EA=B5=AC=EB=B6=84,=20=ED=95=A8=EC=88=98=20JSDoc=20?= =?UTF-8?q?=EC=A0=95=EB=A6=AC=20=EA=B8=B0=EB=8A=A5=20=EB=B3=80=EA=B2=BD=20?= =?UTF-8?q?0=EA=B1=B4.=20DB=20=EB=B3=80=EA=B2=BD=200=EA=B1=B4.=20=EB=B0=B1?= =?UTF-8?q?=EC=97=94=EB=93=9C=20=EB=B3=80=EA=B2=BD=200=EA=B1=B4.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../app/(pop)/pop/screens/[screenId]/page.tsx | 20 +- .../components/pop/designer/PopCanvas.tsx | 134 +----- .../components/pop/designer/PopDesigner.tsx | 43 +- frontend/components/pop/designer/index.ts | 7 +- .../designer/panels/ComponentEditorPanel.tsx | 22 +- .../pop/designer/panels/ConnectionEditor.tsx | 20 +- .../pop/designer/renderers/PopRenderer.tsx | 20 +- .../pop/designer/types/pop-layout.ts | 98 ++-- .../pop/designer/utils/gridUtils.ts | 419 +----------------- .../pop/designer/utils/legacyLoader.ts | 128 ++++++ .../pop/viewer/PopViewerWithModals.tsx | 6 +- .../PopCardListV2Component.tsx | 8 +- .../registry/pop-components/pop-scanner.tsx | 6 +- .../pop-search/PopSearchConfig.tsx | 8 +- .../pop-status-bar/PopStatusBarConfig.tsx | 2 +- 15 files changed, 301 insertions(+), 640 deletions(-) create mode 100644 frontend/components/pop/designer/utils/legacyLoader.ts diff --git a/frontend/app/(pop)/pop/screens/[screenId]/page.tsx b/frontend/app/(pop)/pop/screens/[screenId]/page.tsx index bf2878a5..c7933033 100644 --- a/frontend/app/(pop)/pop/screens/[screenId]/page.tsx +++ b/frontend/app/(pop)/pop/screens/[screenId]/page.tsx @@ -17,17 +17,17 @@ import { ScreenContextProvider } from "@/contexts/ScreenContext"; import { SplitPanelProvider } from "@/lib/registry/components/split-panel-layout/SplitPanelContext"; import { ActiveTabProvider } from "@/contexts/ActiveTabContext"; import { - PopLayoutDataV5, + PopLayoutData, GridMode, - isV5Layout, - createEmptyPopLayoutV5, + isPopLayout, + createEmptyLayout, GAP_PRESETS, GRID_BREAKPOINTS, BLOCK_GAP, BLOCK_PADDING, detectGridMode, } from "@/components/pop/designer/types/pop-layout"; -import { convertV5LayoutToV6 } from "@/components/pop/designer/utils/gridUtils"; +import { loadLegacyLayout } from "@/components/pop/designer/utils/legacyLoader"; // POP 컴포넌트 자동 등록 (레지스트리 초기화 - PopRenderer보다 먼저 import) import "@/lib/registry/pop-components"; import PopViewerWithModals from "@/components/pop/viewer/PopViewerWithModals"; @@ -82,7 +82,7 @@ function PopScreenViewPage() { const { user } = useAuth(); const [screen, setScreen] = useState(null); - const [layout, setLayout] = useState(createEmptyPopLayoutV5()); + const [layout, setLayout] = useState(createEmptyLayout()); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); @@ -119,22 +119,22 @@ function PopScreenViewPage() { try { const popLayout = await screenApi.getLayoutPop(screenId); - if (popLayout && isV5Layout(popLayout)) { - const v6Layout = convertV5LayoutToV6(popLayout); + if (popLayout && isPopLayout(popLayout)) { + const v6Layout = loadLegacyLayout(popLayout); setLayout(v6Layout); const componentCount = Object.keys(popLayout.components).length; console.log(`[POP] v5 레이아웃 로드됨: ${componentCount}개 컴포넌트`); } else if (popLayout) { // 다른 버전 레이아웃은 빈 v5로 처리 console.log("[POP] 레거시 레이아웃 감지, 빈 레이아웃으로 시작합니다:", popLayout.version); - setLayout(createEmptyPopLayoutV5()); + setLayout(createEmptyLayout()); } else { console.log("[POP] 레이아웃 없음"); - setLayout(createEmptyPopLayoutV5()); + setLayout(createEmptyLayout()); } } catch (layoutError) { console.warn("[POP] 레이아웃 로드 실패:", layoutError); - setLayout(createEmptyPopLayoutV5()); + setLayout(createEmptyLayout()); } } catch (error) { console.error("[POP] 화면 로드 실패:", error); diff --git a/frontend/components/pop/designer/PopCanvas.tsx b/frontend/components/pop/designer/PopCanvas.tsx index acb654ae..d12422ec 100644 --- a/frontend/components/pop/designer/PopCanvas.tsx +++ b/frontend/components/pop/designer/PopCanvas.tsx @@ -4,8 +4,8 @@ import { useCallback, useRef, useState, useEffect, useMemo } from "react"; import { useDrop } from "react-dnd"; import { cn } from "@/lib/utils"; import { - PopLayoutDataV5, - PopComponentDefinitionV5, + PopLayoutData, + PopComponentDefinition, PopComponentType, PopGridPosition, GridMode, @@ -22,7 +22,7 @@ import { BLOCK_PADDING, getBlockColumns, } from "./types/pop-layout"; -import { ZoomIn, ZoomOut, Maximize2, Smartphone, Tablet, Lock, RotateCcw, AlertTriangle, EyeOff, Monitor, ChevronDown, ChevronUp } from "lucide-react"; +import { ZoomIn, ZoomOut, Maximize2, Smartphone, Tablet, Lock, RotateCcw, EyeOff, Monitor, ChevronDown, ChevronUp } from "lucide-react"; import { useDrag } from "react-dnd"; import { Button } from "@/components/ui/button"; import { @@ -34,7 +34,7 @@ import { } from "@/components/ui/select"; import { toast } from "sonner"; import PopRenderer from "./renderers/PopRenderer"; -import { findNextEmptyPosition, isOverlapping, getAllEffectivePositions, needsReview } from "./utils/gridUtils"; +import { findNextEmptyPosition, isOverlapping, getAllEffectivePositions } from "./utils/gridUtils"; import { DND_ITEM_TYPES } from "./constants"; /** @@ -95,13 +95,13 @@ const CANVAS_EXTRA_ROWS = 3; // 여유 행 수 // Props // ======================================== interface PopCanvasProps { - layout: PopLayoutDataV5; + layout: PopLayoutData; selectedComponentId: string | null; currentMode: GridMode; onModeChange: (mode: GridMode) => void; onSelectComponent: (id: string | null) => void; onDropComponent: (type: PopComponentType, position: PopGridPosition) => void; - onUpdateComponent: (componentId: string, updates: Partial) => void; + onUpdateComponent: (componentId: string, updates: Partial) => void; onDeleteComponent: (componentId: string) => void; onMoveComponent?: (componentId: string, newPosition: PopGridPosition) => void; onResizeComponent?: (componentId: string, newPosition: PopGridPosition) => void; @@ -163,7 +163,7 @@ export default function PopCanvas({ }, [layout.modals]); // activeCanvasId에 따라 렌더링할 layout 분기 - const activeLayout = useMemo((): PopLayoutDataV5 => { + const activeLayout = useMemo((): PopLayoutData => { if (activeCanvasId === "main") return layout; const modal = layout.modals?.find(m => m.id === activeCanvasId); if (!modal) return layout; // fallback @@ -401,7 +401,7 @@ export default function PopCanvas({ const effectivePositions = getAllEffectivePositions(activeLayout, currentMode); // 이동 중인 컴포넌트의 현재 유효 위치에서 colSpan/rowSpan 가져오기 - // 검토 필요(ReviewPanel에서 클릭)나 숨김 컴포넌트는 effectivePositions에 없으므로 원본 사용 + // 숨김 컴포넌트는 effectivePositions에 없으므로 원본 사용 const currentEffectivePos = effectivePositions.get(dragItem.componentId); const componentData = layout.components[dragItem.componentId]; @@ -472,22 +472,8 @@ export default function PopCanvas({ ); }, [activeLayout.components, hiddenComponentIds]); - // 검토 필요 컴포넌트 목록 - const reviewComponents = useMemo(() => { - return visibleComponents.filter(comp => { - const hasOverride = !!activeLayout.overrides?.[currentMode]?.positions?.[comp.id]; - return needsReview(currentMode, hasOverride); - }); - }, [visibleComponents, activeLayout.overrides, currentMode]); - - // 검토 패널 표시 여부 (12칸 모드가 아니고, 검토 필요 컴포넌트가 있을 때) - const showReviewPanel = currentMode !== "tablet_landscape" && reviewComponents.length > 0; - - // 12칸 모드가 아닐 때만 패널 표시 - // 숨김 패널: 숨김 컴포넌트가 있거나, 그리드에 컴포넌트가 있을 때 드롭 영역으로 표시 const hasGridComponents = Object.keys(activeLayout.components).length > 0; const showHiddenPanel = currentMode !== "tablet_landscape" && (hiddenComponents.length > 0 || hasGridComponents); - const showRightPanel = showReviewPanel || showHiddenPanel; return (
@@ -668,7 +654,7 @@ export default function PopCanvas({
{/* 오른쪽 패널 영역 (초과 컴포넌트 + 숨김 컴포넌트) */} - {showRightPanel && ( + {showHiddenPanel && (
- {/* 검토 필요 패널 */} - {showReviewPanel && ( - - )} - {/* 숨김 컴포넌트 패널 */} {showHiddenPanel && ( void; -} - -function ReviewPanel({ - components, - selectedComponentId, - onSelectComponent, -}: ReviewPanelProps) { - return ( -
- {/* 헤더 */} -
- - - 검토 필요 ({components.length}개) - -
- - {/* 컴포넌트 목록 */} -
- {components.map((comp) => ( - onSelectComponent(comp.id)} - /> - ))} -
- - {/* 안내 문구 */} -
-

- 자동 배치됨. 클릭하여 확인 후 편집 가능 -

-
-
- ); -} - -// ======================================== -// 검토 필요 아이템 (ReviewPanel 내부) -// ======================================== - -interface ReviewItemProps { - component: PopComponentDefinitionV5; - isSelected: boolean; - onSelect: () => void; -} - -function ReviewItem({ - component, - isSelected, - onSelect, -}: ReviewItemProps) { - return ( -
{ - e.stopPropagation(); - onSelect(); - }} - > - - {component.label || component.id} - - - 자동 배치됨 - -
- ); -} - // ======================================== // 숨김 컴포넌트 영역 (오른쪽 패널) // ======================================== interface HiddenPanelProps { - components: PopComponentDefinitionV5[]; + components: PopComponentDefinition[]; selectedComponentId: string | null; onSelectComponent: (id: string | null) => void; onHideComponent?: (componentId: string) => void; @@ -999,7 +889,7 @@ function HiddenPanel({ // ======================================== interface HiddenItemProps { - component: PopComponentDefinitionV5; + component: PopComponentDefinition; isSelected: boolean; onSelect: () => void; } diff --git a/frontend/components/pop/designer/PopDesigner.tsx b/frontend/components/pop/designer/PopDesigner.tsx index de131032..259ead41 100644 --- a/frontend/components/pop/designer/PopDesigner.tsx +++ b/frontend/components/pop/designer/PopDesigner.tsx @@ -19,21 +19,22 @@ import PopCanvas from "./PopCanvas"; import ComponentEditorPanel from "./panels/ComponentEditorPanel"; import ComponentPalette from "./panels/ComponentPalette"; import { - PopLayoutDataV5, + PopLayoutData, PopComponentType, - PopComponentDefinitionV5, + PopComponentDefinition, PopGridPosition, GridMode, GapPreset, - createEmptyPopLayoutV5, - isV5Layout, - addComponentToV5Layout, - createComponentDefinitionV5, + createEmptyLayout, + isPopLayout, + addComponentToLayout, + createComponentDefinition, GRID_BREAKPOINTS, PopModalDefinition, PopDataConnection, } from "./types/pop-layout"; -import { getAllEffectivePositions, convertV5LayoutToV6 } from "./utils/gridUtils"; +import { getAllEffectivePositions } from "./utils/gridUtils"; +import { loadLegacyLayout } from "./utils/legacyLoader"; import { screenApi } from "@/lib/api/screen"; import { ScreenDefinition } from "@/types/screen"; import { PopDesignerContext } from "./PopDesignerContext"; @@ -59,10 +60,10 @@ export default function PopDesigner({ // ======================================== // 레이아웃 상태 // ======================================== - const [layout, setLayout] = useState(createEmptyPopLayoutV5()); + const [layout, setLayout] = useState(createEmptyLayout()); // 히스토리 - const [history, setHistory] = useState([]); + const [history, setHistory] = useState([]); const [historyIndex, setHistoryIndex] = useState(-1); // UI 상태 @@ -84,7 +85,7 @@ export default function PopDesigner({ const [activeCanvasId, setActiveCanvasId] = useState("main"); // 선택된 컴포넌트 (activeCanvasId에 따라 메인 또는 모달에서 조회) - const selectedComponent: PopComponentDefinitionV5 | null = (() => { + const selectedComponent: PopComponentDefinition | null = (() => { if (!selectedComponentId) return null; if (activeCanvasId === "main") { return layout.components[selectedComponentId] || null; @@ -96,7 +97,7 @@ export default function PopDesigner({ // ======================================== // 히스토리 관리 // ======================================== - const saveToHistory = useCallback((newLayout: PopLayoutDataV5) => { + const saveToHistory = useCallback((newLayout: PopLayoutData) => { setHistory((prev) => { const newHistory = prev.slice(0, historyIndex + 1); newHistory.push(JSON.parse(JSON.stringify(newLayout))); @@ -150,11 +151,11 @@ export default function PopDesigner({ try { const loadedLayout = await screenApi.getLayoutPop(selectedScreen.screenId); - if (loadedLayout && isV5Layout(loadedLayout) && loadedLayout.components && Object.keys(loadedLayout.components).length > 0) { + if (loadedLayout && isPopLayout(loadedLayout) && loadedLayout.components && Object.keys(loadedLayout.components).length > 0) { if (!loadedLayout.settings.gapPreset) { loadedLayout.settings.gapPreset = "medium"; } - const v6Layout = convertV5LayoutToV6(loadedLayout); + const v6Layout = loadLegacyLayout(loadedLayout); setLayout(v6Layout); setHistory([v6Layout]); setHistoryIndex(0); @@ -174,7 +175,7 @@ export default function PopDesigner({ console.log(`POP 레이아웃 로드: ${existingIds.length}개 컴포넌트, idCounter: ${maxId + 1}`); } else { // 새 화면 또는 빈 레이아웃 - const emptyLayout = createEmptyPopLayoutV5(); + const emptyLayout = createEmptyLayout(); setLayout(emptyLayout); setHistory([emptyLayout]); setHistoryIndex(0); @@ -183,7 +184,7 @@ export default function PopDesigner({ } catch (error) { console.error("레이아웃 로드 실패:", error); toast.error("레이아웃을 불러오는데 실패했습니다"); - const emptyLayout = createEmptyPopLayoutV5(); + const emptyLayout = createEmptyLayout(); setLayout(emptyLayout); setHistory([emptyLayout]); setHistoryIndex(0); @@ -224,13 +225,13 @@ export default function PopDesigner({ if (activeCanvasId === "main") { // 메인 캔버스 - const newLayout = addComponentToV5Layout(layout, componentId, type, position, `${type} ${idCounter}`); + const newLayout = addComponentToLayout(layout, componentId, type, position, `${type} ${idCounter}`); setLayout(newLayout); saveToHistory(newLayout); } else { // 모달 캔버스 setLayout(prev => { - const comp = createComponentDefinitionV5(componentId, type, position, `${type} ${idCounter}`); + const comp = createComponentDefinition(componentId, type, position, `${type} ${idCounter}`); const newLayout = { ...prev, modals: (prev.modals || []).map(m => { @@ -249,7 +250,7 @@ export default function PopDesigner({ ); const handleUpdateComponent = useCallback( - (componentId: string, updates: Partial) => { + (componentId: string, updates: Partial) => { // 함수적 업데이트로 stale closure 방지 setLayout((prev) => { if (activeCanvasId === "main") { @@ -302,7 +303,7 @@ export default function PopDesigner({ const newId = `conn_${Date.now()}_${Math.random().toString(36).slice(2, 6)}`; const newConnection: PopDataConnection = { ...conn, id: newId }; const prevConnections = prev.dataFlow?.connections || []; - const newLayout: PopLayoutDataV5 = { + const newLayout: PopLayoutData = { ...prev, dataFlow: { ...prev.dataFlow, @@ -321,7 +322,7 @@ export default function PopDesigner({ (connectionId: string, conn: Omit) => { setLayout((prev) => { const prevConnections = prev.dataFlow?.connections || []; - const newLayout: PopLayoutDataV5 = { + const newLayout: PopLayoutData = { ...prev, dataFlow: { ...prev.dataFlow, @@ -342,7 +343,7 @@ export default function PopDesigner({ (connectionId: string) => { setLayout((prev) => { const prevConnections = prev.dataFlow?.connections || []; - const newLayout: PopLayoutDataV5 = { + const newLayout: PopLayoutData = { ...prev, dataFlow: { ...prev.dataFlow, diff --git a/frontend/components/pop/designer/index.ts b/frontend/components/pop/designer/index.ts index 37d86aec..c58ec3db 100644 --- a/frontend/components/pop/designer/index.ts +++ b/frontend/components/pop/designer/index.ts @@ -1,4 +1,4 @@ -// POP 디자이너 컴포넌트 export (v5 그리드 시스템) +// POP 디자이너 컴포넌트 export (블록 그리드 시스템) // 타입 export * from "./types"; @@ -17,11 +17,12 @@ export { default as PopRenderer } from "./renderers/PopRenderer"; // 유틸리티 export * from "./utils/gridUtils"; +export * from "./utils/legacyLoader"; // 핵심 타입 재export (편의) export type { - PopLayoutDataV5, - PopComponentDefinitionV5, + PopLayoutData, + PopComponentDefinition, PopComponentType, PopGridPosition, GridMode, diff --git a/frontend/components/pop/designer/panels/ComponentEditorPanel.tsx b/frontend/components/pop/designer/panels/ComponentEditorPanel.tsx index ec22426d..d79883ad 100644 --- a/frontend/components/pop/designer/panels/ComponentEditorPanel.tsx +++ b/frontend/components/pop/designer/panels/ComponentEditorPanel.tsx @@ -3,7 +3,7 @@ import React from "react"; import { cn } from "@/lib/utils"; import { - PopComponentDefinitionV5, + PopComponentDefinition, PopGridPosition, GridMode, GRID_BREAKPOINTS, @@ -33,15 +33,15 @@ import ConnectionEditor from "./ConnectionEditor"; interface ComponentEditorPanelProps { /** 선택된 컴포넌트 */ - component: PopComponentDefinitionV5 | null; + component: PopComponentDefinition | null; /** 현재 모드 */ currentMode: GridMode; /** 컴포넌트 업데이트 */ - onUpdateComponent?: (updates: Partial) => void; + onUpdateComponent?: (updates: Partial) => void; /** 추가 className */ className?: string; /** 그리드에 배치된 모든 컴포넌트 */ - allComponents?: PopComponentDefinitionV5[]; + allComponents?: PopComponentDefinition[]; /** 컴포넌트 선택 콜백 */ onSelectComponent?: (componentId: string) => void; /** 현재 선택된 컴포넌트 ID */ @@ -249,11 +249,11 @@ export default function ComponentEditorPanel({ // ======================================== interface PositionFormProps { - component: PopComponentDefinitionV5; + component: PopComponentDefinition; currentMode: GridMode; isDefaultMode: boolean; columns: number; - onUpdate?: (updates: Partial) => void; + onUpdate?: (updates: Partial) => void; } function PositionForm({ component, currentMode, isDefaultMode, columns, onUpdate }: PositionFormProps) { @@ -402,13 +402,13 @@ function PositionForm({ component, currentMode, isDefaultMode, columns, onUpdate // ======================================== interface ComponentSettingsFormProps { - component: PopComponentDefinitionV5; - onUpdate?: (updates: Partial) => void; + component: PopComponentDefinition; + onUpdate?: (updates: Partial) => void; currentMode?: GridMode; previewPageIndex?: number; onPreviewPage?: (pageIndex: number) => void; modals?: PopModalDefinition[]; - allComponents?: PopComponentDefinitionV5[]; + allComponents?: PopComponentDefinition[]; connections?: PopDataConnection[]; } @@ -466,8 +466,8 @@ function ComponentSettingsForm({ component, onUpdate, currentMode, previewPageIn // ======================================== interface VisibilityFormProps { - component: PopComponentDefinitionV5; - onUpdate?: (updates: Partial) => void; + component: PopComponentDefinition; + onUpdate?: (updates: Partial) => void; } function VisibilityForm({ component, onUpdate }: VisibilityFormProps) { diff --git a/frontend/components/pop/designer/panels/ConnectionEditor.tsx b/frontend/components/pop/designer/panels/ConnectionEditor.tsx index 84b56935..0a64e82a 100644 --- a/frontend/components/pop/designer/panels/ConnectionEditor.tsx +++ b/frontend/components/pop/designer/panels/ConnectionEditor.tsx @@ -13,7 +13,7 @@ import { SelectValue, } from "@/components/ui/select"; import { - PopComponentDefinitionV5, + PopComponentDefinition, PopDataConnection, } from "../types/pop-layout"; import { @@ -26,8 +26,8 @@ import { getTableColumns } from "@/lib/api/tableManagement"; // ======================================== interface ConnectionEditorProps { - component: PopComponentDefinitionV5; - allComponents: PopComponentDefinitionV5[]; + component: PopComponentDefinition; + allComponents: PopComponentDefinition[]; connections: PopDataConnection[]; onAddConnection?: (conn: Omit) => void; onUpdateConnection?: (connectionId: string, conn: Omit) => void; @@ -102,8 +102,8 @@ export default function ConnectionEditor({ // ======================================== interface SendSectionProps { - component: PopComponentDefinitionV5; - allComponents: PopComponentDefinitionV5[]; + component: PopComponentDefinition; + allComponents: PopComponentDefinition[]; outgoing: PopDataConnection[]; onAddConnection?: (conn: Omit) => void; onUpdateConnection?: (connectionId: string, conn: Omit) => void; @@ -197,15 +197,15 @@ function SendSection({ // ======================================== interface SimpleConnectionFormProps { - component: PopComponentDefinitionV5; - allComponents: PopComponentDefinitionV5[]; + component: PopComponentDefinition; + allComponents: PopComponentDefinition[]; initial?: PopDataConnection; onSubmit: (data: Omit) => void; onCancel?: () => void; submitLabel: string; } -function extractSubTableName(comp: PopComponentDefinitionV5): string | null { +function extractSubTableName(comp: PopComponentDefinition): string | null { const cfg = comp.config as Record | undefined; if (!cfg) return null; @@ -423,8 +423,8 @@ function SimpleConnectionForm({ // ======================================== interface ReceiveSectionProps { - component: PopComponentDefinitionV5; - allComponents: PopComponentDefinitionV5[]; + component: PopComponentDefinition; + allComponents: PopComponentDefinition[]; incoming: PopDataConnection[]; } diff --git a/frontend/components/pop/designer/renderers/PopRenderer.tsx b/frontend/components/pop/designer/renderers/PopRenderer.tsx index 373bed9b..89b4a551 100644 --- a/frontend/components/pop/designer/renderers/PopRenderer.tsx +++ b/frontend/components/pop/designer/renderers/PopRenderer.tsx @@ -5,8 +5,8 @@ import { useDrag } from "react-dnd"; import { cn } from "@/lib/utils"; import { DND_ITEM_TYPES } from "../constants"; import { - PopLayoutDataV5, - PopComponentDefinitionV5, + PopLayoutData, + PopComponentDefinition, PopGridPosition, GridMode, GRID_BREAKPOINTS, @@ -31,7 +31,7 @@ import { PopComponentRegistry } from "@/lib/registry/PopComponentRegistry"; interface PopRendererProps { /** v5 레이아웃 데이터 */ - layout: PopLayoutDataV5; + layout: PopLayoutData; /** 현재 뷰포트 너비 */ viewportWidth: number; /** 현재 모드 (자동 감지 또는 수동 지정) */ @@ -182,7 +182,7 @@ export default function PopRenderer({ }, [isDesignMode, showGridGuide, columns, dynamicRowCount]); // visibility 체크 - const isVisible = (comp: PopComponentDefinitionV5): boolean => { + const isVisible = (comp: PopComponentDefinition): boolean => { if (!comp.visibility) return true; const modeVisibility = comp.visibility[mode]; return modeVisibility !== false; @@ -207,7 +207,7 @@ export default function PopRenderer({ }; // 오버라이드 적용 또는 자동 재배치 - const getEffectivePosition = (comp: PopComponentDefinitionV5): PopGridPosition => { + const getEffectivePosition = (comp: PopComponentDefinition): PopGridPosition => { // 1순위: 오버라이드가 있으면 사용 const override = overrides?.[mode]?.positions?.[comp.id]; if (override) { @@ -225,7 +225,7 @@ export default function PopRenderer({ }; // 오버라이드 숨김 체크 - const isHiddenByOverride = (comp: PopComponentDefinitionV5): boolean => { + const isHiddenByOverride = (comp: PopComponentDefinition): boolean => { return overrides?.[mode]?.hidden?.includes(comp.id) ?? false; }; @@ -322,7 +322,7 @@ export default function PopRenderer({ // ======================================== interface DraggableComponentProps { - component: PopComponentDefinitionV5; + component: PopComponentDefinition; position: PopGridPosition; positionStyle: React.CSSProperties; isSelected: boolean; @@ -423,7 +423,7 @@ function DraggableComponent({ // ======================================== interface ResizeHandlesProps { - component: PopComponentDefinitionV5; + component: PopComponentDefinition; position: PopGridPosition; breakpoint: GridBreakpoint; viewportWidth: number; @@ -544,7 +544,7 @@ function ResizeHandles({ // ======================================== interface ComponentContentProps { - component: PopComponentDefinitionV5; + component: PopComponentDefinition; effectivePosition: PopGridPosition; isDesignMode: boolean; isSelected: boolean; @@ -614,7 +614,7 @@ function ComponentContent({ component, effectivePosition, isDesignMode, isSelect // ======================================== function renderActualComponent( - component: PopComponentDefinitionV5, + component: PopComponentDefinition, effectivePosition?: PopGridPosition, onRequestResize?: (componentId: string, newRowSpan: number, newColSpan?: number) => void, screenId?: string, diff --git a/frontend/components/pop/designer/types/pop-layout.ts b/frontend/components/pop/designer/types/pop-layout.ts index 44e4c1c4..7b008caf 100644 --- a/frontend/components/pop/designer/types/pop-layout.ts +++ b/frontend/components/pop/designer/types/pop-layout.ts @@ -1,6 +1,4 @@ -// POP 디자이너 레이아웃 타입 정의 -// v5.0: CSS Grid 기반 그리드 시스템 -// 2024-02 버전 통합: v1~v4 제거, v5 단일 버전 +// POP 블록 그리드 레이아웃 타입 정의 // ======================================== // 공통 타입 @@ -122,7 +120,7 @@ export function getBlockColumns(viewportWidth: number): number { } /** - * 그리드 모드 (하위 호환용 - V6에서는 뷰포트 프리셋 라벨로만 사용) + * 뷰포트 프리셋 (디자이너 해상도 전환용) */ export type GridMode = | "mobile_portrait" @@ -131,7 +129,7 @@ export type GridMode = | "tablet_landscape"; /** - * 그리드 브레이크포인트 설정 (하위 호환용) + * 뷰포트 프리셋 설정 */ export interface GridBreakpoint { minWidth?: number; @@ -200,31 +198,31 @@ export function detectGridMode(viewportWidth: number): GridMode { } /** - * v5 레이아웃 (그리드 기반) + * POP 레이아웃 데이터 */ -export interface PopLayoutDataV5 { +export interface PopLayoutData { version: "pop-5.0"; // 그리드 설정 gridConfig: PopGridConfig; // 컴포넌트 정의 (ID → 정의) - components: Record; + components: Record; // 데이터 흐름 dataFlow: PopDataFlow; // 전역 설정 - settings: PopGlobalSettingsV5; + settings: PopGlobalSettings; // 메타데이터 metadata?: PopLayoutMetadata; // 모드별 오버라이드 (위치 변경용) overrides?: { - mobile_portrait?: PopModeOverrideV5; - mobile_landscape?: PopModeOverrideV5; - tablet_portrait?: PopModeOverrideV5; + mobile_portrait?: PopModeOverride; + mobile_landscape?: PopModeOverride; + tablet_portrait?: PopModeOverride; }; // 모달 캔버스 목록 (버튼의 "모달 열기" 액션으로 생성) @@ -256,9 +254,9 @@ export interface PopGridPosition { } /** - * v5 컴포넌트 정의 + * POP 컴포넌트 정의 */ -export interface PopComponentDefinitionV5 { +export interface PopComponentDefinition { id: string; type: PopComponentType; label?: string; @@ -303,9 +301,9 @@ export const GAP_PRESETS: Record = { }; /** - * v5 전역 설정 + * POP 전역 설정 */ -export interface PopGlobalSettingsV5 { +export interface PopGlobalSettings { // 터치 최소 크기 (px) touchTargetMin: number; // 기본 48 @@ -317,9 +315,9 @@ export interface PopGlobalSettingsV5 { } /** - * v5 모드별 오버라이드 + * 모드별 오버라이드 (위치/숨김) */ -export interface PopModeOverrideV5 { +export interface PopModeOverride { // 컴포넌트별 위치 오버라이드 positions?: Record>; @@ -328,13 +326,13 @@ export interface PopModeOverrideV5 { } // ======================================== -// v5 유틸리티 함수 +// 레이아웃 유틸리티 함수 // ======================================== /** - * 빈 v5 레이아웃 생성 + * 빈 POP 레이아웃 생성 */ -export const createEmptyPopLayoutV5 = (): PopLayoutDataV5 => ({ +export const createEmptyLayout = (): PopLayoutData => ({ version: "pop-5.0", gridConfig: { rowHeight: BLOCK_SIZE, @@ -351,9 +349,9 @@ export const createEmptyPopLayoutV5 = (): PopLayoutDataV5 => ({ }); /** - * v5 레이아웃 여부 확인 + * POP 레이아웃 데이터인지 확인 */ -export const isV5Layout = (layout: any): layout is PopLayoutDataV5 => { +export const isPopLayout = (layout: any): layout is PopLayoutData => { return layout?.version === "pop-5.0"; }; @@ -382,14 +380,14 @@ export const DEFAULT_COMPONENT_GRID_SIZE: Record ({ +): PopComponentDefinition => ({ id, type, label, @@ -397,21 +395,21 @@ export const createComponentDefinitionV5 = ( }); /** - * v5 레이아웃에 컴포넌트 추가 + * POP 레이아웃에 컴포넌트 추가 */ -export const addComponentToV5Layout = ( - layout: PopLayoutDataV5, +export const addComponentToLayout = ( + layout: PopLayoutData, componentId: string, type: PopComponentType, position: PopGridPosition, label?: string -): PopLayoutDataV5 => { +): PopLayoutData => { const newLayout = { ...layout }; // 컴포넌트 정의 추가 newLayout.components = { ...newLayout.components, - [componentId]: createComponentDefinitionV5(componentId, type, position, label), + [componentId]: createComponentDefinition(componentId, type, position, label), }; return newLayout; @@ -486,12 +484,12 @@ export interface PopModalDefinition { /** 모달 내부 그리드 설정 */ gridConfig: PopGridConfig; /** 모달 내부 컴포넌트 */ - components: Record; + components: Record; /** 모드별 오버라이드 */ overrides?: { - mobile_portrait?: PopModeOverrideV5; - mobile_landscape?: PopModeOverrideV5; - tablet_portrait?: PopModeOverrideV5; + mobile_portrait?: PopModeOverride; + mobile_landscape?: PopModeOverride; + tablet_portrait?: PopModeOverride; }; /** 모달 프레임 설정 (닫기 방식) */ frameConfig?: { @@ -507,15 +505,29 @@ export interface PopModalDefinition { } // ======================================== -// 레거시 타입 별칭 (하위 호환 - 추후 제거) +// 레거시 타입 별칭 (이전 코드 호환용) // ======================================== -// 기존 코드에서 import 오류 방지용 -/** @deprecated v5에서는 PopLayoutDataV5 사용 */ -export type PopLayoutData = PopLayoutDataV5; +/** @deprecated PopLayoutData 사용 */ +export type PopLayoutDataV5 = PopLayoutData; -/** @deprecated v5에서는 PopComponentDefinitionV5 사용 */ -export type PopComponentDefinition = PopComponentDefinitionV5; +/** @deprecated PopComponentDefinition 사용 */ +export type PopComponentDefinitionV5 = PopComponentDefinition; -/** @deprecated v5에서는 PopGridPosition 사용 */ -export type GridPosition = PopGridPosition; +/** @deprecated PopGlobalSettings 사용 */ +export type PopGlobalSettingsV5 = PopGlobalSettings; + +/** @deprecated PopModeOverride 사용 */ +export type PopModeOverrideV5 = PopModeOverride; + +/** @deprecated createEmptyLayout 사용 */ +export const createEmptyPopLayoutV5 = createEmptyLayout; + +/** @deprecated isPopLayout 사용 */ +export const isV5Layout = isPopLayout; + +/** @deprecated addComponentToLayout 사용 */ +export const addComponentToV5Layout = addComponentToLayout; + +/** @deprecated createComponentDefinition 사용 */ +export const createComponentDefinitionV5 = createComponentDefinition; diff --git a/frontend/components/pop/designer/utils/gridUtils.ts b/frontend/components/pop/designer/utils/gridUtils.ts index e5078f64..5a8895d8 100644 --- a/frontend/components/pop/designer/utils/gridUtils.ts +++ b/frontend/components/pop/designer/utils/gridUtils.ts @@ -1,53 +1,25 @@ +// POP 그리드 유틸리티 (리플로우, 겹침 해결, 위치 계산) + import { PopGridPosition, GridMode, GRID_BREAKPOINTS, - GridBreakpoint, - GapPreset, - GAP_PRESETS, - PopLayoutDataV5, - PopComponentDefinitionV5, - BLOCK_SIZE, - BLOCK_GAP, - BLOCK_PADDING, - getBlockColumns, + PopLayoutData, } from "../types/pop-layout"; // ======================================== -// Gap/Padding 조정 (V6: 블록 간격 고정이므로 항상 원본 반환) -// ======================================== - -export function getAdjustedBreakpoint( - base: GridBreakpoint, - preset: GapPreset -): GridBreakpoint { - return { ...base }; -} - -// ======================================== -// 그리드 위치 변환 (V6: 단일 좌표계이므로 변환 불필요) +// 리플로우 (행 그룹 기반 자동 재배치) // ======================================== /** - * V6: 단일 좌표계이므로 변환 없이 원본 반환 - * @deprecated V6에서는 좌표 변환이 불필요합니다 - */ -export function convertPositionToMode( - position: PopGridPosition, - targetMode: GridMode -): PopGridPosition { - return position; -} - -/** - * V6 행 그룹 리플로우 (방식 F) + * 행 그룹 리플로우 * - * 원리: CSS Flexbox wrap과 동일. + * CSS Flexbox wrap 원리로 자동 재배치한다. * 1. 같은 행의 컴포넌트를 한 묶음으로 처리 * 2. 최소 2x2칸 보장 (터치 가능한 최소 크기) * 3. 한 줄에 안 들어가면 다음 줄로 줄바꿈 (숨김 없음) - * 4. 설계 너비의 50% 이상 → 전체 너비 확장 - * 5. 리플로우 후 겹침 해결 (resolveOverlaps) + * 4. 설계 너비의 50% 이상인 컴포넌트는 전체 너비 확장 + * 5. 리플로우 후 겹침 해결 */ export function convertAndResolvePositions( components: Array<{ id: string; position: PopGridPosition }>, @@ -66,7 +38,6 @@ export function convertAndResolvePositions( const MIN_COL_SPAN = 2; const MIN_ROW_SPAN = 2; - // 1. 원본 row 기준 그룹핑 const rowGroups: Record> = {}; components.forEach(comp => { const r = comp.position.row; @@ -77,7 +48,6 @@ export function convertAndResolvePositions( const placed: Array<{ id: string; position: PopGridPosition }> = []; let outputRow = 1; - // 2. 각 행 그룹을 순서대로 처리 const sortedRows = Object.keys(rowGroups).map(Number).sort((a, b) => a - b); for (const rowKey of sortedRows) { @@ -96,7 +66,6 @@ export function convertAndResolvePositions( const scaledRowSpan = Math.max(MIN_ROW_SPAN, pos.rowSpan); - // 현재 줄에 안 들어가면 줄바꿈 if (currentCol + scaledSpan - 1 > targetColumns) { outputRow += Math.max(1, maxRowSpanInLine); currentCol = 1; @@ -120,50 +89,18 @@ export function convertAndResolvePositions( outputRow += Math.max(1, maxRowSpanInLine); } - // 3. 겹침 해결 (행 그룹 간 rowSpan 충돌 처리) return resolveOverlaps(placed, targetColumns); } -// ======================================== -// 검토 필요 판별 (V6: 자동 줄바꿈이므로 검토 필요 없음) -// ======================================== - -/** - * V6: 단일 좌표계 + 자동 줄바꿈이므로 검토 필요 없음 - * 항상 false 반환 - */ -export function needsReview( - currentMode: GridMode, - hasOverride: boolean -): boolean { - return false; -} - -/** - * @deprecated V6에서는 자동 줄바꿈이므로 화면 밖 개념 없음 - */ -export function isOutOfBounds( - originalPosition: PopGridPosition, - currentMode: GridMode, - overridePosition?: PopGridPosition | null -): boolean { - return false; -} - // ======================================== // 겹침 감지 및 해결 // ======================================== -/** - * 두 위치가 겹치는지 확인 - */ export function isOverlapping(a: PopGridPosition, b: PopGridPosition): boolean { - // 열 겹침 체크 const aColEnd = a.col + a.colSpan - 1; const bColEnd = b.col + b.colSpan - 1; const colOverlap = !(aColEnd < b.col || bColEnd < a.col); - // 행 겹침 체크 const aRowEnd = a.row + a.rowSpan - 1; const bRowEnd = b.row + b.rowSpan - 1; const rowOverlap = !(aRowEnd < b.row || bRowEnd < a.row); @@ -171,14 +108,10 @@ export function isOverlapping(a: PopGridPosition, b: PopGridPosition): boolean { return colOverlap && rowOverlap; } -/** - * 겹침 해결 (아래로 밀기) - */ export function resolveOverlaps( positions: Array<{ id: string; position: PopGridPosition }>, columns: number ): Array<{ id: string; position: PopGridPosition }> { - // row, col 순으로 정렬 const sorted = [...positions].sort((a, b) => a.position.row - b.position.row || a.position.col - b.position.col ); @@ -188,21 +121,15 @@ export function resolveOverlaps( sorted.forEach((item) => { let { row, col, colSpan, rowSpan } = item.position; - // 열이 범위를 초과하면 조정 if (col + colSpan - 1 > columns) { colSpan = columns - col + 1; } - // 기존 배치와 겹치면 아래로 이동 let attempts = 0; - const maxAttempts = 100; - - while (attempts < maxAttempts) { + while (attempts < 100) { const currentPos: PopGridPosition = { col, row, colSpan, rowSpan }; const hasOverlap = resolved.some(r => isOverlapping(currentPos, r.position)); - if (!hasOverlap) break; - row++; attempts++; } @@ -217,110 +144,9 @@ export function resolveOverlaps( } // ======================================== -// 좌표 변환 +// 자동 배치 (새 컴포넌트 드롭 시) // ======================================== -/** - * V6: 마우스 좌표 → 블록 그리드 좌표 변환 - * 블록 크기가 고정(BLOCK_SIZE)이므로 계산이 단순함 - */ -export function mouseToGridPosition( - mouseX: number, - mouseY: number, - canvasRect: DOMRect, - columns: number, - rowHeight: number, - gap: number, - padding: number -): { col: number; row: number } { - const relX = mouseX - canvasRect.left - padding; - const relY = mouseY - canvasRect.top - padding; - - const cellStride = BLOCK_SIZE + gap; - - const col = Math.max(1, Math.min(columns, Math.floor(relX / cellStride) + 1)); - const row = Math.max(1, Math.floor(relY / cellStride) + 1); - - return { col, row }; -} - -/** - * V6: 블록 그리드 좌표 → 픽셀 좌표 변환 - */ -export function gridToPixelPosition( - col: number, - row: number, - colSpan: number, - rowSpan: number, - canvasWidth: number, - columns: number, - rowHeight: number, - gap: number, - padding: number -): { x: number; y: number; width: number; height: number } { - const cellStride = BLOCK_SIZE + gap; - - return { - x: padding + (col - 1) * cellStride, - y: padding + (row - 1) * cellStride, - width: BLOCK_SIZE * colSpan + gap * (colSpan - 1), - height: BLOCK_SIZE * rowSpan + gap * (rowSpan - 1), - }; -} - -// ======================================== -// 위치 검증 -// ======================================== - -/** - * 위치가 그리드 범위 내에 있는지 확인 - */ -export function isValidPosition( - position: PopGridPosition, - columns: number -): boolean { - return ( - position.col >= 1 && - position.row >= 1 && - position.colSpan >= 1 && - position.rowSpan >= 1 && - position.col + position.colSpan - 1 <= columns - ); -} - -/** - * 위치를 그리드 범위 내로 조정 - */ -export function clampPosition( - position: PopGridPosition, - columns: number -): PopGridPosition { - let { col, row, colSpan, rowSpan } = position; - - // 최소값 보장 - col = Math.max(1, col); - row = Math.max(1, row); - colSpan = Math.max(1, colSpan); - rowSpan = Math.max(1, rowSpan); - - // 열 범위 초과 방지 - if (col + colSpan - 1 > columns) { - if (col > columns) { - col = 1; - } - colSpan = columns - col + 1; - } - - return { col, row, colSpan, rowSpan }; -} - -// ======================================== -// 자동 배치 -// ======================================== - -/** - * 다음 빈 위치 찾기 - */ export function findNextEmptyPosition( existingPositions: PopGridPosition[], colSpan: number, @@ -329,168 +155,94 @@ export function findNextEmptyPosition( ): PopGridPosition { let row = 1; let col = 1; - - const maxAttempts = 1000; let attempts = 0; - while (attempts < maxAttempts) { + while (attempts < 1000) { const candidatePos: PopGridPosition = { col, row, colSpan, rowSpan }; - // 범위 체크 if (col + colSpan - 1 > columns) { col = 1; row++; continue; } - // 겹침 체크 - const hasOverlap = existingPositions.some(pos => - isOverlapping(candidatePos, pos) - ); + const hasOverlap = existingPositions.some(pos => isOverlapping(candidatePos, pos)); + if (!hasOverlap) return candidatePos; - if (!hasOverlap) { - return candidatePos; - } - - // 다음 위치로 이동 col++; if (col + colSpan - 1 > columns) { col = 1; row++; } - attempts++; } - // 실패 시 마지막 행에 배치 return { col: 1, row: row + 1, colSpan, rowSpan }; } -/** - * 컴포넌트들을 자동으로 배치 - */ -export function autoLayoutComponents( - components: Array<{ id: string; colSpan: number; rowSpan: number }>, - columns: number -): Array<{ id: string; position: PopGridPosition }> { - const result: Array<{ id: string; position: PopGridPosition }> = []; - - let currentRow = 1; - let currentCol = 1; - - components.forEach(comp => { - // 현재 행에 공간이 부족하면 다음 행으로 - if (currentCol + comp.colSpan - 1 > columns) { - currentRow++; - currentCol = 1; - } - - result.push({ - id: comp.id, - position: { - col: currentCol, - row: currentRow, - colSpan: comp.colSpan, - rowSpan: comp.rowSpan, - }, - }); - - currentCol += comp.colSpan; - }); - - return result; -} - // ======================================== -// 유효 위치 계산 (통합 함수) +// 유효 위치 계산 // ======================================== /** - * 컴포넌트의 유효 위치를 계산합니다. + * 컴포넌트의 유효 위치를 계산한다. * 우선순위: 1. 오버라이드 → 2. 자동 재배치 → 3. 원본 위치 - * - * @param componentId 컴포넌트 ID - * @param layout 전체 레이아웃 데이터 - * @param mode 현재 그리드 모드 - * @param autoResolvedPositions 미리 계산된 자동 재배치 위치 (선택적) */ -export function getEffectiveComponentPosition( +function getEffectiveComponentPosition( componentId: string, - layout: PopLayoutDataV5, + layout: PopLayoutData, mode: GridMode, autoResolvedPositions?: Array<{ id: string; position: PopGridPosition }> ): PopGridPosition | null { const component = layout.components[componentId]; if (!component) return null; - // 1순위: 오버라이드가 있으면 사용 const override = layout.overrides?.[mode]?.positions?.[componentId]; if (override) { return { ...component.position, ...override }; } - // 2순위: 자동 재배치된 위치 사용 if (autoResolvedPositions) { const autoResolved = autoResolvedPositions.find(p => p.id === componentId); - if (autoResolved) { - return autoResolved.position; - } + if (autoResolved) return autoResolved.position; } else { - // 자동 재배치 직접 계산 const componentsArray = Object.entries(layout.components).map(([id, comp]) => ({ id, position: comp.position, })); const resolved = convertAndResolvePositions(componentsArray, mode); const autoResolved = resolved.find(p => p.id === componentId); - if (autoResolved) { - return autoResolved.position; - } + if (autoResolved) return autoResolved.position; } - // 3순위: 원본 위치 (12칸 모드) return component.position; } /** - * 모든 컴포넌트의 유효 위치를 일괄 계산합니다. - * 숨김 처리된 컴포넌트는 제외됩니다. - * - * v5.1: 자동 줄바꿈 시스템으로 인해 모든 컴포넌트가 그리드 안에 배치되므로 - * "화면 밖" 개념이 제거되었습니다. + * 모든 컴포넌트의 유효 위치를 일괄 계산한다. + * 숨김 처리된 컴포넌트는 제외. */ export function getAllEffectivePositions( - layout: PopLayoutDataV5, + layout: PopLayoutData, mode: GridMode ): Map { const result = new Map(); - // 숨김 처리된 컴포넌트 ID 목록 const hiddenIds = layout.overrides?.[mode]?.hidden || []; - // 자동 재배치 위치 미리 계산 const componentsArray = Object.entries(layout.components).map(([id, comp]) => ({ id, position: comp.position, })); const autoResolvedPositions = convertAndResolvePositions(componentsArray, mode); - // 각 컴포넌트의 유효 위치 계산 Object.keys(layout.components).forEach(componentId => { - // 숨김 처리된 컴포넌트는 제외 - if (hiddenIds.includes(componentId)) { - return; - } + if (hiddenIds.includes(componentId)) return; const position = getEffectiveComponentPosition( - componentId, - layout, - mode, - autoResolvedPositions + componentId, layout, mode, autoResolvedPositions ); - // v5.1: 자동 줄바꿈으로 인해 모든 컴포넌트가 그리드 안에 있음 - // 따라서 추가 필터링 불필요 if (position) { result.set(componentId, position); } @@ -498,126 +250,3 @@ export function getAllEffectivePositions( return result; } - -// ======================================== -// V5 → V6 런타임 변환 (DB 미수정, 로드 시 변환) -// ======================================== - -const V5_BASE_COLUMNS = 12; -const V5_BASE_ROW_HEIGHT = 48; -const V5_BASE_GAP = 16; -const V5_DESIGN_WIDTH = 1024; - -/** - * V5 레이아웃 판별: gridConfig.rowHeight가 V5 기본값(48)이고 - * 좌표가 12칸 체계인 경우만 V5로 판정 - */ -function isV5GridConfig(layout: PopLayoutDataV5): boolean { - if (layout.gridConfig?.rowHeight === BLOCK_SIZE) return false; - - const maxCol = Object.values(layout.components).reduce((max, comp) => { - const end = comp.position.col + comp.position.colSpan - 1; - return Math.max(max, end); - }, 0); - - return maxCol <= V5_BASE_COLUMNS; -} - -function convertV5PositionToV6( - pos: PopGridPosition, - v6DesignColumns: number, -): PopGridPosition { - const colRatio = v6DesignColumns / V5_BASE_COLUMNS; - const rowRatio = (V5_BASE_ROW_HEIGHT + V5_BASE_GAP) / (BLOCK_SIZE + BLOCK_GAP); - - const newCol = Math.max(1, Math.round((pos.col - 1) * colRatio) + 1); - let newColSpan = Math.max(1, Math.round(pos.colSpan * colRatio)); - const newRowSpan = Math.max(1, Math.round(pos.rowSpan * rowRatio)); - - if (newCol + newColSpan - 1 > v6DesignColumns) { - newColSpan = v6DesignColumns - newCol + 1; - } - - return { col: newCol, row: pos.row, colSpan: newColSpan, rowSpan: newRowSpan }; -} - -/** - * V5 레이아웃을 V6 블록 좌표로 런타임 변환 - * - 기본 모드(tablet_landscape) 좌표를 블록 단위로 변환 - * - 모드별 overrides 폐기 (자동 줄바꿈으로 대체) - * - DB 데이터는 건드리지 않음 (메모리에서만 변환) - */ -export function convertV5LayoutToV6(layout: PopLayoutDataV5): PopLayoutDataV5 { - // V5 오버라이드는 V6에서 무효 (4/6/8칸용 좌표가 13/22/31칸에 맞지 않음) - // 좌표 변환 필요 여부와 무관하게 항상 제거 - if (!isV5GridConfig(layout)) { - return { - ...layout, - gridConfig: { - rowHeight: BLOCK_SIZE, - gap: BLOCK_GAP, - padding: BLOCK_PADDING, - }, - overrides: undefined, - }; - } - - const v6Columns = getBlockColumns(V5_DESIGN_WIDTH); - - const rowGroups: Record = {}; - Object.entries(layout.components).forEach(([id, comp]) => { - const r = comp.position.row; - if (!rowGroups[r]) rowGroups[r] = []; - rowGroups[r].push(id); - }); - - const convertedPositions: Record = {}; - Object.entries(layout.components).forEach(([id, comp]) => { - convertedPositions[id] = convertV5PositionToV6(comp.position, v6Columns); - }); - - const sortedRows = Object.keys(rowGroups).map(Number).sort((a, b) => a - b); - const rowMapping: Record = {}; - let v6Row = 1; - for (const v5Row of sortedRows) { - rowMapping[v5Row] = v6Row; - const maxSpan = Math.max( - ...rowGroups[v5Row].map(id => convertedPositions[id].rowSpan) - ); - v6Row += maxSpan; - } - - const newComponents = { ...layout.components }; - Object.entries(newComponents).forEach(([id, comp]) => { - const converted = convertedPositions[id]; - const mappedRow = rowMapping[comp.position.row] ?? converted.row; - newComponents[id] = { - ...comp, - position: { ...converted, row: mappedRow }, - }; - }); - - const newModals = layout.modals?.map(modal => { - const modalComps = { ...modal.components }; - Object.entries(modalComps).forEach(([id, comp]) => { - modalComps[id] = { - ...comp, - position: convertV5PositionToV6(comp.position, v6Columns), - }; - }); - return { - ...modal, - gridConfig: { rowHeight: BLOCK_SIZE, gap: BLOCK_GAP, padding: BLOCK_PADDING }, - components: modalComps, - overrides: undefined, - }; - }); - - return { - ...layout, - gridConfig: { rowHeight: BLOCK_SIZE, gap: BLOCK_GAP, padding: BLOCK_PADDING }, - components: newComponents, - overrides: undefined, - modals: newModals, - }; -} diff --git a/frontend/components/pop/designer/utils/legacyLoader.ts b/frontend/components/pop/designer/utils/legacyLoader.ts new file mode 100644 index 00000000..42cf20d7 --- /dev/null +++ b/frontend/components/pop/designer/utils/legacyLoader.ts @@ -0,0 +1,128 @@ +// 레거시 레이아웃 로더 +// DB에 저장된 V5(12칸) 좌표를 현재 블록 좌표로 변환한다. +// DB 데이터는 건드리지 않고, 로드 시 메모리에서만 변환. + +import { + PopGridPosition, + PopLayoutData, + BLOCK_SIZE, + BLOCK_GAP, + BLOCK_PADDING, + getBlockColumns, +} from "../types/pop-layout"; + +const LEGACY_COLUMNS = 12; +const LEGACY_ROW_HEIGHT = 48; +const LEGACY_GAP = 16; +const DESIGN_WIDTH = 1024; + +function isLegacyGridConfig(layout: PopLayoutData): boolean { + if (layout.gridConfig?.rowHeight === BLOCK_SIZE) return false; + + const maxCol = Object.values(layout.components).reduce((max, comp) => { + const end = comp.position.col + comp.position.colSpan - 1; + return Math.max(max, end); + }, 0); + + return maxCol <= LEGACY_COLUMNS; +} + +function convertLegacyPosition( + pos: PopGridPosition, + targetColumns: number, +): PopGridPosition { + const colRatio = targetColumns / LEGACY_COLUMNS; + const rowRatio = (LEGACY_ROW_HEIGHT + LEGACY_GAP) / (BLOCK_SIZE + BLOCK_GAP); + + const newCol = Math.max(1, Math.round((pos.col - 1) * colRatio) + 1); + let newColSpan = Math.max(1, Math.round(pos.colSpan * colRatio)); + const newRowSpan = Math.max(1, Math.round(pos.rowSpan * rowRatio)); + + if (newCol + newColSpan - 1 > targetColumns) { + newColSpan = targetColumns - newCol + 1; + } + + return { col: newCol, row: pos.row, colSpan: newColSpan, rowSpan: newRowSpan }; +} + +const BLOCK_GRID_CONFIG = { + rowHeight: BLOCK_SIZE, + gap: BLOCK_GAP, + padding: BLOCK_PADDING, +}; + +/** + * DB에서 로드한 레이아웃을 현재 블록 좌표로 변환한다. + * + * - 12칸 레거시 좌표 → 블록 좌표 변환 + * - 이미 블록 좌표인 경우 → gridConfig만 보정 + * - 구 모드별 overrides는 항상 제거 (리플로우가 대체) + */ +export function loadLegacyLayout(layout: PopLayoutData): PopLayoutData { + if (!isLegacyGridConfig(layout)) { + return { + ...layout, + gridConfig: BLOCK_GRID_CONFIG, + overrides: undefined, + }; + } + + const blockColumns = getBlockColumns(DESIGN_WIDTH); + + const rowGroups: Record = {}; + Object.entries(layout.components).forEach(([id, comp]) => { + const r = comp.position.row; + if (!rowGroups[r]) rowGroups[r] = []; + rowGroups[r].push(id); + }); + + const convertedPositions: Record = {}; + Object.entries(layout.components).forEach(([id, comp]) => { + convertedPositions[id] = convertLegacyPosition(comp.position, blockColumns); + }); + + const sortedRows = Object.keys(rowGroups).map(Number).sort((a, b) => a - b); + const rowMapping: Record = {}; + let currentRow = 1; + for (const legacyRow of sortedRows) { + rowMapping[legacyRow] = currentRow; + const maxSpan = Math.max( + ...rowGroups[legacyRow].map(id => convertedPositions[id].rowSpan) + ); + currentRow += maxSpan; + } + + const newComponents = { ...layout.components }; + Object.entries(newComponents).forEach(([id, comp]) => { + const converted = convertedPositions[id]; + const mappedRow = rowMapping[comp.position.row] ?? converted.row; + newComponents[id] = { + ...comp, + position: { ...converted, row: mappedRow }, + }; + }); + + const newModals = layout.modals?.map(modal => { + const modalComps = { ...modal.components }; + Object.entries(modalComps).forEach(([id, comp]) => { + modalComps[id] = { + ...comp, + position: convertLegacyPosition(comp.position, blockColumns), + }; + }); + return { + ...modal, + gridConfig: BLOCK_GRID_CONFIG, + components: modalComps, + overrides: undefined, + }; + }); + + return { + ...layout, + gridConfig: BLOCK_GRID_CONFIG, + components: newComponents, + overrides: undefined, + modals: newModals, + }; +} diff --git a/frontend/components/pop/viewer/PopViewerWithModals.tsx b/frontend/components/pop/viewer/PopViewerWithModals.tsx index f322d4c0..9fbe0af5 100644 --- a/frontend/components/pop/viewer/PopViewerWithModals.tsx +++ b/frontend/components/pop/viewer/PopViewerWithModals.tsx @@ -20,7 +20,7 @@ import { DialogTitle, } from "@/components/ui/dialog"; import PopRenderer from "../designer/renderers/PopRenderer"; -import type { PopLayoutDataV5, PopModalDefinition, GridMode } from "../designer/types/pop-layout"; +import type { PopLayoutData, PopModalDefinition, GridMode } from "../designer/types/pop-layout"; import { detectGridMode, resolveModalWidth } from "../designer/types/pop-layout"; import { usePopEvent } from "@/hooks/pop/usePopEvent"; import { useConnectionResolver } from "@/hooks/pop/useConnectionResolver"; @@ -31,7 +31,7 @@ import { useConnectionResolver } from "@/hooks/pop/useConnectionResolver"; interface PopViewerWithModalsProps { /** 전체 레이아웃 (모달 정의 포함) */ - layout: PopLayoutDataV5; + layout: PopLayoutData; /** 뷰포트 너비 */ viewportWidth: number; /** 화면 ID (이벤트 버스용) */ @@ -178,7 +178,7 @@ export default function PopViewerWithModals({ const closeOnOverlay = definition.frameConfig?.closeOnOverlay !== false; const closeOnEsc = definition.frameConfig?.closeOnEsc !== false; - const modalLayout: PopLayoutDataV5 = { + const modalLayout: PopLayoutData = { ...layout, gridConfig: definition.gridConfig, components: definition.components, diff --git a/frontend/lib/registry/pop-components/pop-card-list-v2/PopCardListV2Component.tsx b/frontend/lib/registry/pop-components/pop-card-list-v2/PopCardListV2Component.tsx index 55829efb..6fe66e41 100644 --- a/frontend/lib/registry/pop-components/pop-card-list-v2/PopCardListV2Component.tsx +++ b/frontend/lib/registry/pop-components/pop-card-list-v2/PopCardListV2Component.tsx @@ -49,8 +49,8 @@ import { usePopEvent } from "@/hooks/pop/usePopEvent"; import { useCartSync } from "@/hooks/pop/useCartSync"; import { NumberInputModal } from "../pop-card-list/NumberInputModal"; import { renderCellV2 } from "./cell-renderers"; -import type { PopLayoutDataV5 } from "@/components/pop/designer/types/pop-layout"; -import { isV5Layout, detectGridMode } from "@/components/pop/designer/types/pop-layout"; +import type { PopLayoutData } from "@/components/pop/designer/types/pop-layout"; +import { isPopLayout, detectGridMode } from "@/components/pop/designer/types/pop-layout"; import dynamic from "next/dynamic"; const PopViewerWithModals = dynamic(() => import("@/components/pop/viewer/PopViewerWithModals"), { ssr: false }); @@ -216,7 +216,7 @@ export function PopCardListV2Component({ // ===== 모달 열기 (POP 화면) ===== const [popModalOpen, setPopModalOpen] = useState(false); - const [popModalLayout, setPopModalLayout] = useState(null); + const [popModalLayout, setPopModalLayout] = useState(null); const [popModalScreenId, setPopModalScreenId] = useState(""); const [popModalRow, setPopModalRow] = useState(null); @@ -228,7 +228,7 @@ export function PopCardListV2Component({ return; } const popLayout = await screenApi.getLayoutPop(sid); - if (popLayout && isV5Layout(popLayout)) { + if (popLayout && isPopLayout(popLayout)) { setPopModalLayout(popLayout); setPopModalScreenId(String(sid)); setPopModalRow(row); diff --git a/frontend/lib/registry/pop-components/pop-scanner.tsx b/frontend/lib/registry/pop-components/pop-scanner.tsx index e2230170..4ce86cc8 100644 --- a/frontend/lib/registry/pop-components/pop-scanner.tsx +++ b/frontend/lib/registry/pop-components/pop-scanner.tsx @@ -19,7 +19,7 @@ import { usePopEvent } from "@/hooks/pop/usePopEvent"; import { BarcodeScanModal } from "@/components/common/BarcodeScanModal"; import type { PopDataConnection, - PopComponentDefinitionV5, + PopComponentDefinition, } from "@/components/pop/designer/types/pop-layout"; // ======================================== @@ -99,7 +99,7 @@ function parseScanResult( function getConnectedFields( componentId?: string, connections?: PopDataConnection[], - allComponents?: PopComponentDefinitionV5[], + allComponents?: PopComponentDefinition[], ): ConnectedFieldInfo[] { if (!componentId || !connections || !allComponents) return []; @@ -308,7 +308,7 @@ const PARSE_MODE_LABELS: Record = { interface PopScannerConfigPanelProps { config: PopScannerConfig; onUpdate: (config: PopScannerConfig) => void; - allComponents?: PopComponentDefinitionV5[]; + allComponents?: PopComponentDefinition[]; connections?: PopDataConnection[]; componentId?: string; } diff --git a/frontend/lib/registry/pop-components/pop-search/PopSearchConfig.tsx b/frontend/lib/registry/pop-components/pop-search/PopSearchConfig.tsx index 8c619429..b0752146 100644 --- a/frontend/lib/registry/pop-components/pop-search/PopSearchConfig.tsx +++ b/frontend/lib/registry/pop-components/pop-search/PopSearchConfig.tsx @@ -72,7 +72,7 @@ const DEFAULT_CONFIG: PopSearchConfig = { interface ConfigPanelProps { config: PopSearchConfig | undefined; onUpdate: (config: PopSearchConfig) => void; - allComponents?: import("@/components/pop/designer/types/pop-layout").PopComponentDefinitionV5[]; + allComponents?: import("@/components/pop/designer/types/pop-layout").PopComponentDefinition[]; connections?: import("@/components/pop/designer/types/pop-layout").PopDataConnection[]; componentId?: string; } @@ -151,7 +151,7 @@ export function PopSearchConfigPanel({ config, onUpdate, allComponents, connecti interface StepProps { cfg: PopSearchConfig; update: (partial: Partial) => void; - allComponents?: import("@/components/pop/designer/types/pop-layout").PopComponentDefinitionV5[]; + allComponents?: import("@/components/pop/designer/types/pop-layout").PopComponentDefinition[]; connections?: import("@/components/pop/designer/types/pop-layout").PopDataConnection[]; componentId?: string; } @@ -268,7 +268,7 @@ interface FilterConnectionSectionProps { update: (partial: Partial) => void; showFieldName: boolean; fixedFilterMode?: SearchFilterMode; - allComponents?: import("@/components/pop/designer/types/pop-layout").PopComponentDefinitionV5[]; + allComponents?: import("@/components/pop/designer/types/pop-layout").PopComponentDefinition[]; connections?: import("@/components/pop/designer/types/pop-layout").PopDataConnection[]; componentId?: string; } @@ -284,7 +284,7 @@ interface ConnectedComponentInfo { function getConnectedComponentInfo( componentId?: string, connections?: import("@/components/pop/designer/types/pop-layout").PopDataConnection[], - allComponents?: import("@/components/pop/designer/types/pop-layout").PopComponentDefinitionV5[], + allComponents?: import("@/components/pop/designer/types/pop-layout").PopComponentDefinition[], ): ConnectedComponentInfo { const empty: ConnectedComponentInfo = { tableNames: [], displayedColumns: new Set() }; if (!componentId || !connections || !allComponents) return empty; diff --git a/frontend/lib/registry/pop-components/pop-status-bar/PopStatusBarConfig.tsx b/frontend/lib/registry/pop-components/pop-status-bar/PopStatusBarConfig.tsx index 3b0ce864..8118dfe2 100644 --- a/frontend/lib/registry/pop-components/pop-status-bar/PopStatusBarConfig.tsx +++ b/frontend/lib/registry/pop-components/pop-status-bar/PopStatusBarConfig.tsx @@ -22,7 +22,7 @@ import { DEFAULT_STATUS_BAR_CONFIG, STATUS_CHIP_STYLE_LABELS } from "./types"; interface ConfigPanelProps { config: StatusBarConfig | undefined; onUpdate: (config: StatusBarConfig) => void; - allComponents?: import("@/components/pop/designer/types/pop-layout").PopComponentDefinitionV5[]; + allComponents?: import("@/components/pop/designer/types/pop-layout").PopComponentDefinition[]; connections?: import("@/components/pop/designer/types/pop-layout").PopDataConnection[]; componentId?: string; }