diff --git a/docs/design/329-admin-split/README.md b/docs/design/329-admin-split/README.md new file mode 100644 index 0000000..08c3bb1 --- /dev/null +++ b/docs/design/329-admin-split/README.md @@ -0,0 +1,107 @@ +# 설계서: 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 흐름 분리) — 별도 후속.