338 lines
11 KiB
TypeScript
338 lines
11 KiB
TypeScript
import React, { createContext, useContext, useState, useCallback, ReactNode, useMemo } from "react";
|
|
import { LayerDefinition, LayerType, ComponentData } from "@/types/screen-management";
|
|
import { v4 as uuidv4 } from "uuid";
|
|
|
|
interface LayerContextType {
|
|
// 레이어 상태
|
|
layers: LayerDefinition[];
|
|
activeLayerId: string | null;
|
|
activeLayer: LayerDefinition | null;
|
|
|
|
// 레이어 관리
|
|
setLayers: (layers: LayerDefinition[]) => void;
|
|
setActiveLayerId: (id: string | null) => void;
|
|
addLayer: (type: LayerType, name?: string) => void;
|
|
removeLayer: (id: string) => void;
|
|
updateLayer: (id: string, updates: Partial<LayerDefinition>) => void;
|
|
moveLayer: (dragIndex: number, hoverIndex: number) => void;
|
|
toggleLayerVisibility: (id: string) => void;
|
|
toggleLayerLock: (id: string) => void;
|
|
getLayerById: (id: string) => LayerDefinition | undefined;
|
|
|
|
// 컴포넌트 관리 (레이어별)
|
|
addComponentToLayer: (layerId: string, component: ComponentData) => void;
|
|
removeComponentFromLayer: (layerId: string, componentId: string) => void;
|
|
updateComponentInLayer: (layerId: string, componentId: string, updates: Partial<ComponentData>) => void;
|
|
moveComponentToLayer: (componentId: string, fromLayerId: string, toLayerId: string) => void;
|
|
|
|
// 컴포넌트 조회
|
|
getAllComponents: () => ComponentData[];
|
|
getComponentById: (componentId: string) => { component: ComponentData; layerId: string } | null;
|
|
getComponentsInActiveLayer: () => ComponentData[];
|
|
|
|
// 레이어 가시성 (런타임용)
|
|
runtimeVisibleLayers: string[];
|
|
setRuntimeVisibleLayers: React.Dispatch<React.SetStateAction<string[]>>;
|
|
showLayer: (layerId: string) => void;
|
|
hideLayer: (layerId: string) => void;
|
|
toggleLayerRuntime: (layerId: string) => void;
|
|
}
|
|
|
|
const LayerContext = createContext<LayerContextType | undefined>(undefined);
|
|
|
|
export const useLayer = () => {
|
|
const context = useContext(LayerContext);
|
|
if (!context) {
|
|
throw new Error("useLayer must be used within a LayerProvider");
|
|
}
|
|
return context;
|
|
};
|
|
|
|
// LayerProvider가 없을 때 사용할 기본 컨텍스트 (선택적 사용)
|
|
export const useLayerOptional = () => {
|
|
return useContext(LayerContext);
|
|
};
|
|
|
|
interface LayerProviderProps {
|
|
children: ReactNode;
|
|
initialLayers?: LayerDefinition[];
|
|
onLayersChange?: (layers: LayerDefinition[]) => void;
|
|
onActiveLayerChange?: (activeLayerId: string | null) => void; // 🆕 활성 레이어 변경 콜백
|
|
}
|
|
|
|
// 기본 레이어 생성 헬퍼
|
|
export const createDefaultLayer = (components?: ComponentData[]): LayerDefinition => ({
|
|
id: "default-layer",
|
|
name: "기본 레이어",
|
|
type: "base",
|
|
zIndex: 0,
|
|
isVisible: true,
|
|
isLocked: false,
|
|
components: components || [],
|
|
});
|
|
|
|
export const LayerProvider: React.FC<LayerProviderProps> = ({
|
|
children,
|
|
initialLayers = [],
|
|
onLayersChange,
|
|
onActiveLayerChange,
|
|
}) => {
|
|
// 초기 레이어가 없으면 기본 레이어 생성
|
|
const effectiveInitialLayers = initialLayers.length > 0
|
|
? initialLayers
|
|
: [createDefaultLayer()];
|
|
|
|
const [layers, setLayersState] = useState<LayerDefinition[]>(effectiveInitialLayers);
|
|
const [activeLayerIdState, setActiveLayerIdState] = useState<string | null>(
|
|
effectiveInitialLayers.length > 0 ? effectiveInitialLayers[0].id : null,
|
|
);
|
|
|
|
// 🆕 활성 레이어 변경 시 콜백 호출
|
|
const setActiveLayerId = useCallback((id: string | null) => {
|
|
setActiveLayerIdState(id);
|
|
onActiveLayerChange?.(id);
|
|
}, [onActiveLayerChange]);
|
|
|
|
// 활성 레이어 ID (내부 상태 사용)
|
|
const activeLayerId = activeLayerIdState;
|
|
|
|
// 런타임 가시성 상태 (편집기에서의 isVisible과 별개)
|
|
const [runtimeVisibleLayers, setRuntimeVisibleLayers] = useState<string[]>(
|
|
effectiveInitialLayers.filter(l => l.isVisible).map(l => l.id)
|
|
);
|
|
|
|
// 레이어 변경 시 콜백 호출
|
|
const setLayers = useCallback((newLayers: LayerDefinition[]) => {
|
|
setLayersState(newLayers);
|
|
onLayersChange?.(newLayers);
|
|
}, [onLayersChange]);
|
|
|
|
// 활성 레이어 계산
|
|
const activeLayer = useMemo(() => {
|
|
return layers.find(l => l.id === activeLayerId) || null;
|
|
}, [layers, activeLayerId]);
|
|
|
|
const addLayer = useCallback(
|
|
(type: LayerType, name?: string) => {
|
|
const newLayer: LayerDefinition = {
|
|
id: uuidv4(),
|
|
name: name || `새 레이어 ${layers.length + 1}`,
|
|
type,
|
|
zIndex: layers.length,
|
|
isVisible: true,
|
|
isLocked: false,
|
|
components: [],
|
|
// 모달/드로어 기본 설정
|
|
...(type === "modal" || type === "drawer" ? {
|
|
overlayConfig: {
|
|
backdrop: true,
|
|
closeOnBackdropClick: true,
|
|
width: type === "drawer" ? "320px" : "600px",
|
|
height: type === "drawer" ? "100%" : "auto",
|
|
},
|
|
} : {}),
|
|
};
|
|
|
|
setLayers([...layers, newLayer]);
|
|
setActiveLayerId(newLayer.id);
|
|
// 새 레이어는 런타임에서도 기본적으로 표시
|
|
setRuntimeVisibleLayers(prev => [...prev, newLayer.id]);
|
|
},
|
|
[layers, setLayers],
|
|
);
|
|
|
|
const removeLayer = useCallback(
|
|
(id: string) => {
|
|
// 기본 레이어는 삭제 불가
|
|
const layer = layers.find(l => l.id === id);
|
|
if (layer?.type === "base") {
|
|
console.warn("기본 레이어는 삭제할 수 없습니다.");
|
|
return;
|
|
}
|
|
|
|
const filtered = layers.filter((layer) => layer.id !== id);
|
|
setLayers(filtered);
|
|
|
|
if (activeLayerId === id) {
|
|
setActiveLayerId(filtered.length > 0 ? filtered[0].id : null);
|
|
}
|
|
|
|
setRuntimeVisibleLayers(prev => prev.filter(lid => lid !== id));
|
|
},
|
|
[layers, activeLayerId, setLayers],
|
|
);
|
|
|
|
const updateLayer = useCallback((id: string, updates: Partial<LayerDefinition>) => {
|
|
setLayers(layers.map((layer) => (layer.id === id ? { ...layer, ...updates } : layer)));
|
|
}, [layers, setLayers]);
|
|
|
|
const moveLayer = useCallback((dragIndex: number, hoverIndex: number) => {
|
|
const newLayers = [...layers];
|
|
const [removed] = newLayers.splice(dragIndex, 1);
|
|
newLayers.splice(hoverIndex, 0, removed);
|
|
// Update zIndex based on new order
|
|
setLayers(newLayers.map((layer, index) => ({ ...layer, zIndex: index })));
|
|
}, [layers, setLayers]);
|
|
|
|
const toggleLayerVisibility = useCallback((id: string) => {
|
|
setLayers(layers.map((layer) => (layer.id === id ? { ...layer, isVisible: !layer.isVisible } : layer)));
|
|
}, [layers, setLayers]);
|
|
|
|
const toggleLayerLock = useCallback((id: string) => {
|
|
setLayers(layers.map((layer) => (layer.id === id ? { ...layer, isLocked: !layer.isLocked } : layer)));
|
|
}, [layers, setLayers]);
|
|
|
|
const getLayerById = useCallback(
|
|
(id: string) => {
|
|
return layers.find((layer) => layer.id === id);
|
|
},
|
|
[layers],
|
|
);
|
|
|
|
// ===== 컴포넌트 관리 함수 =====
|
|
|
|
const addComponentToLayer = useCallback((layerId: string, component: ComponentData) => {
|
|
setLayers(layers.map(layer => {
|
|
if (layer.id === layerId) {
|
|
return {
|
|
...layer,
|
|
components: [...layer.components, component],
|
|
};
|
|
}
|
|
return layer;
|
|
}));
|
|
}, [layers, setLayers]);
|
|
|
|
const removeComponentFromLayer = useCallback((layerId: string, componentId: string) => {
|
|
setLayers(layers.map(layer => {
|
|
if (layer.id === layerId) {
|
|
return {
|
|
...layer,
|
|
components: layer.components.filter(c => c.id !== componentId),
|
|
};
|
|
}
|
|
return layer;
|
|
}));
|
|
}, [layers, setLayers]);
|
|
|
|
const updateComponentInLayer = useCallback((layerId: string, componentId: string, updates: Partial<ComponentData>) => {
|
|
setLayers(layers.map(layer => {
|
|
if (layer.id === layerId) {
|
|
return {
|
|
...layer,
|
|
components: layer.components.map(c =>
|
|
c.id === componentId ? { ...c, ...updates } as ComponentData : c
|
|
),
|
|
};
|
|
}
|
|
return layer;
|
|
}));
|
|
}, [layers, setLayers]);
|
|
|
|
const moveComponentToLayer = useCallback((componentId: string, fromLayerId: string, toLayerId: string) => {
|
|
if (fromLayerId === toLayerId) return;
|
|
|
|
const fromLayer = layers.find(l => l.id === fromLayerId);
|
|
const component = fromLayer?.components.find(c => c.id === componentId);
|
|
|
|
if (!component) return;
|
|
|
|
setLayers(layers.map(layer => {
|
|
if (layer.id === fromLayerId) {
|
|
return {
|
|
...layer,
|
|
components: layer.components.filter(c => c.id !== componentId),
|
|
};
|
|
}
|
|
if (layer.id === toLayerId) {
|
|
return {
|
|
...layer,
|
|
components: [...layer.components, component],
|
|
};
|
|
}
|
|
return layer;
|
|
}));
|
|
}, [layers, setLayers]);
|
|
|
|
// ===== 컴포넌트 조회 함수 =====
|
|
|
|
const getAllComponents = useCallback((): ComponentData[] => {
|
|
return layers.flatMap(layer => layer.components);
|
|
}, [layers]);
|
|
|
|
const getComponentById = useCallback((componentId: string): { component: ComponentData; layerId: string } | null => {
|
|
for (const layer of layers) {
|
|
const component = layer.components.find(c => c.id === componentId);
|
|
if (component) {
|
|
return { component, layerId: layer.id };
|
|
}
|
|
}
|
|
return null;
|
|
}, [layers]);
|
|
|
|
const getComponentsInActiveLayer = useCallback((): ComponentData[] => {
|
|
const layer = layers.find(l => l.id === activeLayerId);
|
|
return layer?.components || [];
|
|
}, [layers, activeLayerId]);
|
|
|
|
// ===== 런타임 레이어 가시성 관리 =====
|
|
|
|
const showLayer = useCallback((layerId: string) => {
|
|
setRuntimeVisibleLayers(prev => [...new Set([...prev, layerId])]);
|
|
}, []);
|
|
|
|
const hideLayer = useCallback((layerId: string) => {
|
|
setRuntimeVisibleLayers(prev => prev.filter(id => id !== layerId));
|
|
}, []);
|
|
|
|
const toggleLayerRuntime = useCallback((layerId: string) => {
|
|
setRuntimeVisibleLayers(prev =>
|
|
prev.includes(layerId)
|
|
? prev.filter(id => id !== layerId)
|
|
: [...prev, layerId]
|
|
);
|
|
}, []);
|
|
|
|
return (
|
|
<LayerContext.Provider
|
|
value={{
|
|
// 레이어 상태
|
|
layers,
|
|
activeLayerId,
|
|
activeLayer,
|
|
|
|
// 레이어 관리
|
|
setLayers,
|
|
setActiveLayerId,
|
|
addLayer,
|
|
removeLayer,
|
|
updateLayer,
|
|
moveLayer,
|
|
toggleLayerVisibility,
|
|
toggleLayerLock,
|
|
getLayerById,
|
|
|
|
// 컴포넌트 관리
|
|
addComponentToLayer,
|
|
removeComponentFromLayer,
|
|
updateComponentInLayer,
|
|
moveComponentToLayer,
|
|
|
|
// 컴포넌트 조회
|
|
getAllComponents,
|
|
getComponentById,
|
|
getComponentsInActiveLayer,
|
|
|
|
// 런타임 가시성
|
|
runtimeVisibleLayers,
|
|
setRuntimeVisibleLayers,
|
|
showLayer,
|
|
hideLayer,
|
|
toggleLayerRuntime,
|
|
}}
|
|
>
|
|
{children}
|
|
</LayerContext.Provider>
|
|
);
|
|
};
|