탭안에있는 화면 검색필터링 기능
This commit is contained in:
parent
1995c3dca4
commit
be916d3db7
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
};
|
||||
|
||||
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -132,6 +132,9 @@ export interface DynamicComponentRendererProps {
|
|||
mode?: "view" | "edit";
|
||||
// 모달 내에서 렌더링 여부
|
||||
isInModal?: boolean;
|
||||
// 탭 관련 정보 (탭 내부의 컴포넌트에서 사용)
|
||||
parentTabId?: string; // 부모 탭 ID
|
||||
parentTabsComponentId?: string; // 부모 탭 컴포넌트 ID
|
||||
[key: string]: any;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
};
|
||||
|
||||
// 필터 입력 필드 렌더링
|
||||
|
|
|
|||
|
|
@ -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[]; // 특정 탭의 테이블만 반환
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue