diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 2cc817946..73176c5f2 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -15,6 +15,7 @@ "@tanstack/react-query": "^5.66.0", "date-fns": "^4.1.0", "dotenv-webpack": "^8.1.0", + "framer-motion": "^12.23.12", "jest-fixed-jsdom": "^0.0.9", "mixpanel-browser": "^2.60.0", "msw": "^2.8.2", @@ -9936,6 +9937,33 @@ "node": ">= 0.6" } }, + "node_modules/framer-motion": { + "version": "12.23.12", + "resolved": "https://registry.npmjs.org/framer-motion/-/framer-motion-12.23.12.tgz", + "integrity": "sha512-6e78rdVtnBvlEVgu6eFEAgG9v3wLnYEboM8I5O5EXvfKC8gxGQB8wXJdhkMy10iVcn05jl6CNw7/HTsTCfwcWg==", + "license": "MIT", + "dependencies": { + "motion-dom": "^12.23.12", + "motion-utils": "^12.23.6", + "tslib": "^2.4.0" + }, + "peerDependencies": { + "@emotion/is-prop-valid": "*", + "react": "^18.0.0 || ^19.0.0", + "react-dom": "^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@emotion/is-prop-valid": { + "optional": true + }, + "react": { + "optional": true + }, + "react-dom": { + "optional": true + } + } + }, "node_modules/fresh": { "version": "0.5.2", "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", @@ -14426,6 +14454,21 @@ "rrweb": "2.0.0-alpha.13" } }, + "node_modules/motion-dom": { + "version": "12.23.12", + "resolved": "https://registry.npmjs.org/motion-dom/-/motion-dom-12.23.12.tgz", + "integrity": "sha512-RcR4fvMCTESQBD/uKQe49D5RUeDOokkGRmz4ceaJKDBgHYtZtntC/s2vLvY38gqGaytinij/yi3hMcWVcEF5Kw==", + "license": "MIT", + "dependencies": { + "motion-utils": "^12.23.6" + } + }, + "node_modules/motion-utils": { + "version": "12.23.6", + "resolved": "https://registry.npmjs.org/motion-utils/-/motion-utils-12.23.6.tgz", + "integrity": "sha512-eAWoPgr4eFEOFfg2WjIsMoqJTW6Z8MTUCgn/GZ3VRpClWBdnbjryiA3ZSNLyxCTmCQx4RmYX6jX1iWHbenUPNQ==", + "license": "MIT" + }, "node_modules/ms": { "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", @@ -18452,7 +18495,6 @@ "version": "2.8.1", "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", - "dev": true, "license": "0BSD" }, "node_modules/tsx": { diff --git a/frontend/package.json b/frontend/package.json index 2adc6593c..919f90011 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -26,6 +26,7 @@ "@tanstack/react-query": "^5.66.0", "date-fns": "^4.1.0", "dotenv-webpack": "^8.1.0", + "framer-motion": "^12.23.12", "jest-fixed-jsdom": "^0.0.9", "mixpanel-browser": "^2.60.0", "msw": "^2.8.2", diff --git a/frontend/src/apis/application/getApplication.ts b/frontend/src/apis/application/getApplication.ts index 366d3339b..6598ce899 100644 --- a/frontend/src/apis/application/getApplication.ts +++ b/frontend/src/apis/application/getApplication.ts @@ -4,7 +4,7 @@ const getApplication = async (clubId: string) => { try { const response = await fetch(`${API_BASE_URL}/api/club/${clubId}/apply`); if (!response.ok) { - console.error(`Failed to fetch: ${response.statusText}`) + console.error(`Failed to fetch: ${response.statusText}`); throw new Error((await response.json()).message); } @@ -13,7 +13,7 @@ const getApplication = async (clubId: string) => { } catch (error) { // [x] FIXME: // {"statuscode":"800-1","message":"지원서가 존재하지 않습니다.","data":null} - console.error('Error fetching club details', error); + console.error('지원서 조회 중 오류가 발생했습니다', error); throw error; } }; diff --git a/frontend/src/assets/images/banners/banner_desktop1.png b/frontend/src/assets/images/banners/banner_desktop1.png index a8540053e..b28126961 100644 Binary files a/frontend/src/assets/images/banners/banner_desktop1.png and b/frontend/src/assets/images/banners/banner_desktop1.png differ diff --git a/frontend/src/assets/images/banners/banner_desktop2.png b/frontend/src/assets/images/banners/banner_desktop2.png index 796211720..2a8f83c81 100644 Binary files a/frontend/src/assets/images/banners/banner_desktop2.png and b/frontend/src/assets/images/banners/banner_desktop2.png differ diff --git a/frontend/src/assets/images/banners/banner_mobile1.png b/frontend/src/assets/images/banners/banner_mobile1.png index 08115c162..d37c6edf9 100644 Binary files a/frontend/src/assets/images/banners/banner_mobile1.png and b/frontend/src/assets/images/banners/banner_mobile1.png differ diff --git a/frontend/src/assets/images/banners/banner_mobile2.png b/frontend/src/assets/images/banners/banner_mobile2.png index a6f1f1a00..3ebdb93cc 100644 Binary files a/frontend/src/assets/images/banners/banner_mobile2.png and b/frontend/src/assets/images/banners/banner_mobile2.png differ diff --git a/frontend/src/assets/images/icons/category_button/category_all_button_icon.svg b/frontend/src/assets/images/icons/category_button/category_all_button_icon.svg index 684b705cf..a94994ab6 100644 --- a/frontend/src/assets/images/icons/category_button/category_all_button_icon.svg +++ b/frontend/src/assets/images/icons/category_button/category_all_button_icon.svg @@ -1,6 +1,7 @@ - - - - - + + + + + + diff --git a/frontend/src/assets/images/icons/category_button/category_all_button_icon_active.svg b/frontend/src/assets/images/icons/category_button/category_all_button_icon_active.svg new file mode 100644 index 000000000..8de36efe3 --- /dev/null +++ b/frontend/src/assets/images/icons/category_button/category_all_button_icon_active.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/frontend/src/assets/images/icons/category_button/category_hobby_button_icon.svg b/frontend/src/assets/images/icons/category_button/category_hobby_button_icon.svg index 280c0d80a..e50044c06 100644 --- a/frontend/src/assets/images/icons/category_button/category_hobby_button_icon.svg +++ b/frontend/src/assets/images/icons/category_button/category_hobby_button_icon.svg @@ -1,11 +1,7 @@ - - - - - - - - - - + + + + + + diff --git a/frontend/src/assets/images/icons/category_button/category_hobby_button_icon_active.svg b/frontend/src/assets/images/icons/category_button/category_hobby_button_icon_active.svg new file mode 100644 index 000000000..4ecf0d866 --- /dev/null +++ b/frontend/src/assets/images/icons/category_button/category_hobby_button_icon_active.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/frontend/src/assets/images/icons/category_button/category_performance_button_icon.svg b/frontend/src/assets/images/icons/category_button/category_performance_button_icon.svg index 209498dcb..e484552d3 100644 --- a/frontend/src/assets/images/icons/category_button/category_performance_button_icon.svg +++ b/frontend/src/assets/images/icons/category_button/category_performance_button_icon.svg @@ -1,13 +1,5 @@ - - - - - - - - - - - - + + + + diff --git a/frontend/src/assets/images/icons/category_button/category_performance_button_icon_active.svg b/frontend/src/assets/images/icons/category_button/category_performance_button_icon_active.svg new file mode 100644 index 000000000..dbec135e0 --- /dev/null +++ b/frontend/src/assets/images/icons/category_button/category_performance_button_icon_active.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/frontend/src/assets/images/icons/category_button/category_religion_button_icon.svg b/frontend/src/assets/images/icons/category_button/category_religion_button_icon.svg index c8b57f892..8f558c619 100644 --- a/frontend/src/assets/images/icons/category_button/category_religion_button_icon.svg +++ b/frontend/src/assets/images/icons/category_button/category_religion_button_icon.svg @@ -1,20 +1,5 @@ - - - - - - - - - - - - - - - - - - - + + + + diff --git a/frontend/src/assets/images/icons/category_button/category_religion_button_icon_active.svg b/frontend/src/assets/images/icons/category_button/category_religion_button_icon_active.svg new file mode 100644 index 000000000..2e116b990 --- /dev/null +++ b/frontend/src/assets/images/icons/category_button/category_religion_button_icon_active.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/frontend/src/assets/images/icons/category_button/category_sport_button_icon.svg b/frontend/src/assets/images/icons/category_button/category_sport_button_icon.svg index ecd780d9f..9f7efdfb0 100644 --- a/frontend/src/assets/images/icons/category_button/category_sport_button_icon.svg +++ b/frontend/src/assets/images/icons/category_button/category_sport_button_icon.svg @@ -1,30 +1,4 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + diff --git a/frontend/src/assets/images/icons/category_button/category_sport_button_icon_active.svg b/frontend/src/assets/images/icons/category_button/category_sport_button_icon_active.svg new file mode 100644 index 000000000..443de7d09 --- /dev/null +++ b/frontend/src/assets/images/icons/category_button/category_sport_button_icon_active.svg @@ -0,0 +1,4 @@ + + + + diff --git a/frontend/src/assets/images/icons/category_button/category_study_button_icon.svg b/frontend/src/assets/images/icons/category_button/category_study_button_icon.svg index 149a3d8f1..98aa93c0b 100644 --- a/frontend/src/assets/images/icons/category_button/category_study_button_icon.svg +++ b/frontend/src/assets/images/icons/category_button/category_study_button_icon.svg @@ -1,14 +1,5 @@ - - - - - - - - - - - - - + + + + diff --git a/frontend/src/assets/images/icons/category_button/category_study_button_icon_active.svg b/frontend/src/assets/images/icons/category_button/category_study_button_icon_active.svg new file mode 100644 index 000000000..8a4bb4077 --- /dev/null +++ b/frontend/src/assets/images/icons/category_button/category_study_button_icon_active.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/frontend/src/assets/images/icons/category_button/category_volunteer_button_icon.svg b/frontend/src/assets/images/icons/category_button/category_volunteer_button_icon.svg index 4492f88e7..cf0a53ead 100644 --- a/frontend/src/assets/images/icons/category_button/category_volunteer_button_icon.svg +++ b/frontend/src/assets/images/icons/category_button/category_volunteer_button_icon.svg @@ -1,6 +1,5 @@ - - - - - + + + + diff --git a/frontend/src/assets/images/icons/category_button/category_volunteer_button_icon_active.svg b/frontend/src/assets/images/icons/category_button/category_volunteer_button_icon_active.svg new file mode 100644 index 000000000..8ccc84737 --- /dev/null +++ b/frontend/src/assets/images/icons/category_button/category_volunteer_button_icon_active.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/frontend/src/assets/images/icons/category_button/index.ts b/frontend/src/assets/images/icons/category_button/index.ts new file mode 100644 index 000000000..3d3fa1f45 --- /dev/null +++ b/frontend/src/assets/images/icons/category_button/index.ts @@ -0,0 +1,34 @@ +import iconAll from '@/assets/images/icons/category_button/category_all_button_icon.svg'; +import iconVolunteer from '@/assets/images/icons/category_button/category_volunteer_button_icon.svg'; +import iconReligion from '@/assets/images/icons/category_button/category_religion_button_icon.svg'; +import iconHobby from '@/assets/images/icons/category_button/category_hobby_button_icon.svg'; +import iconStudy from '@/assets/images/icons/category_button/category_study_button_icon.svg'; +import iconSport from '@/assets/images/icons/category_button/category_sport_button_icon.svg'; +import iconPerformance from '@/assets/images/icons/category_button/category_performance_button_icon.svg'; +import iconAllActive from '@/assets/images/icons/category_button/category_all_button_icon_active.svg'; +import iconVolunteerActive from '@/assets/images/icons/category_button/category_volunteer_button_icon_active.svg'; +import iconReligionActive from '@/assets/images/icons/category_button/category_religion_button_icon_active.svg'; +import iconHobbyActive from '@/assets/images/icons/category_button/category_hobby_button_icon_active.svg'; +import iconStudyActive from '@/assets/images/icons/category_button/category_study_button_icon_active.svg'; +import iconSportActive from '@/assets/images/icons/category_button/category_sport_button_icon_active.svg'; +import iconPerformanceActive from '@/assets/images/icons/category_button/category_performance_button_icon_active.svg'; + +export const inactiveCategoryIcons : Record = { + all : iconAll, + volunteer : iconVolunteer, + religion : iconReligion, + hobby : iconHobby, + study : iconStudy, + sport : iconSport, + performance : iconPerformance +} + +export const activeCategoryIcons : Record = { + all : iconAllActive, + volunteer : iconVolunteerActive, + religion : iconReligionActive, + hobby : iconHobbyActive, + study : iconStudyActive, + sport : iconSportActive, + performance : iconPerformanceActive +} diff --git a/frontend/src/assets/images/icons/share_filled_icon.svg b/frontend/src/assets/images/icons/share_filled_icon.svg new file mode 100644 index 000000000..8dfa0ff3f --- /dev/null +++ b/frontend/src/assets/images/icons/share_filled_icon.svg @@ -0,0 +1,4 @@ + + + + diff --git a/frontend/src/assets/images/introduce/background-circle-large.svg b/frontend/src/assets/images/introduce/background-circle-large.svg new file mode 100644 index 000000000..14b04f816 --- /dev/null +++ b/frontend/src/assets/images/introduce/background-circle-large.svg @@ -0,0 +1,3 @@ + + + diff --git a/frontend/src/assets/images/introduce/background-circle-small.svg b/frontend/src/assets/images/introduce/background-circle-small.svg new file mode 100644 index 000000000..d10054162 --- /dev/null +++ b/frontend/src/assets/images/introduce/background-circle-small.svg @@ -0,0 +1,3 @@ + + + diff --git a/frontend/src/assets/images/introduce/background-twist-left.svg b/frontend/src/assets/images/introduce/background-twist-left.svg new file mode 100644 index 000000000..1fce2ba6a --- /dev/null +++ b/frontend/src/assets/images/introduce/background-twist-left.svg @@ -0,0 +1,3 @@ + + + diff --git a/frontend/src/assets/images/introduce/background-twist-right.svg b/frontend/src/assets/images/introduce/background-twist-right.svg new file mode 100644 index 000000000..dfdc3a999 --- /dev/null +++ b/frontend/src/assets/images/introduce/background-twist-right.svg @@ -0,0 +1,3 @@ + + + diff --git a/frontend/src/assets/images/introduce/features/desktop/feature_category_mockup.png b/frontend/src/assets/images/introduce/features/desktop/feature_category_mockup.png new file mode 100644 index 000000000..bf6ea818a Binary files /dev/null and b/frontend/src/assets/images/introduce/features/desktop/feature_category_mockup.png differ diff --git a/frontend/src/assets/images/introduce/features/desktop/feature_info_mockup.png b/frontend/src/assets/images/introduce/features/desktop/feature_info_mockup.png new file mode 100644 index 000000000..7af7c06ad Binary files /dev/null and b/frontend/src/assets/images/introduce/features/desktop/feature_info_mockup.png differ diff --git a/frontend/src/assets/images/introduce/features/desktop/feature_introduction_mockup.png b/frontend/src/assets/images/introduce/features/desktop/feature_introduction_mockup.png new file mode 100644 index 000000000..3ad5ff5bc Binary files /dev/null and b/frontend/src/assets/images/introduce/features/desktop/feature_introduction_mockup.png differ diff --git a/frontend/src/assets/images/introduce/features/desktop/feature_recruitment_mockup.png b/frontend/src/assets/images/introduce/features/desktop/feature_recruitment_mockup.png new file mode 100644 index 000000000..02223a7a7 Binary files /dev/null and b/frontend/src/assets/images/introduce/features/desktop/feature_recruitment_mockup.png differ diff --git a/frontend/src/assets/images/introduce/features/index.ts b/frontend/src/assets/images/introduce/features/index.ts new file mode 100644 index 000000000..6080fe2f0 --- /dev/null +++ b/frontend/src/assets/images/introduce/features/index.ts @@ -0,0 +1,25 @@ +// 데스크탑용 +import feature_category_mockup_desktop from './desktop/feature_category_mockup.png'; +import feature_recruitment_mockup_desktop from './desktop/feature_recruitment_mockup.png'; +import feature_info_mockup_desktop from './desktop/feature_info_mockup.png'; +import feature_introduction_mockup_desktop from './desktop/feature_introduction_mockup.png'; + +// 모바일용 +import feature_category_mockup_mobile from './mobile/feature_category_mockup.png'; +import feature_recruitment_mockup_mobile from './mobile/feature_recruitment_mockup.png'; +import feature_info_mockup_mobile from './mobile/feature_info_mockup.png'; +import feature_introduction_mockup_mobile from './mobile/feature_introduction_mockup.png'; + +export const desktopFeatures = [ + { src: feature_category_mockup_desktop, alt: '분과별 카테고리' }, + { src: feature_recruitment_mockup_desktop, alt: '모집 상태 확인' }, + { src: feature_info_mockup_desktop, alt: '지원/정보 확인' }, + { src: feature_introduction_mockup_desktop, alt: '동아리 소개' }, +]; + +export const mobileFeatures = [ + { src: feature_category_mockup_mobile, alt: '분과별 카테고리' }, + { src: feature_recruitment_mockup_mobile, alt: '모집 상태 확인' }, + { src: feature_info_mockup_mobile, alt: '지원/정보 확인' }, + { src: feature_introduction_mockup_mobile, alt: '동아리 소개' }, +]; diff --git a/frontend/src/assets/images/introduce/features/mobile/feature_category_mockup.png b/frontend/src/assets/images/introduce/features/mobile/feature_category_mockup.png new file mode 100644 index 000000000..2bf994679 Binary files /dev/null and b/frontend/src/assets/images/introduce/features/mobile/feature_category_mockup.png differ diff --git a/frontend/src/assets/images/introduce/features/mobile/feature_info_mockup.png b/frontend/src/assets/images/introduce/features/mobile/feature_info_mockup.png new file mode 100644 index 000000000..eb1cffe50 Binary files /dev/null and b/frontend/src/assets/images/introduce/features/mobile/feature_info_mockup.png differ diff --git a/frontend/src/assets/images/introduce/features/mobile/feature_introduction_mockup.png b/frontend/src/assets/images/introduce/features/mobile/feature_introduction_mockup.png new file mode 100644 index 000000000..904758e67 Binary files /dev/null and b/frontend/src/assets/images/introduce/features/mobile/feature_introduction_mockup.png differ diff --git a/frontend/src/assets/images/introduce/features/mobile/feature_recruitment_mockup.png b/frontend/src/assets/images/introduce/features/mobile/feature_recruitment_mockup.png new file mode 100644 index 000000000..bd4b84822 Binary files /dev/null and b/frontend/src/assets/images/introduce/features/mobile/feature_recruitment_mockup.png differ diff --git a/frontend/src/assets/images/introduce/introduce_phone_mockup.png b/frontend/src/assets/images/introduce/introduce_phone_mockup.png new file mode 100644 index 000000000..b084ce135 Binary files /dev/null and b/frontend/src/assets/images/introduce/introduce_phone_mockup.png differ diff --git a/frontend/src/assets/images/logos/moadong_logo_bg.svg b/frontend/src/assets/images/logos/moadong_logo_bg.svg new file mode 100644 index 000000000..d0ed2e0f8 --- /dev/null +++ b/frontend/src/assets/images/logos/moadong_logo_bg.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/frontend/src/components/ClubTag/ClubTag.tsx b/frontend/src/components/ClubTag/ClubTag.tsx index 4711a5a54..860109dc9 100644 --- a/frontend/src/components/ClubTag/ClubTag.tsx +++ b/frontend/src/components/ClubTag/ClubTag.tsx @@ -14,6 +14,7 @@ const TagColors: Record = { interface TagProps { type: string; children?: React.ReactNode; + className?: string; } const StyledTag = styled.span<{ color: string }>` @@ -25,11 +26,15 @@ const StyledTag = styled.span<{ color: string }>` font-weight: 600; color: #3a3a3a; `; +``; -const ClubTag = ({ type, children }: TagProps) => { +const ClubTag = ({ type, children, className }: TagProps) => { const backgroundColor = TagColors[type] || 'rgba(237, 237, 237, 1)'; return ( - {`#${children || type}`} + {`#${children || type}`} ); }; diff --git a/frontend/src/components/common/Footer/Footer.styles.ts b/frontend/src/components/common/Footer/Footer.styles.ts index 9505484f4..ec21b26ea 100644 --- a/frontend/src/components/common/Footer/Footer.styles.ts +++ b/frontend/src/components/common/Footer/Footer.styles.ts @@ -4,7 +4,6 @@ import { media } from '@/styles/mediaQuery'; export const FooterContainer = styled.footer` text-align: left; font-size: 0.75rem; - margin-top: 50px; `; export const Divider = styled.hr` @@ -36,7 +35,6 @@ export const PolicyLink = styled.a` } `; - export const CopyRightText = styled.p``; export const EmailText = styled.p` diff --git a/frontend/src/constants/CLUB_UNION_INFO.ts b/frontend/src/constants/CLUB_UNION_INFO.ts index f3e004ef9..1956391c7 100644 --- a/frontend/src/constants/CLUB_UNION_INFO.ts +++ b/frontend/src/constants/CLUB_UNION_INFO.ts @@ -1,10 +1,4 @@ -import PresidentAvatar from '@/assets/images/icons/category_button/category_all_button_icon.svg'; -import ReligionAvatar from '@/assets/images/icons/category_button/category_religion_button_icon.svg'; -import HobbyAvatar from '@/assets/images/icons/category_button/category_hobby_button_icon.svg'; -import StudyAvatar from '@/assets/images/icons/category_button/category_study_button_icon.svg'; -import VolunteerAvatar from '@/assets/images/icons/category_button/category_volunteer_button_icon.svg'; -import PerformanceAvatar from '@/assets/images/icons/category_button/category_performance_button_icon.svg'; -import SportAvatar from '@/assets/images/icons/category_button/category_sport_button_icon.svg'; +import {inactiveCategoryIcons} from '@/assets/images/icons/category_button'; export interface ClubUnionMember { id: number; @@ -15,16 +9,16 @@ export interface ClubUnionMember { } const MEMBER_AVATARS = { - PRESIDENT: PresidentAvatar, - VICE_PRESIDENT: PresidentAvatar, - SECRETARY: PresidentAvatar, - PROMOTION: PresidentAvatar, - RELIGION: ReligionAvatar, - HOBBY: HobbyAvatar, - STUDY: StudyAvatar, - VOLUNTEER: VolunteerAvatar, - PERFORMANCE: PerformanceAvatar, - SPORT: SportAvatar, + PRESIDENT: inactiveCategoryIcons.all, + VICE_PRESIDENT: inactiveCategoryIcons.all, + SECRETARY: inactiveCategoryIcons.all, + PROMOTION: inactiveCategoryIcons.all, + RELIGION: inactiveCategoryIcons.religion, + HOBBY: inactiveCategoryIcons.hobby, + STUDY: inactiveCategoryIcons.study, + VOLUNTEER: inactiveCategoryIcons.volunteer, + PERFORMANCE: inactiveCategoryIcons.performance, + SPORT: inactiveCategoryIcons.sport, }; // 개발자 가이드: description 필드는 UI가 깨지지 않도록 글자 수를 제한합니다. diff --git a/frontend/src/constants/eventName.ts b/frontend/src/constants/eventName.ts index 76cb84657..7c9703ab2 100644 --- a/frontend/src/constants/eventName.ts +++ b/frontend/src/constants/eventName.ts @@ -16,4 +16,5 @@ export const EVENT_NAME = { MOBILE_MENU_DELETE_BUTTON_CLICKED: 'Mobile Menubar delete Button Clicked' as const, CLUB_UNION_BUTTON_CLICKED: 'Club Union Button Clicked' as const, + APPLICATION_FORM_SUBMITTED: 'Application Form Submitted' as const, } as const; diff --git a/frontend/src/hooks/useDevice.ts b/frontend/src/hooks/useDevice.ts new file mode 100644 index 000000000..0f16df686 --- /dev/null +++ b/frontend/src/hooks/useDevice.ts @@ -0,0 +1,21 @@ +import { useEffect, useState } from 'react'; +import { BREAKPOINT } from '@/styles/mediaQuery'; + +const useDevice = () => { + const [width, setWidth] = useState(window.innerWidth); + + useEffect(() => { + const onResize = () => setWidth(window.innerWidth); + window.addEventListener('resize', onResize); + return () => window.removeEventListener('resize', onResize); + }, []); + + return { + isMobile: width <= BREAKPOINT.mobile, // ≤ 500 + isTablet: width > BREAKPOINT.mobile && width <= BREAKPOINT.tablet, // 501 ~ 700 + isLaptop: width > BREAKPOINT.tablet && width <= BREAKPOINT.laptop, // 701 ~ 1280 + isDesktop: width > BREAKPOINT.laptop, // ≥ 1281 + }; +}; + +export default useDevice; diff --git a/frontend/src/pages/AdminPage/AdminPage.styles.ts b/frontend/src/pages/AdminPage/AdminPage.styles.ts index 3903da32a..3fe8954d2 100644 --- a/frontend/src/pages/AdminPage/AdminPage.styles.ts +++ b/frontend/src/pages/AdminPage/AdminPage.styles.ts @@ -2,14 +2,23 @@ import styled from 'styled-components'; export const AdminPageContainer = styled.div` display: flex; - gap: 34px; margin-top: 98px; + align-items: flex-start; `; +export const Divider = styled.div` + position: sticky; + top: 98px; + width: 1px; + height: calc(100vh - 98px); + background-color: #dcdcdc; + margin: 0 34px; + flex-shrink: 0; +`; + + export const Content = styled.main` width: 100%; - border: 1px solid #dcdcdc; - border-radius: 18px; - padding: 62px 58px; max-width: 977px; + padding: 62px 58px; `; diff --git a/frontend/src/pages/AdminPage/AdminPage.tsx b/frontend/src/pages/AdminPage/AdminPage.tsx index d504d8825..b3c3b0291 100644 --- a/frontend/src/pages/AdminPage/AdminPage.tsx +++ b/frontend/src/pages/AdminPage/AdminPage.tsx @@ -5,6 +5,7 @@ import { Outlet } from 'react-router-dom'; import * as Styled from './AdminPage.styles'; import { useGetClubDetail } from '@/hooks/queries/club/useGetClubDetail'; import { useAdminClubContext } from '@/context/AdminClubContext'; +import { Divider } from './components/SideBar/SideBar.styles'; const AdminPage = () => { const { clubId } = useAdminClubContext(); @@ -25,6 +26,7 @@ const AdminPage = () => { clubLogo={clubDetail?.logo} clubName={clubDetail?.name || ''} /> + diff --git a/frontend/src/pages/AdminPage/components/SideBar/SideBar.styles.ts b/frontend/src/pages/AdminPage/components/SideBar/SideBar.styles.ts index 2f2ecdf96..472c4e056 100644 --- a/frontend/src/pages/AdminPage/components/SideBar/SideBar.styles.ts +++ b/frontend/src/pages/AdminPage/components/SideBar/SideBar.styles.ts @@ -8,6 +8,9 @@ export const SidebarWrapper = styled.aside` overflow-wrap: break-word; white-space: normal; width: 168px; + position: sticky; + top: 98px; + height: fit-content; `; export const SidebarHeader = styled.p` diff --git a/frontend/src/pages/ApplicationFormPage/ApplicationFormPage.tsx b/frontend/src/pages/ApplicationFormPage/ApplicationFormPage.tsx index ecc3a4e56..43a03a438 100644 --- a/frontend/src/pages/ApplicationFormPage/ApplicationFormPage.tsx +++ b/frontend/src/pages/ApplicationFormPage/ApplicationFormPage.tsx @@ -14,12 +14,15 @@ import QuestionContainer from '@/pages/ApplicationFormPage/components/QuestionCo import { parseDescriptionWithLinks } from '@/utils/parseDescriptionWithLinks'; import { validateAnswers } from '@/hooks/useValidateAnswers'; import * as Styled from './ApplicationFormPage.styles'; +import useMixpanelTrack from '@/hooks/useMixpanelTrack'; +import { EVENT_NAME } from '@/constants/eventName'; const ApplicationFormPage = () => { const { clubId } = useParams<{ clubId: string }>(); const navigate = useNavigate(); const questionRefs = useRef>([]); const [invalidQuestionIds, setInvalidQuestionIds] = useState([]); + const trackEvent = useMixpanelTrack(); const { data: clubDetail, error: clubError } = useGetClubDetail(clubId!); const { @@ -86,6 +89,10 @@ const ApplicationFormPage = () => { }; const handleSubmit = async () => { + trackEvent(EVENT_NAME.APPLICATION_FORM_SUBMITTED, { + clubName: clubDetail?.name, + }); + const invalidIds = validateAnswers(formData.questions, getAnswersById); if (invalidIds.length > 0) { setInvalidQuestionIds(invalidIds); @@ -100,7 +107,7 @@ const ApplicationFormPage = () => { `"${clubDetail.name}" 동아리에 성공적으로 지원되었습니다.\n좋은 결과 있으시길 바랍니다🤗`, ); navigate(`/club/${clubId}`, { replace: true }); - } catch { + } catch (error) { alert( '⚠️ 답변 제출에 실패했어요.\n네트워크 상태를 확인하거나 잠시 후 다시 시도해 주세요.', ); diff --git a/frontend/src/pages/ClubDetailPage/ClubDetailPage.tsx b/frontend/src/pages/ClubDetailPage/ClubDetailPage.tsx index eadf0324c..85a689fe0 100644 --- a/frontend/src/pages/ClubDetailPage/ClubDetailPage.tsx +++ b/frontend/src/pages/ClubDetailPage/ClubDetailPage.tsx @@ -13,9 +13,7 @@ import ClubDetailFooter from '@/pages/ClubDetailPage/components/ClubDetailFooter import useTrackPageView from '@/hooks/useTrackPageView'; import useAutoScroll from '@/hooks/InfoTabs/useAutoScroll'; import { useGetClubDetail } from '@/hooks/queries/club/useGetClubDetail'; -import ShareButton from '@/pages/ClubDetailPage/components/ShareButton/ShareButton'; import RecommendedClubs from '@/pages/ClubDetailPage/components/RecommendedClubs/RecommendedClubs'; -import { Club } from '@/types/club'; const ClubDetailPage = () => { const { clubId } = useParams<{ clubId: string }>(); @@ -58,7 +56,6 @@ const ClubDetailPage = () => { recruitmentForm={clubDetail.recruitmentForm} presidentPhoneNumber={clubDetail.presidentPhoneNumber} /> - { feeds={clubDetail.feeds} clubName={clubDetail.name} /> - - +
); diff --git a/frontend/src/pages/ClubDetailPage/components/ClubApplyButton/ClubApplyButton.styles.ts b/frontend/src/pages/ClubDetailPage/components/ClubApplyButton/ClubApplyButton.styles.ts new file mode 100644 index 000000000..f324728b1 --- /dev/null +++ b/frontend/src/pages/ClubDetailPage/components/ClubApplyButton/ClubApplyButton.styles.ts @@ -0,0 +1,44 @@ +import styled from 'styled-components'; + +export const ApplyButtonContainer = styled.div` + width: 100%; + display: flex; + flex-direction: row; + justify-content: center; + text-align: center; + gap: 10px; +`; + +export const ApplyButton = styled.button` + display: flex; + align-items: center; + justify-content: center; + border: none; + border-radius: 10px; + cursor: pointer; + transition: transform 0.2s ease-in-out; + background-color: #3a3a3a; + + padding: 10px 40px; + width: 517px; + height: 44px; + font-size: 16px; + font-style: normal; + font-weight: 500; + color: #fff; + text-align: center; + + &:hover { + background-color: #555; + transform: scale(1.03); + } + + img { + font-size: 12px; + font-weight: 600; + } + + @media (max-width: 500px) { + width: 280px; + } +`; diff --git a/frontend/src/pages/ClubDetailPage/components/ClubApplyButton/ClubApplyButton.tsx b/frontend/src/pages/ClubDetailPage/components/ClubApplyButton/ClubApplyButton.tsx index c4339047e..4916b7024 100644 --- a/frontend/src/pages/ClubDetailPage/components/ClubApplyButton/ClubApplyButton.tsx +++ b/frontend/src/pages/ClubDetailPage/components/ClubApplyButton/ClubApplyButton.tsx @@ -1,83 +1,77 @@ -import styled from 'styled-components'; +import * as Styled from './ClubApplyButton.styles'; import { useNavigate, useParams } from 'react-router-dom'; import { useGetClubDetail } from '@/hooks/queries/club/useGetClubDetail'; import getApplication from '@/apis/application/getApplication'; -import { parseRecruitmentPeriod } from '@/utils/recruitmentPeriodParser'; -import getDeadlineText from '@/utils/getDeadLineText'; import useMixpanelTrack from '@/hooks/useMixpanelTrack'; import { EVENT_NAME } from '@/constants/eventName'; +import ShareButton from '@/pages/ClubDetailPage/components/ShareButton/ShareButton'; -const Button = styled.button` - display: flex; - align-items: center; - justify-content: center; - border: none; - border-radius: 10px; - cursor: pointer; - transition: transform 0.2s ease-in-out; +interface ClubApplyButtonProps { + deadlineText?: string; +} - background-color: #3a3a3a; - color: white; - font-weight: bold; - - width: 148px; - height: 44px; - font-size: 1.25rem; - - &:hover { - background-color: #555; - transform: scale(1.03); - } - - @media (max-width: 500px) { - width: 256px; - height: 44px; - font-size: 1rem; - } -`; +const RECRUITMENT_STATUS = { + ALWAYS: '상시 모집', + CLOSED: '모집 마감', +}; -const ClubApplyButton = () => { +const ClubApplyButton = ({ deadlineText }: ClubApplyButtonProps) => { const { clubId } = useParams<{ clubId: string }>(); const navigate = useNavigate(); const trackEvent = useMixpanelTrack(); const { data: clubDetail } = useGetClubDetail(clubId!); - const handleClick = async () => { - if (!clubId || !clubDetail) return; + if (!clubId || !clubDetail) return; + const handleClick = async () => { trackEvent(EVENT_NAME.CLUB_APPLY_BUTTON_CLICKED); - const { recruitmentStart, recruitmentEnd } = parseRecruitmentPeriod( - clubDetail.recruitmentPeriod, - ); - const deadlineText = getDeadlineText( - recruitmentStart, - recruitmentEnd, - new Date(), - ); - - if (deadlineText === '모집 마감') { + if (deadlineText === RECRUITMENT_STATUS.CLOSED) { alert(`현재 ${clubDetail.name} 동아리는 모집 기간이 아닙니다.`); return; } - // 모아동 지원서 확인 try { await getApplication(clubId); navigate(`/application/${clubId}`); } catch (err: unknown) { const externalFormLink = clubDetail.externalApplicationUrl?.trim(); - if (externalFormLink) { - window.open(externalFormLink, '_blank', 'noopener,noreferrer'); - } else { + if (!externalFormLink) { alert('동아리 모집 정보를 확인해주세요.'); + return; } + window.open(externalFormLink, '_blank', 'noopener,noreferrer'); } }; - return ; + const renderButtonContent = () => { + if (deadlineText === RECRUITMENT_STATUS.CLOSED) { + return RECRUITMENT_STATUS.CLOSED; + } + + return ( + <> + 지원하기 + {deadlineText && deadlineText !== RECRUITMENT_STATUS.ALWAYS && ( + <> +
| + {deadlineText} + + )} + + ); + }; + + return ( + + + + {renderButtonContent()} + + + ); }; export default ClubApplyButton; diff --git a/frontend/src/pages/ClubDetailPage/components/ClubDetailFooter/ClubDetailFooter.styles.ts b/frontend/src/pages/ClubDetailPage/components/ClubDetailFooter/ClubDetailFooter.styles.ts index ba46f15d0..018560f2a 100644 --- a/frontend/src/pages/ClubDetailPage/components/ClubDetailFooter/ClubDetailFooter.styles.ts +++ b/frontend/src/pages/ClubDetailPage/components/ClubDetailFooter/ClubDetailFooter.styles.ts @@ -1,22 +1,18 @@ import styled from 'styled-components'; export const ClubDetailFooterContainer = styled.div` - display: none; + position: sticky; + bottom: 0; + width: 100%; + height: 65px; + z-index: 100; + padding: 10px 40px; - @media (max-width: 500px) { - position: sticky; - bottom: 0; - width: 100%; - height: 65px; - z-index: 100; - padding: 10px 40px; + display: flex; + align-items: center; + justify-content: space-between; + gap: 16px; - display: flex; - align-items: center; - justify-content: space-between; - gap: 16px; - - background-color: white; - border-top: 1px solid #cdcdcd; - } + background-color: white; + border-top: 1px solid #cdcdcd; `; diff --git a/frontend/src/pages/ClubDetailPage/components/ClubDetailFooter/ClubDetailFooter.tsx b/frontend/src/pages/ClubDetailPage/components/ClubDetailFooter/ClubDetailFooter.tsx index 2d9fef9f8..14834569c 100644 --- a/frontend/src/pages/ClubDetailPage/components/ClubDetailFooter/ClubDetailFooter.tsx +++ b/frontend/src/pages/ClubDetailPage/components/ClubDetailFooter/ClubDetailFooter.tsx @@ -1,5 +1,4 @@ import * as Styled from './ClubDetailFooter.styles'; -import DeadlineBadge from '@/pages/ClubDetailPage/components/DeadlineBadge/DeadlineBadge'; import ClubApplyButton from '@/pages/ClubDetailPage/components/ClubApplyButton/ClubApplyButton'; import { parseRecruitmentPeriod } from '@/utils/recruitmentPeriodParser'; import getDeadlineText from '@/utils/getDeadLineText'; @@ -7,12 +6,9 @@ import getDeadlineText from '@/utils/getDeadLineText'; interface ClubDetailFooterProps { recruitmentPeriod: string; recruitmentForm: string; - presidentPhoneNumber: string; } -const ClubDetailFooter = ({ - recruitmentPeriod -}: ClubDetailFooterProps) => { +const ClubDetailFooter = ({ recruitmentPeriod }: ClubDetailFooterProps) => { const { recruitmentStart, recruitmentEnd } = parseRecruitmentPeriod(recruitmentPeriod); @@ -22,10 +18,9 @@ const ClubDetailFooter = ({ new Date(), ); - return ( + return ( - - + ); }; diff --git a/frontend/src/pages/ClubDetailPage/components/ClubDetailHeader/ClubDetailHeader.tsx b/frontend/src/pages/ClubDetailPage/components/ClubDetailHeader/ClubDetailHeader.tsx index 1a84577d7..96180d72f 100644 --- a/frontend/src/pages/ClubDetailPage/components/ClubDetailHeader/ClubDetailHeader.tsx +++ b/frontend/src/pages/ClubDetailPage/components/ClubDetailHeader/ClubDetailHeader.tsx @@ -1,6 +1,5 @@ import * as Styled from './ClubDetailHeader.styles'; import ClubProfile from '@/pages/ClubDetailPage/components/ClubProfile/ClubProfile'; -import ClubApplyButton from '@/pages/ClubDetailPage/components/ClubApplyButton/ClubApplyButton'; interface ClubDetailHeaderProps { name: string; @@ -29,7 +28,6 @@ const ClubDetailHeader = ({ tags={tags} logo={logo} /> - ); }; diff --git a/frontend/src/pages/ClubDetailPage/components/ShareButton/ShareButton.styles.ts b/frontend/src/pages/ClubDetailPage/components/ShareButton/ShareButton.styles.ts index b35582412..1a734a510 100644 --- a/frontend/src/pages/ClubDetailPage/components/ShareButton/ShareButton.styles.ts +++ b/frontend/src/pages/ClubDetailPage/components/ShareButton/ShareButton.styles.ts @@ -3,6 +3,5 @@ import styled from 'styled-components'; export const ShareButtonContainer = styled.div` display: flex; justify-content: flex-end; - width: 100%; cursor: pointer; `; diff --git a/frontend/src/pages/ClubDetailPage/components/ShareButton/ShareButton.tsx b/frontend/src/pages/ClubDetailPage/components/ShareButton/ShareButton.tsx index 4399e23ac..a038e6d7f 100644 --- a/frontend/src/pages/ClubDetailPage/components/ShareButton/ShareButton.tsx +++ b/frontend/src/pages/ClubDetailPage/components/ShareButton/ShareButton.tsx @@ -1,8 +1,7 @@ -import React from 'react'; import * as Styled from './ShareButton.styles'; import { useGetClubDetail } from '@/hooks/queries/club/useGetClubDetail'; import useMixpanelTrack from '@/hooks/useMixpanelTrack'; -import KakaoIcon from '@/assets/images/icons/kakaotalk_sharing_btn_small.png'; +import ShareIcon from '@/assets/images/icons/share_filled_icon.svg'; import { EVENT_NAME } from '@/constants/eventName'; interface ShareButtonProps { @@ -17,12 +16,14 @@ const ShareButton = ({ clubId }: ShareButtonProps) => { const { data: clubDetail } = useGetClubDetail(clubId); const trackEvent = useMixpanelTrack(); + if (!clubDetail) return; + const handleShare = () => { if (!window.Kakao || !window.Kakao.isInitialized()) { alert('카카오 SDK가 아직 준비되지 않았습니다.'); return; } - if (!clubDetail) return; + window.Kakao.Share.sendDefault({ objectType: 'feed', content: { @@ -53,7 +54,7 @@ const ShareButton = ({ clubId }: ShareButtonProps) => { role='button' aria-label='카카오톡으로 동아리 정보 공유하기' > - 카카오톡 공유 + 카카오톡 공유 ); }; diff --git a/frontend/src/pages/IntroducePage/IntroducePage.styles.ts b/frontend/src/pages/IntroducePage/IntroducePage.styles.ts index 361c0029e..f30bf0e2e 100644 --- a/frontend/src/pages/IntroducePage/IntroducePage.styles.ts +++ b/frontend/src/pages/IntroducePage/IntroducePage.styles.ts @@ -1,21 +1,18 @@ import styled from 'styled-components'; -import { HeaderStyles } from '@/components/common/Header/Header.styles'; -import { FooterContainer } from '@/components/common/Footer/Footer.styles'; -export const IntroducePageHeader = styled(HeaderStyles)` - max-width: none; - - @media (max-width: 500px) { - display: flex; - } +export const IntroducePageHeader = styled.header` + width: 100%; + background: #fff; `; - -export const IntroducePageFooter = styled(FooterContainer)` - margin-top: -50px; +export const IntroducePageFooter = styled.footer` + background: #fff; + border-top: 1px solid #eee; `; - -export const IntroduceImage = styled.img` - width: 100vw; - height: auto; - margin-top: 62px; +export const Main = styled.main` + background: #fff; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + overflow-x: hidden; `; diff --git a/frontend/src/pages/IntroducePage/IntroducePage.test.tsx b/frontend/src/pages/IntroducePage/IntroducePage.test.tsx deleted file mode 100644 index d519cded3..000000000 --- a/frontend/src/pages/IntroducePage/IntroducePage.test.tsx +++ /dev/null @@ -1,48 +0,0 @@ -import { render, screen, fireEvent } from '@testing-library/react'; -import '@testing-library/jest-dom'; -import IntroducePage from './IntroducePage'; - -jest.mock('@/components/common/Header/Header', () => ({ - __esModule: true, - default: () =>
Header
, -})); - -jest.mock('@/components/common/Footer/Footer', () => ({ - __esModule: true, - default: () =>
Footer
, -})); - -jest.mock('@/components/common/Spinner/Spinner', () => ({ - __esModule: true, - default: () =>
Loading...
, -})); - -jest.mock('@/assets/images/introduce/Introduce.png', () => 'mocked-image-path'); - -describe('IntroducePage 컴포넌트', () => { - it('초기 상태에서는 스피너가 표시되고 이미지는 숨겨진다', () => { - render(); - - expect(screen.getByTestId('mock-spinner')).toBeInTheDocument(); - - const image = screen.getByAltText('소개 이미지'); - expect(image).toHaveStyle({ display: 'none' }); - }); - - it('이미지 로드가 완료되면 스피너가 사라지고 이미지가 표시된다', () => { - render(); - - const image = screen.getByAltText('소개 이미지'); - fireEvent.load(image); - - expect(screen.queryByTestId('mock-spinner')).not.toBeInTheDocument(); - expect(image).toHaveStyle({ display: 'block' }); - }); - - it('Header와 Footer가 올바르게 렌더링된다', () => { - render(); - - expect(screen.getByTestId('mock-header')).toBeInTheDocument(); - expect(screen.getByTestId('mock-footer')).toBeInTheDocument(); - }); -}); diff --git a/frontend/src/pages/IntroducePage/IntroducePage.tsx b/frontend/src/pages/IntroducePage/IntroducePage.tsx index 5dfc2f6c4..58e5c0fa0 100644 --- a/frontend/src/pages/IntroducePage/IntroducePage.tsx +++ b/frontend/src/pages/IntroducePage/IntroducePage.tsx @@ -1,25 +1,29 @@ -import React, { useState } from 'react'; import * as Styled from './IntroducePage.styles'; import Header from '@/components/common/Header/Header'; import Footer from '@/components/common/Footer/Footer'; -import Spinner from '@/components/common/Spinner/Spinner'; -import IntroduceImage from '@/assets/images/introduce/Introduce.png'; +import IntroSection from './components/sections/1.IntroSection/IntroSection'; +import ProblemSection from './components/sections/2.ProblemSection/ProblemSection'; +import QuestionSection from './components/sections/3.QuestionSection/QuestionSection'; +import CatchphraseSection from './components/sections/4.CatchphraseSection/CatchphraseSection'; +import FeatureSection from './components/sections/5.FeatureSection/FeatureSection'; +import ConvenienceSection from './components/sections/6.ConvenienceSection/ConvenienceSection'; +import ContactSection from './components/sections/7.ContactSection/ContactSection'; const IntroducePage = () => { - const [loading, setLoading] = useState(true); - return ( <>
- {loading && } - setLoading(false)} - /> + + + + + + + + +