From 247547c51617aeb6eb0de00372712a710a4a77cb Mon Sep 17 00:00:00 2001 From: joungmin Date: Tue, 16 Jun 2026 17:09:02 +0900 Subject: [PATCH] =?UTF-8?q?feat(map):=20NaverMapView=20=EC=B1=84=EB=84=90?= =?UTF-8?q?=EB=B3=84=20=EB=A7=88=EC=BB=A4=20=EC=83=89=EC=83=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - GoogleMapView와 동일 팔레트 (8 색) - 식당 첫 채널 기준 색상, activeChannel 있으면 우선 - getChannelColorMap 재사용 패턴 동일 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.7 --- CHANGELOG.md | 4 +++ frontend/src/components/NaverMapView.tsx | 38 ++++++++++++++++++++---- 2 files changed, 37 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 64a0b89..2cb3ce6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,10 @@ ## 2026-06-16 +### 🎨 NaverMapView 채널별 마커 색상 (v0.1.60) +- GoogleMapView와 동일 팔레트 (amber/blue/green/pink/purple/red/teal/yellow) +- 식당의 첫 채널 기준 색상, activeChannel 있으면 그 채널 우선 + ### ⚡ NaverMapView SDK 네이티브 마커 + InfoWindow (v0.1.59) - 마커를 React `absolute div` overlay → `naver.maps.Marker` 네이티브로 교체 - 줌/팬 시 SDK가 GPU 최적화, 매 frame React 리렌더링 없음 → 랙 해소 diff --git a/frontend/src/components/NaverMapView.tsx b/frontend/src/components/NaverMapView.tsx index cf481a6..c4d5aaa 100644 --- a/frontend/src/components/NaverMapView.tsx +++ b/frontend/src/components/NaverMapView.tsx @@ -44,6 +44,30 @@ type NaverInfoWindow = { const NAVER_CLIENT_ID = process.env.NEXT_PUBLIC_NAVER_MAP_CLIENT_ID || ""; +// Channel color palette — GoogleMapView와 동일 +const CHANNEL_COLORS = [ + { border: "#f59e0b" }, // amber (default) + { border: "#3b82f6" }, // blue + { border: "#22c55e" }, // green + { border: "#ec4899" }, // pink + { border: "#a855f7" }, // purple + { border: "#ef4444" }, // red + { border: "#14b8a6" }, // teal + { border: "#eab308" }, // yellow +]; + +function getChannelColorMap(restaurants: Restaurant[]) { + const channels = new Set(); + restaurants.forEach((r) => r.channels?.forEach((ch) => channels.add(ch))); + const map: Record = {}; + let i = 0; + for (const ch of channels) { + map[ch] = CHANNEL_COLORS[i % CHANNEL_COLORS.length]; + i++; + } + return map; +} + function useNaverMaps(): { ready: boolean; error: string | null } { const [ready, setReady] = useState(typeof window !== "undefined" && !!window.naver?.maps); const [error, setError] = useState(null); @@ -101,9 +125,9 @@ function getClusterSize(count: number): number { return 54; } -// SVG data URL — 단일 마커(주황 핀) -function markerIconHtml(): string { - return `
`; +// 단일 마커 — 채널 색상별 +function markerIconHtml(color: string): string { + return `
`; } // SVG data URL — 클러스터(숫자) function clusterIconHtml(count: number, size: number): string { @@ -117,7 +141,9 @@ export default function NaverMapView({ onBoundsChanged, flyTo, onMyLocation, + activeChannel, }: MapViewProps) { + const channelColors = useMemo(() => getChannelColorMap(restaurants), [restaurants]); const { ready, error } = useNaverMaps(); const divRef = useRef(null); const mapRef = useRef(null); @@ -230,12 +256,14 @@ export default function NaverMapView({ markersRef.current.push(marker); } else { const r = (feature.properties as RestaurantProps).restaurant; + const chKey = activeChannel && r.channels?.includes(activeChannel) ? activeChannel : r.channels?.[0]; + const chColor = chKey ? channelColors[chKey] : CHANNEL_COLORS[0]; const marker = new naver.Marker({ position: new naver.LatLng(lat, lng), map: m, title: r.name, icon: { - content: markerIconHtml(), + content: markerIconHtml(chColor?.border ?? "#f59e0b"), anchor: new naver.Point(14, 14), }, }); @@ -252,7 +280,7 @@ export default function NaverMapView({ markersRef.current.push(marker); } } - }, [clusters, getExpansionZoom, onSelectRestaurant]); + }, [clusters, getExpansionZoom, onSelectRestaurant, channelColors, activeChannel]); // 컴포넌트 unmount 시 마커 정리 useEffect(() => {