diff --git a/frontend/.eslintrc.js b/frontend/.eslintrc.js index 9605c722f..28b667cda 100644 --- a/frontend/.eslintrc.js +++ b/frontend/.eslintrc.js @@ -39,7 +39,7 @@ module.exports = { 'no-unused-expressions': 'off', 'react/jsx-props-no-spreading': 'off', 'react/no-unused-prop-types': 'off', - 'import/no-extraneous-dependencies': 'off', + 'no-underscore-dangle': 'off', }, settings: { 'import/resolver': { diff --git a/frontend/src/components/Map/index.tsx b/frontend/src/components/Map/index.tsx index e0471966a..aa347ac3b 100644 --- a/frontend/src/components/Map/index.tsx +++ b/frontend/src/components/Map/index.tsx @@ -23,7 +23,6 @@ const getZoomMinLimit = () => { function Map() { const { Tmapv3 } = window; - const { markers } = useContext(MarkerContext); const { width } = useContext(LayoutWidthContext); const { mapInstance, setMapInstance } = useMapStore((state) => state); @@ -47,26 +46,25 @@ function Map() { const map = new Tmapv3.Map(mapContainer.current, { center: new Tmapv3.LatLng(37.5154, 127.1029), scaleBar: false, + width: '100%', + height: '100%', }); - if (!map) return; - map.setZoomLimit(getZoomMinLimit(), 17); setMapInstance(map); - // eslint-disable-next-line consistent-return return () => { map.destroy(); }; }, []); - useMapClick(mapInstance); - useClickedCoordinate(mapInstance); - useUpdateCoordinates(mapInstance); + useMapClick(); + useClickedCoordinate(); + useUpdateCoordinates(); - useFocusToMarker(mapInstance, markers); - onFocusClickedPin(mapInstance, markers); + useFocusToMarker(markers); + onFocusClickedPin(); return ( @@ -84,6 +82,8 @@ function Map() { } const MapContainer = styled.div` + width: 100%; + height: 100%; position: relative; `; @@ -116,4 +116,4 @@ const CurrentLocationIcon = styled(CurrentLocation)` } `; -export default Map; \ No newline at end of file +export default Map; diff --git a/frontend/src/components/common/Flex/index.ts b/frontend/src/components/common/Flex/index.ts index 713a02872..92cdc63c7 100644 --- a/frontend/src/components/common/Flex/index.ts +++ b/frontend/src/components/common/Flex/index.ts @@ -1,8 +1,11 @@ import { styled } from 'styled-components'; -import Box, { BoxProps } from '../Box'; +import theme from '../../../themes'; +import { colorThemeKey } from '../../../themes/color'; +import { radiusKey } from '../../../themes/radius'; +import { SpaceThemeKeys } from '../../../themes/spacing'; -interface FlexProps extends BoxProps { +interface FlexProps { $flexDirection?: string; $flexWrap?: string; $flexBasis?: string; @@ -14,9 +17,34 @@ interface FlexProps extends BoxProps { $justifyItems?: string; flex?: string; $gap?: string; + + width?: string; + height?: string; + $minWidth?: string; + $minHeight?: string; + $maxWidth?: string; + $maxHeight?: string; + padding?: SpaceThemeKeys | string; + $backgroundColor?: colorThemeKey; + $backdropFilter?: string; + overflow?: string; + color?: colorThemeKey; + position?: string; + right?: string; + top?: string; + left?: string; + bottom?: string; + $borderRadius?: radiusKey; + $borderTop?: string; + $borderRight?: string; + $borderBottom?: string; + $borderLeft?: string; + cursor?: string; + opacity?: string; + $zIndex?: number; } -const Flex = styled(Box)` +const Flex = styled.div` display: flex; flex-direction: ${({ $flexDirection }) => $flexDirection}; flex-wrap: ${({ $flexWrap }) => $flexWrap}; @@ -29,6 +57,39 @@ const Flex = styled(Box)` justify-items: ${({ $justifyItems }) => $justifyItems}; flex: ${({ flex }) => flex}; gap: ${({ $gap }) => $gap}; + + background-color: ${({ $backgroundColor }) => + $backgroundColor && theme.color[$backgroundColor]}; + backdrop-filter: ${({ $backdropFilter }) => $backdropFilter}; + color: ${({ color }) => color && theme.color[color]}; + padding: ${({ padding }) => padding && convertPadding(padding)}; + width: ${({ width }) => width}; + height: ${({ height }) => height}; + min-width: ${({ $minWidth }) => $minWidth}; + min-height: ${({ $minHeight }) => $minHeight}; + max-width: ${({ $maxWidth }) => $maxWidth}; + max-height: ${({ $maxHeight }) => $maxHeight}; + overflow: ${({ overflow }) => overflow}; + position: ${({ position }) => position}; + right: ${({ right }) => right}; + top: ${({ top }) => top}; + left: ${({ left }) => left}; + bottom: ${({ bottom }) => bottom}; + border-radius: ${({ $borderRadius }) => + $borderRadius && theme.radius[$borderRadius]}; + border-top: ${({ $borderTop }) => $borderTop}; + border-right: ${({ $borderRight }) => $borderRight}; + border-bottom: ${({ $borderBottom }) => $borderBottom}; + border-left: ${({ $borderLeft }) => $borderLeft}; + cursor: ${({ cursor }) => cursor}; + opacity: ${({ opacity }) => opacity}; + z-index: ${({ $zIndex }) => $zIndex}; `; +const convertPadding = (padding: SpaceThemeKeys | string) => { + if (typeof padding === 'string' && padding.length > 1) return padding; + + return theme.spacing[Number(padding)]; +}; + export default Flex; diff --git a/frontend/src/constants/index.ts b/frontend/src/constants/index.ts index ca04016c6..e99ff3a62 100644 --- a/frontend/src/constants/index.ts +++ b/frontend/src/constants/index.ts @@ -11,3 +11,5 @@ export const DEFAULT_PROFILE_IMAGE = export const DEFAULT_PROD_URL = process.env.APP_URL || 'https://mapbefine.kro.kr/api'; + +export const PIN_SIZE = 60; diff --git a/frontend/src/constants/pinImage.ts b/frontend/src/constants/pinImage.ts index 3473a659c..25509c78b 100644 --- a/frontend/src/constants/pinImage.ts +++ b/frontend/src/constants/pinImage.ts @@ -9,7 +9,7 @@ export const USER_LOCATION_IMAGE = ` + 1: ` @@ -18,7 +18,7 @@ export const pinImageMap: PinImageMap = { `, - 2: ` + 2: ` @@ -27,7 +27,7 @@ export const pinImageMap: PinImageMap = { `, - 3: ` + 3: ` @@ -36,7 +36,7 @@ export const pinImageMap: PinImageMap = { `, - 4: ` + 4: ` @@ -45,7 +45,7 @@ export const pinImageMap: PinImageMap = { `, - 5: ` + 5: ` @@ -54,7 +54,7 @@ export const pinImageMap: PinImageMap = { `, - 6: ` + 6: ` @@ -63,7 +63,7 @@ export const pinImageMap: PinImageMap = { `, - 7: ` + 7: ` @@ -83,3 +83,40 @@ export const pinColors: PinImageMap = { 6: '#FD842D', 7: '#C340B6', }; + +export const getInfoWindowTemplate = ({ + backgroundColor, + pinName, + pins, + condition, +}: { + backgroundColor: string; + pinName: string; + pins: []; + condition: number; +}) => ` +
+${ + condition !== 1 + ? pins + .map( + ( + pin: any, + ) => `
+ ${pin.name} +
`, + ) + .join('') + : `
+ ${pinName} +
+ ${ + pins.length > 1 + ? ` +
+${pins.length}
+ ` + : '' + } +
` +} +`; diff --git a/frontend/src/context/MarkerContext.tsx b/frontend/src/context/MarkerContext.tsx index 2410c0e31..ec46069c4 100644 --- a/frontend/src/context/MarkerContext.tsx +++ b/frontend/src/context/MarkerContext.tsx @@ -1,18 +1,23 @@ import { createContext, useContext, useState } from 'react'; import { useLocation, useParams } from 'react-router-dom'; -import { pinColors, pinImageMap } from '../constants/pinImage'; +import { + getInfoWindowTemplate, + pinColors, + pinImageMap, +} from '../constants/pinImage'; import useNavigator from '../hooks/useNavigator'; +import useMapStore from '../store/mapInstance'; import { Coordinate, CoordinatesContext } from './CoordinatesContext'; type MarkerContextType = { markers: Marker[]; clickedMarker: Marker | null; - createMarkers: (map: TMap) => void; + createMarkers: () => void; removeMarkers: () => void; removeInfowindows: () => void; - createInfowindows: (map: TMap) => void; - displayClickedMarker: (map: TMap) => void; + createInfowindows: () => void; + displayClickedMarker: () => void; }; const defaultMarkerContext = () => { @@ -35,6 +40,7 @@ interface Props { function MarkerProvider({ children }: Props): JSX.Element { const { Tmapv3 } = window; + const { mapInstance } = useMapStore((state) => state); const [markers, setMarkers] = useState([]); const [infoWindows, setInfoWindows] = useState(null); const [clickedMarker, setClickedMarker] = useState(null); @@ -43,19 +49,33 @@ function MarkerProvider({ children }: Props): JSX.Element { const { routePage } = useNavigator(); const { pathname } = useLocation(); - const createMarker = ( - coordinate: Coordinate, - map: TMap, - markerType: number, - ) => + const createElementsInScreenSize = () => { + if (!mapInstance) return; + + const mapBounds = mapInstance.getBounds(); + const northEast = mapBounds._ne; + const southWest = mapBounds._sw; + + const coordinatesInScreenSize = coordinates.filter( + (coordinate: any) => + coordinate.latitude <= northEast._lat && + coordinate.latitude >= southWest._lat && + coordinate.longitude <= northEast._lng && + coordinate.longitude >= southWest._lng, + ); + + return coordinatesInScreenSize; + }; + + const createMarker = (coordinate: Coordinate, markerType: number) => new Tmapv3.Marker({ position: new Tmapv3.LatLng(coordinate.latitude, coordinate.longitude), iconHTML: pinImageMap[markerType + 1], - map, + map: mapInstance, }); // 현재 클릭된 좌표의 마커 생성 - const displayClickedMarker = (map: TMap) => { + const displayClickedMarker = () => { if (clickedMarker) { clickedMarker.setMap(null); } @@ -65,23 +85,27 @@ function MarkerProvider({ children }: Props): JSX.Element { clickedCoordinate.longitude, ), icon: 'http://tmapapi.sktelecom.com/upload/tmap/marker/pin_g_m_m.png', - map, + map: mapInstance, }); marker.id = 'clickedMarker'; setClickedMarker(marker); }; // coordinates를 받아서 marker를 생성하고, marker를 markers 배열에 추가 - const createMarkers = (map: TMap) => { + const createMarkers = () => { let markerType = -1; let currentTopicId = '-1'; - const newMarkers = coordinates.map((coordinate: any) => { + const markersInScreenSize = createElementsInScreenSize(); + + if (!markersInScreenSize) return; + + const newMarkers = markersInScreenSize.map((coordinate: any) => { if (currentTopicId !== coordinate.topicId) { markerType = (markerType + 1) % 7; currentTopicId = coordinate.topicId; } - const marker = createMarker(coordinate, map, markerType); + const marker = createMarker(coordinate, markerType); marker.id = String(coordinate.id); return marker; }); @@ -96,14 +120,29 @@ function MarkerProvider({ children }: Props): JSX.Element { routePage(`/see-together/${topicId}?pinDetail=${marker.id}`); }); }); + setMarkers(newMarkers); }; - const createInfowindows = (map: TMap) => { + const getCondition = (pins: any) => { + if (!mapInstance) return; + + if (mapInstance.getZoom() === 17 && pins.length > 1) { + return pins.length; + } + + return 1; + }; + + const createInfowindows = () => { let markerType = -1; let currentTopicId = '-1'; - const newInfowindows = coordinates.map((coordinate: any) => { + const windowsInScreenSize = createElementsInScreenSize(); + + if (!windowsInScreenSize) return; + + const newInfowindows = windowsInScreenSize.map((coordinate: any) => { if (currentTopicId !== coordinate.topicId) { markerType = (markerType + 1) % 7; currentTopicId = coordinate.topicId; @@ -113,13 +152,17 @@ function MarkerProvider({ children }: Props): JSX.Element { position: new Tmapv3.LatLng(coordinate.latitude, coordinate.longitude), border: 0, background: 'transparent', - content: `
${coordinate.pinName}
`, - offset: new Tmapv3.Point(0, -60), + content: getInfoWindowTemplate({ + backgroundColor: pinColors[markerType + 1], + pinName: coordinate.pinName, + pins: coordinate.pins, + condition: getCondition(coordinate.pins), + }), + offset: new Tmapv3.Point(0, -64), type: 2, - map, + map: mapInstance, }); + return infoWindow; }); @@ -132,7 +175,9 @@ function MarkerProvider({ children }: Props): JSX.Element { }; const removeInfowindows = () => { - infoWindows?.forEach((infoWindow: InfoWindow) => infoWindow.setMap(null)); + infoWindows?.forEach((infoWindow: InfoWindow) => { + infoWindow.setMap(null); + }); setInfoWindows([]); }; diff --git a/frontend/src/hooks/useAnimateClickedPin.ts b/frontend/src/hooks/useAnimateClickedPin.ts index 1002af841..b12dd059b 100644 --- a/frontend/src/hooks/useAnimateClickedPin.ts +++ b/frontend/src/hooks/useAnimateClickedPin.ts @@ -1,21 +1,38 @@ -import { useEffect, useState } from 'react'; +import { useContext, useEffect, useState } from 'react'; + +import { CoordinatesContext } from '../context/CoordinatesContext'; +import { MarkerContext } from '../context/MarkerContext'; +import useMapStore from '../store/mapInstance'; const useAnimateClickedPin = () => { + const { Tmapv3 } = window; const queryParams = new URLSearchParams(location.search); + const { mapInstance } = useMapStore((state) => state); const [checkQueryParams, setCheckQueryParams] = useState(queryParams); + const { coordinates } = useContext(CoordinatesContext); + const { removeMarkers, removeInfowindows, createMarkers, createInfowindows } = + useContext(MarkerContext); - const onFocusClickedPin = (map: TMap | null, markers: Marker[]) => { + const onFocusClickedPin = () => { useEffect(() => { const currentQueryParams = new URLSearchParams(location.search); + // TODO : 이 부분 로직 검토해보기 if (checkQueryParams === null) { - if (!map) return; + if (!mapInstance) return; const pinId = queryParams.get('pinDetail'); - const marker = markers.find((marker: Marker) => marker.id === pinId); + const clickedMarker = coordinates + .map((pin: any) => { + if (pin.pins.map((pin: any) => pin.id).includes(Number(pinId))) { + return new Tmapv3.LatLng(pin.latitude, pin.longitude); + } + return null; + }) + .find((latLng) => latLng); - if (!marker) return; + if (!clickedMarker) return; - map.setCenter(marker.getPosition()); + mapInstance.setCenter(clickedMarker); setCheckQueryParams(currentQueryParams); return; @@ -26,15 +43,28 @@ const useAnimateClickedPin = () => { currentQueryParams.get('pinDetail') ) { const pinId = queryParams.get('pinDetail'); - const marker = markers.find((marker: Marker) => marker.id === pinId); + const clickedMarker = coordinates + .map((pin: any) => { + if (pin.pins.map((pin: any) => pin.id).includes(Number(pinId))) { + return new Tmapv3.LatLng(pin.latitude, pin.longitude); + } + return null; + }) + .find((latLng) => latLng); - if (marker && map) { - map.setCenter(marker.getPosition()); - map.setZoom(17); + // TODO: useUpdateCoordinates 훅이랑 실행 순서 차이로 인한 업데이트 오류 있는 듯 보임. 이 훅은 sidebar 전용으로 만들어볼 것 + if (clickedMarker && mapInstance) { + removeMarkers(); + removeInfowindows(); + mapInstance.setCenter(clickedMarker); + mapInstance.setZoom(17); + createMarkers(); + createInfowindows(); } + setCheckQueryParams(currentQueryParams); } - }, [markers, map, queryParams]); + }, [coordinates, mapInstance, queryParams.get('pinDetail')]); }; return { checkQueryParams, onFocusClickedPin }; diff --git a/frontend/src/hooks/useClickedCoordinate.ts b/frontend/src/hooks/useClickedCoordinate.ts index f206b889d..a7f352a9e 100644 --- a/frontend/src/hooks/useClickedCoordinate.ts +++ b/frontend/src/hooks/useClickedCoordinate.ts @@ -2,24 +2,22 @@ import { useContext, useEffect } from 'react'; import { CoordinatesContext } from '../context/CoordinatesContext'; import { MarkerContext } from '../context/MarkerContext'; +import useMapStore from '../store/mapInstance'; -export default function useClickedCoordinate(map: TMap | null) { +export default function useClickedCoordinate() { const { Tmapv3 } = window; + const { mapInstance } = useMapStore((state) => state); const { clickedCoordinate } = useContext(CoordinatesContext); const { displayClickedMarker } = useContext(MarkerContext); useEffect(() => { - if (!map) return; - const currentZoom = map.getZoom(); - if (clickedCoordinate.address) displayClickedMarker(map); + if (!mapInstance) return; + const currentZoom = mapInstance.getZoom(); + if (clickedCoordinate.address) displayClickedMarker(mapInstance); // 선택된 좌표가 있으면 해당 좌표로 지도의 중심을 이동 if (clickedCoordinate.latitude && clickedCoordinate.longitude) { - if (currentZoom <= 17) { - map.setZoom(17); - } - - map.panTo( + mapInstance.panTo( new Tmapv3.LatLng( clickedCoordinate.latitude, clickedCoordinate.longitude, diff --git a/frontend/src/hooks/useFocusToMarkers.ts b/frontend/src/hooks/useFocusToMarkers.ts index 80b2cd313..5162de018 100644 --- a/frontend/src/hooks/useFocusToMarkers.ts +++ b/frontend/src/hooks/useFocusToMarkers.ts @@ -1,15 +1,15 @@ import { useEffect, useRef, useState } from 'react'; -const useFocusToMarker = (map: TMap | null, markers: Marker[]) => { +import useMapStore from '../store/mapInstance'; + +const useFocusToMarker = (markers: Marker[]) => { const { Tmapv3 } = window; + const { mapInstance } = useMapStore((state) => state); const bounds = useRef(new Tmapv3.LatLngBounds()); const [markersLength, setMarkersLength] = useState(0); useEffect(() => { - if (map && markers && markers.length === 1) { - map.panTo(markers[0].getPosition()); - } - if (map && markers && markers.length > 1) { + if (mapInstance && markers && markers.length >= 1) { bounds.current = new Tmapv3.LatLngBounds(); markers.forEach((marker: Marker) => { bounds.current.extend(marker.getPosition()); @@ -17,11 +17,21 @@ const useFocusToMarker = (map: TMap | null, markers: Marker[]) => { if (markersLength === 0) { setMarkersLength(markers.length); - map.fitBounds(bounds.current); + + // mapInstance.setCenter(bounds.current.getCenter()); + + // mapInstance.fitBounds(bounds.current, { + // left: 100, // 지도의 왼쪽과의 간격(단위 : px) + // top: 100, // 지도의 위쪽과의 간격(단위 : px) + // right: 100, // 지도의 오른쪽과의 간격(단위 : px) + // bottom: 20, // 지도의 아래쪽과의 간격(단위 : px) + // }); return; } - if (markersLength !== markers.length) map.fitBounds(bounds.current); + if (markersLength !== markers.length) { + // mapInstance.fitBounds(bounds.current); + } } return () => { setMarkersLength(0); diff --git a/frontend/src/hooks/useMapClick.ts b/frontend/src/hooks/useMapClick.ts index 043c5ab86..2e9294a9d 100644 --- a/frontend/src/hooks/useMapClick.ts +++ b/frontend/src/hooks/useMapClick.ts @@ -2,11 +2,13 @@ import { useContext, useEffect } from 'react'; import { CoordinatesContext } from '../context/CoordinatesContext'; import getAddressFromServer from '../lib/getAddressFromServer'; +import useMapStore from '../store/mapInstance'; import useToast from './useToast'; -export default function useMapClick(map: TMap | null) { +export default function useMapClick() { const { setClickedCoordinate } = useContext(CoordinatesContext); const { showToast } = useToast(); + const { mapInstance } = useMapStore((state) => state); const clickHandler = async (evt: evt) => { try { @@ -26,14 +28,14 @@ export default function useMapClick(map: TMap | null) { }; useEffect(() => { - if (!map) return; + if (!mapInstance) return; - map.on('Click', clickHandler); + mapInstance.on('Click', clickHandler); return () => { - if (map) { - map.removeListener('click', clickHandler); + if (mapInstance) { + mapInstance.off('Click', clickHandler); } }; - }, [map]); + }, [mapInstance]); } diff --git a/frontend/src/hooks/useUpdateCoordinates.ts b/frontend/src/hooks/useUpdateCoordinates.ts index 7a5422a45..f455fc369 100644 --- a/frontend/src/hooks/useUpdateCoordinates.ts +++ b/frontend/src/hooks/useUpdateCoordinates.ts @@ -3,7 +3,7 @@ import { useContext, useEffect } from 'react'; import { CoordinatesContext } from '../context/CoordinatesContext'; import { MarkerContext } from '../context/MarkerContext'; -export default function useUpdateCoordinates(map: TMap | null) { +export default function useUpdateCoordinates() { const { coordinates } = useContext(CoordinatesContext); const { markers, @@ -13,15 +13,17 @@ export default function useUpdateCoordinates(map: TMap | null) { removeInfowindows, } = useContext(MarkerContext); + const removePins = (markers: Marker[]) => { + removeMarkers(); + removeInfowindows(); + }; + useEffect(() => { - if (!map) return; - if (markers && markers.length > 0) { - removeMarkers(); - removeInfowindows(); - } + removePins(markers); + if (coordinates.length > 0) { - createMarkers(map); - createInfowindows(map); + createMarkers(); + createInfowindows(); } }, [coordinates]); } diff --git a/frontend/src/pages/SelectedTopic.tsx b/frontend/src/pages/SelectedTopic.tsx index 45e63dd8c..28801531a 100644 --- a/frontend/src/pages/SelectedTopic.tsx +++ b/frontend/src/pages/SelectedTopic.tsx @@ -1,4 +1,4 @@ -import { lazy, Suspense, useContext, useEffect, useState } from 'react'; +import { lazy, Suspense, useContext, useEffect, useRef, useState } from 'react'; import { useParams, useSearchParams } from 'react-router-dom'; import { styled } from 'styled-components'; @@ -6,27 +6,33 @@ import { getApi } from '../apis/getApi'; import Space from '../components/common/Space'; import PullPin from '../components/PullPin'; import PinsOfTopicSkeleton from '../components/Skeletons/PinsOfTopicSkeleton'; -import { LAYOUT_PADDING, SIDEBAR } from '../constants'; +import { LAYOUT_PADDING, PIN_SIZE, SIDEBAR } from '../constants'; +import { 붕어빵지도 } from '../constants/cluster'; import { CoordinatesContext } from '../context/CoordinatesContext'; import useResizeMap from '../hooks/useResizeMap'; import useSetLayoutWidth from '../hooks/useSetLayoutWidth'; import useSetNavbarHighlight from '../hooks/useSetNavbarHighlight'; import useTags from '../hooks/useTags'; -import { PinProps } from '../types/Pin'; +import useMapStore from '../store/mapInstance'; import { TopicDetailProps } from '../types/Topic'; import PinDetail from './PinDetail'; const PinsOfTopic = lazy(() => import('../components/PinsOfTopic')); function SelectedTopic() { + const { Tmapv3 } = window; const { topicId } = useParams(); const [searchParams, _] = useSearchParams(); const [topicDetail, setTopicDetail] = useState(null); const [selectedPinId, setSelectedPinId] = useState(null); const [isOpen, setIsOpen] = useState(true); const [isEditPinDetail, setIsEditPinDetail] = useState(false); - const { setCoordinates } = useContext(CoordinatesContext); + const { coordinates, setCoordinates } = useContext(CoordinatesContext); const { width } = useSetLayoutWidth(SIDEBAR); + const zoomTimerIdRef = useRef(null); + const dragTimerIdRef = useRef(null); + const { mapInstance } = useMapStore((state) => state); + const { tags, setTags, onClickInitTags, onClickCreateTopicWithTags } = useTags(); useSetNavbarHighlight('none'); @@ -39,29 +45,93 @@ function SelectedTopic() { const topic = topicInArray[0]; setTopicDetail(topic); - setCoordinatesTopicDetail(topic); }; - const setCoordinatesTopicDetail = (topic: TopicDetailProps) => { + const getDistanceOfPin = () => { + if (!mapInstance) return; + + const mapBounds = mapInstance.getBounds(); + + const leftWidth = new Tmapv3.LatLng(mapBounds._ne._lat, mapBounds._sw._lng); + const rightWidth = new Tmapv3.LatLng( + mapBounds._ne._lat, + mapBounds._ne._lng, + ); + + const realDistanceOfScreen = leftWidth.distanceTo(rightWidth); + const currentScreenSize = + mapInstance.realToScreen(rightWidth).x - + mapInstance.realToScreen(leftWidth).x; + + return (realDistanceOfScreen / currentScreenSize) * PIN_SIZE; + }; + + const setClusteredCoordinates = async () => { + if (!topicDetail) return; + const newCoordinates: any = []; + const distanceOfPinSize = getDistanceOfPin(); + + const diameterPins = await getApi( + `/topics/clusters?ids=${topicId}&image-diameter=${distanceOfPinSize}`, + ); - topic.pins.forEach((pin: PinProps) => { + diameterPins.forEach((clusterOrPin: any, idx: number) => { newCoordinates.push({ - id: pin.id, topicId, - pinName: pin.name, - latitude: pin.latitude, - longitude: pin.longitude, + id: clusterOrPin.pins[0].id || `cluster ${idx}`, + pinName: clusterOrPin.pins[0].name, + latitude: clusterOrPin.latitude, + longitude: clusterOrPin.longitude, + pins: clusterOrPin.pins, }); }); setCoordinates(newCoordinates); }; - const togglePinDetail = () => { - setIsOpen(!isOpen); + const setPrevCoordinates = () => { + setCoordinates((prev) => [...prev]); }; + useEffect(() => { + getAndSetDataFromServer(); + setTags([]); + }, []); + + useEffect(() => { + setClusteredCoordinates(); + + const onDragEnd = (evt: evt) => { + if (dragTimerIdRef.current) { + clearTimeout(dragTimerIdRef.current); + } + + dragTimerIdRef.current = setTimeout(() => { + setPrevCoordinates(); + }, 100); + }; + const onZoomEnd = (evt: evt) => { + if (zoomTimerIdRef.current) { + clearTimeout(zoomTimerIdRef.current); + } + + zoomTimerIdRef.current = setTimeout(() => { + setClusteredCoordinates(); + }, 100); + }; + + if (!mapInstance) return; + + mapInstance.on('DragEnd', onDragEnd); + mapInstance.on('ZoomEnd', onZoomEnd); + + return () => { + mapInstance.off('DragEnd', onDragEnd); + mapInstance.off('ZoomEnd', onZoomEnd); + }; + }, [topicDetail]); + useEffect(() => { const queryParams = new URLSearchParams(location.search); @@ -74,11 +144,6 @@ function SelectedTopic() { setSelectedPinId(null); }, [searchParams]); - useEffect(() => { - getAndSetDataFromServer(); - setTags([]); - }, []); - if (!topicId || !topicDetail) return <>; return ( @@ -109,7 +174,12 @@ function SelectedTopic() { {selectedPinId && ( <> - + { + setIsOpen(!isOpen); + }} + > ◀ diff --git a/frontend/src/store/mapInstance.ts b/frontend/src/store/mapInstance.ts index 27b8f5544..eed70a409 100644 --- a/frontend/src/store/mapInstance.ts +++ b/frontend/src/store/mapInstance.ts @@ -1,11 +1,11 @@ import { create } from 'zustand'; -interface MapState { +interface MapContext { mapInstance: TMap | null; setMapInstance: (instance: TMap) => void; } -const useMapStore = create((set) => ({ +const useMapStore = create((set) => ({ mapInstance: null, setMapInstance: (instance: TMap) => set(() => ({ mapInstance: instance })), })); diff --git a/frontend/src/types/tmap.d.ts b/frontend/src/types/tmap.d.ts index 4fc3b0ee0..dad4436b0 100644 --- a/frontend/src/types/tmap.d.ts +++ b/frontend/src/types/tmap.d.ts @@ -1,7 +1,14 @@ -interface LatLng {} +interface LatLng { + _lat: number; + _lng: number; + distanceTo(latLng: LatLng): number; +} interface LatLngBounds { extend(latLng: LatLng): void; + getCenter(): LatLng; + _ne: LatLng; + _sw: LatLng; } interface evt { @@ -24,6 +31,9 @@ interface TMap { on(eventType: string, callback: (event: evt) => void): void; removeListener(eventType: string, callback: (event: evt) => void): void; resize(width: number, height: number): void; + getBounds(): LatLngBounds; + realToScreen(latLng: LatLng): Point; + off(eventType: string, callback: (event: evt) => void): void; } interface Marker { @@ -58,7 +68,12 @@ interface Window { Tmapv3: { Map: new ( element: HTMLElement, - options?: { center?: LatLng; scaleBar: boolean }, + options?: { + center?: LatLng; + scaleBar: boolean; + width: string | number; + height: string | number; + }, ) => TMap; LatLng: new (lat: number, lng: number) => LatLng; LatLngBounds: new () => LatLngBounds;