feat(map): NaverMapView 마커에 식당명 박스 (GoogleMap 동일)

- 단순 동그라미 → 박스+화살표+식당명+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 <noreply@anthropic.com>
This commit is contained in:
joungmin
2026-06-16 17:13:37 +09:00
parent 247547c516
commit 78f7e83a0e
2 changed files with 47 additions and 21 deletions

View File

@@ -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 있으면 그 채널 우선

View File

@@ -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 `<div style="width:28px;height:28px;border-radius:9999px;background:${color};border:2px solid #fff;box-shadow:0 2px 6px rgba(0,0,0,.25);"></div>`;
// 단일 마커 — 식당명 박스 + 화살표 핀 (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 `
<div style="display:flex;flex-direction:column;align-items:center;transition:transform .2s ease;transform:scale(${isSelected ? 1.15 : 1});opacity:${opacity};">
<div style="padding:4px 8px;background:${bg};color:${text};font-size:12px;font-weight:600;border-radius:6px;border:${border};box-shadow:${shadow};white-space:nowrap;max-width:120px;overflow:hidden;text-overflow:ellipsis;text-decoration:${deco};">
<span class="material-symbols-rounded" style="font-size:14px;width:14px;height:14px;overflow:hidden;display:inline-flex;align-items:center;justify-content:center;margin-right:3px;vertical-align:middle;color:#E8720C;">${escapeHtml(cuisineIcon)}</span>${escapeHtml(name)}
</div>
<div style="width:0;height:0;border-left:6px solid transparent;border-right:6px solid transparent;border-top:6px solid ${arrowColor};margin-top:-1px;"></div>
</div>
`;
}
// 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(
`<div style="background:#fff;border-radius:8px;padding:8px 12px;box-shadow:0 4px 12px rgba(0,0,0,.18);font-size:13px;font-weight:600;color:#111;white-space:nowrap;">${escapeHtml(r.name)}</div>`
);
iw.open(m, marker);
}
onSelectRestaurant?.(r);
});
markersRef.current.push(marker);
}
}
}, [clusters, getExpansionZoom, onSelectRestaurant, channelColors, activeChannel]);
}, [clusters, getExpansionZoom, onSelectRestaurant, channelColors, activeChannel, selected]);
// 컴포넌트 unmount 시 마커 정리
useEffect(() => {