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)
);