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
96 lines
4.2 KiB
C#
96 lines
4.2 KiB
C#
namespace TaxBaik.Application.Services;
|
|
|
|
using TaxBaik.Domain.Entities;
|
|
using TaxBaik.Domain.Interfaces;
|
|
|
|
public class CompanyService(ICompanyRepository repository)
|
|
{
|
|
public async Task<int> CreateAsync(string companyCode, string companyName, string? contactPerson = null,
|
|
string? phone = null, string? email = null, string? memo = null, CancellationToken ct = default)
|
|
{
|
|
if (string.IsNullOrWhiteSpace(companyCode))
|
|
throw new ValidationException("회사 코드를 입력하세요.");
|
|
|
|
if (string.IsNullOrWhiteSpace(companyName))
|
|
throw new ValidationException("회사명을 입력하세요.");
|
|
|
|
var existing = await repository.GetByCodeAsync(companyCode.Trim(), ct);
|
|
if (existing != null)
|
|
throw new ValidationException("이미 존재하는 회사 코드입니다.");
|
|
|
|
var company = new Company
|
|
{
|
|
CompanyCode = companyCode.Trim(),
|
|
CompanyName = companyName.Trim(),
|
|
ContactPerson = string.IsNullOrWhiteSpace(contactPerson) ? null : contactPerson.Trim(),
|
|
Phone = string.IsNullOrWhiteSpace(phone) ? null : phone.Trim(),
|
|
Email = string.IsNullOrWhiteSpace(email) ? null : email.Trim(),
|
|
Memo = string.IsNullOrWhiteSpace(memo) ? null : memo.Trim(),
|
|
IsActive = true,
|
|
CreatedAt = DateTime.UtcNow,
|
|
UpdatedAt = DateTime.UtcNow
|
|
};
|
|
|
|
return await repository.CreateAsync(company, ct);
|
|
}
|
|
|
|
public async Task<Company?> GetByIdAsync(int id, CancellationToken ct = default) =>
|
|
await repository.GetByIdAsync(id, ct);
|
|
|
|
public async Task<Company?> GetByCodeAsync(string code, CancellationToken ct = default) =>
|
|
await repository.GetByCodeAsync(code, ct);
|
|
|
|
public async Task<IEnumerable<Company>> GetAllActiveAsync(CancellationToken ct = default) =>
|
|
await repository.GetAllActiveAsync(ct);
|
|
|
|
public async Task<(IEnumerable<Company>, int)> GetPagedAsync(int page, int pageSize, CancellationToken ct = default)
|
|
{
|
|
var (items, total) = await repository.GetPagedAsync(NormalizePage(page), NormalizePageSize(pageSize), ct);
|
|
return (items, total);
|
|
}
|
|
|
|
public async Task UpdateAsync(int id, string companyCode, string companyName, string? contactPerson = null,
|
|
string? phone = null, string? email = null, string? memo = null, bool isActive = true, CancellationToken ct = default)
|
|
{
|
|
if (string.IsNullOrWhiteSpace(companyCode))
|
|
throw new ValidationException("회사 코드를 입력하세요.");
|
|
|
|
if (string.IsNullOrWhiteSpace(companyName))
|
|
throw new ValidationException("회사명을 입력하세요.");
|
|
|
|
var company = await repository.GetByIdAsync(id, ct);
|
|
if (company == null)
|
|
throw new ValidationException("회사를 찾을 수 없습니다.");
|
|
|
|
var existing = await repository.GetByCodeAsync(companyCode.Trim(), ct);
|
|
if (existing != null && existing.Id != id)
|
|
throw new ValidationException("이미 존재하는 회사 코드입니다.");
|
|
|
|
company.CompanyCode = companyCode.Trim();
|
|
company.CompanyName = companyName.Trim();
|
|
company.ContactPerson = string.IsNullOrWhiteSpace(contactPerson) ? null : contactPerson.Trim();
|
|
company.Phone = string.IsNullOrWhiteSpace(phone) ? null : phone.Trim();
|
|
company.Email = string.IsNullOrWhiteSpace(email) ? null : email.Trim();
|
|
company.Memo = string.IsNullOrWhiteSpace(memo) ? null : memo.Trim();
|
|
company.IsActive = isActive;
|
|
company.UpdatedAt = DateTime.UtcNow;
|
|
|
|
await repository.UpdateAsync(company, ct);
|
|
}
|
|
|
|
public async Task DeleteAsync(int id, CancellationToken ct = default)
|
|
{
|
|
var company = await repository.GetByIdAsync(id, ct);
|
|
if (company == null)
|
|
throw new ValidationException("회사를 찾을 수 없습니다.");
|
|
|
|
if (company.CompanyCode == "DEFAULT")
|
|
throw new ValidationException("기본 회사는 삭제할 수 없습니다.");
|
|
|
|
await repository.DeleteAsync(id, ct);
|
|
}
|
|
|
|
private static int NormalizePage(int page) => Math.Max(1, page);
|
|
private static int NormalizePageSize(int pageSize) => Math.Clamp(pageSize, 1, 100);
|
|
}
|