Initial commit: 구마모토 여행 계획 사이트

This commit is contained in:
Hyungi Ahn
2025-11-09 09:11:06 +09:00
commit c07e1bc95e
25 changed files with 4623 additions and 0 deletions

146
src/components/Map.tsx Normal file
View File

@@ -0,0 +1,146 @@
import { useEffect, useRef } from 'react'
import { Attraction } from '../types'
interface MapProps {
attractions: Attraction[]
}
declare global {
interface Window {
google: typeof google
}
}
const Map = ({ attractions }: MapProps) => {
const mapRef = useRef<HTMLDivElement>(null)
const mapInstanceRef = useRef<google.maps.Map | null>(null)
const markersRef = useRef<google.maps.Marker[]>([])
useEffect(() => {
const apiKey = import.meta.env.VITE_GOOGLE_MAPS_API_KEY || ''
// Google Maps API 스크립트 로드
if (!window.google && apiKey) {
const script = document.createElement('script')
script.src = `https://maps.googleapis.com/maps/api/js?key=${apiKey}`
script.async = true
script.defer = true
script.onload = () => {
initMap()
}
script.onerror = () => {
console.error('Google Maps API 로드 실패')
}
document.head.appendChild(script)
} else if (window.google) {
initMap()
}
function initMap() {
if (!mapRef.current || !window.google) return
// 구마모토 중심 좌표
const kumamotoCenter = { lat: 32.8031, lng: 130.7079 }
// 지도 생성 (위성 지도)
const map = new window.google.maps.Map(mapRef.current, {
center: kumamotoCenter,
zoom: 10,
mapTypeId: window.google.maps.MapTypeId.SATELLITE, // 위성 지도
mapTypeControl: true,
mapTypeControlOptions: {
style: window.google.maps.MapTypeControlStyle.HORIZONTAL_BAR,
position: window.google.maps.ControlPosition.TOP_RIGHT,
mapTypeIds: [
window.google.maps.MapTypeId.SATELLITE,
window.google.maps.MapTypeId.ROADMAP,
window.google.maps.MapTypeId.HYBRID,
],
},
})
mapInstanceRef.current = map
// 기존 마커 제거
markersRef.current.forEach((marker) => {
marker.setMap(null)
})
markersRef.current = []
// 관광지 마커 추가
attractions.forEach((attraction) => {
if (attraction.coordinates) {
const marker = new window.google.maps.Marker({
position: {
lat: attraction.coordinates.lat,
lng: attraction.coordinates.lng,
},
map: map,
title: attraction.nameKo,
label: {
text: attraction.nameKo,
color: '#fff',
fontSize: '11px',
fontWeight: 'bold',
},
})
// 정보창 추가
const infoWindow = new window.google.maps.InfoWindow({
content: `
<div style="padding: 8px; max-width: 250px;">
<h3 style="margin: 0 0 8px 0; font-weight: bold; color: #333; font-size: 16px;">${attraction.nameKo}</h3>
<p style="margin: 0 0 4px 0; color: #666; font-size: 13px;">${attraction.description}</p>
<p style="margin: 0; color: #888; font-size: 12px;">📍 ${attraction.location}</p>
</div>
`,
})
marker.addListener('click', () => {
infoWindow.open(map, marker)
})
markersRef.current.push(marker)
}
})
}
return () => {
// cleanup
markersRef.current.forEach((marker) => {
marker.setMap(null)
})
markersRef.current = []
}
}, [attractions])
const apiKey = import.meta.env.VITE_GOOGLE_MAPS_API_KEY
return (
<div className="bg-white rounded-lg shadow-md p-6">
<h2 className="text-2xl font-bold text-gray-800 mb-6">🗺 </h2>
<div
ref={mapRef}
className="w-full h-96 rounded-lg overflow-hidden border border-gray-200"
style={{ minHeight: '400px' }}
/>
{!apiKey && (
<div className="mt-4 p-3 bg-blue-50 border border-blue-200 rounded text-sm text-blue-800">
💡 Google Maps API .
<br />
<span className="text-xs mt-1 block">
<strong> $200 </strong> !<br />
로드: 28,000 <br />
API : <a href="https://console.cloud.google.com/google/maps-apis" target="_blank" rel="noopener noreferrer" className="underline">Google Cloud Console</a><br />
<code className="px-1 bg-blue-100 rounded">.env</code> <code className="px-1 bg-blue-100 rounded">VITE_GOOGLE_MAPS_API_KEY=your_key</code>
</span>
</div>
)}
</div>
)
}
export default Map