242 lines
9.8 KiB
Markdown
242 lines
9.8 KiB
Markdown
# 탭 시스템 아키텍처 및 구현 계획
|
|
|
|
## 1. 개요
|
|
|
|
사이드바 메뉴 클릭 시 `router.push()` 페이지 이동 방식에서 **탭 기반 멀티 화면 시스템**으로 전환한다.
|
|
|
|
```
|
|
┌──────────────────────────┐
|
|
│ Tab Data Layer (중앙) │
|
|
API 응답 ────────→│ │
|
|
│ 탭별 상태 저장소 │
|
|
│ ├─ formData │
|
|
│ ├─ selectedRows │
|
|
│ ├─ scrollPosition │
|
|
│ ├─ modalState │
|
|
│ ├─ sortState │
|
|
│ └─ cacheState │
|
|
│ │
|
|
│ 공통 규칙 엔진 │
|
|
│ ├─ 날짜 포맷 규칙 │
|
|
│ ├─ 숫자/통화 포맷 규칙 │
|
|
│ ├─ 로케일 처리 규칙 │
|
|
│ ├─ 유효성 검증 규칙 │
|
|
│ └─ 데이터 타입 변환 규칙 │
|
|
│ │
|
|
│ F5 복원 / 캐시 관리 │
|
|
│ (sessionStorage 중앙관리) │
|
|
└────────────┬─────────────┘
|
|
│
|
|
가공 완료된 데이터
|
|
│
|
|
┌────────────────┼────────────────┐
|
|
│ │ │
|
|
화면 A (경량) 화면 B (경량) 화면 C (경량)
|
|
렌더링만 담당 렌더링만 담당 렌더링만 담당
|
|
```
|
|
|
|
## 2. 레이어 구조
|
|
|
|
| 레이어 | 책임 |
|
|
|---|---|
|
|
| **Tab Data Layer** | 탭별 상태 보관, 캐시, 복원, 데이터 가공 |
|
|
| **공통 규칙 엔진** | 날짜/숫자/로케일 포맷, 유효성 검증 |
|
|
| **화면 컴포넌트** | 가공된 데이터를 받아서 렌더링만 담당 |
|
|
|
|
## 3. 파일 구성
|
|
|
|
| 파일 | 역할 |
|
|
|---|---|
|
|
| `stores/tabStore.ts` | Zustand 기반 탭 상태 관리 |
|
|
| `components/layout/TabBar.tsx` | 탭 바 UI (드래그, 우클릭, 오버플로우) |
|
|
| `components/layout/TabContent.tsx` | 탭별 콘텐츠 렌더링 (컨테이너) |
|
|
| `components/layout/EmptyDashboard.tsx` | 탭 없을 때 안내 화면 |
|
|
| `components/layout/AppLayout.tsx` | 전체 레이아웃 (사이드바 + 탭 + 콘텐츠) |
|
|
| `lib/tabStateCache.ts` | 탭별 상태 캐싱 엔진 |
|
|
| `lib/formatting/rules.ts` | 포맷 규칙 정의 |
|
|
| `lib/formatting/index.ts` | formatDate, formatNumber, formatCurrency |
|
|
| `app/(main)/screens/[screenId]/page.tsx` | 화면별 렌더링 |
|
|
|
|
## 4. 기술 스택
|
|
|
|
- Next.js 15, React 19, Zustand
|
|
- Tailwind CSS, shadcn/ui
|
|
|
|
---
|
|
|
|
## 5. Phase 1: 탭 껍데기
|
|
|
|
### 5-1. Zustand 탭 Store (`stores/tabStore.ts`)
|
|
- [ ] zustand 직접 의존성 추가
|
|
- [ ] Tab 인터페이스: id, type, title, screenId, menuObjid, adminUrl
|
|
- [ ] 탭 목록, 활성 탭 ID
|
|
- [ ] openTab, closeTab, switchTab, refreshTab
|
|
- [ ] closeOtherTabs, closeTabsToLeft, closeTabsToRight, closeAllTabs
|
|
- [ ] updateTabOrder (드래그 순서 변경)
|
|
- [ ] 중복 방지: 같은 탭이면 해당 탭으로 이동
|
|
- [ ] 닫기 후 왼쪽 탭으로 이동, 왼쪽 없으면 오른쪽
|
|
- [ ] sessionStorage 영속화 (persist middleware)
|
|
- [ ] 탭 ID 생성 규칙: V2 화면 `tab-{screenId}-{menuObjid}`, URL 탭 `tab-url-{menuObjid}`
|
|
|
|
### 5-2. TabBar 컴포넌트 (`components/layout/TabBar.tsx`)
|
|
- [ ] 고정 너비 탭, 화면 너비에 맞게 동적 개수
|
|
- [ ] 활성 탭: 새로고침 버튼 + X 버튼
|
|
- [ ] 비활성 탭: X 버튼만
|
|
- [ ] 오버플로우 시 +N 드롭다운 (ResizeObserver 감시)
|
|
- [ ] 드래그 순서 변경 (mousedown/move/up, DOM transform 직접 조작)
|
|
- [ ] 사이드바 메뉴 드래그 드롭 수신 (`application/tab-menu` 커스텀 데이터, 마우스 위치에 삽입)
|
|
- [ ] 우클릭 컨텍스트 메뉴 (새로고침/왼쪽닫기/오른쪽닫기/다른탭닫기/모든탭닫기)
|
|
- [ ] 휠 클릭: 탭 즉시 닫기
|
|
|
|
### 5-3. TabContent 컴포넌트 (`components/layout/TabContent.tsx`)
|
|
- [ ] display:none 방식 (비활성 탭 DOM 유지, 상태 보존)
|
|
- [ ] 지연 마운트 (한 번 활성화된 탭만 마운트)
|
|
- [ ] 안정적 순서 유지 (탭 순서 변경 시 리마운트 방지)
|
|
- [ ] 탭별 모달 격리 (DialogPortalContainerContext)
|
|
- [ ] tab.type === "screen" -> ScreenViewPageWrapper 임베딩
|
|
- [ ] tab.type === "admin" -> 동적 import로 관리자 페이지 렌더링
|
|
|
|
### 5-4. EmptyDashboard 컴포넌트 (`components/layout/EmptyDashboard.tsx`)
|
|
- [ ] 탭이 없을 때 "사이드바에서 메뉴를 선택하여 탭을 추가하세요" 표시
|
|
|
|
### 5-5. AppLayout 수정 (`components/layout/AppLayout.tsx`)
|
|
- [ ] handleMenuClick: router.push -> tabStore.openTab 호출
|
|
- [ ] 레이아웃: main 영역을 TabBar + TabContent로 교체
|
|
- [ ] children prop 제거 (탭이 콘텐츠 관리)
|
|
- [ ] 사이드바 메뉴 드래그 가능하게 (draggable)
|
|
|
|
### 5-6. 라우팅 연동
|
|
- [ ] `app/(main)/layout.tsx` 수정 - children 대신 탭 시스템
|
|
- [ ] URL 직접 접근 시 탭으로 열기 (북마크/공유 링크 대응)
|
|
|
|
---
|
|
|
|
## 6. Phase 2: F5 최대 복원
|
|
|
|
### 6-1. 탭 상태 캐싱 엔진 (`lib/tabStateCache.ts`)
|
|
- [ ] 탭별 상태 저장/복원 (sessionStorage)
|
|
- [ ] 저장 대상: formData, selectedRows, sortState, scrollPosition, modalState, checkboxState
|
|
- [ ] debounce 적용 (상태 변경마다 저장하지 않음)
|
|
|
|
### 6-2. 복원 로직
|
|
- [ ] 활성 탭: fresh API 호출 (캐시 데이터 무시)
|
|
- [ ] 비활성 탭: 캐시에서 복원
|
|
- [ ] 탭 닫기 시 해당 탭의 캐시 키 일괄 삭제
|
|
|
|
### 6-3. 캐시 키 관리 (clearTabStateCache)
|
|
|
|
탭 닫기/새로고침 시 관련 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}`
|
|
|
|
---
|
|
|
|
## 7. Phase 3: 포맷팅 중앙화
|
|
|
|
### 7-1. 포맷팅 규칙 엔진
|
|
|
|
```typescript
|
|
// lib/formatting/rules.ts
|
|
|
|
interface FormatRules {
|
|
date: {
|
|
display: string; // "YYYY-MM-DD"
|
|
datetime: string; // "YYYY-MM-DD HH:mm:ss"
|
|
input: string; // "YYYY-MM-DD"
|
|
};
|
|
number: {
|
|
locale: string; // 사용자 로케일 기반
|
|
decimals: number; // 기본 소수점 자릿수
|
|
};
|
|
currency: {
|
|
code: string; // 회사 설정 기반
|
|
locale: string;
|
|
};
|
|
}
|
|
|
|
export function formatValue(value: any, dataType: string, rules: FormatRules): string;
|
|
export function formatDate(value: any, format?: string): string;
|
|
export function formatNumber(value: any, locale?: string): string;
|
|
export function formatCurrency(value: any, currencyCode?: string): string;
|
|
```
|
|
|
|
### 7-2. 하드코딩 교체 대상
|
|
- [ ] V2DateRenderer.tsx
|
|
- [ ] EditModal.tsx
|
|
- [ ] InteractiveDataTable.tsx
|
|
- [ ] FlowWidget.tsx
|
|
- [ ] AggregationWidgetComponent.tsx
|
|
- [ ] aggregation.ts (피벗)
|
|
- [ ] 기타 하드코딩 파일들
|
|
|
|
---
|
|
|
|
## 8. Phase 4: ScreenViewPage 경량화
|
|
- [ ] 탭 데이터 레이어에서 받은 데이터로 렌더링만 담당
|
|
- [ ] API 호출, 캐시, 복원 로직 제거 (탭 레이어가 담당)
|
|
- [ ] 관리자 페이지도 동일한 데이터 레이어 패턴 적용
|
|
|
|
---
|
|
|
|
---
|
|
|
|
## 구현 완료: 다중 스크롤 영역 F5 복원
|
|
|
|
### 개요
|
|
|
|
split panel 등 한 탭 안에 **스크롤 영역이 여러 개**인 화면에서, F5 새로고침 후에도 각 영역의 스크롤 위치가 복원된다.
|
|
|
|
탭 전환 시에는 `display: none` 방식으로 DOM이 유지되므로 브라우저가 스크롤을 자연 보존한다. 이 기능은 **F5 새로고침** 전용이다.
|
|
|
|
### 동작 방식
|
|
|
|
탭 내 모든 스크롤 가능한 요소를 DOM 경로(`"0/1/0/2"` 형태)와 함께 저장한다.
|
|
|
|
```
|
|
scrollPositions: [
|
|
{ path: "0/1/0/2", top: 150, left: 0 }, // 예: 좌측 패널
|
|
{ path: "0/1/1/3/1", top: 420, left: 0 }, // 예: 우측 패널
|
|
]
|
|
```
|
|
|
|
- **실시간 추적**: 스크롤 이벤트 발생 시 해당 요소의 경로와 위치를 Map에 기록
|
|
- **저장 시점**: 탭 전환 시 + `beforeunload`(F5/닫기) 시 sessionStorage에 저장
|
|
- **복원 시점**: 탭 활성화 시 경로를 기반으로 각 요소를 찾아 개별 복원
|
|
|
|
### 관련 파일 및 주요 함수
|
|
|
|
| 파일 | 역할 |
|
|
|---|---|
|
|
| `lib/tabStateCache.ts` | 스크롤 캡처/복원 핵심 로직 |
|
|
| `components/layout/TabContent.tsx` | 스크롤 이벤트 감지, 저장/복원 호출 |
|
|
|
|
**`tabStateCache.ts` 핵심 함수**:
|
|
|
|
| 함수 | 설명 |
|
|
|---|---|
|
|
| `getElementPath(element, container)` | 요소의 DOM 경로를 자식 인덱스 문자열로 생성 |
|
|
| `captureAllScrollPositions(container)` | TreeWalker로 컨테이너 하위 모든 스크롤 요소의 위치를 일괄 캡처 |
|
|
| `restoreAllScrollPositions(container, positions)` | 경로 기반으로 각 요소를 찾아 스크롤 위치 복원 (콘텐츠 렌더링 대기 폴링 포함) |
|
|
|
|
**`TabContent.tsx` 핵심 Ref**:
|
|
|
|
| Ref | 설명 |
|
|
|---|---|
|
|
| `lastScrollMapRef` | `Map<tabId, Map<path, {top, left}>>` - 탭 내 요소별 최신 스크롤 위치 |
|
|
| `pathCacheRef` | `WeakMap<HTMLElement, string>` - 동일 요소의 경로 재계산 방지용 캐시 |
|
|
|
|
---
|
|
|
|
## 9. 참고 파일
|
|
|
|
| 파일 | 비고 |
|
|
|---|---|
|
|
| `frontend/components/layout/AppLayout.tsx` | 사이드바 + 콘텐츠 레이아웃 |
|
|
| `frontend/app/(main)/screens/[screenId]/page.tsx` | 화면 렌더링 (건드리지 않음) |
|
|
| `frontend/stores/modalDataStore.ts` | Zustand store 참고 패턴 |
|
|
| `frontend/lib/adminPageRegistry.tsx` | 관리자 페이지 레지스트리 |
|