From 206fd06b58f38e10849ea1f53f272e211b8e5792 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9D=B4=EC=84=B8=EB=AF=BC?= <89172499+semnil5202@users.noreply.github.com> Date: Tue, 7 Nov 2023 21:03:11 +0900 Subject: [PATCH 1/3] =?UTF-8?q?[FE]=20Refactor/#613=20=EC=A7=80=EB=8F=84?= =?UTF-8?q?=20=ED=81=B4=EB=9F=AC=EC=8A=A4=ED=84=B0=EB=A7=81=20=EA=B8=B0?= =?UTF-8?q?=EB=8A=A5=20=EC=B6=94=EA=B0=80=20=EB=B0=8F=20=EC=A7=80=EB=8F=84?= =?UTF-8?q?=20=EC=83=81=ED=83=9C=20=EB=A6=AC=ED=8C=A9=ED=86=A0=EB=A7=81=20?= =?UTF-8?q?(#615)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix: Flex 컴포넌트를 상속받는 로컬 스타일드 컴포넌트 타입 오류 해결 Flex를 상속받은 로컬 스타일드 컴포넌트에 특정 제네릭 prop을 추가하면 Box 컴포넌트까지 타입 검사가 내려간다. 이로 인해 Box 컴포넌트에 Flex 컴포넌트 인터페이스를 찾는 타입 오류가 생겨 이 문제를 Flex와 Box간 상속관계를 끊음으로 해결한다. * refactor: map 인스턴스 전역상태 분리로 각종 hook 리팩토링 * refactor: 지도 타입 추가 * chore: eslint 설정 변경 * refactor: map type 추가 * refactor: mapInstance interface 명 수정 * refactor: 핀 svg 사이즈 일괄 60px로 지정 * refactor: 클러스터 적용으로 인한 map 인터렉션 로직 수정 * fix: drag 디바운스 적용 및 pin 정보 가져오는 로직 오류 수정 * refactor: pinSize 상수화 및 일부 파일 개행 수정 * fix: off 함수 타입 추가 및 클러스터링 중복 요청 오류 해결 * refactor: screen size 에 해당하는 marker만 그리는 로직 이동 * refactor: event 타입 변경 및 sidebar 전역 상태 제거 zoom 일 경우에만 실제 서버에 클러스터 핀 갱신 요청을 보내고 dragEnd 상태에서는 보내지 않도록 한다. sidebarMarkers 는 잠시 사용을 보류한다. * refactor: map 및 markers 의존성 제거 * design: 클러스터된 핀 디자인 변경 * fix: map 드래그 경우 클로저 문제로 인한 핀 갱신 오류 수정 * refactor: 최고 줌인 상태에서 클러스터 핀 상태일 때 infoWindow에 모두 표시하도록 변경 * refactor: 불필요한 import 제거 * refactor: mock 데이터에서 실제 서버 요청으로 변경 * refactor: UI 변경사항 대응 --- frontend/.eslintrc.js | 2 +- frontend/src/components/Map/index.tsx | 20 ++-- frontend/src/components/common/Flex/index.ts | 67 +++++++++++- frontend/src/constants/index.ts | 2 + frontend/src/constants/pinImage.ts | 51 +++++++-- frontend/src/context/MarkerContext.tsx | 91 ++++++++++++---- frontend/src/hooks/useAnimateClickedPin.ts | 52 +++++++-- frontend/src/hooks/useClickedCoordinate.ts | 16 ++- frontend/src/hooks/useFocusToMarkers.ts | 24 +++-- frontend/src/hooks/useMapClick.ts | 14 +-- frontend/src/hooks/useUpdateCoordinates.ts | 18 ++-- frontend/src/pages/SelectedTopic.tsx | 108 +++++++++++++++---- frontend/src/store/mapInstance.ts | 4 +- frontend/src/types/tmap.d.ts | 19 +++- 14 files changed, 380 insertions(+), 108 deletions(-) diff --git a/frontend/.eslintrc.js b/frontend/.eslintrc.js index 9605c722..28b667cd 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 e0471966..aa347ac3 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 713a0287..92cdc63c 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 ca04016c..e99ff3a6 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 3473a659..25509c78 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 2410c0e3..ec46069c 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 1002af84..b12dd059 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 f206b889..a7f352a9 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 80b2cd31..5162de01 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 043c5ab8..2e9294a9 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 7a5422a4..f455fc36 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 45e63dd8..28801531 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 27b8f554..eed70a40 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 4fc3b0ee..dad4436b 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; From b05ba6273d03ee242f7f3c4a7d6094c35376ca3a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9D=B4=EC=84=B8=EB=AF=BC?= <89172499+semnil5202@users.noreply.github.com> Date: Fri, 17 Nov 2023 10:17:41 +0900 Subject: [PATCH 2/3] =?UTF-8?q?[FE]=20Refactor/#620=20=EB=AA=A8=EC=95=84?= =?UTF-8?q?=EB=B3=B4=EA=B8=B0=20=ED=81=B4=EB=9F=AC=EC=8A=A4=ED=84=B0?= =?UTF-8?q?=EB=A7=81=20(#622)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * refactor: 불필요한 인자 제거 * refactor: getDistanceOfPin 훅으로 분리 * refactor: InfoWindow 타입 추가 * refactor: 모아보기 상태에서 클러스터링 및 이벤트 핸들링 적용 * refactor: 사용자에게 노출되는 단어 중 도메인 용어인 토픽을 지도로 변환 * refactor: 혼잡한 dom 구조 및 잘못된 indent 수정 * refactor: 한 토픽에 해당하는 핀이 클러스터링 되었을 땐 무지개색 적용 하지 않기 * refactor: 스크린 사이즈 내 좌표가 아닌 초기 좌표로 모아보기 색 구별하도록 변경 * refactor: 모아보기가 비어있을 때 초기화 하는 로직 함수 분리 * refactor: type 정의 위치 수정 --- frontend/src/components/PinPreview/index.tsx | 2 +- frontend/src/components/PullPin/index.tsx | 6 +- frontend/src/components/TopicCard/index.tsx | 2 +- frontend/src/components/TopicInfo/index.tsx | 4 +- frontend/src/constants/pinImage.ts | 36 ++++-- frontend/src/context/MarkerContext.tsx | 70 ++++++----- frontend/src/hooks/useClickedCoordinate.ts | 2 +- frontend/src/hooks/useRealDistanceOfPin.ts | 26 ++++ frontend/src/pages/NewPin.tsx | 2 +- frontend/src/pages/PinDetail.tsx | 2 +- frontend/src/pages/SeeTogether.tsx | 118 +++++++++++++++---- frontend/src/pages/SelectedTopic.tsx | 26 +--- frontend/src/types/tmap.d.ts | 1 + 13 files changed, 202 insertions(+), 95 deletions(-) create mode 100644 frontend/src/hooks/useRealDistanceOfPin.ts diff --git a/frontend/src/components/PinPreview/index.tsx b/frontend/src/components/PinPreview/index.tsx index 0ec637f3..8b02e467 100644 --- a/frontend/src/components/PinPreview/index.tsx +++ b/frontend/src/components/PinPreview/index.tsx @@ -36,7 +36,7 @@ function PinPreview({ const { pathname } = useLocation(); const { routePage } = useNavigator(); const { tags, setTags } = useContext(TagContext); - const [announceText, setAnnounceText] = useState('토픽 핀 선택'); + const [announceText, setAnnounceText] = useState('지도 핀 선택'); const inputRef = useRef(null); const onAddTagOfTopic = (e: React.ChangeEvent) => { diff --git a/frontend/src/components/PullPin/index.tsx b/frontend/src/components/PullPin/index.tsx index 5330e94b..7ac46050 100644 --- a/frontend/src/components/PullPin/index.tsx +++ b/frontend/src/components/PullPin/index.tsx @@ -46,7 +46,7 @@ function PullPin({ tabIndex={1} aria-label={ confirmButton === '같이보기' - ? `선택된 ${tag.title} 토픽 태그` + ? `선택된 ${tag.title} 지도 태그` : `선택된 ${tag.title} 핀 태그` } > @@ -64,7 +64,7 @@ function PullPin({ onClick={onClickClose} aria-label={ confirmButton === '같이보기' - ? '선택된 토픽들 같이보기 취소하기' + ? '선택된 지도들 같이보기 취소하기' : '선택된 핀들 뽑아오기 취소하기' } > @@ -76,7 +76,7 @@ function PullPin({ onClick={onClickConfirm} aria-label={ confirmButton === '같이보기' - ? '선택된 토픽들 같이보기' + ? '선택된 지도들 같이보기' : '선택된 핀들 뽑아오기' } > diff --git a/frontend/src/components/TopicCard/index.tsx b/frontend/src/components/TopicCard/index.tsx index 3e8f6d3b..41fc3503 100644 --- a/frontend/src/components/TopicCard/index.tsx +++ b/frontend/src/components/TopicCard/index.tsx @@ -101,7 +101,7 @@ function TopicCard({ color="black" $fontSize="default" $fontWeight="bold" - aria-label={`토픽 이름 ${name}`} + aria-label={`지도 이름 ${name}`} > {name} diff --git a/frontend/src/components/TopicInfo/index.tsx b/frontend/src/components/TopicInfo/index.tsx index 4635db81..58be537a 100644 --- a/frontend/src/components/TopicInfo/index.tsx +++ b/frontend/src/components/TopicInfo/index.tsx @@ -60,9 +60,9 @@ function TopicInfo({ try { const topicUrl = window.location.href.split('?')[0]; await navigator.clipboard.writeText(topicUrl); - showToast('info', '토픽 링크가 복사되었습니다.'); + showToast('info', '지도 링크가 복사되었습니다.'); } catch (err) { - showToast('error', '토픽 링크를 복사하는데 실패했습니다.'); + showToast('error', '지도 링크를 복사하는데 실패했습니다.'); } }; diff --git a/frontend/src/constants/pinImage.ts b/frontend/src/constants/pinImage.ts index 25509c78..aed85fbe 100644 --- a/frontend/src/constants/pinImage.ts +++ b/frontend/src/constants/pinImage.ts @@ -9,6 +9,25 @@ export const USER_LOCATION_IMAGE = ` + + + + + + + + + + + + + + + + + `, 1: ` @@ -75,6 +94,7 @@ export const pinImageMap: PinImageMap = { }; export const pinColors: PinImageMap = { + 0: '#454545', 1: '#E1325C', 2: '#F9CB55', 3: '#4B5CFA', @@ -103,20 +123,22 @@ ${ ( pin: any, ) => `
- ${pin.name} -
`, + ${pin.name} + `, ) .join('') : `
- ${pinName} -
+ ${pinName} + ${ pins.length > 1 ? ` -
+${pins.length}
+
+ +${pins.length} +
` : '' } - ` + ` } -`; +`; diff --git a/frontend/src/context/MarkerContext.tsx b/frontend/src/context/MarkerContext.tsx index ec46069c..90260356 100644 --- a/frontend/src/context/MarkerContext.tsx +++ b/frontend/src/context/MarkerContext.tsx @@ -20,6 +20,8 @@ type MarkerContextType = { displayClickedMarker: () => void; }; +type ElementType = 'marker' | 'infoWindow'; + const defaultMarkerContext = () => { throw new Error('MarkerContext가 제공되지 않았습니다.'); }; @@ -49,14 +51,36 @@ function MarkerProvider({ children }: Props): JSX.Element { const { routePage } = useNavigator(); const { pathname } = useLocation(); - const createElementsInScreenSize = () => { + const createElementsColor = (elementType: ElementType = 'marker') => { + let markerType = -1; + let currentTopicId = '-1'; + + const colorMap = elementType === 'marker' ? pinImageMap : pinColors; + + const addedMarkerTypeCoordinates = coordinates.map((coordinate) => { + if (coordinate.topicId === 'clustered') { + markerType = -1; + } else if (coordinate.topicId && currentTopicId !== coordinate.topicId) { + markerType += 1; + currentTopicId = coordinate.topicId; + } + + return { ...coordinate, elementColor: colorMap[markerType + 1] }; + }); + + return addedMarkerTypeCoordinates; + }; + + const createElementsInScreenSize = (elementType: ElementType) => { if (!mapInstance) return; const mapBounds = mapInstance.getBounds(); const northEast = mapBounds._ne; const southWest = mapBounds._sw; - const coordinatesInScreenSize = coordinates.filter( + const addedMarkerTypeCoordinates = createElementsColor(elementType); + + const coordinatesInScreenSize = addedMarkerTypeCoordinates.filter( (coordinate: any) => coordinate.latitude <= northEast._lat && coordinate.latitude >= southWest._lat && @@ -67,13 +91,6 @@ function MarkerProvider({ children }: Props): JSX.Element { return coordinatesInScreenSize; }; - const createMarker = (coordinate: Coordinate, markerType: number) => - new Tmapv3.Marker({ - position: new Tmapv3.LatLng(coordinate.latitude, coordinate.longitude), - iconHTML: pinImageMap[markerType + 1], - map: mapInstance, - }); - // 현재 클릭된 좌표의 마커 생성 const displayClickedMarker = () => { if (clickedMarker) { @@ -93,19 +110,16 @@ function MarkerProvider({ children }: Props): JSX.Element { // coordinates를 받아서 marker를 생성하고, marker를 markers 배열에 추가 const createMarkers = () => { - let markerType = -1; - let currentTopicId = '-1'; - - const markersInScreenSize = createElementsInScreenSize(); + const coordinatesInScreenSize = createElementsInScreenSize('marker'); - if (!markersInScreenSize) return; + if (!coordinatesInScreenSize) return; - const newMarkers = markersInScreenSize.map((coordinate: any) => { - if (currentTopicId !== coordinate.topicId) { - markerType = (markerType + 1) % 7; - currentTopicId = coordinate.topicId; - } - const marker = createMarker(coordinate, markerType); + const newMarkers = coordinatesInScreenSize.map((coordinate: any) => { + const marker = new Tmapv3.Marker({ + position: new Tmapv3.LatLng(coordinate.latitude, coordinate.longitude), + iconHTML: coordinate.elementColor, + map: mapInstance, + }); marker.id = String(coordinate.id); return marker; }); @@ -135,25 +149,17 @@ function MarkerProvider({ children }: Props): JSX.Element { }; const createInfowindows = () => { - let markerType = -1; - let currentTopicId = '-1'; - - const windowsInScreenSize = createElementsInScreenSize(); + const coordinatesInScreenSize = createElementsInScreenSize('infoWindow'); - if (!windowsInScreenSize) return; - - const newInfowindows = windowsInScreenSize.map((coordinate: any) => { - if (currentTopicId !== coordinate.topicId) { - markerType = (markerType + 1) % 7; - currentTopicId = coordinate.topicId; - } + if (!coordinatesInScreenSize) return; + const newInfowindows = coordinatesInScreenSize.map((coordinate: any) => { const infoWindow = new Tmapv3.InfoWindow({ position: new Tmapv3.LatLng(coordinate.latitude, coordinate.longitude), border: 0, background: 'transparent', content: getInfoWindowTemplate({ - backgroundColor: pinColors[markerType + 1], + backgroundColor: coordinate.elementColor, pinName: coordinate.pinName, pins: coordinate.pins, condition: getCondition(coordinate.pins), diff --git a/frontend/src/hooks/useClickedCoordinate.ts b/frontend/src/hooks/useClickedCoordinate.ts index a7f352a9..f0d11fcc 100644 --- a/frontend/src/hooks/useClickedCoordinate.ts +++ b/frontend/src/hooks/useClickedCoordinate.ts @@ -13,7 +13,7 @@ export default function useClickedCoordinate() { useEffect(() => { if (!mapInstance) return; const currentZoom = mapInstance.getZoom(); - if (clickedCoordinate.address) displayClickedMarker(mapInstance); + if (clickedCoordinate.address) displayClickedMarker(); // 선택된 좌표가 있으면 해당 좌표로 지도의 중심을 이동 if (clickedCoordinate.latitude && clickedCoordinate.longitude) { diff --git a/frontend/src/hooks/useRealDistanceOfPin.ts b/frontend/src/hooks/useRealDistanceOfPin.ts new file mode 100644 index 00000000..0f577f35 --- /dev/null +++ b/frontend/src/hooks/useRealDistanceOfPin.ts @@ -0,0 +1,26 @@ +import { PIN_SIZE } from '../constants'; + +const useRealDistanceOfPin = () => { + const { Tmapv3 } = window; + + const getDistanceOfPin = (mapInstance: TMap) => { + 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; + }; + + return { getDistanceOfPin }; +}; + +export default useRealDistanceOfPin; diff --git a/frontend/src/pages/NewPin.tsx b/frontend/src/pages/NewPin.tsx index d1e30358..18a7110a 100644 --- a/frontend/src/pages/NewPin.tsx +++ b/frontend/src/pages/NewPin.tsx @@ -356,7 +356,7 @@ function NewPin() { - 내 토픽 리스트 + 내 지도 리스트 핀을 저장할 지도를 선택해주세요. diff --git a/frontend/src/pages/PinDetail.tsx b/frontend/src/pages/PinDetail.tsx index 0e909fa3..3ee8866a 100644 --- a/frontend/src/pages/PinDetail.tsx +++ b/frontend/src/pages/PinDetail.tsx @@ -303,7 +303,7 @@ function PinDetail({ - 내 토픽 리스트 + 내 지도 리스트 핀을 저장할 지도를 선택해주세요. diff --git a/frontend/src/pages/SeeTogether.tsx b/frontend/src/pages/SeeTogether.tsx index c13cef55..9102ef4e 100644 --- a/frontend/src/pages/SeeTogether.tsx +++ b/frontend/src/pages/SeeTogether.tsx @@ -1,4 +1,11 @@ -import { Fragment, Suspense, useContext, useEffect, useState } from 'react'; +import { + Fragment, + Suspense, + useContext, + useEffect, + useRef, + useState, +} from 'react'; import { useLocation, useParams, useSearchParams } from 'react-router-dom'; import { styled } from 'styled-components'; @@ -16,11 +23,12 @@ import { CoordinatesContext } from '../context/CoordinatesContext'; import { MarkerContext } from '../context/MarkerContext'; import { SeeTogetherContext } from '../context/SeeTogetherContext'; import useNavigator from '../hooks/useNavigator'; +import useRealDistanceOfPin from '../hooks/useRealDistanceOfPin'; 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'; @@ -29,7 +37,7 @@ function SeeTogether() { const { topicId } = useParams(); const { routePage } = useNavigator(); - const [searchParams, _] = useSearchParams(); + const [searchParams] = useSearchParams(); const location = useLocation(); const [isOpen, setIsOpen] = useState(true); @@ -39,6 +47,11 @@ function SeeTogether() { null, ); + const zoomTimerIdRef = useRef(null); + const dragTimerIdRef = useRef(null); + + const { mapInstance } = useMapStore((state) => state); + const { getDistanceOfPin } = useRealDistanceOfPin(); const { tags, setTags, onClickInitTags, onClickCreateTopicWithTags } = useTags(); const { setCoordinates } = useContext(CoordinatesContext); @@ -64,34 +77,97 @@ function SeeTogether() { `/topics/ids?ids=${requestTopicIds}`, ); - setTopicDetails([...topics]); - setCoordinatesTopicDetailWithHashMap(topics); + setTopicDetails(topics); }; - const setCoordinatesTopicDetailWithHashMap = (topics: TopicDetailProps[]) => { - if (topicId === '-1' || !topicId) return; + const setClusteredCoordinates = async () => { + if (topicId === '-1' || !topicId || !topicDetails || !mapInstance) return; const newCoordinates: any = []; + const distanceOfPinSize = getDistanceOfPin(mapInstance); + + const diameterPins = await getApi( + `/topics/clusters?ids=${topicId}&image-diameter=${distanceOfPinSize}`, + ); - topics.forEach((topic: TopicDetailProps) => { - topic.pins.forEach((pin: PinProps) => { - newCoordinates.push({ - id: pin.id, - topicId: topic.id, - pinName: pin.name, - latitude: pin.latitude, - longitude: pin.longitude, - }); + diameterPins.forEach((clusterOrPin: any, idx: number) => { + newCoordinates.push({ + topicId: + clusterOrPin.pins.length > 1 && + clusterOrPin.pins.filter( + (pin: any) => pin.topicId !== clusterOrPin.pins[0].topicId, + ).length !== 0 + ? 'clustered' + : clusterOrPin.pins[0].topicId, + id: clusterOrPin.pins[0].id || `cluster ${idx}`, + pinName: clusterOrPin.pins[0].name, + latitude: clusterOrPin.latitude, + longitude: clusterOrPin.longitude, + pins: clusterOrPin.pins, }); }); - setCoordinates(newCoordinates); + const sortedNewCoordinates = newCoordinates.sort((a: any, b: any) => { + if (a.topicId === 'clustered') { + return -1; + } + + return a.topicId - b.topicId; + }); + + setCoordinates(sortedNewCoordinates); + }; + + const setPrevCoordinates = () => { + setCoordinates((prev) => [...prev]); }; const togglePinDetail = () => { setIsOpen(!isOpen); }; + const initMarkerWhenSeeTogetherIsEmpty = () => { + setCoordinates([]); + + if (markers && markers.length > 0) { + removeMarkers(); + removeInfowindows(); + } + }; + + 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); + }; + }, [topicDetails]); + useEffect(() => { const queryParams = new URLSearchParams(location.search); @@ -106,15 +182,9 @@ function SeeTogether() { useEffect(() => { getAndSetDataFromServer(); - }, [topicId]); - - useEffect(() => { setTags([]); - if (markers && markers.length > 0) { - removeMarkers(); - removeInfowindows(); - } + initMarkerWhenSeeTogetherIsEmpty(); }, []); if (!seeTogetherTopics || !topicId) return <>; diff --git a/frontend/src/pages/SelectedTopic.tsx b/frontend/src/pages/SelectedTopic.tsx index 28801531..8c92cd84 100644 --- a/frontend/src/pages/SelectedTopic.tsx +++ b/frontend/src/pages/SelectedTopic.tsx @@ -7,8 +7,8 @@ import Space from '../components/common/Space'; import PullPin from '../components/PullPin'; import PinsOfTopicSkeleton from '../components/Skeletons/PinsOfTopicSkeleton'; import { LAYOUT_PADDING, PIN_SIZE, SIDEBAR } from '../constants'; -import { 붕어빵지도 } from '../constants/cluster'; import { CoordinatesContext } from '../context/CoordinatesContext'; +import useRealDistanceOfPin from '../hooks/useRealDistanceOfPin'; import useResizeMap from '../hooks/useResizeMap'; import useSetLayoutWidth from '../hooks/useSetLayoutWidth'; import useSetNavbarHighlight from '../hooks/useSetNavbarHighlight'; @@ -32,6 +32,7 @@ function SelectedTopic() { const zoomTimerIdRef = useRef(null); const dragTimerIdRef = useRef(null); const { mapInstance } = useMapStore((state) => state); + const { getDistanceOfPin } = useRealDistanceOfPin(); const { tags, setTags, onClickInitTags, onClickCreateTopicWithTags } = useTags(); @@ -47,30 +48,11 @@ function SelectedTopic() { setTopicDetail(topic); }; - 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; + if (!topicDetail || !mapInstance) return; const newCoordinates: any = []; - const distanceOfPinSize = getDistanceOfPin(); + const distanceOfPinSize = getDistanceOfPin(mapInstance); const diameterPins = await getApi( `/topics/clusters?ids=${topicId}&image-diameter=${distanceOfPinSize}`, diff --git a/frontend/src/types/tmap.d.ts b/frontend/src/types/tmap.d.ts index dad4436b..fee0861d 100644 --- a/frontend/src/types/tmap.d.ts +++ b/frontend/src/types/tmap.d.ts @@ -59,6 +59,7 @@ interface InfoWindow { map?: Map; setMap(mapOrNull?: Map | null): void; setPosition(positionOrLatLng?: Position | LatLng): void; + getPosition(): LatLng; setContent(contentOrString?: Content | string): void; open(map?: Map, marker?: Marker, latlng?: LatLng): void; close(): void; From e47f05264e7bd23ac9f7237ed22a75756718e3e1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9D=B4=EC=84=B8=EB=AF=BC?= <89172499+semnil5202@users.noreply.github.com> Date: Fri, 17 Nov 2023 15:26:19 +0900 Subject: [PATCH 3/3] =?UTF-8?q?refactor:=20=EC=82=AC=EC=9A=A9=EC=9E=90?= =?UTF-8?q?=EA=B0=80=20=EC=A7=80=EB=8F=84=EB=A5=BC=20=ED=9A=8C=EC=A0=84=20?= =?UTF-8?q?=EB=B0=8F=20=EA=B8=B0=EC=9A=B8=EC=9D=B8=20=EB=92=A4=20=EC=A7=80?= =?UTF-8?q?=EB=8F=84=20=EC=9D=B4=EB=8F=99=EC=8B=9C=20=EC=9B=90=EB=9E=98?= =?UTF-8?q?=EB=8C=80=EB=A1=9C=20=EC=A1=B0=EC=A0=95=20(#623)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 모바일에서 줌인 줌아웃시 지도가 돌아가는 불편함이 있었으며 클러스터링 안정화를 위해 위와 같이 작업한다. --- frontend/src/pages/SeeTogether.tsx | 9 +++++++++ frontend/src/pages/SelectedTopic.tsx | 9 +++++++++ frontend/src/types/tmap.d.ts | 2 ++ 3 files changed, 20 insertions(+) diff --git a/frontend/src/pages/SeeTogether.tsx b/frontend/src/pages/SeeTogether.tsx index 9102ef4e..daa27336 100644 --- a/frontend/src/pages/SeeTogether.tsx +++ b/frontend/src/pages/SeeTogether.tsx @@ -135,6 +135,13 @@ function SeeTogether() { } }; + const adjustMapDirection = () => { + if (!mapInstance) return; + + mapInstance.setBearing(0); + mapInstance.setPitch(0); + }; + useEffect(() => { setClusteredCoordinates(); @@ -145,6 +152,7 @@ function SeeTogether() { dragTimerIdRef.current = setTimeout(() => { setPrevCoordinates(); + adjustMapDirection(); }, 100); }; const onZoomEnd = (evt: evt) => { @@ -154,6 +162,7 @@ function SeeTogether() { zoomTimerIdRef.current = setTimeout(() => { setClusteredCoordinates(); + adjustMapDirection(); }, 100); }; diff --git a/frontend/src/pages/SelectedTopic.tsx b/frontend/src/pages/SelectedTopic.tsx index 8c92cd84..4f19a6ef 100644 --- a/frontend/src/pages/SelectedTopic.tsx +++ b/frontend/src/pages/SelectedTopic.tsx @@ -76,6 +76,13 @@ function SelectedTopic() { setCoordinates((prev) => [...prev]); }; + const adjustMapDirection = () => { + if (!mapInstance) return; + + mapInstance.setBearing(0); + mapInstance.setPitch(0); + }; + useEffect(() => { getAndSetDataFromServer(); setTags([]); @@ -91,6 +98,7 @@ function SelectedTopic() { dragTimerIdRef.current = setTimeout(() => { setPrevCoordinates(); + adjustMapDirection(); }, 100); }; const onZoomEnd = (evt: evt) => { @@ -100,6 +108,7 @@ function SelectedTopic() { zoomTimerIdRef.current = setTimeout(() => { setClusteredCoordinates(); + adjustMapDirection(); }, 100); }; diff --git a/frontend/src/types/tmap.d.ts b/frontend/src/types/tmap.d.ts index fee0861d..a0b25c43 100644 --- a/frontend/src/types/tmap.d.ts +++ b/frontend/src/types/tmap.d.ts @@ -34,6 +34,8 @@ interface TMap { getBounds(): LatLngBounds; realToScreen(latLng: LatLng): Point; off(eventType: string, callback: (event: evt) => void): void; + setBearing(value: number): void; + setPitch(value: number): void; } interface Marker {