Renommage : TrajetKmTracker -> SchotzIadKmTracker (backend, specs, frontend)

- Backend : projets, solution, namespaces et références
  TrajetKmTracker.Api/.Tests -> SchotzIadKmTracker.Api/.Tests
  (dossiers, .csproj, .sln, .http, namespaces C#). GUID UserSecretsId préservé.
- Specs : dossier .kiro/specs/trajet-km-tracker -> schotziad-km-tracker
  et mise à jour des références internes (chemins, tags de propriétés).
- Frontend : projet Angular trajet-km-tracker -> schotziad-km-tracker
  (package.json, angular.json, chemin dist, Dockerfile), titre applicatif
  'SchotzIAD KM Tracker'.
- Docker et scripts (backend/Dockerfile, frontend/Dockerfile, run-dev.sh) mis à jour.
- Suppression du fichier résiduel package.json.bak.

Backend (solution) et frontend (build production) compilent sans erreur.
This commit is contained in:
2026-09-14 07:50:26 +02:00
parent e3f946f7d4
commit 0e6a97ca61
133 changed files with 462 additions and 503 deletions
@@ -0,0 +1,133 @@
using System.Net.Http.Json;
using System.Text.Json;
using System.Text.Json.Serialization;
using SchotzIadKmTracker.Api.Models.DTOs;
using SchotzIadKmTracker.Api.Services.Exceptions;
namespace SchotzIadKmTracker.Api.Services;
/// <summary>
/// Client typé de l'API BAN. Appelle <c>GET /search/?q={query}&amp;limit=5</c>,
/// parse la réponse GeoJSON (FeatureCollection) et mappe les features en
/// <see cref="BANAdresseDto"/>. Lève <see cref="BANIndisponibleException"/>
/// en cas d'échec ou de timeout.
/// </summary>
public class BANClient : IBANClient
{
private const int MaxResultats = 5;
private static readonly JsonSerializerOptions JsonOptions = new()
{
PropertyNameCaseInsensitive = true
};
private readonly HttpClient _httpClient;
public BANClient(HttpClient httpClient)
{
_httpClient = httpClient;
}
public async Task<IReadOnlyList<BANAdresseDto>> RechercherAsync(
string query,
CancellationToken cancellationToken = default)
{
if (string.IsNullOrWhiteSpace(query))
{
return Array.Empty<BANAdresseDto>();
}
var requestUri = $"/search/?q={Uri.EscapeDataString(query)}&limit={MaxResultats}";
BANFeatureCollection? collection;
try
{
using var response = await _httpClient.GetAsync(requestUri, cancellationToken);
response.EnsureSuccessStatusCode();
collection = await response.Content.ReadFromJsonAsync<BANFeatureCollection>(
JsonOptions, cancellationToken);
}
catch (OperationCanceledException) when (!cancellationToken.IsCancellationRequested)
{
// Le timeout configuré sur le HttpClient déclenche une annulation
// non demandée par l'appelant.
throw new BANIndisponibleException(
"L'appel à l'API BAN a dépassé le délai imparti (timeout).");
}
catch (HttpRequestException ex)
{
throw new BANIndisponibleException(
"L'API BAN est indisponible ou a renvoyé une réponse en erreur.", ex);
}
catch (JsonException ex)
{
throw new BANIndisponibleException(
"La réponse de l'API BAN n'a pas pu être interprétée.", ex);
}
if (collection?.Features is null || collection.Features.Count == 0)
{
return Array.Empty<BANAdresseDto>();
}
var resultats = new List<BANAdresseDto>(collection.Features.Count);
foreach (var feature in collection.Features)
{
var coordinates = feature.Geometry?.Coordinates;
if (coordinates is null || coordinates.Count < 2)
{
continue;
}
// GeoJSON : coordinates = [longitude, latitude].
var longitude = coordinates[0];
var latitude = coordinates[1];
resultats.Add(new BANAdresseDto(
Label: feature.Properties?.Label ?? string.Empty,
Latitude: latitude,
Longitude: longitude,
Postcode: feature.Properties?.Postcode ?? string.Empty,
City: feature.Properties?.City ?? string.Empty));
}
return resultats;
}
// --- Modèles internes de désérialisation de la réponse GeoJSON BAN ---
private sealed class BANFeatureCollection
{
[JsonPropertyName("features")]
public List<BANFeature>? Features { get; set; }
}
private sealed class BANFeature
{
[JsonPropertyName("properties")]
public BANProperties? Properties { get; set; }
[JsonPropertyName("geometry")]
public BANGeometry? Geometry { get; set; }
}
private sealed class BANProperties
{
[JsonPropertyName("label")]
public string? Label { get; set; }
[JsonPropertyName("postcode")]
public string? Postcode { get; set; }
[JsonPropertyName("city")]
public string? City { get; set; }
}
private sealed class BANGeometry
{
// [longitude, latitude]
[JsonPropertyName("coordinates")]
public List<double>? Coordinates { get; set; }
}
}