diff --git a/frontend/src/pages/ClubDetailPage/ClubDetailPage.styles.ts b/frontend/src/pages/ClubDetailPage/ClubDetailPage.styles.ts index f90ebbd11..9c2838f05 100644 --- a/frontend/src/pages/ClubDetailPage/ClubDetailPage.styles.ts +++ b/frontend/src/pages/ClubDetailPage/ClubDetailPage.styles.ts @@ -1,6 +1,7 @@ import styled from 'styled-components'; import { media } from '@/styles/mediaQuery'; import { colors } from '@/styles/theme/colors'; +import { transitions } from '@/styles/theme/transitions'; export const Container = styled.div` width: 100%; @@ -57,12 +58,9 @@ export const TabButton = styled.button<{ $active: boolean }>` border-bottom: 2px solid ${({ $active }) => ($active ? colors.gray[800] : colors.gray[400])}; cursor: pointer; - transition: all 0.2s; - - &:hover { - color: ${colors.gray[800]}; - border-bottom: 2px solid ${colors.gray[800]}; - } + transition: + color ${transitions.duration.normal} ${transitions.easing.easeInOut}, + border-color ${transitions.duration.normal} ${transitions.easing.easeInOut}; ${media.tablet} { flex: 1; @@ -70,4 +68,18 @@ export const TabButton = styled.button<{ $active: boolean }>` } `; -export const TabContent = styled.div``; +export const TabContent = styled.div` + animation: fadeIn ${transitions.duration.normal} + ${transitions.easing.easeInOut}; + + @keyframes fadeIn { + from { + opacity: 0; + transform: translateY(10px); + } + to { + opacity: 1; + transform: translateY(0); + } + } +`; diff --git a/frontend/src/pages/ClubDetailPage/ClubDetailPage.tsx b/frontend/src/pages/ClubDetailPage/ClubDetailPage.tsx index 6337039c9..b85e3ac93 100644 --- a/frontend/src/pages/ClubDetailPage/ClubDetailPage.tsx +++ b/frontend/src/pages/ClubDetailPage/ClubDetailPage.tsx @@ -1,5 +1,5 @@ -import { useState } from 'react'; -import { useParams } from 'react-router-dom'; +import { useCallback, useEffect, useState } from 'react'; +import { useParams, useSearchParams } from 'react-router-dom'; import Footer from '@/components/common/Footer/Footer'; import Header from '@/components/common/Header/Header'; import { PAGE_VIEW, USER_EVENT } from '@/constants/eventName'; @@ -13,17 +13,41 @@ import ClubProfileCard from '@/pages/ClubDetailPage/components/ClubProfileCard/C import * as Styled from './ClubDetailPage.styles'; import ClubDetailFooter from './components/ClubDetailFooter/ClubDetailFooter'; +export const TAB_TYPE = { + INTRO: 'intro', + PHOTOS: 'photos', +} as const; + +type TabType = (typeof TAB_TYPE)[keyof typeof TAB_TYPE]; + const ClubDetailPage = () => { - const [activeTab, setActiveTab] = useState<'intro' | 'photos'>('intro'); + const trackEvent = useMixpanelTrack(); + + const [searchParams, setSearchParams] = useSearchParams(); + const tabParam = searchParams.get('tab') as TabType | null; + + const activeTab: TabType = + tabParam && Object.values(TAB_TYPE).includes(tabParam) + ? tabParam + : TAB_TYPE.INTRO; const { clubId } = useParams<{ clubId: string }>(); const { isLaptop, isDesktop } = useDevice(); - const trackEvent = useMixpanelTrack(); const { data: clubDetail, error } = useGetClubDetail(clubId || ''); useTrackPageView(PAGE_VIEW.CLUB_DETAIL_PAGE, clubDetail?.name, !clubDetail); + const handleIntroTabClick = useCallback(() => { + setSearchParams({ tab: TAB_TYPE.INTRO }); + trackEvent(USER_EVENT.CLUB_INTRO_TAB_CLICKED); + }, [setSearchParams, trackEvent]); + + const handlePhotosTabClick = useCallback(() => { + setSearchParams({ tab: TAB_TYPE.PHOTOS }); + trackEvent(USER_EVENT.CLUB_FEED_TAB_CLICKED); + }, [setSearchParams, trackEvent]); + if (!clubDetail) { return null; } @@ -49,30 +73,24 @@ const ClubDetailPage = () => { { - setActiveTab('intro'); - trackEvent(USER_EVENT.CLUB_INTRO_TAB_CLICKED); - }} + $active={activeTab === TAB_TYPE.INTRO} + onClick={handleIntroTabClick} > 소개 내용 { - setActiveTab('photos'); - trackEvent(USER_EVENT.CLUB_FEED_TAB_CLICKED); - }} + $active={activeTab === TAB_TYPE.PHOTOS} + onClick={handlePhotosTabClick} > 활동사진 - {activeTab === 'intro' && ( + {activeTab === TAB_TYPE.INTRO && ( )} - {activeTab === 'photos' && ( + {activeTab === TAB_TYPE.PHOTOS && ( )} diff --git a/frontend/src/pages/ClubDetailPage/components/ClubFeed/ClubFeed.styles.ts b/frontend/src/pages/ClubDetailPage/components/ClubFeed/ClubFeed.styles.ts index 5eaab4df1..31de8c49b 100644 --- a/frontend/src/pages/ClubDetailPage/components/ClubFeed/ClubFeed.styles.ts +++ b/frontend/src/pages/ClubDetailPage/components/ClubFeed/ClubFeed.styles.ts @@ -1,6 +1,7 @@ import styled from 'styled-components'; import { media } from '@/styles/mediaQuery'; import { colors } from '@/styles/theme/colors'; +import { transitions } from '@/styles/theme/transitions'; export const Container = styled.div` width: 100%; @@ -22,7 +23,7 @@ export const PhotoItem = styled.div` overflow: hidden; cursor: pointer; background-color: ${colors.gray[100]}; - transition: all 0.3s ease; + transition: all ${transitions.duration.normal} ${transitions.easing.ease}; &:hover { transform: translateY(-4px); diff --git a/frontend/src/pages/ClubDetailPage/components/ClubIntroContent/ClubIntroContent.styles.ts b/frontend/src/pages/ClubDetailPage/components/ClubIntroContent/ClubIntroContent.styles.ts index 6516f7af5..dfaf2af63 100644 --- a/frontend/src/pages/ClubDetailPage/components/ClubIntroContent/ClubIntroContent.styles.ts +++ b/frontend/src/pages/ClubDetailPage/components/ClubIntroContent/ClubIntroContent.styles.ts @@ -1,6 +1,7 @@ import styled from 'styled-components'; import { media } from '@/styles/mediaQuery'; import { colors } from '@/styles/theme/colors'; +import { transitions } from '@/styles/theme/transitions'; import { typography } from '@/styles/theme/typography'; const setTypography = (typo: { size: string; weight: number }) => ` @@ -181,16 +182,24 @@ export const ArrowIcon = styled.svg<{ $isOpen: boolean }>` width: 24px; height: 24px; color: ${colors.gray[400]}; - transition: transform 0.3s ease; + transition: transform ${transitions.duration.normal} + ${transitions.easing.ease}; transform: ${({ $isOpen }) => ($isOpen ? 'rotate(180deg)' : 'rotate(0deg)')}; flex-shrink: 0; `; -export const AnswerContainer = styled.div` - padding: 0 20px 20px 20px; +export const AnswerContainer = styled.div<{ $isOpen: boolean }>` + max-height: ${({ $isOpen }) => ($isOpen ? '500px' : '0')}; + opacity: ${({ $isOpen }) => ($isOpen ? '1' : '0')}; + padding: ${({ $isOpen }) => ($isOpen ? '0 20px 20px 20px' : '0 20px')}; + overflow: hidden; + transition: + max-height ${transitions.duration.normal} ${transitions.easing.easeInOut}, + opacity ${transitions.duration.normal} ${transitions.easing.easeInOut}, + padding ${transitions.duration.normal} ${transitions.easing.easeInOut}; ${media.mobile} { - padding: 0 16px 16px 16px; + padding: ${({ $isOpen }) => ($isOpen ? '0 16px 16px 16px' : '0 16px')}; } `; diff --git a/frontend/src/pages/ClubDetailPage/components/ClubIntroContent/ClubIntroContent.tsx b/frontend/src/pages/ClubDetailPage/components/ClubIntroContent/ClubIntroContent.tsx index fdbd3f213..d6d1ab702 100644 --- a/frontend/src/pages/ClubDetailPage/components/ClubIntroContent/ClubIntroContent.tsx +++ b/frontend/src/pages/ClubDetailPage/components/ClubIntroContent/ClubIntroContent.tsx @@ -124,11 +124,9 @@ const ClubIntroContent = ({ /> - {isOpen && ( - - {faq.answer} - - )} + + {faq.answer} + ); })} diff --git a/frontend/src/pages/ClubDetailPage/mockData.ts b/frontend/src/pages/ClubDetailPage/mockData.ts deleted file mode 100644 index 1a4188d2c..000000000 --- a/frontend/src/pages/ClubDetailPage/mockData.ts +++ /dev/null @@ -1,43 +0,0 @@ -import { ClubApiResponse } from '@/types/club'; - -export const mockClubApi: ClubApiResponse = { - id: '67ee2b97b35e3c267e3c2485', - name: 'RCY', - logo: '/src/assets/images/logos/default_profile_image.svg', - cover: '/src/assets/images/banners/banner_desktop1.png', - tags: ['', ''], - state: '활성화', - introduction: '부산 RCY 대학생 봉사단', - description: { - introDescription: '부경대학교 어쩌고 저쩌고', - activityDescription: - '분야, 주제, 개발환경 등을 자율적으로 선택하여 한 학기 동안 팀원들과 프로젝트를 진행하고, 결과물을 제작해 발표 및 전시하는 활동을 합니다.', - awards: [ - { - semester: '2025 1학기', - achievements: [ - '교내 프로그래밍 경진대회 대상, 최우수상, 장려상 배출', - '2024 라이프 스타일 스마트 가전 메이커톤 대상', - ], - }, - ], - idealCandidate: { - tags: ['열정적인', '노력하는'], - content: - '이런 사람이 오면 좋아요, 열정적이고 협업을 중시하는 분들을 환영합니다.', - }, - benefits: - '협업하며 프로젝트를 진행하고,\n직접 결과물을 만들어 개발 경험을 쌓을 수 있습니다.', - faqs: [], - }, - recruitmentPeriod: '2025.08.04 15:00 ~ 2999.01.02 15:00', - recruitmentStatus: 'OPEN', - externalApplicationUrl: 'https://open.kakao.com/o/sBjU8ZKh', - socialLinks: { - instagram: 'https://www.instagram.com/pknu_rcy_official', - youtube: 'https://www.youtube.com/pknu_rcy_official', - x: '', - }, - category: '봉사', - division: '중동', -}; diff --git a/frontend/src/pages/MainPage/MainPage.tsx b/frontend/src/pages/MainPage/MainPage.tsx index 38f427a0b..aef79fe53 100644 --- a/frontend/src/pages/MainPage/MainPage.tsx +++ b/frontend/src/pages/MainPage/MainPage.tsx @@ -35,7 +35,6 @@ const MainPage = () => { }); const clubs = data?.clubs || []; - // const totalCount = data?.totalCount || 0; // ⚠️ 백엔드 업데이트 전까지 임시 주석 const totalCount = data?.totalCount ?? clubs.length; const isEmpty = !isLoading && clubs.length === 0; diff --git a/frontend/src/styles/theme/index.ts b/frontend/src/styles/theme/index.ts index ec8977da1..0a1170919 100644 --- a/frontend/src/styles/theme/index.ts +++ b/frontend/src/styles/theme/index.ts @@ -1,9 +1,11 @@ import { colors } from './colors'; +import { transitions } from './transitions'; import { typography } from './typography'; export const theme = { colors, typography, + transitions, } as const; export type Theme = typeof theme; diff --git a/frontend/src/styles/theme/transitions.ts b/frontend/src/styles/theme/transitions.ts new file mode 100644 index 000000000..7536e34f3 --- /dev/null +++ b/frontend/src/styles/theme/transitions.ts @@ -0,0 +1,13 @@ +export const transitions = { + duration: { + fast: '0.15s', + normal: '0.3s', + slow: '0.5s', + }, + easing: { + ease: 'ease', + easeIn: 'ease-in', + easeOut: 'ease-out', + easeInOut: 'ease-in-out', + }, +} as const;