# 11. 페이지 패턴 가이드

> 대상: 프론트엔드 개발자
> 최종 업데이트: 2026-03-20

---

## 목차

| 번호 | 항목 |
|------|------|
| 11.1 | [리스트 페이지](#111-리스트-페이지) |
| 11.2 | [상세/폼 페이지](#112-상세폼-페이지) |
| 11.3 | [검색 모달](#113-검색-모달) |
| 11.4 | [공통 기능은 공통 컴포넌트에서](#114-공통-기능은-공통-컴포넌트에서) |

---

## 11.1 리스트 페이지

### UniversalListPage (권장)

대부분의 리스트 페이지는 `UniversalListPage`로 구성합니다.

```typescript
const config: UniversalListConfig<MyItem> = {
  // 검색 (클라이언트 사이드 필터링)
  clientSideFiltering: true,
  searchFilter: (item, searchValue) => {
    const q = searchValue.toLowerCase();
    return item.name.toLowerCase().includes(q) || item.code.toLowerCase().includes(q);
  },
  searchPlaceholder: '이름, 코드 검색...',

  // 컬럼 정의
  columns: [...],

  // 모바일 카드
  renderMobileCard: (item) => <MobileCard item={item} />,
};
```

### IntegratedListTemplateV2 필수 적용 항목

IntegratedListTemplateV2 사용 시 다음 10가지를 반드시 확인:

| # | 항목 | 필수 |
|---|------|------|
| 1 | `useColumnSettings` + `ColumnSettingsPopover` | O |
| 2 | `searchValue` + `onSearchChange` (or clientSideFiltering) | O |
| 3 | 체크박스 `Set<string>` 패턴 | O |
| 4 | 페이지네이션 | O |
| 5 | `renderMobileCard` | O |
| 6 | 테이블 행 클릭 -> 상세 이동 | O |
| 7 | 헤더 레이아웃 (StatCards, 필터) | O |
| 8 | `tableHeaderActions` (테이블 내 필터) | 선택 |
| 9 | `filterConfig` (필터 설정) | 선택 |
| 10 | 탭 구성 | 선택 |

### 검색 패턴 (필수)

```typescript
// ✅ 올바른 패턴 -- UniversalListPage + clientSideFiltering
const config: UniversalListConfig<MyItem> = {
  clientSideFiltering: true,
  searchFilter: (item, searchValue) => {
    const q = searchValue.toLowerCase();
    return item.name.toLowerCase().includes(q);
  },
};
// 데이터를 한 번 API로 로드 -> 검색은 메모리에서 즉시 필터링 -> 깜빡임 없음
```

```typescript
// ❌ 금지 패턴 -- IntegratedListTemplateV2에 onSearchChange 직접 연결
<IntegratedListTemplateV2
  searchValue={searchTerm}
  onSearchChange={(q) => { setSearchTerm(q); }}
/>
// 키입력마다 state 변경 -> re-render -> 화면 깜빡임, 한글 조합 불가
```

---

## 11.2 상세/폼 페이지

### 라우팅 규칙

- 별도 `/new` 경로 금지 -> `?mode=new` 쿼리파라미터 사용
- 별도 `/edit` 경로 금지 -> `?mode=edit` 쿼리파라미터 사용

```typescript
// ✅ 올바른 패턴
router.push('/some-page?mode=new');
router.push('/some-page/123?mode=edit');

// ❌ 금지 패턴
router.push('/some-page/new');
router.push('/some-page/123/edit');
```

### 페이지 구조

```typescript
export default function SomePage() {
  const searchParams = useSearchParams();
  const mode = searchParams.get('mode');

  if (mode === 'new') return <SomeForm />;
  return <SomeList />;
}
```

### 헤더 표준

| 위치 | 요소 |
|------|------|
| 상단 좌측 | 페이지 제목 (`<h1>`) |
| 상단 우측 | `<- 목록으로` 링크 |

### 하단 Sticky 액션 바 (필수)

```typescript
<div className="sticky bottom-0 bg-white border-t shadow-sm">
  <div className="px-3 py-3 md:px-6 md:py-4 flex items-center justify-between">
    <Button variant="outline" onClick={() => router.push(listPath)}>
      <X className="h-4 w-4 mr-1" /> 취소
    </Button>
    <Button onClick={handleSubmit} disabled={isSubmitting}>
      <Save className="h-4 w-4 mr-1" /> 저장
    </Button>
  </div>
</div>
```

| 모드 | 좌측 | 우측 |
|------|------|------|
| 등록 (new) | 취소 | 저장 |
| 상세 (view) | 취소 (목록으로) | 수정 |
| 수정 (edit) | 취소 | 저장 |

### 폼 레이아웃

- Card 내부에 버튼 넣지 않음 -> sticky 하단 바 사용
- 아이콘 포함: 취소(`X`), 저장(`Save`), 수정(`Pencil`)

---

## 11.3 검색 모달

### SearchableSelectionModal (필수)

검색+선택 기능이 필요한 모달은 반드시 `SearchableSelectionModal<T>`을 사용합니다.

```typescript
// ✅ 올바른 패턴
<SearchableSelectionModal<VendorItem>
  open={open}
  onOpenChange={setOpen}
  title="거래처 선택"
  fetchData={async () => {
    const result = await getVendors();
    return result.success ? result.data : [];
  }}
  columns={[
    { key: 'vendorName', label: '거래처명' },
    { key: 'businessNumber', label: '사업자번호' },
  ]}
  onSelect={(vendor) => {
    setSelectedVendor(vendor);
  }}
  searchFilter={(item, query) =>
    item.vendorName.includes(query) || item.businessNumber?.includes(query)
  }
/>
```

```typescript
// ❌ 금지 패턴 -- Dialog + Table 직접 조합
<Dialog>
  <Input onChange={...} />
  <Table>...</Table>
</Dialog>
```

---

## 11.4 공통 기능은 공통 컴포넌트에서

리스트 페이지 전체에 적용해야 하는 기능은 개별 페이지 수정 금지. 반드시 공통 레이어에서 처리.

| 기능 | 수정 위치 | 개별 페이지 수정 |
|------|----------|----------------|
| 검색 상태 보존 | `UniversalListPage` | 금지 |
| 검색 X(클리어) 버튼 | `SearchFilter` + `IntegratedListTemplateV2` | 금지 |
| 검색 디바운스 | `UniversalListPage` 내부 300ms | 금지 |
| 체크박스 선택 | `IntegratedListTemplateV2` | 금지 |
| 페이지네이션 | `IntegratedListTemplateV2` | 금지 |
| 모바일 카드/인피니티 | `IntegratedListTemplateV2` | 금지 |
| 컬럼 설정 | `useColumnSettings` + `ColumnSettingsPopover` | 금지 |

**원칙**: "26개 페이지에 하나씩 적용" -> 잘못된 접근. "공통 1곳 수정 -> 전체 자동 적용" -> 올바른 접근.
