diff --git a/backend-node/src/services/tableManagementService.ts b/backend-node/src/services/tableManagementService.ts index 173de022..28da136e 100644 --- a/backend-node/src/services/tableManagementService.ts +++ b/backend-node/src/services/tableManagementService.ts @@ -1165,12 +1165,26 @@ export class TableManagementService { paramCount: number; } | null> { try { - // πŸ”§ λ‚ μ§œ λ²”μœ„ λ¬Έμžμ—΄ "YYYY-MM-DD|YYYY-MM-DD" 체크 (μ΅œμš°μ„ !) + // πŸ”§ νŒŒμ΄ν”„λ‘œ κ΅¬λΆ„λœ λ¬Έμžμ—΄ 처리 (닀쀑선택 λ˜λŠ” λ‚ μ§œ λ²”μœ„) if (typeof value === "string" && value.includes("|")) { const columnInfo = await this.getColumnWebTypeInfo(tableName, columnName); + + // λ‚ μ§œ νƒ€μž…μ΄λ©΄ λ‚ μ§œ λ²”μœ„λ‘œ 처리 if (columnInfo && (columnInfo.webType === "date" || columnInfo.webType === "datetime")) { return this.buildDateRangeCondition(columnName, value, paramIndex); } + + // κ·Έ μ™Έ νƒ€μž…μ΄λ©΄ 닀쀑선택(IN 쑰건)으둜 처리 + const multiValues = value.split("|").filter((v: string) => v.trim() !== ""); + if (multiValues.length > 0) { + const placeholders = multiValues.map((_: string, idx: number) => `$${paramIndex + idx}`).join(", "); + logger.info(`πŸ” 닀쀑선택 ν•„ν„° 적용: ${columnName} IN (${multiValues.join(", ")})`); + return { + whereClause: `${columnName}::text IN (${placeholders})`, + values: multiValues, + paramCount: multiValues.length, + }; + } } // πŸ”§ λ‚ μ§œ λ²”μœ„ 객체 {from, to} 체크 diff --git a/frontend/lib/registry/components/table-search-widget/TableSearchWidget.tsx b/frontend/lib/registry/components/table-search-widget/TableSearchWidget.tsx index e13e3d94..6d513976 100644 --- a/frontend/lib/registry/components/table-search-widget/TableSearchWidget.tsx +++ b/frontend/lib/registry/components/table-search-widget/TableSearchWidget.tsx @@ -3,7 +3,7 @@ import React, { useState, useEffect, useRef } from "react"; import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; -import { Settings, Filter, Layers, X } from "lucide-react"; +import { Settings, Filter, Layers, X, Check, ChevronsUpDown } from "lucide-react"; import { useTableOptions } from "@/contexts/TableOptionsContext"; import { useTableSearchWidgetHeight } from "@/contexts/TableSearchWidgetHeightContext"; import { ColumnVisibilityPanel } from "@/components/screen/table-options/ColumnVisibilityPanel"; @@ -13,6 +13,9 @@ 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; @@ -20,6 +23,7 @@ interface PresetFilter { columnLabel: string; filterType: "text" | "number" | "date" | "select"; width?: number; + multiSelect?: boolean; // 닀쀑선택 μ—¬λΆ€ (select νƒ€μž…μ—μ„œλ§Œ μ‚¬μš©) } interface TableSearchWidgetProps { @@ -280,6 +284,11 @@ export function TableSearchWidget({ component, screenId, onHeightChange }: Table } } + // 닀쀑선택 배열을 처리 (νŒŒμ΄ν”„λ‘œ μ—°κ²°λœ λ¬Έμžμ—΄λ‘œ λ³€ν™˜) + if (filter.filterType === "select" && Array.isArray(filterValue)) { + filterValue = filterValue.join("|"); + } + return { ...filter, value: filterValue || "", @@ -289,6 +298,7 @@ export function TableSearchWidget({ component, screenId, onHeightChange }: Table // 빈 κ°’ 체크 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; }); @@ -343,12 +353,6 @@ export function TableSearchWidget({ component, screenId, onHeightChange }: Table case "select": { let options = selectOptions[filter.columnName] || []; - // ν˜„μž¬ μ„ νƒλœ 값이 μ˜΅μ…˜ λͺ©λ‘μ— μ—†μœΌλ©΄ μΆ”κ°€ (데이터 없을 λ•Œλ„ 선택값 μœ μ§€) - if (value && !options.find((opt) => opt.value === value)) { - const savedLabel = selectedLabels[filter.columnName] || value; - options = [{ value, label: savedLabel }, ...options]; - } - // 쀑볡 제거 (value κΈ°μ€€) const uniqueOptions = options.reduce( (acc, option) => { @@ -360,39 +364,86 @@ export function TableSearchWidget({ component, screenId, onHeightChange }: Table [] 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 ( - + + ); } diff --git a/frontend/lib/registry/components/table-search-widget/TableSearchWidgetConfigPanel.tsx b/frontend/lib/registry/components/table-search-widget/TableSearchWidgetConfigPanel.tsx index 8c4ab6a1..3424abb9 100644 --- a/frontend/lib/registry/components/table-search-widget/TableSearchWidgetConfigPanel.tsx +++ b/frontend/lib/registry/components/table-search-widget/TableSearchWidgetConfigPanel.tsx @@ -29,6 +29,7 @@ interface PresetFilter { columnLabel: string; filterType: "text" | "number" | "date" | "select"; width?: number; + multiSelect?: boolean; // 닀쀑선택 μ—¬λΆ€ (select νƒ€μž…μ—μ„œλ§Œ μ‚¬μš©) } export function TableSearchWidgetConfigPanel({