- 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)
108 lines
3.9 KiB
TypeScript
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">
|
|
← 메인으로
|
|
</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>
|
|
);
|
|
}
|