311 lines
8.6 KiB
Plaintext
311 lines
8.6 KiB
Plaintext
|
|
# TableListComponent 개발 가이드
|
||
|
|
|
||
|
|
## 개요
|
||
|
|
|
||
|
|
`TableListComponent`는 ERP 시스템의 핵심 데이터 그리드 컴포넌트입니다. DevExpress DataGrid 스타일의 고급 기능들을 구현하고 있습니다.
|
||
|
|
|
||
|
|
**파일 위치**: `frontend/lib/registry/components/table-list/TableListComponent.tsx`
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
## 핵심 기능 목록
|
||
|
|
|
||
|
|
### 1. 인라인 편집 (Inline Editing)
|
||
|
|
|
||
|
|
- 셀 더블클릭 또는 F2 키로 편집 모드 진입
|
||
|
|
- 직접 타이핑으로도 편집 모드 진입 가능
|
||
|
|
- Enter로 저장, Escape로 취소
|
||
|
|
- **컬럼별 편집 가능 여부 설정** (`editable` 속성)
|
||
|
|
|
||
|
|
```typescript
|
||
|
|
// ColumnConfig에서 editable 속성 사용
|
||
|
|
interface ColumnConfig {
|
||
|
|
editable?: boolean; // false면 해당 컬럼 인라인 편집 불가
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
**편집 불가 컬럼 체크 필수 위치**:
|
||
|
|
1. `handleCellDoubleClick` - 더블클릭 편집
|
||
|
|
2. `onKeyDown` F2 케이스 - 키보드 편집
|
||
|
|
3. `onKeyDown` default 케이스 - 직접 타이핑 편집
|
||
|
|
4. 컨텍스트 메뉴 "셀 편집" 옵션
|
||
|
|
|
||
|
|
### 2. 배치 편집 (Batch Editing)
|
||
|
|
|
||
|
|
- 여러 셀 수정 후 일괄 저장/취소
|
||
|
|
- `pendingChanges` Map으로 변경사항 추적
|
||
|
|
- 저장 전 유효성 검증
|
||
|
|
|
||
|
|
### 3. 데이터 유효성 검증 (Validation)
|
||
|
|
|
||
|
|
```typescript
|
||
|
|
type ValidationRule = {
|
||
|
|
required?: boolean;
|
||
|
|
min?: number;
|
||
|
|
max?: number;
|
||
|
|
minLength?: number;
|
||
|
|
maxLength?: number;
|
||
|
|
pattern?: RegExp;
|
||
|
|
customMessage?: string;
|
||
|
|
validate?: (value: any, row: any) => string | null;
|
||
|
|
};
|
||
|
|
```
|
||
|
|
|
||
|
|
### 4. 컬럼 헤더 필터 (Header Filter)
|
||
|
|
|
||
|
|
- 각 컬럼 헤더에 필터 아이콘
|
||
|
|
- 고유값 목록에서 다중 선택 필터링
|
||
|
|
- `headerFilters` Map으로 필터 상태 관리
|
||
|
|
|
||
|
|
### 5. 필터 빌더 (Filter Builder)
|
||
|
|
|
||
|
|
```typescript
|
||
|
|
interface FilterCondition {
|
||
|
|
id: string;
|
||
|
|
column: string;
|
||
|
|
operator: "equals" | "notEquals" | "contains" | "notContains" |
|
||
|
|
"startsWith" | "endsWith" | "greaterThan" | "lessThan" |
|
||
|
|
"greaterOrEqual" | "lessOrEqual" | "isEmpty" | "isNotEmpty";
|
||
|
|
value: string;
|
||
|
|
}
|
||
|
|
|
||
|
|
interface FilterGroup {
|
||
|
|
id: string;
|
||
|
|
logic: "AND" | "OR";
|
||
|
|
conditions: FilterCondition[];
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
### 6. 검색 패널 (Search Panel)
|
||
|
|
|
||
|
|
- 전체 데이터 검색
|
||
|
|
- 검색어 하이라이팅
|
||
|
|
- `searchHighlights` Map으로 하이라이트 위치 관리
|
||
|
|
|
||
|
|
### 7. 엑셀 내보내기 (Excel Export)
|
||
|
|
|
||
|
|
- `xlsx` 라이브러리 사용
|
||
|
|
- 현재 표시 데이터 또는 전체 데이터 내보내기
|
||
|
|
|
||
|
|
```typescript
|
||
|
|
import * as XLSX from "xlsx";
|
||
|
|
|
||
|
|
// 사용 예시
|
||
|
|
const worksheet = XLSX.utils.json_to_sheet(exportData);
|
||
|
|
const workbook = XLSX.utils.book_new();
|
||
|
|
XLSX.utils.book_append_sheet(workbook, worksheet, "Sheet1");
|
||
|
|
XLSX.writeFile(workbook, `${tableName}_${timestamp}.xlsx`);
|
||
|
|
```
|
||
|
|
|
||
|
|
### 8. 클립보드 복사 (Copy to Clipboard)
|
||
|
|
|
||
|
|
- 선택된 행 또는 전체 데이터 복사
|
||
|
|
- 탭 구분자로 엑셀 붙여넣기 호환
|
||
|
|
|
||
|
|
### 9. 컨텍스트 메뉴 (Context Menu)
|
||
|
|
|
||
|
|
- 우클릭으로 메뉴 표시
|
||
|
|
- 셀 편집, 행 복사, 행 삭제 등 옵션
|
||
|
|
- 편집 불가 컬럼은 "(잠김)" 표시
|
||
|
|
|
||
|
|
### 10. 키보드 네비게이션
|
||
|
|
|
||
|
|
| 키 | 동작 |
|
||
|
|
|---|---|
|
||
|
|
| Arrow Keys | 셀 이동 |
|
||
|
|
| Tab | 다음 셀 |
|
||
|
|
| Shift+Tab | 이전 셀 |
|
||
|
|
| F2 | 편집 모드 |
|
||
|
|
| Enter | 저장 후 아래로 이동 |
|
||
|
|
| Escape | 편집 취소 |
|
||
|
|
| Ctrl+C | 복사 |
|
||
|
|
| Delete | 셀 값 삭제 |
|
||
|
|
|
||
|
|
### 11. 컬럼 리사이징
|
||
|
|
|
||
|
|
- 컬럼 헤더 경계 드래그로 너비 조절
|
||
|
|
- `columnWidths` 상태로 관리
|
||
|
|
- localStorage에 저장
|
||
|
|
|
||
|
|
### 12. 컬럼 순서 변경
|
||
|
|
|
||
|
|
- 드래그 앤 드롭으로 컬럼 순서 변경
|
||
|
|
- `columnOrder` 상태로 관리
|
||
|
|
- localStorage에 저장
|
||
|
|
|
||
|
|
### 13. 상태 영속성 (State Persistence)
|
||
|
|
|
||
|
|
```typescript
|
||
|
|
// localStorage 키 패턴
|
||
|
|
const stateKey = `tableState_${tableName}_${userId}`;
|
||
|
|
|
||
|
|
// 저장되는 상태
|
||
|
|
interface TableState {
|
||
|
|
columnWidths: Record<string, number>;
|
||
|
|
columnOrder: string[];
|
||
|
|
sortBy: string;
|
||
|
|
sortOrder: "asc" | "desc";
|
||
|
|
frozenColumns: string[];
|
||
|
|
columnVisibility: Record<string, boolean>;
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
### 14. 그룹화 및 그룹 소계
|
||
|
|
|
||
|
|
```typescript
|
||
|
|
interface GroupedData {
|
||
|
|
groupKey: string;
|
||
|
|
groupValues: Record<string, any>;
|
||
|
|
items: any[];
|
||
|
|
count: number;
|
||
|
|
summary?: Record<string, { sum: number; avg: number; count: number }>;
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
### 15. 총계 요약 (Total Summary)
|
||
|
|
|
||
|
|
- 숫자 컬럼의 합계, 평균, 개수 표시
|
||
|
|
- 테이블 하단에 요약 행 렌더링
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
## 캐싱 전략
|
||
|
|
|
||
|
|
```typescript
|
||
|
|
// 테이블 컬럼 캐시
|
||
|
|
const tableColumnCache = new Map<string, { columns: any[]; timestamp: number }>();
|
||
|
|
const TABLE_CACHE_TTL = 5 * 60 * 1000; // 5분
|
||
|
|
|
||
|
|
// API 호출 디바운싱
|
||
|
|
const debouncedApiCall = <T extends any[], R>(
|
||
|
|
key: string,
|
||
|
|
fn: (...args: T) => Promise<R>,
|
||
|
|
delay: number = 300
|
||
|
|
) => { ... };
|
||
|
|
```
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
## 필수 Import
|
||
|
|
|
||
|
|
```typescript
|
||
|
|
import React, { useState, useEffect, useMemo, useCallback, useRef } from "react";
|
||
|
|
import { TableListConfig, ColumnConfig } from "./types";
|
||
|
|
import { tableTypeApi } from "@/lib/api/screen";
|
||
|
|
import { entityJoinApi } from "@/lib/api/entityJoin";
|
||
|
|
import { codeCache } from "@/lib/caching/codeCache";
|
||
|
|
import * as XLSX from "xlsx";
|
||
|
|
import { toast } from "sonner";
|
||
|
|
```
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
## 주요 상태 (State)
|
||
|
|
|
||
|
|
```typescript
|
||
|
|
// 데이터 관련
|
||
|
|
const [tableData, setTableData] = useState<any[]>([]);
|
||
|
|
const [filteredData, setFilteredData] = useState<any[]>([]);
|
||
|
|
const [loading, setLoading] = useState(false);
|
||
|
|
|
||
|
|
// 편집 관련
|
||
|
|
const [editingCell, setEditingCell] = useState<{
|
||
|
|
rowIndex: number;
|
||
|
|
colIndex: number;
|
||
|
|
columnName: string;
|
||
|
|
originalValue: any;
|
||
|
|
} | null>(null);
|
||
|
|
const [editingValue, setEditingValue] = useState<string>("");
|
||
|
|
const [pendingChanges, setPendingChanges] = useState<Map<string, Map<string, any>>>(new Map());
|
||
|
|
const [validationErrors, setValidationErrors] = useState<Map<string, Map<string, string>>>(new Map());
|
||
|
|
|
||
|
|
// 필터 관련
|
||
|
|
const [headerFilters, setHeaderFilters] = useState<Map<string, Set<string>>>(new Map());
|
||
|
|
const [filterGroups, setFilterGroups] = useState<FilterGroup[]>([]);
|
||
|
|
const [globalSearchText, setGlobalSearchText] = useState("");
|
||
|
|
const [searchHighlights, setSearchHighlights] = useState<Map<string, number[]>>(new Map());
|
||
|
|
|
||
|
|
// 컬럼 관련
|
||
|
|
const [columnWidths, setColumnWidths] = useState<Record<string, number>>({});
|
||
|
|
const [columnOrder, setColumnOrder] = useState<string[]>([]);
|
||
|
|
const [columnVisibility, setColumnVisibility] = useState<Record<string, boolean>>({});
|
||
|
|
const [frozenColumns, setFrozenColumns] = useState<string[]>([]);
|
||
|
|
|
||
|
|
// 선택 관련
|
||
|
|
const [selectedRows, setSelectedRows] = useState<Set<string>>(new Set());
|
||
|
|
const [focusedCell, setFocusedCell] = useState<{ rowIndex: number; colIndex: number } | null>(null);
|
||
|
|
|
||
|
|
// 정렬 관련
|
||
|
|
const [sortBy, setSortBy] = useState<string>("");
|
||
|
|
const [sortOrder, setSortOrder] = useState<"asc" | "desc">("asc");
|
||
|
|
|
||
|
|
// 페이지네이션
|
||
|
|
const [currentPage, setCurrentPage] = useState(1);
|
||
|
|
const [pageSize, setPageSize] = useState(20);
|
||
|
|
const [totalCount, setTotalCount] = useState(0);
|
||
|
|
```
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
## 편집 불가 컬럼 구현 체크리스트
|
||
|
|
|
||
|
|
새로운 편집 진입점을 추가할 때 반드시 다음을 확인하세요:
|
||
|
|
|
||
|
|
- [ ] `column.editable === false` 체크 추가
|
||
|
|
- [ ] 편집 불가 시 `toast.warning()` 메시지 표시
|
||
|
|
- [ ] `return` 또는 `break`로 편집 모드 진입 방지
|
||
|
|
|
||
|
|
```typescript
|
||
|
|
// 표준 편집 불가 체크 패턴
|
||
|
|
const column = visibleColumns.find((col) => col.columnName === columnName);
|
||
|
|
if (column?.editable === false) {
|
||
|
|
toast.warning(`'${column.displayName || columnName}' 컬럼은 편집할 수 없습니다.`);
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
## 시각적 표시
|
||
|
|
|
||
|
|
### 편집 불가 컬럼 표시
|
||
|
|
|
||
|
|
```tsx
|
||
|
|
// 헤더에 잠금 아이콘
|
||
|
|
{column.editable === false && (
|
||
|
|
<Lock className="ml-1 h-3 w-3 text-muted-foreground" />
|
||
|
|
)}
|
||
|
|
|
||
|
|
// 셀 배경색
|
||
|
|
className={cn(
|
||
|
|
column.editable === false && "bg-gray-50 dark:bg-gray-900/30"
|
||
|
|
)}
|
||
|
|
```
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
## 성능 최적화
|
||
|
|
|
||
|
|
1. **useMemo 사용**: `visibleColumns`, `filteredData`, `paginatedData` 등 계산 비용이 큰 값
|
||
|
|
2. **useCallback 사용**: 이벤트 핸들러 함수들
|
||
|
|
3. **디바운싱**: API 호출, 검색, 필터링
|
||
|
|
4. **캐싱**: 테이블 컬럼 정보, 코드 데이터
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
## 주의사항
|
||
|
|
|
||
|
|
1. **visibleColumns 정의 순서**: `columnOrder`, `columnVisibility` 상태 이후에 정의해야 함
|
||
|
|
2. **editInputRef 타입 체크**: `select()` 호출 전 `instanceof HTMLInputElement` 확인
|
||
|
|
3. **localStorage 키**: `tableName`과 `userId`를 조합하여 고유하게 생성
|
||
|
|
4. **멀티테넌시**: 모든 API 호출에 `company_code` 필터링 적용 (백엔드에서 자동 처리)
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
## 관련 파일
|
||
|
|
|
||
|
|
- `frontend/lib/registry/components/table-list/types.ts` - 타입 정의
|
||
|
|
- `frontend/lib/registry/components/table-list/TableListConfigPanel.tsx` - 설정 패널
|
||
|
|
- `frontend/components/common/TableOptionsModal.tsx` - 옵션 모달
|
||
|
|
- `frontend/lib/registry/components/table-list/SingleTableWithSticky.tsx` - 스티키 헤더 테이블
|