# 설계서: 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/.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.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 흐름 분리) — 별도 후속.