주요 변경사항: - Safari 쿠키 호환성 개선 (SameSite=Lax, 개발 환경 Secure 제외) - Sidebar 활성 메뉴 자동 스크롤 기능 추가 - Sidebar 스크롤바 스타일링 (호버 시에만 표시) - DashboardLayout sticky 포지셔닝 적용 - IE 브라우저 차단 및 안내 페이지 추가 - 메뉴 탐색 로직 개선 (서브메뉴 우선 매칭) 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
187 lines
7.6 KiB
TypeScript
187 lines
7.6 KiB
TypeScript
import { ChevronRight } from 'lucide-react';
|
|
import type { MenuItem } from '@/store/menuStore';
|
|
import { useEffect, useRef } from 'react';
|
|
|
|
interface SidebarProps {
|
|
menuItems: MenuItem[];
|
|
activeMenu: string;
|
|
expandedMenus: string[];
|
|
sidebarCollapsed: boolean;
|
|
isMobile: boolean;
|
|
onMenuClick: (menuId: string, path: string) => void;
|
|
onToggleSubmenu: (menuId: string) => void;
|
|
onCloseMobileSidebar?: () => void;
|
|
}
|
|
|
|
export default function Sidebar({
|
|
menuItems,
|
|
activeMenu,
|
|
expandedMenus,
|
|
sidebarCollapsed,
|
|
isMobile,
|
|
onMenuClick,
|
|
onToggleSubmenu,
|
|
onCloseMobileSidebar,
|
|
}: SidebarProps) {
|
|
// 활성 메뉴 자동 스크롤을 위한 ref
|
|
// eslint-disable-next-line no-undef
|
|
const activeMenuRef = useRef<HTMLDivElement | null>(null);
|
|
// eslint-disable-next-line no-undef
|
|
const menuContainerRef = useRef<HTMLDivElement | null>(null);
|
|
|
|
// 활성 메뉴가 변경될 때 자동 스크롤
|
|
useEffect(() => {
|
|
if (activeMenuRef.current && menuContainerRef.current) {
|
|
// 부드러운 스크롤로 활성 메뉴를 화면에 표시
|
|
activeMenuRef.current.scrollIntoView({
|
|
behavior: 'smooth',
|
|
block: 'nearest',
|
|
inline: 'nearest',
|
|
});
|
|
}
|
|
}, [activeMenu]); // activeMenu 변경 시에만 스크롤 (메뉴 클릭 시)
|
|
|
|
const handleMenuClick = (menuId: string, path: string, hasChildren: boolean) => {
|
|
if (hasChildren) {
|
|
onToggleSubmenu(menuId);
|
|
} else {
|
|
onMenuClick(menuId, path);
|
|
if (isMobile && onCloseMobileSidebar) {
|
|
onCloseMobileSidebar();
|
|
}
|
|
}
|
|
};
|
|
|
|
return (
|
|
<div className={`h-full flex flex-col clean-glass rounded-2xl overflow-hidden transition-all duration-300 ${
|
|
sidebarCollapsed ? 'sidebar-collapsed' : ''
|
|
}`}>
|
|
{/* 로고 */}
|
|
<div
|
|
className={`text-white relative transition-all duration-300 ${
|
|
sidebarCollapsed ? 'p-5' : 'p-6 md:p-8'
|
|
}`}
|
|
style={{ backgroundColor: '#3B82F6' }}
|
|
>
|
|
<div className={`flex items-center relative z-10 transition-all duration-300 ${
|
|
sidebarCollapsed ? 'justify-center' : 'space-x-4'
|
|
}`}>
|
|
<div className={`rounded-xl flex items-center justify-center clean-shadow backdrop-blur-sm transition-all duration-300 sidebar-logo relative overflow-hidden ${
|
|
sidebarCollapsed ? 'w-11 h-11' : 'w-12 h-12 md:w-14 md:h-14'
|
|
}`} style={{ backgroundColor: '#3B82F6' }}>
|
|
<div className={`text-white font-bold transition-all duration-300 ${
|
|
sidebarCollapsed ? 'text-lg' : 'text-xl md:text-2xl'
|
|
}`}>
|
|
S
|
|
</div>
|
|
<div className="absolute inset-0 bg-gradient-to-br from-white/10 to-transparent opacity-30"></div>
|
|
</div>
|
|
{!sidebarCollapsed && (
|
|
<div className="transition-all duration-300 opacity-100">
|
|
<h1 className="text-xl md:text-2xl font-bold tracking-wide">SAM</h1>
|
|
<p className="text-sm text-white/90 font-medium">Smart Automation Management</p>
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
|
|
{/* 메뉴 */}
|
|
<div
|
|
ref={menuContainerRef}
|
|
className={`sidebar-scroll flex-1 overflow-y-auto transition-all duration-300 ${
|
|
sidebarCollapsed ? 'px-3 py-4' : 'p-4 md:p-6'
|
|
}`}
|
|
>
|
|
<div className={`transition-all duration-300 ${
|
|
sidebarCollapsed ? 'space-y-2' : 'space-y-3'
|
|
}`}>
|
|
{menuItems.map((item) => {
|
|
const IconComponent = item.icon;
|
|
const hasChildren = item.children && item.children.length > 0;
|
|
const isExpanded = expandedMenus.includes(item.id);
|
|
const isActive = activeMenu === item.id;
|
|
|
|
return (
|
|
<div
|
|
key={item.id}
|
|
className="relative"
|
|
ref={isActive ? activeMenuRef : null}
|
|
>
|
|
{/* 메인 메뉴 버튼 */}
|
|
<button
|
|
onClick={() => handleMenuClick(item.id, item.path, !!hasChildren)}
|
|
className={`w-full flex items-center rounded-xl transition-all duration-200 ease-out touch-manipulation group relative overflow-hidden sidebar-menu-item ${
|
|
sidebarCollapsed ? 'p-3 justify-center' : 'space-x-3 p-3 md:p-4'
|
|
} ${
|
|
isActive
|
|
? "text-white clean-shadow scale-[0.98]"
|
|
: "text-foreground hover:bg-accent hover:scale-[1.02] active:scale-[0.98]"
|
|
}`}
|
|
style={isActive ? { backgroundColor: '#3B82F6' } : {}}
|
|
title={sidebarCollapsed ? item.label : undefined}
|
|
>
|
|
<div className={`rounded-lg flex items-center justify-center transition-all duration-200 sidebar-menu-icon ${
|
|
sidebarCollapsed ? 'w-8 h-8' : 'w-9 h-9'
|
|
} ${
|
|
isActive
|
|
? "bg-white/20"
|
|
: "bg-primary/10 group-hover:bg-primary/20"
|
|
}`}>
|
|
{IconComponent && <IconComponent className={`transition-all duration-200 ${
|
|
sidebarCollapsed ? 'h-4 w-4' : 'h-5 w-5'
|
|
} ${
|
|
isActive ? "text-white" : "text-primary"
|
|
}`} />}
|
|
</div>
|
|
{!sidebarCollapsed && (
|
|
<>
|
|
<span className="flex-1 font-medium transition-all duration-200 opacity-100 text-left text-sm">{item.label}</span>
|
|
{hasChildren && (
|
|
<div className={`transition-transform duration-200 ${
|
|
isExpanded ? 'rotate-90' : ''
|
|
}`}>
|
|
<ChevronRight className="h-4 w-4" />
|
|
</div>
|
|
)}
|
|
</>
|
|
)}
|
|
{isActive && !sidebarCollapsed && (
|
|
<div className="absolute inset-0 bg-gradient-to-r from-transparent via-white/10 to-transparent opacity-0 group-hover:opacity-100 transition-opacity duration-500" />
|
|
)}
|
|
</button>
|
|
|
|
{/* 서브메뉴 */}
|
|
{hasChildren && isExpanded && !sidebarCollapsed && (
|
|
<div className="mt-2 ml-4 space-y-1 border-l-2 border-primary/20 pl-4">
|
|
{item.children?.map((subItem) => {
|
|
const SubIcon = subItem.icon;
|
|
const isSubActive = activeMenu === subItem.id;
|
|
return (
|
|
<div
|
|
key={subItem.id}
|
|
ref={isSubActive ? activeMenuRef : null}
|
|
>
|
|
<button
|
|
onClick={() => handleMenuClick(subItem.id, subItem.path, false)}
|
|
className={`w-full flex items-center rounded-lg transition-all duration-200 p-2 space-x-2 group ${
|
|
isSubActive
|
|
? "bg-primary/10 text-primary"
|
|
: "text-muted-foreground hover:bg-accent hover:text-foreground"
|
|
}`}
|
|
>
|
|
<SubIcon className="h-4 w-4" />
|
|
<span className="text-xs font-medium">{subItem.label}</span>
|
|
</button>
|
|
</div>
|
|
);
|
|
})}
|
|
</div>
|
|
)}
|
|
</div>
|
|
);
|
|
})}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
} |