feat: 레이어 시스템 추가 및 관리 기능 구현
- InteractiveScreenViewer 컴포넌트에 레이어 시스템을 도입하여, 레이어의 활성화 및 조건부 표시 로직을 추가하였습니다. - ScreenDesigner 컴포넌트에서 레이어 상태 관리 및 레이어 정보 저장 기능을 구현하였습니다. - 레이어 정의 및 조건부 표시 설정을 위한 새로운 타입과 스키마를 추가하여, 레이어 기반의 UI 구성 요소를 보다 유연하게 관리할 수 있도록 하였습니다. - 레이어별 컴포넌트 렌더링 로직을 추가하여, 모달 및 드로어 형태의 레이어를 효과적으로 처리할 수 있도록 개선하였습니다. - 전반적으로 레이어 시스템을 통해 사용자 경험을 향상시키고, UI 구성의 유연성을 높였습니다.
This commit is contained in:
@@ -123,9 +123,12 @@ interface ScreenDesignerProps {
|
||||
onScreenUpdate?: (updatedScreen: Partial<ScreenDefinition>) => void;
|
||||
}
|
||||
|
||||
// 패널 설정 (통합 패널 1개)
|
||||
import { useLayerOptional, LayerProvider, createDefaultLayer } from "@/contexts/LayerContext";
|
||||
import { LayerManagerPanel } from "./LayerManagerPanel";
|
||||
import { LayerType, LayerDefinition } from "@/types/screen-management";
|
||||
|
||||
// 패널 설정 업데이트
|
||||
const panelConfigs: PanelConfig[] = [
|
||||
// 통합 패널 (컴포넌트 + 편집 탭)
|
||||
{
|
||||
id: "v2",
|
||||
title: "패널",
|
||||
@@ -134,12 +137,17 @@ const panelConfigs: PanelConfig[] = [
|
||||
defaultHeight: 700,
|
||||
shortcutKey: "p",
|
||||
},
|
||||
{
|
||||
id: "layer",
|
||||
title: "레이어",
|
||||
defaultPosition: "right",
|
||||
defaultWidth: 240,
|
||||
defaultHeight: 500,
|
||||
shortcutKey: "l",
|
||||
},
|
||||
];
|
||||
|
||||
export default function ScreenDesigner({ selectedScreen, onBackToList, onScreenUpdate }: ScreenDesignerProps) {
|
||||
// 패널 상태 관리
|
||||
const { panelStates, togglePanel, openPanel, closePanel } = usePanelState(panelConfigs);
|
||||
|
||||
const [layout, setLayout] = useState<LayoutData>({
|
||||
components: [],
|
||||
gridSettings: {
|
||||
@@ -171,6 +179,10 @@ export default function ScreenDesigner({ selectedScreen, onBackToList, onScreenU
|
||||
SCREEN_RESOLUTIONS[0], // 기본값: Full HD
|
||||
);
|
||||
|
||||
// 🆕 패널 상태 관리 (usePanelState 훅)
|
||||
const { panelStates, togglePanel, openPanel, closePanel, closeAllPanels, updatePanelPosition, updatePanelSize } =
|
||||
usePanelState(panelConfigs);
|
||||
|
||||
const [selectedComponent, setSelectedComponent] = useState<ComponentData | null>(null);
|
||||
|
||||
// 🆕 탭 내부 컴포넌트 선택 상태 (중첩 구조 지원)
|
||||
@@ -438,6 +450,17 @@ export default function ScreenDesigner({ selectedScreen, onBackToList, onScreenU
|
||||
const [tables, setTables] = useState<TableInfo[]>([]);
|
||||
const [searchTerm, setSearchTerm] = useState("");
|
||||
|
||||
// 🆕 검색어로 필터링된 테이블 목록
|
||||
const filteredTables = useMemo(() => {
|
||||
if (!searchTerm.trim()) return tables;
|
||||
const term = searchTerm.toLowerCase();
|
||||
return tables.filter(
|
||||
(table) =>
|
||||
table.tableName.toLowerCase().includes(term) ||
|
||||
table.columns?.some((col) => col.columnName.toLowerCase().includes(term)),
|
||||
);
|
||||
}, [tables, searchTerm]);
|
||||
|
||||
// 그룹 생성 다이얼로그
|
||||
const [showGroupCreateDialog, setShowGroupCreateDialog] = useState(false);
|
||||
|
||||
@@ -462,15 +485,25 @@ export default function ScreenDesigner({ selectedScreen, onBackToList, onScreenU
|
||||
return lines;
|
||||
}, [layout.gridSettings?.showGrid, screenResolution.width, screenResolution.height]);
|
||||
|
||||
// 필터된 테이블 목록
|
||||
const filteredTables = useMemo(() => {
|
||||
if (!searchTerm) return tables;
|
||||
return tables.filter(
|
||||
(table) =>
|
||||
table.tableName.toLowerCase().includes(searchTerm.toLowerCase()) ||
|
||||
table.columns.some((col) => col.columnName.toLowerCase().includes(searchTerm.toLowerCase())),
|
||||
);
|
||||
}, [tables, searchTerm]);
|
||||
// 🆕 레이어 활성 상태 관리 (LayerProvider 외부에서 관리)
|
||||
const [activeLayerId, setActiveLayerIdLocal] = useState<string | null>("default-layer");
|
||||
|
||||
// 캔버스에 렌더링할 컴포넌트 필터링 (레이어 기반)
|
||||
// 활성 레이어가 있으면 해당 레이어의 컴포넌트만 표시
|
||||
// layerId가 없는 컴포넌트는 기본 레이어("default-layer")에 속한 것으로 처리
|
||||
const visibleComponents = useMemo(() => {
|
||||
// 레이어 시스템이 활성화되지 않았거나 활성 레이어가 없으면 모든 컴포넌트 표시
|
||||
if (!activeLayerId) {
|
||||
return layout.components;
|
||||
}
|
||||
|
||||
// 활성 레이어에 속한 컴포넌트만 필터링
|
||||
return layout.components.filter((comp) => {
|
||||
// layerId가 없는 컴포넌트는 기본 레이어("default-layer")에 속한 것으로 처리
|
||||
const compLayerId = comp.layerId || "default-layer";
|
||||
return compLayerId === activeLayerId;
|
||||
});
|
||||
}, [layout.components, activeLayerId]);
|
||||
|
||||
// 이미 배치된 컬럼 목록 계산
|
||||
const placedColumns = useMemo(() => {
|
||||
@@ -1798,9 +1831,20 @@ export default function ScreenDesigner({ selectedScreen, onBackToList, onScreenU
|
||||
// 현재 선택된 테이블을 화면의 기본 테이블로 저장
|
||||
const currentMainTableName = tables.length > 0 ? tables[0].tableName : null;
|
||||
|
||||
// 🆕 레이어 정보도 함께 저장 (레이어가 있으면 레이어의 컴포넌트로 업데이트)
|
||||
const updatedLayers = layout.layers?.map((layer) => ({
|
||||
...layer,
|
||||
components: layer.components.map((comp) => {
|
||||
// 분할 패널 업데이트 로직 적용
|
||||
const updatedComp = updatedComponents.find((uc) => uc.id === comp.id);
|
||||
return updatedComp || comp;
|
||||
}),
|
||||
}));
|
||||
|
||||
const layoutWithResolution = {
|
||||
...layout,
|
||||
components: updatedComponents,
|
||||
layers: updatedLayers, // 🆕 레이어 정보 포함
|
||||
screenResolution: screenResolution,
|
||||
mainTableName: currentMainTableName, // 화면의 기본 테이블
|
||||
};
|
||||
@@ -2339,23 +2383,29 @@ export default function ScreenDesigner({ selectedScreen, onBackToList, onScreenU
|
||||
}
|
||||
});
|
||||
|
||||
// 🆕 현재 활성 레이어에 컴포넌트 추가
|
||||
const componentsWithLayerId = newComponents.map((comp) => ({
|
||||
...comp,
|
||||
layerId: activeLayerId || "default-layer",
|
||||
}));
|
||||
|
||||
// 레이아웃에 새 컴포넌트들 추가
|
||||
const newLayout = {
|
||||
...layout,
|
||||
components: [...layout.components, ...newComponents],
|
||||
components: [...layout.components, ...componentsWithLayerId],
|
||||
};
|
||||
|
||||
setLayout(newLayout);
|
||||
saveToHistory(newLayout);
|
||||
|
||||
// 첫 번째 컴포넌트 선택
|
||||
if (newComponents.length > 0) {
|
||||
setSelectedComponent(newComponents[0]);
|
||||
if (componentsWithLayerId.length > 0) {
|
||||
setSelectedComponent(componentsWithLayerId[0]);
|
||||
}
|
||||
|
||||
toast.success(`${template.name} 템플릿이 추가되었습니다.`);
|
||||
},
|
||||
[layout, selectedScreen, saveToHistory],
|
||||
[layout, selectedScreen, saveToHistory, activeLayerId],
|
||||
);
|
||||
|
||||
// 레이아웃 드래그 처리
|
||||
@@ -2409,6 +2459,7 @@ export default function ScreenDesigner({ selectedScreen, onBackToList, onScreenU
|
||||
label: layoutData.label,
|
||||
allowedComponentTypes: layoutData.allowedComponentTypes,
|
||||
dropZoneConfig: layoutData.dropZoneConfig,
|
||||
layerId: activeLayerId || "default-layer", // 🆕 현재 활성 레이어에 추가
|
||||
} as ComponentData;
|
||||
|
||||
// 레이아웃에 새 컴포넌트 추가
|
||||
@@ -2425,7 +2476,7 @@ export default function ScreenDesigner({ selectedScreen, onBackToList, onScreenU
|
||||
|
||||
toast.success(`${layoutData.label} 레이아웃이 추가되었습니다.`);
|
||||
},
|
||||
[layout, screenResolution, saveToHistory, zoomLevel],
|
||||
[layout, screenResolution, saveToHistory, zoomLevel, activeLayerId],
|
||||
);
|
||||
|
||||
// handleZoneComponentDrop은 handleComponentDrop으로 대체됨
|
||||
@@ -3016,6 +3067,7 @@ export default function ScreenDesigner({ selectedScreen, onBackToList, onScreenU
|
||||
position: snappedPosition,
|
||||
size: componentSize,
|
||||
gridColumns: gridColumns, // 컴포넌트별 그리드 컬럼 수 적용
|
||||
layerId: activeLayerId || "default-layer", // 🆕 현재 활성 레이어에 추가
|
||||
componentConfig: {
|
||||
type: component.id, // 새 컴포넌트 시스템의 ID 사용
|
||||
webType: component.webType, // 웹타입 정보 추가
|
||||
@@ -3049,7 +3101,7 @@ export default function ScreenDesigner({ selectedScreen, onBackToList, onScreenU
|
||||
|
||||
toast.success(`${component.name} 컴포넌트가 추가되었습니다.`);
|
||||
},
|
||||
[layout, selectedScreen, saveToHistory],
|
||||
[layout, selectedScreen, saveToHistory, activeLayerId],
|
||||
);
|
||||
|
||||
// 드래그 앤 드롭 처리
|
||||
@@ -3421,6 +3473,7 @@ export default function ScreenDesigner({ selectedScreen, onBackToList, onScreenU
|
||||
tableName: table.tableName,
|
||||
position: { x, y, z: 1 } as Position,
|
||||
size: { width: 300, height: 200 },
|
||||
layerId: activeLayerId || "default-layer", // 🆕 현재 활성 레이어에 추가
|
||||
style: {
|
||||
labelDisplay: true,
|
||||
labelFontSize: "14px",
|
||||
@@ -3671,6 +3724,7 @@ export default function ScreenDesigner({ selectedScreen, onBackToList, onScreenU
|
||||
componentType: v2Mapping.componentType, // v2-input, v2-select 등
|
||||
position: { x: relativeX, y: relativeY, z: 1 } as Position,
|
||||
size: { width: componentWidth, height: getDefaultHeight(column.widgetType) },
|
||||
layerId: activeLayerId || "default-layer", // 🆕 현재 활성 레이어에 추가
|
||||
// 코드 타입인 경우 코드 카테고리 정보 추가
|
||||
...(column.widgetType === "code" &&
|
||||
column.codeCategory && {
|
||||
@@ -3737,6 +3791,7 @@ export default function ScreenDesigner({ selectedScreen, onBackToList, onScreenU
|
||||
componentType: v2Mapping.componentType, // v2-input, v2-select 등
|
||||
position: { x, y, z: 1 } as Position,
|
||||
size: { width: componentWidth, height: getDefaultHeight(column.widgetType) },
|
||||
layerId: activeLayerId || "default-layer", // 🆕 현재 활성 레이어에 추가
|
||||
// 코드 타입인 경우 코드 카테고리 정보 추가
|
||||
...(column.widgetType === "code" &&
|
||||
column.codeCategory && {
|
||||
@@ -4388,7 +4443,8 @@ export default function ScreenDesigner({ selectedScreen, onBackToList, onScreenU
|
||||
bottom: Math.max(selectionDrag.startPoint.y, currentPoint.y),
|
||||
};
|
||||
|
||||
const selectedIds = layout.components
|
||||
// 🆕 visibleComponents만 선택 대상으로 (현재 활성 레이어의 컴포넌트만)
|
||||
const selectedIds = visibleComponents
|
||||
.filter((comp) => {
|
||||
const compRect = {
|
||||
left: comp.position.x,
|
||||
@@ -4411,7 +4467,7 @@ export default function ScreenDesigner({ selectedScreen, onBackToList, onScreenU
|
||||
selectedComponents: selectedIds,
|
||||
}));
|
||||
},
|
||||
[selectionDrag.isSelecting, selectionDrag.startPoint, layout.components, zoomLevel],
|
||||
[selectionDrag.isSelecting, selectionDrag.startPoint, visibleComponents, zoomLevel],
|
||||
);
|
||||
|
||||
// 드래그 선택 종료
|
||||
@@ -4558,6 +4614,7 @@ export default function ScreenDesigner({ selectedScreen, onBackToList, onScreenU
|
||||
z: clipComponent.position.z || 1,
|
||||
} as Position,
|
||||
parentId: undefined, // 붙여넣기 시 부모 관계 해제
|
||||
layerId: activeLayerId || "default-layer", // 🆕 현재 활성 레이어에 붙여넣기
|
||||
};
|
||||
newComponents.push(newComponent);
|
||||
});
|
||||
@@ -4578,7 +4635,7 @@ export default function ScreenDesigner({ selectedScreen, onBackToList, onScreenU
|
||||
|
||||
// console.log("컴포넌트 붙여넣기 완료:", newComponents.length, "개");
|
||||
toast.success(`${newComponents.length}개 컴포넌트가 붙여넣어졌습니다.`);
|
||||
}, [clipboard, layout, saveToHistory]);
|
||||
}, [clipboard, layout, saveToHistory, activeLayerId]);
|
||||
|
||||
// 🆕 플로우 버튼 그룹 생성 (다중 선택된 버튼들을 한 번에 그룹으로)
|
||||
// 🆕 플로우 버튼 그룹 다이얼로그 상태
|
||||
@@ -5374,6 +5431,36 @@ export default function ScreenDesigner({ selectedScreen, onBackToList, onScreenU
|
||||
};
|
||||
}, [layout, selectedComponent]);
|
||||
|
||||
// 🆕 레이어 변경 핸들러 - 레이어 컨텍스트에서 레이어가 변경되면 layout에도 반영
|
||||
// 주의: layout.components는 layerId 속성으로 레이어를 구분하므로, 여기서 덮어쓰지 않음
|
||||
const handleLayersChange = useCallback((newLayers: LayerDefinition[]) => {
|
||||
setLayout((prevLayout) => ({
|
||||
...prevLayout,
|
||||
layers: newLayers,
|
||||
// components는 그대로 유지 - layerId 속성으로 레이어 구분
|
||||
// components: prevLayout.components (기본값으로 유지됨)
|
||||
}));
|
||||
}, []);
|
||||
|
||||
// 🆕 활성 레이어 변경 핸들러
|
||||
const handleActiveLayerChange = useCallback((newActiveLayerId: string | null) => {
|
||||
setActiveLayerIdLocal(newActiveLayerId);
|
||||
}, []);
|
||||
|
||||
// 🆕 초기 레이어 계산 - layout에서 layers가 있으면 사용, 없으면 기본 레이어 생성
|
||||
// 주의: components는 layout.components에 layerId 속성으로 저장되므로, layer.components는 비워둠
|
||||
const initialLayers = useMemo<LayerDefinition[]>(() => {
|
||||
if (layout.layers && layout.layers.length > 0) {
|
||||
// 기존 레이어 구조 사용 (layer.components는 무시하고 빈 배열로 설정)
|
||||
return layout.layers.map(layer => ({
|
||||
...layer,
|
||||
components: [], // layout.components + layerId 방식 사용
|
||||
}));
|
||||
}
|
||||
// layers가 없으면 기본 레이어 생성 (components는 빈 배열)
|
||||
return [createDefaultLayer()];
|
||||
}, [layout.layers]);
|
||||
|
||||
if (!selectedScreen) {
|
||||
return (
|
||||
<div className="bg-background flex h-full items-center justify-center">
|
||||
@@ -5393,7 +5480,12 @@ export default function ScreenDesigner({ selectedScreen, onBackToList, onScreenU
|
||||
|
||||
return (
|
||||
<ScreenPreviewProvider isPreviewMode={false}>
|
||||
<TableOptionsProvider>
|
||||
<LayerProvider
|
||||
initialLayers={initialLayers}
|
||||
onLayersChange={handleLayersChange}
|
||||
onActiveLayerChange={handleActiveLayerChange}
|
||||
>
|
||||
<TableOptionsProvider>
|
||||
<div className="bg-background flex h-full w-full flex-col">
|
||||
{/* 상단 슬림 툴바 */}
|
||||
<SlimToolbar
|
||||
@@ -5428,10 +5520,13 @@ export default function ScreenDesigner({ selectedScreen, onBackToList, onScreenU
|
||||
</div>
|
||||
<div className="flex min-h-0 flex-1 flex-col overflow-hidden">
|
||||
<Tabs defaultValue="components" className="flex min-h-0 flex-1 flex-col">
|
||||
<TabsList className="mx-4 mt-2 grid h-8 w-auto grid-cols-2 gap-1">
|
||||
<TabsList className="mx-4 mt-2 grid h-8 w-auto grid-cols-3 gap-1">
|
||||
<TabsTrigger value="components" className="text-xs">
|
||||
컴포넌트
|
||||
</TabsTrigger>
|
||||
<TabsTrigger value="layers" className="text-xs">
|
||||
레이어
|
||||
</TabsTrigger>
|
||||
<TabsTrigger value="properties" className="text-xs">
|
||||
편집
|
||||
</TabsTrigger>
|
||||
@@ -5457,6 +5552,11 @@ export default function ScreenDesigner({ selectedScreen, onBackToList, onScreenU
|
||||
/>
|
||||
</TabsContent>
|
||||
|
||||
{/* 🆕 레이어 관리 탭 */}
|
||||
<TabsContent value="layers" className="mt-0 flex-1 overflow-hidden">
|
||||
<LayerManagerPanel components={layout.components} />
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="properties" className="mt-0 flex-1 overflow-hidden">
|
||||
{/* 탭 내부 컴포넌트 선택 시에도 V2PropertiesPanel 사용 */}
|
||||
{selectedTabComponentInfo ? (
|
||||
@@ -6088,7 +6188,8 @@ export default function ScreenDesigner({ selectedScreen, onBackToList, onScreenU
|
||||
{/* 컴포넌트들 */}
|
||||
{(() => {
|
||||
// 🆕 플로우 버튼 그룹 감지 및 처리
|
||||
const topLevelComponents = layout.components.filter((component) => !component.parentId);
|
||||
// visibleComponents를 사용하여 활성 레이어의 컴포넌트만 표시
|
||||
const topLevelComponents = visibleComponents.filter((component) => !component.parentId);
|
||||
|
||||
// auto-compact 모드의 버튼들을 그룹별로 묶기
|
||||
const buttonGroups: Record<string, ComponentData[]> = {};
|
||||
@@ -6740,6 +6841,7 @@ export default function ScreenDesigner({ selectedScreen, onBackToList, onScreenU
|
||||
/>
|
||||
</div>
|
||||
</TableOptionsProvider>
|
||||
</LayerProvider>
|
||||
</ScreenPreviewProvider>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user