diff --git a/CHANGELOG.md b/CHANGELOG.md index d9c13fb..3b476f3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,13 @@ ## 2026-06-16 +### ๐Ÿ—บ๏ธ NaverMapView ์•ˆ์ •ํ™” + ์žฌํ™œ์„ฑ (v0.1.57) +- divRef ํ•ญ์ƒ ๋งˆ์šดํŠธ (์ด์ „: ready ๊ฐ€๋“œ๋กœ ์ฒซ ๋ Œ๋” ref ๋ˆ„๋ฝ ๊ฐ€๋Šฅ) +- ๋ช…์‹œ์  width/height + ํšŒ์ƒ‰ ๋ฐฐ๊ฒฝ(์‹œ๊ฐ์  ๋กœ๋”ฉ ํ‘œ์‹œ) +- ResizeObserver + requestAnimationFrame์œผ๋กœ ์ปจํ…Œ์ด๋„ˆ 0ร—0 โ†’ ์ •์ƒ ํฌ๊ธฐ ์‹œ refresh +- try/catch + initError state๋กœ init ์‹คํŒจ ๊ฐ€์‹œํ™” +- Naver ํ‚ค ์žฌํ™œ์„ฑ + ### โช NaverMap ์ž„์‹œ ๋น„ํ™œ์„ฑ, ํ•œ๊ตญ๋„ GoogleMap fallback (v0.1.55) - NaverMapView ๊ณจ๊ฒฉ์ด ์‹ค ์šด์˜์—์„œ ์ง€๋„/๋งˆ์ปค ๋ Œ๋” ์‹คํŒจ (์ •ํ™•ํ•œ ์›์ธ ์ถ”ํ›„ ์ง„๋‹จ) - NEXT_PUBLIC_NAVER_MAP_CLIENT_ID ๋นˆ ๊ฐ’์œผ๋กœ dispatcher๊ฐ€ GoogleMap fallback (ํšŒ๊ท€ 0) diff --git a/frontend/src/components/NaverMapView.tsx b/frontend/src/components/NaverMapView.tsx index 33790cf..2748067 100644 --- a/frontend/src/components/NaverMapView.tsx +++ b/frontend/src/components/NaverMapView.tsx @@ -5,33 +5,29 @@ import Supercluster from "supercluster"; import type { Restaurant } from "@/lib/api"; import { getCuisineIcon } from "@/lib/cuisine-icons"; import Icon from "@/components/Icon"; -import type { MapBounds, FlyTo, MapViewProps } from "@/components/MapView.types"; +import type { MapBounds, MapViewProps } from "@/components/MapView.types"; -// ---- naver maps v3 ํƒ€์ž… ์ตœ์†Œ ์ •์˜ (full ํƒ€์ž…์€ @types/navermaps ์—†์Œ) ---- declare global { interface Window { - naver?: { - maps: NaverMaps; - }; + naver?: { maps: NaverMaps }; } } type NaverMaps = { LatLng: new (lat: number, lng: number) => unknown; Map: new (el: HTMLElement, opts: Record) => NaverMapInstance; Event: { addListener: (target: unknown, type: string, fn: (...args: unknown[]) => void) => unknown }; - Position?: unknown; - MapTypeControlStyle?: unknown; - ZoomControlStyle?: unknown; + Size: new (w: number, h: number) => unknown; }; type NaverMapInstance = { setCenter: (latlng: unknown) => void; setZoom: (zoom: number, useEffect?: boolean) => void; - getCenter: () => { lat: () => number; lng: () => number; x: number; y: number }; + getCenter: () => { lat: () => number; lng: () => number }; getZoom: () => number; getBounds: () => { getNE: () => { lat: () => number; lng: () => number }; getSW: () => { lat: () => number; lng: () => number } }; getProjection: () => { fromCoordToOffset: (latlng: unknown) => { x: number; y: number } }; panTo: (latlng: unknown, opts?: Record) => void; - destroy?: () => void; + refresh: (noEffect?: boolean) => void; + setSize?: (size: unknown) => void; }; const NAVER_CLIENT_ID = process.env.NEXT_PUBLIC_NAVER_MAP_CLIENT_ID || ""; @@ -41,10 +37,7 @@ function useNaverMaps(): { ready: boolean; error: string | null } { const [error, setError] = useState(null); useEffect(() => { - if (!NAVER_CLIENT_ID) { - setError("NEXT_PUBLIC_NAVER_MAP_CLIENT_ID ๋ฏธ์„ค์ •"); - return; - } + if (!NAVER_CLIENT_ID) { setError("NEXT_PUBLIC_NAVER_MAP_CLIENT_ID ๋ฏธ์„ค์ •"); return; } if (window.naver?.maps) { setReady(true); return; } const existing = document.querySelector(`script[data-naver-maps]`); if (existing) { @@ -52,7 +45,6 @@ function useNaverMaps(): { ready: boolean; error: string | null } { return; } const s = document.createElement("script"); - // NCLOUD ์‹  ์ •์ฑ…: ํŒŒ๋ผ๋ฏธํ„ฐ๋Š” ncpKeyId (์˜› ncpClientId๋Š” NAVER Developers์šฉ). s.src = `https://oapi.map.naver.com/openapi/v3/maps.js?ncpKeyId=${NAVER_CLIENT_ID}`; s.async = true; s.dataset.naverMaps = "1"; @@ -110,38 +102,60 @@ export default function NaverMapView({ const mapRef = useRef(null); const [bounds, setBounds] = useState(null); const [zoom, setZoom] = useState(13); + const [initError, setInitError] = useState(null); const { getClusters, getExpansionZoom } = useSupercluster(restaurants); - // 1) ์ง€๋„ ์ธ์Šคํ„ด์Šค 1ํšŒ ์ƒ์„ฑ + // ์ง€๋„ 1ํšŒ ์ƒ์„ฑ โ€” divRef ํ•ญ์ƒ ๋งˆ์šดํŠธ๋˜๋ฏ€๋กœ ready ์‹œ์ ์— ์•ˆ์ •์ ์œผ๋กœ ์žกํž˜ useEffect(() => { if (!ready || !divRef.current || mapRef.current) return; - const n = window.naver!.maps; - const initLat = flyTo?.lat ?? selected?.latitude ?? 37.5665; - const initLng = flyTo?.lng ?? selected?.longitude ?? 126.978; - const initZoom = flyTo?.zoom ?? 13; - const m = new n.Map(divRef.current, { - center: new n.LatLng(initLat, initLng), - zoom: initZoom, - logoControl: false, - mapDataControl: false, - scaleControl: false, - zoomControl: false, - }); - mapRef.current = m; - const sync = () => { - const b = m.getBounds(); - const ne = b.getNE(), sw = b.getSW(); - const nb: MapBounds = { north: ne.lat(), south: sw.lat(), east: ne.lng(), west: sw.lng() }; - setBounds(nb); - setZoom(m.getZoom()); - onBoundsChanged?.(nb); - }; - sync(); - n.Event.addListener(m, "bounds_changed", sync); - n.Event.addListener(m, "zoom_changed", sync); + try { + const n = window.naver!.maps; + const initLat = flyTo?.lat ?? selected?.latitude ?? 37.5665; + const initLng = flyTo?.lng ?? selected?.longitude ?? 126.978; + const initZoom = flyTo?.zoom ?? 13; + const m = new n.Map(divRef.current, { + center: new n.LatLng(initLat, initLng), + zoom: initZoom, + logoControl: false, + mapDataControl: false, + scaleControl: false, + zoomControl: false, + }); + mapRef.current = m; + + // ์ปจํ…Œ์ด๋„ˆ ํฌ๊ธฐ ๋ณ€๊ฒฝ(์˜ˆ: flex layout ์•ˆ์—์„œ ์ฒซ ๋งˆ์šดํŠธ ์‹œ 0ร—0 โ†’ ์‹ค์ œ ํฌ๊ธฐ) โ†’ refresh + const ro = new ResizeObserver(() => { + try { m.refresh(true); } catch { /* noop */ } + }); + ro.observe(divRef.current); + + const sync = () => { + try { + const b = m.getBounds(); + const ne = b.getNE(), sw = b.getSW(); + const nb: MapBounds = { north: ne.lat(), south: sw.lat(), east: ne.lng(), west: sw.lng() }; + setBounds(nb); + setZoom(m.getZoom()); + onBoundsChanged?.(nb); + } catch (e) { + console.warn("[NaverMap] sync failed", e); + } + }; + // ์ปจํ…Œ์ด๋„ˆ ํฌ๊ธฐ๊ฐ€ ์ •ํ•ด์ง„ ๋‹ค์Œ sync (rAF๋กœ ๋‹ค์Œ ํ”„๋ ˆ์ž„) + requestAnimationFrame(() => { + try { m.refresh(true); } catch { /* noop */ } + sync(); + }); + n.Event.addListener(m, "bounds_changed", sync); + n.Event.addListener(m, "zoom_changed", sync); + } catch (e) { + const msg = e instanceof Error ? e.message : String(e); + console.error("[NaverMap] init failed", e); + setInitError(msg); + } }, [ready, flyTo, selected, onBoundsChanged]); - // 2) flyTo ๋ณ€๊ฒฝ ๋ฐ˜์˜ + // flyTo ๋ณ€๊ฒฝ ๋ฐ˜์˜ useEffect(() => { const m = mapRef.current; if (!m || !flyTo || !window.naver?.maps) return; @@ -154,22 +168,38 @@ export default function NaverMapView({ return getClusters(bounds, zoom); }, [bounds, zoom, getClusters]); - // 3) ์ขŒํ‘œ โ†’ ํ™”๋ฉด ํ”ฝ์…€ ๋ณ€ํ™˜ (๋„ค์ด๋ฒ„ projection) const toScreen = useCallback((lat: number, lng: number) => { const m = mapRef.current; if (!m || !window.naver?.maps) return null; - const p = m.getProjection().fromCoordToOffset(new window.naver.maps.LatLng(lat, lng)); - return p; + try { + return m.getProjection().fromCoordToOffset(new window.naver.maps.LatLng(lat, lng)); + } catch { + return null; + } }, []); - if (error) return
{error} โ€” ์ƒˆ๋กœ๊ณ ์นจ ํ›„์—๋„ ๊ฐ™์œผ๋ฉด Google Map์œผ๋กœ ํด๋ฐฑ๋ฉ๋‹ˆ๋‹ค.
; - if (!ready) return
๋„ค์ด๋ฒ„ ์ง€๋„ ๋กœ๋”ฉ ์ค‘โ€ฆ
; - return (
-
+ {/* ์ง€๋„ ์ปจํ…Œ์ด๋„ˆ โ€” ํ•ญ์ƒ ๋งˆ์šดํŠธ, ๋ช…์‹œ์  ํฌ๊ธฐ ๋ณด์žฅ */} +
- {/* ๋งˆ์ปค overlay (์ขŒํ‘œโ†’ํ”ฝ์…€ ๋ณ€ํ™˜ + absolute positioned div) */} + {/* ๋กœ๋”ฉ/์—๋Ÿฌ overlay */} + {(error || initError) && ( +
+ {error ?? initError} โ€” ์ƒˆ๋กœ๊ณ ์นจ ๋˜๋Š” GoogleMap๋กœ fallback +
+ )} + {!ready && !error && ( +
+ ๋„ค์ด๋ฒ„ ์ง€๋„ ๋กœ๋”ฉ ์ค‘โ€ฆ +
+ )} + + {/* ๋งˆ์ปค overlay */} {clusters.map((feature) => { const [lng, lat] = feature.geometry.coordinates; const pt = toScreen(lat, lng); @@ -211,7 +241,6 @@ export default function NaverMapView({ ); })} - {/* ๋‚ด ์œ„์น˜ ๋ฒ„ํŠผ */} {onMyLocation && (