Files
tasteby/frontend/src/app/admin/page.tsx
joungmin 7d95ecb3cb refactor(admin): #329 admin/page.tsx 분리 + localStorage 통일
- page.tsx 2817 LOC → 107 LOC (탭 라우팅 + CacheFlushButton만)
- _panels/ChannelsPanel.tsx (222), VideosPanel.tsx (1282),
  RestaurantsPanel.tsx (675), UsersPanel.tsx (383), DaemonPanel.tsx (231)
- localStorage.getItem("tasteby_token") 10곳 → getAdminToken() (lib/admin-utils)
- 패널 내부 로직/state 그대로 (점진적 접근, 회귀 위험 최소화)

후속 분리:
- (신규) SSE 파싱 6곳을 consumeSseStream으로 통일

설계서: docs/design/329-admin-split/README.md

Refs: #329 (Developer)
2026-06-15 15:52:08 +09:00

108 lines
3.9 KiB
TypeScript

"use client";
import { useState } from "react";
import { api } from "@/lib/api";
import { useAuth } from "@/lib/auth-context";
import { ChannelsPanel } from "./_panels/ChannelsPanel";
import { VideosPanel } from "./_panels/VideosPanel";
import { RestaurantsPanel } from "./_panels/RestaurantsPanel";
import { UsersPanel } from "./_panels/UsersPanel";
import { DaemonPanel } from "./_panels/DaemonPanel";
// #329 — 5개 패널을 _panels/ 디렉토리로 분리. page.tsx는 탭 라우팅 + 헤더만.
type Tab = "channels" | "videos" | "restaurants" | "users" | "daemon";
function CacheFlushButton() {
const [flushing, setFlushing] = useState(false);
const handleFlush = async () => {
if (!confirm("Redis 캐시를 초기화하시겠습니까?")) return;
setFlushing(true);
try {
await api.flushCache();
alert("캐시가 초기화되었습니다.");
} catch (e) {
alert("캐시 초기화 실패: " + (e instanceof Error ? e.message : e));
} finally {
setFlushing(false);
}
};
return (
<button
onClick={handleFlush}
disabled={flushing}
className="px-3 py-1.5 text-xs bg-red-50 text-red-600 border border-red-200 rounded-lg hover:bg-red-100 disabled:opacity-50 transition-colors"
>
{flushing ? "초기화 중..." : "🗑 캐시 초기화"}
</button>
);
}
export default function AdminPage() {
const [tab, setTab] = useState<Tab>("channels");
const { user, isLoading } = useAuth();
const isAdmin = user?.is_admin === true;
if (isLoading) {
return <div className="min-h-screen bg-background flex items-center justify-center text-gray-500"> ...</div>;
}
if (!user) {
return (
<div className="min-h-screen bg-background flex items-center justify-center">
<div className="text-center">
<p className="text-gray-600 mb-4"> </p>
<a href="/" className="text-brand-600 hover:underline"> </a>
</div>
</div>
);
}
return (
<div className="min-h-screen bg-background text-gray-900">
<header className="bg-surface border-b border-brand-100 px-6 py-4">
<div className="flex items-center justify-between">
<div className="flex items-center gap-3">
<img src="/logo-80h.png" alt="Tasteby" className="h-7" />
<span className="text-xl font-bold text-gray-500">Admin</span>
{!isAdmin && (
<span className="px-2 py-0.5 bg-yellow-100 text-yellow-700 rounded text-xs font-medium"> </span>
)}
</div>
<div className="flex items-center gap-3">
{isAdmin && <CacheFlushButton />}
<a href="/" className="text-sm text-brand-600 hover:underline">
&larr;
</a>
</div>
</div>
<nav className="mt-3 flex gap-1">
{(["channels", "videos", "restaurants", "users", "daemon"] as Tab[]).map((t) => (
<button
key={t}
onClick={() => setTab(t)}
className={`px-4 py-2 text-sm rounded-t font-medium ${
tab === t
? "bg-brand-600 text-white"
: "bg-brand-50 text-brand-700 hover:bg-brand-100"
}`}
>
{t === "channels" ? "채널 관리" : t === "videos" ? "영상 관리" : t === "restaurants" ? "식당 관리" : t === "users" ? "유저 관리" : "데몬 설정"}
</button>
))}
</nav>
</header>
<main className="max-w-6xl mx-auto p-6">
{tab === "channels" && <ChannelsPanel isAdmin={isAdmin} />}
{tab === "videos" && <VideosPanel isAdmin={isAdmin} />}
{tab === "restaurants" && <RestaurantsPanel isAdmin={isAdmin} />}
{tab === "users" && <UsersPanel />}
{tab === "daemon" && <DaemonPanel isAdmin={isAdmin} />}
</main>
</div>
);
}