프론트엔드

Admin Multi-Tab Dashboard

프론트엔드

Admin Multi-Tab Dashboard — 실전 적용 구조와 코드 예시

언제 쓰나 · 같은 데이터셋을 서로 다른 관점(리스트/차트/클러스터 등)으로 동시에 보여줘야 할 때 · 각 관점마다 필터는 공유하되, 집계·시각화 방식이 달라야 할 때 · Admin/Analytics 페이지에서 뷰 전환 + 필터 + 요약 KPI를 한 화면에 배치해야 할 때

#frontend#Admin#Multi-Tab#Dashboard
다운로드
# Admin Multi-Tab Dashboard

## 언제 쓰는가
- 같은 데이터셋을 서로 다른 관점(리스트/차트/클러스터 등)으로 동시에 보여줘야 할 때
- 각 관점마다 필터는 공유하되, 집계·시각화 방식이 달라야 할 때
- Admin/Analytics 페이지에서 뷰 전환 + 필터 + 요약 KPI를 한 화면에 배치해야 할 때

## 핵심 구조
- **3층 구조**: SummaryBar(숫자 요약) · FilterBar(공유 필터) · ViewSegment(뷰 전환 스위치)
- **컨테이너 단일 책임**: 필터 state 보유, 서버 fetch(쿼리스트링 동기화) 는 컨테이너만, 프레젠테이션 컴포넌트는 pure
- **데이터 플로우**:
  - 서버 파라미터 변경(필터) → 디바운스 후 refetch → AbortController로 이전 요청 취소
  - 클라이언트 파생(정렬·bucket 필터 등) → `useMemo`로 displayData 계산
  - 뷰 전환은 클라이언트 파생만 교체, 서버 fetch 불필요
- **일부 뷰 강제 파라미터**: 특정 뷰(예: Cluster)는 groupType을 강제 고정
- **파일 구조**:
  - `XxxView.jsx` — 컨테이너
  - `XxxSummaryBar.jsx` · `XxxFilterBar.jsx` · 각 뷰별 프레젠테이션 파일 분리
  - `xxxConstants.js` — 공유 색상 토큰·정렬 comparator

## 코드 예시
```jsx
// 컨테이너
const [view, setView] = useState('grouped')
const [windowDays, setWindowDays] = useState(30)
const [minCount, setMinCount] = useState(3)
const fetchAbortRef = useRef(null)

useEffect(() => {
  const h = setTimeout(() => {
    if (fetchAbortRef.current) fetchAbortRef.current.abort()
    const ctrl = new AbortController()
    fetchAbortRef.current = ctrl
    const params = new URLSearchParams({ windowDays, minCount, groupType: view === 'cluster' ? 'cluster' : groupType })
    fetch(`/api/xxx?${params}`, { signal: ctrl.signal })
      .then(r => r.json()).then(setData)
      .catch(e => e.name !== 'AbortError' && setError(e))
  }, 300)
  return () => clearTimeout(h)
}, [windowDays, minCount, view])

const display = useMemo(() => [...data].sort(sortComparators[sortBy]), [data, sortBy])
```

## 주의사항
- 서버 파라미터와 클라이언트 파생의 책임을 섞지 말 것 (서버 refetch 비용 ↑ 또는 재계산 낭비 ↑)
- AbortController를 쓰지 않으면 이전 요청이 뒤늦게 도착해 state를 덮어씌우는 경쟁 조건 발생
- 필터 초기값은 URL query 또는 localStorage에서 읽어 새로고침 후 복원 가능하게
- 뷰 전환 시 뷰별 특수 필터(cluster 모드 등)를 자동 적용해도 사용자의 수동 선택값은 보존
- 뷰별로 서버 응답이 크게 다르다면 탭 단위로 fetch 분리하는 것이 나을 수 있음

## 재사용 방법
1. 컨테이너(`XxxView.jsx`)를 먼저 복사, 뷰 키와 필터 키만 교체
2. SummaryBar/FilterBar는 각 도메인의 KPI·필터 항목에 맞춰 이름만 바꿔 사용
3. 공통 정렬 comparator·색상 토큰은 `xxxConstants.js`에 모으고 프레젠테이션에서 import
4. 참고 구현: `Obsidian_manage/src/components/growth/patterns/PatternsView.jsx`
jt · v1 · CC0-1.0 · 복사 0