133 lines
4.6 KiB
TypeScript
133 lines
4.6 KiB
TypeScript
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
|
||
? '© 구마모토 여행 지도 | OpenStreetMap contributors'
|
||
: '© <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
|