Skip to content

Commit 7d9e0a2

Browse files
authored
Merge pull request #197 from RealMatchTeam/style/matchinglist-and-test
[refactor] 홈/매칭리스트/비즈니스 탭 UI 통일
2 parents 329efb6 + 1474e36 commit 7d9e0a2

3 files changed

Lines changed: 95 additions & 46 deletions

File tree

app/components/common/Tabs.tsx

Lines changed: 87 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
1-
import { NavLink } from 'react-router';
1+
import { NavLink, useLocation } from 'react-router';
2+
import { useLayoutEffect, useRef, useState } from 'react';
23

34
export interface TabItem {
45
label: string;
@@ -13,50 +14,98 @@ interface TabsProps {
1314
className?: string;
1415
}
1516

17+
const SIDE_MARGIN_PX = 16;
18+
1619
export default function Tabs({ tabs, activeTab, onTabChange, className = "" }: TabsProps) {
20+
const location = useLocation();
21+
const hasPath = tabs.some((t) => t.path);
22+
const currentActiveTab = hasPath
23+
? tabs.find((t) => t.path && location.pathname.startsWith(t.path))?.value || tabs[0]?.value
24+
: activeTab;
25+
26+
const activeIndex = Math.max(
27+
0,
28+
tabs.findIndex((t) => t.value === currentActiveTab)
29+
);
30+
31+
const wrapRef = useRef<HTMLDivElement | null>(null);
32+
const [wrapWidth, setWrapWidth] = useState(0);
33+
34+
useLayoutEffect(() => {
35+
const el = wrapRef.current;
36+
if (!el) return;
37+
38+
const measure = () => setWrapWidth(el.clientWidth);
39+
measure();
40+
41+
const ro = new ResizeObserver(() => measure());
42+
ro.observe(el);
43+
44+
return () => ro.disconnect();
45+
}, []);
46+
47+
const pxToRem = (px: number) => {
48+
if (typeof window === "undefined") return `${px / 16}rem`;
49+
const root = window.getComputedStyle(document.documentElement).fontSize;
50+
const base = Number.parseFloat(root) || 16;
51+
return `${px / base}rem`;
52+
};
53+
54+
const trackWidth = Math.max(0, wrapWidth - SIDE_MARGIN_PX * 2);
55+
const tabWidth = trackWidth / tabs.length;
56+
const indicatorWidth = tabWidth;
57+
const indicatorX = SIDE_MARGIN_PX + tabWidth * activeIndex;
58+
1759
return (
18-
<div className={`flex w-full bg-white border-b border-bluegray-2 shrink-0 ${className}`}>
19-
{tabs.map((tab) => {
20-
const isSelected = activeTab === tab.value;
60+
<div ref={wrapRef} className={`relative w-full h-12.5 bg-white shrink-0 ${className}`}>
61+
<div className="flex h-full items-center px-4">
62+
{tabs.map((tab) => {
63+
const isSelected = currentActiveTab === tab.value;
2164

22-
// Mode 1: Link-based Tab
23-
if (tab.path) {
65+
// Link-based Tab
66+
if (tab.path) {
67+
return (
68+
<NavLink
69+
key={tab.value}
70+
to={tab.path}
71+
className="flex-1 select-none"
72+
>
73+
{({ isActive }) => (
74+
<div
75+
className={`h-full flex items-center justify-center text-title2 transition-colors cursor-pointer ${isActive ? 'text-(--color-core-1)' : 'text-text-gray3'
76+
}`}
77+
>
78+
{tab.label}
79+
</div>
80+
)}
81+
</NavLink>
82+
);
83+
}
84+
85+
// Button-based Tab
2486
return (
25-
<NavLink
87+
<button
2688
key={tab.value}
27-
to={tab.path}
28-
className="flex-1 select-none"
89+
type="button"
90+
onClick={() => onTabChange?.(tab.value)}
91+
className={`flex-1 h-full flex items-center justify-center text-title2 transition-colors ${isSelected ? 'text-(--color-core-1)' : 'text-text-gray3'
92+
}`}
2993
>
30-
{({ isActive }) => (
31-
<div
32-
className={`py-4 text-title-2 text-center relative transition-colors cursor-pointer font-semibold ${isActive ? 'text-core-1' : 'text-text-gray3'
33-
}`}
34-
>
35-
{tab.label}
36-
{isActive && (
37-
<div className="absolute bottom-0 left-1/2 -translate-x-1/2 w-[150px] h-[2px] bg-core-1" />
38-
)}
39-
</div>
40-
)}
41-
</NavLink>
94+
{tab.label}
95+
</button>
4296
);
43-
}
44-
45-
// Mode 2: Button-based Tab (State)
46-
return (
47-
<button
48-
key={tab.value}
49-
onClick={() => onTabChange?.(tab.value)}
50-
className={`flex-1 py-4 text-title-2 text-center relative transition-colors select-none font-semibold ${isSelected ? 'text-core-1' : 'text-text-gray3'
51-
}`}
52-
>
53-
{tab.label}
54-
{isSelected && (
55-
<div className="absolute bottom-0 left-1/2 -translate-x-1/2 w-[150px] h-[2px] bg-core-1" />
56-
)}
57-
</button>
58-
);
59-
})}
97+
})}
98+
</div>
99+
100+
<div className="absolute bottom-0 left-0 right-0 h-px bg-black/10" />
101+
102+
<div
103+
className="absolute bottom-0 h-0.5 bg-(--color-success) transition-transform duration-200"
104+
style={{
105+
width: pxToRem(indicatorWidth),
106+
transform: `translateX(${pxToRem(indicatorX)})`,
107+
}}
108+
/>
60109
</div>
61110
);
62111
}

app/routes/matching/brand/components/BrandFilterBar.tsx

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -16,21 +16,21 @@ export default function BrandFilterBar({ category, onCategoryChange, searchKeywo
1616
<div className="flex gap-2 shrink-0">
1717
<button
1818
className={cn(
19-
"px-[12px] py-[8px] h-10 text-title2 rounded-[8px] whitespace-nowrap cursor-pointer transition-colors",
19+
"px-[10px] py-2 rounded-[8px] text-title2 transition-all",
2020
category === "BEAUTY"
2121
? "bg-core-1 text-white"
22-
: "bg-white text-text-gray3 border border-gray-100"
22+
: "bg-white border border-text-gray5 text-text-gray3"
2323
)}
2424
onClick={() => onCategoryChange("BEAUTY")}
2525
>
2626
뷰티
2727
</button>
2828
<button
2929
className={cn(
30-
"px-[12px] py-[8px] h-10 text-title2 rounded-[8px] whitespace-nowrap cursor-pointer transition-colors",
30+
"px-[10px] py-2 rounded-[8px] text-title2 transition-all",
3131
category === "FASHION"
3232
? "bg-core-1 text-white"
33-
: "bg-white text-text-gray3 border border-gray-100"
33+
: "bg-white border border-text-gray5 text-text-gray3"
3434
)}
3535
onClick={() => onCategoryChange("FASHION")}
3636
>

app/routes/matching/campaign/components/CampaignFilterBar.tsx

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -16,21 +16,21 @@ export default function CampaignFilterBar({ category, onCategoryChange, searchKe
1616
<div className="flex gap-2 shrink-0">
1717
<button
1818
className={cn(
19-
"px-[12px] py-[8px] h-10 text-title2 rounded-[8px] whitespace-nowrap cursor-pointer transition-colors font-semibold",
19+
"px-[10px] py-2 rounded-[8px] text-title2 transition-all",
2020
category === "BEAUTY"
2121
? "bg-core-1 text-white"
22-
: "bg-white text-text-gray3 border border-gray-100"
22+
: "bg-white border border-text-gray5 text-text-gray3"
2323
)}
2424
onClick={() => onCategoryChange("BEAUTY")}
2525
>
2626
뷰티
2727
</button>
2828
<button
2929
className={cn(
30-
"px-[12px] py-[8px] h-10 text-title2 rounded-[8px] whitespace-nowrap cursor-pointer transition-colors font-semibold",
30+
"px-[10px] py-2 rounded-[8px] text-title2 transition-all",
3131
category === "FASHION"
3232
? "bg-core-1 text-white"
33-
: "bg-white text-text-gray3 border border-gray-100"
33+
: "bg-white border border-text-gray5 text-text-gray3"
3434
)}
3535
onClick={() => onCategoryChange("FASHION")}
3636
>

0 commit comments

Comments
 (0)