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