# 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개 에러 패턴 분석) |
