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}
+
+
+
+
);
};
@@ -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');
+}