From 130cf34bf71013394f0c5abd00cf3866a140341b Mon Sep 17 00:00:00 2001 From: hajiiiin <101108440+hajiiiin@users.noreply.github.com> Date: Fri, 14 Mar 2025 17:59:56 +0900 Subject: [PATCH 1/3] =?UTF-8?q?fix:=20=EC=83=81=EC=84=B8=ED=8E=98=EC=9D=B4?= =?UTF-8?q?=EC=A7=80=20=EA=B0=94=EB=8B=A4=EC=99=80=EB=8F=84=20=ED=95=84?= =?UTF-8?q?=ED=84=B0=20=EC=9C=A0=EC=A7=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/Card.tsx | 2 +- src/components/ServiceTab.tsx | 30 ++++++++++++++---------------- 2 files changed, 15 insertions(+), 17 deletions(-) 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" >
( - () => (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"; - + console.log("선택된 타입: ", currentType); if (currentType !== selectedTab) { - setSelectedTab(currentType as "DALLAEMFIT" | "WORKATION"); + //setSelectedTab(currentType as "DALLAEMFIT" | "WORKATION"); + setSelectedTab(SERVICE_TABS.find((t) => t.type === currentType)?.name || "달램핏"); } }, [searchParams]); @@ -53,8 +52,7 @@ 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"); - + setSelectedTab(tabName); onCategoryChange(tabType); handleCategoryReset(); }; From 415f54b3b5369dab523ea330ef0b490960ea3ae8 Mon Sep 17 00:00:00 2001 From: hajiiiin <101108440+hajiiiin@users.noreply.github.com> Date: Fri, 14 Mar 2025 20:46:12 +0900 Subject: [PATCH 2/3] =?UTF-8?q?fix:=20tab=20=ED=95=84=ED=84=B0=EB=A7=81=20?= =?UTF-8?q?searchParams=20=EA=B8=B0=EB=B0=98=EC=9C=BC=EB=A1=9C=20=EB=B3=80?= =?UTF-8?q?=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/MainTab.tsx | 129 ++++++++++++++++++++++++++++++++++ src/components/ServiceTab.tsx | 34 ++++++--- 2 files changed, 154 insertions(+), 9 deletions(-) create mode 100644 src/components/MainTab.tsx diff --git a/src/components/MainTab.tsx b/src/components/MainTab.tsx new file mode 100644 index 0000000..0c1c2d7 --- /dev/null +++ b/src/components/MainTab.tsx @@ -0,0 +1,129 @@ +"use client"; + +import { createContext, useContext, useEffect, useRef, useState } from "react"; +import { useSearchParams, useRouter, usePathname } from "next/navigation"; + +type MainTabContextType = { + activeIndex: number; + setActiveIndex: (index: number) => 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 68cc6bd..e572b59 100644 --- a/src/components/ServiceTab.tsx +++ b/src/components/ServiceTab.tsx @@ -3,7 +3,7 @@ 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"; @@ -38,10 +38,21 @@ export default function ServiceTab({ onCategoryChange, isFilteringLoading }: Ser //searchParams 변경 감지해서 반영 useEffect(() => { const currentType = searchParams.get("type") || "DALLAEMFIT"; + console.log("선택된 타입: ", currentType); - if (currentType !== selectedTab) { - //setSelectedTab(currentType as "DALLAEMFIT" | "WORKATION"); - setSelectedTab(SERVICE_TABS.find((t) => t.type === currentType)?.name || "달램핏"); + + if (currentType) { + const tabName = SERVICE_TABS.find((t) => t.type === currentType)?.name; + + console.log("찾은 탭 이름1:", tabName); + console.log("selectedTab은? ", selectedTab); + if (tabName && tabName == selectedTab) { + setSelectedTab(tabName); + console.log("찾은 탭 이름2:", tabName); + + // 📌 `handleTabChange` 실행 (중복 실행 방지됨) + handleTabChange(tabName); + } } }, [searchParams]); @@ -52,7 +63,12 @@ export default function ServiceTab({ onCategoryChange, isFilteringLoading }: Ser const tabType = SERVICE_TABS.find((t) => t.name === tabName)?.type; if (!tabType) return; - setSelectedTab(tabName); + // URL의 type 값을 가져와서 selectedTab 업데이트 + const currentType = searchParams.get("type") || tabType; // 없으면 클릭한 탭을 기본값으로 + console.log("핸들러 실행됨11:", currentType); + setSelectedTab(currentType); + + setSelectedTab(tabType); onCategoryChange(tabType); handleCategoryReset(); }; @@ -75,7 +91,7 @@ export default function ServiceTab({ onCategoryChange, isFilteringLoading }: Ser return ( <> - c.name)} @@ -91,7 +107,7 @@ export default function ServiceTab({ onCategoryChange, isFilteringLoading }: Ser targetIndex={0} > {SERVICE_TABS.map((tabItem, idx) => ( - + - + ))} - + ); } From abbd658f24f4513c42212e19739fd5d1517dd074 Mon Sep 17 00:00:00 2001 From: hajiiiin <101108440+hajiiiin@users.noreply.github.com> Date: Sat, 15 Mar 2025 00:30:37 +0900 Subject: [PATCH 3/3] =?UTF-8?q?fix:=20=ED=85=8C=EC=8A=A4=ED=8A=B8=EC=9A=A9?= =?UTF-8?q?=20=EC=B6=9C=EB=A0=A5=20=EC=82=AD=EC=A0=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/MainTab.tsx | 4 ++-- src/components/ServiceTab.tsx | 7 ------- 2 files changed, 2 insertions(+), 9 deletions(-) diff --git a/src/components/MainTab.tsx b/src/components/MainTab.tsx index 0c1c2d7..90b36bd 100644 --- a/src/components/MainTab.tsx +++ b/src/components/MainTab.tsx @@ -70,7 +70,7 @@ export default function MainTab({ children, category, targetIndex, gap = "gap-4" setActiveIndex: (index: number) => { setActiveIndex(index); - // 📌 URL도 함께 업데이트 (뒤로 가기 대응) + // URL도 함께 업데이트 const tabType = SERVICE_TABS[index].type; router.push(`${pathname}?type=${tabType}`); }, @@ -122,7 +122,7 @@ MainTab.Item = function ({ index, children }: ItemProps) { ); }; -// 📌 서비스 탭 리스트 (예제) +// 서비스 탭 리스트 const SERVICE_TABS = [ { name: "달램핏", type: "DALLAEMFIT" }, { name: "워케이션", type: "WORKATION" }, diff --git a/src/components/ServiceTab.tsx b/src/components/ServiceTab.tsx index e572b59..ca47559 100644 --- a/src/components/ServiceTab.tsx +++ b/src/components/ServiceTab.tsx @@ -39,18 +39,12 @@ export default function ServiceTab({ onCategoryChange, isFilteringLoading }: Ser useEffect(() => { const currentType = searchParams.get("type") || "DALLAEMFIT"; - console.log("선택된 타입: ", currentType); - if (currentType) { const tabName = SERVICE_TABS.find((t) => t.type === currentType)?.name; - console.log("찾은 탭 이름1:", tabName); - console.log("selectedTab은? ", selectedTab); if (tabName && tabName == selectedTab) { setSelectedTab(tabName); - console.log("찾은 탭 이름2:", tabName); - // 📌 `handleTabChange` 실행 (중복 실행 방지됨) handleTabChange(tabName); } } @@ -65,7 +59,6 @@ export default function ServiceTab({ onCategoryChange, isFilteringLoading }: Ser // URL의 type 값을 가져와서 selectedTab 업데이트 const currentType = searchParams.get("type") || tabType; // 없으면 클릭한 탭을 기본값으로 - console.log("핸들러 실행됨11:", currentType); setSelectedTab(currentType); setSelectedTab(tabType);