다중 정렬 (Sorting)
단일 컬럼 및 다중 컬럼(Multi-column) 정렬 규칙과 정렬 상태 제어(Controlled Sort)를 학습합니다.
import * as React from 'react';
import { AXDataGrid, AXDGColumn, AXDGDataItem, AXDGItemRenderProps, AXDGSortParam, toMoney } from '@axboot/datagrid';
import { useContainerSize } from '../hooks/useContainerSize';
import DataGridContainer from '../components/DataGridContainer';
import { Progress } from 'antd';
import { Key, useState } from 'react';
interface Props {}
interface IListItem {
nation: string;
awpc: number;
wpc: number;
man: number;
woman: number;
ratio: number;
ratioMan: number;
ratioWoman: number;
}
const rawData = [
['대한민국(15+ LFS)', 44504, 28186, 16090, 12097, 63, 73, 53],
['아르메니아(15~75 LFS)', 2204, 1563, 780, 783, 70, 76, 66],
['아제르바이잔(15+ LFS)', 0, 5190, 2664, 2526, 66, 69, 63],
['부탄(15+ LFS)', 482, 320, 169, 151, 66, 71, 61],
['브루나이(15+ LFS)', 370, 238, 144, 94, 64, 72, 54],
['캄보디아(15+ LFS)', 11515, 8756, 4481, 4274, 76, 82, 69],
['키프로스(15+ LFS)', 712, 448, 236, 212, 63, 68, 57],
['조지아(15+ LFS)', 3037, 1911, 1025, 886, 62, 72, 54],
['홍콩(15+ LFS)', 6573, 3988, 1990, 1998, 60, 67, 55],
['인도네시아(15+ LFS)', 200485, 136808, 82760, 54048, 68, 82, 53],
['이란(15+ LFS)', 61658, 26940, 21707, 5233, 43, 70, 17],
['이스라엘(15+ LFS)', 6494, 4124, 2149, 1974, 63, 67, 59],
['일본(15+ LFS)', 110271, 68377, 37997, 30380, 62, 71, 53],
['카자흐스탄(15+ LFS)', 13131, 9203, 0, 0, 70, 0, 0],
['키르기스스탄(15+ LFS)', 4288, 2755, 1620, 1136, 64, 77, 51],
['레바논(15+ LFS)', 3677, 1798, 1230, 567, 48, 70, 29],
['마카오(16+ LFS)', 0, 395, 193, 202, 0, 74, 66],
['말레이시아(15~64 LFS)', 22685, 15582, 9503, 6078, 68, 80, 55],
['몰디브(15+ HIES)', 317, 202, 116, 86, 63, 78, 50],
['몽골(15+ LFS)', 2106, 1326, 706, 620, 63, 70, 55],
['파키스탄(15+ LFS)', 120220, 62030, 47845, 14185, 51, 79, 23],
['필리핀(15+ LFS )', 73008, 43399, 26527, 16872, 59, 72, 46],
['카타르(15+ LFS)', 2393, 2108, 1823, 285, 88, 95, 57],
['사우디아라비아(15+ LFS)', 0, 0, 0, 0, 57, 80, 24],
['싱가포르(15+ LFS)', 3422, 2329, 1251, 1077, 68, 75, 61],
['스리랑카(15+ LFS)', 16424, 8581, 5550, 3032, 52, 72, 34],
['대만(15+ LFS)', 20188, 11946, 6631, 5315, 59, 67, 51],
['태국(15+ LFS)', 56575, 37885, 20611, 17274, 67, 75, 59],
['튀르키예(15+ LFS)', 61468, 32524, 21855, 10669, 52, 72, 34],
['아랍에미리트(15+ LFS)', 9432, 7565, 5693, 1871, 80, 92, 57],
['베트남(15+ LFS)', 73394, 55507, 29068, 26440, 75, 81, 70],
];
const list = rawData.map((data, index) => {
return {
values: {
nation: data[0],
awpc: data[1],
wpc: data[2],
man: data[3],
woman: data[4],
ratio: data[5],
ratioMan: data[5],
ratioWoman: data[5],
},
};
});
const numRender = (item: AXDGItemRenderProps<IListItem>) => <>{toMoney(item.value)}</>;
function SortExample(props: Props) {
const [checkedKeys, setCheckedKeys] = useState<Key[]>();
const [sortParams, setSortParams] = React.useState<AXDGSortParam[]>([]);
const [columns, setColumns] = React.useState<AXDGColumn<IListItem>[]>([
{
key: 'nation',
label: 'Nation',
width: 150,
},
{
key: 'awpc',
label: 'active population',
width: 150,
align: 'right',
itemRender: numRender,
},
{ key: 'wpc', label: 'population', width: 100, align: 'right', itemRender: numRender },
{ key: 'man', label: 'Man', width: 100, align: 'right', itemRender: numRender },
{ key: 'woman', label: 'Woman', width: 100, align: 'right', itemRender: numRender },
{
key: 'ratio',
label: 'Ratio',
width: 150,
align: 'right',
itemRender: item => {
return <Progress size={'small'} percent={item.values.ratio} style={{ margin: 0 }} />;
},
},
{ key: 'ratioMan', label: 'Man', width: 100, align: 'right' },
{ key: 'ratioWoman', label: 'Woman', width: 100, align: 'right' },
]);
const containerRef = React.useRef<HTMLDivElement>(null);
const { width: containerWidth, height: containerHeight } = useContainerSize(containerRef);
const sortedList = React.useMemo(() => {
let i = 0,
l = sortParams.length;
return list
.sort((a, b) => {
for (i = 0; i < l; i++) {
const sortInfo = sortParams[i];
if (sortInfo.key === undefined) {
continue;
}
let valueA = a.values[sortInfo.key as keyof IListItem],
valueB = b.values[sortInfo.key as keyof IListItem];
if (typeof valueA !== typeof valueB) {
valueA = '' + valueA;
valueB = '' + valueB;
}
if (valueA < valueB) {
return sortInfo.orderBy === 'asc' ? -1 : 1;
} else if (valueA > valueB) {
return sortInfo.orderBy === 'asc' ? 1 : -1;
}
}
return 0;
})
.slice() as AXDGDataItem<IListItem>[];
}, [sortParams]);
return (
<DataGridContainer ref={containerRef}>
<AXDataGrid<IListItem>
width={containerWidth}
height={containerHeight}
headerHeight={35}
data={sortedList}
columns={columns}
onChangeColumns={(columnIndex, { width, columns }) => {
console.log('onChangeColumnWidths', columnIndex, width, columns);
setColumns(columns);
}}
rowChecked={{
checkedRowKeys: checkedKeys,
onChange: (ids, keys, selectedAll) => {
console.log('onChange rowSelection', ids, selectedAll);
setCheckedKeys(keys);
},
}}
sort={{
sortParams,
onChange: sortParams => {
console.log('onChange: sortParams', sortParams);
setSortParams(sortParams);
},
}}
showLineNumber
rowKey={'nation'}
/>
</DataGridContainer>
);
}
export default SortExample;import * as React from 'react';
import './DataGridContainer.css';
interface DataGridContainerProps extends React.HTMLAttributes<HTMLDivElement> {
children?: React.ReactNode;
}
/**
* Keeps a DataGrid in a measured, fixed layout box.
*
* AXDataGrid's rendered root is absolutely positioned within this relative
* container. This makes a ResizeObserver measurement authoritative when a
* surrounding flex or grid layout shrinks as well as when it expands.
*/
const DataGridContainer = React.forwardRef<HTMLDivElement, DataGridContainerProps>(
({ className, ...rest }, ref) => (
<div ref={ref} className={`data-grid-container ${className ?? ''}`.trim()} {...rest} />
),
);
DataGridContainer.displayName = 'DataGridContainer';
export default DataGridContainer;.data-grid-container {
position: relative;
width: 100%;
height: 400px;
overflow: hidden;
font-size: 13px;
}
.data-grid-container > .axdg-root {
position: absolute;
inset: 0;
}import * as React from 'react';
export function useContainerSize(ref: React.MutableRefObject<HTMLElement | null>, additionalDeps: unknown[] = []) {
const [width, setWidth] = React.useState(0);
const [height, setHeight] = React.useState(0);
const resizeObserver = React.useRef(
new ResizeObserver(entries => {
if (entries.length !== 1) {
throw new Error('Invalid Container length');
}
const [entry] = entries;
const { width, height } = entry.contentRect;
setWidth(width);
setHeight(height);
}),
);
React.useEffect(() => {
if (!ref.current) return;
const observer = resizeObserver.current;
const element = ref.current;
setWidth(element.clientWidth);
setHeight(element.clientHeight);
observer.observe(element);
return () => {
observer.unobserve(element);
};
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [...additionalDeps, ref]);
return {
width,
height,
};
}1. 언제 사용하며 왜 필요한가요?
단순히 한 컬럼으로만 정렬하는 것을 넘어, **“1차로 부서명 오름차순 정렬 후, 같은 부서 내에서는 2차로 직급/입사일순 정렬”**처럼 여러 컬럼을 복합 정렬해야 하는 실무 요구사항이 빈번합니다.
AXBOOT DataGrid는 sort.sortParams와 sort.onChange로 정렬 상태를 제어합니다. sort 방식에서는 변경된 조건을 부모가 받아 데이터를 정렬해 다시 전달합니다. 헤더 툴박스의 클라이언트 자동 처리가 필요하면 dataControl.mode: 'client'를 사용하세요.
2. 실무 완성형 예제: 다중 정렬 제어
import React, { useMemo, useState } from 'react';
import { AXDataGrid, type AXDGColumn, type AXDGDataItem, type AXDGSortParam } from '@axboot/datagrid';
interface Person {
id: number;
dept: string;
name: string;
score: number;
}
export default function SortGrid() {
const [sortParams, setSortParams] = useState<AXDGSortParam[]>([]);
const [data] = useState<AXDGDataItem<Person>[]>([
{ values: { id: 1, dept: '개발팀', name: '김민수', score: 95 } },
{ values: { id: 2, dept: '개발팀', name: '박도현', score: 88 } },
{ values: { id: 3, dept: '기획팀', name: '이수진', score: 92 } },
{ values: { id: 4, dept: '기획팀', name: '최동욱', score: 95 } },
]);
const columns: AXDGColumn<Person>[] = [
{ key: 'id', label: 'ID', width: 70, align: 'center' },
{ key: 'dept', label: '부서', width: 120, align: 'center' },
{ key: 'name', label: '이름', width: 120, align: 'center' },
{ key: 'score', label: '점수', width: 100, align: 'right' },
];
const sortedData = useMemo(() => [...data].sort((a, b) => {
for (const sort of sortParams) {
if (!sort.key) continue;
const left = a.values[sort.key as keyof Person];
const right = b.values[sort.key as keyof Person];
if (left === right) continue;
const result = left < right ? -1 : 1;
return sort.orderBy === 'asc' ? result : -result;
}
return 0;
}), [data, sortParams]);
return (
<div>
<AXDataGrid<Person>
width={550}
height={240}
columns={columns}
data={sortedData}
rowKey="id"
sort={{
multiSort: true,
sortParams,
onChange: setSortParams,
}}
/>
</div>
);
}