From 78f7e83a0e3825db01d1638103d4752aa1e391e9 Mon Sep 17 00:00:00 2001 From: joungmin Date: Tue, 16 Jun 2026 17:13:37 +0900 Subject: [PATCH] =?UTF-8?q?feat(map):=20NaverMapView=20=EB=A7=88=EC=BB=A4?= =?UTF-8?q?=EC=97=90=20=EC=8B=9D=EB=8B=B9=EB=AA=85=20=EB=B0=95=EC=8A=A4=20?= =?UTF-8?q?(GoogleMap=20=EB=8F=99=EC=9D=BC)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 단순 동그라미 → 박스+화살표+식당명+cuisine 아이콘 핀 - 채널별 bg/text/border/arrow 색상 - 폐업(CLOSED_*) 회색 + 취소선, selected 강조 (1.15× + 파란박스) - InfoWindow 제거 (식당명이 박스로 보임) 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.7 --- CHANGELOG.md | 6 +++ frontend/src/components/NaverMapView.tsx | 62 ++++++++++++++++-------- 2 files changed, 47 insertions(+), 21 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2cb3ce6..faa31a9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,12 @@ ## 2026-06-16 +### 🏷️ NaverMapView 마커에 식당명 박스 (v0.1.61) +- 단순 동그라미 → GoogleMapView와 동일 핀 디자인(박스+화살표+식당명+cuisine 아이콘) +- 채널별 배경/테두리/화살표 색상, 폐업(business_status CLOSED_*) 표시 회색 + 취소선 +- selected 식당 강조 (1.15× scale + 파란 박스), zIndex 1000 +- InfoWindow 제거 (식당명 자체가 박스로 보이므로 불필요) + ### 🎨 NaverMapView 채널별 마커 색상 (v0.1.60) - GoogleMapView와 동일 팔레트 (amber/blue/green/pink/purple/red/teal/yellow) - 식당의 첫 채널 기준 색상, activeChannel 있으면 그 채널 우선 diff --git a/frontend/src/components/NaverMapView.tsx b/frontend/src/components/NaverMapView.tsx index c4d5aaa..96b3558 100644 --- a/frontend/src/components/NaverMapView.tsx +++ b/frontend/src/components/NaverMapView.tsx @@ -4,6 +4,7 @@ import { useCallback, useEffect, useMemo, useRef, useState } from "react"; import Supercluster from "supercluster"; import type { Restaurant } from "@/lib/api"; import Icon from "@/components/Icon"; +import { getCuisineIcon } from "@/lib/cuisine-icons"; import type { MapBounds, MapViewProps } from "@/components/MapView.types"; declare global { @@ -46,14 +47,14 @@ 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 + { bg: "#fff7ed", text: "#78350f", border: "#f59e0b", arrow: "#f59e0b" }, // amber (default) + { bg: "#eff6ff", text: "#1e3a5f", border: "#3b82f6", arrow: "#3b82f6" }, // blue + { bg: "#f0fdf4", text: "#14532d", border: "#22c55e", arrow: "#22c55e" }, // green + { bg: "#fdf2f8", text: "#831843", border: "#ec4899", arrow: "#ec4899" }, // pink + { bg: "#faf5ff", text: "#581c87", border: "#a855f7", arrow: "#a855f7" }, // purple + { bg: "#fff1f2", text: "#7f1d1d", border: "#ef4444", arrow: "#ef4444" }, // red + { bg: "#f0fdfa", text: "#134e4a", border: "#14b8a6", arrow: "#14b8a6" }, // teal + { bg: "#fefce8", text: "#713f12", border: "#eab308", arrow: "#eab308" }, // yellow ]; function getChannelColorMap(restaurants: Restaurant[]) { @@ -125,9 +126,29 @@ function getClusterSize(count: number): number { return 54; } -// 단일 마커 — 채널 색상별 -function markerIconHtml(color: string): string { - return `
`; +// 단일 마커 — 식당명 박스 + 화살표 핀 (GoogleMapView와 동일 디자인) +function markerIconHtml( + name: string, + cuisineIcon: string, + c: typeof CHANNEL_COLORS[0], + opts: { isSelected: boolean; isClosed: boolean } +): string { + const { isSelected, isClosed } = opts; + const bg = isSelected ? "#2563eb" : isClosed ? "#f3f4f6" : c.bg; + const text = isSelected ? "#fff" : isClosed ? "#9ca3af" : c.text; + const border = isSelected ? "2px solid #1d4ed8" : `1.5px solid ${c.border}`; + const shadow = isSelected ? "0 2px 8px rgba(37,99,235,0.4)" : `0 1px 4px ${c.border}40`; + const arrowColor = isSelected ? "#1d4ed8" : c.arrow; + const opacity = isClosed ? 0.5 : 1; + const deco = isClosed ? "line-through" : "none"; + return ` +
+
+ ${escapeHtml(cuisineIcon)}${escapeHtml(name)} +
+
+
+ `; } // SVG data URL — 클러스터(숫자) function clusterIconHtml(count: number, size: number): string { @@ -258,29 +279,28 @@ export default function NaverMapView({ 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 isSelected = selected?.id === r.id; + const isClosed = r.business_status === "CLOSED_PERMANENTLY" || r.business_status === "CLOSED_TEMPORARILY"; + const cuisineIcon = getCuisineIcon(r.cuisine_type); const marker = new naver.Marker({ position: new naver.LatLng(lat, lng), map: m, title: r.name, + zIndex: isSelected ? 1000 : 1, icon: { - content: markerIconHtml(chColor?.border ?? "#f59e0b"), - anchor: new naver.Point(14, 14), + content: markerIconHtml(r.name, cuisineIcon, chColor ?? CHANNEL_COLORS[0], { isSelected, isClosed }), + // 박스 폭 가변 — 화살표 끝(하단 중앙)이 좌표에 위치하도록 추정 anchor + // approxWidth = textLen * 7 + 30 (icon+padding), height = box 24 + arrow 6 = 30 + anchor: new naver.Point(Math.min(r.name.length * 4 + 18, 64), 30), }, }); naver.Event.addListener(marker, "click", () => { - const iw = infoWindowRef.current; - if (iw && m) { - iw.setContent( - `
${escapeHtml(r.name)}
` - ); - iw.open(m, marker); - } onSelectRestaurant?.(r); }); markersRef.current.push(marker); } } - }, [clusters, getExpansionZoom, onSelectRestaurant, channelColors, activeChannel]); + }, [clusters, getExpansionZoom, onSelectRestaurant, channelColors, activeChannel, selected]); // 컴포넌트 unmount 시 마커 정리 useEffect(() => {