Merge pull request 'lhj' (#352) from lhj into main
Reviewed-on: http://39.117.244.52:3000/kjs/ERP-node/pulls/352
This commit is contained in:
commit
3fa57ad2ae
|
|
@ -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>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 데이터 필터 설정 패널
|
* 데이터 필터 설정 패널
|
||||||
* 테이블 리스트, 분할 패널, 플로우 위젯 등에서 사용
|
* 테이블 리스트, 분할 패널, 플로우 위젯 등에서 사용
|
||||||
|
|
@ -42,7 +104,7 @@ export function DataFilterConfigPanel({
|
||||||
enabled: false,
|
enabled: false,
|
||||||
filters: [],
|
filters: [],
|
||||||
matchType: "all",
|
matchType: "all",
|
||||||
}
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
// 카테고리 값 캐시 (컬럼명 -> 카테고리 값 목록)
|
// 카테고리 값 캐시 (컬럼명 -> 카테고리 값 목록)
|
||||||
|
|
@ -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);
|
||||||
|
|
@ -94,14 +156,14 @@ export function DataFilterConfigPanel({
|
||||||
}));
|
}));
|
||||||
|
|
||||||
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,23 +260,48 @@ 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: "기간 내",
|
||||||
|
};
|
||||||
|
|
||||||
|
// 컬럼 라벨 찾기
|
||||||
|
const columnLabel =
|
||||||
|
columns.find((c) => c.columnName === filter.columnName)?.columnLabel || filter.columnName;
|
||||||
|
|
||||||
|
// 필터 요약 텍스트 생성
|
||||||
|
const filterSummary = filter.columnName
|
||||||
|
? `${columnLabel} ${operatorLabels[filter.operator] || filter.operator}${
|
||||||
|
filter.operator !== "is_null" && filter.operator !== "is_not_null" && filter.value
|
||||||
|
? ` ${filter.value}`
|
||||||
|
: ""
|
||||||
|
}`
|
||||||
|
: "설정 필요";
|
||||||
|
|
||||||
|
return (
|
||||||
|
<FilterItemCollapsible
|
||||||
|
key={filter.id}
|
||||||
|
filter={filter}
|
||||||
|
index={index}
|
||||||
|
filterSummary={filterSummary}
|
||||||
|
onRemove={() => handleRemoveFilter(filter.id)}
|
||||||
|
>
|
||||||
{/* 컬럼 선택 (날짜 범위 포함이 아닐 때만 표시) */}
|
{/* 컬럼 선택 (날짜 범위 포함이 아닐 때만 표시) */}
|
||||||
{filter.operator !== "date_range_contains" && (
|
{filter.operator !== "date_range_contains" && (
|
||||||
<div>
|
<div>
|
||||||
|
|
@ -246,9 +331,7 @@ export function DataFilterConfigPanel({
|
||||||
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, columnName: value, valueType, value: "" }
|
|
||||||
: f
|
|
||||||
),
|
),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -271,9 +354,7 @@ export function DataFilterConfigPanel({
|
||||||
<SelectItem key={col.columnName} value={col.columnName}>
|
<SelectItem key={col.columnName} value={col.columnName}>
|
||||||
{col.columnLabel || col.columnName}
|
{col.columnLabel || col.columnName}
|
||||||
{(col.input_type === "category" || col.input_type === "code") && (
|
{(col.input_type === "category" || col.input_type === "code") && (
|
||||||
<span className="ml-2 text-xs text-muted-foreground">
|
<span className="text-muted-foreground ml-2 text-xs">({col.input_type})</span>
|
||||||
({col.input_type})
|
|
||||||
</span>
|
|
||||||
)}
|
)}
|
||||||
</SelectItem>
|
</SelectItem>
|
||||||
))}
|
))}
|
||||||
|
|
@ -293,9 +374,7 @@ export function DataFilterConfigPanel({
|
||||||
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, operator: value, valueType: "dynamic", value: "TODAY" } : f,
|
||||||
? { ...f, operator: value, valueType: "dynamic", value: "TODAY" }
|
|
||||||
: f
|
|
||||||
),
|
),
|
||||||
};
|
};
|
||||||
setLocalConfig(newConfig);
|
setLocalConfig(newConfig);
|
||||||
|
|
@ -332,7 +411,7 @@ export function DataFilterConfigPanel({
|
||||||
{filter.operator === "date_range_contains" && (
|
{filter.operator === "date_range_contains" && (
|
||||||
<>
|
<>
|
||||||
<div className="col-span-2">
|
<div className="col-span-2">
|
||||||
<p className="text-xs text-muted-foreground bg-muted/50 p-2 rounded">
|
<p className="text-muted-foreground bg-muted/50 rounded p-2 text-xs">
|
||||||
💡 날짜 범위 필터링 규칙:
|
💡 날짜 범위 필터링 규칙:
|
||||||
<br />• 시작일만 있고 종료일이 NULL → 시작일 이후 모든 데이터
|
<br />• 시작일만 있고 종료일이 NULL → 시작일 이후 모든 데이터
|
||||||
<br />• 종료일만 있고 시작일이 NULL → 종료일 이전 모든 데이터
|
<br />• 종료일만 있고 시작일이 NULL → 종료일 이전 모든 데이터
|
||||||
|
|
@ -356,10 +435,13 @@ export function DataFilterConfigPanel({
|
||||||
<SelectValue placeholder="시작일 컬럼 선택" />
|
<SelectValue placeholder="시작일 컬럼 선택" />
|
||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
<SelectContent>
|
<SelectContent>
|
||||||
{columns.filter(col =>
|
{columns
|
||||||
col.dataType?.toLowerCase().includes('date') ||
|
.filter(
|
||||||
col.dataType?.toLowerCase().includes('time')
|
(col) =>
|
||||||
).map((col) => (
|
col.dataType?.toLowerCase().includes("date") ||
|
||||||
|
col.dataType?.toLowerCase().includes("time"),
|
||||||
|
)
|
||||||
|
.map((col) => (
|
||||||
<SelectItem key={col.columnName} value={col.columnName}>
|
<SelectItem key={col.columnName} value={col.columnName}>
|
||||||
{col.columnLabel || col.columnName}
|
{col.columnLabel || col.columnName}
|
||||||
</SelectItem>
|
</SelectItem>
|
||||||
|
|
@ -384,10 +466,13 @@ export function DataFilterConfigPanel({
|
||||||
<SelectValue placeholder="종료일 컬럼 선택" />
|
<SelectValue placeholder="종료일 컬럼 선택" />
|
||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
<SelectContent>
|
<SelectContent>
|
||||||
{columns.filter(col =>
|
{columns
|
||||||
col.dataType?.toLowerCase().includes('date') ||
|
.filter(
|
||||||
col.dataType?.toLowerCase().includes('time')
|
(col) =>
|
||||||
).map((col) => (
|
col.dataType?.toLowerCase().includes("date") ||
|
||||||
|
col.dataType?.toLowerCase().includes("time"),
|
||||||
|
)
|
||||||
|
.map((col) => (
|
||||||
<SelectItem key={col.columnName} value={col.columnName}>
|
<SelectItem key={col.columnName} value={col.columnName}>
|
||||||
{col.columnLabel || col.columnName}
|
{col.columnLabel || col.columnName}
|
||||||
</SelectItem>
|
</SelectItem>
|
||||||
|
|
@ -410,9 +495,7 @@ export function DataFilterConfigPanel({
|
||||||
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, valueType: value, value: "TODAY" } : f,
|
||||||
? { ...f, valueType: value, value: "TODAY" }
|
|
||||||
: f
|
|
||||||
),
|
),
|
||||||
};
|
};
|
||||||
setLocalConfig(newConfig);
|
setLocalConfig(newConfig);
|
||||||
|
|
@ -422,9 +505,7 @@ export function DataFilterConfigPanel({
|
||||||
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, valueType: value, value: "" } : f,
|
||||||
? { ...f, valueType: value, value: "" }
|
|
||||||
: f
|
|
||||||
),
|
),
|
||||||
};
|
};
|
||||||
setLocalConfig(newConfig);
|
setLocalConfig(newConfig);
|
||||||
|
|
@ -464,9 +545,9 @@ export function DataFilterConfigPanel({
|
||||||
onValueChange={(value) => handleFilterChange(filter.id, "value", value)}
|
onValueChange={(value) => handleFilterChange(filter.id, "value", value)}
|
||||||
>
|
>
|
||||||
<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] ? "로딩 중..." : "값 선택"
|
placeholder={loadingCategories[filter.columnName] ? "로딩 중..." : "값 선택"}
|
||||||
} />
|
/>
|
||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
<SelectContent>
|
<SelectContent>
|
||||||
{categoryValues[filter.columnName].map((option) => (
|
{categoryValues[filter.columnName].map((option) => (
|
||||||
|
|
@ -491,7 +572,11 @@ export function DataFilterConfigPanel({
|
||||||
value={Array.isArray(filter.value) ? filter.value.join(" ~ ") : filter.value}
|
value={Array.isArray(filter.value) ? filter.value.join(" ~ ") : filter.value}
|
||||||
onChange={(e) => {
|
onChange={(e) => {
|
||||||
const values = e.target.value.split("~").map((v) => v.trim());
|
const values = e.target.value.split("~").map((v) => v.trim());
|
||||||
handleFilterChange(filter.id, "value", values.length === 2 ? values : [values[0] || "", ""]);
|
handleFilterChange(
|
||||||
|
filter.id,
|
||||||
|
"value",
|
||||||
|
values.length === 2 ? values : [values[0] || "", ""],
|
||||||
|
);
|
||||||
}}
|
}}
|
||||||
placeholder="시작 ~ 종료 (예: 2025-01-01 ~ 2025-12-31)"
|
placeholder="시작 ~ 종료 (예: 2025-01-01 ~ 2025-12-31)"
|
||||||
className="h-8 text-xs sm:h-10 sm:text-sm"
|
className="h-8 text-xs sm:h-10 sm:text-sm"
|
||||||
|
|
@ -501,11 +586,13 @@ export function DataFilterConfigPanel({
|
||||||
type={filter.operator === "date_range_contains" ? "date" : "text"}
|
type={filter.operator === "date_range_contains" ? "date" : "text"}
|
||||||
value={Array.isArray(filter.value) ? filter.value[0] || "" : filter.value}
|
value={Array.isArray(filter.value) ? filter.value[0] || "" : filter.value}
|
||||||
onChange={(e) => handleFilterChange(filter.id, "value", e.target.value)}
|
onChange={(e) => handleFilterChange(filter.id, "value", e.target.value)}
|
||||||
placeholder={filter.operator === "date_range_contains" ? "비교할 날짜 선택" : "필터 값 입력"}
|
placeholder={
|
||||||
|
filter.operator === "date_range_contains" ? "비교할 날짜 선택" : "필터 값 입력"
|
||||||
|
}
|
||||||
className="h-8 text-xs sm:h-10 sm:text-sm"
|
className="h-8 text-xs sm:h-10 sm:text-sm"
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
<p className="text-[10px] text-muted-foreground mt-1">
|
<p className="text-muted-foreground mt-1 text-[10px]">
|
||||||
{filter.valueType === "category" && categoryValues[filter.columnName]
|
{filter.valueType === "category" && categoryValues[filter.columnName]
|
||||||
? "카테고리 값을 선택하세요"
|
? "카테고리 값을 선택하세요"
|
||||||
: filter.operator === "in" || filter.operator === "not_in"
|
: filter.operator === "in" || filter.operator === "not_in"
|
||||||
|
|
@ -522,20 +609,19 @@ export function DataFilterConfigPanel({
|
||||||
{/* date_range_contains의 dynamic 타입 안내 */}
|
{/* date_range_contains의 dynamic 타입 안내 */}
|
||||||
{filter.operator === "date_range_contains" && filter.valueType === "dynamic" && (
|
{filter.operator === "date_range_contains" && filter.valueType === "dynamic" && (
|
||||||
<div className="rounded-md bg-blue-50 p-2">
|
<div className="rounded-md bg-blue-50 p-2">
|
||||||
<p className="text-[10px] text-blue-700">
|
<p className="text-[10px] text-blue-700">오늘 날짜를 기준으로 기간 내 데이터를 필터링합니다.</p>
|
||||||
ℹ️ 오늘 날짜를 기준으로 기간 내 데이터를 필터링합니다.
|
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</FilterItemCollapsible>
|
||||||
))}
|
);
|
||||||
|
})}
|
||||||
</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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -88,6 +88,9 @@ import "./mail-recipient-selector/MailRecipientSelectorRenderer"; // 내부 인
|
||||||
// 🆕 연관 데이터 버튼 컴포넌트
|
// 🆕 연관 데이터 버튼 컴포넌트
|
||||||
import "./related-data-buttons/RelatedDataButtonsRenderer"; // 좌측 선택 데이터 기반 연관 테이블 버튼 표시
|
import "./related-data-buttons/RelatedDataButtonsRenderer"; // 좌측 선택 데이터 기반 연관 테이블 버튼 표시
|
||||||
|
|
||||||
|
// 🆕 피벗 그리드 컴포넌트
|
||||||
|
import "./pivot-grid/PivotGridRenderer"; // 피벗 테이블 (행/열 그룹화, 집계, 드릴다운)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 컴포넌트 초기화 함수
|
* 컴포넌트 초기화 함수
|
||||||
*/
|
*/
|
||||||
|
|
|
||||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
|
|
@ -0,0 +1,213 @@
|
||||||
|
"use client";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* PivotGrid 컨텍스트 메뉴 컴포넌트
|
||||||
|
* 우클릭 시 정렬, 필터, 확장/축소 등의 옵션 제공
|
||||||
|
*/
|
||||||
|
|
||||||
|
import React from "react";
|
||||||
|
import {
|
||||||
|
ContextMenu,
|
||||||
|
ContextMenuContent,
|
||||||
|
ContextMenuItem,
|
||||||
|
ContextMenuSeparator,
|
||||||
|
ContextMenuSub,
|
||||||
|
ContextMenuSubContent,
|
||||||
|
ContextMenuSubTrigger,
|
||||||
|
ContextMenuTrigger,
|
||||||
|
} from "@/components/ui/context-menu";
|
||||||
|
import {
|
||||||
|
ArrowUpAZ,
|
||||||
|
ArrowDownAZ,
|
||||||
|
Filter,
|
||||||
|
ChevronDown,
|
||||||
|
ChevronRight,
|
||||||
|
Copy,
|
||||||
|
Eye,
|
||||||
|
EyeOff,
|
||||||
|
BarChart3,
|
||||||
|
} from "lucide-react";
|
||||||
|
import { PivotFieldConfig, AggregationType } from "../types";
|
||||||
|
|
||||||
|
interface PivotContextMenuProps {
|
||||||
|
children: React.ReactNode;
|
||||||
|
// 현재 컨텍스트 정보
|
||||||
|
cellType: "header" | "data" | "rowHeader" | "columnHeader";
|
||||||
|
field?: PivotFieldConfig;
|
||||||
|
rowPath?: string[];
|
||||||
|
columnPath?: string[];
|
||||||
|
value?: any;
|
||||||
|
// 콜백
|
||||||
|
onSort?: (field: string, direction: "asc" | "desc") => void;
|
||||||
|
onFilter?: (field: string) => void;
|
||||||
|
onExpand?: (path: string[]) => void;
|
||||||
|
onCollapse?: (path: string[]) => void;
|
||||||
|
onExpandAll?: () => void;
|
||||||
|
onCollapseAll?: () => void;
|
||||||
|
onCopy?: (value: any) => void;
|
||||||
|
onHideField?: (field: string) => void;
|
||||||
|
onChangeSummary?: (field: string, summaryType: AggregationType) => void;
|
||||||
|
onDrillDown?: (rowPath: string[], columnPath: string[]) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const PivotContextMenu: React.FC<PivotContextMenuProps> = ({
|
||||||
|
children,
|
||||||
|
cellType,
|
||||||
|
field,
|
||||||
|
rowPath,
|
||||||
|
columnPath,
|
||||||
|
value,
|
||||||
|
onSort,
|
||||||
|
onFilter,
|
||||||
|
onExpand,
|
||||||
|
onCollapse,
|
||||||
|
onExpandAll,
|
||||||
|
onCollapseAll,
|
||||||
|
onCopy,
|
||||||
|
onHideField,
|
||||||
|
onChangeSummary,
|
||||||
|
onDrillDown,
|
||||||
|
}) => {
|
||||||
|
const handleCopy = () => {
|
||||||
|
if (value !== undefined && value !== null) {
|
||||||
|
navigator.clipboard.writeText(String(value));
|
||||||
|
onCopy?.(value);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ContextMenu>
|
||||||
|
<ContextMenuTrigger asChild>{children}</ContextMenuTrigger>
|
||||||
|
<ContextMenuContent className="w-48">
|
||||||
|
{/* 정렬 옵션 (헤더에서만) */}
|
||||||
|
{(cellType === "rowHeader" || cellType === "columnHeader") && field && (
|
||||||
|
<>
|
||||||
|
<ContextMenuSub>
|
||||||
|
<ContextMenuSubTrigger>
|
||||||
|
<ArrowUpAZ className="mr-2 h-4 w-4" />
|
||||||
|
정렬
|
||||||
|
</ContextMenuSubTrigger>
|
||||||
|
<ContextMenuSubContent>
|
||||||
|
<ContextMenuItem onClick={() => onSort?.(field.field, "asc")}>
|
||||||
|
<ArrowUpAZ className="mr-2 h-4 w-4" />
|
||||||
|
오름차순
|
||||||
|
</ContextMenuItem>
|
||||||
|
<ContextMenuItem onClick={() => onSort?.(field.field, "desc")}>
|
||||||
|
<ArrowDownAZ className="mr-2 h-4 w-4" />
|
||||||
|
내림차순
|
||||||
|
</ContextMenuItem>
|
||||||
|
</ContextMenuSubContent>
|
||||||
|
</ContextMenuSub>
|
||||||
|
<ContextMenuSeparator />
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* 확장/축소 옵션 */}
|
||||||
|
{(cellType === "rowHeader" || cellType === "columnHeader") && (
|
||||||
|
<>
|
||||||
|
{rowPath && rowPath.length > 0 && (
|
||||||
|
<>
|
||||||
|
<ContextMenuItem onClick={() => onExpand?.(rowPath)}>
|
||||||
|
<ChevronDown className="mr-2 h-4 w-4" />
|
||||||
|
확장
|
||||||
|
</ContextMenuItem>
|
||||||
|
<ContextMenuItem onClick={() => onCollapse?.(rowPath)}>
|
||||||
|
<ChevronRight className="mr-2 h-4 w-4" />
|
||||||
|
축소
|
||||||
|
</ContextMenuItem>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
<ContextMenuItem onClick={onExpandAll}>
|
||||||
|
<ChevronDown className="mr-2 h-4 w-4" />
|
||||||
|
전체 확장
|
||||||
|
</ContextMenuItem>
|
||||||
|
<ContextMenuItem onClick={onCollapseAll}>
|
||||||
|
<ChevronRight className="mr-2 h-4 w-4" />
|
||||||
|
전체 축소
|
||||||
|
</ContextMenuItem>
|
||||||
|
<ContextMenuSeparator />
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* 필터 옵션 */}
|
||||||
|
{field && onFilter && (
|
||||||
|
<>
|
||||||
|
<ContextMenuItem onClick={() => onFilter(field.field)}>
|
||||||
|
<Filter className="mr-2 h-4 w-4" />
|
||||||
|
필터
|
||||||
|
</ContextMenuItem>
|
||||||
|
<ContextMenuSeparator />
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* 집계 함수 변경 (데이터 필드에서만) */}
|
||||||
|
{cellType === "data" && field && onChangeSummary && (
|
||||||
|
<>
|
||||||
|
<ContextMenuSub>
|
||||||
|
<ContextMenuSubTrigger>
|
||||||
|
<BarChart3 className="mr-2 h-4 w-4" />
|
||||||
|
집계 함수
|
||||||
|
</ContextMenuSubTrigger>
|
||||||
|
<ContextMenuSubContent>
|
||||||
|
<ContextMenuItem
|
||||||
|
onClick={() => onChangeSummary(field.field, "sum")}
|
||||||
|
>
|
||||||
|
합계
|
||||||
|
</ContextMenuItem>
|
||||||
|
<ContextMenuItem
|
||||||
|
onClick={() => onChangeSummary(field.field, "count")}
|
||||||
|
>
|
||||||
|
개수
|
||||||
|
</ContextMenuItem>
|
||||||
|
<ContextMenuItem
|
||||||
|
onClick={() => onChangeSummary(field.field, "avg")}
|
||||||
|
>
|
||||||
|
평균
|
||||||
|
</ContextMenuItem>
|
||||||
|
<ContextMenuItem
|
||||||
|
onClick={() => onChangeSummary(field.field, "min")}
|
||||||
|
>
|
||||||
|
최소
|
||||||
|
</ContextMenuItem>
|
||||||
|
<ContextMenuItem
|
||||||
|
onClick={() => onChangeSummary(field.field, "max")}
|
||||||
|
>
|
||||||
|
최대
|
||||||
|
</ContextMenuItem>
|
||||||
|
</ContextMenuSubContent>
|
||||||
|
</ContextMenuSub>
|
||||||
|
<ContextMenuSeparator />
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* 드릴다운 (데이터 셀에서만) */}
|
||||||
|
{cellType === "data" && rowPath && columnPath && onDrillDown && (
|
||||||
|
<>
|
||||||
|
<ContextMenuItem onClick={() => onDrillDown(rowPath, columnPath)}>
|
||||||
|
<Eye className="mr-2 h-4 w-4" />
|
||||||
|
상세 데이터 보기
|
||||||
|
</ContextMenuItem>
|
||||||
|
<ContextMenuSeparator />
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* 필드 숨기기 */}
|
||||||
|
{field && onHideField && (
|
||||||
|
<ContextMenuItem onClick={() => onHideField(field.field)}>
|
||||||
|
<EyeOff className="mr-2 h-4 w-4" />
|
||||||
|
필드 숨기기
|
||||||
|
</ContextMenuItem>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* 복사 */}
|
||||||
|
<ContextMenuItem onClick={handleCopy}>
|
||||||
|
<Copy className="mr-2 h-4 w-4" />
|
||||||
|
복사
|
||||||
|
</ContextMenuItem>
|
||||||
|
</ContextMenuContent>
|
||||||
|
</ContextMenu>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default PivotContextMenu;
|
||||||
|
|
||||||
|
|
@ -94,6 +94,15 @@ const DISPLAY_MODE_OPTIONS: { value: SummaryDisplayMode; label: string }[] = [
|
||||||
{ value: "percentDifferenceFromPrevious", label: "이전 대비 % 차이" },
|
{ value: "percentDifferenceFromPrevious", label: "이전 대비 % 차이" },
|
||||||
];
|
];
|
||||||
|
|
||||||
|
const DATE_GROUP_OPTIONS: { value: string; label: string }[] = [
|
||||||
|
{ value: "none", label: "그룹 없음" },
|
||||||
|
{ value: "year", label: "년" },
|
||||||
|
{ value: "quarter", label: "분기" },
|
||||||
|
{ value: "month", label: "월" },
|
||||||
|
{ value: "week", label: "주" },
|
||||||
|
{ value: "day", label: "일" },
|
||||||
|
];
|
||||||
|
|
||||||
const DATA_TYPE_ICONS: Record<string, React.ReactNode> = {
|
const DATA_TYPE_ICONS: Record<string, React.ReactNode> = {
|
||||||
string: <Type className="h-3.5 w-3.5" />,
|
string: <Type className="h-3.5 w-3.5" />,
|
||||||
number: <Hash className="h-3.5 w-3.5" />,
|
number: <Hash className="h-3.5 w-3.5" />,
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,7 @@
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* FieldPanel 컴포넌트
|
* FieldPanel 컴포넌트
|
||||||
* 피벗 그리드 상단의 필드 배치 영역 (필터, 열, 행, 데이터)
|
* 피벗 그리드 상단의 필드 배치 영역 (열, 행, 데이터)
|
||||||
* 드래그 앤 드롭으로 필드 재배치 가능
|
* 드래그 앤 드롭으로 필드 재배치 가능
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
|
@ -247,7 +247,7 @@ const DroppableArea: React.FC<DroppableAreaProps> = ({
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={cn(
|
className={cn(
|
||||||
"flex-1 min-h-[60px] rounded-md border-2 border-dashed p-2",
|
"flex-1 min-h-[44px] rounded border border-dashed p-1.5",
|
||||||
"transition-colors duration-200",
|
"transition-colors duration-200",
|
||||||
config.color,
|
config.color,
|
||||||
isOver && "border-primary bg-primary/5"
|
isOver && "border-primary bg-primary/5"
|
||||||
|
|
@ -255,7 +255,7 @@ const DroppableArea: React.FC<DroppableAreaProps> = ({
|
||||||
data-area={area}
|
data-area={area}
|
||||||
>
|
>
|
||||||
{/* 영역 헤더 */}
|
{/* 영역 헤더 */}
|
||||||
<div className="flex items-center gap-1.5 mb-2 text-xs font-medium text-muted-foreground">
|
<div className="flex items-center gap-1 mb-1 text-[11px] font-medium text-muted-foreground">
|
||||||
{icon}
|
{icon}
|
||||||
<span>{title}</span>
|
<span>{title}</span>
|
||||||
{areaFields.length > 0 && (
|
{areaFields.length > 0 && (
|
||||||
|
|
@ -267,9 +267,9 @@ const DroppableArea: React.FC<DroppableAreaProps> = ({
|
||||||
|
|
||||||
{/* 필드 목록 */}
|
{/* 필드 목록 */}
|
||||||
<SortableContext items={fieldIds} strategy={horizontalListSortingStrategy}>
|
<SortableContext items={fieldIds} strategy={horizontalListSortingStrategy}>
|
||||||
<div className="flex flex-wrap gap-1.5 min-h-[28px]">
|
<div className="flex flex-wrap gap-1 min-h-[22px]">
|
||||||
{areaFields.length === 0 ? (
|
{areaFields.length === 0 ? (
|
||||||
<span className="text-xs text-muted-foreground/50 italic">
|
<span className="text-[10px] text-muted-foreground/50 italic">
|
||||||
필드를 여기로 드래그
|
필드를 여기로 드래그
|
||||||
</span>
|
</span>
|
||||||
) : (
|
) : (
|
||||||
|
|
@ -443,16 +443,42 @@ export const FieldPanel: React.FC<FieldPanelProps> = ({
|
||||||
? fields.find((f) => `${f.area}-${f.field}` === activeId)
|
? fields.find((f) => `${f.area}-${f.field}` === activeId)
|
||||||
: null;
|
: null;
|
||||||
|
|
||||||
|
// 각 영역의 필드 수 계산
|
||||||
|
const filterCount = fields.filter((f) => f.area === "filter" && f.visible !== false).length;
|
||||||
|
const columnCount = fields.filter((f) => f.area === "column" && f.visible !== false).length;
|
||||||
|
const rowCount = fields.filter((f) => f.area === "row" && f.visible !== false).length;
|
||||||
|
const dataCount = fields.filter((f) => f.area === "data" && f.visible !== false).length;
|
||||||
|
|
||||||
if (collapsed) {
|
if (collapsed) {
|
||||||
return (
|
return (
|
||||||
<div className="border-b border-border px-3 py-2">
|
<div className="border-b border-border px-3 py-1.5 flex items-center justify-between bg-muted/10">
|
||||||
|
<div className="flex items-center gap-3 text-xs text-muted-foreground">
|
||||||
|
{filterCount > 0 && (
|
||||||
|
<span className="flex items-center gap-1">
|
||||||
|
<Filter className="h-3 w-3" />
|
||||||
|
필터 {filterCount}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
<span className="flex items-center gap-1">
|
||||||
|
<Columns className="h-3 w-3" />
|
||||||
|
열 {columnCount}
|
||||||
|
</span>
|
||||||
|
<span className="flex items-center gap-1">
|
||||||
|
<Rows className="h-3 w-3" />
|
||||||
|
행 {rowCount}
|
||||||
|
</span>
|
||||||
|
<span className="flex items-center gap-1">
|
||||||
|
<BarChart3 className="h-3 w-3" />
|
||||||
|
데이터 {dataCount}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
<Button
|
<Button
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
size="sm"
|
size="sm"
|
||||||
onClick={onToggleCollapse}
|
onClick={onToggleCollapse}
|
||||||
className="text-xs"
|
className="text-xs h-6 px-2"
|
||||||
>
|
>
|
||||||
필드 패널 펼치기
|
필드 설정
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
@ -466,9 +492,9 @@ export const FieldPanel: React.FC<FieldPanelProps> = ({
|
||||||
onDragOver={handleDragOver}
|
onDragOver={handleDragOver}
|
||||||
onDragEnd={handleDragEnd}
|
onDragEnd={handleDragEnd}
|
||||||
>
|
>
|
||||||
<div className="border-b border-border bg-muted/20 p-3">
|
<div className="border-b border-border bg-muted/20 p-2">
|
||||||
{/* 2x2 그리드로 영역 배치 */}
|
{/* 4개 영역 배치: 2x2 그리드 */}
|
||||||
<div className="grid grid-cols-2 gap-2">
|
<div className="grid grid-cols-2 gap-1.5">
|
||||||
{/* 필터 영역 */}
|
{/* 필터 영역 */}
|
||||||
<DroppableArea
|
<DroppableArea
|
||||||
area="filter"
|
area="filter"
|
||||||
|
|
@ -516,12 +542,12 @@ export const FieldPanel: React.FC<FieldPanelProps> = ({
|
||||||
|
|
||||||
{/* 접기 버튼 */}
|
{/* 접기 버튼 */}
|
||||||
{onToggleCollapse && (
|
{onToggleCollapse && (
|
||||||
<div className="flex justify-center mt-2">
|
<div className="flex justify-center mt-1.5">
|
||||||
<Button
|
<Button
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
size="sm"
|
size="sm"
|
||||||
onClick={onToggleCollapse}
|
onClick={onToggleCollapse}
|
||||||
className="text-xs h-6"
|
className="text-xs h-5 px-2"
|
||||||
>
|
>
|
||||||
필드 패널 접기
|
필드 패널 접기
|
||||||
</Button>
|
</Button>
|
||||||
|
|
|
||||||
|
|
@ -7,4 +7,5 @@ export { FieldChooser } from "./FieldChooser";
|
||||||
export { DrillDownModal } from "./DrillDownModal";
|
export { DrillDownModal } from "./DrillDownModal";
|
||||||
export { FilterPopup } from "./FilterPopup";
|
export { FilterPopup } from "./FilterPopup";
|
||||||
export { PivotChart } from "./PivotChart";
|
export { PivotChart } from "./PivotChart";
|
||||||
|
export { PivotContextMenu } from "./ContextMenu";
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -90,6 +90,10 @@ export interface PivotFieldConfig {
|
||||||
// 계층 관련
|
// 계층 관련
|
||||||
displayFolder?: string; // 필드 선택기에서 폴더 구조
|
displayFolder?: string; // 필드 선택기에서 폴더 구조
|
||||||
isMeasure?: boolean; // 측정값 전용 필드 (data 영역만 가능)
|
isMeasure?: boolean; // 측정값 전용 필드 (data 영역만 가능)
|
||||||
|
|
||||||
|
// 계산 필드
|
||||||
|
isCalculated?: boolean; // 계산 필드 여부
|
||||||
|
calculateFormula?: string; // 계산 수식 (예: "[Sales] / [Quantity]")
|
||||||
}
|
}
|
||||||
|
|
||||||
// ==================== 데이터 소스 설정 ====================
|
// ==================== 데이터 소스 설정 ====================
|
||||||
|
|
@ -140,11 +144,13 @@ export interface PivotTotalsConfig {
|
||||||
showRowGrandTotals?: boolean; // 행 총합계 표시
|
showRowGrandTotals?: boolean; // 행 총합계 표시
|
||||||
showRowTotals?: boolean; // 행 소계 표시
|
showRowTotals?: boolean; // 행 소계 표시
|
||||||
rowTotalsPosition?: "first" | "last"; // 소계 위치
|
rowTotalsPosition?: "first" | "last"; // 소계 위치
|
||||||
|
rowGrandTotalPosition?: "top" | "bottom"; // 행 총계 위치 (상단/하단)
|
||||||
|
|
||||||
// 열 총합계
|
// 열 총합계
|
||||||
showColumnGrandTotals?: boolean; // 열 총합계 표시
|
showColumnGrandTotals?: boolean; // 열 총합계 표시
|
||||||
showColumnTotals?: boolean; // 열 소계 표시
|
showColumnTotals?: boolean; // 열 소계 표시
|
||||||
columnTotalsPosition?: "first" | "last"; // 소계 위치
|
columnTotalsPosition?: "first" | "last"; // 소계 위치
|
||||||
|
columnGrandTotalPosition?: "left" | "right"; // 열 총계 위치 (좌측/우측)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 필드 선택기 설정
|
// 필드 선택기 설정
|
||||||
|
|
@ -214,6 +220,7 @@ export interface PivotStyleConfig {
|
||||||
alternateRowColors?: boolean;
|
alternateRowColors?: boolean;
|
||||||
highlightTotals?: boolean; // 총합계 강조
|
highlightTotals?: boolean; // 총합계 강조
|
||||||
conditionalFormats?: ConditionalFormatRule[]; // 조건부 서식 규칙
|
conditionalFormats?: ConditionalFormatRule[]; // 조건부 서식 규칙
|
||||||
|
mergeCells?: boolean; // 같은 값 셀 병합
|
||||||
}
|
}
|
||||||
|
|
||||||
// ==================== 내보내기 설정 ====================
|
// ==================== 내보내기 설정 ====================
|
||||||
|
|
|
||||||
|
|
@ -1026,10 +1026,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":
|
||||||
|
|
@ -1038,6 +1042,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;
|
||||||
}
|
}
|
||||||
|
|
@ -1193,10 +1203,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":
|
||||||
|
|
@ -1205,6 +1219,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;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue