- Add is_banner, soldout fields to Car model
- Add banner toggle API (POST /hero-banners/admin/toggle/{car_id})
- Add soldout APIs (POST/DELETE /cars/{car_id}/soldout)
- Add nightly soldout checker in agent (runs at 3:00 AM)
- Update Local Cars UI with banner checkbox and status column
- Remove hero-banners admin page (functionality moved to Cars page)
- Banner cars sorted to top with purple background
- Soldout cars displayed with gray overlay
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
195 lines
6.5 KiB
TypeScript
195 lines
6.5 KiB
TypeScript
'use client';
|
|
|
|
import { useEffect, useState } from 'react';
|
|
import { useRouter, usePathname } from 'next/navigation';
|
|
import Link from 'next/link';
|
|
import { useAuthStore } from '@/lib/store';
|
|
|
|
const menuItems = [
|
|
{ href: '/admin', label: 'Dashboard', icon: '📊' },
|
|
{ href: '/admin/visitor-stats', label: 'Visitor Stats', icon: '👁️' },
|
|
{ href: '/admin/cars', label: 'Cars', icon: '🚗' },
|
|
{ href: '/admin/vehicle-requests', label: 'Vehicle Requests', icon: '📋' },
|
|
{ href: '/admin/purchased', label: 'Purchased Vehicles', icon: '📦' },
|
|
{ href: '/admin/dealers', label: 'Dealers', icon: '🤝' },
|
|
{ href: '/admin/payments', label: 'Payments', icon: '💳' },
|
|
{ href: '/admin/withdrawals', label: 'Withdrawals', icon: '💸' },
|
|
{ href: '/admin/notifications', label: 'Notifications', icon: '🔔' },
|
|
{ href: '/admin/translations', label: 'Translations', icon: '🌐' },
|
|
{ href: '/admin/dealer-translations', label: 'Dealer Descriptions', icon: '📝' },
|
|
{ href: '/admin/users', label: 'Users', icon: '👥' },
|
|
{ href: '/admin/inquiries', label: 'Inquiries', icon: '💬' },
|
|
{ href: '/admin/settings', label: 'Settings', icon: '⚙️' },
|
|
];
|
|
|
|
export default function AdminLayout({
|
|
children,
|
|
}: {
|
|
children: React.ReactNode;
|
|
}) {
|
|
const router = useRouter();
|
|
const pathname = usePathname();
|
|
const { user, token, logout, isLoading: authLoading } = useAuthStore();
|
|
const [sidebarOpen, setSidebarOpen] = useState(true);
|
|
|
|
// 디버그 로그
|
|
useEffect(() => {
|
|
console.log('Admin Layout Debug:', {
|
|
pathname,
|
|
token: token ? 'exists' : 'null',
|
|
user: user ? { id: user.id, email: user.email, is_admin: user.is_admin } : null,
|
|
authLoading
|
|
});
|
|
}, [pathname, token, user, authLoading]);
|
|
|
|
useEffect(() => {
|
|
// 로그인 페이지는 체크 필요 없음
|
|
if (pathname === '/admin/login') {
|
|
return;
|
|
}
|
|
|
|
// 아직 로딩 중이면 대기
|
|
if (authLoading) {
|
|
return;
|
|
}
|
|
|
|
// 토큰이 없으면 로그인 페이지로
|
|
if (!token) {
|
|
router.push('/admin/login');
|
|
return;
|
|
}
|
|
|
|
// user 정보가 없으면 로그인 페이지로
|
|
if (!user) {
|
|
router.push('/admin/login');
|
|
return;
|
|
}
|
|
|
|
// 관리자가 아니면 홈으로
|
|
if (!user.is_admin) {
|
|
console.log('User is not admin, redirecting to home');
|
|
router.push('/');
|
|
return;
|
|
}
|
|
}, [pathname, router, token, user, authLoading]);
|
|
|
|
const handleLogout = () => {
|
|
logout();
|
|
router.push('/admin/login');
|
|
};
|
|
|
|
// 로그인 페이지는 레이아웃 없이 렌더링
|
|
if (pathname === '/admin/login') {
|
|
return <>{children}</>;
|
|
}
|
|
|
|
// 로딩 중
|
|
if (authLoading) {
|
|
return (
|
|
<div className="min-h-screen flex items-center justify-center bg-gray-100">
|
|
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-primary-600"></div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
// 토큰 없음 또는 유저 없음 또는 관리자 아님 -> 빈 화면 (리다이렉트 될 예정)
|
|
if (!token || !user || !user.is_admin) {
|
|
return (
|
|
<div className="min-h-screen flex items-center justify-center bg-gray-100">
|
|
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-primary-600"></div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<div className="min-h-screen bg-gray-100 flex">
|
|
{/* Sidebar */}
|
|
<aside
|
|
className={`${
|
|
sidebarOpen ? 'w-64' : 'w-20'
|
|
} bg-gray-900 text-white transition-all duration-300 flex flex-col`}
|
|
>
|
|
{/* Logo */}
|
|
<div className="p-4 border-b border-gray-800">
|
|
<Link href="/admin" className="flex items-center gap-3">
|
|
<span className="text-2xl">🚗</span>
|
|
{sidebarOpen && (
|
|
<span className="font-bold text-lg">AutonetSellCar</span>
|
|
)}
|
|
</Link>
|
|
</div>
|
|
|
|
{/* Navigation */}
|
|
<nav className="flex-1 py-4">
|
|
{menuItems.map((item) => {
|
|
const isActive = pathname === item.href ||
|
|
(item.href !== '/admin' && pathname.startsWith(item.href));
|
|
|
|
return (
|
|
<Link
|
|
key={item.href}
|
|
href={item.href}
|
|
className={`flex items-center gap-3 px-4 py-3 transition-colors ${
|
|
isActive
|
|
? 'bg-primary-600 text-white'
|
|
: 'text-gray-300 hover:bg-gray-800 hover:text-white'
|
|
}`}
|
|
>
|
|
<span className="text-xl">{item.icon}</span>
|
|
{sidebarOpen && <span>{item.label}</span>}
|
|
</Link>
|
|
);
|
|
})}
|
|
</nav>
|
|
|
|
{/* Footer */}
|
|
<div className="p-4 border-t border-gray-800">
|
|
<button
|
|
onClick={handleLogout}
|
|
className="flex items-center gap-3 text-gray-300 hover:text-white transition-colors w-full"
|
|
>
|
|
<span className="text-xl">🚪</span>
|
|
{sidebarOpen && <span>Logout</span>}
|
|
</button>
|
|
</div>
|
|
</aside>
|
|
|
|
{/* Main Content */}
|
|
<div className="flex-1 flex flex-col">
|
|
{/* Top Bar */}
|
|
<header className="bg-white shadow-sm h-16 flex items-center justify-between px-6">
|
|
<button
|
|
onClick={() => setSidebarOpen(!sidebarOpen)}
|
|
className="p-2 rounded-lg hover:bg-gray-100 transition-colors"
|
|
>
|
|
<svg className="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4 6h16M4 12h16M4 18h16" />
|
|
</svg>
|
|
</button>
|
|
|
|
<div className="flex items-center gap-4">
|
|
<Link
|
|
href="/"
|
|
target="_blank"
|
|
className="text-sm text-gray-600 hover:text-primary-600 flex items-center gap-1"
|
|
>
|
|
<span>View Site</span>
|
|
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M10 6H6a2 2 0 00-2 2v10a2 2 0 002 2h10a2 2 0 002-2v-4M14 4h6m0 0v6m0-6L10 14" />
|
|
</svg>
|
|
</Link>
|
|
<div className="w-8 h-8 rounded-full bg-primary-600 text-white flex items-center justify-center font-bold">
|
|
{user?.name?.charAt(0).toUpperCase() || user?.email?.charAt(0).toUpperCase() || 'A'}
|
|
</div>
|
|
</div>
|
|
</header>
|
|
|
|
{/* Page Content */}
|
|
<main className="flex-1 p-6 overflow-auto">
|
|
{children}
|
|
</main>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|