diff --git a/src/components/common/tab/index.tsx b/src/components/common/tab/index.tsx
deleted file mode 100644
index 4dc8a37..0000000
--- a/src/components/common/tab/index.tsx
+++ /dev/null
@@ -1,78 +0,0 @@
-import { cloneElement, ReactElement, useEffect, useRef, useState } from 'react';
-
-import * as S from './styles';
-
-type TabProps = {
- label: string;
- value: number;
- active?: boolean;
- onClick?: () => void;
-};
-
-type TabsProps = {
- selectedTab: number;
- onChange: (value: number) => void;
- children: ReactElement[];
- position?: string;
-};
-
-type TabPanelProps = {
- children?: ReactElement;
- value: number;
- selectedIndex: number;
-};
-
-export const Tab = ({ label, active, onClick }: TabProps) => {
- return (
-
- {label}
-
- );
-};
-
-export const Tabs = ({
- selectedTab,
- onChange,
- children,
- position,
-}: TabsProps) => {
- const containerRef = useRef(null);
- const [containerWidth, setContainerWidth] = useState(0);
-
- useEffect(() => {
- if (containerRef.current) {
- setContainerWidth(containerRef.current.getBoundingClientRect().width);
- }
- }, [containerRef]);
-
- const sliderWidth = containerWidth / children.length;
-
- const tabs = children.map((child) => {
- const handleClick = () => {
- onChange(child.props.value);
- };
-
- return cloneElement(child, {
- key: child.props.value,
- active: child.props.value === selectedTab,
- onClick: handleClick,
- });
- });
-
- return (
-
- {tabs}
-
-
- );
-};
-
-export const TabPanel = ({ children, value, selectedIndex }: TabPanelProps) => {
- const hidden: boolean = value !== selectedIndex;
-
- return (
-
- {children}
-
- );
-};
diff --git a/src/components/common/tab/styles.ts b/src/components/common/tab/styles.ts
deleted file mode 100644
index d542d83..0000000
--- a/src/components/common/tab/styles.ts
+++ /dev/null
@@ -1,126 +0,0 @@
-import { css, keyframes } from '@emotion/react';
-import styled from '@emotion/styled';
-
-// 챌린지 상세 페이지 css
-
-export const TabsContainer = styled.div`
- display: flex;
- position: relative;
- top: 50px;
- align-self: center;
- width: 90%;
- margin: 0 auto;
- height: 55px;
- border-radius: 20px;
- background-color: var(--color-green-06);
-`;
-
-export const TabPanelContainer = styled.div`
- height: 100%;
- width: 100%;
- position: relative;
- top: 50px;
- text-align: center;
-`;
-
-export const Image = styled.img`
- position: relative;
- margin: auto;
- display: block;
- height: 40%;
- opacity: 20%;
- object-fit: cover;
- margin-bottom: 28px;
- filter: grayscale(100%);
-`;
-
-export const ImageMask = styled.div`
- background-color: var(--color-green-06);
- position: relative;
- top: 50px;
-`;
-
-export const Wrapper = styled.div``;
-
-type StylizedTabProps = {
- active?: boolean;
- onClick?: () => void;
- inactiveStyle?: React.CSSProperties;
-};
-
-export const StylizedTab = styled.div`
- z-index: 1;
- color: var(--color-grey-02);
- width: 50%; /* 각 Tab의 너비를 50%로 설정하여 두 개의 Tab이 꽉 차도록 설정 */
- font-size: var(--font-size-md);
- background-color: transparent;
- border: none;
- height: 50px;
- text-align: center;
- line-height: 50px;
- cursor: pointer;
-
- ${(p) =>
- p.active &&
- css`
- color: var(--color-white);
- font-weight: bold;
- border-radius: 20px;
- animation: ${inset} 0.4s cubic-bezier(0.25, 0.46, 0.45, 0.94) both;
- `}
-`;
-
-type StyledTabPanelProps = {
- active: boolean;
-};
-
-export const StyledTabPanel = styled.div`
- display: ${(p) => (p.active ? 'flex' : 'none')};
- font-size: 2rem;
- flex-direction: column;
- width: 100%;
- height: 100%;
- justify-content: center;
-`;
-
-type TabHeaderContainerProps = {
- position?: string;
-};
-
-export const TabHeaderContainer = styled.div`
- position: ${(props) => props.position || 'absolute'};
- width: 100%;
-`;
-
-export const TabsHolder = styled.div`
- display: flex;
- justify-content: space-between; /* Tab들이 좌우로 정렬되도록 설정 */
- width: 100%;
-`;
-
-type TabSliderProps = {
- width: number;
- index: number;
-};
-
-export const TabSlider = styled.div`
- position: absolute;
- top: 3px;
- height: 50px;
- background-color: var(--color-green-01);
- border-radius: 20px;
- transition: 0.2s;
- transform: ${({ width, index }) => `translateX(${width * index}px)`};
- width: ${({ width }) => `${width}px`};
-`;
-
-const inset = keyframes`
- 0% {
- -webkit-box-shadow: 0 0 0 0 rgba(92, 198, 186, 0);
- box-shadow: 0 0 0 0 rgba(92, 198, 186, 0);
- }
- 100% {
- -webkit-box-shadow: 3px 3px 3px rgba(92, 198, 186, 0.5);
- box-shadow: 3px 3px 3px rgba(92, 198, 186, 0.5);
- }
- `;
diff --git a/src/components/common/tabs/index.tsx b/src/components/common/tabs/index.tsx
new file mode 100644
index 0000000..7f1160d
--- /dev/null
+++ b/src/components/common/tabs/index.tsx
@@ -0,0 +1,74 @@
+import {
+ cloneElement,
+ forwardRef,
+ ReactElement,
+ useEffect,
+ useRef,
+ useState,
+} from 'react';
+
+import * as S from './styles';
+
+type TabsProps = {
+ selectedTab: number;
+ onChange: (value: number) => void;
+ children: ReactElement[];
+};
+
+export const Tabs = ({ selectedTab, onChange, children }: TabsProps) => {
+ const containerRef = useRef(null);
+ const tabRefs = useRef<(HTMLDivElement | null)[]>([]); // Tab refs 배열
+ const [tabWidths, setTabWidths] = useState([]); // 각 Tab 너비 저장
+
+ // 각 Tab의 너비를 계산하여 상태로 저장
+ useEffect(() => {
+ if (tabRefs.current.length > 0) {
+ const widths = tabRefs.current.map(
+ (ref) => ref?.getBoundingClientRect().width || 0
+ );
+ setTabWidths(widths);
+ }
+ }, [children]);
+
+ const sliderWidth = tabWidths[selectedTab] - 4 || 0; // 여백 4px 빼기
+
+ const tabs = children.map((child, index) => {
+ const handleClick = () => {
+ onChange(child.props.value);
+ };
+
+ return cloneElement(child, {
+ key: child.props.value,
+ active: child.props.value === selectedTab,
+ onClick: handleClick,
+ ref: (el: HTMLDivElement) => (tabRefs.current[index] = el), // Tab 요소에 ref 연결
+ });
+ });
+
+ return (
+
+ {tabs}
+
+
+ );
+};
+
+type TabProps = {
+ label: string;
+ value: number;
+ active?: boolean;
+ onClick?: () => void;
+};
+
+export const Tab = forwardRef(
+ ({ label, active, onClick }, ref) => {
+ return (
+
+ {label}
+
+ );
+ }
+);
+
+// displayName 설정으로 경고 해결
+Tab.displayName = 'Tab';
diff --git a/src/components/common/tabs/styles.ts b/src/components/common/tabs/styles.ts
new file mode 100644
index 0000000..993b914
--- /dev/null
+++ b/src/components/common/tabs/styles.ts
@@ -0,0 +1,77 @@
+import { css } from '@emotion/react';
+import styled from '@emotion/styled';
+
+export const StyledTabs = styled.div<{
+ position?: string;
+}>`
+ display: flex;
+ justify-content: space-between; /* Tab들이 좌우로 정렬되도록 설정 */
+ position: relative;
+ align-self: center;
+ height: 46px;
+ margin: 0 16px;
+ padding: 4px 0;
+ border-radius: 10px;
+ background-color: var(--color-green-06);
+`;
+
+// 선택된 탭
+export const TabSlider = styled.div<{
+ width: number;
+ index: number;
+}>`
+ position: absolute;
+ top: 4px;
+ left: 4px;
+ width: ${({ width }) => `${width}px`};
+ height: 38px;
+ background-color: var(--color-white);
+ border-radius: 10px;
+
+ /* 슬라이딩 애니메이션 */
+ transition: 0.2s;
+ transform: ${({ width, index }) => `translateX(${width * index}px)`};
+`;
+
+export const StyledTab = styled.div<{
+ active?: boolean;
+ onClick?: () => void;
+ inactiveStyle?: React.CSSProperties;
+}>`
+ z-index: 1;
+ width: 50%; /* 각 Tab의 너비를 50%로 설정하여 두 개의 Tab이 꽉 차도록 설정 */
+ height: 100%;
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ background-color: transparent;
+ border: none;
+ font-size: var(--font-size-md);
+ color: var(--color-grey-02);
+ cursor: pointer;
+
+ ${(p) =>
+ p.active &&
+ css`
+ color: var(--color-green-01);
+ font-weight: 600;
+ `}
+`;
+
+export const StyledTabPanels = styled.div`
+ height: 100%;
+ width: 100%;
+ position: relative;
+ text-align: center;
+`;
+
+export const StyledTabPanel = styled.div<{
+ active: boolean;
+}>`
+ display: ${(p) => (p.active ? 'flex' : 'none')};
+ font-size: 2rem;
+ flex-direction: column;
+ width: 100%;
+ height: 100%;
+ justify-content: center;
+`;
diff --git a/src/components/common/tabs/tap-panels.tsx b/src/components/common/tabs/tap-panels.tsx
new file mode 100644
index 0000000..6d25cd4
--- /dev/null
+++ b/src/components/common/tabs/tap-panels.tsx
@@ -0,0 +1,27 @@
+import { ReactElement } from 'react';
+
+import * as S from './styles';
+
+type TapPanelsProps = {
+ children: ReactElement[];
+};
+
+export const TabPanels = ({ children }: TapPanelsProps) => {
+ return {children};
+};
+
+type TabPanelProps = {
+ children?: ReactElement;
+ value: number;
+ selectedIndex: number;
+};
+
+export const TabPanel = ({ children, value, selectedIndex }: TabPanelProps) => {
+ const hidden: boolean = value !== selectedIndex;
+
+ return (
+
+ {children}
+
+ );
+};
diff --git a/src/pages/challenge-detail/index.tsx b/src/pages/challenge-detail/index.tsx
index cd0d99d..346baaf 100644
--- a/src/pages/challenge-detail/index.tsx
+++ b/src/pages/challenge-detail/index.tsx
@@ -8,7 +8,8 @@ import { ReviewSection } from './review-section/';
import * as S from './styles';
import { type ChallengeDetailData } from '@/apis/challenge-detail/challenge.detail.response';
import DefaultImage from '@/assets/Default-Image.svg';
-import { Tab, TabPanel, Tabs } from '@/components/common/tab';
+import { Tabs, Tab } from '@/components/common/tabs';
+import { TabPanels, TabPanel } from '@/components/common/tabs/tap-panels';
import TopBar from '@/components/features/layout/top-bar';
// const CHALLENGE_GROUP_ID = 38;
@@ -27,15 +28,15 @@ const ChallengeDetailPage = () => {
const tabsList = [
{
label: '설명',
- component: data ? : null,
+ panel: data ? : null,
},
{
label: '랭킹',
- component: data ? : null,
+ panel: data ? : null,
},
{
label: '리뷰',
- component: data ? : null,
+ panel: data ? : null,
},
];
@@ -55,7 +56,7 @@ const ChallengeDetailPage = () => {
};
fetchChallengeDetail();
- }, []);
+ }, [challengeGroupId]);
// 챌린지 리뷰 페이지에 필요한 챌린지 제목 세션 스토리지에 저장
useEffect(() => {
@@ -78,20 +79,19 @@ const ChallengeDetailPage = () => {
{data?.category}
{data?.title}
-
-
- {tabsList.map((t, index) => (
-
- ))}
-
-
-
+
+
+ {tabsList.map((t, index) => (
+
+ ))}
+
+
{tabsList.map((t, index) => (
- {t.component ?? undefined}
+ {t.panel ?? undefined}
))}
-
+
);
};
diff --git a/src/pages/challenge-detail/styles.ts b/src/pages/challenge-detail/styles.ts
index 7365586..1f7c8fa 100644
--- a/src/pages/challenge-detail/styles.ts
+++ b/src/pages/challenge-detail/styles.ts
@@ -1,21 +1,6 @@
import styled from '@emotion/styled';
-export const TabsContainer = styled.div`
- display: flex;
- position: relative;
- align-self: center;
- margin: 0 16px;
- height: 55px;
- border-radius: 20px;
- background-color: var(--color-green-06);
-`;
-
-export const TabPanelContainer = styled.div`
- height: 100%;
- width: 100%;
- position: relative;
- text-align: center;
-`;
+export const Wrapper = styled.div``;
export const ImageMask = styled.div`
background-color: var(--color-green-06);
@@ -33,8 +18,6 @@ export const Image = styled.img`
filter: grayscale(100%);
`;
-export const Wrapper = styled.div``;
-
export const ChallengeTitleWrapper = styled.div`
margin: 16px;
display: flex;
diff --git a/src/pages/challenge-record/index.tsx b/src/pages/challenge-record/index.tsx
index 0bcf751..99d2035 100644
--- a/src/pages/challenge-record/index.tsx
+++ b/src/pages/challenge-record/index.tsx
@@ -2,14 +2,11 @@ import { useState } from 'react';
import StampBoard from './components/stamp-board';
import Verification from './components/verification';
-import { Tab, TabPanel, Tabs } from '@/components/common/tab';
+import { Tabs, Tab } from '@/components/common/tabs';
+import { TabPanels, TabPanel } from '@/components/common/tabs/tap-panels';
import TopBar from '@/components/features/layout/top-bar';
import styled from '@emotion/styled';
-type TabsContainerProps = {
- position?: string;
-};
-
const ChallengeRecord = () => {
const [activeTab, setActiveTab] = useState<0 | 1>(0);
@@ -21,20 +18,18 @@ const ChallengeRecord = () => {
<>
-
- handleTab(value)}>
-
-
-
-
-
+ handleTab(value)}>
+
+
+
+
-
+
>
);
@@ -49,22 +44,3 @@ const ChallengeRecordLayout = styled.div`
height: 100%;
margin: 0 1.5rem;
`;
-
-const TabsContainer = styled.div`
- display: flex;
- align-self: center;
- width: 100%;
- margin: 1rem auto;
- height: 55px;
- border-radius: 20px;
- background-color: var(--color-green-06);
-`;
-
-const TabPanelContainer = styled.div`
- display: flex;
- flex-direction: column;
- align-items: center;
- height: 100%;
- width: 100%;
- text-align: center;
-`;