"use client"; import React, { useState, useEffect, useRef, useMemo } from "react"; import { Button } from "@/components/ui/button"; 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"; import { TableFilter } from "@/types/table-options"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"; import { ModernDatePicker } from "@/components/screen/filters/ModernDatePicker"; import { useScreenPreview } from "@/contexts/ScreenPreviewContext"; import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover"; import { Checkbox } from "@/components/ui/checkbox"; import { cn } from "@/lib/utils"; interface PresetFilter { id: string; columnName: string; columnLabel: string; filterType: "text" | "number" | "date" | "select"; width?: number; multiSelect?: boolean; // 다중선택 여부 (select 타입에서만 사용) } interface TableSearchWidgetProps { component: { id: string; title?: string; style?: { width?: string; height?: string; padding?: string; backgroundColor?: string; }; componentConfig?: { autoSelectFirstTable?: boolean; // 첫 번째 테이블 자동 선택 여부 showTableSelector?: boolean; // 테이블 선택 드롭다운 표시 여부 filterMode?: "dynamic" | "preset"; // 필터 모드 presetFilters?: PresetFilter[]; // 고정 필터 목록 targetPanelPosition?: "left" | "right" | "auto"; // 분할 패널에서 대상 패널 위치 (기본: "left") }; }; screenId?: number; // 화면 ID onHeightChange?: (height: number) => void; // 높이 변화 콜백 } export function TableSearchWidget({ component, screenId, onHeightChange }: TableSearchWidgetProps) { console.log("🎯🎯🎯 [TableSearchWidget] 함수 시작!", { componentId: component?.id, screenId }); // 🔧 직접 useTableOptions 호출 (에러 발생 시 catch하지 않고 그대로 throw) const tableOptionsContext = useTableOptions(); console.log("✅ [TableSearchWidget] useTableOptions 성공", { hasContext: !!tableOptionsContext }); const { registeredTables, selectedTableId, setSelectedTableId, getTable, getActiveTabTables } = tableOptionsContext; // 등록된 테이블 확인 로그 console.log("🔍 [TableSearchWidget] 등록된 테이블:", { count: registeredTables.size, tables: Array.from(registeredTables.entries()).map(([id, t]) => ({ id, tableName: t.tableName, hasOnFilterChange: typeof t.onFilterChange === "function", })), selectedTableId, }); const { isPreviewMode } = useScreenPreview(); // 미리보기 모드 확인 const { getAllActiveTabIds, activeTabs } = useActiveTab(); // 활성 탭 정보 // 높이 관리 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; } // 탭별 필터 값 저장 (탭 ID -> 필터 값) const [tabFilterValues, setTabFilterValues] = useState>>({}); const [columnVisibilityOpen, setColumnVisibilityOpen] = useState(false); const [filterOpen, setFilterOpen] = useState(false); const [groupingOpen, setGroupingOpen] = useState(false); // 활성화된 필터 목록 const [activeFilters, setActiveFilters] = useState([]); const [filterValues, setFilterValues] = useState>({}); // select 타입 필터의 옵션들 const [selectOptions, setSelectOptions] = useState>>({}); // 선택된 값의 라벨 저장 (데이터 없을 때도 라벨 유지) const [selectedLabels, setSelectedLabels] = useState>({}); // 높이 감지를 위한 ref const containerRef = useRef(null); const autoSelectFirstTable = component.componentConfig?.autoSelectFirstTable ?? true; const showTableSelector = component.componentConfig?.showTableSelector ?? true; const filterMode = component.componentConfig?.filterMode ?? "dynamic"; const presetFilters = component.componentConfig?.presetFilters ?? []; const targetPanelPosition = component.componentConfig?.targetPanelPosition ?? "left"; // 기본값: 좌측 패널 // Map을 배열로 변환 const allTableList = Array.from(registeredTables.values()); // 현재 활성 탭 ID 목록 const activeTabIds = useMemo(() => getAllActiveTabIds(), [activeTabs]); // 대상 패널 위치 + 활성 탭에 따라 테이블 필터링 const tableList = useMemo(() => { // 1단계: 활성 탭 기반 필터링 // - 활성 탭에 속한 테이블만 표시 // - 탭에 속하지 않은 테이블(parentTabId가 없는)도 포함 let filteredByTab = allTableList.filter((table) => { // 탭에 속하지 않는 테이블은 항상 표시 if (!table.parentTabId) return true; // 활성 탭에 속한 테이블만 표시 return activeTabIds.includes(table.parentTabId); }); // 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; }); } // 필터링된 결과가 없으면 탭 기반 필터링 결과만 반환 if (filteredByTab.length === 0) { return allTableList.filter((table) => !table.parentTabId || activeTabIds.includes(table.parentTabId)); } return filteredByTab; }, [allTableList, targetPanelPosition, activeTabIds]); // currentTable은 tableList(필터링된 목록)에서 가져와야 함 const currentTable = useMemo(() => { console.log("🔍 [TableSearchWidget] currentTable 계산:", { selectedTableId, tableListLength: tableList.length, tableList: tableList.map((t) => ({ id: t.tableId, name: t.tableName, parentTabId: t.parentTabId })), }); if (!selectedTableId) return undefined; // 먼저 tableList(필터링된 목록)에서 찾기 const tableFromList = tableList.find((t) => t.tableId === selectedTableId); if (tableFromList) { console.log("✅ [TableSearchWidget] 테이블 찾음 (tableList):", tableFromList.tableName); return tableFromList; } // tableList에 없으면 전체에서 찾기 (폴백) const tableFromAll = getTable(selectedTableId); console.log("🔄 [TableSearchWidget] 테이블 찾음 (전체):", tableFromAll?.tableName); return tableFromAll; }, [selectedTableId, tableList, getTable]); // 🆕 활성 탭 ID 문자열 (변경 감지용) const activeTabIdsStr = useMemo(() => activeTabIds.join(","), [activeTabIds]); // 🆕 이전 활성 탭 ID 추적 (탭 전환 감지용) const prevActiveTabIdsRef = useRef(activeTabIdsStr); // 대상 패널의 첫 번째 테이블 자동 선택 useEffect(() => { if (!autoSelectFirstTable || tableList.length === 0) { return; } // 🆕 탭 전환 감지: 활성 탭이 변경되었는지 확인 const tabChanged = prevActiveTabIdsRef.current !== activeTabIdsStr; if (tabChanged) { console.log("🔄 [TableSearchWidget] 탭 전환 감지:", { 이전탭: prevActiveTabIdsRef.current, 현재탭: activeTabIdsStr, 가용테이블: tableList.map((t) => ({ id: t.tableId, tableName: t.tableName, parentTabId: t.parentTabId })), 현재선택테이블: selectedTableId, }); prevActiveTabIdsRef.current = activeTabIdsStr; // 🆕 탭 전환 시: 해당 탭에 속한 테이블 중 첫 번째 강제 선택 const activeTabTable = tableList.find((t) => t.parentTabId && activeTabIds.includes(t.parentTabId)); const targetTable = activeTabTable || tableList[0]; if (targetTable) { console.log("✅ [TableSearchWidget] 탭 전환으로 테이블 강제 선택:", { 테이블ID: targetTable.tableId, 테이블명: targetTable.tableName, 탭ID: targetTable.parentTabId, 이전테이블: selectedTableId, }); setSelectedTableId(targetTable.tableId); } return; // 탭 전환 시에는 여기서 종료 } // 현재 선택된 테이블이 대상 패널에 있는지 확인 const isCurrentTableInTarget = selectedTableId && tableList.some((t) => t.tableId === selectedTableId); // 현재 선택된 테이블이 대상 패널에 없으면 첫 번째 테이블 선택 if (!selectedTableId || !isCurrentTableInTarget) { const activeTabTable = tableList.find((t) => t.parentTabId && activeTabIds.includes(t.parentTabId)); const targetTable = activeTabTable || tableList[0]; if (targetTable && targetTable.tableId !== selectedTableId) { console.log("✅ [TableSearchWidget] 테이블 자동 선택 (초기):", { 테이블ID: targetTable.tableId, 테이블명: targetTable.tableName, 탭ID: targetTable.parentTabId, }); setSelectedTableId(targetTable.tableId); } } }, [ tableList, selectedTableId, autoSelectFirstTable, setSelectedTableId, targetPanelPosition, activeTabIdsStr, activeTabIds, ]); // 현재 선택된 테이블의 탭 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(() => { console.log("📋 [TableSearchWidget] 필터 설정 useEffect 실행:", { currentTable: currentTable?.tableName, currentTableTabId, filterMode, selectedTableId, 컬럼수: currentTable?.columns?.length, }); if (!currentTable?.tableName) return; // 고정 모드: presetFilters를 activeFilters로 설정 if (filterMode === "preset") { const activeFiltersList: TableFilter[] = presetFilters.map((f) => ({ columnName: f.columnName, operator: "contains", value: "", filterType: f.filterType, 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; } // 동적 모드: 화면별로 독립적인 필터 설정 불러오기 // 참고: FilterPanel.tsx에서도 screenId만 사용하여 저장하므로 키가 일치해야 함 const filterConfigKey = screenId ? `table_filters_${currentTable.tableName}_screen_${screenId}` : `table_filters_${currentTable.tableName}`; const savedFilters = localStorage.getItem(filterConfigKey); console.log("🔑 [TableSearchWidget] 필터 설정 키 확인:", { filterConfigKey, savedFilters: savedFilters ? `${savedFilters.substring(0, 100)}...` : null, screenId, tableName: currentTable.tableName, }); if (savedFilters) { try { const parsed = JSON.parse(savedFilters) as Array<{ columnName: string; columnLabel: string; inputType: string; enabled: boolean; filterType: "text" | "number" | "date" | "select"; width?: number; }>; // enabled된 필터들만 activeFilters로 설정 const activeFiltersList: TableFilter[] = parsed .filter((f) => f.enabled) .map((f) => ({ columnName: f.columnName, operator: "contains", value: "", filterType: f.filterType, width: f.width || 200, })); console.log("📌 [TableSearchWidget] 필터 설정 로드:", { filterConfigKey, 총필터수: parsed.length, 활성화필터수: activeFiltersList.length, 활성화필터: activeFiltersList.map((f) => f.columnName), }); 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); // 파싱 에러 시 필터 초기화 setActiveFilters([]); setFilterValues({}); } } else { // 필터 설정이 없으면 activeFilters와 filterValues 모두 초기화 console.log("⚠️ [TableSearchWidget] 저장된 필터 설정 없음 - 필터 초기화:", { tableName: currentTable.tableName, filterConfigKey, }); setActiveFilters([]); setFilterValues({}); setSelectOptions({}); } // eslint-disable-next-line react-hooks/exhaustive-deps }, [currentTable?.tableName, filterMode, screenId, currentTableTabId, JSON.stringify(presetFilters)]); // select 옵션 초기 로드 (한 번만 실행, 이후 유지) useEffect(() => { if (!currentTable?.getColumnUniqueValues || activeFilters.length === 0) { return; } const loadSelectOptions = async () => { const selectFilters = activeFilters.filter((f) => f.filterType === "select"); if (selectFilters.length === 0) { return; } const newOptions: Record> = { ...selectOptions }; for (const filter of selectFilters) { // 이미 로드된 옵션이 있으면 스킵 (초기값 유지) if (newOptions[filter.columnName] && newOptions[filter.columnName].length > 0) { continue; } try { const options = await currentTable.getColumnUniqueValues(filter.columnName); newOptions[filter.columnName] = options; } catch (error) { console.error("❌ [TableSearchWidget] select 옵션 로드 실패:", filter.columnName, error); } } setSelectOptions(newOptions); }; loadSelectOptions(); }, [activeFilters, currentTable?.tableName, currentTable?.getColumnUniqueValues]); // dataCount 제거, tableName으로 변경 // 높이 변화 감지 및 알림 (실제 화면에서만) 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; // 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); } catch (error) { console.error("저장된 높이 복원 실패:", error); } } }, [screenId, component.id, setWidgetHeight]); const hasMultipleTables = tableList.length > 1; // 필터 값 변경 핸들러 const handleFilterChange = (columnName: string, value: any) => { const newValues = { ...filterValues, [columnName]: value, }; setFilterValues(newValues); // 탭별 필터 값 저장 if (currentTable?.tableName && currentTableTabId) { const storageKey = getTabFilterStorageKey(currentTable.tableName, currentTableTabId); localStorage.setItem(storageKey, JSON.stringify(newValues)); } // 실시간 검색: 값 변경 시 즉시 필터 적용 applyFilters(newValues); }; // 필터 적용 함수 const applyFilters = (values: Record = filterValues) => { // 빈 값이 아닌 필터만 적용 const filtersWithValues = activeFilters .map((filter) => { let filterValue = values[filter.columnName]; // 날짜 범위 객체를 처리 if ( filter.filterType === "date" && filterValue && typeof filterValue === "object" && (filterValue.from || filterValue.to) ) { // 날짜 범위 객체를 문자열 형식으로 변환 (백엔드 재시작 불필요) const formatDate = (date: Date) => { const year = date.getFullYear(); const month = String(date.getMonth() + 1).padStart(2, "0"); const day = String(date.getDate()).padStart(2, "0"); return `${year}-${month}-${day}`; }; // "YYYY-MM-DD|YYYY-MM-DD" 형식으로 변환 const fromStr = filterValue.from ? formatDate(filterValue.from) : ""; const toStr = filterValue.to ? formatDate(filterValue.to) : ""; if (fromStr && toStr) { // 둘 다 있으면 파이프로 연결 filterValue = `${fromStr}|${toStr}`; } else if (fromStr) { // 시작일만 있으면 filterValue = `${fromStr}|`; } else if (toStr) { // 종료일만 있으면 filterValue = `|${toStr}`; } else { filterValue = ""; } } // 다중선택 배열을 처리 (파이프로 연결된 문자열로 변환) if (filter.filterType === "select" && Array.isArray(filterValue)) { filterValue = filterValue.join("|"); } return { ...filter, value: filterValue || "", }; }) .filter((f) => { // 빈 값 체크 if (!f.value) return false; if (typeof f.value === "string" && f.value === "") return false; if (Array.isArray(f.value) && f.value.length === 0) return false; return true; }); console.log("🔍 [TableSearchWidget] applyFilters 호출:", { currentTableId: currentTable?.tableId, currentTableName: currentTable?.tableName, hasOnFilterChange: !!currentTable?.onFilterChange, filtersCount: filtersWithValues.length, filters: filtersWithValues.map((f) => ({ col: f.columnName, op: f.operator, val: f.value, })), }); if (currentTable?.onFilterChange) { currentTable.onFilterChange(filtersWithValues); } else { console.warn("⚠️ [TableSearchWidget] onFilterChange가 없음!", { currentTable }); } }; // 필터 초기화 const handleResetFilters = () => { setFilterValues({}); setSelectedLabels({}); currentTable?.onFilterChange([]); // 탭별 저장된 필터 값도 초기화 if (currentTable?.tableName && currentTableTabId) { const storageKey = getTabFilterStorageKey(currentTable.tableName, currentTableTabId); localStorage.removeItem(storageKey); } }; // 필터 입력 필드 렌더링 const renderFilterInput = (filter: TableFilter) => { const column = currentTable?.columns.find((c) => c.columnName === filter.columnName); const value = filterValues[filter.columnName] || ""; const width = filter.width || 200; // 기본 너비 200px switch (filter.filterType) { case "date": return (
{ if (dateRange.from && dateRange.to) { // 기간이 선택되면 from과 to를 모두 저장 handleFilterChange(filter.columnName, dateRange); } else { handleFilterChange(filter.columnName, ""); } }} includeTime={false} />
); case "number": return ( handleFilterChange(filter.columnName, e.target.value)} className="h-9 text-xs focus-visible:ring-0 focus-visible:ring-offset-0 focus-visible:outline-none sm:text-sm" style={{ width: `${width}px`, height: "36px", minHeight: "36px", outline: "none", boxShadow: "none" }} placeholder={column?.columnLabel} /> ); case "select": { const options = selectOptions[filter.columnName] || []; // 중복 제거 (value 기준) const uniqueOptions = options.reduce( (acc, option) => { if (!acc.find((opt) => opt.value === option.value)) { acc.push(option); } return acc; }, [] as Array<{ value: string; label: string }>, ); // 항상 다중선택 모드 const selectedValues: string[] = Array.isArray(value) ? value : value ? [value] : []; // 선택된 값들의 라벨 표시 const getDisplayText = () => { if (selectedValues.length === 0) return column?.columnLabel || "선택"; if (selectedValues.length === 1) { const opt = uniqueOptions.find((o) => o.value === selectedValues[0]); return opt?.label || selectedValues[0]; } return `${selectedValues.length}개 선택됨`; }; const handleMultiSelectChange = (optionValue: string, checked: boolean) => { let newValues: string[]; if (checked) { newValues = [...selectedValues, optionValue]; } else { newValues = selectedValues.filter((v) => v !== optionValue); } handleFilterChange(filter.columnName, newValues.length > 0 ? newValues : ""); }; return (
{uniqueOptions.length === 0 ? (
옵션 없음
) : (
{uniqueOptions.map((option, index) => (
handleMultiSelectChange(option.value, !selectedValues.includes(option.value))} > handleMultiSelectChange(option.value, checked as boolean)} onClick={(e) => e.stopPropagation()} /> {option.label}
))}
)}
{selectedValues.length > 0 && (
)}
); } default: // text return ( handleFilterChange(filter.columnName, e.target.value)} className="h-9 text-xs focus-visible:ring-0 focus-visible:ring-offset-0 focus-visible:outline-none sm:text-sm" style={{ width: `${width}px`, height: "36px", minHeight: "36px", outline: "none", boxShadow: "none" }} placeholder={column?.columnLabel} /> ); } }; return (
{/* 필터 입력 필드들 */} {activeFilters.length > 0 && (
{activeFilters.map((filter) => (
{renderFilterInput(filter)}
))} {/* 초기화 버튼 */}
)} {/* 필터가 없을 때는 빈 공간 */} {activeFilters.length === 0 &&
} {/* 오른쪽: 데이터 건수 + 설정 버튼들 (고정 모드에서는 숨김) */}
{/* 데이터 건수 표시 */} {currentTable?.dataCount !== undefined && (
{currentTable.dataCount.toLocaleString()}건
)} {/* 동적 모드일 때만 설정 버튼들 표시 (미리보기에서는 비활성화) */} {filterMode === "dynamic" && ( <> )}
{/* 패널들 */} setColumnVisibilityOpen(false)} /> setFilterOpen(false)} onFiltersApplied={(filters) => setActiveFilters(filters)} screenId={screenId} /> setGroupingOpen(false)} />
); }