"use client"; import { useCallback, useEffect, useMemo, useRef, useState } from "react"; import { APIProvider, Map, AdvancedMarker, InfoWindow, useMap, } from "@vis.gl/react-google-maps"; import type { Restaurant } from "@/lib/api"; import { getCuisineIcon } from "@/lib/cuisine-icons"; const SEOUL_CENTER = { lat: 37.5665, lng: 126.978 }; const API_KEY = process.env.NEXT_PUBLIC_GOOGLE_MAPS_API_KEY || ""; // Channel color palette const CHANNEL_COLORS = [ { 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[]) { 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; } export interface MapBounds { north: number; south: number; east: number; west: number; } export interface FlyTo { lat: number; lng: number; zoom?: number; } interface MapViewProps { restaurants: Restaurant[]; selected?: Restaurant | null; onSelectRestaurant?: (r: Restaurant) => void; onBoundsChanged?: (bounds: MapBounds) => void; flyTo?: FlyTo | null; } function MapContent({ restaurants, selected, onSelectRestaurant, onBoundsChanged, flyTo }: MapViewProps) { const map = useMap(); const [infoTarget, setInfoTarget] = useState(null); const channelColors = useMemo(() => getChannelColorMap(restaurants), [restaurants]); const boundsTimerRef = useRef | null>(null); // Report bounds on idle (debounced) useEffect(() => { if (!map) return; const listener = map.addListener("idle", () => { if (boundsTimerRef.current) clearTimeout(boundsTimerRef.current); boundsTimerRef.current = setTimeout(() => { const b = map.getBounds(); if (b && onBoundsChanged) { const ne = b.getNorthEast(); const sw = b.getSouthWest(); onBoundsChanged({ north: ne.lat(), south: sw.lat(), east: ne.lng(), west: sw.lng() }); } }, 300); }); return () => { google.maps.event.removeListener(listener); if (boundsTimerRef.current) clearTimeout(boundsTimerRef.current); }; }, [map, onBoundsChanged]); const handleMarkerClick = useCallback( (r: Restaurant) => { setInfoTarget(r); onSelectRestaurant?.(r); }, [onSelectRestaurant] ); // Fly to a specific location (region filter) useEffect(() => { if (!map || !flyTo) return; map.panTo({ lat: flyTo.lat, lng: flyTo.lng }); if (flyTo.zoom) map.setZoom(flyTo.zoom); }, [map, flyTo]); // Pan and zoom to selected restaurant useEffect(() => { if (!map || !selected) return; map.panTo({ lat: selected.latitude, lng: selected.longitude }); map.setZoom(16); setInfoTarget(selected); }, [map, selected]); return ( <> {restaurants.map((r) => { const isSelected = selected?.id === r.id; const isClosed = r.business_status === "CLOSED_PERMANENTLY"; const chColor = r.channels?.[0] ? channelColors[r.channels[0]] : CHANNEL_COLORS[0]; const c = chColor || CHANNEL_COLORS[0]; return ( handleMarkerClick(r)} zIndex={isSelected ? 1000 : 1} >
{getCuisineIcon(r.cuisine_type)} {r.name}
); })} {infoTarget && ( setInfoTarget(null)} >

{getCuisineIcon(infoTarget.cuisine_type)} {infoTarget.name}

{infoTarget.business_status === "CLOSED_PERMANENTLY" && ( 폐업 )} {infoTarget.business_status === "CLOSED_TEMPORARILY" && ( 임시휴업 )}
{infoTarget.rating && (

{infoTarget.rating} {infoTarget.rating_count && ( ({infoTarget.rating_count.toLocaleString()}) )}

)} {infoTarget.cuisine_type && (

{infoTarget.cuisine_type}

)} {infoTarget.address && (

{infoTarget.address}

)} {infoTarget.price_range && (

{infoTarget.price_range}

)} {infoTarget.phone && (

{infoTarget.phone}

)}
)} ); } export default function MapView({ restaurants, selected, onSelectRestaurant, onBoundsChanged, flyTo }: MapViewProps) { const channelColors = useMemo(() => getChannelColorMap(restaurants), [restaurants]); const channelNames = useMemo(() => Object.keys(channelColors), [channelColors]); return ( {channelNames.length > 1 && (
{channelNames.map((ch) => (
{ch}
))}
)}
); }