Initial commit: Tasteby - YouTube restaurant map service
Backend (FastAPI + Oracle ADB), Frontend (Next.js), daemon worker. Features: channel/video/restaurant management, semantic search, Google OAuth, user reviews. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
75
frontend/src/components/MapView.tsx
Normal file
75
frontend/src/components/MapView.tsx
Normal file
@@ -0,0 +1,75 @@
|
||||
"use client";
|
||||
|
||||
import { useCallback, useState } from "react";
|
||||
import {
|
||||
APIProvider,
|
||||
Map,
|
||||
AdvancedMarker,
|
||||
InfoWindow,
|
||||
} from "@vis.gl/react-google-maps";
|
||||
import type { Restaurant } from "@/lib/api";
|
||||
|
||||
const SEOUL_CENTER = { lat: 37.5665, lng: 126.978 };
|
||||
const API_KEY = process.env.NEXT_PUBLIC_GOOGLE_MAPS_API_KEY || "";
|
||||
|
||||
interface MapViewProps {
|
||||
restaurants: Restaurant[];
|
||||
onSelectRestaurant?: (r: Restaurant) => void;
|
||||
}
|
||||
|
||||
export default function MapView({ restaurants, onSelectRestaurant }: MapViewProps) {
|
||||
const [selected, setSelected] = useState<Restaurant | null>(null);
|
||||
|
||||
const handleMarkerClick = useCallback(
|
||||
(r: Restaurant) => {
|
||||
setSelected(r);
|
||||
onSelectRestaurant?.(r);
|
||||
},
|
||||
[onSelectRestaurant]
|
||||
);
|
||||
|
||||
return (
|
||||
<APIProvider apiKey={API_KEY}>
|
||||
<Map
|
||||
defaultCenter={SEOUL_CENTER}
|
||||
defaultZoom={12}
|
||||
mapId="tasteby-map"
|
||||
className="h-full w-full"
|
||||
>
|
||||
{restaurants.map((r) => (
|
||||
<AdvancedMarker
|
||||
key={r.id}
|
||||
position={{ lat: r.latitude, lng: r.longitude }}
|
||||
onClick={() => handleMarkerClick(r)}
|
||||
/>
|
||||
))}
|
||||
|
||||
{selected && (
|
||||
<InfoWindow
|
||||
position={{ lat: selected.latitude, lng: selected.longitude }}
|
||||
onCloseClick={() => setSelected(null)}
|
||||
>
|
||||
<div className="max-w-xs p-1">
|
||||
<h3 className="font-bold text-base">{selected.name}</h3>
|
||||
{selected.cuisine_type && (
|
||||
<p className="text-sm text-gray-600">{selected.cuisine_type}</p>
|
||||
)}
|
||||
{selected.address && (
|
||||
<p className="text-xs text-gray-500 mt-1">{selected.address}</p>
|
||||
)}
|
||||
{selected.price_range && (
|
||||
<p className="text-xs text-gray-500">{selected.price_range}</p>
|
||||
)}
|
||||
<button
|
||||
onClick={() => onSelectRestaurant?.(selected)}
|
||||
className="mt-2 text-sm text-blue-600 hover:underline"
|
||||
>
|
||||
상세 보기
|
||||
</button>
|
||||
</div>
|
||||
</InfoWindow>
|
||||
)}
|
||||
</Map>
|
||||
</APIProvider>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user