Skip to content

리스트 페이지네이션 — usePagination + provide/inject (FE 메인테이너)

작성: 2026-06-22 · 상태: ✅ 수납 목록 3축(계약·유닛·멤버) 적용. 마스터데이터 7종·부과 목록은 미적용(전파 대기).

수납 목록이 시드 단지 규모(계약 302건)를 페이지네이션 없이 전부 렌더해 끝없는 스크롤 + 모바일 렌더 부담이 있었다. 기본 20건 분할 + 길게 보기 옵션으로 교체. (오너 결정 2026-06-22: 기본 20, 옵션 [20/50/100/전체]. 적용 범위 = 수납 목록만 먼저.)

구조 (3계층 배선)

MainOrg (부모)                 ── usePagination() 1 인스턴스 생성 → provide(PAGINATION_KEY)
 ├─ DynamicTableOrg (자식)     ── inject → watch(collectings.length)로 setTotal + slice(collectings)만 렌더
 └─ PaginationToolBarOrg (자식) ── inject → 컨트롤(처음/이전/페이지윈도/다음/마지막 · rows-per-page · goto)
  • provide/inject 로 형제 두 컴포넌트가 같은 인스턴스를 공유. 모듈 싱글톤 아님 → 리스트마다 독립(축 전환·다중 리스트 cross-talk 없음).
  • slice는 표시 전용: collectings(전체) computed는 불변 → 합계·일괄선택 computed는 전 페이지 기준 유지. 페이지 행만 pagedCollectings로 렌더.
  • 상위 배열을 슬라이스 → 계약/세대 단위로 20씩 분할. 각 행의 차수 펼침·유닛 하위행은 그 행에 종속되므로 영향 없음.

컴포저블 — src/composables/usePagination.js

js
const p = usePagination({ defaultSize = 20 })
p.page / p.pageSize / p.total / p.totalPages / p.isAll   // 상태(ref/computed)
p.rangeStart / p.rangeEnd                                 // "1–20" 표시용
p.setTotal(n)   // 행 수 갱신. 필터 축소 시 현재 페이지 클램프
p.setPage(n) / p.next() / p.prev() / p.first() / p.last() // [1, totalPages] 클램프
p.setPageSize(s)// 'all' | number. 변경 시 1페이지 복귀
p.slice(arr)    // 현재 페이지 분량 (isAll이면 전체)
p.pageWindow    // [{page}|{ellipsis}] — 처음·끝·현재±2 + ellipsis (결정적)
  • 상수: DEFAULT_PAGE_SIZE=20, PAGE_SIZE_OPTIONS=[20,50,100,'all'], pageSizeLabel('all')='전체', PAGINATION_KEY(inject 키 Symbol).
  • 단위 테스트: src/composables/__tests__/usePagination.spec.js (9 케이스 — slice·클램프·필터축소·pageWindow·전체).

SSOT 정합

  • DS는 .pagination CSS(EDS)만 제공pagination-item/item-selected/pagination-addons/pagination-visible/pagination-goto. 마크업 클래스는 그대로 소비(무수정). ellipsis 전용 클래스는 없어 비클릭 <span class="pagination-item pointer-events-none">…</span>로 처리.
  • 페이지 상태·슬라이싱은 앱 로직(DS 관심사 아님) — 컴포저블 소유. DS 카피본 변경 없음.
  • 툴바는 src/components/common/PaginationToolBarOrg.vue로 승격(축별 중복 목업 3개 제거) — 주입 의존만 하므로 완전 제네릭. 전파 시 그대로 재사용.

다른 리스트로 전파하는 법 (3 스텝)

  1. 리스트 부모(MainOrg)에 const pagination = usePagination(); provide(PAGINATION_KEY, pagination).
  2. 행을 렌더하는 테이블 컴포넌트에 inject(PAGINATION_KEY)watch(()=>rows.value.length, n=>pagination?.setTotal(n), {immediate:true}) + v-forpagination.slice(rows)로.
  3. 툴바는 common/PaginationToolBarOrg.vue import. (기존 정적 목업이 있으면 제거.)

데이터 도구 바(생성/연결/해제)와 페이지네이션 툴바는 별개 — DataToolBar는 유지, PaginationToolBar는 목록 하단.

한계 / 후속

  • 프로토는 클라이언트 사이드 슬라이싱(전체를 메모리에 빌드 후 자름). 실서비스는 서버 사이드 page/size 파라미터로 전환 — 컴포저블 인터페이스(setTotal/slice)는 그대로 두고 데이터 소스만 교체(slice는 이미 페이지된 응답이면 no-op).
  • 선택(체크박스) 상태는 페이지 전환 시 데이터 키 기반이라 유지되나, 현 프로토는 selection 미배선(별개 트랙).
  • 검색·정렬과의 상호작용(필터 후 1페이지 리셋)은 setTotal 클램프로 방어 — 명시적 1페이지 리셋이 필요하면 검색 핸들러에서 setPage(1).
  • 전파 대상: 마스터데이터 7종(차량·사업체·현금영수증·자동이체·가상계좌·일원·유닛) · 부과 목록.

검증: scripts/probe-pagination.mjs(계약 — 20행 분할·다음·마지막). 빌드·vitest 392 PASS.