perf: spots/overview — 100+ requêtes individuelles → 1 requête cachée 30 min
Avant : SargassesMap faisait 1 appel /api/spots (API Platform) puis
N appels /api/spots/{id}/score (un par spot, ~100+ pour la Guadeloupe).
Après : un seul GET /api/spots/overview (LATERAL JOIN spots+scores)
avec Cache-Control: public, max-age=1800, stale-while-revalidate=300.
Les marqueurs colorés s'affichent immédiatement depuis la réponse unique ;
/api/spots/{id}/score n'est appelé qu'au clic sur un spot (SpotPanel).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -20,6 +20,46 @@ class SpotScoreController extends AbstractController
|
||||
private Connection $connection,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* GET /api/spots/overview
|
||||
*
|
||||
* Retourne tous les spots avec leur dernier score en UNE seule requête.
|
||||
* Remplace les N appels individuels à /api/spots/{id}/score pour le rendu
|
||||
* initial de la carte.
|
||||
*
|
||||
* Cache HTTP 30 min (scores mis à jour au max 1-2×/jour par ingestion).
|
||||
*/
|
||||
#[Route('/overview', name: 'overview', methods: ['GET'])]
|
||||
public function overview(): JsonResponse
|
||||
{
|
||||
$rows = $this->connection->fetchAllAssociative(
|
||||
"SELECT
|
||||
cp.id::text,
|
||||
cp.name,
|
||||
cp.type,
|
||||
cp.region,
|
||||
ST_X(cp.geometry::geometry) AS longitude,
|
||||
ST_Y(cp.geometry::geometry) AS latitude,
|
||||
ist.score,
|
||||
ist.level,
|
||||
ROUND(CAST(ist.distance_to_nearest_sargassum AS numeric), 2) AS distance_km
|
||||
FROM coastal_point cp
|
||||
LEFT JOIN LATERAL (
|
||||
SELECT score, level, distance_to_nearest_sargassum
|
||||
FROM impact_score
|
||||
WHERE coastal_point_id = cp.id
|
||||
ORDER BY timestamp DESC
|
||||
LIMIT 1
|
||||
) ist ON true
|
||||
ORDER BY cp.name"
|
||||
);
|
||||
|
||||
$response = $this->json($rows);
|
||||
$response->headers->set('Cache-Control', 'public, max-age=1800, stale-while-revalidate=300');
|
||||
|
||||
return $response;
|
||||
}
|
||||
|
||||
/**
|
||||
* GET /api/spots/{id}/score
|
||||
*
|
||||
|
||||
@@ -25,7 +25,7 @@ async function post(path, body) {
|
||||
|
||||
export const api = {
|
||||
spots: {
|
||||
list: () => get('/spots'),
|
||||
overview: () => get('/spots/overview'), // tous les spots + score en 1 requête cachée
|
||||
get: (id) => get(`/spots/${id}`),
|
||||
score: (id, at) => get(`/spots/${id}/score`, { at }),
|
||||
},
|
||||
|
||||
@@ -179,8 +179,9 @@ function SpotMarker({ level, scoreValue, scoreDist, isSelected, name }) {
|
||||
|
||||
export default function SargassesMap({ onSpotSelect, selectedSpotId, activeStep, flyToTarget }) {
|
||||
const mapRef = useRef(null);
|
||||
// spots : [{ id, name, type, region, longitude, latitude, score, level, distance_km }]
|
||||
const [spots, setSpots] = useState([]);
|
||||
const [spotScores, setSpotScores] = useState({});
|
||||
const [spotScores, setSpotScores] = useState({}); // id → { level, value, distance }
|
||||
const [viewState, setViewState] = useState(INITIAL_VIEW);
|
||||
const [layerOpacity, setLayerOpacity] = useState(1);
|
||||
const [mapStyle, setMapStyle] = useState(null); // null = style not yet ready
|
||||
@@ -212,28 +213,28 @@ export default function SargassesMap({ onSpotSelect, selectedSpotId, activeStep,
|
||||
return () => ctrl.abort();
|
||||
}, []);
|
||||
|
||||
// Load coastal spots once
|
||||
// Charge tous les spots + scores en UNE requête (cachée 30 min côté serveur)
|
||||
useEffect(() => {
|
||||
api.spots.list()
|
||||
.then(data => setSpots(Array.isArray(data) ? data : (data['hydra:member'] ?? data.member ?? [])))
|
||||
api.spots.overview()
|
||||
.then(rows => {
|
||||
if (!Array.isArray(rows)) return;
|
||||
setSpots(rows);
|
||||
// Pré-remplir spotScores depuis la même réponse — aucune requête supplémentaire
|
||||
const scores = {};
|
||||
rows.forEach(r => {
|
||||
if (r.level) {
|
||||
scores[r.id] = {
|
||||
level: r.level,
|
||||
value: r.score ?? 0,
|
||||
distance: r.distance_km ?? null,
|
||||
};
|
||||
}
|
||||
});
|
||||
setSpotScores(scores);
|
||||
})
|
||||
.catch(console.error);
|
||||
}, []);
|
||||
|
||||
// Load scores progressively for marker colors
|
||||
useEffect(() => {
|
||||
if (!spots.length) return;
|
||||
spots.forEach(s => {
|
||||
api.spots.score(s.id)
|
||||
.then(d => {
|
||||
const level = d?.score?.level;
|
||||
const value = d?.score?.value ?? 0;
|
||||
const distance = d?.score?.distance ?? null;
|
||||
if (level) setSpotScores(prev => ({ ...prev, [s.id]: { level, value, distance } }));
|
||||
})
|
||||
.catch(() => {});
|
||||
});
|
||||
}, [spots]);
|
||||
|
||||
// Fade transition on step change
|
||||
useEffect(() => {
|
||||
setLayerOpacity(0);
|
||||
|
||||
Reference in New Issue
Block a user