diff --git a/src/components/Card/InformCard/index.test.tsx b/src/components/Card/InformCard/index.test.tsx index 2d2d4a27..5fa1ccea 100644 --- a/src/components/Card/InformCard/index.test.tsx +++ b/src/components/Card/InformCard/index.test.tsx @@ -6,18 +6,39 @@ import useModals from '@hooks/useModals'; import { render, screen } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; import Major from '@type/major'; +import { IconKind } from '@type/styles/icon'; import { act } from 'react-dom/test-utils'; import { MemoryRouter } from 'react-router-dom'; import '@testing-library/jest-dom'; import InformCard from './index'; -const ICON = 'notification'; -const TITLE = '공지사항'; -const PATH = '/announcement'; +type INFORM_CARD_TYPE = 'ANNOUNCEMENT' | 'GRADUATION'; + +type INFORM_CARD_DATA = { + [key in INFORM_CARD_TYPE]: { + title: string; + icon: IconKind & ('school' | 'notification'); + onClick: () => void; + }; +}; + +const graduationLink = 'https://ce.pknu.ac.kr/ce/2889'; +const INFORM_CARD: INFORM_CARD_DATA = { + ANNOUNCEMENT: { + title: '공지사항', + icon: 'notification', + onClick: () => mockRouterTo('/announcement'), + }, + GRADUATION: { + title: '졸업요건', + icon: 'school', + onClick: () => (window.location.href = graduationLink), + }, +}; const setMajorMock = (isRender: boolean) => { - const mockMajor: Major = isRender ? null : '컴퓨터공학과'; + const mockMajor: Major = isRender ? null : '컴퓨터인공지능학부'; const mockSetMajor = jest.fn(); jest.mock('react', () => ({ @@ -50,7 +71,21 @@ jest.mock('@hooks/useModals', () => { }); describe('InformCard 컴포넌트 테스트', () => { + const oldWindowLocation = window.location; + beforeEach(() => { + Object.defineProperty(window, 'location', { + configurable: true, + enumerable: true, + value: new URL(window.location.href), + }); + }); + afterEach(() => { + Object.defineProperty(window, 'location', { + configurable: false, + enumerable: true, + value: oldWindowLocation, + }); jest.clearAllMocks(); }); @@ -58,7 +93,11 @@ describe('InformCard 컴포넌트 테스트', () => { render( - + , { @@ -71,14 +110,18 @@ describe('InformCard 컴포넌트 테스트', () => { await userEvent.click(card); }); - expect(mockRouterTo).toHaveBeenCalledWith(`${PATH}`); + expect(mockRouterTo).toHaveBeenCalled(); }); it('전역상태가 설정 안됐을 경우, 모달 렌더링 테스트', async () => { render( - + , { @@ -99,4 +142,28 @@ describe('InformCard 컴포넌트 테스트', () => { routerTo: expect.any(Function), }); }); + + it('전역상태가 설정 됐을 경우, 졸업요건 클릭 시 페이지 이동 테스트', async () => { + render( + + + + + , + { + wrapper: MemoryRouter, + }, + ); + + const card = screen.getByTestId('card'); + await act(async () => { + await userEvent.click(card); + }); + + expect(window.location.href).toBe(graduationLink); + }); }); diff --git a/src/components/Card/InformCard/index.tsx b/src/components/Card/InformCard/index.tsx index c0e4a0e7..1565407d 100644 --- a/src/components/Card/InformCard/index.tsx +++ b/src/components/Card/InformCard/index.tsx @@ -10,24 +10,23 @@ import { THEME } from '@styles/ThemeProvider/theme'; import { IconKind } from '@type/styles/icon'; import { setSize } from '@utils/styles/size'; +// TODO: InformCard 컴포넌트 Props 및 로직 수정 + interface InformCardProps { icon: IconKind & ('school' | 'notification'); title: string; - path: string; + onClick: () => void; } -const InformCard = ({ icon, title, path }: InformCardProps) => { +const InformCard = ({ icon, title, onClick }: InformCardProps) => { const { major } = useMajor(); - const { routerTo } = useRouter(); - const routerToPath = (path: string) => routerTo(path); const routerToMajorDecision = () => routerTo('/major-decision'); - const { openModal, closeModal } = useModals(); const handleMajorModal = () => { if (major) { - routerToPath(path); + onClick(); return; } openModal(AlertModal, { diff --git a/src/components/Modal/MajorModal/index.test.tsx b/src/components/Modal/MajorModal/index.test.tsx deleted file mode 100644 index e9f045a2..00000000 --- a/src/components/Modal/MajorModal/index.test.tsx +++ /dev/null @@ -1,36 +0,0 @@ -// TODO -// 1. MajorModal 컴포넌트 호출할 때, onClose & routerTo 를 props 로 전달 -// 2. 전달 받아서 호출 후, 테스트 할 내용은 2가지 -// 2-a. 학과 선택하러가기 버튼을 클릭하면 페이지 이동이 제대로 되는지 확인 -// 2-b. 모달 창 바깥 버튼 누르면 제대로 닫히는지 확인 -import { render, screen } from '@testing-library/react'; -import userEvent from '@testing-library/user-event'; -import { MemoryRouter } from 'react-router-dom'; - -import MajorModal from '.'; - -const onCloseMock = jest.fn(); -const routerToMock = jest.fn(); -jest.mock('react-router-dom', () => ({ - ...jest.requireActual('react-router-dom'), - useNavigate: () => routerToMock, -})); - -describe('모달 컴포넌트 테스트', () => { - it('학과 선택 페이지로의 이동이 제대로 되는지 테스트', async () => { - render( - , - { - wrapper: MemoryRouter, - }, - ); - - const pageButton = screen.getByText('학과 선택하기'); - await userEvent.click(pageButton); - - expect(routerToMock).toHaveBeenCalledWith('major-decision'); - }); -}); diff --git a/src/components/Modal/MajorModal/index.tsx b/src/components/Modal/MajorModal/index.tsx deleted file mode 100644 index 475985f8..00000000 --- a/src/components/Modal/MajorModal/index.tsx +++ /dev/null @@ -1,36 +0,0 @@ -import Button from '@components/Button'; -import Icon from '@components/Icon'; -import { css } from '@emotion/react'; -import { THEME } from '@styles/ThemeProvider/theme'; - -import Modal from '..'; - -interface MajorModalProps { - onClose: () => void; - routerTo: () => void; -} - -const MajorModal = ({ onClose, routerTo }: MajorModalProps) => { - return ( - - <> - - 아직 학과를 알려주지 않았어요 - - - - - ); -}; - -export default MajorModal; diff --git a/src/components/Modal/MapBoundsLimitModal/index.tsx b/src/components/Modal/MapBoundsLimitModal/index.tsx deleted file mode 100644 index d5c093f2..00000000 --- a/src/components/Modal/MapBoundsLimitModal/index.tsx +++ /dev/null @@ -1,28 +0,0 @@ -import Modal from '@components/Modal'; -import { css } from '@emotion/react'; -import { THEME } from '@styles/ThemeProvider/theme'; -import React from 'react'; - -interface MapBoundsLimitModal { - onClose: () => void; -} - -const MapBoundsLimitModal = ({ onClose }: MapBoundsLimitModal) => { - return ( - - <> - - 앗! 지도의 범위를 벗어났어요. - - - - ); -}; - -export default MapBoundsLimitModal; diff --git a/src/components/Modal/MapLevelLimitModal/index.tsx b/src/components/Modal/MapLevelLimitModal/index.tsx deleted file mode 100644 index 3a50d516..00000000 --- a/src/components/Modal/MapLevelLimitModal/index.tsx +++ /dev/null @@ -1,28 +0,0 @@ -import { css } from '@emotion/react'; -import { THEME } from '@styles/ThemeProvider/theme'; - -import Modal from '..'; - -interface MapLevetLimitModalProps { - onClose: () => void; -} - -const MapLevetLimitModal = ({ onClose }: MapLevetLimitModalProps) => { - return ( - - <> - - 앗! 지도를 더이상 축소할 수 없어요 - - - - ); -}; - -export default MapLevetLimitModal; diff --git a/src/components/Modal/SuggestionModal/SuggestionInput.tsx b/src/components/Modal/SuggestionModal/SuggestionInput.tsx deleted file mode 100644 index 05f8999b..00000000 --- a/src/components/Modal/SuggestionModal/SuggestionInput.tsx +++ /dev/null @@ -1,87 +0,0 @@ -import http from '@apis/http'; -import Button from '@components/Button'; -import { SERVER_URL } from '@config/index'; -import { css } from '@emotion/react'; -import styled from '@emotion/styled'; -import { THEME } from '@styles/ThemeProvider/theme'; -import { areaResize } from '@utils/styles/textarea-resize'; -import React, { Dispatch, SetStateAction, useRef, useState } from 'react'; - -interface SuggestionInputProps { - setIsSended: Dispatch>; -} - -const SuggestionInput = ({ setIsSended }: SuggestionInputProps) => { - const areaRef = useRef(null); - const [isInvalid, setIsInvalid] = useState(true); - - const onChange = (e: React.ChangeEvent) => { - if (!e.currentTarget.value || e.currentTarget.value.length < 5) { - setIsInvalid(true); - return; - } - setIsInvalid(false); - }; - const onResize = (e: React.KeyboardEvent) => { - areaResize(e.currentTarget); - }; - - const onSuggest = async () => { - setIsSended((prev) => !prev); - await http.post( - `${SERVER_URL}/api/suggestion`, - { - content: areaRef.current?.value, - }, - { - headers: { - 'Content-Type': 'application/json', - }, - }, - ); - }; - - return ( - <> - - 건의사항 - - - - - ); -}; - -export default SuggestionInput; - -const TextArea = styled.textarea` - line-height: 1.5; - padding: 10px; - resize: none; - overflow-y: hidden; - - font-size: 16px; - font-weight: bold; - border-radius: 8px; - - &::placeholder { - color: ${THEME.TEXT.GRAY}; - font-weight: lighter; - } -`; diff --git a/src/components/Modal/SuggestionModal/SuggestionThxMessage.tsx b/src/components/Modal/SuggestionModal/SuggestionThxMessage.tsx deleted file mode 100644 index 88e0ecf0..00000000 --- a/src/components/Modal/SuggestionModal/SuggestionThxMessage.tsx +++ /dev/null @@ -1,20 +0,0 @@ -import { css } from '@emotion/react'; - -const SuggestionThxMessage = () => { - return ( - <> - - 🙇‍♂️ 건의사항을 남겨 주셔서 정말 감사드립니다! 🙇‍♂️
더 좋은 서비스를 - 제공할 수 있도록 노력하겠습니다. -
- - ); -}; - -export default SuggestionThxMessage; diff --git a/src/mocks/browser.ts b/src/mocks/browser.ts index a53b88ba..6829a3f6 100644 --- a/src/mocks/browser.ts +++ b/src/mocks/browser.ts @@ -1,6 +1,7 @@ import { setupWorker } from 'msw'; import { announceHandlers } from './handlers/announceHandlers'; +import { graduationHandler } from './handlers/graudationHandler'; import { majorHandlers } from './handlers/majorHandlers'; import { testHandlers } from './handlers/testHandlers'; @@ -8,4 +9,5 @@ export const worker = setupWorker( ...majorHandlers, ...testHandlers, ...announceHandlers, + ...graduationHandler, ); // 브라우저 환경 서버 diff --git a/src/mocks/handlers/graudationHandler.ts b/src/mocks/handlers/graudationHandler.ts new file mode 100644 index 00000000..01c71ae4 --- /dev/null +++ b/src/mocks/handlers/graudationHandler.ts @@ -0,0 +1,39 @@ +import { SERVER_URL } from '@config/index'; +import { RequestHandler, rest } from 'msw'; + +type GRADUATION_LINK = { + [key in string]: { + link: string; + }; +}; + +const MOCK_GRDUATION_LINK: GRADUATION_LINK = { + 컴퓨터인공지능학부: { + link: 'https://ce.pknu.ac.kr/ce/2889', + }, + 데이터정보과학부: { + link: 'https://www.youtube.com', + }, + '조형학부 건축학전공': { + link: 'https://www.naver.com', + }, + 미디어커뮤니케이션학부: { + link: 'https://www.google.com', + }, +}; + +export const graduationHandler: RequestHandler[] = [ + rest.get(`${SERVER_URL}/api/graduation`, (req, res, ctx) => { + const query = req.url.searchParams; + const major = query.get('major'); + if (major) { + return res( + ctx.status(200), + ctx.json({ + major: major, + graduationLink: MOCK_GRDUATION_LINK[major].link, + }), + ); + } + }), +]; diff --git a/src/pages/Home/index.tsx b/src/pages/Home/index.tsx index b6678be3..70ecabc3 100644 --- a/src/pages/Home/index.tsx +++ b/src/pages/Home/index.tsx @@ -1,7 +1,28 @@ +import http from '@apis/http'; import InformCard from '@components/Card/InformCard'; import { css } from '@emotion/react'; +import useMajor from '@hooks/useMajor'; +import useRouter from '@hooks/useRouter'; +import { useEffect, useState } from 'react'; const Home = () => { + const [graduationLink, setGraduationLink] = useState(''); + const { routerTo } = useRouter(); + const { major } = useMajor(); + + const routerToGraduationRequired = () => { + window.location.href = graduationLink; + }; + + useEffect(() => { + if (!major) return; + const getGraduationLink = async () => { + const response = await http.get(`/api/graduation?major=${major}`); + setGraduationLink(response.data.graduationLink); + }; + getGraduationLink(); + }, [major]); + return ( <>
{ justify-content: center; `} > - - + routerTo('/announcement')} + /> + routerToGraduationRequired()} + />
); diff --git a/src/pages/Map/MapLevelObserver.tsx b/src/pages/Map/MapLevelObserver.tsx index 1755e936..5b7b1f35 100644 --- a/src/pages/Map/MapLevelObserver.tsx +++ b/src/pages/Map/MapLevelObserver.tsx @@ -1,6 +1,8 @@ -import MapLevetLimitModal from '@components/Modal/MapLevelLimitModal'; +import AlertModal from '@components/Modal/AlertModal'; +import { MODAL_MESSAGE } from '@constants/modal-messages'; import { PKNU_MAP_LIMIT } from '@constants/pknu-map'; -import React, { useEffect, useState } from 'react'; +import useModals from '@hooks/useModals'; +import React, { useEffect } from 'react'; interface MapLevelObserverProps { map: any; @@ -12,8 +14,7 @@ const MapLevelObserver = ({ map, centerLocation }: MapLevelObserverProps) => { return null; } - const [isModalOpen, setIsModalOpen] = useState(false); - + const { openModal, closeModal } = useModals(); useEffect(() => { const levelLimitHandler = () => { if (map.getLevel() <= PKNU_MAP_LIMIT.LEVEL) { @@ -21,18 +22,23 @@ const MapLevelObserver = ({ map, centerLocation }: MapLevelObserverProps) => { } map.setLevel(PKNU_MAP_LIMIT.LEVEL); map.setCenter(centerLocation); - setIsModalOpen((prev) => !prev); + openModal(AlertModal, { + message: MODAL_MESSAGE.ALERT.OVER_MAP_LEVEL, + buttonMessage: '닫기', + onClose: () => closeModal(AlertModal), + }); }; window.kakao.maps.event.addListener(map, 'zoom_changed', levelLimitHandler); + return () => { + window.kakao.maps.event.removeListener( + map, + 'zoom_changed', + levelLimitHandler, + ); + }; }); - return ( - <> - {isModalOpen && ( - setIsModalOpen((prev) => !prev)} /> - )} - - ); + return <>; }; export default MapLevelObserver; diff --git a/src/pages/Map/MapboundaryObserver.tsx b/src/pages/Map/MapboundaryObserver.tsx index 441255ac..f7c9ec5a 100644 --- a/src/pages/Map/MapboundaryObserver.tsx +++ b/src/pages/Map/MapboundaryObserver.tsx @@ -1,6 +1,8 @@ -import MapBoundsLimitModal from '@components/Modal/MapBoundsLimitModal'; +import AlertModal from '@components/Modal/AlertModal'; +import { MODAL_MESSAGE } from '@constants/modal-messages'; import { PKNU_MAP_LIMIT } from '@constants/pknu-map'; -import React, { useEffect, useState } from 'react'; +import useModals from '@hooks/useModals'; +import React, { useEffect } from 'react'; interface MapBounds { map: any; @@ -12,7 +14,7 @@ const MapboundaryObserver = ({ map, centerLocation }: MapBounds) => { return null; } - const [isModalOpen, setIsModalOpen] = useState(false); + const { openModal, closeModal } = useModals(); useEffect(() => { const boundayLimitHandler = () => { const { La, Ma } = map.getCenter(); @@ -22,9 +24,13 @@ const MapboundaryObserver = ({ map, centerLocation }: MapBounds) => { Ma <= PKNU_MAP_LIMIT.BOTTOM || La <= PKNU_MAP_LIMIT.LEFT ) { - setIsModalOpen((prev) => !prev); map.setLevel(4); map.setCenter(centerLocation); + openModal(AlertModal, { + message: MODAL_MESSAGE.ALERT.OVER_MAP_LEVEL, + buttonMessage: '닫기', + onClose: () => closeModal(AlertModal), + }); } }; window.kakao.maps.event.addListener(map, 'dragend', boundayLimitHandler); @@ -38,13 +44,7 @@ const MapboundaryObserver = ({ map, centerLocation }: MapBounds) => { }; }, []); - return ( - <> - {isModalOpen && ( - setIsModalOpen((prev) => !prev)} /> - )} - - ); + return <>; }; export default MapboundaryObserver;