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:
parent
842ac27d60
commit
320100c4e2
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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[];
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
};
|
||||
}
|
||||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue