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 (
+
+
+
+ {/* 탭 */}
+
+ {/* 슬라이더 */}
+
+
+ {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) => (
-
+
-
+
))}
-
+
>
);
}