-
Notifications
You must be signed in to change notification settings - Fork 4
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #532 from woowacourse-teams/feat/444-map-feature
카페의 위치를 보여줄 수 있는 지도를 구현
- Loading branch information
Showing
35 changed files
with
888 additions
and
23 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,120 @@ | ||
import React, { Suspense, useEffect, useRef, useState } from 'react'; | ||
import { styled } from 'styled-components'; | ||
import { SEONGSU_BOUNDS_LOCATION, SEONGSU_CAFE_STREET_LOCATION, SEONGSU_MAP_INITIAL_ZOOM_SIZE } from '../constants'; | ||
import useCafesNearLocation from '../hooks/useCafesNearLocation'; | ||
import useCurrentPosition from '../hooks/useCurrentPosition'; | ||
import CafeMarkersContainer from './CafeMarkersContainer'; | ||
import UserMarker from './UserMarker'; | ||
|
||
const CafeMap = () => { | ||
const ref = useRef<HTMLDivElement>(null); | ||
const [googleMap, setGoogleMap] = useState<google.maps.Map | null>(null); | ||
|
||
useEffect(() => { | ||
if (!ref.current) return; | ||
|
||
const googleMap = new window.google.maps.Map(ref.current, { | ||
center: SEONGSU_CAFE_STREET_LOCATION, | ||
zoom: SEONGSU_MAP_INITIAL_ZOOM_SIZE, | ||
disableDefaultUI: true, | ||
clickableIcons: false, | ||
mapId: '32c9cce63f7772a8', | ||
maxZoom: 20, | ||
minZoom: 14, | ||
restriction: { | ||
latLngBounds: SEONGSU_BOUNDS_LOCATION, | ||
}, | ||
}); | ||
|
||
setGoogleMap(googleMap); | ||
}, []); | ||
|
||
return ( | ||
<> | ||
<div ref={ref} id="map" style={{ minHeight: '100vh' }} /> | ||
<Suspense>{googleMap && <CafeMapContent map={googleMap} />}</Suspense> | ||
</> | ||
); | ||
}; | ||
|
||
type CafeMapContentProps = { | ||
map: google.maps.Map; | ||
}; | ||
|
||
const CafeMapContent = (props: CafeMapContentProps) => { | ||
const { map } = props; | ||
|
||
const currentPosition = useCurrentPosition(); | ||
const { refetch } = useCafesNearLocation(map); | ||
|
||
const setPosition = (position: google.maps.LatLngLiteral) => { | ||
map.setCenter(position); | ||
}; | ||
|
||
const moveToCurrentUserLocation = () => { | ||
if (!currentPosition) return; | ||
|
||
const bounds = new google.maps.LatLngBounds(SEONGSU_BOUNDS_LOCATION); | ||
|
||
// 원하는 위치가 범위 내에 있는지 확인합니다. | ||
if (bounds.contains(currentPosition)) { | ||
map.panTo(currentPosition); | ||
setPosition(currentPosition); | ||
} else { | ||
// 경고 메시지를 표시합니다. | ||
alert('서비스는 성수 지역에서만 이용 가능합니다.'); | ||
map.panTo(SEONGSU_CAFE_STREET_LOCATION); | ||
setPosition(SEONGSU_CAFE_STREET_LOCATION); | ||
} | ||
}; | ||
|
||
const moveToSungsuCafeRoadLocation = () => { | ||
map.panTo(SEONGSU_CAFE_STREET_LOCATION); | ||
setPosition(SEONGSU_CAFE_STREET_LOCATION); | ||
}; | ||
|
||
useEffect(() => { | ||
const listener = map.addListener('idle', () => { | ||
refetch(); | ||
}); | ||
|
||
return () => google.maps.event.removeListener(listener); | ||
}, [map]); | ||
|
||
return ( | ||
<> | ||
<MapLocationButtonContainer> | ||
<MapLocationButton onClick={moveToCurrentUserLocation}>내 위치로 이동</MapLocationButton> | ||
<MapLocationButton onClick={moveToSungsuCafeRoadLocation}>성수동 카페거리로 이동</MapLocationButton> | ||
</MapLocationButtonContainer> | ||
|
||
<CafeMarkersContainer map={map} /> | ||
{currentPosition && <UserMarker map={map} position={currentPosition} />} | ||
</> | ||
); | ||
}; | ||
|
||
export default React.memo(CafeMap); | ||
|
||
const MapLocationButtonContainer = styled.div` | ||
position: absolute; | ||
z-index: 999; | ||
top: 65px; | ||
display: flex; | ||
gap: 10px; | ||
width: 100%; | ||
padding: 10px; | ||
`; | ||
|
||
const MapLocationButton = styled.button` | ||
padding: 10px; | ||
color: ${({ theme }) => theme.color.black}; | ||
background-color: ${({ theme }) => theme.color.white}; | ||
border: none; | ||
border-radius: 10px; | ||
box-shadow: ${({ theme }) => theme.shadow.map}; | ||
`; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,174 @@ | ||
import type { MouseEventHandler } from 'react'; | ||
import { useCallback, useEffect } from 'react'; | ||
import { createRoot } from 'react-dom/client'; | ||
import { TfiClose } from 'react-icons/tfi'; | ||
import { styled } from 'styled-components'; | ||
import useObservable from '../hooks/useObservable'; | ||
import type { CafeMapLocation } from '../types'; | ||
import Observable from '../utils/Observable'; | ||
|
||
const openedCafeIdObserverable = new Observable<number | null>(null); | ||
|
||
type CafeMarkerProps = { | ||
cafeLocation: CafeMapLocation; | ||
}; | ||
|
||
const CafeMarker = (props: CafeMarkerProps) => { | ||
const { cafeLocation } = props; | ||
const [openedCafeId, setOpenedCafeId] = useObservable(openedCafeIdObserverable); | ||
|
||
const handleCloseModal = useCallback(() => { | ||
setOpenedCafeId(null); | ||
}, []); | ||
|
||
const handleOpenModal: MouseEventHandler = useCallback(() => { | ||
setOpenedCafeId(cafeLocation.id); | ||
}, []); | ||
|
||
return ( | ||
<div onClick={(event) => event.stopPropagation()}> | ||
{openedCafeId === cafeLocation.id && ( | ||
<ModalContainer> | ||
<ModalCloseButtonContainer> | ||
<ModalCloseButton onClick={handleCloseModal}> | ||
<TfiClose /> | ||
</ModalCloseButton> | ||
</ModalCloseButtonContainer> | ||
<ModalTitle>{cafeLocation.name}</ModalTitle> | ||
<ModalSubtitle>{cafeLocation.address}</ModalSubtitle> | ||
<ModalContent></ModalContent> | ||
<ModalButtonContainer> | ||
<StyledLink href={`https://yozm.cafe/cafes/${cafeLocation.id}`} target="_blank" rel="noreferrer"> | ||
<ModalDetailButton onClick={handleCloseModal}>상세보기</ModalDetailButton> | ||
</StyledLink> | ||
</ModalButtonContainer> | ||
</ModalContainer> | ||
)} | ||
<Marker onClick={handleOpenModal}> | ||
<MarkerIcon /> | ||
</Marker> | ||
</div> | ||
); | ||
}; | ||
|
||
type CafeMarkersProps = { | ||
map: google.maps.Map; | ||
cafeLocation: CafeMapLocation; | ||
}; | ||
|
||
const CafeMarkers = (props: CafeMarkersProps) => { | ||
const { map, cafeLocation } = props; | ||
const { latitude, longitude, name } = cafeLocation; | ||
|
||
useEffect(() => { | ||
const container = document.createElement('div'); | ||
const markerRoot = createRoot(container); | ||
|
||
const newMarker = new google.maps.marker.AdvancedMarkerElement({ | ||
position: { lat: latitude, lng: longitude }, | ||
map, | ||
title: name, | ||
content: container, | ||
}); | ||
|
||
const handleOpenedCafeIdChange = () => { | ||
const isOpened = openedCafeIdObserverable.getState() === cafeLocation.id; | ||
newMarker.zIndex = isOpened ? 1 : 0; | ||
}; | ||
|
||
openedCafeIdObserverable.subscribe(handleOpenedCafeIdChange); | ||
|
||
markerRoot.render(<CafeMarker cafeLocation={cafeLocation} />); | ||
|
||
// eslint-disable-next-line @typescript-eslint/no-empty-function | ||
const noop = () => {}; | ||
|
||
newMarker.addListener('click', noop); | ||
|
||
return () => { | ||
openedCafeIdObserverable.unsubscribe(handleOpenedCafeIdChange); | ||
}; | ||
}, []); | ||
|
||
return <></>; | ||
}; | ||
|
||
export default CafeMarkers; | ||
|
||
const Marker = styled.button` | ||
position: absolute; | ||
transform: translate(-50%, -50%); | ||
`; | ||
|
||
const MarkerIcon = styled.img.attrs({ src: '/assets/coffee-icon.png', alt: '카페마커' })``; | ||
|
||
const ModalContainer = styled.div` | ||
position: absolute; | ||
transform: translate(-50%, -100%) translateY(-30px) translateZ(100px); | ||
width: max-content; | ||
min-width: 160px; | ||
padding: 16px; | ||
background: white; | ||
border: 1px solid rgba(0, 0, 0, 0.05); | ||
border-radius: 10px; | ||
box-shadow: rgba(0, 0, 0, 0.12) 0px 2px 4px 0px; | ||
&::before { | ||
content: ''; | ||
position: absolute; | ||
top: 100%; | ||
left: 50%; | ||
transform: translateX(-50%); | ||
border-color: white transparent transparent transparent; | ||
border-style: solid; | ||
border-width: 10px; | ||
} | ||
`; | ||
|
||
const ModalTitle = styled.h2` | ||
margin-bottom: 8px; | ||
font-size: 20px; | ||
`; | ||
|
||
const ModalSubtitle = styled.h3` | ||
font-size: 12px; | ||
`; | ||
|
||
const ModalContent = styled.div` | ||
margin-bottom: 16px; | ||
`; | ||
|
||
const ModalButtonContainer = styled.div` | ||
display: flex; | ||
justify-content: space-between; | ||
width: 100%; | ||
`; | ||
|
||
const ModalCloseButtonContainer = styled.div` | ||
display: flex; | ||
flex-direction: row-reverse; | ||
`; | ||
|
||
const StyledLink = styled.a` | ||
width: 100%; | ||
`; | ||
|
||
const ModalCloseButton = styled.button``; | ||
|
||
const ModalDetailButton = styled.button` | ||
width: 100%; | ||
padding: 8px 16px; | ||
color: white; | ||
background-color: #f08080; | ||
border: none; | ||
border-radius: 10px; | ||
&:hover { | ||
background-color: #f4a9a8; | ||
} | ||
`; |
Oops, something went wrong.