7dd51a1169
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
135 lines
5.3 KiB
Plaintext
135 lines
5.3 KiB
Plaintext
@page "/admin/companies"
|
|
@attribute [Authorize]
|
|
@inject IApiClient ApiClient
|
|
@inject ISnackbar Snackbar
|
|
|
|
<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.Filled" Color="Color.Primary" StartIcon="@Icons.Material.Filled.Add"
|
|
Href="/taxbaik/admin/companies/create">새 고객사 등록</MudButton>
|
|
</section>
|
|
|
|
<MudPaper Class="admin-surface mb-4 mt-4" Elevation="0">
|
|
<MudStack Row="true" AlignItems="AlignItems.Center" Justify="Justify.SpaceBetween">
|
|
<MudText Typo="Typo.subtitle1">@($"전체 고객사 {totalCompanies}개")</MudText>
|
|
<MudText Typo="Typo.body2">페이지 @currentPage / @totalPages</MudText>
|
|
</MudStack>
|
|
</MudPaper>
|
|
|
|
<MudDataGrid Items="@companies" Striped="true" Hoverable="true" Loading="@isLoading" Class="admin-grid">
|
|
<Columns>
|
|
<PropertyColumn Property="x => x.CompanyCode" Title="회사코드" />
|
|
<PropertyColumn Property="x => x.CompanyName" Title="회사명" />
|
|
<PropertyColumn Property="x => x.ContactPerson" Title="담당자" />
|
|
<PropertyColumn Property="x => x.Phone" Title="전화" />
|
|
<PropertyColumn Property="x => x.Email" Title="이메일" />
|
|
<PropertyColumn Property="x => x.IsActive" Title="활성">
|
|
<CellTemplate Context="cell">
|
|
<MudCheckBox T="bool" Value="@cell.Item.IsActive" Disabled="true" />
|
|
</CellTemplate>
|
|
</PropertyColumn>
|
|
<PropertyColumn Property="x => x.CreatedAt" Title="등록일" Format="yyyy-MM-dd" />
|
|
<TemplateColumn>
|
|
<CellTemplate Context="cell">
|
|
<MudButton Variant="Variant.Outlined" Size="Size.Small" Color="Color.Primary"
|
|
Href="@($"/taxbaik/admin/companies/{cell.Item.Id}/edit")">수정</MudButton>
|
|
</CellTemplate>
|
|
</TemplateColumn>
|
|
</Columns>
|
|
</MudDataGrid>
|
|
|
|
<MudStack Row="true" Justify="Justify.Center" Class="mt-4" Spacing="2">
|
|
<MudButton Variant="Variant.Outlined" Disabled="@(currentPage <= 1 || isLoading)" @onclick="PreviousPage">이전</MudButton>
|
|
<MudButton Variant="Variant.Outlined" Disabled="@(currentPage >= totalPages || isLoading)" @onclick="NextPage">다음</MudButton>
|
|
</MudStack>
|
|
|
|
@code {
|
|
private List<CompanyDto> companies = [];
|
|
private bool isLoading = true;
|
|
private int currentPage = 1;
|
|
private int totalPages = 1;
|
|
private int totalCompanies = 0;
|
|
private const int PageSize = 20;
|
|
|
|
protected override async Task OnInitializedAsync()
|
|
{
|
|
await LoadData();
|
|
}
|
|
|
|
private async Task LoadData()
|
|
{
|
|
try
|
|
{
|
|
isLoading = true;
|
|
var response = await ApiClient.GetAsync<dynamic>($"company?page={currentPage}&pageSize={PageSize}");
|
|
|
|
IDictionary<string, object>? dict = response as IDictionary<string, object>;
|
|
if (dict != null)
|
|
{
|
|
totalCompanies = (int)(dynamic)dict["total"];
|
|
totalPages = (totalCompanies + PageSize - 1) / PageSize;
|
|
|
|
if (dict["data"] is System.Collections.IEnumerable dataList)
|
|
{
|
|
companies = new List<CompanyDto>();
|
|
foreach (var item in dataList)
|
|
{
|
|
if (item is IDictionary<string, object> companyDict)
|
|
{
|
|
companies.Add(new CompanyDto
|
|
{
|
|
Id = (int)(dynamic)companyDict["id"],
|
|
CompanyCode = (string)companyDict["companyCode"],
|
|
CompanyName = (string)companyDict["companyName"],
|
|
ContactPerson = (string?)companyDict["contactPerson"],
|
|
Phone = (string?)companyDict["phone"],
|
|
Email = (string?)companyDict["email"],
|
|
IsActive = (bool)(dynamic)companyDict["isActive"],
|
|
CreatedAt = DateTime.Parse(companyDict["createdAt"].ToString()!)
|
|
});
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
Snackbar.Add($"고객사 로드 실패: {ex.Message}", Severity.Error);
|
|
}
|
|
finally
|
|
{
|
|
isLoading = false;
|
|
}
|
|
}
|
|
|
|
private async Task NextPage()
|
|
{
|
|
currentPage++;
|
|
await LoadData();
|
|
}
|
|
|
|
private async Task PreviousPage()
|
|
{
|
|
currentPage = Math.Max(1, currentPage - 1);
|
|
await LoadData();
|
|
}
|
|
|
|
private class CompanyDto
|
|
{
|
|
public int Id { get; set; }
|
|
public string CompanyCode { get; set; } = "";
|
|
public string CompanyName { get; set; } = "";
|
|
public string? ContactPerson { get; set; }
|
|
public string? Phone { get; set; }
|
|
public string? Email { get; set; }
|
|
public bool IsActive { get; set; }
|
|
public DateTime CreatedAt { get; set; }
|
|
}
|
|
}
|