Initial commit: AutonetSellCar platform with deployment system
- Frontend: Next.js 14 with TypeScript - Backend: FastAPI with SQLAlchemy - Agent: Carmodoo sync agent - Deployment: Docker Compose based staging/production setup - Scripts: Automated deployment with rollback support 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
195
frontend/src/app/admin/layout.tsx
Normal file
195
frontend/src/app/admin/layout.tsx
Normal file
@@ -0,0 +1,195 @@
|
||||
'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/hero-banners', label: 'Hero Banners', 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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user