# 탭 시스템 아키텍처 및 구현 계획

## 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. 캐시 키 관리 (clearTabCache)

탭 닫기/새로고침 시 관련 sessionStorage 키 일괄 제거:
- `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: 컬럼 순서

---

## 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` | 관리자 페이지 레지스트리 |
