그리드? 일단 추가랑 복사기능 되게 했음
This commit is contained in:
@@ -0,0 +1,751 @@
|
||||
"use client";
|
||||
|
||||
/**
|
||||
* PivotGrid 설정 패널
|
||||
* 화면 관리에서 PivotGrid 컴포넌트를 설정하는 UI
|
||||
*/
|
||||
|
||||
import React, { useState, useEffect, useCallback } from "react";
|
||||
import { cn } from "@/lib/utils";
|
||||
import {
|
||||
PivotGridComponentConfig,
|
||||
PivotFieldConfig,
|
||||
PivotAreaType,
|
||||
AggregationType,
|
||||
DateGroupInterval,
|
||||
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 { Separator } from "@/components/ui/separator";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@/components/ui/select";
|
||||
import {
|
||||
Accordion,
|
||||
AccordionContent,
|
||||
AccordionItem,
|
||||
AccordionTrigger,
|
||||
} from "@/components/ui/accordion";
|
||||
import {
|
||||
Plus,
|
||||
Trash2,
|
||||
GripVertical,
|
||||
Settings2,
|
||||
Rows,
|
||||
Columns,
|
||||
Database,
|
||||
Filter,
|
||||
ChevronUp,
|
||||
ChevronDown,
|
||||
} from "lucide-react";
|
||||
import { apiClient } from "@/lib/api/client";
|
||||
|
||||
// ==================== 타입 ====================
|
||||
|
||||
interface TableInfo {
|
||||
table_name: string;
|
||||
table_comment?: string;
|
||||
}
|
||||
|
||||
interface ColumnInfo {
|
||||
column_name: string;
|
||||
data_type: string;
|
||||
column_comment?: string;
|
||||
is_nullable: string;
|
||||
}
|
||||
|
||||
interface PivotGridConfigPanelProps {
|
||||
config: PivotGridComponentConfig;
|
||||
onChange: (config: PivotGridComponentConfig) => void;
|
||||
}
|
||||
|
||||
// ==================== 유틸리티 ====================
|
||||
|
||||
const AREA_LABELS: Record<PivotAreaType, { label: string; icon: React.ReactNode }> = {
|
||||
row: { label: "행 영역", icon: <Rows className="h-4 w-4" /> },
|
||||
column: { label: "열 영역", icon: <Columns className="h-4 w-4" /> },
|
||||
data: { label: "데이터 영역", icon: <Database className="h-4 w-4" /> },
|
||||
filter: { label: "필터 영역", icon: <Filter className="h-4 w-4" /> },
|
||||
};
|
||||
|
||||
const AGGREGATION_OPTIONS: { value: AggregationType; label: string }[] = [
|
||||
{ value: "sum", label: "합계" },
|
||||
{ value: "count", label: "개수" },
|
||||
{ value: "avg", label: "평균" },
|
||||
{ value: "min", label: "최소" },
|
||||
{ value: "max", label: "최대" },
|
||||
{ value: "countDistinct", label: "고유값 개수" },
|
||||
];
|
||||
|
||||
const DATE_GROUP_OPTIONS: { value: DateGroupInterval; label: string }[] = [
|
||||
{ value: "year", label: "연도" },
|
||||
{ value: "quarter", label: "분기" },
|
||||
{ value: "month", label: "월" },
|
||||
{ value: "week", label: "주" },
|
||||
{ value: "day", label: "일" },
|
||||
];
|
||||
|
||||
const DATA_TYPE_OPTIONS: { value: FieldDataType; label: string }[] = [
|
||||
{ value: "string", label: "문자열" },
|
||||
{ value: "number", label: "숫자" },
|
||||
{ value: "date", label: "날짜" },
|
||||
{ value: "boolean", label: "부울" },
|
||||
];
|
||||
|
||||
// DB 타입을 FieldDataType으로 변환
|
||||
function mapDbTypeToFieldType(dbType: string): FieldDataType {
|
||||
const type = dbType.toLowerCase();
|
||||
if (
|
||||
type.includes("int") ||
|
||||
type.includes("numeric") ||
|
||||
type.includes("decimal") ||
|
||||
type.includes("float") ||
|
||||
type.includes("double") ||
|
||||
type.includes("real")
|
||||
) {
|
||||
return "number";
|
||||
}
|
||||
if (
|
||||
type.includes("date") ||
|
||||
type.includes("time") ||
|
||||
type.includes("timestamp")
|
||||
) {
|
||||
return "date";
|
||||
}
|
||||
if (type.includes("bool")) {
|
||||
return "boolean";
|
||||
}
|
||||
return "string";
|
||||
}
|
||||
|
||||
// ==================== 필드 설정 컴포넌트 ====================
|
||||
|
||||
interface FieldConfigItemProps {
|
||||
field: PivotFieldConfig;
|
||||
index: number;
|
||||
onChange: (field: PivotFieldConfig) => void;
|
||||
onRemove: () => void;
|
||||
onMoveUp: () => void;
|
||||
onMoveDown: () => void;
|
||||
isFirst: boolean;
|
||||
isLast: boolean;
|
||||
}
|
||||
|
||||
const FieldConfigItem: React.FC<FieldConfigItemProps> = ({
|
||||
field,
|
||||
index,
|
||||
onChange,
|
||||
onRemove,
|
||||
onMoveUp,
|
||||
onMoveDown,
|
||||
isFirst,
|
||||
isLast,
|
||||
}) => {
|
||||
return (
|
||||
<div className="flex items-start gap-2 p-2 rounded border border-border bg-background">
|
||||
{/* 드래그 핸들 & 순서 버튼 */}
|
||||
<div className="flex flex-col items-center gap-0.5 pt-1">
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="h-5 w-5"
|
||||
onClick={onMoveUp}
|
||||
disabled={isFirst}
|
||||
>
|
||||
<ChevronUp className="h-3 w-3" />
|
||||
</Button>
|
||||
<GripVertical className="h-4 w-4 text-muted-foreground" />
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="h-5 w-5"
|
||||
onClick={onMoveDown}
|
||||
disabled={isLast}
|
||||
>
|
||||
<ChevronDown className="h-3 w-3" />
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{/* 필드 설정 */}
|
||||
<div className="flex-1 space-y-2">
|
||||
{/* 필드명 & 라벨 */}
|
||||
<div className="flex gap-2">
|
||||
<div className="flex-1">
|
||||
<Label className="text-xs">필드명</Label>
|
||||
<Input
|
||||
value={field.field}
|
||||
onChange={(e) => onChange({ ...field, field: e.target.value })}
|
||||
placeholder="column_name"
|
||||
className="h-8 text-xs"
|
||||
/>
|
||||
</div>
|
||||
<div className="flex-1">
|
||||
<Label className="text-xs">표시 라벨</Label>
|
||||
<Input
|
||||
value={field.caption}
|
||||
onChange={(e) => onChange({ ...field, caption: e.target.value })}
|
||||
placeholder="표시명"
|
||||
className="h-8 text-xs"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 데이터 타입 & 집계 함수 */}
|
||||
<div className="flex gap-2">
|
||||
<div className="flex-1">
|
||||
<Label className="text-xs">데이터 타입</Label>
|
||||
<Select
|
||||
value={field.dataType || "string"}
|
||||
onValueChange={(v) =>
|
||||
onChange({ ...field, dataType: v as FieldDataType })
|
||||
}
|
||||
>
|
||||
<SelectTrigger className="h-8 text-xs">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{DATA_TYPE_OPTIONS.map((opt) => (
|
||||
<SelectItem key={opt.value} value={opt.value}>
|
||||
{opt.label}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
{field.area === "data" && (
|
||||
<div className="flex-1">
|
||||
<Label className="text-xs">집계 함수</Label>
|
||||
<Select
|
||||
value={field.summaryType || "sum"}
|
||||
onValueChange={(v) =>
|
||||
onChange({ ...field, summaryType: v as AggregationType })
|
||||
}
|
||||
>
|
||||
<SelectTrigger className="h-8 text-xs">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{AGGREGATION_OPTIONS.map((opt) => (
|
||||
<SelectItem key={opt.value} value={opt.value}>
|
||||
{opt.label}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{field.dataType === "date" &&
|
||||
(field.area === "row" || field.area === "column") && (
|
||||
<div className="flex-1">
|
||||
<Label className="text-xs">그룹 단위</Label>
|
||||
<Select
|
||||
value={field.groupInterval || "__none__"}
|
||||
onValueChange={(v) =>
|
||||
onChange({
|
||||
...field,
|
||||
groupInterval:
|
||||
v === "__none__" ? undefined : (v as DateGroupInterval),
|
||||
})
|
||||
}
|
||||
>
|
||||
<SelectTrigger className="h-8 text-xs">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="__none__">원본값</SelectItem>
|
||||
{DATE_GROUP_OPTIONS.map((opt) => (
|
||||
<SelectItem key={opt.value} value={opt.value}>
|
||||
{opt.label}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 삭제 버튼 */}
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="h-8 w-8 text-destructive hover:text-destructive"
|
||||
onClick={onRemove}
|
||||
>
|
||||
<Trash2 className="h-4 w-4" />
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
// ==================== 영역별 필드 목록 ====================
|
||||
|
||||
interface AreaFieldListProps {
|
||||
area: PivotAreaType;
|
||||
fields: PivotFieldConfig[];
|
||||
allColumns: ColumnInfo[];
|
||||
onFieldsChange: (fields: PivotFieldConfig[]) => void;
|
||||
}
|
||||
|
||||
const AreaFieldList: React.FC<AreaFieldListProps> = ({
|
||||
area,
|
||||
fields,
|
||||
allColumns,
|
||||
onFieldsChange,
|
||||
}) => {
|
||||
const areaFields = fields.filter((f) => f.area === area);
|
||||
const { label, icon } = AREA_LABELS[area];
|
||||
|
||||
const handleAddField = () => {
|
||||
const newField: PivotFieldConfig = {
|
||||
field: "",
|
||||
caption: "",
|
||||
area,
|
||||
areaIndex: areaFields.length,
|
||||
dataType: "string",
|
||||
visible: true,
|
||||
};
|
||||
if (area === "data") {
|
||||
newField.summaryType = "sum";
|
||||
}
|
||||
onFieldsChange([...fields, newField]);
|
||||
};
|
||||
|
||||
const handleAddFromColumn = (column: ColumnInfo) => {
|
||||
const dataType = mapDbTypeToFieldType(column.data_type);
|
||||
const newField: PivotFieldConfig = {
|
||||
field: column.column_name,
|
||||
caption: column.column_comment || column.column_name,
|
||||
area,
|
||||
areaIndex: areaFields.length,
|
||||
dataType,
|
||||
visible: true,
|
||||
};
|
||||
if (area === "data") {
|
||||
newField.summaryType = "sum";
|
||||
}
|
||||
onFieldsChange([...fields, newField]);
|
||||
};
|
||||
|
||||
const handleFieldChange = (index: number, updatedField: PivotFieldConfig) => {
|
||||
const newFields = [...fields];
|
||||
const globalIndex = fields.findIndex(
|
||||
(f) => f.area === area && f.areaIndex === index
|
||||
);
|
||||
if (globalIndex >= 0) {
|
||||
newFields[globalIndex] = updatedField;
|
||||
onFieldsChange(newFields);
|
||||
}
|
||||
};
|
||||
|
||||
const handleRemoveField = (index: number) => {
|
||||
const newFields = fields.filter(
|
||||
(f) => !(f.area === area && f.areaIndex === index)
|
||||
);
|
||||
// 인덱스 재정렬
|
||||
let idx = 0;
|
||||
newFields.forEach((f) => {
|
||||
if (f.area === area) {
|
||||
f.areaIndex = idx++;
|
||||
}
|
||||
});
|
||||
onFieldsChange(newFields);
|
||||
};
|
||||
|
||||
const handleMoveField = (fromIndex: number, direction: "up" | "down") => {
|
||||
const toIndex = direction === "up" ? fromIndex - 1 : fromIndex + 1;
|
||||
if (toIndex < 0 || toIndex >= areaFields.length) return;
|
||||
|
||||
const newAreaFields = [...areaFields];
|
||||
const [moved] = newAreaFields.splice(fromIndex, 1);
|
||||
newAreaFields.splice(toIndex, 0, moved);
|
||||
|
||||
// 인덱스 재정렬
|
||||
newAreaFields.forEach((f, idx) => {
|
||||
f.areaIndex = idx;
|
||||
});
|
||||
|
||||
// 전체 필드 업데이트
|
||||
const newFields = fields.filter((f) => f.area !== area);
|
||||
onFieldsChange([...newFields, ...newAreaFields]);
|
||||
};
|
||||
|
||||
// 이미 추가된 컬럼 제외
|
||||
const availableColumns = allColumns.filter(
|
||||
(col) => !fields.some((f) => f.field === col.column_name)
|
||||
);
|
||||
|
||||
return (
|
||||
<AccordionItem value={area}>
|
||||
<AccordionTrigger className="py-2">
|
||||
<div className="flex items-center gap-2">
|
||||
{icon}
|
||||
<span>{label}</span>
|
||||
<Badge variant="secondary" className="ml-2">
|
||||
{areaFields.length}
|
||||
</Badge>
|
||||
</div>
|
||||
</AccordionTrigger>
|
||||
<AccordionContent className="space-y-2 pt-2">
|
||||
{/* 필드 목록 */}
|
||||
{areaFields
|
||||
.sort((a, b) => (a.areaIndex || 0) - (b.areaIndex || 0))
|
||||
.map((field, idx) => (
|
||||
<FieldConfigItem
|
||||
key={`${field.field}-${idx}`}
|
||||
field={field}
|
||||
index={field.areaIndex || idx}
|
||||
onChange={(f) => handleFieldChange(field.areaIndex || idx, f)}
|
||||
onRemove={() => handleRemoveField(field.areaIndex || idx)}
|
||||
onMoveUp={() => handleMoveField(idx, "up")}
|
||||
onMoveDown={() => handleMoveField(idx, "down")}
|
||||
isFirst={idx === 0}
|
||||
isLast={idx === areaFields.length - 1}
|
||||
/>
|
||||
))}
|
||||
|
||||
{/* 필드 추가 */}
|
||||
<div className="flex gap-2">
|
||||
<Select onValueChange={(v) => {
|
||||
const col = allColumns.find(c => c.column_name === v);
|
||||
if (col) handleAddFromColumn(col);
|
||||
}}>
|
||||
<SelectTrigger className="h-8 text-xs flex-1">
|
||||
<SelectValue placeholder="컬럼 선택" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{availableColumns.length === 0 ? (
|
||||
<SelectItem value="__none__" disabled>
|
||||
추가 가능한 컬럼이 없습니다
|
||||
</SelectItem>
|
||||
) : (
|
||||
availableColumns.map((col) => (
|
||||
<SelectItem key={col.column_name} value={col.column_name}>
|
||||
<div className="flex items-center gap-2">
|
||||
<span>{col.column_name}</span>
|
||||
{col.column_comment && (
|
||||
<span className="text-muted-foreground">
|
||||
({col.column_comment})
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</SelectItem>
|
||||
))
|
||||
)}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
className="h-8"
|
||||
onClick={handleAddField}
|
||||
>
|
||||
<Plus className="h-4 w-4 mr-1" />
|
||||
직접 추가
|
||||
</Button>
|
||||
</div>
|
||||
</AccordionContent>
|
||||
</AccordionItem>
|
||||
);
|
||||
};
|
||||
|
||||
// ==================== 메인 컴포넌트 ====================
|
||||
|
||||
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);
|
||||
|
||||
// 테이블 목록 로드
|
||||
useEffect(() => {
|
||||
const loadTables = async () => {
|
||||
setLoadingTables(true);
|
||||
try {
|
||||
const response = await apiClient.get("/api/table-management/list");
|
||||
if (response.data.success) {
|
||||
setTables(response.data.data || []);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("테이블 목록 로드 실패:", error);
|
||||
} finally {
|
||||
setLoadingTables(false);
|
||||
}
|
||||
};
|
||||
loadTables();
|
||||
}, []);
|
||||
|
||||
// 테이블 선택 시 컬럼 로드
|
||||
useEffect(() => {
|
||||
const loadColumns = async () => {
|
||||
if (!config.dataSource?.tableName) {
|
||||
setColumns([]);
|
||||
return;
|
||||
}
|
||||
|
||||
setLoadingColumns(true);
|
||||
try {
|
||||
const response = await apiClient.get(
|
||||
`/api/table-management/columns/${config.dataSource.tableName}`
|
||||
);
|
||||
if (response.data.success) {
|
||||
setColumns(response.data.data || []);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("컬럼 목록 로드 실패:", error);
|
||||
} finally {
|
||||
setLoadingColumns(false);
|
||||
}
|
||||
};
|
||||
loadColumns();
|
||||
}, [config.dataSource?.tableName]);
|
||||
|
||||
// 설정 업데이트 헬퍼
|
||||
const updateConfig = useCallback(
|
||||
(updates: Partial<PivotGridComponentConfig>) => {
|
||||
onChange({ ...config, ...updates });
|
||||
},
|
||||
[config, onChange]
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
{/* 데이터 소스 설정 */}
|
||||
<div className="space-y-3">
|
||||
<Label className="text-sm font-medium">데이터 소스</Label>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label className="text-xs">테이블 선택</Label>
|
||||
<Select
|
||||
value={config.dataSource?.tableName || "__none__"}
|
||||
onValueChange={(v) =>
|
||||
updateConfig({
|
||||
dataSource: {
|
||||
...config.dataSource,
|
||||
type: "table",
|
||||
tableName: v === "__none__" ? undefined : v,
|
||||
},
|
||||
fields: [], // 테이블 변경 시 필드 초기화
|
||||
})
|
||||
}
|
||||
>
|
||||
<SelectTrigger className="h-9">
|
||||
<SelectValue placeholder="테이블 선택" />
|
||||
</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>{table.table_name}</span>
|
||||
{table.table_comment && (
|
||||
<span className="text-muted-foreground">
|
||||
({table.table_comment})
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Separator />
|
||||
|
||||
{/* 필드 설정 */}
|
||||
{config.dataSource?.tableName && (
|
||||
<div className="space-y-3">
|
||||
<div className="flex items-center justify-between">
|
||||
<Label className="text-sm font-medium">필드 설정</Label>
|
||||
<Badge variant="outline">
|
||||
{columns.length}개 컬럼
|
||||
</Badge>
|
||||
</div>
|
||||
|
||||
{loadingColumns ? (
|
||||
<div className="text-sm text-muted-foreground">
|
||||
컬럼 로딩 중...
|
||||
</div>
|
||||
) : (
|
||||
<Accordion
|
||||
type="multiple"
|
||||
defaultValue={["row", "column", "data"]}
|
||||
className="w-full"
|
||||
>
|
||||
{(["row", "column", "data", "filter"] as PivotAreaType[]).map(
|
||||
(area) => (
|
||||
<AreaFieldList
|
||||
key={area}
|
||||
area={area}
|
||||
fields={config.fields || []}
|
||||
allColumns={columns}
|
||||
onFieldsChange={(fields) => updateConfig({ fields })}
|
||||
/>
|
||||
)
|
||||
)}
|
||||
</Accordion>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<Separator />
|
||||
|
||||
{/* 표시 설정 */}
|
||||
<div className="space-y-3">
|
||||
<Label className="text-sm font-medium">표시 설정</Label>
|
||||
|
||||
<div className="grid grid-cols-2 gap-3">
|
||||
<div className="flex items-center justify-between">
|
||||
<Label className="text-xs">행 총계</Label>
|
||||
<Switch
|
||||
checked={config.totals?.showRowGrandTotals !== false}
|
||||
onCheckedChange={(v) =>
|
||||
updateConfig({
|
||||
totals: { ...config.totals, showRowGrandTotals: v },
|
||||
})
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center justify-between">
|
||||
<Label className="text-xs">열 총계</Label>
|
||||
<Switch
|
||||
checked={config.totals?.showColumnGrandTotals !== false}
|
||||
onCheckedChange={(v) =>
|
||||
updateConfig({
|
||||
totals: { ...config.totals, showColumnGrandTotals: v },
|
||||
})
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center justify-between">
|
||||
<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">
|
||||
<Label className="text-xs">열 소계</Label>
|
||||
<Switch
|
||||
checked={config.totals?.showColumnTotals !== false}
|
||||
onCheckedChange={(v) =>
|
||||
updateConfig({
|
||||
totals: { ...config.totals, showColumnTotals: v },
|
||||
})
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center justify-between">
|
||||
<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">
|
||||
<Label className="text-xs">총계 강조</Label>
|
||||
<Switch
|
||||
checked={config.style?.highlightTotals !== false}
|
||||
onCheckedChange={(v) =>
|
||||
updateConfig({
|
||||
style: { ...config.style, highlightTotals: v },
|
||||
})
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Separator />
|
||||
|
||||
{/* 기능 설정 */}
|
||||
<div className="space-y-3">
|
||||
<Label className="text-sm font-medium">기능 설정</Label>
|
||||
|
||||
<div className="space-y-2">
|
||||
<div className="flex items-center justify-between">
|
||||
<Label className="text-xs">전체 확장/축소 버튼</Label>
|
||||
<Switch
|
||||
checked={config.allowExpandAll !== false}
|
||||
onCheckedChange={(v) =>
|
||||
updateConfig({ allowExpandAll: v })
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center justify-between">
|
||||
<Label className="text-xs">CSV 내보내기</Label>
|
||||
<Switch
|
||||
checked={config.exportConfig?.excel === true}
|
||||
onCheckedChange={(v) =>
|
||||
updateConfig({
|
||||
exportConfig: { ...config.exportConfig, excel: v },
|
||||
})
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Separator />
|
||||
|
||||
{/* 크기 설정 */}
|
||||
<div className="space-y-3">
|
||||
<Label className="text-sm font-medium">크기 설정</Label>
|
||||
|
||||
<div className="grid grid-cols-2 gap-3">
|
||||
<div>
|
||||
<Label className="text-xs">높이</Label>
|
||||
<Input
|
||||
value={config.height || ""}
|
||||
onChange={(e) => updateConfig({ height: e.target.value })}
|
||||
placeholder="auto 또는 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>
|
||||
);
|
||||
};
|
||||
|
||||
export default PivotGridConfigPanel;
|
||||
|
||||
Reference in New Issue
Block a user