From a724b1bf3257d512e90ae2b4c71688fe74d4f3c5 Mon Sep 17 00:00:00 2001 From: hyungi Date: Sun, 9 Nov 2025 09:26:12 +0900 Subject: [PATCH] =?UTF-8?q?feat:=20=EA=B5=AC=EB=A7=88=EB=AA=A8=ED=86=A0=20?= =?UTF-8?q?=EC=A0=84=EC=9A=A9=20Docker=20=EC=A7=80=EB=8F=84=20=EC=84=9C?= =?UTF-8?q?=EB=B9=84=EC=8A=A4=20=EA=B5=AC=EC=B6=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Google Maps API에서 Leaflet + OpenStreetMap으로 전환 - 구마모토 지역 특화 타일 서버 Docker 컨테이너 추가 - PostgreSQL + PostGIS + Mapnik 스택으로 지도 타일 생성 - API 키 불필요한 오픈소스 지도 솔루션 구현 - 개발/프로덕션 환경 Docker Compose 설정 완료 - 빠른 로딩과 오프라인 지원 가능한 지도 서비스 주요 변경사항: - src/components/Map.tsx: Leaflet 기반으로 완전 재작성 - docker/map-server/: 구마모토 지역 타일 서버 구축 - docker-compose.yml, docker-compose.dev.yml: 지도 서버 연동 - package.json: leaflet, react-leaflet 의존성 추가 --- docker-compose.dev.yml | 24 ++ docker-compose.yml | 24 ++ docker/map-server/Dockerfile | 44 ++++ docker/map-server/config/apache-tiles.conf | 31 +++ docker/map-server/config/mapnik-style.xml | 179 ++++++++++++++ .../scripts/download-kumamoto-data.sh | 50 ++++ docker/map-server/scripts/setup-tiles.py | 114 +++++++++ docker/map-server/scripts/start-server.sh | 26 ++ package-lock.json | 51 +++- package.json | 6 +- src/components/Map.tsx | 224 ++++++++---------- 11 files changed, 641 insertions(+), 132 deletions(-) create mode 100644 docker/map-server/Dockerfile create mode 100644 docker/map-server/config/apache-tiles.conf create mode 100644 docker/map-server/config/mapnik-style.xml create mode 100644 docker/map-server/scripts/download-kumamoto-data.sh create mode 100644 docker/map-server/scripts/setup-tiles.py create mode 100644 docker/map-server/scripts/start-server.sh diff --git a/docker-compose.dev.yml b/docker-compose.dev.yml index 585a73d..d1ed09f 100644 --- a/docker-compose.dev.yml +++ b/docker-compose.dev.yml @@ -12,4 +12,28 @@ services: command: sh -c "npm install && npm run dev -- --host" container_name: kumamoto-travel-planner-dev restart: unless-stopped + depends_on: + - map-server + environment: + - VITE_MAP_TILES_URL=http://localhost:8080/tiles + + map-server: + build: + context: ./docker/map-server + dockerfile: Dockerfile + ports: + - "8080:80" + container_name: kumamoto-map-server-dev + restart: unless-stopped + volumes: + - map_data_dev:/var/lib/postgresql/data + - map_tiles_dev:/var/www/html/tiles + environment: + - POSTGRES_DB=kumamoto_map + - POSTGRES_USER=postgres + - POSTGRES_PASSWORD=mapserver123 + +volumes: + map_data_dev: + map_tiles_dev: diff --git a/docker-compose.yml b/docker-compose.yml index fae4a69..012c62a 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -9,4 +9,28 @@ services: - "3000:80" container_name: kumamoto-travel-planner restart: unless-stopped + depends_on: + - map-server + environment: + - VITE_MAP_TILES_URL=http://localhost:8080/tiles + + map-server: + build: + context: ./docker/map-server + dockerfile: Dockerfile + ports: + - "8080:80" + container_name: kumamoto-map-server + restart: unless-stopped + volumes: + - map_data:/var/lib/postgresql/data + - map_tiles:/var/www/html/tiles + environment: + - POSTGRES_DB=kumamoto_map + - POSTGRES_USER=postgres + - POSTGRES_PASSWORD=mapserver123 + +volumes: + map_data: + map_tiles: diff --git a/docker/map-server/Dockerfile b/docker/map-server/Dockerfile new file mode 100644 index 0000000..8a085d4 --- /dev/null +++ b/docker/map-server/Dockerfile @@ -0,0 +1,44 @@ +# 구마모토 지역 전용 타일 서버 +FROM ubuntu:22.04 + +# 필요한 패키지 설치 +RUN apt-get update && apt-get install -y \ + apache2 \ + python3 \ + python3-pip \ + wget \ + unzip \ + osmosis \ + postgresql \ + postgresql-contrib \ + postgis \ + postgresql-postgis-scripts \ + osm2pgsql \ + mapnik-utils \ + python3-mapnik \ + fonts-noto-cjk \ + && rm -rf /var/lib/apt/lists/* + +# 작업 디렉토리 설정 +WORKDIR /opt/map-server + +# 구마모토 지역 OSM 데이터 다운로드 스크립트 +COPY scripts/download-kumamoto-data.sh /opt/map-server/ +COPY scripts/setup-tiles.py /opt/map-server/ +COPY config/mapnik-style.xml /opt/map-server/ + +# 권한 설정 +RUN chmod +x /opt/map-server/download-kumamoto-data.sh + +# Apache 설정 +COPY config/apache-tiles.conf /etc/apache2/sites-available/tiles.conf +RUN a2ensite tiles && a2dissite 000-default + +# 포트 노출 +EXPOSE 80 + +# 시작 스크립트 +COPY scripts/start-server.sh /opt/map-server/ +RUN chmod +x /opt/map-server/start-server.sh + +CMD ["/opt/map-server/start-server.sh"] diff --git a/docker/map-server/config/apache-tiles.conf b/docker/map-server/config/apache-tiles.conf new file mode 100644 index 0000000..202e214 --- /dev/null +++ b/docker/map-server/config/apache-tiles.conf @@ -0,0 +1,31 @@ + + ServerName kumamoto-tiles + DocumentRoot /var/www/html + + # 타일 서빙을 위한 설정 + + Options Indexes FollowSymLinks + AllowOverride None + Require all granted + + # CORS 헤더 추가 (프론트엔드에서 접근 가능하도록) + Header always set Access-Control-Allow-Origin "*" + Header always set Access-Control-Allow-Methods "GET, OPTIONS" + Header always set Access-Control-Allow-Headers "Content-Type" + + + # 타일 캐싱 설정 + + ExpiresActive On + ExpiresDefault "access plus 7 days" + Header append Cache-Control "public" + + + # 타일 URL 리라이팅 + RewriteEngine On + RewriteRule ^/tiles/([0-9]+)/([0-9]+)/([0-9]+)\.png$ /tiles/$1/$2/$3.png [L] + + # 로그 설정 + ErrorLog ${APACHE_LOG_DIR}/tiles_error.log + CustomLog ${APACHE_LOG_DIR}/tiles_access.log combined + diff --git a/docker/map-server/config/mapnik-style.xml b/docker/map-server/config/mapnik-style.xml new file mode 100644 index 0000000..253140e --- /dev/null +++ b/docker/map-server/config/mapnik-style.xml @@ -0,0 +1,179 @@ + + + + + + localhost + 5432 + postgres + kumamoto_map + + + + + + + + + + + + + + + + + + + + + + + landuse + + postgis + localhost + 5432 + postgres + kumamoto_map + (SELECT way, landuse, natural, leisure FROM planet_osm_polygon WHERE landuse IS NOT NULL OR natural IS NOT NULL OR leisure IS NOT NULL) as landuse + way + 3857 + + + + + water + + postgis + localhost + 5432 + postgres + kumamoto_map + (SELECT way, natural, waterway FROM planet_osm_polygon WHERE natural = 'water' UNION SELECT way, natural, waterway FROM planet_osm_line WHERE waterway IN ('river', 'stream')) as water + way + 3857 + + + + + buildings + + postgis + localhost + 5432 + postgres + kumamoto_map + (SELECT way, building FROM planet_osm_polygon WHERE building IS NOT NULL) as buildings + way + 3857 + + + + + roads + + postgis + localhost + 5432 + postgres + kumamoto_map + (SELECT way, highway FROM planet_osm_line WHERE highway IS NOT NULL) as roads + way + 3857 + + + + + tourism + + postgis + localhost + 5432 + postgres + kumamoto_map + (SELECT way, tourism FROM planet_osm_point WHERE tourism IS NOT NULL) as tourism + way + 3857 + + + + + place_labels + + postgis + localhost + 5432 + postgres + kumamoto_map + (SELECT way, name, place FROM planet_osm_point WHERE place IN ('city', 'town') AND name IS NOT NULL) as places + way + 3857 + + + + diff --git a/docker/map-server/scripts/download-kumamoto-data.sh b/docker/map-server/scripts/download-kumamoto-data.sh new file mode 100644 index 0000000..46425a9 --- /dev/null +++ b/docker/map-server/scripts/download-kumamoto-data.sh @@ -0,0 +1,50 @@ +#!/bin/bash + +# 구마모토 지역 OSM 데이터 다운로드 및 설정 + +echo "구마모토 지역 지도 데이터 다운로드 중..." + +# 구마모토 지역 경계 (대략적인 좌표) +# 북: 33.2, 남: 32.4, 동: 131.2, 서: 130.2 +NORTH=33.2 +SOUTH=32.4 +EAST=131.2 +WEST=130.2 + +# 구마모토 지역 OSM 데이터 다운로드 +wget -O /tmp/kumamoto.osm.pbf \ + "https://overpass-api.de/api/interpreter?data=[out:pbf][timeout:300];(relation[admin_level=4][name~'熊本'][name~'Kumamoto'];>;);out meta;" + +# 대안: 일본 전체에서 구마모토 지역 추출 +if [ ! -f /tmp/kumamoto.osm.pbf ] || [ ! -s /tmp/kumamoto.osm.pbf ]; then + echo "Overpass API에서 다운로드 실패, 대안 방법 사용..." + + # 일본 큐슈 지역 데이터 다운로드 (더 안정적) + wget -O /tmp/kyushu.osm.pbf \ + "http://download.geofabrik.de/asia/japan/kyushu-latest.osm.pbf" + + # osmosis를 사용해 구마모토 지역만 추출 + osmosis --read-pbf /tmp/kyushu.osm.pbf \ + --bounding-box top=$NORTH left=$WEST bottom=$SOUTH right=$EAST \ + --write-pbf /tmp/kumamoto.osm.pbf +fi + +echo "구마모토 지역 데이터 다운로드 완료!" + +# PostgreSQL 데이터베이스 설정 +echo "데이터베이스 설정 중..." + +# PostgreSQL 시작 +service postgresql start + +# 데이터베이스 생성 +sudo -u postgres createdb kumamoto_map +sudo -u postgres psql -d kumamoto_map -c "CREATE EXTENSION postgis;" +sudo -u postgres psql -d kumamoto_map -c "CREATE EXTENSION hstore;" + +# OSM 데이터를 PostgreSQL에 임포트 +echo "OSM 데이터 임포트 중..." +sudo -u postgres osm2pgsql -d kumamoto_map -c /tmp/kumamoto.osm.pbf \ + --hstore --style /usr/share/osm2pgsql/default.style --multi-geometry + +echo "구마모토 지도 데이터 설정 완료!" diff --git a/docker/map-server/scripts/setup-tiles.py b/docker/map-server/scripts/setup-tiles.py new file mode 100644 index 0000000..e1520dc --- /dev/null +++ b/docker/map-server/scripts/setup-tiles.py @@ -0,0 +1,114 @@ +#!/usr/bin/env python3 +""" +구마모토 지역 타일 생성 스크립트 +""" + +import os +import math +import mapnik +import psycopg2 +from PIL import Image + +# 구마모토 지역 경계 +KUMAMOTO_BOUNDS = { + 'north': 33.2, + 'south': 32.4, + 'east': 131.2, + 'west': 130.2 +} + +# 타일 크기 +TILE_SIZE = 256 + +def deg2num(lat_deg, lon_deg, zoom): + """위경도를 타일 번호로 변환""" + lat_rad = math.radians(lat_deg) + n = 2.0 ** zoom + xtile = int((lon_deg + 180.0) / 360.0 * n) + ytile = int((1.0 - math.asinh(math.tan(lat_rad)) / math.pi) / 2.0 * n) + return (xtile, ytile) + +def num2deg(xtile, ytile, zoom): + """타일 번호를 위경도로 변환""" + n = 2.0 ** zoom + lon_deg = xtile / n * 360.0 - 180.0 + lat_rad = math.atan(math.sinh(math.pi * (1 - 2 * ytile / n))) + lat_deg = math.degrees(lat_rad) + return (lat_deg, lon_deg) + +def create_mapnik_map(): + """Mapnik 지도 객체 생성""" + m = mapnik.Map(TILE_SIZE, TILE_SIZE) + + # 한국어/일본어 폰트 설정 + mapnik.FontEngine.register_fonts('/usr/share/fonts/truetype/noto/') + + # 스타일 로드 + mapnik.load_map(m, '/opt/map-server/mapnik-style.xml') + + return m + +def generate_tile(m, z, x, y): + """개별 타일 생성""" + # 타일 경계 계산 + lat_north, lon_west = num2deg(x, y, z) + lat_south, lon_east = num2deg(x + 1, y + 1, z) + + # 지도 범위 설정 + bbox = mapnik.Box2d(lon_west, lat_south, lon_east, lat_north) + m.zoom_to_box(bbox) + + # 타일 렌더링 + im = mapnik.Image(TILE_SIZE, TILE_SIZE) + mapnik.render(m, im) + + return im.tostring('png') + +def generate_kumamoto_tiles(): + """구마모토 지역 타일 생성""" + print("구마모토 지역 타일 생성 시작...") + + # Mapnik 지도 생성 + m = create_mapnik_map() + + # 줌 레벨별 타일 생성 (8-16레벨) + for zoom in range(8, 17): + print(f"줌 레벨 {zoom} 타일 생성 중...") + + # 구마모토 지역의 타일 범위 계산 + x_min, y_max = deg2num(KUMAMOTO_BOUNDS['north'], KUMAMOTO_BOUNDS['west'], zoom) + x_max, y_min = deg2num(KUMAMOTO_BOUNDS['south'], KUMAMOTO_BOUNDS['east'], zoom) + + tile_count = 0 + for x in range(x_min, x_max + 1): + for y in range(y_min, y_max + 1): + # 타일 디렉토리 생성 + tile_dir = f"/var/www/html/tiles/{zoom}/{x}" + os.makedirs(tile_dir, exist_ok=True) + + # 타일 파일 경로 + tile_path = f"{tile_dir}/{y}.png" + + # 이미 존재하는 타일은 건너뛰기 + if os.path.exists(tile_path): + continue + + try: + # 타일 생성 + tile_data = generate_tile(m, zoom, x, y) + + # 타일 저장 + with open(tile_path, 'wb') as f: + f.write(tile_data) + + tile_count += 1 + + except Exception as e: + print(f"타일 생성 실패 ({zoom}/{x}/{y}): {e}") + + print(f"줌 레벨 {zoom}: {tile_count}개 타일 생성 완료") + + print("구마모토 지역 타일 생성 완료!") + +if __name__ == "__main__": + generate_kumamoto_tiles() diff --git a/docker/map-server/scripts/start-server.sh b/docker/map-server/scripts/start-server.sh new file mode 100644 index 0000000..6ee3062 --- /dev/null +++ b/docker/map-server/scripts/start-server.sh @@ -0,0 +1,26 @@ +#!/bin/bash + +echo "구마모토 지도 서버 시작 중..." + +# PostgreSQL 시작 +service postgresql start + +# 데이터베이스가 없으면 초기 설정 실행 +if ! sudo -u postgres psql -lqt | cut -d \| -f 1 | grep -qw kumamoto_map; then + echo "초기 데이터베이스 설정 실행 중..." + /opt/map-server/download-kumamoto-data.sh + + echo "타일 생성 중..." + python3 /opt/map-server/setup-tiles.py +else + echo "기존 데이터베이스 사용" +fi + +# Apache 모듈 활성화 +a2enmod rewrite +a2enmod headers +a2enmod expires + +# Apache 시작 +echo "Apache 웹서버 시작..." +apache2ctl -D FOREGROUND diff --git a/package-lock.json b/package-lock.json index 3626560..02d3572 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,9 +8,12 @@ "name": "kumamoto-travel-planner", "version": "1.0.0", "dependencies": { + "@types/leaflet": "^1.9.21", "date-fns": "^2.30.0", + "leaflet": "^1.9.4", "react": "^18.2.0", - "react-dom": "^18.2.0" + "react-dom": "^18.2.0", + "react-leaflet": "^4.2.1" }, "devDependencies": { "@types/react": "^18.2.43", @@ -835,6 +838,17 @@ "node": ">=14" } }, + "node_modules/@react-leaflet/core": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@react-leaflet/core/-/core-2.1.0.tgz", + "integrity": "sha512-Qk7Pfu8BSarKGqILj4x7bCSZ1pjuAPZ+qmRwH5S7mDS91VSbVVsJSrW4qA+GPrro8t69gFYVMWb1Zc4yFmPiVg==", + "license": "Hippocratic-2.1", + "peerDependencies": { + "leaflet": "^1.9.0", + "react": "^18.0.0", + "react-dom": "^18.0.0" + } + }, "node_modules/@rolldown/pluginutils": { "version": "1.0.0-beta.27", "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.27.tgz", @@ -1202,6 +1216,21 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/geojson": { + "version": "7946.0.16", + "resolved": "https://registry.npmjs.org/@types/geojson/-/geojson-7946.0.16.tgz", + "integrity": "sha512-6C8nqWur3j98U6+lXDfTUWIfgvZU+EumvpHKcYjujKH7woYyLj2sUmff0tRhrqM7BohUw7Pz3ZB1jj2gW9Fvmg==", + "license": "MIT" + }, + "node_modules/@types/leaflet": { + "version": "1.9.21", + "resolved": "https://registry.npmjs.org/@types/leaflet/-/leaflet-1.9.21.tgz", + "integrity": "sha512-TbAd9DaPGSnzp6QvtYngntMZgcRk+igFELwR2N99XZn7RXUdKgsXMR+28bUO0rPsWp8MIu/f47luLIQuSLYv/w==", + "license": "MIT", + "dependencies": { + "@types/geojson": "*" + } + }, "node_modules/@types/prop-types": { "version": "15.7.15", "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.15.tgz", @@ -1992,6 +2021,12 @@ "node": ">=6" } }, + "node_modules/leaflet": { + "version": "1.9.4", + "resolved": "https://registry.npmjs.org/leaflet/-/leaflet-1.9.4.tgz", + "integrity": "sha512-nxS1ynzJOmOlHp+iL3FyWqK89GtNL8U8rvlMOsQdTTssxZwCXh8N2NB3GDQOL+YR3XnWyZAxwQixURb+FA74PA==", + "license": "BSD-2-Clause" + }, "node_modules/lilconfig": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-3.1.3.tgz", @@ -2466,6 +2501,20 @@ "react": "^18.3.1" } }, + "node_modules/react-leaflet": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/react-leaflet/-/react-leaflet-4.2.1.tgz", + "integrity": "sha512-p9chkvhcKrWn/H/1FFeVSqLdReGwn2qmiobOQGO3BifX+/vV/39qhY8dGqbdcPh1e6jxh/QHriLXr7a4eLFK4Q==", + "license": "Hippocratic-2.1", + "dependencies": { + "@react-leaflet/core": "^2.1.0" + }, + "peerDependencies": { + "leaflet": "^1.9.0", + "react": "^18.0.0", + "react-dom": "^18.0.0" + } + }, "node_modules/react-refresh": { "version": "0.17.0", "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.17.0.tgz", diff --git a/package.json b/package.json index ace3a95..4867e42 100644 --- a/package.json +++ b/package.json @@ -9,9 +9,12 @@ "preview": "vite preview" }, "dependencies": { + "@types/leaflet": "^1.9.21", + "date-fns": "^2.30.0", + "leaflet": "^1.9.4", "react": "^18.2.0", "react-dom": "^18.2.0", - "date-fns": "^2.30.0" + "react-leaflet": "^4.2.1" }, "devDependencies": { "@types/react": "^18.2.43", @@ -24,4 +27,3 @@ "vite": "^5.0.8" } } - diff --git a/src/components/Map.tsx b/src/components/Map.tsx index 3b7fe1d..7b9a2c2 100644 --- a/src/components/Map.tsx +++ b/src/components/Map.tsx @@ -1,144 +1,110 @@ -import { useEffect, useRef } 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[] } -declare global { - interface Window { - google: typeof google - } -} +// 커스텀 마커 아이콘 설정 +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 mapRef = useRef(null) - const mapInstanceRef = useRef(null) - const markersRef = useRef([]) - - 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: ` -
-

${attraction.nameKo}

-

${attraction.description}

-

📍 ${attraction.location}

-
- `, - }) - - 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 + // 구마모토 중심 좌표 + 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 return (

🗺️ 구마모토 지도

-
- {!apiKey && ( -
- 💡 Google Maps API 키가 설정되지 않았습니다. -
- - • 가족 여행용으로 사용하시면 월 $200 무료 크레딧으로 충분합니다!
- • 지도 로드: 월 28,000회까지 무료
- • API 키 발급: Google Cloud Console
- • .env 파일에 VITE_GOOGLE_MAPS_API_KEY=your_key 추가 -
-
- )} + +
+ + OpenStreetMap contributors' + } + maxZoom={18} + /> + + {attractions.map((attraction) => ( + attraction.coordinates && ( + + +
+

+ {attraction.nameKo} +

+

+ {attraction.description} +

+

+ 📍 {attraction.location} +

+ {attraction.website && ( + + 웹사이트 방문 + + )} +
+
+
+ ) + ))} +
+
+ +
+ {isCustomTiles ? ( + <> + 🎯 구마모토 전용 지도 서버 사용 중! +
+ + • 빠른 로딩 속도와 오프라인 지원
+ • Google Maps API 키 불필요
+ • 구마모토 지역 최적화된 지도 데이터 +
+ + ) : ( + <> + 🌍 OpenStreetMap 사용 중 +
+ + • 무료 오픈소스 지도 서비스
+ • API 키 불필요
+ • Docker 지도 서버 실행 시 더 빠른 성능 제공 +
+ + )} +
) }