refactor: POP 그리드 시스템 명칭 통일 + Dead Code 제거

V5→V6 전환 과정에서 누적된 버전 접미사, 미사용 함수, 레거시 잔재를
정리하여 코드 관리성을 확보한다. 14개 파일 수정, 365줄 순감.
[타입 리네이밍] (14개 파일)
- PopLayoutDataV5 → PopLayoutData
- PopComponentDefinitionV5 → PopComponentDefinition
- PopGlobalSettingsV5 → PopGlobalSettings
- PopModeOverrideV5 → PopModeOverride
- createEmptyPopLayoutV5 → createEmptyLayout
- isV5Layout → isPopLayout
- addComponentToV5Layout → addComponentToLayout
- createComponentDefinitionV5 → createComponentDefinition
- 구 이름은 deprecated 별칭으로 유지 (하위 호환)
[Dead Code 삭제] (gridUtils.ts -350줄)
- getAdjustedBreakpoint, convertPositionToMode, isOutOfBounds,
  mouseToGridPosition, gridToPixelPosition, isValidPosition,
  clampPosition, autoLayoutComponents (전부 외부 사용 0건)
- needsReview + ReviewPanel/ReviewItem (항상 false, 미사용)
- getEffectiveComponentPosition export → 내부 함수로 전환
[레거시 로더 분리] (신규 legacyLoader.ts)
- convertV5LayoutToV6 → loadLegacyLayout (legacyLoader.ts)
- V5 변환 상수/함수를 gridUtils에서 분리
[주석 정리]
- "v5 그리드" → "POP 블록 그리드"
- "하위 호환용" → "뷰포트 프리셋" / "레이아웃 설정용"
- 파일 헤더, 섹션 구분, 함수 JSDoc 정리
기능 변경 0건. DB 변경 0건. 백엔드 변경 0건.
This commit is contained in:
SeongHyun Kim 2026-03-13 16:32:20 +09:00
parent 842ac27d60
commit 320100c4e2
15 changed files with 301 additions and 640 deletions

View File

@ -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<ScreenDefinition | null>(null);
const [layout, setLayout] = useState<PopLayoutDataV5>(createEmptyPopLayoutV5());
const [layout, setLayout] = useState<PopLayoutData>(createEmptyLayout());
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(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);

View File

@ -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<PopComponentDefinitionV5>) => void;
onUpdateComponent: (componentId: string, updates: Partial<PopComponentDefinition>) => 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 (
<div className="flex h-full flex-col bg-muted">
@ -668,7 +654,7 @@ export default function PopCanvas({
<div
className="relative mx-auto my-8 origin-top overflow-visible flex gap-4"
style={{
width: showRightPanel
width: showHiddenPanel
? `${customWidth + 32 + 220}px` // 오른쪽 패널 공간 추가
: `${customWidth + 32}px`,
minHeight: `${dynamicCanvasHeight + 32}px`,
@ -776,20 +762,11 @@ export default function PopCanvas({
</div>
{/* 오른쪽 패널 영역 (초과 컴포넌트 + 숨김 컴포넌트) */}
{showRightPanel && (
{showHiddenPanel && (
<div
className="flex flex-col gap-3"
style={{ marginTop: "32px" }}
>
{/* 검토 필요 패널 */}
{showReviewPanel && (
<ReviewPanel
components={reviewComponents}
selectedComponentId={selectedComponentId}
onSelectComponent={onSelectComponent}
/>
)}
{/* 숨김 컴포넌트 패널 */}
{showHiddenPanel && (
<HiddenPanel
@ -821,99 +798,12 @@ export default function PopCanvas({
// 검토 필요 영역 (오른쪽 패널)
// ========================================
interface ReviewPanelProps {
components: PopComponentDefinitionV5[];
selectedComponentId: string | null;
onSelectComponent: (id: string | null) => void;
}
function ReviewPanel({
components,
selectedComponentId,
onSelectComponent,
}: ReviewPanelProps) {
return (
<div
className="flex flex-col rounded-lg border-2 border-dashed border-primary/40 bg-primary/5"
style={{
width: "200px",
maxHeight: "300px",
}}
>
{/* 헤더 */}
<div className="flex items-center gap-2 border-b border-primary/20 bg-primary/5 px-3 py-2 rounded-t-lg">
<AlertTriangle className="h-4 w-4 text-primary" />
<span className="text-xs font-semibold text-primary">
({components.length})
</span>
</div>
{/* 컴포넌트 목록 */}
<div className="flex-1 overflow-auto p-2 space-y-2">
{components.map((comp) => (
<ReviewItem
key={comp.id}
component={comp}
isSelected={selectedComponentId === comp.id}
onSelect={() => onSelectComponent(comp.id)}
/>
))}
</div>
{/* 안내 문구 */}
<div className="border-t border-primary/20 px-3 py-2 bg-primary/10 rounded-b-lg">
<p className="text-[10px] text-primary leading-tight">
.
</p>
</div>
</div>
);
}
// ========================================
// 검토 필요 아이템 (ReviewPanel 내부)
// ========================================
interface ReviewItemProps {
component: PopComponentDefinitionV5;
isSelected: boolean;
onSelect: () => void;
}
function ReviewItem({
component,
isSelected,
onSelect,
}: ReviewItemProps) {
return (
<div
className={cn(
"flex flex-col gap-1 rounded-md border-2 p-2 cursor-pointer transition-all",
isSelected
? "border-primary bg-primary/10 shadow-sm"
: "border-primary/20 bg-background hover:border-primary/60 hover:bg-primary/10"
)}
onClick={(e) => {
e.stopPropagation();
onSelect();
}}
>
<span className="text-xs font-medium text-primary line-clamp-1">
{component.label || component.id}
</span>
<span className="text-[10px] text-primary bg-primary/10 rounded px-1.5 py-0.5 self-start">
</span>
</div>
);
}
// ========================================
// 숨김 컴포넌트 영역 (오른쪽 패널)
// ========================================
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;
}

View File

@ -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<PopLayoutDataV5>(createEmptyPopLayoutV5());
const [layout, setLayout] = useState<PopLayoutData>(createEmptyLayout());
// 히스토리
const [history, setHistory] = useState<PopLayoutDataV5[]>([]);
const [history, setHistory] = useState<PopLayoutData[]>([]);
const [historyIndex, setHistoryIndex] = useState(-1);
// UI 상태
@ -84,7 +85,7 @@ export default function PopDesigner({
const [activeCanvasId, setActiveCanvasId] = useState<string>("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<PopComponentDefinitionV5>) => {
(componentId: string, updates: Partial<PopComponentDefinition>) => {
// 함수적 업데이트로 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<PopDataConnection, "id">) => {
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,

View File

@ -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,

View File

@ -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<PopComponentDefinitionV5>) => void;
onUpdateComponent?: (updates: Partial<PopComponentDefinition>) => 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<PopComponentDefinitionV5>) => void;
onUpdate?: (updates: Partial<PopComponentDefinition>) => 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<PopComponentDefinitionV5>) => void;
component: PopComponentDefinition;
onUpdate?: (updates: Partial<PopComponentDefinition>) => 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<PopComponentDefinitionV5>) => void;
component: PopComponentDefinition;
onUpdate?: (updates: Partial<PopComponentDefinition>) => void;
}
function VisibilityForm({ component, onUpdate }: VisibilityFormProps) {

View File

@ -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<PopDataConnection, "id">) => void;
onUpdateConnection?: (connectionId: string, conn: Omit<PopDataConnection, "id">) => void;
@ -102,8 +102,8 @@ export default function ConnectionEditor({
// ========================================
interface SendSectionProps {
component: PopComponentDefinitionV5;
allComponents: PopComponentDefinitionV5[];
component: PopComponentDefinition;
allComponents: PopComponentDefinition[];
outgoing: PopDataConnection[];
onAddConnection?: (conn: Omit<PopDataConnection, "id">) => void;
onUpdateConnection?: (connectionId: string, conn: Omit<PopDataConnection, "id">) => void;
@ -197,15 +197,15 @@ function SendSection({
// ========================================
interface SimpleConnectionFormProps {
component: PopComponentDefinitionV5;
allComponents: PopComponentDefinitionV5[];
component: PopComponentDefinition;
allComponents: PopComponentDefinition[];
initial?: PopDataConnection;
onSubmit: (data: Omit<PopDataConnection, "id">) => void;
onCancel?: () => void;
submitLabel: string;
}
function extractSubTableName(comp: PopComponentDefinitionV5): string | null {
function extractSubTableName(comp: PopComponentDefinition): string | null {
const cfg = comp.config as Record<string, unknown> | undefined;
if (!cfg) return null;
@ -423,8 +423,8 @@ function SimpleConnectionForm({
// ========================================
interface ReceiveSectionProps {
component: PopComponentDefinitionV5;
allComponents: PopComponentDefinitionV5[];
component: PopComponentDefinition;
allComponents: PopComponentDefinition[];
incoming: PopDataConnection[];
}

View File

@ -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,

View File

@ -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<string, PopComponentDefinitionV5>;
components: Record<string, PopComponentDefinition>;
// 데이터 흐름
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<GapPreset, GapPresetConfig> = {
};
/**
* 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<string, Partial<PopGridPosition>>;
@ -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<PopComponentType, { colSpan: nu
};
/**
* v5
* POP
*/
export const createComponentDefinitionV5 = (
export const createComponentDefinition = (
id: string,
type: PopComponentType,
position: PopGridPosition,
label?: string
): PopComponentDefinitionV5 => ({
): 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<string, PopComponentDefinitionV5>;
components: Record<string, PopComponentDefinition>;
/** 모드별 오버라이드 */
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;

View File

@ -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<number, Array<{ id: string; position: PopGridPosition }>> = {};
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<string, PopGridPosition> {
const result = new Map<string, PopGridPosition>();
// 숨김 처리된 컴포넌트 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<number, string[]> = {};
Object.entries(layout.components).forEach(([id, comp]) => {
const r = comp.position.row;
if (!rowGroups[r]) rowGroups[r] = [];
rowGroups[r].push(id);
});
const convertedPositions: Record<string, PopGridPosition> = {};
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<number, number> = {};
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,
};
}

View File

@ -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<number, string[]> = {};
Object.entries(layout.components).forEach(([id, comp]) => {
const r = comp.position.row;
if (!rowGroups[r]) rowGroups[r] = [];
rowGroups[r].push(id);
});
const convertedPositions: Record<string, PopGridPosition> = {};
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<number, number> = {};
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,
};
}

View File

@ -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,

View File

@ -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<PopLayoutDataV5 | null>(null);
const [popModalLayout, setPopModalLayout] = useState<PopLayoutData | null>(null);
const [popModalScreenId, setPopModalScreenId] = useState<string>("");
const [popModalRow, setPopModalRow] = useState<RowData | null>(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);

View File

@ -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<string, string> = {
interface PopScannerConfigPanelProps {
config: PopScannerConfig;
onUpdate: (config: PopScannerConfig) => void;
allComponents?: PopComponentDefinitionV5[];
allComponents?: PopComponentDefinition[];
connections?: PopDataConnection[];
componentId?: string;
}

View File

@ -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<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;
}
@ -268,7 +268,7 @@ interface FilterConnectionSectionProps {
update: (partial: Partial<PopSearchConfig>) => 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;

View File

@ -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;
}