Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

식당 마커 클릭 시 식당정보 미리보기 구현 #197

Merged
merged 9 commits into from
Dec 13, 2022
55 changes: 19 additions & 36 deletions client/src/components/MainMap/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,26 +10,27 @@ import hamburgerImageSrc from '@assets/images/hamburger.svg';
import hotdogImageSrc from '@assets/images/hotdog.svg';
import userImageSrc from '@assets/images/user.svg';

import { ReactComponent as GpsIcon } from '@assets/images/gps.svg';
import { ReactComponent as PointCircleIcon } from '@assets/images/point-circle.svg';

import { useMeetLocationStore, useSelectedCategoryStore } from '@store/index';
import {
useSelectedCategoryStore,
useMeetLocationStore,
useSelectedRestaurantDataStore,
useMapStore,
} from '@store/index';
import { useSocketStore } from '@store/socket';

import { CATEGORY_TYPE } from '@constants/category';
import { DEFAULT_ZOOM } from '@constants/map';

import LoadingScreen from '@components/LoadingScreen';
import { useNaverMaps } from '@hooks/useNaverMaps';
import useCurrentLocation from '@hooks/useCurrentLocation';

import classes from '@styles/marker.module.css';

import '@utils/MarkerClustering.js';

import { Socket } from 'socket.io-client';

import { MapControlBox, MapLayout, MapLoadingBox, MapBox } from './styles';
import { MapLayout, MapLoadingBox, MapBox } from './styles';

interface RestaurantType {
id: string;
Expand Down Expand Up @@ -73,15 +74,14 @@ function MainMap({ restaurantData, joinList }: PropsType) {

const joinListMarkersRef = useRef<Map<UserIdType, naver.maps.Marker>>(new Map());
const joinListInfoWindowsRef = useRef<Map<UserIdType, naver.maps.InfoWindow>>(new Map());

const infoWindowsRef = useRef<naver.maps.InfoWindow[]>([]);

const markerClusteringObjectsRef = useRef<Map<CATEGORY_TYPE, MarkerClustering>>(new Map());

const { selectedCategoryData } = useSelectedCategoryStore((state) => state);
const { socket } = useSocketStore((state) => state);
const { userLocation, updateUserLocation, getCurrentLocation } = useCurrentLocation();
const { meetLocation } = useMeetLocationStore();
const { updateMap } = useMapStore((state) => state);
const { meetLocation } = useMeetLocationStore((state) => state);
const { updateSelectedRestaurantData } = useSelectedRestaurantDataStore((state) => state);
Comment on lines +82 to +84
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

궁금한게 Map 을 전역 상태로 하나 더 생성하셨더라구요. 그러면 컴포넌트에 굳이 mapRef를 따로 생성하는게 의미가 있는지 궁금합니다.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

말씀하신대로 불필요해서 수정해야 할 부분입니다. 일단 기능 구현이 목적이었기 때문에 useNaverMaps훅 내부에 있는 로직을 MainMap 안에 다시 옮기는 작업을 하지 않았습니다. 😓


const setMapLocation = (location: LocationType | naver.maps.Coord | null) => {
const map = mapRef.current;
Expand Down Expand Up @@ -305,8 +305,14 @@ function MainMap({ restaurantData, joinList }: PropsType) {
infoWindowsRef.current.push(infoWindow);

// 마커 클릭 이벤트 등록
naver.maps.Event.addListener(marker, 'click', () => {
naver.maps.Event.addListener(marker, 'click', (event) => {
infoWindow.open(map, marker);

updateSelectedRestaurantData(restaurant);

map.setCenter(marker.getPosition());

event.pointerEvent.stop();
});
});

Expand Down Expand Up @@ -340,6 +346,7 @@ function MainMap({ restaurantData, joinList }: PropsType) {

closeAllRestaurantMarkerInfoWindow();
closeAllUserMarkerInfoWindow();
updateSelectedRestaurantData(null);
});
return onDragendListener;
};
Expand Down Expand Up @@ -401,6 +408,7 @@ function MainMap({ restaurantData, joinList }: PropsType) {
return;
}

updateMap(mapRef.current);
setMeetingBoundary(mapRef.current);
const initListener = onInit(mapRef.current);
const clickListener = onClick(mapRef.current);
Expand Down Expand Up @@ -449,31 +457,6 @@ function MainMap({ restaurantData, joinList }: PropsType) {
</MapLoadingBox>
)}
<MapBox ref={mapDivRef} />
<MapControlBox>
<button type="button" onClick={() => setMapLocation(meetLocation)}>
<PointCircleIcon />
</button>
<button
type="button"
onClick={async () => {
/**
* MainPage로 이동 될 부분이라
* 함수로 따로 분리하지 않았습니다.
*/
if (!(socket instanceof Socket)) {
return;
}

const location = await getCurrentLocation();
socket.emit('changeMyLocation', { userLat: location.lat, userLng: location.lng });
updateUserLocation(location);

setMapLocation(userLocation);
}}
>
<GpsIcon />
</button>
</MapControlBox>
</MapLayout>
);
}
Expand Down
23 changes: 0 additions & 23 deletions client/src/components/MainMap/styles.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -29,26 +29,3 @@ export const MapBox = styled.div`
outline: none;
}
`;

export const MapControlBox = styled.div`
position: absolute;
left: 0;
bottom: 0;
margin: 0 0 18px 8px;
z-index: ${palette.MAP_CONTROLLER_Z_INDEX};
gap: 10px;
display: flex;
flex-direction: column;

button {
width: 40px;
height: 40px;
background-color: white;
border: none;
border-radius: 5px;
display: flex;
justify-content: center;
align-items: center;
box-shadow: 0px 0px 4px rgb(0 0 0 / 50%);
}
`;
8 changes: 8 additions & 0 deletions client/src/components/RestaurantDetailLayer/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,14 @@ function RestaurantDetailLayer() {
animate={{ opacity: 1, x: 0 }}
exit={{ opacity: 0, x: 999 }}
transition={{ duration: 1 }}
/**
* 식당 요약정보 -> 식당 상세정보 경로로 레이어가 열렸을 때
* 다시 돌아갔을 경우에도 식당 요약정보가 닫히지 않도록 하기 위함.
* window에 등록한 이벤트 리스너를 실행시키지 않는다.
*/
onClick={(event) => {
event.stopPropagation();
}}
>
<RestaurantDetailModal
updateRestaurantDetailLayerStatus={updateRestaurantDetailLayerStatus}
Expand Down
62 changes: 62 additions & 0 deletions client/src/components/RestaurantPreview/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
import { useEffect, useRef } from 'react';
import RestaurantRow from '@components/RestaurantRow';
import { AnimatePresence } from 'framer-motion';
import { useRestaurantDetailLayerStatusStore, useSelectedRestaurantDataStore } from '@store/index';
import { RESTAURANT_LIST_TYPES, RESTAURANT_DETAIL_TYPES } from '@constants/modal';
import { RestaurantPreviewBox, RestaurantPreviewLayout } from './styles';

function RestaurantPreview() {
const modalRef = useRef<HTMLDivElement>(null);

const { updateRestaurantDetailLayerStatus } = useRestaurantDetailLayerStatusStore(
(state) => state
);
const { selectedRestaurantData, updateSelectedRestaurantData } = useSelectedRestaurantDataStore(
(state) => state
);

useEffect(() => {
const modalCloseWindowEvent = (e: Event) => {
const { target } = e;

if (modalRef.current && target instanceof HTMLElement && modalRef.current.contains(target)) {
return;
}

updateSelectedRestaurantData(null);
};

window.addEventListener('click', modalCloseWindowEvent);

return () => {
window.removeEventListener('click', modalCloseWindowEvent);
};
}, []);

return (
<RestaurantPreviewLayout ref={modalRef}>
{selectedRestaurantData && (
<AnimatePresence>
<RestaurantPreviewBox
initial={{ opacity: 0, y: '100%' }}
animate={{ opacity: 1, y: '0%' }}
exit={{ opacity: 0, y: '100%' }}
transition={{
duration: 0.5,
}}
onClick={() => {
updateRestaurantDetailLayerStatus(RESTAURANT_DETAIL_TYPES.show);
}}
>
<RestaurantRow
restaurant={selectedRestaurantData}
restaurantListType={RESTAURANT_LIST_TYPES.filtered}
/>
</RestaurantPreviewBox>
</AnimatePresence>
)}
</RestaurantPreviewLayout>
);
}

export default RestaurantPreview;
14 changes: 14 additions & 0 deletions client/src/components/RestaurantPreview/styles.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import styled from 'styled-components';
import * as palette from '@styles/Variables';
import { motion } from 'framer-motion';

export const RestaurantPreviewLayout = styled.div`
z-index: ${palette.MAP_CONTROLLER_Z_INDEX};
`;

export const RestaurantPreviewBox = styled(motion.div)`
width: 100%;
border-top-right-radius: 5px;
border-top-left-radius: 5px;
padding: 1% 3% 2% 3%;
`;
2 changes: 0 additions & 2 deletions client/src/components/RestaurantRow/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,6 @@ import { RESTAURANT_LIST_TYPES } from '@constants/modal';
import RestaurantVoteButton from '@components/RestaurantVoteButton';
import { distanceToDisplay } from '@utils/distance';

import { useEffect, useState } from 'react';
import { NAVER_LAT, NAVER_LNG } from '@constants/map';
import {
RestaurantRowBox,
DistanceBox,
Expand Down
3 changes: 1 addition & 2 deletions client/src/components/RestaurantRow/styles.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,7 @@ export const RestaurantRowBox = styled(motion.div)`
background-color: white;
box-sizing: border-box;
border-radius: 10px;
border: 0.1px solid ${palette.BORDER};
box-shadow: 0 0 3px 3px ${palette.BORDER};
box-shadow: 0 0 3px 2px ${palette.BORDER};
flex: none;
display: flex;
flex-direction: row;
Expand Down
82 changes: 66 additions & 16 deletions client/src/pages/MainPage/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,10 @@ import { useEffect, useState, useRef } from 'react';
import { useNavigate, useParams } from 'react-router-dom';
import { io, Socket } from 'socket.io-client';
import { useSocketStore } from '@store/socket';
import { useMeetLocationStore, useRestaurantListLayerStatusStore } from '@store/index';
import { useMeetLocationStore, useRestaurantListLayerStatusStore, useMapStore } from '@store/index';

import { ReactComponent as GpsIcon } from '@assets/images/gps.svg';
import { ReactComponent as PointCircleIcon } from '@assets/images/point-circle.svg';
import { ReactComponent as CandidateListIcon } from '@assets/images/candidate-list.svg';
import { ReactComponent as ListIcon } from '@assets/images/list-icon.svg';
import { ReactComponent as MapIcon } from '@assets/images/map-icon.svg';
Expand All @@ -22,17 +24,21 @@ import RestaurantListLayer from '@components/RestaurantListLayer';
import RestaurantDetailLayer from '@components/RestaurantDetailLayer';
import RestaurantCategory from '@components/RestaurantCategory';
import LoadingScreen from '@components/LoadingScreen';
import RestaurantPreview from '@components/RestaurantPreview';

import { apiService } from '@apis/index';

import {
MapControlBox,
ButtonInnerTextBox,
CandidateListButton,
CategoryBox,
Header,
HeaderBox,
MainPageLayout,
MapOrListButton,
FooterBox,
ControllerBox,
} from './styles';

function MainPage() {
Expand All @@ -43,14 +49,15 @@ function MainPage() {
const socketRef = useRef<Socket | null>(null);

const { setSocket } = useSocketStore((state) => state);
const { map } = useMapStore((state) => state);
const { getCurrentLocation, updateUserLocation } = useCurrentLocation();
const { meetLocation, updateMeetLocation } = useMeetLocationStore();

const [isRoomConnect, setRoomConnect] = useState<boolean>(false);
const [myId, setMyId] = useState<string>('');
const [myName, setMyName] = useState<string>('');
const [joinList, setJoinList] = useState<Map<UserIdType, UserType>>(new Map());
const [restaurantData, setRestaurantData] = useState<RestaurantType[]>([]);
const { meetLocation, updateMeetLocation } = useMeetLocationStore();

const { restaurantListLayerStatus, updateRestaurantListLayerStatus } =
useRestaurantListLayerStatusStore((state) => state);
Expand Down Expand Up @@ -196,20 +203,63 @@ function MainPage() {
<RestaurantCategory />
</CategoryBox>

{/* 식당 후보 목록 <-> 지도 화면 */}
{/* 식당 후보 목록 <-- 전체 식당 목록 */}
<CandidateListButton onClick={handleSwitchCandidateList}>
{isRestaurantCandidateList() ? <MapLocationIcon /> : <CandidateListIcon />}
</CandidateListButton>

{/* 전체 식당 목록 <-> 지도 화면 */}
{/* 전체 식당 목록 <-- 식당 후보 목록 */}
<MapOrListButton onClick={handleSwitchRestaurantList}>
{isRestaurantFilteredList() ? <MapIcon /> : <ListIcon />}
<ButtonInnerTextBox>
{isRestaurantFilteredList() ? '지도보기' : '목록보기'}
</ButtonInnerTextBox>
</MapOrListButton>
<FooterBox>
<ControllerBox>
{/* 식당 후보 목록 <-> 지도 화면 */}
{/* 식당 후보 목록 <-- 전체 식당 목록 */}
<CandidateListButton onClick={handleSwitchCandidateList}>
{isRestaurantCandidateList() ? <MapLocationIcon /> : <CandidateListIcon />}
</CandidateListButton>

{/* 전체 식당 목록 <-> 지도 화면 */}
{/* 전체 식당 목록 <-- 식당 후보 목록 */}
<MapOrListButton onClick={handleSwitchRestaurantList}>
{isRestaurantFilteredList() ? <MapIcon /> : <ListIcon />}
<ButtonInnerTextBox>
{isRestaurantFilteredList() ? '지도보기' : '목록보기'}
</ButtonInnerTextBox>
</MapOrListButton>

<MapControlBox>
<button
type="button"
onClick={() => {
if (!map || !meetLocation) {
return;
}

map.setCenter(meetLocation);
}}
>
<PointCircleIcon />
</button>
<button
type="button"
onClick={async () => {
const socket = socketRef.current;

if (!(socket instanceof Socket)) {
return;
}

const location = await getCurrentLocation();
socket.emit('changeMyLocation', { userLat: location.lat, userLng: location.lng });
updateUserLocation(location);

if (!map) {
return;
}

map.setCenter(location);
}}
>
<GpsIcon />
</button>
</MapControlBox>
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

얘를 별도 컴포넌트로 분리하는게 좋을 것 같습니다.

</ControllerBox>

<RestaurantPreview />
</FooterBox>

{/* 식당 리스트 & 식당 상세정보 Full-Screen 모달 컴포넌트 */}
<RestaurantListLayer restaurantData={restaurantData} />
Expand Down
Loading