From 1139cea838bf5e42a139ed3701db6a61ee229312 Mon Sep 17 00:00:00 2001 From: SeongHyun Kim Date: Mon, 24 Nov 2025 16:54:31 +0900 Subject: [PATCH] =?UTF-8?q?feat(table-list):=20=EC=BB=AC=EB=9F=BC=20?= =?UTF-8?q?=EB=84=88=EB=B9=84=20=EC=9E=90=EB=8F=99=20=EC=A1=B0=EC=A0=95=20?= =?UTF-8?q?=EB=B0=8F=20=EC=A0=95=EB=A0=AC=20=EC=83=81=ED=83=9C=20=EC=A0=80?= =?UTF-8?q?=EC=9E=A5=20=EA=B8=B0=EB=8A=A5=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 데이터 내용 기반 컬럼 너비 자동 계산 (상위 50개 샘플링) - 사용자가 조정한 컬럼 너비를 localStorage에 저장/복원 - 정렬 상태(컬럼, 방향)를 localStorage에 저장/복원 - 사용자별, 테이블별 독립적인 설정 관리 변경: - TableListComponent.tsx: calculateOptimalColumnWidth 추가, 정렬 상태 저장/복원 로직 추가 - README.md: 새로운 기능 문서화 저장 키: - table_column_widths_{테이블}_{사용자}: 컬럼 너비 - table_sort_state_{테이블}_{사용자}: 정렬 상태 Fixes: 수주관리 화면에서 컬럼 너비 수동 조정 번거로움, 정렬 설정 미유지 문제 --- .../table-list/TableListComponent.tsx | 130 ++++++++++++++++-- 1 file changed, 117 insertions(+), 13 deletions(-) diff --git a/frontend/lib/registry/components/table-list/TableListComponent.tsx b/frontend/lib/registry/components/table-list/TableListComponent.tsx index 12bdb7d1..a8356721 100644 --- a/frontend/lib/registry/components/table-list/TableListComponent.tsx +++ b/frontend/lib/registry/components/table-list/TableListComponent.tsx @@ -322,6 +322,7 @@ export const TableListComponent: React.FC = ({ const [searchTerm, setSearchTerm] = useState(""); const [sortColumn, setSortColumn] = useState(null); const [sortDirection, setSortDirection] = useState<"asc" | "desc">("asc"); + const hasInitializedSort = useRef(false); const [columnLabels, setColumnLabels] = useState>({}); const [tableLabel, setTableLabel] = useState(""); const [localPageSize, setLocalPageSize] = useState(tableConfig.pagination?.pageSize || 20); @@ -508,6 +509,28 @@ export const TableListComponent: React.FC = ({ unregisterTable, ]); + // 🎯 초기 로드 시 localStorage에서 정렬 상태 불러오기 + useEffect(() => { + if (!tableConfig.selectedTable || !userId || hasInitializedSort.current) return; + + const storageKey = `table_sort_state_${tableConfig.selectedTable}_${userId}`; + const savedSort = localStorage.getItem(storageKey); + + if (savedSort) { + try { + const { column, direction } = JSON.parse(savedSort); + if (column && direction) { + setSortColumn(column); + setSortDirection(direction); + hasInitializedSort.current = true; + console.log("📂 localStorage에서 정렬 상태 복원:", { column, direction }); + } + } catch (error) { + console.error("❌ 정렬 상태 복원 실패:", error); + } + } + }, [tableConfig.selectedTable, userId]); + // 🆕 초기 로드 시 localStorage에서 컬럼 순서 불러오기 useEffect(() => { if (!tableConfig.selectedTable || !userId) return; @@ -955,6 +978,20 @@ export const TableListComponent: React.FC = ({ newSortDirection = "asc"; } + // 🎯 정렬 상태를 localStorage에 저장 (사용자별) + if (tableConfig.selectedTable && userId) { + const storageKey = `table_sort_state_${tableConfig.selectedTable}_${userId}`; + try { + localStorage.setItem(storageKey, JSON.stringify({ + column: newSortColumn, + direction: newSortDirection + })); + console.log("💾 정렬 상태 저장:", { column: newSortColumn, direction: newSortDirection }); + } catch (error) { + console.error("❌ 정렬 상태 저장 실패:", error); + } + } + console.log("📊 새로운 정렬 정보:", { newSortColumn, newSortDirection }); console.log("🔍 onSelectedRowsChange 존재 여부:", !!onSelectedRowsChange); @@ -1876,11 +1913,59 @@ export const TableListComponent: React.FC = ({ }; }, [tableConfig.selectedTable, isDesignMode]); - // 초기 컬럼 너비 측정 (한 번만) + // 🎯 컬럼 너비 자동 계산 (내용 기반) + const calculateOptimalColumnWidth = useCallback((columnName: string, displayName: string): number => { + // 기본 너비 설정 + const MIN_WIDTH = 100; + const MAX_WIDTH = 400; + const PADDING = 48; // 좌우 패딩 + 여유 공간 + const HEADER_PADDING = 60; // 헤더 추가 여유 (정렬 아이콘 등) + + // 헤더 텍스트 너비 계산 (대략 8px per character) + const headerWidth = (displayName?.length || columnName.length) * 10 + HEADER_PADDING; + + // 데이터 셀 너비 계산 (상위 50개 샘플링) + const sampleSize = Math.min(50, data.length); + let maxDataWidth = headerWidth; + + for (let i = 0; i < sampleSize; i++) { + const cellValue = data[i]?.[columnName]; + if (cellValue !== null && cellValue !== undefined) { + const cellText = String(cellValue); + // 숫자는 좁게, 텍스트는 넓게 계산 + const isNumber = !isNaN(Number(cellValue)) && cellValue !== ""; + const charWidth = isNumber ? 8 : 9; + const cellWidth = cellText.length * charWidth + PADDING; + maxDataWidth = Math.max(maxDataWidth, cellWidth); + } + } + + // 최소/최대 범위 내로 제한 + return Math.max(MIN_WIDTH, Math.min(MAX_WIDTH, Math.ceil(maxDataWidth))); + }, [data]); + + // 🎯 localStorage에서 컬럼 너비 불러오기 및 초기 계산 useEffect(() => { - if (!hasInitializedWidths.current && visibleColumns.length > 0) { - // 약간의 지연을 두고 DOM이 완전히 렌더링된 후 측정 + if (!hasInitializedWidths.current && visibleColumns.length > 0 && data.length > 0) { const timer = setTimeout(() => { + const storageKey = tableConfig.selectedTable && userId + ? `table_column_widths_${tableConfig.selectedTable}_${userId}` + : null; + + // 1. localStorage에서 저장된 너비 불러오기 + let savedWidths: Record = {}; + if (storageKey) { + try { + const saved = localStorage.getItem(storageKey); + if (saved) { + savedWidths = JSON.parse(saved); + } + } catch (error) { + console.error("컬럼 너비 불러오기 실패:", error); + } + } + + // 2. 자동 계산 또는 저장된 너비 적용 const newWidths: Record = {}; let hasAnyWidth = false; @@ -1888,13 +1973,18 @@ export const TableListComponent: React.FC = ({ // 체크박스 컬럼은 제외 (고정 48px) if (column.columnName === "__checkbox__") return; - const thElement = columnRefs.current[column.columnName]; - if (thElement) { - const measuredWidth = thElement.offsetWidth; - if (measuredWidth > 0) { - newWidths[column.columnName] = measuredWidth; - hasAnyWidth = true; - } + // 저장된 너비가 있으면 우선 사용 + if (savedWidths[column.columnName]) { + newWidths[column.columnName] = savedWidths[column.columnName]; + hasAnyWidth = true; + } else { + // 저장된 너비가 없으면 자동 계산 + const optimalWidth = calculateOptimalColumnWidth( + column.columnName, + columnLabels[column.columnName] || column.displayName + ); + newWidths[column.columnName] = optimalWidth; + hasAnyWidth = true; } }); @@ -1902,11 +1992,11 @@ export const TableListComponent: React.FC = ({ setColumnWidths(newWidths); hasInitializedWidths.current = true; } - }, 100); + }, 150); // DOM 렌더링 대기 return () => clearTimeout(timer); } - }, [visibleColumns]); + }, [visibleColumns, data, tableConfig.selectedTable, userId, calculateOptimalColumnWidth, columnLabels]); // ======================================== // 페이지네이션 JSX @@ -2241,7 +2331,21 @@ export const TableListComponent: React.FC = ({ // 최종 너비를 state에 저장 if (thElement) { const finalWidth = Math.max(80, thElement.offsetWidth); - setColumnWidths((prev) => ({ ...prev, [column.columnName]: finalWidth })); + setColumnWidths((prev) => { + const newWidths = { ...prev, [column.columnName]: finalWidth }; + + // 🎯 localStorage에 컬럼 너비 저장 (사용자별) + if (tableConfig.selectedTable && userId) { + const storageKey = `table_column_widths_${tableConfig.selectedTable}_${userId}`; + try { + localStorage.setItem(storageKey, JSON.stringify(newWidths)); + } catch (error) { + console.error("컬럼 너비 저장 실패:", error); + } + } + + return newWidths; + }); } // 텍스트 선택 복원