Files
sam-react-prod/claudedocs/guides/universal-list/[REF] UniversalListPage-QA-patterns.md

314 lines
8.1 KiB
Markdown
Raw Normal View History

# UniversalListPage 검수 패턴 가이드
> **목적**: 55개 페이지 검수 시 발생하는 공통 에러 패턴과 해결책 정리
> **작성일**: 2026-01-16
> **기준**: 지금까지 검수 중 발견된 13개 이상의 에러 분석
---
## 검수 항목 체크리스트
| 항목 | 아이콘 | 설명 |
|------|--------|------|
| 검색 | 🔍 | 검색창 입력 시 필터링 동작 |
| 탭 | 📑 | 탭 버튼 클릭 시 데이터 전환 |
| 필터 | 🎛️ | 필터 선택/적용/초기화 동작 |
| 체크박스 | ☑️ | 테이블 행 체크박스 선택 동작 |
| 상세 | 👁️ | 테이블 로우 클릭 → 상세페이지/모달 이동 |
| 등록 | | 등록 버튼 클릭 → 등록페이지 이동 |
---
## 🚨 공통 에러 패턴 및 해결책
### 1. `headerActions.call is not a function`
**증상**: 페이지 로드 시 에러 발생, 콘솔에 에러 메시지 표시
**원인**: `headerActions`가 ReactNode로 정의되어 있음 (함수가 아님)
**잘못된 코드**:
```typescript
// ❌ ReactNode로 정의
const headerActions = (
<Button onClick={() => console.log('click')}>
버튼
</Button>
);
```
**올바른 코드**:
```typescript
// ✅ 함수로 정의
const headerActions = () => (
<Button onClick={() => console.log('click')}>
버튼
</Button>
);
```
---
### 2. 탭 클릭해도 데이터가 변경되지 않음
**증상**: 탭 버튼 클릭은 되지만 테이블 데이터가 그대로 유지됨
**원인 A (클라이언트 사이드 필터링)**:
- `filteredData`(이미 필터링된 데이터)를 `initialData`에 전달
- UniversalListPage 내부 상태가 외부 데이터 변경을 감지 못함
**해결책 A**:
```typescript
// ✅ 전체 데이터 전달 + tabFilter 함수 추가
const config = {
// ...
clientSideFiltering: true,
tabFilter: (item, activeTab) => {
if (activeTab === 'all') return true;
return item.type === activeTab;
},
searchFilter: (item, search) => {
return item.name.toLowerCase().includes(search.toLowerCase());
},
};
<UniversalListPage
config={config}
initialData={data} // ✅ 전체 데이터
onTabChange={setActiveTab}
/>
```
**원인 B (서버 사이드 필터링)**:
- `onTabChange` prop이 누락됨
**해결책 B**:
```typescript
// ✅ onTabChange prop 추가
<UniversalListPage
config={config}
initialData={items}
onTabChange={handleTypeChange} // ✅ 추가
externalPagination={{...}}
/>
```
---
### 3. 승인/거절 팝업에 선택 건수가 0으로 표시
**증상**: 체크박스 선택 후 버튼 클릭하면 팝업에 "0건" 표시
**원인**: `headerActions`에서 받는 `selected`와 컴포넌트 내부 `selectedItems` 상태가 동기화되지 않음
**잘못된 코드**:
```typescript
// ❌ selected를 내부 상태로 복사하지 않음
const handleApproveClick = useCallback(() => {
setApproveDialogOpen(true);
}, []);
// headerActions에서
<Button onClick={() => handleApproveClick()}>승인</Button>
```
**올바른 코드**:
```typescript
// ✅ selected를 받아서 내부 상태로 복사
const handleApproveClick = useCallback((selected: Set<string>) => {
setSelectedItems(selected); // 복사!
setApproveDialogOpen(true);
}, []);
// headerActions에서
headerActions: ({ selected }) => (
<Button onClick={() => handleApproveClick(selected)}>승인</Button>
)
```
---
### 4. `externalSelection.onToggleSelection is not a function`
**증상**: 체크박스 클릭 시 에러 발생
**원인**: `externalSelection` 프로퍼티 이름이 타입과 불일치
**잘못된 코드**:
```typescript
// ❌ 잘못된 프로퍼티 이름
externalSelection={{
selectedItems,
setSelectedItems, // ❌
toggleSelection, // ❌
toggleSelectAll, // ❌
}}
```
**올바른 코드**:
```typescript
// ✅ 올바른 프로퍼티 이름
externalSelection={{
selectedItems,
onToggleSelection: toggleSelection, // ✅
onToggleSelectAll: toggleSelectAll, // ✅
getItemId: (item) => item.id,
}}
```
---
### 5. `externalPagination` NaN 또는 globalIndex 오류
**증상**: 번호 컬럼에 NaN 표시, 페이지네이션 동작 안함
**원인**: `externalPagination` 프로퍼티 형태 불일치
**올바른 형태**:
```typescript
externalPagination={{
currentPage: pagination.currentPage,
totalPages: pagination.totalPages,
totalItems: pagination.totalItems,
itemsPerPage: pagination.perPage, // ✅ itemsPerPage (perPage 아님)
onPageChange: handlePageChange,
}}
```
---
### 6. 프리셋 버튼 (당월/전월/오늘) 미표시
**증상**: DateRangeSelector는 표시되지만 프리셋 버튼 없음
**원인**: `showPresets: false` 설정
**해결책**:
```typescript
dateRangeSelector: {
enabled: true,
showPresets: true, // ✅ true로 설정
startDate,
endDate,
onStartDateChange,
onEndDateChange,
},
```
---
### 7. 탭 카운트가 모두 동일하게 표시
**증상**: 모든 탭에 같은 숫자가 표시됨
**원인**: `config.tabs` 변경 시 UniversalListPage 내부 상태가 업데이트되지 않음
**해결책** (이미 UniversalListPage에 적용됨):
```typescript
// UniversalListPage/index.tsx에서
useEffect(() => {
if (config.tabs) {
setTabs(config.tabs);
}
}, [config.tabs]);
```
---
## 📋 검수 순서 권장
### Step 1: 페이지 로드 확인
- [ ] 에러 없이 페이지 로드되는가?
- [ ] 콘솔에 에러 메시지 없는가?
### Step 2: 기본 UI 확인
- [ ] 테이블/카드 목록 정상 표시되는가?
- [ ] 통계 카드 (있는 경우) 정상 표시되는가?
- [ ] 탭 버튼 (있는 경우) 정상 표시되는가?
### Step 3: 탭 기능 (있는 경우)
- [ ] 탭 클릭 시 데이터가 변경되는가?
- [ ] 탭별 건수가 정확하게 표시되는가?
- [ ] 탭 변경 후 검색/필터가 유지되는가?
### Step 4: 검색 기능
- [ ] 검색창에 입력 시 필터링되는가?
- [ ] 검색어 삭제 시 전체 목록 표시되는가?
### Step 5: 필터 기능 (있는 경우)
- [ ] PC에서 필터 선택 시 데이터 필터링되는가?
- [ ] 모바일에서 필터 바텀시트 열리는가?
- [ ] 필터 적용/초기화 정상 동작하는가?
### Step 6: 체크박스 선택
- [ ] 개별 체크박스 선택/해제 되는가?
- [ ] 전체 선택 체크박스 동작하는가?
- [ ] 선택 건수가 정확히 표시되는가?
### Step 7: 상세 이동
- [ ] 행 클릭 또는 상세 버튼 클릭 시 이동하는가?
- [ ] URL 파라미터 올바르게 전달되는가?
### Step 8: 등록 버튼 (있는 경우)
- [ ] 등록 버튼 표시되는가?
- [ ] 클릭 시 등록 페이지로 이동하는가?
### Step 9: 커스텀 액션 (승인/거절/삭제 등)
- [ ] 버튼이 올바른 위치에 표시되는가?
- [ ] 선택된 항목 수가 정확히 팝업에 표시되는가?
- [ ] 액션 실행 후 데이터가 갱신되는가?
---
## 🔧 데이터 흐름 패턴
### 패턴 A: 클라이언트 사이드 필터링
```
initialData={전체데이터}
config.tabFilter() → 탭 필터링
config.searchFilter() → 검색 필터링
내부 페이지네이션 → displayData
```
**적합한 경우**:
- 데이터량 적음 (500개 이하)
- 전체 데이터를 한번에 로드 가능
### 패턴 B: 서버 사이드 필터링
```
initialData={API로 받은 데이터}
onTabChange → 외부 상태 변경 → API 재호출
onSearchChange → 외부 상태 변경 → API 재호출
externalPagination으로 페이지 제어
```
**적합한 경우**:
- 데이터량 많음 (1000개 이상)
- 페이지네이션된 API 사용
---
## 발견된 에러 통계
| 에러 유형 | 발생 횟수 | 패턴 |
|----------|----------|------|
| headerActions 함수 아님 | 2회 | 거래처관리(영업), 단가관리(판매) |
| 탭 데이터 미갱신 | 2회 | 단가관리(판매), 품목관리 |
| 선택 건수 0 표시 | 1회 | 휴가관리 |
| externalSelection 형태 불일치 | 1회 | 휴가관리 |
| showPresets 누락 | 2회 | 근태관리, 사원관리 |
| 탭 카운트 동기화 | 1회 | 휴가관리 |
---
## 변경 이력
| 날짜 | 내용 |
|------|------|
| 2026-01-16 | 문서 초안 작성 (13개 에러 패턴 분석) |