diff --git a/src/@types/announcement.ts b/src/@types/announcement.ts index db26c25f..451930ee 100644 --- a/src/@types/announcement.ts +++ b/src/@types/announcement.ts @@ -1,3 +1,8 @@ +import { + ANNOUNCEMENT_CATEGORY, + ANNOUNCEMENT_TYPE, +} from '@constants/announcement'; + type AnnounceItemType = '고정' | '일반'; export interface AnnounceItem { @@ -11,3 +16,9 @@ export interface AnnounceItem { export type AnnounceItemList = { [key in AnnounceItemType]: AnnounceItem[]; }; + +export type AnnouncementCategory = + (typeof ANNOUNCEMENT_CATEGORY)[keyof typeof ANNOUNCEMENT_CATEGORY]; + +export type AnnouncementType = + (typeof ANNOUNCEMENT_TYPE)[keyof typeof ANNOUNCEMENT_TYPE]; diff --git a/src/components/Card/AnnounceCard/AnnounceList/index.tsx b/src/components/Card/AnnounceCard/AnnounceList/index.tsx index d6f28700..ffdb7032 100644 --- a/src/components/Card/AnnounceCard/AnnounceList/index.tsx +++ b/src/components/Card/AnnounceCard/AnnounceList/index.tsx @@ -1,5 +1,10 @@ -import { AnnounceItemList } from '@type/announcement'; +import { ANNOUNCEMENT_TYPE } from '@constants/announcement'; +import styled from '@emotion/styled'; +import { AnnounceSearchList } from '@pages/Announcement/components'; +import { THEME } from '@styles/ThemeProvider/theme'; +import { AnnounceItemList, AnnouncementType } from '@type/announcement'; import { AxiosError, AxiosResponse } from 'axios'; +import { Fragment } from 'react'; import AnnounceCard from '..'; @@ -13,30 +18,44 @@ interface AnnounceListProps { resource: { read: () => Resource; }; + type: AnnouncementType; } -const AnnounceList = ({ resource }: AnnounceListProps) => { +const AnnounceList = ({ resource, type }: AnnounceListProps) => { const announceList: Resource = resource.read(); - if (announceList === null || announceList instanceof Error) { - return null; + return <>; } - const { 고정: pinned, 일반: normal } = announceList as AnnounceItemList; + + const { 고정: pinnedAnnouncement, 일반: normalAnnouncemnet } = + announceList as AnnounceItemList; return ( <> - {pinned.map((announce, idx) => ( -
- -
- ))} - {normal.map((announce, idx) => ( -
- -
- ))} + + {type === ANNOUNCEMENT_TYPE.NORMAL && + normalAnnouncemnet.map((announce, idx) => ( + + + + ))} + {type === ANNOUNCEMENT_TYPE.PINNED && + pinnedAnnouncement.map((announce, idx) => ( + + + + ))} + {type === ANNOUNCEMENT_TYPE.SEARCH && ( + + )} ); }; export default AnnounceList; + +const BoundaryLine = styled.div` + border-bottom: 1px solid ${THEME.TEXT.BLACK}; +`; diff --git a/src/components/Card/AnnounceCard/index.test.tsx b/src/components/Card/AnnounceCard/index.test.tsx index 6558704d..02d09044 100644 --- a/src/components/Card/AnnounceCard/index.test.tsx +++ b/src/components/Card/AnnounceCard/index.test.tsx @@ -1,4 +1,5 @@ import http from '@apis/http'; +import MajorProvider from '@components/MajorProvider'; import { render, screen } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; import { AnnounceItemList } from '@type/announcement'; @@ -34,7 +35,12 @@ describe('공지사항 카드 컴포넌트 테스트', () => { const { 고정, 일반 } = announceList; 일반.forEach(async (annouce) => { - render(, { wrapper: MemoryRouter }); + render( + + + , + { wrapper: MemoryRouter }, + ); }); const annouceCards = screen.getAllByTestId('card'); diff --git a/src/components/Card/AnnounceCard/index.tsx b/src/components/Card/AnnounceCard/index.tsx index 073e9c4e..42346fbd 100644 --- a/src/components/Card/AnnounceCard/index.tsx +++ b/src/components/Card/AnnounceCard/index.tsx @@ -1,39 +1,34 @@ -import Icon from '@components/Icon'; -import { css } from '@emotion/react'; import styled from '@emotion/styled'; +import useMajor from '@hooks/useMajor'; import { THEME } from '@styles/ThemeProvider/theme'; import { AnnounceItem } from '@type/announcement'; +import openLink from '@utils/router/openLink'; interface AnnounceCardProps extends AnnounceItem { - pinned?: boolean; + author?: string; } const AnnounceCard = ({ title, link, uploadDate, - pinned = false, + author, }: AnnounceCardProps) => { - const onClick = () => { - window.open(link, '_blank'); - }; + const { major } = useMajor(); uploadDate = uploadDate.slice(2); return ( - + openLink(link)} data-testid="card"> - {pinned && } - {title} - - {uploadDate} + {title} + + 20{uploadDate} + + {author ? author : major} + + ); }; @@ -41,53 +36,61 @@ const AnnounceCard = ({ export default AnnounceCard; const Card = styled.div` - height: 28px; - padding: 10px; + min-height: 50px; display: flex; flex-direction: column; justify-content: center; - color: ${THEME.TEXT.BLACK}; + + transition: 0.3s; + &:active { + transform: scale(0.95); + opacity: 0.6; + } `; const ContentContainer = styled.div` + padding: 20px 0 20px 0; display: flex; - align-items: center; + line-height: 1.5; + flex-direction: column; + + gap: 10px; `; -const AnnounceTitle = styled.span<{ pinned: boolean }>` +const AnnounceTitle = styled.span` + display: flex; + align-items: center; flex: 9; - font-size: 15px; - font-weight: ${({ pinned }) => (pinned ? 'bold' : 500)}; - margin-left: ${({ pinned }) => (pinned ? '' : '28px')}; - - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; - - &: hover { - cursor: pointer; - } - - transition: 0.3s; - &:active { - transform: scale(0.95); - opacity: 0.6; - } + font-size: 16px; + font-weight: 500; `; -const VertialSeparator = styled.div` +const VertialBoundaryLine = styled.div` border-left: 1px solid gray; height: 12px; margin: 0 5px; `; const AnnounceDate = styled.span` - flex: 1; - font-size: 10px; - font-weight: bold; - text-align: end; + font-size: 13px; white-space: nowrap; color: ${THEME.TEXT.GRAY}; + padding-right: 5px; +`; + +const HorizonBoundaryLine = styled.div` + border-bottom: 1px solid ${THEME.BACKGROUND}; +`; + +const SubContent = styled.div` + display: flex; + align-items: center; +`; + +const Source = styled.div` + font-size: 13px; + color: gray; + padding-left: 5px; `; diff --git a/src/components/Card/InformCard/index.test.tsx b/src/components/Card/InformCard/index.test.tsx index 0eda9a28..a32daa6b 100644 --- a/src/components/Card/InformCard/index.test.tsx +++ b/src/components/Card/InformCard/index.test.tsx @@ -43,12 +43,13 @@ const setMajorMock = (isRender: boolean) => { jest.mock('react', () => ({ ...jest.requireActual('react'), - useState: () => [mockMajor, mockSetMajor], + useState: () => [mockMajor, mockSetMajor, graduationLink], })); return { major: mockMajor, setMajor: mockSetMajor, + graduationLink, }; }; diff --git a/src/components/Card/InformCard/index.tsx b/src/components/Card/InformCard/index.tsx index 3261abaa..5d3e688f 100644 --- a/src/components/Card/InformCard/index.tsx +++ b/src/components/Card/InformCard/index.tsx @@ -23,9 +23,10 @@ const InformCard = ({ }: InformCardProps) => { const { major } = useMajor(); const { routerTo } = useRouter(); - const routerToMajorDecision = () => routerTo('/major-decision'); const { openModal, closeModal } = useModals(); + const routeToMajorDecisionPage = () => routerTo('/major-decision'); + const handleMajorModal = () => { if (!majorRequired || major) { onClick(); @@ -38,7 +39,7 @@ const InformCard = ({ onClose: () => closeModal(modals.alert), routerTo: () => { closeModal(modals.alert); - routerToMajorDecision(); + routeToMajorDecisionPage(); }, }); }; @@ -46,40 +47,13 @@ const InformCard = ({ return ( <> - -
- -
-
- - - {title} - - - {title} 보러가기 - - + + + + + {title} + {title} 보러가기 +
); @@ -89,33 +63,38 @@ export default InformCard; const Card = styled.div` display: flex; - flex-direction: row; + align-items: center; padding: 3% 1% 2% 0; - color: ${THEME.TEXT.GRAY}; - height: 70px; - - & > svg { - margin: 10px 0; - } - cursor: pointer; + height: 4rem; transition: all 0.2s ease-in-out; - &:active { - transform: scale(0.95); - opacity: 0.6; + span:nth-of-type(1) { + font-size: 12px; + color: ${THEME.TEXT.GRAY}; } -`; -const Wrapper = styled.div` - &:first-of-type { - display: flex; - align-items: center; + span:nth-of-type(2) { + font-size: 16px; + font-weight: bold; + color: ${THEME.TEXT.BLACK}; } +`; - &:nth-of-type(2) { - display: flex; - flex-direction: column; - padding: 4% 0 3% 3%; - } +const TextContainer = styled.div` + display: flex; + flex-direction: column; + gap: 5px; +`; + +const IconContainer = styled.div` + height: 45px; + width: 45px; + display: flex; + justify-content: center; + align-items: center; + margin-right: 10px; + + border-radius: 50%; + background-color: ${THEME.PRIMARY}; `; diff --git a/src/components/Carousel/index.tsx b/src/components/Carousel/index.tsx index 58336a48..9ae046b2 100644 --- a/src/components/Carousel/index.tsx +++ b/src/components/Carousel/index.tsx @@ -67,7 +67,7 @@ const CarouselContainer = styled.div` padding: 1rem 0 1rem; width: 100%; margin: 0 auto; - &: hover { + &:hover { cursor: pointer; } `; @@ -100,7 +100,7 @@ const Button = styled.button` border-radius: 0.5rem; margin-top: 1rem; - &: hover { + &:hover { cursor: pointer; } `; diff --git a/src/components/List/DepartmentList/index.test.tsx b/src/components/List/DepartmentList/index.test.tsx index dadb8d8b..6a231171 100644 --- a/src/components/List/DepartmentList/index.test.tsx +++ b/src/components/List/DepartmentList/index.test.tsx @@ -31,6 +31,7 @@ jest.mock('@hooks/useModals', () => { }); describe.skip('학과선택 테스트', () => { + const mockGraduationLink = 'https://ce.pknu.ac.kr/ce/2889'; const mockUseMajor = useMajor as jest.MockedFunction; const mockSetMajor = jest.fn(); @@ -38,6 +39,7 @@ describe.skip('학과선택 테스트', () => { mockUseMajor.mockReturnValue({ setMajor: mockSetMajor, major: '컴퓨터공학과', + graduationLink: mockGraduationLink, }); }); diff --git a/src/components/MajorProvider/index.tsx b/src/components/MajorProvider/index.tsx index 7bc2feaf..b60ac618 100644 --- a/src/components/MajorProvider/index.tsx +++ b/src/components/MajorProvider/index.tsx @@ -1,36 +1,41 @@ +import http from '@apis/http'; import MajorContext from '@contexts/major'; +import { AxiosResponse } from 'axios'; import React, { useEffect, useState } from 'react'; +interface GraduationLink { + department: string; + link: string | null; +} + interface MajorProviderProps { children: React.ReactNode; } const MajorProvider = ({ children }: MajorProviderProps) => { const [major, setMajor] = useState(null); + const [graduationLink, setGraduationLink] = useState(''); useEffect(() => { const storedMajor = localStorage.getItem('major'); - if (!storedMajor) return; setMajor(storedMajor); }, []); useEffect(() => { - const handleStorageChange = (event: StorageEvent) => { - if (event.key === 'major') { - const storedMajor = event.newValue; - if (storedMajor !== null) setMajor(storedMajor); - } - }; - window.addEventListener('storage', handleStorageChange); - - return () => { - window.removeEventListener('storage', handleStorageChange); - }; - }, []); + if (!major) return; + + (async () => { + const response: AxiosResponse = await http.get( + `/api/graduation?major=${major}`, + ); + const graduationLink = response.data.link; + setGraduationLink(graduationLink); + })(); + }, [major]); return ( - + {children} ); diff --git a/src/components/Modal/ConfirmModal/index.tsx b/src/components/Modal/ConfirmModal/index.tsx index 161f3536..d0016eb3 100644 --- a/src/components/Modal/ConfirmModal/index.tsx +++ b/src/components/Modal/ConfirmModal/index.tsx @@ -41,8 +41,8 @@ const ConfirmModal = ({ justify-content: center; `} > - + diff --git a/src/constants/.gitkeep b/src/constants/.gitkeep deleted file mode 100644 index e69de29b..00000000 diff --git a/src/constants/announcement.ts b/src/constants/announcement.ts new file mode 100644 index 00000000..0b8e547a --- /dev/null +++ b/src/constants/announcement.ts @@ -0,0 +1,15 @@ +export const ANNOUNCEMENT_TITLE = { + SCHOOL: '학교 공지사항', + MAROR: '학과 공지사항', +}; + +export const ANNOUNCEMENT_CATEGORY = { + SCHOOL: 'school', + MAJOR: 'major', +} as const; + +export const ANNOUNCEMENT_TYPE = { + NORMAL: 'normal', + PINNED: 'pinned', + SEARCH: 'search', +} as const; diff --git a/src/constants/path.ts b/src/constants/path.ts new file mode 100644 index 00000000..45e2d1e6 --- /dev/null +++ b/src/constants/path.ts @@ -0,0 +1,14 @@ +type Category = 'school' | 'major'; + +const PATH = { + SCHOOL_ANNOUNCEMENT: '/school/:type', + MAJOR_ANNOUNCEMENT: '/major/:type', + NORMAL_ANNOUNCEMENT: (category: Category) => + `/announcement/${category}/normal`, + PINNED_ANNOUNCEMENT: (category: Category) => + `/announcement/${category}/pinned`, + SEARCH_ANNOUNCEMENT: (category: Category, keyword: string) => + `/announcement/${category}/search?q=${keyword}`, +} as const; + +export default PATH; diff --git a/src/constants/placeholder-message.ts b/src/constants/placeholder-message.ts index 3ad0c513..553ee67a 100644 --- a/src/constants/placeholder-message.ts +++ b/src/constants/placeholder-message.ts @@ -1,6 +1,7 @@ const PLCACEHOLDER_MESSAGES = { SUGGESTION: '건의사항을 5글자 이상 남겨주세요', SEARCH_BUILDING: '건물번호 또는 건물이름을 검색해주세요', + SEARCH_TITLE: '제목을 검색하세요', } as const; export default PLCACEHOLDER_MESSAGES; diff --git a/src/constants/toast-message.ts b/src/constants/toast-message.ts index bef46bad..08f04ef3 100644 --- a/src/constants/toast-message.ts +++ b/src/constants/toast-message.ts @@ -1,7 +1,8 @@ const TOAST_MESSAGES = { OUT_OF_BOUNDARY: '학교 외부로 이동할 수 없어요!', OUT_OF_SHOOL: '학교 밖에서는 내 위치 정보를 제공하지 않아요!', - SHARE_LOCATION: '위치 정보를 공유해 주세요.', + SHARE_LOCATION: '위치 정보를 공유해 주세요', + SEARCH_KEYWORD: '검색어를 입력해주세요', } as const; export default TOAST_MESSAGES; diff --git a/src/contexts/major.ts b/src/contexts/major.ts index 750de426..4cf8a5be 100644 --- a/src/contexts/major.ts +++ b/src/contexts/major.ts @@ -4,6 +4,7 @@ import { createContext } from 'react'; interface MajorState { major: Major; setMajor: React.Dispatch>; + graduationLink: string | null; } const MajorContext = createContext(null); diff --git a/src/pages/Announcement/components/AnnounceContainer.tsx b/src/pages/Announcement/components/AnnounceContainer.tsx new file mode 100644 index 00000000..dd8bedce --- /dev/null +++ b/src/pages/Announcement/components/AnnounceContainer.tsx @@ -0,0 +1,120 @@ +import fetchAnnounceList from '@apis/Suspense/fetch-announce-list'; +import AnnounceList from '@components/Card/AnnounceCard/AnnounceList'; +import AnnounceCardSkeleton from '@components/Card/AnnounceCard/Skeleton'; +import { ANNOUNCEMENT_TYPE } from '@constants/announcement'; +import PATH from '@constants/path'; +import { keyframes } from '@emotion/react'; +import styled from '@emotion/styled'; +import useRouter from '@hooks/useRouter'; +import { + AnnounceItemList, + AnnouncementCategory, + AnnouncementType, +} from '@type/announcement'; +import React, { Suspense, useMemo } from 'react'; +import { useParams } from 'react-router-dom'; + +import AnnounceSearch from './AnnounceSearch'; +import AnnounceTypeButtons from './AnnounceTypeButtons'; + +interface AnnounceContainerProps { + title: string; + category: AnnouncementCategory; + endPoint: string | null; +} + +const AnnounceContainer = ({ + title, + category, + endPoint, +}: AnnounceContainerProps) => { + const { type } = useParams(); + if (!type) return <>; + + const { routerTo } = useRouter(); + + const showNormalAnnouncement = () => + routerTo(PATH.NORMAL_ANNOUNCEMENT(category)); + const showPinnedAnnouncement = () => + routerTo(PATH.PINNED_ANNOUNCEMENT(category)); + + const resource = useMemo( + () => fetchAnnounceList(endPoint), + [], + ); + + return ( + + {title} + + + + + + + }> + + + + + ); +}; + +export default AnnounceContainer; + +const Container = styled.div` + overflow-x: hidden; + display: flex; + flex-direction: column; + + row-gap: 15px; + padding: 10px; +`; + +const AnnounceTitle = styled.span` + margin-top: 1rem; + font-size: 1.5rem; + font-weight: bold; +`; + +const ButtonContainer = styled.div` + display: flex; + column-gap: 10px; +`; + +const getAnimationType = (type: AnnouncementType) => { + if (type === 'search') return 'none'; + return type === 'normal' ? AnnounceSlideLeft : AnnounceSlideRight; +}; + +const AnnounceListContainer = styled.div<{ type: AnnouncementType }>` + width: 100%; + overflow: hidden; + animation: ${({ type }) => getAnimationType(type)} 0.3s forwards; +`; + +const AnnounceSlideRight = keyframes` + from { + transform: translateX(-100%); + } + to { + transform: translateX(0%); + } +`; + +const AnnounceSlideLeft = keyframes` + from { + transform: translateX(100%); + } + to { + transform: translateX(0%); + } +`; diff --git a/src/pages/Announcement/components/AnnounceNotFound.tsx b/src/pages/Announcement/components/AnnounceNotFound.tsx new file mode 100644 index 00000000..4a7def4a --- /dev/null +++ b/src/pages/Announcement/components/AnnounceNotFound.tsx @@ -0,0 +1,32 @@ +import Icon from '@components/Icon'; +import styled from '@emotion/styled'; +import { THEME } from '@styles/ThemeProvider/theme'; +import React from 'react'; + +interface AnnounceNotFoundProps { + keyword: string; +} + +const AnnounceNotFound = ({ keyword }: AnnounceNotFoundProps) => { + return ( + + + {`"${keyword}"`}에 관한 공지사항이 없습니다. + + ); +}; + +export default AnnounceNotFound; + +const Container = styled.div` + height: 20rem; + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; + gap: 10px; +`; + +const Title = styled.span` + color: ${THEME.TEXT.GRAY}; +`; diff --git a/src/pages/Announcement/components/AnnounceSearch.tsx b/src/pages/Announcement/components/AnnounceSearch.tsx new file mode 100644 index 00000000..79cca090 --- /dev/null +++ b/src/pages/Announcement/components/AnnounceSearch.tsx @@ -0,0 +1,86 @@ +import Icon from '@components/Icon'; +import PATH from '@constants/path'; +import PLCACEHOLDER_MESSAGES from '@constants/placeholder-message'; +import TOAST_MESSAGES from '@constants/toast-message'; +import styled from '@emotion/styled'; +import useRouter from '@hooks/useRouter'; +import useToasts from '@hooks/useToast'; +import React, { useRef } from 'react'; + +interface AnnounceSearchProps { + category: 'school' | 'major'; +} + +const AnnounceSearch = ({ category }: AnnounceSearchProps) => { + const { routerTo } = useRouter(); + const { addToast } = useToasts(); + + const inputRef = useRef(null); + + const handleSubmit = (e: React.FormEvent) => { + if (!inputRef.current) return; + e.preventDefault(); + if (inputRef.current.value.length === 0) { + addToast(TOAST_MESSAGES.SEARCH_KEYWORD); + return; + } + + routerTo(PATH.SEARCH_ANNOUNCEMENT(category, inputRef.current.value)); + inputRef.current.value = ''; + inputRef.current.blur(); + }; + + return ( +
+ + + handleSubmit}> + + + +
+ ); +}; + +export default AnnounceSearch; + +const StyledForm = styled.form` + display: flex; + position: relative; +`; + +const StyledInput = styled.input` + -webkit-appearance: none; + appearance: none; + + width: 100%; + padding: 10px; + border: 0; + border-radius: 15px; + background-color: #7a9dd30f; + color: #7a9dd366; + font-size: 14px; + text-indent: 5px; + + &::placeholder { + color: #7a9dd366; + font-size: 14px; + } + box-shadow: 0px 0px 5px rgba(0, 0, 0, 0.15); + &:focus { + outline: none; + box-shadow: 0px 0px 8px rgba(0, 0, 0, 0.2); + } +`; + +const StyledIconWrapper = styled.button` + position: absolute; + right: 0.5rem; + top: 50%; + transform: translateY(-50%); + background-color: transparent; +`; diff --git a/src/pages/Announcement/components/AnnounceSearchList.tsx b/src/pages/Announcement/components/AnnounceSearchList.tsx new file mode 100644 index 00000000..8788bf2c --- /dev/null +++ b/src/pages/Announcement/components/AnnounceSearchList.tsx @@ -0,0 +1,36 @@ +import AnnounceCard from '@components/Card/AnnounceCard'; +import { AnnounceItem } from '@type/announcement'; +import React, { Fragment } from 'react'; +import { useLocation } from 'react-router-dom'; + +import AnnounceNotFound from './AnnounceNotFound'; + +interface AnnounceSearchProps { + announceList: AnnounceItem[]; +} + +const AnnounceSearchList = ({ announceList }: AnnounceSearchProps) => { + const { search } = useLocation(); + + const searchKeyword = decodeURI(search.split('=')[1]); + const searchResult = announceList.filter((announceItem) => + announceItem.title.includes(searchKeyword), + ); + const hasSearchResult = () => searchResult.length !== 0; + + return ( + + {hasSearchResult() ? ( + searchResult.map((announce, idx) => ( + + + + )) + ) : ( + + )} + + ); +}; + +export default AnnounceSearchList; diff --git a/src/pages/Announcement/components/AnnounceTypeButtons.tsx b/src/pages/Announcement/components/AnnounceTypeButtons.tsx new file mode 100644 index 00000000..1b309767 --- /dev/null +++ b/src/pages/Announcement/components/AnnounceTypeButtons.tsx @@ -0,0 +1,35 @@ +import Button from '@components/Button'; +import { css } from '@emotion/react'; +import { THEME } from '@styles/ThemeProvider/theme'; +import React from 'react'; + +interface AnnounceTypeButtonsProps { + type: '일반' | '고정'; + isActive: boolean; + onClick: () => void; +} + +const AnnounceTypeButtons = ({ + type, + isActive, + onClick, +}: AnnounceTypeButtonsProps) => { + return ( + + ); +}; + +export default AnnounceTypeButtons; diff --git a/src/pages/Announcement/components/index.ts b/src/pages/Announcement/components/index.ts new file mode 100644 index 00000000..2bce755e --- /dev/null +++ b/src/pages/Announcement/components/index.ts @@ -0,0 +1,4 @@ +export { default as AnnounceContainer } from './AnnounceContainer'; +export { default as AnnounceSearch } from './AnnounceSearch'; +export { default as AnnounceSearchList } from './AnnounceSearchList'; +export { default as AnnounceTypeButtons } from './AnnounceTypeButtons'; diff --git a/src/pages/Announcement/index.tsx b/src/pages/Announcement/index.tsx index 69c17be7..4044c138 100644 --- a/src/pages/Announcement/index.tsx +++ b/src/pages/Announcement/index.tsx @@ -1,136 +1,41 @@ -import fetchAnnounceList from '@apis/Suspense/fetch-announce-list'; -import Button from '@components/Button'; -import AnnounceList from '@components/Card/AnnounceCard/AnnounceList'; -import AnnounceCardSkeleton from '@components/Card/AnnounceCard/Skeleton'; -import { MODAL_BUTTON_MESSAGE, MODAL_MESSAGE } from '@constants/modal-messages'; -import { css, keyframes } from '@emotion/react'; -import styled from '@emotion/styled'; +import { + ANNOUNCEMENT_CATEGORY, + ANNOUNCEMENT_TITLE, +} from '@constants/announcement'; +import PATH from '@constants/path'; import useMajor from '@hooks/useMajor'; -import useModals, { modals } from '@hooks/useModals'; -import useRouter from '@hooks/useRouter'; -import { THEME } from '@styles/ThemeProvider/theme'; -import { Suspense } from 'react'; +import React from 'react'; +import { Route, Routes } from 'react-router-dom'; + +import { AnnounceContainer } from './components'; const Announcement = () => { const { major } = useMajor(); - const { routerTo } = useRouter(); - const { openModal, closeModal } = useModals(); - - const announceKeyword = decodeURI(window.location.pathname.split('/')[2]); - const isActiveSchoolAnnouncement = () => announceKeyword === 'undefined'; - - const routerToMajorDecision = () => routerTo('/major-decision'); - const routerToSchoolAnnouncement = () => routerTo(''); - const routerToMajorAnnouncement = () => { - if (!major) { - openModal(modals.alert, { - message: MODAL_MESSAGE.ALERT.SET_MAJOR, - buttonMessage: MODAL_BUTTON_MESSAGE.SET_MAJOR, - iconKind: 'plus', - onClose: () => closeModal(modals.alert), - routerTo: () => { - closeModal(modals.alert); - routerToMajorDecision(); - }, - }); - } - routerTo(`${major}`); - }; return ( - - - - - - - - }> - + - - - + } + /> + + } + /> + ); }; export default Announcement; - -const Container = styled.div` - overflow-x: hidden; -`; - -const ButtonContainer = styled.div` - width: 100%; - display: flex; - position: relative; - overflow-x: none; -`; - -const ButtonBottomBar = styled.span<{ isActiveSchool: boolean }>` - &:before { - content: ''; - position: absolute; - bottom: 0; - left: ${({ isActiveSchool }) => (isActiveSchool ? '0' : '50%')}; - width: 50%; - height: 3px; - background-color: ${THEME.PRIMARY}; - transition: left 0.3s ease-in-out; - } -`; - -const AnnounceContainer = styled.div<{ isActiveSchool: boolean }>` - width: 100%; - overflow: hidden; - animation: ${({ isActiveSchool }) => - isActiveSchool ? AnnounceSlideLeft : AnnounceSlideRight} - 0.3s forwards; -`; - -const AnnounceSlideRight = keyframes` - from { - transform: translateX(-100%); - } - to { - transform: translateX(0%); - } -`; - -const AnnounceSlideLeft = keyframes` - from { - transform: translateX(100%); - } - to { - transform: translateX(0%); - } -`; diff --git a/src/pages/Home/Home.test.tsx b/src/pages/Home/Home.test.tsx index 4a962a09..1cc65f48 100644 --- a/src/pages/Home/Home.test.tsx +++ b/src/pages/Home/Home.test.tsx @@ -6,7 +6,7 @@ import '@testing-library/jest-dom'; import Home from './index'; -describe('App 컴포넌트 테스트', () => { +describe('Home Page 컴포넌트 테스트', () => { it('페이지에 공지사항 및 졸업요건 컴포넌트가 렌더링된다.', () => { render( @@ -18,7 +18,7 @@ describe('App 컴포넌트 테스트', () => { wrapper: MemoryRouter, }, ); - const notificationText = screen.getByText('공지사항'); + const notificationText = screen.getByText('학교 공지사항'); expect(notificationText).toBeInTheDocument(); const requirementText = screen.getByText('졸업요건'); diff --git a/src/pages/Home/components/InformCardList.tsx b/src/pages/Home/components/InformCardList.tsx new file mode 100644 index 00000000..e7102cba --- /dev/null +++ b/src/pages/Home/components/InformCardList.tsx @@ -0,0 +1,37 @@ +import InformCard from '@components/Card/InformCard'; +import { ANNOUNCEMENT_TITLE } from '@constants/announcement'; +import PATH from '@constants/path'; +import useMajor from '@hooks/useMajor'; +import useRouter from '@hooks/useRouter'; +import openLink from '@utils/router/openLink'; +import React from 'react'; + +const InformCardList = () => { + const { graduationLink } = useMajor(); + const { routerTo } = useRouter(); + + return ( + <> + routerTo(PATH.NORMAL_ANNOUNCEMENT('school'))} + /> + routerTo(PATH.NORMAL_ANNOUNCEMENT('major'))} + /> + openLink(graduationLink)} + /> + + ); +}; + +export default InformCardList; diff --git a/src/pages/Home/index.tsx b/src/pages/Home/index.tsx index e4eff870..5bbe61b6 100644 --- a/src/pages/Home/index.tsx +++ b/src/pages/Home/index.tsx @@ -1,49 +1,15 @@ -import http from '@apis/http'; -import InformCard from '@components/Card/InformCard'; import Carousel from '@components/Carousel'; import styled from '@emotion/styled'; -import useMajor from '@hooks/useMajor'; -import useRouter from '@hooks/useRouter'; import { THEME } from '@styles/ThemeProvider/theme'; -import { AxiosResponse } from 'axios'; -import { useEffect, useState } from 'react'; -const Home = () => { - const [graduationLink, setGraduationLink] = useState(''); - const { routerTo } = useRouter(); - const { major } = useMajor(); - - const routerToGraduationRequiredPage = (graduationLink: string | null) => { - graduationLink && window.open(graduationLink, '_blank'); - }; - - useEffect(() => { - if (!major) return; - const getGraduationLink = async () => { - const response: AxiosResponse = await http.get( - `/api/graduation?major=${major}`, - ); - setGraduationLink(response.data.link); - }; - getGraduationLink(); - }, [major]); +import InformCardList from './components/InformCardList'; +const Home = () => { return ( 학교 - routerTo('/announcement')} - /> - routerToGraduationRequiredPage(graduationLink)} - /> + 비교과 @@ -76,8 +42,3 @@ const InformTitle = styled.div` font-size: 20px; font-weight: bold; `; - -interface GraduationLink { - department: string; - link: string | null; -} diff --git a/src/pages/MajorDecision/index.test.tsx b/src/pages/MajorDecision/index.test.tsx index 9793ddf2..301c8177 100644 --- a/src/pages/MajorDecision/index.test.tsx +++ b/src/pages/MajorDecision/index.test.tsx @@ -15,7 +15,9 @@ jest.mock('react-router-dom', () => ({ })); describe.skip('학과선택 페이지 로직 테스트', () => { + const mockGraduationLink = 'https://ce.pknu.ac.kr/ce/2889'; const mockSetMajor = jest.fn(); + beforeEach(() => { jest.mock('react', () => ({ ...jest.requireActual('react'), @@ -30,7 +32,13 @@ describe.skip('학과선택 페이지 로직 테스트', () => { it('전공 선택 버튼 클릭 후, 상태 변경 테스트', async () => { render( - + , diff --git a/src/utils/.gitkeep b/src/utils/.gitkeep deleted file mode 100644 index e69de29b..00000000 diff --git a/src/utils/router/openLink.ts b/src/utils/router/openLink.ts new file mode 100644 index 00000000..0a0bd152 --- /dev/null +++ b/src/utils/router/openLink.ts @@ -0,0 +1,5 @@ +export default function openLink(link: string | null) { + if (!link) return; + + window.open(link, '_blank'); +}