From 1474e3666f0ae229242e5735642b3ca5b06772a0 Mon Sep 17 00:00:00 2001 From: seyun31 <2ne1jenna@naver.com> Date: Sun, 15 Feb 2026 23:25:27 +0900 Subject: [PATCH] =?UTF-8?q?refactor:=20=ED=99=88/=EB=A7=A4=EC=B9=AD?= =?UTF-8?q?=EB=A6=AC=EC=8A=A4=ED=8A=B8/=EB=B9=84=EC=A6=88=EB=8B=88?= =?UTF-8?q?=EC=8A=A4=20=ED=83=AD=20UI=20=ED=86=B5=EC=9D=BC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/components/common/Tabs.tsx | 125 ++++++++++++------ .../brand/components/BrandFilterBar.tsx | 8 +- .../campaign/components/CampaignFilterBar.tsx | 8 +- 3 files changed, 95 insertions(+), 46 deletions(-) diff --git a/app/components/common/Tabs.tsx b/app/components/common/Tabs.tsx index d9348dd1..60fd7893 100644 --- a/app/components/common/Tabs.tsx +++ b/app/components/common/Tabs.tsx @@ -1,4 +1,5 @@ -import { NavLink } from 'react-router'; +import { NavLink, useLocation } from 'react-router'; +import { useLayoutEffect, useRef, useState } from 'react'; export interface TabItem { label: string; @@ -13,50 +14,98 @@ interface TabsProps { className?: string; } +const SIDE_MARGIN_PX = 16; + export default function Tabs({ tabs, activeTab, onTabChange, className = "" }: TabsProps) { + const location = useLocation(); + const hasPath = tabs.some((t) => t.path); + const currentActiveTab = hasPath + ? tabs.find((t) => t.path && location.pathname.startsWith(t.path))?.value || tabs[0]?.value + : activeTab; + + const activeIndex = Math.max( + 0, + tabs.findIndex((t) => t.value === currentActiveTab) + ); + + const wrapRef = useRef(null); + const [wrapWidth, setWrapWidth] = useState(0); + + useLayoutEffect(() => { + const el = wrapRef.current; + if (!el) return; + + const measure = () => setWrapWidth(el.clientWidth); + measure(); + + const ro = new ResizeObserver(() => measure()); + ro.observe(el); + + return () => ro.disconnect(); + }, []); + + const pxToRem = (px: number) => { + if (typeof window === "undefined") return `${px / 16}rem`; + const root = window.getComputedStyle(document.documentElement).fontSize; + const base = Number.parseFloat(root) || 16; + return `${px / base}rem`; + }; + + const trackWidth = Math.max(0, wrapWidth - SIDE_MARGIN_PX * 2); + const tabWidth = trackWidth / tabs.length; + const indicatorWidth = tabWidth; + const indicatorX = SIDE_MARGIN_PX + tabWidth * activeIndex; + return ( -
- {tabs.map((tab) => { - const isSelected = activeTab === tab.value; +
+
+ {tabs.map((tab) => { + const isSelected = currentActiveTab === tab.value; - // Mode 1: Link-based Tab - if (tab.path) { + // Link-based Tab + if (tab.path) { + return ( + + {({ isActive }) => ( +
+ {tab.label} +
+ )} +
+ ); + } + + // Button-based Tab return ( - onTabChange?.(tab.value)} + className={`flex-1 h-full flex items-center justify-center text-title2 transition-colors ${isSelected ? 'text-(--color-core-1)' : 'text-text-gray3' + }`} > - {({ isActive }) => ( -
- {tab.label} - {isActive && ( -
- )} -
- )} - + {tab.label} + ); - } - - // Mode 2: Button-based Tab (State) - return ( - - ); - })} + })} +
+ +
+ +
); } diff --git a/app/routes/matching/brand/components/BrandFilterBar.tsx b/app/routes/matching/brand/components/BrandFilterBar.tsx index 77077ff8..f8067b05 100644 --- a/app/routes/matching/brand/components/BrandFilterBar.tsx +++ b/app/routes/matching/brand/components/BrandFilterBar.tsx @@ -16,10 +16,10 @@ export default function BrandFilterBar({ category, onCategoryChange, searchKeywo