= ({
field,
onRemove,
onSettingsChange,
}) => {
const {
attributes,
listeners,
setNodeRef,
transform,
transition,
isDragging,
} = useSortable({ id: `${field.area}-${field.field}` });
const style = {
transform: CSS.Transform.toString(transform),
transition,
};
return (
{/* 드래그 핸들 */}
{/* 필드 라벨 */}
{field.area === "data" && (
<>
onSettingsChange?.({ ...field, summaryType: "sum" })
}
>
합계
onSettingsChange?.({ ...field, summaryType: "count" })
}
>
개수
onSettingsChange?.({ ...field, summaryType: "avg" })
}
>
평균
onSettingsChange?.({ ...field, summaryType: "min" })
}
>
최소
onSettingsChange?.({ ...field, summaryType: "max" })
}
>
최대
>
)}
onSettingsChange?.({
...field,
sortOrder: field.sortOrder === "asc" ? "desc" : "asc",
})
}
>
{field.sortOrder === "asc" ? "내림차순 정렬" : "오름차순 정렬"}
onSettingsChange?.({ ...field, visible: false })}
>
필드 숨기기
{/* 삭제 버튼 */}
);
};
// ==================== 드롭 영역 ====================
const DroppableArea: React.FC = ({
area,
fields,
title,
icon,
onFieldRemove,
onFieldSettingsChange,
isOver,
}) => {
const config = AREA_CONFIG[area];
const areaFields = fields.filter((f) => f.area === area && f.visible !== false);
const fieldIds = areaFields.map((f) => `${area}-${f.field}`);
return (
{/* 영역 헤더 */}
{icon}
{title}
{areaFields.length > 0 && (
{areaFields.length}
)}
{/* 필드 목록 */}
{areaFields.length === 0 ? (
필드를 여기로 드래그
) : (
areaFields.map((field) => (
onFieldRemove(field)}
onSettingsChange={onFieldSettingsChange}
/>
))
)}
);
};
// ==================== 유틸리티 ====================
function getSummaryLabel(type: string): string {
const labels: Record = {
sum: "합계",
count: "개수",
avg: "평균",
min: "최소",
max: "최대",
countDistinct: "고유",
};
return labels[type] || type;
}
// ==================== 메인 컴포넌트 ====================
export const FieldPanel: React.FC = ({
fields,
onFieldsChange,
onFieldRemove,
onFieldSettingsChange,
collapsed = false,
onToggleCollapse,
}) => {
const [activeId, setActiveId] = useState(null);
const [overArea, setOverArea] = useState(null);
const sensors = useSensors(
useSensor(PointerSensor, {
activationConstraint: {
distance: 8,
},
}),
useSensor(KeyboardSensor, {
coordinateGetter: sortableKeyboardCoordinates,
})
);
// 드래그 시작
const handleDragStart = (event: DragStartEvent) => {
setActiveId(event.active.id as string);
};
// 드래그 오버
const handleDragOver = (event: DragOverEvent) => {
const { over } = event;
if (!over) {
setOverArea(null);
return;
}
// 드롭 영역 감지
const overId = over.id as string;
const targetArea = overId.split("-")[0] as PivotAreaType;
if (["filter", "column", "row", "data"].includes(targetArea)) {
setOverArea(targetArea);
}
};
// 드래그 종료
const handleDragEnd = (event: DragEndEvent) => {
const { active, over } = event;
setActiveId(null);
setOverArea(null);
if (!over) return;
const activeId = active.id as string;
const overId = over.id as string;
// 필드 정보 파싱
const [sourceArea, sourceField] = activeId.split("-") as [
PivotAreaType,
string
];
const [targetArea] = overId.split("-") as [PivotAreaType, string];
// 같은 영역 내 정렬
if (sourceArea === targetArea) {
const areaFields = fields.filter((f) => f.area === sourceArea);
const sourceIndex = areaFields.findIndex((f) => f.field === sourceField);
const targetIndex = areaFields.findIndex(
(f) => `${f.area}-${f.field}` === overId
);
if (sourceIndex !== targetIndex && targetIndex >= 0) {
// 순서 변경
const newFields = [...fields];
const fieldToMove = newFields.find(
(f) => f.field === sourceField && f.area === sourceArea
);
if (fieldToMove) {
fieldToMove.areaIndex = targetIndex;
// 다른 필드들 인덱스 조정
newFields
.filter((f) => f.area === sourceArea && f.field !== sourceField)
.sort((a, b) => (a.areaIndex || 0) - (b.areaIndex || 0))
.forEach((f, idx) => {
f.areaIndex = idx >= targetIndex ? idx + 1 : idx;
});
}
onFieldsChange(newFields);
}
return;
}
// 다른 영역으로 이동
if (["filter", "column", "row", "data"].includes(targetArea)) {
const newFields = fields.map((f) => {
if (f.field === sourceField && f.area === sourceArea) {
return {
...f,
area: targetArea as PivotAreaType,
areaIndex: fields.filter((ff) => ff.area === targetArea).length,
};
}
return f;
});
onFieldsChange(newFields);
}
};
// 필드 제거
const handleFieldRemove = (field: PivotFieldConfig) => {
if (onFieldRemove) {
onFieldRemove(field);
} else {
// 기본 동작: visible을 false로 설정
const newFields = fields.map((f) =>
f.field === field.field && f.area === field.area
? { ...f, visible: false }
: f
);
onFieldsChange(newFields);
}
};
// 필드 설정 변경
const handleFieldSettingsChange = (updatedField: PivotFieldConfig) => {
if (onFieldSettingsChange) {
onFieldSettingsChange(updatedField);
}
const newFields = fields.map((f) =>
f.field === updatedField.field && f.area === updatedField.area
? updatedField
: f
);
onFieldsChange(newFields);
};
// 활성 필드 찾기 (드래그 중인 필드)
const activeField = activeId
? fields.find((f) => `${f.area}-${f.field}` === activeId)
: 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) {
return (
{filterCount > 0 && (
필터 {filterCount}
)}
열 {columnCount}
행 {rowCount}
데이터 {dataCount}
);
}
return (
{/* 4개 영역 배치: 2x2 그리드 */}
{/* 필터 영역 */}
{/* 열 영역 */}
{/* 행 영역 */}
{/* 데이터 영역 */}
{/* 접기 버튼 */}
{onToggleCollapse && (
)}
{/* 드래그 오버레이 */}
{activeField ? (
{activeField.caption}
) : null}
);
};
export default FieldPanel;