diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx
index 5e2e23096..4f6b76937 100644
--- a/frontend/src/App.tsx
+++ b/frontend/src/App.tsx
@@ -19,6 +19,8 @@ import PhotoEditTab from '@/pages/AdminPage/tabs/PhotoEditTab/PhotoEditTab';
import ApplicationFormPage from './pages/ApplicationFormPage/ApplicationFormPage';
import ApplicantsTab from './pages/AdminPage/tabs/ApplicantsTab/ApplicantsTab';
import ApplicantDetailPage from './pages/AdminPage/tabs/ApplicantsTab/ApplicantDetailPage/ApplicantDetailPage';
+import ClubUnionPage from './pages/ClubUnionPage/ClubUnionPage';
+
const queryClient = new QueryClient();
@@ -72,8 +74,6 @@ const App = () => {
path='account-edit'
element={}
/>
- {/*π λ©μΈ λΈλμΉμμλ μ κ·Ό μ°¨λ¨ (λ°°ν¬μ© μ°¨λ¨ λͺ©μ )*/}
- {/*develop-fe λΈλμΉμμλ μ κ·Ό κ°λ₯νλλ‘ νκ³ κ°λ° μμ */}
}
@@ -92,12 +92,11 @@ const App = () => {
}
/>
- {/*π μ¬μ©μμ© μ§μμ μμ± νμ΄μ§λ λ©μΈμμλ λΉνμ±ν μ²λ¦¬ */}
- {/*π develop-feμμλ λ€μ λ
ΈμΆ μμ */}
}
/>
+ } />
} />
diff --git a/frontend/src/components/common/Header/Header.tsx b/frontend/src/components/common/Header/Header.tsx
index 6a571ca54..9f9532221 100644
--- a/frontend/src/components/common/Header/Header.tsx
+++ b/frontend/src/components/common/Header/Header.tsx
@@ -1,139 +1,156 @@
+import { memo } from 'react';
import { useLocation } from 'react-router-dom';
import * as Styled from './Header.styles';
+
import SearchBox from '@/pages/MainPage/components/SearchBox/SearchBox';
-import useHeaderService from '@/services/header/useHeaderService';
-import useMobileMenu from '@/services/header/useMobileMenu';
import useIsMobile from '@/hooks/useIsMobile';
+import useMobileMenu from '@/services/header/useMobileMenu';
+import useHeaderService from '@/services/header/useHeaderService';
+
import DesktopMainIcon from '@/assets/images/moadong_name_logo.svg';
import MobileMainIcon from '@/assets/images/logos/moadong_mobile_logo.svg';
-import MenuBar from '@/assets/images/icons/menu_button_icon.svg';
+import MenuBarIcon from '@/assets/images/icons/menu_button_icon.svg';
import DeleteIcon from '@/assets/images/introduce/delete.png';
-interface MobileHeaderProps {
- handleHomeClick: (device: 'mobile' | 'desktop') => void;
- handleMenuClick: () => void;
+interface NavLinkData {
+ label: string;
+ handler: () => void;
}
interface DesktopHeaderProps {
isAdminPage: boolean;
- handleHomeClick: (device: 'mobile' | 'desktop') => void;
- handleIntroduceClick: () => void;
+ navLinks: NavLinkData[];
+ onHomeClick: () => void;
}
-interface MobileMenuProp {
+interface MobileHeaderProps {
+ onHomeClick: () => void;
+ onMenuClick: () => void;
+}
+
+interface MobileMenuDrawerProps {
isOpen: boolean;
onClose: () => void;
- handleHomeClick: (device: 'mobile' | 'desktop') => void;
- handleIntroduceClick: () => void;
+ navLinks: NavLinkData[];
+ onHomeClick: () => void;
}
-const MobileMenuDrawer = ({
- isOpen,
- onClose,
- handleHomeClick,
- handleIntroduceClick,
-}: MobileMenuProp) => {
- return (
+const DesktopHeader = memo(
+ ({ isAdminPage, navLinks, onHomeClick }: DesktopHeaderProps) => (
+
+
+
+
+
+
+ {!isAdminPage &&
+ navLinks.map((link) => (
+
+ {link.label}
+
+ ))}
+
+ {!isAdminPage && }
+
+
+ ),
+);
+
+const MobileMenuDrawer = memo(
+ ({ isOpen, onClose, navLinks, onHomeClick }: MobileMenuDrawerProps) => (
-
-
- handleHomeClick('mobile')}
- />
-
-
-
- λͺ¨μλ μκ°
+
+
+
+
+ {navLinks.map((link) => (
+ {
+ link.handler();
+ onClose();
+ }}
+ >
+ {link.label}
-
+ ))}
- );
-};
+ ),
+);
-const MobileHeader = ({
- handleHomeClick,
- handleMenuClick,
-}: MobileHeaderProps) => (
+const MobileHeader = memo(({ onHomeClick, onMenuClick }: MobileHeaderProps) => (
-
-
handleHomeClick('mobile')}
- />
+
+
-
-
+
+
-);
-
-const DesktopHeader = ({
- isAdminPage,
- handleHomeClick,
- handleIntroduceClick,
-}: DesktopHeaderProps) => (
-
-
-
-
-
handleHomeClick('desktop')}
- />
-
- {!isAdminPage && (
-
- λͺ¨μλ μκ°
-
- )}
-
- {!isAdminPage && }
-
-
-);
+));
const Header = () => {
const isMobile = useIsMobile();
const location = useLocation();
const isAdminPage = location.pathname.startsWith('/admin');
- const { handleHomeClick, handleIntroduceClick, handleMenuClick } =
- useHeaderService();
+ const {
+ handleHomeClick,
+ handleIntroduceClick,
+ handleClubUnionClick,
+ handleMenuClick,
+ } = useHeaderService();
const { isMenuOpen, openMenu, closeMenu } = useMobileMenu({
handleMenuClick,
});
- return isMobile ? (
+ const navLinks: NavLinkData[] = [
+ { label: 'λͺ¨μλ μκ°', handler: handleIntroduceClick },
+ { label: 'μ΄λμ리μ°ν©ν μκ°', handler: handleClubUnionClick },
+ ];
+
+ return (
<>
-
+ {isMobile ? (
+ handleHomeClick('mobile')}
+ onMenuClick={openMenu}
+ />
+ ) : (
+ handleHomeClick('desktop')}
+ />
+ )}
{
+ handleHomeClick('mobile');
+ closeMenu();
+ }}
/>
>
- ) : (
-
);
};
diff --git a/frontend/src/constants/CLUB_UNION_INFO.ts b/frontend/src/constants/CLUB_UNION_INFO.ts
new file mode 100644
index 000000000..f3e004ef9
--- /dev/null
+++ b/frontend/src/constants/CLUB_UNION_INFO.ts
@@ -0,0 +1,122 @@
+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';
+
+export interface ClubUnionMember {
+ id: number;
+ name: string;
+ role: string;
+ description: string;
+ imageSrc: string;
+}
+
+const MEMBER_AVATARS = {
+ PRESIDENT: PresidentAvatar,
+ VICE_PRESIDENT: PresidentAvatar,
+ SECRETARY: PresidentAvatar,
+ PROMOTION: PresidentAvatar,
+ RELIGION: ReligionAvatar,
+ HOBBY: HobbyAvatar,
+ STUDY: StudyAvatar,
+ VOLUNTEER: VolunteerAvatar,
+ PERFORMANCE: PerformanceAvatar,
+ SPORT: SportAvatar,
+};
+
+// κ°λ°μ κ°μ΄λ: description νλλ UIκ° κΉ¨μ§μ§ μλλ‘ κΈμ μλ₯Ό μ νν©λλ€.
+// (κΆμ₯) λͺ¨λ°μΌ: 50μ μ΄λ΄, λ°μ€ν¬ν±: 100μ μ΄λ΄
+export const CLUB_UNION_MEMBERS: ClubUnionMember[] = [
+ {
+ id: 1,
+ name: 'μ΄μ μ',
+ role: 'νμ₯',
+ description: 'λΆκ²½λνκ΅μ μ€μλμ리, μ¨ μ΄λμ리μ°ν©νκ° μ±
μμ§κ² μ΅λλ€.',
+ imageSrc: MEMBER_AVATARS.PRESIDENT,
+ },
+ {
+ id: 2,
+ name: 'κΉνμ°',
+ role: 'λΆνμ₯',
+ description:
+ 'μ¬λ¬λΆμ λμ리 μνμ΄ νμλ‘μμ§ μ μλλ‘ νμ 보νκ² μ΅λλ€.',
+ imageSrc: MEMBER_AVATARS.VICE_PRESIDENT,
+ },
+ {
+ id: 3,
+ name: 'μ΅μ§ν',
+ role: 'μ¬λ¬΄κ΅μ₯',
+ description: 'λμ리λ₯Ό μν΄ λ
Έλ ₯νλ μ¨ μ΄λμ°μκ² λ§μ κ΄μ¬ λΆνλ립λλ€.',
+ imageSrc: MEMBER_AVATARS.SECRETARY,
+ },
+ {
+ id: 4,
+ name: 'μ΄μ£Όμ',
+ role: 'ν보κ΅μ₯',
+ description: 'μ΄λμ°μ κ°μΉλ₯Ό μλ¦¬κ³ μν΅μ μ΄λλ ν보κ΅μ₯μ
λλ€!',
+ imageSrc: MEMBER_AVATARS.PROMOTION,
+ },
+ {
+ id: 5,
+ name: 'κΉλν',
+ role: 'μ’
κ΅λΆκ³Όμ₯',
+ description: 'λ―Ώκ³ λ°λ₯΄λ λ λ ν μ΄λμ°μ μν΄ λ
Έλ ₯νκ² μ΅λλ€.',
+ imageSrc: MEMBER_AVATARS.RELIGION,
+ },
+ {
+ id: 6,
+ name: 'μ΄μ μ',
+ role: 'μ·¨λ―Έκ΅μλΆκ³Όμ₯',
+ description: '2025λ
λμ리λ€μ νλμ΄ μ μ΄λ£¨μ΄μ§λλ‘ μ΄μ¬ν νκ² μ΅λλ€.',
+ imageSrc: MEMBER_AVATARS.HOBBY,
+ },
+ {
+ id: 7,
+ name: 'μ±κΈ°μΌ',
+ role: 'νμ λΆκ³Όμ₯',
+ description: '곡λΆλ λμ리λ ν¬κΈ° λͺ» νλ νμ λΆκ³Όμ₯μ
λλ€!',
+ imageSrc: MEMBER_AVATARS.STUDY,
+ },
+ {
+ id: 8,
+ name: 'κΉνμ§',
+ role: 'λ΄μ¬λΆκ³Όμ₯',
+ description: 'λν μνμ κ½, λ΄μ¬λμ리 λ§μ κ΄μ¬ λΆνλ립λλ€!',
+ imageSrc: MEMBER_AVATARS.VOLUNTEER,
+ },
+ {
+ id: 9,
+ name: 'λ°μ§μ€',
+ role: '곡μ°1λΆκ³Όμ₯',
+ description:
+ 'λ λμ λμ리 μνμ λ§λ€κΈ° μν΄ μ±
μκ°μ κ°μ§κ³ μνκ² μ΅λλ€!',
+ imageSrc: MEMBER_AVATARS.PERFORMANCE,
+ },
+ {
+ id: 10,
+ name: 'κ³ λ³΄λ―Ό',
+ role: '곡μ°2λΆκ³Όμ₯',
+ description:
+ '곡μ°2λΆκ³Όμ μνν μ΄μκ³Ό λ©μ§ 무λλ₯Ό μν΄ μ΅μ μ λ€νκ² μ΅λλ€!',
+ imageSrc: MEMBER_AVATARS.PERFORMANCE,
+ },
+ {
+ id: 11,
+ name: 'μ΄κΈμ ',
+ role: 'μ΄λ1λΆκ³Όμ₯',
+ description:
+ 'μ΄μ κ°λν μ΄λ1λΆκ³Ό! λΆκ²½λ νμ° μ¬λ¬λΆμ νκΈ°μ°¬ λμ리 νλμ μν΄ μ΅μ μ λ€νκ² μ΅λλ€!',
+ imageSrc: MEMBER_AVATARS.SPORT,
+ },
+ {
+ id: 12,
+ name: 'μ΄μλ―Ό',
+ role: 'μ΄λ2λΆκ³Όμ₯',
+ description:
+ 'μ¬λ―Έμ κ±΄κ° λ λ§λ¦¬ ν λΌλ₯Ό μ‘μ μ μλ μ΄λ2λΆκ³Όμμ μ¬λ¬λΆμ λ§λ¬μΌλ©΄ μ’κ² μ΅λλ€!',
+ imageSrc: MEMBER_AVATARS.SPORT,
+ },
+];
diff --git a/frontend/src/constants/eventName.ts b/frontend/src/constants/eventName.ts
index fc9440017..76cb84657 100644
--- a/frontend/src/constants/eventName.ts
+++ b/frontend/src/constants/eventName.ts
@@ -15,4 +15,5 @@ export const EVENT_NAME = {
MOBILE_MENU_BUTTON_CLICKED: 'Mobile Menu Button Clicked' as const,
MOBILE_MENU_DELETE_BUTTON_CLICKED:
'Mobile Menubar delete Button Clicked' as const,
+ CLUB_UNION_BUTTON_CLICKED: 'Club Union Button Clicked' as const,
} as const;
diff --git a/frontend/src/pages/ClubUnionPage/ClubUnionPage.styles.ts b/frontend/src/pages/ClubUnionPage/ClubUnionPage.styles.ts
new file mode 100644
index 000000000..93bd8d237
--- /dev/null
+++ b/frontend/src/pages/ClubUnionPage/ClubUnionPage.styles.ts
@@ -0,0 +1,139 @@
+import styled from 'styled-components';
+import { media } from '@/styles/mediaQuery';
+
+export const Title = styled.h1`
+ font-size: 2.5rem;
+ font-weight: 800;
+ margin-top: 100px;
+ margin-bottom: 40px;
+ text-align: center;
+ color: #222;
+
+ ${media.mobile} {
+ font-size: 2rem;
+ margin-top: 80px;
+ margin-bottom: 30px;
+ }
+`;
+
+export const IntroductionText = styled.p`
+ font-size: 1.1rem;
+ line-height: 1.7;
+ text-align: center;
+ color: #555;
+ max-width: 600px;
+ margin: 0 auto 60px;
+
+ ${media.mobile} {
+ font-size: 1rem;
+ margin-bottom: 40px;
+ }
+`;
+
+export const ProfileGrid = styled.div`
+ display: grid;
+ grid-template-columns: repeat(auto-fill, minmax(220px, 1fr));
+ gap: 40px 30px;
+
+ ${media.tablet} {
+ grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
+ gap: 30px 20px;
+ }
+
+ ${media.mobile} {
+ grid-template-columns: repeat(2, 1fr);
+ gap: 20px 15px;
+ }
+`;
+
+export const InfoOverlay = styled.div`
+ position: absolute;
+ top: 0;
+ left: 0;
+ width: 100%;
+ height: 100%;
+ background-color: rgba(0, 0, 0, 0.65);
+ color: white;
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ justify-content: center;
+ text-align: center;
+ padding: 15px;
+ opacity: 0;
+ transition: opacity 0.3s ease;
+ border-radius: 50%;
+ box-sizing: border-box;
+`;
+
+export const ProfileImage = styled.img`
+ width: 100%;
+ height: 100%;
+ object-fit: cover;
+ transition:
+ transform 0.3s ease,
+ filter 0.3s ease;
+`;
+
+export const NameBadge = styled.div`
+ position: absolute;
+ bottom: 10%;
+ left: 50%;
+ transform: translateX(-50%);
+ background-color: rgba(255, 255, 255, 0.8);
+ color: #333;
+ padding: 5px 15px;
+ border-radius: 15px;
+ font-weight: 600;
+ font-size: 1rem;
+ transition: opacity 0.3s ease;
+ white-space: nowrap;
+`;
+
+export const ProfileCardContainer = styled.div`
+ position: relative;
+ aspect-ratio: 1 / 1;
+ border-radius: 50%;
+ overflow: hidden;
+ cursor: pointer;
+ box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
+
+ &:hover {
+ ${ProfileImage} {
+ transform: scale(1.1);
+ filter: brightness(0.5);
+ }
+ ${InfoOverlay} {
+ opacity: 1;
+ }
+ ${NameBadge} {
+ opacity: 0;
+ }
+ }
+`;
+
+// μ€λ²λ μ΄ λ΄λΆ ν
μ€νΈ μ€νμΌ
+export const Role = styled.p`
+ font-size: 1.1rem;
+ font-weight: 700;
+ color: #ff5414;
+ margin: 0 0 8px;
+`;
+
+export const Name = styled.p`
+ font-size: 1.3rem;
+ font-weight: 800;
+ margin: 0 0 12px;
+`;
+
+export const Description = styled.p`
+ font-size: 0.9rem;
+ line-height: 1.5;
+ margin: 0;
+`;
+
+export const Contact = styled.p`
+ font-size: 0.9rem;
+ margin-top: 12px;
+ opacity: 0.8;
+`;
diff --git a/frontend/src/pages/ClubUnionPage/ClubUnionPage.tsx b/frontend/src/pages/ClubUnionPage/ClubUnionPage.tsx
new file mode 100644
index 000000000..b7dba7ef6
--- /dev/null
+++ b/frontend/src/pages/ClubUnionPage/ClubUnionPage.tsx
@@ -0,0 +1,43 @@
+import Header from '@/components/common/Header/Header';
+import * as Styled from './ClubUnionPage.styles';
+import { CLUB_UNION_MEMBERS } from '@/constants/CLUB_UNION_INFO';
+import { PageContainer } from '@/styles/PageContainer.styles';
+import Footer from '@/components/common/Footer/Footer';
+
+const ClubUnionPage = () => {
+ return (
+ <>
+
+
+ μ΄λμ리μ°ν©ν μκ°
+
+ μλ
νμΈμ! λΆκ²½λνκ΅ μ 16λ μ΄λμ리μ°ν©ν 'μ¨'μ
λλ€.
+
μ¨ λμ리λ₯Ό μνμ¬, μ¨ νμ λ€ν΄.
+
+
+ {CLUB_UNION_MEMBERS.map((member) => (
+
+
+
+ {/* νμμ 보μ΄λ μ΄λ¦ λ°°μ§ */}
+ {member.name}
+
+ {/* νΈλ² μ λνλλ μ 보 */}
+
+ {member.role}
+ {member.name}
+ {member.description}
+
+
+ ))}
+
+
+
+ >
+ );
+};
+
+export default ClubUnionPage;
diff --git a/frontend/src/services/header/useHeaderService.ts b/frontend/src/services/header/useHeaderService.ts
index 3792b7241..83b247b94 100644
--- a/frontend/src/services/header/useHeaderService.ts
+++ b/frontend/src/services/header/useHeaderService.ts
@@ -1,3 +1,4 @@
+import { useCallback } from 'react';
import { useNavigate } from 'react-router-dom';
import { useSearch } from '@/context/SearchContext';
import useMixpanelTrack from '@/hooks/useMixpanelTrack';
@@ -13,25 +14,34 @@ const useHeaderService = () => {
const navigate = useNavigate();
const trackEvent = useMixpanelTrack();
- const navigateToHome = (device: keyof typeof trackEventNames) => {
- navigate('/');
- setKeyword('');
- setInputValue('');
- trackEvent(trackEventNames[device]);
- };
+ const navigateToHome = useCallback(
+ (device: keyof typeof trackEventNames) => {
+ navigate('/');
+ setKeyword('');
+ setInputValue('');
+ trackEvent(trackEventNames[device]);
+ },
+ [navigate, setKeyword, setInputValue, trackEvent],
+ );
- const goIntroducePage = () => {
+ const handleIntroduceClick = useCallback(() => {
navigate('/introduce');
trackEvent(EVENT_NAME.INTRODUCE_BUTTON_CLICKED);
- };
+ }, [navigate, trackEvent]);
- const handleMenuClick = () => {
+ const handleClubUnionClick = useCallback(() => {
+ navigate('/club-union');
+ trackEvent(EVENT_NAME.CLUB_UNION_BUTTON_CLICKED);
+ }, [navigate, trackEvent]);
+
+ const handleMenuClick = useCallback(() => {
trackEvent(EVENT_NAME.MOBILE_MENU_BUTTON_CLICKED);
- };
+ }, [trackEvent]);
return {
handleHomeClick: navigateToHome,
- handleIntroduceClick: goIntroducePage,
+ handleIntroduceClick,
+ handleClubUnionClick,
handleMenuClick,
};
};