diff --git a/public/images/Artboard_1.png b/public/images/Artboard_1.png new file mode 100644 index 0000000..877c64b Binary files /dev/null and b/public/images/Artboard_1.png differ diff --git a/src/components/atoms/Divider/index.tsx b/src/components/atoms/Divider/index.tsx new file mode 100644 index 0000000..5473b95 --- /dev/null +++ b/src/components/atoms/Divider/index.tsx @@ -0,0 +1,34 @@ +import { ComponentProps } from 'react'; + +interface Props extends ComponentProps<'div'> { + space?: number; + size?: 'sm' | 'md' | 'lg'; + color?: string; + type?: 'vertical' | 'horizontal'; +} + +const Divider = ({ + space = 22, + size = 'md', + color = '#ccc', + type = 'horizontal', + ...props +}: Props) => { + const horizontalStyle = { + marginTop: space, + marginBottom: space, + background: color, + height: size === 'md' ? '2px' : size === 'lg' ? '3px' : '1px', + }; + + const verticalStyle = { + marginLeft: space, + marginRight: space, + background: color, + width: size === 'md' ? '2px' : size === 'lg' ? '3px' : '1px', + }; + + return
; +}; + +export default Divider; diff --git a/src/components/layout/header/index.tsx b/src/components/layout/header/index.tsx index f13c793..4cb147d 100644 --- a/src/components/layout/header/index.tsx +++ b/src/components/layout/header/index.tsx @@ -8,7 +8,7 @@ import { MENU_ITEMS } from '@/constants/menus'; import toast from 'react-hot-toast'; import { signOut } from '@/components/signIn/utils/logout'; -type MenuType = 'community' | 'study' | 'project' | 'portfolio' | 'review'; +type MenuType = 'community' | 'study' | 'project' | 'portfolio' | 'review' | 'activity'; const Header = () => { const [selectedMenu, setSelectedMenu] = useState(null); @@ -45,7 +45,7 @@ const Header = () => { if (liElement) { const menuItem = liElement.getAttribute('data-menu') as MenuType; if (menuItem) { - if (menuItem !== 'community' && menuItem !== 'review') { + if (menuItem !== 'community' && menuItem !== 'review' && menuItem !== 'activity') { alert('준비중입니다.'); navigate(`/community`); } diff --git a/src/constants/menus.ts b/src/constants/menus.ts index a7eba73..23cea72 100644 --- a/src/constants/menus.ts +++ b/src/constants/menus.ts @@ -1,6 +1,7 @@ export const MENU_ITEMS = [ { name: '소근소근', path: 'community' }, { name: '스터디', path: 'study' }, + { name: '활동', path: 'activity' }, { name: '프로젝트', path: 'project' }, { name: '포트폴리오', path: 'portfolio' }, { name: '리뷰', path: 'review' }, diff --git a/src/pages/activity/components/Item.module.scss b/src/pages/activity/components/Item.module.scss new file mode 100644 index 0000000..855deec --- /dev/null +++ b/src/pages/activity/components/Item.module.scss @@ -0,0 +1,79 @@ +@use '@/styles/abstracts/variables' as v; +@use '@/styles/abstracts/mixins' as m; + +.list { + width: 33.333%; + + @include m.mobile { + width: 50%; + } + + margin-bottom: 3rem; + + & > button { + display: flex; + flex-flow: column; + + box-shadow: rgba(120, 120, 120, 0.2) 0px 1px 8px 0px; + border-radius: 10px; + + padding: 0.5rem; + + width: 220px; + + border: 2px solid v.$extra-light-gray; + + background-color: transparent; + + &:hover { + box-shadow: v.$red-100 0px 2px 8px 0px; + } + // 2 5 8 11 + + & > div:first-of-type { + border-radius: 10px; + overflow: hidden; + + height: 200px; + + & > img { + display: block; + width: 100%; + height: 100%; + + object-fit: cover; + } + } + & > div:nth-of-type(2) { + font-size: 1.2rem; + width: 90%; + + text-align: left; + + overflow: hidden; + white-space: nowrap; + text-overflow: ellipsis; + + padding: 0.7rem 0; + } + & > div:last-of-type { + display: flex; + justify-content: space-between; + + color: v.$gray; + font-size: 0.9rem; + + width: 100%; + + & > span:first-of-type { + width: 80%; + + text-align: left; + + overflow: hidden; + white-space: nowrap; + text-overflow: ellipsis; + } + } + } +} diff --git a/src/pages/activity/components/Item.tsx b/src/pages/activity/components/Item.tsx new file mode 100644 index 0000000..80c4045 --- /dev/null +++ b/src/pages/activity/components/Item.tsx @@ -0,0 +1,38 @@ +import { ComponentProps } from 'react'; +import { useNavigate } from 'react-router-dom'; +import styles from './Item.module.scss'; + +interface Props extends ComponentProps<'li'> { + title: string; + organization: string; + imageUrl: string; + dDay: number; + itemId: number; + type?: 'activities' | 'competitions'; +} + +const Item = ({ title, organization, imageUrl, dDay, itemId, type }: Props) => { + const navigate = useNavigate(); + + return ( +
  • + +
  • + ); +}; + +export default Item; diff --git a/src/pages/activity/components/StatusBadge.module.scss b/src/pages/activity/components/StatusBadge.module.scss new file mode 100644 index 0000000..6ca8a10 --- /dev/null +++ b/src/pages/activity/components/StatusBadge.module.scss @@ -0,0 +1,33 @@ +@use '@/styles/abstracts/variables' as v; + +.status { + position: relative; + background-color: transparent; + + margin-right: 1rem; + padding: 0.2rem 0.6rem; + + border: 1px solid v.$light-gray; + border-radius: 1rem; + + &.active { + border: 1px solid v.$red-300; + } + + & > span:first-of-type { + display: inline-block; + + margin-right: 0.5rem; + + width: 10px; + height: 10px; + + background-color: v.$light-gray; + + border-radius: 50%; + } + + & > span.active:first-of-type { + background-color: v.$red-300; + } +} diff --git a/src/pages/activity/components/StatusBadge.tsx b/src/pages/activity/components/StatusBadge.tsx new file mode 100644 index 0000000..2d55682 --- /dev/null +++ b/src/pages/activity/components/StatusBadge.tsx @@ -0,0 +1,20 @@ +import { ComponentProps } from 'react'; +import styles from './StatusBadge.module.scss'; + +interface Props extends ComponentProps<'button'> { + text: string; + active?: boolean; +} + +const StatusBadge = ({ text, active = false, ...props }: Props) => { + const className = `${styles.status} ${active ? styles.active : ''}`; + + return ( + + ); +}; + +export default StatusBadge; diff --git a/src/pages/activity/components/TabTitle.module.scss b/src/pages/activity/components/TabTitle.module.scss new file mode 100644 index 0000000..0766313 --- /dev/null +++ b/src/pages/activity/components/TabTitle.module.scss @@ -0,0 +1,38 @@ +@use '@/styles/abstracts/variables' as v; + +.title { + position: relative; + + margin: 0 1rem 0.5rem 0; + + font-size: v.$text-xl; + font-weight: v.$font-medium; + + background-color: transparent; + + &::before { + position: absolute; + content: ''; + bottom: 0; + left: 0; + height: 1rem; + width: 0; + z-index: -1; + background-color: v.$red-200; + opacity: 0.5; + + transition: all 0.2s; + } + + &.active::before { + position: absolute; + content: ''; + bottom: 0; + left: 0; + height: 1rem; + width: 100%; + z-index: -1; + background-color: v.$red-200; + opacity: 0.5; + } +} diff --git a/src/pages/activity/components/TabTitle.tsx b/src/pages/activity/components/TabTitle.tsx new file mode 100644 index 0000000..012b12f --- /dev/null +++ b/src/pages/activity/components/TabTitle.tsx @@ -0,0 +1,19 @@ +import { ComponentProps } from 'react'; +import styles from './TabTitle.module.scss'; + +interface Props extends ComponentProps<'button'> { + text: string; + active?: boolean; +} + +const TabTitle = ({ text, active = false, ...props }: Props) => { + const className = `${styles.title} ${active ? styles.active : ''}`; + + return ( + + ); +}; + +export default TabTitle; diff --git a/src/pages/activity/constants.ts b/src/pages/activity/constants.ts new file mode 100644 index 0000000..7f1c2f7 --- /dev/null +++ b/src/pages/activity/constants.ts @@ -0,0 +1,151 @@ +export const listARes = { + code: 200, + message: '공모전 목록을 성공적으로 조회했습니다.', + data: { + totalPages: 4, + isLastPage: true, + totalCompetitions: 20, + competitions: [ + { + id: 0, + title: '공모전 제목dddddddddddddddddddddd', + organization: '공모전 주최ㅇㅇㅇㅇㅇㅇㅇㅇㅇㅇㅇㅇㅇㅇㅇ', + imageUrl: '/images/Artboard_1.png', + dDay: 20, + }, + { + id: 1, + title: '공모전 제목', + organization: '공모전 주최', + imageUrl: '/images/Artboard_1.png', + dDay: 20, + }, + { + id: 2, + title: '공모전 제목', + organization: '공모전 주최', + imageUrl: '/images/Artboard_1.png', + dDay: 20, + }, + { + id: 3, + title: '공모전 제목', + organization: '공모전 주최', + imageUrl: '/images/Artboard_1.png', + dDay: 20, + }, + { + id: 4, + title: '공모전 제목', + organization: '공모전 주최', + imageUrl: '/images/Artboard_1.png', + dDay: 20, + }, + { + id: 5, + title: '공모전 제목', + organization: '공모전 주최', + imageUrl: '/images/Artboard_1.png', + dDay: 20, + }, + ], + }, +}; + +export const listBRes = { + code: 200, + message: '대외 활동 목록을 성공적으로 조회했습니다.', + data: { + totalPages: 11, + isLastPage: true, + totalActivities: 63, + activities: [ + { + id: 0, + title: '대외 활동 제목dddddddddddddddddddddd', + organization: '대외 활동 주최ㅇㅇㅇㅇㅇㅇㅇㅇㅇㅇㅇㅇㅇㅇㅇ', + imageUrl: '/images/Artboard_1.png', + dDay: 20, + }, + { + id: 1, + title: '대외 활동 제목', + organization: '대외 활동 주최', + imageUrl: '/images/Artboard_1.png', + dDay: 20, + }, + { + id: 2, + title: '대외 활동 제목', + organization: '대외 활동 주최', + imageUrl: '/images/Artboard_1.png', + dDay: 20, + }, + { + id: 3, + title: '대외 활동 제목', + organization: '대외 활동 주최', + imageUrl: '/images/Artboard_1.png', + dDay: 20, + }, + { + id: 4, + title: '대외 활동 제목', + organization: '대외 활동 주최', + imageUrl: '/images/Artboard_1.png', + dDay: 20, + }, + { + id: 5, + title: '대외 활동 제목', + organization: '대외 활동 주최', + imageUrl: '/images/Artboard_1.png', + dDay: 20, + }, + ], + }, +}; + +export const itemADetail = { + code: 200, + message: '공모전 상세 정보를 성공적으로 조회했습니다.', + data: { + id: 1, + title: '한국 전력 공사 IT 대외활동 (공)', + organization: '한국전력공사 (공)', + corporate_type: '중소기업 (공)', + participate: '대학생 (공)', + award_scale: '5000만 원 (공)', + start_date: '23.1.2', + end_date: '23.2.3', + homepageUrl: 'https://blog.naver.com/gyeryongcity1/222972814128', + activity_benefit: '기타, 상장 수여 (공)', + bonus_benefit: '해외 대학 대학원 추천 제도 (공)', + description: '전액 무료 ! (공)', + imageUrl: '/images/Artboard_1.png', + }, +}; + +export const itemBDetail = { + code: 200, + message: '대외 활동 상세 정보를 성공적으로 조회했습니다.', + data: { + id: 1, + title: '한국 전력 공사 IT 대외활동 (대)', + organization: '한국전력공사 (대)', + corporate_type: '중소기업 (대)', + participate: '대학생 (대)', + start_date: '2023-01-02', + end_date: '2023-02-03', + period: '2023-03-02 ~ 2023-12-31', + recruitment: '30', + area: '부산 (대)', + preferred_skills: '컴퓨터활용자격증보유 (대)', + homepageUrl: 'https://blog.naver.com/gyeryongcity1/222972814128', + activity_benefit: '실무 교육, 수료증 및 인증서 (대)', + activity_field: '멘토링 (대)', + bonus_benefit: '훈련장려금 지급 (대)', + description: '전액 무료 ! (대)', + imageUrl: '/images/Artboard_1.png', + }, +}; diff --git a/src/pages/activity/index.module.scss b/src/pages/activity/index.module.scss new file mode 100644 index 0000000..520c798 --- /dev/null +++ b/src/pages/activity/index.module.scss @@ -0,0 +1,176 @@ +@use '@/styles/abstracts/variables' as v; +@use '@/styles/abstracts/mixins' as m; + +.container { + max-width: 800px; + margin: 1rem auto; + padding-inline: 1rem; + + @include m.mobile { + max-width: 500px; + } +} + +.tabContainer { + display: flex; + + margin-bottom: 1.7rem; +} + +.searchContainer { + display: flex; + align-items: center; + + & > div:first-of-type { + width: 50%; + } + + @include m.tablet { + flex-direction: column; + align-items: flex-start; + + & > div:first-of-type { + width: 100%; + margin-bottom: 0.4rem; + } + & > div:last-of-type { + width: 300px; + } + } + @include m.mobile { + flex-direction: column; + align-items: flex-start; + + & > div:first-of-type { + width: 100%; + margin-bottom: 1rem; + } + & > div:last-of-type { + width: 300px; + } + } +} + +.searchInputWrap { + display: flex; + align-items: center; + + width: 50%; + + & > div:first-of-type { + position: relative; + + display: flex; + align-items: center; + + flex-grow: 1; + + & > input { + width: 100%; + + padding: 0.4rem; + + border: 1px solid v.$light-gray; + border-radius: 0.3rem; + } + + & > span { + position: absolute; + display: flex; + align-items: center; + + right: 10px; + } + } +} + +.itemOrder { + font-size: 0.7rem; + + & > button { + background-color: transparent; + margin: 0 0.3rem; + } +} + +.listContainer { + display: flex; + flex-flow: row wrap; + + @include m.desktop { + & > li:nth-of-type(3n + 2) { + display: flex; + justify-content: center; + } + & > li:nth-of-type(3n + 3) { + display: flex; + justify-content: flex-end; + } + } + @include m.tablet { + & > li:nth-of-type(3n + 2) { + display: flex; + justify-content: center; + } + & > li:nth-of-type(3n + 3) { + display: flex; + justify-content: flex-end; + } + } + + @include m.mobile { + & > li:nth-of-type(2n) { + display: flex; + justify-content: flex-end; + } + } +} + +.page { + display: flex; + justify-content: center; + align-items: center; + + & > span { + display: flex; + justify-content: center; + align-items: center; + + & > button { + width: 20px; + height: 20px; + + margin: 0.3rem; + + border-radius: 50%; + + &:hover { + background-color: v.$red-100; + } + } + } + + & > button { + padding: 0.3rem; + border-radius: 50%; + + &:hover { + background-color: v.$red-100; + } + } + + & > button:first-of-type { + margin-right: 1.5rem; + } + & > button:last-of-type { + margin-left: 1.5rem; + } + + & button { + display: flex; + justify-content: center; + align-items: center; + + background-color: transparent; + } +} diff --git a/src/pages/activity/index.tsx b/src/pages/activity/index.tsx new file mode 100644 index 0000000..87b2e29 --- /dev/null +++ b/src/pages/activity/index.tsx @@ -0,0 +1,157 @@ +import Divider from '@/components/atoms/Divider'; +import { useState } from 'react'; +import { GrNext, GrPrevious } from 'react-icons/gr'; +import { IoIosSearch } from 'react-icons/io'; +import Item from './components/Item'; +import StatusBadge from './components/StatusBadge'; +import TabTitle from './components/TabTitle'; +import { listARes, listBRes } from './constants'; +import styles from './index.module.scss'; + +const ActivityPage = () => { + const [currentTab, setCurrentTab] = useState(0); + const [currentPage, setCurrentPage] = useState(1); + const [currentPageList, setCurrentPageList] = useState(0); + + const [order, setOrder] = useState<'latest' | 'd-day'>('latest'); + + const [filter, setFilter] = useState([ + { title: '모집 전', active: false }, + { title: '모집 중', active: false }, + { title: '모집 마감', active: false }, + ]); + + const filteringItem = (index: number) => { + setFilter((prev) => + prev.map((item, i) => (i === index ? { ...item, active: !item.active } : item)) + ); + }; + + return ( +
    + {/* 헤더 */} +
    + setCurrentTab(0)} /> + setCurrentTab(1)} /> +
    + {/* 필터링 검색 바 */} +
    +
    + {filter.map((item, index) => { + return ( + filteringItem(index)} + /> + ); + })} +
    +
    +
    + + + + +
    +
    + + +
    +
    +
    + + {/* 아이템 리스트 */} +
      + {currentTab === 0 && + listARes.data.competitions.map((item) => { + return ( + + ); + })} + + {currentTab === 1 && + listBRes.data.activities.map((item) => { + return ( + + ); + })} +
    +
    + + + + {/** * + * TODO: 리스트 아이템 개수 연결 + */} + {Array.from({ length: 49 }, (_, i) => i + 1) + .slice(currentPageList * 5, currentPageList * 5 + 5) + .map((number, index) => { + return ( + + ); + })} + + +
    +
    + ); +}; + +export default ActivityPage; diff --git a/src/pages/activityDetail/index.module.scss b/src/pages/activityDetail/index.module.scss new file mode 100644 index 0000000..3b44aff --- /dev/null +++ b/src/pages/activityDetail/index.module.scss @@ -0,0 +1,126 @@ +@use '@/styles/abstracts/variables' as v; +@use '@/styles/abstracts/mixins' as m; + +.container { + max-width: 1000px; + margin: 1rem auto; + padding-inline: 1rem; + + .title { + position: relative; + + display: inline-block; + + margin-bottom: 0.5rem; + + font-size: v.$text-xl; + font-weight: v.$font-medium; + + &::before { + position: absolute; + content: ''; + bottom: 0; + left: 0; + height: 1rem; + width: 100%; + z-index: -1; + background-color: v.$red-100; + opacity: 0.5; + } + } + + @include m.tablet { + max-width: 680px; + } + @include m.mobile { + max-width: 680px; + } +} + +.infoWrap { + display: flex; + justify-content: space-between; + + & > div { + border-left: 1px solid #ccc; + } + + & > div:first-of-type { + flex-grow: 1; + font-size: 1.3rem; + border: none; + } + & > div:nth-of-type(2) { + flex-grow: 5; + + padding-left: 1rem; + } + & > div:last-of-type { + flex-grow: 5; + + padding-left: 1rem; + } + + & > div > div { + margin-block: 0.3rem; + } + + & > div > div > span:first-of-type { + display: inline-block; + width: 30%; + + font-size: 0.8rem; + } + & > div > div > span:last-of-type { + display: inline-block; + width: 70%; + + white-space: wrap; + color: v.$gray; + + font-size: 0.8rem; + } + + @include m.tablet { + flex-flow: column; + + & > div { + border: none; + } + + & > div:nth-of-type(2) { + padding: 0; + } + & > div:last-of-type { + padding: 0; + } + + & > div:first-of-type { + display: none; + } + } + @include m.mobile { + flex-flow: column; + + & > div { + border: none; + } + + & > div:nth-of-type(2) { + padding: 0; + } + & > div:last-of-type { + padding: 0; + } + + & > div:first-of-type { + display: none; + } + } +} + +.content { + & > img { + width: 100%; + } +} diff --git a/src/pages/activityDetail/index.tsx b/src/pages/activityDetail/index.tsx new file mode 100644 index 0000000..fda0557 --- /dev/null +++ b/src/pages/activityDetail/index.tsx @@ -0,0 +1,148 @@ +import { useLocation } from 'react-router-dom'; +import styles from './index.module.scss'; +import { itemADetail, itemBDetail } from '../activity/constants'; +import Divider from '@/components/atoms/Divider'; + +const ActivityDetailPage = () => { + // const { id } = useParams(); + const location = useLocation(); + const queryParams = new URLSearchParams(location.search); + const tabName = queryParams.get('t'); + + return ( +
    +
    + {tabName === 'competitions' ? itemADetail.data.title : itemBDetail.data.title} +
    + +
    +
    + 기본 +
    + 정보 +
    +
    +
    + {tabName === 'competitions' ? '공모전 주최' : '대외 활동 주최'} + + {tabName === 'competitions' + ? itemADetail.data.organization || '' + : itemBDetail.data.organization} + +
    +
    + 기업 형태 + + {tabName === 'competitions' + ? itemADetail.data.corporate_type || '' + : itemBDetail.data.corporate_type} + +
    +
    + 참여 대상 + + {tabName === 'competitions' + ? itemADetail.data.participate || '' + : itemBDetail.data.participate} + +
    +
    + 접수 기간 + + {tabName === 'competitions' + ? `${itemADetail.data.start_date || ''} ~ ${itemADetail.data.end_date || ''}` || '' + : `${itemBDetail.data.start_date || ''} ~ ${itemBDetail.data.end_date || ''}`} + +
    + + {tabName === 'activities' && ( +
    + 활동 기간 + {itemBDetail.data.period} +
    + )} + {tabName === 'activities' && ( +
    + 모집 인원 + {itemBDetail.data.recruitment}명 +
    + )} +
    +
    + {tabName === 'competitions' && ( +
    + 시상 규모 + {itemADetail.data.activity_benefit || ''} +
    + )} + + {tabName === 'activities' && ( +
    + 활동 지역 + {itemBDetail.data.area} +
    + )} + {tabName === 'activities' && ( +
    + 우대 역량 + {itemBDetail.data.preferred_skills} +
    + )} + {tabName === 'activities' && ( +
    + 활동 분야 + {itemBDetail.data.activity_field} +
    + )} + +
    + 활동 혜택 + + {tabName === 'competitions' + ? itemADetail.data.activity_benefit || '' + : itemBDetail.data.activity_benefit} + +
    +
    + 추가 혜택 + + {tabName === 'competitions' + ? itemADetail.data.bonus_benefit || '' + : itemBDetail.data.bonus_benefit} + +
    + +
    +
    + +
    + detail +
    + {tabName === 'competitions' ? itemADetail.data.description : itemBDetail.data.description} +
    +
    +
    + ); +}; + +export default ActivityDetailPage; diff --git a/src/routes/router.tsx b/src/routes/router.tsx index e30d466..5489000 100644 --- a/src/routes/router.tsx +++ b/src/routes/router.tsx @@ -1,15 +1,17 @@ +import SignIn from '@/components/signIn'; import { createBrowserRouter } from 'react-router-dom'; import App from '../App'; -import SignIn from '@/components/signIn'; -import ArticleDetailPage from '@/pages/articleDetail'; import SignUp from '@/components/signUp'; -import MyPage from '@/pages/myPage'; -import ArticleListPage from '@/pages/articleList'; -import AdminLogin from '@/pages/adminLogin'; import AdminBoardDetail from '@/pages/AdminBoardDetail'; +import ActivityPage from '@/pages/activity'; +import ActivityDetailPage from '@/pages/activityDetail'; import AdminBoard from '@/pages/adminBoard'; +import AdminLogin from '@/pages/adminLogin'; +import ArticleDetailPage from '@/pages/articleDetail'; +import ArticleListPage from '@/pages/articleList'; import CampReviewListPage from '@/pages/campReviewList'; +import MyPage from '@/pages/myPage'; import ReviewDetailListPage from '@/pages/reviewDetailList'; import { Suspense, lazy } from 'react'; @@ -59,6 +61,8 @@ export const router = createBrowserRouter([ ), }, + { path: '/activity', element: }, + { path: '/activity/:id', element: }, { path: '/mypage', element: }, { path: '/admin/login', element: }, { path: '/admin/board', element: },