diff --git a/src/@types/modals.ts b/src/@types/modals.ts index e68b92d5..c1866f80 100644 --- a/src/@types/modals.ts +++ b/src/@types/modals.ts @@ -1,8 +1,5 @@ -import { modals } from '@hooks/useModals'; - export type Modals = | Array<{ - Component: (typeof modals)[keyof typeof modals]; - props: object; + Component: React.ReactElement<{ chidren: React.ReactNode }>; }> | []; diff --git a/src/App.tsx b/src/App.tsx index f7d013ef..b69efcad 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -5,14 +5,12 @@ import Announcement from '@pages/Announcement'; import FAQPage from '@pages/FAQ'; import Home from '@pages/Home'; import MajorDecision from '@pages/MajorDecision'; -import Map from '@pages/Map'; +import MapPage from '@pages/Map'; import My from '@pages/My'; import Tip from '@pages/Tip'; import RouteChangeTracker from '@utils/routeChangeTracker'; import { Routes, Route } from 'react-router-dom'; -import { OverlayProvider } from './components/Providers'; - const App = () => { RouteChangeTracker(); @@ -28,9 +26,7 @@ const App = () => { } /> } /> - }> - } /> - + } /> diff --git a/src/components/Card/InformCard/index.test.tsx b/src/components/Card/InformCard/index.test.tsx index 4612960e..01dd8c0d 100644 --- a/src/components/Card/InformCard/index.test.tsx +++ b/src/components/Card/InformCard/index.test.tsx @@ -1,6 +1,6 @@ -import AlertModal from '@components/Common/Modal/AlertModal'; +import Modal from '@components/Common/Modal'; import ModalsProvider from '@components/Providers/ModalsProvider'; -import { MODAL_MESSAGE } from '@constants/modal-messages'; +import { MODAL_BUTTON_MESSAGE, MODAL_MESSAGE } from '@constants/modal-messages'; import MajorContext from '@contexts/major'; import useModals from '@hooks/useModals'; import { render, screen } from '@testing-library/react'; @@ -140,13 +140,16 @@ describe('InformCard 컴포넌트 테스트', () => { await userEvent.click(card); }); - expect(useModals().openModal).toHaveBeenCalledWith(AlertModal, { - message: MODAL_MESSAGE.ALERT.SET_MAJOR, - buttonMessage: '전공선택하러 가기', - iconKind: 'plus', - onClose: expect.any(Function), - routerTo: expect.any(Function), - }); + expect(useModals().openModal).toHaveBeenCalledWith( + + + + , + ); }); it('전역상태가 설정 됐을 경우, 졸업요건 클릭 시 페이지 이동 테스트', async () => { diff --git a/src/components/Card/InformCard/index.tsx b/src/components/Card/InformCard/index.tsx index 61a3f3c9..67b95b32 100644 --- a/src/components/Card/InformCard/index.tsx +++ b/src/components/Card/InformCard/index.tsx @@ -1,8 +1,9 @@ import Icon from '@components/Common/Icon'; +import Modal from '@components/Common/Modal'; import { MODAL_BUTTON_MESSAGE, MODAL_MESSAGE } from '@constants/modal-messages'; import styled from '@emotion/styled'; import useMajor from '@hooks/useMajor'; -import useModals, { modals } from '@hooks/useModals'; +import useModals from '@hooks/useModals'; import useRouter from '@hooks/useRouter'; import { THEME } from '@styles/ThemeProvider/theme'; import { IconKind } from '@type/styles/icon'; @@ -22,7 +23,7 @@ const InformCard = ({ }: InformCardProps) => { const { major } = useMajor(); const { routerTo } = useRouter(); - const { openModal, closeModal } = useModals(); + const { openModal } = useModals(); const routeToMajorDecisionPage = () => routerTo('/major-decision'); @@ -31,42 +32,39 @@ const InformCard = ({ onClick(); return; } - 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); - routeToMajorDecisionPage(); - }, - }); + + openModal( + + + + , + ); }; return ( - <> - - - - - - {title} - {title} 보러가기 - - - + + + + + + {title} + {title} 보러가기 + + ); }; export default InformCard; const Card = styled.div` - display: flex; - align-items: center; padding: 3% 1% 2% 0; height: 4rem; - - transition: all 0.2s ease-in-out; + display: flex; + align-items: center; span:nth-of-type(1) { font-size: 12px; @@ -78,6 +76,8 @@ const Card = styled.div` font-weight: bold; color: ${THEME.TEXT.BLACK}; } + + transition: all 0.2s ease-in-out; `; const TextContainer = styled.div` @@ -93,7 +93,6 @@ const IconContainer = styled.div` justify-content: center; align-items: center; margin-right: 10px; - border-radius: 50%; background-color: ${THEME.PRIMARY}; `; diff --git a/src/components/Common/Button/Toggle/index.tsx b/src/components/Common/Button/Toggle/index.tsx index bf25ed3f..1385f9a3 100644 --- a/src/components/Common/Button/Toggle/index.tsx +++ b/src/components/Common/Button/Toggle/index.tsx @@ -20,11 +20,11 @@ const ToggleButton = (props: Props) => { export default ToggleButton; -// chore : Circle interface 위치 수정 interface Circle { isOn: boolean; animation: boolean; } + const Button = styled.button` position: relative; border: none; diff --git a/src/components/Common/Modal/AlertModal/index.tsx b/src/components/Common/Modal/AlertModal/index.tsx deleted file mode 100644 index 51cad411..00000000 --- a/src/components/Common/Modal/AlertModal/index.tsx +++ /dev/null @@ -1,60 +0,0 @@ -import Button from '@components/Common/Button'; -import Icon from '@components/Common/Icon'; -import { css } from '@emotion/react'; -import { THEME } from '@styles/ThemeProvider/theme'; -import { IconKind } from '@type/styles/icon'; -import React from 'react'; - -import Modal from '..'; - -interface AlertModalProps { - message: string; - buttonMessage?: string; - open: boolean; - iconKind?: IconKind; - onClose?: () => void; - routerTo?: () => void; -} - -const AlertModal = ({ - message, - buttonMessage, - open, - iconKind, - onClose = () => undefined, - routerTo = onClose, -}: AlertModalProps) => { - return ( - <> - {open && ( - - <> - - {message} - - {buttonMessage && ( - - )} - - - )} - - ); -}; - -export default AlertModal; diff --git a/src/components/Common/Modal/ConfirmModal/index.tsx b/src/components/Common/Modal/ConfirmModal/index.tsx deleted file mode 100644 index a39e9883..00000000 --- a/src/components/Common/Modal/ConfirmModal/index.tsx +++ /dev/null @@ -1,54 +0,0 @@ -import Button from '@components/Common/Button'; -import { css } from '@emotion/react'; -import { THEME } from '@styles/ThemeProvider/theme'; -import React from 'react'; - -import Modal from '..'; - -interface ConfirmModalProps { - message: string; - open: boolean; - onConfirmButtonClick: () => void; - onCancelButtonClick: () => void; -} - -const ConfirmModal = ({ - message, - open, - onConfirmButtonClick, - onCancelButtonClick, -}: ConfirmModalProps) => { - return ( - <> - {open && ( - - <> - - {message} - -
- - -
- -
- )} - - ); -}; - -export default ConfirmModal; diff --git a/src/components/Common/Modal/ModalButton.tsx b/src/components/Common/Modal/ModalButton.tsx new file mode 100644 index 00000000..5edf2a1d --- /dev/null +++ b/src/components/Common/Modal/ModalButton.tsx @@ -0,0 +1,37 @@ +import { css } from '@emotion/react'; +import useModals from '@hooks/useModals'; +import { THEME } from '@styles/ThemeProvider/theme'; +import { IconKind } from '@type/styles/icon'; +import React from 'react'; + +import Button from '../Button'; +import Icon from '../Icon'; + +interface ModalButtonProps { + text: string; + iconKind?: IconKind & 'plus'; + onClick?: () => void; +} + +const ModalButton = ({ text, iconKind, onClick }: ModalButtonProps) => { + const { closeModal } = useModals(); + + const onButtonClick = () => { + closeModal(); + onClick && onClick(); + }; + + return ( + + ); +}; + +export default ModalButton; diff --git a/src/components/Common/Modal/ModalTitle.tsx b/src/components/Common/Modal/ModalTitle.tsx new file mode 100644 index 00000000..1502ad57 --- /dev/null +++ b/src/components/Common/Modal/ModalTitle.tsx @@ -0,0 +1,22 @@ +import styled from '@emotion/styled'; +import { THEME } from '@styles/ThemeProvider/theme'; +import React from 'react'; + +interface ModalTitleProps { + title: string; +} + +const ModalTitle = ({ title }: ModalTitleProps) => { + return {title}; +}; + +export default ModalTitle; + +const Title = styled.span` + padding: 20px 0 20px 0; + text-align: center; + line-height: 1.3; + font-weight: bold; + white-space: pre-line; + color: ${THEME.TEXT.GRAY}; +`; diff --git a/src/components/Common/Modal/Modals/index.tsx b/src/components/Common/Modal/Modals/index.tsx index 3e47c916..491a2f7e 100644 --- a/src/components/Common/Modal/Modals/index.tsx +++ b/src/components/Common/Modal/Modals/index.tsx @@ -7,8 +7,8 @@ const Modals = () => { return ( <> {modals && - modals.map(({ Component, props }, idx) => { - return ; + modals.map(({ Component }, idx) => { + return {Component}; })} ); diff --git a/src/components/Common/Modal/SuggestionModal/index.tsx b/src/components/Common/Modal/SuggestionModal/index.tsx deleted file mode 100644 index 6c9ddef8..00000000 --- a/src/components/Common/Modal/SuggestionModal/index.tsx +++ /dev/null @@ -1,129 +0,0 @@ -import http from '@apis/http'; -import Button from '@components/Common/Button'; -import Icon from '@components/Common/Icon'; -import { SERVER_URL } from '@config/index'; -import { MODAL_BUTTON_MESSAGE, MODAL_MESSAGE } from '@constants/modal-messages'; -import PLCACEHOLDER_MESSAGES from '@constants/placeholder-message'; -import { css } from '@emotion/react'; -import styled from '@emotion/styled'; -import useModals, { modals } from '@hooks/useModals'; -import { THEME } from '@styles/ThemeProvider/theme'; -import { areaResize } from '@utils/styles/textarea-resize'; -import React, { useRef, useState } from 'react'; - -import Modal from '..'; - -interface SuggestionModalProps { - title: string; - buttonMessage: string; - onClose: () => void; -} - -const SuggestionModal = ({ - title, - buttonMessage, - onClose, -}: SuggestionModalProps) => { - const areaRef = useRef(null); - const [isInvalid, setIsInvalid] = useState(true); - const { openModal, closeModal } = useModals(); - - const postSuggestion = async () => { - await http.post( - `${SERVER_URL}/api/suggestion`, - { - content: areaRef.current?.value, - }, - { - headers: { - 'Content-Type': 'application/json', - }, - }, - ); - }; - - const handleSuggestionPostModal = () => { - postSuggestion(); - onClose(); - closeModal(modals.confirm); - openModal(modals.alert, { - message: MODAL_MESSAGE.SUCCEED.POST_SUGGESTION, - buttonMessage: MODAL_BUTTON_MESSAGE.CONFIRM, - onClose: () => closeModal(modals.alert), - }); - }; - - const handleSuggestionConfirmModal = () => { - openModal(modals.confirm, { - message: MODAL_MESSAGE.CONFIRM.POST_SUGGESTION, - onConfirmButtonClick: () => handleSuggestionPostModal(), - onCancelButtonClick: () => closeModal(modals.confirm), - }); - }; - - 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); - }; - - return ( - - <> - - - {title} - - - - - - - - ); -}; - -export default SuggestionModal; - -const SuggestionArea = styled.textarea` - line-height: 1.5; - padding: 10px; - resize: none; - overflow-y: scoll; - - font-size: 16px; - font-weight: bold; - border-radius: 8px; - - &::placeholder { - color: ${THEME.TEXT.GRAY}; - font-weight: lighter; - } -`; - -const SuggestionHeader = styled.header` - display: flex; - align-items: center; - justify-content: space-between; - margin-bottom: 10px; -`; diff --git a/src/components/Common/Modal/domain/getModalSubElement.ts b/src/components/Common/Modal/domain/getModalSubElement.ts new file mode 100644 index 00000000..c4d7068d --- /dev/null +++ b/src/components/Common/Modal/domain/getModalSubElement.ts @@ -0,0 +1,20 @@ +import { Children, isValidElement } from 'react'; + +import ModalButton from '../ModalButton'; +import ModalTitle from '../ModalTitle'; + +type ModalChildType = typeof ModalTitle | typeof ModalButton; + +const getModalSubElement = ( + children: React.ReactNode, + childType: ModalChildType, +) => { + const childrenToArray = Children.toArray(children); + const targetChild = childrenToArray + .filter((child) => isValidElement(child) && child.type === childType) + .slice(0, 2); + + return targetChild; +}; + +export default getModalSubElement; diff --git a/src/components/Common/Modal/index.tsx b/src/components/Common/Modal/index.tsx index 80376c6c..949a666e 100644 --- a/src/components/Common/Modal/index.tsx +++ b/src/components/Common/Modal/index.tsx @@ -1,29 +1,45 @@ import { keyframes } from '@emotion/react'; import styled from '@emotion/styled'; +import useModals from '@hooks/useModals'; import { THEME } from '@styles/ThemeProvider/theme'; import React from 'react'; -interface ModalProps { - onClose: () => void; - children: JSX.Element; -} +import getModalSubElement from './domain/getModalSubElement'; +import ModalButton from './ModalButton'; +import ModalTitle from './ModalTitle'; -const Modal = ({ children, onClose }: ModalProps) => { - const onClick = (e: React.MouseEvent) => { +type StaticPropsWithChidren = T & { children: React.ReactNode }; + +const Modal = ({ children }: StaticPropsWithChidren) => { + const { closeModal } = useModals(); + + const modalTitle = getModalSubElement(children, ModalTitle); + const modalButtons = getModalSubElement(children, ModalButton); + const hasModalButtons = modalButtons.length !== 0; + + const onBackgroundClick = (e: React.MouseEvent) => { if (e.target === e.currentTarget) { - onClose(); + closeModal(); } }; return ( - - {children} + + + {modalTitle} + {hasModalButtons && ( + {modalButtons} + )} + ); }; export default Modal; +Modal.ModalTitle = ModalTitle; +Modal.ModalButton = ModalButton; + const ModalBackground = styled.div` position: fixed; top: 0; @@ -36,6 +52,7 @@ const ModalBackground = styled.div` background-color: rgba(0, 0, 0, 0.6); z-index: 9999; `; + const modalIn = keyframes` from{ opacity: 0; @@ -48,17 +65,21 @@ const modalIn = keyframes` `; const ModalContent = styled.div` + max-height: 70vh; + max-width: 480px; + width: 80%; display: flex; flex-direction: column; - - animation: ${modalIn} 0.3s ease-out; - width: 80%; - max-width: 480px; - max-height: 70vh; + padding: 0 30px 0 30px; overflow: auto; - padding: 30px; border-radius: 15px; background-color: ${THEME.TEXT.WHITE}; - color: ${THEME.TEXT.GRAY}; - font-weight: bold; + animation: ${modalIn} 0.3s ease-out; +`; + +const ModalButtonsContainer = styled.div` + padding: 0 0 20px 0; + display: flex; + justify-content: center; + gap: 0.5rem; `; diff --git a/src/components/List/DepartmentList/DepartmentItem.tsx b/src/components/List/DepartmentList/DepartmentItem.tsx index f54ce020..e4ef0681 100644 --- a/src/components/List/DepartmentList/DepartmentItem.tsx +++ b/src/components/List/DepartmentList/DepartmentItem.tsx @@ -1,12 +1,13 @@ import http from '@apis/http'; import Button from '@components/Common/Button'; import Icon from '@components/Common/Icon'; +import Modal from '@components/Common/Modal'; import { SERVER_URL } from '@config/index'; import { MODAL_BUTTON_MESSAGE, MODAL_MESSAGE } from '@constants/modal-messages'; import { css } from '@emotion/react'; import styled from '@emotion/styled'; import useMajor from '@hooks/useMajor'; -import useModals, { modals } from '@hooks/useModals'; +import useModals from '@hooks/useModals'; import useRouter from '@hooks/useRouter'; import { THEME } from '@styles/ThemeProvider/theme'; import { AxiosError, AxiosResponse } from 'axios'; @@ -26,23 +27,21 @@ const DepartmentItem = ({ resource }: DepartmentItemProps) => { const { routerTo } = useRouter(); const { major, setMajor } = useMajor(); - const { openModal, closeModal } = useModals(); + const { openModal } = useModals(); const [selected, setSelected] = useState(''); const [buttonDisable, setButtonDisable] = useState(true); const routerToHome = () => { - closeModal(modals.alert); routerTo('/'); }; + const handleMajorClick: React.MouseEventHandler = (e) => { if (e.currentTarget.textContent === null) return; setSelected(e.currentTarget.textContent); setButtonDisable(false); }; - const handlerMajorSetModal = () => { - closeModal(modals.confirm); - + const handleMajorSetting = () => { const storedSubscribe = localStorage.getItem('subscribe'); if (major && storedSubscribe) { http.delete(`${SERVER_URL}/api/subscription/major`, { @@ -54,21 +53,32 @@ const DepartmentItem = ({ resource }: DepartmentItemProps) => { localStorage.setItem('major', afterSpace); setMajor(afterSpace); - openModal(modals.alert, { - message: MODAL_MESSAGE.SUCCEED.SET_MAJOR, - buttonMessage: MODAL_BUTTON_MESSAGE.GO_HOME, - onClose: () => routerToHome(), - routerTo: () => routerToHome(), - }); + openModal( + + + + , + ); }; - const handleMajorConfirmModal = () => { - openModal(modals.confirm, { - message: - selected.substring(selected.indexOf(' ') + 1) + - MODAL_MESSAGE.CONFIRM.SET_MAJOR, - onConfirmButtonClick: () => handlerMajorSetModal(), - onCancelButtonClick: () => closeModal(modals.confirm), - }); + + const handleMajorConfirm = () => { + const modalTitle = + selected.substring(selected.indexOf(' ') + 1) + + MODAL_MESSAGE.CONFIRM.SET_MAJOR; + + openModal( + + + + + , + ); }; return ( @@ -90,7 +100,7 @@ const DepartmentItem = ({ resource }: DepartmentItemProps) => { ))} - diff --git a/src/components/List/DepartmentList/index.test.tsx b/src/components/List/DepartmentList/index.test.tsx index f1de6981..071ba63a 100644 --- a/src/components/List/DepartmentList/index.test.tsx +++ b/src/components/List/DepartmentList/index.test.tsx @@ -1,8 +1,8 @@ -import ConfirmModal from '@components/Common/Modal/ConfirmModal'; +import Modal from '@components/Common/Modal'; import DepartmentList from '@components/List/DepartmentList'; import MajorProvider from '@components/Providers/MajorProvider'; import ModalsProvider from '@components/Providers/ModalsProvider'; -import { MODAL_MESSAGE } from '@constants/modal-messages'; +import { MODAL_BUTTON_MESSAGE, MODAL_MESSAGE } from '@constants/modal-messages'; import useMajor from '@hooks/useMajor'; import useModals from '@hooks/useModals'; import { render, act, screen } from '@testing-library/react'; @@ -100,11 +100,15 @@ describe.skip('학과선택 테스트', () => { await userEvent.click(confirmButton); }); - expect(useModals().openModal).toHaveBeenCalledWith(ConfirmModal, { - message: MODAL_MESSAGE.CONFIRM.SET_MAJOR, - onCancelButtonClick: expect.any(Function), - onConfirmButtonClick: expect.any(Function), - }); + expect(useModals().openModal).toHaveBeenCalledWith( + + + + , + ); }); it('학과 이름에 스페이스가 있는 경우 (학부, 전공이 모두 있는경우) 테스트', async () => { diff --git a/src/components/Providers/MapProvider/index.tsx b/src/components/Providers/MapProvider/index.tsx new file mode 100644 index 00000000..2d0c7704 --- /dev/null +++ b/src/components/Providers/MapProvider/index.tsx @@ -0,0 +1,46 @@ +import PknuMapContext from '@contexts/map'; +import UserLocationContext from '@contexts/user-location'; +import styled from '@emotion/styled'; +import { + FilterButtons, + MapHeader, + PknuMap, + RefreshButtons, +} from '@pages/Map/components'; +import { Location } from '@type/map'; +import React, { useState } from 'react'; + +import OverlayProvider from '../OverlayProvider'; + +interface MapProps { + children: React.ReactNode; +} + +const Map = ({ children }: MapProps) => { + const [map, setMap] = useState(null); + const [userLocation, setUserLocation] = useState(null); + + return ( + + + + {children} + + + + ); +}; + +export default Map; + +Map.PknuMap = PknuMap; +Map.MapHeader = MapHeader; +Map.FilterButtons = FilterButtons; +Map.RefreshButtons = RefreshButtons; + +const MapContainer = styled.div` + height: calc(100vh - 8vh); + display: flex; + flex-direction: column; + position: relative; +`; diff --git a/src/components/Providers/OverlayProvider/index.tsx b/src/components/Providers/OverlayProvider/index.tsx index 191d1037..58eea83c 100644 --- a/src/components/Providers/OverlayProvider/index.tsx +++ b/src/components/Providers/OverlayProvider/index.tsx @@ -1,19 +1,40 @@ +import Modal from '@components/Common/Modal'; import OverlayContext from '@contexts/overlays'; import useModals from '@hooks/useModals'; import useUserLocation from '@hooks/useUserLocation'; -import React from 'react'; +import React, { useMemo } from 'react'; import { Outlet } from 'react-router-dom'; import CustomOverlay from './overlay'; -const OverlayProvider = () => { - const { openModal, closeModal } = useModals(); +interface OverlayProviderProps { + children: React.ReactNode; +} + +const OverlayProvider = ({ children }: OverlayProviderProps) => { const userLocation = useUserLocation(); - const customOverlay = new CustomOverlay(openModal, closeModal, userLocation); + const { openModal } = useModals(); + + const handleOpenModal = ( + title: string, + btn1Text: string, + onClick?: () => void, + btn2Text?: string, + ) => { + openModal( + + + + {btn2Text && } + , + ); + }; + + const customOverlay = new CustomOverlay(handleOpenModal, userLocation); return ( - + {children} ); }; diff --git a/src/components/Providers/OverlayProvider/overlay.ts b/src/components/Providers/OverlayProvider/overlay.ts index 5d4bcdba..c19708f1 100644 --- a/src/components/Providers/OverlayProvider/overlay.ts +++ b/src/components/Providers/OverlayProvider/overlay.ts @@ -1,9 +1,9 @@ import { MODAL_BUTTON_MESSAGE, MODAL_MESSAGE } from '@constants/modal-messages'; import { PKNU_BUILDINGS } from '@constants/pknu-map'; -import { CloseModal, OpenModal, modals } from '@hooks/useModals'; import { THEME } from '@styles/ThemeProvider/theme'; import { BuildingType, Location, PKNUBuilding } from '@type/map'; import { hasLocationPermission } from '@utils/map'; +import openLink from '@utils/router/openLink'; import { CSSProperties } from 'react'; interface ICustomOverlay { @@ -15,19 +15,20 @@ interface ICustomOverlay { ): void; } +type OpenModal = ( + title: string, + btn1Text: string, + onClick?: () => void, + btn2Text?: string, +) => void; + class CustomOverlay implements ICustomOverlay { private overlays: Record; private openModal: OpenModal; - private closeModal: CloseModal; private userLocation: Location | null; - constructor( - openModal: OpenModal, - closeModal: CloseModal, - userLocation: Location | null, - ) { + constructor(openModal: OpenModal, userLocation: Location | null) { this.openModal = openModal; - this.closeModal = closeModal; this.userLocation = userLocation; this.overlays = { A: [], @@ -52,31 +53,36 @@ class CustomOverlay implements ICustomOverlay { return this.overlays[type].length >= PKNU_BUILDINGS[type].buildings.length; } - private routeHandler(building: PKNUBuilding) { - const { buildingNumber, buildingName, latlng } = building; + private openNoLocationModal() { + this.openModal( + MODAL_MESSAGE.ALERT.NO_LOCATION_PERMISSON, + MODAL_BUTTON_MESSAGE.CLOSE, + ); + } + + private openConfirmRoutingModal(buildingInfo: PKNUBuilding) { + const { buildingNumber, buildingName, latlng } = buildingInfo; const [lat, lng] = latlng; + const kakaoMapAppURL = `kakaomap://route?sp=${this.userLocation?.LAT},${this.userLocation?.LNG}&ep=${lat},${lng}`; + const kakaoMapWebURL = `https://map.kakao.com/link/from/현위치,${this.userLocation?.LAT},${this.userLocation?.LNG}/to/${buildingName},${lat},${lng}`; + const isKakaoMapInstalled = /KAKAOMAP/i.test(navigator.userAgent); + const openUrl = isKakaoMapInstalled ? kakaoMapAppURL : kakaoMapWebURL; + + this.openModal( + `목적지(${buildingNumber})로 길찾기를 시작할까요?`, + MODAL_BUTTON_MESSAGE.NO, + () => openLink(openUrl), + MODAL_BUTTON_MESSAGE.YES, + ); + } + + private handleRoutingModal(building: PKNUBuilding) { if (!this.userLocation) return; + hasLocationPermission(this.userLocation) - ? this.openModal(modals.confirm, { - message: `목적지(${buildingNumber})로 길찾기를 시작할까요?`, - onConfirmButtonClick: () => { - const kakaoMapWebURL = `https://map.kakao.com/link/from/현위치,${this.userLocation?.LAT},${this.userLocation?.LNG}/to/${buildingName},${lat},${lng}`; - const kakaoMapAppURL = `kakaomap://route?sp=${this.userLocation?.LAT},${this.userLocation?.LNG}&ep=${lat},${lng}`; - const isKakaoMapInstalled = /KAKAOMAP/i.test(navigator.userAgent); - window.open( - isKakaoMapInstalled ? kakaoMapAppURL : kakaoMapWebURL, - '_blank', - ); - this.closeModal(modals.confirm); - }, - onCancelButtonClick: () => this.closeModal(modals.confirm), - }) - : this.openModal(modals.alert, { - message: MODAL_MESSAGE.ALERT.NO_LOCATION_PERMISSON, - buttonMessage: MODAL_BUTTON_MESSAGE.CLOSE, - onClose: () => this.closeModal(modals.alert), - }); + ? this.openConfirmRoutingModal(building) + : this.openNoLocationModal(); } private createOverlayContent( @@ -94,7 +100,7 @@ class CustomOverlay implements ICustomOverlay { }); const buildingNumberText = document.createTextNode(building.buildingNumber); content.appendChild(buildingNumberText); - content.onclick = () => this.routeHandler(building); + content.onclick = () => this.handleRoutingModal(building); return content; } @@ -120,6 +126,7 @@ class CustomOverlay implements ICustomOverlay { private getTypeOverlays(buildingType: BuildingType, map: any) { const type = buildingType as keyof typeof this.overlays; const typeOverlays: any[] = []; + PKNU_BUILDINGS[type].buildings.forEach((building) => { const overlay = this.createOverlay(type, building); overlay.setMap(map); @@ -163,10 +170,12 @@ class CustomOverlay implements ICustomOverlay { }); return; } + if (this.isAllOverlayInMap(type)) { newOverlays[type] = [...this.overlays[type]]; return; } + const typeOverlays = this.getTypeOverlays(type, map); newOverlays[type] = [...this.overlays[type], ...typeOverlays]; }); diff --git a/src/contexts/map.ts b/src/contexts/map.ts new file mode 100644 index 00000000..45c281e9 --- /dev/null +++ b/src/contexts/map.ts @@ -0,0 +1,10 @@ +import { SetStateAction, createContext } from 'react'; + +interface PknuMapState { + map: any; + setMap: React.Dispatch>; +} + +const PknuMapStateContext = createContext(null); + +export default PknuMapStateContext; diff --git a/src/contexts/user-location.ts b/src/contexts/user-location.ts new file mode 100644 index 00000000..8b61a9de --- /dev/null +++ b/src/contexts/user-location.ts @@ -0,0 +1,13 @@ +import { Location } from '@type/map'; +import { SetStateAction, createContext } from 'react'; + +type UserLocation = Location | null; + +interface UserLocationState { + userLocation: UserLocation; + setUserLocation: React.Dispatch>; +} + +const UserLocationContext = createContext(null); + +export default UserLocationContext; diff --git a/src/hooks/useBuildingTypes.ts b/src/hooks/useBuildingTypes.ts new file mode 100644 index 00000000..7367dbed --- /dev/null +++ b/src/hooks/useBuildingTypes.ts @@ -0,0 +1,27 @@ +import { BuildingType } from '@type/map'; +import { useState } from 'react'; + +const useBuildingTypes = () => { + const [activeTypes, setActiveTypes] = useState>( + { + A: true, + B: false, + C: false, + D: false, + E: false, + }, + ); + + const handleBuildingTypes = (type: BuildingType) => { + setActiveTypes((prevActiveTypes) => { + return { + ...prevActiveTypes, + [type]: !prevActiveTypes[type], + }; + }); + }; + + return { activeTypes, handleBuildingTypes }; +}; + +export default useBuildingTypes; diff --git a/src/hooks/useMap.ts b/src/hooks/useMap.ts new file mode 100644 index 00000000..76932177 --- /dev/null +++ b/src/hooks/useMap.ts @@ -0,0 +1,29 @@ +import { PKNU_MAP_CENTER_LOCATION } from '@constants/pknu-map'; +import PknuMapContext from '@contexts/map'; +import { useContext } from 'react'; + +const useMap = () => { + const pknuMapContext = useContext(PknuMapContext); + + if (!pknuMapContext) { + throw new Error('PknuMapDispatchContext does not exists'); + } + + const { map, setMap } = pknuMapContext; + + const setPknuMap = () => { + const container = document.getElementById('map'); + const options = { + center: PKNU_MAP_CENTER_LOCATION, + level: 4, + minLevel: 1, + maxLevel: 4, + }; + const map = new window.kakao.maps.Map(container as HTMLDivElement, options); + setMap(map); + }; + + return { map, setPknuMap }; +}; + +export default useMap; diff --git a/src/hooks/useModals.ts b/src/hooks/useModals.ts index d8b87b65..735b64b4 100644 --- a/src/hooks/useModals.ts +++ b/src/hooks/useModals.ts @@ -1,32 +1,5 @@ -import AlertModal from '@components/Common/Modal/AlertModal'; -import ConfirmModal from '@components/Common/Modal/ConfirmModal'; -import SuggestionModal from '@components/Common/Modal/SuggestionModal'; import ModalsContext from '@contexts/modals'; -import { - ComponentProps, - FunctionComponent, - useCallback, - useContext, -} from 'react'; - -export const modals = { - confirm: ConfirmModal as FunctionComponent< - ComponentProps - >, - alert: AlertModal as FunctionComponent>, - suggestion: SuggestionModal as FunctionComponent< - ComponentProps - >, -} as const; - -export type OpenModal = ( - Component: T, - props: Omit, 'open'>, -) => void; - -export type CloseModal = ( - Component: T, -) => void; +import { useCallback, useContext } from 'react'; const useModals = () => { const setModals = useContext(ModalsContext.ModalDispatch); @@ -34,23 +7,17 @@ const useModals = () => { throw new Error('ModalContext does not exists.'); } - const openModal: OpenModal = useCallback( - (Component, props) => { - setModals((modals) => [ - ...modals, - { Component, props: { ...props, open: true } }, - ]); - }, - [setModals], - ); - const closeModal: CloseModal = useCallback( - (Component) => { - setModals((modals) => - modals.filter((modal) => modal.Component !== Component), - ); + const openModal = useCallback( + (Component: React.ReactElement<{ chidren: React.ReactNode }>) => { + setModals((modals) => [...modals, { Component }]); }, [setModals], ); + + const closeModal = useCallback(() => { + setModals((modals) => modals.slice(0, -1)); + }, [setModals]); + return { openModal, closeModal, diff --git a/src/hooks/useOverlays.ts b/src/hooks/useOverlays.ts index aa7693ca..baa7086e 100644 --- a/src/hooks/useOverlays.ts +++ b/src/hooks/useOverlays.ts @@ -3,6 +3,7 @@ import { useContext } from 'react'; const useOverlays = () => { const overlayContext = useContext(OverlayContext); + if (!overlayContext) { throw new Error('OverlayContext does not exists.'); } diff --git a/src/hooks/useUserLocation.ts b/src/hooks/useUserLocation.ts index eba76d00..3abac9b9 100644 --- a/src/hooks/useUserLocation.ts +++ b/src/hooks/useUserLocation.ts @@ -1,9 +1,15 @@ import { NO_PROVIDE_LOCATION } from '@constants/pknu-map'; -import { Location } from '@type/map'; -import { useEffect, useState } from 'react'; +import UserLocationContext from '@contexts/user-location'; +import { useContext, useEffect } from 'react'; const useUserLocation = () => { - const [userLocation, setUserLocation] = useState(null); + const context = useContext(UserLocationContext); + + if (!context) { + throw new Error('UserLocationContext does not exists.'); + } + + const { userLocation, setUserLocation } = context; const success = (position: any) => { setUserLocation({ @@ -11,6 +17,7 @@ const useUserLocation = () => { LNG: position.coords.longitude, }); }; + const failed = () => { setUserLocation({ ...NO_PROVIDE_LOCATION, diff --git a/src/pages/Map/components/FilterButtons.tsx b/src/pages/Map/components/FilterButtons.tsx index a543a084..b07f64a9 100644 --- a/src/pages/Map/components/FilterButtons.tsx +++ b/src/pages/Map/components/FilterButtons.tsx @@ -1,62 +1,41 @@ import { PKNU_BUILDINGS } from '@constants/pknu-map'; import { css } from '@emotion/react'; import styled from '@emotion/styled'; +import useBuildingTypes from '@hooks/useBuildingTypes'; +import useMap from '@hooks/useMap'; import useOverlays from '@hooks/useOverlays'; +import useUserLocation from '@hooks/useUserLocation'; import { THEME } from '@styles/ThemeProvider/theme'; -import { BuildingType, Location } from '@type/map'; -import React, { CSSProperties, useEffect, useState } from 'react'; +import { BuildingType } from '@type/map'; +import React, { CSSProperties, useEffect } from 'react'; -interface FilterButtonsProps { - map: any; - userLocation: Location | null; -} - -const FilterButtons = ({ map, userLocation }: FilterButtonsProps) => { - if (!map || !userLocation) return <>; - const [activeTypes, setActiveTypes] = useState>( - { - A: true, - B: false, - C: false, - D: false, - E: false, - }, - ); +const FilterButtons = () => { + const userLocation = useUserLocation(); + const { map } = useMap(); + const { activeTypes, handleBuildingTypes } = useBuildingTypes(); const { handleOverlays } = useOverlays(); - const handleBuildingTypes = (type: BuildingType) => { - setActiveTypes((prevActiveTypes) => { - return { - ...prevActiveTypes, - [type]: !prevActiveTypes[type], - }; - }); - }; + const onClick: React.MouseEventHandler = (e) => { + if (!(e.target instanceof HTMLButtonElement)) return; - const handleButtonClick: React.MouseEventHandler = (e) => { - if (e.target instanceof HTMLSpanElement) { - handleBuildingTypes(e.target.innerText as BuildingType); - } else if ( - e.target instanceof HTMLButtonElement && - typeof e.target.textContent === 'string' - ) { - handleBuildingTypes(e.target.textContent as BuildingType); - } + handleBuildingTypes(e.target.textContent as BuildingType); }; useEffect(() => { + if (!map || !userLocation) return; + handleOverlays(activeTypes, map); - }, [activeTypes]); + }, [map, userLocation, activeTypes]); return ( - + {Object.keys(activeTypes).map((type) => ( - {type} + {type} ))} @@ -99,11 +78,4 @@ const FilterButton = styled.button` text-shadow: isActive && '0px 2px 2px rgba(0, 0, 0, 0.1)'; border: 1px solid ${isActive ? activeColor : THEME.TEXT.GRAY}; `} - transition: text-shadow 0.2s ease-in-out; - span { - display: inline-block; - position: relative; - transform: translateY(${({ isActive }) => (isActive ? '0' : '1px')}); - transition: transform 0.2s ease-in-out; - } `; diff --git a/src/pages/Map/components/MapHeader.tsx b/src/pages/Map/components/MapHeader.tsx index 1c3fb276..7a1f1cc6 100644 --- a/src/pages/Map/components/MapHeader.tsx +++ b/src/pages/Map/components/MapHeader.tsx @@ -1,21 +1,20 @@ import Icon from '@components/Common/Icon'; -import { MODAL_BUTTON_MESSAGE, MODAL_MESSAGE } from '@constants/modal-messages'; +import { MODAL_MESSAGE } from '@constants/modal-messages'; import { PKNU_BUILDINGS } from '@constants/pknu-map'; import PLCACEHOLDER_MESSAGES from '@constants/placeholder-message'; import styled from '@emotion/styled'; -import useModals, { modals } from '@hooks/useModals'; +import useMap from '@hooks/useMap'; import useOverlays from '@hooks/useOverlays'; +import useToasts from '@hooks/useToast'; import { THEME } from '@styles/ThemeProvider/theme'; import { BuildingType, PKNUBuilding } from '@type/map'; +import getBuildingInfo from '@utils/map/get-building-info'; import React, { useRef } from 'react'; -interface MapHeaderProps { - map: any; -} - -const MapHeader = ({ map }: MapHeaderProps) => { +const MapHeader = () => { + const { map } = useMap(); const inputRef = useRef(null); - const { openModal, closeModal } = useModals(); + const { addToast } = useToasts(); const { addOverlay } = useOverlays(); const handleZoomIn = (buildingType: BuildingType, building: PKNUBuilding) => { @@ -27,39 +26,22 @@ const MapHeader = ({ map }: MapHeaderProps) => { ); }; - const getBuildingInfo = ( - keyword: string, - ): [BuildingType, number] | undefined => { - keyword = keyword.split(' ').join('').toUpperCase(); - for (const buildingType of Object.keys(PKNU_BUILDINGS)) { - const index = PKNU_BUILDINGS[ - buildingType as BuildingType - ].buildings.findIndex( - (PKNU_BUILDING) => - PKNU_BUILDING.buildingName === keyword || - PKNU_BUILDING.buildingNumber === keyword, - ); - if (index !== -1) return [buildingType as BuildingType, index]; - } - return; - }; const handleBuildingSearch = (e: React.FormEvent) => { e.preventDefault(); - if (!inputRef.current || inputRef.current.value.length < 1) { - return openModal(modals.alert, { - message: MODAL_MESSAGE.ALERT.NO_SEARCH_KEYWORD, - buttonMessage: MODAL_BUTTON_MESSAGE.CLOSE, - onClose: () => closeModal(modals.alert), - }); + + const hasSearchKeyword = + inputRef.current && inputRef.current.value.length >= 1; + if (!hasSearchKeyword) { + addToast(MODAL_MESSAGE.ALERT.NO_SEARCH_KEYWORD); + return; } + const searchResult = getBuildingInfo(inputRef.current?.value); if (!searchResult) { - return openModal(modals.alert, { - message: MODAL_MESSAGE.ALERT.SEARCH_FAILED, - buttonMessage: MODAL_BUTTON_MESSAGE.CLOSE, - onClose: () => closeModal(modals.alert), - }); + addToast(MODAL_MESSAGE.ALERT.SEARCH_FAILED); + return; } + const [buildingType, index] = searchResult; inputRef.current.value = ''; handleZoomIn(buildingType, PKNU_BUILDINGS[buildingType].buildings[index]); diff --git a/src/pages/Map/components/PknuMap.tsx b/src/pages/Map/components/PknuMap.tsx new file mode 100644 index 00000000..672e4d43 --- /dev/null +++ b/src/pages/Map/components/PknuMap.tsx @@ -0,0 +1,55 @@ +import Modal from '@components/Common/Modal'; +import { MODAL_MESSAGE } from '@constants/modal-messages'; +import styled from '@emotion/styled'; +import useMap from '@hooks/useMap'; +import useModals from '@hooks/useModals'; +import useUserLocation from '@hooks/useUserLocation'; +import { isUserInShcool } from '@utils/map'; +import React, { useEffect } from 'react'; + +import { handleMapBoundary } from '../handlers'; + +const PknuMap = () => { + const { openModal, closeModal } = useModals(); + const { map, setPknuMap } = useMap(); + const userLocation = useUserLocation(); + + useEffect(() => { + setPknuMap(); + }, []); + + useEffect(() => { + if (!map) return; + + if (!userLocation) { + openModal( + + + , + ); + return; + } + closeModal(); + + if (!isUserInShcool(userLocation.LAT, userLocation.LNG)) return; + + const userLocationMarker = new window.kakao.maps.Marker({ + position: new window.kakao.maps.LatLng( + userLocation.LAT, + userLocation.LNG, + ), + }); + userLocationMarker.setMap(map); + }, [map, userLocation]); + + handleMapBoundary(map); + + return ; +}; + +export default PknuMap; + +const Map = styled.div` + height: calc(100vh - 90px); + width: 100%; +`; diff --git a/src/pages/Map/components/RefreshButtons.tsx b/src/pages/Map/components/RefreshButtons.tsx index 615b0581..49303995 100644 --- a/src/pages/Map/components/RefreshButtons.tsx +++ b/src/pages/Map/components/RefreshButtons.tsx @@ -2,22 +2,23 @@ import Icon from '@components/Common/Icon'; import { PKNU_MAP_CENTER } from '@constants/pknu-map'; import TOAST_MESSAGES from '@constants/toast-message'; import styled from '@emotion/styled'; +import useMap from '@hooks/useMap'; import useToasts from '@hooks/useToast'; +import useUserLocation from '@hooks/useUserLocation'; import { THEME } from '@styles/ThemeProvider/theme'; import { Location } from '@type/map'; import { hasLocationPermission, isUserInShcool } from '@utils/map'; import React from 'react'; -interface RefreshButtonsProps { - map: any; - userLocation: Location | null; -} +const RefreshButtons = () => { + const { map } = useMap(); + const userLocation = useUserLocation(); -const RefreshButtons = ({ map, userLocation }: RefreshButtonsProps) => { - if (!map || !userLocation) return <>; const { addToast } = useToasts(); - const handleMapCenter = (location: Location) => { + const handleMapCenter = (location: Location | null) => { + if (!location) return; + if (!hasLocationPermission(location)) { addToast(TOAST_MESSAGES.SHARE_LOCATION); return; @@ -26,6 +27,7 @@ const RefreshButtons = ({ map, userLocation }: RefreshButtonsProps) => { addToast(TOAST_MESSAGES.OUT_OF_SHOOL); return; } + const centerLocation = new window.kakao.maps.LatLng( location.LAT, location.LNG, diff --git a/src/pages/Map/components/index.ts b/src/pages/Map/components/index.ts index c63d33d0..ff304975 100644 --- a/src/pages/Map/components/index.ts +++ b/src/pages/Map/components/index.ts @@ -1,3 +1,4 @@ +export { default as PknuMap } from './PknuMap'; export { default as MapHeader } from './MapHeader'; export { default as FilterButtons } from './FilterButtons'; export { default as RefreshButtons } from './RefreshButtons'; diff --git a/src/pages/Map/index.tsx b/src/pages/Map/index.tsx index 6c998db7..1f571044 100644 --- a/src/pages/Map/index.tsx +++ b/src/pages/Map/index.tsx @@ -1,12 +1,4 @@ -import { PKNU_MAP_CENTER_LOCATION } from '@constants/pknu-map'; -import styled from '@emotion/styled'; -import useLocationHandler from '@hooks/useLocationHandler'; -import useModals from '@hooks/useModals'; -import useUserLocation from '@hooks/useUserLocation'; -import { useEffect, useState } from 'react'; - -import { FilterButtons, MapHeader, RefreshButtons } from './components'; -import handleMapBoundary from './handlers/boundary'; +import Map from '@components/Providers/MapProvider'; declare global { interface Window { @@ -14,46 +6,15 @@ declare global { } } -const Map = () => { - const [map, setMap] = useState(null); - const { openModal, closeModal } = useModals(); - const userLocation = useUserLocation(); - - useEffect(() => { - const container = document.getElementById('map'); - const options = { - center: PKNU_MAP_CENTER_LOCATION, - level: 4, - minLevel: 1, - maxLevel: 4, - }; - const map = new window.kakao.maps.Map(container as HTMLDivElement, options); - setMap(map); - }, []); - - useLocationHandler(map, openModal, closeModal); - handleMapBoundary(map); - +const MapPage = () => { return ( - - - - - - + + + + + + ); }; -export default Map; - -const MapContainer = styled.section` - height: calc(100vh - 8vh); - display: flex; - flex-direction: column; - position: relative; -`; - -const KakaoMap = styled.div` - height: calc(100vh - 90px); - width: 100%; -`; +export default MapPage; diff --git a/src/pages/My/index.test.tsx b/src/pages/My/index.test.tsx index 4ff828cd..b92eb2d8 100644 --- a/src/pages/My/index.test.tsx +++ b/src/pages/My/index.test.tsx @@ -1,6 +1,8 @@ +import Modal from '@components/Common/Modal'; import MajorProvider from '@components/Providers/MajorProvider'; import ModalsProvider from '@components/Providers/ModalsProvider'; -import useModals, { modals } from '@hooks/useModals'; +import { MODAL_BUTTON_MESSAGE, MODAL_MESSAGE } from '@constants/modal-messages'; +import useModals from '@hooks/useModals'; import { render, screen } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; import { act } from 'react-dom/test-utils'; @@ -32,7 +34,8 @@ describe('마이 페이지 동작 테스트', () => { afterEach(() => { jest.clearAllMocks(); }); - it('건의사항 남기기 버튼 클릭 시 모달 렌더링 테스트', async () => { + + it.skip('건의사항 남기기 버튼 클릭 시 모달 렌더링 테스트', async () => { render( @@ -46,11 +49,15 @@ describe('마이 페이지 동작 테스트', () => { await userEvent.click(modalButton); }); - expect(useModals().openModal).toHaveBeenCalledWith(modals.suggestion, { - title: '건의사항', - buttonMessage: '보내기', - onClose: expect.any(Function), - }); + expect(useModals().openModal).toHaveBeenCalledWith( + + + + , + ); }); it('전공수정 버튼 클릭 시 페이지 이동 테스트', async () => { diff --git a/src/pages/My/index.tsx b/src/pages/My/index.tsx index 45ed7baa..567dae49 100644 --- a/src/pages/My/index.tsx +++ b/src/pages/My/index.tsx @@ -2,13 +2,14 @@ import http from '@apis/http'; import Button from '@components/Common/Button'; import ToggleButton from '@components/Common/Button/Toggle'; import Icon from '@components/Common/Icon'; +import Modal from '@components/Common/Modal'; import { SERVER_URL } from '@config/index'; import { MODAL_BUTTON_MESSAGE, MODAL_MESSAGE } from '@constants/modal-messages'; import { css } from '@emotion/react'; import styled from '@emotion/styled'; import urlBase64ToUint8Array from '@hooks/urlBase64ToUint8Array'; import useMajor from '@hooks/useMajor'; -import useModals, { modals } from '@hooks/useModals'; +import useModals from '@hooks/useModals'; import useRouter from '@hooks/useRouter'; import { THEME } from '@styles/ThemeProvider/theme'; import { MouseEventHandler, useEffect, useState } from 'react'; @@ -18,7 +19,7 @@ const My = () => { const [animation, setAnimation] = useState(false); const { major } = useMajor(); const { routerTo } = useRouter(); - const { openModal, closeModal } = useModals(); + const { openModal } = useModals(); const routerToMajorDecision = () => routerTo('/major-decision'); @@ -36,54 +37,50 @@ const My = () => { ); }; - const handleSuggestionModal = () => { - openModal(modals.suggestion, { - title: MODAL_MESSAGE.SUGGESTION_TITLE, - buttonMessage: MODAL_BUTTON_MESSAGE.SEND_SUGGESTION, - onClose: () => closeModal(modals.suggestion), - }); - }; - const handleNotiModal: MouseEventHandler = () => { if (!major) { - openModal(modals.alert, { - message: MODAL_MESSAGE.ALERT.SET_MAJOR, - buttonMessage: MODAL_BUTTON_MESSAGE.CONFIRM, - onClose: () => closeModal(modals.alert), - routerTo: () => { - closeModal(modals.alert); - routerToMajorDecision(); - }, - }); + openModal( + + + + , + ); return; } if (subscribe) { - openModal(modals.confirm, { - message: MODAL_MESSAGE.CONFIRM.STOP_ALARM, - onConfirmButtonClick: async () => { - await http.delete(`${SERVER_URL}/api/subscription/major`, { - data: { subscription: subscribe, major }, - }); - setSubscribe(null); - closeModal(modals.confirm); - localStorage.removeItem('subscribe'); - }, - onCancelButtonClick: () => { - closeModal(modals.confirm); - }, - }); + openModal( + + + + { + await http.delete(`${SERVER_URL}/api/subscription/major`, { + data: { subscription: subscribe, major }, + }); + setSubscribe(null); + localStorage.removeItem('subscribe'); + }} + /> + , + ); return; } - openModal(modals.confirm, { - message: MODAL_MESSAGE.CONFIRM.GET_ALARM, - onConfirmButtonClick: () => { - closeModal(modals.confirm); - handleSubscribeTopic(); - }, - onCancelButtonClick: () => closeModal(modals.confirm), - }); + openModal( + + + + + , + ); }; const handleSubscribeTopic = async () => { @@ -91,11 +88,12 @@ const My = () => { if (!('serviceWorker' in navigator)) { postSuggestion(); - openModal(modals.alert, { - message: MODAL_MESSAGE.ALERT.FAIL_SUBSCRIBE_NOTI1, - buttonMessage: MODAL_BUTTON_MESSAGE.CLOSE, - onClose: () => closeModal(modals.alert), - }); + openModal( + + + + , + ); return; } @@ -103,11 +101,14 @@ const My = () => { const permission = await Notification.requestPermission(); if (permission !== 'granted') { - openModal(modals.alert, { - message: MODAL_MESSAGE.ALERT.NOT_SUBSCRIB_NOTI, - buttonMessage: MODAL_BUTTON_MESSAGE.CLOSE, - onClose: () => closeModal(modals.alert), - }); + openModal( + + + + , + ); return; } @@ -131,11 +132,12 @@ const My = () => { } } catch (error) { postSuggestion(error as string); - openModal(modals.alert, { - message: MODAL_MESSAGE.ALERT.FAIL_SUBSCRIBE_NOTI2, - buttonMessage: MODAL_BUTTON_MESSAGE.CLOSE, - onClose: () => closeModal(modals.alert), - }); + openModal( + + + + , + ); return; } }; @@ -184,7 +186,7 @@ const My = () => { - diff --git a/src/utils/map/get-building-info.ts b/src/utils/map/get-building-info.ts new file mode 100644 index 00000000..4a626bb6 --- /dev/null +++ b/src/utils/map/get-building-info.ts @@ -0,0 +1,23 @@ +import { PKNU_BUILDINGS } from '@constants/pknu-map'; +import { BuildingType } from '@type/map'; + +const getBuildingInfo = ( + keyword: string, +): [BuildingType, number] | undefined => { + const formattedKeyword = keyword.replaceAll(' ', '').toUpperCase(); + + for (const buildingType of Object.keys(PKNU_BUILDINGS)) { + const index = PKNU_BUILDINGS[ + buildingType as BuildingType + ].buildings.findIndex( + (PKNU_BUILDING) => + PKNU_BUILDING.buildingName === formattedKeyword || + PKNU_BUILDING.buildingNumber === formattedKeyword, + ); + if (index !== -1) return [buildingType as BuildingType, index]; + } + + return; +}; + +export default getBuildingInfo; diff --git a/src/utils/map/location-permission.ts b/src/utils/map/location-permission.ts index c786b6a7..3f7a01c8 100644 --- a/src/utils/map/location-permission.ts +++ b/src/utils/map/location-permission.ts @@ -1,7 +1,7 @@ import { NO_PROVIDE_LOCATION } from '@constants/pknu-map'; import { Location } from '@type/map'; -const hasLocationPermission = (location: Location) => { +const hasLocationPermission = (location: Location | null) => { return ( location && JSON.stringify(location) !== JSON.stringify(NO_PROVIDE_LOCATION) );