DataGrid 컬럼 너비 마우스 드래그 리사이저 + localStorage 영구 저장
- 헤더 우측 6px 영역 드래그 핸들 (col-resize 커서 + hover 강조) - mousedown→document mousemove/mouseup으로 너비 계산 (최소 40px) - columnWidths state로 inline width 적용 (Tailwind w-[Xpx]는 fallback) - gridId prop이 있으면 localStorage에 영구 저장 → 새로고침 후 유지 - 컬럼 순서 드래그(@dnd-kit)와 충돌 안 하도록 핸들 영역 분리 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -99,6 +99,7 @@ function SortableHeaderCell({
|
||||
col, sortKey, sortDir, onSort,
|
||||
headerFilterValues, uniqueValues, onToggleFilter, onClearFilter,
|
||||
frozenLeftClass = "left-0",
|
||||
widthPx, onResizeStart,
|
||||
}: {
|
||||
col: DataGridColumn;
|
||||
sortKey: string | null;
|
||||
@@ -109,6 +110,10 @@ function SortableHeaderCell({
|
||||
onToggleFilter: (colKey: string, value: string) => void;
|
||||
onClearFilter: (colKey: string) => void;
|
||||
frozenLeftClass?: string;
|
||||
/** 사용자 리사이즈로 결정된 현재 너비(px). 없으면 col.width Tailwind 클래스 사용 */
|
||||
widthPx?: number;
|
||||
/** 리사이즈 핸들 mousedown 핸들러 */
|
||||
onResizeStart?: (e: React.MouseEvent, colKey: string, currentWidthPx: number) => void;
|
||||
}) {
|
||||
const [filterSearch, setFilterSearch] = useState("");
|
||||
const { attributes, listeners, setNodeRef, transform, transition, isDragging } = useSortable({ id: col.key });
|
||||
@@ -119,16 +124,23 @@ function SortableHeaderCell({
|
||||
opacity: isDragging ? 0.5 : 1,
|
||||
cursor: "grab",
|
||||
};
|
||||
if (widthPx != null) {
|
||||
style.width = widthPx;
|
||||
style.minWidth = widthPx;
|
||||
style.maxWidth = widthPx;
|
||||
}
|
||||
|
||||
const isSorted = sortKey === col.key;
|
||||
const hasFilter = headerFilterValues.size > 0;
|
||||
const effectiveWidthPx = widthPx ?? parseWidthClass(col.width) ?? 100;
|
||||
|
||||
return (
|
||||
<TableHead
|
||||
ref={setNodeRef}
|
||||
style={style}
|
||||
className={cn(
|
||||
col.width, col.minWidth, "select-none relative",
|
||||
widthPx == null && col.width, widthPx == null && col.minWidth,
|
||||
"select-none relative",
|
||||
col.frozen && cn("sticky z-20 bg-background", frozenLeftClass),
|
||||
)}
|
||||
>
|
||||
@@ -218,10 +230,28 @@ function SortableHeaderCell({
|
||||
</Popover>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* 리사이저 핸들 — 우측 가장자리 6px 영역에서 드래그하여 컬럼 너비 조정 */}
|
||||
{onResizeStart && (
|
||||
<div
|
||||
onMouseDown={(e) => onResizeStart(e, col.key, effectiveWidthPx)}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
className="absolute right-0 top-0 h-full w-1.5 cursor-col-resize hover:bg-primary/40 active:bg-primary/60 transition-colors z-10"
|
||||
aria-label="컬럼 너비 조정"
|
||||
title="드래그하여 컬럼 너비 조정"
|
||||
/>
|
||||
)}
|
||||
</TableHead>
|
||||
);
|
||||
}
|
||||
|
||||
// w-[XXXpx] Tailwind 클래스에서 px 정수 추출. 없으면 undefined.
|
||||
function parseWidthClass(cls?: string): number | undefined {
|
||||
if (!cls) return undefined;
|
||||
const m = cls.match(/w-\[(\d+)px\]/);
|
||||
return m ? Number(m[1]) : undefined;
|
||||
}
|
||||
|
||||
// --- DataGrid ---
|
||||
|
||||
export function DataGrid({
|
||||
@@ -286,6 +316,52 @@ export function DataGrid({
|
||||
}
|
||||
}, [gridId]); // eslint-disable-line react-hooks/exhaustive-deps
|
||||
|
||||
// 컬럼별 너비(px) — 사용자가 핸들로 드래그하면 갱신. localStorage에 영구 저장(gridId 있을 때).
|
||||
const [columnWidths, setColumnWidths] = useState<Record<string, number>>({});
|
||||
useEffect(() => {
|
||||
if (!gridId) return;
|
||||
const saved = localStorage.getItem(`datagrid_col_widths_${gridId}`);
|
||||
if (saved) {
|
||||
try { setColumnWidths(JSON.parse(saved)); } catch { /* skip */ }
|
||||
}
|
||||
}, [gridId]);
|
||||
const persistColumnWidths = useCallback((next: Record<string, number>) => {
|
||||
setColumnWidths(next);
|
||||
if (gridId) {
|
||||
try { localStorage.setItem(`datagrid_col_widths_${gridId}`, JSON.stringify(next)); } catch { /* skip */ }
|
||||
}
|
||||
}, [gridId]);
|
||||
|
||||
// 리사이즈 드래그 핸들러 — 헤더 우측 핸들에서 mousedown 발생 시 호출.
|
||||
const startResize = useCallback((e: React.MouseEvent, colKey: string, currentWidthPx: number) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
const startX = e.clientX;
|
||||
const startWidth = currentWidthPx;
|
||||
const onMove = (ev: MouseEvent) => {
|
||||
const delta = ev.clientX - startX;
|
||||
const next = Math.max(40, Math.round(startWidth + delta));
|
||||
setColumnWidths((prev) => ({ ...prev, [colKey]: next }));
|
||||
};
|
||||
const onUp = () => {
|
||||
document.removeEventListener("mousemove", onMove);
|
||||
document.removeEventListener("mouseup", onUp);
|
||||
document.body.style.cursor = "";
|
||||
document.body.style.userSelect = "";
|
||||
// 최종 값으로 영구 저장 (state 최신값 직접 읽기 위해 setter 형태로)
|
||||
setColumnWidths((latest) => {
|
||||
if (gridId) {
|
||||
try { localStorage.setItem(`datagrid_col_widths_${gridId}`, JSON.stringify(latest)); } catch { /* skip */ }
|
||||
}
|
||||
return latest;
|
||||
});
|
||||
};
|
||||
document.addEventListener("mousemove", onMove);
|
||||
document.addEventListener("mouseup", onUp);
|
||||
document.body.style.cursor = "col-resize";
|
||||
document.body.style.userSelect = "none";
|
||||
}, [gridId]);
|
||||
|
||||
// 컬럼별 고유값 계산 (필터 팝오버용)
|
||||
const columnUniqueValues = useMemo(() => {
|
||||
const result: Record<string, string[]> = {};
|
||||
@@ -600,6 +676,8 @@ export function DataGrid({
|
||||
onToggleFilter={toggleHeaderFilter}
|
||||
onClearFilter={clearHeaderFilter}
|
||||
frozenLeftClass={frozenLeftClass}
|
||||
widthPx={columnWidths[col.key]}
|
||||
onResizeStart={startResize}
|
||||
/>
|
||||
))}
|
||||
</TableRow>
|
||||
@@ -671,11 +749,15 @@ export function DataGrid({
|
||||
{pageOffset + rowIdx + 1}
|
||||
</TableCell>
|
||||
)}
|
||||
{columns.map((col) => (
|
||||
{columns.map((col) => {
|
||||
const w = columnWidths[col.key];
|
||||
const inlineStyle = w != null ? { width: w, minWidth: w, maxWidth: w } : undefined;
|
||||
return (
|
||||
<TableCell
|
||||
key={col.key}
|
||||
style={inlineStyle}
|
||||
className={cn(
|
||||
col.width, col.minWidth, "py-2.5",
|
||||
w == null && col.width, w == null && col.minWidth, "py-2.5",
|
||||
col.editable && "cursor-text",
|
||||
isSelected && "bg-accent",
|
||||
col.frozen && cn("sticky z-[5]", frozenLeftClass, stickyBgClass),
|
||||
@@ -689,7 +771,8 @@ export function DataGrid({
|
||||
>
|
||||
{renderCell(row, col, rowIdx)}
|
||||
</TableCell>
|
||||
))}
|
||||
);
|
||||
})}
|
||||
</TableRow>
|
||||
);})}
|
||||
</TableBody>
|
||||
|
||||
Reference in New Issue
Block a user