diff --git a/frontend/components/screen/config-panels/DataFilterConfigPanel.tsx b/frontend/components/screen/config-panels/DataFilterConfigPanel.tsx index 46b4d799..f3e65199 100644 --- a/frontend/components/screen/config-panels/DataFilterConfigPanel.tsx +++ b/frontend/components/screen/config-panels/DataFilterConfigPanel.tsx @@ -6,7 +6,8 @@ import { Input } from "@/components/ui/input"; import { Label } from "@/components/ui/label"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"; import { Switch } from "@/components/ui/switch"; -import { Trash2, Plus } from "lucide-react"; +import { Collapsible, CollapsibleContent, CollapsibleTrigger } from "@/components/ui/collapsible"; +import { Trash2, Plus, ChevronDown, ChevronRight } from "lucide-react"; import { ColumnFilter, DataFilterConfig } from "@/types/screen-management"; import { UnifiedColumnInfo } from "@/types/table-management"; import { getCategoryValues } from "@/lib/api/tableCategoryValue"; @@ -19,6 +20,67 @@ interface DataFilterConfigPanelProps { menuObjid?: number; // ๐Ÿ†• ๋ฉ”๋‰ด OBJID (์นดํ…Œ๊ณ ๋ฆฌ ๊ฐ’ ์กฐํšŒ ์‹œ ํ•„์š”) } +/** + * ์ ‘์„ ์ˆ˜ ์žˆ๋Š” ํ•„ํ„ฐ ํ•ญ๋ชฉ ์ปดํฌ๋„ŒํŠธ + */ +interface FilterItemCollapsibleProps { + filter: ColumnFilter; + index: number; + filterSummary: string; + onRemove: () => void; + children: React.ReactNode; +} + +const FilterItemCollapsible: React.FC = ({ + filter, + index, + filterSummary, + onRemove, + children, +}) => { + const [isOpen, setIsOpen] = useState(!filter.columnName); // ์„ค์ • ์•ˆ ๋œ ํ•„ํ„ฐ๋Š” ์—ด๋ฆฐ ์ƒํƒœ๋กœ + + return ( + +
+ +
+ {/* ์ƒ๋‹จ: ํ•„ํ„ฐ ๋ฒˆํ˜ธ + ์‚ญ์ œ ๋ฒ„ํŠผ */} +
+
+ {isOpen ? ( + + ) : ( + + )} + ํ•„ํ„ฐ {index + 1} +
+ +
+ {/* ํ•˜๋‹จ: ํ•„ํ„ฐ ์š”์•ฝ (์ „์ฒด ๋„ˆ๋น„ ์‚ฌ์šฉ) */} +
+ + {filterSummary} + +
+
+
+ {children} +
+
+ ); +}; + /** * ๋ฐ์ดํ„ฐ ํ•„ํ„ฐ ์„ค์ • ํŒจ๋„ * ํ…Œ์ด๋ธ” ๋ฆฌ์ŠคํŠธ, ๋ถ„ํ•  ํŒจ๋„, ํ”Œ๋กœ์šฐ ์œ„์ ฏ ๋“ฑ์—์„œ ์‚ฌ์šฉ @@ -36,13 +98,13 @@ export function DataFilterConfigPanel({ menuObjid, sampleColumns: columns.slice(0, 3), }); - + const [localConfig, setLocalConfig] = useState( config || { enabled: false, filters: [], matchType: "all", - } + }, ); // ์นดํ…Œ๊ณ ๋ฆฌ ๊ฐ’ ์บ์‹œ (์ปฌ๋Ÿผ๋ช… -> ์นดํ…Œ๊ณ ๋ฆฌ ๊ฐ’ ๋ชฉ๋ก) @@ -52,7 +114,7 @@ export function DataFilterConfigPanel({ useEffect(() => { if (config) { setLocalConfig(config); - + // ๐Ÿ†• ๊ธฐ์กด ํ•„ํ„ฐ ์ค‘ ์นดํ…Œ๊ณ ๋ฆฌ ํƒ€์ž…์ธ ๊ฒƒ๋“ค์˜ ๊ฐ’์„ ๋กœ๋“œ config.filters?.forEach((filter) => { if (filter.valueType === "category" && filter.columnName) { @@ -69,7 +131,7 @@ export function DataFilterConfigPanel({ return; // ์ด๋ฏธ ๋กœ๋“œ๋˜์—ˆ๊ฑฐ๋‚˜ ๋กœ๋”ฉ ์ค‘์ด๋ฉด ์Šคํ‚ต } - setLoadingCategories(prev => ({ ...prev, [columnName]: true })); + setLoadingCategories((prev) => ({ ...prev, [columnName]: true })); try { console.log("๐Ÿ” ์นดํ…Œ๊ณ ๋ฆฌ ๊ฐ’ ๋กœ๋“œ ์‹œ์ž‘:", { @@ -82,7 +144,7 @@ export function DataFilterConfigPanel({ tableName, columnName, false, // includeInactive - menuObjid // ๐Ÿ†• ๋ฉ”๋‰ด OBJID ์ „๋‹ฌ + menuObjid, // ๐Ÿ†• ๋ฉ”๋‰ด OBJID ์ „๋‹ฌ ); console.log("๐Ÿ“ฆ ์นดํ…Œ๊ณ ๋ฆฌ ๊ฐ’ ๋กœ๋“œ ์‘๋‹ต:", response); @@ -92,16 +154,16 @@ export function DataFilterConfigPanel({ value: item.valueCode, label: item.valueLabel, })); - + console.log("โœ… ์นดํ…Œ๊ณ ๋ฆฌ ๊ฐ’ ์„ค์ •:", { columnName, valuesCount: values.length }); - setCategoryValues(prev => ({ ...prev, [columnName]: values })); + setCategoryValues((prev) => ({ ...prev, [columnName]: values })); } else { console.warn("โš ๏ธ ์นดํ…Œ๊ณ ๋ฆฌ ๊ฐ’ ๋กœ๋“œ ์‹คํŒจ ๋˜๋Š” ๋ฐ์ดํ„ฐ ์—†์Œ:", response); } } catch (error) { console.error(`โŒ ์นดํ…Œ๊ณ ๋ฆฌ ๊ฐ’ ๋กœ๋“œ ์‹คํŒจ (${columnName}):`, error); } finally { - setLoadingCategories(prev => ({ ...prev, [columnName]: false })); + setLoadingCategories((prev) => ({ ...prev, [columnName]: false })); } }; @@ -145,9 +207,7 @@ export function DataFilterConfigPanel({ const handleFilterChange = (filterId: string, field: keyof ColumnFilter, value: any) => { const newConfig = { ...localConfig, - filters: localConfig.filters.map((filter) => - filter.id === filterId ? { ...filter, [field]: value } : filter - ), + filters: localConfig.filters.map((filter) => (filter.id === filterId ? { ...filter, [field]: value } : filter)), }; setLocalConfig(newConfig); onConfigChange(newConfig); @@ -178,7 +238,7 @@ export function DataFilterConfigPanel({ <> {/* ํ…Œ์ด๋ธ”๋ช… ํ‘œ์‹œ */} {tableName && ( -
+
ํ…Œ์ด๋ธ”: {tableName}
)} @@ -200,235 +260,127 @@ export function DataFilterConfigPanel({ )} {/* ํ•„ํ„ฐ ๋ชฉ๋ก */} -
- {localConfig.filters.map((filter, index) => ( -
-
- - ํ•„ํ„ฐ {index + 1} - - -
+
+ {localConfig.filters.map((filter, index) => { + // ์—ฐ์‚ฐ์ž ํ‘œ์‹œ ํ…์ŠคํŠธ + const operatorLabels: Record = { + equals: "=", + not_equals: "!=", + greater_than: ">", + less_than: "<", + greater_than_or_equal: ">=", + less_than_or_equal: "<=", + between: "BETWEEN", + in: "IN", + not_in: "NOT IN", + contains: "LIKE", + starts_with: "์‹œ์ž‘", + ends_with: "๋", + is_null: "IS NULL", + is_not_null: "IS NOT NULL", + date_range_contains: "๊ธฐ๊ฐ„ ๋‚ด", + }; - {/* ์ปฌ๋Ÿผ ์„ ํƒ (๋‚ ์งœ ๋ฒ”์œ„ ํฌํ•จ์ด ์•„๋‹ ๋•Œ๋งŒ ํ‘œ์‹œ) */} - {filter.operator !== "date_range_contains" && ( -
- - -
- )} + // ์ปฌ๋Ÿผ ๋ผ๋ฒจ ์ฐพ๊ธฐ + const columnLabel = + columns.find((c) => c.columnName === filter.columnName)?.columnLabel || filter.columnName; - {/* ์—ฐ์‚ฐ์ž ์„ ํƒ */} -
- - -
+ // ํ•„ํ„ฐ ์š”์•ฝ ํ…์ŠคํŠธ ์ƒ์„ฑ + const filterSummary = filter.columnName + ? `${columnLabel} ${operatorLabels[filter.operator] || filter.operator}${ + filter.operator !== "is_null" && filter.operator !== "is_not_null" && filter.value + ? ` ${filter.value}` + : "" + }` + : "์„ค์ • ํ•„์š”"; - {/* ๋‚ ์งœ ๋ฒ”์œ„ ํฌํ•จ - ์‹œ์ž‘์ผ/์ข…๋ฃŒ์ผ ์ปฌ๋Ÿผ ์„ ํƒ */} - {filter.operator === "date_range_contains" && ( - <> -
-

- ๐Ÿ’ก ๋‚ ์งœ ๋ฒ”์œ„ ํ•„ํ„ฐ๋ง ๊ทœ์น™: -
โ€ข ์‹œ์ž‘์ผ๋งŒ ์žˆ๊ณ  ์ข…๋ฃŒ์ผ์ด NULL โ†’ ์‹œ์ž‘์ผ ์ดํ›„ ๋ชจ๋“  ๋ฐ์ดํ„ฐ -
โ€ข ์ข…๋ฃŒ์ผ๋งŒ ์žˆ๊ณ  ์‹œ์ž‘์ผ์ด NULL โ†’ ์ข…๋ฃŒ์ผ ์ด์ „ ๋ชจ๋“  ๋ฐ์ดํ„ฐ -
โ€ข ๋‘˜ ๋‹ค ์žˆ์œผ๋ฉด โ†’ ๊ธฐ๊ฐ„ ๋‚ด ๋ฐ์ดํ„ฐ๋งŒ -

-
+ return ( + handleRemoveFilter(filter.id)} + > + {/* ์ปฌ๋Ÿผ ์„ ํƒ (๋‚ ์งœ ๋ฒ”์œ„ ํฌํ•จ์ด ์•„๋‹ ๋•Œ๋งŒ ํ‘œ์‹œ) */} + {filter.operator !== "date_range_contains" && (
- + -
-
- - -
- - )} + const column = columns.find((col) => col.columnName === value); - {/* ๊ฐ’ ํƒ€์ž… ์„ ํƒ (์นดํ…Œ๊ณ ๋ฆฌ/์ฝ”๋“œ ์ปฌ๋Ÿผ ๋˜๋Š” date_range_contains) */} - {(isCategoryOrCodeColumn(filter.columnName) || filter.operator === "date_range_contains") && ( -
- - +
+ )} + + {/* ์—ฐ์‚ฐ์ž ์„ ํƒ */} +
+ +
- )} - {/* ๊ฐ’ ์ž…๋ ฅ (NULL ์ฒดํฌ ๋ฐ date_range_contains์˜ dynamic ์ œ์™ธ) */} - {filter.operator !== "is_null" && - filter.operator !== "is_not_null" && - !(filter.operator === "date_range_contains" && filter.valueType === "dynamic") && ( -
- - {/* ์นดํ…Œ๊ณ ๋ฆฌ ํƒ€์ž…์ด๊ณ  ๊ฐ’ ํƒ€์ž…์ด category์ธ ๊ฒฝ์šฐ ์…€๋ ‰ํŠธ๋ฐ•์Šค */} - {filter.valueType === "category" && categoryValues[filter.columnName] ? ( + {/* ๋‚ ์งœ ๋ฒ”์œ„ ํฌํ•จ - ์‹œ์ž‘์ผ/์ข…๋ฃŒ์ผ ์ปฌ๋Ÿผ ์„ ํƒ */} + {filter.operator === "date_range_contains" && ( + <> +
+

+ ๐Ÿ’ก ๋‚ ์งœ ๋ฒ”์œ„ ํ•„ํ„ฐ๋ง ๊ทœ์น™: +
โ€ข ์‹œ์ž‘์ผ๋งŒ ์žˆ๊ณ  ์ข…๋ฃŒ์ผ์ด NULL โ†’ ์‹œ์ž‘์ผ ์ดํ›„ ๋ชจ๋“  ๋ฐ์ดํ„ฐ +
โ€ข ์ข…๋ฃŒ์ผ๋งŒ ์žˆ๊ณ  ์‹œ์ž‘์ผ์ด NULL โ†’ ์ข…๋ฃŒ์ผ ์ด์ „ ๋ชจ๋“  ๋ฐ์ดํ„ฐ +
โ€ข ๋‘˜ ๋‹ค ์žˆ์œผ๋ฉด โ†’ ๊ธฐ๊ฐ„ ๋‚ด ๋ฐ์ดํ„ฐ๋งŒ +

+
+
+ + +
+
+ + +
+ + )} + + {/* ๊ฐ’ ํƒ€์ž… ์„ ํƒ (์นดํ…Œ๊ณ ๋ฆฌ/์ฝ”๋“œ ์ปฌ๋Ÿผ ๋˜๋Š” date_range_contains) */} + {(isCategoryOrCodeColumn(filter.columnName) || filter.operator === "date_range_contains") && ( +
+ - ) : filter.operator === "in" || filter.operator === "not_in" ? ( - { - const values = e.target.value.split(",").map((v) => v.trim()); - handleFilterChange(filter.id, "value", values); - }} - placeholder="์‰ผํ‘œ๋กœ ๊ตฌ๋ถ„ (์˜ˆ: ๊ฐ’1, ๊ฐ’2, ๊ฐ’3)" - className="h-8 text-xs sm:h-10 sm:text-sm" - /> - ) : filter.operator === "between" ? ( - { - const values = e.target.value.split("~").map((v) => v.trim()); - handleFilterChange(filter.id, "value", values.length === 2 ? values : [values[0] || "", ""]); - }} - placeholder="์‹œ์ž‘ ~ ์ข…๋ฃŒ (์˜ˆ: 2025-01-01 ~ 2025-12-31)" - className="h-8 text-xs sm:h-10 sm:text-sm" - /> - ) : ( - handleFilterChange(filter.id, "value", e.target.value)} - placeholder={filter.operator === "date_range_contains" ? "๋น„๊ตํ•  ๋‚ ์งœ ์„ ํƒ" : "ํ•„ํ„ฐ ๊ฐ’ ์ž…๋ ฅ"} - className="h-8 text-xs sm:h-10 sm:text-sm" - /> +
+ )} + + {/* ๊ฐ’ ์ž…๋ ฅ (NULL ์ฒดํฌ ๋ฐ date_range_contains์˜ dynamic ์ œ์™ธ) */} + {filter.operator !== "is_null" && + filter.operator !== "is_not_null" && + !(filter.operator === "date_range_contains" && filter.valueType === "dynamic") && ( +
+ + {/* ์นดํ…Œ๊ณ ๋ฆฌ ํƒ€์ž…์ด๊ณ  ๊ฐ’ ํƒ€์ž…์ด category์ธ ๊ฒฝ์šฐ ์…€๋ ‰ํŠธ๋ฐ•์Šค */} + {filter.valueType === "category" && categoryValues[filter.columnName] ? ( + + ) : filter.operator === "in" || filter.operator === "not_in" ? ( + { + const values = e.target.value.split(",").map((v) => v.trim()); + handleFilterChange(filter.id, "value", values); + }} + placeholder="์‰ผํ‘œ๋กœ ๊ตฌ๋ถ„ (์˜ˆ: ๊ฐ’1, ๊ฐ’2, ๊ฐ’3)" + className="h-8 text-xs sm:h-10 sm:text-sm" + /> + ) : filter.operator === "between" ? ( + { + const values = e.target.value.split("~").map((v) => v.trim()); + handleFilterChange( + filter.id, + "value", + values.length === 2 ? values : [values[0] || "", ""], + ); + }} + placeholder="์‹œ์ž‘ ~ ์ข…๋ฃŒ (์˜ˆ: 2025-01-01 ~ 2025-12-31)" + className="h-8 text-xs sm:h-10 sm:text-sm" + /> + ) : ( + handleFilterChange(filter.id, "value", e.target.value)} + placeholder={ + filter.operator === "date_range_contains" ? "๋น„๊ตํ•  ๋‚ ์งœ ์„ ํƒ" : "ํ•„ํ„ฐ ๊ฐ’ ์ž…๋ ฅ" + } + className="h-8 text-xs sm:h-10 sm:text-sm" + /> + )} +

+ {filter.valueType === "category" && categoryValues[filter.columnName] + ? "์นดํ…Œ๊ณ ๋ฆฌ ๊ฐ’์„ ์„ ํƒํ•˜์„ธ์š”" + : filter.operator === "in" || filter.operator === "not_in" + ? "์—ฌ๋Ÿฌ ๊ฐ’์€ ์‰ผํ‘œ(,)๋กœ ๊ตฌ๋ถ„ํ•˜์„ธ์š”" + : filter.operator === "between" + ? "์‹œ์ž‘๊ณผ ์ข…๋ฃŒ ๊ฐ’์„ ~๋กœ ๊ตฌ๋ถ„ํ•˜์„ธ์š”" + : filter.operator === "date_range_contains" + ? "๊ธฐ๊ฐ„ ๋‚ด์— ํฌํ•จ๋˜๋Š”์ง€ ํ™•์ธํ•  ๋‚ ์งœ๋ฅผ ์„ ํƒํ•˜์„ธ์š”" + : "ํ•„ํ„ฐ๋งํ•  ๊ฐ’์„ ์ž…๋ ฅํ•˜์„ธ์š”"} +

+
)} -

- {filter.valueType === "category" && categoryValues[filter.columnName] - ? "์นดํ…Œ๊ณ ๋ฆฌ ๊ฐ’์„ ์„ ํƒํ•˜์„ธ์š”" - : filter.operator === "in" || filter.operator === "not_in" - ? "์—ฌ๋Ÿฌ ๊ฐ’์€ ์‰ผํ‘œ(,)๋กœ ๊ตฌ๋ถ„ํ•˜์„ธ์š”" - : filter.operator === "between" - ? "์‹œ์ž‘๊ณผ ์ข…๋ฃŒ ๊ฐ’์„ ~๋กœ ๊ตฌ๋ถ„ํ•˜์„ธ์š”" - : filter.operator === "date_range_contains" - ? "๊ธฐ๊ฐ„ ๋‚ด์— ํฌํ•จ๋˜๋Š”์ง€ ํ™•์ธํ•  ๋‚ ์งœ๋ฅผ ์„ ํƒํ•˜์„ธ์š”" - : "ํ•„ํ„ฐ๋งํ•  ๊ฐ’์„ ์ž…๋ ฅํ•˜์„ธ์š”"} -

-
- )} - - {/* date_range_contains์˜ dynamic ํƒ€์ž… ์•ˆ๋‚ด */} - {filter.operator === "date_range_contains" && filter.valueType === "dynamic" && ( -
-

- โ„น๏ธ ์˜ค๋Š˜ ๋‚ ์งœ๋ฅผ ๊ธฐ์ค€์œผ๋กœ ๊ธฐ๊ฐ„ ๋‚ด ๋ฐ์ดํ„ฐ๋ฅผ ํ•„ํ„ฐ๋งํ•ฉ๋‹ˆ๋‹ค. -

-
- )} -
- ))} + + {/* date_range_contains์˜ dynamic ํƒ€์ž… ์•ˆ๋‚ด */} + {filter.operator === "date_range_contains" && filter.valueType === "dynamic" && ( +
+

์˜ค๋Š˜ ๋‚ ์งœ๋ฅผ ๊ธฐ์ค€์œผ๋กœ ๊ธฐ๊ฐ„ ๋‚ด ๋ฐ์ดํ„ฐ๋ฅผ ํ•„ํ„ฐ๋งํ•ฉ๋‹ˆ๋‹ค.

+
+ )} + + ); + })}
{/* ํ•„ํ„ฐ ์ถ”๊ฐ€ ๋ฒ„ํŠผ */} {columns.length === 0 && ( -

- ํ…Œ์ด๋ธ”์„ ๋จผ์ € ์„ ํƒํ•ด์ฃผ์„ธ์š” -

+

ํ…Œ์ด๋ธ”์„ ๋จผ์ € ์„ ํƒํ•ด์ฃผ์„ธ์š”

)} )}
); } - diff --git a/frontend/lib/registry/components/split-panel-layout/SplitPanelLayoutComponent.tsx b/frontend/lib/registry/components/split-panel-layout/SplitPanelLayoutComponent.tsx index 9da76559..809e3adf 100644 --- a/frontend/lib/registry/components/split-panel-layout/SplitPanelLayoutComponent.tsx +++ b/frontend/lib/registry/components/split-panel-layout/SplitPanelLayoutComponent.tsx @@ -1014,10 +1014,14 @@ export const SplitPanelLayoutComponent: React.FC // ์ถ”๊ฐ€ dataFilter ์ ์šฉ let filteredData = result.data || []; const dataFilter = componentConfig.rightPanel?.dataFilter; - if (dataFilter?.enabled && dataFilter.conditions?.length > 0) { + // filters ๋˜๋Š” conditions ๋ฐฐ์—ด ์ง€์› (DataFilterConfigPanel์€ filters ์‚ฌ์šฉ) + const filterConditions = dataFilter?.filters || dataFilter?.conditions || []; + if (dataFilter?.enabled && filterConditions.length > 0) { filteredData = filteredData.filter((item: any) => { - return dataFilter.conditions.every((cond: any) => { - const value = item[cond.column]; + return filterConditions.every((cond: any) => { + // columnName ๋˜๋Š” column ์ง€์› + const columnName = cond.columnName || cond.column; + const value = item[columnName]; const condValue = cond.value; switch (cond.operator) { case "equals": @@ -1026,6 +1030,12 @@ export const SplitPanelLayoutComponent: React.FC return value !== condValue; case "contains": return String(value).includes(String(condValue)); + case "is_null": + case "NULL": + return value === null || value === undefined || value === ""; + case "is_not_null": + case "NOT NULL": + return value !== null && value !== undefined && value !== ""; default: return true; } @@ -1138,10 +1148,14 @@ export const SplitPanelLayoutComponent: React.FC // ๋ฐ์ดํ„ฐ ํ•„ํ„ฐ ์ ์šฉ const dataFilter = tabConfig.dataFilter; - if (dataFilter?.enabled && dataFilter.conditions?.length > 0) { + // filters ๋˜๋Š” conditions ๋ฐฐ์—ด ์ง€์› (DataFilterConfigPanel์€ filters ์‚ฌ์šฉ) + const filterConditions = dataFilter?.filters || dataFilter?.conditions || []; + if (dataFilter?.enabled && filterConditions.length > 0) { resultData = resultData.filter((item: any) => { - return dataFilter.conditions.every((cond: any) => { - const value = item[cond.column]; + return filterConditions.every((cond: any) => { + // columnName ๋˜๋Š” column ์ง€์› + const columnName = cond.columnName || cond.column; + const value = item[columnName]; const condValue = cond.value; switch (cond.operator) { case "equals": @@ -1150,6 +1164,12 @@ export const SplitPanelLayoutComponent: React.FC return value !== condValue; case "contains": return String(value).includes(String(condValue)); + case "is_null": + case "NULL": + return value === null || value === undefined || value === ""; + case "is_not_null": + case "NOT NULL": + return value !== null && value !== undefined && value !== ""; default: return true; }