diff --git a/frontend/components/common/DataGrid.tsx b/frontend/components/common/DataGrid.tsx index 47f88a5d..e9ec6f51 100644 --- a/frontend/components/common/DataGrid.tsx +++ b/frontend/components/common/DataGrid.tsx @@ -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 ( @@ -218,10 +230,28 @@ function SortableHeaderCell({ )} + + {/* 리사이저 핸들 — 우측 가장자리 6px 영역에서 드래그하여 컬럼 너비 조정 */} + {onResizeStart && ( +
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="드래그하여 컬럼 너비 조정" + /> + )} ); } +// 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>({}); + 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) => { + 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 = {}; @@ -600,6 +676,8 @@ export function DataGrid({ onToggleFilter={toggleHeaderFilter} onClearFilter={clearHeaderFilter} frozenLeftClass={frozenLeftClass} + widthPx={columnWidths[col.key]} + onResizeStart={startResize} /> ))} @@ -671,11 +749,15 @@ export function DataGrid({ {pageOffset + rowIdx + 1} )} - {columns.map((col) => ( + {columns.map((col) => { + const w = columnWidths[col.key]; + const inlineStyle = w != null ? { width: w, minWidth: w, maxWidth: w } : undefined; + return ( {renderCell(row, col, rowIdx)} - ))} + ); + })} );})}