일단 퇴사일 입력하면 필터링 적용되게 수정해놓음

This commit is contained in:
leeheejin 2026-01-12 17:25:12 +09:00
parent 31e87e0bca
commit c2836a0209
2 changed files with 422 additions and 319 deletions

View File

@ -6,7 +6,8 @@ import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label"; import { Label } from "@/components/ui/label";
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
import { Switch } from "@/components/ui/switch"; 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 { ColumnFilter, DataFilterConfig } from "@/types/screen-management";
import { UnifiedColumnInfo } from "@/types/table-management"; import { UnifiedColumnInfo } from "@/types/table-management";
import { getCategoryValues } from "@/lib/api/tableCategoryValue"; import { getCategoryValues } from "@/lib/api/tableCategoryValue";
@ -19,6 +20,67 @@ interface DataFilterConfigPanelProps {
menuObjid?: number; // 🆕 메뉴 OBJID (카테고리 값 조회 시 필요) menuObjid?: number; // 🆕 메뉴 OBJID (카테고리 값 조회 시 필요)
} }
/**
*
*/
interface FilterItemCollapsibleProps {
filter: ColumnFilter;
index: number;
filterSummary: string;
onRemove: () => void;
children: React.ReactNode;
}
const FilterItemCollapsible: React.FC<FilterItemCollapsibleProps> = ({
filter,
index,
filterSummary,
onRemove,
children,
}) => {
const [isOpen, setIsOpen] = useState(!filter.columnName); // 설정 안 된 필터는 열린 상태로
return (
<Collapsible open={isOpen} onOpenChange={setIsOpen}>
<div className="rounded-lg border p-2">
<CollapsibleTrigger asChild>
<div className="hover:bg-muted/50 cursor-pointer rounded p-1">
{/* 상단: 필터 번호 + 삭제 버튼 */}
<div className="flex items-center justify-between">
<div className="flex items-center gap-1">
{isOpen ? (
<ChevronDown className="text-muted-foreground h-3 w-3 shrink-0" />
) : (
<ChevronRight className="text-muted-foreground h-3 w-3 shrink-0" />
)}
<span className="text-muted-foreground text-xs font-medium"> {index + 1}</span>
</div>
<Button
variant="ghost"
size="sm"
className="h-5 w-5 shrink-0 p-0"
onClick={(e) => {
e.stopPropagation();
onRemove();
}}
>
<Trash2 className="h-3 w-3" />
</Button>
</div>
{/* 하단: 필터 요약 (전체 너비 사용) */}
<div className="mt-1 pl-4">
<span className="text-xs font-medium text-blue-600" title={filterSummary}>
{filterSummary}
</span>
</div>
</div>
</CollapsibleTrigger>
<CollapsibleContent className="space-y-2 pt-2">{children}</CollapsibleContent>
</div>
</Collapsible>
);
};
/** /**
* *
* , , * , ,
@ -36,13 +98,13 @@ export function DataFilterConfigPanel({
menuObjid, menuObjid,
sampleColumns: columns.slice(0, 3), sampleColumns: columns.slice(0, 3),
}); });
const [localConfig, setLocalConfig] = useState<DataFilterConfig>( const [localConfig, setLocalConfig] = useState<DataFilterConfig>(
config || { config || {
enabled: false, enabled: false,
filters: [], filters: [],
matchType: "all", matchType: "all",
} },
); );
// 카테고리 값 캐시 (컬럼명 -> 카테고리 값 목록) // 카테고리 값 캐시 (컬럼명 -> 카테고리 값 목록)
@ -52,7 +114,7 @@ export function DataFilterConfigPanel({
useEffect(() => { useEffect(() => {
if (config) { if (config) {
setLocalConfig(config); setLocalConfig(config);
// 🆕 기존 필터 중 카테고리 타입인 것들의 값을 로드 // 🆕 기존 필터 중 카테고리 타입인 것들의 값을 로드
config.filters?.forEach((filter) => { config.filters?.forEach((filter) => {
if (filter.valueType === "category" && filter.columnName) { if (filter.valueType === "category" && filter.columnName) {
@ -69,7 +131,7 @@ export function DataFilterConfigPanel({
return; // 이미 로드되었거나 로딩 중이면 스킵 return; // 이미 로드되었거나 로딩 중이면 스킵
} }
setLoadingCategories(prev => ({ ...prev, [columnName]: true })); setLoadingCategories((prev) => ({ ...prev, [columnName]: true }));
try { try {
console.log("🔍 카테고리 값 로드 시작:", { console.log("🔍 카테고리 값 로드 시작:", {
@ -82,7 +144,7 @@ export function DataFilterConfigPanel({
tableName, tableName,
columnName, columnName,
false, // includeInactive false, // includeInactive
menuObjid // 🆕 메뉴 OBJID 전달 menuObjid, // 🆕 메뉴 OBJID 전달
); );
console.log("📦 카테고리 값 로드 응답:", response); console.log("📦 카테고리 값 로드 응답:", response);
@ -92,16 +154,16 @@ export function DataFilterConfigPanel({
value: item.valueCode, value: item.valueCode,
label: item.valueLabel, label: item.valueLabel,
})); }));
console.log("✅ 카테고리 값 설정:", { columnName, valuesCount: values.length }); console.log("✅ 카테고리 값 설정:", { columnName, valuesCount: values.length });
setCategoryValues(prev => ({ ...prev, [columnName]: values })); setCategoryValues((prev) => ({ ...prev, [columnName]: values }));
} else { } else {
console.warn("⚠️ 카테고리 값 로드 실패 또는 데이터 없음:", response); console.warn("⚠️ 카테고리 값 로드 실패 또는 데이터 없음:", response);
} }
} catch (error) { } catch (error) {
console.error(`❌ 카테고리 값 로드 실패 (${columnName}):`, error); console.error(`❌ 카테고리 값 로드 실패 (${columnName}):`, error);
} finally { } 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 handleFilterChange = (filterId: string, field: keyof ColumnFilter, value: any) => {
const newConfig = { const newConfig = {
...localConfig, ...localConfig,
filters: localConfig.filters.map((filter) => filters: localConfig.filters.map((filter) => (filter.id === filterId ? { ...filter, [field]: value } : filter)),
filter.id === filterId ? { ...filter, [field]: value } : filter
),
}; };
setLocalConfig(newConfig); setLocalConfig(newConfig);
onConfigChange(newConfig); onConfigChange(newConfig);
@ -178,7 +238,7 @@ export function DataFilterConfigPanel({
<> <>
{/* 테이블명 표시 */} {/* 테이블명 표시 */}
{tableName && ( {tableName && (
<div className="text-xs text-muted-foreground"> <div className="text-muted-foreground text-xs">
: <span className="font-medium">{tableName}</span> : <span className="font-medium">{tableName}</span>
</div> </div>
)} )}
@ -200,235 +260,127 @@ export function DataFilterConfigPanel({
)} )}
{/* 필터 목록 */} {/* 필터 목록 */}
<div className="space-y-3 max-h-[600px] overflow-y-auto pr-2"> <div className="max-h-[600px] space-y-2 overflow-y-auto pr-2">
{localConfig.filters.map((filter, index) => ( {localConfig.filters.map((filter, index) => {
<div key={filter.id} className="rounded-lg border p-3 space-y-2"> // 연산자 표시 텍스트
<div className="flex items-center justify-between mb-2"> const operatorLabels: Record<string, string> = {
<span className="text-xs font-medium text-muted-foreground"> equals: "=",
{index + 1} not_equals: "!=",
</span> greater_than: ">",
<Button less_than: "<",
variant="ghost" greater_than_or_equal: ">=",
size="sm" less_than_or_equal: "<=",
className="h-6 w-6 p-0" between: "BETWEEN",
onClick={() => handleRemoveFilter(filter.id)} in: "IN",
> not_in: "NOT IN",
<Trash2 className="h-3 w-3" /> contains: "LIKE",
</Button> starts_with: "시작",
</div> ends_with: "끝",
is_null: "IS NULL",
is_not_null: "IS NOT NULL",
date_range_contains: "기간 내",
};
{/* 컬럼 선택 (날짜 범위 포함이 아닐 때만 표시) */} // 컬럼 라벨 찾기
{filter.operator !== "date_range_contains" && ( const columnLabel =
<div> columns.find((c) => c.columnName === filter.columnName)?.columnLabel || filter.columnName;
<Label className="text-xs"></Label>
<Select
value={filter.columnName}
onValueChange={(value) => {
const column = columns.find((col) => col.columnName === value);
console.log("🔍 컬럼 선택:", {
columnName: value,
input_type: column?.input_type,
column,
});
// 컬럼 타입에 따라 valueType 자동 설정
let valueType: "static" | "category" | "code" = "static";
if (column?.input_type === "category") {
valueType = "category";
console.log("📦 카테고리 컬럼 감지, 값 로딩 시작:", value);
loadCategoryValues(value); // 카테고리 값 로드
} else if (column?.input_type === "code") {
valueType = "code";
}
// 한 번에 모든 변경사항 적용
const newConfig = {
...localConfig,
filters: localConfig.filters.map((f) =>
f.id === filter.id
? { ...f, columnName: value, valueType, value: "" }
: f
),
};
console.log("✅ 필터 설정 업데이트:", {
filterId: filter.id,
columnName: value,
valueType,
newConfig,
});
setLocalConfig(newConfig);
onConfigChange(newConfig);
}}
>
<SelectTrigger className="h-8 text-xs sm:h-10 sm:text-sm">
<SelectValue placeholder="컬럼 선택" />
</SelectTrigger>
<SelectContent>
{columns.map((col) => (
<SelectItem key={col.columnName} value={col.columnName}>
{col.columnLabel || col.columnName}
{(col.input_type === "category" || col.input_type === "code") && (
<span className="ml-2 text-xs text-muted-foreground">
({col.input_type})
</span>
)}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
)}
{/* 연산자 선택 */} // 필터 요약 텍스트 생성
<div> const filterSummary = filter.columnName
<Label className="text-xs"></Label> ? `${columnLabel} ${operatorLabels[filter.operator] || filter.operator}${
<Select filter.operator !== "is_null" && filter.operator !== "is_not_null" && filter.value
value={filter.operator} ? ` ${filter.value}`
onValueChange={(value: any) => { : ""
// date_range_contains 선택 시 한 번에 모든 변경사항 적용 }`
if (value === "date_range_contains") { : "설정 필요";
const newConfig = {
...localConfig,
filters: localConfig.filters.map((f) =>
f.id === filter.id
? { ...f, operator: value, valueType: "dynamic", value: "TODAY" }
: f
),
};
setLocalConfig(newConfig);
onConfigChange(newConfig);
} else {
handleFilterChange(filter.id, "operator", value);
}
}}
>
<SelectTrigger className="h-8 text-xs sm:h-10 sm:text-sm">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="equals"> (=)</SelectItem>
<SelectItem value="not_equals"> ()</SelectItem>
<SelectItem value="greater_than"> (&gt;)</SelectItem>
<SelectItem value="less_than"> (&lt;)</SelectItem>
<SelectItem value="greater_than_or_equal"> ()</SelectItem>
<SelectItem value="less_than_or_equal"> ()</SelectItem>
<SelectItem value="between"> (BETWEEN)</SelectItem>
<SelectItem value="in"> (IN)</SelectItem>
<SelectItem value="not_in"> (NOT IN)</SelectItem>
<SelectItem value="contains"> (LIKE %value%)</SelectItem>
<SelectItem value="starts_with"> (LIKE value%)</SelectItem>
<SelectItem value="ends_with"> (LIKE %value)</SelectItem>
<SelectItem value="is_null">NULL</SelectItem>
<SelectItem value="is_not_null">NOT NULL</SelectItem>
<SelectItem value="date_range_contains"> ( )</SelectItem>
</SelectContent>
</Select>
</div>
{/* 날짜 범위 포함 - 시작일/종료일 컬럼 선택 */} return (
{filter.operator === "date_range_contains" && ( <FilterItemCollapsible
<> key={filter.id}
<div className="col-span-2"> filter={filter}
<p className="text-xs text-muted-foreground bg-muted/50 p-2 rounded"> index={index}
💡 : filterSummary={filterSummary}
<br /> NULL onRemove={() => handleRemoveFilter(filter.id)}
<br /> NULL >
<br /> {/* 컬럼 선택 (날짜 범위 포함이 아닐 때만 표시) */}
</p> {filter.operator !== "date_range_contains" && (
</div>
<div> <div>
<Label className="text-xs"> </Label> <Label className="text-xs"></Label>
<Select <Select
value={filter.rangeConfig?.startColumn || ""} value={filter.columnName}
onValueChange={(value) => { onValueChange={(value) => {
const newRangeConfig = { const column = columns.find((col) => col.columnName === value);
...filter.rangeConfig,
startColumn: value,
endColumn: filter.rangeConfig?.endColumn || "",
};
handleFilterChange(filter.id, "rangeConfig", newRangeConfig);
}}
>
<SelectTrigger className="h-8 text-xs sm:h-10 sm:text-sm">
<SelectValue placeholder="시작일 컬럼 선택" />
</SelectTrigger>
<SelectContent>
{columns.filter(col =>
col.dataType?.toLowerCase().includes('date') ||
col.dataType?.toLowerCase().includes('time')
).map((col) => (
<SelectItem key={col.columnName} value={col.columnName}>
{col.columnLabel || col.columnName}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
<div>
<Label className="text-xs"> </Label>
<Select
value={filter.rangeConfig?.endColumn || ""}
onValueChange={(value) => {
const newRangeConfig = {
...filter.rangeConfig,
startColumn: filter.rangeConfig?.startColumn || "",
endColumn: value,
};
handleFilterChange(filter.id, "rangeConfig", newRangeConfig);
}}
>
<SelectTrigger className="h-8 text-xs sm:h-10 sm:text-sm">
<SelectValue placeholder="종료일 컬럼 선택" />
</SelectTrigger>
<SelectContent>
{columns.filter(col =>
col.dataType?.toLowerCase().includes('date') ||
col.dataType?.toLowerCase().includes('time')
).map((col) => (
<SelectItem key={col.columnName} value={col.columnName}>
{col.columnLabel || col.columnName}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
</>
)}
{/* 값 타입 선택 (카테고리/코드 컬럼 또는 date_range_contains) */} console.log("🔍 컬럼 선택:", {
{(isCategoryOrCodeColumn(filter.columnName) || filter.operator === "date_range_contains") && ( columnName: value,
<div> input_type: column?.input_type,
<Label className="text-xs"> </Label> column,
<Select });
value={filter.valueType}
onValueChange={(value: any) => { // 컬럼 타입에 따라 valueType 자동 설정
// dynamic 선택 시 한 번에 valueType과 value를 설정 let valueType: "static" | "category" | "code" = "static";
if (value === "dynamic" && filter.operator === "date_range_contains") { if (column?.input_type === "category") {
valueType = "category";
console.log("📦 카테고리 컬럼 감지, 값 로딩 시작:", value);
loadCategoryValues(value); // 카테고리 값 로드
} else if (column?.input_type === "code") {
valueType = "code";
}
// 한 번에 모든 변경사항 적용
const newConfig = { const newConfig = {
...localConfig, ...localConfig,
filters: localConfig.filters.map((f) => filters: localConfig.filters.map((f) =>
f.id === filter.id f.id === filter.id ? { ...f, columnName: value, valueType, value: "" } : f,
? { ...f, valueType: value, value: "TODAY" } ),
: f };
console.log("✅ 필터 설정 업데이트:", {
filterId: filter.id,
columnName: value,
valueType,
newConfig,
});
setLocalConfig(newConfig);
onConfigChange(newConfig);
}}
>
<SelectTrigger className="h-8 text-xs sm:h-10 sm:text-sm">
<SelectValue placeholder="컬럼 선택" />
</SelectTrigger>
<SelectContent>
{columns.map((col) => (
<SelectItem key={col.columnName} value={col.columnName}>
{col.columnLabel || col.columnName}
{(col.input_type === "category" || col.input_type === "code") && (
<span className="text-muted-foreground ml-2 text-xs">({col.input_type})</span>
)}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
)}
{/* 연산자 선택 */}
<div>
<Label className="text-xs"></Label>
<Select
value={filter.operator}
onValueChange={(value: any) => {
// date_range_contains 선택 시 한 번에 모든 변경사항 적용
if (value === "date_range_contains") {
const newConfig = {
...localConfig,
filters: localConfig.filters.map((f) =>
f.id === filter.id ? { ...f, operator: value, valueType: "dynamic", value: "TODAY" } : f,
), ),
}; };
setLocalConfig(newConfig); setLocalConfig(newConfig);
onConfigChange(newConfig); onConfigChange(newConfig);
} else { } else {
// static이나 다른 타입은 value를 빈 문자열로 초기화 handleFilterChange(filter.id, "operator", value);
const newConfig = {
...localConfig,
filters: localConfig.filters.map((f) =>
f.id === filter.id
? { ...f, valueType: value, value: "" }
: f
),
};
setLocalConfig(newConfig);
onConfigChange(newConfig);
} }
}} }}
> >
@ -436,106 +388,240 @@ export function DataFilterConfigPanel({
<SelectValue /> <SelectValue />
</SelectTrigger> </SelectTrigger>
<SelectContent> <SelectContent>
<SelectItem value="static"> </SelectItem> <SelectItem value="equals"> (=)</SelectItem>
{filter.operator === "date_range_contains" && ( <SelectItem value="not_equals"> ()</SelectItem>
<SelectItem value="dynamic"> ( )</SelectItem> <SelectItem value="greater_than"> (&gt;)</SelectItem>
)} <SelectItem value="less_than"> (&lt;)</SelectItem>
{isCategoryOrCodeColumn(filter.columnName) && ( <SelectItem value="greater_than_or_equal"> ()</SelectItem>
<> <SelectItem value="less_than_or_equal"> ()</SelectItem>
<SelectItem value="category"> </SelectItem> <SelectItem value="between"> (BETWEEN)</SelectItem>
<SelectItem value="code"> </SelectItem> <SelectItem value="in"> (IN)</SelectItem>
</> <SelectItem value="not_in"> (NOT IN)</SelectItem>
)} <SelectItem value="contains"> (LIKE %value%)</SelectItem>
<SelectItem value="starts_with"> (LIKE value%)</SelectItem>
<SelectItem value="ends_with"> (LIKE %value)</SelectItem>
<SelectItem value="is_null">NULL</SelectItem>
<SelectItem value="is_not_null">NOT NULL</SelectItem>
<SelectItem value="date_range_contains"> ( )</SelectItem>
</SelectContent> </SelectContent>
</Select> </Select>
</div> </div>
)}
{/* 값 입력 (NULL 체크 및 date_range_contains의 dynamic 제외) */} {/* 날짜 범위 포함 - 시작일/종료일 컬럼 선택 */}
{filter.operator !== "is_null" && {filter.operator === "date_range_contains" && (
filter.operator !== "is_not_null" && <>
!(filter.operator === "date_range_contains" && filter.valueType === "dynamic") && ( <div className="col-span-2">
<div> <p className="text-muted-foreground bg-muted/50 rounded p-2 text-xs">
<Label className="text-xs"></Label> 💡 :
{/* 카테고리 타입이고 값 타입이 category인 경우 셀렉트박스 */} <br /> NULL
{filter.valueType === "category" && categoryValues[filter.columnName] ? ( <br /> NULL
<br />
</p>
</div>
<div>
<Label className="text-xs"> </Label>
<Select
value={filter.rangeConfig?.startColumn || ""}
onValueChange={(value) => {
const newRangeConfig = {
...filter.rangeConfig,
startColumn: value,
endColumn: filter.rangeConfig?.endColumn || "",
};
handleFilterChange(filter.id, "rangeConfig", newRangeConfig);
}}
>
<SelectTrigger className="h-8 text-xs sm:h-10 sm:text-sm">
<SelectValue placeholder="시작일 컬럼 선택" />
</SelectTrigger>
<SelectContent>
{columns
.filter(
(col) =>
col.dataType?.toLowerCase().includes("date") ||
col.dataType?.toLowerCase().includes("time"),
)
.map((col) => (
<SelectItem key={col.columnName} value={col.columnName}>
{col.columnLabel || col.columnName}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
<div>
<Label className="text-xs"> </Label>
<Select
value={filter.rangeConfig?.endColumn || ""}
onValueChange={(value) => {
const newRangeConfig = {
...filter.rangeConfig,
startColumn: filter.rangeConfig?.startColumn || "",
endColumn: value,
};
handleFilterChange(filter.id, "rangeConfig", newRangeConfig);
}}
>
<SelectTrigger className="h-8 text-xs sm:h-10 sm:text-sm">
<SelectValue placeholder="종료일 컬럼 선택" />
</SelectTrigger>
<SelectContent>
{columns
.filter(
(col) =>
col.dataType?.toLowerCase().includes("date") ||
col.dataType?.toLowerCase().includes("time"),
)
.map((col) => (
<SelectItem key={col.columnName} value={col.columnName}>
{col.columnLabel || col.columnName}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
</>
)}
{/* 값 타입 선택 (카테고리/코드 컬럼 또는 date_range_contains) */}
{(isCategoryOrCodeColumn(filter.columnName) || filter.operator === "date_range_contains") && (
<div>
<Label className="text-xs"> </Label>
<Select <Select
value={Array.isArray(filter.value) ? filter.value[0] : filter.value} value={filter.valueType}
onValueChange={(value) => handleFilterChange(filter.id, "value", value)} onValueChange={(value: any) => {
// dynamic 선택 시 한 번에 valueType과 value를 설정
if (value === "dynamic" && filter.operator === "date_range_contains") {
const newConfig = {
...localConfig,
filters: localConfig.filters.map((f) =>
f.id === filter.id ? { ...f, valueType: value, value: "TODAY" } : f,
),
};
setLocalConfig(newConfig);
onConfigChange(newConfig);
} else {
// static이나 다른 타입은 value를 빈 문자열로 초기화
const newConfig = {
...localConfig,
filters: localConfig.filters.map((f) =>
f.id === filter.id ? { ...f, valueType: value, value: "" } : f,
),
};
setLocalConfig(newConfig);
onConfigChange(newConfig);
}
}}
> >
<SelectTrigger className="h-8 text-xs sm:h-10 sm:text-sm"> <SelectTrigger className="h-8 text-xs sm:h-10 sm:text-sm">
<SelectValue placeholder={ <SelectValue />
loadingCategories[filter.columnName] ? "로딩 중..." : "값 선택"
} />
</SelectTrigger> </SelectTrigger>
<SelectContent> <SelectContent>
{categoryValues[filter.columnName].map((option) => ( <SelectItem value="static"> </SelectItem>
<SelectItem key={option.value} value={option.value}> {filter.operator === "date_range_contains" && (
{option.label} <SelectItem value="dynamic"> ( )</SelectItem>
</SelectItem> )}
))} {isCategoryOrCodeColumn(filter.columnName) && (
<>
<SelectItem value="category"> </SelectItem>
<SelectItem value="code"> </SelectItem>
</>
)}
</SelectContent> </SelectContent>
</Select> </Select>
) : filter.operator === "in" || filter.operator === "not_in" ? ( </div>
<Input )}
value={Array.isArray(filter.value) ? filter.value.join(", ") : filter.value}
onChange={(e) => { {/* 값 입력 (NULL 체크 및 date_range_contains의 dynamic 제외) */}
const values = e.target.value.split(",").map((v) => v.trim()); {filter.operator !== "is_null" &&
handleFilterChange(filter.id, "value", values); filter.operator !== "is_not_null" &&
}} !(filter.operator === "date_range_contains" && filter.valueType === "dynamic") && (
placeholder="쉼표로 구분 (예: 값1, 값2, 값3)" <div>
className="h-8 text-xs sm:h-10 sm:text-sm" <Label className="text-xs"></Label>
/> {/* 카테고리 타입이고 값 타입이 category인 경우 셀렉트박스 */}
) : filter.operator === "between" ? ( {filter.valueType === "category" && categoryValues[filter.columnName] ? (
<Input <Select
value={Array.isArray(filter.value) ? filter.value.join(" ~ ") : filter.value} value={Array.isArray(filter.value) ? filter.value[0] : filter.value}
onChange={(e) => { onValueChange={(value) => handleFilterChange(filter.id, "value", value)}
const values = e.target.value.split("~").map((v) => v.trim()); >
handleFilterChange(filter.id, "value", values.length === 2 ? values : [values[0] || "", ""]); <SelectTrigger className="h-8 text-xs sm:h-10 sm:text-sm">
}} <SelectValue
placeholder="시작 ~ 종료 (예: 2025-01-01 ~ 2025-12-31)" placeholder={loadingCategories[filter.columnName] ? "로딩 중..." : "값 선택"}
className="h-8 text-xs sm:h-10 sm:text-sm" />
/> </SelectTrigger>
) : ( <SelectContent>
<Input {categoryValues[filter.columnName].map((option) => (
type={filter.operator === "date_range_contains" ? "date" : "text"} <SelectItem key={option.value} value={option.value}>
value={Array.isArray(filter.value) ? filter.value[0] || "" : filter.value} {option.label}
onChange={(e) => handleFilterChange(filter.id, "value", e.target.value)} </SelectItem>
placeholder={filter.operator === "date_range_contains" ? "비교할 날짜 선택" : "필터 값 입력"} ))}
className="h-8 text-xs sm:h-10 sm:text-sm" </SelectContent>
/> </Select>
) : filter.operator === "in" || filter.operator === "not_in" ? (
<Input
value={Array.isArray(filter.value) ? filter.value.join(", ") : filter.value}
onChange={(e) => {
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" ? (
<Input
value={Array.isArray(filter.value) ? filter.value.join(" ~ ") : filter.value}
onChange={(e) => {
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"
/>
) : (
<Input
type={filter.operator === "date_range_contains" ? "date" : "text"}
value={Array.isArray(filter.value) ? filter.value[0] || "" : filter.value}
onChange={(e) => handleFilterChange(filter.id, "value", e.target.value)}
placeholder={
filter.operator === "date_range_contains" ? "비교할 날짜 선택" : "필터 값 입력"
}
className="h-8 text-xs sm:h-10 sm:text-sm"
/>
)}
<p className="text-muted-foreground mt-1 text-[10px]">
{filter.valueType === "category" && categoryValues[filter.columnName]
? "카테고리 값을 선택하세요"
: filter.operator === "in" || filter.operator === "not_in"
? "여러 값은 쉼표(,)로 구분하세요"
: filter.operator === "between"
? "시작과 종료 값을 ~로 구분하세요"
: filter.operator === "date_range_contains"
? "기간 내에 포함되는지 확인할 날짜를 선택하세요"
: "필터링할 값을 입력하세요"}
</p>
</div>
)} )}
<p className="text-[10px] text-muted-foreground mt-1">
{filter.valueType === "category" && categoryValues[filter.columnName] {/* date_range_contains의 dynamic 타입 안내 */}
? "카테고리 값을 선택하세요" {filter.operator === "date_range_contains" && filter.valueType === "dynamic" && (
: filter.operator === "in" || filter.operator === "not_in" <div className="rounded-md bg-blue-50 p-2">
? "여러 값은 쉼표(,)로 구분하세요" <p className="text-[10px] text-blue-700"> .</p>
: filter.operator === "between" </div>
? "시작과 종료 값을 ~로 구분하세요" )}
: filter.operator === "date_range_contains" </FilterItemCollapsible>
? "기간 내에 포함되는지 확인할 날짜를 선택하세요" );
: "필터링할 값을 입력하세요"} })}
</p>
</div>
)}
{/* date_range_contains의 dynamic 타입 안내 */}
{filter.operator === "date_range_contains" && filter.valueType === "dynamic" && (
<div className="rounded-md bg-blue-50 p-2">
<p className="text-[10px] text-blue-700">
.
</p>
</div>
)}
</div>
))}
</div> </div>
{/* 필터 추가 버튼 */} {/* 필터 추가 버튼 */}
<Button <Button
variant="outline" variant="outline"
size="sm" size="sm"
className="w-full h-8 text-xs sm:h-10 sm:text-sm" className="h-8 w-full text-xs sm:h-10 sm:text-sm"
onClick={handleAddFilter} onClick={handleAddFilter}
disabled={columns.length === 0} disabled={columns.length === 0}
> >
@ -544,13 +630,10 @@ export function DataFilterConfigPanel({
</Button> </Button>
{columns.length === 0 && ( {columns.length === 0 && (
<p className="text-xs text-muted-foreground text-center"> <p className="text-muted-foreground text-center text-xs"> </p>
</p>
)} )}
</> </>
)} )}
</div> </div>
); );
} }

View File

@ -1014,10 +1014,14 @@ export const SplitPanelLayoutComponent: React.FC<SplitPanelLayoutComponentProps>
// 추가 dataFilter 적용 // 추가 dataFilter 적용
let filteredData = result.data || []; let filteredData = result.data || [];
const dataFilter = componentConfig.rightPanel?.dataFilter; 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) => { filteredData = filteredData.filter((item: any) => {
return dataFilter.conditions.every((cond: any) => { return filterConditions.every((cond: any) => {
const value = item[cond.column]; // columnName 또는 column 지원
const columnName = cond.columnName || cond.column;
const value = item[columnName];
const condValue = cond.value; const condValue = cond.value;
switch (cond.operator) { switch (cond.operator) {
case "equals": case "equals":
@ -1026,6 +1030,12 @@ export const SplitPanelLayoutComponent: React.FC<SplitPanelLayoutComponentProps>
return value !== condValue; return value !== condValue;
case "contains": case "contains":
return String(value).includes(String(condValue)); 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: default:
return true; return true;
} }
@ -1138,10 +1148,14 @@ export const SplitPanelLayoutComponent: React.FC<SplitPanelLayoutComponentProps>
// 데이터 필터 적용 // 데이터 필터 적용
const dataFilter = tabConfig.dataFilter; 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) => { resultData = resultData.filter((item: any) => {
return dataFilter.conditions.every((cond: any) => { return filterConditions.every((cond: any) => {
const value = item[cond.column]; // columnName 또는 column 지원
const columnName = cond.columnName || cond.column;
const value = item[columnName];
const condValue = cond.value; const condValue = cond.value;
switch (cond.operator) { switch (cond.operator) {
case "equals": case "equals":
@ -1150,6 +1164,12 @@ export const SplitPanelLayoutComponent: React.FC<SplitPanelLayoutComponentProps>
return value !== condValue; return value !== condValue;
case "contains": case "contains":
return String(value).includes(String(condValue)); 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: default:
return true; return true;
} }