검색필터 다중선택 기능
This commit is contained in:
parent
142fb15dc0
commit
a3d3db5437
|
|
@ -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} 체크
|
||||
|
|
|
|||
|
|
@ -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 (
|
||||
<Select
|
||||
value={value}
|
||||
onValueChange={(val) => {
|
||||
// 선택한 값의 라벨 저장
|
||||
const selectedOption = uniqueOptions.find((opt) => opt.value === val);
|
||||
if (selectedOption) {
|
||||
setSelectedLabels((prev) => ({
|
||||
...prev,
|
||||
[filter.columnName]: selectedOption.label,
|
||||
}));
|
||||
}
|
||||
handleFilterChange(filter.columnName, val);
|
||||
}}
|
||||
>
|
||||
<SelectTrigger
|
||||
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"
|
||||
style={{ width: `${width}px`, height: "36px", minHeight: "36px", outline: "none", boxShadow: "none" }}
|
||||
<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",
|
||||
selectedValues.length === 0 && "text-muted-foreground"
|
||||
)}
|
||||
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>
|
||||
<PopoverContent
|
||||
className="p-0"
|
||||
style={{ width: `${width}px` }}
|
||||
align="start"
|
||||
>
|
||||
<SelectValue placeholder={column?.columnLabel || "선택"} />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{uniqueOptions.length === 0 ? (
|
||||
<div className="text-muted-foreground px-2 py-1.5 text-xs">옵션 없음</div>
|
||||
) : (
|
||||
uniqueOptions.map((option, index) => (
|
||||
<SelectItem key={`${filter.columnName}-${option.value}-${index}`} value={option.value}>
|
||||
{option.label}
|
||||
</SelectItem>
|
||||
))
|
||||
<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}`}
|
||||
className="flex items-center space-x-2 rounded-sm px-2 py-1.5 hover:bg-accent cursor-pointer"
|
||||
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>
|
||||
</Select>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -29,6 +29,7 @@ interface PresetFilter {
|
|||
columnLabel: string;
|
||||
filterType: "text" | "number" | "date" | "select";
|
||||
width?: number;
|
||||
multiSelect?: boolean; // 다중선택 여부 (select 타입에서만 사용)
|
||||
}
|
||||
|
||||
export function TableSearchWidgetConfigPanel({
|
||||
|
|
|
|||
Loading…
Reference in New Issue