탭안에있는 화면 검색필터링 기능

This commit is contained in:
kjs 2025-12-17 15:00:15 +09:00
parent 1995c3dca4
commit be916d3db7
12 changed files with 419 additions and 171 deletions

View File

@ -18,10 +18,11 @@ import { DynamicComponentRenderer } from "@/lib/registry/DynamicComponentRendere
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"; // 🆕 높이 관리
import { ScreenContextProvider } from "@/contexts/ScreenContext"; // 🆕 컴포넌트 간 통신
import { SplitPanelProvider } from "@/lib/registry/components/split-panel-layout/SplitPanelContext"; // 🆕 분할 패널 리사이즈
import { TableOptionsProvider } from "@/contexts/TableOptionsContext"; // 테이블 옵션
import { TableSearchWidgetHeightProvider, useTableSearchWidgetHeight } from "@/contexts/TableSearchWidgetHeightContext"; // 높이 관리
import { ScreenContextProvider } from "@/contexts/ScreenContext"; // 컴포넌트 간 통신
import { SplitPanelProvider } from "@/lib/registry/components/split-panel-layout/SplitPanelContext"; // 분할 패널 리사이즈
import { ActiveTabProvider } from "@/contexts/ActiveTabContext"; // 활성 탭 관리
function ScreenViewPage() {
const params = useParams();
@ -307,7 +308,8 @@ function ScreenViewPage() {
return (
<ScreenPreviewProvider isPreviewMode={false}>
<TableOptionsProvider>
<ActiveTabProvider>
<TableOptionsProvider>
<div ref={containerRef} className="bg-background h-full w-full overflow-auto p-3">
{/* 레이아웃 준비 중 로딩 표시 */}
{!layoutReady && (
@ -786,7 +788,8 @@ function ScreenViewPage() {
}}
/>
</div>
</TableOptionsProvider>
</TableOptionsProvider>
</ActiveTabProvider>
</ScreenPreviewProvider>
);
}

View File

@ -29,7 +29,6 @@ import {
Plus,
Minus,
ArrowRight,
Save,
Zap,
} from "lucide-react";
import { importFromExcel, getExcelSheetNames } from "@/lib/utils/excelExport";
@ -52,12 +51,6 @@ interface ColumnMapping {
systemColumn: string | null;
}
interface UploadConfig {
name: string;
type: string;
mappings: ColumnMapping[];
}
export const ExcelUploadModal: React.FC<ExcelUploadModalProps> = ({
open,
onOpenChange,
@ -88,8 +81,6 @@ export const ExcelUploadModal: React.FC<ExcelUploadModalProps> = ({
const [excelColumns, setExcelColumns] = useState<string[]>([]);
const [systemColumns, setSystemColumns] = useState<TableColumn[]>([]);
const [columnMappings, setColumnMappings] = useState<ColumnMapping[]>([]);
const [configName, setConfigName] = useState<string>("");
const [configType, setConfigType] = useState<string>("");
// 4단계: 확인
const [isUploading, setIsUploading] = useState(false);
@ -114,7 +105,7 @@ export const ExcelUploadModal: React.FC<ExcelUploadModalProps> = ({
const data = await importFromExcel(selectedFile, sheets[0]);
setAllData(data);
setDisplayData(data.slice(0, 10));
setDisplayData(data); // 전체 데이터를 미리보기에 표시 (스크롤 가능)
if (data.length > 0) {
const columns = Object.keys(data[0]);
@ -139,7 +130,7 @@ export const ExcelUploadModal: React.FC<ExcelUploadModalProps> = ({
try {
const data = await importFromExcel(file, sheetName);
setAllData(data);
setDisplayData(data.slice(0, 10));
setDisplayData(data); // 전체 데이터를 미리보기에 표시 (스크롤 가능)
if (data.length > 0) {
const columns = Object.keys(data[0]);
@ -275,28 +266,6 @@ export const ExcelUploadModal: React.FC<ExcelUploadModalProps> = ({
);
};
// 설정 저장
const handleSaveConfig = () => {
if (!configName.trim()) {
toast.error("거래처명을 입력해주세요.");
return;
}
const config: UploadConfig = {
name: configName,
type: configType,
mappings: columnMappings,
};
const savedConfigs = JSON.parse(
localStorage.getItem("excelUploadConfigs") || "[]"
);
savedConfigs.push(config);
localStorage.setItem("excelUploadConfigs", JSON.stringify(savedConfigs));
toast.success("설정이 저장되었습니다.");
};
// 다음 단계
const handleNext = () => {
if (currentStep === 1 && !file) {
@ -327,7 +296,8 @@ export const ExcelUploadModal: React.FC<ExcelUploadModalProps> = ({
setIsUploading(true);
try {
const mappedData = displayData.map((row) => {
// allData를 사용하여 전체 데이터 업로드 (displayData는 미리보기용 10개만)
const mappedData = allData.map((row) => {
const mappedRow: Record<string, any> = {};
columnMappings.forEach((mapping) => {
if (mapping.systemColumn) {
@ -389,8 +359,6 @@ export const ExcelUploadModal: React.FC<ExcelUploadModalProps> = ({
setExcelColumns([]);
setSystemColumns([]);
setColumnMappings([]);
setConfigName("");
setConfigType("");
}
}, [open]);
@ -699,27 +667,25 @@ export const ExcelUploadModal: React.FC<ExcelUploadModalProps> = ({
</div>
)}
{/* 3단계: 컬럼 매핑 - 3단 레이아웃 */}
{/* 3단계: 컬럼 매핑 */}
{currentStep === 3 && (
<div className="grid grid-cols-1 gap-4 lg:grid-cols-[2fr_3fr_2fr]">
{/* 왼쪽: 컬럼 매핑 설정 제목 + 자동 매핑 버튼 */}
<div className="space-y-4">
<div>
<h3 className="mb-3 text-sm font-semibold sm:text-base"> </h3>
<Button
type="button"
variant="default"
size="sm"
onClick={handleAutoMapping}
className="h-8 w-full text-xs sm:h-9 sm:text-sm"
>
<Zap className="mr-2 h-4 w-4" />
</Button>
</div>
<div className="space-y-4">
{/* 상단: 제목 + 자동 매핑 버튼 */}
<div className="flex items-center justify-between">
<h3 className="text-sm font-semibold sm:text-base"> </h3>
<Button
type="button"
variant="default"
size="sm"
onClick={handleAutoMapping}
className="h-8 text-xs sm:h-9 sm:text-sm"
>
<Zap className="mr-2 h-4 w-4" />
</Button>
</div>
{/* 중앙: 매핑 리스트 */}
{/* 매핑 리스트 */}
<div className="space-y-2">
<div className="grid grid-cols-[1fr_auto_1fr] gap-2 text-[10px] font-medium text-muted-foreground sm:text-xs">
<div> </div>
@ -772,50 +738,6 @@ export const ExcelUploadModal: React.FC<ExcelUploadModalProps> = ({
))}
</div>
</div>
{/* 오른쪽: 현재 설정 저장 */}
<div className="rounded-md border border-border bg-muted/30 p-4">
<div className="mb-4 flex items-center gap-2">
<Save className="h-4 w-4" />
<h3 className="text-sm font-semibold sm:text-base"> </h3>
</div>
<div className="space-y-3">
<div>
<Label htmlFor="config-name" className="text-[10px] sm:text-xs">
*
</Label>
<Input
id="config-name"
value={configName}
onChange={(e) => setConfigName(e.target.value)}
placeholder="거래처 선택"
className="mt-1 h-8 text-xs sm:h-10 sm:text-sm"
/>
</div>
<div>
<Label htmlFor="config-type" className="text-[10px] sm:text-xs">
</Label>
<Input
id="config-type"
value={configType}
onChange={(e) => setConfigType(e.target.value)}
placeholder="유형을 입력하세요 (예: 원자재)"
className="mt-1 h-8 text-xs sm:h-10 sm:text-sm"
/>
</div>
<Button
type="button"
variant="default"
size="sm"
onClick={handleSaveConfig}
className="h-8 w-full text-xs sm:h-9 sm:text-sm"
>
<Save className="mr-2 h-3 w-3" />
</Button>
</div>
</div>
</div>
)}
@ -832,7 +754,7 @@ export const ExcelUploadModal: React.FC<ExcelUploadModalProps> = ({
<span className="font-medium">:</span> {selectedSheet}
</p>
<p>
<span className="font-medium"> :</span> {displayData.length}
<span className="font-medium"> :</span> {allData.length}
</p>
<p>
<span className="font-medium">:</span> {tableName}

View File

@ -12,6 +12,7 @@ import { useAuth } from "@/hooks/useAuth";
import { TableOptionsProvider } from "@/contexts/TableOptionsContext";
import { TableSearchWidgetHeightProvider } from "@/contexts/TableSearchWidgetHeightContext";
import { useSplitPanelContext } from "@/contexts/SplitPanelContext";
import { ActiveTabProvider } from "@/contexts/ActiveTabContext";
interface ScreenModalState {
isOpen: boolean;
@ -666,6 +667,7 @@ export const ScreenModal: React.FC<ScreenModalProps> = ({ className }) => {
</div>
</div>
) : screenData ? (
<ActiveTabProvider>
<TableOptionsProvider>
<div
className="relative mx-auto bg-white"
@ -738,6 +740,7 @@ export const ScreenModal: React.FC<ScreenModalProps> = ({ className }) => {
})}
</div>
</TableOptionsProvider>
</ActiveTabProvider>
) : (
<div className="flex h-full items-center justify-center">
<p className="text-muted-foreground"> .</p>

View File

@ -51,6 +51,7 @@ import { useScreenPreview } from "@/contexts/ScreenPreviewContext";
import { TableOptionsProvider } from "@/contexts/TableOptionsContext";
import { TableOptionsToolbar } from "./table-options/TableOptionsToolbar";
import { SplitPanelProvider } from "@/lib/registry/components/split-panel-layout/SplitPanelContext";
import { ActiveTabProvider } from "@/contexts/ActiveTabContext";
/**
* 🔗
@ -2103,7 +2104,8 @@ export const InteractiveScreenViewer: React.FC<InteractiveScreenViewerProps> = (
return (
<SplitPanelProvider>
<TableOptionsProvider>
<ActiveTabProvider>
<TableOptionsProvider>
<div className="flex h-full flex-col">
{/* 테이블 옵션 툴바 */}
<TableOptionsToolbar />
@ -2210,7 +2212,8 @@ export const InteractiveScreenViewer: React.FC<InteractiveScreenViewerProps> = (
</div>
</DialogContent>
</Dialog>
</TableOptionsProvider>
</TableOptionsProvider>
</ActiveTabProvider>
</SplitPanelProvider>
);
};

View File

@ -39,22 +39,25 @@ interface InteractiveScreenViewerProps {
id: number;
tableName?: string;
};
menuObjid?: number; // 🆕 메뉴 OBJID (코드 스코프용)
menuObjid?: number; // 메뉴 OBJID (코드 스코프용)
onSave?: () => Promise<void>;
onRefresh?: () => void;
onFlowRefresh?: () => void;
// 🆕 외부에서 전달받는 사용자 정보 (ScreenModal 등에서 사용)
// 외부에서 전달받는 사용자 정보 (ScreenModal 등에서 사용)
userId?: string;
userName?: string;
companyCode?: string;
// 🆕 그룹 데이터 (EditModal에서 전달)
// 그룹 데이터 (EditModal에서 전달)
groupedData?: Record<string, any>[];
// 🆕 비활성화할 필드 목록 (EditModal에서 전달)
// 비활성화할 필드 목록 (EditModal에서 전달)
disabledFields?: string[];
// 🆕 EditModal 내부인지 여부 (button-primary가 EditModal의 handleSave 사용하도록)
// EditModal 내부인지 여부 (button-primary가 EditModal의 handleSave 사용하도록)
isInModal?: boolean;
// 🆕 원본 데이터 (수정 모드에서 UPDATE 판단용)
// 원본 데이터 (수정 모드에서 UPDATE 판단용)
originalData?: Record<string, any> | null;
// 탭 관련 정보 (탭 내부의 컴포넌트에서 사용)
parentTabId?: string; // 부모 탭 ID
parentTabsComponentId?: string; // 부모 탭 컴포넌트 ID
}
export const InteractiveScreenViewerDynamic: React.FC<InteractiveScreenViewerProps> = ({
@ -74,7 +77,9 @@ export const InteractiveScreenViewerDynamic: React.FC<InteractiveScreenViewerPro
groupedData,
disabledFields = [],
isInModal = false,
originalData, // 🆕 원본 데이터 (수정 모드에서 UPDATE 판단용)
originalData,
parentTabId,
parentTabsComponentId,
}) => {
const { isPreviewMode } = useScreenPreview(); // 프리뷰 모드 확인
const { userName: authUserName, user: authUser } = useAuth();
@ -359,43 +364,43 @@ export const InteractiveScreenViewerDynamic: React.FC<InteractiveScreenViewerPro
component={comp}
isInteractive={true}
formData={formData}
originalData={originalData || undefined} // 🆕 원본 데이터 전달 (UPDATE 판단용)
originalData={originalData || undefined}
onFormDataChange={handleFormDataChange}
screenId={screenInfo?.id}
tableName={screenInfo?.tableName}
menuObjid={menuObjid} // 🆕 메뉴 OBJID 전달
userId={user?.userId} // ✅ 사용자 ID 전달
userName={user?.userName} // ✅ 사용자 이름 전달
companyCode={user?.companyCode} // ✅ 회사 코드 전달
onSave={onSave} // 🆕 EditModal의 handleSave 콜백 전달
allComponents={allComponents} // 🆕 같은 화면의 모든 컴포넌트 전달 (TableList 자동 감지용)
menuObjid={menuObjid}
userId={user?.userId}
userName={user?.userName}
companyCode={user?.companyCode}
onSave={onSave}
allComponents={allComponents}
selectedRowsData={selectedRowsData}
onSelectedRowsChange={(selectedRows, selectedData) => {
console.log("🔍 테이블에서 선택된 행 데이터:", selectedData);
console.log("테이블에서 선택된 행 데이터:", selectedData);
setSelectedRowsData(selectedData);
}}
// 🆕 그룹 데이터 전달 (EditModal → ModalRepeaterTable)
groupedData={groupedData}
// 🆕 비활성화 필드 전달 (EditModal → 각 컴포넌트)
disabledFields={disabledFields}
flowSelectedData={flowSelectedData}
flowSelectedStepId={flowSelectedStepId}
onFlowSelectedDataChange={(selectedData, stepId) => {
console.log("🔍 플로우에서 선택된 데이터:", { selectedData, stepId });
console.log("플로우에서 선택된 데이터:", { selectedData, stepId });
setFlowSelectedData(selectedData);
setFlowSelectedStepId(stepId);
}}
onRefresh={
onRefresh ||
(() => {
// 부모로부터 전달받은 onRefresh 또는 기본 동작
console.log("🔄 InteractiveScreenViewerDynamic onRefresh 호출");
console.log("InteractiveScreenViewerDynamic onRefresh 호출");
})
}
onFlowRefresh={onFlowRefresh}
onClose={() => {
// buttonActions.ts가 이미 처리함
}}
// 탭 관련 정보 전달
parentTabId={parentTabId}
parentTabsComponentId={parentTabsComponentId}
/>
);
}

View File

@ -7,15 +7,18 @@ import { X, Loader2 } from "lucide-react";
import type { TabsComponent, TabItem } from "@/types/screen-management";
import { InteractiveScreenViewerDynamic } from "@/components/screen/InteractiveScreenViewerDynamic";
import { cn } from "@/lib/utils";
import { useActiveTab } from "@/contexts/ActiveTabContext";
interface TabsWidgetProps {
component: TabsComponent;
className?: string;
style?: React.CSSProperties;
menuObjid?: number; // 🆕 부모 화면의 메뉴 OBJID
menuObjid?: number; // 부모 화면의 메뉴 OBJID
}
export function TabsWidget({ component, className, style, menuObjid }: TabsWidgetProps) {
// ActiveTab context 사용
const { setActiveTab, removeTabsComponent } = useActiveTab();
const {
tabs = [],
defaultTab,
@ -51,12 +54,30 @@ export function TabsWidget({ component, className, style, menuObjid }: TabsWidge
setVisibleTabs(tabs.filter((tab) => !tab.disabled));
}, [tabs]);
// 선택된 탭 변경 시 localStorage에 저장
// 선택된 탭 변경 시 localStorage에 저장 + ActiveTab Context 업데이트
useEffect(() => {
if (persistSelection && typeof window !== "undefined") {
localStorage.setItem(storageKey, selectedTab);
}
}, [selectedTab, persistSelection, storageKey]);
// ActiveTab Context에 현재 활성 탭 정보 등록
const currentTabInfo = visibleTabs.find(t => t.id === selectedTab);
if (currentTabInfo) {
setActiveTab(component.id, {
tabId: selectedTab,
tabsComponentId: component.id,
screenId: currentTabInfo.screenId,
label: currentTabInfo.label,
});
}
}, [selectedTab, persistSelection, storageKey, component.id, visibleTabs, setActiveTab]);
// 컴포넌트 언마운트 시 ActiveTab Context에서 제거
useEffect(() => {
return () => {
removeTabsComponent(component.id);
};
}, [component.id, removeTabsComponent]);
// 초기 로드 시 선택된 탭의 화면 불러오기
useEffect(() => {
@ -220,16 +241,18 @@ export function TabsWidget({ component, className, style, menuObjid }: TabsWidge
margin: "0 auto",
}}
>
{components.map((component: any) => (
{components.map((comp: any) => (
<InteractiveScreenViewerDynamic
key={component.id}
component={component}
key={comp.id}
component={comp}
allComponents={components}
screenInfo={{
id: tab.screenId,
tableName: layoutData.tableName,
}}
menuObjid={menuObjid}
parentTabId={tab.id}
parentTabsComponentId={component.id}
/>
))}
</div>

View File

@ -0,0 +1,139 @@
"use client";
import React, {
createContext,
useContext,
useState,
useCallback,
ReactNode,
} from "react";
/**
*
*/
export interface ActiveTabInfo {
tabId: string; // 탭 고유 ID
tabsComponentId: string; // 부모 탭 컴포넌트 ID
screenId?: number; // 탭에 연결된 화면 ID
label?: string; // 탭 라벨
}
/**
* Context
*/
interface ActiveTabContextValue {
// 현재 활성 탭 정보 (탭 컴포넌트 ID -> 활성 탭 정보)
activeTabs: Map<string, ActiveTabInfo>;
// 활성 탭 설정
setActiveTab: (tabsComponentId: string, tabInfo: ActiveTabInfo) => void;
// 활성 탭 조회
getActiveTab: (tabsComponentId: string) => ActiveTabInfo | undefined;
// 특정 탭 컴포넌트의 활성 탭 ID 조회
getActiveTabId: (tabsComponentId: string) => string | undefined;
// 전체 활성 탭 ID 목록 (모든 탭 컴포넌트에서)
getAllActiveTabIds: () => string[];
// 탭 컴포넌트 제거 시 정리
removeTabsComponent: (tabsComponentId: string) => void;
}
const ActiveTabContext = createContext<ActiveTabContextValue | undefined>(undefined);
export const ActiveTabProvider: React.FC<{ children: ReactNode }> = ({ children }) => {
const [activeTabs, setActiveTabs] = useState<Map<string, ActiveTabInfo>>(new Map());
/**
*
*/
const setActiveTab = useCallback((tabsComponentId: string, tabInfo: ActiveTabInfo) => {
setActiveTabs((prev) => {
const newMap = new Map(prev);
newMap.set(tabsComponentId, tabInfo);
return newMap;
});
}, []);
/**
*
*/
const getActiveTab = useCallback(
(tabsComponentId: string) => {
return activeTabs.get(tabsComponentId);
},
[activeTabs]
);
/**
* ID
*/
const getActiveTabId = useCallback(
(tabsComponentId: string) => {
return activeTabs.get(tabsComponentId)?.tabId;
},
[activeTabs]
);
/**
* ID
*/
const getAllActiveTabIds = useCallback(() => {
return Array.from(activeTabs.values()).map((info) => info.tabId);
}, [activeTabs]);
/**
*
*/
const removeTabsComponent = useCallback((tabsComponentId: string) => {
setActiveTabs((prev) => {
const newMap = new Map(prev);
newMap.delete(tabsComponentId);
return newMap;
});
}, []);
return (
<ActiveTabContext.Provider
value={{
activeTabs,
setActiveTab,
getActiveTab,
getActiveTabId,
getAllActiveTabIds,
removeTabsComponent,
}}
>
{children}
</ActiveTabContext.Provider>
);
};
/**
* Context Hook
*/
export const useActiveTab = () => {
const context = useContext(ActiveTabContext);
if (!context) {
// Context가 없으면 기본값 반환 (탭이 없는 화면에서 사용 시)
return {
activeTabs: new Map(),
setActiveTab: () => {},
getActiveTab: () => undefined,
getActiveTabId: () => undefined,
getAllActiveTabIds: () => [],
removeTabsComponent: () => {},
};
}
return context;
};
/**
* Optional Context Hook ( undefined )
*/
export const useActiveTabOptional = () => {
return useContext(ActiveTabContext);
};

View File

@ -3,12 +3,14 @@ import React, {
useContext,
useState,
useCallback,
useMemo,
ReactNode,
} from "react";
import {
TableRegistration,
TableOptionsContextValue,
} from "@/types/table-options";
import { useActiveTab } from "./ActiveTabContext";
const TableOptionsContext = createContext<TableOptionsContextValue | undefined>(
undefined
@ -89,6 +91,35 @@ export const TableOptionsProvider: React.FC<{ children: ReactNode }> = ({
});
}, []);
// ActiveTab context 사용 (optional - 에러 방지)
const activeTabContext = useActiveTab();
/**
*
*/
const getActiveTabTables = useCallback(() => {
const allTables = Array.from(registeredTables.values());
const activeTabIds = activeTabContext.getAllActiveTabIds();
// 활성 탭이 없으면 탭에 속하지 않은 테이블만 반환
if (activeTabIds.length === 0) {
return allTables.filter(table => !table.parentTabId);
}
// 활성 탭에 속한 테이블 + 탭에 속하지 않은 테이블
return allTables.filter(table =>
!table.parentTabId || activeTabIds.includes(table.parentTabId)
);
}, [registeredTables, activeTabContext]);
/**
*
*/
const getTablesForTab = useCallback((tabId: string) => {
const allTables = Array.from(registeredTables.values());
return allTables.filter(table => table.parentTabId === tabId);
}, [registeredTables]);
return (
<TableOptionsContext.Provider
value={{
@ -99,6 +130,8 @@ export const TableOptionsProvider: React.FC<{ children: ReactNode }> = ({
updateTableDataCount,
selectedTableId,
setSelectedTableId,
getActiveTabTables,
getTablesForTab,
}}
>
{children}

View File

@ -132,6 +132,9 @@ export interface DynamicComponentRendererProps {
mode?: "view" | "edit";
// 모달 내에서 렌더링 여부
isInModal?: boolean;
// 탭 관련 정보 (탭 내부의 컴포넌트에서 사용)
parentTabId?: string; // 부모 탭 ID
parentTabsComponentId?: string; // 부모 탭 컴포넌트 ID
[key: string]: any;
}

View File

@ -208,6 +208,9 @@ export interface TableListComponentProps {
) => void;
onConfigChange?: (config: any) => void;
refreshKey?: number;
// 탭 관련 정보 (탭 내부의 테이블에서 사용)
parentTabId?: string; // 부모 탭 ID
parentTabsComponentId?: string; // 부모 탭 컴포넌트 ID
}
// ========================================
@ -224,7 +227,7 @@ export const TableListComponent: React.FC<TableListComponentProps> = ({
config,
className,
style,
formData: propFormData, // 🆕 부모에서 전달받은 formData
formData: propFormData,
onFormDataChange,
componentConfig,
onSelectedRowsChange,
@ -232,7 +235,9 @@ export const TableListComponent: React.FC<TableListComponentProps> = ({
refreshKey,
tableName,
userId,
screenId, // 화면 ID 추출
screenId,
parentTabId,
parentTabsComponentId,
}) => {
// ========================================
// 설정 및 스타일
@ -1016,7 +1021,11 @@ export const TableListComponent: React.FC<TableListComponentProps> = ({
onGroupChange: setGrouping,
onColumnVisibilityChange: setColumnVisibility,
getColumnUniqueValues, // 고유 값 조회 함수 등록
onGroupSumChange: setGroupSumConfig, // 🆕 그룹별 합산 설정
onGroupSumChange: setGroupSumConfig, // 그룹별 합산 설정
// 탭 관련 정보 (탭 내부의 테이블인 경우)
parentTabId,
parentTabsComponentId,
screenId: screenId ? Number(screenId) : undefined,
};
registerTable(registration);

View File

@ -6,6 +6,7 @@ import { Input } from "@/components/ui/input";
import { Settings, Filter, Layers, X, Check, ChevronsUpDown } from "lucide-react";
import { useTableOptions } from "@/contexts/TableOptionsContext";
import { useTableSearchWidgetHeight } from "@/contexts/TableSearchWidgetHeightContext";
import { useActiveTab } from "@/contexts/ActiveTabContext";
import { ColumnVisibilityPanel } from "@/components/screen/table-options/ColumnVisibilityPanel";
import { FilterPanel } from "@/components/screen/table-options/FilterPanel";
import { GroupingPanel } from "@/components/screen/table-options/GroupingPanel";
@ -49,8 +50,9 @@ interface TableSearchWidgetProps {
}
export function TableSearchWidget({ component, screenId, onHeightChange }: TableSearchWidgetProps) {
const { registeredTables, selectedTableId, setSelectedTableId, getTable } = useTableOptions();
const { registeredTables, selectedTableId, setSelectedTableId, getTable, getActiveTabTables } = useTableOptions();
const { isPreviewMode } = useScreenPreview(); // 미리보기 모드 확인
const { getAllActiveTabIds, activeTabs } = useActiveTab(); // 활성 탭 정보
// 높이 관리 context (실제 화면에서만 사용)
let setWidgetHeight:
@ -63,6 +65,9 @@ export function TableSearchWidget({ component, screenId, onHeightChange }: Table
// Context가 없으면 (디자이너 모드) 무시
setWidgetHeight = undefined;
}
// 탭별 필터 값 저장 (탭 ID -> 필터 값)
const [tabFilterValues, setTabFilterValues] = useState<Record<string, Record<string, any>>>({});
const [columnVisibilityOpen, setColumnVisibilityOpen] = useState(false);
const [filterOpen, setFilterOpen] = useState(false);
@ -88,38 +93,48 @@ export function TableSearchWidget({ component, screenId, onHeightChange }: Table
// Map을 배열로 변환
const allTableList = Array.from(registeredTables.values());
// 대상 패널 위치에 따라 테이블 필터링 (tableId 패턴 기반)
// 현재 활성 탭 ID 목록
const activeTabIds = useMemo(() => getAllActiveTabIds(), [activeTabs]);
// 대상 패널 위치 + 활성 탭에 따라 테이블 필터링
const tableList = useMemo(() => {
// "auto"면 모든 테이블 반환
if (targetPanelPosition === "auto") {
return allTableList;
}
// 테이블 ID 패턴으로 필터링
// card-display-XXX: 좌측 패널 (카드 디스플레이)
// datatable-XXX, table-list-XXX: 우측 패널 (테이블 리스트)
const filteredTables = allTableList.filter(table => {
const tableId = table.tableId.toLowerCase();
if (targetPanelPosition === "left") {
// 좌측 패널 대상: card-display만
return tableId.includes("card-display") || tableId.includes("card");
} else if (targetPanelPosition === "right") {
// 우측 패널 대상: datatable, table-list 등 (card-display 제외)
const isCardDisplay = tableId.includes("card-display") || tableId.includes("card");
return !isCardDisplay;
}
return true;
// 1단계: 활성 탭 기반 필터링
// - 활성 탭에 속한 테이블만 표시
// - 탭에 속하지 않은 테이블(parentTabId가 없는)도 포함
let filteredByTab = allTableList.filter(table => {
// 탭에 속하지 않는 테이블은 항상 표시
if (!table.parentTabId) return true;
// 활성 탭에 속한 테이블만 표시
return activeTabIds.includes(table.parentTabId);
});
// 필터링된 결과가 없으면 모든 테이블 반환 (폴백)
if (filteredTables.length === 0) {
return allTableList;
// 2단계: 대상 패널 위치에 따라 추가 필터링
if (targetPanelPosition !== "auto") {
filteredByTab = filteredByTab.filter(table => {
const tableId = table.tableId.toLowerCase();
if (targetPanelPosition === "left") {
// 좌측 패널 대상: card-display만
return tableId.includes("card-display") || tableId.includes("card");
} else if (targetPanelPosition === "right") {
// 우측 패널 대상: datatable, table-list 등 (card-display 제외)
const isCardDisplay = tableId.includes("card-display") || tableId.includes("card");
return !isCardDisplay;
}
return true;
});
}
return filteredTables;
}, [allTableList, targetPanelPosition]);
// 필터링된 결과가 없으면 탭 기반 필터링 결과만 반환
if (filteredByTab.length === 0) {
return allTableList.filter(table =>
!table.parentTabId || activeTabIds.includes(table.parentTabId)
);
}
return filteredByTab;
}, [allTableList, targetPanelPosition, activeTabIds]);
// currentTable은 tableList(필터링된 목록)에서 가져와야 함
const currentTable = useMemo(() => {
@ -151,6 +166,34 @@ export function TableSearchWidget({ component, screenId, onHeightChange }: Table
}
}, [tableList, selectedTableId, autoSelectFirstTable, setSelectedTableId, targetPanelPosition]);
// 현재 선택된 테이블의 탭 ID (탭별 필터 저장용)
const currentTableTabId = currentTable?.parentTabId;
// 탭별 필터 값 저장 키 생성
const getTabFilterStorageKey = (tableName: string, tabId?: string) => {
const baseKey = screenId
? `table_filter_values_${tableName}_screen_${screenId}`
: `table_filter_values_${tableName}`;
return tabId ? `${baseKey}_tab_${tabId}` : baseKey;
};
// 탭 변경 시 이전 탭의 필터 값 저장 + 새 탭의 필터 값 복원
useEffect(() => {
if (!currentTable?.tableName) return;
// 현재 필터 값이 있으면 탭별로 저장
if (Object.keys(filterValues).length > 0 && currentTableTabId) {
const storageKey = getTabFilterStorageKey(currentTable.tableName, currentTableTabId);
localStorage.setItem(storageKey, JSON.stringify(filterValues));
// 메모리 캐시에도 저장
setTabFilterValues(prev => ({
...prev,
[currentTableTabId]: filterValues
}));
}
}, [currentTableTabId, currentTable?.tableName]);
// 현재 테이블의 저장된 필터 불러오기 (동적 모드) 또는 고정 필터 적용 (고정 모드)
useEffect(() => {
if (!currentTable?.tableName) return;
@ -165,14 +208,32 @@ export function TableSearchWidget({ component, screenId, onHeightChange }: Table
width: f.width || 200,
}));
setActiveFilters(activeFiltersList);
// 탭별 저장된 필터 값 복원
if (currentTableTabId) {
const storageKey = getTabFilterStorageKey(currentTable.tableName, currentTableTabId);
const savedValues = localStorage.getItem(storageKey);
if (savedValues) {
try {
const parsedValues = JSON.parse(savedValues);
setFilterValues(parsedValues);
// 즉시 필터 적용
setTimeout(() => applyFilters(parsedValues), 100);
} catch {
setFilterValues({});
}
} else {
setFilterValues({});
}
}
return;
}
// 동적 모드: 화면별로 독립적인 필터 설정 불러오기
const storageKey = screenId
? `table_filters_${currentTable.tableName}_screen_${screenId}`
// 동적 모드: 화면별 + 탭별로 독립적인 필터 설정 불러오기
const filterConfigKey = screenId
? `table_filters_${currentTable.tableName}_screen_${screenId}${currentTableTabId ? `_tab_${currentTableTabId}` : ''}`
: `table_filters_${currentTable.tableName}`;
const savedFilters = localStorage.getItem(storageKey);
const savedFilters = localStorage.getItem(filterConfigKey);
if (savedFilters) {
try {
@ -193,16 +254,39 @@ export function TableSearchWidget({ component, screenId, onHeightChange }: Table
operator: "contains",
value: "",
filterType: f.filterType,
width: f.width || 200, // 저장된 너비 포함
width: f.width || 200,
}));
setActiveFilters(activeFiltersList);
// 탭별 저장된 필터 값 복원
if (currentTableTabId) {
const valuesStorageKey = getTabFilterStorageKey(currentTable.tableName, currentTableTabId);
const savedValues = localStorage.getItem(valuesStorageKey);
if (savedValues) {
try {
const parsedValues = JSON.parse(savedValues);
setFilterValues(parsedValues);
// 즉시 필터 적용
setTimeout(() => applyFilters(parsedValues), 100);
} catch {
setFilterValues({});
}
} else {
setFilterValues({});
}
} else {
setFilterValues({});
}
} catch (error) {
console.error("저장된 필터 불러오기 실패:", error);
}
} else {
// 필터 설정이 없으면 초기화
setFilterValues({});
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [currentTable?.tableName, filterMode, screenId, JSON.stringify(presetFilters)]);
}, [currentTable?.tableName, filterMode, screenId, currentTableTabId, JSON.stringify(presetFilters)]);
// select 옵션 초기 로드 (한 번만 실행, 이후 유지)
useEffect(() => {
@ -300,6 +384,12 @@ export function TableSearchWidget({ component, screenId, onHeightChange }: Table
setFilterValues(newValues);
// 탭별 필터 값 저장
if (currentTable?.tableName && currentTableTabId) {
const storageKey = getTabFilterStorageKey(currentTable.tableName, currentTableTabId);
localStorage.setItem(storageKey, JSON.stringify(newValues));
}
// 실시간 검색: 값 변경 시 즉시 필터 적용
applyFilters(newValues);
};
@ -365,6 +455,12 @@ export function TableSearchWidget({ component, screenId, onHeightChange }: Table
setFilterValues({});
setSelectedLabels({});
currentTable?.onFilterChange([]);
// 탭별 저장된 필터 값도 초기화
if (currentTable?.tableName && currentTableTabId) {
const storageKey = getTabFilterStorageKey(currentTable.tableName, currentTableTabId);
localStorage.removeItem(storageKey);
}
};
// 필터 입력 필드 렌더링

View File

@ -55,12 +55,17 @@ export interface TableRegistration {
tableName: string; // 실제 DB 테이블명 (예: "item_info")
columns: TableColumn[];
dataCount?: number; // 현재 표시된 데이터 건수
// 탭 관련 정보 (탭 내부에 있는 테이블의 경우)
parentTabId?: string; // 부모 탭 ID
parentTabsComponentId?: string; // 부모 탭 컴포넌트 ID
screenId?: number; // 소속 화면 ID
// 콜백 함수들
onFilterChange: (filters: TableFilter[]) => void;
onGroupChange: (groups: string[]) => void;
onColumnVisibilityChange: (columns: ColumnVisibility[]) => void;
onGroupSumChange?: (config: GroupSumConfig | null) => void; // 🆕 그룹별 합산 설정 변경
onGroupSumChange?: (config: GroupSumConfig | null) => void; // 그룹별 합산 설정 변경
// 데이터 조회 함수 (선택 타입 필터용)
getColumnUniqueValues?: (columnName: string) => Promise<Array<{ label: string; value: string }>>;
@ -77,4 +82,8 @@ export interface TableOptionsContextValue {
updateTableDataCount: (tableId: string, count: number) => void; // 데이터 건수 업데이트
selectedTableId: string | null;
setSelectedTableId: (tableId: string | null) => void;
// 활성 탭 기반 필터링
getActiveTabTables: () => TableRegistration[]; // 현재 활성 탭의 테이블만 반환
getTablesForTab: (tabId: string) => TableRegistration[]; // 특정 탭의 테이블만 반환
}