Эх сурвалжийг харах

feat: backend proxy /geocode/reverse для Nominatim, MapPicker идёт через API

neyrogovnarik 1 сар өмнө
parent
commit
b80d91ca06

+ 1 - 0
backend/cmd/api/main.go

@@ -194,6 +194,7 @@ func main() {
 			r.Get("/places", placeHandler.List)
 			r.Get("/places/{id}", placeHandler.GetByID)
 			r.Get("/users/{id}", userHandler.GetProfile)
+			r.Get("/geocode/reverse", handlers.GeocodeReverse)
 		})
 
 		// WebSocket visitor dots

+ 63 - 0
backend/internal/handlers/geocode.go

@@ -0,0 +1,63 @@
+package handlers
+
+import (
+	"encoding/json"
+	"io"
+	"net/http"
+	"net/url"
+	"time"
+)
+
+const nominatimURL = "https://nominatim.openstreetmap.org/reverse"
+
+var geocodeClient = &http.Client{Timeout: 5 * time.Second}
+
+type nominatimResponse struct {
+	DisplayName string `json:"display_name"`
+}
+
+func GeocodeReverse(w http.ResponseWriter, r *http.Request) {
+	lat := r.URL.Query().Get("lat")
+	lon := r.URL.Query().Get("lon")
+	if lat == "" || lon == "" {
+		writeError(w, http.StatusBadRequest, "lat and lon are required", nil)
+		return
+	}
+
+	u, _ := url.Parse(nominatimURL)
+	q := u.Query()
+	q.Set("lat", lat)
+	q.Set("lon", lon)
+	q.Set("format", "json")
+	q.Set("accept-language", "ru")
+	u.RawQuery = q.Encode()
+
+	req, _ := http.NewRequestWithContext(r.Context(), http.MethodGet, u.String(), nil)
+	req.Header.Set("User-Agent", "Photoplaces/1.0")
+
+	resp, err := geocodeClient.Do(req)
+	if err != nil {
+		writeError(w, http.StatusBadGateway, "geocoding service unavailable", err)
+		return
+	}
+	defer resp.Body.Close()
+
+	if resp.StatusCode != http.StatusOK {
+		body, _ := io.ReadAll(resp.Body)
+		writeJSON(w, http.StatusOK, map[string]interface{}{
+			"display_name": nil,
+			"raw_error":    string(body),
+		})
+		return
+	}
+
+	var data nominatimResponse
+	if err := json.NewDecoder(resp.Body).Decode(&data); err != nil {
+		writeError(w, http.StatusInternalServerError, "failed to parse geocoding response", err)
+		return
+	}
+
+	writeJSON(w, http.StatusOK, map[string]interface{}{
+		"display_name": data.DisplayName,
+	})
+}

+ 3 - 5
frontend/src/components/MapPicker.tsx

@@ -3,6 +3,7 @@
 import { useEffect, useRef, useState } from 'react'
 import L from 'leaflet'
 import 'leaflet/dist/leaflet.css'
+import { api } from '@/lib/api'
 
 interface MapPickerProps {
   onSelect: (lat: number, lng: number, address?: string) => void
@@ -58,12 +59,9 @@ export default function MapPicker({ onSelect, onClose }: MapPickerProps) {
   const handleConfirm = async () => {
     setLoading(true)
     try {
-      const res = await fetch(
-        `https://nominatim.openstreetmap.org/reverse?lat=${lat}&lon=${lng}&format=json&accept-language=ru`,
-        { headers: { 'User-Agent': 'Photoplaces/1.0' } }
+      const data = await api.get<{ display_name: string | null }>(
+        `/geocode/reverse?lat=${lat}&lon=${lng}`
       )
-      if (!res.ok) { onSelect(lat, lng); return }
-      const data = await res.json()
       onSelect(lat, lng, data.display_name || undefined)
     } catch {
       onSelect(lat, lng)