feat: TableSearchWidget 높이 자동 조정 및 컴포넌트 재배치 기능 구현
- TableSearchWidgetHeightContext 추가: 위젯 높이 변화 관리 - TableSearchWidget: ResizeObserver로 높이 변화 감지 및 localStorage 저장 - 실제 화면(/screens/[screenId]/page.tsx)에서만 동작 (디자이너 제외) - TableSearchWidget 아래 컴포넌트들의 Y 위치를 높이 차이만큼 자동 조정 - 화면 로딩 시 저장된 높이 복원 및 컴포넌트 위치 재조정 주요 변경사항: 1. 필터가 여러 줄로 wrap될 때 높이 자동 증가 2. 높이가 늘어난 만큼 아래 컴포넌트들이 자동으로 아래로 이동 3. 새로고침 후에도 설정 유지 (localStorage) 4. 화면 디자이너에서는 기존대로 동작 (영향 없음) 기술 구현: - Context API로 위젯 높이 전역 관리 - ResizeObserver로 실시간 높이 감지 - localStorage로 사용자별 높이 설정 영구 저장 - 컴포넌트 렌더링 시 동적 Y 위치 계산
This commit is contained in:
parent
6d1743c524
commit
a883187889
|
|
@ -1,11 +1,11 @@
|
|||
"use client";
|
||||
|
||||
import React, { useEffect, useState } from "react";
|
||||
import React, { useEffect, useState, useMemo } from "react";
|
||||
import { useParams, useSearchParams } from "next/navigation";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Loader2 } from "lucide-react";
|
||||
import { screenApi } from "@/lib/api/screen";
|
||||
import { ScreenDefinition, LayoutData } from "@/types/screen";
|
||||
import { ScreenDefinition, LayoutData, ComponentData } from "@/types/screen";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { toast } from "sonner";
|
||||
import { initializeComponents } from "@/lib/registry/components";
|
||||
|
|
@ -19,8 +19,9 @@ import { ScreenPreviewProvider } from "@/contexts/ScreenPreviewContext";
|
|||
import { useAuth } from "@/hooks/useAuth"; // 🆕 사용자 정보
|
||||
import { useResponsive } from "@/lib/hooks/useResponsive"; // 🆕 반응형 감지
|
||||
import { TableOptionsProvider } from "@/contexts/TableOptionsContext"; // 🆕 테이블 옵션
|
||||
import { TableSearchWidgetHeightProvider, useTableSearchWidgetHeight } from "@/contexts/TableSearchWidgetHeightContext"; // 🆕 높이 관리
|
||||
|
||||
export default function ScreenViewPage() {
|
||||
function ScreenViewPage() {
|
||||
const params = useParams();
|
||||
const searchParams = useSearchParams();
|
||||
const router = useRouter();
|
||||
|
|
@ -35,6 +36,9 @@ export default function ScreenViewPage() {
|
|||
// 🆕 모바일 환경 감지
|
||||
const { isMobile } = useResponsive();
|
||||
|
||||
// 🆕 TableSearchWidget 높이 관리
|
||||
const { getHeightDiff } = useTableSearchWidgetHeight();
|
||||
|
||||
const [screen, setScreen] = useState<ScreenDefinition | null>(null);
|
||||
const [layout, setLayout] = useState<LayoutData | null>(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
|
|
@ -393,10 +397,55 @@ export default function ScreenViewPage() {
|
|||
|
||||
const regularComponents = topLevelComponents.filter((c) => !processedButtonIds.has(c.id));
|
||||
|
||||
// TableSearchWidget 높이 차이를 계산하여 Y 위치 조정
|
||||
const adjustedComponents = regularComponents.map((component) => {
|
||||
const isTableSearchWidget = (component as any).componentId === "table-search-widget";
|
||||
|
||||
if (isTableSearchWidget) {
|
||||
// TableSearchWidget 자체는 조정하지 않음
|
||||
return component;
|
||||
}
|
||||
|
||||
// TableSearchWidget들을 찾아서 해당 위젯 아래에 있는지 확인
|
||||
const tableSearchWidgets = regularComponents.filter(
|
||||
(c) => (c as any).componentId === "table-search-widget"
|
||||
);
|
||||
|
||||
let totalHeightAdjustment = 0;
|
||||
|
||||
for (const widget of tableSearchWidgets) {
|
||||
// 현재 컴포넌트가 이 위젯 아래에 있는지 확인
|
||||
if (component.position.y > widget.position.y) {
|
||||
const heightDiff = getHeightDiff(screenId, widget.id);
|
||||
if (heightDiff > 0) {
|
||||
totalHeightAdjustment += heightDiff;
|
||||
console.log("📏 [ScreenView] 컴포넌트 위치 조정:", {
|
||||
componentId: component.id,
|
||||
originalY: component.position.y,
|
||||
adjustment: heightDiff,
|
||||
newY: component.position.y + heightDiff,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (totalHeightAdjustment > 0) {
|
||||
return {
|
||||
...component,
|
||||
position: {
|
||||
...component.position,
|
||||
y: component.position.y + totalHeightAdjustment,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
return component;
|
||||
});
|
||||
|
||||
return (
|
||||
<>
|
||||
{/* 일반 컴포넌트들 */}
|
||||
{regularComponents.map((component) => {
|
||||
{adjustedComponents.map((component) => {
|
||||
// 화면 관리 해상도를 사용하므로 위치 조정 불필요
|
||||
return (
|
||||
<RealtimePreview
|
||||
|
|
@ -712,3 +761,14 @@ export default function ScreenViewPage() {
|
|||
</ScreenPreviewProvider>
|
||||
);
|
||||
}
|
||||
|
||||
// 실제 컴포넌트를 Provider로 감싸기
|
||||
function ScreenViewPageWrapper() {
|
||||
return (
|
||||
<TableSearchWidgetHeightProvider>
|
||||
<ScreenViewPage />
|
||||
</TableSearchWidgetHeightProvider>
|
||||
);
|
||||
}
|
||||
|
||||
export default ScreenViewPageWrapper;
|
||||
|
|
|
|||
|
|
@ -0,0 +1,94 @@
|
|||
"use client";
|
||||
|
||||
import React, { createContext, useContext, useState, useCallback } from "react";
|
||||
|
||||
interface WidgetHeight {
|
||||
screenId: number;
|
||||
componentId: string;
|
||||
height: number;
|
||||
originalHeight: number; // 디자이너에서 설정한 원래 높이
|
||||
}
|
||||
|
||||
interface TableSearchWidgetHeightContextValue {
|
||||
widgetHeights: Map<string, WidgetHeight>;
|
||||
setWidgetHeight: (screenId: number, componentId: string, height: number, originalHeight: number) => void;
|
||||
getWidgetHeight: (screenId: number, componentId: string) => WidgetHeight | undefined;
|
||||
getHeightDiff: (screenId: number, componentId: string) => number; // 실제 높이 - 원래 높이
|
||||
}
|
||||
|
||||
const TableSearchWidgetHeightContext = createContext<TableSearchWidgetHeightContextValue | undefined>(
|
||||
undefined
|
||||
);
|
||||
|
||||
export function TableSearchWidgetHeightProvider({ children }: { children: React.ReactNode }) {
|
||||
const [widgetHeights, setWidgetHeights] = useState<Map<string, WidgetHeight>>(new Map());
|
||||
|
||||
const setWidgetHeight = useCallback(
|
||||
(screenId: number, componentId: string, height: number, originalHeight: number) => {
|
||||
const key = `${screenId}_${componentId}`;
|
||||
|
||||
setWidgetHeights((prev) => {
|
||||
const newMap = new Map(prev);
|
||||
newMap.set(key, {
|
||||
screenId,
|
||||
componentId,
|
||||
height,
|
||||
originalHeight,
|
||||
});
|
||||
|
||||
console.log("📏 [TableSearchWidgetHeightContext] 높이 저장:", {
|
||||
key,
|
||||
height,
|
||||
originalHeight,
|
||||
heightDiff: height - originalHeight,
|
||||
});
|
||||
|
||||
return newMap;
|
||||
});
|
||||
},
|
||||
[]
|
||||
);
|
||||
|
||||
const getWidgetHeight = useCallback(
|
||||
(screenId: number, componentId: string): WidgetHeight | undefined => {
|
||||
const key = `${screenId}_${componentId}`;
|
||||
return widgetHeights.get(key);
|
||||
},
|
||||
[widgetHeights]
|
||||
);
|
||||
|
||||
const getHeightDiff = useCallback(
|
||||
(screenId: number, componentId: string): number => {
|
||||
const widgetHeight = getWidgetHeight(screenId, componentId);
|
||||
if (!widgetHeight) return 0;
|
||||
|
||||
const diff = widgetHeight.height - widgetHeight.originalHeight;
|
||||
return diff;
|
||||
},
|
||||
[getWidgetHeight]
|
||||
);
|
||||
|
||||
return (
|
||||
<TableSearchWidgetHeightContext.Provider
|
||||
value={{
|
||||
widgetHeights,
|
||||
setWidgetHeight,
|
||||
getWidgetHeight,
|
||||
getHeightDiff,
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</TableSearchWidgetHeightContext.Provider>
|
||||
);
|
||||
}
|
||||
|
||||
export function useTableSearchWidgetHeight() {
|
||||
const context = useContext(TableSearchWidgetHeightContext);
|
||||
if (!context) {
|
||||
throw new Error(
|
||||
"useTableSearchWidgetHeight must be used within TableSearchWidgetHeightProvider"
|
||||
);
|
||||
}
|
||||
return context;
|
||||
}
|
||||
|
||||
|
|
@ -1,10 +1,11 @@
|
|||
"use client";
|
||||
|
||||
import React, { useState, useEffect } from "react";
|
||||
import React, { useState, useEffect, useRef } from "react";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Settings, Filter, Layers, X } from "lucide-react";
|
||||
import { useTableOptions } from "@/contexts/TableOptionsContext";
|
||||
import { useTableSearchWidgetHeight } from "@/contexts/TableSearchWidgetHeightContext";
|
||||
import { ColumnVisibilityPanel } from "@/components/screen/table-options/ColumnVisibilityPanel";
|
||||
import { FilterPanel } from "@/components/screen/table-options/FilterPanel";
|
||||
import { GroupingPanel } from "@/components/screen/table-options/GroupingPanel";
|
||||
|
|
@ -32,10 +33,23 @@ interface TableSearchWidgetProps {
|
|||
showTableSelector?: boolean; // 테이블 선택 드롭다운 표시 여부
|
||||
};
|
||||
};
|
||||
screenId?: number; // 화면 ID
|
||||
onHeightChange?: (height: number) => void; // 높이 변화 콜백
|
||||
}
|
||||
|
||||
export function TableSearchWidget({ component }: TableSearchWidgetProps) {
|
||||
export function TableSearchWidget({ component, screenId, onHeightChange }: TableSearchWidgetProps) {
|
||||
const { registeredTables, selectedTableId, setSelectedTableId, getTable } = useTableOptions();
|
||||
|
||||
// 높이 관리 context (실제 화면에서만 사용)
|
||||
let setWidgetHeight: ((screenId: number, componentId: string, height: number, originalHeight: number) => void) | undefined;
|
||||
try {
|
||||
const heightContext = useTableSearchWidgetHeight();
|
||||
setWidgetHeight = heightContext.setWidgetHeight;
|
||||
} catch (e) {
|
||||
// Context가 없으면 (디자이너 모드) 무시
|
||||
setWidgetHeight = undefined;
|
||||
}
|
||||
|
||||
const [columnVisibilityOpen, setColumnVisibilityOpen] = useState(false);
|
||||
const [filterOpen, setFilterOpen] = useState(false);
|
||||
const [groupingOpen, setGroupingOpen] = useState(false);
|
||||
|
|
@ -47,6 +61,9 @@ export function TableSearchWidget({ component }: TableSearchWidgetProps) {
|
|||
const [selectOptions, setSelectOptions] = useState<Record<string, Array<{ label: string; value: string }>>>({});
|
||||
// 선택된 값의 라벨 저장 (데이터 없을 때도 라벨 유지)
|
||||
const [selectedLabels, setSelectedLabels] = useState<Record<string, string>>({});
|
||||
|
||||
// 높이 감지를 위한 ref
|
||||
const containerRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
const autoSelectFirstTable = component.componentConfig?.autoSelectFirstTable ?? true;
|
||||
const showTableSelector = component.componentConfig?.showTableSelector ?? true;
|
||||
|
|
@ -164,6 +181,73 @@ export function TableSearchWidget({ component }: TableSearchWidgetProps) {
|
|||
}
|
||||
}, [selectedTableId, currentTable?.dataCount]);
|
||||
|
||||
// 높이 변화 감지 및 알림 (실제 화면에서만)
|
||||
useEffect(() => {
|
||||
if (!containerRef.current || !screenId || !setWidgetHeight) return;
|
||||
|
||||
// 컴포넌트의 원래 높이 (디자이너에서 설정한 높이)
|
||||
const originalHeight = (component as any).size?.height || 50;
|
||||
|
||||
const resizeObserver = new ResizeObserver((entries) => {
|
||||
for (const entry of entries) {
|
||||
const newHeight = entry.contentRect.height;
|
||||
|
||||
console.log("📏 [TableSearchWidget] 높이 변화 감지:", {
|
||||
screenId,
|
||||
componentId: component.id,
|
||||
originalHeight,
|
||||
newHeight,
|
||||
heightDiff: newHeight - originalHeight,
|
||||
});
|
||||
|
||||
// Context에 높이 저장 (다른 컴포넌트 위치 조정에 사용)
|
||||
setWidgetHeight(screenId, component.id, newHeight, originalHeight);
|
||||
|
||||
// localStorage에 높이 저장 (새로고침 시 복원용)
|
||||
localStorage.setItem(
|
||||
`table_search_widget_height_screen_${screenId}_${component.id}`,
|
||||
JSON.stringify({ height: newHeight, originalHeight })
|
||||
);
|
||||
|
||||
// 콜백이 있으면 호출
|
||||
if (onHeightChange) {
|
||||
onHeightChange(newHeight);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
resizeObserver.observe(containerRef.current);
|
||||
|
||||
return () => {
|
||||
resizeObserver.disconnect();
|
||||
};
|
||||
}, [screenId, component.id, setWidgetHeight, onHeightChange]);
|
||||
|
||||
// 화면 로딩 시 저장된 높이 복원
|
||||
useEffect(() => {
|
||||
if (!screenId || !setWidgetHeight) return;
|
||||
|
||||
const storageKey = `table_search_widget_height_screen_${screenId}_${component.id}`;
|
||||
const savedData = localStorage.getItem(storageKey);
|
||||
|
||||
if (savedData) {
|
||||
try {
|
||||
const { height, originalHeight } = JSON.parse(savedData);
|
||||
setWidgetHeight(screenId, component.id, height, originalHeight);
|
||||
|
||||
console.log("📥 [TableSearchWidget] 저장된 높이 복원:", {
|
||||
screenId,
|
||||
componentId: component.id,
|
||||
height,
|
||||
originalHeight,
|
||||
heightDiff: height - originalHeight,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("저장된 높이 복원 실패:", error);
|
||||
}
|
||||
}
|
||||
}, [screenId, component.id, setWidgetHeight]);
|
||||
|
||||
const hasMultipleTables = tableList.length > 1;
|
||||
|
||||
// 필터 값 변경 핸들러
|
||||
|
|
@ -326,6 +410,7 @@ export function TableSearchWidget({ component }: TableSearchWidgetProps) {
|
|||
|
||||
return (
|
||||
<div
|
||||
ref={containerRef}
|
||||
className="flex w-full flex-wrap items-center gap-2 border-b bg-card"
|
||||
style={{
|
||||
padding: component.style?.padding || "0.75rem",
|
||||
|
|
|
|||
Loading…
Reference in New Issue