- /admin/analytics 페이지 신규 추가 - 일별 방문자 추이 바 차트 (7일/30일/90일 전환) - 오늘/이번주/기간별 요약 카드 - 유입 경로 (채널별 비율 바) - 기기 유형 분포 (PC/모바일/태블릿) - 상위 페이지 조회수 - GET /api/admin/analytics 라우트 신규 추가 (@google-analytics/data) - 사이드바에 방문자 분석 메뉴 추가 - 카페24 리뉴얼 견적 비교 SVG 에셋 추가 (public/marketing/quote-cafe24.svg) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
179 lines
7.4 KiB
TypeScript
179 lines
7.4 KiB
TypeScript
'use client';
|
|
|
|
import Link from 'next/link';
|
|
import { usePathname, useRouter } from 'next/navigation';
|
|
import { useState } from 'react';
|
|
|
|
const NAV_ITEMS = [
|
|
{
|
|
href: '/admin/dashboard',
|
|
label: '대시보드',
|
|
icon: (
|
|
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2}
|
|
d="M3 12l2-2m0 0l7-7 7 7M5 10v10a1 1 0 001 1h3m10-11l2 2m-2-2v10a1 1 0 01-1 1h-3m-6 0a1 1 0 001-1v-4a1 1 0 011-1h2a1 1 0 011 1v4a1 1 0 001 1m-6 0h6" />
|
|
</svg>
|
|
),
|
|
},
|
|
{
|
|
href: '/admin/members',
|
|
label: '회원 관리',
|
|
icon: (
|
|
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2}
|
|
d="M17 20h5v-2a3 3 0 00-5.356-1.857M17 20H7m10 0v-2c0-.656-.126-1.283-.356-1.857M7 20H2v-2a3 3 0 015.356-1.857M7 20v-2c0-.656.126-1.283.356-1.857m0 0a5.002 5.002 0 019.288 0M15 7a3 3 0 11-6 0 3 3 0 016 0z" />
|
|
</svg>
|
|
),
|
|
},
|
|
{
|
|
href: '/admin/services',
|
|
label: '서비스 설정',
|
|
icon: (
|
|
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2}
|
|
d="M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 002.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 001.065 2.572c1.756.426 1.756 2.924 0 3.35a1.724 1.724 0 00-1.066 2.573c.94 1.543-.826 3.31-2.37 2.37a1.724 1.724 0 00-2.572 1.065c-.426 1.756-2.924 1.756-3.35 0a1.724 1.724 0 00-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 00-1.065-2.572c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 001.066-2.573c-.94-1.543.826-3.31 2.37-2.37.996.608 2.296.07 2.572-1.065z" />
|
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" />
|
|
</svg>
|
|
),
|
|
},
|
|
{
|
|
href: '/admin/contacts',
|
|
label: '문의 내역',
|
|
icon: (
|
|
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2}
|
|
d="M3 8l7.89 5.26a2 2 0 002.22 0L21 8M5 19h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z" />
|
|
</svg>
|
|
),
|
|
},
|
|
{
|
|
href: '/admin/quotes',
|
|
label: '견적서 관리',
|
|
icon: (
|
|
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2}
|
|
d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" />
|
|
</svg>
|
|
),
|
|
},
|
|
{
|
|
href: '/admin/marketing',
|
|
label: '마케팅 에셋',
|
|
icon: (
|
|
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2}
|
|
d="M4 16l4.586-4.586a2 2 0 012.828 0L16 16m-2-2l1.586-1.586a2 2 0 012.828 0L20 14m-6-6h.01M6 20h12a2 2 0 002-2V6a2 2 0 00-2-2H6a2 2 0 00-2 2v12a2 2 0 002 2z" />
|
|
</svg>
|
|
),
|
|
},
|
|
{
|
|
href: '/admin/analytics',
|
|
label: '방문자 분석',
|
|
icon: (
|
|
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2}
|
|
d="M9 19v-6a2 2 0 00-2-2H5a2 2 0 00-2 2v6a2 2 0 002 2h2a2 2 0 002-2zm0 0V9a2 2 0 012-2h2a2 2 0 012 2v10m-6 0a2 2 0 002 2h2a2 2 0 002-2m0 0V5a2 2 0 012-2h2a2 2 0 012 2v14a2 2 0 01-2 2h-2a2 2 0 01-2-2z" />
|
|
</svg>
|
|
),
|
|
},
|
|
];
|
|
|
|
interface AdminSidebarProps {
|
|
isOpen?: boolean;
|
|
onClose?: () => void;
|
|
}
|
|
|
|
export default function AdminSidebar({ isOpen = false, onClose }: AdminSidebarProps) {
|
|
const pathname = usePathname();
|
|
const router = useRouter();
|
|
const [loggingOut, setLoggingOut] = useState(false);
|
|
|
|
async function handleLogout() {
|
|
setLoggingOut(true);
|
|
await fetch('/api/admin/logout', { method: 'POST' });
|
|
router.push('/admin/login');
|
|
}
|
|
|
|
return (
|
|
<aside className={`
|
|
w-60 flex-shrink-0 bg-slate-900 flex flex-col h-screen
|
|
fixed top-0 left-0 z-30 transition-transform duration-300
|
|
lg:static lg:translate-x-0
|
|
${isOpen ? 'translate-x-0' : '-translate-x-full'}
|
|
`}>
|
|
{/* 로고 */}
|
|
<div className="px-5 py-5 border-b border-slate-700/60">
|
|
<div className="flex items-center justify-between">
|
|
<div className="flex items-center gap-3">
|
|
<div className="w-9 h-9 rounded-xl bg-gradient-to-br from-red-500 to-orange-500 flex items-center justify-center text-white font-bold text-sm">
|
|
관
|
|
</div>
|
|
<div>
|
|
<p className="text-white font-bold text-sm leading-tight">관리자 패널</p>
|
|
<p className="text-slate-400 text-xs">쟁승메이드</p>
|
|
</div>
|
|
</div>
|
|
{/* 모바일 닫기 버튼 */}
|
|
{onClose && (
|
|
<button
|
|
onClick={onClose}
|
|
className="lg:hidden p-1.5 rounded-lg text-slate-400 hover:text-white hover:bg-slate-800 transition"
|
|
aria-label="메뉴 닫기"
|
|
>
|
|
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
|
|
</svg>
|
|
</button>
|
|
)}
|
|
</div>
|
|
</div>
|
|
|
|
{/* 네비게이션 */}
|
|
<nav className="flex-1 py-4 px-3 space-y-1 overflow-y-auto">
|
|
{NAV_ITEMS.map((item) => {
|
|
const active = pathname.startsWith(item.href);
|
|
return (
|
|
<Link
|
|
key={item.href}
|
|
href={item.href}
|
|
className={`flex items-center gap-3 px-3 py-2.5 rounded-lg text-sm font-medium transition-all ${
|
|
active
|
|
? 'bg-gradient-to-r from-red-600/30 to-orange-500/20 text-white border border-red-500/30'
|
|
: 'text-slate-400 hover:text-white hover:bg-slate-800'
|
|
}`}
|
|
>
|
|
<span className={active ? 'text-red-400' : ''}>{item.icon}</span>
|
|
{item.label}
|
|
</Link>
|
|
);
|
|
})}
|
|
</nav>
|
|
|
|
{/* 사이트로 돌아가기 + 로그아웃 */}
|
|
<div className="px-3 py-4 border-t border-slate-700/60 space-y-2">
|
|
<Link
|
|
href="/"
|
|
className="flex items-center gap-3 px-3 py-2.5 rounded-lg text-sm text-slate-400 hover:text-white hover:bg-slate-800 transition-all"
|
|
>
|
|
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2}
|
|
d="M10 19l-7-7m0 0l7-7m-7 7h18" />
|
|
</svg>
|
|
사이트로 돌아가기
|
|
</Link>
|
|
<button
|
|
onClick={handleLogout}
|
|
disabled={loggingOut}
|
|
className="w-full flex items-center gap-3 px-3 py-2.5 rounded-lg text-sm text-red-400 hover:text-red-300 hover:bg-red-900/20 transition-all"
|
|
>
|
|
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2}
|
|
d="M17 16l4-4m0 0l-4-4m4 4H7m6 4v1a3 3 0 01-3 3H6a3 3 0 01-3-3V7a3 3 0 013-3h4a3 3 0 013 3v1" />
|
|
</svg>
|
|
{loggingOut ? '로그아웃 중...' : '로그아웃'}
|
|
</button>
|
|
</div>
|
|
</aside>
|
|
);
|
|
}
|