검색필터 다중선택 기능
This commit is contained in:
parent
142fb15dc0
commit
a3d3db5437
|
|
@ -1165,12 +1165,26 @@ export class TableManagementService {
|
||||||
paramCount: number;
|
paramCount: number;
|
||||||
} | null> {
|
} | null> {
|
||||||
try {
|
try {
|
||||||
// 🔧 날짜 범위 문자열 "YYYY-MM-DD|YYYY-MM-DD" 체크 (최우선!)
|
// 🔧 파이프로 구분된 문자열 처리 (다중선택 또는 날짜 범위)
|
||||||
if (typeof value === "string" && value.includes("|")) {
|
if (typeof value === "string" && value.includes("|")) {
|
||||||
const columnInfo = await this.getColumnWebTypeInfo(tableName, columnName);
|
const columnInfo = await this.getColumnWebTypeInfo(tableName, columnName);
|
||||||
|
|
||||||
|
// 날짜 타입이면 날짜 범위로 처리
|
||||||
if (columnInfo && (columnInfo.webType === "date" || columnInfo.webType === "datetime")) {
|
if (columnInfo && (columnInfo.webType === "date" || columnInfo.webType === "datetime")) {
|
||||||
return this.buildDateRangeCondition(columnName, value, paramIndex);
|
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} 체크
|
// 🔧 날짜 범위 객체 {from, to} 체크
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,7 @@
|
||||||
import React, { useState, useEffect, useRef } from "react";
|
import React, { useState, useEffect, useRef } from "react";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { Input } from "@/components/ui/input";
|
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 { useTableOptions } from "@/contexts/TableOptionsContext";
|
||||||
import { useTableSearchWidgetHeight } from "@/contexts/TableSearchWidgetHeightContext";
|
import { useTableSearchWidgetHeight } from "@/contexts/TableSearchWidgetHeightContext";
|
||||||
import { ColumnVisibilityPanel } from "@/components/screen/table-options/ColumnVisibilityPanel";
|
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 { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
|
||||||
import { ModernDatePicker } from "@/components/screen/filters/ModernDatePicker";
|
import { ModernDatePicker } from "@/components/screen/filters/ModernDatePicker";
|
||||||
import { useScreenPreview } from "@/contexts/ScreenPreviewContext";
|
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 {
|
interface PresetFilter {
|
||||||
id: string;
|
id: string;
|
||||||
|
|
@ -20,6 +23,7 @@ interface PresetFilter {
|
||||||
columnLabel: string;
|
columnLabel: string;
|
||||||
filterType: "text" | "number" | "date" | "select";
|
filterType: "text" | "number" | "date" | "select";
|
||||||
width?: number;
|
width?: number;
|
||||||
|
multiSelect?: boolean; // 다중선택 여부 (select 타입에서만 사용)
|
||||||
}
|
}
|
||||||
|
|
||||||
interface TableSearchWidgetProps {
|
interface TableSearchWidgetProps {
|
||||||
|
|
@ -280,6 +284,11 @@ export function TableSearchWidget({ component, screenId, onHeightChange }: Table
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 다중선택 배열을 처리 (파이프로 연결된 문자열로 변환)
|
||||||
|
if (filter.filterType === "select" && Array.isArray(filterValue)) {
|
||||||
|
filterValue = filterValue.join("|");
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
...filter,
|
...filter,
|
||||||
value: filterValue || "",
|
value: filterValue || "",
|
||||||
|
|
@ -289,6 +298,7 @@ export function TableSearchWidget({ component, screenId, onHeightChange }: Table
|
||||||
// 빈 값 체크
|
// 빈 값 체크
|
||||||
if (!f.value) return false;
|
if (!f.value) return false;
|
||||||
if (typeof f.value === "string" && 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;
|
return true;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -343,12 +353,6 @@ export function TableSearchWidget({ component, screenId, onHeightChange }: Table
|
||||||
case "select": {
|
case "select": {
|
||||||
let options = selectOptions[filter.columnName] || [];
|
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 기준)
|
// 중복 제거 (value 기준)
|
||||||
const uniqueOptions = options.reduce(
|
const uniqueOptions = options.reduce(
|
||||||
(acc, option) => {
|
(acc, option) => {
|
||||||
|
|
@ -360,39 +364,86 @@ export function TableSearchWidget({ component, screenId, onHeightChange }: Table
|
||||||
[] as Array<{ value: string; label: string }>,
|
[] 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 (
|
return (
|
||||||
<Select
|
<Popover>
|
||||||
value={value}
|
<PopoverTrigger asChild>
|
||||||
onValueChange={(val) => {
|
<Button
|
||||||
// 선택한 값의 라벨 저장
|
variant="outline"
|
||||||
const selectedOption = uniqueOptions.find((opt) => opt.value === val);
|
role="combobox"
|
||||||
if (selectedOption) {
|
className={cn(
|
||||||
setSelectedLabels((prev) => ({
|
"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",
|
||||||
...prev,
|
selectedValues.length === 0 && "text-muted-foreground"
|
||||||
[filter.columnName]: selectedOption.label,
|
)}
|
||||||
}));
|
style={{ width: `${width}px`, height: "36px", minHeight: "36px", outline: "none", boxShadow: "none" }}
|
||||||
}
|
>
|
||||||
handleFilterChange(filter.columnName, val);
|
<span className="truncate">{getDisplayText()}</span>
|
||||||
}}
|
<ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" />
|
||||||
>
|
</Button>
|
||||||
<SelectTrigger
|
</PopoverTrigger>
|
||||||
className="h-9 min-h-9 text-xs focus:ring-0 focus:outline-none focus-visible:ring-0 focus-visible:ring-offset-0 focus-visible:outline-none sm:text-sm"
|
<PopoverContent
|
||||||
style={{ width: `${width}px`, height: "36px", minHeight: "36px", outline: "none", boxShadow: "none" }}
|
className="p-0"
|
||||||
|
style={{ width: `${width}px` }}
|
||||||
|
align="start"
|
||||||
>
|
>
|
||||||
<SelectValue placeholder={column?.columnLabel || "선택"} />
|
<div className="max-h-60 overflow-auto">
|
||||||
</SelectTrigger>
|
{uniqueOptions.length === 0 ? (
|
||||||
<SelectContent>
|
<div className="text-muted-foreground px-3 py-2 text-xs">옵션 없음</div>
|
||||||
{uniqueOptions.length === 0 ? (
|
) : (
|
||||||
<div className="text-muted-foreground px-2 py-1.5 text-xs">옵션 없음</div>
|
<div className="p-1">
|
||||||
) : (
|
{uniqueOptions.map((option, index) => (
|
||||||
uniqueOptions.map((option, index) => (
|
<div
|
||||||
<SelectItem key={`${filter.columnName}-${option.value}-${index}`} value={option.value}>
|
key={`${filter.columnName}-multi-${option.value}-${index}`}
|
||||||
{option.label}
|
className="flex items-center space-x-2 rounded-sm px-2 py-1.5 hover:bg-accent cursor-pointer"
|
||||||
</SelectItem>
|
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"
|
||||||
|
className="w-full h-7 text-xs"
|
||||||
|
onClick={() => handleFilterChange(filter.columnName, "")}
|
||||||
|
>
|
||||||
|
선택 초기화
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
)}
|
)}
|
||||||
</SelectContent>
|
</PopoverContent>
|
||||||
</Select>
|
</Popover>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -29,6 +29,7 @@ interface PresetFilter {
|
||||||
columnLabel: string;
|
columnLabel: string;
|
||||||
filterType: "text" | "number" | "date" | "select";
|
filterType: "text" | "number" | "date" | "select";
|
||||||
width?: number;
|
width?: number;
|
||||||
|
multiSelect?: boolean; // 다중선택 여부 (select 타입에서만 사용)
|
||||||
}
|
}
|
||||||
|
|
||||||
export function TableSearchWidgetConfigPanel({
|
export function TableSearchWidgetConfigPanel({
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue