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:
@@ -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 있으면 그 채널 우선
|
||||
|
||||
@@ -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(() => {
|
||||
|
||||
Reference in New Issue
Block a user