diff --git a/frontend/src/api/place/index.ts b/frontend/src/api/place/index.ts index cdddf061..a90fe9f5 100644 --- a/frontend/src/api/place/index.ts +++ b/frontend/src/api/place/index.ts @@ -54,7 +54,7 @@ export const putPlaceToCourse = async ({ }) => { const { data } = await axiosInstance.put( END_POINTS.PUT_PLACE_TO_COURSE(id), - { places }, + { pins: places }, ); return { ...data, id }; }; diff --git a/frontend/src/components/ImageSkeleton.tsx b/frontend/src/components/ImageSkeleton.tsx index 50ba208c..52f18f7b 100644 --- a/frontend/src/components/ImageSkeleton.tsx +++ b/frontend/src/components/ImageSkeleton.tsx @@ -15,7 +15,6 @@ const ImageWithSkeleton = ({ height, }: ImageWithSkeletonProps) => { const [loaded, setLoaded] = useState(false); - const [showSkeleton, setShowSkeleton] = useState(true); const [error, setError] = useState(false); useEffect(() => { @@ -23,15 +22,11 @@ const ImageWithSkeleton = ({ img.src = src; img.onload = () => { - setShowSkeleton(false); - setTimeout(() => { - setLoaded(true); - }, 3000); + setLoaded(true); }; img.onerror = () => { setError(true); - setShowSkeleton(false); }; }, [src]); @@ -49,9 +44,15 @@ const ImageWithSkeleton = ({ return (
- {!loaded &&
} + {!loaded && ( +
+ )} {loaded && ( - {alt} + {alt} )}
); diff --git a/frontend/src/components/Place/DeletePlaceButton.tsx b/frontend/src/components/Place/DeletePlaceButton.tsx index 5c2a51fa..0a59ecc2 100644 --- a/frontend/src/components/Place/DeletePlaceButton.tsx +++ b/frontend/src/components/Place/DeletePlaceButton.tsx @@ -18,7 +18,8 @@ const DeletePlaceButton = ({ placeId, places }: DeletePlaceButtonProps) => { const putPlaceToCourseMutation = usePutPlaceToCourseMutation(); const addToast = useStore((state) => state.addToast); - const onClickMapMode = () => { + const onClickMapMode = (e: React.MouseEvent) => { + e.stopPropagation(); deletePlaceMutation.mutate( { id, placeId }, { @@ -39,7 +40,8 @@ const DeletePlaceButton = ({ placeId, places }: DeletePlaceButtonProps) => { [places], ); - const onClickCourseMode = () => { + const onClickCourseMode = (e: React.MouseEvent) => { + e.stopPropagation(); putPlaceToCourseMutation.mutate( { id, places: newPlaces }, { diff --git a/frontend/src/components/Place/PlaceListPanel.tsx b/frontend/src/components/Place/PlaceListPanel.tsx index d300aa2c..8dc3f620 100644 --- a/frontend/src/components/Place/PlaceListPanel.tsx +++ b/frontend/src/components/Place/PlaceListPanel.tsx @@ -84,8 +84,8 @@ const PlaceListPanel = ({ {places.map((place, index) => ( {(provided, snapshot) => ( diff --git a/frontend/src/constants/api.ts b/frontend/src/constants/api.ts index a1ae6a8b..8b073540 100644 --- a/frontend/src/constants/api.ts +++ b/frontend/src/constants/api.ts @@ -79,4 +79,6 @@ export const USER_ERROR_MESSAGE = { E303: '해당 장소가 이미 존재합니다.', E304: '유효하지 않은 지도입니다. 다시 확인해 주세요.', E201: '요청한 장소를 찾을 수 없습니다.', + E999: '서버에 문제가 발생했습니다. 잠시 후 다시 시도해 주세요.', + E900: '양식이 잘못 되었습니다. 다시 확인해 주세요.', }; diff --git a/frontend/src/hooks/useMarker.ts b/frontend/src/hooks/useMarker.ts index 95bb8ff5..3ab39b88 100644 --- a/frontend/src/hooks/useMarker.ts +++ b/frontend/src/hooks/useMarker.ts @@ -61,11 +61,10 @@ export const useMarker = (props: MarkerProps) => { newMarker.map = map; setMarker(newMarker); addMarker(newMarker); - console.log(newMarker, 'add new marker'); return () => { - newMarker.map = null; setMarker(null); + google.maps.event.clearInstanceListeners(newMarker); removeMarker(newMarker); }; }, [map, order]); @@ -90,14 +89,10 @@ export const useMarker = (props: MarkerProps) => { infoWindow.open({ anchor: marker, map }); moveTo(position?.lat as number, position?.lng as number); }); + google.maps.event.addListener(map, 'click', () => { infoWindow.close(); }); - - return () => { - google.maps.event.clearInstanceListeners(marker); - google.maps.event.clearInstanceListeners(map); - }; }, [marker, onClick]); return marker; }; diff --git a/frontend/src/lib/CustomMarkerClusterer.ts b/frontend/src/lib/CustomMarkerClusterer.ts index 3aafed48..072caeef 100644 --- a/frontend/src/lib/CustomMarkerClusterer.ts +++ b/frontend/src/lib/CustomMarkerClusterer.ts @@ -1,4 +1,5 @@ import { + ClusterStats, Marker, MarkerClusterer, MarkerClustererEvents, @@ -17,11 +18,20 @@ export class CustomMarkerClusterer extends MarkerClusterer { this.markerLatLngSet = new Set(); } + public onAdd(): void { + const map = this.getMap(); + if (!map) return; + this.idleListener = map.addListener('idle', () => { + this.render(); + }); + this.render(); + } + public addMarker( marker: google.maps.marker.AdvancedMarkerElement, noDraw?: boolean, ): void { - const markerLatLng = `${marker.position?.lat}${marker.position?.lng}`; + const markerLatLng = `${marker.position?.lat} ${marker.position?.lng}`; if (this.markerLatLngSet.has(markerLatLng)) { return; } @@ -34,6 +44,29 @@ export class CustomMarkerClusterer extends MarkerClusterer { } } + public removeMarker( + marker: google.maps.marker.AdvancedMarkerElement, + noDraw?: boolean, + ): boolean { + const index = this.markers.indexOf(marker); + if (index === -1) { + // Marker is not in our list of markers, so do nothing: + return false; + } + + const markerLatLng = `${marker.position?.lat} ${marker.position?.lng}`; + this.markerLatLngSet.delete(markerLatLng); + + MarkerUtils.setMap(marker, null); + this.markers.splice(index, 1); // Remove the marker from the list of managed markers + + if (!noDraw) { + this.render(); + } + + return true; + } + public render(): void { const map = this.getMap(); if (map instanceof google.maps.Map && map.getProjection()) { @@ -47,7 +80,7 @@ export class CustomMarkerClusterer extends MarkerClusterer { map, mapCanvasProjection: this.getProjection(), }); - + console.log(changed, 'given changed'); // Allow algorithms to return flag on whether the clusters/markers have changed. if (changed || changed === undefined) { // Accumulate the markers of the clusters composed of a single marker. @@ -71,7 +104,6 @@ export class CustomMarkerClusterer extends MarkerClusterer { // The marker: // - was previously rendered because it is from a cluster with 1 marker, // - should no more be rendered as it is not in singleMarker. - console.log('cluster.marker removed', cluster.marker); MarkerUtils.setMap(cluster.marker!, null); } } else { @@ -83,7 +115,7 @@ export class CustomMarkerClusterer extends MarkerClusterer { this.clusters = clusters; this.renderClusters(); // Delayed removal of the markers of the former groups. - console.log('groupMarkers', groupMarkers); + setTimeout(() => { groupMarkers.forEach((marker) => { MarkerUtils.setMap(marker, null); @@ -97,6 +129,39 @@ export class CustomMarkerClusterer extends MarkerClusterer { ); } } + + protected renderClusters(): void { + // Generate stats to pass to renderers. + const stats = new ClusterStats(this.markers, this.clusters); + + const map = this.getMap() as google.maps.Map; + + this.clusters.forEach((cluster) => { + if (cluster.markers?.length === 1) { + cluster.marker = cluster.markers[0]; + } else { + // Generate the marker to represent the group. + cluster.marker = this.renderer.render(cluster, stats, map); + // Make sure all individual markers are removed from the map. + cluster.markers?.forEach((marker) => MarkerUtils.setMap(marker, null)); + if (this.onClusterClick) { + cluster.marker.addListener( + 'click', + /* istanbul ignore next */ + (event: google.maps.MapMouseEvent) => { + google.maps.event.trigger( + this, + MarkerClustererEvents.CLUSTER_CLICK, + cluster, + ); + this.onClusterClick(event, cluster, map); + }, + ); + } + } + MarkerUtils.setMap(cluster.marker, map); + }); + } } export const clustererOptions = { diff --git a/frontend/src/lib/CustomSuperCluseterAlgorithm.ts b/frontend/src/lib/CustomSuperCluseterAlgorithm.ts index 1232ade2..d418cf3b 100644 --- a/frontend/src/lib/CustomSuperCluseterAlgorithm.ts +++ b/frontend/src/lib/CustomSuperCluseterAlgorithm.ts @@ -1,10 +1,64 @@ import { + AlgorithmInput, + AlgorithmOutput, + getPaddedViewport, + MarkerUtils, SuperClusterViewportAlgorithm, SuperClusterViewportOptions, + SuperClusterViewportState, } from '@googlemaps/markerclusterer'; +import equal from 'fast-deep-equal'; export class CustomSuperClusterAlgorithm extends SuperClusterViewportAlgorithm { constructor({ ...options }: SuperClusterViewportOptions) { super(options); + this.clusters = []; + } + + public calculate(input: AlgorithmInput): AlgorithmOutput { + const state: SuperClusterViewportState = { + zoom: Math.round(input.map.getZoom()!), + view: getPaddedViewport( + input.map.getBounds()!, + input.mapCanvasProjection, + this.viewportPadding, + ), + }; + + // let changed = !equal(this.state, state); + let changed = false; + if (!equal(input.markers, this.markers)) { + // TODO use proxy to avoid copy? + this.markers = [...input.markers]; + + const points = this.markers.map((marker) => { + const position = MarkerUtils.getPosition(marker); + const coordinates = [position.lng(), position.lat()]; + return { + type: 'Feature' as const, + geometry: { + type: 'Point' as const, + coordinates, + }, + properties: { marker }, + }; + }); + this.superCluster.load(points); + } + + const newClusters = this.cluster(input); + console.log(newClusters.length, this.clusters.length); + //this.clusters.length !== newClusters.length + //!equal(this.clusters, newClusters) + if (this.clusters.length !== newClusters.length) { + this.clusters = newClusters; + changed = true; + } else { + changed = false; + } + + this.state = state; + + return { clusters: this.clusters, changed }; } } diff --git a/frontend/src/lib/SuperClusterAlgorithm.ts b/frontend/src/lib/SuperClusterAlgorithm.ts new file mode 100644 index 00000000..b4d86330 --- /dev/null +++ b/frontend/src/lib/SuperClusterAlgorithm.ts @@ -0,0 +1,57 @@ +import { + AlgorithmInput, + AlgorithmOutput, + getPaddedViewport, + MarkerUtils, + SuperClusterViewportAlgorithm, + SuperClusterViewportOptions, + SuperClusterViewportState, +} from '@googlemaps/markerclusterer'; +import equal from 'fast-deep-equal'; + +export class SuperClusterAlgorithmTest extends SuperClusterViewportAlgorithm { + constructor({ ...options }: SuperClusterViewportOptions) { + super(options); + this.clusters = []; + } + + public calculate(input: AlgorithmInput): AlgorithmOutput { + const state: SuperClusterViewportState = { + zoom: Math.round(input.map.getZoom()!), + view: getPaddedViewport( + input.map.getBounds()!, + input.mapCanvasProjection, + this.viewportPadding, + ), + }; + + let changed = !equal(this.state, state); + console.log('Origin changed', changed); + if (!equal(input.markers, this.markers)) { + changed = true; + // TODO use proxy to avoid copy? + this.markers = [...input.markers]; + + const points = this.markers.map((marker) => { + const position = MarkerUtils.getPosition(marker); + const coordinates = [position.lng(), position.lat()]; + return { + type: 'Feature' as const, + geometry: { + type: 'Point' as const, + coordinates, + }, + properties: { marker }, + }; + }); + this.superCluster.load(points); + } + + if (changed) { + this.clusters = this.cluster(input); + this.state = state; + } + + return { clusters: this.clusters, changed }; + } +} diff --git a/frontend/src/store/googleMapSlice/index.ts b/frontend/src/store/googleMapSlice/index.ts index 03c149ab..f72c4199 100644 --- a/frontend/src/store/googleMapSlice/index.ts +++ b/frontend/src/store/googleMapSlice/index.ts @@ -10,15 +10,17 @@ import { clustererOptions, CustomMarkerClusterer, } from '@/lib/CustomMarkerClusterer'; +import { CustomSuperClusterAlgorithm } from '@/lib/CustomSuperCluseterAlgorithm'; export type GoogleMapState = { googleMap: google.maps.Map | null; markerClusterer: MarkerClusterer | null; - markers: google.maps.marker.AdvancedMarkerElement[]; + setGoogleMap: (map: google.maps.Map) => void; initializeMap: (container: HTMLElement) => void; moveTo: (lat: number, lng: number) => void; addMarker: (marker: google.maps.marker.AdvancedMarkerElement) => void; + removeMarker: (marker: google.maps.marker.AdvancedMarkerElement) => void; }; @@ -30,18 +32,20 @@ export const createGoogleMapSlice: StateCreator< > = (set, get) => ({ googleMap: null, markerClusterer: null, - markers: [], setGoogleMap: (map: google.maps.Map) => set({ googleMap: map }), initializeMap: async (container: HTMLElement) => { await loadGoogleMapsApi(); const GoogleMap = getGoogleMapClass(); const map = new GoogleMap(container, INITIAL_MAP_CONFIG); + + const { algorithm, ...restClustererOptions } = clustererOptions; const markerClusterer = new CustomMarkerClusterer({ map, - ...clustererOptions, + algorithm: new CustomSuperClusterAlgorithm({ maxZoom: 18 }), + ...restClustererOptions, }); - + console.log(algorithm); set({ googleMap: map, markerClusterer }); }, @@ -54,14 +58,12 @@ export const createGoogleMapSlice: StateCreator< }, addMarker: (marker: google.maps.marker.AdvancedMarkerElement) => { - const { markerClusterer, markers } = get(); + const { markerClusterer } = get(); markerClusterer?.addMarker(marker); - set({ markers: [...markers, marker] }); }, removeMarker: (marker: google.maps.marker.AdvancedMarkerElement) => { - const { markerClusterer, markers } = get(); + const { markerClusterer } = get(); markerClusterer?.removeMarker(marker); - set({ markers: markers.filter((m) => m !== marker) }); }, }); diff --git a/frontend/tailwind.config.js b/frontend/tailwind.config.js index 0631da93..7f3872ce 100644 --- a/frontend/tailwind.config.js +++ b/frontend/tailwind.config.js @@ -30,6 +30,7 @@ module.exports = { animation: { slideInUp: 'slideInUp 0.5s ease-out forwards', fadeOut: 'fadeOut 0.5s ease-out forwards', + pulse: 'pulse 2s cubic-bezier(0.4, 0, 0.6, 1) infinite', }, }, keyframes: { @@ -41,6 +42,10 @@ module.exports = { '0%': { opacity: '1' }, '100%': { opacity: '0' }, }, + pulse: { + '0%, 100%': { opacity: 1 }, + '50%': { opacity: 0.5 }, + }, }, }, plugins: [require('tailwind-scrollbar')],