일단 퇴사일 입력하면 필터링 적용되게 수정해놓음
This commit is contained in:
parent
31e87e0bca
commit
c2836a0209
|
|
@ -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<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>
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* 데이터 필터 설정 패널
|
||||
* 테이블 리스트, 분할 패널, 플로우 위젯 등에서 사용
|
||||
|
|
@ -42,7 +104,7 @@ export function DataFilterConfigPanel({
|
|||
enabled: false,
|
||||
filters: [],
|
||||
matchType: "all",
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
// 카테고리 값 캐시 (컬럼명 -> 카테고리 값 목록)
|
||||
|
|
@ -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);
|
||||
|
|
@ -94,14 +156,14 @@ export function DataFilterConfigPanel({
|
|||
}));
|
||||
|
||||
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 && (
|
||||
<div className="text-xs text-muted-foreground">
|
||||
<div className="text-muted-foreground text-xs">
|
||||
테이블: <span className="font-medium">{tableName}</span>
|
||||
</div>
|
||||
)}
|
||||
|
|
@ -200,235 +260,127 @@ export function DataFilterConfigPanel({
|
|||
)}
|
||||
|
||||
{/* 필터 목록 */}
|
||||
<div className="space-y-3 max-h-[600px] overflow-y-auto pr-2">
|
||||
{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">
|
||||
<span className="text-xs font-medium text-muted-foreground">
|
||||
필터 {index + 1}
|
||||
</span>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className="h-6 w-6 p-0"
|
||||
onClick={() => handleRemoveFilter(filter.id)}
|
||||
>
|
||||
<Trash2 className="h-3 w-3" />
|
||||
</Button>
|
||||
</div>
|
||||
<div className="max-h-[600px] space-y-2 overflow-y-auto pr-2">
|
||||
{localConfig.filters.map((filter, index) => {
|
||||
// 연산자 표시 텍스트
|
||||
const operatorLabels: Record<string, string> = {
|
||||
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" && (
|
||||
<div>
|
||||
<Label className="text-xs">컬럼</Label>
|
||||
<Select
|
||||
value={filter.columnName}
|
||||
onValueChange={(value) => {
|
||||
const column = columns.find((col) => col.columnName === value);
|
||||
// 컬럼 라벨 찾기
|
||||
const columnLabel =
|
||||
columns.find((c) => c.columnName === filter.columnName)?.columnLabel || filter.columnName;
|
||||
|
||||
console.log("🔍 컬럼 선택:", {
|
||||
columnName: value,
|
||||
input_type: column?.input_type,
|
||||
column,
|
||||
});
|
||||
// 필터 요약 텍스트 생성
|
||||
const filterSummary = filter.columnName
|
||||
? `${columnLabel} ${operatorLabels[filter.operator] || filter.operator}${
|
||||
filter.operator !== "is_null" && filter.operator !== "is_not_null" && filter.value
|
||||
? ` ${filter.value}`
|
||||
: ""
|
||||
}`
|
||||
: "설정 필요";
|
||||
|
||||
// 컬럼 타입에 따라 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>
|
||||
<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);
|
||||
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">크다 (>)</SelectItem>
|
||||
<SelectItem value="less_than">작다 (<)</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>
|
||||
|
||||
{/* 날짜 범위 포함 - 시작일/종료일 컬럼 선택 */}
|
||||
{filter.operator === "date_range_contains" && (
|
||||
<>
|
||||
<div className="col-span-2">
|
||||
<p className="text-xs text-muted-foreground bg-muted/50 p-2 rounded">
|
||||
💡 날짜 범위 필터링 규칙:
|
||||
<br />• 시작일만 있고 종료일이 NULL → 시작일 이후 모든 데이터
|
||||
<br />• 종료일만 있고 시작일이 NULL → 종료일 이전 모든 데이터
|
||||
<br />• 둘 다 있으면 → 기간 내 데이터만
|
||||
</p>
|
||||
</div>
|
||||
return (
|
||||
<FilterItemCollapsible
|
||||
key={filter.id}
|
||||
filter={filter}
|
||||
index={index}
|
||||
filterSummary={filterSummary}
|
||||
onRemove={() => handleRemoveFilter(filter.id)}
|
||||
>
|
||||
{/* 컬럼 선택 (날짜 범위 포함이 아닐 때만 표시) */}
|
||||
{filter.operator !== "date_range_contains" && (
|
||||
<div>
|
||||
<Label className="text-xs">시작일 컬럼</Label>
|
||||
<Label className="text-xs">컬럼</Label>
|
||||
<Select
|
||||
value={filter.rangeConfig?.startColumn || ""}
|
||||
value={filter.columnName}
|
||||
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>
|
||||
</>
|
||||
)}
|
||||
const column = columns.find((col) => col.columnName === value);
|
||||
|
||||
{/* 값 타입 선택 (카테고리/코드 컬럼 또는 date_range_contains) */}
|
||||
{(isCategoryOrCodeColumn(filter.columnName) || filter.operator === "date_range_contains") && (
|
||||
<div>
|
||||
<Label className="text-xs">값 타입</Label>
|
||||
<Select
|
||||
value={filter.valueType}
|
||||
onValueChange={(value: any) => {
|
||||
// dynamic 선택 시 한 번에 valueType과 value를 설정
|
||||
if (value === "dynamic" && filter.operator === "date_range_contains") {
|
||||
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, valueType: value, value: "TODAY" }
|
||||
: 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="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);
|
||||
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);
|
||||
handleFilterChange(filter.id, "operator", value);
|
||||
}
|
||||
}}
|
||||
>
|
||||
|
|
@ -436,106 +388,240 @@ export function DataFilterConfigPanel({
|
|||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="static">직접 입력</SelectItem>
|
||||
{filter.operator === "date_range_contains" && (
|
||||
<SelectItem value="dynamic">동적 값 (오늘 날짜)</SelectItem>
|
||||
)}
|
||||
{isCategoryOrCodeColumn(filter.columnName) && (
|
||||
<>
|
||||
<SelectItem value="category">카테고리 선택</SelectItem>
|
||||
<SelectItem value="code">코드 선택</SelectItem>
|
||||
</>
|
||||
)}
|
||||
<SelectItem value="equals">같음 (=)</SelectItem>
|
||||
<SelectItem value="not_equals">같지 않음 (≠)</SelectItem>
|
||||
<SelectItem value="greater_than">크다 (>)</SelectItem>
|
||||
<SelectItem value="less_than">작다 (<)</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>
|
||||
)}
|
||||
|
||||
{/* 값 입력 (NULL 체크 및 date_range_contains의 dynamic 제외) */}
|
||||
{filter.operator !== "is_null" &&
|
||||
filter.operator !== "is_not_null" &&
|
||||
!(filter.operator === "date_range_contains" && filter.valueType === "dynamic") && (
|
||||
<div>
|
||||
<Label className="text-xs">값</Label>
|
||||
{/* 카테고리 타입이고 값 타입이 category인 경우 셀렉트박스 */}
|
||||
{filter.valueType === "category" && categoryValues[filter.columnName] ? (
|
||||
{/* 날짜 범위 포함 - 시작일/종료일 컬럼 선택 */}
|
||||
{filter.operator === "date_range_contains" && (
|
||||
<>
|
||||
<div className="col-span-2">
|
||||
<p className="text-muted-foreground bg-muted/50 rounded p-2 text-xs">
|
||||
💡 날짜 범위 필터링 규칙:
|
||||
<br />• 시작일만 있고 종료일이 NULL → 시작일 이후 모든 데이터
|
||||
<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
|
||||
value={Array.isArray(filter.value) ? filter.value[0] : filter.value}
|
||||
onValueChange={(value) => handleFilterChange(filter.id, "value", value)}
|
||||
value={filter.valueType}
|
||||
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">
|
||||
<SelectValue placeholder={
|
||||
loadingCategories[filter.columnName] ? "로딩 중..." : "값 선택"
|
||||
} />
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{categoryValues[filter.columnName].map((option) => (
|
||||
<SelectItem key={option.value} value={option.value}>
|
||||
{option.label}
|
||||
</SelectItem>
|
||||
))}
|
||||
<SelectItem value="static">직접 입력</SelectItem>
|
||||
{filter.operator === "date_range_contains" && (
|
||||
<SelectItem value="dynamic">동적 값 (오늘 날짜)</SelectItem>
|
||||
)}
|
||||
{isCategoryOrCodeColumn(filter.columnName) && (
|
||||
<>
|
||||
<SelectItem value="category">카테고리 선택</SelectItem>
|
||||
<SelectItem value="code">코드 선택</SelectItem>
|
||||
</>
|
||||
)}
|
||||
</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-[10px] text-muted-foreground mt-1">
|
||||
{filter.valueType === "category" && categoryValues[filter.columnName]
|
||||
? "카테고리 값을 선택하세요"
|
||||
: filter.operator === "in" || filter.operator === "not_in"
|
||||
? "여러 값은 쉼표(,)로 구분하세요"
|
||||
: filter.operator === "between"
|
||||
? "시작과 종료 값을 ~로 구분하세요"
|
||||
: filter.operator === "date_range_contains"
|
||||
? "기간 내에 포함되는지 확인할 날짜를 선택하세요"
|
||||
: "필터링할 값을 입력하세요"}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</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>
|
||||
))}
|
||||
{/* 값 입력 (NULL 체크 및 date_range_contains의 dynamic 제외) */}
|
||||
{filter.operator !== "is_null" &&
|
||||
filter.operator !== "is_not_null" &&
|
||||
!(filter.operator === "date_range_contains" && filter.valueType === "dynamic") && (
|
||||
<div>
|
||||
<Label className="text-xs">값</Label>
|
||||
{/* 카테고리 타입이고 값 타입이 category인 경우 셀렉트박스 */}
|
||||
{filter.valueType === "category" && categoryValues[filter.columnName] ? (
|
||||
<Select
|
||||
value={Array.isArray(filter.value) ? filter.value[0] : filter.value}
|
||||
onValueChange={(value) => handleFilterChange(filter.id, "value", value)}
|
||||
>
|
||||
<SelectTrigger className="h-8 text-xs sm:h-10 sm:text-sm">
|
||||
<SelectValue
|
||||
placeholder={loadingCategories[filter.columnName] ? "로딩 중..." : "값 선택"}
|
||||
/>
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{categoryValues[filter.columnName].map((option) => (
|
||||
<SelectItem key={option.value} value={option.value}>
|
||||
{option.label}
|
||||
</SelectItem>
|
||||
))}
|
||||
</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>
|
||||
)}
|
||||
|
||||
{/* 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>
|
||||
)}
|
||||
</FilterItemCollapsible>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
|
||||
{/* 필터 추가 버튼 */}
|
||||
<Button
|
||||
variant="outline"
|
||||
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}
|
||||
disabled={columns.length === 0}
|
||||
>
|
||||
|
|
@ -544,13 +630,10 @@ export function DataFilterConfigPanel({
|
|||
</Button>
|
||||
|
||||
{columns.length === 0 && (
|
||||
<p className="text-xs text-muted-foreground text-center">
|
||||
테이블을 먼저 선택해주세요
|
||||
</p>
|
||||
<p className="text-muted-foreground text-center text-xs">테이블을 먼저 선택해주세요</p>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1014,10 +1014,14 @@ export const SplitPanelLayoutComponent: React.FC<SplitPanelLayoutComponentProps>
|
|||
// 추가 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<SplitPanelLayoutComponentProps>
|
|||
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<SplitPanelLayoutComponentProps>
|
|||
|
||||
// 데이터 필터 적용
|
||||
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<SplitPanelLayoutComponentProps>
|
|||
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;
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue