ERP-node/frontend/lib/registry/components/pivot-grid/PivotGridConfigPanel.tsx

799 lines
30 KiB
TypeScript
Raw Normal View History

"use client";
/**
2026-01-09 14:41:27 +09:00
* PivotGrid -
*
* :
* 1.
* 2. //
*/
import React, { useState, useEffect, useCallback } from "react";
import { cn } from "@/lib/utils";
import {
PivotGridComponentConfig,
PivotFieldConfig,
PivotAreaType,
AggregationType,
FieldDataType,
} from "./types";
import { Label } from "@/components/ui/label";
import { Input } from "@/components/ui/input";
import { Button } from "@/components/ui/button";
import { Switch } from "@/components/ui/switch";
import { Badge } from "@/components/ui/badge";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import {
Rows,
Columns,
2026-01-09 14:41:27 +09:00
Calculator,
X,
Plus,
GripVertical,
Table2,
BarChart3,
Settings,
ChevronDown,
2026-01-09 14:41:27 +09:00
ChevronUp,
Info,
} from "lucide-react";
2026-01-09 14:41:27 +09:00
import { tableTypeApi } from "@/lib/api/screen";
// ==================== 타입 ====================
interface TableInfo {
table_name: string;
table_comment?: string;
}
interface ColumnInfo {
column_name: string;
data_type: string;
column_comment?: string;
}
interface PivotGridConfigPanelProps {
config: PivotGridComponentConfig;
onChange: (config: PivotGridComponentConfig) => void;
}
// DB 타입을 FieldDataType으로 변환
function mapDbTypeToFieldType(dbType: string): FieldDataType {
const type = dbType.toLowerCase();
2026-01-09 14:41:27 +09:00
if (type.includes("int") || type.includes("numeric") || type.includes("decimal") || type.includes("float")) {
return "number";
}
2026-01-09 14:41:27 +09:00
if (type.includes("date") || type.includes("time") || type.includes("timestamp")) {
return "date";
}
if (type.includes("bool")) {
return "boolean";
}
return "string";
}
2026-01-09 14:41:27 +09:00
// ==================== 컬럼 칩 컴포넌트 ====================
2026-01-09 14:41:27 +09:00
interface ColumnChipProps {
column: ColumnInfo;
isUsed: boolean;
onClick: () => void;
}
2026-01-09 14:41:27 +09:00
const ColumnChip: React.FC<ColumnChipProps> = ({ column, isUsed, onClick }) => {
const dataType = mapDbTypeToFieldType(column.data_type);
const typeColor = {
number: "bg-blue-100 text-blue-700 border-blue-200",
string: "bg-green-100 text-green-700 border-green-200",
date: "bg-purple-100 text-purple-700 border-purple-200",
boolean: "bg-orange-100 text-orange-700 border-orange-200",
}[dataType];
2026-01-09 14:41:27 +09:00
return (
<button
type="button"
onClick={onClick}
disabled={isUsed}
className={cn(
"inline-flex items-center gap-1 px-2 py-1 rounded-md text-xs border transition-all",
isUsed
? "bg-muted text-muted-foreground border-muted cursor-not-allowed opacity-50"
: cn(typeColor, "hover:shadow-sm cursor-pointer")
)}
>
<span className="font-medium truncate max-w-[120px]">
{column.column_comment || column.column_name}
</span>
</button>
);
};
2026-01-09 14:41:27 +09:00
// ==================== 영역 드롭존 컴포넌트 ====================
2026-01-09 14:41:27 +09:00
interface AreaDropZoneProps {
area: PivotAreaType;
2026-01-09 14:41:27 +09:00
label: string;
description: string;
icon: React.ReactNode;
fields: PivotFieldConfig[];
2026-01-09 14:41:27 +09:00
columns: ColumnInfo[];
onAddField: (column: ColumnInfo) => void;
onRemoveField: (index: number) => void;
onUpdateField: (index: number, updates: Partial<PivotFieldConfig>) => void;
color: string;
}
2026-01-09 14:41:27 +09:00
const AreaDropZone: React.FC<AreaDropZoneProps> = ({
area,
2026-01-09 14:41:27 +09:00
label,
description,
icon,
fields,
2026-01-09 14:41:27 +09:00
columns,
onAddField,
onRemoveField,
onUpdateField,
color,
}) => {
2026-01-09 14:41:27 +09:00
const [isExpanded, setIsExpanded] = useState(true);
// 사용 가능한 컬럼 (이미 추가된 컬럼 제외)
const availableColumns = columns.filter(
(col) => !fields.some((f) => f.field === col.column_name)
);
return (
2026-01-09 14:41:27 +09:00
<div className={cn("rounded-lg border-2 p-3", color)}>
{/* 헤더 */}
<div
className="flex items-center justify-between cursor-pointer"
onClick={() => setIsExpanded(!isExpanded)}
>
<div className="flex items-center gap-2">
{icon}
2026-01-09 14:41:27 +09:00
<span className="font-medium text-sm">{label}</span>
<Badge variant="secondary" className="text-xs">
{fields.length}
</Badge>
</div>
2026-01-09 14:41:27 +09:00
{isExpanded ? <ChevronUp className="h-4 w-4" /> : <ChevronDown className="h-4 w-4" />}
</div>
{/* 설명 */}
<p className="text-xs text-muted-foreground mt-1">{description}</p>
{isExpanded && (
<div className="mt-3 space-y-2">
{/* 추가된 필드 목록 */}
{fields.length > 0 ? (
<div className="space-y-1">
{fields.map((field, idx) => (
<div
key={`${field.field}-${idx}`}
className="flex items-center gap-2 bg-background rounded-md px-2 py-1.5 border"
>
<GripVertical className="h-3 w-3 text-muted-foreground" />
<span className="flex-1 text-xs font-medium truncate">
{field.caption || field.field}
</span>
{/* 데이터 영역일 때 집계 함수 선택 */}
{area === "data" && (
<Select
value={field.summaryType || "sum"}
onValueChange={(v) => onUpdateField(idx, { summaryType: v as AggregationType })}
>
<SelectTrigger className="h-6 w-20 text-xs">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="sum"></SelectItem>
<SelectItem value="count"></SelectItem>
<SelectItem value="avg"></SelectItem>
<SelectItem value="min"></SelectItem>
<SelectItem value="max"></SelectItem>
</SelectContent>
</Select>
)}
<Button
variant="ghost"
size="icon"
className="h-5 w-5"
onClick={() => onRemoveField(idx)}
>
<X className="h-3 w-3" />
</Button>
</div>
))}
</div>
) : (
<div className="text-xs text-muted-foreground text-center py-2 border border-dashed rounded-md">
</div>
)}
{/* 컬럼 추가 드롭다운 */}
{availableColumns.length > 0 && (
<Select onValueChange={(v) => {
const col = columns.find(c => c.column_name === v);
if (col) onAddField(col);
}}>
<SelectTrigger className="h-8 text-xs">
<Plus className="h-3 w-3 mr-1" />
<span> </span>
</SelectTrigger>
<SelectContent>
{availableColumns.map((col) => (
<SelectItem key={col.column_name} value={col.column_name}>
2026-01-09 14:41:27 +09:00
<div className="flex items-center gap-2">
<span>{col.column_comment || col.column_name}</span>
<span className="text-muted-foreground text-xs">
({mapDbTypeToFieldType(col.data_type)})
</span>
</div>
</SelectItem>
2026-01-09 14:41:27 +09:00
))}
</SelectContent>
</Select>
)}
</div>
2026-01-09 14:41:27 +09:00
)}
</div>
);
};
// ==================== 메인 컴포넌트 ====================
export const PivotGridConfigPanel: React.FC<PivotGridConfigPanelProps> = ({
config,
onChange,
}) => {
const [tables, setTables] = useState<TableInfo[]>([]);
const [columns, setColumns] = useState<ColumnInfo[]>([]);
const [loadingTables, setLoadingTables] = useState(false);
const [loadingColumns, setLoadingColumns] = useState(false);
2026-01-09 14:41:27 +09:00
const [showAdvanced, setShowAdvanced] = useState(false);
// 테이블 목록 로드
useEffect(() => {
const loadTables = async () => {
setLoadingTables(true);
try {
2026-01-09 14:41:27 +09:00
const tableList = await tableTypeApi.getTables();
const mappedTables: TableInfo[] = tableList.map((t: any) => ({
table_name: t.tableName,
table_comment: t.tableLabel || t.displayName || t.tableName,
}));
setTables(mappedTables);
} catch (error) {
console.error("테이블 목록 로드 실패:", error);
} finally {
setLoadingTables(false);
}
};
loadTables();
}, []);
// 테이블 선택 시 컬럼 로드
useEffect(() => {
const loadColumns = async () => {
if (!config.dataSource?.tableName) {
setColumns([]);
return;
}
setLoadingColumns(true);
try {
2026-01-09 14:41:27 +09:00
const columnList = await tableTypeApi.getColumns(config.dataSource.tableName);
const mappedColumns: ColumnInfo[] = columnList.map((c: any) => ({
column_name: c.columnName || c.column_name,
data_type: c.dataType || c.data_type || "text",
column_comment: c.columnLabel || c.column_label || c.columnName || c.column_name,
}));
setColumns(mappedColumns);
} catch (error) {
console.error("컬럼 목록 로드 실패:", error);
} finally {
setLoadingColumns(false);
}
};
loadColumns();
}, [config.dataSource?.tableName]);
// 설정 업데이트 헬퍼
const updateConfig = useCallback(
(updates: Partial<PivotGridComponentConfig>) => {
onChange({ ...config, ...updates });
},
[config, onChange]
);
2026-01-09 14:41:27 +09:00
// 필드 추가
const handleAddField = (area: PivotAreaType, column: ColumnInfo) => {
const currentFields = config.fields || [];
const areaFields = currentFields.filter(f => f.area === area);
const newField: PivotFieldConfig = {
field: column.column_name,
caption: column.column_comment || column.column_name,
area,
areaIndex: areaFields.length,
dataType: mapDbTypeToFieldType(column.data_type),
visible: true,
};
if (area === "data") {
newField.summaryType = "sum";
}
updateConfig({ fields: [...currentFields, newField] });
};
// 필드 제거
const handleRemoveField = (area: PivotAreaType, index: number) => {
const currentFields = config.fields || [];
const newFields = currentFields.filter(
(f) => !(f.area === area && f.areaIndex === index)
);
// 인덱스 재정렬
let idx = 0;
newFields.forEach((f) => {
if (f.area === area) {
f.areaIndex = idx++;
}
});
updateConfig({ fields: newFields });
};
// 필드 업데이트
const handleUpdateField = (area: PivotAreaType, index: number, updates: Partial<PivotFieldConfig>) => {
const currentFields = config.fields || [];
const newFields = currentFields.map((f) => {
if (f.area === area && f.areaIndex === index) {
return { ...f, ...updates };
}
return f;
});
updateConfig({ fields: newFields });
};
// 영역별 필드 가져오기
const getFieldsByArea = (area: PivotAreaType) => {
return (config.fields || [])
.filter(f => f.area === area)
.sort((a, b) => (a.areaIndex || 0) - (b.areaIndex || 0));
};
return (
<div className="space-y-4">
2026-01-09 14:41:27 +09:00
{/* 사용 가이드 */}
<div className="bg-blue-50 border border-blue-200 rounded-lg p-3">
<div className="flex items-start gap-2">
<Info className="h-4 w-4 text-blue-600 mt-0.5" />
<div className="text-xs text-blue-800">
<p className="font-medium mb-1"> </p>
<ol className="list-decimal list-inside space-y-0.5 text-blue-700">
<li> <strong></strong> </li>
<li><strong> </strong> (: 지역, )</li>
<li><strong> </strong> (: , )</li>
<li><strong></strong> (: 매출, )</li>
</ol>
</div>
</div>
</div>
2026-01-09 14:41:27 +09:00
{/* STEP 1: 테이블 선택 */}
<div className="space-y-2">
<div className="flex items-center gap-2">
<Table2 className="h-4 w-4 text-primary" />
<Label className="text-sm font-semibold">STEP 1. </Label>
</div>
<Select
value={config.dataSource?.tableName || "__none__"}
onValueChange={(v) =>
updateConfig({
dataSource: {
...config.dataSource,
type: "table",
tableName: v === "__none__" ? undefined : v,
},
fields: [], // 테이블 변경 시 필드 초기화
})
}
>
<SelectTrigger className="h-10">
<SelectValue placeholder={loadingTables ? "로딩 중..." : "테이블을 선택하세요"} />
</SelectTrigger>
<SelectContent>
<SelectItem value="__none__"> </SelectItem>
{tables.map((table) => (
<SelectItem key={table.table_name} value={table.table_name}>
<div className="flex items-center gap-2">
<span className="font-medium">{table.table_comment || table.table_name}</span>
{table.table_comment && (
<span className="text-muted-foreground text-xs">({table.table_name})</span>
)}
</div>
</SelectItem>
))}
</SelectContent>
</Select>
</div>
2026-01-09 14:41:27 +09:00
{/* STEP 2: 필드 배치 */}
{config.dataSource?.tableName && (
<div className="space-y-3">
2026-01-09 14:41:27 +09:00
<div className="flex items-center gap-2">
<BarChart3 className="h-4 w-4 text-primary" />
<Label className="text-sm font-semibold">STEP 2. </Label>
{loadingColumns && <span className="text-xs text-muted-foreground">( ...)</span>}
</div>
2026-01-09 14:41:27 +09:00
{/* 사용 가능한 컬럼 목록 */}
{columns.length > 0 && (
<div className="bg-muted/30 rounded-lg p-3 space-y-2">
<Label className="text-xs text-muted-foreground"> </Label>
<div className="flex flex-wrap gap-1">
{columns.map((col) => {
const isUsed = (config.fields || []).some(f => f.field === col.column_name);
return (
<ColumnChip
key={col.column_name}
column={col}
isUsed={isUsed}
onClick={() => {/* 클릭 시 아무것도 안함 - 드롭존에서 추가 */}}
/>
);
})}
</div>
</div>
)}
2026-01-09 14:41:27 +09:00
{/* 영역별 드롭존 */}
<div className="grid gap-3">
<AreaDropZone
area="row"
label="행 그룹"
description="세로로 그룹화할 항목 (예: 지역, 부서, 제품)"
icon={<Rows className="h-4 w-4 text-emerald-600" />}
fields={getFieldsByArea("row")}
columns={columns}
onAddField={(col) => handleAddField("row", col)}
onRemoveField={(idx) => handleRemoveField("row", idx)}
onUpdateField={(idx, updates) => handleUpdateField("row", idx, updates)}
color="border-emerald-200 bg-emerald-50/50"
/>
2026-01-09 14:41:27 +09:00
<AreaDropZone
area="column"
label="열 그룹"
description="가로로 펼칠 항목 (예: 월, 분기, 연도)"
icon={<Columns className="h-4 w-4 text-blue-600" />}
fields={getFieldsByArea("column")}
columns={columns}
onAddField={(col) => handleAddField("column", col)}
onRemoveField={(idx) => handleRemoveField("column", idx)}
onUpdateField={(idx, updates) => handleUpdateField("column", idx, updates)}
color="border-blue-200 bg-blue-50/50"
/>
2026-01-09 14:41:27 +09:00
<AreaDropZone
area="data"
label="값 (집계)"
description="합계, 평균 등을 계산할 숫자 항목 (예: 매출, 수량)"
icon={<Calculator className="h-4 w-4 text-amber-600" />}
fields={getFieldsByArea("data")}
columns={columns}
onAddField={(col) => handleAddField("data", col)}
onRemoveField={(idx) => handleRemoveField("data", idx)}
onUpdateField={(idx, updates) => handleUpdateField("data", idx, updates)}
color="border-amber-200 bg-amber-50/50"
/>
</div>
</div>
2026-01-09 14:41:27 +09:00
)}
2026-01-09 14:41:27 +09:00
{/* 고급 설정 토글 */}
<div className="pt-2">
<Button
variant="ghost"
size="sm"
className="w-full justify-between"
onClick={() => setShowAdvanced(!showAdvanced)}
>
<div className="flex items-center gap-2">
<Settings className="h-4 w-4" />
<span> </span>
</div>
2026-01-09 14:41:27 +09:00
{showAdvanced ? <ChevronUp className="h-4 w-4" /> : <ChevronDown className="h-4 w-4" />}
</Button>
</div>
2026-01-09 14:41:27 +09:00
{/* 고급 설정 */}
{showAdvanced && (
<div className="space-y-4 pt-2 border-t">
{/* 표시 설정 */}
<div className="space-y-3">
<Label className="text-xs font-medium text-muted-foreground"> </Label>
<div className="grid grid-cols-2 gap-2">
<div className="flex items-center justify-between p-2 rounded-md bg-muted/30">
<Label className="text-xs"> </Label>
<Switch
checked={config.totals?.showRowGrandTotals !== false}
onCheckedChange={(v) =>
updateConfig({ totals: { ...config.totals, showRowGrandTotals: v } })
}
2026-01-09 14:41:27 +09:00
/>
</div>
2026-01-09 14:41:27 +09:00
<div className="flex items-center justify-between p-2 rounded-md bg-muted/30">
<Label className="text-xs"> </Label>
<Switch
checked={config.totals?.showColumnGrandTotals !== false}
onCheckedChange={(v) =>
updateConfig({ totals: { ...config.totals, showColumnGrandTotals: v } })
}
/>
</div>
2026-01-09 14:41:27 +09:00
<div className="flex items-center justify-between p-2 rounded-md bg-muted/30">
<Label className="text-xs"> </Label>
<Select
value={config.totals?.rowGrandTotalPosition || "bottom"}
onValueChange={(v) =>
updateConfig({ totals: { ...config.totals, rowGrandTotalPosition: v as "top" | "bottom" } })
}
>
<SelectTrigger className="h-6 w-16 text-xs">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="top"></SelectItem>
<SelectItem value="bottom"></SelectItem>
</SelectContent>
</Select>
</div>
<div className="flex items-center justify-between p-2 rounded-md bg-muted/30">
<Label className="text-xs"> </Label>
<Select
value={config.totals?.columnGrandTotalPosition || "right"}
onValueChange={(v) =>
updateConfig({ totals: { ...config.totals, columnGrandTotalPosition: v as "left" | "right" } })
}
>
<SelectTrigger className="h-6 w-16 text-xs">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="left"></SelectItem>
<SelectItem value="right"></SelectItem>
</SelectContent>
</Select>
</div>
<div className="flex items-center justify-between p-2 rounded-md bg-muted/30">
<Label className="text-xs"> </Label>
<Switch
checked={config.totals?.showRowTotals !== false}
onCheckedChange={(v) =>
updateConfig({ totals: { ...config.totals, showRowTotals: v } })
}
/>
</div>
<div className="flex items-center justify-between p-2 rounded-md bg-muted/30">
<Label className="text-xs"> </Label>
<Switch
checked={config.totals?.showColumnTotals !== false}
onCheckedChange={(v) =>
updateConfig({ totals: { ...config.totals, showColumnTotals: v } })
}
/>
</div>
2026-01-09 14:41:27 +09:00
<div className="flex items-center justify-between p-2 rounded-md bg-muted/30">
<Label className="text-xs"></Label>
<Switch
checked={config.style?.alternateRowColors !== false}
onCheckedChange={(v) =>
updateConfig({ style: { ...config.style, alternateRowColors: v } })
}
/>
</div>
<div className="flex items-center justify-between p-2 rounded-md bg-muted/30">
<Label className="text-xs"> </Label>
<Switch
checked={config.style?.mergeCells === true}
onCheckedChange={(v) =>
updateConfig({ style: { ...config.style, mergeCells: v } })
}
/>
</div>
2026-01-09 14:41:27 +09:00
<div className="flex items-center justify-between p-2 rounded-md bg-muted/30">
<Label className="text-xs">CSV </Label>
<Switch
2026-01-09 14:41:27 +09:00
checked={config.exportConfig?.excel === true}
onCheckedChange={(v) =>
2026-01-09 14:41:27 +09:00
updateConfig({ exportConfig: { ...config.exportConfig, excel: v } })
}
/>
</div>
<div className="flex items-center justify-between p-2 rounded-md bg-muted/30">
<Label className="text-xs"> </Label>
<Switch
checked={config.saveState === true}
onCheckedChange={(v) =>
updateConfig({ saveState: v })
}
/>
</div>
</div>
</div>
2026-01-09 14:41:27 +09:00
{/* 크기 설정 */}
<div className="space-y-2">
<Label className="text-xs font-medium text-muted-foreground"> </Label>
<div className="grid grid-cols-2 gap-2">
<div>
<Label className="text-xs"></Label>
<Input
value={config.height || ""}
onChange={(e) => updateConfig({ height: e.target.value })}
placeholder="400px"
className="h-8 text-xs"
/>
</div>
<div>
<Label className="text-xs"> </Label>
<Input
value={config.maxHeight || ""}
onChange={(e) => updateConfig({ maxHeight: e.target.value })}
placeholder="600px"
className="h-8 text-xs"
/>
</div>
</div>
</div>
{/* 조건부 서식 */}
<div className="space-y-2">
<Label className="text-xs font-medium text-muted-foreground"> </Label>
<div className="space-y-2">
{(config.style?.conditionalFormats || []).map((rule, index) => (
<div key={rule.id} className="flex items-center gap-2 p-2 rounded-md bg-muted/30">
<Select
value={rule.type}
onValueChange={(v) => {
const newFormats = [...(config.style?.conditionalFormats || [])];
newFormats[index] = { ...rule, type: v as any };
updateConfig({ style: { ...config.style, conditionalFormats: newFormats } });
}}
>
<SelectTrigger className="h-7 w-24 text-xs">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="colorScale"> </SelectItem>
<SelectItem value="dataBar"> </SelectItem>
<SelectItem value="iconSet"> </SelectItem>
<SelectItem value="cellValue"> </SelectItem>
</SelectContent>
</Select>
{rule.type === "colorScale" && (
<div className="flex items-center gap-1">
<input
type="color"
value={rule.colorScale?.minColor || "#ff0000"}
onChange={(e) => {
const newFormats = [...(config.style?.conditionalFormats || [])];
newFormats[index] = { ...rule, colorScale: { ...rule.colorScale, minColor: e.target.value, maxColor: rule.colorScale?.maxColor || "#00ff00" } };
updateConfig({ style: { ...config.style, conditionalFormats: newFormats } });
}}
className="w-6 h-6 rounded cursor-pointer"
title="최소값 색상"
/>
<span className="text-xs"></span>
<input
type="color"
value={rule.colorScale?.maxColor || "#00ff00"}
onChange={(e) => {
const newFormats = [...(config.style?.conditionalFormats || [])];
newFormats[index] = { ...rule, colorScale: { ...rule.colorScale, minColor: rule.colorScale?.minColor || "#ff0000", maxColor: e.target.value } };
updateConfig({ style: { ...config.style, conditionalFormats: newFormats } });
}}
className="w-6 h-6 rounded cursor-pointer"
title="최대값 색상"
/>
</div>
)}
{rule.type === "dataBar" && (
<input
type="color"
value={rule.dataBar?.color || "#3b82f6"}
onChange={(e) => {
const newFormats = [...(config.style?.conditionalFormats || [])];
newFormats[index] = { ...rule, dataBar: { color: e.target.value } };
updateConfig({ style: { ...config.style, conditionalFormats: newFormats } });
}}
className="w-6 h-6 rounded cursor-pointer"
title="바 색상"
/>
)}
{rule.type === "iconSet" && (
<Select
value={rule.iconSet?.type || "traffic"}
onValueChange={(v) => {
const newFormats = [...(config.style?.conditionalFormats || [])];
newFormats[index] = { ...rule, iconSet: { type: v as any, thresholds: [33, 67] } };
updateConfig({ style: { ...config.style, conditionalFormats: newFormats } });
}}
>
<SelectTrigger className="h-7 w-20 text-xs">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="arrows"></SelectItem>
<SelectItem value="traffic"></SelectItem>
<SelectItem value="rating"></SelectItem>
<SelectItem value="flags"></SelectItem>
</SelectContent>
</Select>
)}
<Button
variant="ghost"
size="icon"
className="h-6 w-6 ml-auto"
onClick={() => {
const newFormats = (config.style?.conditionalFormats || []).filter((_, i) => i !== index);
updateConfig({ style: { ...config.style, conditionalFormats: newFormats } });
}}
>
<X className="h-3 w-3" />
</Button>
</div>
))}
<Button
variant="outline"
size="sm"
className="w-full"
onClick={() => {
const newFormats = [
...(config.style?.conditionalFormats || []),
{ id: `cf_${Date.now()}`, type: "colorScale" as const, colorScale: { minColor: "#ff0000", maxColor: "#00ff00" } }
];
updateConfig({ style: { ...config.style, conditionalFormats: newFormats } });
}}
>
<Plus className="h-3 w-3 mr-1" />
</Button>
</div>
</div>
</div>
2026-01-09 14:41:27 +09:00
)}
</div>
);
};
export default PivotGridConfigPanel;