Files
travel/src/components/Map.tsx

133 lines
4.6 KiB
TypeScript
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { useEffect, useState } from 'react'
import { MapContainer, TileLayer, Marker, Popup } from 'react-leaflet'
import { Icon } from 'leaflet'
import { Attraction } from '../types'
import 'leaflet/dist/leaflet.css'
interface MapProps {
attractions: Attraction[]
}
// 커스텀 마커 아이콘 설정
const attractionIcon = new Icon({
iconUrl: 'https://raw.githubusercontent.com/pointhi/leaflet-color-markers/master/img/marker-icon-2x-red.png',
shadowUrl: 'https://cdnjs.cloudflare.com/ajax/libs/leaflet/0.7.7/images/marker-shadow.png',
iconSize: [25, 41],
iconAnchor: [12, 41],
popupAnchor: [1, -34],
shadowSize: [41, 41]
})
const Map = ({ attractions }: MapProps) => {
const [isMounted, setIsMounted] = useState(false)
// 클라이언트 사이드에서만 렌더링 (SSR 에러 방지)
useEffect(() => {
setIsMounted(true)
}, [])
// 구마모토 중심 좌표
const kumamotoCenter: [number, number] = [32.8031, 130.7079]
// 타일 서버 URL (Docker 컨테이너 또는 기본 OSM)
const tilesUrl = import.meta.env.VITE_MAP_TILES_URL || 'https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png'
const isCustomTiles = !!import.meta.env.VITE_MAP_TILES_URL
// 서버 사이드에서는 로딩 화면 표시
if (!isMounted) {
return (
<div className="bg-white rounded-lg shadow-md p-6">
<h2 className="text-2xl font-bold text-gray-800 mb-6">🗺 </h2>
<div className="w-full h-96 rounded-lg overflow-hidden border border-gray-200 flex items-center justify-center bg-gray-100">
<p className="text-gray-500"> ...</p>
</div>
</div>
)
}
return (
<div className="bg-white rounded-lg shadow-md p-6">
<h2 className="text-2xl font-bold text-gray-800 mb-6">🗺 </h2>
<div className="w-full h-96 rounded-lg overflow-hidden border border-gray-200">
<MapContainer
center={kumamotoCenter}
zoom={10}
style={{ height: '100%', width: '100%' }}
className="rounded-lg"
>
<TileLayer
url={tilesUrl}
attribution={
isCustomTiles
? '&copy; 구마모토 여행 지도 | OpenStreetMap contributors'
: '&copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors'
}
maxZoom={18}
/>
{attractions.map((attraction) => (
attraction.coordinates && (
<Marker
key={attraction.id}
position={[attraction.coordinates.lat, attraction.coordinates.lng]}
icon={attractionIcon}
>
<Popup>
<div className="p-2 max-w-xs">
<h3 className="font-bold text-gray-800 mb-2 text-base">
{attraction.nameKo}
</h3>
<p className="text-gray-600 text-sm mb-2">
{attraction.description}
</p>
<p className="text-gray-500 text-xs">
📍 {attraction.location}
</p>
{attraction.website && (
<a
href={attraction.website}
target="_blank"
rel="noopener noreferrer"
className="text-blue-500 text-xs underline mt-1 block"
>
</a>
)}
</div>
</Popup>
</Marker>
)
))}
</MapContainer>
</div>
<div className="mt-4 p-3 bg-green-50 border border-green-200 rounded text-sm text-green-800">
{isCustomTiles ? (
<>
🎯 <strong> </strong> !
<br />
<span className="text-xs mt-1 block">
<br />
Google Maps API <br />
</span>
</>
) : (
<>
🌍 <strong>OpenStreetMap</strong>
<br />
<span className="text-xs mt-1 block">
<br />
API <br />
Docker
</span>
</>
)}
</div>
</div>
)
}
export default Map