feat: implement multi-tenant company management system
TaxBaik CI/CD / build-and-deploy (push) Successful in 48s
TaxBaik CI/CD / build-and-deploy (push) Successful in 48s
Architecture: - Create companies table with company_code as unique identifier - Add company_id foreign key to admin_users for multi-tenant support - Implement backward compatibility with DEFAULT company for existing users Core Components: - Company entity with full CRUD operations - ICompanyRepository interface following Repository pattern - CompanyRepository with Dapper implementation - CompanyService with business logic and validation - CompanyController with REST API endpoints Admin UI: - CompanyForm reusable component (Create/Edit pattern) - CompanyList.razor with pagination and company overview - CompanyCreate.razor for registering new companies - CompanyEdit.razor for managing existing companies with delete - All pages follow admin-page-hero pattern for consistency SOLID Principles: - Single Responsibility: Each component has one reason to change - Open/Closed: Extensible without modifying existing code - Interface Segregation: Clean repository and service contracts - Dependency Inversion: All layers depend on abstractions Database Migration (V014): - Creates companies table with active/inactive status - Assigns existing admin users to DEFAULT company - Provides foundation for role-based access control Future Enhancement: - Admin users can belong to specific companies - Data filtering based on company_id (multi-tenant isolation) - Company-based permission model
This commit is contained in:
@@ -0,0 +1,128 @@
|
||||
@page "/admin/companies/{id:int}/edit"
|
||||
@attribute [Authorize]
|
||||
@using TaxBaik.Web.Components.Admin.Forms
|
||||
@inject IApiClient ApiClient
|
||||
@inject NavigationManager Navigation
|
||||
@inject ISnackbar Snackbar
|
||||
@inject IDialogService DialogService
|
||||
|
||||
<PageTitle>고객사 수정</PageTitle>
|
||||
|
||||
<section class="admin-page-hero">
|
||||
<div>
|
||||
<MudText Typo="Typo.caption" Class="admin-eyebrow">Settings</MudText>
|
||||
<MudText Typo="Typo.h4" Class="admin-page-title">고객사 수정</MudText>
|
||||
<MudText Typo="Typo.body2" Class="admin-page-subtitle">고객사 정보를 수정합니다.</MudText>
|
||||
</div>
|
||||
<MudButton Variant="Variant.Outlined" StartIcon="@Icons.Material.Filled.Close" @onclick="GoBack">취소</MudButton>
|
||||
</section>
|
||||
|
||||
@if (isLoading)
|
||||
{
|
||||
<MudProgressLinear Color="Color.Primary" Indeterminate="true" Class="mt-4" />
|
||||
}
|
||||
else if (formModel == null)
|
||||
{
|
||||
<MudAlert Severity="Severity.Error" Class="mt-4">고객사를 찾을 수 없습니다.</MudAlert>
|
||||
}
|
||||
else
|
||||
{
|
||||
<MudPaper Class="pa-4 mt-4" Elevation="1">
|
||||
<CompanyForm ButtonText="수정" InitialData="formModel" OnSubmit="HandleUpdate" OnCancel="GoBack" />
|
||||
|
||||
<MudDivider Class="my-4" />
|
||||
|
||||
<MudButton Variant="Variant.Outlined" Color="Color.Error" @onclick="DeleteCompany" Class="mt-2">
|
||||
고객사 삭제
|
||||
</MudButton>
|
||||
</MudPaper>
|
||||
}
|
||||
|
||||
@code {
|
||||
[Parameter]
|
||||
public int Id { get; set; }
|
||||
|
||||
private CompanyForm.CompanyFormModel? formModel;
|
||||
private bool isLoading = true;
|
||||
|
||||
protected override async Task OnInitializedAsync()
|
||||
{
|
||||
try
|
||||
{
|
||||
var company = await ApiClient.GetAsync<dynamic>($"company/{Id}");
|
||||
IDictionary<string, object>? dict = company as IDictionary<string, object>;
|
||||
if (dict != null)
|
||||
{
|
||||
formModel = new CompanyForm.CompanyFormModel
|
||||
{
|
||||
CompanyCode = (string)dict["companyCode"],
|
||||
CompanyName = (string)dict["companyName"],
|
||||
ContactPerson = (string?)dict["contactPerson"],
|
||||
Phone = (string?)dict["phone"],
|
||||
Email = (string?)dict["email"],
|
||||
Memo = (string?)dict["memo"],
|
||||
IsActive = (bool)(dynamic)dict["isActive"]
|
||||
};
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Snackbar.Add($"고객사 로드 실패: {ex.Message}", Severity.Error);
|
||||
}
|
||||
finally
|
||||
{
|
||||
isLoading = false;
|
||||
}
|
||||
}
|
||||
|
||||
private void GoBack()
|
||||
{
|
||||
Navigation.NavigateTo("/taxbaik/admin/companies");
|
||||
}
|
||||
|
||||
private async Task HandleUpdate(CompanyForm.CompanyFormModel model)
|
||||
{
|
||||
try
|
||||
{
|
||||
await ApiClient.PutAsync<object>($"company/{Id}", new
|
||||
{
|
||||
companyCode = model.CompanyCode,
|
||||
companyName = model.CompanyName,
|
||||
contactPerson = model.ContactPerson,
|
||||
phone = model.Phone,
|
||||
email = model.Email,
|
||||
memo = model.Memo,
|
||||
isActive = model.IsActive
|
||||
});
|
||||
|
||||
Snackbar.Add("고객사가 수정되었습니다.", Severity.Success);
|
||||
Navigation.NavigateTo("/taxbaik/admin/companies");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Snackbar.Add($"수정 실패: {ex.Message}", Severity.Error);
|
||||
}
|
||||
}
|
||||
|
||||
private async Task DeleteCompany()
|
||||
{
|
||||
var result = await DialogService.ShowMessageBox(
|
||||
"고객사 삭제",
|
||||
"정말 삭제하시겠습니까? 이 작업은 취소할 수 없습니다.",
|
||||
"삭제", "취소");
|
||||
|
||||
if (result != true)
|
||||
return;
|
||||
|
||||
try
|
||||
{
|
||||
await ApiClient.DeleteAsync($"company/{Id}");
|
||||
Snackbar.Add("고객사가 삭제되었습니다.", Severity.Success);
|
||||
Navigation.NavigateTo("/taxbaik/admin/companies");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Snackbar.Add($"삭제 실패: {ex.Message}", Severity.Error);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user