Files
sam-react-prod/src/components/layout/HeaderFavoritesBar.tsx

210 lines
6.9 KiB
TypeScript
Raw Normal View History

'use client';
import { useCallback, useEffect, useRef, useState } from 'react';
import { useRouter } from 'next/navigation';
import { MoreHorizontal } from 'lucide-react';
import { Button } from '@/components/ui/button';
import {
Tooltip,
TooltipContent,
TooltipProvider,
TooltipTrigger,
} from '@/components/ui/tooltip';
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
} from '@/components/ui/dropdown-menu';
import { useFavoritesStore } from '@/stores/favoritesStore';
import { iconMap } from '@/lib/utils/menuTransform';
import type { FavoriteItem } from '@/stores/favoritesStore';
type DisplayMode = 'full' | 'icon-only' | 'overflow';
interface HeaderFavoritesBarProps {
isMobile: boolean;
}
export default function HeaderFavoritesBar({ isMobile }: HeaderFavoritesBarProps) {
const router = useRouter();
const { favorites } = useFavoritesStore();
const containerRef = useRef<HTMLDivElement>(null);
const [displayMode, setDisplayMode] = useState<DisplayMode>('full');
// 반응형: ResizeObserver로 컨테이너 너비 감지
useEffect(() => {
if (isMobile) {
setDisplayMode('overflow');
return;
}
const container = containerRef.current;
if (!container) return;
const observer = new ResizeObserver((entries) => {
for (const entry of entries) {
const width = entry.contentRect.width;
if (width < 300 || favorites.length > 4) {
setDisplayMode('overflow');
} else if (width < 600) {
setDisplayMode('icon-only');
} else {
setDisplayMode('full');
}
}
});
observer.observe(container);
return () => observer.disconnect();
}, [isMobile, favorites.length]);
const handleClick = useCallback(
(item: FavoriteItem) => {
router.push(item.path);
},
[router]
);
if (favorites.length === 0) return null;
const getIcon = (iconName: string) => {
const Icon = iconMap[iconName];
return Icon || null;
};
// 모바일: 최대 2개 아이콘 + 나머지 드롭다운
if (isMobile) {
const visible = favorites.slice(0, 2);
const overflow = favorites.slice(2);
return (
<div className="flex items-center space-x-0.5 sm:space-x-1">
{visible.map((item) => {
const Icon = getIcon(item.iconName);
if (!Icon) return null;
return (
<Button
key={item.id}
variant="default"
size="sm"
onClick={() => handleClick(item)}
className="min-w-[28px] min-h-[28px] min-[320px]:min-w-[36px] min-[320px]:min-h-[36px] sm:min-w-[44px] sm:min-h-[44px] p-0 rounded-lg min-[320px]:rounded-xl bg-blue-600 hover:bg-blue-700 text-white flex items-center justify-center"
title={item.label}
>
<Icon className="h-3.5 w-3.5 min-[320px]:h-4 min-[320px]:w-4 sm:h-5 sm:w-5" />
</Button>
);
})}
{overflow.length > 0 && (
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button
variant="default"
size="sm"
className="min-w-[28px] min-h-[28px] min-[320px]:min-w-[36px] min-[320px]:min-h-[36px] sm:min-w-[44px] sm:min-h-[44px] p-0 rounded-lg min-[320px]:rounded-xl bg-slate-600 hover:bg-slate-700 text-white flex items-center justify-center"
>
<MoreHorizontal className="h-3.5 w-3.5 min-[320px]:h-4 min-[320px]:w-4 sm:h-5 sm:w-5" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end" className="w-48">
{overflow.map((item) => {
const Icon = getIcon(item.iconName);
return (
<DropdownMenuItem
key={item.id}
onClick={() => handleClick(item)}
className="flex items-center gap-2 cursor-pointer"
>
{Icon && <Icon className="h-4 w-4" />}
<span>{item.label}</span>
</DropdownMenuItem>
);
})}
</DropdownMenuContent>
</DropdownMenu>
)}
</div>
);
}
// 데스크톱
const visibleCount = displayMode === 'overflow' ? 3 : favorites.length;
const visibleItems = favorites.slice(0, visibleCount);
const overflowItems = favorites.slice(visibleCount);
return (
<TooltipProvider delayDuration={300}>
<div ref={containerRef} className="flex items-center space-x-2">
{visibleItems.map((item) => {
const Icon = getIcon(item.iconName);
if (!Icon) return null;
if (displayMode === 'full') {
return (
<Button
key={item.id}
variant="default"
size="sm"
onClick={() => handleClick(item)}
className="rounded-xl bg-blue-600 hover:bg-blue-700 text-white px-4 py-2 flex items-center gap-2 transition-all duration-200"
>
<Icon className="h-4 w-4" />
<span className="hidden xl:inline">{item.label}</span>
</Button>
);
}
// icon-only 또는 overflow의 visible 부분
return (
<Tooltip key={item.id}>
<TooltipTrigger asChild>
<Button
variant="default"
size="sm"
onClick={() => handleClick(item)}
className="rounded-xl bg-blue-600 hover:bg-blue-700 text-white p-2 flex items-center justify-center transition-all duration-200"
>
<Icon className="h-4 w-4" />
</Button>
</TooltipTrigger>
<TooltipContent side="bottom">
<p>{item.label}</p>
</TooltipContent>
</Tooltip>
);
})}
{overflowItems.length > 0 && (
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button
variant="outline"
size="sm"
className="rounded-xl p-2 flex items-center justify-center"
>
<MoreHorizontal className="h-4 w-4" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end" className="w-48">
{overflowItems.map((item) => {
const Icon = getIcon(item.iconName);
return (
<DropdownMenuItem
key={item.id}
onClick={() => handleClick(item)}
className="flex items-center gap-2 cursor-pointer"
>
{Icon && <Icon className="h-4 w-4" />}
<span>{item.label}</span>
</DropdownMenuItem>
);
})}
</DropdownMenuContent>
</DropdownMenu>
)}
</div>
</TooltipProvider>
);
}