행 순서 재배치 (Row Reorder)
행 좌측의 드래그 핸들을 잡고 위아래로 드래그하여 데이터 행의 표시 순서를 직관적으로 변경하는 방법을 학습합니다.
import * as React from 'react';
import { AXDataGrid, AXDGColumn, AXDGDataItemStatus } from '@axboot/datagrid';
import { useContainerSize } from '../hooks/useContainerSize';
import DataGridContainer from '../components/DataGridContainer';
import { useState } from 'react';
interface Props {}
interface IListItem {
id: string;
title: string;
writer: string;
createAt: string;
}
const _list = Array.from(Array(100)).map((v, i) => ({
values: {
id: `ID_${i}`,
title: `title_${i}`,
writer: `writer_${i}`,
createAt: `2022-09-08`,
},
}));
export default function ReorderExample(props: Props) {
const [columns, setColumns] = useState<AXDGColumn<IListItem>[]>([
{
key: '_',
label: '상태',
width: 50,
align: 'center',
itemRender: ({ item }) => {
return <>{item.status !== undefined ? AXDGDataItemStatus[item.status] : ''}</>;
},
getClassName: item => {
return item.status ? 'editable' : '';
},
},
{
key: 'id',
label: 'No',
width: 100,
},
{
key: 'title',
label: 'Title',
width: 300,
itemRender: ({ values }) => {
return (
<>
{values.writer} / {values.title}
</>
);
},
},
{
key: 'writer',
label: 'Writer',
width: 100,
itemRender: ({ values: values }) => {
return <>{values.writer} / A</>;
},
},
{
key: 'createAt',
label: 'Date-A',
width: 100,
},
{
key: 'createAt',
label: 'Date-B',
width: 100,
},
{
key: 'createAt',
label: 'Date-C',
width: 100,
},
{
key: 'createAt',
label: 'Date-D',
width: 100,
},
{
key: 'createAt',
label: 'Date-E',
width: 100,
},
]);
const [list, setList] = useState(_list);
const containerRef = React.useRef<HTMLDivElement>(null);
const { width: containerWidth, height: containerHeight } = useContainerSize(containerRef);
return (
<DataGridContainer ref={containerRef} className={'editor-example'}>
<AXDataGrid<IListItem>
width={containerWidth}
height={containerHeight}
data={list}
columns={columns}
onClick={item => console.log(item)}
columnSortable={false}
showLineNumber
reorder={{
enabled: true, // Set to true to enable drag-and-drop reordering
onReorder: data => {
// console.log('Reordered data:', data);
setList(data);
return true;
},
}}
/>
</DataGridContainer>
);
}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. 언제 사용하며 왜 필요한가요?
메뉴 관리(Menu Tree), 배너 노출 우선순위 설정, 할 일(TODO) 순서 변경 등 **“사용자가 직접 항목의 순서를 조정하여 저장해야 하는 기능”**에서 행 드래그 재배치는 필수적인 UX입니다.
AXBOOT DataGrid의 reorder 기능을 활성화하면:
- 행 번호 영역 좌측에 전용 드래그 핸들 아이콘(
grip-vertical)이 자동 표시됩니다. - 행을 잡고 위아래로 끌어다 놓으면
onReorder콜백이 트리거되어 새 배열을 즉시 전달합니다. - 가상 스크롤 상태에서도 포인터 위치와 실제 스크롤 오프셋을 기준으로 대상 행을 계산합니다.
2. 실무 완성형 예제: 배너 노출 순서 관리 그리드
import React, { useState } from 'react';
import { AXDataGrid, type AXDGColumn, type AXDGDataItem } from '@axboot/datagrid';
interface BannerItem {
id: number;
title: string;
linkUrl: string;
active: boolean;
}
export default function BannerReorderGrid() {
const [data, setData] = useState<AXDGDataItem<BannerItem>[]>([
{ values: { id: 1, title: '메인 상단 여름 시즌 프로모션 배너', linkUrl: '/events/summer', active: true } },
{ values: { id: 2, title: '신규 회원 가입 10% 웰컴 쿠폰 안내', linkUrl: '/welcome', active: true } },
{ values: { id: 3, title: '카카오페이 결제 시 5천원 즉시 할인', linkUrl: '/events/kakaopay', active: false } },
{ values: { id: 4, title: '프리미엄 멤버십 오픈 기념 이벤트', linkUrl: '/membership', active: true } },
]);
const columns: AXDGColumn<BannerItem>[] = [
{ key: 'id', label: 'ID', width: 60, align: 'center' },
{ key: 'title', label: '배너 제목', width: 280 },
{ key: 'linkUrl', label: '연결 링크', width: 180 },
{
key: 'active',
label: '노출여부',
width: 90,
align: 'center',
itemRender: ({ values }) => (
<span style={{ color: values.active ? '#16a34a' : '#94a3b8', fontWeight: 600 }}>
{values.active ? '노출중' : '비활성'}
</span>
),
},
];
return (
<div>
<div style={{ marginBottom: 10, fontSize: 13, color: '#475569' }}>
💡 행 좌측의 드래그 핸들(점 6개 아이콘)을 마우스로 잡고 위아래로 끌어서 순서를 변경해보세요.
</div>
<AXDataGrid<BannerItem>
width={720}
height={260}
columns={columns}
data={data}
rowKey="id"
showLineNumber={true} // 줄번호 영역 표시 (필수)
reorder={{
enabled: true, // 행 드래그 재배치 활성화
onReorder: (newData: AXDGDataItem<BannerItem>[]) => {
console.log('재배치 완료:', newData.map(d => d.values.title));
setData(newData);
return true; // 성공 시 true 반환
},
}}
/>
</div>
);
}
3. 실무 팁 & 주의사항 (Gotchas)
[!IMPORTANT] 정렬/필터 활성화 시 행 재배치 비활성화: 컬럼 정렬이나 필터가 걸려 있는 상태에서는 데이터의 인덱스가 왜곡되므로, 행 순서를 직접 드래그하여 변경하려면 정렬과 필터가 없는 원본 목록 상태에서 수행하는 것이 안전합니다.