diff --git a/src/components/Card.tsx b/src/components/Card.tsx index 0afc3c1..a531916 100644 --- a/src/components/Card.tsx +++ b/src/components/Card.tsx @@ -70,7 +70,7 @@ export default function Card({ variants={cardVariants} initial="hidden" animate={inView ? "visible" : "hidden"} - transition={{ duration: 0.7, ease: "easeInOut" }} + transition={{ duration: 0.5, ease: "easeInOut" }} className="relative my-5" >
void; + addTabRefs: (index: number, ref: HTMLLIElement | null) => void; + sliderStyle: { width: number; translateX: number }; +}; + +const MainTabContext = createContext(null); + +function useTabContext() { + const context = useContext(MainTabContext); + if (!context) { + throw new Error("Tab compound components must be used within a Tab.Root"); + } + return context; +} + +// Tab의 props +type MainTabProps = { + children: React.ReactNode; + category?: React.ReactNode; // 카태고리 버튼 + targetIndex?: number; // 클릭 시 카테고리가 나와야 할 index + gap?: string; // 탭과 카테고리의 간격 +}; +// Tab 루트 컴포넌트 +export default function MainTab({ children, category, targetIndex, gap = "gap-4" }: MainTabProps) { + const searchParams = useSearchParams(); + const router = useRouter(); + const pathname = usePathname(); + + // 현재 활성화된 탭의 인덱스 + const [activeIndex, setActiveIndex] = useState(0); + // 슬라이더의 길이 및 X축 이동거리 + const [sliderStyle, setSliderStyle] = useState({ width: 0, translateX: 0 }); + // 탭들의 ref + const tabRefs = useRef<(HTMLLIElement | null)[]>([]); + + // URL에서 `type` 값을 읽어 `activeIndex` 업데이트 (뒤로 가기, 새로고침 대응) + useEffect(() => { + const currentType = searchParams.get("type") || "DALLAEMFIT"; + const selectedIndex = SERVICE_TABS.findIndex((t) => t.type === currentType); + + if (selectedIndex !== -1 && selectedIndex !== activeIndex) { + setActiveIndex(selectedIndex); + } + }, [searchParams]); + + useEffect(() => { + if (!tabRefs.current.length) return; // 아직 ref 배열이 비어 있다면 패스 + // 현재 활성화 된 Tab + const activeTab = tabRefs.current[activeIndex]; + if (activeTab) { + const width = activeTab.offsetWidth; + // 활성 탭 이전 탭들의 누적 offsetWidth를 계산. gap인 12px을 더해준다. + const offsetLeft = tabRefs.current + .slice(0, activeIndex) + .reduce((acc, el) => acc + (el?.offsetWidth || 0) + 12, 0); + setSliderStyle({ width, translateX: offsetLeft }); + } + }, [activeIndex]); + + // context에 전달할 값들 + const contextValue = { + activeIndex, + setActiveIndex: (index: number) => { + setActiveIndex(index); + + // URL도 함께 업데이트 + const tabType = SERVICE_TABS[index].type; + router.push(`${pathname}?type=${tabType}`); + }, + addTabRefs: (index: number, ref: HTMLLIElement | null) => { + tabRefs.current[index] = ref; + }, + sliderStyle, + }; + return ( + +
+
+ {/* 탭 */} +
    {children}
+ {/* 슬라이더 */} +
+
+ {targetIndex === activeIndex &&
{category}
} +
+ + ); +} + +// 탭 아이템 +type ItemProps = { + index: number; + children: React.ReactNode; +}; +MainTab.Item = function ({ index, children }: ItemProps) { + const { activeIndex, setActiveIndex, addTabRefs } = useTabContext(); + + return ( +
  • setActiveIndex(index)} + ref={(el) => { + addTabRefs(index, el); + }} + // 활성화된 탭이면 text의 색을 변경 + className={`${activeIndex === index && "text-gray-900"} mb-1 flex cursor-pointer items-center gap-1 transition-colors duration-300`} + > + {children} +
  • + ); +}; + +// 서비스 탭 리스트 +const SERVICE_TABS = [ + { name: "달램핏", type: "DALLAEMFIT" }, + { name: "워케이션", type: "WORKATION" }, +]; diff --git a/src/components/ServiceTab.tsx b/src/components/ServiceTab.tsx index 9c439e2..ca47559 100644 --- a/src/components/ServiceTab.tsx +++ b/src/components/ServiceTab.tsx @@ -1,8 +1,9 @@ "use client"; import { useState, useEffect, useCallback } from "react"; +import { useSearchParams } from "next/navigation"; import CategoryButton from "@/components/CategoryButton"; -import Tab from "@/components/Tab"; +import MainTab from "@/components/MainTab"; import Dalaemfit from "@/images/dalaemfit.svg"; import Workation from "@/images/workation.svg"; @@ -23,26 +24,29 @@ type ServiceTabProps = { isFilteringLoading?: boolean; // 필터링 중인지 판단하는 변수 }; -export default function ServiceTab({ searchParams, onCategoryChange, isFilteringLoading }: ServiceTabProps) { - const [selectedTab, setSelectedTab] = useState<"DALLAEMFIT" | "WORKATION">( - () => (searchParams.get("type") || "DALLAEMFIT") as "DALLAEMFIT" | "WORKATION", - ); - const [selectedCategory, setSelectedCategory] = useState("전체"); +export default function ServiceTab({ onCategoryChange, isFilteringLoading }: ServiceTabProps) { + const searchParams = useSearchParams(); - // URL이 변경되었을 때 필터링 로딩 상태 해제 - useEffect(() => { - if (!isFilteringLoading) return; + const [selectedTab, setSelectedTab] = useState( + () => SERVICE_TABS.find((t) => t.type === searchParams.get("type"))?.name || "DALLAEMFIT", + ); - const currentType = searchParams.get("type") || "DALLAEMFIT"; - setSelectedTab(currentType as "DALLAEMFIT" | "WORKATION"); - }, [searchParams, isFilteringLoading]); + const [selectedCategory, setSelectedCategory] = useState( + () => CATEGORIES.find((c) => c.type === searchParams.get("type"))?.name || "전체", + ); - // searchParams 변경 감지해서 반영 + //searchParams 변경 감지해서 반영 useEffect(() => { const currentType = searchParams.get("type") || "DALLAEMFIT"; - if (currentType !== selectedTab) { - setSelectedTab(currentType as "DALLAEMFIT" | "WORKATION"); + if (currentType) { + const tabName = SERVICE_TABS.find((t) => t.type === currentType)?.name; + + if (tabName && tabName == selectedTab) { + setSelectedTab(tabName); + + handleTabChange(tabName); + } } }, [searchParams]); @@ -53,8 +57,11 @@ export default function ServiceTab({ searchParams, onCategoryChange, isFiltering const tabType = SERVICE_TABS.find((t) => t.name === tabName)?.type; if (!tabType) return; - setSelectedTab(tabType as "DALLAEMFIT" | "WORKATION"); + // URL의 type 값을 가져와서 selectedTab 업데이트 + const currentType = searchParams.get("type") || tabType; // 없으면 클릭한 탭을 기본값으로 + setSelectedTab(currentType); + setSelectedTab(tabType); onCategoryChange(tabType); handleCategoryReset(); }; @@ -77,7 +84,7 @@ export default function ServiceTab({ searchParams, onCategoryChange, isFiltering return ( <> - c.name)} @@ -93,7 +100,7 @@ export default function ServiceTab({ searchParams, onCategoryChange, isFiltering targetIndex={0} > {SERVICE_TABS.map((tabItem, idx) => ( - + - + ))} - + ); }