λΈ”λ ˆμ΄μ € Form Validation with IStringLocalizer

데이터 검증 λ©”μ‹œμ§€λ₯Ό 지역화할 λ•Œ λ§ˆλ•…ν•œ μˆ˜λ‹¨μ΄ μ—†μ–΄, κ³ λ―Όλ“€ ν•œ 번 μ”© 해보셨을 κ²λ‹ˆλ‹€.

λ‹·λ„·μ˜ 데이터 μ–΄λ…Έν…Œμ΄μ…˜μ„ μ‚¬μš©ν•˜λŠ” 방법이 μžˆμ§€λ§Œ, μ—¬κΈ°μ—λŠ” 두 가지 λ¬Έμ œκ°€ μžˆμŠ΅λ‹ˆλ‹€.

첫째둜, λΌμ΄λΈŒλŸ¬λ¦¬μ—μ„œ μ œκ³΅ν•˜λŠ” 영문 λ©”μ‹œμ§€ ν…œν”Œλ¦Ώμ€ λ³€κ²½ μ μš©ν•˜λŠ” 것이 νž˜λ“€λ‹€λŠ” κ²ƒμž…λ‹ˆλ‹€.
이λ₯Ό μœ„ν•΄μ„œλŠ” 수 λ§Žμ€ μ½”λ“œκ°€ ν•„μš”ν•©λ‹ˆλ‹€. (λͺ‡ λͺ‡ μ˜€ν”ˆ μ†ŒμŠ€ ν”„λ‘œμ νŠΈ μžˆκΈ°λŠ” ν•©λ‹ˆλ‹€λ§Œ, λΆˆνŽΈν•΄ λ³΄μ—¬μ„œ μ‚¬μš©ν•΄λ³΄μ§€ μ•Šμ•˜μŠ΅λ‹ˆλ‹€.)

λ‘˜μ§Έλ‘œ, μ»€μŠ€ν…€ resx νŒŒμΌμ„ μž‘μ„±ν•˜λŠ” 경우, Key-Valueλ₯Ό λ¨Όμ € κ³ λ―Όν•΄μ•Ό ν•΄μ„œ, 둜직 μ½”λ“œ 보닀 뢀담이 λ§Žμ•„μ§ˆ 수 μžˆμŠ΅λ‹ˆλ‹€.

λΈ”λ ˆμ΄μ €λŠ” λ‹€ν–‰μŠ€λŸ½κ²Œλ„ EditForm μš”μ†Œκ°€ 검증 λ©”μ‹œμ§€λ‘œ 동적인 λ¬Έμžμ—΄μ„ μ‚¬μš©ν•  수 μžˆκ²Œλ” μ§€μ›ν•©λ‹ˆλ‹€.

이λ₯Ό IStringLocalizer 와 ν•¨κ»˜ μ‚¬μš©ν•˜λ©΄ 보닀 κ°„νŽΈν•˜κ²Œ μ§€μ—­ν™”λœ Validation 메세지λ₯Ό μž‘μ„±ν•  수 μžˆμŠ΅λ‹ˆλ‹€.

@*MyComponent.razor*@
@implements IDisposable

@IStringLocalizer<AppTexts> Texts

<EditForm EditContext="_editContext" OnValidSubmit="Submit" FormName="WeekDaysSelection">
    <div>
        <label>
            <InputCheckbox @bind-Value="Model!.HasMon" /> @Texts["μ›”"]
        </label>
    </div>
    <div>
        <label>
            <InputCheckbox @bind-Value="Model!.HasTue" /> @Texts["ν™”"]
        </label>
    </div>
@* μƒλž΅ *@
    <div>
        <label>
            <InputCheckbox @bind-Value="Model!.HasSun" /> @Texts["일"]
        </label>
    </div>
    <div>
        <ValidationMessage For="() => Model!.Selecteds" />
    </div>
    <div>
        <button type="submit">@Texts["확인"]</button>
    </div>
</EditForm>

@code {
    [SupplyParameterFromForm]
    public WeekDays? Model { get; set; }

    private EditContext? _editContext;
    private ValidationMessageStore? _messageStore;

    protected override void OnInitialized()
    {
        Model ??= new();
        _editContext = new(Model);
        _editContext.OnValidationRequested += HandleValidationRequested;
        _messageStore = new(_editContext);
    }

    // 데이터 검증 μš”μ²­ ν•Έλ“€λŸ¬
    private void HandleValidationRequested(object? sender,
        ValidationRequestedEventArgs args)
    {
        _messageStore?.Clear();

        var selected = Model!.Selecteds.Count();
        if (selected < 1)
        {
            _messageStore?.Add(() => Model.Selecteds, Texts["ν•˜λ‚˜ 이상 선택해야 ν•©λ‹ˆλ‹€."]);
        }
        else if (selected > 5)
        {
            _messageStore?.Add(() => Model.Selecteds, Texts["5개 보닀 많이 μ„ νƒν•˜λ©΄ μ•ˆλ©λ‹ˆλ‹€."]);
        }
    }

    private void Submit()
    {
        // 데이터 검증이 μ„±κ³΅ν–ˆμ„ λ•Œ μ‹€ν–‰λ˜λŠ” μ½”λ“œ       
    }

    public void Dispose()
    {
        if (_editContext is not null)
        {
            _editContext.OnValidationRequested -= HandleValidationRequested;
        }
    }

    public class WeekDays
    {
       public bool HasSun { get; set; }
       public bool HasMon { get; set; }
       public bool HasTue { get; set; }
       public bool HasWed { get; set; }
       public bool HasThu { get; set; }
       public bool HasFri { get; set; }
       public bool HasSat { get; set; }

       public IEnumerable<DayOfWeek> Selecteds
       {
          get
          {
             if (HasSun) yield return DayOfWeek.Sunday;
             if (HasMon) yield return DayOfWeek.Monday;
             if (HasTue) yield return DayOfWeek.Tuesday;
             if (HasWed) yield return DayOfWeek.Wednesday;
             if (HasThu) yield return DayOfWeek.Thursday;
             if (HasFri) yield return DayOfWeek.Friday;
             if (HasSat) yield return DayOfWeek.Saturday;
         }
      }
}

EditForm.EditContext

이 κ°μ²΄λŠ” Form 을 톡해 μ‚¬μš©μžκ°€ 데이터λ₯Ό νŽΈμ§‘ν•˜λŠ” 상황(Context)을 λ‚˜νƒ€λ‚΄λŠ”λ°, μœ μš©ν•œ 메타 정보λ₯Ό μ œκ³΅ν•©λ‹ˆλ‹€. .

μ•„λž˜ μ½”λ“œλŠ” 데이터 λͺ¨λΈλ‘œ EditContext λ₯Ό μƒμ„±ν•˜κ³ , 메타 정보 쀑 관심 μžˆλŠ” λΆ€λΆ„, 즉, 데이터 검증 μš”μ²­ 이벀트λ₯Ό κ΅¬λ…ν•˜λŠ” μ½”λ“œμž…λ‹ˆλ‹€.

    protected override void OnInitialized()
    {
        Model ??= new();
        _editContext = new(Model);
        editContext.OnValidationRequested += HandleValidationRequested;
        messageStore = new(editContext);
    }

λ§ˆμ§€λ§‰ 쀄은, μƒμ„±λœ EditContext둜 ValidationMessageStore 객체λ₯Ό μ΄ˆκΈ°ν™”ν•˜λŠ”λ°, μ΄λ ‡κ²Œ μ΄ˆκΈ°ν™”λœ λ©”μ‹œμ§€ μ €μž₯μ†Œ κ°μ²΄λŠ” μ•„λž˜ μš”μ†Œκ°€ μ‚¬μš©ν•©λ‹ˆλ‹€.

    <div>
        <ValidationMessage For="() => Model!.Selecteds" />
    </div>

ValidationMessageStore 객체와 ValidationMessage μš”μ†Œμ˜ 연관성은 λ§€μ§μž…λ‹ˆλ‹€.

검증 처리 κ³Όμ •

EditContext.OnValidationRequested 이벀트의 μ „νŒŒλŠ” EditForm 이 κ΄€μž₯ν•˜λŠ”λ°, OnValidSubmit 이 null 이 아닐 λ•Œ 이벀트 μ „νŒŒλ₯Ό μš”μ²­ν•©λ‹ˆλ‹€.

<EditForm EditContext="_editContext" OnValidSubmit="Submit" ...

λ§Œμ•½ μ•„λž˜μ™€ 같이 μ„€μ •ν•˜λ©΄, μ΄λ²€νŠΈλŠ” μ „νŒŒλ˜μ§€ μ•ŠμŠ΅λ‹ˆλ‹€.

<EditForm EditContext="_editContext" OnSubmit="Submit" ...

Form Dataκ°€ λ„μ°©ν–ˆμ„ λ•Œ, EditForm 이 데이터λ₯Ό κ²€μ¦ν•˜λŠ” μ „λ°˜μ μΈ 처리 과정은 μ•„λž˜μ™€ κ°™μŠ΅λ‹ˆλ‹€.

OnValidSubmit == null ? ( OnSubmit == null ? Do nothing : OnSubmit 호좜)
: EditContext.EditContext.OnValidationRequested 호좜

ValidationMessage μš”μ†Œκ°€ EditContext 객체λ₯Ό CasCade λ°›μŒ.
ValidationMessageStore μ—μ„œ EditContext에 ν•΄λ‹Ήν•˜λŠ” λ©”μ‹œμ§€λ₯Ό 찾음. => μžˆλŠ” 경우만, λ©”μ‹œμ§€ λ Œλ”λ§.

ValidationMessage μš”μ†ŒλŠ” ValidationMessageStore μ—μ„œ λ©”μ‹œμ§€λ₯Ό μ°ΎκΈ° λ•Œλ¬Έμ—, μƒˆλ‘œμš΄ 검증 μš”μ²­ λ•Œλ§ˆλ‹€, 이λ₯Ό λΉ„μ›Œμ£Όμ§€ μ•ŠμœΌλ©΄, μ§€λ‚œ 번 λ©”μ‹œμ§€ μ½”λ“œκ°€ λ³΄μ—¬μ§€κ²Œ λ©λ‹ˆλ‹€.

    // 데이터 검증 μš”μ²­ ν•Έλ“€λŸ¬
    private void HandleValidationRequested(object? sender,
        ValidationRequestedEventArgs args)
    {
        _messageStore?.Clear();

μš”μ²­ ν•Έλ“€λŸ¬μ˜ μ‹€ν–‰ λŸ°νƒ€μž„

μš”μ²­ ν•Έλ“€λŸ¬μ˜ 싀행은 λΈ”λ ˆμ΄μ €μ˜ ν˜ΈμŠ€νŒ… λͺ¨λΈμ— 따라 λ‹¬λΌμ§‘λ‹ˆλ‹€.
Static SSR/Interactive Server 의 경우 μ„œλ²„μ—μ„œ, Interactive Webassembly 인 경우 ν΄λΌμ΄μ–ΈνŠΈμ—μ„œ μ‹€ν–‰λ©λ‹ˆλ‹€.

Form Data - Model 바인딩

μ΄λŠ” λΈ”λ ˆμ΄μ €κ°€ μ²˜λ¦¬ν•©λ‹ˆλ‹€.
λΈ”λ ˆμ΄μ €λŠ” ν˜ΈμŠ€νŒ… λͺ¨λΈμ— 따라 μ•„λž˜μ™€ 같이 λͺ¨λΈμ„ μ½”λ“œμ— μ „λ‹¬ν•©λ‹ˆλ‹€.

  1. Static SSR인 경우,

    1. Enhance Form 인 경우
      blazor.web.js 의 fetch μš”μ²­μ„ 톡해,
    2. μ•„λ‹Œ 경우
      Html Form submission을 톡해
  2. Interactive Server 인 경우, μ›Ήμ†ŒμΌ“μ„ 톡해,

  3. Interactive Webassembly 인 경우, wasm - js interop 을 톡해 μ „λ‹¬ν•©λ‹ˆλ‹€.

참고둜, μ•„μ£Ό 가끔, 바인딩이 μ•ˆλ˜λŠ” κ²½μš°κ°€ μžˆμŠ΅λ‹ˆλ‹€.
이 경우, 도움 μ•ˆλ˜λŠ” μ—λŸ¬ λ©”μ‹œμ§€λ§Œ λ³΄μ—¬μ§ˆ 뿐이라, 저도 원인을 μ°Ύμ§€λŠ” λͺ»ν–ˆμŠ΅λ‹ˆλ‹€.

아무리 봐도, μ½”λ“œμ— λ¬Έμ œκ°€ μ—†μ–΄ 보인닀면, λΉ„μ£Όμ–Ό μŠ€νŠœλ””μ˜€λ₯Ό λͺ‡ 번 μ—΄κ³  λ‹«μœΌλ©΄ 해결이 λ©λ‹ˆλ‹€.
μ•ˆλ  λ•ŒλŠ” 릴리슀 λͺ¨λ“œλ‘œ λΉŒλ“œν•΄λ„ μ•ˆλ˜λ‹ˆ, κ·Έλƒ₯ VSλ₯Ό 끄고 λ‹«λŠ” 게 μ’‹μŠ΅λ‹ˆλ‹€. 될 λ•ŒκΉŒμ§€.

팁이라면, EditForm 을 톡해 ν• λ‹Ήλ˜λŠ” νŒŒλΌλ―Έν„°λŠ” OnParametersSet λ³΄λ‹€λŠ” OnInitialized μ—μ„œ 할당을 μ²˜λ¦¬ν•˜λŠ” 게 κ·Έλ‚˜λ§ˆ, λ°œμƒν™•λ₯ μ„ μ€„μ΄λŠ” 것 κ°™μŠ΅λ‹ˆλ‹€.

IStringLocalizer<T>

이 κ°μ²΄λŠ” T.resx νŒŒμΌμ„ μ°Ύκ³ , 파일이 μ—†κ±°λ‚˜, μžˆμ–΄λ„ ν•΄λ‹Ή key κ°€ μ‘΄μž¬ν•˜μ§€ μ•ŠμœΌλ©΄, key 값을 λ°˜ν™˜ν•©λ‹ˆλ‹€.

예제둜 μ„€λͺ…ν•˜λ©΄,

@IStringLocalizer<AppTexts> Texts

<EditForm EditContext="_editContext" OnValidSubmit="Submit" FormName="WeekDaysSelection">
    <div>
        <label>
            <InputCheckbox @bind-Value="Model!.HasMon" /> @Texts["μ›”"]
        </label>
 ...

IStringLocalizer<AppTexts>λŠ” μ„œλΉ„μŠ€ μ»¨ν…Œμ΄λ„ˆλ‘œλΆ€ν„° μ£Όμž…λ°›κ³ , μ½”λ“œμ—μ„œ Texts[β€œμ›”β€] 을 ν˜ΈμΆœν–ˆλŠ”λ°,

AppTexts.resx 파일이 μ—†κ±°λ‚˜, μžˆμ–΄λ„, 이 νŒŒμΌμ— key "μ›”"에 ν•΄λ‹Ήν•˜λŠ” value κ°€ μ—†μœΌλ©΄, "μ›”"을 λ°˜ν™˜ν•œλ‹€λŠ” μ˜λ―Έμž…λ‹ˆλ‹€.

μ΄λŠ” AppTexts.resx 파일이 μ‘΄μž¬ν•˜μ§€ μ•Šμ•„λ„ 됨을 μ˜λ―Έν•©λ‹ˆλ‹€.
λ‹€λ§Œ, 자리 지킴이 파일인 AppTexts.cs νŒŒμΌμ€ μžˆμ–΄μ•Ό ν•©λ‹ˆλ‹€.

public class AppTexts { }

IStringLocalizer<AppTexts> 객체의 λͺ¨λ“  ν‚€λ₯Ό ν•œκΈ€λ‘œλ§Œ μ§€μ •ν•˜λ©΄, κ°€μƒμ˜ AppTexts.resx λŠ” ν•œκΈ€ μžμ› 파일과 κ°™κΈ° λ•Œλ¬Έμ—, UIλ₯Ό ν•œκΈ€λ‘œ λΉ λ₯΄κ²Œ μž‘μ„±ν•  수 μžˆμŠ΅λ‹ˆλ‹€.

λ‚˜μ€‘μ— 영문이 ν•„μš”ν•œ κ²½μš°μ—λ§Œ, AppTexts.en.resx νŒŒμΌμ„ μž‘μ„±ν•˜λ©΄ λ©λ‹ˆλ‹€.
λ‹€λ§Œ, μ—¬κΈ° μ €κΈ° λΆ„μ‚°λœ ν•œκΈ€ μžμ›μ„ μ°ΎλŠ” 게 손이 많이 κ°€λŠ” μΌμ΄κΈ°λŠ” ν•©λ‹ˆλ‹€.

4개의 μ’‹μ•„μš”

이제 μ œκ°€μ’€ μ—¬μœ κ°€ μƒκ²¨μ„œ Blzor , css , front 단을
κ³΅λΆ€ν•΄λ³Όμƒκ°μž…λ‹ˆλ‹€. @BigSquare λ‹˜μ„ 글을 μ’€ 정독 ν•΄λ³Όλ €κ³ μš”
그리고 μ°¨κΈ° μ›Ή μ†”λ£¨μ…˜ bolilerplateλ₯Ό κ΅¬μƒμ€‘μ΄λΌμ„œμš” γ…Ž

2개의 μ’‹μ•„μš”

λΈ”λ ˆμ΄μ € λ¬Έμ„œμ—μ„œλŠ” μš”μ†Œλ₯Ό μž‘μ„±ν•  λ•Œ, λ Œλ”λ§ λͺ¨λΈκ³Ό 상관없이 ν•˜λΌκ³  ν•˜λŠ”λ°, ν˜„μ‹€μ μœΌλ‘œ λ‹¬μ„±ν•˜κΈ° 맀우 νž˜λ“  λͺ©ν‘œμΈ 것 κ°™μŠ΅λ‹ˆλ‹€.
(μ˜ˆμ œλŠ” κ°€λŠ₯해도, ν˜„μ‹€ 앱은 어렀움이 λ§ŽμŠ΅λ‹ˆλ‹€)

λ”°λΌμ„œ, μ—¬λŸ¬ 가지 λ Œλ”λ§ λͺ¨λΈμ˜ νŠΉμ§•μ„ λͺ¨λ‘ μ‚΄ν”ΌκΈ° λ³΄λ‹€λŠ”, ν•œ κ°€μ§€λ§Œ 정해놓고 κ·Έκ²ƒλ§Œ μ‚¬μš©ν•˜λŠ” 것을 μΆ”μ²œν•©λ‹ˆλ‹€.

  1. λΈ”λ ˆμ΄μ € μ›Ήμ–΄μ…ˆλΈ”λ¦¬
    μ „ν˜•μ μΈ 3 tier λͺ¨λΈλ‘œ, javascript의 역할을 C# μ½”λ“œκ°€ λ‹΄λ‹Ήν•˜κ³ , λ‘œλ”© νƒ€μž„μ„ λΉΌλ©΄ μ„±λŠ₯도 κ°€μž₯ μ’‹μŠ΅λ‹ˆλ‹€.
    ν‘œμ€€ μ›Ή API ν˜ΈμΆœμ—λŠ” javascript κ°€ μ—¬μ „νžˆ ν•„μš”ν•˜μ§€λ§Œ, 어렡지 μ•Šμ€ μ½”λ“œμ΄κ³ , μ˜ˆμ œλ“€λ„ μ‰½κ²Œ ꡬ할 수 μžˆμ–΄(빙신도 κ½€ μž˜ν•©λ‹ˆλ‹€) νŠΉλ³„νžˆ λ§Žμ€ 곡뢀가 ν•„μš”ν•˜μ§€ μ•ŠμŠ΅λ‹ˆλ‹€.
    API와 DBλ₯Ό λ³„λ„λ‘œ μž‘μ„±ν•΄μ•Ό ν•©λ‹ˆλ‹€.
    이 λ Œλ”λ§ λͺ¨λΈμ˜ κ°€μž₯ 큰 μž₯점은 거의 μ•„λ¬΄λŸ° μˆ˜μ • 없이, WPF, 윈폼, MAUI ν•˜μ΄λΈŒλ¦¬λ“œ ν˜ΈμŠ€νŒ… λͺ¨λΈκ³Όμ˜ ν˜Έν™˜μ„±μ΄ 맀우 μ’‹μŠ΅λ‹ˆλ‹€. 거의 κ·ΈλŒ€λ‘œ μ‚¬μš©ν•  수 μžˆμ„ μ •λ„μž…λ‹ˆλ‹€.
    λ¬Όλ‘ , λ³„λ„μ˜ APIλ₯Ό μž‘μ„±ν•΄μ•Ό ν•˜λŠ” 뢀담이 μžˆμ§€λ§Œ, μ§€κΈˆμ€ supabase λ“±κ³Ό 같이 경제적인 Saas 도ꡬ가 λ§ŽμœΌλ―€λ‘œ 였히렀 개발 κ²½μ œμ„± λ©΄μ—μ„œ κ°€μž₯ 이득이라 ν•  수 μžˆμŠ΅λ‹ˆλ‹€. (Db만 μž‘μ„±ν•˜λ©΄ APIκ°€ μžλ™μœΌλ‘œ μƒμ„±λ©λ‹ˆλ‹€)

  2. λΈ”λ ˆμ΄μ € μ„œλ²„
    2 tier λͺ¨λΈμ— μ ν•©ν•˜κ³ , μ„±λŠ₯은 쀑간, javascript 의 μš©λ„λŠ” μœ„μ™€ κ°™μŠ΅λ‹ˆλ‹€.
    DB 만 κ΅¬μ„±ν•˜λ©΄ λ©λ‹ˆλ‹€.
    μ˜ˆμ „μ— λ¬Έμ œκ°€ λ§Žμ•˜λ˜ μ›Ήμ†ŒμΌ“ 연결이 λŠκ²Όμ„ λ•Œ 화면이 8.0μ—μ„œλŠ” κ°œμ„ λ˜μ—ˆμŠ΅λ‹ˆλ‹€.

  3. Static SSR
    전톡적인 μ›Ήμ•±κ³Ό 거의 λΉ„μŠ·ν•˜κ²Œ λ™μž‘ν•΄μ„œ, μ„œλ²„μ˜ μ›λ¦¬λ§Œ μ•Œλ©΄, κ΅¬ν˜„ν•˜κΈ°λŠ” 맀우 κ°„λ‹¨ν•©λ‹ˆλ‹€.
    κ·ΈλŸ¬λ‚˜, μ„±λŠ₯적으둜 κ°€μž₯ 느리고, ν΄λΌμ΄μ–ΈνŠΈ μ½”λ“œμ— C#을 μ“Έ 수 μ—†λ‹€λŠ” 게 κ°€μž₯ 큰 λ‹¨μ μž…λ‹ˆλ‹€. λ”°λΌμ„œ, λ°˜λ“œμ‹œ javascript λ₯Ό μ΅ν˜€μ•Ό ν•©λ‹ˆλ‹€.

λ Œλ”λ§ λͺ¨λ“œλ₯Ό μ„žμ–΄ μ“°λŠ” 것은 개인적으둜 λΉ„μΆ”ν•©λ‹ˆλ‹€.

개인적인 μΆ”μ²œ μˆœμ„œλŠ” μ›Ήμ–΄μ…ˆλΈ”λ¦¬ > Static SSR > μ„œλ²„ μž…λ‹ˆλ‹€.

μ•ˆλ…•ν•˜μ„Έμš”.
지역화 λ•Œλ¬Έμ— λ§Žμ€ 고민을 ν•˜κ³  μžˆμŠ΅λ‹ˆλ‹€.
μ‰½κ²Œ κ°€λŠ” 방법을 μ°Ύλ‹€κ³  κ²°κ΅­μ—λŠ” 정상적이고 ν™•μ‹€ν•œ λ°©λ²•μœΌλ‘œ κ°€κ²Œ λ˜λ„€μš” γ… γ… 
IStringLocalizer을 κ΅¬ν˜„ν•΄μ„œ λ‚΄λΆ€ λ‘œμ§μ—μ„œ DBμ—μ„œ κ΄€λ¦¬ν•˜λŠ” 지역화 λ¦¬μ†ŒμŠ€λ₯Ό κ°€μ Έμ™€μ„œ 지역화λ₯Ό ν–ˆμŠ΅λ‹ˆλ‹€.
λ¬Έμ œλŠ” EditForm Validation 메세지λ₯Ό μ§€μ—­ν™”ν•˜λŠ”κ²Œ 문제 μ˜€λŠ”λ°β€¦
κ²°κ΅­μ—λŠ” κΈ°μ‘΄ Attribute듀을 상속받아 λ‹€μ‹œ κ΅¬ν˜„μ„ ν–ˆμ–΄μš”.
κ΅¬ν˜„ν•œ λ‚΄λΆ€λ‘œμ§μ—λŠ” IStringLocalizer을 κ΅¬ν˜„ν•œ κ±Έ μž¬μ‚¬μš©ν•©λ‹ˆλ‹€.
κΉƒν—ˆλΈŒμ— 아직 μ˜¬λ¦¬μ§€λŠ” μ•Šμ•˜λŠ”λ° λͺ‡μΌ μ•ˆμœΌλ‘œ μ˜¬λΌκ°€λ©΄ κ³΅μœ ν•΄ λ³Όκ»˜μš” !!

1개의 μ’‹μ•„μš”

고생 많이 ν•˜μ…¨λ„€μš”. μ €λŠ” κ·Έ 만큼 λΆ€μ§€λŸ°ν•˜μ§€ μ•Šμ•„μ„œ γ… γ… 

그런데, Db 에 μ €μž₯ν•˜λŠ” 방식은 μ„±λŠ₯ λ¬Έμ œκ°€ μžˆμ§€λŠ” μ•Šμ€μ§€μš”?
μ–΄λ–»κ²Œ κ΅¬ν˜„ν•˜μ…¨λŠ”μ§€ μ°Έ κΆκΈˆν•©λ‹ˆλ‹€. μ–Όλ₯Έ 올렀 μ£Όμ„Έμš”. ^^

FluentValidation + Blazored.FluentValidation ν™œμš©ν•˜μ—¬ λ©”μ‹œμ§€ μƒμ„±ν•˜κ³  μžˆμŠ΅λ‹ˆλ‹€. ν•œκ΅­μ–΄ 둜캘 μ—λŸ¬λ©”μ‹œμ§€λ„ μ§€μ›ν•˜κ³ , κΈ°λ³Έ μœ νš¨μ„± 검사 ν–‰μœ„λ₯Ό μ—¬λŸ¬κ°œ μ§€μ›ν•΄μ„œ λ³„λ„μ˜ λ©”μ‹œμ§€λ₯Ό λ”°λ‘œ μž‘μ„±ν•  일이 거의 μ—†λ”λΌκ΅¬μš”.
LanguageManager μ˜€νƒ€ μžˆλŠ” λΆ€λΆ„ μ»€μŠ€ν…€, DisplayNameResolver 만 μ»€μŠ€ν…€ν–ˆμŠ΅λ‹ˆλ‹€.

DTO

namespace Business.Models
{
    public class EditableTankLevelHistoryDto : IKeyEntity
    {
        public int Id { get; set; }
        
        public int TankId { get; set; }

        [Display(Name = "탱크")]
        public string TankName { get; set; } = null!;

        public DateTime HistoryAt { get; set; } = new DateTime(
            DateTime.Now.Year,
            DateTime.Now.Month,
            DateTime.Now.Day, 
            DateTime.Now.Hour, 
            DateTime.Now.Minute, 
            0
        );

        [Display(Name = "레벨(mm)")]
        public int Level { get; set; }
        public object?[] KeyValues => [Id];

        public class FromMap : RelationProfile<TankLevelHistory, EditableTankLevelHistoryDto> 
        {
            public FromMap()
            {
                Mapping.ForMember(d => d.TankName, opts =>
                {
                    opts.MapFrom(s => s.Tank.Name);
                });
            }
        }
        public class ToMap : RelationProfile<EditableTankLevelHistoryDto, TankLevelHistory> { }

        public class Validator : AbstractValidator<EditableTankLevelHistoryDto>
        {
            public Validator()
            {
                RuleFor(x => x.TankId).GreaterThan(0);
                RuleFor(x => x.Level).GreaterThan(0);
            }
        }
    }
}

Program.cs(μ„œλΉ„μŠ€ 등둝)

builder.Services.AddValidatorsFromAssembly(typeof(ProjectInfo).Assembly);
ValidatorOptions.Global.LanguageManager = new CustomFluentValidationLanguageManager();
ValidatorOptions.Global.DisplayNameResolver = (type, memberInfo, expression) =>
{
    return memberInfo.GetCustomAttribute<System.ComponentModel.DataAnnotations.DisplayAttribute>()?.GetName()
        ?? memberInfo?.Name
        ?? type.Name;
};

View

...
        <EditForm id="editform" Model="@EditModel" Context="EditFormContext" OnInvalidSubmit="OnInvalidSubmit"
                  OnValidSubmit="SaveConfirmPopup.Show">
            <FluentValidationValidator />
...
@code {
...
    void OnInvalidSubmit(EditContext context)
    {
        foreach (var msg in context.GetValidationMessages())
        {
            Snackbar.Add(msg, Severity.Error);
        }
    }
}
3개의 μ’‹μ•„μš”

κΈ°λ³Έ λ©”μ‹œμ§€μ— ν•œκ΅­μ–΄λ₯Ό μ§€μ›ν•œλ‹€κ³  ν•˜λ‹ˆ, 맀우 λ‹€ν–‰μŠ€λŸ½λ„€μš”.

ν•œ 번 μ°Ύμ•„ λ³΄κ² μŠ΅λ‹ˆλ‹€.

많이 μ“°κ³  μ‚¬μš©μ„±λ„ ν›Œλ₯­ν•œ 라이브러리라 정말 잘 μ“°κ³ μžˆμŠ΅λ‹ˆλ‹€ γ…Žγ…Ž

1개의 μ’‹μ•„μš”

μ‚¬μš©ν•΄λ³΄λ‹ˆ, μžμž˜ν•œ μ½”λ“œλ“€μ΄ 많이 μ€„μ–΄λ“œλŠ” 것 κ°™μŠ΅λ‹ˆλ‹€.

특히, RuleFor().Must() κ°€ 맀우 λ²”μš©μ μ΄λ„€μš”.

~~ (이) κ°€ μœ νš¨ν•˜μ§€ μ•ŠμŠ΅λ‹ˆλ‹€.

1개의 μ’‹μ•„μš”

λ‹·λ„· μž¬λ‹¨ 라이브러리라 κΎΈμ€€νžˆ μœ μ§€κ΄€λ¦¬λ˜λŠ”κ²ƒλ„ μž₯점인것 κ°™μŠ΅λ‹ˆλ‹€. γ…Žγ…Ž