ERP-node/docs/ycshin-node/탭_시스템_설계.md

9.8 KiB

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

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. 포맷팅 규칙 엔진

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