# [계획서] 페이징 단락(그룹) 번호 네비게이션 - PageGroupNav 공통 컴포넌트 > 관련 문서: [맥락노트](./PGN[맥락]-페이징-단락이동.md) | [체크리스트](./PGN[체크]-페이징-단락이동.md) ## 개요 페이지네이션의 핵심 컨트롤(`<< < [번호들] > >>`)을 **재사용 가능한 공통 컴포넌트 `PageGroupNav`**로 분리합니다. 현재의 단순 `1 / n` 텍스트 표시를 **10개 단위 페이지 번호 버튼 그룹**으로 교체하고, `< >` 버튼을 **단락(그룹) 이동**으로, `<< >>` 버튼을 **첫/끝 단락 이동**으로 변경합니다. ### 접근 전략: C안 (핵심 컨트롤 분리 + 단계적 적용) - **1단계 (이번 작업)**: `PageGroupNav.tsx` 생성 + v2-table-list에 적용 - **2단계 (별도 작업)**: 나머지 페이징 사용처에 점진적 적용 이 전략을 선택한 이유: - 레이아웃을 강제하지 않는 순수 컨트롤 컴포넌트 → 어디든 조합 가능 - v2-table-list에서 먼저 검증 후 확산 → 리스크 최소화 - 2단계는 `import` 한 줄로 적용 가능 → 미래 작업 비용 최소 --- ## 현재 동작 ### 페이지네이션 UI ``` [<<] [<] 1 / 38 [>] [>>] ``` | 버튼 | 현재 동작 | |------|----------| | `<<` | 첫 페이지(1)로 이동 | | `<` | 이전 페이지(`currentPage - 1`)로 이동 | | 중앙 | `currentPage / totalPages` 텍스트 표시 (클릭 불가) | | `>` | 다음 페이지(`currentPage + 1`)로 이동 | | `>>` | 마지막 페이지(`totalPages`)로 이동 | ### 비활성화 조건 - `<<` `<` : `currentPage === 1` - `>` `>>` : `currentPage >= totalPages` ### 현재 코드 (TableListComponent.tsx, 5139~5182행) ```tsx {/* 중앙 페이지네이션 컨트롤 */}
``` --- ## 변경 후 동작 ### 페이지네이션 UI ``` [<<] [<] [1] [2] [3] [4] [5] [6] [7] [8] [9] [10] [>] [>>] ``` | 버튼 | 변경 후 동작 | |------|-------------| | `<<` | **첫 번째 단락**으로 이동 (1페이지 선택) | | `<` | **이전 단락**의 첫 페이지로 이동 | | 중앙 | 현재 단락의 페이지 번호 버튼 나열 (클릭으로 해당 페이지 이동) | | `>` | **다음 단락**의 첫 페이지로 이동 | | `>>` | **마지막 단락**의 첫 페이지로 이동 (마지막 페이지가 아님) | ### 비활성화 조건 - `<<` `<` : **첫 번째 단락**(1~10)을 보고 있을 때 - `>` `>>` : **마지막 단락**을 보고 있을 때 ### 단락(그룹) 개념 - 10개 단위로 페이지를 묶어 하나의 "단락"으로 취급 - 단락 1: 1~10, 단락 2: 11~20, 단락 3: 21~30, ... - 마지막 단락은 10개 미만일 수 있음 (예: 31~38) ### 고정 슬롯 레이아웃 (핵심 제약) **페이지 번호 영역은 항상 10개 슬롯을 고정 렌더링한다.** - 각 슬롯은 동일한 고정 너비(`w-9` 등)를 가짐 - 1자리(`1`)든 2자리(`11`)든 3자리(`100`)든 버튼 너비가 동일 - 마지막 단락이 10개 미만이면 남은 슬롯은 빈 공간(투명 placeholder)으로 채움 - 이로써 `< >` 버튼을 연속 클릭해도 **번호 버튼과 화살표 버튼의 위치가 절대 변하지 않음** ``` 단락 1~10: [1] [2] [3] [4] [5] [6] [7] [8] [9] [10] ← 10개 모두 채움 단락 11~20: [11][12][13][14][15][16][17][18][19][20] ← 너비 동일 단락 31~38: [31][32][33][34][35][36][37][38][ ][ ] ← 빈 슬롯 2개로 위치 고정 ``` --- ## 시각적 동작 예시 총 38페이지 기준: ### 단락별 페이지 번호 표시 | 현재 페이지 | 표시 번호 | `<<` `<` | `>` `>>` | |-------------|-----------|----------|----------| | 1 | **[1]** [2] [3] ... [10] | 비활성 | 활성 | | 5 | [1] [2] [3] [4] **[5]** [6] [7] [8] [9] [10] | 비활성 | 활성 | | 10 | [1] [2] [3] [4] [5] [6] [7] [8] [9] **[10]** | 비활성 | 활성 | | 11 | **[11]** [12] [13] ... [20] | 활성 | 활성 | | 25 | [21] [22] [23] [24] **[25]** [26] [27] [28] [29] [30] | 활성 | 활성 | | 31 | **[31]** [32] [33] ... [38] [ ] [ ] | 활성 | 비활성 | | 38 | [31] [32] [33] [34] [35] [36] [37] **[38]** [ ] [ ] | 활성 | 비활성 | ### 버튼 클릭 시나리오 | 현재 상태 | 클릭 | 결과 | |----------|------|------| | 5페이지 (단락 1~10) | `>` | 11페이지 선택, 단락 11~20 표시 | | 15페이지 (단락 11~20) | `<` | 1페이지 선택, 단락 1~10 표시 | | 15페이지 (단락 11~20) | `>>` | 31페이지 선택, 단락 31~38 표시 | | 35페이지 (단락 31~38) | `<<` | 1페이지 선택, 단락 1~10 표시 | | 5페이지 (단락 1~10) | `[7]` | 7페이지 선택, 단락 1~10 유지 | --- ## 아키텍처 ### 컴포넌트 구조 (C안) ```mermaid flowchart TD subgraph PageGroupNav ["PageGroupNav.tsx (새 공통 컴포넌트)"] Props["props: currentPage, totalPages, onPageChange, disabled, groupSize"] Logic["단락 계산 + 고정 슬롯 + 비활성화"] UI["<< < [번호들] > >>"] Props --> Logic --> UI end subgraph Phase1 ["1단계: 이번 작업"] V2Table["v2-table-list paginationJSX"] end subgraph Phase2 ["2단계: 별도 작업 (미래)"] TableList["table-list (구형)"] PaginationTsx["Pagination.tsx (관리자)"] DrillDown["DrillDown 모달"] Mail["메일 수신/발송"] Others["감사로그, 배치, DataTable 등"] end PageGroupNav --> V2Table PageGroupNav -.-> TableList PageGroupNav -.-> PaginationTsx PageGroupNav -.-> DrillDown PageGroupNav -.-> Mail PageGroupNav -.-> Others ``` ### v2-table-list 내부 데이터 흐름 ```mermaid flowchart TD A["currentPage, totalPages (state)"] --> B[PageGroupNav] B -->|onPageChange| C[handlePageChange] C --> D[setCurrentPage + onConfigChange] D --> E[백엔드 API 호출] E --> F[데이터 갱신] F --> A ``` ### v2-table-list 페이징 바 레이아웃 (변경 없음) ``` ┌─────────────────────────────────────────────────────────────────┐ │ [페이지크기 입력] │ << < [PageGroupNav] > >> │ [내보내기][새로고침] │ │ 좌측(유지) │ 중앙(교체) │ 우측(유지) │ └─────────────────────────────────────────────────────────────────┘ ``` --- ## 변경 대상 파일 ### 1단계 (이번 작업) | 구분 | 파일 | 변경 내용 | 변경 규모 | |------|------|----------|----------| | 생성 | `frontend/components/common/PageGroupNav.tsx` | 페이지 그룹 네비게이션 공통 컴포넌트 | 약 80줄 신규 | | 수정 | `frontend/lib/registry/components/v2-table-list/TableListComponent.tsx` | `paginationJSX` 중앙 영역을 `PageGroupNav`로 교체 (5139~5182행) | 약 40줄 → 5줄 | - `handlePageChange` 함수는 기존 것을 그대로 사용 (동작 변경 없음) - 좌측(페이지크기 입력), 우측(내보내기/새로고침) 영역은 변경하지 않음 - 백엔드 변경 없음, DB 변경 없음 ### 1단계 적용 범위 v2-table-list를 사용하는 **모든 동적 화면**에 자동 적용: - 품목정보, 거래처관리, 판매품목정보, 설비정보 등 ### 2단계 적용 대상 (별도 작업, 미래) | 사용처 | 파일 | 현재 페이징 형태 | |--------|------|----------------| | table-list (구형) | `lib/registry/components/table-list/TableListComponent.tsx` | `<< < 현재/총 > >>` | | 공통 Pagination | `components/common/Pagination.tsx` | 번호 ±2 + `...` | | 피벗 드릴다운 | `lib/registry/components/pivot-grid/components/DrillDownModal.tsx` | `<< < 현재/총 > >>` | | v2 피벗 드릴다운 | `lib/registry/components/v2-pivot-grid/components/DrillDownModal.tsx` | 동일 | | 메일 수신함 | `app/(main)/admin/automaticMng/mail/receive/page.tsx` | 번호 5개 클릭 | | 메일 발송함 | `app/(main)/admin/automaticMng/mail/sent/page.tsx` | 동일 | | 감사 로그 | `app/(main)/admin/audit-log/page.tsx` | `< 현재/총 >` | | 배치 관리 | `app/(main)/admin/automaticMng/batchmngList/page.tsx` | 번호 5개 클릭 | | DataTable | `components/common/DataTable.tsx` | `<< < > >>` + 텍스트 | | FlowWidget | `components/screen/widgets/FlowWidget.tsx` | shadcn Pagination | --- ## 코드 설계 ### PageGroupNav.tsx 공통 컴포넌트 ```tsx // frontend/components/common/PageGroupNav.tsx "use client"; import { ChevronLeft, ChevronRight, ChevronsLeft, ChevronsRight } from "lucide-react"; import { Button } from "@/components/ui/button"; const DEFAULT_GROUP_SIZE = 10; interface PageGroupNavProps { currentPage: number; totalPages: number; onPageChange: (page: number) => void; disabled?: boolean; groupSize?: number; } export function PageGroupNav({ currentPage, totalPages, onPageChange, disabled = false, groupSize = DEFAULT_GROUP_SIZE, }: PageGroupNavProps) { const safeTotal = Math.max(1, totalPages); const currentGroupIndex = Math.floor((currentPage - 1) / groupSize); const groupStartPage = currentGroupIndex * groupSize + 1; const lastGroupIndex = Math.floor((safeTotal - 1) / groupSize); const lastGroupStartPage = lastGroupIndex * groupSize + 1; const isFirstGroup = currentGroupIndex === 0; const isLastGroup = currentGroupIndex === lastGroupIndex; // 10개 고정 슬롯 배열 const slots: (number | null)[] = []; for (let i = 0; i < groupSize; i++) { const page = groupStartPage + i; slots.push(page <= safeTotal ? page : null); } return (
{/* << 첫 단락 */} {/* < 이전 단락 */} {/* 페이지 번호 (고정 슬롯) */} {slots.map((page, idx) => page !== null ? ( ) : (
) )} {/* > 다음 단락 */} {/* >> 마지막 단락 */}
); } ``` ### v2-table-list 통합 (paginationJSX 중앙 영역 교체) 기존 5139~5182행의 `
` 블록을 다음으로 교체: ```tsx import { PageGroupNav } from "@/components/common/PageGroupNav"; // paginationJSX 내부 중앙 영역 ``` 좌측(페이지크기 입력), 우측(내보내기/새로고침)은 기존 코드 그대로 유지. --- ## 설계 원칙 - **레이아웃 무관 컴포넌트**: PageGroupNav는 순수 컨트롤만 담당. 외부 레이아웃(좌측/우측 부가 기능)을 강제하지 않음 - **기존 동작 무변경**: `handlePageChange` 함수는 수정하지 않음. 좌측/우측 영역도 변경하지 않음 - **고정 슬롯 레이아웃**: 페이지 번호 영역은 항상 `groupSize`개(기본 10) 슬롯 고정. 마지막 단락에서 부족하면 빈 div로 채움 - **고정 너비 버튼**: 모든 번호 버튼은 `w-8 sm:w-9` 고정. 1자리/2자리/3자리에 관계없이 동일 - **위치 불변**: `< >` `<< >>` 버튼을 연속 클릭해도 모든 버튼의 위치가 절대 변하지 않음 - **현재 페이지 강조**: `variant="default"`(primary) + `ring-2 ring-primary font-bold`, 나머지 `variant="outline"` - **엣지 케이스**: totalPages가 0이거나 1일 때도 정상 동작 (`safeTotal = Math.max(1, totalPages)`) - **빈 슬롯 접근성**: 빈 슬롯에 `cursor-default` 적용 (클릭 가능한 것처럼 보이지 않게) - **단계적 적용**: 1단계에서 v2-table-list로 검증 후, 2단계에서 나머지 사용처에 점진 적용 --- ## 추가 구현: 표시갯수(pageSize) 캐시 정책 ### 문제 기존 pageSize는 `onConfigChange`로 부모에 전파되어 DB에 저장되거나, `localStorage`에 저장되어 새로고침해도 사용자가 변경한 값이 남아있었음. ### 해결 | 항목 | 정책 | |------|------| | 저장소 | sessionStorage (탭 닫으면 자동 소멸) | | 키 구조 | `pageSize_{tabId}_{tableName}` (탭별 격리) | | 기본값 | 20 | | DB 전파 | 안 함 (onConfigChange 제거) | | F5 새로고침 | 활성 탭 캐시 삭제 → 기본값 20 | | 탭 바 새로고침 | 활성 탭 캐시 삭제 → 기본값 20 | | 비활성 탭 전환 | 캐시에서 복원 | | 입력 UX | onChange는 표시만, onBlur/Enter로 실제 적용 | ### 테이블 캐시 탭 격리 동일한 정책을 테이블 관련 캐시 전체에 적용: | 키 | 구조 | |----|------| | `tableState_{tabId}_{tableName}` | 컬럼 너비, 정렬, 틀고정, 그리드선, 헤더필터 | | `pageSize_{tabId}_{tableName}` | 표시갯수 | | `filterSettings_{tabId}_{base}` | 검색 필터 설정 | | `groupSettings_{tabId}_{base}` | 그룹 설정 | 사용자 설정(컬럼 가시성/순서/정렬 상태)은 localStorage에 유지 (세션 간 보존).