# 설계서: admin/page.tsx 분리 + 토큰/SSE 유틸 통일 (#329)

> **상태**: Approved
> **작성**: [AI] Architect · **최종수정**: 2026-06-15
> **추적성** — Redmine: #329 · 부모: #304 (현행화 frontend-admin, 09-Done)
> · 구현 파일: `frontend/src/app/admin/page.tsx`, `frontend/src/app/admin/_panels/*.tsx` (신규), `frontend/src/lib/admin-utils.ts`
> · 테스트: 수동 (각 탭 진입 + 기존 시나리오 회귀)

## 1. 목적 (Why)

`admin/page.tsx`가 2817 LOC 단일 파일. 5개 패널이 같은 파일 안에 함께 있어 (a) 빌드 변경 시 무관한 패널까지 재빌드/재배포, (b) 코드 리뷰 시 충돌 가능성↑, (c) 로직 격리 어려움. 또한 `localStorage` 직접 호출 + SSE 파싱 코드 중복이 남아있어 #304 후속에서 한 번 더 정리.

## 2. 범위 (Scope)

- **포함**
  - 5개 패널을 `app/admin/_panels/<Panel>.tsx`로 추출 (Next.js underscore prefix로 라우팅 제외).
  - `page.tsx`는 탭 라우팅 + 헤더 + 패널 import만.
  - 남은 `localStorage.getItem("tasteby_token")` 호출 → `getAdminToken()`/`authHeaders()` 교체.
  - SSE 파싱 중복(약 5~6곳) → `consumeSseStream()` 활용.
  - 공유 타입(`AdminUser`/`UserFavorite`/`UserReview`/`UserMemo`/`VideoSortKey`)을 _panels 파일 내부 또는 `api.ts`에 옮김(짧은 타입만).
- **제외**
  - 패널 내부 로직 변경 (state/effect 그대로).
  - catch{/*ignore*/} 일괄 로깅 (별도 후속).
  - ad-hoc 타입 캐스팅 정리 (별도 후속).
  - 디자인 시스템 색상 통일.

## 3. 인수조건

- [ ] 5개 파일 신규: `_panels/{Channels,Videos,Restaurants,Users,Daemon}Panel.tsx`.
- [ ] `page.tsx` < 200 LOC (이전 2817).
- [ ] localStorage 직접 호출이 admin 페이지 내에 0건.
- [ ] SSE reader 직접 호출(`response.body?.getReader()`)이 0건.
- [ ] 모든 탭 진입 + 기존 시나리오 회귀 없음(빌드 통과 + 수동 smoke).
- [ ] dev 빌드 + 운영 배포 성공.

## 4. 컨텍스트 & 제약

- Next.js 16 (App Router). `_` prefix 디렉토리는 route 제외.
- `"use client"` 지시문 각 패널 파일 상단에 필요.
- React Server Components 미사용 (모두 클라이언트 컴포넌트).
- `useAuth()` 훅이 `auth-context`에 있음. 부모 `AdminPage`가 isAdmin 판정 후 prop으로 패널에 전달.
- 패널 간 상태 공유 없음 (각각 독립).

## 5. 아키텍처 개요

```
app/admin/
  page.tsx                     ← 탭 라우팅 + 헤더 + CacheFlushButton + 패널 import
  _panels/
    ChannelsPanel.tsx          ← 214 LOC
    VideosPanel.tsx            ← 1272 LOC (가장 큼)
    RestaurantsPanel.tsx       ← 667 LOC
    UsersPanel.tsx             ← 332 LOC
    DaemonPanel.tsx            ← 223 LOC

lib/admin-utils.ts (이미 존재)
  ├─ getAdminToken()
  ├─ authHeaders()
  └─ consumeSseStream()
```

## 6. 함수 명세

| 단위 | 책임 | 비고 |
|------|------|------|
| `page.tsx` (재작성) | 탭 라우팅 + 패널 import | < 200 LOC |
| `_panels/*.tsx` (신규 5개) | 각 패널 로직 그대로 옮김 | "use client" |
| `localStorage` 호출 (~10곳) | `getAdminToken()`/`authHeaders()`로 통일 | 의미 동일 |
| SSE `getReader` (~5곳) | `consumeSseStream(resp, onEvent)`로 통일 | 의미 동일 |

## 7. 흐름

1. `_panels/` 디렉토리 생성.
2. 각 패널 함수 + 그에 종속된 타입/상수를 `<Panel>Panel.tsx`로 잘라 옮김.
3. 각 파일에 `"use client"` + import 추가.
4. `localStorage.getItem("tasteby_token")` → `getAdminToken()` 일괄.
5. SSE `getReader/decoder/buf.split/match` 패턴 → `consumeSseStream(resp, onEvent)` 일괄.
6. `page.tsx` 재작성 — 탭 라우팅 + 패널 import.
7. `npm run build`.
8. `pm2 restart` 또는 `deploy.sh --frontend-only`.

## 8. 엣지케이스

- **순환 import**: 패널 간 의존 없음 → 안전.
- **Type 중복**: `AdminUser` 등 패널 내부 타입은 그대로 옮김. 공유 타입은 `api.ts`에 있음.
- **default export vs named**: 각 패널은 named export. `page.tsx`에서 `import { ChannelsPanel } from "./_panels/ChannelsPanel"`.
- **빌드 크기**: 동일(코드 splitting은 별도 작업).

## 9. 테스트

- 빌드: `npm run build` 통과.
- 수동:
  - `/admin` 접근 → 5탭 모두 진입 가능.
  - 채널 추가, 영상 강제 추출(SSE), 식당 검색/수정, 유저 권한 토글, 데몬 설정 변경 — 모두 정상.
- 자동: 별도 후속(테스트 인프라 #343).

## 10. 리스크 & 대안

- **선택**: 5개 파일 추출 + 내부 로직 그대로.
- **대안 A**: 추출 + 내부 리팩터링 동시 — 회귀 위험↑, 별도 후속이 안전.
- **대안 B**: Atomic Design (atoms/molecules/organisms) — 큰 재구조화. 미루기.
- **트레이드오프**: 외형은 동일, 유지보수성/충돌 가능성만 개선. 점진적 접근.

## 11. 미해결 질문

- 공통 panel layout(헤더/리스트/페이징) 추상화 — 후속.
- VideosPanel 1272 LOC 내부 분할(상태 머신 + SSE 흐름 분리) — 별도 후속.
