lhj #376
|
|
@ -299,6 +299,8 @@ export const PivotGridComponent: React.FC<PivotGridProps> = ({
|
||||||
// ==================== 상태 ====================
|
// ==================== 상태 ====================
|
||||||
|
|
||||||
const [fields, setFields] = useState<PivotFieldConfig[]>(initialFields);
|
const [fields, setFields] = useState<PivotFieldConfig[]>(initialFields);
|
||||||
|
// 초기 필드 설정 저장 (초기화용)
|
||||||
|
const initialFieldsRef = useRef<PivotFieldConfig[]>(initialFields);
|
||||||
const [pivotState, setPivotState] = useState<PivotGridState>({
|
const [pivotState, setPivotState] = useState<PivotGridState>({
|
||||||
expandedRowPaths: [],
|
expandedRowPaths: [],
|
||||||
expandedColumnPaths: [],
|
expandedColumnPaths: [],
|
||||||
|
|
@ -1129,6 +1131,7 @@ export const PivotGridComponent: React.FC<PivotGridProps> = ({
|
||||||
onFieldsChange={handleFieldsChange}
|
onFieldsChange={handleFieldsChange}
|
||||||
collapsed={!showFieldPanel}
|
collapsed={!showFieldPanel}
|
||||||
onToggleCollapse={() => setShowFieldPanel(!showFieldPanel)}
|
onToggleCollapse={() => setShowFieldPanel(!showFieldPanel)}
|
||||||
|
initialFields={initialFieldsRef.current}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{/* 안내 메시지 */}
|
{/* 안내 메시지 */}
|
||||||
|
|
@ -1405,6 +1408,7 @@ export const PivotGridComponent: React.FC<PivotGridProps> = ({
|
||||||
onFieldsChange={handleFieldsChange}
|
onFieldsChange={handleFieldsChange}
|
||||||
collapsed={!showFieldPanel}
|
collapsed={!showFieldPanel}
|
||||||
onToggleCollapse={() => setShowFieldPanel(!showFieldPanel)}
|
onToggleCollapse={() => setShowFieldPanel(!showFieldPanel)}
|
||||||
|
initialFields={initialFieldsRef.current}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{/* 헤더 툴바 */}
|
{/* 헤더 툴바 */}
|
||||||
|
|
|
||||||
|
|
@ -37,6 +37,10 @@ import {
|
||||||
BarChart3,
|
BarChart3,
|
||||||
GripVertical,
|
GripVertical,
|
||||||
ChevronDown,
|
ChevronDown,
|
||||||
|
RotateCcw,
|
||||||
|
FilterX,
|
||||||
|
LayoutGrid,
|
||||||
|
Trash2,
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import {
|
import {
|
||||||
|
|
@ -56,6 +60,8 @@ interface FieldPanelProps {
|
||||||
onFieldSettingsChange?: (field: PivotFieldConfig) => void;
|
onFieldSettingsChange?: (field: PivotFieldConfig) => void;
|
||||||
collapsed?: boolean;
|
collapsed?: boolean;
|
||||||
onToggleCollapse?: () => void;
|
onToggleCollapse?: () => void;
|
||||||
|
/** 초기 필드 설정 (필드 배치 초기화용) */
|
||||||
|
initialFields?: PivotFieldConfig[];
|
||||||
}
|
}
|
||||||
|
|
||||||
interface FieldChipProps {
|
interface FieldChipProps {
|
||||||
|
|
@ -123,15 +129,23 @@ const SortableFieldChip: React.FC<FieldChipProps> = ({
|
||||||
transition,
|
transition,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// 필터 적용 여부 확인
|
||||||
|
const hasFilter = field.filterValues && field.filterValues.length > 0;
|
||||||
|
const filterCount = field.filterValues?.length || 0;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
ref={setNodeRef}
|
ref={setNodeRef}
|
||||||
style={style}
|
style={style}
|
||||||
className={cn(
|
className={cn(
|
||||||
"inline-flex items-center gap-1 px-2 py-1 rounded-md text-xs",
|
"inline-flex items-center gap-1 px-2 py-1 rounded-md text-xs",
|
||||||
"bg-background border border-border shadow-sm",
|
"bg-background border shadow-sm",
|
||||||
"hover:bg-accent/50 transition-colors",
|
"hover:bg-accent/50 transition-colors",
|
||||||
isDragging && "opacity-50 shadow-lg"
|
isDragging && "opacity-50 shadow-lg",
|
||||||
|
// 필터 적용 시 강조 표시
|
||||||
|
hasFilter
|
||||||
|
? "border-primary bg-primary/5"
|
||||||
|
: "border-border"
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{/* 드래그 핸들 */}
|
{/* 드래그 핸들 */}
|
||||||
|
|
@ -143,11 +157,24 @@ const SortableFieldChip: React.FC<FieldChipProps> = ({
|
||||||
<GripVertical className="h-3 w-3" />
|
<GripVertical className="h-3 w-3" />
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
|
{/* 필터 아이콘 (필터 적용 시) */}
|
||||||
|
{hasFilter && (
|
||||||
|
<Filter className="h-3 w-3 text-primary" />
|
||||||
|
)}
|
||||||
|
|
||||||
{/* 필드 라벨 */}
|
{/* 필드 라벨 */}
|
||||||
<DropdownMenu>
|
<DropdownMenu>
|
||||||
<DropdownMenuTrigger asChild>
|
<DropdownMenuTrigger asChild>
|
||||||
<button className="flex items-center gap-1 hover:text-primary">
|
<button className="flex items-center gap-1 hover:text-primary">
|
||||||
<span className="font-medium">{field.caption}</span>
|
<span className={cn("font-medium", hasFilter && "text-primary")}>
|
||||||
|
{field.caption}
|
||||||
|
</span>
|
||||||
|
{/* 필터 적용 개수 배지 */}
|
||||||
|
{hasFilter && (
|
||||||
|
<span className="bg-primary text-primary-foreground text-[10px] px-1 rounded">
|
||||||
|
{filterCount}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
{field.area === "data" && field.summaryType && (
|
{field.area === "data" && field.summaryType && (
|
||||||
<span className="text-muted-foreground">
|
<span className="text-muted-foreground">
|
||||||
({getSummaryLabel(field.summaryType)})
|
({getSummaryLabel(field.summaryType)})
|
||||||
|
|
@ -208,6 +235,19 @@ const SortableFieldChip: React.FC<FieldChipProps> = ({
|
||||||
{field.sortOrder === "asc" ? "내림차순 정렬" : "오름차순 정렬"}
|
{field.sortOrder === "asc" ? "내림차순 정렬" : "오름차순 정렬"}
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
<DropdownMenuSeparator />
|
<DropdownMenuSeparator />
|
||||||
|
{/* 필터 초기화 (필터가 적용된 경우에만 표시) */}
|
||||||
|
{hasFilter && (
|
||||||
|
<>
|
||||||
|
<DropdownMenuItem
|
||||||
|
onClick={() => onSettingsChange?.({ ...field, filterValues: [] })}
|
||||||
|
className="text-orange-600"
|
||||||
|
>
|
||||||
|
<Filter className="h-3 w-3 mr-2" />
|
||||||
|
필터 초기화 ({filterCount}개 선택됨)
|
||||||
|
</DropdownMenuItem>
|
||||||
|
<DropdownMenuSeparator />
|
||||||
|
</>
|
||||||
|
)}
|
||||||
<DropdownMenuItem
|
<DropdownMenuItem
|
||||||
onClick={() => onSettingsChange?.({ ...field, visible: false })}
|
onClick={() => onSettingsChange?.({ ...field, visible: false })}
|
||||||
>
|
>
|
||||||
|
|
@ -326,10 +366,73 @@ export const FieldPanel: React.FC<FieldPanelProps> = ({
|
||||||
onFieldSettingsChange,
|
onFieldSettingsChange,
|
||||||
collapsed = false,
|
collapsed = false,
|
||||||
onToggleCollapse,
|
onToggleCollapse,
|
||||||
|
initialFields,
|
||||||
}) => {
|
}) => {
|
||||||
const [activeId, setActiveId] = useState<string | null>(null);
|
const [activeId, setActiveId] = useState<string | null>(null);
|
||||||
const [overArea, setOverArea] = useState<PivotAreaType | null>(null);
|
const [overArea, setOverArea] = useState<PivotAreaType | null>(null);
|
||||||
|
|
||||||
|
// 필터만 초기화
|
||||||
|
const handleResetFilters = () => {
|
||||||
|
const newFields = fields.map((f) => ({
|
||||||
|
...f,
|
||||||
|
filterValues: [],
|
||||||
|
filterType: "include" as const,
|
||||||
|
}));
|
||||||
|
onFieldsChange(newFields);
|
||||||
|
};
|
||||||
|
|
||||||
|
// 필드 배치 초기화 (initialFields가 있으면 사용, 없으면 모든 필드를 row로)
|
||||||
|
const handleResetLayout = () => {
|
||||||
|
if (initialFields && initialFields.length > 0) {
|
||||||
|
// initialFields의 영역 배치를 복원하되 현재 필터 값은 유지
|
||||||
|
const newFields = fields.map((f) => {
|
||||||
|
const initial = initialFields.find((i) => i.field === f.field);
|
||||||
|
if (initial) {
|
||||||
|
return {
|
||||||
|
...f,
|
||||||
|
area: initial.area,
|
||||||
|
areaIndex: initial.areaIndex,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return f;
|
||||||
|
});
|
||||||
|
onFieldsChange(newFields);
|
||||||
|
} else {
|
||||||
|
// 기본값: 숫자는 data, 나머지는 row로
|
||||||
|
const newFields = fields.map((f, idx) => ({
|
||||||
|
...f,
|
||||||
|
area: f.dataType === "number" ? "data" : "row" as PivotAreaType,
|
||||||
|
areaIndex: idx,
|
||||||
|
visible: true,
|
||||||
|
}));
|
||||||
|
onFieldsChange(newFields);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 전체 초기화 (필드 배치 + 필터)
|
||||||
|
const handleResetAll = () => {
|
||||||
|
if (initialFields && initialFields.length > 0) {
|
||||||
|
// initialFields로 완전히 복원
|
||||||
|
onFieldsChange([...initialFields]);
|
||||||
|
} else {
|
||||||
|
// 기본값으로 초기화
|
||||||
|
const newFields = fields.map((f, idx) => ({
|
||||||
|
...f,
|
||||||
|
area: f.dataType === "number" ? "data" : "row" as PivotAreaType,
|
||||||
|
areaIndex: idx,
|
||||||
|
visible: true,
|
||||||
|
filterValues: [],
|
||||||
|
filterType: "include" as const,
|
||||||
|
}));
|
||||||
|
onFieldsChange(newFields);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 필터가 적용된 필드 개수
|
||||||
|
const filteredFieldCount = fields.filter(
|
||||||
|
(f) => f.filterValues && f.filterValues.length > 0
|
||||||
|
).length;
|
||||||
|
|
||||||
const sensors = useSensors(
|
const sensors = useSensors(
|
||||||
useSensor(PointerSensor, {
|
useSensor(PointerSensor, {
|
||||||
activationConstraint: {
|
activationConstraint: {
|
||||||
|
|
@ -576,19 +679,60 @@ export const FieldPanel: React.FC<FieldPanelProps> = ({
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* 접기 버튼 */}
|
{/* 하단 버튼 영역 */}
|
||||||
{onToggleCollapse && (
|
<div className="flex items-center justify-between mt-1.5">
|
||||||
<div className="flex justify-center mt-1.5">
|
{/* 초기화 드롭다운 */}
|
||||||
|
<DropdownMenu>
|
||||||
|
<DropdownMenuTrigger asChild>
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="sm"
|
||||||
|
className="text-xs h-6 px-2 text-muted-foreground hover:text-foreground"
|
||||||
|
>
|
||||||
|
<RotateCcw className="h-3 w-3 mr-1" />
|
||||||
|
초기화
|
||||||
|
{filteredFieldCount > 0 && (
|
||||||
|
<span className="ml-1 bg-orange-500 text-white text-[10px] px-1 rounded">
|
||||||
|
{filteredFieldCount}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
<ChevronDown className="h-3 w-3 ml-1" />
|
||||||
|
</Button>
|
||||||
|
</DropdownMenuTrigger>
|
||||||
|
<DropdownMenuContent align="start" className="w-48">
|
||||||
|
<DropdownMenuItem onClick={handleResetFilters}>
|
||||||
|
<FilterX className="h-3.5 w-3.5 mr-2 text-orange-500" />
|
||||||
|
필터만 초기화
|
||||||
|
{filteredFieldCount > 0 && (
|
||||||
|
<span className="ml-auto text-xs text-muted-foreground">
|
||||||
|
({filteredFieldCount}개)
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</DropdownMenuItem>
|
||||||
|
<DropdownMenuItem onClick={handleResetLayout}>
|
||||||
|
<LayoutGrid className="h-3.5 w-3.5 mr-2 text-blue-500" />
|
||||||
|
필드 배치 초기화
|
||||||
|
</DropdownMenuItem>
|
||||||
|
<DropdownMenuSeparator />
|
||||||
|
<DropdownMenuItem onClick={handleResetAll} className="text-destructive">
|
||||||
|
<Trash2 className="h-3.5 w-3.5 mr-2" />
|
||||||
|
전체 초기화
|
||||||
|
</DropdownMenuItem>
|
||||||
|
</DropdownMenuContent>
|
||||||
|
</DropdownMenu>
|
||||||
|
|
||||||
|
{/* 접기 버튼 */}
|
||||||
|
{onToggleCollapse && (
|
||||||
<Button
|
<Button
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
size="sm"
|
size="sm"
|
||||||
onClick={onToggleCollapse}
|
onClick={onToggleCollapse}
|
||||||
className="text-xs h-5 px-2"
|
className="text-xs h-6 px-2"
|
||||||
>
|
>
|
||||||
필드 패널 접기
|
필드 패널 접기
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
)}
|
||||||
)}
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* 드래그 오버레이 */}
|
{/* 드래그 오버레이 */}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue