Files
AutonetSellCar/frontend/src/app/admin/layout.tsx
AutonetSellCar Deploy c9fd7611a7 feat: Add banner toggle and soldout tracking to Cars page
- 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>
2025-12-31 12:50:40 +09:00

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>
);
}