Skip to content

Commit

Permalink
feat: 특정 줌 레벨까지 마커의 크기를 줄여서 보여주는 기능을 구현한다 (#830)
Browse files Browse the repository at this point in the history
* chore: @googlemaps/markerclusterer 패키지 설치

[#157]

* feat: 마커 클러스터링 기능 추가

[#157]

* refactor: 불필요한 console.log 삭제

[#157]

* refactor: 클러스터링 제거하고 점 마커 도입

- 임시로 데모 만들어본 것입니다. 추후 제거할 가능성이 높습니다.

[#157]

* refactor: 작은 마커 디자인 추가

[#157]

* refactor: 줌 레벨 16에서의 마커를 점 형태에서 기본 마커 형식을 유지한 작은 마커로 수정

[#157]

* comment: 사용하지 않는 주석 제거

[#157]

* refactor: 줌 레벨 17 이상일 경우 CarffeineMarker가 렌더링 되도록 수정

- 아직 줌레벨을 실시간으로 트래킹해 17 이상인지 여부를 판별하고 있지 않으므로 재요청이 발생한 경우에만 마커가 다르게 찍히고 있습니다.

[#157]

* refactor: 불필요한 코드 삭제

[#157]

* refactor: 줌레벨, 마커 크기 비율 상수화

[#157]

* refactor: zoom 상태에 따른 부수적인 동작 제거

- 각 상태에 따라 렌더링 되는 컴포넌트에 역할 위임

[#157]

* refactor: 줌 레벨이 17 이상일 경우 카페인 마커가 렌더링 되도록 수정

- zoom state에 max 추가
- max가 추가됨에 따라 변경이 필요한 컴포넌트들 수정
- zoom state의 변화에 따라 카페인 마커가 렌더링 되는 기능 구현
- zoom state가 high, max인 경우에 HighZoomMarkerContainer 컴포넌트 마운트
- MaxZoomMarkerContainer를 만들려 했으나 HighZoomMarkerContainer와 공유하는 값들이 많아 일단 보류

[#157]

* refactor: 중복되는 로직 함수 분리

- zoom state에 따라 CarffeineMarker, DefaultMarker를 선택해 렌더링하는 로직이 두 군데에서 사용되고 있어 이 부분을 함수로 분리해주었습니다.

[#157]

* chore: 사용하지 않는 패키지 제거

[#157]

* refactor: 사용하지 않는 store 제거

[#157]

* refactor: 하드코딩 된 마커 색상 값 상수 활용하도록 수정

[#157]

* refactor: 마커 인스턴스 네이밍 변경

[#157]

* fix: 메서드명 변경에 따른 오류 수정

[#157]

* fix: 빌드 오류 수정

[#157]
  • Loading branch information
kyw0716 authored Oct 6, 2023
1 parent 58d8eb3 commit 2a35093
Show file tree
Hide file tree
Showing 15 changed files with 136 additions and 56 deletions.
22 changes: 1 addition & 21 deletions frontend/src/components/google-maps/map/CarFfeineListener.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,30 +2,23 @@ import { useEffect } from 'react';

import { useQueryClient } from '@tanstack/react-query';

import { useRenderStationMarker } from '@marker/HighZoomMarkerContainer/hooks/useRenderStationMarker';

import { debounce } from '@utils/debounce';
import { useExternalValue, useSetExternalState } from '@utils/external-state';
import { getDisplayPosition } from '@utils/google-maps';
import { isCachedRegion } from '@utils/google-maps/isCachedRegion';
import { setLocalStorage } from '@utils/storage';

import { getGoogleMapStore } from '@stores/google-maps/googleMapStore';
import { markerInstanceStore } from '@stores/google-maps/markerInstanceStore';
import { zoomActions, zoomStore } from '@stores/google-maps/zoomStore';
import { warningModalActions } from '@stores/layout/warningModalStore';
import { profileMenuOpenStore } from '@stores/profileMenuOpenStore';

import ZoomWarningModal from '@ui/WarningModal';

import { QUERY_KEY_STATION_MARKERS } from '@constants/queryKeys';
import { LOCAL_KEY_LAST_POSITION } from '@constants/storageKeys';

const CarFfeineMapListener = () => {
const googleMap = useExternalValue(getGoogleMapStore());
const queryClient = useQueryClient();
const setIsProfileMenuOpen = useSetExternalState(profileMenuOpenStore);
const { removeAllMarkers } = useRenderStationMarker();
const zoom = useExternalValue(zoomStore);

const debouncedHighZoomHandler = debounce(() => {
Expand All @@ -47,6 +40,7 @@ const CarFfeineMapListener = () => {
if (zoom.state === 'high') {
debouncedHighZoomHandler();
}

zoomActions.setZoom(googleMap.getZoom());
});

Expand All @@ -57,20 +51,6 @@ const CarFfeineMapListener = () => {
});
}, []);

/**
* zoom.state가 바뀌었을 때만 1번 실행된다.
*/
useEffect(() => {
removeAllMarkers(markerInstanceStore.getState());
queryClient.setQueryData([QUERY_KEY_STATION_MARKERS], () => []);

if (zoom.state === 'middle') {
warningModalActions.openModal(<ZoomWarningModal />);
} else {
warningModalActions.closeModal();
}
}, [zoom.state]);

return <></>;
};

Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,13 @@
import { useEffect } from 'react';

import { useStationMarkers } from '@marker/HighZoomMarkerContainer/hooks/useStationMarkers';

import { useExternalValue } from '@utils/external-state';

import type { StationMarkerInstance } from '@stores/google-maps/markerInstanceStore';
import { markerInstanceStore } from '@stores/google-maps/markerInstanceStore';
import { zoomStore } from '@stores/google-maps/zoomStore';
import type { ZoomBreakpoints } from '@stores/google-maps/zoomStore/types';

import { useRenderStationMarker } from './hooks/useRenderStationMarker';

Expand All @@ -10,10 +17,38 @@ const HighZoomMarkerContainer = () => {
createNewMarkerInstances,
getRemainedMarkerInstances,
removeMarkersOutsideBounds,
renderMarkerInstances,
removeAllMarkers,
renderDefaultMarkers,
renderCarffeineMarkers,
} = useRenderStationMarker();
const { state: zoomState } = useExternalValue(zoomStore);

const renderMarkerByZoomState = (
zoomState: keyof ZoomBreakpoints,
markerInstances: StationMarkerInstance[]
) => {
if (zoomState === 'max') {
renderCarffeineMarkers(markerInstances, stationMarkers);
}
if (zoomState === 'high') {
renderDefaultMarkers(markerInstances, stationMarkers);
}
};

useEffect(() => {
if (stationMarkers !== undefined) {
renderMarkerByZoomState(zoomState, markerInstanceStore.getState());
}
}, [zoomState]);

useEffect(() => {
return () => {
// MarkerContainers 컴포넌트에서 HighZoomMarkerContainer 컴포넌트가 unmount될 때 모든 마커를 지워준다.
removeAllMarkers(markerInstanceStore.getState());
};
}, []);

if (!stationMarkers || !isSuccess) {
if (stationMarkers === undefined || !isSuccess) {
return <></>;
}

Expand All @@ -28,7 +63,7 @@ const HighZoomMarkerContainer = () => {
);

removeMarkersOutsideBounds(markerInstanceStore.getState(), stationMarkers);
renderMarkerInstances(newMarkerInstances, stationMarkers);
renderMarkerByZoomState(zoomState, newMarkerInstances);

markerInstanceStore.setState([...remainedMarkerInstances, ...newMarkerInstances]);

Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import styled from 'styled-components';

import type { StationMarker } from '@type';

import type { MARKER_COLORS } from './CarFfeineMarker.style';
Expand All @@ -11,15 +13,23 @@ const CarFfeineMarker = (station: StationMarker) => {
const state: StationAvailability = availableCount === 0 ? 'noAvailable' : 'available';

return (
<Marker
data-testid="carFfeineMarker"
data-marker-id={`marker-${station.stationId}`}
title={stationName}
state={state}
>
{availableCount}
</Marker>
<Container>
<Marker
data-testid="carFfeineMarker"
data-marker-id={`marker-${station.stationId}`}
title={stationName}
state={state}
>
{availableCount}
</Marker>
</Container>
);
};

const Container = styled.div`
position: absolute;
left: -13.5px;
top: -35px;
`;

export default CarFfeineMarker;
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export const DEFAULT_MARKER_SIZE_RATIO = 0.5;
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ import StationDetailsWindow from '@ui/StationDetailsWindow';
import type { StationDetails, StationMarker, StationSummary } from '@type';

import CarFfeineMarker from '../components/CarFfeineMarker';
import { MARKER_COLORS } from '../components/CarFfeineMarker/CarFfeineMarker.style';
import { DEFAULT_MARKER_SIZE_RATIO } from '../constants';

export const useRenderStationMarker = () => {
const googleMap = getStoreSnapshot(getGoogleMapStore());
Expand All @@ -30,7 +32,7 @@ export const useRenderStationMarker = () => {
title: stationName,
});

bindMarkerClickHandler([{ stationId, markerInstance }]);
bindMarkerClickHandler([{ stationId, instance: markerInstance }]);

return markerInstance;
};
Expand All @@ -53,7 +55,7 @@ export const useRenderStationMarker = () => {

return {
stationId,
markerInstance,
instance: markerInstance,
};
});

Expand All @@ -66,19 +68,18 @@ export const useRenderStationMarker = () => {
prevMarkerInstances: StationMarkerInstance[],
currentMarkers: StationMarker[]
) => {
const markersOutOfBounds = prevMarkerInstances.filter(
(prevMarker) =>
!currentMarkers.some((currentMarker) => currentMarker.stationId === prevMarker.stationId)
const markersOutOfBounds = prevMarkerInstances.filter((prevMarker) =>
currentMarkers.every((currentMarker) => currentMarker.stationId !== prevMarker.stationId)
);

markersOutOfBounds.forEach((marker) => {
marker.markerInstance.map = null;
marker.instance.map = null;
});
};

const removeAllMarkers = (prevMarkerInstances: StationMarkerInstance[]) => {
prevMarkerInstances.forEach((marker) => {
marker.markerInstance.map = null;
marker.instance.map = null;
});
};

Expand All @@ -91,11 +92,40 @@ export const useRenderStationMarker = () => {
);
};

const renderMarkerInstances = (
newMarkerInstances: StationMarkerInstance[],
const renderDefaultMarkers = (
markerInstances: StationMarkerInstance[],
markers: StationMarker[] | StationSummary[]
) => {
newMarkerInstances.forEach(({ markerInstance, stationId }) => {
markers.forEach((marker) => {
const markerInstance = markerInstances.find(
(markerInstance) => markerInstance.stationId === marker.stationId
)?.instance;

if (markerInstance) {
const defaultMarkerDesign = new google.maps.marker.PinElement({
scale: DEFAULT_MARKER_SIZE_RATIO,
background:
marker.availableCount > 0
? MARKER_COLORS.available.background
: MARKER_COLORS.noAvailable.background,
borderColor:
marker.availableCount > 0
? MARKER_COLORS.available.border
: MARKER_COLORS.noAvailable.border,
glyph: '',
});

markerInstance.map = googleMap;
markerInstance.content = defaultMarkerDesign.element;
}
});
};

const renderCarffeineMarkers = (
markerInstances: StationMarkerInstance[],
markers: StationMarker[] | StationSummary[]
) => {
markerInstances.forEach(({ instance: markerInstance, stationId }) => {
const container = document.createElement('div');

markerInstance.content = container;
Expand All @@ -104,12 +134,13 @@ export const useRenderStationMarker = () => {
const markerInformation = markers.find(
(stationMarker) => stationMarker.stationId === stationId
);

createRoot(container).render(<CarFfeineMarker {...markerInformation} />);
});
};

const bindMarkerClickHandler = (markerInstances: StationMarkerInstance[]) => {
markerInstances.forEach(({ markerInstance, stationId }) => {
markerInstances.forEach(({ instance: markerInstance, stationId }) => {
markerInstance.addListener('click', () => {
openStationInfoWindow(stationId, markerInstance);

Expand All @@ -125,7 +156,8 @@ export const useRenderStationMarker = () => {
createNewMarkerInstances,
removeMarkersOutsideBounds,
getRemainedMarkerInstances,
renderMarkerInstances,
renderDefaultMarkers,
renderCarffeineMarkers,
removeAllMarkers,
};
};
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,9 @@ const MarkersContainers = () => {

return (
<>
{markerMode.state === 'high' && <MemoizedHighZoomMarkerContainer />}
{(markerMode.state === 'high' || markerMode.state === 'max') && (
<MemoizedHighZoomMarkerContainer />
)}
{/* 이 아래는 앞으로 추가될 기능을 미리 대응하는 컴포넌트 */}
{markerMode.state === 'middle' && <MemoizedMiddleZoomMarkerContainer />}
{markerMode.state === 'low' && <MemoizedLowZoomMarkerContainer />}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,17 @@
import { useEffect } from 'react';

import { warningModalActions } from '@stores/layout/warningModalStore';

import ZoomWarningModal from '@ui/WarningModal';

const MiddleZoomMarkerContainer = () => {
useEffect(() => {
warningModalActions.openModal(<ZoomWarningModal />);

return () => {
warningModalActions.closeModal();
};
}, []);
return <></>;
};

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,15 +8,15 @@ const meta = {
title: 'UI/StationInfoWindow/Buttons',
component: SummaryButtons,
args: {
handleCloseStationSummary: () => {
handleCloseStationWindow: () => {
alert('충전소 간단 정보창이 닫혔습니다.');
},
handleOpenStationDetail: () => {
alert('충전소 상세 정보창이 열렸습니다.');
},
},
argTypes: {
handleCloseStationSummary: {
handleCloseStationWindow: {
description: '충전소 간단 정보창을 닫을 수 있습니다.',
},
handleOpenStationDetail: {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ export const useStationSearchWindow = () => {

const { openLastPanel } = useNavigationBar();
const { openStationInfoWindow } = useStationInfoWindow();
const { createNewMarkerInstance, renderMarkerInstances } = useRenderStationMarker();
const { createNewMarkerInstance, renderDefaultMarkers } = useRenderStationMarker();

const screen = useMediaQueries();

Expand Down Expand Up @@ -87,11 +87,13 @@ export const useStationSearchWindow = () => {

const markerInstance = createNewMarkerInstance(stationDetails);

setMarkerInstances((prev) => [...prev, { stationId, markerInstance }]);
renderMarkerInstances(
[{ stationId, markerInstance }],
setMarkerInstances((prev) => [...prev, { stationId, instance: markerInstance }]);

renderDefaultMarkers(
[{ stationId, instance: markerInstance }],
[convertStationDetailsToSummary(stationDetails)]
);

openStationInfoWindow(stationId, markerInstance);

queryClient.setQueryData([QUERY_KEY_STATION_DETAILS, stationId], stationDetails);
Expand Down
1 change: 1 addition & 0 deletions frontend/src/constants/googleMaps.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ export const ZOOM_BREAKPOINTS: ZoomBreakpoints = {
low: MIN_ZOOM_LEVEL,
middle: 12,
high: INITIAL_ZOOM_LEVEL, // 기존 코드와 호환을 위해 일단 이렇게 처리했습니다.
max: 17,
};

export const DELTA_MULTIPLE = 2;
2 changes: 1 addition & 1 deletion frontend/src/hooks/google-maps/useStationInfoWindow.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ export const useStationInfoWindow = () => {
const stationMarker = markerInstanceStore
.getState()
.find((stationMarker) => stationMarker.stationId === stationId);
const markerInstance = stationMarkerInstance ?? stationMarker.markerInstance;
const markerInstance = stationMarkerInstance ?? stationMarker.instance;

moveMapToStationMarker(markerInstance);

Expand Down
2 changes: 1 addition & 1 deletion frontend/src/stores/google-maps/markerInstanceStore.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { store } from '@utils/external-state';

export interface StationMarkerInstance {
stationId: string;
markerInstance: google.maps.marker.AdvancedMarkerElement;
instance: google.maps.marker.AdvancedMarkerElement;
}

export const markerInstanceStore = store<StationMarkerInstance[]>([]);
1 change: 1 addition & 0 deletions frontend/src/stores/google-maps/zoomStore/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ export interface ZoomBreakpoints {
low: number;
middle: number;
high: number;
max: number;
}

export type ZoomState = keyof ZoomBreakpoints;
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ describe('markerModeStore', () => {
[12, 'middle'],
[15, 'middle'],
[16, 'high'],
[20, 'high'],
[20, 'max'],
])('getZoomState(%s) returns %s', (zoom, expected) => {
expect(getZoomState(zoom)).toBe(expected);
});
Expand Down
Loading

0 comments on commit 2a35093

Please sign in to comment.