2025-11-12 10:48:24 +09:00
|
|
|
"use client";
|
|
|
|
|
|
2025-12-03 18:48:23 +09:00
|
|
|
import React, { useState, useEffect, useRef, useMemo } from "react";
|
2025-11-12 10:48:24 +09:00
|
|
|
import { Button } from "@/components/ui/button";
|
2025-11-12 12:06:58 +09:00
|
|
|
import { Input } from "@/components/ui/input";
|
2025-12-01 10:36:57 +09:00
|
|
|
import { Settings, Filter, Layers, X, Check, ChevronsUpDown } from "lucide-react";
|
2025-11-12 10:48:24 +09:00
|
|
|
import { useTableOptions } from "@/contexts/TableOptionsContext";
|
2025-11-12 14:54:49 +09:00
|
|
|
import { useTableSearchWidgetHeight } from "@/contexts/TableSearchWidgetHeightContext";
|
2025-12-17 15:00:15 +09:00
|
|
|
import { useActiveTab } from "@/contexts/ActiveTabContext";
|
2025-11-12 10:48:24 +09:00
|
|
|
import { ColumnVisibilityPanel } from "@/components/screen/table-options/ColumnVisibilityPanel";
|
|
|
|
|
import { FilterPanel } from "@/components/screen/table-options/FilterPanel";
|
|
|
|
|
import { GroupingPanel } from "@/components/screen/table-options/GroupingPanel";
|
2025-11-12 12:06:58 +09:00
|
|
|
import { TableFilter } from "@/types/table-options";
|
2025-11-12 18:51:20 +09:00
|
|
|
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
|
2025-11-25 17:48:23 +09:00
|
|
|
import { ModernDatePicker } from "@/components/screen/filters/ModernDatePicker";
|
2025-11-26 14:58:18 +09:00
|
|
|
import { useScreenPreview } from "@/contexts/ScreenPreviewContext";
|
2025-12-01 10:36:57 +09:00
|
|
|
import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover";
|
|
|
|
|
import { Checkbox } from "@/components/ui/checkbox";
|
|
|
|
|
import { cn } from "@/lib/utils";
|
2025-11-12 10:48:24 +09:00
|
|
|
|
2025-11-20 16:21:18 +09:00
|
|
|
interface PresetFilter {
|
|
|
|
|
id: string;
|
|
|
|
|
columnName: string;
|
|
|
|
|
columnLabel: string;
|
|
|
|
|
filterType: "text" | "number" | "date" | "select";
|
|
|
|
|
width?: number;
|
2025-12-01 10:36:57 +09:00
|
|
|
multiSelect?: boolean; // 다중선택 여부 (select 타입에서만 사용)
|
2025-11-20 16:21:18 +09:00
|
|
|
}
|
|
|
|
|
|
2025-11-12 10:48:24 +09:00
|
|
|
interface TableSearchWidgetProps {
|
|
|
|
|
component: {
|
|
|
|
|
id: string;
|
|
|
|
|
title?: string;
|
|
|
|
|
style?: {
|
|
|
|
|
width?: string;
|
|
|
|
|
height?: string;
|
|
|
|
|
padding?: string;
|
|
|
|
|
backgroundColor?: string;
|
|
|
|
|
};
|
|
|
|
|
componentConfig?: {
|
|
|
|
|
autoSelectFirstTable?: boolean; // 첫 번째 테이블 자동 선택 여부
|
|
|
|
|
showTableSelector?: boolean; // 테이블 선택 드롭다운 표시 여부
|
2025-11-20 16:21:18 +09:00
|
|
|
filterMode?: "dynamic" | "preset"; // 필터 모드
|
|
|
|
|
presetFilters?: PresetFilter[]; // 고정 필터 목록
|
2025-12-03 18:48:23 +09:00
|
|
|
targetPanelPosition?: "left" | "right" | "auto"; // 분할 패널에서 대상 패널 위치 (기본: "left")
|
2025-11-12 10:48:24 +09:00
|
|
|
};
|
|
|
|
|
};
|
2025-11-12 14:54:49 +09:00
|
|
|
screenId?: number; // 화면 ID
|
|
|
|
|
onHeightChange?: (height: number) => void; // 높이 변화 콜백
|
2025-11-12 10:48:24 +09:00
|
|
|
}
|
|
|
|
|
|
2025-11-12 14:54:49 +09:00
|
|
|
export function TableSearchWidget({ component, screenId, onHeightChange }: TableSearchWidgetProps) {
|
2025-12-24 14:46:51 +09:00
|
|
|
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,
|
|
|
|
|
});
|
2025-11-26 14:58:18 +09:00
|
|
|
const { isPreviewMode } = useScreenPreview(); // 미리보기 모드 확인
|
2025-12-17 15:00:15 +09:00
|
|
|
const { getAllActiveTabIds, activeTabs } = useActiveTab(); // 활성 탭 정보
|
2025-11-12 18:51:20 +09:00
|
|
|
|
2025-11-12 14:54:49 +09:00
|
|
|
// 높이 관리 context (실제 화면에서만 사용)
|
2025-11-12 18:51:20 +09:00
|
|
|
let setWidgetHeight:
|
|
|
|
|
| ((screenId: number, componentId: string, height: number, originalHeight: number) => void)
|
|
|
|
|
| undefined;
|
2025-11-12 14:54:49 +09:00
|
|
|
try {
|
|
|
|
|
const heightContext = useTableSearchWidgetHeight();
|
|
|
|
|
setWidgetHeight = heightContext.setWidgetHeight;
|
|
|
|
|
} catch (e) {
|
|
|
|
|
// Context가 없으면 (디자이너 모드) 무시
|
|
|
|
|
setWidgetHeight = undefined;
|
|
|
|
|
}
|
2025-12-24 14:46:51 +09:00
|
|
|
|
2025-12-17 15:00:15 +09:00
|
|
|
// 탭별 필터 값 저장 (탭 ID -> 필터 값)
|
|
|
|
|
const [tabFilterValues, setTabFilterValues] = useState<Record<string, Record<string, any>>>({});
|
2025-11-12 18:51:20 +09:00
|
|
|
|
2025-11-12 10:48:24 +09:00
|
|
|
const [columnVisibilityOpen, setColumnVisibilityOpen] = useState(false);
|
|
|
|
|
const [filterOpen, setFilterOpen] = useState(false);
|
|
|
|
|
const [groupingOpen, setGroupingOpen] = useState(false);
|
2025-11-12 18:51:20 +09:00
|
|
|
|
2025-11-12 12:06:58 +09:00
|
|
|
// 활성화된 필터 목록
|
|
|
|
|
const [activeFilters, setActiveFilters] = useState<TableFilter[]>([]);
|
2025-11-25 17:48:23 +09:00
|
|
|
const [filterValues, setFilterValues] = useState<Record<string, any>>({});
|
2025-11-12 12:06:58 +09:00
|
|
|
// select 타입 필터의 옵션들
|
|
|
|
|
const [selectOptions, setSelectOptions] = useState<Record<string, Array<{ label: string; value: string }>>>({});
|
2025-11-12 14:16:16 +09:00
|
|
|
// 선택된 값의 라벨 저장 (데이터 없을 때도 라벨 유지)
|
|
|
|
|
const [selectedLabels, setSelectedLabels] = useState<Record<string, string>>({});
|
2025-11-12 18:51:20 +09:00
|
|
|
|
2025-11-12 14:54:49 +09:00
|
|
|
// 높이 감지를 위한 ref
|
|
|
|
|
const containerRef = useRef<HTMLDivElement>(null);
|
2025-11-12 10:48:24 +09:00
|
|
|
|
|
|
|
|
const autoSelectFirstTable = component.componentConfig?.autoSelectFirstTable ?? true;
|
|
|
|
|
const showTableSelector = component.componentConfig?.showTableSelector ?? true;
|
2025-11-20 16:21:18 +09:00
|
|
|
const filterMode = component.componentConfig?.filterMode ?? "dynamic";
|
|
|
|
|
const presetFilters = component.componentConfig?.presetFilters ?? [];
|
2025-12-03 18:48:23 +09:00
|
|
|
const targetPanelPosition = component.componentConfig?.targetPanelPosition ?? "left"; // 기본값: 좌측 패널
|
2025-11-12 10:48:24 +09:00
|
|
|
|
|
|
|
|
// Map을 배열로 변환
|
2025-12-03 18:48:23 +09:00
|
|
|
const allTableList = Array.from(registeredTables.values());
|
2025-12-24 14:46:51 +09:00
|
|
|
|
2025-12-17 15:00:15 +09:00
|
|
|
// 현재 활성 탭 ID 목록
|
|
|
|
|
const activeTabIds = useMemo(() => getAllActiveTabIds(), [activeTabs]);
|
2025-12-24 14:46:51 +09:00
|
|
|
|
2025-12-17 15:00:15 +09:00
|
|
|
// 대상 패널 위치 + 활성 탭에 따라 테이블 필터링
|
2025-12-03 18:48:23 +09:00
|
|
|
const tableList = useMemo(() => {
|
2025-12-17 15:00:15 +09:00
|
|
|
// 1단계: 활성 탭 기반 필터링
|
|
|
|
|
// - 활성 탭에 속한 테이블만 표시
|
|
|
|
|
// - 탭에 속하지 않은 테이블(parentTabId가 없는)도 포함
|
2025-12-24 14:46:51 +09:00
|
|
|
let filteredByTab = allTableList.filter((table) => {
|
2025-12-17 15:00:15 +09:00
|
|
|
// 탭에 속하지 않는 테이블은 항상 표시
|
|
|
|
|
if (!table.parentTabId) return true;
|
|
|
|
|
// 활성 탭에 속한 테이블만 표시
|
|
|
|
|
return activeTabIds.includes(table.parentTabId);
|
2025-12-03 18:48:23 +09:00
|
|
|
});
|
|
|
|
|
|
2025-12-17 15:00:15 +09:00
|
|
|
// 2단계: 대상 패널 위치에 따라 추가 필터링
|
|
|
|
|
if (targetPanelPosition !== "auto") {
|
2025-12-24 14:46:51 +09:00
|
|
|
filteredByTab = filteredByTab.filter((table) => {
|
2025-12-17 15:00:15 +09:00
|
|
|
const tableId = table.tableId.toLowerCase();
|
2025-12-24 14:46:51 +09:00
|
|
|
|
2025-12-17 15:00:15 +09:00
|
|
|
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;
|
|
|
|
|
}
|
2025-12-24 14:46:51 +09:00
|
|
|
|
2025-12-17 15:00:15 +09:00
|
|
|
return true;
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 필터링된 결과가 없으면 탭 기반 필터링 결과만 반환
|
|
|
|
|
if (filteredByTab.length === 0) {
|
2025-12-24 14:46:51 +09:00
|
|
|
return allTableList.filter((table) => !table.parentTabId || activeTabIds.includes(table.parentTabId));
|
2025-12-03 18:48:23 +09:00
|
|
|
}
|
|
|
|
|
|
2025-12-17 15:00:15 +09:00
|
|
|
return filteredByTab;
|
|
|
|
|
}, [allTableList, targetPanelPosition, activeTabIds]);
|
2025-12-03 18:48:23 +09:00
|
|
|
|
|
|
|
|
// currentTable은 tableList(필터링된 목록)에서 가져와야 함
|
|
|
|
|
const currentTable = useMemo(() => {
|
2025-12-18 09:53:26 +09:00
|
|
|
console.log("🔍 [TableSearchWidget] currentTable 계산:", {
|
|
|
|
|
selectedTableId,
|
|
|
|
|
tableListLength: tableList.length,
|
2025-12-24 14:46:51 +09:00
|
|
|
tableList: tableList.map((t) => ({ id: t.tableId, name: t.tableName, parentTabId: t.parentTabId })),
|
2025-12-18 09:53:26 +09:00
|
|
|
});
|
2025-12-24 14:46:51 +09:00
|
|
|
|
2025-12-03 18:48:23 +09:00
|
|
|
if (!selectedTableId) return undefined;
|
2025-12-24 14:46:51 +09:00
|
|
|
|
2025-12-03 18:48:23 +09:00
|
|
|
// 먼저 tableList(필터링된 목록)에서 찾기
|
2025-12-24 14:46:51 +09:00
|
|
|
const tableFromList = tableList.find((t) => t.tableId === selectedTableId);
|
2025-12-03 18:48:23 +09:00
|
|
|
if (tableFromList) {
|
2025-12-18 09:53:26 +09:00
|
|
|
console.log("✅ [TableSearchWidget] 테이블 찾음 (tableList):", tableFromList.tableName);
|
2025-12-03 18:48:23 +09:00
|
|
|
return tableFromList;
|
|
|
|
|
}
|
2025-12-24 14:46:51 +09:00
|
|
|
|
2025-12-03 18:48:23 +09:00
|
|
|
// tableList에 없으면 전체에서 찾기 (폴백)
|
2025-12-18 09:53:26 +09:00
|
|
|
const tableFromAll = getTable(selectedTableId);
|
|
|
|
|
console.log("🔄 [TableSearchWidget] 테이블 찾음 (전체):", tableFromAll?.tableName);
|
|
|
|
|
return tableFromAll;
|
2025-12-03 18:48:23 +09:00
|
|
|
}, [selectedTableId, tableList, getTable]);
|
2025-11-12 10:48:24 +09:00
|
|
|
|
2025-12-18 09:53:26 +09:00
|
|
|
// 🆕 활성 탭 ID 문자열 (변경 감지용)
|
|
|
|
|
const activeTabIdsStr = useMemo(() => activeTabIds.join(","), [activeTabIds]);
|
2025-12-24 14:46:51 +09:00
|
|
|
|
2025-12-18 09:53:26 +09:00
|
|
|
// 🆕 이전 활성 탭 ID 추적 (탭 전환 감지용)
|
|
|
|
|
const prevActiveTabIdsRef = useRef<string>(activeTabIdsStr);
|
2025-12-24 14:46:51 +09:00
|
|
|
|
2025-12-03 18:48:23 +09:00
|
|
|
// 대상 패널의 첫 번째 테이블 자동 선택
|
2025-11-12 10:48:24 +09:00
|
|
|
useEffect(() => {
|
2025-12-03 18:48:23 +09:00
|
|
|
if (!autoSelectFirstTable || tableList.length === 0) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
2025-12-18 09:53:26 +09:00
|
|
|
// 🆕 탭 전환 감지: 활성 탭이 변경되었는지 확인
|
|
|
|
|
const tabChanged = prevActiveTabIdsRef.current !== activeTabIdsStr;
|
|
|
|
|
if (tabChanged) {
|
|
|
|
|
console.log("🔄 [TableSearchWidget] 탭 전환 감지:", {
|
|
|
|
|
이전탭: prevActiveTabIdsRef.current,
|
|
|
|
|
현재탭: activeTabIdsStr,
|
2025-12-24 14:46:51 +09:00
|
|
|
가용테이블: tableList.map((t) => ({ id: t.tableId, tableName: t.tableName, parentTabId: t.parentTabId })),
|
|
|
|
|
현재선택테이블: selectedTableId,
|
2025-12-18 09:53:26 +09:00
|
|
|
});
|
|
|
|
|
prevActiveTabIdsRef.current = activeTabIdsStr;
|
2025-12-24 14:46:51 +09:00
|
|
|
|
2025-12-18 09:53:26 +09:00
|
|
|
// 🆕 탭 전환 시: 해당 탭에 속한 테이블 중 첫 번째 강제 선택
|
2025-12-24 14:46:51 +09:00
|
|
|
const activeTabTable = tableList.find((t) => t.parentTabId && activeTabIds.includes(t.parentTabId));
|
2025-12-18 09:53:26 +09:00
|
|
|
const targetTable = activeTabTable || tableList[0];
|
2025-12-24 14:46:51 +09:00
|
|
|
|
2025-12-18 09:53:26 +09:00
|
|
|
if (targetTable) {
|
|
|
|
|
console.log("✅ [TableSearchWidget] 탭 전환으로 테이블 강제 선택:", {
|
|
|
|
|
테이블ID: targetTable.tableId,
|
|
|
|
|
테이블명: targetTable.tableName,
|
|
|
|
|
탭ID: targetTable.parentTabId,
|
2025-12-24 14:46:51 +09:00
|
|
|
이전테이블: selectedTableId,
|
2025-12-18 09:53:26 +09:00
|
|
|
});
|
|
|
|
|
setSelectedTableId(targetTable.tableId);
|
|
|
|
|
}
|
|
|
|
|
return; // 탭 전환 시에는 여기서 종료
|
|
|
|
|
}
|
|
|
|
|
|
2025-12-03 18:48:23 +09:00
|
|
|
// 현재 선택된 테이블이 대상 패널에 있는지 확인
|
2025-12-24 14:46:51 +09:00
|
|
|
const isCurrentTableInTarget = selectedTableId && tableList.some((t) => t.tableId === selectedTableId);
|
2025-11-12 18:51:20 +09:00
|
|
|
|
2025-12-18 09:53:26 +09:00
|
|
|
// 현재 선택된 테이블이 대상 패널에 없으면 첫 번째 테이블 선택
|
2025-12-03 18:48:23 +09:00
|
|
|
if (!selectedTableId || !isCurrentTableInTarget) {
|
2025-12-24 14:46:51 +09:00
|
|
|
const activeTabTable = tableList.find((t) => t.parentTabId && activeTabIds.includes(t.parentTabId));
|
2025-12-18 09:53:26 +09:00
|
|
|
const targetTable = activeTabTable || tableList[0];
|
2025-12-24 14:46:51 +09:00
|
|
|
|
2025-12-18 09:53:26 +09:00
|
|
|
if (targetTable && targetTable.tableId !== selectedTableId) {
|
|
|
|
|
console.log("✅ [TableSearchWidget] 테이블 자동 선택 (초기):", {
|
|
|
|
|
테이블ID: targetTable.tableId,
|
|
|
|
|
테이블명: targetTable.tableName,
|
2025-12-24 14:46:51 +09:00
|
|
|
탭ID: targetTable.parentTabId,
|
2025-12-18 09:53:26 +09:00
|
|
|
});
|
|
|
|
|
setSelectedTableId(targetTable.tableId);
|
|
|
|
|
}
|
2025-11-12 10:48:24 +09:00
|
|
|
}
|
2025-12-24 14:46:51 +09:00
|
|
|
}, [
|
|
|
|
|
tableList,
|
|
|
|
|
selectedTableId,
|
|
|
|
|
autoSelectFirstTable,
|
|
|
|
|
setSelectedTableId,
|
|
|
|
|
targetPanelPosition,
|
|
|
|
|
activeTabIdsStr,
|
|
|
|
|
activeTabIds,
|
|
|
|
|
]);
|
2025-11-12 10:48:24 +09:00
|
|
|
|
2025-12-17 15:00:15 +09:00
|
|
|
// 현재 선택된 테이블의 탭 ID (탭별 필터 저장용)
|
|
|
|
|
const currentTableTabId = currentTable?.parentTabId;
|
|
|
|
|
|
|
|
|
|
// 탭별 필터 값 저장 키 생성
|
|
|
|
|
const getTabFilterStorageKey = (tableName: string, tabId?: string) => {
|
2025-12-24 14:46:51 +09:00
|
|
|
const baseKey = screenId
|
2025-12-17 15:00:15 +09:00
|
|
|
? `table_filter_values_${tableName}_screen_${screenId}`
|
|
|
|
|
: `table_filter_values_${tableName}`;
|
|
|
|
|
return tabId ? `${baseKey}_tab_${tabId}` : baseKey;
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// 탭 변경 시 이전 탭의 필터 값 저장 + 새 탭의 필터 값 복원
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
if (!currentTable?.tableName) return;
|
2025-12-24 14:46:51 +09:00
|
|
|
|
2025-12-17 15:00:15 +09:00
|
|
|
// 현재 필터 값이 있으면 탭별로 저장
|
|
|
|
|
if (Object.keys(filterValues).length > 0 && currentTableTabId) {
|
|
|
|
|
const storageKey = getTabFilterStorageKey(currentTable.tableName, currentTableTabId);
|
|
|
|
|
localStorage.setItem(storageKey, JSON.stringify(filterValues));
|
2025-12-24 14:46:51 +09:00
|
|
|
|
2025-12-17 15:00:15 +09:00
|
|
|
// 메모리 캐시에도 저장
|
2025-12-24 14:46:51 +09:00
|
|
|
setTabFilterValues((prev) => ({
|
2025-12-17 15:00:15 +09:00
|
|
|
...prev,
|
2025-12-24 14:46:51 +09:00
|
|
|
[currentTableTabId]: filterValues,
|
2025-12-17 15:00:15 +09:00
|
|
|
}));
|
|
|
|
|
}
|
|
|
|
|
}, [currentTableTabId, currentTable?.tableName]);
|
|
|
|
|
|
2025-11-20 16:21:18 +09:00
|
|
|
// 현재 테이블의 저장된 필터 불러오기 (동적 모드) 또는 고정 필터 적용 (고정 모드)
|
2025-11-12 12:06:58 +09:00
|
|
|
useEffect(() => {
|
2025-12-18 09:53:26 +09:00
|
|
|
console.log("📋 [TableSearchWidget] 필터 설정 useEffect 실행:", {
|
|
|
|
|
currentTable: currentTable?.tableName,
|
|
|
|
|
currentTableTabId,
|
|
|
|
|
filterMode,
|
|
|
|
|
selectedTableId,
|
2025-12-24 14:46:51 +09:00
|
|
|
컬럼수: currentTable?.columns?.length,
|
2025-12-18 09:53:26 +09:00
|
|
|
});
|
2025-11-20 16:21:18 +09:00
|
|
|
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);
|
2025-12-24 14:46:51 +09:00
|
|
|
|
2025-12-17 15:00:15 +09:00
|
|
|
// 탭별 저장된 필터 값 복원
|
|
|
|
|
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({});
|
|
|
|
|
}
|
|
|
|
|
}
|
2025-11-20 16:21:18 +09:00
|
|
|
return;
|
|
|
|
|
}
|
2025-11-12 18:51:20 +09:00
|
|
|
|
2025-12-18 09:53:26 +09:00
|
|
|
// 동적 모드: 화면별로 독립적인 필터 설정 불러오기
|
|
|
|
|
// 참고: FilterPanel.tsx에서도 screenId만 사용하여 저장하므로 키가 일치해야 함
|
2025-12-24 14:46:51 +09:00
|
|
|
const filterConfigKey = screenId
|
2025-12-18 09:53:26 +09:00
|
|
|
? `table_filters_${currentTable.tableName}_screen_${screenId}`
|
2025-11-20 16:21:18 +09:00
|
|
|
: `table_filters_${currentTable.tableName}`;
|
2025-12-17 15:00:15 +09:00
|
|
|
const savedFilters = localStorage.getItem(filterConfigKey);
|
2025-11-20 16:21:18 +09:00
|
|
|
|
2025-12-18 09:53:26 +09:00
|
|
|
console.log("🔑 [TableSearchWidget] 필터 설정 키 확인:", {
|
|
|
|
|
filterConfigKey,
|
|
|
|
|
savedFilters: savedFilters ? `${savedFilters.substring(0, 100)}...` : null,
|
|
|
|
|
screenId,
|
2025-12-24 14:46:51 +09:00
|
|
|
tableName: currentTable.tableName,
|
2025-12-18 09:53:26 +09:00
|
|
|
});
|
|
|
|
|
|
2025-11-20 16:21:18 +09:00
|
|
|
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,
|
2025-12-17 15:00:15 +09:00
|
|
|
width: f.width || 200,
|
2025-11-20 16:21:18 +09:00
|
|
|
}));
|
|
|
|
|
|
2025-12-18 09:53:26 +09:00
|
|
|
console.log("📌 [TableSearchWidget] 필터 설정 로드:", {
|
|
|
|
|
filterConfigKey,
|
|
|
|
|
총필터수: parsed.length,
|
|
|
|
|
활성화필터수: activeFiltersList.length,
|
2025-12-24 14:46:51 +09:00
|
|
|
활성화필터: activeFiltersList.map((f) => f.columnName),
|
2025-12-18 09:53:26 +09:00
|
|
|
});
|
|
|
|
|
|
2025-11-20 16:21:18 +09:00
|
|
|
setActiveFilters(activeFiltersList);
|
2025-12-24 14:46:51 +09:00
|
|
|
|
2025-12-17 15:00:15 +09:00
|
|
|
// 탭별 저장된 필터 값 복원
|
|
|
|
|
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({});
|
|
|
|
|
}
|
2025-11-20 16:21:18 +09:00
|
|
|
} catch (error) {
|
|
|
|
|
console.error("저장된 필터 불러오기 실패:", error);
|
2025-12-18 09:53:26 +09:00
|
|
|
// 파싱 에러 시 필터 초기화
|
|
|
|
|
setActiveFilters([]);
|
|
|
|
|
setFilterValues({});
|
2025-11-12 12:06:58 +09:00
|
|
|
}
|
2025-12-17 15:00:15 +09:00
|
|
|
} else {
|
2025-12-18 09:53:26 +09:00
|
|
|
// 필터 설정이 없으면 activeFilters와 filterValues 모두 초기화
|
|
|
|
|
console.log("⚠️ [TableSearchWidget] 저장된 필터 설정 없음 - 필터 초기화:", {
|
|
|
|
|
tableName: currentTable.tableName,
|
2025-12-24 14:46:51 +09:00
|
|
|
filterConfigKey,
|
2025-12-18 09:53:26 +09:00
|
|
|
});
|
|
|
|
|
setActiveFilters([]);
|
2025-12-17 15:00:15 +09:00
|
|
|
setFilterValues({});
|
2025-12-18 09:53:26 +09:00
|
|
|
setSelectOptions({});
|
2025-11-12 12:06:58 +09:00
|
|
|
}
|
2025-11-20 16:21:18 +09:00
|
|
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
2025-12-17 15:00:15 +09:00
|
|
|
}, [currentTable?.tableName, filterMode, screenId, currentTableTabId, JSON.stringify(presetFilters)]);
|
2025-11-12 14:02:58 +09:00
|
|
|
|
2025-11-12 14:16:16 +09:00
|
|
|
// select 옵션 초기 로드 (한 번만 실행, 이후 유지)
|
2025-11-12 14:02:58 +09:00
|
|
|
useEffect(() => {
|
|
|
|
|
if (!currentTable?.getColumnUniqueValues || activeFilters.length === 0) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const loadSelectOptions = async () => {
|
2025-11-12 18:51:20 +09:00
|
|
|
const selectFilters = activeFilters.filter((f) => f.filterType === "select");
|
|
|
|
|
|
2025-11-12 14:02:58 +09:00
|
|
|
if (selectFilters.length === 0) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
2025-11-12 14:16:16 +09:00
|
|
|
const newOptions: Record<string, Array<{ label: string; value: string }>> = { ...selectOptions };
|
2025-11-12 18:51:20 +09:00
|
|
|
|
2025-11-12 14:02:58 +09:00
|
|
|
for (const filter of selectFilters) {
|
2025-11-12 14:16:16 +09:00
|
|
|
// 이미 로드된 옵션이 있으면 스킵 (초기값 유지)
|
|
|
|
|
if (newOptions[filter.columnName] && newOptions[filter.columnName].length > 0) {
|
|
|
|
|
continue;
|
|
|
|
|
}
|
2025-11-12 18:51:20 +09:00
|
|
|
|
2025-11-12 14:02:58 +09:00
|
|
|
try {
|
|
|
|
|
const options = await currentTable.getColumnUniqueValues(filter.columnName);
|
|
|
|
|
newOptions[filter.columnName] = options;
|
|
|
|
|
} catch (error) {
|
|
|
|
|
console.error("❌ [TableSearchWidget] select 옵션 로드 실패:", filter.columnName, error);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
setSelectOptions(newOptions);
|
|
|
|
|
};
|
2025-11-12 18:51:20 +09:00
|
|
|
|
2025-11-12 14:02:58 +09:00
|
|
|
loadSelectOptions();
|
2025-11-12 14:16:16 +09:00
|
|
|
}, [activeFilters, currentTable?.tableName, currentTable?.getColumnUniqueValues]); // dataCount 제거, tableName으로 변경
|
2025-11-12 12:06:58 +09:00
|
|
|
|
2025-11-12 14:54:49 +09:00
|
|
|
// 높이 변화 감지 및 알림 (실제 화면에서만)
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
if (!containerRef.current || !screenId || !setWidgetHeight) return;
|
2025-11-12 18:51:20 +09:00
|
|
|
|
2025-11-12 14:54:49 +09:00
|
|
|
// 컴포넌트의 원래 높이 (디자이너에서 설정한 높이)
|
|
|
|
|
const originalHeight = (component as any).size?.height || 50;
|
|
|
|
|
|
|
|
|
|
const resizeObserver = new ResizeObserver((entries) => {
|
|
|
|
|
for (const entry of entries) {
|
|
|
|
|
const newHeight = entry.contentRect.height;
|
2025-11-12 18:51:20 +09:00
|
|
|
|
2025-11-12 14:54:49 +09:00
|
|
|
// Context에 높이 저장 (다른 컴포넌트 위치 조정에 사용)
|
|
|
|
|
setWidgetHeight(screenId, component.id, newHeight, originalHeight);
|
2025-11-12 18:51:20 +09:00
|
|
|
|
2025-11-12 14:54:49 +09:00
|
|
|
// localStorage에 높이 저장 (새로고침 시 복원용)
|
|
|
|
|
localStorage.setItem(
|
|
|
|
|
`table_search_widget_height_screen_${screenId}_${component.id}`,
|
2025-11-12 18:51:20 +09:00
|
|
|
JSON.stringify({ height: newHeight, originalHeight }),
|
2025-11-12 14:54:49 +09:00
|
|
|
);
|
2025-11-12 18:51:20 +09:00
|
|
|
|
2025-11-12 14:54:49 +09:00
|
|
|
// 콜백이 있으면 호출
|
|
|
|
|
if (onHeightChange) {
|
|
|
|
|
onHeightChange(newHeight);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
resizeObserver.observe(containerRef.current);
|
|
|
|
|
|
|
|
|
|
return () => {
|
|
|
|
|
resizeObserver.disconnect();
|
|
|
|
|
};
|
|
|
|
|
}, [screenId, component.id, setWidgetHeight, onHeightChange]);
|
|
|
|
|
|
|
|
|
|
// 화면 로딩 시 저장된 높이 복원
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
if (!screenId || !setWidgetHeight) return;
|
2025-11-12 18:51:20 +09:00
|
|
|
|
2025-11-12 14:54:49 +09:00
|
|
|
const storageKey = `table_search_widget_height_screen_${screenId}_${component.id}`;
|
|
|
|
|
const savedData = localStorage.getItem(storageKey);
|
2025-11-12 18:51:20 +09:00
|
|
|
|
2025-11-12 14:54:49 +09:00
|
|
|
if (savedData) {
|
|
|
|
|
try {
|
|
|
|
|
const { height, originalHeight } = JSON.parse(savedData);
|
|
|
|
|
setWidgetHeight(screenId, component.id, height, originalHeight);
|
|
|
|
|
} catch (error) {
|
|
|
|
|
console.error("저장된 높이 복원 실패:", error);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}, [screenId, component.id, setWidgetHeight]);
|
|
|
|
|
|
2025-11-12 10:48:24 +09:00
|
|
|
const hasMultipleTables = tableList.length > 1;
|
|
|
|
|
|
2025-11-12 12:06:58 +09:00
|
|
|
// 필터 값 변경 핸들러
|
2025-11-25 17:48:23 +09:00
|
|
|
const handleFilterChange = (columnName: string, value: any) => {
|
2025-11-12 14:16:16 +09:00
|
|
|
const newValues = {
|
|
|
|
|
...filterValues,
|
2025-11-12 12:06:58 +09:00
|
|
|
[columnName]: value,
|
2025-11-12 14:16:16 +09:00
|
|
|
};
|
2025-11-12 18:51:20 +09:00
|
|
|
|
2025-11-12 14:16:16 +09:00
|
|
|
setFilterValues(newValues);
|
2025-11-12 18:51:20 +09:00
|
|
|
|
2025-12-17 15:00:15 +09:00
|
|
|
// 탭별 필터 값 저장
|
|
|
|
|
if (currentTable?.tableName && currentTableTabId) {
|
|
|
|
|
const storageKey = getTabFilterStorageKey(currentTable.tableName, currentTableTabId);
|
|
|
|
|
localStorage.setItem(storageKey, JSON.stringify(newValues));
|
|
|
|
|
}
|
|
|
|
|
|
2025-11-12 14:16:16 +09:00
|
|
|
// 실시간 검색: 값 변경 시 즉시 필터 적용
|
|
|
|
|
applyFilters(newValues);
|
2025-11-12 12:06:58 +09:00
|
|
|
};
|
|
|
|
|
|
2025-11-12 14:16:16 +09:00
|
|
|
// 필터 적용 함수
|
2025-11-25 17:48:23 +09:00
|
|
|
const applyFilters = (values: Record<string, any> = filterValues) => {
|
2025-11-12 12:06:58 +09:00
|
|
|
// 빈 값이 아닌 필터만 적용
|
2025-11-12 18:51:20 +09:00
|
|
|
const filtersWithValues = activeFilters
|
2025-11-25 17:48:23 +09:00
|
|
|
.map((filter) => {
|
|
|
|
|
let filterValue = values[filter.columnName];
|
2025-12-24 14:46:51 +09:00
|
|
|
|
2025-11-25 17:48:23 +09:00
|
|
|
// 날짜 범위 객체를 처리
|
2025-12-24 14:46:51 +09:00
|
|
|
if (
|
|
|
|
|
filter.filterType === "date" &&
|
|
|
|
|
filterValue &&
|
|
|
|
|
typeof filterValue === "object" &&
|
|
|
|
|
(filterValue.from || filterValue.to)
|
|
|
|
|
) {
|
2025-11-25 17:48:23 +09:00
|
|
|
// 날짜 범위 객체를 문자열 형식으로 변환 (백엔드 재시작 불필요)
|
|
|
|
|
const formatDate = (date: Date) => {
|
|
|
|
|
const year = date.getFullYear();
|
2025-12-24 14:46:51 +09:00
|
|
|
const month = String(date.getMonth() + 1).padStart(2, "0");
|
|
|
|
|
const day = String(date.getDate()).padStart(2, "0");
|
2025-11-25 17:48:23 +09:00
|
|
|
return `${year}-${month}-${day}`;
|
|
|
|
|
};
|
2025-12-24 14:46:51 +09:00
|
|
|
|
2025-11-25 17:48:23 +09:00
|
|
|
// "YYYY-MM-DD|YYYY-MM-DD" 형식으로 변환
|
|
|
|
|
const fromStr = filterValue.from ? formatDate(filterValue.from) : "";
|
|
|
|
|
const toStr = filterValue.to ? formatDate(filterValue.to) : "";
|
2025-12-24 14:46:51 +09:00
|
|
|
|
2025-11-25 17:48:23 +09:00
|
|
|
if (fromStr && toStr) {
|
|
|
|
|
// 둘 다 있으면 파이프로 연결
|
|
|
|
|
filterValue = `${fromStr}|${toStr}`;
|
|
|
|
|
} else if (fromStr) {
|
|
|
|
|
// 시작일만 있으면
|
|
|
|
|
filterValue = `${fromStr}|`;
|
|
|
|
|
} else if (toStr) {
|
|
|
|
|
// 종료일만 있으면
|
|
|
|
|
filterValue = `|${toStr}`;
|
|
|
|
|
} else {
|
|
|
|
|
filterValue = "";
|
|
|
|
|
}
|
|
|
|
|
}
|
2025-12-24 14:46:51 +09:00
|
|
|
|
2025-12-01 10:36:57 +09:00
|
|
|
// 다중선택 배열을 처리 (파이프로 연결된 문자열로 변환)
|
|
|
|
|
if (filter.filterType === "select" && Array.isArray(filterValue)) {
|
|
|
|
|
filterValue = filterValue.join("|");
|
|
|
|
|
}
|
2025-12-24 14:46:51 +09:00
|
|
|
|
2025-11-25 17:48:23 +09:00
|
|
|
return {
|
|
|
|
|
...filter,
|
|
|
|
|
value: filterValue || "",
|
|
|
|
|
};
|
|
|
|
|
})
|
|
|
|
|
.filter((f) => {
|
|
|
|
|
// 빈 값 체크
|
|
|
|
|
if (!f.value) return false;
|
|
|
|
|
if (typeof f.value === "string" && f.value === "") return false;
|
2025-12-01 10:36:57 +09:00
|
|
|
if (Array.isArray(f.value) && f.value.length === 0) return false;
|
2025-11-25 17:48:23 +09:00
|
|
|
return true;
|
|
|
|
|
});
|
2025-11-12 12:06:58 +09:00
|
|
|
|
2025-12-24 14:46:51 +09:00
|
|
|
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 });
|
|
|
|
|
}
|
2025-11-12 12:06:58 +09:00
|
|
|
};
|
|
|
|
|
|
2025-11-12 14:16:16 +09:00
|
|
|
// 필터 초기화
|
|
|
|
|
const handleResetFilters = () => {
|
|
|
|
|
setFilterValues({});
|
|
|
|
|
setSelectedLabels({});
|
|
|
|
|
currentTable?.onFilterChange([]);
|
2025-12-24 14:46:51 +09:00
|
|
|
|
2025-12-17 15:00:15 +09:00
|
|
|
// 탭별 저장된 필터 값도 초기화
|
|
|
|
|
if (currentTable?.tableName && currentTableTabId) {
|
|
|
|
|
const storageKey = getTabFilterStorageKey(currentTable.tableName, currentTableTabId);
|
|
|
|
|
localStorage.removeItem(storageKey);
|
|
|
|
|
}
|
2025-11-12 14:16:16 +09:00
|
|
|
};
|
|
|
|
|
|
2025-11-12 12:06:58 +09:00
|
|
|
// 필터 입력 필드 렌더링
|
|
|
|
|
const renderFilterInput = (filter: TableFilter) => {
|
|
|
|
|
const column = currentTable?.columns.find((c) => c.columnName === filter.columnName);
|
|
|
|
|
const value = filterValues[filter.columnName] || "";
|
2025-11-12 14:50:06 +09:00
|
|
|
const width = filter.width || 200; // 기본 너비 200px
|
2025-11-12 12:06:58 +09:00
|
|
|
|
|
|
|
|
switch (filter.filterType) {
|
|
|
|
|
case "date":
|
|
|
|
|
return (
|
2025-11-25 17:48:23 +09:00
|
|
|
<div style={{ width: `${width}px` }}>
|
|
|
|
|
<ModernDatePicker
|
|
|
|
|
label={column?.columnLabel || filter.columnName}
|
2025-12-24 14:46:51 +09:00
|
|
|
value={value ? (typeof value === "string" ? { from: new Date(value), to: new Date(value) } : value) : {}}
|
2025-11-25 17:48:23 +09:00
|
|
|
onChange={(dateRange) => {
|
|
|
|
|
if (dateRange.from && dateRange.to) {
|
|
|
|
|
// 기간이 선택되면 from과 to를 모두 저장
|
|
|
|
|
handleFilterChange(filter.columnName, dateRange);
|
|
|
|
|
} else {
|
|
|
|
|
handleFilterChange(filter.columnName, "");
|
|
|
|
|
}
|
|
|
|
|
}}
|
|
|
|
|
includeTime={false}
|
|
|
|
|
/>
|
|
|
|
|
</div>
|
2025-11-12 12:06:58 +09:00
|
|
|
);
|
|
|
|
|
|
|
|
|
|
case "number":
|
|
|
|
|
return (
|
|
|
|
|
<Input
|
|
|
|
|
type="number"
|
|
|
|
|
value={value}
|
|
|
|
|
onChange={(e) => handleFilterChange(filter.columnName, e.target.value)}
|
2025-11-12 18:51:20 +09:00
|
|
|
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" }}
|
2025-11-12 12:06:58 +09:00
|
|
|
placeholder={column?.columnLabel}
|
|
|
|
|
/>
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
case "select": {
|
2025-12-24 14:46:51 +09:00
|
|
|
const options = selectOptions[filter.columnName] || [];
|
2025-11-12 18:51:20 +09:00
|
|
|
|
2025-11-12 14:50:06 +09:00
|
|
|
// 중복 제거 (value 기준)
|
2025-11-12 18:51:20 +09:00
|
|
|
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 }>,
|
|
|
|
|
);
|
|
|
|
|
|
2025-12-01 10:36:57 +09:00
|
|
|
// 항상 다중선택 모드
|
2025-12-24 14:46:51 +09:00
|
|
|
const selectedValues: string[] = Array.isArray(value) ? value : value ? [value] : [];
|
|
|
|
|
|
2025-12-01 10:36:57 +09:00
|
|
|
// 선택된 값들의 라벨 표시
|
|
|
|
|
const getDisplayText = () => {
|
|
|
|
|
if (selectedValues.length === 0) return column?.columnLabel || "선택";
|
|
|
|
|
if (selectedValues.length === 1) {
|
2025-12-24 14:46:51 +09:00
|
|
|
const opt = uniqueOptions.find((o) => o.value === selectedValues[0]);
|
2025-12-01 10:36:57 +09:00
|
|
|
return opt?.label || selectedValues[0];
|
|
|
|
|
}
|
|
|
|
|
return `${selectedValues.length}개 선택됨`;
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const handleMultiSelectChange = (optionValue: string, checked: boolean) => {
|
|
|
|
|
let newValues: string[];
|
|
|
|
|
if (checked) {
|
|
|
|
|
newValues = [...selectedValues, optionValue];
|
|
|
|
|
} else {
|
2025-12-24 14:46:51 +09:00
|
|
|
newValues = selectedValues.filter((v) => v !== optionValue);
|
2025-12-01 10:36:57 +09:00
|
|
|
}
|
|
|
|
|
handleFilterChange(filter.columnName, newValues.length > 0 ? newValues : "");
|
|
|
|
|
};
|
|
|
|
|
|
2025-11-12 12:06:58 +09:00
|
|
|
return (
|
2025-12-01 10:36:57 +09:00
|
|
|
<Popover>
|
|
|
|
|
<PopoverTrigger asChild>
|
|
|
|
|
<Button
|
|
|
|
|
variant="outline"
|
|
|
|
|
role="combobox"
|
|
|
|
|
className={cn(
|
|
|
|
|
"h-9 min-h-9 justify-between text-xs font-normal focus:ring-0 focus:outline-none focus-visible:ring-0 focus-visible:ring-offset-0 sm:text-sm",
|
2025-12-24 14:46:51 +09:00
|
|
|
selectedValues.length === 0 && "text-muted-foreground",
|
2025-12-01 10:36:57 +09:00
|
|
|
)}
|
|
|
|
|
style={{ width: `${width}px`, height: "36px", minHeight: "36px", outline: "none", boxShadow: "none" }}
|
|
|
|
|
>
|
|
|
|
|
<span className="truncate">{getDisplayText()}</span>
|
|
|
|
|
<ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" />
|
|
|
|
|
</Button>
|
|
|
|
|
</PopoverTrigger>
|
2025-12-24 14:46:51 +09:00
|
|
|
<PopoverContent className="p-0" style={{ width: `${width}px` }} align="start">
|
2025-12-01 10:36:57 +09:00
|
|
|
<div className="max-h-60 overflow-auto">
|
|
|
|
|
{uniqueOptions.length === 0 ? (
|
|
|
|
|
<div className="text-muted-foreground px-3 py-2 text-xs">옵션 없음</div>
|
|
|
|
|
) : (
|
|
|
|
|
<div className="p-1">
|
|
|
|
|
{uniqueOptions.map((option, index) => (
|
|
|
|
|
<div
|
|
|
|
|
key={`${filter.columnName}-multi-${option.value}-${index}`}
|
2025-12-24 14:46:51 +09:00
|
|
|
className="hover:bg-accent flex cursor-pointer items-center space-x-2 rounded-sm px-2 py-1.5"
|
2025-12-01 10:36:57 +09:00
|
|
|
onClick={() => handleMultiSelectChange(option.value, !selectedValues.includes(option.value))}
|
|
|
|
|
>
|
|
|
|
|
<Checkbox
|
|
|
|
|
checked={selectedValues.includes(option.value)}
|
|
|
|
|
onCheckedChange={(checked) => handleMultiSelectChange(option.value, checked as boolean)}
|
|
|
|
|
onClick={(e) => e.stopPropagation()}
|
|
|
|
|
/>
|
|
|
|
|
<span className="text-xs sm:text-sm">{option.label}</span>
|
|
|
|
|
</div>
|
|
|
|
|
))}
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
|
|
|
|
</div>
|
|
|
|
|
{selectedValues.length > 0 && (
|
|
|
|
|
<div className="border-t p-1">
|
|
|
|
|
<Button
|
|
|
|
|
variant="ghost"
|
|
|
|
|
size="sm"
|
2025-12-24 14:46:51 +09:00
|
|
|
className="h-7 w-full text-xs"
|
2025-12-01 10:36:57 +09:00
|
|
|
onClick={() => handleFilterChange(filter.columnName, "")}
|
|
|
|
|
>
|
|
|
|
|
선택 초기화
|
|
|
|
|
</Button>
|
|
|
|
|
</div>
|
2025-11-12 12:06:58 +09:00
|
|
|
)}
|
2025-12-01 10:36:57 +09:00
|
|
|
</PopoverContent>
|
|
|
|
|
</Popover>
|
2025-11-12 12:06:58 +09:00
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
default: // text
|
|
|
|
|
return (
|
|
|
|
|
<Input
|
|
|
|
|
type="text"
|
|
|
|
|
value={value}
|
|
|
|
|
onChange={(e) => handleFilterChange(filter.columnName, e.target.value)}
|
2025-11-12 18:51:20 +09:00
|
|
|
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" }}
|
2025-11-12 12:06:58 +09:00
|
|
|
placeholder={column?.columnLabel}
|
|
|
|
|
/>
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
2025-11-12 10:48:24 +09:00
|
|
|
return (
|
|
|
|
|
<div
|
2025-11-12 14:54:49 +09:00
|
|
|
ref={containerRef}
|
2025-11-12 18:51:20 +09:00
|
|
|
className="bg-card flex w-full flex-wrap items-center gap-2 border-b"
|
2025-11-12 10:48:24 +09:00
|
|
|
style={{
|
|
|
|
|
padding: component.style?.padding || "0.75rem",
|
|
|
|
|
backgroundColor: component.style?.backgroundColor,
|
2025-11-12 14:50:06 +09:00
|
|
|
minHeight: "48px",
|
2025-11-12 10:48:24 +09:00
|
|
|
}}
|
|
|
|
|
>
|
2025-11-12 12:06:58 +09:00
|
|
|
{/* 필터 입력 필드들 */}
|
|
|
|
|
{activeFilters.length > 0 && (
|
2025-11-12 14:50:06 +09:00
|
|
|
<div className="flex flex-1 flex-wrap items-center gap-2">
|
2025-11-12 12:06:58 +09:00
|
|
|
{activeFilters.map((filter) => (
|
2025-11-12 18:51:20 +09:00
|
|
|
<div key={filter.columnName}>{renderFilterInput(filter)}</div>
|
2025-11-12 12:06:58 +09:00
|
|
|
))}
|
2025-11-12 18:51:20 +09:00
|
|
|
|
2025-11-12 14:16:16 +09:00
|
|
|
{/* 초기화 버튼 */}
|
2025-11-12 18:51:20 +09:00
|
|
|
<Button variant="outline" size="sm" onClick={handleResetFilters} className="h-9 shrink-0 text-xs sm:text-sm">
|
2025-11-12 14:16:16 +09:00
|
|
|
<X className="mr-1 h-3 w-3 sm:h-4 sm:w-4" />
|
|
|
|
|
초기화
|
2025-11-12 12:06:58 +09:00
|
|
|
</Button>
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
2025-11-12 10:48:24 +09:00
|
|
|
|
2025-11-12 12:06:58 +09:00
|
|
|
{/* 필터가 없을 때는 빈 공간 */}
|
|
|
|
|
{activeFilters.length === 0 && <div className="flex-1" />}
|
2025-11-12 10:48:24 +09:00
|
|
|
|
2025-11-20 16:21:18 +09:00
|
|
|
{/* 오른쪽: 데이터 건수 + 설정 버튼들 (고정 모드에서는 숨김) */}
|
2025-11-12 18:51:20 +09:00
|
|
|
<div className="flex flex-shrink-0 items-center gap-2">
|
2025-11-12 12:06:58 +09:00
|
|
|
{/* 데이터 건수 표시 */}
|
|
|
|
|
{currentTable?.dataCount !== undefined && (
|
2025-11-12 18:51:20 +09:00
|
|
|
<div className="bg-muted text-muted-foreground rounded-md px-3 py-1.5 text-xs font-medium sm:text-sm">
|
2025-11-12 12:06:58 +09:00
|
|
|
{currentTable.dataCount.toLocaleString()}건
|
2025-11-12 10:48:24 +09:00
|
|
|
</div>
|
|
|
|
|
)}
|
|
|
|
|
|
2025-11-26 14:58:18 +09:00
|
|
|
{/* 동적 모드일 때만 설정 버튼들 표시 (미리보기에서는 비활성화) */}
|
2025-11-20 16:21:18 +09:00
|
|
|
{filterMode === "dynamic" && (
|
|
|
|
|
<>
|
|
|
|
|
<Button
|
|
|
|
|
variant="outline"
|
|
|
|
|
size="sm"
|
2025-11-26 14:58:18 +09:00
|
|
|
onClick={() => !isPreviewMode && setColumnVisibilityOpen(true)}
|
|
|
|
|
disabled={!selectedTableId || isPreviewMode}
|
2025-11-20 16:21:18 +09:00
|
|
|
className="h-8 text-xs sm:h-9 sm:text-sm"
|
|
|
|
|
>
|
|
|
|
|
<Settings className="mr-1 h-3 w-3 sm:h-4 sm:w-4" />
|
|
|
|
|
테이블 옵션
|
|
|
|
|
</Button>
|
|
|
|
|
|
|
|
|
|
<Button
|
|
|
|
|
variant="outline"
|
|
|
|
|
size="sm"
|
2025-11-26 14:58:18 +09:00
|
|
|
onClick={() => !isPreviewMode && setFilterOpen(true)}
|
|
|
|
|
disabled={!selectedTableId || isPreviewMode}
|
2025-11-20 16:21:18 +09:00
|
|
|
className="h-8 text-xs sm:h-9 sm:text-sm"
|
|
|
|
|
>
|
|
|
|
|
<Filter className="mr-1 h-3 w-3 sm:h-4 sm:w-4" />
|
|
|
|
|
필터 설정
|
|
|
|
|
</Button>
|
|
|
|
|
|
|
|
|
|
<Button
|
|
|
|
|
variant="outline"
|
|
|
|
|
size="sm"
|
2025-11-26 14:58:18 +09:00
|
|
|
onClick={() => !isPreviewMode && setGroupingOpen(true)}
|
|
|
|
|
disabled={!selectedTableId || isPreviewMode}
|
2025-11-20 16:21:18 +09:00
|
|
|
className="h-8 text-xs sm:h-9 sm:text-sm"
|
|
|
|
|
>
|
|
|
|
|
<Layers className="mr-1 h-3 w-3 sm:h-4 sm:w-4" />
|
|
|
|
|
그룹 설정
|
|
|
|
|
</Button>
|
|
|
|
|
</>
|
|
|
|
|
)}
|
2025-11-12 10:48:24 +09:00
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
{/* 패널들 */}
|
2025-11-12 18:51:20 +09:00
|
|
|
<ColumnVisibilityPanel isOpen={columnVisibilityOpen} onClose={() => setColumnVisibilityOpen(false)} />
|
|
|
|
|
<FilterPanel
|
|
|
|
|
isOpen={filterOpen}
|
2025-11-12 12:06:58 +09:00
|
|
|
onClose={() => setFilterOpen(false)}
|
|
|
|
|
onFiltersApplied={(filters) => setActiveFilters(filters)}
|
2025-11-20 16:21:18 +09:00
|
|
|
screenId={screenId}
|
2025-11-12 12:06:58 +09:00
|
|
|
/>
|
2025-11-12 10:48:24 +09:00
|
|
|
<GroupingPanel isOpen={groupingOpen} onClose={() => setGroupingOpen(false)} />
|
|
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
}
|