From 5f6830434de4eb300683401aacf1db09e447075e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9C=A0=EB=B3=91=EC=B2=A0?= Date: Wed, 11 Feb 2026 15:29:46 +0900 Subject: [PATCH] =?UTF-8?q?refactor(WEB):=20HeaderFavoritesBar=20=EB=A6=AC?= =?UTF-8?q?=ED=8C=A9=ED=86=A0=EB=A7=81=20=EB=B0=8F=20=EC=A6=90=EA=B2=A8?= =?UTF-8?q?=EC=B0=BE=EA=B8=B0=20=EA=B4=80=EB=A0=A8=20=EC=BD=94=EB=93=9C=20?= =?UTF-8?q?=EA=B0=9C=EC=84=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - HeaderFavoritesBar 컴포넌트 코드 간소화 - Sidebar 즐겨찾기 연동 수정 - favoritesStore 로직 개선 Co-Authored-By: Claude Opus 4.6 --- src/components/layout/HeaderFavoritesBar.tsx | 218 +++++++------------ src/components/layout/Sidebar.tsx | 8 +- src/stores/favoritesStore.ts | 8 +- 3 files changed, 93 insertions(+), 141 deletions(-) diff --git a/src/components/layout/HeaderFavoritesBar.tsx b/src/components/layout/HeaderFavoritesBar.tsx index 2ccaa400..669002f5 100644 --- a/src/components/layout/HeaderFavoritesBar.tsx +++ b/src/components/layout/HeaderFavoritesBar.tsx @@ -1,8 +1,8 @@ 'use client'; -import { useCallback, useEffect, useRef, useState } from 'react'; +import { useCallback, useEffect, useState } from 'react'; import { useRouter } from 'next/navigation'; -import { MoreHorizontal } from 'lucide-react'; +import { Star } from 'lucide-react'; import { Button } from '@/components/ui/button'; import { Tooltip, @@ -20,8 +20,6 @@ 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; } @@ -29,35 +27,18 @@ interface HeaderFavoritesBarProps { export default function HeaderFavoritesBar({ isMobile }: HeaderFavoritesBarProps) { const router = useRouter(); const { favorites } = useFavoritesStore(); - const containerRef = useRef(null); - const [displayMode, setDisplayMode] = useState('full'); + const [isTablet, setIsTablet] = useState(false); - // 반응형: ResizeObserver로 컨테이너 너비 감지 + // 태블릿 감지 (768~1024) 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 check = () => { + const w = window.innerWidth; + setIsTablet(w >= 768 && w < 1024); + }; + check(); + window.addEventListener('resize', check); + return () => window.removeEventListener('resize', check); + }, []); const handleClick = useCallback( (item: FavoriteItem) => { @@ -69,93 +50,87 @@ export default function HeaderFavoritesBar({ isMobile }: HeaderFavoritesBarProps if (favorites.length === 0) return null; const getIcon = (iconName: string) => { - const Icon = iconMap[iconName]; - return Icon || null; + return iconMap[iconName] || null; }; - // 모바일: 최대 2개 아이콘 + 나머지 드롭다운 - if (isMobile) { - const visible = favorites.slice(0, 2); - const overflow = favorites.slice(2); - + // 모바일 & 태블릿: 별 아이콘 드롭다운 + if (isMobile || isTablet) { return ( -
- {visible.map((item) => { - const Icon = getIcon(item.iconName); - if (!Icon) return null; - return ( - - ); - })} - {overflow.length > 0 && ( - - - + + + {favorites.map((item) => { + const Icon = getIcon(item.iconName); + return ( + handleClick(item)} + className="flex items-center gap-2 cursor-pointer" > - - - - - {overflow.map((item) => { - const Icon = getIcon(item.iconName); - return ( - handleClick(item)} - className="flex items-center gap-2 cursor-pointer" - > - {Icon && } - {item.label} - - ); - })} - - - )} -
+ {Icon && } + {item.label} + + ); + })} + + ); } - // 데스크톱 - const visibleCount = displayMode === 'overflow' ? 3 : favorites.length; - const visibleItems = favorites.slice(0, visibleCount); - const overflowItems = favorites.slice(visibleCount); + // 데스크톱: 8개 이하 → 아이콘 버튼, 9개 이상 → 별 드롭다운 + const DESKTOP_ICON_LIMIT = 8; + + if (favorites.length > DESKTOP_ICON_LIMIT) { + return ( + + + + + + {favorites.map((item) => { + const Icon = getIcon(item.iconName); + return ( + handleClick(item)} + className="flex items-center gap-2 cursor-pointer" + > + {Icon && } + {item.label} + + ); + })} + + + ); + } return ( -
- {visibleItems.map((item) => { +
+ {favorites.map((item) => { const Icon = getIcon(item.iconName); if (!Icon) return null; - - if (displayMode === 'full') { - return ( - - ); - } - - // icon-only 또는 overflow의 visible 부분 return ( @@ -163,7 +138,7 @@ export default function HeaderFavoritesBar({ isMobile }: HeaderFavoritesBarProps 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" + className="rounded-xl bg-blue-600 hover:bg-blue-700 text-white w-10 h-10 p-0 flex items-center justify-center transition-all duration-200" > @@ -174,35 +149,6 @@ export default function HeaderFavoritesBar({ isMobile }: HeaderFavoritesBarProps ); })} - - {overflowItems.length > 0 && ( - - - - - - {overflowItems.map((item) => { - const Icon = getIcon(item.iconName); - return ( - handleClick(item)} - className="flex items-center gap-2 cursor-pointer" - > - {Icon && } - {item.label} - - ); - })} - - - )}
); diff --git a/src/components/layout/Sidebar.tsx b/src/components/layout/Sidebar.tsx index 84e8ce7a..ecf9a89d 100644 --- a/src/components/layout/Sidebar.tsx +++ b/src/components/layout/Sidebar.tsx @@ -1,9 +1,10 @@ import { ChevronRight, ChevronsDownUp, ChevronsUpDown, Circle, Star } from 'lucide-react'; import type { MenuItem } from '@/stores/menuStore'; import { useEffect, useRef, useCallback } from 'react'; -import { useFavoritesStore } from '@/stores/favoritesStore'; +import { useFavoritesStore, MAX_FAVORITES } from '@/stores/favoritesStore'; import { getIconName } from '@/lib/utils/menuTransform'; import type { FavoriteItem } from '@/stores/favoritesStore'; +import { toast } from 'sonner'; interface SidebarProps { menuItems: MenuItem[]; @@ -64,7 +65,10 @@ function MenuItemComponent({ path: item.path, addedAt: Date.now(), }; - toggleFavorite(favItem); + const result = toggleFavorite(favItem); + if (result === 'max_reached') { + toast.warning(`즐겨찾기는 최대 ${MAX_FAVORITES}개까지 등록할 수 있습니다.`); + } }, [item, toggleFavorite]); const handleClick = () => { diff --git a/src/stores/favoritesStore.ts b/src/stores/favoritesStore.ts index ce8ef6be..a998c871 100644 --- a/src/stores/favoritesStore.ts +++ b/src/stores/favoritesStore.ts @@ -10,7 +10,7 @@ export interface FavoriteItem { addedAt: number; } -const MAX_FAVORITES = 8; +export const MAX_FAVORITES = 10; function getUserId(): string { if (typeof window === 'undefined') return 'default'; @@ -26,7 +26,7 @@ function getStorageKey(): string { interface FavoritesState { favorites: FavoriteItem[]; - toggleFavorite: (item: FavoriteItem) => void; + toggleFavorite: (item: FavoriteItem) => 'added' | 'removed' | 'max_reached'; isFavorite: (id: string) => boolean; setFavorites: (items: FavoriteItem[]) => void; initializeIfEmpty: (defaults: FavoriteItem[]) => void; @@ -43,9 +43,11 @@ export const useFavoritesStore = create()( if (exists) { set({ favorites: favorites.filter((f) => f.id !== item.id) }); + return 'removed'; } else { - if (favorites.length >= MAX_FAVORITES) return; + if (favorites.length >= MAX_FAVORITES) return 'max_reached'; set({ favorites: [...favorites, { ...item, addedAt: Date.now() }] }); + return 'added'; } },