Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
89 changes: 86 additions & 3 deletions src/features/map/components/MapViewer.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,12 @@
import { forwardRef, useCallback, useEffect, useImperativeHandle, useRef } from 'react';
import { MapCoordinate, MapPlace, DEFAULT_CENTER, DEFAULT_MARKER_COLOR } from '../types';
import { MapCoordinate, MapPlace, MapPin, DEFAULT_CENTER, DEFAULT_MARKER_COLOR } from '../types';
import {
createMapPinOverlay,
disposeMapPinOverlay,
toMapPinMarkerProps,
updateMapPinMarker,
type MapPinOverlayEntry,
} from '../utils/mapPinMarker';

// 지도 줌 하한선 (레벨 단위 유지, 상한선은 API 지원 한도까지 허용)
const MIN_ZOOM = 6;
Expand Down Expand Up @@ -155,9 +162,12 @@ type MapViewerProps = {
zoom: number;
placeResults: MapPlace[];
selectedPlaceId: string | null;
mapPins: MapPin[];
selectedMapPinId: string | null;
onZoomChanged?: (newZoom: number) => void;
onCenterChanged?: (center: MapCoordinate) => void;
onSelectPlace?: (placeId: string) => void;
onSelectMapPin?: (pinId: string) => void;
};

export type MapViewerHandle = {
Expand All @@ -166,7 +176,18 @@ export type MapViewerHandle = {
};

export const MapViewer = forwardRef<MapViewerHandle, MapViewerProps>(function MapViewer(
{ isLoaded, zoom, placeResults, selectedPlaceId, onZoomChanged, onCenterChanged, onSelectPlace },
{
isLoaded,
zoom,
placeResults,
selectedPlaceId,
mapPins,
selectedMapPinId,
onZoomChanged,
onCenterChanged,
onSelectPlace,
onSelectMapPin,
},
ref,
) {
const mapRef = useRef<HTMLDivElement>(null);
Expand All @@ -175,10 +196,13 @@ export const MapViewer = forwardRef<MapViewerHandle, MapViewerProps>(function Ma
const headingMarkerRef = useRef<google.maps.Marker | null>(null);

const placeMarkersRef = useRef<{ id: string; place: MapPlace; marker: google.maps.Marker }[]>([]);
const mapPinOverlaysRef = useRef<{ id: string; entry: MapPinOverlayEntry }[]>([]);
const infoWindowRef = useRef<google.maps.InfoWindow | null>(null);
const onCenterChangedRef = useRef(onCenterChanged);
const onSelectPlaceRef = useRef(onSelectPlace);
const onSelectMapPinRef = useRef(onSelectMapPin);
const selectedPlaceIdRef = useRef(selectedPlaceId);
const selectedMapPinIdRef = useRef(selectedMapPinId);

useEffect(() => {
onCenterChangedRef.current = onCenterChanged;
Expand All @@ -188,6 +212,14 @@ export const MapViewer = forwardRef<MapViewerHandle, MapViewerProps>(function Ma
onSelectPlaceRef.current = onSelectPlace;
}, [onSelectPlace]);

useEffect(() => {
onSelectMapPinRef.current = onSelectMapPin;
}, [onSelectMapPin]);

useEffect(() => {
selectedMapPinIdRef.current = selectedMapPinId;
}, [selectedMapPinId]);

useEffect(() => {
selectedPlaceIdRef.current = selectedPlaceId;
}, [selectedPlaceId]);
Expand Down Expand Up @@ -274,6 +306,7 @@ export const MapViewer = forwardRef<MapViewerHandle, MapViewerProps>(function Ma
strictBounds: true,
},
disableDefaultUI: true,
gestureHandling: 'greedy',
styles: FIXED_MAP_STYLES, // Map ID가 없어야 이 스타일이 정상 작동함
});
mapInstanceRef.current = map;
Expand Down Expand Up @@ -323,6 +356,19 @@ export const MapViewer = forwardRef<MapViewerHandle, MapViewerProps>(function Ma
icon: createMarkerIcon(mapsApi, DEFAULT_MARKER_COLOR),
zIndex: 10,
});

// 실제 기기 방향 값이 들어오기 전에 항상 방향 쐐기가 보이도록 함. 기본값(0도, 북쪽)
headingMarkerRef.current = new mapsApi.Marker({
map,
position: pos,
icon: createHeadingIcon(mapsApi, DEFAULT_MARKER_COLOR, 0),
clickable: false,
zIndex: 11,
});

// 사용자 제스처가 필요 없는 브라우저(iOS 외)에서는 위치를 얻는 시점에 나침반도 바로 켠다.
enableCompassIfNeeded();

onCenterChangedRef.current?.(pos);
} else {
markerRef.current.setPosition(pos);
Expand All @@ -342,7 +388,7 @@ export const MapViewer = forwardRef<MapViewerHandle, MapViewerProps>(function Ma
ignore = true;
navigator.geolocation.clearWatch(watchId);
};
}, [isLoaded]);
}, [isLoaded, enableCompassIfNeeded]);

// --- "현재 위치" 버튼에서 호출할 재중심 이동 ---
const recenterToCurrentLocation = useCallback(() => {
Expand Down Expand Up @@ -448,6 +494,43 @@ export const MapViewer = forwardRef<MapViewerHandle, MapViewerProps>(function Ma
}
}, [selectedPlaceId, placeResults]);

// --- 지도 핀(OverlayView) 렌더링 ---
useEffect(() => {
const map = mapInstanceRef.current;
if (!isLoaded || !map) return;

const selectedId = selectedMapPinIdRef.current;

mapPinOverlaysRef.current = mapPins.map((pin) => {
const entry = createMapPinOverlay({
position: { lat: pin.lat, lng: pin.lng },
zIndex: pin.id === selectedId ? 200 : 100,
onClick: () => onSelectMapPinRef.current?.(pin.id),
...toMapPinMarkerProps(pin, pin.id === selectedId),
});
entry.overlay.setMap(map);

return { id: pin.id, entry };
});

return () => {
mapPinOverlaysRef.current.forEach(({ entry }) => disposeMapPinOverlay(entry));
mapPinOverlaysRef.current = [];
};
}, [isLoaded, mapPins]);

Comment thread
coderabbitai[bot] marked this conversation as resolved.
Resolved
// --- 선택된 지도 핀 강조 ---
useEffect(() => {
mapPinOverlaysRef.current.forEach(({ id, entry }) => {
const pin = mapPins.find((candidate) => candidate.id === id);
if (!pin) return;

const isSelected = id === selectedMapPinId;
updateMapPinMarker(entry.mount, toMapPinMarkerProps(pin, isSelected));
entry.overlay.setZIndex(isSelected ? 200 : 100);
});
}, [selectedMapPinId, mapPins]);

return (
<main className="relative h-full w-full">
{!isLoaded && (
Expand Down
5 changes: 5 additions & 0 deletions src/pages/MapPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { SearchLauncher } from '@/components/ui/SearchInput';
import type { MapPlace } from '@/features/map/types';
import { loadGoogleMapsScript } from '@/features/map/utils';
import { MapViewer, type MapViewerHandle } from '@/features/map/components/MapViewer';
import { MOCK_MAP_PINS } from '@/features/map/constants/mockMapPins';
import { BottomNav, type NavItemId } from '@/components/BottomNav';
import type { PinSearchPlace } from '@/features/pin/types';
import BookmarkIcon from '@/assets/icons/bookmark.svg?react';
Expand Down Expand Up @@ -37,6 +38,7 @@ const MapPage: React.FC<MapPageProps> = ({ selectedMapPlace }) => {
[selectedMapPlace],
);
const selectedPlaceId = selectedMapPlace?.id ?? null;
const [selectedMapPinId, setSelectedMapPinId] = useState<string | null>(null);

const mapViewerRef = useRef<MapViewerHandle>(null);

Expand Down Expand Up @@ -150,7 +152,10 @@ const MapPage: React.FC<MapPageProps> = ({ selectedMapPlace }) => {
zoom={zoom}
placeResults={placeResults}
selectedPlaceId={selectedPlaceId}
mapPins={MOCK_MAP_PINS}
selectedMapPinId={selectedMapPinId}
onZoomChanged={handleZoomChange}
onSelectMapPin={setSelectedMapPinId}
/>
</div>
);
Expand Down
Loading