Distinguer scénario, dessin et couleurs sur chaque livre

Le rôle vit sur le LIEN livre-auteur, jamais sur l'auteur : le même auteur
scénarise un album et dessine le suivant, et il ne doit pas exister en deux
fiches pour autant — ce que l'index unique sur la clé de regroupement
interdit de toute façon.

Les rôles se retrouvent par clé d'auteur résolue et non par position :
ResoudreAsync écarte les noms vides et fusionne les variantes, donc sa
liste peut être plus courte que la saisie.

Piège traité : le champ des noms reconstruit la liste à chaque frappe.
Decouper reprend les rôles connus en rapprochant sur la forme normalisée,
sans quoi corriger une faute de frappe les effacerait tous.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
mathieu
2026-08-19 22:37:20 +02:00
co-authored by Claude Opus 5
parent 9ab6f23b8c
commit 07cb48b2be
16 changed files with 895 additions and 22 deletions
+45 -11
View File
@@ -1,5 +1,6 @@
using System.Text.Json.Serialization;
using MaBibli.Shared.Entites;
using MaBibli.Shared.Textes;
namespace MaBibli.Shared.Dtos;
@@ -25,10 +26,10 @@ public record EnregistrementLivre
public string Titre { get; set; } = string.Empty;
/// <summary>
/// Noms des auteurs, dans l'ordre de la couverture. Le serveur les rattache aux fiches
/// existantes quand c'est sûr, et en crée sinon.
/// Auteurs, dans l'ordre de la couverture, chacun avec son rôle sur <b>ce</b> livre. Le
/// serveur les rattache aux fiches existantes quand c'est sûr, et en crée sinon.
/// </summary>
public List<string> Auteurs { get; set; } = [];
public List<AuteurSaisi> Auteurs { get; set; } = [];
public string? Editeur { get; set; }
@@ -57,18 +58,51 @@ public record EnregistrementLivre
[JsonIgnore]
public string? Auteur
{
get => Auteurs.Count == 0 ? null : string.Join($"{SeparateurAuteurs} ", Auteurs);
set => Auteurs = Decouper(value);
get => Auteurs.Count == 0 ? null : string.Join($"{SeparateurAuteurs} ", Auteurs.Select(a => a.Nom));
set => Auteurs = Decouper(value, Auteurs);
}
/// <summary>Découpe une saisie texte en noms d'auteurs, en écartant les entrées vides.</summary>
public static List<string> Decouper(string? saisie) =>
string.IsNullOrWhiteSpace(saisie)
? []
: saisie.Split(SeparateurAuteurs, StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries)
.ToList();
/// <summary>
/// Découpe une saisie texte en auteurs, en écartant les entrées vides.
/// </summary>
/// <param name="connus">
/// Auteurs déjà saisis, dont les <b>rôles</b> sont repris quand le nom réapparaît.
/// </param>
/// <remarks>
/// ⚠️ Sans cette reprise, corriger une faute de frappe dans le champ des noms effacerait le
/// rôle des auteurs voisins : le champ se relie à chaque frappe, et il reconstruirait la
/// liste entière à « non précisé ». Le rapprochement se fait sur la forme normalisée du nom,
/// pour survivre à un accent ajouté après coup.
/// </remarks>
public static List<AuteurSaisi> Decouper(string? saisie, IEnumerable<AuteurSaisi>? connus = null)
{
if (string.IsNullOrWhiteSpace(saisie))
{
return [];
}
var roles = new Dictionary<string, RoleAuteur>(StringComparer.Ordinal);
foreach (var connu in connus ?? [])
{
roles.TryAdd(NormalisationTexte.Normaliser(connu.Nom), connu.Role);
}
return saisie
.Split(SeparateurAuteurs, StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries)
.Select(nom => new AuteurSaisi(
nom, roles.GetValueOrDefault(NormalisationTexte.Normaliser(nom), RoleAuteur.NonPrecise)))
.ToList();
}
}
/// <summary>Un auteur tel qu'il est saisi : son nom, et son rôle sur ce livre.</summary>
/// <remarks>
/// Le rôle n'appartient pas à la personne mais au <b>lien</b> : le même auteur scénarise un
/// album et dessine le suivant. Voir <see cref="RoleAuteur"/>.
/// </remarks>
public record AuteurSaisi(string Nom, RoleAuteur Role = RoleAuteur.NonPrecise);
/// <summary>Charge utile du changement de statut de lecture — l'action la plus fréquente.</summary>
/// <remarks>
/// Le statut posé est toujours celui de l'appelant : il n'y a pas de champ « utilisateur »,
+7
View File
@@ -12,6 +12,13 @@ public record AuteurDto
/// <summary>Nombre de livres du catalogue rattachés à cet auteur. Renseigné par la liste des auteurs.</summary>
public int NombreDeLivres { get; init; }
/// <summary>
/// Rôle sur le livre où cet auteur figure. <b>N'a de sens qu'à l'intérieur d'un
/// <see cref="LivreDto"/></b> : la liste des auteurs, elle, ne parle d'aucun livre en
/// particulier et le laisse à <c>NonPrecise</c>.
/// </summary>
public RoleAuteur Role { get; init; }
}
/// <summary>
+9
View File
@@ -60,6 +60,15 @@ public class LivreAuteur
/// <summary>Rang de l'auteur sur la couverture, à partir de 0.</summary>
public int Position { get; set; }
/// <summary>
/// Rôle sur <b>ce livre</b> : scénario, dessin, couleurs. Voir <see cref="RoleAuteur"/>.
/// </summary>
/// <remarks>
/// Il est porté par le lien et non par l'auteur : le même dessinateur peut avoir scénarisé
/// un autre album, et il ne doit pas pour autant exister en deux fiches.
/// </remarks>
public RoleAuteur Role { get; set; }
}
/// <summary>
+32
View File
@@ -0,0 +1,32 @@
namespace MaBibli.Shared.Entites;
/// <summary>
/// Rôle d'un auteur <b>sur un livre donné</b>, et non une propriété de la personne.
/// </summary>
/// <remarks>
/// Le même auteur scénarise un album et dessine le suivant : le rôle vit donc sur le lien
/// <see cref="LivreAuteur"/>, jamais sur <see cref="Auteur"/>. C'est ce qui permet aussi de
/// garder <b>une seule fiche</b> par personne, à laquelle tous ses livres se rattachent.
/// <para>
/// ⚠️ <see cref="NonPrecise"/> est la valeur par défaut, et ne veut pas dire « auteur du
/// texte » : la quasi-totalité des livres n'a qu'un signataire dont le rôle ne se pose pas.
/// Même règle que <see cref="TypeDocument"/> — l'interface n'affiche que ce qui a été choisi.
/// </para>
/// <para>
/// Liste volontairement courte : le besoin remonté est la <b>bande dessinée</b>, où l'on veut
/// savoir qui écrit et qui dessine. Une valeur s'ajoute sans migration, la renommer une fois
/// posée sur des centaines de liens, non.
/// </para>
/// </remarks>
public enum RoleAuteur
{
/// <summary>Aucun rôle particulier — le cas de l'immense majorité des livres.</summary>
NonPrecise = 0,
Scenario = 1,
Dessin = 2,
/// <summary>Mise en couleurs : un troisième nom figure souvent sur un album.</summary>
Couleurs = 3,
}