- V2 컴포넌트를 원본에 통합 후 V2 파일 삭제 (InspectionModal, BillDetail, ContractDocumentModal, LaborDetailClient, PricingDetailClient, QuoteRegistration) - store → stores 디렉토리 이동 및 favoritesStore 추가 - dashboard_type3~5 추가 및 기존 대시보드 차트/훅 분리 - Sidebar 리팩토링 및 HeaderFavoritesBar 추가 - DashboardSwitcher 컴포넌트 추가 - 백업 파일(.v1-backup) 및 불필요 코드 정리 - InspectionPreviewModal 레이아웃 개선 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
210 lines
6.9 KiB
TypeScript
210 lines
6.9 KiB
TypeScript
'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>
|
|
);
|
|
}
|