docs: Add project conventions and guidelines for ERP/PLM project
- Introduced a comprehensive document outlining project conventions for the WACE ERP/PLM project. - Included sections on project structure, backend practices, frontend practices, and specific implementation patterns. - Established guidelines for file creation order, controller and service patterns, pagination handling, and caching strategies. - Enhanced documentation to improve consistency and maintainability across the codebase. These additions serve as a reference for developers to follow best practices and ensure uniformity in the project's development process.
This commit is contained in:
@@ -0,0 +1,389 @@
|
||||
# [계획서] 페이징 단락(그룹) 번호 네비게이션 - 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
|
||||
{/* 중앙 페이지네이션 컨트롤 */}
|
||||
<div className="flex items-center gap-2 sm:gap-4">
|
||||
<Button onClick={() => handlePageChange(1)}
|
||||
disabled={currentPage === 1 || loading}> {/* << */}
|
||||
<Button onClick={() => handlePageChange(currentPage - 1)}
|
||||
disabled={currentPage === 1 || loading}> {/* < */}
|
||||
|
||||
<span>{currentPage} / {totalPages || 1}</span>
|
||||
|
||||
<Button onClick={() => handlePageChange(currentPage + 1)}
|
||||
disabled={currentPage >= totalPages || loading}> {/* > */}
|
||||
<Button onClick={() => handlePageChange(totalPages)}
|
||||
disabled={currentPage >= totalPages || loading}> {/* >> */}
|
||||
</div>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 변경 후 동작
|
||||
|
||||
### 페이지네이션 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 (
|
||||
<div className="flex items-center gap-1">
|
||||
{/* << 첫 단락 */}
|
||||
<Button variant="outline" size="sm"
|
||||
onClick={() => onPageChange(1)}
|
||||
disabled={isFirstGroup || disabled}
|
||||
className="h-8 w-8 p-0 sm:h-9 sm:w-9">
|
||||
<ChevronsLeft className="h-3 w-3 sm:h-4 sm:w-4" />
|
||||
</Button>
|
||||
|
||||
{/* < 이전 단락 */}
|
||||
<Button variant="outline" size="sm"
|
||||
onClick={() => onPageChange((currentGroupIndex - 1) * groupSize + 1)}
|
||||
disabled={isFirstGroup || disabled}
|
||||
className="h-8 w-8 p-0 sm:h-9 sm:w-9">
|
||||
<ChevronLeft className="h-3 w-3 sm:h-4 sm:w-4" />
|
||||
</Button>
|
||||
|
||||
{/* 페이지 번호 (고정 슬롯) */}
|
||||
{slots.map((page, idx) =>
|
||||
page !== null ? (
|
||||
<Button key={idx} size="sm"
|
||||
variant={page === currentPage ? "default" : "outline"}
|
||||
onClick={() => onPageChange(page)}
|
||||
disabled={disabled}
|
||||
className="h-8 w-8 p-0 text-xs sm:h-9 sm:w-9 sm:text-sm">
|
||||
{page}
|
||||
</Button>
|
||||
) : (
|
||||
<div key={idx} className="h-8 w-8 sm:h-9 sm:w-9" />
|
||||
)
|
||||
)}
|
||||
|
||||
{/* > 다음 단락 */}
|
||||
<Button variant="outline" size="sm"
|
||||
onClick={() => onPageChange((currentGroupIndex + 1) * groupSize + 1)}
|
||||
disabled={isLastGroup || disabled}
|
||||
className="h-8 w-8 p-0 sm:h-9 sm:w-9">
|
||||
<ChevronRight className="h-3 w-3 sm:h-4 sm:w-4" />
|
||||
</Button>
|
||||
|
||||
{/* >> 마지막 단락 */}
|
||||
<Button variant="outline" size="sm"
|
||||
onClick={() => onPageChange(lastGroupStartPage)}
|
||||
disabled={isLastGroup || disabled}
|
||||
className="h-8 w-8 p-0 sm:h-9 sm:w-9">
|
||||
<ChevronsRight className="h-3 w-3 sm:h-4 sm:w-4" />
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
### v2-table-list 통합 (paginationJSX 중앙 영역 교체)
|
||||
|
||||
기존 5139~5182행의 `<div className="flex items-center gap-2 sm:gap-4">` 블록을 다음으로 교체:
|
||||
|
||||
```tsx
|
||||
import { PageGroupNav } from "@/components/common/PageGroupNav";
|
||||
|
||||
// paginationJSX 내부 중앙 영역
|
||||
<PageGroupNav
|
||||
currentPage={currentPage}
|
||||
totalPages={totalPages}
|
||||
onPageChange={handlePageChange}
|
||||
disabled={loading}
|
||||
/>
|
||||
```
|
||||
|
||||
좌측(페이지크기 입력), 우측(내보내기/새로고침)은 기존 코드 그대로 유지.
|
||||
|
||||
---
|
||||
|
||||
## 설계 원칙
|
||||
|
||||
- **레이아웃 무관 컴포넌트**: 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에 유지 (세션 간 보존).
|
||||
@@ -0,0 +1,128 @@
|
||||
# [맥락노트] 페이징 단락(그룹) 번호 네비게이션 - PageGroupNav 공통 컴포넌트
|
||||
|
||||
> 관련 문서: [계획서](./PGN[계획]-페이징-단락이동.md) | [체크리스트](./PGN[체크]-페이징-단락이동.md)
|
||||
|
||||
---
|
||||
|
||||
## 왜 이 작업을 하는가
|
||||
|
||||
- 현재 페이지네이션은 `1 / 38` 텍스트만 표시하고 `< >`로 한 페이지씩 이동
|
||||
- 수십 페이지가 있을 때 원하는 페이지로 빠르게 이동할 수 없음
|
||||
- 페이지 번호를 직접 클릭할 수 있어야 UX가 개선됨
|
||||
|
||||
---
|
||||
|
||||
## 핵심 결정 사항과 근거
|
||||
|
||||
### 1. 공통 컴포넌트로 분리 (C안)
|
||||
|
||||
- **결정**: `PageGroupNav.tsx`라는 순수 컨트롤 컴포넌트를 별도 파일로 생성
|
||||
- **근거**: 프로젝트에 페이징이 15곳 이상 존재. 인라인 수정하면 같은 로직을 복사해야 함
|
||||
- **대안 검토 A**: v2-table-list 인라인만 수정 → 기각 (미래 확장 시 복사-붙여넣기 기술 부채)
|
||||
- **대안 검토 B**: 기존 `Pagination.tsx` 업그레이드 → 기각 (전체 행 레이아웃이 포함되어 v2-table-list와 레이아웃 충돌)
|
||||
- **대안 검토 D**: 전체 한번에 적용 → 기각 (12파일 동시 수정은 블래스트 반경이 큼)
|
||||
|
||||
### 2. 레이아웃 무관 설계
|
||||
|
||||
- **결정**: PageGroupNav는 `<< < [번호들] > >>`만 렌더링. 외부 레이아웃(페이지크기, 내보내기 등)을 포함하지 않음
|
||||
- **근거**: 사용처마다 레이아웃이 다름. v2-table-list는 좌측(페이지크기)+중앙(컨트롤)+우측(내보내기), Pagination.tsx는 좌측(페이지정보)+우측(크기선택+컨트롤). 레이아웃을 강제하면 props 분기가 증가하여 복잡해짐
|
||||
|
||||
### 3. 10개 단위 단락(그룹)
|
||||
|
||||
- **결정**: 페이지를 10개씩 묶어 하나의 단락으로 취급
|
||||
- **근거**: 사용자에게 익숙한 패턴 (네이버, 구글 등). 5개는 너무 적고, 20개는 너무 많음
|
||||
- **확장성**: `groupSize` props로 기본값 10을 변경 가능하게 설계
|
||||
|
||||
### 4. `< >` = 단락 이동, `<< >>` = 첫/끝 단락
|
||||
|
||||
- **결정**: `<`는 이전 단락 첫 페이지, `>`는 다음 단락 첫 페이지. `<<`는 1페이지, `>>`는 마지막 단락 첫 페이지
|
||||
- **근거**: 사용자 요청. 기존의 "한 페이지씩 이동"은 번호 클릭으로 대체됨
|
||||
- **주의**: `>>`는 마지막 **페이지**가 아닌 마지막 **단락의 첫 페이지**로 이동. 예: 총 38페이지일 때 `>>` 클릭 → 31페이지 선택 (38이 아님)
|
||||
|
||||
### 5. 고정 슬롯 + 고정 너비
|
||||
|
||||
- **결정**: 항상 10개 슬롯을 렌더링하고, 모든 버튼은 동일한 고정 너비(`w-8 sm:w-9`)
|
||||
- **근거**: `< >` 버튼을 연속 클릭할 때 번호 자릿수(1자리→2자리)나 페이지 수(10개→8개) 변화로 버튼 위치가 흔들리면 안 됨
|
||||
- **구현**: 마지막 단락에서 페이지가 10개 미만이면 남은 슬롯은 동일 크기의 빈 `<div>`로 채움
|
||||
|
||||
### 6. 단계적 적용 (1단계: v2-table-list만)
|
||||
|
||||
- **결정**: 이번 작업은 v2-table-list에만 적용. 나머지는 별도 작업으로 점진 적용
|
||||
- **근거**: 15곳 동시 수정은 리스크가 높음. v2-table-list가 가장 많이 사용되므로 여기서 검증 후 확산
|
||||
|
||||
### 7. 비활성화 기준은 단락 기준
|
||||
|
||||
- **결정**: `<< <`는 첫 번째 단락일 때 비활성화 (currentPage === 1이 아님). `> >>`는 마지막 단락일 때 비활성화
|
||||
- **근거**: 기존은 currentPage 기준이었지만, 단락 이동이므로 단락 기준으로 변경이 자연스러움. 첫 단락 안에서 5페이지에 있어도 `<`는 비활성화
|
||||
|
||||
---
|
||||
|
||||
## 관련 파일 위치
|
||||
|
||||
| 구분 | 파일 경로 | 설명 |
|
||||
|------|----------|------|
|
||||
| 생성 | `frontend/components/common/PageGroupNav.tsx` | 페이지 그룹 네비게이션 공통 컴포넌트 |
|
||||
| 수정 | `frontend/lib/registry/components/v2-table-list/TableListComponent.tsx` | paginationJSX 중앙 영역 교체 (5139~5182행) |
|
||||
| 참고 | `frontend/components/common/Pagination.tsx` | 기존 공통 페이지네이션 (이번에 수정 안 함) |
|
||||
|
||||
---
|
||||
|
||||
## 기술 참고
|
||||
|
||||
### 단락 계산 공식
|
||||
|
||||
```
|
||||
groupSize = 10 (기본값)
|
||||
currentGroupIndex = Math.floor((currentPage - 1) / groupSize)
|
||||
groupStartPage = currentGroupIndex * groupSize + 1
|
||||
groupEndPage = Math.min(groupStartPage + groupSize - 1, totalPages)
|
||||
|
||||
lastGroupIndex = Math.floor((totalPages - 1) / groupSize)
|
||||
lastGroupStartPage = lastGroupIndex * groupSize + 1
|
||||
|
||||
isFirstGroup = currentGroupIndex === 0
|
||||
isLastGroup = currentGroupIndex === lastGroupIndex
|
||||
```
|
||||
|
||||
### 고정 슬롯 배열 생성
|
||||
|
||||
```
|
||||
slots = [groupStart, groupStart+1, ..., groupEnd, null, null, ...] (총 groupSize개)
|
||||
예: 단락 31~38 → [31, 32, 33, 34, 35, 36, 37, 38, null, null]
|
||||
```
|
||||
|
||||
### handlePageChange 호출 흐름
|
||||
|
||||
```
|
||||
PageGroupNav onPageChange(page)
|
||||
→ TableListComponent handlePageChange(newPage)
|
||||
→ setCurrentPage(newPage)
|
||||
→ useEffect 트리거 → 백엔드 API 재호출 (page 파라미터 변경)
|
||||
```
|
||||
|
||||
- handlePageChange는 `setCurrentPage`만 호출. `onConfigChange` 전파는 제거됨 (pageSize/currentPage는 세션 전용)
|
||||
- handlePageChange는 기존 함수 그대로 사용. PageGroupNav가 올바른 page 값을 전달하기만 하면 됨
|
||||
|
||||
---
|
||||
|
||||
## 추가 결정: 표시갯수(pageSize) 캐시 정책
|
||||
|
||||
### 8. pageSize는 세션 전용, DB에 저장 안 함
|
||||
|
||||
- **결정**: pageSize를 `onConfigChange`로 부모/DB에 전파하지 않음. sessionStorage에만 탭별로 저장
|
||||
- **근거**: pageSize는 일시적 탐색 설정이지 영구 화면 설정이 아님. DB에 저장하면 다른 사용자에게도 영향이 가고, 새로고침 시 의도치 않은 값이 남음
|
||||
- **F5 정책**: 활성 탭은 캐시 삭제 → 기본값 20으로 fresh start. 비활성 탭은 캐시 유지
|
||||
|
||||
### 9. 테이블 캐시는 탭별 격리 (탭 ID 스코프)
|
||||
|
||||
- **결정**: `tableState_*`, `pageSize_*`, `filterSettings_*`, `groupSettings_*` 키를 `{prefix}_{tabId}_{tableName}` 구조로 변경
|
||||
- **근거**: 같은 테이블이 여러 탭에서 열릴 수 있음. 탭 구분 없으면 "활성 탭 캐시만 삭제" 불가능
|
||||
- **구현**: `useTabId()` 훅으로 현재 탭 ID 접근. `clearTabCache(tabId)`에서 해당 탭의 모든 관련 키 일괄 삭제
|
||||
|
||||
### 10. localStorage vs sessionStorage 분류
|
||||
|
||||
- **결정**: 탭별 캐시는 sessionStorage, 사용자 설정은 localStorage
|
||||
- **근거**: 탭별 캐시(컬럼 너비 캐시, 필터, 그룹, pageSize)는 탭 닫으면 무의미. 사용자 설정(컬럼 가시성, 순서, 정렬)은 사용자가 의도적으로 변경한 환경설정이므로 세션 간 보존
|
||||
- **분류**:
|
||||
- sessionStorage: `tableState_*`, `pageSize_*`, `filterSettings_*`, `groupSettings_*`
|
||||
- localStorage: `table_column_visibility_*`, `table_sort_state_*`, `table_column_order_*`
|
||||
@@ -0,0 +1,90 @@
|
||||
# [체크리스트] 페이징 단락(그룹) 번호 네비게이션 - PageGroupNav 공통 컴포넌트
|
||||
|
||||
> 관련 문서: [계획서](./PGN[계획]-페이징-단락이동.md) | [맥락노트](./PGN[맥락]-페이징-단락이동.md)
|
||||
|
||||
---
|
||||
|
||||
## 공정 상태
|
||||
|
||||
- 전체 진행률: **100%** (완료)
|
||||
- 현재 단계: 4단계 완료
|
||||
|
||||
---
|
||||
|
||||
## 구현 체크리스트
|
||||
|
||||
### 1단계: PageGroupNav 공통 컴포넌트 생성
|
||||
|
||||
- [x] `frontend/components/common/PageGroupNav.tsx` 파일 생성
|
||||
- [x] `PageGroupNavProps` 인터페이스 정의 (currentPage, totalPages, onPageChange, disabled, groupSize)
|
||||
- [x] 단락 계산 로직 구현 (currentGroupIndex, groupStartPage, lastGroupIndex 등)
|
||||
- [x] 10개 고정 슬롯 배열 생성 (빈 슬롯은 null)
|
||||
- [x] `<<` 첫 단락 버튼 (isFirstGroup일 때 비활성화)
|
||||
- [x] `<` 이전 단락 버튼 (isFirstGroup일 때 비활성화)
|
||||
- [x] 페이지 번호 버튼 렌더링 (현재 페이지 variant="default", 나머지 variant="outline")
|
||||
- [x] 빈 슬롯 렌더링 (동일 크기 빈 div)
|
||||
- [x] `>` 다음 단락 버튼 (isLastGroup일 때 비활성화)
|
||||
- [x] `>>` 마지막 단락 버튼 (isLastGroup일 때 비활성화, 마지막 단락 첫 페이지로 이동)
|
||||
- [x] 고정 너비 스타일 적용 (h-8 w-8 sm:h-9 sm:w-9)
|
||||
- [x] totalPages가 0 또는 1일 때 엣지 케이스 처리
|
||||
|
||||
### 2단계: v2-table-list 통합
|
||||
|
||||
- [x] `TableListComponent.tsx`에 `PageGroupNav` import 추가
|
||||
- [x] `paginationJSX`의 중앙 컨트롤 영역(5139~5182행)을 `<PageGroupNav>` 호출로 교체
|
||||
- [x] props 연결: currentPage, totalPages, handlePageChange, loading
|
||||
- [x] 좌측(페이지크기 입력) 영역 변경 없음 확인
|
||||
- [x] 우측(내보내기/새로고침) 영역 변경 없음 확인
|
||||
|
||||
### 3단계: 검증
|
||||
|
||||
- [x] 품목정보 화면에서 페이지 번호 클릭 동작 확인
|
||||
- [x] `< >` 단락 이동 동작 확인 (1~10 → 11~20 → ...)
|
||||
- [x] `<< >>` 첫/끝 단락 이동 동작 확인
|
||||
- [x] `>>` 클릭 시 마지막 단락의 첫 페이지 선택 확인 (마지막 페이지가 아님)
|
||||
- [x] 첫 단락에서 `<< <` 비활성화 확인
|
||||
- [x] 마지막 단락에서 `> >>` 비활성화 확인
|
||||
- [x] 고정 슬롯: 단락 이동 시 버튼 위치 변동 없음 확인
|
||||
- [x] 고정 너비: 1자리/2자리 숫자에서 버튼 크기 동일 확인
|
||||
- [x] 마지막 단락이 10개 미만일 때 빈 슬롯으로 위치 고정 확인
|
||||
- [x] totalPages가 1일 때 정상 동작 확인 (단일 페이지)
|
||||
- [x] 로딩 중 모든 버튼 비활성화 확인
|
||||
- [x] 페이지 크기 변경 시 첫 페이지로 리셋 확인
|
||||
|
||||
### 4단계: 정리
|
||||
|
||||
- [x] 린트 에러 없음 확인
|
||||
- [x] 이 체크리스트 완료 표시 업데이트
|
||||
|
||||
### 5단계: 표시갯수(pageSize) 캐시 정책
|
||||
|
||||
- [x] 표시갯수 입력 시 onChange → 표시만 변경, 실제 적용은 onBlur/Enter
|
||||
- [x] 입력 필드 값 string 타입으로 변경 (백스페이스로 비우기 가능)
|
||||
- [x] 표시갯수 변경 시 1페이지로 리셋 + 데이터 정상 로드
|
||||
- [x] onConfigChange로 DB/부모 전파 제거 (pageSize는 세션 전용)
|
||||
- [x] localStorage → sessionStorage 전환 (탭 닫으면 자동 소멸)
|
||||
- [x] 키를 탭 ID 스코프로 변경 (`pageSize_{tabId}_{tableName}`)
|
||||
- [x] F5 새로고침 시 활성 탭 캐시 삭제 → 기본값 20 초기화
|
||||
- [x] 탭 바 새로고침 버튼 시 캐시 삭제 → 기본값 20 초기화
|
||||
- [x] 비활성 탭 캐시 유지 (탭 전환 시 복원)
|
||||
|
||||
### 6단계: 테이블 캐시 탭 격리
|
||||
|
||||
- [x] tableStateKey 탭 ID 스코프 (`tableState_{tabId}_{tableName}`) + sessionStorage
|
||||
- [x] filterSettingKey 탭 ID 스코프 (`filterSettings_{tabId}_{base}`) + sessionStorage
|
||||
- [x] groupSettingKey 탭 ID 스코프 (`groupSettings_{tabId}_{base}`) + sessionStorage
|
||||
- [x] clearTabCache 확장 (tableState_/pageSize_/filterSettings_/groupSettings_ 일괄 삭제)
|
||||
- [x] TabContent.tsx 모듈 레벨 플래그로 F5 감지 → 활성 탭 캐시만 삭제
|
||||
- [x] tabStore.refreshTab에 clearTabCache 추가
|
||||
|
||||
---
|
||||
|
||||
## 변경 이력
|
||||
|
||||
| 날짜 | 내용 |
|
||||
|------|------|
|
||||
| 2026-03-11 | 계획서, 맥락노트, 체크리스트 작성 완료 |
|
||||
| 2026-03-11 | 1단계(PageGroupNav 생성) + 2단계(v2-table-list 통합) + 4단계(린트) 완료. 3단계(수동 검증)은 브라우저에서 확인 필요 |
|
||||
| 2026-03-11 | 추가 개선: 선택 페이지 강조(ring + font-bold), 빈 슬롯 cursor-default 적용. 3단계 검증 완료. 전체 완료 |
|
||||
| 2026-03-11 | 5단계: pageSize 입력 UX 개선 + 캐시 정책 (sessionStorage + 탭 스코프 + F5/탭새로고침 초기화) |
|
||||
| 2026-03-11 | 6단계: 테이블 전체 캐시를 탭별 격리 (localStorage → sessionStorage + 탭 ID 스코프) |
|
||||
@@ -123,15 +123,49 @@
|
||||
- [ ] 비활성 탭: 캐시에서 복원
|
||||
- [ ] 탭 닫기 시 해당 탭의 캐시 키 일괄 삭제
|
||||
|
||||
### 6-3. 캐시 키 관리 (clearTabStateCache)
|
||||
### 6-3. 캐시 키 관리 (clearTabCache)
|
||||
|
||||
탭 닫기/새로고침 시 관련 sessionStorage 키 일괄 제거:
|
||||
- `tab-cache-{screenId}-{menuObjid}`
|
||||
- `page-scroll-{screenId}-{menuObjid}`
|
||||
- `tsp-{screenId}-*`, `table-state-{screenId}-*`
|
||||
- `split-sel-{screenId}-*`, `catval-sel-{screenId}-*`
|
||||
- `bom-tree-{screenId}-*`
|
||||
- URL 탭: `tsp-{urlHash}-*`, `admin-scroll-{url}`
|
||||
- `tab-cache-{tabId}` (폼/스크롤 캐시)
|
||||
- `tableState_{tabId}_*` (컬럼 너비, 정렬, 틀고정, 그리드선, 헤더필터)
|
||||
- `pageSize_{tabId}_*` (표시갯수)
|
||||
- `filterSettings_{tabId}_*` (검색 필터 설정)
|
||||
- `groupSettings_{tabId}_*` (그룹 설정)
|
||||
|
||||
### 6-4. F5 새로고침 시 캐시 정책 (구현 완료)
|
||||
|
||||
| 탭 상태 | F5 시 동작 |
|
||||
|---------|-----------|
|
||||
| **활성 탭** | `clearTabCache(activeTabId)` → 캐시 전체 삭제 → fresh API 호출 |
|
||||
| **비활성 탭** | 캐시 유지 → 탭 전환 시 복원 |
|
||||
|
||||
**구현 방식**: `TabContent.tsx`에 모듈 레벨 플래그(`hasHandledPageLoad`)를 사용.
|
||||
전체 페이지 로드 시 모듈이 재실행되어 플래그가 `false`로 리셋.
|
||||
SPA 내비게이션에서는 모듈이 유지되므로 `true`로 남아 중복 실행 방지.
|
||||
|
||||
### 6-5. 탭 바 새로고침 버튼 (구현 완료)
|
||||
|
||||
`tabStore.refreshTab(tabId)` 호출 시:
|
||||
1. `clearTabCache(tabId)` → 해당 탭의 모든 sessionStorage 캐시 삭제
|
||||
2. `refreshKey` 증가 → 컴포넌트 리마운트 → 기본값으로 초기화
|
||||
|
||||
### 6-6. 저장소 분류 기준 (구현 완료)
|
||||
|
||||
| 데이터 성격 | 저장소 | 키 구조 | 비고 |
|
||||
|------------|--------|---------|------|
|
||||
| 탭별 캐시 | sessionStorage | `{prefix}_{tabId}_{tableName}` | 탭 닫으면 소멸 |
|
||||
| 사용자 설정 | localStorage | `{prefix}_{tableName}_{userId}` | 세션 간 보존 |
|
||||
|
||||
**탭별 캐시 (sessionStorage)**:
|
||||
- tableState: 컬럼 너비, 정렬, 틀고정, 그리드선, 헤더필터
|
||||
- pageSize: 표시갯수
|
||||
- filterSettings: 검색 필터 설정
|
||||
- groupSettings: 그룹 설정
|
||||
|
||||
**사용자 설정 (localStorage)**:
|
||||
- table_column_visibility: 컬럼 표시/숨김
|
||||
- table_sort_state: 정렬 상태
|
||||
- table_column_order: 컬럼 순서
|
||||
|
||||
---
|
||||
|
||||
|
||||
Reference in New Issue
Block a user