From eaa7c5185871a41a15c5f9b62122fd41ff748656 Mon Sep 17 00:00:00 2001 From: suhyun113 <163711629+suhyun113@users.noreply.github.com> Date: Tue, 3 Mar 2026 23:12:46 +0900 Subject: [PATCH 001/172] =?UTF-8?q?feat:=20=ED=99=8D=EB=B3=B4=20=ED=8E=98?= =?UTF-8?q?=EC=9D=B4=EC=A7=80=20=EB=9D=BC=EC=9A=B0=ED=8A=B8=20=EB=B0=8F=20?= =?UTF-8?q?=ED=8E=98=EC=9D=B4=EC=A7=80=20=EC=9E=84=EC=8B=9C=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/src/App.tsx | 18 ++++++++++++++++++ .../PromotionPage/PromotionDetailPage.tsx | 15 +++++++++++++++ .../pages/PromotionPage/PromotionListPage.tsx | 10 ++++++++++ 3 files changed, 43 insertions(+) create mode 100644 frontend/src/pages/PromotionPage/PromotionDetailPage.tsx create mode 100644 frontend/src/pages/PromotionPage/PromotionListPage.tsx diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 6a605dc6c..bdfd596b0 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -22,6 +22,8 @@ import { import LegacyClubDetailPage from './pages/ClubDetailPage/LegacyClubDetailPage'; import ErrorTestPage from './pages/ErrorTestPage/ErrorTestPage'; import IntroductionPage from './pages/FestivalPage/IntroductionPage/IntroductionPage'; +import PromotionDetailPage from './pages/PromotionPage/PromotionDetailPage'; +import PromotionListPage from './pages/PromotionPage/PromotionListPage'; const queryClient = new QueryClient({ defaultOptions: { @@ -137,6 +139,22 @@ const App = () => { } /> + + + + } + /> + + + + } + /> {/* 개발 환경에서만 사용 가능한 에러 테스트 페이지 */} {import.meta.env.DEV && ( } /> diff --git a/frontend/src/pages/PromotionPage/PromotionDetailPage.tsx b/frontend/src/pages/PromotionPage/PromotionDetailPage.tsx new file mode 100644 index 000000000..1833e0b76 --- /dev/null +++ b/frontend/src/pages/PromotionPage/PromotionDetailPage.tsx @@ -0,0 +1,15 @@ +import { useParams } from 'react-router-dom'; + +const PromotionDetailPage = () => { + const { promotionId } = useParams(); + + return ( +
+

홍보 상세 페이지

+

홍보 ID: {promotionId}

+

여기에 홍보 상세 정보 들어올 예정

+
+ ); +}; + +export default PromotionDetailPage; diff --git a/frontend/src/pages/PromotionPage/PromotionListPage.tsx b/frontend/src/pages/PromotionPage/PromotionListPage.tsx new file mode 100644 index 000000000..938ff281c --- /dev/null +++ b/frontend/src/pages/PromotionPage/PromotionListPage.tsx @@ -0,0 +1,10 @@ +const PromotionListPage = () => { + return ( +
+

홍보 목록 페이지

+

여기에 홍보 카드 2열로 배치 예정

+
+ ); +}; + +export default PromotionListPage; From e9b41a9107c234b424d41c5922742c7fe8727799 Mon Sep 17 00:00:00 2001 From: suhyun113 <163711629+suhyun113@users.noreply.github.com> Date: Tue, 3 Mar 2026 23:35:08 +0900 Subject: [PATCH 002/172] =?UTF-8?q?feat:=20=ED=99=8D=EB=B3=B4=20=ED=8E=98?= =?UTF-8?q?=EC=9D=B4=EC=A7=80=20Mixpanel=20=ED=8A=B8=EB=9E=98=ED=82=B9=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/src/constants/eventName.ts | 2 ++ frontend/src/pages/PromotionPage/PromotionDetailPage.tsx | 3 +++ frontend/src/pages/PromotionPage/PromotionListPage.tsx | 4 ++++ 3 files changed, 9 insertions(+) diff --git a/frontend/src/constants/eventName.ts b/frontend/src/constants/eventName.ts index 6f095fb12..2e39401f3 100644 --- a/frontend/src/constants/eventName.ts +++ b/frontend/src/constants/eventName.ts @@ -107,6 +107,8 @@ export const PAGE_VIEW = { INTRODUCE_PAGE: 'IntroducePage', CLUB_UNION_PAGE: 'ClubUnionPage', FESTIVAL_INTRODUCTION_PAGE: '동소한 페이지', + PROMOTION_LIST_PAGE: '홍보 목록 페이지', + PROMOTION_DETAIL_PAGE: '홍보 상세 페이지', // 관리자 LOGIN_PAGE: '로그인페이지', diff --git a/frontend/src/pages/PromotionPage/PromotionDetailPage.tsx b/frontend/src/pages/PromotionPage/PromotionDetailPage.tsx index 1833e0b76..0ff8c27ac 100644 --- a/frontend/src/pages/PromotionPage/PromotionDetailPage.tsx +++ b/frontend/src/pages/PromotionPage/PromotionDetailPage.tsx @@ -1,6 +1,9 @@ +import { PAGE_VIEW } from '@/constants/eventName'; +import useTrackPageView from '@/hooks/Mixpanel/useTrackPageView'; import { useParams } from 'react-router-dom'; const PromotionDetailPage = () => { + useTrackPageView(PAGE_VIEW.PROMOTION_DETAIL_PAGE); const { promotionId } = useParams(); return ( diff --git a/frontend/src/pages/PromotionPage/PromotionListPage.tsx b/frontend/src/pages/PromotionPage/PromotionListPage.tsx index 938ff281c..b86e4768b 100644 --- a/frontend/src/pages/PromotionPage/PromotionListPage.tsx +++ b/frontend/src/pages/PromotionPage/PromotionListPage.tsx @@ -1,4 +1,8 @@ +import { PAGE_VIEW } from '@/constants/eventName'; +import useTrackPageView from '@/hooks/Mixpanel/useTrackPageView'; + const PromotionListPage = () => { + useTrackPageView(PAGE_VIEW.PROMOTION_LIST_PAGE); return (

홍보 목록 페이지

From 85ceb2ffc54d2b4be76cd318c9dc4fece3053a51 Mon Sep 17 00:00:00 2001 From: suhyun113 <163711629+suhyun113@users.noreply.github.com> Date: Wed, 4 Mar 2026 00:34:15 +0900 Subject: [PATCH 003/172] =?UTF-8?q?feat:=20=ED=99=8D=EB=B3=B4=20=EC=B9=A9?= =?UTF-8?q?=20=EC=B6=94=EA=B0=80=20=EB=B0=8F=20=ED=99=8D=EB=B3=B4=20?= =?UTF-8?q?=EB=AA=A9=EB=A1=9D=20=ED=8E=98=EC=9D=B4=EC=A7=80=20=EC=B4=88?= =?UTF-8?q?=EA=B8=B0=20=EB=A0=88=EC=9D=B4=EC=95=84=EC=9B=83=20=EC=83=9D?= =?UTF-8?q?=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../MainPage/components/Filter/Filter.tsx | 1 + .../PromotionPage/PromotionListPage.styles.ts | 18 ++++++++++++++++ .../pages/PromotionPage/PromotionListPage.tsx | 21 +++++++++++++++---- 3 files changed, 36 insertions(+), 4 deletions(-) create mode 100644 frontend/src/pages/PromotionPage/PromotionListPage.styles.ts diff --git a/frontend/src/pages/MainPage/components/Filter/Filter.tsx b/frontend/src/pages/MainPage/components/Filter/Filter.tsx index 20336f379..d9b610f31 100644 --- a/frontend/src/pages/MainPage/components/Filter/Filter.tsx +++ b/frontend/src/pages/MainPage/components/Filter/Filter.tsx @@ -7,6 +7,7 @@ import * as Styled from './Filter.styles'; const FILTER_OPTIONS = [ { label: '동소한', path: '/festival-introduction' }, { label: '동아리', path: '/' }, + { label: '홍보', path: '/promotions' }, ] as const; const FESTIVAL_PATH = '/festival-introduction'; diff --git a/frontend/src/pages/PromotionPage/PromotionListPage.styles.ts b/frontend/src/pages/PromotionPage/PromotionListPage.styles.ts new file mode 100644 index 000000000..12b898061 --- /dev/null +++ b/frontend/src/pages/PromotionPage/PromotionListPage.styles.ts @@ -0,0 +1,18 @@ +import { media } from "@/styles/mediaQuery"; +import styled from "styled-components"; + +export const Container = styled.div` + width: 100%; + max-width: 550px; + margin: 0 auto; + padding-top: 24px; + + ${media.mobile} { + padding-top: 0; + } +`; + +export const Wrapper = styled.div` + margin-top: 4px; + padding: 0px 20px 66px; 20px; +`; diff --git a/frontend/src/pages/PromotionPage/PromotionListPage.tsx b/frontend/src/pages/PromotionPage/PromotionListPage.tsx index b86e4768b..a63b60b5c 100644 --- a/frontend/src/pages/PromotionPage/PromotionListPage.tsx +++ b/frontend/src/pages/PromotionPage/PromotionListPage.tsx @@ -1,13 +1,26 @@ +import Footer from '@/components/common/Footer/Footer'; +import Header from '@/components/common/Header/Header'; import { PAGE_VIEW } from '@/constants/eventName'; import useTrackPageView from '@/hooks/Mixpanel/useTrackPageView'; +import isInAppWebView from '@/utils/isInAppWebView'; +import Filter from '../MainPage/components/Filter/Filter'; +import * as Styled from './PromotionListPage.styles'; const PromotionListPage = () => { useTrackPageView(PAGE_VIEW.PROMOTION_LIST_PAGE); + return ( -
-

홍보 목록 페이지

-

여기에 홍보 카드 2열로 배치 예정

-
+ <> +
+ + {!isInAppWebView() && } + +

홍보 목록 페이지

+

여기에 홍보 카드 2열로 배치 예정

+
+
+
+ ); }; From c2e67fc912f7c65b9f9086304d8bfaad33dfb76b Mon Sep 17 00:00:00 2001 From: suhyun113 <163711629+suhyun113@users.noreply.github.com> Date: Wed, 4 Mar 2026 00:45:30 +0900 Subject: [PATCH 004/172] =?UTF-8?q?feat:=20=ED=99=8D=EB=B3=B4=20=EC=B9=B4?= =?UTF-8?q?=EB=93=9C=202=EC=97=B4=20=EA=B7=B8=EB=A6=AC=EB=93=9C=20?= =?UTF-8?q?=EA=B5=AC=EC=A1=B0=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../pages/PromotionPage/PromotionListPage.tsx | 4 ++-- .../components/PromotionGrid.styles.ts | 7 +++++++ .../PromotionPage/components/PromotionGrid.tsx | 17 +++++++++++++++++ 3 files changed, 26 insertions(+), 2 deletions(-) create mode 100644 frontend/src/pages/PromotionPage/components/PromotionGrid.styles.ts create mode 100644 frontend/src/pages/PromotionPage/components/PromotionGrid.tsx diff --git a/frontend/src/pages/PromotionPage/PromotionListPage.tsx b/frontend/src/pages/PromotionPage/PromotionListPage.tsx index a63b60b5c..20b55b631 100644 --- a/frontend/src/pages/PromotionPage/PromotionListPage.tsx +++ b/frontend/src/pages/PromotionPage/PromotionListPage.tsx @@ -4,6 +4,7 @@ import { PAGE_VIEW } from '@/constants/eventName'; import useTrackPageView from '@/hooks/Mixpanel/useTrackPageView'; import isInAppWebView from '@/utils/isInAppWebView'; import Filter from '../MainPage/components/Filter/Filter'; +import PromottionGrid from './components/PromotionGrid'; import * as Styled from './PromotionListPage.styles'; const PromotionListPage = () => { @@ -15,8 +16,7 @@ const PromotionListPage = () => { {!isInAppWebView() && } -

홍보 목록 페이지

-

여기에 홍보 카드 2열로 배치 예정

+
diff --git a/frontend/src/pages/PromotionPage/components/PromotionGrid.styles.ts b/frontend/src/pages/PromotionPage/components/PromotionGrid.styles.ts new file mode 100644 index 000000000..21df427c2 --- /dev/null +++ b/frontend/src/pages/PromotionPage/components/PromotionGrid.styles.ts @@ -0,0 +1,7 @@ +import styled from "styled-components"; + +export const Grid = styled.div` + display: grid; + grid-template-columns: repeat(2, 1fr); + gap: 7px; +`; \ No newline at end of file diff --git a/frontend/src/pages/PromotionPage/components/PromotionGrid.tsx b/frontend/src/pages/PromotionPage/components/PromotionGrid.tsx new file mode 100644 index 000000000..d089695d6 --- /dev/null +++ b/frontend/src/pages/PromotionPage/components/PromotionGrid.tsx @@ -0,0 +1,17 @@ +import * as Styled from './PromotionGrid.styles'; + +const dummyPromotions = [1, 2, 3, 4]; + +const PromotionGrid = () => { + return ( + + {dummyPromotions.map((promotion) => ( +
+

홍보 카드 {promotion}

+
+ ))} +
+ ); +}; + +export default PromotionGrid; From 6a350374d8e2728908c8a616ba6f63ecd99877a0 Mon Sep 17 00:00:00 2001 From: suhyun113 <163711629+suhyun113@users.noreply.github.com> Date: Wed, 4 Mar 2026 01:00:07 +0900 Subject: [PATCH 005/172] =?UTF-8?q?refactor:=20=ED=8C=8C=EC=9D=BC=20?= =?UTF-8?q?=EA=B2=BD=EB=A1=9C=20=EC=9D=B4=EB=8F=99?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/src/pages/PromotionPage/PromotionDetailPage.tsx | 2 +- frontend/src/pages/PromotionPage/PromotionListPage.styles.ts | 4 ++-- frontend/src/pages/PromotionPage/PromotionListPage.tsx | 2 +- .../components/{ => PromotionGrid}/PromotionGrid.styles.ts | 4 ++-- .../components/{ => PromotionGrid}/PromotionGrid.tsx | 0 5 files changed, 6 insertions(+), 6 deletions(-) rename frontend/src/pages/PromotionPage/components/{ => PromotionGrid}/PromotionGrid.styles.ts (70%) rename frontend/src/pages/PromotionPage/components/{ => PromotionGrid}/PromotionGrid.tsx (100%) diff --git a/frontend/src/pages/PromotionPage/PromotionDetailPage.tsx b/frontend/src/pages/PromotionPage/PromotionDetailPage.tsx index 0ff8c27ac..f3b09d92a 100644 --- a/frontend/src/pages/PromotionPage/PromotionDetailPage.tsx +++ b/frontend/src/pages/PromotionPage/PromotionDetailPage.tsx @@ -1,6 +1,6 @@ +import { useParams } from 'react-router-dom'; import { PAGE_VIEW } from '@/constants/eventName'; import useTrackPageView from '@/hooks/Mixpanel/useTrackPageView'; -import { useParams } from 'react-router-dom'; const PromotionDetailPage = () => { useTrackPageView(PAGE_VIEW.PROMOTION_DETAIL_PAGE); diff --git a/frontend/src/pages/PromotionPage/PromotionListPage.styles.ts b/frontend/src/pages/PromotionPage/PromotionListPage.styles.ts index 12b898061..ab6c5098b 100644 --- a/frontend/src/pages/PromotionPage/PromotionListPage.styles.ts +++ b/frontend/src/pages/PromotionPage/PromotionListPage.styles.ts @@ -1,5 +1,5 @@ -import { media } from "@/styles/mediaQuery"; -import styled from "styled-components"; +import styled from 'styled-components'; +import { media } from '@/styles/mediaQuery'; export const Container = styled.div` width: 100%; diff --git a/frontend/src/pages/PromotionPage/PromotionListPage.tsx b/frontend/src/pages/PromotionPage/PromotionListPage.tsx index 20b55b631..e50cceeec 100644 --- a/frontend/src/pages/PromotionPage/PromotionListPage.tsx +++ b/frontend/src/pages/PromotionPage/PromotionListPage.tsx @@ -4,7 +4,7 @@ import { PAGE_VIEW } from '@/constants/eventName'; import useTrackPageView from '@/hooks/Mixpanel/useTrackPageView'; import isInAppWebView from '@/utils/isInAppWebView'; import Filter from '../MainPage/components/Filter/Filter'; -import PromottionGrid from './components/PromotionGrid'; +import PromottionGrid from './components/PromotionGrid/PromotionGrid'; import * as Styled from './PromotionListPage.styles'; const PromotionListPage = () => { diff --git a/frontend/src/pages/PromotionPage/components/PromotionGrid.styles.ts b/frontend/src/pages/PromotionPage/components/PromotionGrid/PromotionGrid.styles.ts similarity index 70% rename from frontend/src/pages/PromotionPage/components/PromotionGrid.styles.ts rename to frontend/src/pages/PromotionPage/components/PromotionGrid/PromotionGrid.styles.ts index 21df427c2..425b8fbff 100644 --- a/frontend/src/pages/PromotionPage/components/PromotionGrid.styles.ts +++ b/frontend/src/pages/PromotionPage/components/PromotionGrid/PromotionGrid.styles.ts @@ -1,7 +1,7 @@ -import styled from "styled-components"; +import styled from 'styled-components'; export const Grid = styled.div` display: grid; grid-template-columns: repeat(2, 1fr); gap: 7px; -`; \ No newline at end of file +`; diff --git a/frontend/src/pages/PromotionPage/components/PromotionGrid.tsx b/frontend/src/pages/PromotionPage/components/PromotionGrid/PromotionGrid.tsx similarity index 100% rename from frontend/src/pages/PromotionPage/components/PromotionGrid.tsx rename to frontend/src/pages/PromotionPage/components/PromotionGrid/PromotionGrid.tsx From 25cf2569899ee4e4270c17dc48542ba5d28ea898 Mon Sep 17 00:00:00 2001 From: suhyun113 <163711629+suhyun113@users.noreply.github.com> Date: Wed, 4 Mar 2026 12:18:40 +0900 Subject: [PATCH 006/172] =?UTF-8?q?feat:=20=ED=99=8D=EB=B3=B4=20=EC=B9=B4?= =?UTF-8?q?=EB=93=9C=20=EC=83=9D=EC=84=B1=20=EB=B0=8F=20=EC=B4=88=EA=B8=B0?= =?UTF-8?q?=20=EB=A0=88=EC=9D=B4=EC=95=84=EC=9B=83=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../PromotionCard/PromotionCard.styles.ts | 36 +++++++++++++++++++ .../PromotionCard/PromotionCard.tsx | 22 ++++++++++++ .../PromotionGrid/PromotionGrid.tsx | 3 +- 3 files changed, 60 insertions(+), 1 deletion(-) create mode 100644 frontend/src/pages/PromotionPage/components/PromotionCard/PromotionCard.styles.ts create mode 100644 frontend/src/pages/PromotionPage/components/PromotionCard/PromotionCard.tsx diff --git a/frontend/src/pages/PromotionPage/components/PromotionCard/PromotionCard.styles.ts b/frontend/src/pages/PromotionPage/components/PromotionCard/PromotionCard.styles.ts new file mode 100644 index 000000000..20fd9487e --- /dev/null +++ b/frontend/src/pages/PromotionPage/components/PromotionCard/PromotionCard.styles.ts @@ -0,0 +1,36 @@ +import styled from 'styled-components'; +import { colors } from '@/styles/theme/colors'; + +export const Container = styled.div` + border-radius: 14px; + overflow: hidden; + background: ${colors.base.white}; + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.08); +`; + +export const ImageWrapper = styled.div` + position: relative; + width: 100%; + height: 180px; // 수정 예정 +`; + +export const Image = styled.div` + width: 100%; + height: 100%; + background: #ddd; +`; + +export const DdayWrapper = styled.div` + position: absolute; + top: 10px; + left: 10px; +`; + +export const Content = styled.div` + padding: 10px; +`; + +export const Title = styled.h3` + font-size: 14px; + font-weight: 700; +`; diff --git a/frontend/src/pages/PromotionPage/components/PromotionCard/PromotionCard.tsx b/frontend/src/pages/PromotionPage/components/PromotionCard/PromotionCard.tsx new file mode 100644 index 000000000..60561b347 --- /dev/null +++ b/frontend/src/pages/PromotionPage/components/PromotionCard/PromotionCard.tsx @@ -0,0 +1,22 @@ +import * as Styled from './PromotionCard.styles'; + +const PromotionCard = () => { + return ( + + + + + {/* */} + + + + + 💌✨WAP 최종 전시회 초대장 ✨💌 + {/* */} + {/* */} + + + ); +}; + +export default PromotionCard; diff --git a/frontend/src/pages/PromotionPage/components/PromotionGrid/PromotionGrid.tsx b/frontend/src/pages/PromotionPage/components/PromotionGrid/PromotionGrid.tsx index d089695d6..ce29b332f 100644 --- a/frontend/src/pages/PromotionPage/components/PromotionGrid/PromotionGrid.tsx +++ b/frontend/src/pages/PromotionPage/components/PromotionGrid/PromotionGrid.tsx @@ -1,3 +1,4 @@ +import PromotionCard from '../PromotionCard/PromotionCard'; import * as Styled from './PromotionGrid.styles'; const dummyPromotions = [1, 2, 3, 4]; @@ -7,7 +8,7 @@ const PromotionGrid = () => { {dummyPromotions.map((promotion) => (
-

홍보 카드 {promotion}

+
))}
From 5054047330abe82ddbfc75ad80b6c3cfb656ce0b Mon Sep 17 00:00:00 2001 From: suhyun113 <163711629+suhyun113@users.noreply.github.com> Date: Wed, 4 Mar 2026 15:56:19 +0900 Subject: [PATCH 007/172] =?UTF-8?q?feat:=20=EB=8F=99=EC=95=84=EB=A6=AC=20?= =?UTF-8?q?=ED=99=8D=EB=B3=B4=20=EC=B9=B4=EB=93=9C=20=EB=94=94=EC=9E=90?= =?UTF-8?q?=EC=9D=B8=20=EB=A0=88=EC=9D=B4=EC=95=84=EC=9B=83=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../PromotionCard/CardMeta/CardMeta.styles.ts | 35 +++++++++++++++++++ .../PromotionCard/CardMeta/CardMeta.tsx | 21 +++++++++++ .../PromotionCard/ClubTag/ClubTag.styles.ts | 17 +++++++++ .../PromotionCard/ClubTag/ClubTag.tsx | 11 ++++++ .../DdayBadge/DdayBadge.styles.ts | 15 ++++++++ .../PromotionCard/DdayBadge/DdayBadge.tsx | 11 ++++++ .../PromotionCard/PromotionCard.styles.ts | 5 --- .../PromotionCard/PromotionCard.tsx | 10 +++--- 8 files changed, 116 insertions(+), 9 deletions(-) create mode 100644 frontend/src/pages/PromotionPage/components/PromotionCard/CardMeta/CardMeta.styles.ts create mode 100644 frontend/src/pages/PromotionPage/components/PromotionCard/CardMeta/CardMeta.tsx create mode 100644 frontend/src/pages/PromotionPage/components/PromotionCard/ClubTag/ClubTag.styles.ts create mode 100644 frontend/src/pages/PromotionPage/components/PromotionCard/ClubTag/ClubTag.tsx create mode 100644 frontend/src/pages/PromotionPage/components/PromotionCard/DdayBadge/DdayBadge.styles.ts create mode 100644 frontend/src/pages/PromotionPage/components/PromotionCard/DdayBadge/DdayBadge.tsx diff --git a/frontend/src/pages/PromotionPage/components/PromotionCard/CardMeta/CardMeta.styles.ts b/frontend/src/pages/PromotionPage/components/PromotionCard/CardMeta/CardMeta.styles.ts new file mode 100644 index 000000000..ace968e4a --- /dev/null +++ b/frontend/src/pages/PromotionPage/components/PromotionCard/CardMeta/CardMeta.styles.ts @@ -0,0 +1,35 @@ +import styled from 'styled-components'; +import { colors } from '@/styles/theme/colors'; + +export const Container = styled.div` + display: flex; + flex-direction: column; +`; + +export const Title = styled.h3` + font-size: 14px; + font-weight: 700; + margin-bottom: 4px; +`; + +export const MetaRow = styled.div` + display: flex; + align-items: center; + margin-top: 3px; +`; + +export const Icon = styled.div` + display: flex; + align-items: center; + color: ${colors.gray[500]}; +`; + +export const MetaText = styled.span` + font-size: 12px; + font-weight: 600; + color: ${colors.gray[600]}; + + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +`; diff --git a/frontend/src/pages/PromotionPage/components/PromotionCard/CardMeta/CardMeta.tsx b/frontend/src/pages/PromotionPage/components/PromotionCard/CardMeta/CardMeta.tsx new file mode 100644 index 000000000..01d2bb967 --- /dev/null +++ b/frontend/src/pages/PromotionPage/components/PromotionCard/CardMeta/CardMeta.tsx @@ -0,0 +1,21 @@ +import * as Styled from './CardMeta.styles'; + +const CardMeta = () => { + return ( + + 💌✨WAP 최종 전시회 초대장 ✨💌 + + + + 부경대학교 향파관 3층 + + + + + 11월 28일 금요일 + + + ); +}; + +export default CardMeta; diff --git a/frontend/src/pages/PromotionPage/components/PromotionCard/ClubTag/ClubTag.styles.ts b/frontend/src/pages/PromotionPage/components/PromotionCard/ClubTag/ClubTag.styles.ts new file mode 100644 index 000000000..197d2c628 --- /dev/null +++ b/frontend/src/pages/PromotionPage/components/PromotionCard/ClubTag/ClubTag.styles.ts @@ -0,0 +1,17 @@ +import styled from 'styled-components'; +import { colors } from '@/styles/theme/colors'; + +export const Container = styled.div` + width: 43px; + height: 25px; + border-radius: 8px; + background-color: ${colors.gray[100]}; + padding: 4px 8px; + margin-top: 4px; +`; + +export const ClubText = styled.h1` + color: ${colors.gray[800]}; + font-size: 12px; + font-weight: 600; +`; diff --git a/frontend/src/pages/PromotionPage/components/PromotionCard/ClubTag/ClubTag.tsx b/frontend/src/pages/PromotionPage/components/PromotionCard/ClubTag/ClubTag.tsx new file mode 100644 index 000000000..58e83ea24 --- /dev/null +++ b/frontend/src/pages/PromotionPage/components/PromotionCard/ClubTag/ClubTag.tsx @@ -0,0 +1,11 @@ +import * as Styled from './ClubTag.styles'; + +const ClubTag = () => { + return ( + + WAP + + ); +}; + +export default ClubTag; diff --git a/frontend/src/pages/PromotionPage/components/PromotionCard/DdayBadge/DdayBadge.styles.ts b/frontend/src/pages/PromotionPage/components/PromotionCard/DdayBadge/DdayBadge.styles.ts new file mode 100644 index 000000000..19f8b5969 --- /dev/null +++ b/frontend/src/pages/PromotionPage/components/PromotionCard/DdayBadge/DdayBadge.styles.ts @@ -0,0 +1,15 @@ +import { colors } from '@/styles/theme/colors'; +import styled from 'styled-components'; + +export const Container = styled.div` + border-radius: 50px; + background-color: ${colors.base.white}; + padding: 4px 10px; + // Glass 효과는 이후 적용 예정 +`; + +export const DdayText = styled.h1` + color: ${colors.gray[800]}; + font-size: 10px; + font-weight: 600; +`; \ No newline at end of file diff --git a/frontend/src/pages/PromotionPage/components/PromotionCard/DdayBadge/DdayBadge.tsx b/frontend/src/pages/PromotionPage/components/PromotionCard/DdayBadge/DdayBadge.tsx new file mode 100644 index 000000000..cebb4eb0e --- /dev/null +++ b/frontend/src/pages/PromotionPage/components/PromotionCard/DdayBadge/DdayBadge.tsx @@ -0,0 +1,11 @@ +import * as Styled from './DdayBadge.styles'; + +const DdayBadge = () => { + return ( + + D-10 + + ); +}; + +export default DdayBadge; diff --git a/frontend/src/pages/PromotionPage/components/PromotionCard/PromotionCard.styles.ts b/frontend/src/pages/PromotionPage/components/PromotionCard/PromotionCard.styles.ts index 20fd9487e..629a3e449 100644 --- a/frontend/src/pages/PromotionPage/components/PromotionCard/PromotionCard.styles.ts +++ b/frontend/src/pages/PromotionPage/components/PromotionCard/PromotionCard.styles.ts @@ -29,8 +29,3 @@ export const DdayWrapper = styled.div` export const Content = styled.div` padding: 10px; `; - -export const Title = styled.h3` - font-size: 14px; - font-weight: 700; -`; diff --git a/frontend/src/pages/PromotionPage/components/PromotionCard/PromotionCard.tsx b/frontend/src/pages/PromotionPage/components/PromotionCard/PromotionCard.tsx index 60561b347..cf8fb5320 100644 --- a/frontend/src/pages/PromotionPage/components/PromotionCard/PromotionCard.tsx +++ b/frontend/src/pages/PromotionPage/components/PromotionCard/PromotionCard.tsx @@ -1,3 +1,6 @@ +import CardMeta from './CardMeta/CardMeta'; +import ClubTag from './ClubTag/ClubTag'; +import DdayBadge from './DdayBadge/DdayBadge'; import * as Styled from './PromotionCard.styles'; const PromotionCard = () => { @@ -6,14 +9,13 @@ const PromotionCard = () => { - {/* */} + - 💌✨WAP 최종 전시회 초대장 ✨💌 - {/* */} - {/* */} + + ); From f547f14e784152cd20be3e80331861baa381b621 Mon Sep 17 00:00:00 2001 From: suhyun113 <163711629+suhyun113@users.noreply.github.com> Date: Thu, 5 Mar 2026 00:23:28 +0900 Subject: [PATCH 008/172] =?UTF-8?q?chore:=20=ED=99=8D=EB=B3=B4=20=EC=B9=B4?= =?UTF-8?q?=EB=93=9C=20=EC=9C=84=EC=B9=98,=20=EC=8B=9C=EA=B0=84=20?= =?UTF-8?q?=EC=95=84=EC=9D=B4=EC=BD=98=20svg=20=ED=8C=8C=EC=9D=BC=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/src/assets/images/icons/location_icon.svg | 3 +++ frontend/src/assets/images/icons/time_icon.svg | 3 +++ 2 files changed, 6 insertions(+) create mode 100644 frontend/src/assets/images/icons/location_icon.svg create mode 100644 frontend/src/assets/images/icons/time_icon.svg diff --git a/frontend/src/assets/images/icons/location_icon.svg b/frontend/src/assets/images/icons/location_icon.svg new file mode 100644 index 000000000..ffa02839a --- /dev/null +++ b/frontend/src/assets/images/icons/location_icon.svg @@ -0,0 +1,3 @@ + + + diff --git a/frontend/src/assets/images/icons/time_icon.svg b/frontend/src/assets/images/icons/time_icon.svg new file mode 100644 index 000000000..98823d140 --- /dev/null +++ b/frontend/src/assets/images/icons/time_icon.svg @@ -0,0 +1,3 @@ + + + From 93752a0241826c4a49ea45e671ebf0b2dc6cbf9d Mon Sep 17 00:00:00 2001 From: suhyun113 <163711629+suhyun113@users.noreply.github.com> Date: Thu, 5 Mar 2026 01:48:45 +0900 Subject: [PATCH 009/172] =?UTF-8?q?feat:=20api=20=EC=97=B0=EB=8F=99=20?= =?UTF-8?q?=EB=B0=8F=20=EC=8A=A4=ED=83=80=EC=9D=BC=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../pages/PromotionPage/PromotionListPage.tsx | 7 +++- .../PromotionCard/CardMeta/CardMeta.styles.ts | 4 +++ .../PromotionCard/CardMeta/CardMeta.tsx | 36 +++++++++++++------ .../PromotionCard/ClubTag/ClubTag.styles.ts | 5 +-- .../PromotionCard/ClubTag/ClubTag.tsx | 8 +++-- .../PromotionCard/DdayBadge/DdayBadge.tsx | 30 ++++++++++++++-- .../PromotionCard/PromotionCard.styles.ts | 11 ++++-- .../PromotionCard/PromotionCard.tsx | 21 ++++++++--- .../PromotionGrid/PromotionGrid.tsx | 16 +++++---- 9 files changed, 106 insertions(+), 32 deletions(-) diff --git a/frontend/src/pages/PromotionPage/PromotionListPage.tsx b/frontend/src/pages/PromotionPage/PromotionListPage.tsx index e50cceeec..88b7847a7 100644 --- a/frontend/src/pages/PromotionPage/PromotionListPage.tsx +++ b/frontend/src/pages/PromotionPage/PromotionListPage.tsx @@ -2,6 +2,7 @@ import Footer from '@/components/common/Footer/Footer'; import Header from '@/components/common/Header/Header'; import { PAGE_VIEW } from '@/constants/eventName'; import useTrackPageView from '@/hooks/Mixpanel/useTrackPageView'; +import { useGetPromotionArticles } from '@/hooks/Queries/usePromotion'; import isInAppWebView from '@/utils/isInAppWebView'; import Filter from '../MainPage/components/Filter/Filter'; import PromottionGrid from './components/PromotionGrid/PromotionGrid'; @@ -10,13 +11,17 @@ import * as Styled from './PromotionListPage.styles'; const PromotionListPage = () => { useTrackPageView(PAGE_VIEW.PROMOTION_LIST_PAGE); + const { data, isLoading, isError } = useGetPromotionArticles(); + return ( <>
{!isInAppWebView() && } - + {isLoading &&

로딩 중...

} + {isError &&

오류가 발생했습니다.

} + {!isLoading && !isError && }
diff --git a/frontend/src/pages/PromotionPage/components/PromotionCard/CardMeta/CardMeta.styles.ts b/frontend/src/pages/PromotionPage/components/PromotionCard/CardMeta/CardMeta.styles.ts index ace968e4a..8c81bf22c 100644 --- a/frontend/src/pages/PromotionPage/components/PromotionCard/CardMeta/CardMeta.styles.ts +++ b/frontend/src/pages/PromotionPage/components/PromotionCard/CardMeta/CardMeta.styles.ts @@ -19,8 +19,12 @@ export const MetaRow = styled.div` `; export const Icon = styled.div` + width: 14px; + height: 14px; + padding: 1.5px 0px; display: flex; align-items: center; + justify-content: center; color: ${colors.gray[500]}; `; diff --git a/frontend/src/pages/PromotionPage/components/PromotionCard/CardMeta/CardMeta.tsx b/frontend/src/pages/PromotionPage/components/PromotionCard/CardMeta/CardMeta.tsx index 01d2bb967..c783cd4d4 100644 --- a/frontend/src/pages/PromotionPage/components/PromotionCard/CardMeta/CardMeta.tsx +++ b/frontend/src/pages/PromotionPage/components/PromotionCard/CardMeta/CardMeta.tsx @@ -1,19 +1,35 @@ import * as Styled from './CardMeta.styles'; +import LocationIcon from '@/assets/images/icons/location_icon.svg'; +import TimeIcon from '@/assets/images/icons/time_icon.svg'; + +interface CardMetaProps { + title: string; + location: string | null; + startDate: string; +} + +const CardMeta = ({ title, location, startDate }: CardMetaProps) => { + const formattedStartDate = new Date(startDate).toLocaleDateString(); -const CardMeta = () => { return ( - 💌✨WAP 최종 전시회 초대장 ✨💌 - - - - 부경대학교 향파관 3층 - + {title} + + {location && ( - - - 11월 28일 금요일 + + Location + + {location} + )} + + + + Time + + {formattedStartDate} + ); }; diff --git a/frontend/src/pages/PromotionPage/components/PromotionCard/ClubTag/ClubTag.styles.ts b/frontend/src/pages/PromotionPage/components/PromotionCard/ClubTag/ClubTag.styles.ts index 197d2c628..a97c0569c 100644 --- a/frontend/src/pages/PromotionPage/components/PromotionCard/ClubTag/ClubTag.styles.ts +++ b/frontend/src/pages/PromotionPage/components/PromotionCard/ClubTag/ClubTag.styles.ts @@ -2,8 +2,9 @@ import styled from 'styled-components'; import { colors } from '@/styles/theme/colors'; export const Container = styled.div` - width: 43px; - height: 25px; + display: inline-flex; + align-items: center; + justify-content: center; border-radius: 8px; background-color: ${colors.gray[100]}; padding: 4px 8px; diff --git a/frontend/src/pages/PromotionPage/components/PromotionCard/ClubTag/ClubTag.tsx b/frontend/src/pages/PromotionPage/components/PromotionCard/ClubTag/ClubTag.tsx index 58e83ea24..0fbaf46e3 100644 --- a/frontend/src/pages/PromotionPage/components/PromotionCard/ClubTag/ClubTag.tsx +++ b/frontend/src/pages/PromotionPage/components/PromotionCard/ClubTag/ClubTag.tsx @@ -1,9 +1,13 @@ import * as Styled from './ClubTag.styles'; -const ClubTag = () => { +interface ClubTagProps { + clubName: string; +} + +const ClubTag = ({ clubName }: ClubTagProps) => { return ( - WAP + {clubName} ); }; diff --git a/frontend/src/pages/PromotionPage/components/PromotionCard/DdayBadge/DdayBadge.tsx b/frontend/src/pages/PromotionPage/components/PromotionCard/DdayBadge/DdayBadge.tsx index cebb4eb0e..72e479678 100644 --- a/frontend/src/pages/PromotionPage/components/PromotionCard/DdayBadge/DdayBadge.tsx +++ b/frontend/src/pages/PromotionPage/components/PromotionCard/DdayBadge/DdayBadge.tsx @@ -1,11 +1,35 @@ import * as Styled from './DdayBadge.styles'; -const DdayBadge = () => { +interface DdayBadgeProps { + startDate: string; +} + +const DdayBadge = ({ startDate }: DdayBadgeProps) => { + const today = new Date(); + const start = new Date(startDate); + + today.setHours(0, 0, 0, 0); + start.setHours(0, 0, 0, 0); + + const diff = Math.ceil( + (start.getTime() - today.getTime()) / (1000 * 60 * 60 * 24) + ); + + let label: string; + + if (diff > 0) { + label = `D-${diff}`; + } else if (diff === 0) { + label = 'D-Day'; + } else { + label = '종료'; + } + return ( - D-10 + {label} ); }; -export default DdayBadge; +export default DdayBadge; \ No newline at end of file diff --git a/frontend/src/pages/PromotionPage/components/PromotionCard/PromotionCard.styles.ts b/frontend/src/pages/PromotionPage/components/PromotionCard/PromotionCard.styles.ts index 629a3e449..39faa8562 100644 --- a/frontend/src/pages/PromotionPage/components/PromotionCard/PromotionCard.styles.ts +++ b/frontend/src/pages/PromotionPage/components/PromotionCard/PromotionCard.styles.ts @@ -11,13 +11,18 @@ export const Container = styled.div` export const ImageWrapper = styled.div` position: relative; width: 100%; - height: 180px; // 수정 예정 + height: 164px; // 수정 예정 `; -export const Image = styled.div` +export const Image = styled.div<{ $imageUrl?: string }>` width: 100%; height: 100%; - background: #ddd; + + background-color: #ddd; + background-image: ${({ $imageUrl }) => ($imageUrl ? `url(${$imageUrl})` : 'none')}; + background-size: cover; + background-position: center; + background-repeat: no-repeat; `; export const DdayWrapper = styled.div` diff --git a/frontend/src/pages/PromotionPage/components/PromotionCard/PromotionCard.tsx b/frontend/src/pages/PromotionPage/components/PromotionCard/PromotionCard.tsx index cf8fb5320..4365a11ab 100644 --- a/frontend/src/pages/PromotionPage/components/PromotionCard/PromotionCard.tsx +++ b/frontend/src/pages/PromotionPage/components/PromotionCard/PromotionCard.tsx @@ -1,21 +1,32 @@ +import { PromotionArticle } from '@/types/promotion'; import CardMeta from './CardMeta/CardMeta'; import ClubTag from './ClubTag/ClubTag'; import DdayBadge from './DdayBadge/DdayBadge'; import * as Styled from './PromotionCard.styles'; -const PromotionCard = () => { +interface PromotionCardProps { + article: PromotionArticle; +} + +const PromotionCard = ({ article }: PromotionCardProps) => { + const imageUrl = article.images?.[0]; + return ( - + - + - - + + ); diff --git a/frontend/src/pages/PromotionPage/components/PromotionGrid/PromotionGrid.tsx b/frontend/src/pages/PromotionPage/components/PromotionGrid/PromotionGrid.tsx index ce29b332f..833f86232 100644 --- a/frontend/src/pages/PromotionPage/components/PromotionGrid/PromotionGrid.tsx +++ b/frontend/src/pages/PromotionPage/components/PromotionGrid/PromotionGrid.tsx @@ -1,15 +1,19 @@ +import { PromotionArticle } from '@/types/promotion'; import PromotionCard from '../PromotionCard/PromotionCard'; import * as Styled from './PromotionGrid.styles'; -const dummyPromotions = [1, 2, 3, 4]; +interface PromotionGridProps { + articles: PromotionArticle[]; +} -const PromotionGrid = () => { +const PromotionGrid = ({ articles }: PromotionGridProps) => { return ( - {dummyPromotions.map((promotion) => ( -
- -
+ {articles.map((article) => ( + ))}
); From 5bfc34b18875489030d769a5f9fb3966c927e017 Mon Sep 17 00:00:00 2001 From: suhyun113 <163711629+suhyun113@users.noreply.github.com> Date: Thu, 5 Mar 2026 02:06:56 +0900 Subject: [PATCH 010/172] =?UTF-8?q?feat:=20CardMeta=20=EB=82=A0=EC=A7=9C?= =?UTF-8?q?=20=EB=8D=B0=EC=9D=B4=ED=84=B0=20=EC=B6=9C=EB=A0=A5=20=ED=98=95?= =?UTF-8?q?=EC=8B=9D=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../components/PromotionCard/CardMeta/CardMeta.tsx | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/frontend/src/pages/PromotionPage/components/PromotionCard/CardMeta/CardMeta.tsx b/frontend/src/pages/PromotionPage/components/PromotionCard/CardMeta/CardMeta.tsx index c783cd4d4..562b61bb8 100644 --- a/frontend/src/pages/PromotionPage/components/PromotionCard/CardMeta/CardMeta.tsx +++ b/frontend/src/pages/PromotionPage/components/PromotionCard/CardMeta/CardMeta.tsx @@ -9,7 +9,12 @@ interface CardMetaProps { } const CardMeta = ({ title, location, startDate }: CardMetaProps) => { - const formattedStartDate = new Date(startDate).toLocaleDateString(); + const startDateObj = new Date(startDate); + const formattedStartDate = startDateObj.toLocaleDateString('ko-KR', { + month: 'long', + day: 'numeric', + weekday: 'long', + }); return ( From 0a31760f6fccdd0fa34bb12ac5935464db3e9d85 Mon Sep 17 00:00:00 2001 From: suhyun113 <163711629+suhyun113@users.noreply.github.com> Date: Thu, 5 Mar 2026 03:43:40 +0900 Subject: [PATCH 011/172] =?UTF-8?q?style:=20=EA=B8=B0=EB=B3=B8=20=EC=B9=B4?= =?UTF-8?q?=EB=93=9C=20=EC=8A=A4=ED=83=80=EC=9D=BC=20=EC=86=8D=EC=84=B1=20?= =?UTF-8?q?=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../PromotionCard/CardMeta/CardMeta.styles.ts | 5 ++--- .../PromotionCard/ClubTag/ClubTag.styles.ts | 2 +- .../components/PromotionCard/PromotionCard.styles.ts | 12 +++++++++++- .../components/PromotionGrid/PromotionGrid.styles.ts | 9 ++++++++- 4 files changed, 22 insertions(+), 6 deletions(-) diff --git a/frontend/src/pages/PromotionPage/components/PromotionCard/CardMeta/CardMeta.styles.ts b/frontend/src/pages/PromotionPage/components/PromotionCard/CardMeta/CardMeta.styles.ts index 8c81bf22c..d6bd1dc8c 100644 --- a/frontend/src/pages/PromotionPage/components/PromotionCard/CardMeta/CardMeta.styles.ts +++ b/frontend/src/pages/PromotionPage/components/PromotionCard/CardMeta/CardMeta.styles.ts @@ -8,7 +8,7 @@ export const Container = styled.div` export const Title = styled.h3` font-size: 14px; - font-weight: 700; + font-weight: 600; margin-bottom: 4px; `; @@ -30,9 +30,8 @@ export const Icon = styled.div` export const MetaText = styled.span` font-size: 12px; - font-weight: 600; + font-weight: 400; color: ${colors.gray[600]}; - white-space: nowrap; overflow: hidden; text-overflow: ellipsis; diff --git a/frontend/src/pages/PromotionPage/components/PromotionCard/ClubTag/ClubTag.styles.ts b/frontend/src/pages/PromotionPage/components/PromotionCard/ClubTag/ClubTag.styles.ts index a97c0569c..2c888f776 100644 --- a/frontend/src/pages/PromotionPage/components/PromotionCard/ClubTag/ClubTag.styles.ts +++ b/frontend/src/pages/PromotionPage/components/PromotionCard/ClubTag/ClubTag.styles.ts @@ -11,7 +11,7 @@ export const Container = styled.div` margin-top: 4px; `; -export const ClubText = styled.h1` +export const ClubText = styled.span` color: ${colors.gray[800]}; font-size: 12px; font-weight: 600; diff --git a/frontend/src/pages/PromotionPage/components/PromotionCard/PromotionCard.styles.ts b/frontend/src/pages/PromotionPage/components/PromotionCard/PromotionCard.styles.ts index 39faa8562..fe51d6519 100644 --- a/frontend/src/pages/PromotionPage/components/PromotionCard/PromotionCard.styles.ts +++ b/frontend/src/pages/PromotionPage/components/PromotionCard/PromotionCard.styles.ts @@ -1,17 +1,27 @@ import styled from 'styled-components'; import { colors } from '@/styles/theme/colors'; +import { media } from '@/styles/mediaQuery'; export const Container = styled.div` + width: 226px; border-radius: 14px; overflow: hidden; background: ${colors.base.white}; box-shadow: 0 4px 12px rgba(0, 0, 0, 0.08); + + ${media.mobile} { + width: 200px; + } + + ${media.mini_mobile} { + width: 164px; + } `; export const ImageWrapper = styled.div` position: relative; width: 100%; - height: 164px; // 수정 예정 + height: 164px; `; export const Image = styled.div<{ $imageUrl?: string }>` diff --git a/frontend/src/pages/PromotionPage/components/PromotionGrid/PromotionGrid.styles.ts b/frontend/src/pages/PromotionPage/components/PromotionGrid/PromotionGrid.styles.ts index 425b8fbff..cb389bf2b 100644 --- a/frontend/src/pages/PromotionPage/components/PromotionGrid/PromotionGrid.styles.ts +++ b/frontend/src/pages/PromotionPage/components/PromotionGrid/PromotionGrid.styles.ts @@ -1,7 +1,14 @@ +import { media } from '@/styles/mediaQuery'; import styled from 'styled-components'; export const Grid = styled.div` display: grid; + gap: 14px; + justify-items: center; + grid-template-columns: repeat(2, 1fr); - gap: 7px; + + ${media.mobile} { + gap: 7px; + } `; From 996f9acac6b5ec05f55632121b311007a6226459 Mon Sep 17 00:00:00 2001 From: suhyun113 <163711629+suhyun113@users.noreply.github.com> Date: Thu, 5 Mar 2026 04:07:18 +0900 Subject: [PATCH 012/172] =?UTF-8?q?style:=20Dday=20Glass=20=ED=9A=A8?= =?UTF-8?q?=EA=B3=BC=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../PromotionCard/DdayBadge/DdayBadge.styles.ts | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/frontend/src/pages/PromotionPage/components/PromotionCard/DdayBadge/DdayBadge.styles.ts b/frontend/src/pages/PromotionPage/components/PromotionCard/DdayBadge/DdayBadge.styles.ts index 19f8b5969..f528e7f7e 100644 --- a/frontend/src/pages/PromotionPage/components/PromotionCard/DdayBadge/DdayBadge.styles.ts +++ b/frontend/src/pages/PromotionPage/components/PromotionCard/DdayBadge/DdayBadge.styles.ts @@ -2,14 +2,24 @@ import { colors } from '@/styles/theme/colors'; import styled from 'styled-components'; export const Container = styled.div` - border-radius: 50px; - background-color: ${colors.base.white}; + display: inline-flex; + align-items: center; + justify-content: center; + padding: 4px 10px; - // Glass 효과는 이후 적용 예정 + border-radius: 50px; + + /* Glass 효과 */ + background: rgba(245, 245, 245, 0.6); + backdrop-filter: blur(12px); + box-shadow: + inset 0 1px 1px rgb(255, 255, 255), + 0 2px 8px rgba(0, 0, 0, 0.08); `; export const DdayText = styled.h1` color: ${colors.gray[800]}; font-size: 10px; font-weight: 600; + letter-spacing: -0.02em; `; \ No newline at end of file From ed8359cfbb9c5fe866a18d09098babe7b72fbef2 Mon Sep 17 00:00:00 2001 From: suhyun113 <163711629+suhyun113@users.noreply.github.com> Date: Thu, 5 Mar 2026 04:20:59 +0900 Subject: [PATCH 013/172] =?UTF-8?q?refactor:=20=ED=8C=8C=EC=9D=BC=20?= =?UTF-8?q?=EC=9C=84=EC=B9=98=20=EC=9D=B4=EB=8F=99?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/src/pages/PromotionPage/PromotionListPage.tsx | 2 +- .../{ => list}/PromotionCard/CardMeta/CardMeta.styles.ts | 0 .../components/{ => list}/PromotionCard/CardMeta/CardMeta.tsx | 0 .../{ => list}/PromotionCard/ClubTag/ClubTag.styles.ts | 0 .../components/{ => list}/PromotionCard/ClubTag/ClubTag.tsx | 0 .../{ => list}/PromotionCard/DdayBadge/DdayBadge.styles.ts | 0 .../components/{ => list}/PromotionCard/DdayBadge/DdayBadge.tsx | 0 .../components/{ => list}/PromotionCard/PromotionCard.styles.ts | 0 .../components/{ => list}/PromotionCard/PromotionCard.tsx | 2 +- .../components/{ => list}/PromotionGrid/PromotionGrid.styles.ts | 0 .../components/{ => list}/PromotionGrid/PromotionGrid.tsx | 0 11 files changed, 2 insertions(+), 2 deletions(-) rename frontend/src/pages/PromotionPage/components/{ => list}/PromotionCard/CardMeta/CardMeta.styles.ts (100%) rename frontend/src/pages/PromotionPage/components/{ => list}/PromotionCard/CardMeta/CardMeta.tsx (100%) rename frontend/src/pages/PromotionPage/components/{ => list}/PromotionCard/ClubTag/ClubTag.styles.ts (100%) rename frontend/src/pages/PromotionPage/components/{ => list}/PromotionCard/ClubTag/ClubTag.tsx (100%) rename frontend/src/pages/PromotionPage/components/{ => list}/PromotionCard/DdayBadge/DdayBadge.styles.ts (100%) rename frontend/src/pages/PromotionPage/components/{ => list}/PromotionCard/DdayBadge/DdayBadge.tsx (100%) rename frontend/src/pages/PromotionPage/components/{ => list}/PromotionCard/PromotionCard.styles.ts (100%) rename frontend/src/pages/PromotionPage/components/{ => list}/PromotionCard/PromotionCard.tsx (99%) rename frontend/src/pages/PromotionPage/components/{ => list}/PromotionGrid/PromotionGrid.styles.ts (100%) rename frontend/src/pages/PromotionPage/components/{ => list}/PromotionGrid/PromotionGrid.tsx (100%) diff --git a/frontend/src/pages/PromotionPage/PromotionListPage.tsx b/frontend/src/pages/PromotionPage/PromotionListPage.tsx index 88b7847a7..2575dca67 100644 --- a/frontend/src/pages/PromotionPage/PromotionListPage.tsx +++ b/frontend/src/pages/PromotionPage/PromotionListPage.tsx @@ -5,7 +5,7 @@ import useTrackPageView from '@/hooks/Mixpanel/useTrackPageView'; import { useGetPromotionArticles } from '@/hooks/Queries/usePromotion'; import isInAppWebView from '@/utils/isInAppWebView'; import Filter from '../MainPage/components/Filter/Filter'; -import PromottionGrid from './components/PromotionGrid/PromotionGrid'; +import PromottionGrid from './components/list/PromotionGrid/PromotionGrid'; import * as Styled from './PromotionListPage.styles'; const PromotionListPage = () => { diff --git a/frontend/src/pages/PromotionPage/components/PromotionCard/CardMeta/CardMeta.styles.ts b/frontend/src/pages/PromotionPage/components/list/PromotionCard/CardMeta/CardMeta.styles.ts similarity index 100% rename from frontend/src/pages/PromotionPage/components/PromotionCard/CardMeta/CardMeta.styles.ts rename to frontend/src/pages/PromotionPage/components/list/PromotionCard/CardMeta/CardMeta.styles.ts diff --git a/frontend/src/pages/PromotionPage/components/PromotionCard/CardMeta/CardMeta.tsx b/frontend/src/pages/PromotionPage/components/list/PromotionCard/CardMeta/CardMeta.tsx similarity index 100% rename from frontend/src/pages/PromotionPage/components/PromotionCard/CardMeta/CardMeta.tsx rename to frontend/src/pages/PromotionPage/components/list/PromotionCard/CardMeta/CardMeta.tsx diff --git a/frontend/src/pages/PromotionPage/components/PromotionCard/ClubTag/ClubTag.styles.ts b/frontend/src/pages/PromotionPage/components/list/PromotionCard/ClubTag/ClubTag.styles.ts similarity index 100% rename from frontend/src/pages/PromotionPage/components/PromotionCard/ClubTag/ClubTag.styles.ts rename to frontend/src/pages/PromotionPage/components/list/PromotionCard/ClubTag/ClubTag.styles.ts diff --git a/frontend/src/pages/PromotionPage/components/PromotionCard/ClubTag/ClubTag.tsx b/frontend/src/pages/PromotionPage/components/list/PromotionCard/ClubTag/ClubTag.tsx similarity index 100% rename from frontend/src/pages/PromotionPage/components/PromotionCard/ClubTag/ClubTag.tsx rename to frontend/src/pages/PromotionPage/components/list/PromotionCard/ClubTag/ClubTag.tsx diff --git a/frontend/src/pages/PromotionPage/components/PromotionCard/DdayBadge/DdayBadge.styles.ts b/frontend/src/pages/PromotionPage/components/list/PromotionCard/DdayBadge/DdayBadge.styles.ts similarity index 100% rename from frontend/src/pages/PromotionPage/components/PromotionCard/DdayBadge/DdayBadge.styles.ts rename to frontend/src/pages/PromotionPage/components/list/PromotionCard/DdayBadge/DdayBadge.styles.ts diff --git a/frontend/src/pages/PromotionPage/components/PromotionCard/DdayBadge/DdayBadge.tsx b/frontend/src/pages/PromotionPage/components/list/PromotionCard/DdayBadge/DdayBadge.tsx similarity index 100% rename from frontend/src/pages/PromotionPage/components/PromotionCard/DdayBadge/DdayBadge.tsx rename to frontend/src/pages/PromotionPage/components/list/PromotionCard/DdayBadge/DdayBadge.tsx diff --git a/frontend/src/pages/PromotionPage/components/PromotionCard/PromotionCard.styles.ts b/frontend/src/pages/PromotionPage/components/list/PromotionCard/PromotionCard.styles.ts similarity index 100% rename from frontend/src/pages/PromotionPage/components/PromotionCard/PromotionCard.styles.ts rename to frontend/src/pages/PromotionPage/components/list/PromotionCard/PromotionCard.styles.ts diff --git a/frontend/src/pages/PromotionPage/components/PromotionCard/PromotionCard.tsx b/frontend/src/pages/PromotionPage/components/list/PromotionCard/PromotionCard.tsx similarity index 99% rename from frontend/src/pages/PromotionPage/components/PromotionCard/PromotionCard.tsx rename to frontend/src/pages/PromotionPage/components/list/PromotionCard/PromotionCard.tsx index 4365a11ab..53c175f4a 100644 --- a/frontend/src/pages/PromotionPage/components/PromotionCard/PromotionCard.tsx +++ b/frontend/src/pages/PromotionPage/components/list/PromotionCard/PromotionCard.tsx @@ -10,7 +10,7 @@ interface PromotionCardProps { const PromotionCard = ({ article }: PromotionCardProps) => { const imageUrl = article.images?.[0]; - + return ( diff --git a/frontend/src/pages/PromotionPage/components/PromotionGrid/PromotionGrid.styles.ts b/frontend/src/pages/PromotionPage/components/list/PromotionGrid/PromotionGrid.styles.ts similarity index 100% rename from frontend/src/pages/PromotionPage/components/PromotionGrid/PromotionGrid.styles.ts rename to frontend/src/pages/PromotionPage/components/list/PromotionGrid/PromotionGrid.styles.ts diff --git a/frontend/src/pages/PromotionPage/components/PromotionGrid/PromotionGrid.tsx b/frontend/src/pages/PromotionPage/components/list/PromotionGrid/PromotionGrid.tsx similarity index 100% rename from frontend/src/pages/PromotionPage/components/PromotionGrid/PromotionGrid.tsx rename to frontend/src/pages/PromotionPage/components/list/PromotionGrid/PromotionGrid.tsx From 1224c3d4458731b6945b283b1b3e82edf84d2d0d Mon Sep 17 00:00:00 2001 From: suhyun113 <163711629+suhyun113@users.noreply.github.com> Date: Thu, 5 Mar 2026 05:15:08 +0900 Subject: [PATCH 014/172] =?UTF-8?q?feat:=20=EC=9D=B4=EB=B2=A4=ED=8A=B8=20?= =?UTF-8?q?=EC=83=81=EC=84=B8=20=ED=8E=98=EC=9D=B4=EC=A7=80=20=EC=9D=B4?= =?UTF-8?q?=EB=8F=99=20=EB=B0=8F=20=EC=9D=B4=EB=B2=A4=ED=8A=B8=20=EC=A0=95?= =?UTF-8?q?=EB=B3=B4=20=ED=83=91=EB=B0=94=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../PromotionPage/PromotionDetailPage.tsx | 14 ++++--- .../PromotionDetailTopBar.styles.ts | 40 +++++++++++++++++++ .../PromotionDetailTopBar.tsx | 28 +++++++++++++ .../PromotionCard/PromotionCard.styles.ts | 1 + .../list/PromotionCard/PromotionCard.tsx | 9 ++++- 5 files changed, 86 insertions(+), 6 deletions(-) create mode 100644 frontend/src/pages/PromotionPage/components/detail/PromotionDetailTopBar/PromotionDetailTopBar.styles.ts create mode 100644 frontend/src/pages/PromotionPage/components/detail/PromotionDetailTopBar/PromotionDetailTopBar.tsx diff --git a/frontend/src/pages/PromotionPage/PromotionDetailPage.tsx b/frontend/src/pages/PromotionPage/PromotionDetailPage.tsx index f3b09d92a..51e6be213 100644 --- a/frontend/src/pages/PromotionPage/PromotionDetailPage.tsx +++ b/frontend/src/pages/PromotionPage/PromotionDetailPage.tsx @@ -1,17 +1,21 @@ import { useParams } from 'react-router-dom'; import { PAGE_VIEW } from '@/constants/eventName'; import useTrackPageView from '@/hooks/Mixpanel/useTrackPageView'; +import PromotionDetailTopBar from './components/detail/PromotionDetailTopBar/PromotionDetailTopBar'; const PromotionDetailPage = () => { useTrackPageView(PAGE_VIEW.PROMOTION_DETAIL_PAGE); const { promotionId } = useParams(); return ( -
-

홍보 상세 페이지

-

홍보 ID: {promotionId}

-

여기에 홍보 상세 정보 들어올 예정

-
+ <> + +
+

홍보 상세 페이지

+

홍보 ID: {promotionId}

+

여기에 홍보 상세 정보 들어올 예정

+
+ ); }; diff --git a/frontend/src/pages/PromotionPage/components/detail/PromotionDetailTopBar/PromotionDetailTopBar.styles.ts b/frontend/src/pages/PromotionPage/components/detail/PromotionDetailTopBar/PromotionDetailTopBar.styles.ts new file mode 100644 index 000000000..c140bfd4d --- /dev/null +++ b/frontend/src/pages/PromotionPage/components/detail/PromotionDetailTopBar/PromotionDetailTopBar.styles.ts @@ -0,0 +1,40 @@ +import styled from 'styled-components'; +import { colors } from '@/styles/theme/colors'; + +export const Container = styled.header` + position: relative; + height: 48px; + background: ${colors.base.white}; + border-bottom: 1px solid ${colors.gray[300]}; + display: flex; + align-items: center; + padding: 0 18px; +`; + +export const BackButton = styled.button` + width: 36px; + height: 36px; + + display: flex; + align-items: center; + justify-content: center; + + background: none; + border: none; + cursor: pointer; + border-radius: 8px; + + &:active { + background: ${colors.gray[100]}; + } +`; + +export const Title = styled.h1` + position: absolute; + left: 50%; + transform: translateX(-50%); + + font-size: 20px; + font-weight: 700; + color: ${colors.gray[900]}; +`; diff --git a/frontend/src/pages/PromotionPage/components/detail/PromotionDetailTopBar/PromotionDetailTopBar.tsx b/frontend/src/pages/PromotionPage/components/detail/PromotionDetailTopBar/PromotionDetailTopBar.tsx new file mode 100644 index 000000000..bc6ec641a --- /dev/null +++ b/frontend/src/pages/PromotionPage/components/detail/PromotionDetailTopBar/PromotionDetailTopBar.tsx @@ -0,0 +1,28 @@ +import { useNavigate } from 'react-router-dom'; +import PrevButtonIcon from '@/assets/images/icons/prev_button_icon.svg?react'; +import * as Styled from './PromotionDetailTopBar.styles'; + +const PromotionDetailTopBar = () => { + const navigate = useNavigate(); + + const handleBackClick = () => { + if (window.history.state && window.history.state.idx > 0) { + navigate(-1); + } else { + navigate('/', { replace: true }); + } + }; + + return ( + + + + + + 이벤트 정보 + + + ); +}; + +export default PromotionDetailTopBar; \ No newline at end of file diff --git a/frontend/src/pages/PromotionPage/components/list/PromotionCard/PromotionCard.styles.ts b/frontend/src/pages/PromotionPage/components/list/PromotionCard/PromotionCard.styles.ts index fe51d6519..fbffdad51 100644 --- a/frontend/src/pages/PromotionPage/components/list/PromotionCard/PromotionCard.styles.ts +++ b/frontend/src/pages/PromotionPage/components/list/PromotionCard/PromotionCard.styles.ts @@ -8,6 +8,7 @@ export const Container = styled.div` overflow: hidden; background: ${colors.base.white}; box-shadow: 0 4px 12px rgba(0, 0, 0, 0.08); + cursor: pointer; ${media.mobile} { width: 200px; diff --git a/frontend/src/pages/PromotionPage/components/list/PromotionCard/PromotionCard.tsx b/frontend/src/pages/PromotionPage/components/list/PromotionCard/PromotionCard.tsx index 53c175f4a..effde5b40 100644 --- a/frontend/src/pages/PromotionPage/components/list/PromotionCard/PromotionCard.tsx +++ b/frontend/src/pages/PromotionPage/components/list/PromotionCard/PromotionCard.tsx @@ -3,16 +3,23 @@ import CardMeta from './CardMeta/CardMeta'; import ClubTag from './ClubTag/ClubTag'; import DdayBadge from './DdayBadge/DdayBadge'; import * as Styled from './PromotionCard.styles'; +import { useNavigate } from 'react-router-dom'; interface PromotionCardProps { article: PromotionArticle; } const PromotionCard = ({ article }: PromotionCardProps) => { + const navigateToPromotionDetail = useNavigate(); + + const handleCardClick = () => { + navigateToPromotionDetail(`/promotions/${article.clubId}`); + }; + const imageUrl = article.images?.[0]; return ( - + From 6a5d17ba64add01091f4229b0dab4e3b7fa28c5b Mon Sep 17 00:00:00 2001 From: suhyun113 <163711629+suhyun113@users.noreply.github.com> Date: Thu, 5 Mar 2026 09:16:41 +0900 Subject: [PATCH 015/172] =?UTF-8?q?feat:=20=EC=9D=B4=EB=B2=A4=ED=8A=B8=20?= =?UTF-8?q?=EC=A0=95=EB=B3=B4=20=ED=8E=98=EC=9D=B4=EC=A7=80=20=EC=97=B0?= =?UTF-8?q?=EB=8F=99=20=EB=B0=8F=20=EB=A0=88=EC=9D=B4=EC=95=84=EC=9B=83=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../PromotionDetailPage.styles.ts | 11 ++++ .../PromotionPage/PromotionDetailPage.tsx | 40 +++++++++++--- .../PromotionClubCTA.styles.ts | 27 +++++++++ .../PromotionClubCTA/PromotionClubCTA.tsx | 26 +++++++++ .../PromotionImageGallery.styles.ts | 44 +++++++++++++++ .../PromotionImageGallery.tsx | 47 ++++++++++++++++ .../PromotionInfoSection.styles.ts | 37 +++++++++++++ .../PromotionInfoSection.tsx | 55 +++++++++++++++++++ .../PromotionTitleSection.styles.ts | 23 ++++++++ .../PromotionTitleSection.tsx | 23 ++++++++ .../RelatedPromotionSection.styles.ts | 29 ++++++++++ .../RelatedPromotionSection.tsx | 24 ++++++++ 12 files changed, 378 insertions(+), 8 deletions(-) create mode 100644 frontend/src/pages/PromotionPage/PromotionDetailPage.styles.ts create mode 100644 frontend/src/pages/PromotionPage/components/detail/PromotionClubCTA/PromotionClubCTA.styles.ts create mode 100644 frontend/src/pages/PromotionPage/components/detail/PromotionClubCTA/PromotionClubCTA.tsx create mode 100644 frontend/src/pages/PromotionPage/components/detail/PromotionImageGallery/PromotionImageGallery.styles.ts create mode 100644 frontend/src/pages/PromotionPage/components/detail/PromotionImageGallery/PromotionImageGallery.tsx create mode 100644 frontend/src/pages/PromotionPage/components/detail/PromotionInfoSection/PromotionInfoSection.styles.ts create mode 100644 frontend/src/pages/PromotionPage/components/detail/PromotionInfoSection/PromotionInfoSection.tsx create mode 100644 frontend/src/pages/PromotionPage/components/detail/PromotionTitleSection/PromotionTitleSection.styles.ts create mode 100644 frontend/src/pages/PromotionPage/components/detail/PromotionTitleSection/PromotionTitleSection.tsx create mode 100644 frontend/src/pages/PromotionPage/components/detail/RelatedPromotionSection/RelatedPromotionSection.styles.ts create mode 100644 frontend/src/pages/PromotionPage/components/detail/RelatedPromotionSection/RelatedPromotionSection.tsx diff --git a/frontend/src/pages/PromotionPage/PromotionDetailPage.styles.ts b/frontend/src/pages/PromotionPage/PromotionDetailPage.styles.ts new file mode 100644 index 000000000..8e0d619da --- /dev/null +++ b/frontend/src/pages/PromotionPage/PromotionDetailPage.styles.ts @@ -0,0 +1,11 @@ +import styled from 'styled-components'; + +export const Container = styled.div` + min-height: 100vh; + background: #fff; +`; + +export const Message = styled.p` + padding: 40px 18px; + text-align: center; +`; \ No newline at end of file diff --git a/frontend/src/pages/PromotionPage/PromotionDetailPage.tsx b/frontend/src/pages/PromotionPage/PromotionDetailPage.tsx index 51e6be213..6229ab7af 100644 --- a/frontend/src/pages/PromotionPage/PromotionDetailPage.tsx +++ b/frontend/src/pages/PromotionPage/PromotionDetailPage.tsx @@ -1,21 +1,45 @@ import { useParams } from 'react-router-dom'; import { PAGE_VIEW } from '@/constants/eventName'; import useTrackPageView from '@/hooks/Mixpanel/useTrackPageView'; +import { useGetPromotionArticles } from '@/hooks/Queries/usePromotion'; +import PromotionClubCTA from './components/detail/PromotionClubCTA/PromotionClubCTA'; import PromotionDetailTopBar from './components/detail/PromotionDetailTopBar/PromotionDetailTopBar'; +import PromotionImageGallery from './components/detail/PromotionImageGallery/PromotionImageGallery'; +import PromotionInfoSection from './components/detail/PromotionInfoSection/PromotionInfoSection'; +import PromotionTitleSection from './components/detail/PromotionTitleSection/PromotionTitleSection'; +import RelatedPromotionSection from './components/detail/RelatedPromotionSection/RelatedPromotionSection'; +import * as Styled from './PromotionDetailPage.styles'; const PromotionDetailPage = () => { useTrackPageView(PAGE_VIEW.PROMOTION_DETAIL_PAGE); - const { promotionId } = useParams(); + + const { promotionId } = useParams<{ promotionId: string }>(); + const { data, isLoading, isError } = useGetPromotionArticles(); + + const article = + data?.find((item) => item.clubId === promotionId) ?? null; return ( - <> + -
-

홍보 상세 페이지

-

홍보 ID: {promotionId}

-

여기에 홍보 상세 정보 들어올 예정

-
- + + {isLoading && 로딩 중...} + {isError && 오류가 발생했습니다.} + + {!isLoading && !isError && !article && ( + 존재하지 않는 이벤트입니다. + )} + + {!isLoading && !isError && article && ( + <> + + + + + + + )} +
); }; diff --git a/frontend/src/pages/PromotionPage/components/detail/PromotionClubCTA/PromotionClubCTA.styles.ts b/frontend/src/pages/PromotionPage/components/detail/PromotionClubCTA/PromotionClubCTA.styles.ts new file mode 100644 index 000000000..c6d5314b6 --- /dev/null +++ b/frontend/src/pages/PromotionPage/components/detail/PromotionClubCTA/PromotionClubCTA.styles.ts @@ -0,0 +1,27 @@ +import styled from 'styled-components'; +import { colors } from '@/styles/theme/colors'; + +export const Container = styled.section` + padding: 24px 18px; +`; + +export const Question = styled.div` + font-size: 15px; + font-weight: 600; + margin-bottom: 12px; +`; + +export const Button = styled.button` + width: 100%; + padding: 14px; + border-radius: 12px; + + background: ${colors.gray[200]}; + border: none; + cursor: pointer; + font-weight: 600; + + &:active { + background: ${colors.gray[300]}; + } +`; \ No newline at end of file diff --git a/frontend/src/pages/PromotionPage/components/detail/PromotionClubCTA/PromotionClubCTA.tsx b/frontend/src/pages/PromotionPage/components/detail/PromotionClubCTA/PromotionClubCTA.tsx new file mode 100644 index 000000000..d08df0b27 --- /dev/null +++ b/frontend/src/pages/PromotionPage/components/detail/PromotionClubCTA/PromotionClubCTA.tsx @@ -0,0 +1,26 @@ +import { useNavigate } from 'react-router-dom'; +import * as Styled from './PromotionClubCTA.styles'; + +interface Props { + clubId: string; +} + +const PromotionClubCTA = ({ clubId }: Props) => { + const navigate = useNavigate(); + + return ( + + + 동아리 정보가 궁금하다면? + + + navigate(`/club/${clubId}`)} + > + 동아리 정보 보러가기 → + + + ); +}; + +export default PromotionClubCTA; \ No newline at end of file diff --git a/frontend/src/pages/PromotionPage/components/detail/PromotionImageGallery/PromotionImageGallery.styles.ts b/frontend/src/pages/PromotionPage/components/detail/PromotionImageGallery/PromotionImageGallery.styles.ts new file mode 100644 index 000000000..11c64dba2 --- /dev/null +++ b/frontend/src/pages/PromotionPage/components/detail/PromotionImageGallery/PromotionImageGallery.styles.ts @@ -0,0 +1,44 @@ +import styled from 'styled-components'; +import { colors } from '@/styles/theme/colors'; + +export const Wrapper = styled.section` + padding: 0 18px; +`; + +export const ImageContainer = styled.div<{ $expanded: boolean }>` + position: relative; + overflow: hidden; + + max-height: ${({ $expanded }) => + $expanded ? 'none' : '700px'}; +`; + +export const Image = styled.img` + width: 100%; + border-radius: 12px; + margin-bottom: 12px; +`; + +export const Gradient = styled.div` + position: absolute; + bottom: 0; + left: 0; + right: 0; + height: 120px; + + background: linear-gradient( + to bottom, + rgba(255,255,255,0) 0%, + rgba(255,255,255,1) 100% + ); +`; + +export const MoreButton = styled.button` + width: 100%; + margin-top: 12px; /* 이미지와 12px */ + padding: 12px; + border-radius: 10px; + background: ${colors.gray[200]}; + border: none; + cursor: pointer; +`; \ No newline at end of file diff --git a/frontend/src/pages/PromotionPage/components/detail/PromotionImageGallery/PromotionImageGallery.tsx b/frontend/src/pages/PromotionPage/components/detail/PromotionImageGallery/PromotionImageGallery.tsx new file mode 100644 index 000000000..0e326ba02 --- /dev/null +++ b/frontend/src/pages/PromotionPage/components/detail/PromotionImageGallery/PromotionImageGallery.tsx @@ -0,0 +1,47 @@ +import { useRef, useState, useEffect } from 'react'; +import * as Styled from './PromotionImageGallery.styles'; + +interface PromotionImageGalleryProps { + images: string[]; +} + +const MAX_HEIGHT = 700; + +const PromotionImageGallery = ({ images }: PromotionImageGalleryProps) => { + const containerRef = useRef(null); + const [expanded, setExpanded] = useState(false); + const [showButton, setShowButton] = useState(false); + + useEffect(() => { + if (containerRef.current) { + if (containerRef.current.scrollHeight > MAX_HEIGHT) { + setShowButton(true); + } + } + }, []); + + return ( + + + {images.map((src, idx) => ( + + ))} + + {!expanded && showButton && } + + + {showButton && ( + setExpanded(!expanded)} + > + {expanded ? '접기 ▲' : '이미지 더보기 ▼'} + + )} + + ); +}; + +export default PromotionImageGallery; \ No newline at end of file diff --git a/frontend/src/pages/PromotionPage/components/detail/PromotionInfoSection/PromotionInfoSection.styles.ts b/frontend/src/pages/PromotionPage/components/detail/PromotionInfoSection/PromotionInfoSection.styles.ts new file mode 100644 index 000000000..c36b9b3b9 --- /dev/null +++ b/frontend/src/pages/PromotionPage/components/detail/PromotionInfoSection/PromotionInfoSection.styles.ts @@ -0,0 +1,37 @@ +import styled from 'styled-components'; +import { colors } from '@/styles/theme/colors'; + +export const Container = styled.section` + padding: 20px 18px; +`; + +export const SectionTitle = styled.h3` + font-size: 16px; + font-weight: 700; + margin-bottom: 12px; +`; + +export const Card = styled.div` + background: ${colors.gray[100]}; + border-radius: 14px; + padding: 16px; +`; + +export const Item = styled.div` + margin-bottom: 16px; + + &:last-child { + margin-bottom: 0; + } +`; + +export const Label = styled.div` + font-weight: 600; + margin-bottom: 6px; +`; + +export const Value = styled.div` + font-size: 14px; + color: ${colors.gray[800]}; + line-height: 1.6; +`; \ No newline at end of file diff --git a/frontend/src/pages/PromotionPage/components/detail/PromotionInfoSection/PromotionInfoSection.tsx b/frontend/src/pages/PromotionPage/components/detail/PromotionInfoSection/PromotionInfoSection.tsx new file mode 100644 index 000000000..5444a6454 --- /dev/null +++ b/frontend/src/pages/PromotionPage/components/detail/PromotionInfoSection/PromotionInfoSection.tsx @@ -0,0 +1,55 @@ +import { PromotionArticle } from '@/types/promotion'; +import * as Styled from './PromotionInfoSection.styles'; + +interface Props { + article: PromotionArticle; +} + +const formatDate = (dateStr: string) => { + const date = new Date(dateStr); + return date.toLocaleString('ko-KR', { + month: 'long', + day: 'numeric', + hour: '2-digit', + minute: '2-digit', + }); +}; + +const PromotionInfoSection = ({ article }: Props) => { + return ( + + 상세정보 + + + + 📅 일시 + + {formatDate(article.eventStartDate)} -{' '} + {formatDate(article.eventEndDate)} + + + + {article.location && ( + + 📍 장소 + {article.location} + + )} + + + 🎉 프로그램 안내 + + {article.description.split('\n').map((line, i) => ( + + {line} +
+
+ ))} +
+
+
+
+ ); +}; + +export default PromotionInfoSection; \ No newline at end of file diff --git a/frontend/src/pages/PromotionPage/components/detail/PromotionTitleSection/PromotionTitleSection.styles.ts b/frontend/src/pages/PromotionPage/components/detail/PromotionTitleSection/PromotionTitleSection.styles.ts new file mode 100644 index 000000000..890660c30 --- /dev/null +++ b/frontend/src/pages/PromotionPage/components/detail/PromotionTitleSection/PromotionTitleSection.styles.ts @@ -0,0 +1,23 @@ +import styled from 'styled-components'; +import { colors } from '@/styles/theme/colors'; + +export const Container = styled.section` + padding: 16px 18px 8px 18px; + + display: flex; + flex-direction: column; + align-items: center; + text-align: center; +`; + +export const TagWrapper = styled.div` + margin-bottom: 10px; +`; + +export const Title = styled.h2` + font-size: 18px; + font-weight: 700; + color: ${colors.gray[900]}; + line-height: 1.4; + word-break: keep-all; +`; \ No newline at end of file diff --git a/frontend/src/pages/PromotionPage/components/detail/PromotionTitleSection/PromotionTitleSection.tsx b/frontend/src/pages/PromotionPage/components/detail/PromotionTitleSection/PromotionTitleSection.tsx new file mode 100644 index 000000000..f35df692d --- /dev/null +++ b/frontend/src/pages/PromotionPage/components/detail/PromotionTitleSection/PromotionTitleSection.tsx @@ -0,0 +1,23 @@ +import { PromotionArticle } from '@/types/promotion'; +import ClubTag from '../../list/PromotionCard/ClubTag/ClubTag'; +import * as Styled from './PromotionTitleSection.styles'; + +interface Props { + article: PromotionArticle; +} + +const PromotionTitleSection = ({ article }: Props) => { + return ( + + + + + + + {article.title} + + + ); +}; + +export default PromotionTitleSection; \ No newline at end of file diff --git a/frontend/src/pages/PromotionPage/components/detail/RelatedPromotionSection/RelatedPromotionSection.styles.ts b/frontend/src/pages/PromotionPage/components/detail/RelatedPromotionSection/RelatedPromotionSection.styles.ts new file mode 100644 index 000000000..a933392e0 --- /dev/null +++ b/frontend/src/pages/PromotionPage/components/detail/RelatedPromotionSection/RelatedPromotionSection.styles.ts @@ -0,0 +1,29 @@ +import styled from 'styled-components'; +import { colors } from '@/styles/theme/colors'; + +export const Container = styled.section` + padding: 24px 18px 60px 18px; +`; + +export const Title = styled.h3` + font-size: 16px; + font-weight: 700; + margin-bottom: 12px; +`; + +export const Card = styled.div` + background: ${colors.base.white}; + border-radius: 14px; + padding: 16px; + box-shadow: 0 4px 12px rgba(0,0,0,0.05); +`; + +export const CardTitle = styled.div` + font-weight: 700; + margin-bottom: 6px; +`; + +export const CardDesc = styled.div` + font-size: 14px; + color: ${colors.gray[600]}; +`; \ No newline at end of file diff --git a/frontend/src/pages/PromotionPage/components/detail/RelatedPromotionSection/RelatedPromotionSection.tsx b/frontend/src/pages/PromotionPage/components/detail/RelatedPromotionSection/RelatedPromotionSection.tsx new file mode 100644 index 000000000..32ae0f551 --- /dev/null +++ b/frontend/src/pages/PromotionPage/components/detail/RelatedPromotionSection/RelatedPromotionSection.tsx @@ -0,0 +1,24 @@ +import * as Styled from './RelatedPromotionSection.styles'; + +interface Props { + currentClubId: string; +} + +const RelatedPromotionSection = ({ currentClubId }: Props) => { + return ( + + 이런 이벤트는 어때요? + + + + 다른 동아리 행사 예시 + + + 관련 이벤트 영역 (추후 API 연동) + + + + ); +}; + +export default RelatedPromotionSection; \ No newline at end of file From 25adc9f1a457be92f2cf750ca148c39e95a3b670 Mon Sep 17 00:00:00 2001 From: suhyun113 <163711629+suhyun113@users.noreply.github.com> Date: Thu, 5 Mar 2026 09:27:27 +0900 Subject: [PATCH 016/172] =?UTF-8?q?style:=20clubTag=20margin=20=EC=86=8D?= =?UTF-8?q?=EC=84=B1=20=EB=B6=84=EB=A6=AC=20=EB=B0=8F=20TitleSection=20?= =?UTF-8?q?=EC=8A=A4=ED=83=80=EC=9D=BC=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../PromotionTitleSection.styles.ts | 9 ++++----- .../list/PromotionCard/ClubTag/ClubTag.styles.ts | 1 - .../list/PromotionCard/PromotionCard.styles.ts | 4 ++++ .../components/list/PromotionCard/PromotionCard.tsx | 8 +++++--- 4 files changed, 13 insertions(+), 9 deletions(-) diff --git a/frontend/src/pages/PromotionPage/components/detail/PromotionTitleSection/PromotionTitleSection.styles.ts b/frontend/src/pages/PromotionPage/components/detail/PromotionTitleSection/PromotionTitleSection.styles.ts index 890660c30..4b8b98c81 100644 --- a/frontend/src/pages/PromotionPage/components/detail/PromotionTitleSection/PromotionTitleSection.styles.ts +++ b/frontend/src/pages/PromotionPage/components/detail/PromotionTitleSection/PromotionTitleSection.styles.ts @@ -2,7 +2,7 @@ import styled from 'styled-components'; import { colors } from '@/styles/theme/colors'; export const Container = styled.section` - padding: 16px 18px 8px 18px; + padding: 20px 21.5px 20px 21.5px; display: flex; flex-direction: column; @@ -11,13 +11,12 @@ export const Container = styled.section` `; export const TagWrapper = styled.div` - margin-bottom: 10px; + margin-bottom: 8px; `; export const Title = styled.h2` - font-size: 18px; + font-size: 24px; font-weight: 700; - color: ${colors.gray[900]}; - line-height: 1.4; + color: ${colors.gray[800]}; word-break: keep-all; `; \ No newline at end of file diff --git a/frontend/src/pages/PromotionPage/components/list/PromotionCard/ClubTag/ClubTag.styles.ts b/frontend/src/pages/PromotionPage/components/list/PromotionCard/ClubTag/ClubTag.styles.ts index 2c888f776..81b7f016c 100644 --- a/frontend/src/pages/PromotionPage/components/list/PromotionCard/ClubTag/ClubTag.styles.ts +++ b/frontend/src/pages/PromotionPage/components/list/PromotionCard/ClubTag/ClubTag.styles.ts @@ -8,7 +8,6 @@ export const Container = styled.div` border-radius: 8px; background-color: ${colors.gray[100]}; padding: 4px 8px; - margin-top: 4px; `; export const ClubText = styled.span` diff --git a/frontend/src/pages/PromotionPage/components/list/PromotionCard/PromotionCard.styles.ts b/frontend/src/pages/PromotionPage/components/list/PromotionCard/PromotionCard.styles.ts index fbffdad51..b757e5d22 100644 --- a/frontend/src/pages/PromotionPage/components/list/PromotionCard/PromotionCard.styles.ts +++ b/frontend/src/pages/PromotionPage/components/list/PromotionCard/PromotionCard.styles.ts @@ -45,3 +45,7 @@ export const DdayWrapper = styled.div` export const Content = styled.div` padding: 10px; `; + +export const TagWrapper = styled.div` + margin-top: 4px; +`; diff --git a/frontend/src/pages/PromotionPage/components/list/PromotionCard/PromotionCard.tsx b/frontend/src/pages/PromotionPage/components/list/PromotionCard/PromotionCard.tsx index effde5b40..53ab252db 100644 --- a/frontend/src/pages/PromotionPage/components/list/PromotionCard/PromotionCard.tsx +++ b/frontend/src/pages/PromotionPage/components/list/PromotionCard/PromotionCard.tsx @@ -1,9 +1,9 @@ +import { useNavigate } from 'react-router-dom'; import { PromotionArticle } from '@/types/promotion'; import CardMeta from './CardMeta/CardMeta'; import ClubTag from './ClubTag/ClubTag'; import DdayBadge from './DdayBadge/DdayBadge'; import * as Styled from './PromotionCard.styles'; -import { useNavigate } from 'react-router-dom'; interface PromotionCardProps { article: PromotionArticle; @@ -32,8 +32,10 @@ const PromotionCard = ({ article }: PromotionCardProps) => { title={article.title} location={article.location} startDate={article.eventStartDate} - /> - + /> + + +
); From fa2b62b563aab4767110e287f144ca8b62752526 Mon Sep 17 00:00:00 2001 From: suhyun113 <163711629+suhyun113@users.noreply.github.com> Date: Thu, 5 Mar 2026 09:53:46 +0900 Subject: [PATCH 017/172] =?UTF-8?q?feat:=20=EC=9D=B4=EB=AF=B8=EC=A7=80?= =?UTF-8?q?=EB=8D=94=EB=B3=B4=EA=B8=B0=20=EB=B2=84=ED=8A=BC=20=EC=BB=B4?= =?UTF-8?q?=ED=8F=AC=EB=84=8C=ED=8A=B8=20=EB=B6=84=EB=A6=AC=20=EB=B0=8F=20?= =?UTF-8?q?=EB=B9=84=EB=8F=99=EA=B8=B0=20=EB=A1=9C=EB=94=A9=20=EC=8B=9C?= =?UTF-8?q?=EC=A0=90=20height=20=EC=B8=A1=EC=A0=95=20=EC=8B=A4=ED=8C=A8=20?= =?UTF-8?q?=EB=AC=B8=EC=A0=9C=20ResizeObserver=EB=A1=9C=20=ED=95=B4?= =?UTF-8?q?=EA=B2=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ImageMoreButton/ImageMoreButton.styles.ts | 17 +++++++++ .../ImageMoreButton/ImageMoreButton.tsx | 16 +++++++++ .../PromotionImageGallery.styles.ts | 25 +++++++------ .../PromotionImageGallery.tsx | 36 +++++++++++++------ 4 files changed, 73 insertions(+), 21 deletions(-) create mode 100644 frontend/src/pages/PromotionPage/components/detail/PromotionImageGallery/ImageMoreButton/ImageMoreButton.styles.ts create mode 100644 frontend/src/pages/PromotionPage/components/detail/PromotionImageGallery/ImageMoreButton/ImageMoreButton.tsx diff --git a/frontend/src/pages/PromotionPage/components/detail/PromotionImageGallery/ImageMoreButton/ImageMoreButton.styles.ts b/frontend/src/pages/PromotionPage/components/detail/PromotionImageGallery/ImageMoreButton/ImageMoreButton.styles.ts new file mode 100644 index 000000000..8ad2d3715 --- /dev/null +++ b/frontend/src/pages/PromotionPage/components/detail/PromotionImageGallery/ImageMoreButton/ImageMoreButton.styles.ts @@ -0,0 +1,17 @@ +import styled from 'styled-components'; +import { colors } from '@/styles/theme/colors'; + +export const Button = styled.button` + width: 100%; + margin-top: 12px; + padding: 14px; + border-radius: 12px; + background: ${colors.gray[200]}; + border: none; + cursor: pointer; + font-weight: 600; + + &:hover { + background: ${colors.gray[300]}; + } +`; \ No newline at end of file diff --git a/frontend/src/pages/PromotionPage/components/detail/PromotionImageGallery/ImageMoreButton/ImageMoreButton.tsx b/frontend/src/pages/PromotionPage/components/detail/PromotionImageGallery/ImageMoreButton/ImageMoreButton.tsx new file mode 100644 index 000000000..ad80c021f --- /dev/null +++ b/frontend/src/pages/PromotionPage/components/detail/PromotionImageGallery/ImageMoreButton/ImageMoreButton.tsx @@ -0,0 +1,16 @@ +import * as Styled from './ImageMoreButton.styles'; + +interface Props { + expanded: boolean; + onClick: () => void; +} + +const ImageMoreButton = ({ expanded, onClick }: Props) => { + return ( + + {expanded ? '접기 ▲' : '이미지 더보기 ▼'} + + ); +}; + +export default ImageMoreButton; \ No newline at end of file diff --git a/frontend/src/pages/PromotionPage/components/detail/PromotionImageGallery/PromotionImageGallery.styles.ts b/frontend/src/pages/PromotionPage/components/detail/PromotionImageGallery/PromotionImageGallery.styles.ts index 11c64dba2..55dc170e4 100644 --- a/frontend/src/pages/PromotionPage/components/detail/PromotionImageGallery/PromotionImageGallery.styles.ts +++ b/frontend/src/pages/PromotionPage/components/detail/PromotionImageGallery/PromotionImageGallery.styles.ts @@ -2,21 +2,20 @@ import styled from 'styled-components'; import { colors } from '@/styles/theme/colors'; export const Wrapper = styled.section` - padding: 0 18px; + margin: 0; `; export const ImageContainer = styled.div<{ $expanded: boolean }>` position: relative; overflow: hidden; - max-height: ${({ $expanded }) => - $expanded ? 'none' : '700px'}; + max-height: ${({ $expanded }) => ($expanded ? 'none' : '700px')}; `; export const Image = styled.img` width: 100%; - border-radius: 12px; - margin-bottom: 12px; + display: block; + margin-bottom: 2px; `; export const Gradient = styled.div` @@ -24,21 +23,27 @@ export const Gradient = styled.div` bottom: 0; left: 0; right: 0; - height: 120px; + height: 140px; + + backdrop-filter: blur(6px); + -webkit-backdrop-filter: blur(6px); background: linear-gradient( to bottom, - rgba(255,255,255,0) 0%, - rgba(255,255,255,1) 100% + rgba(255, 255, 255, 0) 0%, + rgba(255, 255, 255, 0.7) 60%, + rgba(255, 255, 255, 1) 100% ); + + pointer-events: none; `; export const MoreButton = styled.button` width: 100%; - margin-top: 12px; /* 이미지와 12px */ + margin-top: 12px; padding: 12px; border-radius: 10px; background: ${colors.gray[200]}; border: none; cursor: pointer; -`; \ No newline at end of file +`; diff --git a/frontend/src/pages/PromotionPage/components/detail/PromotionImageGallery/PromotionImageGallery.tsx b/frontend/src/pages/PromotionPage/components/detail/PromotionImageGallery/PromotionImageGallery.tsx index 0e326ba02..cbcf27417 100644 --- a/frontend/src/pages/PromotionPage/components/detail/PromotionImageGallery/PromotionImageGallery.tsx +++ b/frontend/src/pages/PromotionPage/components/detail/PromotionImageGallery/PromotionImageGallery.tsx @@ -1,10 +1,17 @@ import { useRef, useState, useEffect } from 'react'; import * as Styled from './PromotionImageGallery.styles'; +import ImageMoreButton from './ImageMoreButton/ImageMoreButton'; interface PromotionImageGalleryProps { images: string[]; } +const testImages = [ + 'https://picsum.photos/800/900', + 'https://picsum.photos/800/1000', + 'https://picsum.photos/800/1100', +]; + const MAX_HEIGHT = 700; const PromotionImageGallery = ({ images }: PromotionImageGalleryProps) => { @@ -12,13 +19,21 @@ const PromotionImageGallery = ({ images }: PromotionImageGalleryProps) => { const [expanded, setExpanded] = useState(false); const [showButton, setShowButton] = useState(false); - useEffect(() => { + const checkHeight = () => { if (containerRef.current) { - if (containerRef.current.scrollHeight > MAX_HEIGHT) { - setShowButton(true); - } + setShowButton( + containerRef.current.scrollHeight > MAX_HEIGHT + ); } - }, []); + }; + + useEffect(() => { + checkHeight(); + }, [testImages]); + + useEffect(() => { + checkHeight(); + }, [testImages]); return ( @@ -26,7 +41,7 @@ const PromotionImageGallery = ({ images }: PromotionImageGalleryProps) => { ref={containerRef} $expanded={expanded} > - {images.map((src, idx) => ( + {testImages.map((src, idx) => ( ))} @@ -34,11 +49,10 @@ const PromotionImageGallery = ({ images }: PromotionImageGalleryProps) => { {showButton && ( - setExpanded(!expanded)} - > - {expanded ? '접기 ▲' : '이미지 더보기 ▼'} - + setExpanded((prev) => !prev)} + /> )} ); From 64feaf3b06a0473b92fb3ba2c8ed1085eecf36f6 Mon Sep 17 00:00:00 2001 From: suhyun113 <163711629+suhyun113@users.noreply.github.com> Date: Thu, 5 Mar 2026 10:07:23 +0900 Subject: [PATCH 018/172] =?UTF-8?q?fix:=20=EC=9D=B4=EB=AF=B8=EC=A7=80=20?= =?UTF-8?q?=EB=8D=94=EB=B3=B4=EA=B8=B0=20=EB=B2=84=ED=8A=BC=20=EC=83=9D?= =?UTF-8?q?=EC=84=B1=20=EC=95=88=20=EB=90=98=EB=8A=94=20=EB=AC=B8=EC=A0=9C?= =?UTF-8?q?=20=ED=95=B4=EA=B2=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ImageMoreButton/ImageMoreButton.styles.ts | 6 ++-- .../PromotionImageGallery.styles.ts | 36 +++++++++---------- .../PromotionImageGallery.tsx | 23 ++++++------ 3 files changed, 32 insertions(+), 33 deletions(-) diff --git a/frontend/src/pages/PromotionPage/components/detail/PromotionImageGallery/ImageMoreButton/ImageMoreButton.styles.ts b/frontend/src/pages/PromotionPage/components/detail/PromotionImageGallery/ImageMoreButton/ImageMoreButton.styles.ts index 8ad2d3715..4b6670886 100644 --- a/frontend/src/pages/PromotionPage/components/detail/PromotionImageGallery/ImageMoreButton/ImageMoreButton.styles.ts +++ b/frontend/src/pages/PromotionPage/components/detail/PromotionImageGallery/ImageMoreButton/ImageMoreButton.styles.ts @@ -5,9 +5,9 @@ export const Button = styled.button` width: 100%; margin-top: 12px; padding: 14px; - border-radius: 12px; - background: ${colors.gray[200]}; - border: none; + border-radius: 10px; + background: ${colors.gray[100]}; + border: 1px solid ${colors.gray[400]}; cursor: pointer; font-weight: 600; diff --git a/frontend/src/pages/PromotionPage/components/detail/PromotionImageGallery/PromotionImageGallery.styles.ts b/frontend/src/pages/PromotionPage/components/detail/PromotionImageGallery/PromotionImageGallery.styles.ts index 55dc170e4..d71d37abd 100644 --- a/frontend/src/pages/PromotionPage/components/detail/PromotionImageGallery/PromotionImageGallery.styles.ts +++ b/frontend/src/pages/PromotionPage/components/detail/PromotionImageGallery/PromotionImageGallery.styles.ts @@ -23,27 +23,27 @@ export const Gradient = styled.div` bottom: 0; left: 0; right: 0; - height: 140px; + height: 180px; - backdrop-filter: blur(6px); - -webkit-backdrop-filter: blur(6px); + pointer-events: none; - background: linear-gradient( + backdrop-filter: blur(12px); + -webkit-backdrop-filter: blur(12px); + + mask-image: linear-gradient( to bottom, - rgba(255, 255, 255, 0) 0%, - rgba(255, 255, 255, 0.7) 60%, - rgba(255, 255, 255, 1) 100% + rgba(0,0,0,0) 0%, + rgba(0,0,0,0.2) 30%, + rgba(0,0,0,0.5) 60%, + rgba(0,0,0,0.8) 80%, + rgba(0,0,0,1) 100% ); - pointer-events: none; -`; - -export const MoreButton = styled.button` - width: 100%; - margin-top: 12px; - padding: 12px; - border-radius: 10px; - background: ${colors.gray[200]}; - border: none; - cursor: pointer; + background: linear-gradient( + to bottom, + rgba(255,255,255,0) 0%, + rgba(255,255,255,0.2) 40%, + rgba(255,255,255,0.6) 70%, + rgba(255,255,255,1) 100% + ); `; diff --git a/frontend/src/pages/PromotionPage/components/detail/PromotionImageGallery/PromotionImageGallery.tsx b/frontend/src/pages/PromotionPage/components/detail/PromotionImageGallery/PromotionImageGallery.tsx index cbcf27417..049fbbe44 100644 --- a/frontend/src/pages/PromotionPage/components/detail/PromotionImageGallery/PromotionImageGallery.tsx +++ b/frontend/src/pages/PromotionPage/components/detail/PromotionImageGallery/PromotionImageGallery.tsx @@ -19,20 +19,19 @@ const PromotionImageGallery = ({ images }: PromotionImageGalleryProps) => { const [expanded, setExpanded] = useState(false); const [showButton, setShowButton] = useState(false); - const checkHeight = () => { - if (containerRef.current) { - setShowButton( - containerRef.current.scrollHeight > MAX_HEIGHT - ); - } - }; - useEffect(() => { - checkHeight(); - }, [testImages]); + if (!containerRef.current) return; - useEffect(() => { - checkHeight(); + const observer = new ResizeObserver(() => { + if (!containerRef.current) return; + + const height = containerRef.current.scrollHeight; + setShowButton(height > MAX_HEIGHT); + }); + + observer.observe(containerRef.current); + + return () => observer.disconnect(); }, [testImages]); return ( From 5b3d900f5ec5ffe4026e08154634cb294021cb0d Mon Sep 17 00:00:00 2001 From: suhyun113 <163711629+suhyun113@users.noreply.github.com> Date: Thu, 5 Mar 2026 10:20:24 +0900 Subject: [PATCH 019/172] =?UTF-8?q?style:=20moreArrawIcon=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80=20=EB=B0=8F=20=EC=9D=B4=EB=AF=B8=EC=A7=80=20=EB=8D=94?= =?UTF-8?q?=EB=B3=B4=EA=B8=B0=20=EB=B2=84=ED=8A=BC=20=EB=94=94=EC=9E=90?= =?UTF-8?q?=EC=9D=B8=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../assets/images/icons/more_arraw_icon.svg | 3 +++ .../ImageMoreButton/ImageMoreButton.styles.ts | 25 +++++++++++++++---- .../ImageMoreButton/ImageMoreButton.tsx | 8 +++++- .../PromotionImageGallery.styles.ts | 4 +++ .../PromotionImageGallery.tsx | 23 ++++++++--------- 5 files changed, 45 insertions(+), 18 deletions(-) create mode 100644 frontend/src/assets/images/icons/more_arraw_icon.svg diff --git a/frontend/src/assets/images/icons/more_arraw_icon.svg b/frontend/src/assets/images/icons/more_arraw_icon.svg new file mode 100644 index 000000000..4fb7b3b2d --- /dev/null +++ b/frontend/src/assets/images/icons/more_arraw_icon.svg @@ -0,0 +1,3 @@ + + + diff --git a/frontend/src/pages/PromotionPage/components/detail/PromotionImageGallery/ImageMoreButton/ImageMoreButton.styles.ts b/frontend/src/pages/PromotionPage/components/detail/PromotionImageGallery/ImageMoreButton/ImageMoreButton.styles.ts index 4b6670886..dff1ad136 100644 --- a/frontend/src/pages/PromotionPage/components/detail/PromotionImageGallery/ImageMoreButton/ImageMoreButton.styles.ts +++ b/frontend/src/pages/PromotionPage/components/detail/PromotionImageGallery/ImageMoreButton/ImageMoreButton.styles.ts @@ -5,13 +5,28 @@ export const Button = styled.button` width: 100%; margin-top: 12px; padding: 14px; - border-radius: 10px; + border-radius: 12px; background: ${colors.gray[100]}; border: 1px solid ${colors.gray[400]}; cursor: pointer; - font-weight: 600; + font-size: 16px; + font-weight: 700; + color: ${colors.gray[800]}; - &:hover { - background: ${colors.gray[300]}; - } + display: flex; + justify-content: center; +`; + +export const Content = styled.div` + display: flex; + align-items: center; + gap: 6px; +`; + +export const Arrow = styled.span<{ $expanded: boolean }>` + display: flex; + transition: transform 0.25s ease; + + transform: ${({ $expanded }) => + $expanded ? 'rotate(180deg)' : 'rotate(0deg)'}; `; \ No newline at end of file diff --git a/frontend/src/pages/PromotionPage/components/detail/PromotionImageGallery/ImageMoreButton/ImageMoreButton.tsx b/frontend/src/pages/PromotionPage/components/detail/PromotionImageGallery/ImageMoreButton/ImageMoreButton.tsx index ad80c021f..3fce71749 100644 --- a/frontend/src/pages/PromotionPage/components/detail/PromotionImageGallery/ImageMoreButton/ImageMoreButton.tsx +++ b/frontend/src/pages/PromotionPage/components/detail/PromotionImageGallery/ImageMoreButton/ImageMoreButton.tsx @@ -1,3 +1,4 @@ +import ArrowIcon from '@/assets/images/icons/more_arraw_icon.svg?react'; import * as Styled from './ImageMoreButton.styles'; interface Props { @@ -8,7 +9,12 @@ interface Props { const ImageMoreButton = ({ expanded, onClick }: Props) => { return ( - {expanded ? '접기 ▲' : '이미지 더보기 ▼'} + + 이미지 {expanded ? '접기' : '더보기'} + + + + ); }; diff --git a/frontend/src/pages/PromotionPage/components/detail/PromotionImageGallery/PromotionImageGallery.styles.ts b/frontend/src/pages/PromotionPage/components/detail/PromotionImageGallery/PromotionImageGallery.styles.ts index d71d37abd..3d23f9fc3 100644 --- a/frontend/src/pages/PromotionPage/components/detail/PromotionImageGallery/PromotionImageGallery.styles.ts +++ b/frontend/src/pages/PromotionPage/components/detail/PromotionImageGallery/PromotionImageGallery.styles.ts @@ -47,3 +47,7 @@ export const Gradient = styled.div` rgba(255,255,255,1) 100% ); `; + +export const ImageMoreButtonWrapper = styled.div` + padding: 0 20px; +`; diff --git a/frontend/src/pages/PromotionPage/components/detail/PromotionImageGallery/PromotionImageGallery.tsx b/frontend/src/pages/PromotionPage/components/detail/PromotionImageGallery/PromotionImageGallery.tsx index 049fbbe44..5f8d88fc3 100644 --- a/frontend/src/pages/PromotionPage/components/detail/PromotionImageGallery/PromotionImageGallery.tsx +++ b/frontend/src/pages/PromotionPage/components/detail/PromotionImageGallery/PromotionImageGallery.tsx @@ -1,6 +1,6 @@ -import { useRef, useState, useEffect } from 'react'; -import * as Styled from './PromotionImageGallery.styles'; +import { useEffect, useRef, useState } from 'react'; import ImageMoreButton from './ImageMoreButton/ImageMoreButton'; +import * as Styled from './PromotionImageGallery.styles'; interface PromotionImageGalleryProps { images: string[]; @@ -36,25 +36,24 @@ const PromotionImageGallery = ({ images }: PromotionImageGalleryProps) => { return ( - + {testImages.map((src, idx) => ( - + ))} {!expanded && showButton && } {showButton && ( - setExpanded((prev) => !prev)} - /> + + setExpanded((prev) => !prev)} + /> + )} ); }; -export default PromotionImageGallery; \ No newline at end of file +export default PromotionImageGallery; From 75c3df50d116b063137b87c2a515dc673b87b14f Mon Sep 17 00:00:00 2001 From: suhyun113 <163711629+suhyun113@users.noreply.github.com> Date: Thu, 5 Mar 2026 10:31:02 +0900 Subject: [PATCH 020/172] =?UTF-8?q?style:=20=ED=96=89=EC=82=AC=20=EC=83=81?= =?UTF-8?q?=EC=84=B8=EC=A0=95=EB=B3=B4=20=EC=8A=A4=ED=83=80=EC=9D=BC=20?= =?UTF-8?q?=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../PromotionInfoSection.styles.ts | 18 ++++++++++-------- .../PromotionInfoSection.tsx | 1 + 2 files changed, 11 insertions(+), 8 deletions(-) diff --git a/frontend/src/pages/PromotionPage/components/detail/PromotionInfoSection/PromotionInfoSection.styles.ts b/frontend/src/pages/PromotionPage/components/detail/PromotionInfoSection/PromotionInfoSection.styles.ts index c36b9b3b9..c3e4346a2 100644 --- a/frontend/src/pages/PromotionPage/components/detail/PromotionInfoSection/PromotionInfoSection.styles.ts +++ b/frontend/src/pages/PromotionPage/components/detail/PromotionInfoSection/PromotionInfoSection.styles.ts @@ -2,13 +2,14 @@ import styled from 'styled-components'; import { colors } from '@/styles/theme/colors'; export const Container = styled.section` - padding: 20px 18px; + padding: 24px 20px; `; export const SectionTitle = styled.h3` - font-size: 16px; + font-size: 20px; font-weight: 700; - margin-bottom: 12px; + margin-bottom: 4px; + color: ${colors.gray[800]}; `; export const Card = styled.div` @@ -18,7 +19,7 @@ export const Card = styled.div` `; export const Item = styled.div` - margin-bottom: 16px; + margin-bottom: 22px; &:last-child { margin-bottom: 0; @@ -26,12 +27,13 @@ export const Item = styled.div` `; export const Label = styled.div` - font-weight: 600; - margin-bottom: 6px; + font-size: 14px; + font-weight: 400; + color: ${colors.gray[800]}; `; export const Value = styled.div` font-size: 14px; + font-weight: 400; color: ${colors.gray[800]}; - line-height: 1.6; -`; \ No newline at end of file +`; diff --git a/frontend/src/pages/PromotionPage/components/detail/PromotionInfoSection/PromotionInfoSection.tsx b/frontend/src/pages/PromotionPage/components/detail/PromotionInfoSection/PromotionInfoSection.tsx index 5444a6454..0d645abdb 100644 --- a/frontend/src/pages/PromotionPage/components/detail/PromotionInfoSection/PromotionInfoSection.tsx +++ b/frontend/src/pages/PromotionPage/components/detail/PromotionInfoSection/PromotionInfoSection.tsx @@ -10,6 +10,7 @@ const formatDate = (dateStr: string) => { return date.toLocaleString('ko-KR', { month: 'long', day: 'numeric', + weekday: 'short', hour: '2-digit', minute: '2-digit', }); From 88771a3f55441b18a21aa39978bed7df5063b0fc Mon Sep 17 00:00:00 2001 From: suhyun113 <163711629+suhyun113@users.noreply.github.com> Date: Thu, 5 Mar 2026 10:50:33 +0900 Subject: [PATCH 021/172] =?UTF-8?q?feat:=20ArrowButton=20=EA=B3=B5?= =?UTF-8?q?=ED=86=B5=EC=9C=BC=EB=A1=9C=20=EB=B6=84=EB=A6=AC=20=EB=B0=8F=20?= =?UTF-8?q?=EB=B0=A9=ED=96=A5=EC=97=90=20=EB=94=B0=EB=9D=BC=20=EC=A0=81?= =?UTF-8?q?=EC=9A=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../PromotionArrowButton.styles.ts} | 16 +++++++++++--- .../PromotionArrowButton.tsx} | 17 +++++++++------ .../PromotionClubCTA.styles.ts | 9 ++++---- .../PromotionClubCTA/PromotionClubCTA.tsx | 21 +++++++++++-------- .../PromotionImageGallery.styles.ts | 1 - .../PromotionImageGallery.tsx | 7 ++++--- 6 files changed, 45 insertions(+), 26 deletions(-) rename frontend/src/pages/PromotionPage/components/detail/{PromotionImageGallery/ImageMoreButton/ImageMoreButton.styles.ts => PromotionArrowButton/PromotionArrowButton.styles.ts} (62%) rename frontend/src/pages/PromotionPage/components/detail/{PromotionImageGallery/ImageMoreButton/ImageMoreButton.tsx => PromotionArrowButton/PromotionArrowButton.tsx} (50%) diff --git a/frontend/src/pages/PromotionPage/components/detail/PromotionImageGallery/ImageMoreButton/ImageMoreButton.styles.ts b/frontend/src/pages/PromotionPage/components/detail/PromotionArrowButton/PromotionArrowButton.styles.ts similarity index 62% rename from frontend/src/pages/PromotionPage/components/detail/PromotionImageGallery/ImageMoreButton/ImageMoreButton.styles.ts rename to frontend/src/pages/PromotionPage/components/detail/PromotionArrowButton/PromotionArrowButton.styles.ts index dff1ad136..071be3157 100644 --- a/frontend/src/pages/PromotionPage/components/detail/PromotionImageGallery/ImageMoreButton/ImageMoreButton.styles.ts +++ b/frontend/src/pages/PromotionPage/components/detail/PromotionArrowButton/PromotionArrowButton.styles.ts @@ -23,10 +23,20 @@ export const Content = styled.div` gap: 6px; `; -export const Arrow = styled.span<{ $expanded: boolean }>` +export const Arrow = styled.span<{ $direction: string }>` display: flex; transition: transform 0.25s ease; - transform: ${({ $expanded }) => - $expanded ? 'rotate(180deg)' : 'rotate(0deg)'}; + transform: ${({ $direction }) => { + switch ($direction) { + case 'up': + return 'rotate(180deg)'; + case 'right': + return 'rotate(-90deg)'; + case 'left': + return 'rotate(90deg)'; + default: + return 'rotate(0deg)'; + } + }}; `; \ No newline at end of file diff --git a/frontend/src/pages/PromotionPage/components/detail/PromotionImageGallery/ImageMoreButton/ImageMoreButton.tsx b/frontend/src/pages/PromotionPage/components/detail/PromotionArrowButton/PromotionArrowButton.tsx similarity index 50% rename from frontend/src/pages/PromotionPage/components/detail/PromotionImageGallery/ImageMoreButton/ImageMoreButton.tsx rename to frontend/src/pages/PromotionPage/components/detail/PromotionArrowButton/PromotionArrowButton.tsx index 3fce71749..aaf819640 100644 --- a/frontend/src/pages/PromotionPage/components/detail/PromotionImageGallery/ImageMoreButton/ImageMoreButton.tsx +++ b/frontend/src/pages/PromotionPage/components/detail/PromotionArrowButton/PromotionArrowButton.tsx @@ -1,17 +1,22 @@ import ArrowIcon from '@/assets/images/icons/more_arraw_icon.svg?react'; -import * as Styled from './ImageMoreButton.styles'; +import * as Styled from './PromotionArrowButton.styles'; interface Props { - expanded: boolean; + text: string; + direction?: 'down' | 'up' | 'right' | 'left'; onClick: () => void; } -const ImageMoreButton = ({ expanded, onClick }: Props) => { +const PromotionArrowButton = ({ + text, + direction = 'down', + onClick, +}: Props) => { return ( - 이미지 {expanded ? '접기' : '더보기'} - + {text} + @@ -19,4 +24,4 @@ const ImageMoreButton = ({ expanded, onClick }: Props) => { ); }; -export default ImageMoreButton; \ No newline at end of file +export default PromotionArrowButton; \ No newline at end of file diff --git a/frontend/src/pages/PromotionPage/components/detail/PromotionClubCTA/PromotionClubCTA.styles.ts b/frontend/src/pages/PromotionPage/components/detail/PromotionClubCTA/PromotionClubCTA.styles.ts index c6d5314b6..9e26210c8 100644 --- a/frontend/src/pages/PromotionPage/components/detail/PromotionClubCTA/PromotionClubCTA.styles.ts +++ b/frontend/src/pages/PromotionPage/components/detail/PromotionClubCTA/PromotionClubCTA.styles.ts @@ -2,12 +2,13 @@ import styled from 'styled-components'; import { colors } from '@/styles/theme/colors'; export const Container = styled.section` - padding: 24px 18px; + padding: 0px 20px 24px 20px; `; export const Question = styled.div` - font-size: 15px; - font-weight: 600; + font-size: 20px; + font-weight: 700; + color: ${colors.gray[800]}; margin-bottom: 12px; `; @@ -24,4 +25,4 @@ export const Button = styled.button` &:active { background: ${colors.gray[300]}; } -`; \ No newline at end of file +`; diff --git a/frontend/src/pages/PromotionPage/components/detail/PromotionClubCTA/PromotionClubCTA.tsx b/frontend/src/pages/PromotionPage/components/detail/PromotionClubCTA/PromotionClubCTA.tsx index d08df0b27..f48253e5b 100644 --- a/frontend/src/pages/PromotionPage/components/detail/PromotionClubCTA/PromotionClubCTA.tsx +++ b/frontend/src/pages/PromotionPage/components/detail/PromotionClubCTA/PromotionClubCTA.tsx @@ -1,4 +1,5 @@ import { useNavigate } from 'react-router-dom'; +import ArrowButton from '../PromotionArrowButton/PromotionArrowButton'; import * as Styled from './PromotionClubCTA.styles'; interface Props { @@ -8,19 +9,21 @@ interface Props { const PromotionClubCTA = ({ clubId }: Props) => { const navigate = useNavigate(); + const handleNavigate = () => { + navigate(`/clubs/${clubId}`); + }; + return ( - - 동아리 정보가 궁금하다면? - + 동아리 정보가 궁금하다면? - navigate(`/club/${clubId}`)} - > - 동아리 정보 보러가기 → - + ); }; -export default PromotionClubCTA; \ No newline at end of file +export default PromotionClubCTA; diff --git a/frontend/src/pages/PromotionPage/components/detail/PromotionImageGallery/PromotionImageGallery.styles.ts b/frontend/src/pages/PromotionPage/components/detail/PromotionImageGallery/PromotionImageGallery.styles.ts index 3d23f9fc3..0a5fbe01e 100644 --- a/frontend/src/pages/PromotionPage/components/detail/PromotionImageGallery/PromotionImageGallery.styles.ts +++ b/frontend/src/pages/PromotionPage/components/detail/PromotionImageGallery/PromotionImageGallery.styles.ts @@ -1,5 +1,4 @@ import styled from 'styled-components'; -import { colors } from '@/styles/theme/colors'; export const Wrapper = styled.section` margin: 0; diff --git a/frontend/src/pages/PromotionPage/components/detail/PromotionImageGallery/PromotionImageGallery.tsx b/frontend/src/pages/PromotionPage/components/detail/PromotionImageGallery/PromotionImageGallery.tsx index 5f8d88fc3..8a94dd505 100644 --- a/frontend/src/pages/PromotionPage/components/detail/PromotionImageGallery/PromotionImageGallery.tsx +++ b/frontend/src/pages/PromotionPage/components/detail/PromotionImageGallery/PromotionImageGallery.tsx @@ -1,5 +1,5 @@ import { useEffect, useRef, useState } from 'react'; -import ImageMoreButton from './ImageMoreButton/ImageMoreButton'; +import ArrowButton from '../PromotionArrowButton/PromotionArrowButton'; import * as Styled from './PromotionImageGallery.styles'; interface PromotionImageGalleryProps { @@ -46,8 +46,9 @@ const PromotionImageGallery = ({ images }: PromotionImageGalleryProps) => { {showButton && ( - setExpanded((prev) => !prev)} /> From 37494cb5facc06d3f79ed13db0a2532ed3e97275 Mon Sep 17 00:00:00 2001 From: suhyun113 <163711629+suhyun113@users.noreply.github.com> Date: Thu, 5 Mar 2026 13:40:58 +0900 Subject: [PATCH 022/172] =?UTF-8?q?feat:=20PromotionCard=20=EC=BB=B4?= =?UTF-8?q?=ED=8F=AC=EB=84=8C=ED=8A=B8=20=EC=9E=AC=EC=82=AC=EC=9A=A9=20?= =?UTF-8?q?=EB=B0=8F=20=EA=B4=80=EB=A0=A8=20=EC=9D=B4=EB=B2=A4=ED=8A=B8=20?= =?UTF-8?q?=EC=84=B9=EC=85=98=20=EC=9E=84=EC=8B=9C=20=EC=B6=94=EA=B0=80=20?= =?UTF-8?q?=EB=B0=8F=20=ED=85=8D=EC=8A=A4=ED=8A=B8=20=EB=A7=90=EC=A4=84?= =?UTF-8?q?=EC=9E=84=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../PromotionPage/PromotionDetailPage.tsx | 56 +++++++++++-------- .../pages/PromotionPage/PromotionListPage.tsx | 6 +- .../PromotionImageGallery.tsx | 10 +--- .../RelatedPromotionCard.styles.ts | 21 +++++++ .../RelatedPromotionCard.tsx | 28 ++++++++++ .../RelatedPromotionSection.styles.ts | 23 ++------ .../RelatedPromotionSection.tsx | 45 +++++++++++---- .../PromotionCard/CardMeta/CardMeta.styles.ts | 23 +++++++- .../list/PromotionCard/CardMeta/CardMeta.tsx | 15 ++++- .../PromotionCard/DdayBadge/DdayBadge.tsx | 20 ++----- .../list/PromotionCard/PromotionCard.tsx | 9 ++- .../PromotionPage/data/dummyActiveEvent.ts | 38 +++++++++++++ frontend/src/utils/getDday.ts | 14 +++++ 13 files changed, 225 insertions(+), 83 deletions(-) create mode 100644 frontend/src/pages/PromotionPage/components/detail/RelatedPromotionSection/RelatedPromotionCard/RelatedPromotionCard.styles.ts create mode 100644 frontend/src/pages/PromotionPage/components/detail/RelatedPromotionSection/RelatedPromotionCard/RelatedPromotionCard.tsx create mode 100644 frontend/src/pages/PromotionPage/data/dummyActiveEvent.ts create mode 100644 frontend/src/utils/getDday.ts diff --git a/frontend/src/pages/PromotionPage/PromotionDetailPage.tsx b/frontend/src/pages/PromotionPage/PromotionDetailPage.tsx index 6229ab7af..dd189a8b3 100644 --- a/frontend/src/pages/PromotionPage/PromotionDetailPage.tsx +++ b/frontend/src/pages/PromotionPage/PromotionDetailPage.tsx @@ -1,4 +1,5 @@ import { useParams } from 'react-router-dom'; +import Footer from '@/components/common/Footer/Footer'; import { PAGE_VIEW } from '@/constants/eventName'; import useTrackPageView from '@/hooks/Mixpanel/useTrackPageView'; import { useGetPromotionArticles } from '@/hooks/Queries/usePromotion'; @@ -9,37 +10,46 @@ import PromotionInfoSection from './components/detail/PromotionInfoSection/Promo import PromotionTitleSection from './components/detail/PromotionTitleSection/PromotionTitleSection'; import RelatedPromotionSection from './components/detail/RelatedPromotionSection/RelatedPromotionSection'; import * as Styled from './PromotionDetailPage.styles'; +import { dummyPromotionArticles } from './data/dummyActiveEvent'; const PromotionDetailPage = () => { useTrackPageView(PAGE_VIEW.PROMOTION_DETAIL_PAGE); const { promotionId } = useParams<{ promotionId: string }>(); - const { data, isLoading, isError } = useGetPromotionArticles(); + // const { data, isLoading, isError } = useGetPromotionArticles(); + const data = dummyPromotionArticles; + const isLoading = false; + const isError = false; - const article = - data?.find((item) => item.clubId === promotionId) ?? null; + const article = data?.find((item) => item.clubId === promotionId) ?? null; return ( - - - - {isLoading && 로딩 중...} - {isError && 오류가 발생했습니다.} - - {!isLoading && !isError && !article && ( - 존재하지 않는 이벤트입니다. - )} - - {!isLoading && !isError && article && ( - <> - - - - - - - )} - + <> + + + + {isLoading && 로딩 중...} + {isError && 오류가 발생했습니다.} + + {!isLoading && !isError && !article && ( + 존재하지 않는 이벤트입니다. + )} + + {!isLoading && !isError && article && ( + <> + + + + + + + )} + +
+ ); }; diff --git a/frontend/src/pages/PromotionPage/PromotionListPage.tsx b/frontend/src/pages/PromotionPage/PromotionListPage.tsx index 2575dca67..0a9548dee 100644 --- a/frontend/src/pages/PromotionPage/PromotionListPage.tsx +++ b/frontend/src/pages/PromotionPage/PromotionListPage.tsx @@ -6,12 +6,16 @@ import { useGetPromotionArticles } from '@/hooks/Queries/usePromotion'; import isInAppWebView from '@/utils/isInAppWebView'; import Filter from '../MainPage/components/Filter/Filter'; import PromottionGrid from './components/list/PromotionGrid/PromotionGrid'; +import { dummyPromotionArticles } from './data/dummyActiveEvent'; import * as Styled from './PromotionListPage.styles'; const PromotionListPage = () => { useTrackPageView(PAGE_VIEW.PROMOTION_LIST_PAGE); - const { data, isLoading, isError } = useGetPromotionArticles(); + // const { data, isLoading, isError } = useGetPromotionArticles(); + const data = dummyPromotionArticles; + const isLoading = false; + const isError = false; return ( <> diff --git a/frontend/src/pages/PromotionPage/components/detail/PromotionImageGallery/PromotionImageGallery.tsx b/frontend/src/pages/PromotionPage/components/detail/PromotionImageGallery/PromotionImageGallery.tsx index 8a94dd505..863a7af25 100644 --- a/frontend/src/pages/PromotionPage/components/detail/PromotionImageGallery/PromotionImageGallery.tsx +++ b/frontend/src/pages/PromotionPage/components/detail/PromotionImageGallery/PromotionImageGallery.tsx @@ -6,12 +6,6 @@ interface PromotionImageGalleryProps { images: string[]; } -const testImages = [ - 'https://picsum.photos/800/900', - 'https://picsum.photos/800/1000', - 'https://picsum.photos/800/1100', -]; - const MAX_HEIGHT = 700; const PromotionImageGallery = ({ images }: PromotionImageGalleryProps) => { @@ -32,12 +26,12 @@ const PromotionImageGallery = ({ images }: PromotionImageGalleryProps) => { observer.observe(containerRef.current); return () => observer.disconnect(); - }, [testImages]); + }, [images]); return ( - {testImages.map((src, idx) => ( + {images.map((src, idx) => ( ))} diff --git a/frontend/src/pages/PromotionPage/components/detail/RelatedPromotionSection/RelatedPromotionCard/RelatedPromotionCard.styles.ts b/frontend/src/pages/PromotionPage/components/detail/RelatedPromotionSection/RelatedPromotionCard/RelatedPromotionCard.styles.ts new file mode 100644 index 000000000..a9c1b2c63 --- /dev/null +++ b/frontend/src/pages/PromotionPage/components/detail/RelatedPromotionSection/RelatedPromotionCard/RelatedPromotionCard.styles.ts @@ -0,0 +1,21 @@ +import styled from 'styled-components'; +import { colors } from '@/styles/theme/colors'; + +export const Card = styled.div` + background: ${colors.base.white}; + border-radius: 14px; + padding: 20px; + cursor: pointer; + + box-shadow: 0 6px 18px rgba(0,0,0,0.06); + + transition: transform 0.2s ease; + + &:hover { + transform: translateY(-2px); + } +`; + +export const ClubTagWrapper = styled.div` + margin-bottom: 6px; +`; \ No newline at end of file diff --git a/frontend/src/pages/PromotionPage/components/detail/RelatedPromotionSection/RelatedPromotionCard/RelatedPromotionCard.tsx b/frontend/src/pages/PromotionPage/components/detail/RelatedPromotionSection/RelatedPromotionCard/RelatedPromotionCard.tsx new file mode 100644 index 000000000..8b25ea804 --- /dev/null +++ b/frontend/src/pages/PromotionPage/components/detail/RelatedPromotionSection/RelatedPromotionCard/RelatedPromotionCard.tsx @@ -0,0 +1,28 @@ +import { PromotionArticle } from '@/types/promotion'; +import CardMeta from '../../../list/PromotionCard/CardMeta/CardMeta'; +import ClubTag from '../../../list/PromotionCard/ClubTag/ClubTag'; +import * as Styled from './RelatedPromotionCard.styles'; + +interface Props { + article: PromotionArticle; + onClick: () => void; +} + +const RelatedPromotionCard = ({ article, onClick }: Props) => { + return ( + + + + + + + + ); +}; + +export default RelatedPromotionCard; diff --git a/frontend/src/pages/PromotionPage/components/detail/RelatedPromotionSection/RelatedPromotionSection.styles.ts b/frontend/src/pages/PromotionPage/components/detail/RelatedPromotionSection/RelatedPromotionSection.styles.ts index a933392e0..0452b9c36 100644 --- a/frontend/src/pages/PromotionPage/components/detail/RelatedPromotionSection/RelatedPromotionSection.styles.ts +++ b/frontend/src/pages/PromotionPage/components/detail/RelatedPromotionSection/RelatedPromotionSection.styles.ts @@ -2,28 +2,13 @@ import styled from 'styled-components'; import { colors } from '@/styles/theme/colors'; export const Container = styled.section` - padding: 24px 18px 60px 18px; + padding: 20px; + background-color: ${colors.gray[300]}; `; export const Title = styled.h3` - font-size: 16px; + font-size: 20px; font-weight: 700; + color: ${colors.gray[800]}; margin-bottom: 12px; `; - -export const Card = styled.div` - background: ${colors.base.white}; - border-radius: 14px; - padding: 16px; - box-shadow: 0 4px 12px rgba(0,0,0,0.05); -`; - -export const CardTitle = styled.div` - font-weight: 700; - margin-bottom: 6px; -`; - -export const CardDesc = styled.div` - font-size: 14px; - color: ${colors.gray[600]}; -`; \ No newline at end of file diff --git a/frontend/src/pages/PromotionPage/components/detail/RelatedPromotionSection/RelatedPromotionSection.tsx b/frontend/src/pages/PromotionPage/components/detail/RelatedPromotionSection/RelatedPromotionSection.tsx index 32ae0f551..ac2d3fe8f 100644 --- a/frontend/src/pages/PromotionPage/components/detail/RelatedPromotionSection/RelatedPromotionSection.tsx +++ b/frontend/src/pages/PromotionPage/components/detail/RelatedPromotionSection/RelatedPromotionSection.tsx @@ -1,22 +1,47 @@ +import { useNavigate } from 'react-router-dom'; +import { PromotionArticle } from '@/types/promotion'; +import { getDDay } from '@/utils/getDday'; +import RelatedPromotionCard from './RelatedPromotionCard/RelatedPromotionCard'; import * as Styled from './RelatedPromotionSection.styles'; interface Props { currentClubId: string; + articles: PromotionArticle[]; } -const RelatedPromotionSection = ({ currentClubId }: Props) => { +const RelatedPromotionSection = ({ + currentClubId, + articles, +}: Props) => { + const navigate = useNavigate(); + + const activeEvents = articles + .filter((a) => { + const dday = getDDay(a.eventStartDate); + return ( + a.clubId !== currentClubId && + dday >= 0 + ); + }) + .slice(0, 1); + + if (activeEvents.length === 0) return null; + return ( - 이런 이벤트는 어때요? + + 이런 이벤트는 어때요? + - - - 다른 동아리 행사 예시 - - - 관련 이벤트 영역 (추후 API 연동) - - + {activeEvents.map((event) => ( + + navigate(`/promotions/${event.clubId}`) + } + /> + ))} ); }; diff --git a/frontend/src/pages/PromotionPage/components/list/PromotionCard/CardMeta/CardMeta.styles.ts b/frontend/src/pages/PromotionPage/components/list/PromotionCard/CardMeta/CardMeta.styles.ts index d6bd1dc8c..a278dccd3 100644 --- a/frontend/src/pages/PromotionPage/components/list/PromotionCard/CardMeta/CardMeta.styles.ts +++ b/frontend/src/pages/PromotionPage/components/list/PromotionCard/CardMeta/CardMeta.styles.ts @@ -4,18 +4,37 @@ import { colors } from '@/styles/theme/colors'; export const Container = styled.div` display: flex; flex-direction: column; + gap: 6px; +`; + +export const TitleSection = styled.div` + gap: 6px; `; export const Title = styled.h3` font-size: 14px; font-weight: 600; - margin-bottom: 4px; + min-width: 0; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + margin-bottom: 6px; +`; + +export const Description = styled.span` + display: block; + min-width: 0; + font-size: 14px; + font-weight: 400; + color: ${colors.gray[600]}; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; `; export const MetaRow = styled.div` display: flex; align-items: center; - margin-top: 3px; `; export const Icon = styled.div` diff --git a/frontend/src/pages/PromotionPage/components/list/PromotionCard/CardMeta/CardMeta.tsx b/frontend/src/pages/PromotionPage/components/list/PromotionCard/CardMeta/CardMeta.tsx index 562b61bb8..2dc35e65c 100644 --- a/frontend/src/pages/PromotionPage/components/list/PromotionCard/CardMeta/CardMeta.tsx +++ b/frontend/src/pages/PromotionPage/components/list/PromotionCard/CardMeta/CardMeta.tsx @@ -1,14 +1,20 @@ -import * as Styled from './CardMeta.styles'; import LocationIcon from '@/assets/images/icons/location_icon.svg'; import TimeIcon from '@/assets/images/icons/time_icon.svg'; +import * as Styled from './CardMeta.styles'; interface CardMetaProps { title: string; + description: string; location: string | null; startDate: string; } -const CardMeta = ({ title, location, startDate }: CardMetaProps) => { +const CardMeta = ({ + title, + description, + location, + startDate, +}: CardMetaProps) => { const startDateObj = new Date(startDate); const formattedStartDate = startDateObj.toLocaleDateString('ko-KR', { month: 'long', @@ -18,7 +24,10 @@ const CardMeta = ({ title, location, startDate }: CardMetaProps) => { return ( - {title} + + {title} + {description} + {location && ( diff --git a/frontend/src/pages/PromotionPage/components/list/PromotionCard/DdayBadge/DdayBadge.tsx b/frontend/src/pages/PromotionPage/components/list/PromotionCard/DdayBadge/DdayBadge.tsx index 72e479678..79bdeba9f 100644 --- a/frontend/src/pages/PromotionPage/components/list/PromotionCard/DdayBadge/DdayBadge.tsx +++ b/frontend/src/pages/PromotionPage/components/list/PromotionCard/DdayBadge/DdayBadge.tsx @@ -1,25 +1,15 @@ import * as Styled from './DdayBadge.styles'; interface DdayBadgeProps { - startDate: string; + dday: number; } -const DdayBadge = ({ startDate }: DdayBadgeProps) => { - const today = new Date(); - const start = new Date(startDate); - - today.setHours(0, 0, 0, 0); - start.setHours(0, 0, 0, 0); - - const diff = Math.ceil( - (start.getTime() - today.getTime()) / (1000 * 60 * 60 * 24) - ); - +const DdayBadge = ({ dday }: DdayBadgeProps) => { let label: string; - if (diff > 0) { - label = `D-${diff}`; - } else if (diff === 0) { + if (dday > 0) { + label = `D-${dday}`; + } else if (dday === 0) { label = 'D-Day'; } else { label = '종료'; diff --git a/frontend/src/pages/PromotionPage/components/list/PromotionCard/PromotionCard.tsx b/frontend/src/pages/PromotionPage/components/list/PromotionCard/PromotionCard.tsx index 53ab252db..3432864b7 100644 --- a/frontend/src/pages/PromotionPage/components/list/PromotionCard/PromotionCard.tsx +++ b/frontend/src/pages/PromotionPage/components/list/PromotionCard/PromotionCard.tsx @@ -1,5 +1,6 @@ import { useNavigate } from 'react-router-dom'; import { PromotionArticle } from '@/types/promotion'; +import { getDDay } from '@/utils/getDday'; import CardMeta from './CardMeta/CardMeta'; import ClubTag from './ClubTag/ClubTag'; import DdayBadge from './DdayBadge/DdayBadge'; @@ -9,8 +10,11 @@ interface PromotionCardProps { article: PromotionArticle; } -const PromotionCard = ({ article }: PromotionCardProps) => { +const PromotionCard = ({ + article, +}: PromotionCardProps) => { const navigateToPromotionDetail = useNavigate(); + const dday = getDDay(article.eventStartDate); const handleCardClick = () => { navigateToPromotionDetail(`/promotions/${article.clubId}`); @@ -23,13 +27,14 @@ const PromotionCard = ({ article }: PromotionCardProps) => { - + diff --git a/frontend/src/pages/PromotionPage/data/dummyActiveEvent.ts b/frontend/src/pages/PromotionPage/data/dummyActiveEvent.ts new file mode 100644 index 000000000..3a48030ae --- /dev/null +++ b/frontend/src/pages/PromotionPage/data/dummyActiveEvent.ts @@ -0,0 +1,38 @@ +import { PromotionArticle } from '@/types/promotion'; + +export const dummyPromotionArticles: PromotionArticle[] = [ + { + clubName: '백경미술연구회', + clubId: 'dummy-1', + title: 'EXODUS : 대탈출ㅇㅇㅇㅇㅇㅇㅇㅇㅇㅇㅇ', + location: '부경대학교 향파관 3층', + eventStartDate: '2026-03-05', // 진행 예정 + eventEndDate: '2026-12-31', + description: '테스트 진행 중 행사입니다.dkkkkkkkkkkkkkkkkkkkkkkk', + images: [ + 'https://picsum.photos/800/900', + 'https://picsum.photos/800/1000', + 'https://picsum.photos/800/1100', + ], + }, + { + clubName: 'WAP', + clubId: 'dummy-2', + title: '🔥 해커톤 모집 🔥', + location: '온라인', + eventStartDate: '2026-11-15', + eventEndDate: '2026-11-15', + description: '해커톤 테스트 행사입니다.', + images: ['https://picsum.photos/800/1000'], + }, + { + clubName: '종료 테스트', + clubId: 'dummy-3', + title: '지난 행사', + location: '테스트 장소', + eventStartDate: '2023-01-01', // 종료된 행사 + eventEndDate: '2023-01-01', + description: '이미 종료된 행사입니다.', + images: [], + }, +]; diff --git a/frontend/src/utils/getDday.ts b/frontend/src/utils/getDday.ts new file mode 100644 index 000000000..5caf09029 --- /dev/null +++ b/frontend/src/utils/getDday.ts @@ -0,0 +1,14 @@ +export const getDDay = (startDate: string) => { + const today = new Date(); + const start = new Date(startDate); + + today.setHours(0, 0, 0, 0); + start.setHours(0, 0, 0, 0); + + const diff = Math.ceil( + (start.getTime() - today.getTime()) / + (1000 * 60 * 60 * 24) + ); + + return diff; +}; \ No newline at end of file From 632f2ac002e04c7ffd0bf7129a6d122e560e40a2 Mon Sep 17 00:00:00 2001 From: suhyun113 <163711629+suhyun113@users.noreply.github.com> Date: Sun, 8 Mar 2026 21:08:42 +0900 Subject: [PATCH 023/172] =?UTF-8?q?feat:=20=EB=8D=94=EB=AF=B8=20=EB=8D=B0?= =?UTF-8?q?=EC=9D=B4=ED=84=B0=20=EC=A0=9C=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/src/mocks/handlers/promotion.ts | 14 +++---- .../PromotionDetailPage.styles.ts | 36 ++++++++++++++++++ .../PromotionPage/PromotionDetailPage.tsx | 6 +-- .../pages/PromotionPage/PromotionListPage.tsx | 6 +-- .../PromotionPage/data/dummyActiveEvent.ts | 38 ------------------- 5 files changed, 45 insertions(+), 55 deletions(-) delete mode 100644 frontend/src/pages/PromotionPage/data/dummyActiveEvent.ts diff --git a/frontend/src/mocks/handlers/promotion.ts b/frontend/src/mocks/handlers/promotion.ts index 066cc40a1..1e990f5db 100644 --- a/frontend/src/mocks/handlers/promotion.ts +++ b/frontend/src/mocks/handlers/promotion.ts @@ -7,13 +7,13 @@ const API_BASE_URL = // Mock 데이터 export const mockPromotionArticles: PromotionArticle[] = [ { - clubName: '모각코 동아리', - clubId: 'club-1', - title: '2024 봄 신입 부원 모집', - location: '서울 강남구', - eventStartDate: '2024-03-01', - eventEndDate: '2024-03-31', - description: '함께 성장하는 개발자 커뮤니티에 참여하세요!', + clubName: 'WAP', + clubId: '67e54ae51cfd27718dd40bec', + title: '💌✨WAP 최종 전시회 초대장 ✨💌', + location: '부경대학교 동원 장보고관 1층', + eventStartDate: '2025-11-28 09:00', + eventEndDate: '2025-11-28', + description: 'WAP 최종 전시회에 여러분을 초대합니다! \n\n이번 전시회에서는 WAP 팀이 한 학기 동안 열심히 준비한 프로젝트들을 선보입니다. 다양한 작품과 아이디어가 가득한 이번 전시회에서 여러분의 많은 관심과 참여 부탁드립니다! 🙌\n\n#WAP #최종전시회 #부경대학교', images: [ 'https://images.unsplash.com/photo-1522071820081-009f0129c71c?w=800', ], diff --git a/frontend/src/pages/PromotionPage/PromotionDetailPage.styles.ts b/frontend/src/pages/PromotionPage/PromotionDetailPage.styles.ts index 8e0d619da..1cfcc2cd2 100644 --- a/frontend/src/pages/PromotionPage/PromotionDetailPage.styles.ts +++ b/frontend/src/pages/PromotionPage/PromotionDetailPage.styles.ts @@ -1,10 +1,46 @@ import styled from 'styled-components'; +import { media } from '@/styles/mediaQuery'; export const Container = styled.div` + width: 100%; min-height: 100vh; background: #fff; `; +export const ContentWrapper = styled.div` + max-width: 1180px; + width: 100%; + margin: 0 auto; + + display: flex; + gap: 24px; + margin-top: 100px; + + ${media.laptop} { + padding: 0 20px; + } + + ${media.tablet} { + flex-direction: column; + padding: 0; + gap: 0; + max-width: 100%; + margin-top: 0; + } +`; + +export const LeftSection = styled.div` + width: 420px; + + ${media.tablet} { + width: 100%; + } +`; + +export const RightSection = styled.div` + flex: 1; +`; + export const Message = styled.p` padding: 40px 18px; text-align: center; diff --git a/frontend/src/pages/PromotionPage/PromotionDetailPage.tsx b/frontend/src/pages/PromotionPage/PromotionDetailPage.tsx index dd189a8b3..bc93a8e2a 100644 --- a/frontend/src/pages/PromotionPage/PromotionDetailPage.tsx +++ b/frontend/src/pages/PromotionPage/PromotionDetailPage.tsx @@ -10,16 +10,12 @@ import PromotionInfoSection from './components/detail/PromotionInfoSection/Promo import PromotionTitleSection from './components/detail/PromotionTitleSection/PromotionTitleSection'; import RelatedPromotionSection from './components/detail/RelatedPromotionSection/RelatedPromotionSection'; import * as Styled from './PromotionDetailPage.styles'; -import { dummyPromotionArticles } from './data/dummyActiveEvent'; const PromotionDetailPage = () => { useTrackPageView(PAGE_VIEW.PROMOTION_DETAIL_PAGE); const { promotionId } = useParams<{ promotionId: string }>(); - // const { data, isLoading, isError } = useGetPromotionArticles(); - const data = dummyPromotionArticles; - const isLoading = false; - const isError = false; + const { data, isLoading, isError } = useGetPromotionArticles(); const article = data?.find((item) => item.clubId === promotionId) ?? null; diff --git a/frontend/src/pages/PromotionPage/PromotionListPage.tsx b/frontend/src/pages/PromotionPage/PromotionListPage.tsx index 0a9548dee..2575dca67 100644 --- a/frontend/src/pages/PromotionPage/PromotionListPage.tsx +++ b/frontend/src/pages/PromotionPage/PromotionListPage.tsx @@ -6,16 +6,12 @@ import { useGetPromotionArticles } from '@/hooks/Queries/usePromotion'; import isInAppWebView from '@/utils/isInAppWebView'; import Filter from '../MainPage/components/Filter/Filter'; import PromottionGrid from './components/list/PromotionGrid/PromotionGrid'; -import { dummyPromotionArticles } from './data/dummyActiveEvent'; import * as Styled from './PromotionListPage.styles'; const PromotionListPage = () => { useTrackPageView(PAGE_VIEW.PROMOTION_LIST_PAGE); - // const { data, isLoading, isError } = useGetPromotionArticles(); - const data = dummyPromotionArticles; - const isLoading = false; - const isError = false; + const { data, isLoading, isError } = useGetPromotionArticles(); return ( <> diff --git a/frontend/src/pages/PromotionPage/data/dummyActiveEvent.ts b/frontend/src/pages/PromotionPage/data/dummyActiveEvent.ts deleted file mode 100644 index 3a48030ae..000000000 --- a/frontend/src/pages/PromotionPage/data/dummyActiveEvent.ts +++ /dev/null @@ -1,38 +0,0 @@ -import { PromotionArticle } from '@/types/promotion'; - -export const dummyPromotionArticles: PromotionArticle[] = [ - { - clubName: '백경미술연구회', - clubId: 'dummy-1', - title: 'EXODUS : 대탈출ㅇㅇㅇㅇㅇㅇㅇㅇㅇㅇㅇ', - location: '부경대학교 향파관 3층', - eventStartDate: '2026-03-05', // 진행 예정 - eventEndDate: '2026-12-31', - description: '테스트 진행 중 행사입니다.dkkkkkkkkkkkkkkkkkkkkkkk', - images: [ - 'https://picsum.photos/800/900', - 'https://picsum.photos/800/1000', - 'https://picsum.photos/800/1100', - ], - }, - { - clubName: 'WAP', - clubId: 'dummy-2', - title: '🔥 해커톤 모집 🔥', - location: '온라인', - eventStartDate: '2026-11-15', - eventEndDate: '2026-11-15', - description: '해커톤 테스트 행사입니다.', - images: ['https://picsum.photos/800/1000'], - }, - { - clubName: '종료 테스트', - clubId: 'dummy-3', - title: '지난 행사', - location: '테스트 장소', - eventStartDate: '2023-01-01', // 종료된 행사 - eventEndDate: '2023-01-01', - description: '이미 종료된 행사입니다.', - images: [], - }, -]; From d54d3566954f0a5e492c061f3c3cee269e38d2fe Mon Sep 17 00:00:00 2001 From: suhyun113 <163711629+suhyun113@users.noreply.github.com> Date: Sun, 8 Mar 2026 21:59:16 +0900 Subject: [PATCH 024/172] =?UTF-8?q?feat:=20=EB=8D=B0=EC=8A=A4=ED=81=AC?= =?UTF-8?q?=ED=83=91=20=EB=B2=84=EC=A0=84=20=EC=9D=B4=EB=B2=A4=ED=8A=B8=20?= =?UTF-8?q?=EC=A0=95=EB=B3=B4=20=ED=8E=98=EC=9D=B4=EC=A7=80=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../PromotionDetailPage.styles.ts | 38 +++++++++++++++---- .../PromotionPage/PromotionDetailPage.tsx | 34 ++++++++++++----- .../PromotionImageGallery.styles.ts | 8 ++++ .../PromotionTitleSection.styles.ts | 17 +++++++-- 4 files changed, 77 insertions(+), 20 deletions(-) diff --git a/frontend/src/pages/PromotionPage/PromotionDetailPage.styles.ts b/frontend/src/pages/PromotionPage/PromotionDetailPage.styles.ts index 1cfcc2cd2..ed24f1a4c 100644 --- a/frontend/src/pages/PromotionPage/PromotionDetailPage.styles.ts +++ b/frontend/src/pages/PromotionPage/PromotionDetailPage.styles.ts @@ -1,20 +1,45 @@ import styled from 'styled-components'; import { media } from '@/styles/mediaQuery'; +export const DesktopHeader = styled.div` + display: block; + + ${media.tablet} { + display: none; + } +`; + export const Container = styled.div` width: 100%; min-height: 100vh; background: #fff; `; +export const MobileTopBar = styled.div` + display: none; + + ${media.tablet} { + display: block; + } +`; + +export const TitleWrapper = styled.div` + max-width: 1180px; + margin: 40px auto 0 auto; + padding: 0 20px; + + ${media.tablet} { + display: none; + } +`; + export const ContentWrapper = styled.div` max-width: 1180px; width: 100%; - margin: 0 auto; + margin: 20px auto; display: flex; - gap: 24px; - margin-top: 100px; + gap: 40px; ${media.laptop} { padding: 0 20px; @@ -22,10 +47,8 @@ export const ContentWrapper = styled.div` ${media.tablet} { flex-direction: column; - padding: 0; gap: 0; - max-width: 100%; - margin-top: 0; + padding: 0; } `; @@ -34,6 +57,7 @@ export const LeftSection = styled.div` ${media.tablet} { width: 100%; + order: 2; } `; @@ -44,4 +68,4 @@ export const RightSection = styled.div` export const Message = styled.p` padding: 40px 18px; text-align: center; -`; \ No newline at end of file +`; diff --git a/frontend/src/pages/PromotionPage/PromotionDetailPage.tsx b/frontend/src/pages/PromotionPage/PromotionDetailPage.tsx index bc93a8e2a..ce846f751 100644 --- a/frontend/src/pages/PromotionPage/PromotionDetailPage.tsx +++ b/frontend/src/pages/PromotionPage/PromotionDetailPage.tsx @@ -1,5 +1,6 @@ import { useParams } from 'react-router-dom'; import Footer from '@/components/common/Footer/Footer'; +import Header from '@/components/common/Header/Header'; import { PAGE_VIEW } from '@/constants/eventName'; import useTrackPageView from '@/hooks/Mixpanel/useTrackPageView'; import { useGetPromotionArticles } from '@/hooks/Queries/usePromotion'; @@ -21,8 +22,13 @@ const PromotionDetailPage = () => { return ( <> + +
+ - + + + {isLoading && 로딩 중...} {isError && 오류가 발생했습니다.} @@ -33,14 +39,24 @@ const PromotionDetailPage = () => { {!isLoading && !isError && article && ( <> - - - - - + + + + + + + + + + + + + + + )} diff --git a/frontend/src/pages/PromotionPage/components/detail/PromotionImageGallery/PromotionImageGallery.styles.ts b/frontend/src/pages/PromotionPage/components/detail/PromotionImageGallery/PromotionImageGallery.styles.ts index 0a5fbe01e..958d807f6 100644 --- a/frontend/src/pages/PromotionPage/components/detail/PromotionImageGallery/PromotionImageGallery.styles.ts +++ b/frontend/src/pages/PromotionPage/components/detail/PromotionImageGallery/PromotionImageGallery.styles.ts @@ -1,7 +1,15 @@ +import { media } from '@/styles/mediaQuery'; import styled from 'styled-components'; export const Wrapper = styled.section` margin: 0; + + position: sticky; + top: 120px; + + ${media.tablet} { + position: static; + } `; export const ImageContainer = styled.div<{ $expanded: boolean }>` diff --git a/frontend/src/pages/PromotionPage/components/detail/PromotionTitleSection/PromotionTitleSection.styles.ts b/frontend/src/pages/PromotionPage/components/detail/PromotionTitleSection/PromotionTitleSection.styles.ts index 4b8b98c81..077695c4d 100644 --- a/frontend/src/pages/PromotionPage/components/detail/PromotionTitleSection/PromotionTitleSection.styles.ts +++ b/frontend/src/pages/PromotionPage/components/detail/PromotionTitleSection/PromotionTitleSection.styles.ts @@ -1,13 +1,18 @@ import styled from 'styled-components'; import { colors } from '@/styles/theme/colors'; +import { media } from '@/styles/mediaQuery'; export const Container = styled.section` - padding: 20px 21.5px 20px 21.5px; - display: flex; flex-direction: column; align-items: center; - text-align: center; + text-align: left; + + ${media.tablet} { + padding: 20px; + align-items: center; + text-align: center; + } `; export const TagWrapper = styled.div` @@ -15,8 +20,12 @@ export const TagWrapper = styled.div` `; export const Title = styled.h2` - font-size: 24px; + font-size: 28px; font-weight: 700; color: ${colors.gray[800]}; word-break: keep-all; + + ${media.tablet} { + font-size: 24px; + } `; \ No newline at end of file From 5682ba36a5cdef1c49308808e363a950a994b3bb Mon Sep 17 00:00:00 2001 From: suhyun113 <163711629+suhyun113@users.noreply.github.com> Date: Sun, 8 Mar 2026 22:06:40 +0900 Subject: [PATCH 025/172] =?UTF-8?q?feat:=20=EB=AA=A8=EB=B0=94=EC=9D=BC=20?= =?UTF-8?q?=EA=B8=B0=EC=A4=80=20=EC=83=81=EB=8B=A8=EB=B0=94=20=EA=B3=A0?= =?UTF-8?q?=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/pages/PromotionPage/PromotionDetailPage.styles.ts | 7 ++++++- .../PromotionDetailTopBar/PromotionDetailTopBar.styles.ts | 7 +++++++ 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/frontend/src/pages/PromotionPage/PromotionDetailPage.styles.ts b/frontend/src/pages/PromotionPage/PromotionDetailPage.styles.ts index ed24f1a4c..3ec22566f 100644 --- a/frontend/src/pages/PromotionPage/PromotionDetailPage.styles.ts +++ b/frontend/src/pages/PromotionPage/PromotionDetailPage.styles.ts @@ -1,5 +1,6 @@ import styled from 'styled-components'; import { media } from '@/styles/mediaQuery'; +import { colors } from '@/styles/theme/colors'; export const DesktopHeader = styled.div` display: block; @@ -17,9 +18,13 @@ export const Container = styled.div` export const MobileTopBar = styled.div` display: none; - + ${media.tablet} { display: block; + position: sticky; + top: 0; + z-index: 100; + background-color: ${colors.base.white}; } `; diff --git a/frontend/src/pages/PromotionPage/components/detail/PromotionDetailTopBar/PromotionDetailTopBar.styles.ts b/frontend/src/pages/PromotionPage/components/detail/PromotionDetailTopBar/PromotionDetailTopBar.styles.ts index c140bfd4d..8eb4845df 100644 --- a/frontend/src/pages/PromotionPage/components/detail/PromotionDetailTopBar/PromotionDetailTopBar.styles.ts +++ b/frontend/src/pages/PromotionPage/components/detail/PromotionDetailTopBar/PromotionDetailTopBar.styles.ts @@ -1,5 +1,6 @@ import styled from 'styled-components'; import { colors } from '@/styles/theme/colors'; +import { media } from '@/styles/mediaQuery'; export const Container = styled.header` position: relative; @@ -9,6 +10,12 @@ export const Container = styled.header` display: flex; align-items: center; padding: 0 18px; + + ${media.tablet} { + position: sticky; + top: 0; + z-index: 10; + } `; export const BackButton = styled.button` From fd4c8081f04dfe60a15e299320077de15e4feefc Mon Sep 17 00:00:00 2001 From: suhyun113 <163711629+suhyun113@users.noreply.github.com> Date: Wed, 11 Mar 2026 20:17:31 +0900 Subject: [PATCH 026/172] =?UTF-8?q?fix:=20location=20nullable=20=EC=A0=9C?= =?UTF-8?q?=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/src/apis/promotion.test.ts | 4 ++-- frontend/src/mocks/handlers/promotion.ts | 2 +- frontend/src/types/promotion.ts | 4 ++-- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/frontend/src/apis/promotion.test.ts b/frontend/src/apis/promotion.test.ts index 9d3700b68..3dc9c1c74 100644 --- a/frontend/src/apis/promotion.test.ts +++ b/frontend/src/apis/promotion.test.ts @@ -49,7 +49,7 @@ describe('promotion API', () => { clubName: '테스트 클럽 2', clubId: 'club2', title: '테스트 홍보글 2', - location: null, + location: '부산', eventStartDate: '2024-02-01', eventEndDate: '2024-02-28', description: '설명 2', @@ -138,7 +138,7 @@ describe('promotion API', () => { const mockPayload: CreatePromotionArticleRequest = { clubId: 'club1', title: '새로운 홍보글', - location: null, + location: '부산', eventStartDate: '2024-03-01', eventEndDate: '2024-03-31', description: '홍보 내용', diff --git a/frontend/src/mocks/handlers/promotion.ts b/frontend/src/mocks/handlers/promotion.ts index 1e990f5db..726cd3e56 100644 --- a/frontend/src/mocks/handlers/promotion.ts +++ b/frontend/src/mocks/handlers/promotion.ts @@ -22,7 +22,7 @@ export const mockPromotionArticles: PromotionArticle[] = [ clubName: '디자인 스터디', clubId: 'club-2', title: 'UI/UX 디자인 워크샵', - location: null, + location: '온라인 (Zoom)', eventStartDate: '2024-04-15', eventEndDate: '2024-04-20', description: '실무 디자이너와 함께하는 5일간의 집중 워크샵', diff --git a/frontend/src/types/promotion.ts b/frontend/src/types/promotion.ts index 7abc2838b..d9db78cc4 100644 --- a/frontend/src/types/promotion.ts +++ b/frontend/src/types/promotion.ts @@ -2,7 +2,7 @@ export interface PromotionArticle { clubName: string; clubId: string; title: string; - location: string | null; + location: string; eventStartDate: string; eventEndDate: string; description: string; @@ -12,7 +12,7 @@ export interface PromotionArticle { export interface CreatePromotionArticleRequest { clubId: string; title: string; - location: string | null; + location: string; eventStartDate: string; eventEndDate: string; description: string; From 5683f10cc30864dc5e8a926bdded4748c4d1bee4 Mon Sep 17 00:00:00 2001 From: suhyun113 <163711629+suhyun113@users.noreply.github.com> Date: Fri, 13 Mar 2026 01:52:46 +0900 Subject: [PATCH 027/172] =?UTF-8?q?style:=20=EC=83=81=EC=84=B8=20=ED=8E=98?= =?UTF-8?q?=EC=9D=B4=EC=A7=80=20=EB=AA=A8=EB=B0=94=EC=9D=BC=20=ED=83=80?= =?UTF-8?q?=EC=9D=B4=ED=8B=80=20=EA=B0=84=EA=B2=A9=20=EC=88=98=EC=A0=95=20?= =?UTF-8?q?=EB=B0=8F=20=EB=8D=B0=EC=8A=A4=ED=81=AC=ED=83=91=20=EC=9D=B4?= =?UTF-8?q?=EB=AF=B8=EC=A7=80=20=EC=A1=B0=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../PromotionDetailPage.styles.ts | 9 ++---- .../PromotionPage/PromotionListPage.styles.ts | 4 +-- .../PromotionImageGallery.styles.ts | 28 ++++++++++--------- .../PromotionTitleSection.styles.ts | 1 - 4 files changed, 20 insertions(+), 22 deletions(-) diff --git a/frontend/src/pages/PromotionPage/PromotionDetailPage.styles.ts b/frontend/src/pages/PromotionPage/PromotionDetailPage.styles.ts index 3ec22566f..cfeeb4de0 100644 --- a/frontend/src/pages/PromotionPage/PromotionDetailPage.styles.ts +++ b/frontend/src/pages/PromotionPage/PromotionDetailPage.styles.ts @@ -14,6 +14,7 @@ export const Container = styled.div` width: 100%; min-height: 100vh; background: #fff; + padding-top: 24px; `; export const MobileTopBar = styled.div` @@ -30,12 +31,8 @@ export const MobileTopBar = styled.div` export const TitleWrapper = styled.div` max-width: 1180px; - margin: 40px auto 0 auto; - padding: 0 20px; - - ${media.tablet} { - display: none; - } + margin: 0 auto; + padding: 20px 21.5px 0px; `; export const ContentWrapper = styled.div` diff --git a/frontend/src/pages/PromotionPage/PromotionListPage.styles.ts b/frontend/src/pages/PromotionPage/PromotionListPage.styles.ts index ab6c5098b..a0d345522 100644 --- a/frontend/src/pages/PromotionPage/PromotionListPage.styles.ts +++ b/frontend/src/pages/PromotionPage/PromotionListPage.styles.ts @@ -5,7 +5,7 @@ export const Container = styled.div` width: 100%; max-width: 550px; margin: 0 auto; - padding-top: 24px; + padding-top: 22px; ${media.mobile} { padding-top: 0; @@ -14,5 +14,5 @@ export const Container = styled.div` export const Wrapper = styled.div` margin-top: 4px; - padding: 0px 20px 66px; 20px; + padding: 0px 20px 66px; `; diff --git a/frontend/src/pages/PromotionPage/components/detail/PromotionImageGallery/PromotionImageGallery.styles.ts b/frontend/src/pages/PromotionPage/components/detail/PromotionImageGallery/PromotionImageGallery.styles.ts index 958d807f6..256e9ab31 100644 --- a/frontend/src/pages/PromotionPage/components/detail/PromotionImageGallery/PromotionImageGallery.styles.ts +++ b/frontend/src/pages/PromotionPage/components/detail/PromotionImageGallery/PromotionImageGallery.styles.ts @@ -1,5 +1,5 @@ -import { media } from '@/styles/mediaQuery'; import styled from 'styled-components'; +import { media } from '@/styles/mediaQuery'; export const Wrapper = styled.section` margin: 0; @@ -15,8 +15,13 @@ export const Wrapper = styled.section` export const ImageContainer = styled.div<{ $expanded: boolean }>` position: relative; overflow: hidden; + border-radius: 20px; max-height: ${({ $expanded }) => ($expanded ? 'none' : '700px')}; + + ${media.tablet} { + border-radius: 0px; + } `; export const Image = styled.img` @@ -27,9 +32,6 @@ export const Image = styled.img` export const Gradient = styled.div` position: absolute; - bottom: 0; - left: 0; - right: 0; height: 180px; pointer-events: none; @@ -39,19 +41,19 @@ export const Gradient = styled.div` mask-image: linear-gradient( to bottom, - rgba(0,0,0,0) 0%, - rgba(0,0,0,0.2) 30%, - rgba(0,0,0,0.5) 60%, - rgba(0,0,0,0.8) 80%, - rgba(0,0,0,1) 100% + rgba(0, 0, 0, 0) 0%, + rgba(0, 0, 0, 0.2) 30%, + rgba(0, 0, 0, 0.5) 60%, + rgba(0, 0, 0, 0.8) 80%, + rgba(0, 0, 0, 1) 100% ); background: linear-gradient( to bottom, - rgba(255,255,255,0) 0%, - rgba(255,255,255,0.2) 40%, - rgba(255,255,255,0.6) 70%, - rgba(255,255,255,1) 100% + rgba(255, 255, 255, 0) 0%, + rgba(255, 255, 255, 0.2) 40%, + rgba(255, 255, 255, 0.6) 70%, + rgba(255, 255, 255, 1) 100% ); `; diff --git a/frontend/src/pages/PromotionPage/components/detail/PromotionTitleSection/PromotionTitleSection.styles.ts b/frontend/src/pages/PromotionPage/components/detail/PromotionTitleSection/PromotionTitleSection.styles.ts index 077695c4d..2bd0168d6 100644 --- a/frontend/src/pages/PromotionPage/components/detail/PromotionTitleSection/PromotionTitleSection.styles.ts +++ b/frontend/src/pages/PromotionPage/components/detail/PromotionTitleSection/PromotionTitleSection.styles.ts @@ -9,7 +9,6 @@ export const Container = styled.section` text-align: left; ${media.tablet} { - padding: 20px; align-items: center; text-align: center; } From 0023079cf402b5327628d7c7de1cf4f1f185fa72 Mon Sep 17 00:00:00 2001 From: suhyun113 <163711629+suhyun113@users.noreply.github.com> Date: Fri, 13 Mar 2026 02:19:37 +0900 Subject: [PATCH 028/172] =?UTF-8?q?feat:=20=EA=B4=80=EB=A0=A8=20=EB=8F=99?= =?UTF-8?q?=EC=95=84=EB=A6=AC=20=EC=BB=B4=ED=8F=AC=EB=84=8C=ED=8A=B8=20?= =?UTF-8?q?=EB=B9=84=ED=99=9C=EC=84=B1=ED=99=94?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/src/mocks/handlers/promotion.ts | 8 ++++---- .../pages/PromotionPage/PromotionDetailPage.tsx | 16 ++++++++++++---- 2 files changed, 16 insertions(+), 8 deletions(-) diff --git a/frontend/src/mocks/handlers/promotion.ts b/frontend/src/mocks/handlers/promotion.ts index 726cd3e56..2d6843524 100644 --- a/frontend/src/mocks/handlers/promotion.ts +++ b/frontend/src/mocks/handlers/promotion.ts @@ -11,8 +11,8 @@ export const mockPromotionArticles: PromotionArticle[] = [ clubId: '67e54ae51cfd27718dd40bec', title: '💌✨WAP 최종 전시회 초대장 ✨💌', location: '부경대학교 동원 장보고관 1층', - eventStartDate: '2025-11-28 09:00', - eventEndDate: '2025-11-28', + eventStartDate: '2025-11-28 06:00', + eventEndDate: '2025-11-28 09:00', description: 'WAP 최종 전시회에 여러분을 초대합니다! \n\n이번 전시회에서는 WAP 팀이 한 학기 동안 열심히 준비한 프로젝트들을 선보입니다. 다양한 작품과 아이디어가 가득한 이번 전시회에서 여러분의 많은 관심과 참여 부탁드립니다! 🙌\n\n#WAP #최종전시회 #부경대학교', images: [ 'https://images.unsplash.com/photo-1522071820081-009f0129c71c?w=800', @@ -23,8 +23,8 @@ export const mockPromotionArticles: PromotionArticle[] = [ clubId: 'club-2', title: 'UI/UX 디자인 워크샵', location: '온라인 (Zoom)', - eventStartDate: '2024-04-15', - eventEndDate: '2024-04-20', + eventStartDate: '2026-04-15', + eventEndDate: '2026-04-20', description: '실무 디자이너와 함께하는 5일간의 집중 워크샵', images: [], }, diff --git a/frontend/src/pages/PromotionPage/PromotionDetailPage.tsx b/frontend/src/pages/PromotionPage/PromotionDetailPage.tsx index ce846f751..884cc6a98 100644 --- a/frontend/src/pages/PromotionPage/PromotionDetailPage.tsx +++ b/frontend/src/pages/PromotionPage/PromotionDetailPage.tsx @@ -19,6 +19,7 @@ const PromotionDetailPage = () => { const { data, isLoading, isError } = useGetPromotionArticles(); const article = data?.find((item) => item.clubId === promotionId) ?? null; + const showRelatedPromotion = false; // 관련 이벤트 추천 기능은 현재 비활성화 상태 return ( <> @@ -47,10 +48,17 @@ const PromotionDetailPage = () => { - + {/* + TODO: 관련 이벤트 추천 기능 + 현재는 기획 미정으로 비활성화 상태. + showRelatedPromotion 값을 true로 변경하면 활성화됨. + */} + {showRelatedPromotion && ( + + )} From 073eec1870864a72c03c19ab7c0fb7b825232ac8 Mon Sep 17 00:00:00 2001 From: suhyun113 <163711629+suhyun113@users.noreply.github.com> Date: Sun, 15 Mar 2026 04:28:17 +0900 Subject: [PATCH 029/172] =?UTF-8?q?style:=20=EC=83=81=EC=84=B8=ED=8E=98?= =?UTF-8?q?=EC=9D=B4=EC=A7=80=20=ED=83=80=EC=9D=B4=ED=8B=80=20=EC=84=B9?= =?UTF-8?q?=EC=85=98=20=EA=B5=AC=EB=B6=84=EC=84=A0=20=EC=B6=94=EA=B0=80=20?= =?UTF-8?q?=EB=B0=8F=20=EC=97=AC=EB=B0=B1=20=EC=A1=B0=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/src/mocks/handlers/promotion.ts | 2 ++ .../PromotionPage/PromotionDetailPage.styles.ts | 16 ++++++++++------ .../PromotionClubCTA/PromotionClubCTA.styles.ts | 9 ++++++++- .../PromotionImageGallery.styles.ts | 14 +++++++------- .../PromotionInfoSection.styles.ts | 7 ++++++- .../PromotionTitleSection.styles.ts | 15 +++++++++++++-- .../PromotionTitleSection.tsx | 1 + 7 files changed, 47 insertions(+), 17 deletions(-) diff --git a/frontend/src/mocks/handlers/promotion.ts b/frontend/src/mocks/handlers/promotion.ts index 2d6843524..0df98d424 100644 --- a/frontend/src/mocks/handlers/promotion.ts +++ b/frontend/src/mocks/handlers/promotion.ts @@ -16,6 +16,8 @@ export const mockPromotionArticles: PromotionArticle[] = [ description: 'WAP 최종 전시회에 여러분을 초대합니다! \n\n이번 전시회에서는 WAP 팀이 한 학기 동안 열심히 준비한 프로젝트들을 선보입니다. 다양한 작품과 아이디어가 가득한 이번 전시회에서 여러분의 많은 관심과 참여 부탁드립니다! 🙌\n\n#WAP #최종전시회 #부경대학교', images: [ 'https://images.unsplash.com/photo-1522071820081-009f0129c71c?w=800', + 'https://images.unsplash.com/photo-1522071820081-009f0129c71c?w=800', + 'https://images.unsplash.com/photo-1522071820081-009f0129c71c?w=800', ], }, { diff --git a/frontend/src/pages/PromotionPage/PromotionDetailPage.styles.ts b/frontend/src/pages/PromotionPage/PromotionDetailPage.styles.ts index cfeeb4de0..6259d3537 100644 --- a/frontend/src/pages/PromotionPage/PromotionDetailPage.styles.ts +++ b/frontend/src/pages/PromotionPage/PromotionDetailPage.styles.ts @@ -4,7 +4,7 @@ import { colors } from '@/styles/theme/colors'; export const DesktopHeader = styled.div` display: block; - + padding-top: 98px; ${media.tablet} { display: none; } @@ -14,7 +14,6 @@ export const Container = styled.div` width: 100%; min-height: 100vh; background: #fff; - padding-top: 24px; `; export const MobileTopBar = styled.div` @@ -31,8 +30,12 @@ export const MobileTopBar = styled.div` export const TitleWrapper = styled.div` max-width: 1180px; - margin: 0 auto; - padding: 20px 21.5px 0px; + margin: 0px auto; + padding: 20px auto 0px; + + ${media.tablet} { + margin: 20px auto; + } `; export const ContentWrapper = styled.div` @@ -41,15 +44,16 @@ export const ContentWrapper = styled.div` margin: 20px auto; display: flex; - gap: 40px; + gap: 50px; ${media.laptop} { padding: 0 20px; + gap: 30px; } ${media.tablet} { flex-direction: column; - gap: 0; + gap: 0px; padding: 0; } `; diff --git a/frontend/src/pages/PromotionPage/components/detail/PromotionClubCTA/PromotionClubCTA.styles.ts b/frontend/src/pages/PromotionPage/components/detail/PromotionClubCTA/PromotionClubCTA.styles.ts index 9e26210c8..cbd8af2a3 100644 --- a/frontend/src/pages/PromotionPage/components/detail/PromotionClubCTA/PromotionClubCTA.styles.ts +++ b/frontend/src/pages/PromotionPage/components/detail/PromotionClubCTA/PromotionClubCTA.styles.ts @@ -1,8 +1,15 @@ import styled from 'styled-components'; +import { media } from '@/styles/mediaQuery'; import { colors } from '@/styles/theme/colors'; export const Container = styled.section` - padding: 0px 20px 24px 20px; + padding: 0px 0px 0px 0px; + margin: 16px 0px; + + ${media.tablet} { + padding: 0px 20px 24px 20px; + margin: 0px 0px; + } `; export const Question = styled.div` diff --git a/frontend/src/pages/PromotionPage/components/detail/PromotionImageGallery/PromotionImageGallery.styles.ts b/frontend/src/pages/PromotionPage/components/detail/PromotionImageGallery/PromotionImageGallery.styles.ts index 256e9ab31..a566a87d6 100644 --- a/frontend/src/pages/PromotionPage/components/detail/PromotionImageGallery/PromotionImageGallery.styles.ts +++ b/frontend/src/pages/PromotionPage/components/detail/PromotionImageGallery/PromotionImageGallery.styles.ts @@ -15,19 +15,19 @@ export const Wrapper = styled.section` export const ImageContainer = styled.div<{ $expanded: boolean }>` position: relative; overflow: hidden; - border-radius: 20px; - max-height: ${({ $expanded }) => ($expanded ? 'none' : '700px')}; - - ${media.tablet} { - border-radius: 0px; - } `; export const Image = styled.img` width: 100%; display: block; - margin-bottom: 2px; + margin-bottom: 3px; + border-radius: 20px; + + ${media.tablet} { + border-radius: 0px; + margin-bottom: 2px; + } `; export const Gradient = styled.div` diff --git a/frontend/src/pages/PromotionPage/components/detail/PromotionInfoSection/PromotionInfoSection.styles.ts b/frontend/src/pages/PromotionPage/components/detail/PromotionInfoSection/PromotionInfoSection.styles.ts index c3e4346a2..847bca371 100644 --- a/frontend/src/pages/PromotionPage/components/detail/PromotionInfoSection/PromotionInfoSection.styles.ts +++ b/frontend/src/pages/PromotionPage/components/detail/PromotionInfoSection/PromotionInfoSection.styles.ts @@ -1,8 +1,13 @@ import styled from 'styled-components'; import { colors } from '@/styles/theme/colors'; +import { media } from '@/styles/mediaQuery'; export const Container = styled.section` - padding: 24px 20px; + padding: 0px 0px; + + ${media.tablet} { + padding: 24px 20px; + } `; export const SectionTitle = styled.h3` diff --git a/frontend/src/pages/PromotionPage/components/detail/PromotionTitleSection/PromotionTitleSection.styles.ts b/frontend/src/pages/PromotionPage/components/detail/PromotionTitleSection/PromotionTitleSection.styles.ts index 2bd0168d6..0c910d8bd 100644 --- a/frontend/src/pages/PromotionPage/components/detail/PromotionTitleSection/PromotionTitleSection.styles.ts +++ b/frontend/src/pages/PromotionPage/components/detail/PromotionTitleSection/PromotionTitleSection.styles.ts @@ -1,6 +1,6 @@ import styled from 'styled-components'; -import { colors } from '@/styles/theme/colors'; import { media } from '@/styles/mediaQuery'; +import { colors } from '@/styles/theme/colors'; export const Container = styled.section` display: flex; @@ -27,4 +27,15 @@ export const Title = styled.h2` ${media.tablet} { font-size: 24px; } -`; \ No newline at end of file +`; + +export const Divider = styled.div` + width: 100%; + height: 1px; + background-color: ${colors.gray[500]}; + margin: 20px 0; + + ${media.tablet} { + display: none; + } +`; diff --git a/frontend/src/pages/PromotionPage/components/detail/PromotionTitleSection/PromotionTitleSection.tsx b/frontend/src/pages/PromotionPage/components/detail/PromotionTitleSection/PromotionTitleSection.tsx index f35df692d..1baea4406 100644 --- a/frontend/src/pages/PromotionPage/components/detail/PromotionTitleSection/PromotionTitleSection.tsx +++ b/frontend/src/pages/PromotionPage/components/detail/PromotionTitleSection/PromotionTitleSection.tsx @@ -16,6 +16,7 @@ const PromotionTitleSection = ({ article }: Props) => { {article.title} + ); }; From e825ac1cf606d481d2a2a18f840cf592358a5dc4 Mon Sep 17 00:00:00 2001 From: suhyun113 <163711629+suhyun113@users.noreply.github.com> Date: Sun, 15 Mar 2026 21:34:38 +0900 Subject: [PATCH 030/172] =?UTF-8?q?fix:=20=EC=9D=B4=EB=AF=B8=EC=A7=80=20?= =?UTF-8?q?=EA=B2=BD=EA=B3=84=20=EB=B8=94=EB=9F=AC=20=EC=B2=98=EB=A6=AC=20?= =?UTF-8?q?=EB=88=84=EB=9D=BD=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/src/mocks/handlers/promotion.ts | 6 ++++-- .../PromotionImageGallery/PromotionImageGallery.styles.ts | 2 ++ 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/frontend/src/mocks/handlers/promotion.ts b/frontend/src/mocks/handlers/promotion.ts index 0df98d424..866638912 100644 --- a/frontend/src/mocks/handlers/promotion.ts +++ b/frontend/src/mocks/handlers/promotion.ts @@ -13,7 +13,8 @@ export const mockPromotionArticles: PromotionArticle[] = [ location: '부경대학교 동원 장보고관 1층', eventStartDate: '2025-11-28 06:00', eventEndDate: '2025-11-28 09:00', - description: 'WAP 최종 전시회에 여러분을 초대합니다! \n\n이번 전시회에서는 WAP 팀이 한 학기 동안 열심히 준비한 프로젝트들을 선보입니다. 다양한 작품과 아이디어가 가득한 이번 전시회에서 여러분의 많은 관심과 참여 부탁드립니다! 🙌\n\n#WAP #최종전시회 #부경대학교', + description: + 'WAP 최종 전시회에 여러분을 초대합니다! \n\n이번 전시회에서는 WAP 팀이 한 학기 동안 열심히 준비한 프로젝트들을 선보입니다. 다양한 작품과 아이디어가 가득한 이번 전시회에서 여러분의 많은 관심과 참여 부탁드립니다! 🙌\n\n#WAP #최종전시회 #부경대학교', images: [ 'https://images.unsplash.com/photo-1522071820081-009f0129c71c?w=800', 'https://images.unsplash.com/photo-1522071820081-009f0129c71c?w=800', @@ -40,7 +41,8 @@ export const mockPromotionArticles: PromotionArticle[] = [ description: '학생 창업팀의 아이디어를 공유하는 자리', images: [ 'https://images.unsplash.com/photo-1559136555-9303baea8ebd?w=800', - 'https://images.unsplash.com/photo-2540575467063-178a50c2df87?w=800', + 'https://images.unsplash.com/photo-1559136555-9303baea8ebd?w=800', + 'https://images.unsplash.com/photo-1559136555-9303baea8ebd?w=800', ], }, ]; diff --git a/frontend/src/pages/PromotionPage/components/detail/PromotionImageGallery/PromotionImageGallery.styles.ts b/frontend/src/pages/PromotionPage/components/detail/PromotionImageGallery/PromotionImageGallery.styles.ts index a566a87d6..34ba96aa2 100644 --- a/frontend/src/pages/PromotionPage/components/detail/PromotionImageGallery/PromotionImageGallery.styles.ts +++ b/frontend/src/pages/PromotionPage/components/detail/PromotionImageGallery/PromotionImageGallery.styles.ts @@ -32,6 +32,8 @@ export const Image = styled.img` export const Gradient = styled.div` position: absolute; + bottom: 0; + width: 100%; height: 180px; pointer-events: none; From 7b0b981b1d417711f3bfc1fb38c60cbb233038af Mon Sep 17 00:00:00 2001 From: suhyun113 <163711629+suhyun113@users.noreply.github.com> Date: Mon, 16 Mar 2026 19:46:40 +0900 Subject: [PATCH 031/172] =?UTF-8?q?feat:=20=EB=8D=B0=EC=8A=A4=ED=81=AC?= =?UTF-8?q?=ED=83=91=EC=9D=BC=EB=95=8C=20=ED=99=8D=EB=B3=B4=EC=9D=B4?= =?UTF-8?q?=EB=B2=A4=ED=8A=B8=20=ED=97=A4=EB=8D=94=20=EC=B6=94=EA=B0=80=20?= =?UTF-8?q?=EB=B0=8F=20=EC=B9=A9=20=EB=B2=84=ED=8A=BC=20=EC=A0=9C=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/components/common/Footer/Footer.tsx | 1 + .../src/components/common/Header/Header.tsx | 4 +- frontend/src/constants/eventName.ts | 1 + .../hooks/Header/useHeaderNavigation.test.ts | 46 +++++++++++++++---- .../src/hooks/Header/useHeaderNavigation.ts | 7 ++- .../PromotionPage/PromotionListPage.styles.ts | 6 +-- .../pages/PromotionPage/PromotionListPage.tsx | 2 +- .../PromotionImageGallery.styles.ts | 1 + 8 files changed, 51 insertions(+), 17 deletions(-) diff --git a/frontend/src/components/common/Footer/Footer.tsx b/frontend/src/components/common/Footer/Footer.tsx index 4e7b6dee1..ac5307e96 100644 --- a/frontend/src/components/common/Footer/Footer.tsx +++ b/frontend/src/components/common/Footer/Footer.tsx @@ -1,4 +1,5 @@ import * as Styled from './Footer.styles'; +import { useNavigate } from 'react-router-dom'; const Footer = () => { return ( diff --git a/frontend/src/components/common/Header/Header.tsx b/frontend/src/components/common/Header/Header.tsx index 6e8125683..ac4def0b9 100644 --- a/frontend/src/components/common/Header/Header.tsx +++ b/frontend/src/components/common/Header/Header.tsx @@ -29,7 +29,7 @@ const Header = ({ showOn, hideOn }: HeaderProps) => { handleHomeClick, handleIntroduceClick, handleClubUnionClick, - handleAdminClick, + handlePromotionClick, } = useHeaderNavigation(); const isAdminPage = location.pathname.startsWith('/admin'); @@ -71,7 +71,7 @@ const Header = ({ showOn, hideOn }: HeaderProps) => { handler: handleClubUnionClick, path: '/club-union', }, - { label: '관리자 페이지', handler: handleAdminClick, path: '/admin' }, + { label: '홍보•이벤트', handler: handlePromotionClick, path: '/promotions' }, ]; const closeMenu = () => { diff --git a/frontend/src/constants/eventName.ts b/frontend/src/constants/eventName.ts index 2e39401f3..e3ce80afd 100644 --- a/frontend/src/constants/eventName.ts +++ b/frontend/src/constants/eventName.ts @@ -18,6 +18,7 @@ export const USER_EVENT = { MOBILE_HOME_BUTTON_CLICKED: 'Mobile Home Button Clicked', MOBILE_MENU_BUTTON_CLICKED: 'Mobile Menu Button Clicked', MOBILE_MENU_DELETE_BUTTON_CLICKED: 'Mobile Menubar delete Button Clicked', + PROMOTION_BUTTON_CLICKED: 'Promotion Button Clicked', ADMIN_BUTTON_CLICKED: 'Admin Button Clicked', // 탭 & 섹션 diff --git a/frontend/src/hooks/Header/useHeaderNavigation.test.ts b/frontend/src/hooks/Header/useHeaderNavigation.test.ts index 676394b63..0faee84a5 100644 --- a/frontend/src/hooks/Header/useHeaderNavigation.test.ts +++ b/frontend/src/hooks/Header/useHeaderNavigation.test.ts @@ -188,32 +188,58 @@ describe('useHeaderNavigation 테스트', () => { }); }); - describe('관리자 버튼 클릭 테스트', () => { - it('관리자 버튼 클릭 시 관리자 페이지로 이동한다', () => { + describe('홍보 게시판 버튼 클릭 테스트', () => { + it('홍보 게시판 버튼 클릭 시 홍보 페이지로 이동한다', () => { // Given const { result } = renderHook(() => useHeaderNavigation()); // When - result.current.handleAdminClick(); + result.current.handlePromotionClick(); // Then - expect(mockNavigate).toHaveBeenCalledWith('/admin'); + expect(mockNavigate).toHaveBeenCalledWith('/promotion'); }); - it('관리자 버튼 클릭 시 Mixpanel 이벤트를 전송한다', () => { + it('홍보 게시판 버튼 클릭 시 Mixpanel 이벤트를 전송한다', () => { // Given const { result } = renderHook(() => useHeaderNavigation()); // When - result.current.handleAdminClick(); + result.current.handlePromotionClick(); // Then expect(mockTrackEvent).toHaveBeenCalledWith( - USER_EVENT.ADMIN_BUTTON_CLICKED, + USER_EVENT.PROMOTION_BUTTON_CLICKED, ); }); }); + // describe('관리자 버튼 클릭 테스트', () => { + // it('관리자 버튼 클릭 시 관리자 페이지로 이동한다', () => { + // // Given + // const { result } = renderHook(() => useHeaderNavigation()); + + // // When + // result.current.handleAdminClick(); + + // // Then + // expect(mockNavigate).toHaveBeenCalledWith('/admin'); + // }); + + // it('관리자 버튼 클릭 시 Mixpanel 이벤트를 전송한다', () => { + // // Given + // const { result } = renderHook(() => useHeaderNavigation()); + + // // When + // result.current.handleAdminClick(); + + // // Then + // expect(mockTrackEvent).toHaveBeenCalledWith( + // USER_EVENT.ADMIN_BUTTON_CLICKED, + // ); + // }); + // }); + describe('반환값 검증 테스트', () => { it('모든 핸들러 함수를 반환한다', () => { // Given & When @@ -234,7 +260,7 @@ describe('useHeaderNavigation 테스트', () => { expect(typeof result.current.handleHomeClick).toBe('function'); expect(typeof result.current.handleIntroduceClick).toBe('function'); expect(typeof result.current.handleClubUnionClick).toBe('function'); - expect(typeof result.current.handleAdminClick).toBe('function'); + expect(typeof result.current.handlePromotionClick).toBe('function'); }); }); @@ -246,7 +272,7 @@ describe('useHeaderNavigation 테스트', () => { // When result.current.handleHomeClick(); result.current.handleIntroduceClick(); - result.current.handleAdminClick(); + result.current.handlePromotionClick(); // Then expect(mockNavigate).toHaveBeenCalledTimes(3); @@ -263,7 +289,7 @@ describe('useHeaderNavigation 테스트', () => { result.current.handleHomeClick(); result.current.handleIntroduceClick(); result.current.handleClubUnionClick(); - result.current.handleAdminClick(); + result.current.handlePromotionClick(); // Then expect(mockTrackEvent).toHaveBeenCalledTimes(4); diff --git a/frontend/src/hooks/Header/useHeaderNavigation.ts b/frontend/src/hooks/Header/useHeaderNavigation.ts index 1a418af5f..fc80c532d 100644 --- a/frontend/src/hooks/Header/useHeaderNavigation.ts +++ b/frontend/src/hooks/Header/useHeaderNavigation.ts @@ -27,6 +27,11 @@ const useHeaderNavigation = () => { trackEvent(USER_EVENT.CLUB_UNION_BUTTON_CLICKED); }, [navigate, trackEvent]); + const handlePromotionClick = useCallback(() => { + navigate('/promotions'); + trackEvent(USER_EVENT.PROMOTION_BUTTON_CLICKED); + }, [navigate, trackEvent]); + const handleAdminClick = useCallback(() => { navigate('/admin'); trackEvent(USER_EVENT.ADMIN_BUTTON_CLICKED); @@ -36,7 +41,7 @@ const useHeaderNavigation = () => { handleHomeClick, handleIntroduceClick, handleClubUnionClick, - handleAdminClick, + handlePromotionClick, }; }; diff --git a/frontend/src/pages/PromotionPage/PromotionListPage.styles.ts b/frontend/src/pages/PromotionPage/PromotionListPage.styles.ts index a0d345522..c38f67d35 100644 --- a/frontend/src/pages/PromotionPage/PromotionListPage.styles.ts +++ b/frontend/src/pages/PromotionPage/PromotionListPage.styles.ts @@ -5,7 +5,7 @@ export const Container = styled.div` width: 100%; max-width: 550px; margin: 0 auto; - padding-top: 22px; + padding-top: 92px; ${media.mobile} { padding-top: 0; @@ -13,6 +13,6 @@ export const Container = styled.div` `; export const Wrapper = styled.div` - margin-top: 4px; - padding: 0px 20px 66px; + margin-top: 4px; + padding: 0px 20px 66px; `; diff --git a/frontend/src/pages/PromotionPage/PromotionListPage.tsx b/frontend/src/pages/PromotionPage/PromotionListPage.tsx index 2575dca67..cf8168e41 100644 --- a/frontend/src/pages/PromotionPage/PromotionListPage.tsx +++ b/frontend/src/pages/PromotionPage/PromotionListPage.tsx @@ -17,7 +17,7 @@ const PromotionListPage = () => { <>
- {!isInAppWebView() && } + {!isInAppWebView() && } {isLoading &&

로딩 중...

} {isError &&

오류가 발생했습니다.

} diff --git a/frontend/src/pages/PromotionPage/components/detail/PromotionImageGallery/PromotionImageGallery.styles.ts b/frontend/src/pages/PromotionPage/components/detail/PromotionImageGallery/PromotionImageGallery.styles.ts index 34ba96aa2..eddb68248 100644 --- a/frontend/src/pages/PromotionPage/components/detail/PromotionImageGallery/PromotionImageGallery.styles.ts +++ b/frontend/src/pages/PromotionPage/components/detail/PromotionImageGallery/PromotionImageGallery.styles.ts @@ -35,6 +35,7 @@ export const Gradient = styled.div` bottom: 0; width: 100%; height: 180px; + z-index: 1; pointer-events: none; From 6eaa6aeefd0495d112383593900c380b6d1fe026 Mon Sep 17 00:00:00 2001 From: suhyun113 <163711629+suhyun113@users.noreply.github.com> Date: Mon, 16 Mar 2026 20:15:26 +0900 Subject: [PATCH 032/172] =?UTF-8?q?feat:=20=EA=B4=80=EB=A6=AC=EC=9E=90=20?= =?UTF-8?q?=ED=8E=98=EC=9D=B4=EC=A7=80=20=EB=B2=84=ED=8A=BC=20=ED=91=B8?= =?UTF-8?q?=ED=84=B0=EB=A1=9C=20=EB=B6=84=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../components/common/Footer/Footer.styles.ts | 43 +++++++++++++++- .../src/components/common/Footer/Footer.tsx | 38 ++++++++------ .../hooks/Header/useHeaderNavigation.test.ts | 50 +++++++++---------- .../src/hooks/Header/useHeaderNavigation.ts | 1 + 4 files changed, 90 insertions(+), 42 deletions(-) diff --git a/frontend/src/components/common/Footer/Footer.styles.ts b/frontend/src/components/common/Footer/Footer.styles.ts index ec21b26ea..fbcc3e646 100644 --- a/frontend/src/components/common/Footer/Footer.styles.ts +++ b/frontend/src/components/common/Footer/Footer.styles.ts @@ -1,5 +1,6 @@ import styled from 'styled-components'; import { media } from '@/styles/mediaQuery'; +import { colors } from '@/styles/theme/colors'; export const FooterContainer = styled.footer` text-align: left; @@ -11,14 +12,24 @@ export const Divider = styled.hr` border-top: 1px solid #c5c5c5; `; -export const FooterContent = styled.div` +export const LeftSection = styled.div` display: flex; flex-direction: column; + gap: 4px; +`; + +export const FooterContent = styled.div` + display: flex; + justify-content: space-between; + align-items: flex-end; padding: 20px 140px 30px 140px; line-height: 1.25rem; color: #818181; - ${media.mobile} { + ${media.tablet} { + flex-direction: column; + align-items: flex-start; + gap: 10px; font-size: 0.625rem; padding: 20px 20px 30px 20px; } @@ -47,3 +58,31 @@ export const EmailText = styled.p` } } `; + +export const AdminButton = styled.button` + background: ${colors.base.white}; + border: 1px solid ${colors.gray[400]}; + border-radius: 6px; + + padding: 6px 12px; + font-size: 0.75rem; + color: ${colors.gray[700]}; + + cursor: pointer; + transition: all 0.15s ease; + + &:hover { + background: ${colors.gray[100]}; + border-color: ${colors.gray[500]}; + color: ${colors.gray[800]}; + } + + &:active { + background: ${colors.gray[200]}; + } + + ${media.mobile} { + align-self: flex-start; + margin-top: 6px; + } +`; diff --git a/frontend/src/components/common/Footer/Footer.tsx b/frontend/src/components/common/Footer/Footer.tsx index ac5307e96..8f7894542 100644 --- a/frontend/src/components/common/Footer/Footer.tsx +++ b/frontend/src/components/common/Footer/Footer.tsx @@ -1,26 +1,34 @@ +import useHeaderNavigation from '@/hooks/Header/useHeaderNavigation'; import * as Styled from './Footer.styles'; -import { useNavigate } from 'react-router-dom'; const Footer = () => { + const { handleAdminClick } = useHeaderNavigation(); + return ( <> - - 개인정보 처리방침 - - - Copyright © moodong. All Rights Reserved - - - e-mail:{' '} - pknu.moadong@gmail.com - + + + 개인정보 처리방침 + + + Copyright © moodong. All Rights Reserved + + + e-mail:{' '} + pknu.moadong@gmail.com + + + + + 동아리 운영 페이지 + diff --git a/frontend/src/hooks/Header/useHeaderNavigation.test.ts b/frontend/src/hooks/Header/useHeaderNavigation.test.ts index 0faee84a5..27b5ebacb 100644 --- a/frontend/src/hooks/Header/useHeaderNavigation.test.ts +++ b/frontend/src/hooks/Header/useHeaderNavigation.test.ts @@ -214,31 +214,31 @@ describe('useHeaderNavigation 테스트', () => { }); }); - // describe('관리자 버튼 클릭 테스트', () => { - // it('관리자 버튼 클릭 시 관리자 페이지로 이동한다', () => { - // // Given - // const { result } = renderHook(() => useHeaderNavigation()); - - // // When - // result.current.handleAdminClick(); - - // // Then - // expect(mockNavigate).toHaveBeenCalledWith('/admin'); - // }); - - // it('관리자 버튼 클릭 시 Mixpanel 이벤트를 전송한다', () => { - // // Given - // const { result } = renderHook(() => useHeaderNavigation()); - - // // When - // result.current.handleAdminClick(); - - // // Then - // expect(mockTrackEvent).toHaveBeenCalledWith( - // USER_EVENT.ADMIN_BUTTON_CLICKED, - // ); - // }); - // }); + describe('관리자 버튼 클릭 테스트', () => { + it('관리자 버튼 클릭 시 관리자 페이지로 이동한다', () => { + // Given + const { result } = renderHook(() => useHeaderNavigation()); + + // When + result.current.handleAdminClick(); + + // Then + expect(mockNavigate).toHaveBeenCalledWith('/admin'); + }); + + it('관리자 버튼 클릭 시 Mixpanel 이벤트를 전송한다', () => { + // Given + const { result } = renderHook(() => useHeaderNavigation()); + + // When + result.current.handleAdminClick(); + + // Then + expect(mockTrackEvent).toHaveBeenCalledWith( + USER_EVENT.ADMIN_BUTTON_CLICKED, + ); + }); + }); describe('반환값 검증 테스트', () => { it('모든 핸들러 함수를 반환한다', () => { diff --git a/frontend/src/hooks/Header/useHeaderNavigation.ts b/frontend/src/hooks/Header/useHeaderNavigation.ts index fc80c532d..3097f4e3a 100644 --- a/frontend/src/hooks/Header/useHeaderNavigation.ts +++ b/frontend/src/hooks/Header/useHeaderNavigation.ts @@ -42,6 +42,7 @@ const useHeaderNavigation = () => { handleIntroduceClick, handleClubUnionClick, handlePromotionClick, + handleAdminClick, }; }; From c6a992f77b40e620b832dc195e917ada55bd0b7b Mon Sep 17 00:00:00 2001 From: suhyun113 <163711629+suhyun113@users.noreply.github.com> Date: Mon, 16 Mar 2026 20:29:35 +0900 Subject: [PATCH 033/172] =?UTF-8?q?style:=20=EB=8D=B0=ED=83=91=EC=97=90?= =?UTF-8?q?=EC=84=9C=20=ED=91=B8=ED=84=B0=20=EC=82=AC=EC=9D=B4=20=EC=97=AC?= =?UTF-8?q?=EB=B0=B1=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/pages/PromotionPage/PromotionDetailPage.styles.ts | 2 +- .../PromotionImageGallery/PromotionImageGallery.styles.ts | 6 +++++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/frontend/src/pages/PromotionPage/PromotionDetailPage.styles.ts b/frontend/src/pages/PromotionPage/PromotionDetailPage.styles.ts index 6259d3537..5b51afdfe 100644 --- a/frontend/src/pages/PromotionPage/PromotionDetailPage.styles.ts +++ b/frontend/src/pages/PromotionPage/PromotionDetailPage.styles.ts @@ -41,7 +41,7 @@ export const TitleWrapper = styled.div` export const ContentWrapper = styled.div` max-width: 1180px; width: 100%; - margin: 20px auto; + margin: 20px auto 66.49px; display: flex; gap: 50px; diff --git a/frontend/src/pages/PromotionPage/components/detail/PromotionImageGallery/PromotionImageGallery.styles.ts b/frontend/src/pages/PromotionPage/components/detail/PromotionImageGallery/PromotionImageGallery.styles.ts index eddb68248..acb098be7 100644 --- a/frontend/src/pages/PromotionPage/components/detail/PromotionImageGallery/PromotionImageGallery.styles.ts +++ b/frontend/src/pages/PromotionPage/components/detail/PromotionImageGallery/PromotionImageGallery.styles.ts @@ -61,5 +61,9 @@ export const Gradient = styled.div` `; export const ImageMoreButtonWrapper = styled.div` - padding: 0 20px; + padding: 0px 0px 32px; + + ${media.mobile} { + padding: 0 20px; + } `; From 25e80189a1cfd52c51a7c15cbd720061ec62160d Mon Sep 17 00:00:00 2001 From: suhyun113 <163711629+suhyun113@users.noreply.github.com> Date: Mon, 16 Mar 2026 20:37:29 +0900 Subject: [PATCH 034/172] =?UTF-8?q?refactor:=20prettier=20=EC=BD=94?= =?UTF-8?q?=EB=93=9C=20=ED=8F=AC=EB=A7=B7=20=EC=A0=95=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/components/common/Header/Header.tsx | 6 +++++- .../PromotionArrowButton.styles.ts | 2 +- .../PromotionArrowButton.tsx | 8 ++------ .../PromotionDetailTopBar.styles.ts | 2 +- .../PromotionDetailTopBar.tsx | 11 +++++----- .../PromotionInfoSection.styles.ts | 2 +- .../PromotionInfoSection.tsx | 2 +- .../PromotionTitleSection.tsx | 6 ++---- .../RelatedPromotionCard.styles.ts | 6 +++--- .../RelatedPromotionSection.tsx | 20 +++++-------------- .../DdayBadge/DdayBadge.styles.ts | 12 +++++------ .../PromotionCard/DdayBadge/DdayBadge.tsx | 2 +- .../PromotionCard/PromotionCard.styles.ts | 7 ++++--- .../list/PromotionCard/PromotionCard.tsx | 4 +--- .../PromotionGrid/PromotionGrid.styles.ts | 2 +- .../list/PromotionGrid/PromotionGrid.tsx | 5 +---- frontend/src/utils/getDday.ts | 5 ++--- 17 files changed, 42 insertions(+), 60 deletions(-) diff --git a/frontend/src/components/common/Header/Header.tsx b/frontend/src/components/common/Header/Header.tsx index ac4def0b9..3c0ad32ac 100644 --- a/frontend/src/components/common/Header/Header.tsx +++ b/frontend/src/components/common/Header/Header.tsx @@ -71,7 +71,11 @@ const Header = ({ showOn, hideOn }: HeaderProps) => { handler: handleClubUnionClick, path: '/club-union', }, - { label: '홍보•이벤트', handler: handlePromotionClick, path: '/promotions' }, + { + label: '홍보•이벤트', + handler: handlePromotionClick, + path: '/promotions', + }, ]; const closeMenu = () => { diff --git a/frontend/src/pages/PromotionPage/components/detail/PromotionArrowButton/PromotionArrowButton.styles.ts b/frontend/src/pages/PromotionPage/components/detail/PromotionArrowButton/PromotionArrowButton.styles.ts index 071be3157..ba394c15e 100644 --- a/frontend/src/pages/PromotionPage/components/detail/PromotionArrowButton/PromotionArrowButton.styles.ts +++ b/frontend/src/pages/PromotionPage/components/detail/PromotionArrowButton/PromotionArrowButton.styles.ts @@ -39,4 +39,4 @@ export const Arrow = styled.span<{ $direction: string }>` return 'rotate(0deg)'; } }}; -`; \ No newline at end of file +`; diff --git a/frontend/src/pages/PromotionPage/components/detail/PromotionArrowButton/PromotionArrowButton.tsx b/frontend/src/pages/PromotionPage/components/detail/PromotionArrowButton/PromotionArrowButton.tsx index aaf819640..71eb75842 100644 --- a/frontend/src/pages/PromotionPage/components/detail/PromotionArrowButton/PromotionArrowButton.tsx +++ b/frontend/src/pages/PromotionPage/components/detail/PromotionArrowButton/PromotionArrowButton.tsx @@ -7,11 +7,7 @@ interface Props { onClick: () => void; } -const PromotionArrowButton = ({ - text, - direction = 'down', - onClick, -}: Props) => { +const PromotionArrowButton = ({ text, direction = 'down', onClick }: Props) => { return ( @@ -24,4 +20,4 @@ const PromotionArrowButton = ({ ); }; -export default PromotionArrowButton; \ No newline at end of file +export default PromotionArrowButton; diff --git a/frontend/src/pages/PromotionPage/components/detail/PromotionDetailTopBar/PromotionDetailTopBar.styles.ts b/frontend/src/pages/PromotionPage/components/detail/PromotionDetailTopBar/PromotionDetailTopBar.styles.ts index 8eb4845df..2c6a120d7 100644 --- a/frontend/src/pages/PromotionPage/components/detail/PromotionDetailTopBar/PromotionDetailTopBar.styles.ts +++ b/frontend/src/pages/PromotionPage/components/detail/PromotionDetailTopBar/PromotionDetailTopBar.styles.ts @@ -1,6 +1,6 @@ import styled from 'styled-components'; -import { colors } from '@/styles/theme/colors'; import { media } from '@/styles/mediaQuery'; +import { colors } from '@/styles/theme/colors'; export const Container = styled.header` position: relative; diff --git a/frontend/src/pages/PromotionPage/components/detail/PromotionDetailTopBar/PromotionDetailTopBar.tsx b/frontend/src/pages/PromotionPage/components/detail/PromotionDetailTopBar/PromotionDetailTopBar.tsx index bc6ec641a..4890d24f0 100644 --- a/frontend/src/pages/PromotionPage/components/detail/PromotionDetailTopBar/PromotionDetailTopBar.tsx +++ b/frontend/src/pages/PromotionPage/components/detail/PromotionDetailTopBar/PromotionDetailTopBar.tsx @@ -15,14 +15,13 @@ const PromotionDetailTopBar = () => { return ( - - - - - 이벤트 정보 + + + + 이벤트 정보 ); }; -export default PromotionDetailTopBar; \ No newline at end of file +export default PromotionDetailTopBar; diff --git a/frontend/src/pages/PromotionPage/components/detail/PromotionInfoSection/PromotionInfoSection.styles.ts b/frontend/src/pages/PromotionPage/components/detail/PromotionInfoSection/PromotionInfoSection.styles.ts index 847bca371..64073aedc 100644 --- a/frontend/src/pages/PromotionPage/components/detail/PromotionInfoSection/PromotionInfoSection.styles.ts +++ b/frontend/src/pages/PromotionPage/components/detail/PromotionInfoSection/PromotionInfoSection.styles.ts @@ -1,6 +1,6 @@ import styled from 'styled-components'; -import { colors } from '@/styles/theme/colors'; import { media } from '@/styles/mediaQuery'; +import { colors } from '@/styles/theme/colors'; export const Container = styled.section` padding: 0px 0px; diff --git a/frontend/src/pages/PromotionPage/components/detail/PromotionInfoSection/PromotionInfoSection.tsx b/frontend/src/pages/PromotionPage/components/detail/PromotionInfoSection/PromotionInfoSection.tsx index 0d645abdb..8e5c4ab45 100644 --- a/frontend/src/pages/PromotionPage/components/detail/PromotionInfoSection/PromotionInfoSection.tsx +++ b/frontend/src/pages/PromotionPage/components/detail/PromotionInfoSection/PromotionInfoSection.tsx @@ -53,4 +53,4 @@ const PromotionInfoSection = ({ article }: Props) => { ); }; -export default PromotionInfoSection; \ No newline at end of file +export default PromotionInfoSection; diff --git a/frontend/src/pages/PromotionPage/components/detail/PromotionTitleSection/PromotionTitleSection.tsx b/frontend/src/pages/PromotionPage/components/detail/PromotionTitleSection/PromotionTitleSection.tsx index 1baea4406..0602d460c 100644 --- a/frontend/src/pages/PromotionPage/components/detail/PromotionTitleSection/PromotionTitleSection.tsx +++ b/frontend/src/pages/PromotionPage/components/detail/PromotionTitleSection/PromotionTitleSection.tsx @@ -13,12 +13,10 @@ const PromotionTitleSection = ({ article }: Props) => { - - {article.title} - + {article.title}
); }; -export default PromotionTitleSection; \ No newline at end of file +export default PromotionTitleSection; diff --git a/frontend/src/pages/PromotionPage/components/detail/RelatedPromotionSection/RelatedPromotionCard/RelatedPromotionCard.styles.ts b/frontend/src/pages/PromotionPage/components/detail/RelatedPromotionSection/RelatedPromotionCard/RelatedPromotionCard.styles.ts index a9c1b2c63..315e17f40 100644 --- a/frontend/src/pages/PromotionPage/components/detail/RelatedPromotionSection/RelatedPromotionCard/RelatedPromotionCard.styles.ts +++ b/frontend/src/pages/PromotionPage/components/detail/RelatedPromotionSection/RelatedPromotionCard/RelatedPromotionCard.styles.ts @@ -7,7 +7,7 @@ export const Card = styled.div` padding: 20px; cursor: pointer; - box-shadow: 0 6px 18px rgba(0,0,0,0.06); + box-shadow: 0 6px 18px rgba(0, 0, 0, 0.06); transition: transform 0.2s ease; @@ -17,5 +17,5 @@ export const Card = styled.div` `; export const ClubTagWrapper = styled.div` - margin-bottom: 6px; -`; \ No newline at end of file + margin-bottom: 6px; +`; diff --git a/frontend/src/pages/PromotionPage/components/detail/RelatedPromotionSection/RelatedPromotionSection.tsx b/frontend/src/pages/PromotionPage/components/detail/RelatedPromotionSection/RelatedPromotionSection.tsx index ac2d3fe8f..3bc6a8215 100644 --- a/frontend/src/pages/PromotionPage/components/detail/RelatedPromotionSection/RelatedPromotionSection.tsx +++ b/frontend/src/pages/PromotionPage/components/detail/RelatedPromotionSection/RelatedPromotionSection.tsx @@ -9,19 +9,13 @@ interface Props { articles: PromotionArticle[]; } -const RelatedPromotionSection = ({ - currentClubId, - articles, -}: Props) => { +const RelatedPromotionSection = ({ currentClubId, articles }: Props) => { const navigate = useNavigate(); const activeEvents = articles .filter((a) => { const dday = getDDay(a.eventStartDate); - return ( - a.clubId !== currentClubId && - dday >= 0 - ); + return a.clubId !== currentClubId && dday >= 0; }) .slice(0, 1); @@ -29,21 +23,17 @@ const RelatedPromotionSection = ({ return ( - - 이런 이벤트는 어때요? - + 이런 이벤트는 어때요? {activeEvents.map((event) => ( - navigate(`/promotions/${event.clubId}`) - } + onClick={() => navigate(`/promotions/${event.clubId}`)} /> ))} ); }; -export default RelatedPromotionSection; \ No newline at end of file +export default RelatedPromotionSection; diff --git a/frontend/src/pages/PromotionPage/components/list/PromotionCard/DdayBadge/DdayBadge.styles.ts b/frontend/src/pages/PromotionPage/components/list/PromotionCard/DdayBadge/DdayBadge.styles.ts index f528e7f7e..a3dc8861f 100644 --- a/frontend/src/pages/PromotionPage/components/list/PromotionCard/DdayBadge/DdayBadge.styles.ts +++ b/frontend/src/pages/PromotionPage/components/list/PromotionCard/DdayBadge/DdayBadge.styles.ts @@ -1,5 +1,5 @@ -import { colors } from '@/styles/theme/colors'; import styled from 'styled-components'; +import { colors } from '@/styles/theme/colors'; export const Container = styled.div` display: inline-flex; @@ -18,8 +18,8 @@ export const Container = styled.div` `; export const DdayText = styled.h1` - color: ${colors.gray[800]}; - font-size: 10px; - font-weight: 600; - letter-spacing: -0.02em; -`; \ No newline at end of file + color: ${colors.gray[800]}; + font-size: 10px; + font-weight: 600; + letter-spacing: -0.02em; +`; diff --git a/frontend/src/pages/PromotionPage/components/list/PromotionCard/DdayBadge/DdayBadge.tsx b/frontend/src/pages/PromotionPage/components/list/PromotionCard/DdayBadge/DdayBadge.tsx index 79bdeba9f..9661f03f3 100644 --- a/frontend/src/pages/PromotionPage/components/list/PromotionCard/DdayBadge/DdayBadge.tsx +++ b/frontend/src/pages/PromotionPage/components/list/PromotionCard/DdayBadge/DdayBadge.tsx @@ -22,4 +22,4 @@ const DdayBadge = ({ dday }: DdayBadgeProps) => { ); }; -export default DdayBadge; \ No newline at end of file +export default DdayBadge; diff --git a/frontend/src/pages/PromotionPage/components/list/PromotionCard/PromotionCard.styles.ts b/frontend/src/pages/PromotionPage/components/list/PromotionCard/PromotionCard.styles.ts index b757e5d22..b02f9097b 100644 --- a/frontend/src/pages/PromotionPage/components/list/PromotionCard/PromotionCard.styles.ts +++ b/frontend/src/pages/PromotionPage/components/list/PromotionCard/PromotionCard.styles.ts @@ -1,6 +1,6 @@ import styled from 'styled-components'; -import { colors } from '@/styles/theme/colors'; import { media } from '@/styles/mediaQuery'; +import { colors } from '@/styles/theme/colors'; export const Container = styled.div` width: 226px; @@ -13,7 +13,7 @@ export const Container = styled.div` ${media.mobile} { width: 200px; } - + ${media.mini_mobile} { width: 164px; } @@ -30,7 +30,8 @@ export const Image = styled.div<{ $imageUrl?: string }>` height: 100%; background-color: #ddd; - background-image: ${({ $imageUrl }) => ($imageUrl ? `url(${$imageUrl})` : 'none')}; + background-image: ${({ $imageUrl }) => + $imageUrl ? `url(${$imageUrl})` : 'none'}; background-size: cover; background-position: center; background-repeat: no-repeat; diff --git a/frontend/src/pages/PromotionPage/components/list/PromotionCard/PromotionCard.tsx b/frontend/src/pages/PromotionPage/components/list/PromotionCard/PromotionCard.tsx index 3432864b7..a2c49388b 100644 --- a/frontend/src/pages/PromotionPage/components/list/PromotionCard/PromotionCard.tsx +++ b/frontend/src/pages/PromotionPage/components/list/PromotionCard/PromotionCard.tsx @@ -10,9 +10,7 @@ interface PromotionCardProps { article: PromotionArticle; } -const PromotionCard = ({ - article, -}: PromotionCardProps) => { +const PromotionCard = ({ article }: PromotionCardProps) => { const navigateToPromotionDetail = useNavigate(); const dday = getDDay(article.eventStartDate); diff --git a/frontend/src/pages/PromotionPage/components/list/PromotionGrid/PromotionGrid.styles.ts b/frontend/src/pages/PromotionPage/components/list/PromotionGrid/PromotionGrid.styles.ts index cb389bf2b..6f7f92cff 100644 --- a/frontend/src/pages/PromotionPage/components/list/PromotionGrid/PromotionGrid.styles.ts +++ b/frontend/src/pages/PromotionPage/components/list/PromotionGrid/PromotionGrid.styles.ts @@ -1,5 +1,5 @@ -import { media } from '@/styles/mediaQuery'; import styled from 'styled-components'; +import { media } from '@/styles/mediaQuery'; export const Grid = styled.div` display: grid; diff --git a/frontend/src/pages/PromotionPage/components/list/PromotionGrid/PromotionGrid.tsx b/frontend/src/pages/PromotionPage/components/list/PromotionGrid/PromotionGrid.tsx index 833f86232..1efd103dd 100644 --- a/frontend/src/pages/PromotionPage/components/list/PromotionGrid/PromotionGrid.tsx +++ b/frontend/src/pages/PromotionPage/components/list/PromotionGrid/PromotionGrid.tsx @@ -10,10 +10,7 @@ const PromotionGrid = ({ articles }: PromotionGridProps) => { return ( {articles.map((article) => ( - + ))} ); diff --git a/frontend/src/utils/getDday.ts b/frontend/src/utils/getDday.ts index 5caf09029..e8c3afcab 100644 --- a/frontend/src/utils/getDday.ts +++ b/frontend/src/utils/getDday.ts @@ -6,9 +6,8 @@ export const getDDay = (startDate: string) => { start.setHours(0, 0, 0, 0); const diff = Math.ceil( - (start.getTime() - today.getTime()) / - (1000 * 60 * 60 * 24) + (start.getTime() - today.getTime()) / (1000 * 60 * 60 * 24), ); return diff; -}; \ No newline at end of file +}; From 2aea6f98b3006792124ffc7c9ae4c893e1e9bd45 Mon Sep 17 00:00:00 2001 From: suhyun113 <163711629+suhyun113@users.noreply.github.com> Date: Mon, 16 Mar 2026 20:51:26 +0900 Subject: [PATCH 035/172] =?UTF-8?q?fix:=20Storybook=20Router=20=EB=AC=B8?= =?UTF-8?q?=EC=A0=9C=20=ED=95=B4=EA=B2=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/components/common/Footer/Footer.stories.tsx | 10 ++++++++-- frontend/src/hooks/Header/useHeaderNavigation.test.ts | 2 +- 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/frontend/src/components/common/Footer/Footer.stories.tsx b/frontend/src/components/common/Footer/Footer.stories.tsx index a884f7b54..337b5d1ed 100644 --- a/frontend/src/components/common/Footer/Footer.stories.tsx +++ b/frontend/src/components/common/Footer/Footer.stories.tsx @@ -1,4 +1,4 @@ -import React from 'react'; +import { BrowserRouter } from 'react-router-dom'; import type { Meta, StoryObj } from '@storybook/react'; import Footer from './Footer'; @@ -8,7 +8,13 @@ const meta = { parameters: { layout: 'fullscreen', }, - decorators: [(Story) => ], + decorators: [ + (Story) => ( + + + + ), + ], tags: ['autodocs'], } satisfies Meta; diff --git a/frontend/src/hooks/Header/useHeaderNavigation.test.ts b/frontend/src/hooks/Header/useHeaderNavigation.test.ts index 27b5ebacb..f50ffccdc 100644 --- a/frontend/src/hooks/Header/useHeaderNavigation.test.ts +++ b/frontend/src/hooks/Header/useHeaderNavigation.test.ts @@ -197,7 +197,7 @@ describe('useHeaderNavigation 테스트', () => { result.current.handlePromotionClick(); // Then - expect(mockNavigate).toHaveBeenCalledWith('/promotion'); + expect(mockNavigate).toHaveBeenCalledWith('/promotions'); }); it('홍보 게시판 버튼 클릭 시 Mixpanel 이벤트를 전송한다', () => { From 668c7566ba0730ef816f95da4b63b6e6fa4995e8 Mon Sep 17 00:00:00 2001 From: suhyun113 <163711629+suhyun113@users.noreply.github.com> Date: Mon, 16 Mar 2026 20:58:17 +0900 Subject: [PATCH 036/172] =?UTF-8?q?test:=20=EB=84=A4=EB=B9=84=EA=B2=8C?= =?UTF-8?q?=EC=9D=B4=EC=85=98=20=ED=85=8C=EC=8A=A4=ED=8A=B8=20=EC=8B=A4?= =?UTF-8?q?=ED=8C=A8=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/src/hooks/Header/useHeaderNavigation.test.ts | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/frontend/src/hooks/Header/useHeaderNavigation.test.ts b/frontend/src/hooks/Header/useHeaderNavigation.test.ts index f50ffccdc..027dd20a5 100644 --- a/frontend/src/hooks/Header/useHeaderNavigation.test.ts +++ b/frontend/src/hooks/Header/useHeaderNavigation.test.ts @@ -249,6 +249,7 @@ describe('useHeaderNavigation 테스트', () => { expect(result.current).toHaveProperty('handleHomeClick'); expect(result.current).toHaveProperty('handleIntroduceClick'); expect(result.current).toHaveProperty('handleClubUnionClick'); + expect(result.current).toHaveProperty('handlePromotionClick'); expect(result.current).toHaveProperty('handleAdminClick'); }); @@ -261,6 +262,7 @@ describe('useHeaderNavigation 테스트', () => { expect(typeof result.current.handleIntroduceClick).toBe('function'); expect(typeof result.current.handleClubUnionClick).toBe('function'); expect(typeof result.current.handlePromotionClick).toBe('function'); + expect(typeof result.current.handleAdminClick).toBe('function'); }); }); @@ -273,12 +275,14 @@ describe('useHeaderNavigation 테스트', () => { result.current.handleHomeClick(); result.current.handleIntroduceClick(); result.current.handlePromotionClick(); + result.current.handleAdminClick(); // Then - expect(mockNavigate).toHaveBeenCalledTimes(3); + expect(mockNavigate).toHaveBeenCalledTimes(4); expect(mockNavigate).toHaveBeenNthCalledWith(1, '/'); expect(mockNavigate).toHaveBeenNthCalledWith(2, '/introduce'); - expect(mockNavigate).toHaveBeenNthCalledWith(3, '/admin'); + expect(mockNavigate).toHaveBeenNthCalledWith(3, '/promotions'); + expect(mockNavigate).toHaveBeenNthCalledWith(4, '/admin'); }); it('모든 네비게이션 액션이 Mixpanel 이벤트를 트리거한다', () => { @@ -290,9 +294,10 @@ describe('useHeaderNavigation 테스트', () => { result.current.handleIntroduceClick(); result.current.handleClubUnionClick(); result.current.handlePromotionClick(); + result.current.handleAdminClick(); // Then - expect(mockTrackEvent).toHaveBeenCalledTimes(4); + expect(mockTrackEvent).toHaveBeenCalledTimes(5); }); }); }); From 4a657c2e7a3cd327de39f6fc71a65df42ff7dad6 Mon Sep 17 00:00:00 2001 From: suhyun113 <163711629+suhyun113@users.noreply.github.com> Date: Tue, 17 Mar 2026 00:30:40 +0900 Subject: [PATCH 037/172] =?UTF-8?q?feat:=20=EB=8F=99=EC=95=84=EB=A6=AC=20?= =?UTF-8?q?=EC=A0=95=EB=B3=B4=20=EB=B3=B4=EB=9F=AC=EA=B0=80=EA=B8=B0=20?= =?UTF-8?q?=EB=9D=BC=EC=9A=B0=ED=8A=B8=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/src/pages/PromotionPage/PromotionDetailPage.tsx | 2 +- .../components/detail/PromotionClubCTA/PromotionClubCTA.tsx | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/frontend/src/pages/PromotionPage/PromotionDetailPage.tsx b/frontend/src/pages/PromotionPage/PromotionDetailPage.tsx index 884cc6a98..4ae127c4f 100644 --- a/frontend/src/pages/PromotionPage/PromotionDetailPage.tsx +++ b/frontend/src/pages/PromotionPage/PromotionDetailPage.tsx @@ -47,7 +47,7 @@ const PromotionDetailPage = () => { - + {/* TODO: 관련 이벤트 추천 기능 현재는 기획 미정으로 비활성화 상태. diff --git a/frontend/src/pages/PromotionPage/components/detail/PromotionClubCTA/PromotionClubCTA.tsx b/frontend/src/pages/PromotionPage/components/detail/PromotionClubCTA/PromotionClubCTA.tsx index f48253e5b..690662892 100644 --- a/frontend/src/pages/PromotionPage/components/detail/PromotionClubCTA/PromotionClubCTA.tsx +++ b/frontend/src/pages/PromotionPage/components/detail/PromotionClubCTA/PromotionClubCTA.tsx @@ -3,14 +3,14 @@ import ArrowButton from '../PromotionArrowButton/PromotionArrowButton'; import * as Styled from './PromotionClubCTA.styles'; interface Props { - clubId: string; + clubName: string; } -const PromotionClubCTA = ({ clubId }: Props) => { +const PromotionClubCTA = ({ clubName }: Props) => { const navigate = useNavigate(); const handleNavigate = () => { - navigate(`/clubs/${clubId}`); + navigate(`/clubDetail/@${clubName}`); }; return ( From e9eef2fbf2be9fe5f4a84ca215b3ca57bdeb87a7 Mon Sep 17 00:00:00 2001 From: suhyun113 <163711629+suhyun113@users.noreply.github.com> Date: Tue, 17 Mar 2026 00:59:59 +0900 Subject: [PATCH 038/172] =?UTF-8?q?fix:=20=EC=84=9C=EB=B2=84=20=EB=8D=B0?= =?UTF-8?q?=EC=9D=B4=ED=84=B0=20=ED=98=B8=EC=B6=9C=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 --- frontend/src/mocks/handlers/promotion.ts | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/frontend/src/mocks/handlers/promotion.ts b/frontend/src/mocks/handlers/promotion.ts index 866638912..4565763bf 100644 --- a/frontend/src/mocks/handlers/promotion.ts +++ b/frontend/src/mocks/handlers/promotion.ts @@ -50,11 +50,11 @@ export const mockPromotionArticles: PromotionArticle[] = [ // MSW 핸들러 export const promotionHandlers = [ // GET /api/promotion - 홍보게시판 목록 조회 - http.get(`${API_BASE_URL}/api/promotion`, () => { - return HttpResponse.json({ - articles: mockPromotionArticles, - }); - }), + // http.get(`${API_BASE_URL}/api/promotion`, () => { + // return HttpResponse.json({ + // articles: mockPromotionArticles, + // }); + // }), // POST /api/promotion - 홍보게시판 글 작성 http.post(`${API_BASE_URL}/api/promotion`, async ({ request }) => { From c9b2f992e369c34e9d0b1d2835dedacec2945fcc Mon Sep 17 00:00:00 2001 From: suhyun113 <163711629+suhyun113@users.noreply.github.com> Date: Tue, 17 Mar 2026 02:04:04 +0900 Subject: [PATCH 039/172] =?UTF-8?q?style:=20glass=20=EC=86=8D=EC=84=B1=20?= =?UTF-8?q?=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit style: glass 속성 수정 --- .../list/PromotionCard/DdayBadge/DdayBadge.styles.ts | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/frontend/src/pages/PromotionPage/components/list/PromotionCard/DdayBadge/DdayBadge.styles.ts b/frontend/src/pages/PromotionPage/components/list/PromotionCard/DdayBadge/DdayBadge.styles.ts index a3dc8861f..3ae56f4ec 100644 --- a/frontend/src/pages/PromotionPage/components/list/PromotionCard/DdayBadge/DdayBadge.styles.ts +++ b/frontend/src/pages/PromotionPage/components/list/PromotionCard/DdayBadge/DdayBadge.styles.ts @@ -10,11 +10,12 @@ export const Container = styled.div` border-radius: 50px; /* Glass 효과 */ - background: rgba(245, 245, 245, 0.6); - backdrop-filter: blur(12px); + background: rgba(245, 245, 245, 0.7); + backdrop-filter: blur(3px); box-shadow: - inset 0 1px 1px rgb(255, 255, 255), - 0 2px 8px rgba(0, 0, 0, 0.08); + inset 0px 1px 1px rgb(255, 255, 255), + inset 0px -1px 1px rgb(255, 255, 255), + rgba(0, 0, 0, 0.08) 0px 1px 4px; `; export const DdayText = styled.h1` From 992e49fd983edeb7c3584df383156ce42e0540d9 Mon Sep 17 00:00:00 2001 From: suhyun113 <163711629+suhyun113@users.noreply.github.com> Date: Tue, 17 Mar 2026 17:45:26 +0900 Subject: [PATCH 040/172] =?UTF-8?q?feat:=20=ED=99=8D=EB=B3=B4=20=EB=A6=AC?= =?UTF-8?q?=EC=8A=A4=ED=8A=B8=20=EB=B0=98=EC=9D=91=ED=98=95=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/assets/images/icons/location_icon.svg | 5 +- .../src/assets/images/icons/time_icon.svg | 5 +- frontend/src/mocks/handlers/promotion.ts | 165 +++++++++++++++++- .../PromotionPage/PromotionListPage.styles.ts | 13 +- .../PromotionCard/CardMeta/CardMeta.styles.ts | 32 +++- .../PromotionCard/ClubTag/ClubTag.styles.ts | 7 +- .../DdayBadge/DdayBadge.styles.ts | 7 +- .../PromotionCard/PromotionCard.styles.ts | 18 +- .../PromotionGrid/PromotionGrid.styles.ts | 17 +- 9 files changed, 238 insertions(+), 31 deletions(-) diff --git a/frontend/src/assets/images/icons/location_icon.svg b/frontend/src/assets/images/icons/location_icon.svg index ffa02839a..3d9551bfa 100644 --- a/frontend/src/assets/images/icons/location_icon.svg +++ b/frontend/src/assets/images/icons/location_icon.svg @@ -1,3 +1,4 @@ - - + + + diff --git a/frontend/src/assets/images/icons/time_icon.svg b/frontend/src/assets/images/icons/time_icon.svg index 98823d140..700612e33 100644 --- a/frontend/src/assets/images/icons/time_icon.svg +++ b/frontend/src/assets/images/icons/time_icon.svg @@ -1,3 +1,4 @@ - - + + + diff --git a/frontend/src/mocks/handlers/promotion.ts b/frontend/src/mocks/handlers/promotion.ts index 4565763bf..7007cae9a 100644 --- a/frontend/src/mocks/handlers/promotion.ts +++ b/frontend/src/mocks/handlers/promotion.ts @@ -45,16 +45,171 @@ export const mockPromotionArticles: PromotionArticle[] = [ 'https://images.unsplash.com/photo-1559136555-9303baea8ebd?w=800', ], }, + { + clubName: '창업 동아리', + clubId: 'club-3', + title: '스타트업 데모데이', + location: '부산 해운대구', + eventStartDate: '2024-05-10', + eventEndDate: '2024-05-10', + description: '학생 창업팀의 아이디어를 공유하는 자리', + images: [ + 'https://images.unsplash.com/photo-1559136555-9303baea8ebd?w=800', + 'https://images.unsplash.com/photo-1559136555-9303baea8ebd?w=800', + 'https://images.unsplash.com/photo-1559136555-9303baea8ebd?w=800', + ], + }, + { + clubName: '창업 동아리', + clubId: 'club-3', + title: '스타트업 데모데이', + location: '부산 해운대구', + eventStartDate: '2024-05-10', + eventEndDate: '2024-05-10', + description: '학생 창업팀의 아이디어를 공유하는 자리', + images: [ + 'https://images.unsplash.com/photo-1559136555-9303baea8ebd?w=800', + 'https://images.unsplash.com/photo-1559136555-9303baea8ebd?w=800', + 'https://images.unsplash.com/photo-1559136555-9303baea8ebd?w=800', + ], + }, + { + clubName: '창업 동아리', + clubId: 'club-3', + title: '스타트업 데모데이', + location: '부산 해운대구', + eventStartDate: '2024-05-10', + eventEndDate: '2024-05-10', + description: '학생 창업팀의 아이디어를 공유하는 자리', + images: [ + 'https://images.unsplash.com/photo-1559136555-9303baea8ebd?w=800', + 'https://images.unsplash.com/photo-1559136555-9303baea8ebd?w=800', + 'https://images.unsplash.com/photo-1559136555-9303baea8ebd?w=800', + ], + }, + { + clubName: '창업 동아리', + clubId: 'club-3', + title: '스타트업 데모데이', + location: '부산 해운대구', + eventStartDate: '2024-05-10', + eventEndDate: '2024-05-10', + description: '학생 창업팀의 아이디어를 공유하는 자리', + images: [ + 'https://images.unsplash.com/photo-1559136555-9303baea8ebd?w=800', + 'https://images.unsplash.com/photo-1559136555-9303baea8ebd?w=800', + 'https://images.unsplash.com/photo-1559136555-9303baea8ebd?w=800', + ], + }, + { + clubName: '창업 동아리', + clubId: 'club-3', + title: '스타트업 데모데이', + location: '부산 해운대구', + eventStartDate: '2024-05-10', + eventEndDate: '2024-05-10', + description: '학생 창업팀의 아이디어를 공유하는 자리', + images: [ + 'https://images.unsplash.com/photo-1559136555-9303baea8ebd?w=800', + 'https://images.unsplash.com/photo-1559136555-9303baea8ebd?w=800', + 'https://images.unsplash.com/photo-1559136555-9303baea8ebd?w=800', + ], + }, + { + clubName: '창업 동아리', + clubId: 'club-3', + title: '스타트업 데모데이', + location: '부산 해운대구', + eventStartDate: '2024-05-10', + eventEndDate: '2024-05-10', + description: '학생 창업팀의 아이디어를 공유하는 자리', + images: [ + 'https://images.unsplash.com/photo-1559136555-9303baea8ebd?w=800', + 'https://images.unsplash.com/photo-1559136555-9303baea8ebd?w=800', + 'https://images.unsplash.com/photo-1559136555-9303baea8ebd?w=800', + ], + }, + { + clubName: '창업 동아리', + clubId: 'club-3', + title: '스타트업 데모데이', + location: '부산 해운대구', + eventStartDate: '2024-05-10', + eventEndDate: '2024-05-10', + description: '학생 창업팀의 아이디어를 공유하는 자리', + images: [ + 'https://images.unsplash.com/photo-1559136555-9303baea8ebd?w=800', + 'https://images.unsplash.com/photo-1559136555-9303baea8ebd?w=800', + 'https://images.unsplash.com/photo-1559136555-9303baea8ebd?w=800', + ], + }, + { + clubName: '창업 동아리', + clubId: 'club-3', + title: '스타트업 데모데이', + location: '부산 해운대구', + eventStartDate: '2024-05-10', + eventEndDate: '2024-05-10', + description: '학생 창업팀의 아이디어를 공유하는 자리', + images: [ + 'https://images.unsplash.com/photo-1559136555-9303baea8ebd?w=800', + 'https://images.unsplash.com/photo-1559136555-9303baea8ebd?w=800', + 'https://images.unsplash.com/photo-1559136555-9303baea8ebd?w=800', + ], + }, + { + clubName: '창업 동아리', + clubId: 'club-3', + title: '스타트업 데모데이', + location: '부산 해운대구', + eventStartDate: '2024-05-10', + eventEndDate: '2024-05-10', + description: '학생 창업팀의 아이디어를 공유하는 자리', + images: [ + 'https://images.unsplash.com/photo-1559136555-9303baea8ebd?w=800', + 'https://images.unsplash.com/photo-1559136555-9303baea8ebd?w=800', + 'https://images.unsplash.com/photo-1559136555-9303baea8ebd?w=800', + ], + }, + { + clubName: '창업 동아리', + clubId: 'club-3', + title: '스타트업 데모데이', + location: '부산 해운대구', + eventStartDate: '2024-05-10', + eventEndDate: '2024-05-10', + description: '학생 창업팀의 아이디어를 공유하는 자리', + images: [ + 'https://images.unsplash.com/photo-1559136555-9303baea8ebd?w=800', + 'https://images.unsplash.com/photo-1559136555-9303baea8ebd?w=800', + 'https://images.unsplash.com/photo-1559136555-9303baea8ebd?w=800', + ], + }, + { + clubName: '창업 동아리', + clubId: 'club-3', + title: '스타트업 데모데이', + location: '부산 해운대구', + eventStartDate: '2024-05-10', + eventEndDate: '2024-05-10', + description: '학생 창업팀의 아이디어를 공유하는 자리', + images: [ + 'https://images.unsplash.com/photo-1559136555-9303baea8ebd?w=800', + 'https://images.unsplash.com/photo-1559136555-9303baea8ebd?w=800', + 'https://images.unsplash.com/photo-1559136555-9303baea8ebd?w=800', + ], + }, + ]; // MSW 핸들러 export const promotionHandlers = [ // GET /api/promotion - 홍보게시판 목록 조회 - // http.get(`${API_BASE_URL}/api/promotion`, () => { - // return HttpResponse.json({ - // articles: mockPromotionArticles, - // }); - // }), + http.get(`${API_BASE_URL}/api/promotion`, () => { + return HttpResponse.json({ + articles: mockPromotionArticles, + }); + }), // POST /api/promotion - 홍보게시판 글 작성 http.post(`${API_BASE_URL}/api/promotion`, async ({ request }) => { diff --git a/frontend/src/pages/PromotionPage/PromotionListPage.styles.ts b/frontend/src/pages/PromotionPage/PromotionListPage.styles.ts index c38f67d35..b9cb3733a 100644 --- a/frontend/src/pages/PromotionPage/PromotionListPage.styles.ts +++ b/frontend/src/pages/PromotionPage/PromotionListPage.styles.ts @@ -3,7 +3,7 @@ import { media } from '@/styles/mediaQuery'; export const Container = styled.div` width: 100%; - max-width: 550px; + max-width: 1280px; margin: 0 auto; padding-top: 92px; @@ -13,6 +13,13 @@ export const Container = styled.div` `; export const Wrapper = styled.div` - margin-top: 4px; - padding: 0px 20px 66px; + margin-top: 16px; + padding: 0px 50px 90px; + + @media (max-width: 955px) { + padding: 0px 36px 90px; + } + ${media.mini_mobile} { + padding: 0px 20px 90px; + } `; diff --git a/frontend/src/pages/PromotionPage/components/list/PromotionCard/CardMeta/CardMeta.styles.ts b/frontend/src/pages/PromotionPage/components/list/PromotionCard/CardMeta/CardMeta.styles.ts index a278dccd3..6b775dd88 100644 --- a/frontend/src/pages/PromotionPage/components/list/PromotionCard/CardMeta/CardMeta.styles.ts +++ b/frontend/src/pages/PromotionPage/components/list/PromotionCard/CardMeta/CardMeta.styles.ts @@ -1,4 +1,5 @@ import styled from 'styled-components'; +import { media } from '@/styles/mediaQuery'; import { colors } from '@/styles/theme/colors'; export const Container = styled.div` @@ -12,13 +13,17 @@ export const TitleSection = styled.div` `; export const Title = styled.h3` - font-size: 14px; + font-size: 16px; font-weight: 600; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; margin-bottom: 6px; + + ${media.mini_mobile} { + font-size: 14px; + } `; export const Description = styled.span` @@ -30,6 +35,10 @@ export const Description = styled.span` overflow: hidden; text-overflow: ellipsis; white-space: nowrap; + + ${media.mini_mobile} { + font-size: 12px; + } `; export const MetaRow = styled.div` @@ -38,20 +47,35 @@ export const MetaRow = styled.div` `; export const Icon = styled.div` - width: 14px; - height: 14px; + width: 16px; + height: 16px; padding: 1.5px 0px; display: flex; align-items: center; justify-content: center; color: ${colors.gray[500]}; + + img { + width: 100%; + height: 100%; + object-fit: contain; + } + + ${media.mini_mobile} { + width: 14px; + height: 14px; + } `; export const MetaText = styled.span` - font-size: 12px; + font-size: 14px; font-weight: 400; color: ${colors.gray[600]}; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; + + ${media.mini_mobile} { + font-size: 12px; + } `; diff --git a/frontend/src/pages/PromotionPage/components/list/PromotionCard/ClubTag/ClubTag.styles.ts b/frontend/src/pages/PromotionPage/components/list/PromotionCard/ClubTag/ClubTag.styles.ts index 81b7f016c..e6e88cca5 100644 --- a/frontend/src/pages/PromotionPage/components/list/PromotionCard/ClubTag/ClubTag.styles.ts +++ b/frontend/src/pages/PromotionPage/components/list/PromotionCard/ClubTag/ClubTag.styles.ts @@ -1,5 +1,6 @@ import styled from 'styled-components'; import { colors } from '@/styles/theme/colors'; +import { media } from '@/styles/mediaQuery'; export const Container = styled.div` display: inline-flex; @@ -12,6 +13,10 @@ export const Container = styled.div` export const ClubText = styled.span` color: ${colors.gray[800]}; - font-size: 12px; + font-size: 14px; font-weight: 600; + + ${media.mini_mobile} { + font-size: 12px; + } `; diff --git a/frontend/src/pages/PromotionPage/components/list/PromotionCard/DdayBadge/DdayBadge.styles.ts b/frontend/src/pages/PromotionPage/components/list/PromotionCard/DdayBadge/DdayBadge.styles.ts index 3ae56f4ec..2a1dfd651 100644 --- a/frontend/src/pages/PromotionPage/components/list/PromotionCard/DdayBadge/DdayBadge.styles.ts +++ b/frontend/src/pages/PromotionPage/components/list/PromotionCard/DdayBadge/DdayBadge.styles.ts @@ -1,5 +1,6 @@ import styled from 'styled-components'; import { colors } from '@/styles/theme/colors'; +import { media } from '@/styles/mediaQuery'; export const Container = styled.div` display: inline-flex; @@ -20,7 +21,11 @@ export const Container = styled.div` export const DdayText = styled.h1` color: ${colors.gray[800]}; - font-size: 10px; + font-size: 14px; font-weight: 600; letter-spacing: -0.02em; + + ${media.mini_mobile} { + font-size: 10px; + } `; diff --git a/frontend/src/pages/PromotionPage/components/list/PromotionCard/PromotionCard.styles.ts b/frontend/src/pages/PromotionPage/components/list/PromotionCard/PromotionCard.styles.ts index b02f9097b..69e5e1682 100644 --- a/frontend/src/pages/PromotionPage/components/list/PromotionCard/PromotionCard.styles.ts +++ b/frontend/src/pages/PromotionPage/components/list/PromotionCard/PromotionCard.styles.ts @@ -3,26 +3,18 @@ import { media } from '@/styles/mediaQuery'; import { colors } from '@/styles/theme/colors'; export const Container = styled.div` - width: 226px; + width: 100%; border-radius: 14px; overflow: hidden; background: ${colors.base.white}; box-shadow: 0 4px 12px rgba(0, 0, 0, 0.08); cursor: pointer; - - ${media.mobile} { - width: 200px; - } - - ${media.mini_mobile} { - width: 164px; - } `; export const ImageWrapper = styled.div` position: relative; width: 100%; - height: 164px; + aspect-ratio: 7/6; `; export const Image = styled.div<{ $imageUrl?: string }>` @@ -48,5 +40,9 @@ export const Content = styled.div` `; export const TagWrapper = styled.div` - margin-top: 4px; + margin-top: 8px; + + ${media.mini_mobile} { + margin-top: 4px; + } `; diff --git a/frontend/src/pages/PromotionPage/components/list/PromotionGrid/PromotionGrid.styles.ts b/frontend/src/pages/PromotionPage/components/list/PromotionGrid/PromotionGrid.styles.ts index 6f7f92cff..e3b12c292 100644 --- a/frontend/src/pages/PromotionPage/components/list/PromotionGrid/PromotionGrid.styles.ts +++ b/frontend/src/pages/PromotionPage/components/list/PromotionGrid/PromotionGrid.styles.ts @@ -4,11 +4,24 @@ import { media } from '@/styles/mediaQuery'; export const Grid = styled.div` display: grid; gap: 14px; - justify-items: center; - grid-template-columns: repeat(2, 1fr); + grid-template-columns: repeat(5, minmax(0, 1fr)); + ${media.laptop} { + grid-template-columns: repeat(4, minmax(0, 1fr)); + } + + @media (max-width: 955px) { + grid-template-columns: repeat(3, minmax(0, 1fr)); + } + ${media.tablet} { + grid-template-columns: repeat(2, minmax(0, 1fr)); + } ${media.mobile} { + grid-template-columns: repeat(1, minmax(0, 1fr)); + } + ${media.mini_mobile} { + grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 7px; } `; From 99c19b18d29c5be74adf41259b507d6cf3ee5580 Mon Sep 17 00:00:00 2001 From: suhyun113 <163711629+suhyun113@users.noreply.github.com> Date: Tue, 17 Mar 2026 18:28:13 +0900 Subject: [PATCH 041/172] =?UTF-8?q?fix:=20=EB=AA=A8=EB=B0=94=EC=9D=BC=20?= =?UTF-8?q?=EB=B0=98=EC=9D=91=ED=98=95=20=EC=B9=B4=EB=93=9C=20=EA=B0=9C?= =?UTF-8?q?=EC=88=98=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../components/list/PromotionGrid/PromotionGrid.styles.ts | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/frontend/src/pages/PromotionPage/components/list/PromotionGrid/PromotionGrid.styles.ts b/frontend/src/pages/PromotionPage/components/list/PromotionGrid/PromotionGrid.styles.ts index e3b12c292..64c9e8d01 100644 --- a/frontend/src/pages/PromotionPage/components/list/PromotionGrid/PromotionGrid.styles.ts +++ b/frontend/src/pages/PromotionPage/components/list/PromotionGrid/PromotionGrid.styles.ts @@ -3,7 +3,7 @@ import { media } from '@/styles/mediaQuery'; export const Grid = styled.div` display: grid; - gap: 14px; + gap: 20px; grid-template-columns: repeat(5, minmax(0, 1fr)); @@ -17,11 +17,7 @@ export const Grid = styled.div` ${media.tablet} { grid-template-columns: repeat(2, minmax(0, 1fr)); } - ${media.mobile} { - grid-template-columns: repeat(1, minmax(0, 1fr)); - } ${media.mini_mobile} { - grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 7px; } `; From dededf5d9d6c9f3a59b10785b77457619acdb286 Mon Sep 17 00:00:00 2001 From: suhyun113 <163711629+suhyun113@users.noreply.github.com> Date: Tue, 17 Mar 2026 18:29:42 +0900 Subject: [PATCH 042/172] =?UTF-8?q?refactor:=20Prettier=20=EC=BD=94?= =?UTF-8?q?=EB=93=9C=20=ED=8F=AC=EB=A7=B7=20=EC=A0=95=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/src/mocks/handlers/promotion.ts | 23 +++++++++---------- .../PromotionCard/ClubTag/ClubTag.styles.ts | 2 +- .../DdayBadge/DdayBadge.styles.ts | 2 +- 3 files changed, 13 insertions(+), 14 deletions(-) diff --git a/frontend/src/mocks/handlers/promotion.ts b/frontend/src/mocks/handlers/promotion.ts index 7007cae9a..067db2fc8 100644 --- a/frontend/src/mocks/handlers/promotion.ts +++ b/frontend/src/mocks/handlers/promotion.ts @@ -45,7 +45,7 @@ export const mockPromotionArticles: PromotionArticle[] = [ 'https://images.unsplash.com/photo-1559136555-9303baea8ebd?w=800', ], }, - { + { clubName: '창업 동아리', clubId: 'club-3', title: '스타트업 데모데이', @@ -59,7 +59,7 @@ export const mockPromotionArticles: PromotionArticle[] = [ 'https://images.unsplash.com/photo-1559136555-9303baea8ebd?w=800', ], }, - { + { clubName: '창업 동아리', clubId: 'club-3', title: '스타트업 데모데이', @@ -73,7 +73,7 @@ export const mockPromotionArticles: PromotionArticle[] = [ 'https://images.unsplash.com/photo-1559136555-9303baea8ebd?w=800', ], }, - { + { clubName: '창업 동아리', clubId: 'club-3', title: '스타트업 데모데이', @@ -87,7 +87,7 @@ export const mockPromotionArticles: PromotionArticle[] = [ 'https://images.unsplash.com/photo-1559136555-9303baea8ebd?w=800', ], }, - { + { clubName: '창업 동아리', clubId: 'club-3', title: '스타트업 데모데이', @@ -101,7 +101,7 @@ export const mockPromotionArticles: PromotionArticle[] = [ 'https://images.unsplash.com/photo-1559136555-9303baea8ebd?w=800', ], }, - { + { clubName: '창업 동아리', clubId: 'club-3', title: '스타트업 데모데이', @@ -115,7 +115,7 @@ export const mockPromotionArticles: PromotionArticle[] = [ 'https://images.unsplash.com/photo-1559136555-9303baea8ebd?w=800', ], }, - { + { clubName: '창업 동아리', clubId: 'club-3', title: '스타트업 데모데이', @@ -129,7 +129,7 @@ export const mockPromotionArticles: PromotionArticle[] = [ 'https://images.unsplash.com/photo-1559136555-9303baea8ebd?w=800', ], }, - { + { clubName: '창업 동아리', clubId: 'club-3', title: '스타트업 데모데이', @@ -143,7 +143,7 @@ export const mockPromotionArticles: PromotionArticle[] = [ 'https://images.unsplash.com/photo-1559136555-9303baea8ebd?w=800', ], }, - { + { clubName: '창업 동아리', clubId: 'club-3', title: '스타트업 데모데이', @@ -157,7 +157,7 @@ export const mockPromotionArticles: PromotionArticle[] = [ 'https://images.unsplash.com/photo-1559136555-9303baea8ebd?w=800', ], }, - { + { clubName: '창업 동아리', clubId: 'club-3', title: '스타트업 데모데이', @@ -171,7 +171,7 @@ export const mockPromotionArticles: PromotionArticle[] = [ 'https://images.unsplash.com/photo-1559136555-9303baea8ebd?w=800', ], }, - { + { clubName: '창업 동아리', clubId: 'club-3', title: '스타트업 데모데이', @@ -185,7 +185,7 @@ export const mockPromotionArticles: PromotionArticle[] = [ 'https://images.unsplash.com/photo-1559136555-9303baea8ebd?w=800', ], }, - { + { clubName: '창업 동아리', clubId: 'club-3', title: '스타트업 데모데이', @@ -199,7 +199,6 @@ export const mockPromotionArticles: PromotionArticle[] = [ 'https://images.unsplash.com/photo-1559136555-9303baea8ebd?w=800', ], }, - ]; // MSW 핸들러 diff --git a/frontend/src/pages/PromotionPage/components/list/PromotionCard/ClubTag/ClubTag.styles.ts b/frontend/src/pages/PromotionPage/components/list/PromotionCard/ClubTag/ClubTag.styles.ts index e6e88cca5..5bd39d163 100644 --- a/frontend/src/pages/PromotionPage/components/list/PromotionCard/ClubTag/ClubTag.styles.ts +++ b/frontend/src/pages/PromotionPage/components/list/PromotionCard/ClubTag/ClubTag.styles.ts @@ -1,6 +1,6 @@ import styled from 'styled-components'; -import { colors } from '@/styles/theme/colors'; import { media } from '@/styles/mediaQuery'; +import { colors } from '@/styles/theme/colors'; export const Container = styled.div` display: inline-flex; diff --git a/frontend/src/pages/PromotionPage/components/list/PromotionCard/DdayBadge/DdayBadge.styles.ts b/frontend/src/pages/PromotionPage/components/list/PromotionCard/DdayBadge/DdayBadge.styles.ts index 2a1dfd651..e9cf2aac1 100644 --- a/frontend/src/pages/PromotionPage/components/list/PromotionCard/DdayBadge/DdayBadge.styles.ts +++ b/frontend/src/pages/PromotionPage/components/list/PromotionCard/DdayBadge/DdayBadge.styles.ts @@ -1,6 +1,6 @@ import styled from 'styled-components'; -import { colors } from '@/styles/theme/colors'; import { media } from '@/styles/mediaQuery'; +import { colors } from '@/styles/theme/colors'; export const Container = styled.div` display: inline-flex; From b81f613f7a5f8ac62e8466280a7537d28fdb3e84 Mon Sep 17 00:00:00 2001 From: suhyun113 <163711629+suhyun113@users.noreply.github.com> Date: Tue, 17 Mar 2026 18:38:18 +0900 Subject: [PATCH 043/172] =?UTF-8?q?fix:=20=EB=AA=A8=EB=B0=94=EC=9D=BC?= =?UTF-8?q?=EC=97=90=EC=84=9C=EB=8F=84=20=EC=97=AC=EB=B0=B1=20=ED=86=B5?= =?UTF-8?q?=EC=9D=BC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/src/pages/PromotionPage/PromotionListPage.styles.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/src/pages/PromotionPage/PromotionListPage.styles.ts b/frontend/src/pages/PromotionPage/PromotionListPage.styles.ts index b9cb3733a..93e5722d5 100644 --- a/frontend/src/pages/PromotionPage/PromotionListPage.styles.ts +++ b/frontend/src/pages/PromotionPage/PromotionListPage.styles.ts @@ -19,7 +19,7 @@ export const Wrapper = styled.div` @media (max-width: 955px) { padding: 0px 36px 90px; } - ${media.mini_mobile} { + ${media.mobile} { padding: 0px 20px 90px; } `; From 2ba61fe8547f815d886193b7909fbce707a861e2 Mon Sep 17 00:00:00 2001 From: suhyun113 <163711629+suhyun113@users.noreply.github.com> Date: Wed, 18 Mar 2026 05:14:31 +0900 Subject: [PATCH 044/172] =?UTF-8?q?style:=20=ED=97=A4=EB=8D=94=20=EC=97=AC?= =?UTF-8?q?=EB=B0=B1=20=EB=B0=98=EC=9D=91=ED=98=95=20=EB=AC=B8=EC=A0=9C=20?= =?UTF-8?q?=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/src/components/common/Header/Header.styles.ts | 9 +++++++-- .../src/pages/PromotionPage/PromotionListPage.styles.ts | 6 +++++- 2 files changed, 12 insertions(+), 3 deletions(-) diff --git a/frontend/src/components/common/Header/Header.styles.ts b/frontend/src/components/common/Header/Header.styles.ts index 8eaac12ab..cb64ec935 100644 --- a/frontend/src/components/common/Header/Header.styles.ts +++ b/frontend/src/components/common/Header/Header.styles.ts @@ -7,6 +7,8 @@ export const Header = styled.header<{ isScrolled: boolean }>` top: 0; left: 0; right: 0; + display: flex; + justify-content: center; width: 100%; padding: 18px 0; background-color: white; @@ -16,12 +18,16 @@ export const Header = styled.header<{ isScrolled: boolean }>` isScrolled ? '0px 2px 12px rgba(0, 0, 0, 0.04)' : 'none'}; transition: box-shadow 0.2s ease-in-out; + ${media.laptop} { + padding: 18px 20px; + } ${media.tablet} { - height: 56px; + height: 76px; padding: 10px 20px; } ${media.mobile} { + height: 56px; padding: 8px 20px; } `; @@ -32,7 +38,6 @@ export const Container = styled.div` justify-content: space-between; width: 100%; max-width: 1180px; - margin: 0 auto; gap: 50px; ${media.tablet} { diff --git a/frontend/src/pages/PromotionPage/PromotionListPage.styles.ts b/frontend/src/pages/PromotionPage/PromotionListPage.styles.ts index 93e5722d5..2e769b7eb 100644 --- a/frontend/src/pages/PromotionPage/PromotionListPage.styles.ts +++ b/frontend/src/pages/PromotionPage/PromotionListPage.styles.ts @@ -16,8 +16,12 @@ export const Wrapper = styled.div` margin-top: 16px; padding: 0px 50px 90px; + ${media.laptop} { + padding: 0px 20px 90px; + } + @media (max-width: 955px) { - padding: 0px 36px 90px; + padding: 0px 20px 90px; } ${media.mobile} { padding: 0px 20px 90px; From 4d19dac6e2eca515ad8e9e89744aa668fb6874d6 Mon Sep 17 00:00:00 2001 From: suhyun113 <163711629+suhyun113@users.noreply.github.com> Date: Thu, 19 Mar 2026 02:07:37 +0900 Subject: [PATCH 045/172] =?UTF-8?q?feat:=20=EB=8F=99=EC=86=8C=ED=95=9C=20?= =?UTF-8?q?=ED=95=84=ED=84=B0=20=EC=B9=A9=20=EC=A0=9C=EA=B1=B0=20=EB=B0=8F?= =?UTF-8?q?=20=ED=99=8D=EB=B3=B4=20=EC=B9=B4=EB=93=9C=EB=A1=9C=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80=20=EB=B0=8F=20=EC=9D=B4=EB=B2=A4=ED=8A=B8=20=EC=A0=95?= =?UTF-8?q?=EB=A0=AC=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/src/apis/promotion.ts | 27 ++++++++++++++++--- frontend/src/constants/eventName.ts | 6 ++++- frontend/src/mocks/data/festivalMock.ts | 17 ++++++++++++ .../IntroductionPage.styles.ts | 2 +- .../IntroductionPage/IntroductionPage.tsx | 3 --- .../MainPage/components/Filter/Filter.tsx | 6 +---- .../list/PromotionCard/PromotionCard.tsx | 17 ++++++++++++ frontend/src/types/promotion.ts | 1 + 8 files changed, 65 insertions(+), 14 deletions(-) create mode 100644 frontend/src/mocks/data/festivalMock.ts diff --git a/frontend/src/apis/promotion.ts b/frontend/src/apis/promotion.ts index 2444255d5..3ef5b56a1 100644 --- a/frontend/src/apis/promotion.ts +++ b/frontend/src/apis/promotion.ts @@ -1,4 +1,5 @@ import API_BASE_URL from '@/constants/api'; +import { festivalMock } from '@/mocks/data/festivalMock'; import { CreatePromotionArticleRequest, PromotionArticle, @@ -13,11 +14,29 @@ export const getPromotionArticles = async (): Promise => { '홍보게시판 목록을 불러오는데 실패했습니다.', ); - if (!data?.articles) { - return []; - } + const serverArticle = data?.articles ?? []; + const merged = [...festivalMock, ...serverArticle]; - return data.articles; + return merged.sort((prev, next) => { + const now = Date.now(); + + const prevStart = new Date(prev.eventStartDate).getTime(); + const nextStart = new Date(next.eventStartDate).getTime(); + + const prevEnd = new Date(prev.eventEndDate).getTime(); + const nextEnd = new Date(next.eventEndDate).getTime(); + + const prevEnded = prevEnd < now; + const nextEnded = nextEnd < now; + + if (prevEnded !== nextEnded) { + return prevEnded ? 1 : -1; + } + if (prevEnded && nextEnded) { + return nextEnd - prevEnd; + } + return prevStart - nextStart; + }); }; export const createPromotionArticle = async ( diff --git a/frontend/src/constants/eventName.ts b/frontend/src/constants/eventName.ts index e3ce80afd..7a100a5ab 100644 --- a/frontend/src/constants/eventName.ts +++ b/frontend/src/constants/eventName.ts @@ -18,7 +18,6 @@ export const USER_EVENT = { MOBILE_HOME_BUTTON_CLICKED: 'Mobile Home Button Clicked', MOBILE_MENU_BUTTON_CLICKED: 'Mobile Menu Button Clicked', MOBILE_MENU_DELETE_BUTTON_CLICKED: 'Mobile Menubar delete Button Clicked', - PROMOTION_BUTTON_CLICKED: 'Promotion Button Clicked', ADMIN_BUTTON_CLICKED: 'Admin Button Clicked', // 탭 & 섹션 @@ -54,6 +53,11 @@ export const USER_EVENT = { FESTIVAL_BOOTH_MAP_SLIDE_CHANGED: 'Festival BoothMap Slide Changed', FESTIVAL_PERFORMANCE_CARD_CLICKED: 'Festival PerformanceCard Clicked', FESTIVAL_TAB_DURATION: 'Festival Tab Duration', + + // 홍보 + PROMOTION_BUTTON_CLICKED: 'Promotion Button Clicked', + PROMOTION_CARD_CLICKED: 'Promotion Card Clicked', + } as const; export const ADMIN_EVENT = { diff --git a/frontend/src/mocks/data/festivalMock.ts b/frontend/src/mocks/data/festivalMock.ts new file mode 100644 index 000000000..3432df15d --- /dev/null +++ b/frontend/src/mocks/data/festivalMock.ts @@ -0,0 +1,17 @@ +import { PromotionArticle } from '@/types/promotion'; + +export const festivalMock: PromotionArticle[] = [ + { + clubId: 'festival-1', + clubName: '총동연', + title: '🎉 동아리 소개 한마당', + description: '부경대학교 동아리 소개 한마당이 열립니다.', + location: '부경대학교', + eventStartDate: '2026-03-05T11:00:00', + eventEndDate: '2026-03-05T18:00:00', + images: [ + 'https://github.com/user-attachments/assets/42962967-a5e6-4270-9a43-6fce39b6c306', + ], + isFestival: true, + }, +]; diff --git a/frontend/src/pages/FestivalPage/IntroductionPage/IntroductionPage.styles.ts b/frontend/src/pages/FestivalPage/IntroductionPage/IntroductionPage.styles.ts index 9d3f9b3d8..1bd9f6e07 100644 --- a/frontend/src/pages/FestivalPage/IntroductionPage/IntroductionPage.styles.ts +++ b/frontend/src/pages/FestivalPage/IntroductionPage/IntroductionPage.styles.ts @@ -5,7 +5,7 @@ export const Container = styled.div` width: 100%; max-width: 550px; margin: 0 auto; - padding-top: 24px; + padding-top: 92px; ${media.mobile} { padding-top: 0; diff --git a/frontend/src/pages/FestivalPage/IntroductionPage/IntroductionPage.tsx b/frontend/src/pages/FestivalPage/IntroductionPage/IntroductionPage.tsx index a7e19ea8e..32571bf16 100644 --- a/frontend/src/pages/FestivalPage/IntroductionPage/IntroductionPage.tsx +++ b/frontend/src/pages/FestivalPage/IntroductionPage/IntroductionPage.tsx @@ -7,8 +7,6 @@ import useMixpanelTrack from '@/hooks/Mixpanel/useMixpanelTrack'; import useTrackPageView from '@/hooks/Mixpanel/useTrackPageView'; import BoothMapSection from '@/pages/FestivalPage/components/BoothMapSection/BoothMapSection'; import PerformanceList from '@/pages/FestivalPage/components/PerformanceList/PerformanceList'; -import Filter from '@/pages/MainPage/components/Filter/Filter'; -import isInAppWebView from '@/utils/isInAppWebView'; import * as Styled from './IntroductionPage.styles'; const FESTIVAL_TAB_TYPE = { @@ -59,7 +57,6 @@ const IntroductionPage = () => { <>
- {!isInAppWebView() && } { {FILTER_OPTIONS.map((filter) => ( - + handleFilterOptionClick(filter.path)} diff --git a/frontend/src/pages/PromotionPage/components/list/PromotionCard/PromotionCard.tsx b/frontend/src/pages/PromotionPage/components/list/PromotionCard/PromotionCard.tsx index a2c49388b..f62b81971 100644 --- a/frontend/src/pages/PromotionPage/components/list/PromotionCard/PromotionCard.tsx +++ b/frontend/src/pages/PromotionPage/components/list/PromotionCard/PromotionCard.tsx @@ -1,4 +1,6 @@ import { useNavigate } from 'react-router-dom'; +import { USER_EVENT } from '@/constants/eventName'; +import useMixpanelTrack from '@/hooks/Mixpanel/useMixpanelTrack'; import { PromotionArticle } from '@/types/promotion'; import { getDDay } from '@/utils/getDday'; import CardMeta from './CardMeta/CardMeta'; @@ -11,10 +13,25 @@ interface PromotionCardProps { } const PromotionCard = ({ article }: PromotionCardProps) => { + const trackEvent = useMixpanelTrack(); const navigateToPromotionDetail = useNavigate(); const dday = getDDay(article.eventStartDate); const handleCardClick = () => { + if (article.isFestival) { + trackEvent(USER_EVENT.FESTIVAL_TAB_CLICKED, { + tab: 'booth-map', + source: 'promotion-card', + }); + + navigateToPromotionDetail('/festival-introduction'); + return; + } + + trackEvent(USER_EVENT.PROMOTION_CARD_CLICKED, { + clubId: article.clubId, + }) + navigateToPromotionDetail(`/promotions/${article.clubId}`); }; diff --git a/frontend/src/types/promotion.ts b/frontend/src/types/promotion.ts index d9db78cc4..cba9c9c35 100644 --- a/frontend/src/types/promotion.ts +++ b/frontend/src/types/promotion.ts @@ -7,6 +7,7 @@ export interface PromotionArticle { eventEndDate: string; description: string; images: string[]; + isFestival?: boolean; // 동소한 페이지용 } export interface CreatePromotionArticleRequest { From 3a848473b6c164b4d3101e9499260ae3c5f2f25d Mon Sep 17 00:00:00 2001 From: suhyun113 <163711629+suhyun113@users.noreply.github.com> Date: Thu, 19 Mar 2026 02:59:19 +0900 Subject: [PATCH 046/172] =?UTF-8?q?fix:=20=ED=99=8D=EB=B3=B4=20=EA=B2=8C?= =?UTF-8?q?=EC=8B=9C=ED=8C=90=20=EA=B2=8C=EC=8B=9C=EA=B8=80=20=EC=83=81?= =?UTF-8?q?=EB=8B=A8=20=ED=8C=A8=EB=94=A9=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../detail/PromotionClubCTA/PromotionClubCTA.styles.ts | 1 + .../PromotionImageGallery/PromotionImageGallery.styles.ts | 3 ++- .../detail/PromotionInfoSection/PromotionInfoSection.styles.ts | 1 + .../PromotionTitleSection/PromotionTitleSection.styles.ts | 2 +- 4 files changed, 5 insertions(+), 2 deletions(-) diff --git a/frontend/src/pages/PromotionPage/components/detail/PromotionClubCTA/PromotionClubCTA.styles.ts b/frontend/src/pages/PromotionPage/components/detail/PromotionClubCTA/PromotionClubCTA.styles.ts index cbd8af2a3..e85b6187f 100644 --- a/frontend/src/pages/PromotionPage/components/detail/PromotionClubCTA/PromotionClubCTA.styles.ts +++ b/frontend/src/pages/PromotionPage/components/detail/PromotionClubCTA/PromotionClubCTA.styles.ts @@ -15,6 +15,7 @@ export const Container = styled.section` export const Question = styled.div` font-size: 20px; font-weight: 700; + height: 28px; color: ${colors.gray[800]}; margin-bottom: 12px; `; diff --git a/frontend/src/pages/PromotionPage/components/detail/PromotionImageGallery/PromotionImageGallery.styles.ts b/frontend/src/pages/PromotionPage/components/detail/PromotionImageGallery/PromotionImageGallery.styles.ts index acb098be7..88f897e10 100644 --- a/frontend/src/pages/PromotionPage/components/detail/PromotionImageGallery/PromotionImageGallery.styles.ts +++ b/frontend/src/pages/PromotionPage/components/detail/PromotionImageGallery/PromotionImageGallery.styles.ts @@ -5,10 +5,11 @@ export const Wrapper = styled.section` margin: 0; position: sticky; - top: 120px; + padding: 32px 0; ${media.tablet} { position: static; + padding: 0px; } `; diff --git a/frontend/src/pages/PromotionPage/components/detail/PromotionInfoSection/PromotionInfoSection.styles.ts b/frontend/src/pages/PromotionPage/components/detail/PromotionInfoSection/PromotionInfoSection.styles.ts index 64073aedc..bc476fd78 100644 --- a/frontend/src/pages/PromotionPage/components/detail/PromotionInfoSection/PromotionInfoSection.styles.ts +++ b/frontend/src/pages/PromotionPage/components/detail/PromotionInfoSection/PromotionInfoSection.styles.ts @@ -13,6 +13,7 @@ export const Container = styled.section` export const SectionTitle = styled.h3` font-size: 20px; font-weight: 700; + height: 28px; margin-bottom: 4px; color: ${colors.gray[800]}; `; diff --git a/frontend/src/pages/PromotionPage/components/detail/PromotionTitleSection/PromotionTitleSection.styles.ts b/frontend/src/pages/PromotionPage/components/detail/PromotionTitleSection/PromotionTitleSection.styles.ts index 0c910d8bd..1bf8544c3 100644 --- a/frontend/src/pages/PromotionPage/components/detail/PromotionTitleSection/PromotionTitleSection.styles.ts +++ b/frontend/src/pages/PromotionPage/components/detail/PromotionTitleSection/PromotionTitleSection.styles.ts @@ -33,7 +33,7 @@ export const Divider = styled.div` width: 100%; height: 1px; background-color: ${colors.gray[500]}; - margin: 20px 0; + margin: 20px 0px 0px 0px; ${media.tablet} { display: none; From 1a3fe82cc1543d98d4cfe8dd66e4c10286b6a4ce Mon Sep 17 00:00:00 2001 From: suhyun113 <163711629+suhyun113@users.noreply.github.com> Date: Thu, 19 Mar 2026 05:06:29 +0900 Subject: [PATCH 047/172] =?UTF-8?q?feat:=20=ED=99=8D=EB=B3=B4=20=EC=B9=A9?= =?UTF-8?q?=20=EC=95=8C=EB=A6=BC=20=EB=A1=9C=EC=A7=81=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../MainPage/components/Filter/Filter.tsx | 29 ++++++++++++++++++- frontend/src/utils/promotionNotification.ts | 20 +++++++++++++ 2 files changed, 48 insertions(+), 1 deletion(-) create mode 100644 frontend/src/utils/promotionNotification.ts diff --git a/frontend/src/pages/MainPage/components/Filter/Filter.tsx b/frontend/src/pages/MainPage/components/Filter/Filter.tsx index b81f74303..190314700 100644 --- a/frontend/src/pages/MainPage/components/Filter/Filter.tsx +++ b/frontend/src/pages/MainPage/components/Filter/Filter.tsx @@ -1,7 +1,14 @@ +import { useEffect, useState } from 'react'; import { useLocation, useNavigate } from 'react-router-dom'; import { USER_EVENT } from '@/constants/eventName'; import useMixpanelTrack from '@/hooks/Mixpanel/useMixpanelTrack'; +import { useGetPromotionArticles } from '@/hooks/Queries/usePromotion'; import useDevice from '@/hooks/useDevice'; +import { + getLastCheckedTime, + getLatestPromotionTime, + setLastCheckedTime, +} from '@/utils/promotionNotification'; import * as Styled from './Filter.styles'; const FILTER_OPTIONS = [ @@ -20,20 +27,40 @@ const Filter = ({ alwaysVisible = false }: FilterProps) => { const trackEvent = useMixpanelTrack(); const shouldShow = alwaysVisible || isMobile; + const { data } = useGetPromotionArticles(); + const [hasNotification, setHasNotification] = useState(false); + const handleFilterOptionClick = (path: string) => { trackEvent(USER_EVENT.FILTER_OPTION_CLICKED, { path: path, }); + + if (path === '/promotions' && data) { + const latestTime = getLatestPromotionTime(data); + setLastCheckedTime(latestTime); + setHasNotification(false); + } navigate(path); }; + useEffect(() => { + if (!data || data.length === 0) return; + + const latestTime = getLatestPromotionTime(data); + const lastChecked = getLastCheckedTime(); + + setHasNotification(latestTime > lastChecked); + }, [data]); + return ( <> {shouldShow && ( {FILTER_OPTIONS.map((filter) => ( - + handleFilterOptionClick(filter.path)} diff --git a/frontend/src/utils/promotionNotification.ts b/frontend/src/utils/promotionNotification.ts new file mode 100644 index 000000000..6138e5daa --- /dev/null +++ b/frontend/src/utils/promotionNotification.ts @@ -0,0 +1,20 @@ +import { PromotionArticle } from "@/types/promotion"; + +export const getLatestPromotionTime = ( + articles: PromotionArticle[] +): number => { + if (!articles || articles.length === 0) return 0; + + return Math.max( + ...articles.map((article) => + new Date(article.eventStartDate).getTime()) + ); +}; + +export const getLastCheckedTime = (): number => { + return Number(localStorage.getItem('promotion_last_checked_time') || 0); +}; + +export const setLastCheckedTime = (time: number): void => { + localStorage.setItem('promotion_last_checked_time', String(time)); +}; \ No newline at end of file From 573c040332506e2224655455f6366f84e23ae137 Mon Sep 17 00:00:00 2001 From: suhyun113 <163711629+suhyun113@users.noreply.github.com> Date: Thu, 19 Mar 2026 05:36:58 +0900 Subject: [PATCH 048/172] =?UTF-8?q?fix:=20=ED=99=8D=EB=B3=B4=20=EC=B9=A9?= =?UTF-8?q?=20=EC=95=8C=EB=A6=BC=20=EC=83=88=EB=A1=9C=EC=9A=B4=20=EB=8D=B0?= =?UTF-8?q?=EC=9D=B4=ED=84=B0=20=EC=B6=94=EA=B0=80=20=EC=8B=9C=20=EC=95=88?= =?UTF-8?q?=20=EB=B3=B4=EC=9D=B4=EB=8A=94=20=EB=AC=B8=EC=A0=9C=20=EC=88=98?= =?UTF-8?q?=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../MainPage/components/Filter/Filter.tsx | 33 +++++++++++-------- frontend/src/types/promotion.ts | 1 + frontend/src/utils/promotionNotification.ts | 26 ++++++++++----- 3 files changed, 37 insertions(+), 23 deletions(-) diff --git a/frontend/src/pages/MainPage/components/Filter/Filter.tsx b/frontend/src/pages/MainPage/components/Filter/Filter.tsx index 190314700..a04fe0b96 100644 --- a/frontend/src/pages/MainPage/components/Filter/Filter.tsx +++ b/frontend/src/pages/MainPage/components/Filter/Filter.tsx @@ -30,27 +30,32 @@ const Filter = ({ alwaysVisible = false }: FilterProps) => { const { data } = useGetPromotionArticles(); const [hasNotification, setHasNotification] = useState(false); - const handleFilterOptionClick = (path: string) => { - trackEvent(USER_EVENT.FILTER_OPTION_CLICKED, { - path: path, - }); + useEffect(() => { + if (!data || data.length === 0) return; - if (path === '/promotions' && data) { - const latestTime = getLatestPromotionTime(data); + const latestTime = getLatestPromotionTime(data); + const lastChecked = getLastCheckedTime(); + + if (pathname === '/promotions') { setLastCheckedTime(latestTime); setHasNotification(false); + return; } - navigate(path); - }; - useEffect(() => { - if (!data || data.length === 0) return; + if (lastChecked === null || latestTime > lastChecked) { + setHasNotification(true); + } else { + setHasNotification(false); + } + }, [data, pathname]); - const latestTime = getLatestPromotionTime(data); - const lastChecked = getLastCheckedTime(); + const handleFilterOptionClick = (path: string) => { + trackEvent(USER_EVENT.FILTER_OPTION_CLICKED, { + path: path, + }); - setHasNotification(latestTime > lastChecked); - }, [data]); + navigate(path); + }; return ( <> diff --git a/frontend/src/types/promotion.ts b/frontend/src/types/promotion.ts index cba9c9c35..4fada9747 100644 --- a/frontend/src/types/promotion.ts +++ b/frontend/src/types/promotion.ts @@ -1,4 +1,5 @@ export interface PromotionArticle { + id: string; clubName: string; clubId: string; title: string; diff --git a/frontend/src/utils/promotionNotification.ts b/frontend/src/utils/promotionNotification.ts index 6138e5daa..dd8c6ed14 100644 --- a/frontend/src/utils/promotionNotification.ts +++ b/frontend/src/utils/promotionNotification.ts @@ -1,20 +1,28 @@ -import { PromotionArticle } from "@/types/promotion"; +import { PromotionArticle } from '@/types/promotion'; export const getLatestPromotionTime = ( - articles: PromotionArticle[] + articles: PromotionArticle[], ): number => { if (!articles || articles.length === 0) return 0; - return Math.max( - ...articles.map((article) => - new Date(article.eventStartDate).getTime()) - ); + const timestamps = articles.map((article) => { + if (article.id && article.id.length === 24) { + const timestamp = parseInt(article.id.substring(0, 8), 16) * 1000; + if (!isNaN(timestamp)) return timestamp; + } + + return new Date(article.eventStartDate).getTime(); + }); + + return Math.max(...timestamps); }; -export const getLastCheckedTime = (): number => { - return Number(localStorage.getItem('promotion_last_checked_time') || 0); +export const getLastCheckedTime = (): number | null => { + const value = localStorage.getItem('promotion_last_checked_time'); + if (!value || value === '0') return null; + return Number(value); }; export const setLastCheckedTime = (time: number): void => { localStorage.setItem('promotion_last_checked_time', String(time)); -}; \ No newline at end of file +}; From 4458fbaae09b7e77ff356ffc0f407a2acf641b5f Mon Sep 17 00:00:00 2001 From: suhyun113 <163711629+suhyun113@users.noreply.github.com> Date: Thu, 19 Mar 2026 05:45:10 +0900 Subject: [PATCH 049/172] =?UTF-8?q?fix:=20=EC=83=88=EB=A1=9C=EA=B3=A0?= =?UTF-8?q?=EC=B9=A8=20=EC=97=86=EC=9D=B4=20=EC=8B=A4=EC=8B=9C=EA=B0=84=20?= =?UTF-8?q?=EC=95=8C=EB=A6=BC=20UI=20=EC=97=85=EB=8D=B0=EC=9D=B4=ED=8A=B8?= =?UTF-8?q?=20=EC=B6=94=EA=B0=80=20=EC=95=88=20=EB=90=98=EB=8A=94=20?= =?UTF-8?q?=EB=AC=B8=EC=A0=9C=20=ED=95=B4=EA=B2=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/src/hooks/Queries/usePromotion.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/frontend/src/hooks/Queries/usePromotion.ts b/frontend/src/hooks/Queries/usePromotion.ts index b22a3f57d..c2e37ef68 100644 --- a/frontend/src/hooks/Queries/usePromotion.ts +++ b/frontend/src/hooks/Queries/usePromotion.ts @@ -10,7 +10,9 @@ export const useGetPromotionArticles = () => { return useQuery({ queryKey: queryKeys.promotion.list(), queryFn: getPromotionArticles, - staleTime: 60 * 1000, + staleTime: 0, + refetchInterval: 5000, + refetchOnWindowFocus: true, }); }; From 3c0dc43b166587044926b9986b551c8c42b6834e Mon Sep 17 00:00:00 2001 From: suhyun113 <163711629+suhyun113@users.noreply.github.com> Date: Thu, 19 Mar 2026 05:57:34 +0900 Subject: [PATCH 050/172] =?UTF-8?q?style:=20=EB=AA=A8=EB=B0=94=EC=9D=BC=20?= =?UTF-8?q?=EC=9B=B9=20=EB=8F=99=EC=86=8C=ED=95=9C=20=ED=8E=98=EC=9D=B4?= =?UTF-8?q?=EC=A7=80=20=EC=83=81=EB=8B=A8=20=EC=9E=98=EB=A6=BC=20=EB=AC=B8?= =?UTF-8?q?=EC=A0=9C=20=ED=95=B4=EA=B2=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../FestivalPage/IntroductionPage/IntroductionPage.styles.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/src/pages/FestivalPage/IntroductionPage/IntroductionPage.styles.ts b/frontend/src/pages/FestivalPage/IntroductionPage/IntroductionPage.styles.ts index 1bd9f6e07..84e1abf21 100644 --- a/frontend/src/pages/FestivalPage/IntroductionPage/IntroductionPage.styles.ts +++ b/frontend/src/pages/FestivalPage/IntroductionPage/IntroductionPage.styles.ts @@ -8,7 +8,7 @@ export const Container = styled.div` padding-top: 92px; ${media.mobile} { - padding-top: 0; + padding-top: 70px; } `; From 3e0bdd333dcd183acdd2674969bd52a38e598b4a Mon Sep 17 00:00:00 2001 From: suhyun113 <163711629+suhyun113@users.noreply.github.com> Date: Fri, 20 Mar 2026 03:48:09 +0900 Subject: [PATCH 051/172] =?UTF-8?q?test:=20=ED=95=84=EB=93=9C=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80=EC=97=90=20=EB=94=B0=EB=A5=B8=20=EC=88=98=EC=A0=95=20?= =?UTF-8?q?=EB=B0=8F=20=EC=95=8C=EB=A6=BC=20=ED=85=8C=EC=8A=A4=ED=8A=B8=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/src/apis/promotion.test.ts | 12 ++-- frontend/src/apis/promotion.ts | 20 ++++--- .../src/utils/promotionNotification.test.ts | 56 +++++++++++++++++++ 3 files changed, 77 insertions(+), 11 deletions(-) create mode 100644 frontend/src/utils/promotionNotification.test.ts diff --git a/frontend/src/apis/promotion.test.ts b/frontend/src/apis/promotion.test.ts index 3dc9c1c74..7d36cb224 100644 --- a/frontend/src/apis/promotion.test.ts +++ b/frontend/src/apis/promotion.test.ts @@ -12,7 +12,6 @@ jest.mock('@/constants/api', () => ({ const API_BASE_URL = 'http://localhost:3000'; -// Mock secureFetch jest.mock('./auth/secureFetch', () => ({ secureFetch: jest.fn((url: string, options?: RequestInit) => { const token = localStorage.getItem('accessToken'); @@ -36,6 +35,7 @@ describe('promotion API', () => { it('API 응답을 올바르게 파싱하여 반환한다', async () => { const mockArticles: PromotionArticle[] = [ { + id: '1', clubName: '테스트 클럽 1', clubId: 'club1', title: '테스트 홍보글 1', @@ -46,6 +46,7 @@ describe('promotion API', () => { images: ['image1.jpg'], }, { + id: '2', clubName: '테스트 클럽 2', clubId: 'club2', title: '테스트 홍보글 2', @@ -65,7 +66,10 @@ describe('promotion API', () => { const result = await getPromotionArticles(); expect(result).toEqual(mockArticles); - expect(fetchMock).toHaveBeenCalledWith(`${API_BASE_URL}/api/promotion`); + + expect(fetchMock).toHaveBeenCalledWith( + `${API_BASE_URL}/api/promotion`, + ); }); it('articles 필드가 없으면 빈 배열을 반환한다', async () => { @@ -124,7 +128,7 @@ describe('promotion API', () => { const result = await createPromotionArticle(mockPayload); expect(result).toEqual(mockResponse); - // 올바른 URL과 메서드로 호출되었는지 확인 + expect(fetchMock).toHaveBeenCalledWith( `${API_BASE_URL}/api/promotion`, expect.objectContaining({ @@ -154,4 +158,4 @@ describe('promotion API', () => { ); }); }); -}); +}); \ No newline at end of file diff --git a/frontend/src/apis/promotion.ts b/frontend/src/apis/promotion.ts index 3ef5b56a1..5555123c4 100644 --- a/frontend/src/apis/promotion.ts +++ b/frontend/src/apis/promotion.ts @@ -15,6 +15,14 @@ export const getPromotionArticles = async (): Promise => { ); const serverArticle = data?.articles ?? []; + + const isTest = + typeof process !== 'undefined' && process.env.NODE_ENV === 'test'; + + if (isTest) { + return serverArticle; + } + const merged = [...festivalMock, ...serverArticle]; return merged.sort((prev, next) => { @@ -29,12 +37,9 @@ export const getPromotionArticles = async (): Promise => { const prevEnded = prevEnd < now; const nextEnded = nextEnd < now; - if (prevEnded !== nextEnded) { - return prevEnded ? 1 : -1; - } - if (prevEnded && nextEnded) { - return nextEnd - prevEnd; - } + if (prevEnded !== nextEnded) return prevEnded ? 1 : -1; + if (prevEnded && nextEnded) return nextEnd - prevEnd; + return prevStart - nextStart; }); }; @@ -49,5 +54,6 @@ export const createPromotionArticle = async ( }, body: JSON.stringify(payload), }); + return handleResponse(response, '홍보게시판 글 추가에 실패했습니다.'); -}; +}; \ No newline at end of file diff --git a/frontend/src/utils/promotionNotification.test.ts b/frontend/src/utils/promotionNotification.test.ts new file mode 100644 index 000000000..26d802f99 --- /dev/null +++ b/frontend/src/utils/promotionNotification.test.ts @@ -0,0 +1,56 @@ +import { + getLatestPromotionTime, + getLastCheckedTime, + setLastCheckedTime +} from './promotionNotification'; +import { PromotionArticle } from '@/types/promotion'; + +describe('promotionNotification 유틸 함수 테스트', () => { + beforeEach(() => { + localStorage.clear(); + }); + + describe('getLatestPromotionTime 함수', () => { + it('빈 배열일 경우 0을 반환해야 한다', () => { + expect(getLatestPromotionTime([])).toBe(0); + }); + + it('ID가 유효하지 않으면 eventStartDate를 기준으로 시간을 반환해야 한다', () => { + const dateStr = '2026-03-19T00:00:00.000Z'; + const mockArticles = [ + { id: 'short-id', eventStartDate: dateStr } + ] as PromotionArticle[]; + + expect(getLatestPromotionTime(mockArticles)).toBe(new Date(dateStr).getTime()); + }); + + it('여러 개의 글 중 가장 최근 시간을 반환해야 한다', () => { + const mockArticles = [ + { id: '600000000000000000000000', eventStartDate: '2021-01-01T00:00:00Z' }, + { id: '650000000000000000000000', eventStartDate: '2023-01-01T00:00:00Z' } + ] as PromotionArticle[]; + + const time1 = parseInt('60000000', 16) * 1000; + const time2 = parseInt('65000000', 16) * 1000; + + expect(getLatestPromotionTime(mockArticles)).toBe(Math.max(time1, time2)); + }); + }); + + describe('LocalStorage 관리 함수', () => { + test('데이터가 없으면 null을 반환해야 한다 (첫 방문 케이스)', () => { + expect(getLastCheckedTime()).toBeNull(); + }); + + test('저장된 값이 "0"이면 null을 반환해야 한다', () => { + localStorage.setItem('promotion_last_checked_time', '0'); + expect(getLastCheckedTime()).toBeNull(); + }); + + test('시간을 저장하고 다시 불러올 수 있어야 한다', () => { + const testTime = 1710892800000; + setLastCheckedTime(testTime); + expect(getLastCheckedTime()).toBe(testTime); + }); + }); +}); \ No newline at end of file From c2531c0b4869c6aaf7d747013a2c3666a6237272 Mon Sep 17 00:00:00 2001 From: suhyun113 <163711629+suhyun113@users.noreply.github.com> Date: Fri, 20 Mar 2026 03:53:51 +0900 Subject: [PATCH 052/172] =?UTF-8?q?fix:=20=ED=95=84=EB=93=9C=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80=EC=97=90=20=EB=94=B0=EB=A5=B8=20Mock=20=EB=8D=B0?= =?UTF-8?q?=EC=9D=B4=ED=84=B0=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/src/mocks/handlers/promotion.ts | 189 ++--------------------- 1 file changed, 14 insertions(+), 175 deletions(-) diff --git a/frontend/src/mocks/handlers/promotion.ts b/frontend/src/mocks/handlers/promotion.ts index 067db2fc8..fbc6ab043 100644 --- a/frontend/src/mocks/handlers/promotion.ts +++ b/frontend/src/mocks/handlers/promotion.ts @@ -7,6 +7,7 @@ const API_BASE_URL = // Mock 데이터 export const mockPromotionArticles: PromotionArticle[] = [ { + id: '1', clubName: 'WAP', clubId: '67e54ae51cfd27718dd40bec', title: '💌✨WAP 최종 전시회 초대장 ✨💌', @@ -21,182 +22,20 @@ export const mockPromotionArticles: PromotionArticle[] = [ 'https://images.unsplash.com/photo-1522071820081-009f0129c71c?w=800', ], }, - { - clubName: '디자인 스터디', - clubId: 'club-2', - title: 'UI/UX 디자인 워크샵', - location: '온라인 (Zoom)', - eventStartDate: '2026-04-15', - eventEndDate: '2026-04-20', - description: '실무 디자이너와 함께하는 5일간의 집중 워크샵', - images: [], - }, - { - clubName: '창업 동아리', - clubId: 'club-3', - title: '스타트업 데모데이', - location: '부산 해운대구', - eventStartDate: '2024-05-10', - eventEndDate: '2024-05-10', - description: '학생 창업팀의 아이디어를 공유하는 자리', - images: [ - 'https://images.unsplash.com/photo-1559136555-9303baea8ebd?w=800', - 'https://images.unsplash.com/photo-1559136555-9303baea8ebd?w=800', - 'https://images.unsplash.com/photo-1559136555-9303baea8ebd?w=800', - ], - }, - { - clubName: '창업 동아리', - clubId: 'club-3', - title: '스타트업 데모데이', - location: '부산 해운대구', - eventStartDate: '2024-05-10', - eventEndDate: '2024-05-10', - description: '학생 창업팀의 아이디어를 공유하는 자리', - images: [ - 'https://images.unsplash.com/photo-1559136555-9303baea8ebd?w=800', - 'https://images.unsplash.com/photo-1559136555-9303baea8ebd?w=800', - 'https://images.unsplash.com/photo-1559136555-9303baea8ebd?w=800', - ], - }, - { - clubName: '창업 동아리', - clubId: 'club-3', - title: '스타트업 데모데이', - location: '부산 해운대구', - eventStartDate: '2024-05-10', - eventEndDate: '2024-05-10', - description: '학생 창업팀의 아이디어를 공유하는 자리', - images: [ - 'https://images.unsplash.com/photo-1559136555-9303baea8ebd?w=800', - 'https://images.unsplash.com/photo-1559136555-9303baea8ebd?w=800', - 'https://images.unsplash.com/photo-1559136555-9303baea8ebd?w=800', - ], - }, - { - clubName: '창업 동아리', - clubId: 'club-3', - title: '스타트업 데모데이', - location: '부산 해운대구', - eventStartDate: '2024-05-10', - eventEndDate: '2024-05-10', - description: '학생 창업팀의 아이디어를 공유하는 자리', - images: [ - 'https://images.unsplash.com/photo-1559136555-9303baea8ebd?w=800', - 'https://images.unsplash.com/photo-1559136555-9303baea8ebd?w=800', - 'https://images.unsplash.com/photo-1559136555-9303baea8ebd?w=800', - ], - }, - { - clubName: '창업 동아리', - clubId: 'club-3', - title: '스타트업 데모데이', - location: '부산 해운대구', - eventStartDate: '2024-05-10', - eventEndDate: '2024-05-10', - description: '학생 창업팀의 아이디어를 공유하는 자리', - images: [ - 'https://images.unsplash.com/photo-1559136555-9303baea8ebd?w=800', - 'https://images.unsplash.com/photo-1559136555-9303baea8ebd?w=800', - 'https://images.unsplash.com/photo-1559136555-9303baea8ebd?w=800', - ], - }, - { - clubName: '창업 동아리', - clubId: 'club-3', - title: '스타트업 데모데이', - location: '부산 해운대구', - eventStartDate: '2024-05-10', - eventEndDate: '2024-05-10', - description: '학생 창업팀의 아이디어를 공유하는 자리', - images: [ - 'https://images.unsplash.com/photo-1559136555-9303baea8ebd?w=800', - 'https://images.unsplash.com/photo-1559136555-9303baea8ebd?w=800', - 'https://images.unsplash.com/photo-1559136555-9303baea8ebd?w=800', - ], - }, - { - clubName: '창업 동아리', - clubId: 'club-3', - title: '스타트업 데모데이', - location: '부산 해운대구', - eventStartDate: '2024-05-10', - eventEndDate: '2024-05-10', - description: '학생 창업팀의 아이디어를 공유하는 자리', - images: [ - 'https://images.unsplash.com/photo-1559136555-9303baea8ebd?w=800', - 'https://images.unsplash.com/photo-1559136555-9303baea8ebd?w=800', - 'https://images.unsplash.com/photo-1559136555-9303baea8ebd?w=800', - ], - }, - { - clubName: '창업 동아리', - clubId: 'club-3', - title: '스타트업 데모데이', - location: '부산 해운대구', - eventStartDate: '2024-05-10', - eventEndDate: '2024-05-10', - description: '학생 창업팀의 아이디어를 공유하는 자리', - images: [ - 'https://images.unsplash.com/photo-1559136555-9303baea8ebd?w=800', - 'https://images.unsplash.com/photo-1559136555-9303baea8ebd?w=800', - 'https://images.unsplash.com/photo-1559136555-9303baea8ebd?w=800', - ], - }, - { - clubName: '창업 동아리', - clubId: 'club-3', - title: '스타트업 데모데이', - location: '부산 해운대구', - eventStartDate: '2024-05-10', - eventEndDate: '2024-05-10', - description: '학생 창업팀의 아이디어를 공유하는 자리', - images: [ - 'https://images.unsplash.com/photo-1559136555-9303baea8ebd?w=800', - 'https://images.unsplash.com/photo-1559136555-9303baea8ebd?w=800', - 'https://images.unsplash.com/photo-1559136555-9303baea8ebd?w=800', - ], - }, - { - clubName: '창업 동아리', - clubId: 'club-3', - title: '스타트업 데모데이', - location: '부산 해운대구', - eventStartDate: '2024-05-10', - eventEndDate: '2024-05-10', - description: '학생 창업팀의 아이디어를 공유하는 자리', - images: [ - 'https://images.unsplash.com/photo-1559136555-9303baea8ebd?w=800', - 'https://images.unsplash.com/photo-1559136555-9303baea8ebd?w=800', - 'https://images.unsplash.com/photo-1559136555-9303baea8ebd?w=800', - ], - }, - { - clubName: '창업 동아리', - clubId: 'club-3', - title: '스타트업 데모데이', - location: '부산 해운대구', - eventStartDate: '2024-05-10', - eventEndDate: '2024-05-10', - description: '학생 창업팀의 아이디어를 공유하는 자리', - images: [ - 'https://images.unsplash.com/photo-1559136555-9303baea8ebd?w=800', - 'https://images.unsplash.com/photo-1559136555-9303baea8ebd?w=800', - 'https://images.unsplash.com/photo-1559136555-9303baea8ebd?w=800', - ], - }, - { - clubName: '창업 동아리', - clubId: 'club-3', - title: '스타트업 데모데이', - location: '부산 해운대구', - eventStartDate: '2024-05-10', - eventEndDate: '2024-05-10', - description: '학생 창업팀의 아이디어를 공유하는 자리', + { + id: '2', + clubName: 'WAP', + clubId: '67e54ae51cfd27718dd40bec', + title: '💌✨WAP 최종 전시회 초대장 ✨💌', + location: '부경대학교 동원 장보고관 1층', + eventStartDate: '2025-11-28 06:00', + eventEndDate: '2025-11-28 09:00', + description: + 'WAP 최종 전시회에 여러분을 초대합니다! \n\n이번 전시회에서는 WAP 팀이 한 학기 동안 열심히 준비한 프로젝트들을 선보입니다. 다양한 작품과 아이디어가 가득한 이번 전시회에서 여러분의 많은 관심과 참여 부탁드립니다! 🙌\n\n#WAP #최종전시회 #부경대학교', images: [ - 'https://images.unsplash.com/photo-1559136555-9303baea8ebd?w=800', - 'https://images.unsplash.com/photo-1559136555-9303baea8ebd?w=800', - 'https://images.unsplash.com/photo-1559136555-9303baea8ebd?w=800', + 'https://images.unsplash.com/photo-1522071820081-009f0129c71c?w=800', + 'https://images.unsplash.com/photo-1522071820081-009f0129c71c?w=800', + 'https://images.unsplash.com/photo-1522071820081-009f0129c71c?w=800', ], }, ]; From 102a2ca5e3e98ae126f23ab4d6cdb2e0582bf442 Mon Sep 17 00:00:00 2001 From: suhyun113 <163711629+suhyun113@users.noreply.github.com> Date: Fri, 20 Mar 2026 03:54:33 +0900 Subject: [PATCH 053/172] =?UTF-8?q?refactor:=20Prettier=20=EC=BD=94?= =?UTF-8?q?=EB=93=9C=20=ED=8F=AC=EB=A7=B7=20=EC=A0=95=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/src/apis/promotion.test.ts | 6 ++-- frontend/src/apis/promotion.ts | 2 +- frontend/src/constants/eventName.ts | 1 - frontend/src/mocks/handlers/promotion.ts | 2 +- .../list/PromotionCard/PromotionCard.tsx | 2 +- .../src/utils/promotionNotification.test.ts | 30 ++++++++++++------- 6 files changed, 24 insertions(+), 19 deletions(-) diff --git a/frontend/src/apis/promotion.test.ts b/frontend/src/apis/promotion.test.ts index 7d36cb224..3b7bd1ef6 100644 --- a/frontend/src/apis/promotion.test.ts +++ b/frontend/src/apis/promotion.test.ts @@ -67,9 +67,7 @@ describe('promotion API', () => { expect(result).toEqual(mockArticles); - expect(fetchMock).toHaveBeenCalledWith( - `${API_BASE_URL}/api/promotion`, - ); + expect(fetchMock).toHaveBeenCalledWith(`${API_BASE_URL}/api/promotion`); }); it('articles 필드가 없으면 빈 배열을 반환한다', async () => { @@ -158,4 +156,4 @@ describe('promotion API', () => { ); }); }); -}); \ No newline at end of file +}); diff --git a/frontend/src/apis/promotion.ts b/frontend/src/apis/promotion.ts index 5555123c4..f067689f2 100644 --- a/frontend/src/apis/promotion.ts +++ b/frontend/src/apis/promotion.ts @@ -56,4 +56,4 @@ export const createPromotionArticle = async ( }); return handleResponse(response, '홍보게시판 글 추가에 실패했습니다.'); -}; \ No newline at end of file +}; diff --git a/frontend/src/constants/eventName.ts b/frontend/src/constants/eventName.ts index 7a100a5ab..f773e4df1 100644 --- a/frontend/src/constants/eventName.ts +++ b/frontend/src/constants/eventName.ts @@ -57,7 +57,6 @@ export const USER_EVENT = { // 홍보 PROMOTION_BUTTON_CLICKED: 'Promotion Button Clicked', PROMOTION_CARD_CLICKED: 'Promotion Card Clicked', - } as const; export const ADMIN_EVENT = { diff --git a/frontend/src/mocks/handlers/promotion.ts b/frontend/src/mocks/handlers/promotion.ts index fbc6ab043..432014646 100644 --- a/frontend/src/mocks/handlers/promotion.ts +++ b/frontend/src/mocks/handlers/promotion.ts @@ -22,7 +22,7 @@ export const mockPromotionArticles: PromotionArticle[] = [ 'https://images.unsplash.com/photo-1522071820081-009f0129c71c?w=800', ], }, - { + { id: '2', clubName: 'WAP', clubId: '67e54ae51cfd27718dd40bec', diff --git a/frontend/src/pages/PromotionPage/components/list/PromotionCard/PromotionCard.tsx b/frontend/src/pages/PromotionPage/components/list/PromotionCard/PromotionCard.tsx index f62b81971..b1910d2d1 100644 --- a/frontend/src/pages/PromotionPage/components/list/PromotionCard/PromotionCard.tsx +++ b/frontend/src/pages/PromotionPage/components/list/PromotionCard/PromotionCard.tsx @@ -30,7 +30,7 @@ const PromotionCard = ({ article }: PromotionCardProps) => { trackEvent(USER_EVENT.PROMOTION_CARD_CLICKED, { clubId: article.clubId, - }) + }); navigateToPromotionDetail(`/promotions/${article.clubId}`); }; diff --git a/frontend/src/utils/promotionNotification.test.ts b/frontend/src/utils/promotionNotification.test.ts index 26d802f99..6bb8f96f3 100644 --- a/frontend/src/utils/promotionNotification.test.ts +++ b/frontend/src/utils/promotionNotification.test.ts @@ -1,9 +1,9 @@ -import { - getLatestPromotionTime, - getLastCheckedTime, - setLastCheckedTime -} from './promotionNotification'; import { PromotionArticle } from '@/types/promotion'; +import { + getLastCheckedTime, + getLatestPromotionTime, + setLastCheckedTime, +} from './promotionNotification'; describe('promotionNotification 유틸 함수 테스트', () => { beforeEach(() => { @@ -18,21 +18,29 @@ describe('promotionNotification 유틸 함수 테스트', () => { it('ID가 유효하지 않으면 eventStartDate를 기준으로 시간을 반환해야 한다', () => { const dateStr = '2026-03-19T00:00:00.000Z'; const mockArticles = [ - { id: 'short-id', eventStartDate: dateStr } + { id: 'short-id', eventStartDate: dateStr }, ] as PromotionArticle[]; - expect(getLatestPromotionTime(mockArticles)).toBe(new Date(dateStr).getTime()); + expect(getLatestPromotionTime(mockArticles)).toBe( + new Date(dateStr).getTime(), + ); }); it('여러 개의 글 중 가장 최근 시간을 반환해야 한다', () => { const mockArticles = [ - { id: '600000000000000000000000', eventStartDate: '2021-01-01T00:00:00Z' }, - { id: '650000000000000000000000', eventStartDate: '2023-01-01T00:00:00Z' } + { + id: '600000000000000000000000', + eventStartDate: '2021-01-01T00:00:00Z', + }, + { + id: '650000000000000000000000', + eventStartDate: '2023-01-01T00:00:00Z', + }, ] as PromotionArticle[]; const time1 = parseInt('60000000', 16) * 1000; const time2 = parseInt('65000000', 16) * 1000; - + expect(getLatestPromotionTime(mockArticles)).toBe(Math.max(time1, time2)); }); }); @@ -53,4 +61,4 @@ describe('promotionNotification 유틸 함수 테스트', () => { expect(getLastCheckedTime()).toBe(testTime); }); }); -}); \ No newline at end of file +}); From b100393909a690f6f6dc626d0d0e63807625cac3 Mon Sep 17 00:00:00 2001 From: suhyun113 <163711629+suhyun113@users.noreply.github.com> Date: Sun, 22 Mar 2026 20:27:45 +0900 Subject: [PATCH 054/172] =?UTF-8?q?style:=20dday=20=ED=85=8D=EC=8A=A4?= =?UTF-8?q?=ED=8A=B8=20=EC=83=89=EC=83=81=20=EB=B3=80=EA=B2=BD=20=EB=B0=8F?= =?UTF-8?q?=20=EC=A2=85=EB=A3=8C=EC=9D=B8=20=EA=B2=BD=EC=9A=B0=20=EC=83=89?= =?UTF-8?q?=EC=83=81=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../list/PromotionCard/DdayBadge/DdayBadge.styles.ts | 4 ++-- .../components/list/PromotionCard/DdayBadge/DdayBadge.tsx | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/frontend/src/pages/PromotionPage/components/list/PromotionCard/DdayBadge/DdayBadge.styles.ts b/frontend/src/pages/PromotionPage/components/list/PromotionCard/DdayBadge/DdayBadge.styles.ts index e9cf2aac1..7d2c355ae 100644 --- a/frontend/src/pages/PromotionPage/components/list/PromotionCard/DdayBadge/DdayBadge.styles.ts +++ b/frontend/src/pages/PromotionPage/components/list/PromotionCard/DdayBadge/DdayBadge.styles.ts @@ -19,8 +19,8 @@ export const Container = styled.div` rgba(0, 0, 0, 0.08) 0px 1px 4px; `; -export const DdayText = styled.h1` - color: ${colors.gray[800]}; +export const DdayText = styled.h1<{ isEnded: boolean }>` + color: ${({ isEnded }) => (isEnded ? colors.gray[700] : colors.gray[900])}; font-size: 14px; font-weight: 600; letter-spacing: -0.02em; diff --git a/frontend/src/pages/PromotionPage/components/list/PromotionCard/DdayBadge/DdayBadge.tsx b/frontend/src/pages/PromotionPage/components/list/PromotionCard/DdayBadge/DdayBadge.tsx index 9661f03f3..44d2e288a 100644 --- a/frontend/src/pages/PromotionPage/components/list/PromotionCard/DdayBadge/DdayBadge.tsx +++ b/frontend/src/pages/PromotionPage/components/list/PromotionCard/DdayBadge/DdayBadge.tsx @@ -17,7 +17,7 @@ const DdayBadge = ({ dday }: DdayBadgeProps) => { return ( - {label} + {label} ); }; From e378c4b741cc582c5e5389487d84ad7130dc4241 Mon Sep 17 00:00:00 2001 From: seongwon seo Date: Mon, 23 Mar 2026 00:35:20 +0900 Subject: [PATCH 055/172] =?UTF-8?q?feat:=20=EC=BA=98=EB=A6=B0=EB=8D=94=20O?= =?UTF-8?q?Auth=20API=20=EC=9C=A0=ED=8B=B8=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/src/apis/calendarOAuth.ts | 267 +++++++++++++++++++++++++++++ 1 file changed, 267 insertions(+) create mode 100644 frontend/src/apis/calendarOAuth.ts diff --git a/frontend/src/apis/calendarOAuth.ts b/frontend/src/apis/calendarOAuth.ts new file mode 100644 index 000000000..a8e1c6f4c --- /dev/null +++ b/frontend/src/apis/calendarOAuth.ts @@ -0,0 +1,267 @@ +import API_BASE_URL from '@/constants/api'; +import { secureFetch } from './auth/secureFetch'; +import { handleResponse } from './utils/apiHelpers'; + +export interface GoogleCalendarItem { + id: string; + summary: string; + primary?: boolean; +} + +export interface GoogleEventItem { + id: string; + summary?: string; + htmlLink?: string; + start?: { + dateTime?: string; + date?: string; + }; + end?: { + dateTime?: string; + date?: string; + }; +} + +export interface NotionSearchItem { + id: string; + object: string; + url?: string; + last_edited_time?: string; + properties?: Record; +} + +export interface NotionDatabaseOption { + id: string; + title: string; +} + +export const fetchGoogleCalendarList = async (accessToken: string) => { + const response = await fetch( + 'https://www.googleapis.com/calendar/v3/users/me/calendarList', + { + headers: { + Authorization: `Bearer ${accessToken}`, + }, + }, + ); + + if (!response.ok) { + throw new Error('Google 캘린더 목록 조회에 실패했습니다.'); + } + + const data = await response.json(); + return (data.items ?? []) as GoogleCalendarItem[]; +}; + +export const fetchGooglePrimaryEvents = async (accessToken: string) => { + const query = new URLSearchParams({ + maxResults: '10', + singleEvents: 'true', + orderBy: 'startTime', + timeMin: new Date().toISOString(), + }); + + const response = await fetch( + `https://www.googleapis.com/calendar/v3/calendars/primary/events?${query.toString()}`, + { + headers: { + Authorization: `Bearer ${accessToken}`, + }, + }, + ); + + if (!response.ok) { + throw new Error('Google 캘린더 이벤트 조회에 실패했습니다.'); + } + + const data = await response.json(); + return (data.items ?? []) as GoogleEventItem[]; +}; + +interface NotionTokenRequest { + code: string; +} + +interface NotionTokenResponse { + accessToken?: string; + workspaceName?: string; + workspaceId?: string; +} + +interface NotionAuthorizeResponse { + authorizeUrl: string; +} + +interface NotionPagesPayload { + items?: NotionSearchItem[]; + results?: NotionSearchItem[]; + total_results?: number; + totalResults?: number; + database_id?: string; + databaseId?: string; +} + +interface NotionDatabasePayload { + id: string; + object?: string; + title?: Array<{ plain_text?: string }>; +} + +export interface NotionPagesResponse { + items: NotionSearchItem[]; + totalResults: number; + databaseId?: string; +} + +export const fetchNotionAuthorizeUrl = async (state?: string) => { + const params = new URLSearchParams(); + if (state) { + params.set('state', state); + } + + const url = `${API_BASE_URL}/api/integration/notion/oauth/authorize${params.toString() ? `?${params.toString()}` : ''}`; + const response = await secureFetch(url, { + method: 'GET', + headers: { + Accept: 'application/json', + }, + }); + + const data = await handleResponse( + response, + 'Notion 인가 URL 생성에 실패했습니다.', + ); + + if (!data?.authorizeUrl) { + throw new Error('Notion 인가 URL이 비어있습니다.'); + } + return data.authorizeUrl; +}; + +export const exchangeNotionCode = async ({ code }: NotionTokenRequest) => { + const response = await secureFetch( + `${API_BASE_URL}/api/integration/notion/oauth/token`, + { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + code, + }), + }, + ); + + const data = await handleResponse( + response, + 'Notion 토큰 교환에 실패했습니다.', + ); + return data; +}; + +export const fetchNotionPages = async () => { + const response = await secureFetch( + `${API_BASE_URL}/api/integration/notion/pages`, + { + method: 'GET', + headers: { + Accept: 'application/json', + }, + }, + ); + + const data = await handleResponse( + response, + 'Notion 데이터 조회에 실패했습니다.', + ); + if (Array.isArray(data)) { + return { + items: data, + totalResults: data.length, + } satisfies NotionPagesResponse; + } + + const items = (data?.items ?? data?.results ?? []) as NotionSearchItem[]; + const totalResults = + data?.total_results ?? data?.totalResults ?? items.length; + const databaseId = data?.database_id ?? data?.databaseId; + return { + items, + totalResults, + databaseId, + } satisfies NotionPagesResponse; +}; + +export const fetchNotionDatabasePages = async ({ + databaseId, + dateProperty, +}: { + databaseId: string; + dateProperty?: string; +}) => { + const params = new URLSearchParams(); + if (dateProperty) { + params.set('dateProperty', dateProperty); + } + + const query = params.toString(); + const response = await secureFetch( + `${API_BASE_URL}/api/integration/notion/databases/${databaseId}/pages${query ? `?${query}` : ''}`, + { + method: 'GET', + headers: { + Accept: 'application/json', + }, + }, + ); + + const data = await handleResponse( + response, + 'Notion 데이터베이스 페이지 조회에 실패했습니다.', + ); + + if (Array.isArray(data)) { + return { + items: data, + totalResults: data.length, + databaseId, + } satisfies NotionPagesResponse; + } + + const items = (data?.items ?? data?.results ?? []) as NotionSearchItem[]; + const totalResults = + data?.total_results ?? data?.totalResults ?? items.length; + const resolvedDatabaseId = + data?.database_id ?? data?.databaseId ?? databaseId; + return { + items, + totalResults, + databaseId: resolvedDatabaseId, + } satisfies NotionPagesResponse; +}; + +export const fetchNotionDatabases = async () => { + const response = await secureFetch( + `${API_BASE_URL}/api/integration/notion/databases`, + { + method: 'GET', + headers: { + Accept: 'application/json', + }, + }, + ); + + const data = await handleResponse< + { results?: NotionDatabasePayload[] } | NotionDatabasePayload[] + >(response, 'Notion 데이터베이스 목록 조회에 실패했습니다.'); + + const databases = Array.isArray(data) ? data : (data?.results ?? []); + return databases.map((database) => ({ + id: database.id, + title: + database.title + ?.map((segment) => segment.plain_text ?? '') + .join('') + .trim() || '(이름 없는 데이터베이스)', + })) as NotionDatabaseOption[]; +}; From f4e5c7ca33306294833f12dcf9868378c6799c8c Mon Sep 17 00:00:00 2001 From: seongwon seo Date: Mon, 23 Mar 2026 00:35:20 +0900 Subject: [PATCH 056/172] =?UTF-8?q?feat:=20=EC=BA=98=EB=A6=B0=EB=8D=94=20?= =?UTF-8?q?=EB=8F=99=EA=B8=B0=ED=99=94=20=EC=9C=A0=ED=8B=B8=20=EB=B6=84?= =?UTF-8?q?=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/src/utils/calendarSyncUtils.ts | 159 ++++++++++++++++++++++++ 1 file changed, 159 insertions(+) create mode 100644 frontend/src/utils/calendarSyncUtils.ts diff --git a/frontend/src/utils/calendarSyncUtils.ts b/frontend/src/utils/calendarSyncUtils.ts new file mode 100644 index 000000000..301847535 --- /dev/null +++ b/frontend/src/utils/calendarSyncUtils.ts @@ -0,0 +1,159 @@ +import type { NotionSearchItem } from '@/apis/calendarOAuth'; + +/** + * CalendarSyncTab 전용 유틸 모음. + * - OAuth 보조 유틸(redirect/state/token 표시) + * - 캘린더 날짜 계산 유틸 + * - Notion page -> 캘린더 이벤트 변환 유틸 + */ + +/** 캘린더 헤더 요일 라벨(일~토). */ +export const WEEKDAY_LABELS = [ + '일', + '월', + '화', + '수', + '목', + '금', + '토', +] as const; + +/** 현재 origin 기준 CalendarSync 콜백 URI를 만든다. */ +export const buildDefaultRedirectUri = () => + `${window.location.origin}/admin/calendar-sync`; + +/** OAuth state용 난수 문자열을 생성한다. */ +export const createState = () => + globalThis.crypto?.randomUUID?.() ?? Math.random().toString(36).slice(2); + +/** 토큰 표시용 마스킹 문자열을 만든다. */ +export const maskToken = (token: string) => { + if (token.length <= 16) return token; + return `${token.slice(0, 8)}...${token.slice(-8)}`; +}; + +/** + * 날짜/시간 문자열을 한국어 로케일 텍스트로 변환한다. + * 파싱 실패 시 원문을 그대로 반환한다. + */ +export const formatDateText = (dateText?: string) => { + if (!dateText) return '-'; + try { + const date = new Date(dateText); + if (Number.isNaN(date.getTime())) { + return dateText; + } + return date.toLocaleString('ko-KR'); + } catch { + return dateText; + } +}; + +/** + * 다양한 날짜 문자열을 `YYYY-MM-DD` 키로 정규화한다. + * 유효하지 않은 값이면 null을 반환한다. + */ +export const parseDateKey = (dateText: string) => { + if (/^\d{4}-\d{2}-\d{2}$/.test(dateText)) { + return dateText; + } + + const parsed = new Date(dateText); + if (Number.isNaN(parsed.getTime())) return null; + + const localYear = parsed.getFullYear(); + const localMonth = String(parsed.getMonth() + 1).padStart(2, '0'); + const localDay = String(parsed.getDate()).padStart(2, '0'); + return `${localYear}-${localMonth}-${localDay}`; +}; + +/** Date 객체를 `YYYY-MM-DD` 키 문자열로 변환한다. */ +export const buildDateKeyFromDate = (date: Date) => + `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, '0')}-${String(date.getDate()).padStart(2, '0')}`; + +/** `YYYY-MM-DD` 키 문자열을 Date 객체(로컬 시간대)로 변환한다. */ +export const dateFromKey = (dateKey: string) => { + const [year, month, day] = dateKey.split('-').map(Number); + return new Date(year, month - 1, day); +}; + +/** + * 월 기준 캘린더 그리드(주 시작~주 끝 포함) 날짜 배열을 생성한다. + * 반환 배열은 7의 배수 길이를 가진다. + */ +export const buildMonthCalendarDays = (month: Date) => { + const monthStart = new Date(month.getFullYear(), month.getMonth(), 1); + const monthEnd = new Date(month.getFullYear(), month.getMonth() + 1, 0); + + const gridStart = new Date(monthStart); + gridStart.setDate(monthStart.getDate() - monthStart.getDay()); + + const gridEnd = new Date(monthEnd); + gridEnd.setDate(monthEnd.getDate() + (6 - monthEnd.getDay())); + + const days: Date[] = []; + const cursor = new Date(gridStart); + while (cursor <= gridEnd) { + days.push(new Date(cursor)); + cursor.setDate(cursor.getDate() + 1); + } + return days; +}; + +/** 월 라벨을 `YYYY년 MM월` 형식으로 포맷한다. */ +export const formatMonthLabel = (month: Date) => + `${month.getFullYear()}년 ${String(month.getMonth() + 1).padStart(2, '0')}월`; + +/** Notion page에서 추출한 캘린더 이벤트 모델. */ +export interface NotionCalendarEvent { + id: string; + title: string; + start: string; + dateKey: string; + end?: string; + url?: string; +} + +/** + * Notion page 응답을 캘린더 이벤트로 변환한다. + * - date 타입 속성이 없거나 + * - 날짜 파싱 불가한 경우 null을 반환한다. + */ +export const parseNotionCalendarEvent = ( + item: NotionSearchItem, +): NotionCalendarEvent | null => { + const properties = item.properties; + if (!properties) return null; + + const entries = Object.values(properties) as Array>; + const dateProperty = entries.find( + (property) => + property?.type === 'date' && + typeof (property.date as { start?: string } | undefined)?.start === + 'string', + ) as { date?: { start?: string; end?: string } } | undefined; + + const start = dateProperty?.date?.start; + if (!start) return null; + const dateKey = parseDateKey(start); + if (!dateKey) return null; + + const titleProperty = entries.find( + (property) => property?.type === 'title' && Array.isArray(property.title), + ) as { title?: Array<{ plain_text?: string }> } | undefined; + + const title = + titleProperty?.title + ?.map((segment) => segment.plain_text ?? '') + .join('') + .trim() || '(제목 없음)'; + + return { + id: item.id, + title, + start, + dateKey, + end: dateProperty?.date?.end, + url: item.url, + }; +}; From 46361ce0b9549507a5bbd96f69069f443e00903c Mon Sep 17 00:00:00 2001 From: seongwon seo Date: Mon, 23 Mar 2026 00:35:20 +0900 Subject: [PATCH 057/172] =?UTF-8?q?test:=20=EC=BA=98=EB=A6=B0=EB=8D=94=20?= =?UTF-8?q?=EB=8F=99=EA=B8=B0=ED=99=94=20=EC=9C=A0=ED=8B=B8=20=ED=85=8C?= =?UTF-8?q?=EC=8A=A4=ED=8A=B8=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/src/utils/calendarSyncUtils.test.ts | 155 +++++++++++++++++++ 1 file changed, 155 insertions(+) create mode 100644 frontend/src/utils/calendarSyncUtils.test.ts diff --git a/frontend/src/utils/calendarSyncUtils.test.ts b/frontend/src/utils/calendarSyncUtils.test.ts new file mode 100644 index 000000000..99d155426 --- /dev/null +++ b/frontend/src/utils/calendarSyncUtils.test.ts @@ -0,0 +1,155 @@ +import type { NotionSearchItem } from '@/apis/calendarOAuth'; +import { + buildDateKeyFromDate, + buildDefaultRedirectUri, + buildMonthCalendarDays, + createState, + dateFromKey, + formatDateText, + formatMonthLabel, + maskToken, + parseDateKey, + parseNotionCalendarEvent, + WEEKDAY_LABELS, +} from './calendarSyncUtils'; + +describe('calendarSyncUtils', () => { + describe('기본 유틸', () => { + it('redirect uri는 calendar-sync 경로를 포함한다', () => { + expect(buildDefaultRedirectUri()).toContain('/admin/calendar-sync'); + }); + + it('state 문자열을 생성한다', () => { + const state = createState(); + expect(typeof state).toBe('string'); + expect(state.length).toBeGreaterThan(0); + }); + + it('토큰 마스킹은 앞 8자리/뒤 8자리만 노출한다', () => { + const token = '12345678abcdefghijklmnop87654321'; + expect(maskToken(token)).toBe('12345678...87654321'); + expect(maskToken('short-token')).toBe('short-token'); + }); + }); + + describe('날짜 유틸', () => { + it('요일 라벨은 일~토 순서를 유지한다', () => { + expect(WEEKDAY_LABELS).toEqual([ + '일', + '월', + '화', + '수', + '목', + '금', + '토', + ]); + }); + + it('parseDateKey는 YYYY-MM-DD 형식을 그대로 반환한다', () => { + expect(parseDateKey('2026-03-22')).toBe('2026-03-22'); + }); + + it('parseDateKey는 ISO datetime을 YYYY-MM-DD로 정규화한다', () => { + expect(parseDateKey('2026-03-22T06:25:00.000Z')).toBeTruthy(); + expect(parseDateKey('invalid-date')).toBeNull(); + }); + + it('date key와 Date 객체 변환이 일관된다', () => { + const key = '2026-03-19'; + const date = dateFromKey(key); + expect(buildDateKeyFromDate(date)).toBe(key); + }); + + it('월 캘린더 그리드는 주 단위(7개) 배수로 생성된다', () => { + const days = buildMonthCalendarDays(new Date(2026, 2, 1)); + expect(days.length % 7).toBe(0); + expect(buildDateKeyFromDate(days[0])).toBe('2026-03-01'); + expect(buildDateKeyFromDate(days[days.length - 1])).toBe('2026-04-04'); + }); + + it('월 라벨은 YYYY년 MM월 형식이다', () => { + expect(formatMonthLabel(new Date(2026, 2, 1))).toBe('2026년 03월'); + }); + + it('유효하지 않은 날짜 텍스트는 원문을 반환한다', () => { + expect(formatDateText(undefined)).toBe('-'); + expect(formatDateText('not-a-date')).toBe('not-a-date'); + }); + }); + + describe('Notion 이벤트 변환', () => { + const createNotionItem = ( + overrides?: Partial, + ): NotionSearchItem => ({ + id: 'page-1', + object: 'page', + url: 'https://www.notion.so/example', + properties: { + 날짜: { + type: 'date', + date: { + start: '2026-03-19', + end: null, + }, + }, + 이름: { + type: 'title', + title: [{ plain_text: '캘린더 테스트' }], + }, + }, + ...overrides, + }); + + it('날짜/제목 속성이 있으면 캘린더 이벤트로 변환한다', () => { + const item = createNotionItem(); + const event = parseNotionCalendarEvent(item); + + expect(event).not.toBeNull(); + expect(event?.id).toBe(item.id); + expect(event?.title).toBe('캘린더 테스트'); + expect(event?.dateKey).toBe('2026-03-19'); + }); + + it('title 속성이 없어도 기본 제목으로 변환한다', () => { + const item = createNotionItem({ + properties: { + 날짜: { + type: 'date', + date: { + start: '2026-03-19', + end: null, + }, + }, + } as NotionSearchItem['properties'], + }); + + const event = parseNotionCalendarEvent(item); + expect(event?.title).toBe('(제목 없음)'); + }); + + it('date 속성이 없거나 잘못된 경우 null을 반환한다', () => { + const missingDate = createNotionItem({ + properties: { + 이름: { + type: 'title', + title: [{ plain_text: '제목만 있음' }], + }, + } as NotionSearchItem['properties'], + }); + const invalidDate = createNotionItem({ + properties: { + 날짜: { + type: 'date', + date: { + start: 'invalid-date', + end: null, + }, + }, + } as NotionSearchItem['properties'], + }); + + expect(parseNotionCalendarEvent(missingDate)).toBeNull(); + expect(parseNotionCalendarEvent(invalidDate)).toBeNull(); + }); + }); +}); From ea8239017ed1c06731922f213463ce7635eba41d Mon Sep 17 00:00:00 2001 From: seongwon seo Date: Mon, 23 Mar 2026 00:35:35 +0900 Subject: [PATCH 058/172] =?UTF-8?q?style:=20=EC=BA=98=EB=A6=B0=EB=8D=94=20?= =?UTF-8?q?=EC=97=B0=EB=8F=99=20=ED=83=AD=20=EC=8A=A4=ED=83=80=EC=9D=BC=20?= =?UTF-8?q?=EC=A0=95=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../CalendarSyncTab/CalendarSyncTab.styles.ts | 285 ++++++++++++++++++ 1 file changed, 285 insertions(+) create mode 100644 frontend/src/pages/AdminPage/tabs/CalendarSyncTab/CalendarSyncTab.styles.ts diff --git a/frontend/src/pages/AdminPage/tabs/CalendarSyncTab/CalendarSyncTab.styles.ts b/frontend/src/pages/AdminPage/tabs/CalendarSyncTab/CalendarSyncTab.styles.ts new file mode 100644 index 000000000..00c49247b --- /dev/null +++ b/frontend/src/pages/AdminPage/tabs/CalendarSyncTab/CalendarSyncTab.styles.ts @@ -0,0 +1,285 @@ +import styled from 'styled-components'; + +export const Container = styled.div` + display: flex; + flex-direction: column; + gap: 24px; +`; + +export const Description = styled.p` + font-size: 0.94rem; + line-height: 1.5; + color: #4b5563; +`; + +export const ConfigGrid = styled.div` + display: grid; + grid-template-columns: repeat(2, minmax(0, 1fr)); + gap: 14px; + + @media (max-width: 1024px) { + grid-template-columns: 1fr; + } +`; + +export const Block = styled.div` + border: 1px solid #e5e7eb; + border-radius: 12px; + padding: 14px; +`; + +export const BlockTitle = styled.h4` + margin: 0 0 10px; + font-size: 1rem; + font-weight: 700; +`; + +export const Buttons = styled.div` + margin-top: 12px; + display: flex; + gap: 10px; + flex-wrap: wrap; +`; + +export const SelectRow = styled.div` + display: flex; + gap: 10px; + align-items: center; + margin-top: 10px; + flex-wrap: wrap; +`; + +export const Select = styled.select` + min-width: 240px; + height: 42px; + border: 1px solid #d1d5db; + border-radius: 10px; + padding: 0 12px; + font-size: 0.9rem; + color: #111827; + background: #ffffff; +`; + +export const TokenText = styled.code` + display: block; + margin-top: 10px; + padding: 10px; + border-radius: 8px; + background: #f8fafc; + font-size: 0.82rem; + color: #1f2937; + word-break: break-all; +`; + +export const StatusText = styled.p` + margin-top: 10px; + font-size: 0.9rem; + font-weight: 600; + color: #0f766e; +`; + +export const ErrorText = styled.p` + margin-top: 8px; + font-size: 0.9rem; + color: #b91c1c; + font-weight: 600; +`; + +export const DataGrid = styled.div` + display: grid; + grid-template-columns: repeat(2, minmax(0, 1fr)); + gap: 14px; + + @media (max-width: 1024px) { + grid-template-columns: 1fr; + } +`; + +export const DataCard = styled.div` + border: 1px solid #e5e7eb; + border-radius: 12px; + padding: 14px; + background: #ffffff; +`; + +export const WideDataCard = styled(DataCard)` + grid-column: 1 / -1; +`; + +export const DataTitle = styled.h4` + margin: 0 0 10px; + font-size: 1rem; + font-weight: 700; +`; + +export const Empty = styled.p` + font-size: 0.9rem; + color: #6b7280; +`; + +export const List = styled.ul` + margin: 0; + padding-left: 18px; + display: flex; + flex-direction: column; + gap: 8px; +`; + +export const ListItem = styled.li` + font-size: 0.92rem; + color: #111827; + line-height: 1.45; +`; + +export const ExternalLink = styled.a` + color: #1d4ed8; +`; + +export const CalendarBoard = styled.div` + display: flex; + flex-direction: column; + gap: 10px; +`; + +export const TogglePanel = styled.div` + border: 1px solid #e5e7eb; + border-radius: 10px; + padding: 10px; + background: #f9fafb; +`; + +export const ToggleHeader = styled.div` + display: flex; + justify-content: space-between; + align-items: center; + gap: 8px; + margin-bottom: 8px; +`; + +export const ToggleTitle = styled.h6` + margin: 0; + font-size: 0.9rem; + font-weight: 700; + color: #111827; +`; + +export const ToggleActions = styled.div` + display: flex; + gap: 8px; +`; + +export const ToggleActionButton = styled.button` + border: none; + background: transparent; + color: #1d4ed8; + font-size: 0.82rem; + font-weight: 700; + cursor: pointer; + padding: 0; +`; + +export const ToggleList = styled.div` + display: flex; + flex-direction: column; + gap: 6px; + max-height: 180px; + overflow-y: auto; +`; + +export const ToggleItem = styled.label` + display: flex; + align-items: center; + gap: 8px; + font-size: 0.84rem; + color: #374151; +`; + +export const ToggleCheckbox = styled.input` + width: 14px; + height: 14px; +`; + +export const ToggleText = styled.span` + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +`; + +export const CalendarHeader = styled.div` + display: flex; + align-items: center; + justify-content: space-between; + gap: 8px; +`; + +export const CalendarMonth = styled.h5` + margin: 0; + font-size: 1rem; + font-weight: 700; + color: #111827; +`; + +export const CalendarWeekRow = styled.div` + display: grid; + grid-template-columns: repeat(7, minmax(0, 1fr)); + gap: 8px; +`; + +export const CalendarWeekCell = styled.div` + text-align: center; + font-size: 0.82rem; + font-weight: 700; + color: #4b5563; +`; + +export const CalendarGrid = styled.div` + display: grid; + grid-template-columns: repeat(7, minmax(0, 1fr)); + gap: 8px; +`; + +export const CalendarCell = styled.div<{ $muted: boolean }>` + min-height: 120px; + border: 1px solid #e5e7eb; + border-radius: 10px; + padding: 8px; + background: ${({ $muted }) => ($muted ? '#f9fafb' : '#ffffff')}; + opacity: ${({ $muted }) => ($muted ? 0.55 : 1)}; + display: flex; + flex-direction: column; + gap: 8px; + + @media (max-width: 768px) { + min-height: 96px; + padding: 6px; + } +`; + +export const CalendarDayNumber = styled.span` + font-size: 0.82rem; + font-weight: 600; + color: #374151; +`; + +export const CalendarEventList = styled.div` + display: flex; + flex-direction: column; + gap: 4px; +`; + +export const CalendarEvent = styled.div` + font-size: 0.8rem; + line-height: 1.35; + color: #111827; + padding: 4px 6px; + border-radius: 6px; + background: #eff6ff; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +`; + +export const CalendarTitle = styled.span` + font-size: 0.82rem; + color: #111827; +`; From 02135e47ae79ba68ba052d058efbad2a0989b0f8 Mon Sep 17 00:00:00 2001 From: seongwon seo Date: Mon, 23 Mar 2026 00:35:35 +0900 Subject: [PATCH 059/172] =?UTF-8?q?feat:=20=EC=BA=98=EB=A6=B0=EB=8D=94=20?= =?UTF-8?q?=EC=97=B0=EB=8F=99=20=ED=83=AD=20UI=20=EA=B5=AC=EC=84=B1=20?= =?UTF-8?q?=EA=B0=9C=EC=84=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../tabs/CalendarSyncTab/CalendarSyncTab.tsx | 281 ++++++++++++++++++ 1 file changed, 281 insertions(+) create mode 100644 frontend/src/pages/AdminPage/tabs/CalendarSyncTab/CalendarSyncTab.tsx diff --git a/frontend/src/pages/AdminPage/tabs/CalendarSyncTab/CalendarSyncTab.tsx b/frontend/src/pages/AdminPage/tabs/CalendarSyncTab/CalendarSyncTab.tsx new file mode 100644 index 000000000..320dfab37 --- /dev/null +++ b/frontend/src/pages/AdminPage/tabs/CalendarSyncTab/CalendarSyncTab.tsx @@ -0,0 +1,281 @@ +import Button from '@/components/common/Button/Button'; +import { + buildDateKeyFromDate, + formatDateText, + maskToken, + WEEKDAY_LABELS, +} from '@/utils/calendarSyncUtils'; +import * as Styled from './CalendarSyncTab.styles'; +import { useCalendarSync } from './hooks/useCalendarSync'; + +const CalendarSyncTab = () => { + const { + googleToken, + googleCalendars, + googleEvents, + notionItems, + notionTotalResults, + notionDatabaseSourceId, + notionDatabaseOptions, + selectedNotionDatabaseId, + setSelectedNotionDatabaseId, + isNotionDatabaseApplying, + statusMessage, + errorMessage, + isGoogleLoading, + isNotionLoading, + notionWorkspaceName, + canStartGoogleOAuth, + notionCalendarEvents, + notionVisibleCalendarEvents, + notionEventsByDate, + notionEventEnabledMap, + notionCalendarDays, + notionCalendarLabel, + visibleMonth, + startGoogleOAuth, + startNotionOAuth, + goToPreviousMonth, + goToNextMonth, + toggleNotionEvent, + setAllNotionEventsEnabled, + applySelectedNotionDatabase, + } = useCalendarSync(); + + return ( + + + + Google 캘린더 + + + + {googleToken && ( + {maskToken(googleToken)} + )} + + + + Notion 캘린더 + + 연결할 데이터베이스를 선택하고 적용하세요. + + + setSelectedNotionDatabaseId(e.target.value)} + > + {notionDatabaseOptions.length === 0 ? ( + + ) : ( + notionDatabaseOptions.map((database) => ( + + )) + )} + + + + + + + {notionWorkspaceName && ( + + 연결된 워크스페이스: {notionWorkspaceName} + + )} + + + + {statusMessage && {statusMessage}} + {errorMessage && {errorMessage}} + + + + Google 캘린더 목록 + {googleCalendars.length === 0 ? ( + + 아직 데이터가 없습니다. Google 캘린더 가져오기를 먼저 + 완료해주세요. + + ) : ( + + {googleCalendars.map((calendar) => ( + + {calendar.summary || '(제목 없음)'} + {calendar.primary ? ' (기본 캘린더)' : ''} + + ))} + + )} + + + + Google 캘린더 이벤트 + {googleEvents.length === 0 ? ( + + 이벤트가 없거나 아직 조회되지 않았습니다. + + ) : ( + + {googleEvents.map((event) => ( + + {event.summary || '(제목 없음)'} | 시작:{' '} + {formatDateText(event.start?.dateTime ?? event.start?.date)} + {event.htmlLink && ( + <> + {' '} + |{' '} + + 열기 + + + )} + + ))} + + )} + + + + Notion 캘린더 일정 + + 전체 {notionTotalResults}개 / 캘린더 표시{' '} + {notionVisibleCalendarEvents.length}개 + + {notionDatabaseSourceId && ( + + 데이터베이스: {notionDatabaseSourceId} + + )} + {notionItems.length === 0 ? ( + + 아직 데이터가 없습니다. Notion 캘린더 가져오기를 먼저 + 완료해주세요. + + ) : notionCalendarEvents.length === 0 ? ( + + 날짜 속성이 있는 Notion 페이지가 없습니다. (예: 날짜/Date 타입 + 속성) + + ) : ( + + + + 표시할 페이지 선택 + + setAllNotionEventsEnabled(true)} + > + 전체 ON + + setAllNotionEventsEnabled(false)} + > + 전체 OFF + + + + + {notionCalendarEvents.map((event) => ( + + toggleNotionEvent(event.id)} + /> + + {event.title} ({event.dateKey}) + + + ))} + + + + + + {notionCalendarLabel} + + + + + {WEEKDAY_LABELS.map((label) => ( + + {label} + + ))} + + + {notionCalendarDays.map((day) => { + const dateKey = buildDateKeyFromDate(day); + const events = notionEventsByDate[dateKey] ?? []; + const isOutsideMonth = + day.getMonth() !== visibleMonth.getMonth() || + day.getFullYear() !== visibleMonth.getFullYear(); + + return ( + + + {day.getDate()} + + + {events.map((event) => ( + + {event.url ? ( + + {event.title} + + ) : ( + + {event.title} + + )} + + ))} + + + ); + })} + + + )} + + + + ); +}; + +export default CalendarSyncTab; From f2fffe654dffc8d5db7a21b1b047bda330de9805 Mon Sep 17 00:00:00 2001 From: seongwon seo Date: Mon, 23 Mar 2026 00:35:35 +0900 Subject: [PATCH 060/172] =?UTF-8?q?refactor:=20=EA=B5=AC=EA=B8=80=20?= =?UTF-8?q?=EC=BA=98=EB=A6=B0=EB=8D=94=20=EB=A1=9C=EC=A7=81=20=ED=9B=85=20?= =?UTF-8?q?=EB=B6=84=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../hooks/useGoogleCalendarData.ts | 119 ++++++++++++++++++ 1 file changed, 119 insertions(+) create mode 100644 frontend/src/pages/AdminPage/tabs/CalendarSyncTab/hooks/useGoogleCalendarData.ts diff --git a/frontend/src/pages/AdminPage/tabs/CalendarSyncTab/hooks/useGoogleCalendarData.ts b/frontend/src/pages/AdminPage/tabs/CalendarSyncTab/hooks/useGoogleCalendarData.ts new file mode 100644 index 000000000..c4fa92f5a --- /dev/null +++ b/frontend/src/pages/AdminPage/tabs/CalendarSyncTab/hooks/useGoogleCalendarData.ts @@ -0,0 +1,119 @@ +import { useEffect, useMemo, useState } from 'react'; +import { + fetchGoogleCalendarList, + fetchGooglePrimaryEvents, + GoogleCalendarItem, + GoogleEventItem, +} from '@/apis/calendarOAuth'; +import { + buildDefaultRedirectUri, + createState, +} from '@/utils/calendarSyncUtils'; + +const GOOGLE_STATE_KEY = 'admin_calendar_sync_google_state'; +const GOOGLE_TOKEN_KEY = 'admin_calendar_sync_google_token'; + +interface UseGoogleCalendarDataParams { + onError: (message: string) => void; + onStatus: (message: string) => void; + clearError: () => void; +} + +export const useGoogleCalendarData = ({ + onError, + onStatus, + clearError, +}: UseGoogleCalendarDataParams) => { + const googleClientId = import.meta.env.VITE_GOOGLE_CLIENT_ID?.trim() ?? ''; + const redirectUri = buildDefaultRedirectUri(); + + const [googleToken, setGoogleToken] = useState( + () => sessionStorage.getItem(GOOGLE_TOKEN_KEY) ?? '', + ); + const [googleCalendars, setGoogleCalendars] = useState( + [], + ); + const [googleEvents, setGoogleEvents] = useState([]); + const [isGoogleLoading, setIsGoogleLoading] = useState(false); + + const canStartGoogleOAuth = useMemo( + () => googleClientId.length > 0, + [googleClientId], + ); + + const startGoogleOAuth = () => { + if (!canStartGoogleOAuth) { + onError('VITE_GOOGLE_CLIENT_ID 설정이 필요합니다.'); + return; + } + + const state = createState(); + sessionStorage.setItem(GOOGLE_STATE_KEY, state); + + const params = new URLSearchParams({ + client_id: googleClientId, + redirect_uri: redirectUri, + response_type: 'token', + scope: 'https://www.googleapis.com/auth/calendar.readonly', + include_granted_scopes: 'true', + prompt: 'consent', + state, + }); + + window.location.href = `https://accounts.google.com/o/oauth2/v2/auth?${params.toString()}`; + }; + + useEffect(() => { + const hash = window.location.hash.startsWith('#') + ? window.location.hash.slice(1) + : ''; + + if (!hash) return; + + const params = new URLSearchParams(hash); + const token = params.get('access_token'); + const state = params.get('state'); + const expectedState = sessionStorage.getItem(GOOGLE_STATE_KEY); + + if (token && state && expectedState && state === expectedState) { + setGoogleToken(token); + sessionStorage.setItem(GOOGLE_TOKEN_KEY, token); + onStatus('Google OAuth 인증이 완료되었습니다.'); + clearError(); + } + + const cleanUrl = `${window.location.pathname}${window.location.search}`; + window.history.replaceState({}, document.title, cleanUrl); + }, [clearError, onStatus]); + + useEffect(() => { + if (!googleToken) return; + + setIsGoogleLoading(true); + clearError(); + + Promise.all([ + fetchGoogleCalendarList(googleToken), + fetchGooglePrimaryEvents(googleToken), + ]) + .then(([calendars, events]) => { + setGoogleCalendars(calendars); + setGoogleEvents(events); + }) + .catch((error: Error) => { + onError(error.message); + }) + .finally(() => { + setIsGoogleLoading(false); + }); + }, [clearError, googleToken, onError]); + + return { + googleToken, + googleCalendars, + googleEvents, + isGoogleLoading, + canStartGoogleOAuth, + startGoogleOAuth, + }; +}; From ad35d747abe748731e9593aae54785c78dc18882 Mon Sep 17 00:00:00 2001 From: seongwon seo Date: Mon, 23 Mar 2026 00:35:35 +0900 Subject: [PATCH 061/172] =?UTF-8?q?refactor:=20=EB=85=B8=EC=85=98=20?= =?UTF-8?q?=EB=8D=B0=EC=9D=B4=ED=84=B0=20=EB=A1=9C=EC=A7=81=20=ED=9B=85=20?= =?UTF-8?q?=EB=B6=84=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../hooks/useNotionCalendarData.ts | 127 ++++++++++++++++++ 1 file changed, 127 insertions(+) create mode 100644 frontend/src/pages/AdminPage/tabs/CalendarSyncTab/hooks/useNotionCalendarData.ts diff --git a/frontend/src/pages/AdminPage/tabs/CalendarSyncTab/hooks/useNotionCalendarData.ts b/frontend/src/pages/AdminPage/tabs/CalendarSyncTab/hooks/useNotionCalendarData.ts new file mode 100644 index 000000000..9857688f9 --- /dev/null +++ b/frontend/src/pages/AdminPage/tabs/CalendarSyncTab/hooks/useNotionCalendarData.ts @@ -0,0 +1,127 @@ +import { useCallback, useEffect, useState } from 'react'; +import { + fetchNotionDatabasePages, + fetchNotionDatabases, + fetchNotionPages, + NotionDatabaseOption, + NotionPagesResponse, + NotionSearchItem, +} from '@/apis/calendarOAuth'; + +interface UseNotionCalendarDataParams { + onError: (message: string) => void; + onStatus: (message: string) => void; + clearError: () => void; +} + +export const useNotionCalendarData = ({ + onError, + onStatus, + clearError, +}: UseNotionCalendarDataParams) => { + const [notionItems, setNotionItems] = useState([]); + const [notionTotalResults, setNotionTotalResults] = useState(0); + const [notionDatabaseSourceId, setNotionDatabaseSourceId] = useState(''); + const [notionDatabaseOptions, setNotionDatabaseOptions] = useState< + NotionDatabaseOption[] + >([]); + const [selectedNotionDatabaseId, setSelectedNotionDatabaseId] = useState(''); + const [isNotionLoading, setIsNotionLoading] = useState(false); + const [isNotionDatabaseApplying, setIsNotionDatabaseApplying] = + useState(false); + + const applyPagesResponse = useCallback((response: NotionPagesResponse) => { + setNotionItems(response.items); + setNotionTotalResults(response.totalResults); + setNotionDatabaseSourceId(response.databaseId ?? ''); + }, []); + + const loadNotionPages = useCallback(async () => { + setIsNotionLoading(true); + try { + const response = await fetchNotionPages(); + applyPagesResponse(response); + return response; + } catch (error: unknown) { + const status = + typeof error === 'object' && + error !== null && + 'status' in error && + typeof (error as { status?: unknown }).status === 'number' + ? (error as { status: number }).status + : undefined; + + if (status === 401 || status === 403) { + return null; + } + + if (error instanceof Error) { + onError(error.message); + } + return null; + } finally { + setIsNotionLoading(false); + } + }, [applyPagesResponse, onError]); + + const applySelectedNotionDatabase = useCallback(() => { + if (!selectedNotionDatabaseId) { + onError('먼저 Notion 데이터베이스를 선택해주세요.'); + return; + } + + setIsNotionDatabaseApplying(true); + clearError(); + + fetchNotionDatabasePages({ + databaseId: selectedNotionDatabaseId, + }) + .then((pagesResponse) => { + applyPagesResponse(pagesResponse); + onStatus('선택한 Notion 데이터베이스를 연결했습니다.'); + }) + .catch((error: Error) => { + onError(error.message); + }) + .finally(() => { + setIsNotionDatabaseApplying(false); + }); + }, [ + applyPagesResponse, + clearError, + onError, + onStatus, + selectedNotionDatabaseId, + ]); + + useEffect(() => { + fetchNotionDatabases() + .then((options) => { + setNotionDatabaseOptions(options); + setSelectedNotionDatabaseId( + (previous) => previous || options[0]?.id || '', + ); + }) + .catch(() => { + // OAuth 전 단계에서는 목록 실패가 자연스러울 수 있다. + }); + }, []); + + useEffect(() => { + loadNotionPages(); + }, [loadNotionPages]); + + return { + notionItems, + notionTotalResults, + notionDatabaseSourceId, + notionDatabaseOptions, + selectedNotionDatabaseId, + setSelectedNotionDatabaseId, + isNotionLoading, + isNotionDatabaseApplying, + applyPagesResponse, + loadNotionPages, + applySelectedNotionDatabase, + }; +}; From ea354035b7f38ef6bb789197f13bac53c966a3d6 Mon Sep 17 00:00:00 2001 From: seongwon seo Date: Mon, 23 Mar 2026 00:35:35 +0900 Subject: [PATCH 062/172] =?UTF-8?q?refactor:=20=EB=85=B8=EC=85=98=20?= =?UTF-8?q?=EC=BA=98=EB=A6=B0=EB=8D=94=20UI=20=EC=83=81=ED=83=9C=20?= =?UTF-8?q?=ED=9B=85=20=EB=B6=84=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../hooks/useNotionCalendarUiState.ts | 125 ++++++++++++++++++ 1 file changed, 125 insertions(+) create mode 100644 frontend/src/pages/AdminPage/tabs/CalendarSyncTab/hooks/useNotionCalendarUiState.ts diff --git a/frontend/src/pages/AdminPage/tabs/CalendarSyncTab/hooks/useNotionCalendarUiState.ts b/frontend/src/pages/AdminPage/tabs/CalendarSyncTab/hooks/useNotionCalendarUiState.ts new file mode 100644 index 000000000..354ed056f --- /dev/null +++ b/frontend/src/pages/AdminPage/tabs/CalendarSyncTab/hooks/useNotionCalendarUiState.ts @@ -0,0 +1,125 @@ +import { useEffect, useMemo, useState } from 'react'; +import { NotionSearchItem } from '@/apis/calendarOAuth'; +import { + buildMonthCalendarDays, + dateFromKey, + formatMonthLabel, + NotionCalendarEvent, + parseNotionCalendarEvent, +} from '@/utils/calendarSyncUtils'; + +interface UseNotionCalendarUiStateParams { + notionItems: NotionSearchItem[]; +} + +export const useNotionCalendarUiState = ({ + notionItems, +}: UseNotionCalendarUiStateParams) => { + const [visibleMonth, setVisibleMonth] = useState(() => { + const now = new Date(); + return new Date(now.getFullYear(), now.getMonth(), 1); + }); + const [notionEventEnabledMap, setNotionEventEnabledMap] = useState< + Record + >({}); + + const notionCalendarEvents = useMemo( + () => + notionItems + .map(parseNotionCalendarEvent) + .filter((event): event is NotionCalendarEvent => event !== null) + .sort((a, b) => a.dateKey.localeCompare(b.dateKey)), + [notionItems], + ); + const notionVisibleCalendarEvents = useMemo( + () => + notionCalendarEvents.filter( + (event) => notionEventEnabledMap[event.id] !== false, + ), + [notionCalendarEvents, notionEventEnabledMap], + ); + const notionEventsByDate = useMemo( + () => + notionVisibleCalendarEvents.reduce>( + (accumulator, event) => { + if (!accumulator[event.dateKey]) { + accumulator[event.dateKey] = []; + } + accumulator[event.dateKey].push(event); + return accumulator; + }, + {}, + ), + [notionVisibleCalendarEvents], + ); + const notionCalendarDays = useMemo( + () => buildMonthCalendarDays(visibleMonth), + [visibleMonth], + ); + const notionCalendarLabel = useMemo( + () => formatMonthLabel(visibleMonth), + [visibleMonth], + ); + + useEffect(() => { + if (notionCalendarEvents.length === 0) return; + + const firstEventDate = dateFromKey(notionCalendarEvents[0].dateKey); + setVisibleMonth( + new Date(firstEventDate.getFullYear(), firstEventDate.getMonth(), 1), + ); + }, [notionCalendarEvents]); + + useEffect(() => { + setNotionEventEnabledMap((previous) => { + const next: Record = {}; + notionCalendarEvents.forEach((event) => { + next[event.id] = previous[event.id] ?? true; + }); + return next; + }); + }, [notionCalendarEvents]); + + const goToPreviousMonth = () => { + setVisibleMonth( + new Date(visibleMonth.getFullYear(), visibleMonth.getMonth() - 1, 1), + ); + }; + + const goToNextMonth = () => { + setVisibleMonth( + new Date(visibleMonth.getFullYear(), visibleMonth.getMonth() + 1, 1), + ); + }; + + const toggleNotionEvent = (id: string) => { + setNotionEventEnabledMap((previous) => ({ + ...previous, + [id]: !(previous[id] ?? true), + })); + }; + + const setAllNotionEventsEnabled = (enabled: boolean) => { + setNotionEventEnabledMap((previous) => { + const next = { ...previous }; + notionCalendarEvents.forEach((event) => { + next[event.id] = enabled; + }); + return next; + }); + }; + + return { + visibleMonth, + notionCalendarEvents, + notionVisibleCalendarEvents, + notionEventsByDate, + notionEventEnabledMap, + notionCalendarDays, + notionCalendarLabel, + goToPreviousMonth, + goToNextMonth, + toggleNotionEvent, + setAllNotionEventsEnabled, + }; +}; From 7dec85a564e8c985c19c741395e6a8dc2f851c48 Mon Sep 17 00:00:00 2001 From: seongwon seo Date: Mon, 23 Mar 2026 00:35:35 +0900 Subject: [PATCH 063/172] =?UTF-8?q?refactor:=20=EB=85=B8=EC=85=98=20OAuth?= =?UTF-8?q?=20=EB=A1=9C=EC=A7=81=20=ED=9B=85=20=EB=B6=84=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../CalendarSyncTab/hooks/useNotionOAuth.ts | 86 +++++++++++++++++++ 1 file changed, 86 insertions(+) create mode 100644 frontend/src/pages/AdminPage/tabs/CalendarSyncTab/hooks/useNotionOAuth.ts diff --git a/frontend/src/pages/AdminPage/tabs/CalendarSyncTab/hooks/useNotionOAuth.ts b/frontend/src/pages/AdminPage/tabs/CalendarSyncTab/hooks/useNotionOAuth.ts new file mode 100644 index 000000000..fdf48ba02 --- /dev/null +++ b/frontend/src/pages/AdminPage/tabs/CalendarSyncTab/hooks/useNotionOAuth.ts @@ -0,0 +1,86 @@ +import { useEffect, useState } from 'react'; +import { + exchangeNotionCode, + fetchNotionAuthorizeUrl, +} from '@/apis/calendarOAuth'; +import { createState } from '@/utils/calendarSyncUtils'; + +const NOTION_STATE_KEY = 'admin_calendar_sync_notion_state'; + +interface UseNotionOAuthParams { + loadNotionPages: () => Promise; + onWorkspaceName: (name: string) => void; + onError: (message: string) => void; + onStatus: (message: string) => void; + clearError: () => void; +} + +export const useNotionOAuth = ({ + loadNotionPages, + onWorkspaceName, + onError, + onStatus, + clearError, +}: UseNotionOAuthParams) => { + const [isNotionOAuthLoading, setIsNotionOAuthLoading] = useState(false); + + const startNotionOAuth = () => { + const state = createState(); + sessionStorage.setItem(NOTION_STATE_KEY, state); + setIsNotionOAuthLoading(true); + clearError(); + + fetchNotionAuthorizeUrl(state) + .then((authorizeUrl) => { + window.location.href = authorizeUrl; + }) + .catch((error: Error) => { + onError(error.message); + }) + .finally(() => { + setIsNotionOAuthLoading(false); + }); + }; + + useEffect(() => { + const params = new URLSearchParams(window.location.search); + const code = params.get('code'); + const state = params.get('state'); + const error = params.get('error'); + const expectedState = sessionStorage.getItem(NOTION_STATE_KEY); + + if (error) { + onError(`Notion OAuth 실패: ${error}`); + return; + } + + if (!code || !state || !expectedState || state !== expectedState) { + return; + } + + setIsNotionOAuthLoading(true); + clearError(); + + exchangeNotionCode({ code }) + .then((tokenResponse) => { + onWorkspaceName(tokenResponse?.workspaceName ?? ''); + return loadNotionPages(); + }) + .then(() => { + onStatus('Notion OAuth 인증이 완료되었습니다.'); + }) + .catch((oauthError: Error) => { + onError(oauthError.message); + }) + .finally(() => { + setIsNotionOAuthLoading(false); + const cleanUrl = window.location.pathname; + window.history.replaceState({}, document.title, cleanUrl); + }); + }, [clearError, loadNotionPages, onError, onStatus, onWorkspaceName]); + + return { + isNotionOAuthLoading, + startNotionOAuth, + }; +}; From 989aab3612c4cbc4c2964611f51e6cc4b0ad42d5 Mon Sep 17 00:00:00 2001 From: seongwon seo Date: Mon, 23 Mar 2026 00:35:35 +0900 Subject: [PATCH 064/172] =?UTF-8?q?refactor:=20=EC=BA=98=EB=A6=B0=EB=8D=94?= =?UTF-8?q?=20=EC=97=B0=EB=8F=99=20=EC=A1=B0=ED=95=A9=20=ED=9B=85=20?= =?UTF-8?q?=EA=B5=AC=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../CalendarSyncTab/hooks/useCalendarSync.ts | 73 +++++++++++++++++++ 1 file changed, 73 insertions(+) create mode 100644 frontend/src/pages/AdminPage/tabs/CalendarSyncTab/hooks/useCalendarSync.ts diff --git a/frontend/src/pages/AdminPage/tabs/CalendarSyncTab/hooks/useCalendarSync.ts b/frontend/src/pages/AdminPage/tabs/CalendarSyncTab/hooks/useCalendarSync.ts new file mode 100644 index 000000000..6fabef4d6 --- /dev/null +++ b/frontend/src/pages/AdminPage/tabs/CalendarSyncTab/hooks/useCalendarSync.ts @@ -0,0 +1,73 @@ +import { useState } from 'react'; +import { useGoogleCalendarData } from './useGoogleCalendarData'; +import { useNotionCalendarData } from './useNotionCalendarData'; +import { useNotionCalendarUiState } from './useNotionCalendarUiState'; +import { useNotionOAuth } from './useNotionOAuth'; + +export const useCalendarSync = () => { + const [statusMessage, setStatusMessage] = useState(''); + const [errorMessage, setErrorMessage] = useState(''); + const [notionWorkspaceName, setNotionWorkspaceName] = useState(''); + + const clearError = () => setErrorMessage(''); + + const google = useGoogleCalendarData({ + onError: setErrorMessage, + onStatus: setStatusMessage, + clearError, + }); + + const notionData = useNotionCalendarData({ + onError: setErrorMessage, + onStatus: setStatusMessage, + clearError, + }); + + const notionUi = useNotionCalendarUiState({ + notionItems: notionData.notionItems, + }); + + const notionOAuth = useNotionOAuth({ + loadNotionPages: notionData.loadNotionPages, + onWorkspaceName: setNotionWorkspaceName, + onError: setErrorMessage, + onStatus: setStatusMessage, + clearError, + }); + + return { + googleToken: google.googleToken, + googleCalendars: google.googleCalendars, + googleEvents: google.googleEvents, + notionItems: notionData.notionItems, + notionTotalResults: notionData.notionTotalResults, + notionDatabaseSourceId: notionData.notionDatabaseSourceId, + notionDatabaseOptions: notionData.notionDatabaseOptions, + selectedNotionDatabaseId: notionData.selectedNotionDatabaseId, + setSelectedNotionDatabaseId: notionData.setSelectedNotionDatabaseId, + isNotionDatabaseApplying: notionData.isNotionDatabaseApplying, + statusMessage, + errorMessage, + isGoogleLoading: google.isGoogleLoading, + isNotionLoading: + notionData.isNotionLoading || + notionOAuth.isNotionOAuthLoading || + notionData.isNotionDatabaseApplying, + notionWorkspaceName, + canStartGoogleOAuth: google.canStartGoogleOAuth, + notionCalendarEvents: notionUi.notionCalendarEvents, + notionVisibleCalendarEvents: notionUi.notionVisibleCalendarEvents, + notionEventsByDate: notionUi.notionEventsByDate, + notionEventEnabledMap: notionUi.notionEventEnabledMap, + notionCalendarDays: notionUi.notionCalendarDays, + notionCalendarLabel: notionUi.notionCalendarLabel, + visibleMonth: notionUi.visibleMonth, + startGoogleOAuth: google.startGoogleOAuth, + startNotionOAuth: notionOAuth.startNotionOAuth, + goToPreviousMonth: notionUi.goToPreviousMonth, + goToNextMonth: notionUi.goToNextMonth, + toggleNotionEvent: notionUi.toggleNotionEvent, + setAllNotionEventsEnabled: notionUi.setAllNotionEventsEnabled, + applySelectedNotionDatabase: notionData.applySelectedNotionDatabase, + }; +}; From 3c96345da8ea63047601c6ed5889eb130315e5f8 Mon Sep 17 00:00:00 2001 From: seongwon seo Date: Mon, 23 Mar 2026 00:36:01 +0900 Subject: [PATCH 065/172] =?UTF-8?q?feat:=20=EC=BA=98=EB=A6=B0=EB=8D=94=20?= =?UTF-8?q?=EB=9D=BC=EC=9A=B0=ED=8A=B8=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/src/pages/AdminPage/AdminRoutes.tsx | 2 ++ frontend/src/pages/AdminPage/components/SideBar/SideBar.tsx | 1 + 2 files changed, 3 insertions(+) diff --git a/frontend/src/pages/AdminPage/AdminRoutes.tsx b/frontend/src/pages/AdminPage/AdminRoutes.tsx index 8c5c7e687..a4bdb3d1c 100644 --- a/frontend/src/pages/AdminPage/AdminRoutes.tsx +++ b/frontend/src/pages/AdminPage/AdminRoutes.tsx @@ -5,6 +5,7 @@ import ApplicantDetailPage from '@/pages/AdminPage/tabs/ApplicantsTab/ApplicantD import ApplicantsListTab from '@/pages/AdminPage/tabs/ApplicantsTab/ApplicantsListTab/ApplicantsListTab'; import ApplicationEditTab from '@/pages/AdminPage/tabs/ApplicationEditTab/ApplicationEditTab'; import ApplicationListTab from '@/pages/AdminPage/tabs/ApplicationListTab/ApplicationListTab'; +import CalendarSyncTab from '@/pages/AdminPage/tabs/CalendarSyncTab/CalendarSyncTab'; import ClubInfoEditTab from '@/pages/AdminPage/tabs/ClubInfoEditTab/ClubInfoEditTab'; import PhotoEditTab from '@/pages/AdminPage/tabs/PhotoEditTab/PhotoEditTab'; import RecruitEditTab from '@/pages/AdminPage/tabs/RecruitEditTab/RecruitEditTab'; @@ -18,6 +19,7 @@ export default function AdminRoutes() { } /> } /> } /> + } /> } /> } /> } /> diff --git a/frontend/src/pages/AdminPage/components/SideBar/SideBar.tsx b/frontend/src/pages/AdminPage/components/SideBar/SideBar.tsx index c6304f5fe..03add299c 100644 --- a/frontend/src/pages/AdminPage/components/SideBar/SideBar.tsx +++ b/frontend/src/pages/AdminPage/components/SideBar/SideBar.tsx @@ -23,6 +23,7 @@ const tabs: TabCategory[] = [ { label: '기본 정보 수정', path: '/admin/club-info' }, { label: '소개 정보 수정', path: '/admin/club-intro' }, { label: '활동 사진 수정', path: '/admin/photo-edit' }, + { label: '동아리 일정 관리', path: '/admin/calendar-sync' }, ], }, { From 4881b43d6ea672b89d029f5fbfb75cda5b79c47a Mon Sep 17 00:00:00 2001 From: suhyun113 <163711629+suhyun113@users.noreply.github.com> Date: Mon, 23 Mar 2026 03:41:12 +0900 Subject: [PATCH 066/172] =?UTF-8?q?style:=20=ED=99=8D=EB=B3=B4=20=EC=B9=B4?= =?UTF-8?q?=EB=93=9C=20=EC=97=AC=EB=B0=B1=20=EB=B0=8F=20=EC=9D=B4=EB=AF=B8?= =?UTF-8?q?=EC=A7=80=20=ED=81=AC=EA=B8=B0=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../PromotionCard/CardMeta/CardMeta.styles.ts | 24 ++++-------- .../list/PromotionCard/CardMeta/CardMeta.tsx | 37 ++++++++----------- .../PromotionCard/PromotionCard.styles.ts | 8 +++- 3 files changed, 29 insertions(+), 40 deletions(-) diff --git a/frontend/src/pages/PromotionPage/components/list/PromotionCard/CardMeta/CardMeta.styles.ts b/frontend/src/pages/PromotionPage/components/list/PromotionCard/CardMeta/CardMeta.styles.ts index 6b775dd88..c45d224c1 100644 --- a/frontend/src/pages/PromotionPage/components/list/PromotionCard/CardMeta/CardMeta.styles.ts +++ b/frontend/src/pages/PromotionPage/components/list/PromotionCard/CardMeta/CardMeta.styles.ts @@ -5,11 +5,6 @@ import { colors } from '@/styles/theme/colors'; export const Container = styled.div` display: flex; flex-direction: column; - gap: 6px; -`; - -export const TitleSection = styled.div` - gap: 6px; `; export const Title = styled.h3` @@ -19,25 +14,22 @@ export const Title = styled.h3` overflow: hidden; text-overflow: ellipsis; white-space: nowrap; - margin-bottom: 6px; + margin-bottom: 4px; ${media.mini_mobile} { font-size: 14px; + margin-bottom: 2px; } `; -export const Description = styled.span` - display: block; - min-width: 0; - font-size: 14px; - font-weight: 400; - color: ${colors.gray[600]}; - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; +export const MetaContainer = styled.div` + display: flex; + flex-direction: column; + gap: 2px; ${media.mini_mobile} { - font-size: 12px; + font-size: 14px; + margin-bottom: 1px; } `; diff --git a/frontend/src/pages/PromotionPage/components/list/PromotionCard/CardMeta/CardMeta.tsx b/frontend/src/pages/PromotionPage/components/list/PromotionCard/CardMeta/CardMeta.tsx index 2dc35e65c..b72b2b77d 100644 --- a/frontend/src/pages/PromotionPage/components/list/PromotionCard/CardMeta/CardMeta.tsx +++ b/frontend/src/pages/PromotionPage/components/list/PromotionCard/CardMeta/CardMeta.tsx @@ -4,17 +4,11 @@ import * as Styled from './CardMeta.styles'; interface CardMetaProps { title: string; - description: string; location: string | null; startDate: string; } -const CardMeta = ({ - title, - description, - location, - startDate, -}: CardMetaProps) => { +const CardMeta = ({ title, location, startDate }: CardMetaProps) => { const startDateObj = new Date(startDate); const formattedStartDate = startDateObj.toLocaleDateString('ko-KR', { month: 'long', @@ -24,26 +18,25 @@ const CardMeta = ({ return ( - - {title} - {description} - + {title} + + + {location && ( + + +
Location + + {location} + + )} - {location && ( - Location + Time - {location} + {formattedStartDate} - )} - - - - Time - - {formattedStartDate} - + ); }; diff --git a/frontend/src/pages/PromotionPage/components/list/PromotionCard/PromotionCard.styles.ts b/frontend/src/pages/PromotionPage/components/list/PromotionCard/PromotionCard.styles.ts index 69e5e1682..3f85ba07b 100644 --- a/frontend/src/pages/PromotionPage/components/list/PromotionCard/PromotionCard.styles.ts +++ b/frontend/src/pages/PromotionPage/components/list/PromotionCard/PromotionCard.styles.ts @@ -14,7 +14,7 @@ export const Container = styled.div` export const ImageWrapper = styled.div` position: relative; width: 100%; - aspect-ratio: 7/6; + aspect-ratio: 1 / 1; `; export const Image = styled.div<{ $imageUrl?: string }>` @@ -36,7 +36,11 @@ export const DdayWrapper = styled.div` `; export const Content = styled.div` - padding: 10px; + padding: 14px; + + ${media.mini_mobile} { + padding: 10px; + } `; export const TagWrapper = styled.div` From bb1915e0c4c61ae93eab7b2d9a1d889c7c5a4e72 Mon Sep 17 00:00:00 2001 From: suhyun113 <163711629+suhyun113@users.noreply.github.com> Date: Mon, 23 Mar 2026 18:13:49 +0900 Subject: [PATCH 067/172] =?UTF-8?q?refactor:=20Filter=20=EC=BB=B4=ED=8F=AC?= =?UTF-8?q?=EB=84=8C=ED=8A=B8=20=ED=8C=8C=EC=9D=BC=20=EC=9C=84=EC=B9=98=20?= =?UTF-8?q?=EC=9D=B4=EB=8F=99?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../components => components/common}/Filter/Filter.stories.tsx | 0 .../components => components/common}/Filter/Filter.styles.ts | 0 .../MainPage/components => components/common}/Filter/Filter.tsx | 0 frontend/src/pages/MainPage/MainPage.tsx | 2 +- frontend/src/pages/PromotionPage/PromotionListPage.tsx | 2 +- 5 files changed, 2 insertions(+), 2 deletions(-) rename frontend/src/{pages/MainPage/components => components/common}/Filter/Filter.stories.tsx (100%) rename frontend/src/{pages/MainPage/components => components/common}/Filter/Filter.styles.ts (100%) rename frontend/src/{pages/MainPage/components => components/common}/Filter/Filter.tsx (100%) diff --git a/frontend/src/pages/MainPage/components/Filter/Filter.stories.tsx b/frontend/src/components/common/Filter/Filter.stories.tsx similarity index 100% rename from frontend/src/pages/MainPage/components/Filter/Filter.stories.tsx rename to frontend/src/components/common/Filter/Filter.stories.tsx diff --git a/frontend/src/pages/MainPage/components/Filter/Filter.styles.ts b/frontend/src/components/common/Filter/Filter.styles.ts similarity index 100% rename from frontend/src/pages/MainPage/components/Filter/Filter.styles.ts rename to frontend/src/components/common/Filter/Filter.styles.ts diff --git a/frontend/src/pages/MainPage/components/Filter/Filter.tsx b/frontend/src/components/common/Filter/Filter.tsx similarity index 100% rename from frontend/src/pages/MainPage/components/Filter/Filter.tsx rename to frontend/src/components/common/Filter/Filter.tsx diff --git a/frontend/src/pages/MainPage/MainPage.tsx b/frontend/src/pages/MainPage/MainPage.tsx index e88dbc642..2ec471033 100644 --- a/frontend/src/pages/MainPage/MainPage.tsx +++ b/frontend/src/pages/MainPage/MainPage.tsx @@ -8,7 +8,7 @@ import { useGetCardList } from '@/hooks/Queries/useClub'; import Banner from '@/pages/MainPage/components/Banner/Banner'; import CategoryButtonList from '@/pages/MainPage/components/CategoryButtonList/CategoryButtonList'; import ClubCard from '@/pages/MainPage/components/ClubCard/ClubCard'; -import Filter from '@/pages/MainPage/components/Filter/Filter'; +import Filter from '@/components/common/Filter/Filter'; import Popup from '@/pages/MainPage/components/Popup/Popup'; import { useSelectedCategory } from '@/store/useCategoryStore'; import { useSearchIsSearching, useSearchKeyword } from '@/store/useSearchStore'; diff --git a/frontend/src/pages/PromotionPage/PromotionListPage.tsx b/frontend/src/pages/PromotionPage/PromotionListPage.tsx index cf8168e41..b15486085 100644 --- a/frontend/src/pages/PromotionPage/PromotionListPage.tsx +++ b/frontend/src/pages/PromotionPage/PromotionListPage.tsx @@ -4,7 +4,7 @@ import { PAGE_VIEW } from '@/constants/eventName'; import useTrackPageView from '@/hooks/Mixpanel/useTrackPageView'; import { useGetPromotionArticles } from '@/hooks/Queries/usePromotion'; import isInAppWebView from '@/utils/isInAppWebView'; -import Filter from '../MainPage/components/Filter/Filter'; +import Filter from '../../components/common/Filter/Filter'; import PromottionGrid from './components/list/PromotionGrid/PromotionGrid'; import * as Styled from './PromotionListPage.styles'; From e4f183a362e9bc4d3a78b40678523ad4de11903e Mon Sep 17 00:00:00 2001 From: suhyun113 <163711629+suhyun113@users.noreply.github.com> Date: Mon, 23 Mar 2026 19:11:43 +0900 Subject: [PATCH 068/172] =?UTF-8?q?refactor:=20Filter=20=EC=BB=B4=ED=8F=AC?= =?UTF-8?q?=EB=84=8C=ED=8A=B8=20=EB=A1=9C=EC=A7=81=20=EB=B6=84=EB=A6=AC=20?= =?UTF-8?q?=EB=B0=8F=20usePromotionNotification=20=ED=9B=85=20=EB=8F=84?= =?UTF-8?q?=EC=9E=85?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/components/common/Filter/Filter.tsx | 30 +-------------- .../hooks/Queries/usePromotionNotification.ts | 37 +++++++++++++++++++ frontend/src/pages/MainPage/MainPage.tsx | 4 +- .../pages/PromotionPage/PromotionListPage.tsx | 4 +- 4 files changed, 45 insertions(+), 30 deletions(-) create mode 100644 frontend/src/hooks/Queries/usePromotionNotification.ts diff --git a/frontend/src/components/common/Filter/Filter.tsx b/frontend/src/components/common/Filter/Filter.tsx index a04fe0b96..75ccbb752 100644 --- a/frontend/src/components/common/Filter/Filter.tsx +++ b/frontend/src/components/common/Filter/Filter.tsx @@ -4,11 +4,6 @@ import { USER_EVENT } from '@/constants/eventName'; import useMixpanelTrack from '@/hooks/Mixpanel/useMixpanelTrack'; import { useGetPromotionArticles } from '@/hooks/Queries/usePromotion'; import useDevice from '@/hooks/useDevice'; -import { - getLastCheckedTime, - getLatestPromotionTime, - setLastCheckedTime, -} from '@/utils/promotionNotification'; import * as Styled from './Filter.styles'; const FILTER_OPTIONS = [ @@ -18,37 +13,16 @@ const FILTER_OPTIONS = [ interface FilterProps { alwaysVisible?: boolean; + hasNotification: boolean; } -const Filter = ({ alwaysVisible = false }: FilterProps) => { +const Filter = ({ alwaysVisible = false, hasNotification }: FilterProps) => { const { isMobile } = useDevice(); const navigate = useNavigate(); const { pathname } = useLocation(); const trackEvent = useMixpanelTrack(); const shouldShow = alwaysVisible || isMobile; - const { data } = useGetPromotionArticles(); - const [hasNotification, setHasNotification] = useState(false); - - useEffect(() => { - if (!data || data.length === 0) return; - - const latestTime = getLatestPromotionTime(data); - const lastChecked = getLastCheckedTime(); - - if (pathname === '/promotions') { - setLastCheckedTime(latestTime); - setHasNotification(false); - return; - } - - if (lastChecked === null || latestTime > lastChecked) { - setHasNotification(true); - } else { - setHasNotification(false); - } - }, [data, pathname]); - const handleFilterOptionClick = (path: string) => { trackEvent(USER_EVENT.FILTER_OPTION_CLICKED, { path: path, diff --git a/frontend/src/hooks/Queries/usePromotionNotification.ts b/frontend/src/hooks/Queries/usePromotionNotification.ts new file mode 100644 index 000000000..8890d34d0 --- /dev/null +++ b/frontend/src/hooks/Queries/usePromotionNotification.ts @@ -0,0 +1,37 @@ +import { useEffect, useState } from 'react'; +import { useLocation } from 'react-router-dom'; +import { useGetPromotionArticles } from '@/hooks/Queries/usePromotion'; +import { + getLastCheckedTime, + getLatestPromotionTime, + setLastCheckedTime, +} from '@/utils/promotionNotification'; + +const usePromotionNotification = () => { + const { data } = useGetPromotionArticles(); + const { pathname } = useLocation(); + const [hasNotification, setHasNotification] = useState(false); + + useEffect(() => { + if (!data || data.length === 0) return; + + const latestTime = getLatestPromotionTime(data); + const lastChecked = getLastCheckedTime(); + + if (pathname === '/promotions') { + setLastCheckedTime(latestTime); + setHasNotification(false); + return; + } + + if (lastChecked === null || latestTime > lastChecked) { + setHasNotification(true); + } else { + setHasNotification(false); + } + }, [data, pathname]); + + return hasNotification; +}; + +export default usePromotionNotification; \ No newline at end of file diff --git a/frontend/src/pages/MainPage/MainPage.tsx b/frontend/src/pages/MainPage/MainPage.tsx index 2ec471033..94d42a187 100644 --- a/frontend/src/pages/MainPage/MainPage.tsx +++ b/frontend/src/pages/MainPage/MainPage.tsx @@ -14,6 +14,7 @@ import { useSelectedCategory } from '@/store/useCategoryStore'; import { useSearchIsSearching, useSearchKeyword } from '@/store/useSearchStore'; import { Club } from '@/types/club'; import * as Styled from './MainPage.styles'; +import usePromotionNotification from '@/hooks/Queries/usePromotionNotification'; const MainPage = () => { useTrackPageView(PAGE_VIEW.MAIN_PAGE); @@ -34,6 +35,7 @@ const MainPage = () => { category: searchCategory, division, }); + const hasNotification = usePromotionNotification(); const clubs = data?.clubs || []; const totalCount = data?.totalCount ?? clubs.length; @@ -50,7 +52,7 @@ const MainPage = () => { <>
- + diff --git a/frontend/src/pages/PromotionPage/PromotionListPage.tsx b/frontend/src/pages/PromotionPage/PromotionListPage.tsx index b15486085..9192b40ea 100644 --- a/frontend/src/pages/PromotionPage/PromotionListPage.tsx +++ b/frontend/src/pages/PromotionPage/PromotionListPage.tsx @@ -3,6 +3,7 @@ import Header from '@/components/common/Header/Header'; import { PAGE_VIEW } from '@/constants/eventName'; import useTrackPageView from '@/hooks/Mixpanel/useTrackPageView'; import { useGetPromotionArticles } from '@/hooks/Queries/usePromotion'; +import usePromotionNotification from '@/hooks/Queries/usePromotionNotification'; import isInAppWebView from '@/utils/isInAppWebView'; import Filter from '../../components/common/Filter/Filter'; import PromottionGrid from './components/list/PromotionGrid/PromotionGrid'; @@ -12,12 +13,13 @@ const PromotionListPage = () => { useTrackPageView(PAGE_VIEW.PROMOTION_LIST_PAGE); const { data, isLoading, isError } = useGetPromotionArticles(); + const hasNotification = usePromotionNotification(); return ( <>
- {!isInAppWebView() && } + {!isInAppWebView() && } {isLoading &&

로딩 중...

} {isError &&

오류가 발생했습니다.

} From 0e7b45b85019786f24dc904726d6cecb4763bc15 Mon Sep 17 00:00:00 2001 From: suhyun113 <163711629+suhyun113@users.noreply.github.com> Date: Mon, 23 Mar 2026 19:25:48 +0900 Subject: [PATCH 069/172] =?UTF-8?q?fix:=20Story=20=ED=8C=8C=EC=9D=BC=20?= =?UTF-8?q?=EC=98=A4=EB=A5=98=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/src/components/common/Filter/Filter.stories.tsx | 7 +++++-- frontend/src/components/common/Filter/Filter.tsx | 2 -- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/frontend/src/components/common/Filter/Filter.stories.tsx b/frontend/src/components/common/Filter/Filter.stories.tsx index e924d8bc6..582041b69 100644 --- a/frontend/src/components/common/Filter/Filter.stories.tsx +++ b/frontend/src/components/common/Filter/Filter.stories.tsx @@ -10,8 +10,11 @@ const setViewportWidth = (width: number) => { }; const meta = { - title: 'Pages/MainPage/Components/Filter', + title: 'Components/Common/Filter', component: Filter, + args: { + hasNotification: false, + }, parameters: { layout: 'fullscreen', docs: { @@ -44,7 +47,7 @@ export const PromotionTab: Story = { (Story) => { setViewportWidth(375); return ( - + ); diff --git a/frontend/src/components/common/Filter/Filter.tsx b/frontend/src/components/common/Filter/Filter.tsx index 75ccbb752..1ce54b02f 100644 --- a/frontend/src/components/common/Filter/Filter.tsx +++ b/frontend/src/components/common/Filter/Filter.tsx @@ -1,8 +1,6 @@ -import { useEffect, useState } from 'react'; import { useLocation, useNavigate } from 'react-router-dom'; import { USER_EVENT } from '@/constants/eventName'; import useMixpanelTrack from '@/hooks/Mixpanel/useMixpanelTrack'; -import { useGetPromotionArticles } from '@/hooks/Queries/usePromotion'; import useDevice from '@/hooks/useDevice'; import * as Styled from './Filter.styles'; From 81c972c03471a600c00785ddecc220be2e420556 Mon Sep 17 00:00:00 2001 From: suhyun113 <163711629+suhyun113@users.noreply.github.com> Date: Mon, 23 Mar 2026 19:26:40 +0900 Subject: [PATCH 070/172] =?UTF-8?q?refactor:=20Prettier=20=EC=BD=94?= =?UTF-8?q?=EB=93=9C=20=ED=8F=AC=EB=A7=B7=20=EC=A0=95=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/src/hooks/Queries/usePromotionNotification.ts | 2 +- frontend/src/pages/MainPage/MainPage.tsx | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/frontend/src/hooks/Queries/usePromotionNotification.ts b/frontend/src/hooks/Queries/usePromotionNotification.ts index 8890d34d0..dab18c5bf 100644 --- a/frontend/src/hooks/Queries/usePromotionNotification.ts +++ b/frontend/src/hooks/Queries/usePromotionNotification.ts @@ -34,4 +34,4 @@ const usePromotionNotification = () => { return hasNotification; }; -export default usePromotionNotification; \ No newline at end of file +export default usePromotionNotification; diff --git a/frontend/src/pages/MainPage/MainPage.tsx b/frontend/src/pages/MainPage/MainPage.tsx index 94d42a187..ba46e95dc 100644 --- a/frontend/src/pages/MainPage/MainPage.tsx +++ b/frontend/src/pages/MainPage/MainPage.tsx @@ -1,20 +1,20 @@ import { useMemo, useState } from 'react'; +import Filter from '@/components/common/Filter/Filter'; import Footer from '@/components/common/Footer/Footer'; import Header from '@/components/common/Header/Header'; import Spinner from '@/components/common/Spinner/Spinner'; import { PAGE_VIEW } from '@/constants/eventName'; import useTrackPageView from '@/hooks/Mixpanel/useTrackPageView'; import { useGetCardList } from '@/hooks/Queries/useClub'; +import usePromotionNotification from '@/hooks/Queries/usePromotionNotification'; import Banner from '@/pages/MainPage/components/Banner/Banner'; import CategoryButtonList from '@/pages/MainPage/components/CategoryButtonList/CategoryButtonList'; import ClubCard from '@/pages/MainPage/components/ClubCard/ClubCard'; -import Filter from '@/components/common/Filter/Filter'; import Popup from '@/pages/MainPage/components/Popup/Popup'; import { useSelectedCategory } from '@/store/useCategoryStore'; import { useSearchIsSearching, useSearchKeyword } from '@/store/useSearchStore'; import { Club } from '@/types/club'; import * as Styled from './MainPage.styles'; -import usePromotionNotification from '@/hooks/Queries/usePromotionNotification'; const MainPage = () => { useTrackPageView(PAGE_VIEW.MAIN_PAGE); From 1572f9cf73040416eea06ebb3dd489e545e5f381 Mon Sep 17 00:00:00 2001 From: suhyun113 <163711629+suhyun113@users.noreply.github.com> Date: Mon, 23 Mar 2026 20:56:06 +0900 Subject: [PATCH 071/172] =?UTF-8?q?fix:=20build=20=EC=97=90=EB=9F=AC=20?= =?UTF-8?q?=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/src/mocks/data/festivalMock.ts | 1 + frontend/src/mocks/handlers/promotion.ts | 4 ++-- .../pages/FestivalPage/IntroductionPage/IntroductionPage.tsx | 1 + .../components/list/PromotionCard/PromotionCard.tsx | 1 - 4 files changed, 4 insertions(+), 3 deletions(-) diff --git a/frontend/src/mocks/data/festivalMock.ts b/frontend/src/mocks/data/festivalMock.ts index 3432df15d..46f9a9ea7 100644 --- a/frontend/src/mocks/data/festivalMock.ts +++ b/frontend/src/mocks/data/festivalMock.ts @@ -2,6 +2,7 @@ import { PromotionArticle } from '@/types/promotion'; export const festivalMock: PromotionArticle[] = [ { + id: '600000000000000000000003', clubId: 'festival-1', clubName: '총동연', title: '🎉 동아리 소개 한마당', diff --git a/frontend/src/mocks/handlers/promotion.ts b/frontend/src/mocks/handlers/promotion.ts index 432014646..38664df01 100644 --- a/frontend/src/mocks/handlers/promotion.ts +++ b/frontend/src/mocks/handlers/promotion.ts @@ -7,7 +7,7 @@ const API_BASE_URL = // Mock 데이터 export const mockPromotionArticles: PromotionArticle[] = [ { - id: '1', + id: '600000000000000000000001', clubName: 'WAP', clubId: '67e54ae51cfd27718dd40bec', title: '💌✨WAP 최종 전시회 초대장 ✨💌', @@ -23,7 +23,7 @@ export const mockPromotionArticles: PromotionArticle[] = [ ], }, { - id: '2', + id: '600000000000000000000002', clubName: 'WAP', clubId: '67e54ae51cfd27718dd40bec', title: '💌✨WAP 최종 전시회 초대장 ✨💌', diff --git a/frontend/src/pages/FestivalPage/IntroductionPage/IntroductionPage.tsx b/frontend/src/pages/FestivalPage/IntroductionPage/IntroductionPage.tsx index 6e6d407d1..ddd3147c1 100644 --- a/frontend/src/pages/FestivalPage/IntroductionPage/IntroductionPage.tsx +++ b/frontend/src/pages/FestivalPage/IntroductionPage/IntroductionPage.tsx @@ -8,6 +8,7 @@ import useTrackPageView from '@/hooks/Mixpanel/useTrackPageView'; import BoothMapSection from '@/pages/FestivalPage/components/BoothMapSection/BoothMapSection'; import PerformanceList from '@/pages/FestivalPage/components/PerformanceList/PerformanceList'; import * as Styled from './IntroductionPage.styles'; +import isInAppWebView from '@/utils/isInAppWebView'; const FESTIVAL_TAB_TYPE = { BOOTH_MAP: 'booth-map', diff --git a/frontend/src/pages/PromotionPage/components/list/PromotionCard/PromotionCard.tsx b/frontend/src/pages/PromotionPage/components/list/PromotionCard/PromotionCard.tsx index b1910d2d1..09a4566e6 100644 --- a/frontend/src/pages/PromotionPage/components/list/PromotionCard/PromotionCard.tsx +++ b/frontend/src/pages/PromotionPage/components/list/PromotionCard/PromotionCard.tsx @@ -49,7 +49,6 @@ const PromotionCard = ({ article }: PromotionCardProps) => { From 52112ab0ef319f7254fea514fd6653b77b671c2d Mon Sep 17 00:00:00 2001 From: suhyun113 <163711629+suhyun113@users.noreply.github.com> Date: Mon, 23 Mar 2026 20:57:48 +0900 Subject: [PATCH 072/172] =?UTF-8?q?refactor:=20Prettier=20=EC=BD=94?= =?UTF-8?q?=EB=93=9C=20=ED=8F=AC=EB=A7=B7=20=EC=A0=95=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../pages/FestivalPage/IntroductionPage/IntroductionPage.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/src/pages/FestivalPage/IntroductionPage/IntroductionPage.tsx b/frontend/src/pages/FestivalPage/IntroductionPage/IntroductionPage.tsx index ddd3147c1..4921970bc 100644 --- a/frontend/src/pages/FestivalPage/IntroductionPage/IntroductionPage.tsx +++ b/frontend/src/pages/FestivalPage/IntroductionPage/IntroductionPage.tsx @@ -7,8 +7,8 @@ import useMixpanelTrack from '@/hooks/Mixpanel/useMixpanelTrack'; import useTrackPageView from '@/hooks/Mixpanel/useTrackPageView'; import BoothMapSection from '@/pages/FestivalPage/components/BoothMapSection/BoothMapSection'; import PerformanceList from '@/pages/FestivalPage/components/PerformanceList/PerformanceList'; -import * as Styled from './IntroductionPage.styles'; import isInAppWebView from '@/utils/isInAppWebView'; +import * as Styled from './IntroductionPage.styles'; const FESTIVAL_TAB_TYPE = { BOOTH_MAP: 'booth-map', From bc81f2a9ae0ced1d1a167b990391240118e2d1eb Mon Sep 17 00:00:00 2001 From: suhyun113 <163711629+suhyun113@users.noreply.github.com> Date: Tue, 24 Mar 2026 17:35:10 +0900 Subject: [PATCH 073/172] =?UTF-8?q?fix:=20build=20=EC=97=90=EB=9F=AC=20?= =?UTF-8?q?=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../RelatedPromotionCard/RelatedPromotionCard.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/frontend/src/pages/PromotionPage/components/detail/RelatedPromotionSection/RelatedPromotionCard/RelatedPromotionCard.tsx b/frontend/src/pages/PromotionPage/components/detail/RelatedPromotionSection/RelatedPromotionCard/RelatedPromotionCard.tsx index 8b25ea804..ccad0f3c5 100644 --- a/frontend/src/pages/PromotionPage/components/detail/RelatedPromotionSection/RelatedPromotionCard/RelatedPromotionCard.tsx +++ b/frontend/src/pages/PromotionPage/components/detail/RelatedPromotionSection/RelatedPromotionCard/RelatedPromotionCard.tsx @@ -17,7 +17,6 @@ const RelatedPromotionCard = ({ article, onClick }: Props) => { From bd10eb59f717f626bbdd4883f143a756acc7681f Mon Sep 17 00:00:00 2001 From: seongwon seo Date: Tue, 24 Mar 2026 17:53:29 +0900 Subject: [PATCH 074/172] =?UTF-8?q?fix:=20=EC=BA=98=EB=A6=B0=EB=8D=94=20?= =?UTF-8?q?=EB=82=A0=EC=A7=9C=20=ED=8C=8C=EC=8B=B1=EA=B3=BC=20Notion=20OAu?= =?UTF-8?q?th=20=EC=BD=9C=EB=B0=B1=20URL=20=EC=A0=95=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../CalendarSyncTab/hooks/useNotionOAuth.ts | 13 ++++++++-- frontend/src/utils/calendarSyncUtils.test.ts | 5 +++- frontend/src/utils/calendarSyncUtils.ts | 25 +++++++++++-------- 3 files changed, 30 insertions(+), 13 deletions(-) diff --git a/frontend/src/pages/AdminPage/tabs/CalendarSyncTab/hooks/useNotionOAuth.ts b/frontend/src/pages/AdminPage/tabs/CalendarSyncTab/hooks/useNotionOAuth.ts index fdf48ba02..4c65577c9 100644 --- a/frontend/src/pages/AdminPage/tabs/CalendarSyncTab/hooks/useNotionOAuth.ts +++ b/frontend/src/pages/AdminPage/tabs/CalendarSyncTab/hooks/useNotionOAuth.ts @@ -43,18 +43,28 @@ export const useNotionOAuth = ({ }; useEffect(() => { + const clearOAuthParamsFromUrl = () => { + const cleanUrl = window.location.pathname; + window.history.replaceState({}, document.title, cleanUrl); + }; + const params = new URLSearchParams(window.location.search); const code = params.get('code'); const state = params.get('state'); const error = params.get('error'); + const hasOAuthParams = Boolean(code || state || error); const expectedState = sessionStorage.getItem(NOTION_STATE_KEY); if (error) { onError(`Notion OAuth 실패: ${error}`); + clearOAuthParamsFromUrl(); return; } if (!code || !state || !expectedState || state !== expectedState) { + if (hasOAuthParams) { + clearOAuthParamsFromUrl(); + } return; } @@ -74,8 +84,7 @@ export const useNotionOAuth = ({ }) .finally(() => { setIsNotionOAuthLoading(false); - const cleanUrl = window.location.pathname; - window.history.replaceState({}, document.title, cleanUrl); + clearOAuthParamsFromUrl(); }); }, [clearError, loadNotionPages, onError, onStatus, onWorkspaceName]); diff --git a/frontend/src/utils/calendarSyncUtils.test.ts b/frontend/src/utils/calendarSyncUtils.test.ts index 99d155426..4621ddd8a 100644 --- a/frontend/src/utils/calendarSyncUtils.test.ts +++ b/frontend/src/utils/calendarSyncUtils.test.ts @@ -50,7 +50,10 @@ describe('calendarSyncUtils', () => { }); it('parseDateKey는 ISO datetime을 YYYY-MM-DD로 정규화한다', () => { - expect(parseDateKey('2026-03-22T06:25:00.000Z')).toBeTruthy(); + expect(parseDateKey('2026-03-22T06:25:00.000Z')).toBe('2026-03-22'); + expect(parseDateKey('Sun, 22 Mar 2026 06:25:00 GMT')).toBe( + '2026-03-22', + ); expect(parseDateKey('invalid-date')).toBeNull(); }); diff --git a/frontend/src/utils/calendarSyncUtils.ts b/frontend/src/utils/calendarSyncUtils.ts index 301847535..4193a3904 100644 --- a/frontend/src/utils/calendarSyncUtils.ts +++ b/frontend/src/utils/calendarSyncUtils.ts @@ -54,17 +54,19 @@ export const formatDateText = (dateText?: string) => { * 유효하지 않은 값이면 null을 반환한다. */ export const parseDateKey = (dateText: string) => { - if (/^\d{4}-\d{2}-\d{2}$/.test(dateText)) { - return dateText; + const datePart = dateText.match(/^(\d{4}-\d{2}-\d{2})/); + if (datePart) { + return datePart[1]; } const parsed = new Date(dateText); if (Number.isNaN(parsed.getTime())) return null; - const localYear = parsed.getFullYear(); - const localMonth = String(parsed.getMonth() + 1).padStart(2, '0'); - const localDay = String(parsed.getDate()).padStart(2, '0'); - return `${localYear}-${localMonth}-${localDay}`; + // 문자열에 날짜 파트가 없을 때도 시간대 영향 없이 같은 UTC 날짜 키를 유지한다. + const utcYear = parsed.getUTCFullYear(); + const utcMonth = String(parsed.getUTCMonth() + 1).padStart(2, '0'); + const utcDay = String(parsed.getUTCDate()).padStart(2, '0'); + return `${utcYear}-${utcMonth}-${utcDay}`; }; /** Date 객체를 `YYYY-MM-DD` 키 문자열로 변환한다. */ @@ -92,10 +94,13 @@ export const buildMonthCalendarDays = (month: Date) => { gridEnd.setDate(monthEnd.getDate() + (6 - monthEnd.getDay())); const days: Date[] = []; - const cursor = new Date(gridStart); - while (cursor <= gridEnd) { - days.push(new Date(cursor)); - cursor.setDate(cursor.getDate() + 1); + const oneDayMs = 24 * 60 * 60 * 1000; + for ( + let timestamp = gridStart.getTime(); + timestamp <= gridEnd.getTime(); + timestamp += oneDayMs + ) { + days.push(new Date(timestamp)); } return days; }; From 1309e42fb36cd943b6c1819de9e1ac9aaddb530c Mon Sep 17 00:00:00 2001 From: suhyun113 <163711629+suhyun113@users.noreply.github.com> Date: Wed, 25 Mar 2026 06:21:54 +0900 Subject: [PATCH 075/172] =?UTF-8?q?feat:=20=EB=82=A0=EC=A7=9C=20=ED=8F=AC?= =?UTF-8?q?=EB=A7=B7=20=EC=9C=A0=ED=8B=B8(KST=20=EB=B3=80=ED=99=98)=20?= =?UTF-8?q?=EB=B6=84=EB=A6=AC=20=EB=B0=8F=20=ED=85=8C=EC=8A=A4=ED=8A=B8=20?= =?UTF-8?q?=EC=BD=94=EB=93=9C=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../PromotionInfoSection.tsx | 16 +---- .../list/PromotionCard/CardMeta/CardMeta.tsx | 8 +-- frontend/src/utils/formatKSTDateTime.test.ts | 70 +++++++++++++++++++ frontend/src/utils/formatKSTDateTime.ts | 29 ++++++++ 4 files changed, 104 insertions(+), 19 deletions(-) create mode 100644 frontend/src/utils/formatKSTDateTime.test.ts create mode 100644 frontend/src/utils/formatKSTDateTime.ts diff --git a/frontend/src/pages/PromotionPage/components/detail/PromotionInfoSection/PromotionInfoSection.tsx b/frontend/src/pages/PromotionPage/components/detail/PromotionInfoSection/PromotionInfoSection.tsx index 8e5c4ab45..afe7b6747 100644 --- a/frontend/src/pages/PromotionPage/components/detail/PromotionInfoSection/PromotionInfoSection.tsx +++ b/frontend/src/pages/PromotionPage/components/detail/PromotionInfoSection/PromotionInfoSection.tsx @@ -1,21 +1,11 @@ import { PromotionArticle } from '@/types/promotion'; import * as Styled from './PromotionInfoSection.styles'; +import { formatKSTDateTimeFull } from '@/utils/formatKSTDateTime'; interface Props { article: PromotionArticle; } -const formatDate = (dateStr: string) => { - const date = new Date(dateStr); - return date.toLocaleString('ko-KR', { - month: 'long', - day: 'numeric', - weekday: 'short', - hour: '2-digit', - minute: '2-digit', - }); -}; - const PromotionInfoSection = ({ article }: Props) => { return ( @@ -25,8 +15,8 @@ const PromotionInfoSection = ({ article }: Props) => { 📅 일시 - {formatDate(article.eventStartDate)} -{' '} - {formatDate(article.eventEndDate)} + {formatKSTDateTimeFull(article.eventStartDate)} -{' '} + {formatKSTDateTimeFull(article.eventEndDate)} diff --git a/frontend/src/pages/PromotionPage/components/list/PromotionCard/CardMeta/CardMeta.tsx b/frontend/src/pages/PromotionPage/components/list/PromotionCard/CardMeta/CardMeta.tsx index b72b2b77d..e17f384e0 100644 --- a/frontend/src/pages/PromotionPage/components/list/PromotionCard/CardMeta/CardMeta.tsx +++ b/frontend/src/pages/PromotionPage/components/list/PromotionCard/CardMeta/CardMeta.tsx @@ -1,6 +1,7 @@ import LocationIcon from '@/assets/images/icons/location_icon.svg'; import TimeIcon from '@/assets/images/icons/time_icon.svg'; import * as Styled from './CardMeta.styles'; +import { formatKSTDate } from '@/utils/formatKSTDateTime'; interface CardMetaProps { title: string; @@ -9,12 +10,7 @@ interface CardMetaProps { } const CardMeta = ({ title, location, startDate }: CardMetaProps) => { - const startDateObj = new Date(startDate); - const formattedStartDate = startDateObj.toLocaleDateString('ko-KR', { - month: 'long', - day: 'numeric', - weekday: 'long', - }); + const formattedStartDate = formatKSTDate(startDate); return ( diff --git a/frontend/src/utils/formatKSTDateTime.test.ts b/frontend/src/utils/formatKSTDateTime.test.ts new file mode 100644 index 000000000..77be3b0d9 --- /dev/null +++ b/frontend/src/utils/formatKSTDateTime.test.ts @@ -0,0 +1,70 @@ +import { + formatKSTDate, + formatKSTDateTime, + formatKSTDateTimeFull, +} from './formatKSTDateTime'; + +describe('formatKSTDateTime', () => { + it('UTC 시간을 KST로 변환한다', () => { + const utc = '2026-03-25T00:00:00Z'; + + const result = formatKSTDateTime(utc, { + hour: '2-digit', + minute: '2-digit', + hour12: false, + }); + + // 00:00 UTC → 09:00 KST + expect(result).toMatch(/09|9/); + }); + + it('날짜 포맷 옵션이 정상 적용된다', () => { + const utc = '2026-03-25T00:00:00Z'; + + const result = formatKSTDateTime(utc, { + month: 'long', + day: 'numeric', + }); + + expect(result).toContain('3월'); + expect(result).toContain('25'); + }); + + it('빈 값이면 빈 문자열 반환', () => { + expect(formatKSTDateTime('', {})).toBe(''); + }); +}); + +describe('formatKSTDate', () => { + it('KST 기준 날짜만 올바르게 반환한다', () => { + const utc = '2026-03-25T16:00:00Z'; + // → KST: 3월 26일 + + const result = formatKSTDate(utc); + + expect(result).toContain('3월'); + expect(result).toContain('26'); // 날짜 변환 확인 + }); +}); + +describe('formatKSTDateTimeFull', () => { + it('날짜 + 시간 포맷이 정상 적용된다', () => { + const utc = '2026-03-25T00:00:00Z'; + // → KST: 09:00 + + const result = formatKSTDateTimeFull(utc); + + expect(result).toContain('3월'); + expect(result).toContain('25'); + expect(result).toMatch(/오전|오후/); // 한국 시간 포맷 + }); + + it('날짜 경계가 넘어가는 경우도 올바르게 처리한다', () => { + const utc = '2026-03-25T16:00:00Z'; + // → KST: 3월 26일 01:00 + + const result = formatKSTDateTimeFull(utc); + + expect(result).toContain('26'); // 날짜 넘어갔는지 확인 + }); +}); diff --git a/frontend/src/utils/formatKSTDateTime.ts b/frontend/src/utils/formatKSTDateTime.ts new file mode 100644 index 000000000..0574c22fc --- /dev/null +++ b/frontend/src/utils/formatKSTDateTime.ts @@ -0,0 +1,29 @@ +export const formatKSTDateTime = ( + dateStr: string, + options: Intl.DateTimeFormatOptions = {} +) => { + if (!dateStr) return ''; + const date = new Date(dateStr); + + const formatter = new Intl.DateTimeFormat('ko-KR', { + timeZone: 'Asia/Seoul', + ...options, + }); + return formatter.format(date); +}; + +export const formatKSTDate = (dateStr: string) => + formatKSTDateTime(dateStr, { + month: 'long', + day: 'numeric', + weekday: 'long', + }); + +export const formatKSTDateTimeFull = (dateStr: string) => + formatKSTDateTime(dateStr, { + month: 'long', + day: 'numeric', + weekday: 'short', + hour: '2-digit', + minute: '2-digit', + }); \ No newline at end of file From 18a4d544105711c20a122abca644c13f2adcd5d5 Mon Sep 17 00:00:00 2001 From: suhyun113 <163711629+suhyun113@users.noreply.github.com> Date: Wed, 25 Mar 2026 06:26:29 +0900 Subject: [PATCH 076/172] =?UTF-8?q?fix:=20=ED=99=8D=EB=B3=B4=20=EC=83=81?= =?UTF-8?q?=EC=84=B8=20=EC=A1=B0=ED=9A=8C=20id=20=EC=B0=B8=EC=A1=B0=20?= =?UTF-8?q?=EC=98=A4=EB=A5=98=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - clubId 기준으로 조회하던 로직을 id 기준으로 변경 - 잘못된 데이터 매핑으로 인해 일정이 다르게 표시되던 문제 해결 --- frontend/src/pages/PromotionPage/PromotionDetailPage.tsx | 2 +- .../components/list/PromotionCard/PromotionCard.tsx | 2 +- .../components/list/PromotionGrid/PromotionGrid.tsx | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/frontend/src/pages/PromotionPage/PromotionDetailPage.tsx b/frontend/src/pages/PromotionPage/PromotionDetailPage.tsx index 4ae127c4f..b709657cf 100644 --- a/frontend/src/pages/PromotionPage/PromotionDetailPage.tsx +++ b/frontend/src/pages/PromotionPage/PromotionDetailPage.tsx @@ -18,7 +18,7 @@ const PromotionDetailPage = () => { const { promotionId } = useParams<{ promotionId: string }>(); const { data, isLoading, isError } = useGetPromotionArticles(); - const article = data?.find((item) => item.clubId === promotionId) ?? null; + const article = data?.find((item) => item.id === promotionId) ?? null; const showRelatedPromotion = false; // 관련 이벤트 추천 기능은 현재 비활성화 상태 return ( diff --git a/frontend/src/pages/PromotionPage/components/list/PromotionCard/PromotionCard.tsx b/frontend/src/pages/PromotionPage/components/list/PromotionCard/PromotionCard.tsx index 09a4566e6..81d904e18 100644 --- a/frontend/src/pages/PromotionPage/components/list/PromotionCard/PromotionCard.tsx +++ b/frontend/src/pages/PromotionPage/components/list/PromotionCard/PromotionCard.tsx @@ -32,7 +32,7 @@ const PromotionCard = ({ article }: PromotionCardProps) => { clubId: article.clubId, }); - navigateToPromotionDetail(`/promotions/${article.clubId}`); + navigateToPromotionDetail(`/promotions/${article.id}`); }; const imageUrl = article.images?.[0]; diff --git a/frontend/src/pages/PromotionPage/components/list/PromotionGrid/PromotionGrid.tsx b/frontend/src/pages/PromotionPage/components/list/PromotionGrid/PromotionGrid.tsx index 1efd103dd..e21f638d6 100644 --- a/frontend/src/pages/PromotionPage/components/list/PromotionGrid/PromotionGrid.tsx +++ b/frontend/src/pages/PromotionPage/components/list/PromotionGrid/PromotionGrid.tsx @@ -10,7 +10,7 @@ const PromotionGrid = ({ articles }: PromotionGridProps) => { return ( {articles.map((article) => ( - + ))} ); From 972b2464f8c3d98ec03f7896765335a79af7030c Mon Sep 17 00:00:00 2001 From: suhyun113 <163711629+suhyun113@users.noreply.github.com> Date: Wed, 25 Mar 2026 07:24:46 +0900 Subject: [PATCH 077/172] =?UTF-8?q?fix:=20D-day=20=EA=B3=84=EC=82=B0=20?= =?UTF-8?q?=EB=A1=9C=EC=A7=81=20=EA=B8=B0=EA=B0=84=20=EA=B8=B0=EC=A4=80?= =?UTF-8?q?=EC=9C=BC=EB=A1=9C=20=EA=B0=9C=EC=84=A0=20=EB=B0=8F=20=ED=85=8C?= =?UTF-8?q?=EC=8A=A4=ED=8A=B8=20=EC=BD=94=EB=93=9C=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - D-day 계산을 시작일이 아닌 이벤트 기간 기준으로 수정 --- .../RelatedPromotionSection.tsx | 6 +- .../list/PromotionCard/PromotionCard.tsx | 2 +- frontend/src/utils/getDday.test.ts | 66 +++++++++++++++++++ frontend/src/utils/getDday.ts | 21 ++++-- 4 files changed, 85 insertions(+), 10 deletions(-) create mode 100644 frontend/src/utils/getDday.test.ts diff --git a/frontend/src/pages/PromotionPage/components/detail/RelatedPromotionSection/RelatedPromotionSection.tsx b/frontend/src/pages/PromotionPage/components/detail/RelatedPromotionSection/RelatedPromotionSection.tsx index 3bc6a8215..cdf54135d 100644 --- a/frontend/src/pages/PromotionPage/components/detail/RelatedPromotionSection/RelatedPromotionSection.tsx +++ b/frontend/src/pages/PromotionPage/components/detail/RelatedPromotionSection/RelatedPromotionSection.tsx @@ -13,9 +13,9 @@ const RelatedPromotionSection = ({ currentClubId, articles }: Props) => { const navigate = useNavigate(); const activeEvents = articles - .filter((a) => { - const dday = getDDay(a.eventStartDate); - return a.clubId !== currentClubId && dday >= 0; + .filter((article) => { + const dday = getDDay(article.eventStartDate, article.eventEndDate); + return article.clubId !== currentClubId && dday >= 0; }) .slice(0, 1); diff --git a/frontend/src/pages/PromotionPage/components/list/PromotionCard/PromotionCard.tsx b/frontend/src/pages/PromotionPage/components/list/PromotionCard/PromotionCard.tsx index 81d904e18..74d90bf61 100644 --- a/frontend/src/pages/PromotionPage/components/list/PromotionCard/PromotionCard.tsx +++ b/frontend/src/pages/PromotionPage/components/list/PromotionCard/PromotionCard.tsx @@ -15,7 +15,7 @@ interface PromotionCardProps { const PromotionCard = ({ article }: PromotionCardProps) => { const trackEvent = useMixpanelTrack(); const navigateToPromotionDetail = useNavigate(); - const dday = getDDay(article.eventStartDate); + const dday = getDDay(article.eventStartDate, article.eventEndDate); const handleCardClick = () => { if (article.isFestival) { diff --git a/frontend/src/utils/getDday.test.ts b/frontend/src/utils/getDday.test.ts new file mode 100644 index 000000000..4c64e4d0a --- /dev/null +++ b/frontend/src/utils/getDday.test.ts @@ -0,0 +1,66 @@ +import { getDDay } from './getDday'; + +describe('getDDay', () => { + beforeEach(() => { + jest.useFakeTimers(); + }); + + afterEach(() => { + jest.useRealTimers(); + }); + + it('행사 시작 전이면 D-n 반환', () => { + jest.setSystemTime(new Date('2026-03-20')); + + const result = getDDay( + '2026-03-25', + '2026-03-27' + ); + + expect(result).toBe(5); + }); + + it('행사 시작일이면 D-Day (0) 반환', () => { + jest.setSystemTime(new Date('2026-03-25')); + + const result = getDDay( + '2026-03-25', + '2026-03-27' + ); + + expect(result).toBe(0); + }); + + it('행사 중간 날짜도 D-Day (0) 반환', () => { + jest.setSystemTime(new Date('2026-03-26')); + + const result = getDDay( + '2026-03-25', + '2026-03-27' + ); + + expect(result).toBe(0); + }); + + it('행사 마지막 날도 D-Day (0) 반환', () => { + jest.setSystemTime(new Date('2026-03-27')); + + const result = getDDay( + '2026-03-25', + '2026-03-27' + ); + + expect(result).toBe(0); + }); + + it('행사 종료 후이면 -1 반환', () => { + jest.setSystemTime(new Date('2026-03-28')); + + const result = getDDay( + '2026-03-25', + '2026-03-27' + ); + + expect(result).toBe(-1); + }); +}); \ No newline at end of file diff --git a/frontend/src/utils/getDday.ts b/frontend/src/utils/getDday.ts index e8c3afcab..1fc3371cf 100644 --- a/frontend/src/utils/getDday.ts +++ b/frontend/src/utils/getDday.ts @@ -1,13 +1,22 @@ -export const getDDay = (startDate: string) => { +export const getDDay = (startDate: string, endDate: string) => { const today = new Date(); const start = new Date(startDate); + const end = new Date(endDate); today.setHours(0, 0, 0, 0); start.setHours(0, 0, 0, 0); + end.setHours(0, 0, 0, 0); - const diff = Math.ceil( - (start.getTime() - today.getTime()) / (1000 * 60 * 60 * 24), - ); + if (today >= start && today <= end) { + return 0; + } - return diff; -}; + if (today < start) { + const diff = Math.ceil( + (start.getTime() - today.getTime()) / (1000 * 60 * 60 * 24), + ); + return diff; + } + + return -1; +}; \ No newline at end of file From 255591d986932763ef75e7fa1957d49733635ba2 Mon Sep 17 00:00:00 2001 From: suhyun113 <163711629+suhyun113@users.noreply.github.com> Date: Wed, 25 Mar 2026 07:51:46 +0900 Subject: [PATCH 078/172] =?UTF-8?q?refactor:=20promotion=20=EA=B4=80?= =?UTF-8?q?=EB=A0=A8=20=EC=9C=A0=ED=8B=B8=20=ED=95=A8=EC=88=98=20=EB=8F=84?= =?UTF-8?q?=EB=A9=94=EC=9D=B8=20=ED=8F=B4=EB=8D=94=EB=A1=9C=20=EB=B6=84?= =?UTF-8?q?=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/src/hooks/Queries/usePromotionNotification.ts | 2 +- .../detail/RelatedPromotionSection/RelatedPromotionSection.tsx | 2 +- .../components/list/PromotionCard/PromotionCard.tsx | 2 +- frontend/src/{ => pages/PromotionPage}/utils/getDday.test.ts | 0 frontend/src/{ => pages/PromotionPage}/utils/getDday.ts | 0 .../PromotionPage}/utils/promotionNotification.test.ts | 2 +- .../{ => pages/PromotionPage}/utils/promotionNotification.ts | 0 7 files changed, 4 insertions(+), 4 deletions(-) rename frontend/src/{ => pages/PromotionPage}/utils/getDday.test.ts (100%) rename frontend/src/{ => pages/PromotionPage}/utils/getDday.ts (100%) rename frontend/src/{ => pages/PromotionPage}/utils/promotionNotification.test.ts (97%) rename frontend/src/{ => pages/PromotionPage}/utils/promotionNotification.ts (100%) diff --git a/frontend/src/hooks/Queries/usePromotionNotification.ts b/frontend/src/hooks/Queries/usePromotionNotification.ts index dab18c5bf..d672232fa 100644 --- a/frontend/src/hooks/Queries/usePromotionNotification.ts +++ b/frontend/src/hooks/Queries/usePromotionNotification.ts @@ -5,7 +5,7 @@ import { getLastCheckedTime, getLatestPromotionTime, setLastCheckedTime, -} from '@/utils/promotionNotification'; +} from '@/pages/PromotionPage/utils/promotionNotification'; const usePromotionNotification = () => { const { data } = useGetPromotionArticles(); diff --git a/frontend/src/pages/PromotionPage/components/detail/RelatedPromotionSection/RelatedPromotionSection.tsx b/frontend/src/pages/PromotionPage/components/detail/RelatedPromotionSection/RelatedPromotionSection.tsx index cdf54135d..13afa4739 100644 --- a/frontend/src/pages/PromotionPage/components/detail/RelatedPromotionSection/RelatedPromotionSection.tsx +++ b/frontend/src/pages/PromotionPage/components/detail/RelatedPromotionSection/RelatedPromotionSection.tsx @@ -1,6 +1,6 @@ import { useNavigate } from 'react-router-dom'; import { PromotionArticle } from '@/types/promotion'; -import { getDDay } from '@/utils/getDday'; +import { getDDay } from '@/pages/PromotionPage/utils/getDday'; import RelatedPromotionCard from './RelatedPromotionCard/RelatedPromotionCard'; import * as Styled from './RelatedPromotionSection.styles'; diff --git a/frontend/src/pages/PromotionPage/components/list/PromotionCard/PromotionCard.tsx b/frontend/src/pages/PromotionPage/components/list/PromotionCard/PromotionCard.tsx index 74d90bf61..abee6e88c 100644 --- a/frontend/src/pages/PromotionPage/components/list/PromotionCard/PromotionCard.tsx +++ b/frontend/src/pages/PromotionPage/components/list/PromotionCard/PromotionCard.tsx @@ -2,7 +2,7 @@ import { useNavigate } from 'react-router-dom'; import { USER_EVENT } from '@/constants/eventName'; import useMixpanelTrack from '@/hooks/Mixpanel/useMixpanelTrack'; import { PromotionArticle } from '@/types/promotion'; -import { getDDay } from '@/utils/getDday'; +import { getDDay } from '@/pages/PromotionPage/utils/getDday'; import CardMeta from './CardMeta/CardMeta'; import ClubTag from './ClubTag/ClubTag'; import DdayBadge from './DdayBadge/DdayBadge'; diff --git a/frontend/src/utils/getDday.test.ts b/frontend/src/pages/PromotionPage/utils/getDday.test.ts similarity index 100% rename from frontend/src/utils/getDday.test.ts rename to frontend/src/pages/PromotionPage/utils/getDday.test.ts diff --git a/frontend/src/utils/getDday.ts b/frontend/src/pages/PromotionPage/utils/getDday.ts similarity index 100% rename from frontend/src/utils/getDday.ts rename to frontend/src/pages/PromotionPage/utils/getDday.ts diff --git a/frontend/src/utils/promotionNotification.test.ts b/frontend/src/pages/PromotionPage/utils/promotionNotification.test.ts similarity index 97% rename from frontend/src/utils/promotionNotification.test.ts rename to frontend/src/pages/PromotionPage/utils/promotionNotification.test.ts index 6bb8f96f3..65d7068e9 100644 --- a/frontend/src/utils/promotionNotification.test.ts +++ b/frontend/src/pages/PromotionPage/utils/promotionNotification.test.ts @@ -3,7 +3,7 @@ import { getLastCheckedTime, getLatestPromotionTime, setLastCheckedTime, -} from './promotionNotification'; +} from '@/pages/PromotionPage/utils/promotionNotification'; describe('promotionNotification 유틸 함수 테스트', () => { beforeEach(() => { diff --git a/frontend/src/utils/promotionNotification.ts b/frontend/src/pages/PromotionPage/utils/promotionNotification.ts similarity index 100% rename from frontend/src/utils/promotionNotification.ts rename to frontend/src/pages/PromotionPage/utils/promotionNotification.ts From aced3e619553c9d9a7ea23e99f385f417aa559f1 Mon Sep 17 00:00:00 2001 From: seongwon seo Date: Wed, 25 Mar 2026 16:39:29 +0900 Subject: [PATCH 079/172] fix: lint error --- frontend/src/utils/calendarSyncUtils.test.ts | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/frontend/src/utils/calendarSyncUtils.test.ts b/frontend/src/utils/calendarSyncUtils.test.ts index 4621ddd8a..bc6e13ad1 100644 --- a/frontend/src/utils/calendarSyncUtils.test.ts +++ b/frontend/src/utils/calendarSyncUtils.test.ts @@ -51,9 +51,7 @@ describe('calendarSyncUtils', () => { it('parseDateKey는 ISO datetime을 YYYY-MM-DD로 정규화한다', () => { expect(parseDateKey('2026-03-22T06:25:00.000Z')).toBe('2026-03-22'); - expect(parseDateKey('Sun, 22 Mar 2026 06:25:00 GMT')).toBe( - '2026-03-22', - ); + expect(parseDateKey('Sun, 22 Mar 2026 06:25:00 GMT')).toBe('2026-03-22'); expect(parseDateKey('invalid-date')).toBeNull(); }); From 7a840ac5fa24fe573873276d93e06ad4d8f2ef13 Mon Sep 17 00:00:00 2001 From: suhyun113 <163711629+suhyun113@users.noreply.github.com> Date: Wed, 25 Mar 2026 19:26:00 +0900 Subject: [PATCH 080/172] =?UTF-8?q?fix:=20=ED=99=8D=EB=B3=B4=20=EC=9D=B4?= =?UTF-8?q?=EB=B2=A4=ED=8A=B8=20=EC=A0=95=EB=A0=AC=20util=20=EB=B6=84?= =?UTF-8?q?=EB=A6=AC=20=EB=B0=8F=20=ED=85=8C=EC=8A=A4=ED=8A=B8=20=EC=BD=94?= =?UTF-8?q?=EB=93=9C=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../utils/sortPromotions.test.ts | 122 ++++++++++++++++++ .../PromotionPage/utils/sortPromotions.ts | 27 ++++ 2 files changed, 149 insertions(+) create mode 100644 frontend/src/pages/PromotionPage/utils/sortPromotions.test.ts create mode 100644 frontend/src/pages/PromotionPage/utils/sortPromotions.ts diff --git a/frontend/src/pages/PromotionPage/utils/sortPromotions.test.ts b/frontend/src/pages/PromotionPage/utils/sortPromotions.test.ts new file mode 100644 index 000000000..ed8d8c8c1 --- /dev/null +++ b/frontend/src/pages/PromotionPage/utils/sortPromotions.test.ts @@ -0,0 +1,122 @@ +import { sortPromotions } from './sortPromotions'; +import { PromotionArticle } from '@/types/promotion'; + +const createArticle = ( + title: string, + start: string, + end: string +): PromotionArticle => ({ + id: title, + title, + clubId: 'club', + clubName: 'club', + location: 'location', + description: '', + images: [], + eventStartDate: start, + eventEndDate: end, +}); + +describe('sortPromotions', () => { + const NOW = new Date('2026-04-10T00:00:00Z').getTime(); + + it('진행중 이벤트가 가장 위로 온다', () => { + const ongoing = createArticle( + 'ongoing', + '2026-04-09T00:00:00Z', + '2026-04-11T00:00:00Z' + ); + + const upcoming = createArticle( + 'upcoming', + '2026-04-12T00:00:00Z', + '2026-04-13T00:00:00Z' + ); + + const result = sortPromotions([upcoming, ongoing], NOW); + + expect(result[0].title).toBe('ongoing'); + }); + + it('예정 이벤트는 D-day 가까운 순으로 정렬된다', () => { + const soon = createArticle( + 'soon', + '2026-04-11T00:00:00Z', + '2026-04-12T00:00:00Z' + ); + + const later = createArticle( + 'later', + '2026-04-20T00:00:00Z', + '2026-04-21T00:00:00Z' + ); + + const result = sortPromotions([later, soon], NOW); + + expect(result[0].title).toBe('soon'); + }); + + it('종료 이벤트는 맨 아래로 간다', () => { + const ended = createArticle( + 'ended', + '2026-04-01T00:00:00Z', + '2026-04-05T00:00:00Z' + ); + + const upcoming = createArticle( + 'upcoming', + '2026-04-12T00:00:00Z', + '2026-04-13T00:00:00Z' + ); + + const result = sortPromotions([ended, upcoming], NOW); + + expect(result[result.length - 1].title).toBe('ended'); + }); + + it('종료 이벤트끼리는 최신순으로 정렬된다', () => { + const old = createArticle( + 'old', + '2026-04-01T00:00:00Z', + '2026-04-02T00:00:00Z' + ); + + const recent = createArticle( + 'recent', + '2026-04-05T00:00:00Z', + '2026-04-06T00:00:00Z' + ); + + const result = sortPromotions([old, recent], NOW); + + expect(result[0].title).toBe('recent'); + }); + + it('전체 정렬 순서: 진행중 → 예정 → 종료', () => { + const ongoing = createArticle( + 'ongoing', + '2026-04-09T00:00:00Z', + '2026-04-11T00:00:00Z' + ); + + const upcoming = createArticle( + 'upcoming', + '2026-04-12T00:00:00Z', + '2026-04-13T00:00:00Z' + ); + + const ended = createArticle( + 'ended', + '2026-04-01T00:00:00Z', + '2026-04-02T00:00:00Z' + ); + + const result = sortPromotions([ended, upcoming, ongoing], NOW); + + expect(result.map(a => a.title)).toEqual([ + 'ongoing', + 'upcoming', + 'ended', + ]); + }); +}); \ No newline at end of file diff --git a/frontend/src/pages/PromotionPage/utils/sortPromotions.ts b/frontend/src/pages/PromotionPage/utils/sortPromotions.ts new file mode 100644 index 000000000..356e96d02 --- /dev/null +++ b/frontend/src/pages/PromotionPage/utils/sortPromotions.ts @@ -0,0 +1,27 @@ +import { PromotionArticle } from '@/types/promotion'; + +export const sortPromotions = ( + articles: PromotionArticle[], + now: number = Date.now(), +) => { + return [...articles].sort((a, b) => { + const aStart = new Date(a.eventStartDate).getTime(); + const aEnd = new Date(a.eventEndDate).getTime(); + const bStart = new Date(b.eventStartDate).getTime(); + const bEnd = new Date(b.eventEndDate).getTime(); + + const getStatusWeight = (start: number, end: number) => { + if (start <= now && end >= now) return 1; // 진행 중 + if (start > now) return 2; // 예정 + return 3; // 종료 + }; + + const aStatusWeight = getStatusWeight(aStart, aEnd); + const bStatusWeight = getStatusWeight(bStart, bEnd); + + if (aStatusWeight !== bStatusWeight) return aStatusWeight - bStatusWeight; + if (aStatusWeight === 2) return aStart - bStart; + if (aStatusWeight === 3) return bStart - aStart; + return 0; + }); +}; From 18a8cca28683bcc3dd9cc23a409bdde86f510b9f Mon Sep 17 00:00:00 2001 From: suhyun113 <163711629+suhyun113@users.noreply.github.com> Date: Wed, 25 Mar 2026 19:33:56 +0900 Subject: [PATCH 081/172] =?UTF-8?q?fix:=20D-day=20=EA=B3=84=EC=82=B0=20?= =?UTF-8?q?=EB=A1=9C=EC=A7=81=20=EC=8B=9C=EA=B0=84=20=EA=B8=B0=EC=A4=80=20?= =?UTF-8?q?=ED=8F=AC=ED=95=A8=ED=95=98=EB=8F=84=EB=A1=9D=20=EC=88=98?= =?UTF-8?q?=EC=A0=95=20=EB=B0=8F=20=ED=85=8C=EC=8A=A4=ED=8A=B8=20=EC=BD=94?= =?UTF-8?q?=EB=93=9C=20=EB=B3=B4=EC=99=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/src/apis/promotion.ts | 19 ++---------- .../pages/PromotionPage/utils/getDday.test.ts | 30 +++++++++---------- .../src/pages/PromotionPage/utils/getDday.ts | 27 ++++++++--------- 3 files changed, 30 insertions(+), 46 deletions(-) diff --git a/frontend/src/apis/promotion.ts b/frontend/src/apis/promotion.ts index f067689f2..6b8b9f167 100644 --- a/frontend/src/apis/promotion.ts +++ b/frontend/src/apis/promotion.ts @@ -6,6 +6,7 @@ import { } from '@/types/promotion'; import { secureFetch } from './auth/secureFetch'; import { handleResponse } from './utils/apiHelpers'; +import { sortPromotions } from '@/pages/PromotionPage/utils/sortPromotions'; export const getPromotionArticles = async (): Promise => { const response = await fetch(`${API_BASE_URL}/api/promotion`); @@ -25,23 +26,7 @@ export const getPromotionArticles = async (): Promise => { const merged = [...festivalMock, ...serverArticle]; - return merged.sort((prev, next) => { - const now = Date.now(); - - const prevStart = new Date(prev.eventStartDate).getTime(); - const nextStart = new Date(next.eventStartDate).getTime(); - - const prevEnd = new Date(prev.eventEndDate).getTime(); - const nextEnd = new Date(next.eventEndDate).getTime(); - - const prevEnded = prevEnd < now; - const nextEnded = nextEnd < now; - - if (prevEnded !== nextEnded) return prevEnded ? 1 : -1; - if (prevEnded && nextEnded) return nextEnd - prevEnd; - - return prevStart - nextStart; - }); + return sortPromotions(merged); }; export const createPromotionArticle = async ( diff --git a/frontend/src/pages/PromotionPage/utils/getDday.test.ts b/frontend/src/pages/PromotionPage/utils/getDday.test.ts index 4c64e4d0a..12dd5f7d3 100644 --- a/frontend/src/pages/PromotionPage/utils/getDday.test.ts +++ b/frontend/src/pages/PromotionPage/utils/getDday.test.ts @@ -10,55 +10,55 @@ describe('getDDay', () => { }); it('행사 시작 전이면 D-n 반환', () => { - jest.setSystemTime(new Date('2026-03-20')); + jest.setSystemTime(new Date('2026-03-20T00:00:00Z')); const result = getDDay( - '2026-03-25', - '2026-03-27' + '2026-03-25T00:00:00Z', + '2026-03-27T00:00:00Z' ); expect(result).toBe(5); }); it('행사 시작일이면 D-Day (0) 반환', () => { - jest.setSystemTime(new Date('2026-03-25')); + jest.setSystemTime(new Date('2026-03-25T00:00:00Z')); const result = getDDay( - '2026-03-25', - '2026-03-27' + '2026-03-25T00:00:00Z', + '2026-03-27T00:00:00Z' ); expect(result).toBe(0); }); it('행사 중간 날짜도 D-Day (0) 반환', () => { - jest.setSystemTime(new Date('2026-03-26')); + jest.setSystemTime(new Date('2026-03-26T12:00:00Z')); const result = getDDay( - '2026-03-25', - '2026-03-27' + '2026-03-25T00:00:00Z', + '2026-03-27T00:00:00Z' ); expect(result).toBe(0); }); it('행사 마지막 날도 D-Day (0) 반환', () => { - jest.setSystemTime(new Date('2026-03-27')); + jest.setSystemTime(new Date('2026-03-27T00:00:00Z')); const result = getDDay( - '2026-03-25', - '2026-03-27' + '2026-03-25T00:00:00Z', + '2026-03-27T23:59:59Z' ); expect(result).toBe(0); }); it('행사 종료 후이면 -1 반환', () => { - jest.setSystemTime(new Date('2026-03-28')); + jest.setSystemTime(new Date('2026-03-28T00:00:00Z')); const result = getDDay( - '2026-03-25', - '2026-03-27' + '2026-03-25T00:00:00Z', + '2026-03-27T00:00:00Z' ); expect(result).toBe(-1); diff --git a/frontend/src/pages/PromotionPage/utils/getDday.ts b/frontend/src/pages/PromotionPage/utils/getDday.ts index 1fc3371cf..c0a02fc6f 100644 --- a/frontend/src/pages/PromotionPage/utils/getDday.ts +++ b/frontend/src/pages/PromotionPage/utils/getDday.ts @@ -1,21 +1,20 @@ -export const getDDay = (startDate: string, endDate: string) => { - const today = new Date(); - const start = new Date(startDate); - const end = new Date(endDate); +export const getDDay = (eventStartDate: string, eventEndDate: string) => { + const currentTime = new Date().getTime(); + const eventStartTime = new Date(eventStartDate).getTime(); + const eventEndTime = new Date(eventEndDate).getTime(); - today.setHours(0, 0, 0, 0); - start.setHours(0, 0, 0, 0); - end.setHours(0, 0, 0, 0); + if (currentTime < eventStartTime) { + const remainingTimeUntilStart = eventStartTime - currentTime; + + const remainingDays = Math.ceil( + remainingTimeUntilStart / (1000 * 60 * 60 * 24), + ); - if (today >= start && today <= end) { - return 0; + return remainingDays; } - if (today < start) { - const diff = Math.ceil( - (start.getTime() - today.getTime()) / (1000 * 60 * 60 * 24), - ); - return diff; + if (currentTime >= eventStartTime && currentTime <= eventEndTime) { + return 0; } return -1; From 60283f66876899852e2c2302b37a6e546c6be1ec Mon Sep 17 00:00:00 2001 From: suhyun113 <163711629+suhyun113@users.noreply.github.com> Date: Wed, 25 Mar 2026 19:35:00 +0900 Subject: [PATCH 082/172] =?UTF-8?q?refactor:=20Prettier=20=EC=BD=94?= =?UTF-8?q?=EB=93=9C=20=ED=8F=AC=EB=A7=B7=20=EC=A0=95=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/src/apis/promotion.ts | 2 +- .../PromotionInfoSection.tsx | 2 +- .../RelatedPromotionSection.tsx | 2 +- .../list/PromotionCard/CardMeta/CardMeta.tsx | 2 +- .../list/PromotionCard/PromotionCard.tsx | 2 +- .../pages/PromotionPage/utils/getDday.test.ts | 27 ++++------------- .../src/pages/PromotionPage/utils/getDday.ts | 6 ++-- .../utils/promotionNotification.test.ts | 2 +- .../utils/sortPromotions.test.ts | 30 +++++++++---------- frontend/src/utils/formatKSTDateTime.ts | 4 +-- 10 files changed, 32 insertions(+), 47 deletions(-) diff --git a/frontend/src/apis/promotion.ts b/frontend/src/apis/promotion.ts index 6b8b9f167..c34200477 100644 --- a/frontend/src/apis/promotion.ts +++ b/frontend/src/apis/promotion.ts @@ -1,12 +1,12 @@ import API_BASE_URL from '@/constants/api'; import { festivalMock } from '@/mocks/data/festivalMock'; +import { sortPromotions } from '@/pages/PromotionPage/utils/sortPromotions'; import { CreatePromotionArticleRequest, PromotionArticle, } from '@/types/promotion'; import { secureFetch } from './auth/secureFetch'; import { handleResponse } from './utils/apiHelpers'; -import { sortPromotions } from '@/pages/PromotionPage/utils/sortPromotions'; export const getPromotionArticles = async (): Promise => { const response = await fetch(`${API_BASE_URL}/api/promotion`); diff --git a/frontend/src/pages/PromotionPage/components/detail/PromotionInfoSection/PromotionInfoSection.tsx b/frontend/src/pages/PromotionPage/components/detail/PromotionInfoSection/PromotionInfoSection.tsx index afe7b6747..5d226c67b 100644 --- a/frontend/src/pages/PromotionPage/components/detail/PromotionInfoSection/PromotionInfoSection.tsx +++ b/frontend/src/pages/PromotionPage/components/detail/PromotionInfoSection/PromotionInfoSection.tsx @@ -1,6 +1,6 @@ import { PromotionArticle } from '@/types/promotion'; -import * as Styled from './PromotionInfoSection.styles'; import { formatKSTDateTimeFull } from '@/utils/formatKSTDateTime'; +import * as Styled from './PromotionInfoSection.styles'; interface Props { article: PromotionArticle; diff --git a/frontend/src/pages/PromotionPage/components/detail/RelatedPromotionSection/RelatedPromotionSection.tsx b/frontend/src/pages/PromotionPage/components/detail/RelatedPromotionSection/RelatedPromotionSection.tsx index 13afa4739..97273fbfd 100644 --- a/frontend/src/pages/PromotionPage/components/detail/RelatedPromotionSection/RelatedPromotionSection.tsx +++ b/frontend/src/pages/PromotionPage/components/detail/RelatedPromotionSection/RelatedPromotionSection.tsx @@ -1,6 +1,6 @@ import { useNavigate } from 'react-router-dom'; -import { PromotionArticle } from '@/types/promotion'; import { getDDay } from '@/pages/PromotionPage/utils/getDday'; +import { PromotionArticle } from '@/types/promotion'; import RelatedPromotionCard from './RelatedPromotionCard/RelatedPromotionCard'; import * as Styled from './RelatedPromotionSection.styles'; diff --git a/frontend/src/pages/PromotionPage/components/list/PromotionCard/CardMeta/CardMeta.tsx b/frontend/src/pages/PromotionPage/components/list/PromotionCard/CardMeta/CardMeta.tsx index e17f384e0..4255ce663 100644 --- a/frontend/src/pages/PromotionPage/components/list/PromotionCard/CardMeta/CardMeta.tsx +++ b/frontend/src/pages/PromotionPage/components/list/PromotionCard/CardMeta/CardMeta.tsx @@ -1,7 +1,7 @@ import LocationIcon from '@/assets/images/icons/location_icon.svg'; import TimeIcon from '@/assets/images/icons/time_icon.svg'; -import * as Styled from './CardMeta.styles'; import { formatKSTDate } from '@/utils/formatKSTDateTime'; +import * as Styled from './CardMeta.styles'; interface CardMetaProps { title: string; diff --git a/frontend/src/pages/PromotionPage/components/list/PromotionCard/PromotionCard.tsx b/frontend/src/pages/PromotionPage/components/list/PromotionCard/PromotionCard.tsx index abee6e88c..f8f5070da 100644 --- a/frontend/src/pages/PromotionPage/components/list/PromotionCard/PromotionCard.tsx +++ b/frontend/src/pages/PromotionPage/components/list/PromotionCard/PromotionCard.tsx @@ -1,8 +1,8 @@ import { useNavigate } from 'react-router-dom'; import { USER_EVENT } from '@/constants/eventName'; import useMixpanelTrack from '@/hooks/Mixpanel/useMixpanelTrack'; -import { PromotionArticle } from '@/types/promotion'; import { getDDay } from '@/pages/PromotionPage/utils/getDday'; +import { PromotionArticle } from '@/types/promotion'; import CardMeta from './CardMeta/CardMeta'; import ClubTag from './ClubTag/ClubTag'; import DdayBadge from './DdayBadge/DdayBadge'; diff --git a/frontend/src/pages/PromotionPage/utils/getDday.test.ts b/frontend/src/pages/PromotionPage/utils/getDday.test.ts index 12dd5f7d3..91e63d97b 100644 --- a/frontend/src/pages/PromotionPage/utils/getDday.test.ts +++ b/frontend/src/pages/PromotionPage/utils/getDday.test.ts @@ -12,10 +12,7 @@ describe('getDDay', () => { it('행사 시작 전이면 D-n 반환', () => { jest.setSystemTime(new Date('2026-03-20T00:00:00Z')); - const result = getDDay( - '2026-03-25T00:00:00Z', - '2026-03-27T00:00:00Z' - ); + const result = getDDay('2026-03-25T00:00:00Z', '2026-03-27T00:00:00Z'); expect(result).toBe(5); }); @@ -23,10 +20,7 @@ describe('getDDay', () => { it('행사 시작일이면 D-Day (0) 반환', () => { jest.setSystemTime(new Date('2026-03-25T00:00:00Z')); - const result = getDDay( - '2026-03-25T00:00:00Z', - '2026-03-27T00:00:00Z' - ); + const result = getDDay('2026-03-25T00:00:00Z', '2026-03-27T00:00:00Z'); expect(result).toBe(0); }); @@ -34,10 +28,7 @@ describe('getDDay', () => { it('행사 중간 날짜도 D-Day (0) 반환', () => { jest.setSystemTime(new Date('2026-03-26T12:00:00Z')); - const result = getDDay( - '2026-03-25T00:00:00Z', - '2026-03-27T00:00:00Z' - ); + const result = getDDay('2026-03-25T00:00:00Z', '2026-03-27T00:00:00Z'); expect(result).toBe(0); }); @@ -45,10 +36,7 @@ describe('getDDay', () => { it('행사 마지막 날도 D-Day (0) 반환', () => { jest.setSystemTime(new Date('2026-03-27T00:00:00Z')); - const result = getDDay( - '2026-03-25T00:00:00Z', - '2026-03-27T23:59:59Z' - ); + const result = getDDay('2026-03-25T00:00:00Z', '2026-03-27T23:59:59Z'); expect(result).toBe(0); }); @@ -56,11 +44,8 @@ describe('getDDay', () => { it('행사 종료 후이면 -1 반환', () => { jest.setSystemTime(new Date('2026-03-28T00:00:00Z')); - const result = getDDay( - '2026-03-25T00:00:00Z', - '2026-03-27T00:00:00Z' - ); + const result = getDDay('2026-03-25T00:00:00Z', '2026-03-27T00:00:00Z'); expect(result).toBe(-1); }); -}); \ No newline at end of file +}); diff --git a/frontend/src/pages/PromotionPage/utils/getDday.ts b/frontend/src/pages/PromotionPage/utils/getDday.ts index c0a02fc6f..b1946d03a 100644 --- a/frontend/src/pages/PromotionPage/utils/getDday.ts +++ b/frontend/src/pages/PromotionPage/utils/getDday.ts @@ -5,7 +5,7 @@ export const getDDay = (eventStartDate: string, eventEndDate: string) => { if (currentTime < eventStartTime) { const remainingTimeUntilStart = eventStartTime - currentTime; - + const remainingDays = Math.ceil( remainingTimeUntilStart / (1000 * 60 * 60 * 24), ); @@ -14,8 +14,8 @@ export const getDDay = (eventStartDate: string, eventEndDate: string) => { } if (currentTime >= eventStartTime && currentTime <= eventEndTime) { - return 0; + return 0; } return -1; -}; \ No newline at end of file +}; diff --git a/frontend/src/pages/PromotionPage/utils/promotionNotification.test.ts b/frontend/src/pages/PromotionPage/utils/promotionNotification.test.ts index 65d7068e9..a7303ad96 100644 --- a/frontend/src/pages/PromotionPage/utils/promotionNotification.test.ts +++ b/frontend/src/pages/PromotionPage/utils/promotionNotification.test.ts @@ -1,9 +1,9 @@ -import { PromotionArticle } from '@/types/promotion'; import { getLastCheckedTime, getLatestPromotionTime, setLastCheckedTime, } from '@/pages/PromotionPage/utils/promotionNotification'; +import { PromotionArticle } from '@/types/promotion'; describe('promotionNotification 유틸 함수 테스트', () => { beforeEach(() => { diff --git a/frontend/src/pages/PromotionPage/utils/sortPromotions.test.ts b/frontend/src/pages/PromotionPage/utils/sortPromotions.test.ts index ed8d8c8c1..c9da3d1ab 100644 --- a/frontend/src/pages/PromotionPage/utils/sortPromotions.test.ts +++ b/frontend/src/pages/PromotionPage/utils/sortPromotions.test.ts @@ -1,10 +1,10 @@ -import { sortPromotions } from './sortPromotions'; import { PromotionArticle } from '@/types/promotion'; +import { sortPromotions } from './sortPromotions'; const createArticle = ( title: string, start: string, - end: string + end: string, ): PromotionArticle => ({ id: title, title, @@ -24,13 +24,13 @@ describe('sortPromotions', () => { const ongoing = createArticle( 'ongoing', '2026-04-09T00:00:00Z', - '2026-04-11T00:00:00Z' + '2026-04-11T00:00:00Z', ); const upcoming = createArticle( 'upcoming', '2026-04-12T00:00:00Z', - '2026-04-13T00:00:00Z' + '2026-04-13T00:00:00Z', ); const result = sortPromotions([upcoming, ongoing], NOW); @@ -42,13 +42,13 @@ describe('sortPromotions', () => { const soon = createArticle( 'soon', '2026-04-11T00:00:00Z', - '2026-04-12T00:00:00Z' + '2026-04-12T00:00:00Z', ); const later = createArticle( 'later', '2026-04-20T00:00:00Z', - '2026-04-21T00:00:00Z' + '2026-04-21T00:00:00Z', ); const result = sortPromotions([later, soon], NOW); @@ -60,13 +60,13 @@ describe('sortPromotions', () => { const ended = createArticle( 'ended', '2026-04-01T00:00:00Z', - '2026-04-05T00:00:00Z' + '2026-04-05T00:00:00Z', ); const upcoming = createArticle( 'upcoming', '2026-04-12T00:00:00Z', - '2026-04-13T00:00:00Z' + '2026-04-13T00:00:00Z', ); const result = sortPromotions([ended, upcoming], NOW); @@ -78,13 +78,13 @@ describe('sortPromotions', () => { const old = createArticle( 'old', '2026-04-01T00:00:00Z', - '2026-04-02T00:00:00Z' + '2026-04-02T00:00:00Z', ); const recent = createArticle( 'recent', '2026-04-05T00:00:00Z', - '2026-04-06T00:00:00Z' + '2026-04-06T00:00:00Z', ); const result = sortPromotions([old, recent], NOW); @@ -96,27 +96,27 @@ describe('sortPromotions', () => { const ongoing = createArticle( 'ongoing', '2026-04-09T00:00:00Z', - '2026-04-11T00:00:00Z' + '2026-04-11T00:00:00Z', ); const upcoming = createArticle( 'upcoming', '2026-04-12T00:00:00Z', - '2026-04-13T00:00:00Z' + '2026-04-13T00:00:00Z', ); const ended = createArticle( 'ended', '2026-04-01T00:00:00Z', - '2026-04-02T00:00:00Z' + '2026-04-02T00:00:00Z', ); const result = sortPromotions([ended, upcoming, ongoing], NOW); - expect(result.map(a => a.title)).toEqual([ + expect(result.map((a) => a.title)).toEqual([ 'ongoing', 'upcoming', 'ended', ]); }); -}); \ No newline at end of file +}); diff --git a/frontend/src/utils/formatKSTDateTime.ts b/frontend/src/utils/formatKSTDateTime.ts index 0574c22fc..307165dcc 100644 --- a/frontend/src/utils/formatKSTDateTime.ts +++ b/frontend/src/utils/formatKSTDateTime.ts @@ -1,6 +1,6 @@ export const formatKSTDateTime = ( dateStr: string, - options: Intl.DateTimeFormatOptions = {} + options: Intl.DateTimeFormatOptions = {}, ) => { if (!dateStr) return ''; const date = new Date(dateStr); @@ -26,4 +26,4 @@ export const formatKSTDateTimeFull = (dateStr: string) => weekday: 'short', hour: '2-digit', minute: '2-digit', - }); \ No newline at end of file + }); From 68a63112b3183c06f715461cdfb5b495be6e2b26 Mon Sep 17 00:00:00 2001 From: suhyun113 <163711629+suhyun113@users.noreply.github.com> Date: Thu, 26 Mar 2026 20:22:32 +0900 Subject: [PATCH 083/172] =?UTF-8?q?feat:=20=EC=B4=9D=EB=8F=99=EC=95=84?= =?UTF-8?q?=EB=A6=AC=EC=97=B0=ED=95=A9=ED=9A=8C=20=EB=8C=80=ED=91=9C?= =?UTF-8?q?=EC=9E=90=20=EC=9D=B4=EB=AF=B8=EC=A7=80=20=EC=B6=94=EA=B0=80=20?= =?UTF-8?q?=EB=B0=8F=20=EB=A9=A4=EB=B2=84=20=ED=83=80=EC=9E=85=EB=B3=84=20?= =?UTF-8?q?=ED=94=84=EB=A1=9C=ED=95=84=20=EB=B0=B0=EA=B2=BD=EC=83=89=20?= =?UTF-8?q?=EC=A0=81=EC=9A=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../images/icons/category_button/index.ts | 2 + .../icons/club_union_representative_icon.svg | 7 +++ frontend/src/constants/clubUnionInfo.ts | 50 ++++++++++++------- .../ClubUnionPage/ClubUnionPage.styles.ts | 30 +++++++---- .../src/pages/ClubUnionPage/ClubUnionPage.tsx | 20 +++++++- 5 files changed, 81 insertions(+), 28 deletions(-) create mode 100644 frontend/src/assets/images/icons/club_union_representative_icon.svg diff --git a/frontend/src/assets/images/icons/category_button/index.ts b/frontend/src/assets/images/icons/category_button/index.ts index b49dafdb6..e159fffe2 100644 --- a/frontend/src/assets/images/icons/category_button/index.ts +++ b/frontend/src/assets/images/icons/category_button/index.ts @@ -12,6 +12,7 @@ import iconStudyActive from '@/assets/images/icons/category_button/category_stud import iconStudy from '@/assets/images/icons/category_button/category_study_button_icon.svg'; import iconVolunteerActive from '@/assets/images/icons/category_button/category_volunteer_button_icon_active.svg'; import iconVolunteer from '@/assets/images/icons/category_button/category_volunteer_button_icon.svg'; +import iconRepresentative from '@/assets/images/icons/club_union_representative_icon.svg'; export const inactiveCategoryIcons: Record = { all: iconAll, @@ -21,6 +22,7 @@ export const inactiveCategoryIcons: Record = { study: iconStudy, sport: iconSport, performance: iconPerformance, + representative: iconRepresentative, }; export const activeCategoryIcons: Record = { diff --git a/frontend/src/assets/images/icons/club_union_representative_icon.svg b/frontend/src/assets/images/icons/club_union_representative_icon.svg new file mode 100644 index 000000000..ac06cec8d --- /dev/null +++ b/frontend/src/assets/images/icons/club_union_representative_icon.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/frontend/src/constants/clubUnionInfo.ts b/frontend/src/constants/clubUnionInfo.ts index 0ac79afab..4fd6d2e3b 100644 --- a/frontend/src/constants/clubUnionInfo.ts +++ b/frontend/src/constants/clubUnionInfo.ts @@ -6,14 +6,15 @@ export interface ClubUnionMember { role: string; description: string; imageSrc: string; + type: keyof typeof MEMBER_AVATARS; } const MEMBER_AVATARS = { - PRESIDENT: inactiveCategoryIcons.all, - VICE_PRESIDENT: inactiveCategoryIcons.all, - PLANNING: inactiveCategoryIcons.all, - SECRETARY: inactiveCategoryIcons.all, - PROMOTION: inactiveCategoryIcons.all, + PRESIDENT: inactiveCategoryIcons.representative, + VICE_PRESIDENT: inactiveCategoryIcons.representative, + PLANNING: inactiveCategoryIcons.representative, + SECRETARY: inactiveCategoryIcons.representative, + PROMOTION: inactiveCategoryIcons.representative, RELIGION: inactiveCategoryIcons.religion, HOBBY: inactiveCategoryIcons.hobby, STUDY: inactiveCategoryIcons.study, @@ -36,6 +37,7 @@ export const CLUB_UNION_MEMBERS: ClubUnionMember[] = [ role: '회장', description: '', imageSrc: MEMBER_AVATARS.PRESIDENT, + type: 'PRESIDENT', }, { id: 2, @@ -43,6 +45,7 @@ export const CLUB_UNION_MEMBERS: ClubUnionMember[] = [ role: '부회장', description: '', imageSrc: MEMBER_AVATARS.VICE_PRESIDENT, + type: 'VICE_PRESIDENT', }, { id: 3, @@ -50,6 +53,7 @@ export const CLUB_UNION_MEMBERS: ClubUnionMember[] = [ role: '기획국장', description: '', imageSrc: MEMBER_AVATARS.PLANNING, + type: 'PLANNING', }, { id: 4, @@ -57,6 +61,7 @@ export const CLUB_UNION_MEMBERS: ClubUnionMember[] = [ role: '사무국장', description: '', imageSrc: MEMBER_AVATARS.SECRETARY, + type: 'SECRETARY', }, { id: 5, @@ -64,6 +69,7 @@ export const CLUB_UNION_MEMBERS: ClubUnionMember[] = [ role: '홍보국장', description: '', imageSrc: MEMBER_AVATARS.PROMOTION, + type: 'PROMOTION', }, { id: 6, @@ -71,6 +77,7 @@ export const CLUB_UNION_MEMBERS: ClubUnionMember[] = [ role: '봉사분과장', description: '', imageSrc: MEMBER_AVATARS.VOLUNTEER, + type: 'VOLUNTEER', }, { id: 7, @@ -78,6 +85,7 @@ export const CLUB_UNION_MEMBERS: ClubUnionMember[] = [ role: '종교분과장', description: '', imageSrc: MEMBER_AVATARS.RELIGION, + type: 'RELIGION', }, { id: 8, @@ -85,6 +93,7 @@ export const CLUB_UNION_MEMBERS: ClubUnionMember[] = [ role: '취미교양분과장', description: '', imageSrc: MEMBER_AVATARS.HOBBY, + type: 'HOBBY', }, { id: 9, @@ -92,33 +101,38 @@ export const CLUB_UNION_MEMBERS: ClubUnionMember[] = [ role: '학술분과장', description: '', imageSrc: MEMBER_AVATARS.STUDY, + type: 'STUDY', }, - { + { id: 10, - name: '권민준', - role: '공연1분과장', + name: '김민제', + role: '운동1분과장', description: '', - imageSrc: MEMBER_AVATARS.PERFORMANCE, + imageSrc: MEMBER_AVATARS.SPORT, + type: 'SPORT', }, { id: 11, - name: '곽현우', - role: '공연2분과장', + name: '이상재', + role: '운동2분과장', description: '', - imageSrc: MEMBER_AVATARS.PERFORMANCE, + imageSrc: MEMBER_AVATARS.SPORT, + type: 'SPORT', }, { id: 12, - name: '김민제', - role: '운동1분과장', + name: '권민준', + role: '공연1분과장', description: '', - imageSrc: MEMBER_AVATARS.SPORT, + imageSrc: MEMBER_AVATARS.PERFORMANCE, + type: 'PERFORMANCE', }, { id: 13, - name: '이상재', - role: '운동2분과장', + name: '곽현우', + role: '공연2분과장', description: '', - imageSrc: MEMBER_AVATARS.SPORT, + imageSrc: MEMBER_AVATARS.PERFORMANCE, + type: 'PERFORMANCE', }, ]; diff --git a/frontend/src/pages/ClubUnionPage/ClubUnionPage.styles.ts b/frontend/src/pages/ClubUnionPage/ClubUnionPage.styles.ts index 13e67a94b..e6cf6523c 100644 --- a/frontend/src/pages/ClubUnionPage/ClubUnionPage.styles.ts +++ b/frontend/src/pages/ClubUnionPage/ClubUnionPage.styles.ts @@ -1,5 +1,6 @@ import styled from 'styled-components'; import { media } from '@/styles/mediaQuery'; +import { colors } from '@/styles/theme/colors'; export const Title = styled.h1` font-size: 2.5rem; @@ -86,6 +87,7 @@ export const ProfileGrid = styled.div` export const InfoOverlay = styled.div` position: absolute; + inset: 0; top: 0; left: 0; width: 100%; @@ -105,9 +107,13 @@ export const InfoOverlay = styled.div` `; export const ProfileImage = styled.img` - width: 100%; - height: 100%; - object-fit: cover; + width: 80%; + height: 80%; + object-fit: contain; + position: absolute; + top: 42%; + left: 50%; + transform: translate(-50%, -50%); transition: transform 0.3s ease, filter 0.3s ease; @@ -118,17 +124,21 @@ export const NameBadge = styled.div` bottom: 10%; left: 50%; transform: translateX(-50%); - background-color: rgba(255, 255, 255, 0.8); - color: #333; - padding: 5px 15px; - border-radius: 15px; + background-color: ${colors.base.white}; + color: ${colors.base.black}; + padding: 3px 12px; + border-radius: 30px; font-weight: 600; font-size: 1rem; transition: opacity 0.3s ease; white-space: nowrap; + + ${media.mobile} { + font-size: 0.8rem; + } `; -export const ProfileCardContainer = styled.div` +export const ProfileCardContainer = styled.div<{ bgColor: string }>` position: relative; width: 180px; height: 180px; @@ -136,6 +146,7 @@ export const ProfileCardContainer = styled.div` overflow: hidden; cursor: pointer; box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1); + background-color: ${({ bgColor }) => bgColor}; ${media.laptop} { width: 160px; @@ -154,8 +165,9 @@ export const ProfileCardContainer = styled.div` &:hover { ${ProfileImage} { - transform: scale(1.1); + transform: translate(-50%, -50%) scale(1.4); filter: brightness(0.5); + top: 50%; } ${InfoOverlay} { opacity: 1; diff --git a/frontend/src/pages/ClubUnionPage/ClubUnionPage.tsx b/frontend/src/pages/ClubUnionPage/ClubUnionPage.tsx index 6cf769d41..c12028294 100644 --- a/frontend/src/pages/ClubUnionPage/ClubUnionPage.tsx +++ b/frontend/src/pages/ClubUnionPage/ClubUnionPage.tsx @@ -7,6 +7,21 @@ import useMixpanelTrack from '@/hooks/Mixpanel/useMixpanelTrack'; import useTrackPageView from '@/hooks/Mixpanel/useTrackPageView'; import { PageContainer } from '@/styles/PageContainer.styles'; import * as Styled from './ClubUnionPage.styles'; +import { colors } from '@/styles/theme/colors'; + +const MEMBER_COLORS = { + PRESIDENT: colors.accent[1][500], + VICE_PRESIDENT: colors.accent[1][500], + PLANNING: colors.accent[1][500], + SECRETARY: colors.accent[1][500], + PROMOTION: colors.accent[1][500], + VOLUNTEER: colors.secondary[1].back, + RELIGION: colors.secondary[2].back, + HOBBY: colors.secondary[3].back, + STUDY: colors.secondary[4].back, + SPORT: colors.secondary[5].back, + PERFORMANCE: colors.secondary[6].back, +}; const ClubUnionPage = () => { useTrackPageView(PAGE_VIEW.CLUB_UNION_PAGE); @@ -51,7 +66,10 @@ const ClubUnionPage = () => { {CLUB_UNION_MEMBERS.map((member) => ( - + Date: Thu, 26 Mar 2026 22:02:25 +0900 Subject: [PATCH 084/172] =?UTF-8?q?feat:=20=EC=B4=9D=EB=8F=99=EC=97=B0=20?= =?UTF-8?q?=EA=B0=81=EC=98=A4=20=EC=B6=94=EA=B0=80=20=EB=B0=8F=20=ED=83=9C?= =?UTF-8?q?=EB=B8=94=EB=A6=BF=20=EC=9D=B4=ED=95=98=EC=97=90=EC=84=9C=20?= =?UTF-8?q?=EC=88=A8=EA=B9=80=20=EC=B2=98=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/src/constants/clubUnionInfo.ts | 28 +++++++++---------- .../ClubUnionPage/ClubUnionPage.styles.ts | 4 +++ 2 files changed, 18 insertions(+), 14 deletions(-) diff --git a/frontend/src/constants/clubUnionInfo.ts b/frontend/src/constants/clubUnionInfo.ts index 4fd6d2e3b..76f4d41af 100644 --- a/frontend/src/constants/clubUnionInfo.ts +++ b/frontend/src/constants/clubUnionInfo.ts @@ -29,13 +29,13 @@ export const CLUB_UNION_SNS = { } as const; // 개발자 가이드: description 필드는 UI가 깨지지 않도록 글자 수를 제한합니다. -// (권장) 모바일: 50자 이내, 데스크톱: 100자 이내 +// (권장) 데스크톱: 30자 이내 export const CLUB_UNION_MEMBERS: ClubUnionMember[] = [ { id: 1, name: '이주은', role: '회장', - description: '', + description: '모두가 바라는 미래가 이루어질 수 있도록 노력하겠습니다.', imageSrc: MEMBER_AVATARS.PRESIDENT, type: 'PRESIDENT', }, @@ -43,7 +43,7 @@ export const CLUB_UNION_MEMBERS: ClubUnionMember[] = [ id: 2, name: '이정원', role: '부회장', - description: '', + description: '동아리가 한 해동안 잘 운영될 수 있도록 노력하겠습니다.', imageSrc: MEMBER_AVATARS.VICE_PRESIDENT, type: 'VICE_PRESIDENT', }, @@ -51,7 +51,7 @@ export const CLUB_UNION_MEMBERS: ClubUnionMember[] = [ id: 3, name: '최지현', role: '기획국장', - description: '', + description: '동아리의 바램이 이루어지도록 질 높은 행사를 만들어가겠습니다.', imageSrc: MEMBER_AVATARS.PLANNING, type: 'PLANNING', }, @@ -59,7 +59,7 @@ export const CLUB_UNION_MEMBERS: ClubUnionMember[] = [ id: 4, name: '김민서', role: '사무국장', - description: '', + description: '작은 바람이 모여 우리가 될 수 있도록, 함께 하겠습니다.', imageSrc: MEMBER_AVATARS.SECRETARY, type: 'SECRETARY', }, @@ -67,7 +67,7 @@ export const CLUB_UNION_MEMBERS: ClubUnionMember[] = [ id: 5, name: '최동희', role: '홍보국장', - description: '', + description: '모두가 바라는 대로, 즐거운 동아리 활동에 앞장서 노력하겠습니다.', imageSrc: MEMBER_AVATARS.PROMOTION, type: 'PROMOTION', }, @@ -75,7 +75,7 @@ export const CLUB_UNION_MEMBERS: ClubUnionMember[] = [ id: 6, name: '전호진', role: '봉사분과장', - description: '', + description: '나눔은 나눌수록 배가 됩니다. 작은 나눔을 통해 삶의 따스함을 느낍시다.', imageSrc: MEMBER_AVATARS.VOLUNTEER, type: 'VOLUNTEER', }, @@ -83,7 +83,7 @@ export const CLUB_UNION_MEMBERS: ClubUnionMember[] = [ id: 7, name: '신가윤', role: '종교분과장', - description: '', + description: '여러분의 소원이 현실이 되도록, 열심히 하겠습니다.', imageSrc: MEMBER_AVATARS.RELIGION, type: 'RELIGION', }, @@ -91,7 +91,7 @@ export const CLUB_UNION_MEMBERS: ClubUnionMember[] = [ id: 8, name: '정상윤', role: '취미교양분과장', - description: '', + description: '목소리에 귀 기울이며, 바라는 방향으로 함께 나아가겠습니다.', imageSrc: MEMBER_AVATARS.HOBBY, type: 'HOBBY', }, @@ -99,7 +99,7 @@ export const CLUB_UNION_MEMBERS: ClubUnionMember[] = [ id: 9, name: '김은새', role: '학술분과장', - description: '', + description: '기대에 부응할 수 있도록, 책임감 있게 움직이겠습니다.', imageSrc: MEMBER_AVATARS.STUDY, type: 'STUDY', }, @@ -107,7 +107,7 @@ export const CLUB_UNION_MEMBERS: ClubUnionMember[] = [ id: 10, name: '김민제', role: '운동1분과장', - description: '', + description: '동아리의 연결과 성장을 이끄는 we:sh가 함께하겠습니다.', imageSrc: MEMBER_AVATARS.SPORT, type: 'SPORT', }, @@ -115,7 +115,7 @@ export const CLUB_UNION_MEMBERS: ClubUnionMember[] = [ id: 11, name: '이상재', role: '운동2분과장', - description: '', + description: '여러분이 바라는 대로, 원하는 대로 열심히 하겠습니다.', imageSrc: MEMBER_AVATARS.SPORT, type: 'SPORT', }, @@ -123,7 +123,7 @@ export const CLUB_UNION_MEMBERS: ClubUnionMember[] = [ id: 12, name: '권민준', role: '공연1분과장', - description: '', + description: '많은 학우분들이 공연과 다양한 볼거리를 즐길 수 있도록 노력하겠습니다.', imageSrc: MEMBER_AVATARS.PERFORMANCE, type: 'PERFORMANCE', }, @@ -131,7 +131,7 @@ export const CLUB_UNION_MEMBERS: ClubUnionMember[] = [ id: 13, name: '곽현우', role: '공연2분과장', - description: '', + description: '여러분들의 즐거운 동아리 생활을 위해 열심히 노력하겠습니다.', imageSrc: MEMBER_AVATARS.PERFORMANCE, type: 'PERFORMANCE', }, diff --git a/frontend/src/pages/ClubUnionPage/ClubUnionPage.styles.ts b/frontend/src/pages/ClubUnionPage/ClubUnionPage.styles.ts index e6cf6523c..2f5db26d6 100644 --- a/frontend/src/pages/ClubUnionPage/ClubUnionPage.styles.ts +++ b/frontend/src/pages/ClubUnionPage/ClubUnionPage.styles.ts @@ -196,6 +196,10 @@ export const Description = styled.p` font-size: 0.9rem; line-height: 1.5; margin: 0; + + ${media.tablet} { + display: none; + } `; export const Contact = styled.p` From ec68da956195f0a3ba9412df3468fa36a860090d Mon Sep 17 00:00:00 2001 From: suhyun113 <163711629+suhyun113@users.noreply.github.com> Date: Thu, 26 Mar 2026 22:22:42 +0900 Subject: [PATCH 085/172] =?UTF-8?q?refactor:=20Prettier=20=EC=BD=94?= =?UTF-8?q?=EB=93=9C=20=ED=8F=AC=EB=A7=B7=20=EC=A0=95=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/src/constants/clubUnionInfo.ts | 14 +++++++++----- frontend/src/pages/ClubUnionPage/ClubUnionPage.tsx | 2 +- 2 files changed, 10 insertions(+), 6 deletions(-) diff --git a/frontend/src/constants/clubUnionInfo.ts b/frontend/src/constants/clubUnionInfo.ts index 76f4d41af..563e77263 100644 --- a/frontend/src/constants/clubUnionInfo.ts +++ b/frontend/src/constants/clubUnionInfo.ts @@ -51,7 +51,8 @@ export const CLUB_UNION_MEMBERS: ClubUnionMember[] = [ id: 3, name: '최지현', role: '기획국장', - description: '동아리의 바램이 이루어지도록 질 높은 행사를 만들어가겠습니다.', + description: + '동아리의 바램이 이루어지도록 질 높은 행사를 만들어가겠습니다.', imageSrc: MEMBER_AVATARS.PLANNING, type: 'PLANNING', }, @@ -67,7 +68,8 @@ export const CLUB_UNION_MEMBERS: ClubUnionMember[] = [ id: 5, name: '최동희', role: '홍보국장', - description: '모두가 바라는 대로, 즐거운 동아리 활동에 앞장서 노력하겠습니다.', + description: + '모두가 바라는 대로, 즐거운 동아리 활동에 앞장서 노력하겠습니다.', imageSrc: MEMBER_AVATARS.PROMOTION, type: 'PROMOTION', }, @@ -75,7 +77,8 @@ export const CLUB_UNION_MEMBERS: ClubUnionMember[] = [ id: 6, name: '전호진', role: '봉사분과장', - description: '나눔은 나눌수록 배가 됩니다. 작은 나눔을 통해 삶의 따스함을 느낍시다.', + description: + '나눔은 나눌수록 배가 됩니다. 작은 나눔을 통해 삶의 따스함을 느낍시다.', imageSrc: MEMBER_AVATARS.VOLUNTEER, type: 'VOLUNTEER', }, @@ -103,7 +106,7 @@ export const CLUB_UNION_MEMBERS: ClubUnionMember[] = [ imageSrc: MEMBER_AVATARS.STUDY, type: 'STUDY', }, - { + { id: 10, name: '김민제', role: '운동1분과장', @@ -123,7 +126,8 @@ export const CLUB_UNION_MEMBERS: ClubUnionMember[] = [ id: 12, name: '권민준', role: '공연1분과장', - description: '많은 학우분들이 공연과 다양한 볼거리를 즐길 수 있도록 노력하겠습니다.', + description: + '많은 학우분들이 공연과 다양한 볼거리를 즐길 수 있도록 노력하겠습니다.', imageSrc: MEMBER_AVATARS.PERFORMANCE, type: 'PERFORMANCE', }, diff --git a/frontend/src/pages/ClubUnionPage/ClubUnionPage.tsx b/frontend/src/pages/ClubUnionPage/ClubUnionPage.tsx index c12028294..649241dcc 100644 --- a/frontend/src/pages/ClubUnionPage/ClubUnionPage.tsx +++ b/frontend/src/pages/ClubUnionPage/ClubUnionPage.tsx @@ -6,8 +6,8 @@ import { PAGE_VIEW, USER_EVENT } from '@/constants/eventName'; import useMixpanelTrack from '@/hooks/Mixpanel/useMixpanelTrack'; import useTrackPageView from '@/hooks/Mixpanel/useTrackPageView'; import { PageContainer } from '@/styles/PageContainer.styles'; -import * as Styled from './ClubUnionPage.styles'; import { colors } from '@/styles/theme/colors'; +import * as Styled from './ClubUnionPage.styles'; const MEMBER_COLORS = { PRESIDENT: colors.accent[1][500], From a6113ed8817675b413af4019f981f5ccfd0e32e0 Mon Sep 17 00:00:00 2001 From: seongwon seo Date: Fri, 27 Mar 2026 00:36:48 +0900 Subject: [PATCH 086/172] =?UTF-8?q?feat:=20=EB=8F=99=EC=95=84=EB=A6=AC=20?= =?UTF-8?q?=EC=9D=BC=EC=A0=95=20get=20api=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/src/apis/club.ts | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/frontend/src/apis/club.ts b/frontend/src/apis/club.ts index 259b708b9..9d3171749 100644 --- a/frontend/src/apis/club.ts +++ b/frontend/src/apis/club.ts @@ -1,5 +1,5 @@ import API_BASE_URL from '@/constants/api'; -import { ClubDescription, ClubDetail } from '@/types/club'; +import { ClubCalendarEvent, ClubDescription, ClubDetail } from '@/types/club'; import { secureFetch } from './auth/secureFetch'; import { handleResponse } from './utils/apiHelpers'; @@ -46,6 +46,21 @@ export const getClubList = async ( }; }; +export const getClubCalendarEvents = async ( + clubId: string, +): Promise => { + const response = await fetch(`${API_BASE_URL}/api/club/${clubId}/calendar-events`); + const data = await handleResponse<{ + calendarEvents?: ClubCalendarEvent[]; + }>(response, '동아리 일정 정보를 불러오는데 실패했습니다.'); + + if (!Array.isArray(data?.calendarEvents)) { + return []; + } + + return data.calendarEvents; +}; + export const updateClubDescription = async ( updatedData: ClubDescription, ): Promise => { From 7fd173c64eb94d1a9cc301138a02e54f7374cb14 Mon Sep 17 00:00:00 2001 From: seongwon seo Date: Fri, 27 Mar 2026 00:37:03 +0900 Subject: [PATCH 087/172] =?UTF-8?q?feat:=20=EC=9D=BC=EC=A0=95=EB=B3=B4?= =?UTF-8?q?=EA=B8=B0=20=ED=83=AD=20=ED=81=B4=EB=A6=AD=20=EC=9D=B4=EB=B2=A4?= =?UTF-8?q?=ED=8A=B8=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/src/constants/eventName.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/frontend/src/constants/eventName.ts b/frontend/src/constants/eventName.ts index d14f7b485..d02fc9e6a 100644 --- a/frontend/src/constants/eventName.ts +++ b/frontend/src/constants/eventName.ts @@ -32,6 +32,7 @@ export const USER_EVENT = { CLUB_CARD_CLICKED: 'ClubCard Clicked', CLUB_INTRO_TAB_CLICKED: 'Club Intro Tab Clicked', CLUB_FEED_TAB_CLICKED: 'Club Feed Tab Clicked', + CLUB_SCHEDULE_TAB_CLICKED: 'Club Schedule Tab Clicked', // 동아리 지원 CLUB_APPLY_BUTTON_CLICKED: 'Club Apply Button Clicked', From 81a2d5d7245237669493e4c78856edbe1cefd019 Mon Sep 17 00:00:00 2001 From: seongwon seo Date: Fri, 27 Mar 2026 00:37:25 +0900 Subject: [PATCH 088/172] =?UTF-8?q?feat:=20=EC=BA=98=EB=A6=B0=EB=8D=94=20q?= =?UTF-8?q?uerykey=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/src/constants/queryKeys.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/frontend/src/constants/queryKeys.ts b/frontend/src/constants/queryKeys.ts index 764b0fab9..f6451c989 100644 --- a/frontend/src/constants/queryKeys.ts +++ b/frontend/src/constants/queryKeys.ts @@ -12,6 +12,8 @@ export const queryKeys = { club: { all: ['clubs'] as const, detail: (clubParam: string) => ['clubDetail', clubParam] as const, + calendarEvents: (clubParam: string) => + ['clubCalendarEvents', clubParam] as const, list: ( keyword: string, recruitmentStatus: string, From b2701c890797aed98cd13b88a138c7a4aac592cd Mon Sep 17 00:00:00 2001 From: seongwon seo Date: Fri, 27 Mar 2026 00:37:43 +0900 Subject: [PATCH 089/172] =?UTF-8?q?feat:=20=EC=BA=98=EB=A6=B0=EB=8D=94=20u?= =?UTF-8?q?seQuery=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/src/hooks/Queries/useClub.ts | 25 ++++++++++++++++++++++++- 1 file changed, 24 insertions(+), 1 deletion(-) diff --git a/frontend/src/hooks/Queries/useClub.ts b/frontend/src/hooks/Queries/useClub.ts index 5e57e9dd3..4f584dc87 100644 --- a/frontend/src/hooks/Queries/useClub.ts +++ b/frontend/src/hooks/Queries/useClub.ts @@ -5,13 +5,19 @@ import { useQueryClient, } from '@tanstack/react-query'; import { + getClubCalendarEvents, getClubDetail, getClubList, updateClubDescription, updateClubDetail, } from '@/apis/club'; import { queryKeys } from '@/constants/queryKeys'; -import { ClubDescription, ClubDetail, ClubSearchResponse } from '@/types/club'; +import { + ClubCalendarEvent, + ClubDescription, + ClubDetail, + ClubSearchResponse, +} from '@/types/club'; import convertGoogleDriveUrl from '@/utils/convertGoogleDriveUrl'; interface UseGetCardListProps { @@ -38,6 +44,23 @@ export const useGetClubDetail = (clubParam: string) => { }); }; +export const useGetClubCalendarEvents = (clubParam: string) => { + return useQuery({ + queryKey: queryKeys.club.calendarEvents(clubParam), + queryFn: () => getClubCalendarEvents(clubParam), + staleTime: 60 * 1000, + enabled: !!clubParam, + select: (data) => + data.filter( + (event): event is ClubCalendarEvent => + !!event && + typeof event.id === 'string' && + typeof event.title === 'string' && + typeof event.start === 'string', + ), + }); +}; + export const useGetCardList = ({ keyword, recruitmentStatus, From cf154336ac8763f60ae51117f81af05e062446aa Mon Sep 17 00:00:00 2001 From: seongwon seo Date: Fri, 27 Mar 2026 00:38:22 +0900 Subject: [PATCH 090/172] =?UTF-8?q?feat:=20=EC=BA=98=EB=A6=B0=EB=8D=94=20?= =?UTF-8?q?=EC=9D=B4=EB=B2=A4=ED=8A=B8=20=ED=83=80=EC=9E=85=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/src/types/club.ts | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/frontend/src/types/club.ts b/frontend/src/types/club.ts index 0f32efbbb..70dbf7ef0 100644 --- a/frontend/src/types/club.ts +++ b/frontend/src/types/club.ts @@ -34,6 +34,15 @@ export interface ClubDetail extends Club { externalApplicationUrl?: string; } +export interface ClubCalendarEvent { + id: string; + title: string; + start: string; + end?: string; + url?: string; + description?: string; +} + export interface ClubDescription { id: string; recruitmentStart: string | null; From 3841cd693340da24ace3b8a6b2491fbfed148f66 Mon Sep 17 00:00:00 2001 From: seongwon seo Date: Fri, 27 Mar 2026 00:38:35 +0900 Subject: [PATCH 091/172] =?UTF-8?q?feat:=20=EC=83=81=EC=84=B8=ED=8E=98?= =?UTF-8?q?=EC=9D=B4=EC=A7=80=20=EC=9D=BC=EC=A0=95=EB=B3=B4=EA=B8=B0=20?= =?UTF-8?q?=ED=83=AD=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../pages/ClubDetailPage/ClubDetailPage.tsx | 25 ++++++++++++++++--- 1 file changed, 22 insertions(+), 3 deletions(-) diff --git a/frontend/src/pages/ClubDetailPage/ClubDetailPage.tsx b/frontend/src/pages/ClubDetailPage/ClubDetailPage.tsx index 06dd814c7..4d3a00f39 100644 --- a/frontend/src/pages/ClubDetailPage/ClubDetailPage.tsx +++ b/frontend/src/pages/ClubDetailPage/ClubDetailPage.tsx @@ -6,12 +6,16 @@ import UnderlineTabs from '@/components/common/UnderlineTabs/UnderlineTabs'; import { PAGE_VIEW, USER_EVENT } from '@/constants/eventName'; import useMixpanelTrack from '@/hooks/Mixpanel/useMixpanelTrack'; import useTrackPageView from '@/hooks/Mixpanel/useTrackPageView'; -import { useGetClubDetail } from '@/hooks/Queries/useClub'; +import { + useGetClubCalendarEvents, + useGetClubDetail, +} from '@/hooks/Queries/useClub'; import { useScrollTo } from '@/hooks/Scroll/useScrollTo'; import useDevice from '@/hooks/useDevice'; import ClubFeed from '@/pages/ClubDetailPage/components/ClubFeed/ClubFeed'; import ClubIntroContent from '@/pages/ClubDetailPage/components/ClubIntroContent/ClubIntroContent'; import ClubProfileCard from '@/pages/ClubDetailPage/components/ClubProfileCard/ClubProfileCard'; +import ClubScheduleCalendar from '@/pages/ClubDetailPage/components/ClubScheduleCalendar/ClubScheduleCalendar'; import isInAppWebView from '@/utils/isInAppWebView'; import * as Styled from './ClubDetailPage.styles'; import ClubDetailFooter from './components/ClubDetailFooter/ClubDetailFooter'; @@ -20,11 +24,12 @@ import ClubDetailTopBar from './components/ClubDetailTopBar/ClubDetailTopBar'; export const TAB_TYPE = { INTRO: 'intro', PHOTOS: 'photos', + SCHEDULE: 'schedule', } as const; type TabType = (typeof TAB_TYPE)[keyof typeof TAB_TYPE]; -// 소개내용/활동사진 탭 클릭 시 스크롤이 탑바 하단에 정확히 위치하도록 하는 높이 값 +// 탭 클릭 시 스크롤이 탑바 하단에 정확히 위치하도록 하는 높이 값 const TOP_BAR_HEIGHT = 50; const ClubDetailPage = () => { @@ -48,6 +53,9 @@ const ClubDetailPage = () => { const { data: clubDetail, error } = useGetClubDetail( (clubName ?? clubId) || '', ); + const { data: calendarEvents = [] } = useGetClubCalendarEvents( + (clubName ?? clubId) || '', + ); useTrackPageView(PAGE_VIEW.CLUB_DETAIL_PAGE, clubDetail?.name, !clubDetail); @@ -64,7 +72,9 @@ const ClubDetailPage = () => { trackEvent( tabKey === TAB_TYPE.INTRO ? USER_EVENT.CLUB_INTRO_TAB_CLICKED - : USER_EVENT.CLUB_FEED_TAB_CLICKED, + : tabKey === TAB_TYPE.PHOTOS + ? USER_EVENT.CLUB_FEED_TAB_CLICKED + : USER_EVENT.CLUB_SCHEDULE_TAB_CLICKED, ); }, [setSearchParams, trackEvent], @@ -88,6 +98,7 @@ const ClubDetailPage = () => { tabs={[ { key: TAB_TYPE.INTRO, label: '소개내용' }, { key: TAB_TYPE.PHOTOS, label: '활동사진' }, + { key: TAB_TYPE.SCHEDULE, label: '일정 보기' }, ]} activeTab={activeTab} onTabClick={(tabKey) => { @@ -113,6 +124,7 @@ const ClubDetailPage = () => { tabs={[ { key: TAB_TYPE.INTRO, label: '소개 내용' }, { key: TAB_TYPE.PHOTOS, label: '활동사진' }, + { key: TAB_TYPE.SCHEDULE, label: '일정 보기' }, ]} activeKey={activeTab} onTabClick={(tabKey) => handleTabClick(tabKey as TabType)} @@ -134,6 +146,13 @@ const ClubDetailPage = () => { >
+
+ +
From 0466d499e261513d5ac5329979271ab1f07a9a47 Mon Sep 17 00:00:00 2001 From: suhyun113 <163711629+suhyun113@users.noreply.github.com> Date: Fri, 27 Mar 2026 02:09:49 +0900 Subject: [PATCH 092/172] =?UTF-8?q?fix:=20styled-components=20=EC=BB=A4?= =?UTF-8?q?=EC=8A=A4=ED=85=80=20props=20DOM=20=EC=A0=84=EB=8B=AC=20?= =?UTF-8?q?=EB=AC=B8=EC=A0=9C=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/src/pages/ClubUnionPage/ClubUnionPage.styles.ts | 4 ++-- frontend/src/pages/ClubUnionPage/ClubUnionPage.tsx | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/frontend/src/pages/ClubUnionPage/ClubUnionPage.styles.ts b/frontend/src/pages/ClubUnionPage/ClubUnionPage.styles.ts index 2f5db26d6..530faabaf 100644 --- a/frontend/src/pages/ClubUnionPage/ClubUnionPage.styles.ts +++ b/frontend/src/pages/ClubUnionPage/ClubUnionPage.styles.ts @@ -138,7 +138,7 @@ export const NameBadge = styled.div` } `; -export const ProfileCardContainer = styled.div<{ bgColor: string }>` +export const ProfileCardContainer = styled.div<{ $bgColor: string }>` position: relative; width: 180px; height: 180px; @@ -146,7 +146,7 @@ export const ProfileCardContainer = styled.div<{ bgColor: string }>` overflow: hidden; cursor: pointer; box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1); - background-color: ${({ bgColor }) => bgColor}; + background-color: ${({ $bgColor }) => $bgColor}; ${media.laptop} { width: 160px; diff --git a/frontend/src/pages/ClubUnionPage/ClubUnionPage.tsx b/frontend/src/pages/ClubUnionPage/ClubUnionPage.tsx index 649241dcc..6e8bc3763 100644 --- a/frontend/src/pages/ClubUnionPage/ClubUnionPage.tsx +++ b/frontend/src/pages/ClubUnionPage/ClubUnionPage.tsx @@ -68,7 +68,7 @@ const ClubUnionPage = () => { {CLUB_UNION_MEMBERS.map((member) => ( Date: Fri, 27 Mar 2026 11:09:06 +0900 Subject: [PATCH 093/172] =?UTF-8?q?feat:=20=EB=8F=99=EC=95=84=EB=A6=AC=20?= =?UTF-8?q?=EC=83=81=EC=84=B8=20=ED=8E=98=EC=9D=B4=EC=A7=80=20=EC=9D=BC?= =?UTF-8?q?=EC=A0=95=20=EC=BA=98=EB=A6=B0=EB=8D=94=20=EC=BB=B4=ED=8F=AC?= =?UTF-8?q?=EB=84=8C=ED=8A=B8=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ClubScheduleCalendar.styles.ts | 192 +++++++++++ .../ClubScheduleCalendar.tsx | 297 ++++++++++++++++++ 2 files changed, 489 insertions(+) create mode 100644 frontend/src/pages/ClubDetailPage/components/ClubScheduleCalendar/ClubScheduleCalendar.styles.ts create mode 100644 frontend/src/pages/ClubDetailPage/components/ClubScheduleCalendar/ClubScheduleCalendar.tsx diff --git a/frontend/src/pages/ClubDetailPage/components/ClubScheduleCalendar/ClubScheduleCalendar.styles.ts b/frontend/src/pages/ClubDetailPage/components/ClubScheduleCalendar/ClubScheduleCalendar.styles.ts new file mode 100644 index 000000000..9a1d0e7a3 --- /dev/null +++ b/frontend/src/pages/ClubDetailPage/components/ClubScheduleCalendar/ClubScheduleCalendar.styles.ts @@ -0,0 +1,192 @@ +import styled from 'styled-components'; +import { media } from '@/styles/mediaQuery'; +import { colors } from '@/styles/theme/colors'; + +export const Container = styled.div` + width: 100%; + display: flex; + flex-direction: column; + gap: 16px; +`; + +export const CalendarCard = styled.section` + border-radius: 20px; + border: 1px solid ${colors.gray[200]}; + background-color: ${colors.base.white}; + padding: 16px 14px; +`; + +export const MonthHeader = styled.div` + display: flex; + align-items: center; + justify-content: space-between; + margin-bottom: 14px; +`; + +export const MonthLabel = styled.h3` + font-size: 30px; + line-height: 1; + font-weight: 700; + color: ${colors.gray[900]}; +`; + +export const MonthMoveButton = styled.button` + width: 28px; + height: 28px; + border: none; + border-radius: 999px; + background-color: ${colors.gray[100]}; + color: ${colors.gray[700]}; + font-size: 16px; + cursor: pointer; +`; + +export const WeekdayGrid = styled.div` + display: grid; + grid-template-columns: repeat(7, minmax(0, 1fr)); + gap: 6px; + margin-bottom: 8px; +`; + +export const Weekday = styled.span<{ $dayIndex: number }>` + text-align: center; + font-size: 12px; + font-weight: 700; + color: ${({ $dayIndex }) => + $dayIndex === 0 + ? colors.secondary[1].main + : $dayIndex === 6 + ? colors.secondary[4].main + : colors.gray[700]}; +`; + +export const DayGrid = styled.div` + display: grid; + grid-template-columns: repeat(7, minmax(0, 1fr)); + gap: 6px; +`; + +export const DayCell = styled.button<{ + $isCurrentMonth: boolean; + $isSelected: boolean; +}>` + border: none; + border-radius: 12px; + min-height: 54px; + background-color: ${({ $isSelected }) => + $isSelected ? colors.gray[100] : 'transparent'}; + color: ${({ $isCurrentMonth }) => + $isCurrentMonth ? colors.gray[900] : colors.gray[500]}; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + gap: 4px; + cursor: pointer; + padding: 6px 0; +`; + +export const DayNumber = styled.span<{ $highlightColor?: string }>` + width: 28px; + height: 28px; + border-radius: 999px; + display: inline-flex; + align-items: center; + justify-content: center; + font-size: 16px; + font-weight: 600; + background-color: ${({ $highlightColor }) => + $highlightColor ?? 'transparent'}; + color: ${({ $highlightColor }) => + $highlightColor ? colors.base.white : 'inherit'}; +`; + +export const DotRow = styled.div` + display: flex; + align-items: center; + gap: 4px; + min-height: 6px; +`; + +export const Dot = styled.span<{ $color: string }>` + width: 6px; + height: 6px; + border-radius: 999px; + background-color: ${({ $color }) => $color}; +`; + +export const ScheduleCard = styled.section` + border-radius: 20px; + border: 1px solid ${colors.gray[200]}; + background-color: ${colors.base.white}; + padding: 18px 16px; +`; + +export const SectionTitle = styled.h4` + font-size: 20px; + font-weight: 700; + color: ${colors.gray[900]}; + margin-bottom: 14px; +`; + +export const EventList = styled.div` + display: flex; + flex-direction: column; + gap: 12px; +`; + +export const EventItem = styled.article` + border-radius: 12px; + background-color: ${colors.gray[100]}; + padding: 12px; +`; + +export const EventHeader = styled.div` + display: flex; + align-items: center; + gap: 8px; + margin-bottom: 6px; +`; + +export const EventTitle = styled.p` + font-size: 17px; + font-weight: 700; + color: ${colors.gray[900]}; + overflow-wrap: anywhere; +`; + +export const EventDescription = styled.p` + font-size: 14px; + line-height: 1.45; + color: ${colors.gray[700]}; +`; + +export const EmptyText = styled.p` + font-size: 15px; + color: ${colors.gray[600]}; +`; + +export const EventLink = styled.a` + font-size: 13px; + font-weight: 600; + color: ${colors.gray[800]}; + text-decoration: underline; + text-underline-offset: 3px; +`; + +export const SelectedDateLabel = styled.p` + font-size: 14px; + color: ${colors.gray[700]}; + margin-bottom: 10px; +`; + +export const MobilePanelHint = styled.p` + display: none; + + ${media.tablet} { + display: block; + font-size: 13px; + color: ${colors.gray[600]}; + margin-bottom: 8px; + } +`; diff --git a/frontend/src/pages/ClubDetailPage/components/ClubScheduleCalendar/ClubScheduleCalendar.tsx b/frontend/src/pages/ClubDetailPage/components/ClubScheduleCalendar/ClubScheduleCalendar.tsx new file mode 100644 index 000000000..5007ff17e --- /dev/null +++ b/frontend/src/pages/ClubDetailPage/components/ClubScheduleCalendar/ClubScheduleCalendar.tsx @@ -0,0 +1,297 @@ +import { useMemo, useState } from 'react'; +import type { ClubCalendarEvent } from '@/types/club'; +import { + buildDateKeyFromDate, + buildMonthCalendarDays, + dateFromKey, + formatMonthLabel, + parseDateKey, + WEEKDAY_LABELS, +} from '@/utils/calendarSyncUtils'; +import * as Styled from './ClubScheduleCalendar.styles'; + +const EVENT_COLORS = [ + '#FF7DA4', + '#FFD54A', + '#5FD8C0', + '#7094FF', + '#FFA04D', + '#C379F6', + '#7ED957', + '#4FC3F7', +] as const; + +interface CalendarEventItem { + id: string; + title: string; + description?: string; + url?: string; + dateKey: string; + groupKey: string; +} + +interface ClubScheduleCalendarProps { + events: ClubCalendarEvent[]; +} + +const normalizeEventGroupKey = (title: string) => { + const normalized = title + .toLowerCase() + .replace(/[\d]+/g, '') + .replace(/\([^)]*\)/g, '') + .replace(/[^가-힣a-z\s]/g, ' ') + .replace(/\s+/g, ' ') + .trim(); + + return normalized || title.trim().toLowerCase(); +}; + +const hashText = (value: string) => { + let hash = 0; + for (let index = 0; index < value.length; index += 1) { + hash = (hash * 31 + value.charCodeAt(index)) >>> 0; + } + return hash; +}; + +const buildSeededPalette = (seed: string) => { + const palette = [...EVENT_COLORS]; + let hash = hashText(seed) || 1; + + for (let index = palette.length - 1; index > 0; index -= 1) { + hash = (hash * 1664525 + 1013904223) >>> 0; + const swapIndex = hash % (index + 1); + [palette[index], palette[swapIndex]] = [palette[swapIndex], palette[index]]; + } + + return palette; +}; + +const formatSelectedDate = (dateKey: string) => { + const date = dateFromKey(dateKey); + return `${date.getMonth() + 1}월 ${date.getDate()}일`; +}; + +const ClubScheduleCalendar = ({ events }: ClubScheduleCalendarProps) => { + const parsedEvents = useMemo(() => { + const normalizedEvents = events.flatMap((event) => { + const dateKey = parseDateKey(event.start); + if (!dateKey) return []; + + return { + id: event.id, + title: event.title, + description: event.description, + url: event.url, + dateKey, + groupKey: normalizeEventGroupKey(event.title), + } satisfies CalendarEventItem; + }); + + return normalizedEvents.sort((a, b) => a.dateKey.localeCompare(b.dateKey)); + }, [events]); + + const firstEventMonth = parsedEvents[0] + ? dateFromKey(parsedEvents[0].dateKey) + : new Date(); + const [visibleMonth, setVisibleMonth] = useState( + new Date(firstEventMonth.getFullYear(), firstEventMonth.getMonth(), 1), + ); + + const monthKey = `${visibleMonth.getFullYear()}-${visibleMonth.getMonth() + 1}`; + + const monthEvents = useMemo(() => { + return parsedEvents.filter((event) => { + const date = dateFromKey(event.dateKey); + return ( + date.getFullYear() === visibleMonth.getFullYear() && + date.getMonth() === visibleMonth.getMonth() + ); + }); + }, [parsedEvents, visibleMonth]); + + const colorByGroup = useMemo(() => { + const groups = Array.from( + new Set(monthEvents.map((event) => event.groupKey)), + ); + const palette = buildSeededPalette(monthKey); + return groups.reduce>( + (accumulator, group, index) => { + accumulator[group] = palette[index % palette.length]; + return accumulator; + }, + {}, + ); + }, [monthEvents, monthKey]); + + const eventsByDate = useMemo(() => { + return monthEvents.reduce>( + (accumulator, event) => { + if (!accumulator[event.dateKey]) { + accumulator[event.dateKey] = []; + } + accumulator[event.dateKey].push(event); + return accumulator; + }, + {}, + ); + }, [monthEvents]); + + const [selectedDateKey, setSelectedDateKey] = useState(() => { + if (parsedEvents[0]) return parsedEvents[0].dateKey; + return buildDateKeyFromDate(new Date()); + }); + + const calendarDays = useMemo( + () => buildMonthCalendarDays(visibleMonth), + [visibleMonth], + ); + + const selectedEvents = eventsByDate[selectedDateKey] ?? []; + + const changeMonth = (diff: number) => { + const next = new Date( + visibleMonth.getFullYear(), + visibleMonth.getMonth() + diff, + 1, + ); + setVisibleMonth(next); + + const firstDayKey = buildDateKeyFromDate(next); + const firstEventInMonth = parsedEvents.find((event) => { + const date = dateFromKey(event.dateKey); + return ( + date.getFullYear() === next.getFullYear() && + date.getMonth() === next.getMonth() + ); + }); + setSelectedDateKey(firstEventInMonth?.dateKey ?? firstDayKey); + }; + + if (parsedEvents.length === 0) { + return ( + + 일정 + 등록된 행사 일정이 없습니다. + + ); + } + + return ( + + + + changeMonth(-1)} + > + ◀ + + + {formatMonthLabel(visibleMonth)} + + changeMonth(1)} + > + ▶ + + + + + {WEEKDAY_LABELS.map((day, dayIndex) => ( + + {day} + + ))} + + + + {calendarDays.map((day) => { + const dateKey = buildDateKeyFromDate(day); + const dayEvents = eventsByDate[dateKey] ?? []; + const firstColor = dayEvents[0] + ? colorByGroup[dayEvents[0].groupKey] + : undefined; + const isCurrentMonth = day.getMonth() === visibleMonth.getMonth(); + const uniqueColors = Array.from( + new Set( + dayEvents + .map((event) => colorByGroup[event.groupKey]) + .filter((color): color is string => !!color), + ), + ); + + return ( + setSelectedDateKey(dateKey)} + > + + {day.getDate()} + + + {uniqueColors.slice(0, 3).map((color) => ( + + ))} + + + ); + })} + + + + + 일정 + + {formatSelectedDate(selectedDateKey)} + + + 같은 이름 계열의 이벤트는 같은 색으로 표시됩니다. + + + {selectedEvents.length === 0 ? ( + + 선택한 날짜에 등록된 일정이 없습니다. + + ) : ( + + {selectedEvents.map((event) => { + const eventColor = + colorByGroup[event.groupKey] ?? EVENT_COLORS[0]; + return ( + + + + {event.title} + + {event.description && ( + + {event.description} + + )} + {event.url && ( + + 일정 상세 보기 + + )} + + ); + })} + + )} + + + ); +}; + +export default ClubScheduleCalendar; From b2aca7baf9586f2ed622ec154c6cc45754944467 Mon Sep 17 00:00:00 2001 From: seongwon seo Date: Fri, 27 Mar 2026 11:11:23 +0900 Subject: [PATCH 094/172] fix: lint error --- frontend/src/apis/club.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/frontend/src/apis/club.ts b/frontend/src/apis/club.ts index 9d3171749..f4085bcf6 100644 --- a/frontend/src/apis/club.ts +++ b/frontend/src/apis/club.ts @@ -49,7 +49,9 @@ export const getClubList = async ( export const getClubCalendarEvents = async ( clubId: string, ): Promise => { - const response = await fetch(`${API_BASE_URL}/api/club/${clubId}/calendar-events`); + const response = await fetch( + `${API_BASE_URL}/api/club/${clubId}/calendar-events`, + ); const data = await handleResponse<{ calendarEvents?: ClubCalendarEvent[]; }>(response, '동아리 일정 정보를 불러오는데 실패했습니다.'); From b887d97870d27f5939ab5360d3208b34d5ed4bbc Mon Sep 17 00:00:00 2001 From: seongwon seo Date: Sat, 28 Mar 2026 20:38:01 +0900 Subject: [PATCH 095/172] =?UTF-8?q?refactor:=20Google/Notion=20=ED=83=80?= =?UTF-8?q?=EC=9E=85=EC=9D=84=20=EB=B3=84=EB=8F=84=20=ED=8C=8C=EC=9D=BC?= =?UTF-8?q?=EB=A1=9C=20=EB=B6=84=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - src/types/google.ts: GoogleCalendarItem, GoogleEventItem - src/types/notion.ts: NotionSearchItem, NotionDatabaseOption, NotionPagesResponse - calendarOAuth.ts에서 re-export하여 기존 import 호환성 유지 --- frontend/src/apis/calendarOAuth.ts | 46 ++++++------------------------ frontend/src/types/google.ts | 19 ++++++++++++ frontend/src/types/notion.ts | 18 ++++++++++++ 3 files changed, 45 insertions(+), 38 deletions(-) create mode 100644 frontend/src/types/google.ts create mode 100644 frontend/src/types/notion.ts diff --git a/frontend/src/apis/calendarOAuth.ts b/frontend/src/apis/calendarOAuth.ts index a8e1c6f4c..cb103a56a 100644 --- a/frontend/src/apis/calendarOAuth.ts +++ b/frontend/src/apis/calendarOAuth.ts @@ -1,39 +1,15 @@ import API_BASE_URL from '@/constants/api'; +import type { GoogleCalendarItem, GoogleEventItem } from '@/types/google'; +import type { + NotionDatabaseOption, + NotionPagesResponse, + NotionSearchItem, +} from '@/types/notion'; import { secureFetch } from './auth/secureFetch'; import { handleResponse } from './utils/apiHelpers'; -export interface GoogleCalendarItem { - id: string; - summary: string; - primary?: boolean; -} - -export interface GoogleEventItem { - id: string; - summary?: string; - htmlLink?: string; - start?: { - dateTime?: string; - date?: string; - }; - end?: { - dateTime?: string; - date?: string; - }; -} - -export interface NotionSearchItem { - id: string; - object: string; - url?: string; - last_edited_time?: string; - properties?: Record; -} - -export interface NotionDatabaseOption { - id: string; - title: string; -} +export type { GoogleCalendarItem, GoogleEventItem }; +export type { NotionSearchItem, NotionDatabaseOption, NotionPagesResponse }; export const fetchGoogleCalendarList = async (accessToken: string) => { const response = await fetch( @@ -107,12 +83,6 @@ interface NotionDatabasePayload { title?: Array<{ plain_text?: string }>; } -export interface NotionPagesResponse { - items: NotionSearchItem[]; - totalResults: number; - databaseId?: string; -} - export const fetchNotionAuthorizeUrl = async (state?: string) => { const params = new URLSearchParams(); if (state) { diff --git a/frontend/src/types/google.ts b/frontend/src/types/google.ts new file mode 100644 index 000000000..0fd3d29dc --- /dev/null +++ b/frontend/src/types/google.ts @@ -0,0 +1,19 @@ +export interface GoogleCalendarItem { + id: string; + summary: string; + primary?: boolean; +} + +export interface GoogleEventItem { + id: string; + summary?: string; + htmlLink?: string; + start?: { + dateTime?: string; + date?: string; + }; + end?: { + dateTime?: string; + date?: string; + }; +} diff --git a/frontend/src/types/notion.ts b/frontend/src/types/notion.ts new file mode 100644 index 000000000..6b86fd70b --- /dev/null +++ b/frontend/src/types/notion.ts @@ -0,0 +1,18 @@ +export interface NotionSearchItem { + id: string; + object: string; + url?: string; + last_edited_time?: string; + properties?: Record; +} + +export interface NotionDatabaseOption { + id: string; + title: string; +} + +export interface NotionPagesResponse { + items: NotionSearchItem[]; + totalResults: number; + databaseId?: string; +} From 195ecd9d7b668d1c6f76a0c99da3bda979d7ddc6 Mon Sep 17 00:00:00 2001 From: seongwon seo Date: Sat, 28 Mar 2026 20:41:10 +0900 Subject: [PATCH 096/172] =?UTF-8?q?fix:=20clearError=EB=A5=BC=20useCallbac?= =?UTF-8?q?k=EC=9C=BC=EB=A1=9C=20=EA=B0=90=EC=8B=B8=EC=84=9C=20=EC=B0=B8?= =?UTF-8?q?=EC=A1=B0=EA=B0=80=20=EC=9C=A0=EC=A7=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../AdminPage/tabs/CalendarSyncTab/hooks/useCalendarSync.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/frontend/src/pages/AdminPage/tabs/CalendarSyncTab/hooks/useCalendarSync.ts b/frontend/src/pages/AdminPage/tabs/CalendarSyncTab/hooks/useCalendarSync.ts index 6fabef4d6..eb4f2e989 100644 --- a/frontend/src/pages/AdminPage/tabs/CalendarSyncTab/hooks/useCalendarSync.ts +++ b/frontend/src/pages/AdminPage/tabs/CalendarSyncTab/hooks/useCalendarSync.ts @@ -1,4 +1,4 @@ -import { useState } from 'react'; +import { useCallback, useState } from 'react'; import { useGoogleCalendarData } from './useGoogleCalendarData'; import { useNotionCalendarData } from './useNotionCalendarData'; import { useNotionCalendarUiState } from './useNotionCalendarUiState'; @@ -9,7 +9,7 @@ export const useCalendarSync = () => { const [errorMessage, setErrorMessage] = useState(''); const [notionWorkspaceName, setNotionWorkspaceName] = useState(''); - const clearError = () => setErrorMessage(''); + const clearError = useCallback(() => setErrorMessage(''), []); const google = useGoogleCalendarData({ onError: setErrorMessage, From 71ee3ceda7d8c0df104014924ed3892d6c9bfda9 Mon Sep 17 00:00:00 2001 From: seongwon seo Date: Sat, 28 Mar 2026 20:43:35 +0900 Subject: [PATCH 097/172] =?UTF-8?q?fix:=20OAuth=20=EC=99=84=EB=A3=8C=20?= =?UTF-8?q?=ED=9B=84=20sessionStorage=20state=20=EC=A0=95=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - OAuth 에러 시 sessionStorage에서 state 제거 - OAuth 완료 시 finally에서 state 제거 - stale state로 인한 예기치 않은 동작 방지 --- .../AdminPage/tabs/CalendarSyncTab/hooks/useNotionOAuth.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/frontend/src/pages/AdminPage/tabs/CalendarSyncTab/hooks/useNotionOAuth.ts b/frontend/src/pages/AdminPage/tabs/CalendarSyncTab/hooks/useNotionOAuth.ts index 4c65577c9..7b764bddd 100644 --- a/frontend/src/pages/AdminPage/tabs/CalendarSyncTab/hooks/useNotionOAuth.ts +++ b/frontend/src/pages/AdminPage/tabs/CalendarSyncTab/hooks/useNotionOAuth.ts @@ -57,6 +57,7 @@ export const useNotionOAuth = ({ if (error) { onError(`Notion OAuth 실패: ${error}`); + sessionStorage.removeItem(NOTION_STATE_KEY); clearOAuthParamsFromUrl(); return; } @@ -84,6 +85,7 @@ export const useNotionOAuth = ({ }) .finally(() => { setIsNotionOAuthLoading(false); + sessionStorage.removeItem(NOTION_STATE_KEY); clearOAuthParamsFromUrl(); }); }, [clearError, loadNotionPages, onError, onStatus, onWorkspaceName]); From 0f2377e0211b76e4d2f52f459b6953597e98b5e2 Mon Sep 17 00:00:00 2001 From: seongwon seo Date: Sat, 28 Mar 2026 20:49:57 +0900 Subject: [PATCH 098/172] =?UTF-8?q?fix:=20parseDateKey=20datetime=20?= =?UTF-8?q?=ED=83=80=EC=9E=84=EC=A1=B4=20=EC=B2=98=EB=A6=AC=20=EA=B0=9C?= =?UTF-8?q?=EC=84=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 순수 날짜(YYYY-MM-DD)만 그대로 반환하도록 정규식 수정 - datetime 문자열은 UTC 기준으로 파싱하여 날짜 추출 - 타임존에 따른 날짜 밀림 문제 방지 --- frontend/src/utils/calendarSyncUtils.ts | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/frontend/src/utils/calendarSyncUtils.ts b/frontend/src/utils/calendarSyncUtils.ts index 4193a3904..be94e9705 100644 --- a/frontend/src/utils/calendarSyncUtils.ts +++ b/frontend/src/utils/calendarSyncUtils.ts @@ -7,7 +7,6 @@ import type { NotionSearchItem } from '@/apis/calendarOAuth'; * - Notion page -> 캘린더 이벤트 변환 유틸 */ -/** 캘린더 헤더 요일 라벨(일~토). */ export const WEEKDAY_LABELS = [ '일', '월', @@ -51,18 +50,20 @@ export const formatDateText = (dateText?: string) => { /** * 다양한 날짜 문자열을 `YYYY-MM-DD` 키로 정규화한다. + * - 순수 날짜 문자열(YYYY-MM-DD)은 그대로 반환 + * - datetime 문자열은 UTC 기준으로 파싱하여 날짜 추출 * 유효하지 않은 값이면 null을 반환한다. */ export const parseDateKey = (dateText: string) => { - const datePart = dateText.match(/^(\d{4}-\d{2}-\d{2})/); - if (datePart) { - return datePart[1]; + // 순수 날짜 형식(시간 없음)만 그대로 반환 - 종일 이벤트 + if (/^\d{4}-\d{2}-\d{2}$/.test(dateText)) { + return dateText; } + // datetime 형식은 UTC 기준으로 파싱 const parsed = new Date(dateText); if (Number.isNaN(parsed.getTime())) return null; - // 문자열에 날짜 파트가 없을 때도 시간대 영향 없이 같은 UTC 날짜 키를 유지한다. const utcYear = parsed.getUTCFullYear(); const utcMonth = String(parsed.getUTCMonth() + 1).padStart(2, '0'); const utcDay = String(parsed.getUTCDate()).padStart(2, '0'); From ab0a3f08e3982119ac338b34b665aaa86c6fe780 Mon Sep 17 00:00:00 2001 From: seongwon seo Date: Sat, 28 Mar 2026 20:51:43 +0900 Subject: [PATCH 099/172] =?UTF-8?q?fix:=20Google=20OAuth=20state=20?= =?UTF-8?q?=EA=B2=80=EC=A6=9D=20=ED=9B=84=20sessionStorage=20=EC=A0=95?= =?UTF-8?q?=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - OAuth 성공 시 GOOGLE_STATE_KEY 즉시 제거 - replay 공격 방지 및 fresh state 보장 --- .../tabs/CalendarSyncTab/hooks/useGoogleCalendarData.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/frontend/src/pages/AdminPage/tabs/CalendarSyncTab/hooks/useGoogleCalendarData.ts b/frontend/src/pages/AdminPage/tabs/CalendarSyncTab/hooks/useGoogleCalendarData.ts index c4fa92f5a..b877dd379 100644 --- a/frontend/src/pages/AdminPage/tabs/CalendarSyncTab/hooks/useGoogleCalendarData.ts +++ b/frontend/src/pages/AdminPage/tabs/CalendarSyncTab/hooks/useGoogleCalendarData.ts @@ -76,6 +76,7 @@ export const useGoogleCalendarData = ({ const expectedState = sessionStorage.getItem(GOOGLE_STATE_KEY); if (token && state && expectedState && state === expectedState) { + sessionStorage.removeItem(GOOGLE_STATE_KEY); setGoogleToken(token); sessionStorage.setItem(GOOGLE_TOKEN_KEY, token); onStatus('Google OAuth 인증이 완료되었습니다.'); From 12982cdf528814bbf6c62957946f5f572d8c1bfd Mon Sep 17 00:00:00 2001 From: seongwon seo Date: Sat, 28 Mar 2026 22:19:37 +0900 Subject: [PATCH 100/172] =?UTF-8?q?feat:=20=EC=BA=98=EB=A6=B0=EB=8D=94=20?= =?UTF-8?q?=EB=8D=B0=EC=9D=B4=ED=84=B0=20=EC=97=86=EC=9C=BC=EB=A9=B4=20?= =?UTF-8?q?=EC=9D=BC=EC=A0=95=20=ED=83=AD=20=EC=88=A8=EA=B9=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - hasCalendarEvents 플래그로 캘린더 이벤트 유무 확인 - tabs, topBarTabs를 useMemo로 조건부 생성 - 일정 탭 컨텐츠도 조건부 렌더링 --- .../pages/ClubDetailPage/ClubDetailPage.tsx | 56 +++++++++++++------ 1 file changed, 38 insertions(+), 18 deletions(-) diff --git a/frontend/src/pages/ClubDetailPage/ClubDetailPage.tsx b/frontend/src/pages/ClubDetailPage/ClubDetailPage.tsx index 4d3a00f39..1f736460c 100644 --- a/frontend/src/pages/ClubDetailPage/ClubDetailPage.tsx +++ b/frontend/src/pages/ClubDetailPage/ClubDetailPage.tsx @@ -1,4 +1,4 @@ -import { useCallback, useRef } from 'react'; +import { useCallback, useMemo, useRef } from 'react'; import { useParams, useSearchParams } from 'react-router-dom'; import Footer from '@/components/common/Footer/Footer'; import Header from '@/components/common/Header/Header'; @@ -57,6 +57,32 @@ const ClubDetailPage = () => { (clubName ?? clubId) || '', ); + const hasCalendarEvents = calendarEvents.length > 0; + + const tabs = useMemo( + () => + [ + { key: TAB_TYPE.INTRO, label: '소개 내용' }, + { key: TAB_TYPE.PHOTOS, label: '활동사진' }, + hasCalendarEvents + ? { key: TAB_TYPE.SCHEDULE, label: '일정 보기' } + : null, + ].filter(Boolean) as Array<{ key: TabType; label: string }>, + [hasCalendarEvents], + ); + + const topBarTabs = useMemo( + () => + [ + { key: TAB_TYPE.INTRO, label: '소개내용' }, + { key: TAB_TYPE.PHOTOS, label: '활동사진' }, + hasCalendarEvents + ? { key: TAB_TYPE.SCHEDULE, label: '일정 보기' } + : null, + ].filter(Boolean) as Array<{ key: TabType; label: string }>, + [hasCalendarEvents], + ); + useTrackPageView(PAGE_VIEW.CLUB_DETAIL_PAGE, clubDetail?.name, !clubDetail); const contentRef = useRef(null); @@ -95,11 +121,7 @@ const ClubDetailPage = () => { { handleTabClick(tabKey as TabType); @@ -121,11 +143,7 @@ const ClubDetailPage = () => { handleTabClick(tabKey as TabType)} centerOnMobile @@ -146,13 +164,15 @@ const ClubDetailPage = () => { > -
- -
+ {hasCalendarEvents && ( +
+ +
+ )}
From 6f045007af6b021f551abfbd49e352e0ee9dadd3 Mon Sep 17 00:00:00 2001 From: seongwon seo Date: Sun, 29 Mar 2026 00:58:40 +0900 Subject: [PATCH 101/172] =?UTF-8?q?feat:=20=EC=BA=98=EB=A6=B0=EB=8D=94=20?= =?UTF-8?q?=EB=8D=B0=EC=9D=B4=ED=84=B0=20lazy=20loading=20=EB=B0=8F=20?= =?UTF-8?q?=EC=BA=90=EC=8B=B1=20=EC=B5=9C=EC=A0=81=ED=99=94?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - hasCalendarEvents 플래그로 탭 표시 여부 결정 - 일정 탭 클릭 시에만 캘린더 API 호출 (lazy loading) - staleTime 1분 → 5분으로 증가하여 불필요한 재요청 방지 --- frontend/src/hooks/Queries/useClub.ts | 9 ++++++--- frontend/src/pages/ClubDetailPage/ClubDetailPage.tsx | 6 ++++-- frontend/src/types/club.ts | 1 + 3 files changed, 11 insertions(+), 5 deletions(-) diff --git a/frontend/src/hooks/Queries/useClub.ts b/frontend/src/hooks/Queries/useClub.ts index 4f584dc87..976015018 100644 --- a/frontend/src/hooks/Queries/useClub.ts +++ b/frontend/src/hooks/Queries/useClub.ts @@ -44,12 +44,15 @@ export const useGetClubDetail = (clubParam: string) => { }); }; -export const useGetClubCalendarEvents = (clubParam: string) => { +export const useGetClubCalendarEvents = ( + clubParam: string, + options?: { enabled?: boolean }, +) => { return useQuery({ queryKey: queryKeys.club.calendarEvents(clubParam), queryFn: () => getClubCalendarEvents(clubParam), - staleTime: 60 * 1000, - enabled: !!clubParam, + staleTime: 5 * 60 * 1000, + enabled: (options?.enabled ?? true) && !!clubParam, select: (data) => data.filter( (event): event is ClubCalendarEvent => diff --git a/frontend/src/pages/ClubDetailPage/ClubDetailPage.tsx b/frontend/src/pages/ClubDetailPage/ClubDetailPage.tsx index 1f736460c..5e880f21d 100644 --- a/frontend/src/pages/ClubDetailPage/ClubDetailPage.tsx +++ b/frontend/src/pages/ClubDetailPage/ClubDetailPage.tsx @@ -53,12 +53,14 @@ const ClubDetailPage = () => { const { data: clubDetail, error } = useGetClubDetail( (clubName ?? clubId) || '', ); + + const hasCalendarEvents = clubDetail?.hasCalendarEvents ?? false; + const { data: calendarEvents = [] } = useGetClubCalendarEvents( (clubName ?? clubId) || '', + { enabled: hasCalendarEvents && activeTab === TAB_TYPE.SCHEDULE }, ); - const hasCalendarEvents = calendarEvents.length > 0; - const tabs = useMemo( () => [ diff --git a/frontend/src/types/club.ts b/frontend/src/types/club.ts index 70dbf7ef0..f6f93a4e3 100644 --- a/frontend/src/types/club.ts +++ b/frontend/src/types/club.ts @@ -32,6 +32,7 @@ export interface ClubDetail extends Club { socialLinks: Record; externalApplicationUrl?: string; + hasCalendarEvents?: boolean; } export interface ClubCalendarEvent { From a1dbfcd4da29f6f5e2dd50b5d0a63d1d125fcfdf Mon Sep 17 00:00:00 2001 From: seongwon seo Date: Sun, 29 Mar 2026 16:28:41 +0900 Subject: [PATCH 102/172] =?UTF-8?q?feat:=20=EC=83=81=EC=84=B8=ED=8E=98?= =?UTF-8?q?=EC=9D=B4=EC=A7=80=20=EB=B0=A9=EB=AC=B8=20=EC=8B=9C=20=EB=AA=A8?= =?UTF-8?q?=EC=A7=91=20=EC=83=81=ED=83=9C=20Mixpanel=20=EB=A1=9C=EA=B9=85?= =?UTF-8?q?=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 4.5 --- frontend/src/hooks/Mixpanel/useTrackPageView.ts | 7 ++++++- frontend/src/pages/ClubDetailPage/ClubDetailPage.tsx | 7 ++++++- 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/frontend/src/hooks/Mixpanel/useTrackPageView.ts b/frontend/src/hooks/Mixpanel/useTrackPageView.ts index 8e2308ed9..969d67708 100644 --- a/frontend/src/hooks/Mixpanel/useTrackPageView.ts +++ b/frontend/src/hooks/Mixpanel/useTrackPageView.ts @@ -6,14 +6,17 @@ const useTrackPageView = ( pageName: string, clubName?: string, skip: boolean = false, + recruitmentStatus?: string, ) => { const location = useLocation(); const isTracked = useRef(false); const startTime = useRef(Date.now()); const clubNameRef = useRef(clubName); + const recruitmentStatusRef = useRef(recruitmentStatus); useEffect(() => { clubNameRef.current = clubName; + recruitmentStatusRef.current = recruitmentStatus; if (skip) return; @@ -25,6 +28,7 @@ const useTrackPageView = ( timestamp: startTime.current, referrer: document.referrer || 'direct', clubName: clubNameRef.current, + recruitmentStatus: recruitmentStatusRef.current, }); const trackPageDuration = () => { @@ -37,6 +41,7 @@ const useTrackPageView = ( duration: duration, duration_seconds: Math.round(duration / 1000), clubName: clubNameRef.current, + recruitmentStatus: recruitmentStatusRef.current, }); }; @@ -54,7 +59,7 @@ const useTrackPageView = ( window.removeEventListener('beforeunload', trackPageDuration); document.removeEventListener('visibilitychange', handleVisibilityChange); }; - }, [location.pathname, clubName, skip, pageName]); + }, [location.pathname, clubName, skip, pageName, recruitmentStatus]); }; export default useTrackPageView; diff --git a/frontend/src/pages/ClubDetailPage/ClubDetailPage.tsx b/frontend/src/pages/ClubDetailPage/ClubDetailPage.tsx index 5e880f21d..85b2d347a 100644 --- a/frontend/src/pages/ClubDetailPage/ClubDetailPage.tsx +++ b/frontend/src/pages/ClubDetailPage/ClubDetailPage.tsx @@ -85,7 +85,12 @@ const ClubDetailPage = () => { [hasCalendarEvents], ); - useTrackPageView(PAGE_VIEW.CLUB_DETAIL_PAGE, clubDetail?.name, !clubDetail); + useTrackPageView( + PAGE_VIEW.CLUB_DETAIL_PAGE, + clubDetail?.name, + !clubDetail, + clubDetail?.recruitmentStatus, + ); const contentRef = useRef(null); const { scrollToElement } = useScrollTo(); From 726254522361772a286b45a787744927f4361326 Mon Sep 17 00:00:00 2001 From: seongwon seo Date: Sun, 29 Mar 2026 16:29:07 +0900 Subject: [PATCH 103/172] =?UTF-8?q?chore:=20=EB=B6=88=ED=95=84=EC=9A=94=20?= =?UTF-8?q?=ED=8C=8C=EC=9D=BC=EC=A0=9C=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/.claude/commands/tm/auto-implement-tasks.md | 1 - 1 file changed, 1 deletion(-) delete mode 100644 frontend/.claude/commands/tm/auto-implement-tasks.md diff --git a/frontend/.claude/commands/tm/auto-implement-tasks.md b/frontend/.claude/commands/tm/auto-implement-tasks.md deleted file mode 100644 index 42d99226f..000000000 --- a/frontend/.claude/commands/tm/auto-implement-tasks.md +++ /dev/null @@ -1 +0,0 @@ -- Assess test coverage needs From d1cfef2c9b346423f3450f4e6acb23efc6c35eac Mon Sep 17 00:00:00 2001 From: seongwon seo Date: Sun, 29 Mar 2026 16:37:39 +0900 Subject: [PATCH 104/172] =?UTF-8?q?fix:=20recruitmentStatus=EB=A5=BC=20?= =?UTF-8?q?=EB=B3=84=EB=8F=84=20effect=EB=A1=9C=20=EB=B6=84=EB=A6=AC?= =?UTF-8?q?=ED=95=98=EC=97=AC=20=EC=A4=91=EB=B3=B5=20=EC=9D=B4=EB=B2=A4?= =?UTF-8?q?=ED=8A=B8=20=EB=B0=A9=EC=A7=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 4.5 --- frontend/src/hooks/Mixpanel/useTrackPageView.ts | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/frontend/src/hooks/Mixpanel/useTrackPageView.ts b/frontend/src/hooks/Mixpanel/useTrackPageView.ts index 969d67708..23351fc7c 100644 --- a/frontend/src/hooks/Mixpanel/useTrackPageView.ts +++ b/frontend/src/hooks/Mixpanel/useTrackPageView.ts @@ -14,9 +14,13 @@ const useTrackPageView = ( const clubNameRef = useRef(clubName); const recruitmentStatusRef = useRef(recruitmentStatus); + // ref 동기화는 별도 effect에서 처리 (방문 이벤트 중복 방지) useEffect(() => { - clubNameRef.current = clubName; recruitmentStatusRef.current = recruitmentStatus; + }, [recruitmentStatus]); + + useEffect(() => { + clubNameRef.current = clubName; if (skip) return; @@ -59,7 +63,7 @@ const useTrackPageView = ( window.removeEventListener('beforeunload', trackPageDuration); document.removeEventListener('visibilitychange', handleVisibilityChange); }; - }, [location.pathname, clubName, skip, pageName, recruitmentStatus]); + }, [location.pathname, clubName, skip, pageName]); }; export default useTrackPageView; From 1a9126dac2277257367be9bf9ef621b19b1a9a41 Mon Sep 17 00:00:00 2001 From: seongwon seo Date: Sun, 29 Mar 2026 17:05:32 +0900 Subject: [PATCH 105/172] =?UTF-8?q?chore:=20commit=20=EC=BB=A4=EB=A7=A8?= =?UTF-8?q?=EB=93=9C=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/.claude/commands/commit.md | 149 ++++++++++++++++++++++++++++ 1 file changed, 149 insertions(+) create mode 100644 frontend/.claude/commands/commit.md diff --git a/frontend/.claude/commands/commit.md b/frontend/.claude/commands/commit.md new file mode 100644 index 000000000..3a484b065 --- /dev/null +++ b/frontend/.claude/commands/commit.md @@ -0,0 +1,149 @@ +--- +description: 세션 작업 기록 + 기능 문서화 + 변경 내용 커밋 +allowed-tools: Bash(mkdir:*), Bash(ls:*), Bash(date:*), Bash(git status), Bash(git diff*), Bash(git log*), Bash(git add *), Bash(git commit *), Read, Write, Edit, Glob, Grep +--- + +# 작업 지시 + +현재 세션의 작업 내용을 기록하고, 기능별 문서를 자동 생성한 뒤, 변경 파일을 커밋합니다. + +--- + +## Phase 1: 세션 기록 + +먼저 세션 작업 내용을 dailyNote에 기록합니다. + +1. `dailyNote/` 폴더가 없으면 생성 +2. 오늘 날짜 파일 확인 (형식: `YYYY-MM-DD.md`) +3. 파일이 없으면 새로 생성, 있으면 기존 내용 뒤에 `---` 구분선 추가 후 이어서 작성 + +**기록할 내용:** + +- 세션에서 논의한 내용 +- 고민한 흔적과 의사결정 과정 (Decision Log) +- 트러블슈팅 경험 +- 새로 배운 기술이나 발견한 것들 + +**기록 형식 예시:** + +```markdown +## [14:30] 세션 요약 + +### 논의 내용 + +- React Query의 stale time 설정에 대해 고민 + +### Decision Log + +- useEffect 의존성 배열에서 함수 참조 문제 해결을 위해 useCallback 적용 결정 + +### 트러블슈팅 + +- MSW 핸들러에서 응답 지연 시 스토리북 렌더링 이슈 발견 → delay 시간 조정으로 해결 +``` + +--- + +## Phase 2: 기능 문서화 (선택적) + +세션에서 새로운 기능을 구현하거나 중요한 변경이 있었다면 문서화합니다. + +### 기능 매핑 테이블 + +| 기능 | 문서 경로 | 키워드 | +| ------------------- | ---------------------------------- | ----------------------------------------------------------------------------------------- | +| Main Page | `docs/features/main/` | MainPage, ClubCard, Filter, Banner, CategoryButton, SearchBox, Popup | +| Club Detail | `docs/features/club-detail/` | ClubDetailPage, ClubFeed, ClubProfileCard, ShareButton, ApplyButton, ClubScheduleCalendar | +| Admin - Info | `docs/features/admin/info/` | ClubInfoEditTab, MakeTags, SelectTags | +| Admin - Intro | `docs/features/admin/intro/` | ClubIntroEditTab, AwardEditor, FAQEditor | +| Admin - Photo | `docs/features/admin/photo/` | PhotoEditTab, ImagePreview, ClubLogoEditor, ClubCoverEditor | +| Admin - Recruit | `docs/features/admin/recruit/` | RecruitEditTab, DateTimeRangePicker, MarkdownEditor | +| Admin - Application | `docs/features/admin/application/` | ApplicationEditTab, QuestionBuilder, ApplicationListTab | +| Admin - Applicants | `docs/features/admin/applicants/` | ApplicantsTab, ApplicantDetailPage, ApplicantsListTab | +| Admin - Calendar | `docs/features/admin/calendar/` | CalendarSyncTab, calendarOAuth | +| Admin - Account | `docs/features/admin/account/` | AccountEditTab, LoginTab, PrivateRoute | +| Application Form | `docs/features/application-form/` | ApplicationFormPage, QuestionAnswerer, QuestionContainer | +| Auth | `docs/features/auth/` | auth, secureFetch, refreshAccessToken, JWT | +| API Layer | `docs/features/api/` | apis, apiHelpers, handleResponse, withErrorHandling | +| Hooks | `docs/features/hooks/` | useClub, useApplication, useApplicants, React Query | +| Store | `docs/features/store/` | Zustand, useCategoryStore, useSearchStore | +| Components | `docs/features/components/` | 공용 컴포넌트 | +| Utils | `docs/features/utils/` | debounce, validateSocialLink, formatRelativeDateTime, recruitmentDateParser | +| Experiments | `docs/features/experiments/` | A/B test, useExperiment, Mixpanel | +| Festival | `docs/features/festival/` | FestivalPage, PerformanceCard, TimelineRow, BoothMapSection | +| Introduce | `docs/features/introduce/` | IntroducePage, FeatureSection | + +어려우면 사용자에게 물어봅니다. 매핑에 없는 기능이면 새 폴더를 만듭니다. + +**문서 형식:** + +```markdown +# [제목 — 세션 핵심 주제] + +[본문 — 세션에서 논의/작업한 내용을 정리] + +## 관련 코드 + +- `[세션에서 다룬 파일 경로]` — [간단한 설명] +``` + +**원칙:** + +- 억지로 내용을 늘리지 않는다 +- 대화체 → 문서체로 변환 +- 제목, 본문: 한국어 / 코드, 경로, 기술 용어: 영어 + +**결과 출력:** + +``` +## 문서화 완료 +- **경로**: `docs/features/[기능]/[파일명]` +- **내용**: [1줄 요약] +``` + +--- + +## Phase 3: Git Commit + +기록이 완료되면 커밋을 수행합니다. + +1. `git status`로 변경된 파일 확인 +2. `git diff`로 staged + unstaged 변경사항 확인 +3. `git log --oneline -5`로 최근 커밋 스타일 참고 +4. 변경 내용을 분석하여 커밋 메시지 작성 +5. 관련 파일만 `git add`로 스테이징 + - `docs/features/` 문서 파일 포함 + - `dailyNote/`는 gitignore 대상이므로 제외 +6. **커밋 전에 변경 내용과 커밋 메시지를 사용자에게 확인 요청** +7. 사용자 승인 후 커밋 실행 + +**커밋 메시지 형식:** + +``` +(): + + + +``` + +**타입:** + +- `feat`: 새로운 기능 +- `fix`: 버그 수정 +- `docs`: 문서 변경 +- `style`: 코드 포맷팅 +- `refactor`: 리팩토링 +- `test`: 테스트 추가/수정 +- `chore`: 기타 변경 + +**스코프 예시:** + +- `main`, `club-detail`, `admin`, `application`, `auth`, `api`, `hooks`, `store`, `utils`, `components` + +--- + +## 참고사항 + +- dailyNote는 `.gitignore`에 포함되어 커밋 대상이 아님 +- 문서화는 중요한 변경이 있을 때만 수행 (매 세션 필수 아님) +- 사소한 변경은 Phase 1만 수행하고 커밋하지 않아도 됨 From 49d395191a45b54d1500bc3de1f6d28e4834d195 Mon Sep 17 00:00:00 2001 From: seongwon seo Date: Sun, 29 Mar 2026 19:41:44 +0900 Subject: [PATCH 106/172] =?UTF-8?q?docs(commands):=20commit=20=EC=BB=A4?= =?UTF-8?q?=EB=A7=A8=EB=93=9C=20=EB=AC=B8=EB=B2=95=20=EB=B0=8F=20=EC=84=A4?= =?UTF-8?q?=EB=AA=85=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - allowed-tools 와일드카드 문법 표준화 - 코드 블록 언어 식별자 추가 - git diff HEAD 사용법 명확화 Co-Authored-By: Claude Opus 4.5 --- frontend/.claude/commands/commit.md | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/frontend/.claude/commands/commit.md b/frontend/.claude/commands/commit.md index 3a484b065..3301e2cc8 100644 --- a/frontend/.claude/commands/commit.md +++ b/frontend/.claude/commands/commit.md @@ -1,6 +1,6 @@ --- description: 세션 작업 기록 + 기능 문서화 + 변경 내용 커밋 -allowed-tools: Bash(mkdir:*), Bash(ls:*), Bash(date:*), Bash(git status), Bash(git diff*), Bash(git log*), Bash(git add *), Bash(git commit *), Read, Write, Edit, Glob, Grep +allowed-tools: Bash(mkdir *), Bash(ls *), Bash(date *), Bash(git status), Bash(git diff *), Bash(git log *), Bash(git add *), Bash(git commit *), Read, Write, Edit, Glob, Grep --- # 작업 지시 @@ -95,7 +95,7 @@ allowed-tools: Bash(mkdir:*), Bash(ls:*), Bash(date:*), Bash(git status), Bash(g **결과 출력:** -``` +```text ## 문서화 완료 - **경로**: `docs/features/[기능]/[파일명]` - **내용**: [1줄 요약] @@ -108,7 +108,7 @@ allowed-tools: Bash(mkdir:*), Bash(ls:*), Bash(date:*), Bash(git status), Bash(g 기록이 완료되면 커밋을 수행합니다. 1. `git status`로 변경된 파일 확인 -2. `git diff`로 staged + unstaged 변경사항 확인 +2. `git diff HEAD`로 모든 변경사항 확인 (또는 `git diff`와 `git diff --staged`를 각각 실행) 3. `git log --oneline -5`로 최근 커밋 스타일 참고 4. 변경 내용을 분석하여 커밋 메시지 작성 5. 관련 파일만 `git add`로 스테이징 @@ -119,11 +119,10 @@ allowed-tools: Bash(mkdir:*), Bash(ls:*), Bash(date:*), Bash(git status), Bash(g **커밋 메시지 형식:** -``` +```text (): - ``` **타입:** From 5878734bb57b4e515ef1dfe629dfedec20df41ca Mon Sep 17 00:00:00 2001 From: seongwon seo Date: Sun, 29 Mar 2026 20:52:01 +0900 Subject: [PATCH 107/172] =?UTF-8?q?chore(commands):=20test=20=EC=BB=A4?= =?UTF-8?q?=EB=A7=A8=EB=93=9C=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Jest 단위 테스트, RTL 컴포넌트 테스트, Playwright E2E 테스트 지원 - 프로젝트 테스트 패턴 및 체크리스트 포함 --- frontend/.claude/commands/test.md | 220 ++++++++++++++++++++++++++++++ 1 file changed, 220 insertions(+) create mode 100644 frontend/.claude/commands/test.md diff --git a/frontend/.claude/commands/test.md b/frontend/.claude/commands/test.md new file mode 100644 index 000000000..1f437ff25 --- /dev/null +++ b/frontend/.claude/commands/test.md @@ -0,0 +1,220 @@ +--- +description: 테스트 코드 작성 (Jest / RTL / Playwright) +allowed-tools: Bash(npm run test *), Bash(npx jest *), Bash(npx playwright *), Read, Write, Edit, Glob, Grep +--- + +# 테스트 코드 작성 + +대상 파일 또는 기능에 대한 테스트 코드를 작성합니다. + +--- + +## Step 1: 테스트 유형 확인 + +사용자에게 테스트 유형을 확인합니다: + +1. **단위 테스트 (Jest)** - 유틸리티 함수, 훅, 순수 로직 +2. **컴포넌트 테스트 (RTL)** - React 컴포넌트 렌더링 및 인터랙션 +3. **E2E 테스트 (Playwright)** - 사용자 시나리오 기반 통합 테스트 + +--- + +## Step 2: 대상 파일 분석 + +테스트 대상 파일을 읽고 분석합니다: + +- 함수/컴포넌트의 입력과 출력 +- 엣지 케이스 및 예외 상황 +- 의존성 (외부 API, 스토어, 라우터 등) + +--- + +## Step 3: 테스트 작성 + +### 단위 테스트 (Jest) + +**파일 위치**: 대상 파일과 동일 경로에 `*.test.ts` 생성 + +**패턴**: + +```typescript +import { targetFunction } from './targetFile'; + +describe('targetFunction', () => { + beforeEach(() => { + // 테스트 환경 설정 + }); + + afterEach(() => { + // 정리 + }); + + it('정상 케이스를 처리한다', () => { + const result = targetFunction(input); + expect(result).toBe(expected); + }); + + it('엣지 케이스를 처리한다', () => { + // ... + }); + + it('예외 상황에서 적절히 동작한다', () => { + // ... + }); +}); +``` + +**체크리스트**: + +- [ ] 정상 동작 케이스 +- [ ] 경계값 테스트 +- [ ] 예외/에러 케이스 +- [ ] 타이머 사용 시 `jest.useFakeTimers()` +- [ ] 비동기 함수는 `async/await` 사용 + +--- + +### 컴포넌트 테스트 (RTL) + +**파일 위치**: 컴포넌트와 동일 경로에 `*.test.tsx` 생성 + +**패턴**: + +```typescript +import { render, screen, fireEvent, waitFor } from '@testing-library/react'; +import { BrowserRouter } from 'react-router-dom'; +import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; +import TargetComponent from './TargetComponent'; + +const queryClient = new QueryClient({ + defaultOptions: { + queries: { retry: false }, + }, +}); + +const renderWithProviders = (ui: React.ReactElement) => { + return render( + + {ui} + + ); +}; + +describe('TargetComponent', () => { + beforeEach(() => { + queryClient.clear(); + }); + + it('초기 상태가 올바르게 렌더링된다', () => { + renderWithProviders(); + expect(screen.getByText('예상 텍스트')).toBeInTheDocument(); + }); + + it('사용자 인터랙션에 올바르게 반응한다', async () => { + renderWithProviders(); + + fireEvent.click(screen.getByRole('button', { name: '버튼명' })); + + await waitFor(() => { + expect(screen.getByText('변경된 텍스트')).toBeInTheDocument(); + }); + }); + + it('로딩 상태를 표시한다', () => { + // ... + }); + + it('에러 상태를 처리한다', () => { + // ... + }); +}); +``` + +**체크리스트**: + +- [ ] 초기 렌더링 상태 +- [ ] 사용자 인터랙션 (클릭, 입력 등) +- [ ] 로딩/에러 상태 +- [ ] 조건부 렌더링 +- [ ] Props 변경에 따른 업데이트 +- [ ] 필요시 MSW로 API 모킹 + +--- + +### E2E 테스트 (Playwright) + +**파일 위치**: `e2e/` 폴더에 `*.spec.ts` 생성 + +**패턴**: + +```typescript +import { expect, test } from '@playwright/test'; + +test.describe('기능명', () => { + test.beforeEach(async ({ page }) => { + await page.goto('/target-page'); + }); + + test('사용자 시나리오를 완료한다', async ({ page }) => { + // 1. 페이지 로드 확인 + await expect( + page.getByRole('heading', { name: '페이지 제목' }), + ).toBeVisible(); + + // 2. 사용자 액션 + await page.getByRole('button', { name: '버튼명' }).click(); + + // 3. 결과 확인 + await expect(page.getByText('성공 메시지')).toBeVisible(); + }); + + test('에러 케이스를 처리한다', async ({ page }) => { + // ... + }); +}); +``` + +**체크리스트**: + +- [ ] 핵심 사용자 플로우 +- [ ] 폼 제출 및 유효성 검사 +- [ ] 네비게이션 +- [ ] 반응형 (모바일/데스크톱) +- [ ] 에러 처리 및 복구 + +--- + +## Step 4: 테스트 실행 + +```bash +# 단위/컴포넌트 테스트 +npx jest path/to/file.test.ts + +# 전체 테스트 +npm run test + +# E2E 테스트 +npx playwright test e2e/file.spec.ts + +# E2E 테스트 (UI 모드) +npx playwright test --ui +``` + +--- + +## 작성 원칙 + +1. **테스트 설명은 한국어로** - `it('정상적으로 동작한다')` +2. **Given-When-Then 구조** - 준비 → 실행 → 검증 +3. **독립적인 테스트** - 테스트 간 의존성 없음 +4. **의미 있는 테스트명** - 무엇을 테스트하는지 명확히 +5. **과도한 모킹 지양** - 실제 동작에 가깝게 + +--- + +## 참고: 프로젝트 테스트 설정 + +- Jest 설정: `jest.config.js` +- RTL 설정: `@testing-library/react`, `@testing-library/jest-dom` +- MSW 핸들러: `src/mocks/handlers/` +- Playwright 설정: `playwright.config.ts` (없으면 생성 필요) From 88ee498f34fd9b452a85fb97eac47fd02aef55f6 Mon Sep 17 00:00:00 2001 From: suhyun113 <163711629+suhyun113@users.noreply.github.com> Date: Mon, 30 Mar 2026 05:30:40 +0900 Subject: [PATCH 108/172] =?UTF-8?q?feat:=20=ED=99=8D=EB=B3=B4=20=EA=B2=8C?= =?UTF-8?q?=EC=8B=9C=EB=AC=BC=EC=9D=B4=200=EA=B0=9C=EC=9D=B8=20=EA=B2=BD?= =?UTF-8?q?=EC=9A=B0=20=EC=B2=98=EB=A6=AC=20=EB=A1=9C=EC=A7=81=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../pages/PromotionPage/PromotionListPage.styles.ts | 9 +++++++++ .../src/pages/PromotionPage/PromotionListPage.tsx | 12 ++++++++++-- 2 files changed, 19 insertions(+), 2 deletions(-) diff --git a/frontend/src/pages/PromotionPage/PromotionListPage.styles.ts b/frontend/src/pages/PromotionPage/PromotionListPage.styles.ts index 2e769b7eb..ef2cb8806 100644 --- a/frontend/src/pages/PromotionPage/PromotionListPage.styles.ts +++ b/frontend/src/pages/PromotionPage/PromotionListPage.styles.ts @@ -1,5 +1,6 @@ import styled from 'styled-components'; import { media } from '@/styles/mediaQuery'; +import { colors } from '@/styles/theme/colors'; export const Container = styled.div` width: 100%; @@ -23,7 +24,15 @@ export const Wrapper = styled.div` @media (max-width: 955px) { padding: 0px 20px 90px; } + ${media.mobile} { padding: 0px 20px 90px; } `; + +export const EmptyText = styled.p` + text-align: center; + font-size: 16px; + color: ${colors.gray[700]}; + padding: 120px 0; +`; \ No newline at end of file diff --git a/frontend/src/pages/PromotionPage/PromotionListPage.tsx b/frontend/src/pages/PromotionPage/PromotionListPage.tsx index 9192b40ea..ec079123e 100644 --- a/frontend/src/pages/PromotionPage/PromotionListPage.tsx +++ b/frontend/src/pages/PromotionPage/PromotionListPage.tsx @@ -20,10 +20,18 @@ const PromotionListPage = () => {
{!isInAppWebView() && } + {isLoading &&

로딩 중...

} {isError &&

오류가 발생했습니다.

} - {!isLoading && !isError && } + + {!isLoading && !isError && data?.length === 0 && ( + 등록된 이벤트가 없어요. + )} + + {!isLoading && !isError && data && data.length > 0 && ( + + )}