feat: implement database-driven Common Code system for admin comboboxes
TaxBaik CI/CD / build-and-deploy (push) Successful in 1m48s
TaxBaik CI/CD / build-and-deploy (push) Successful in 1m48s
This commit is contained in:
@@ -27,6 +27,7 @@ public static class DependencyInjection
|
|||||||
services.AddScoped<RevenueTrackingService>();
|
services.AddScoped<RevenueTrackingService>();
|
||||||
services.AddScoped<TelegramReportService>();
|
services.AddScoped<TelegramReportService>();
|
||||||
services.AddScoped<PortalUserService>();
|
services.AddScoped<PortalUserService>();
|
||||||
|
services.AddScoped<CommonCodeService>();
|
||||||
return services;
|
return services;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,20 @@
|
|||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using TaxBaik.Domain.Entities;
|
||||||
|
using TaxBaik.Domain.Interfaces;
|
||||||
|
|
||||||
|
namespace TaxBaik.Application.Services;
|
||||||
|
|
||||||
|
public class CommonCodeService(ICommonCodeRepository commonCodeRepository)
|
||||||
|
{
|
||||||
|
public async Task<IEnumerable<CommonCode>> GetByGroupAsync(string codeGroup, CancellationToken ct = default)
|
||||||
|
{
|
||||||
|
return await commonCodeRepository.GetByGroupAsync(codeGroup, ct);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<IEnumerable<CommonCode>> GetAllActiveAsync(CancellationToken ct = default)
|
||||||
|
{
|
||||||
|
return await commonCodeRepository.GetAllActiveAsync(ct);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
namespace TaxBaik.Domain.Entities;
|
||||||
|
|
||||||
|
public class CommonCode
|
||||||
|
{
|
||||||
|
public string CodeGroup { get; set; } = string.Empty;
|
||||||
|
public string CodeValue { get; set; } = string.Empty;
|
||||||
|
public string CodeName { get; set; } = string.Empty;
|
||||||
|
public int SortOrder { get; set; }
|
||||||
|
public bool IsActive { get; set; } = true;
|
||||||
|
}
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using TaxBaik.Domain.Entities;
|
||||||
|
|
||||||
|
namespace TaxBaik.Domain.Interfaces;
|
||||||
|
|
||||||
|
public interface ICommonCodeRepository
|
||||||
|
{
|
||||||
|
Task<IEnumerable<CommonCode>> GetByGroupAsync(string codeGroup, CancellationToken ct = default);
|
||||||
|
Task<IEnumerable<CommonCode>> GetAllActiveAsync(CancellationToken ct = default);
|
||||||
|
}
|
||||||
@@ -27,6 +27,7 @@ public static class DependencyInjection
|
|||||||
services.AddScoped<IConsultingActivityRepository, ConsultingActivityRepository>();
|
services.AddScoped<IConsultingActivityRepository, ConsultingActivityRepository>();
|
||||||
services.AddScoped<IContractRepository, ContractRepository>();
|
services.AddScoped<IContractRepository, ContractRepository>();
|
||||||
services.AddScoped<IRevenueTrackingRepository, RevenueTrackingRepository>();
|
services.AddScoped<IRevenueTrackingRepository, RevenueTrackingRepository>();
|
||||||
|
services.AddScoped<ICommonCodeRepository, CommonCodeRepository>();
|
||||||
|
|
||||||
return services;
|
return services;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,33 @@
|
|||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Data;
|
||||||
|
using System.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using Dapper;
|
||||||
|
using TaxBaik.Domain.Entities;
|
||||||
|
using TaxBaik.Domain.Interfaces;
|
||||||
|
|
||||||
|
namespace TaxBaik.Infrastructure.Repositories;
|
||||||
|
|
||||||
|
public class CommonCodeRepository(IDbConnectionFactory connectionFactory) : BaseRepository(connectionFactory), ICommonCodeRepository
|
||||||
|
{
|
||||||
|
public async Task<IEnumerable<CommonCode>> GetByGroupAsync(string codeGroup, CancellationToken ct = default)
|
||||||
|
{
|
||||||
|
using var conn = Conn();
|
||||||
|
return await conn.QueryAsync<CommonCode>(
|
||||||
|
@"SELECT code_group as CodeGroup, code_value as CodeValue, code_name as CodeName, sort_order as SortOrder, is_active as IsActive
|
||||||
|
FROM common_codes
|
||||||
|
WHERE code_group = @CodeGroup AND is_active = TRUE
|
||||||
|
ORDER BY sort_order",
|
||||||
|
new { CodeGroup = codeGroup });
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<IEnumerable<CommonCode>> GetAllActiveAsync(CancellationToken ct = default)
|
||||||
|
{
|
||||||
|
using var conn = Conn();
|
||||||
|
return await conn.QueryAsync<CommonCode>(
|
||||||
|
@"SELECT code_group as CodeGroup, code_value as CodeValue, code_name as CodeName, sort_order as SortOrder, is_active as IsActive
|
||||||
|
FROM common_codes
|
||||||
|
WHERE is_active = TRUE
|
||||||
|
ORDER BY code_group, sort_order");
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -2,6 +2,7 @@
|
|||||||
@using TaxBaik.Web.Services.AdminClients
|
@using TaxBaik.Web.Services.AdminClients
|
||||||
@inject ITaxProfileBrowserClient TaxProfileClient
|
@inject ITaxProfileBrowserClient TaxProfileClient
|
||||||
@inject IClientBrowserClient ClientClient
|
@inject IClientBrowserClient ClientClient
|
||||||
|
@inject ICommonCodeBrowserClient CommonCodeClient
|
||||||
@inject ISnackbar Snackbar
|
@inject ISnackbar Snackbar
|
||||||
@inject IDialogService DialogService
|
@inject IDialogService DialogService
|
||||||
@attribute [Authorize]
|
@attribute [Authorize]
|
||||||
@@ -91,20 +92,16 @@ else
|
|||||||
}
|
}
|
||||||
</MudSelect>
|
</MudSelect>
|
||||||
<MudSelect T="string" @bind-Value="profileForm.BusinessType" Label="사업 유형" Variant="Variant.Outlined" FullWidth="true" Class="mb-4" Required="true">
|
<MudSelect T="string" @bind-Value="profileForm.BusinessType" Label="사업 유형" Variant="Variant.Outlined" FullWidth="true" Class="mb-4" Required="true">
|
||||||
<MudSelectItem Value="@("일반제조업")">일반제조업</MudSelectItem>
|
@foreach (var type in businessTypes)
|
||||||
<MudSelectItem Value="@("도소매업")">도소매업</MudSelectItem>
|
{
|
||||||
<MudSelectItem Value="@("서비스업")">서비스업</MudSelectItem>
|
<MudSelectItem Value="@type.CodeValue">@type.CodeName</MudSelectItem>
|
||||||
<MudSelectItem Value="@("정보통신업")">정보통신업</MudSelectItem>
|
}
|
||||||
<MudSelectItem Value="@("부동산업")">부동산업</MudSelectItem>
|
|
||||||
<MudSelectItem Value="@("건설업")">건설업</MudSelectItem>
|
|
||||||
<MudSelectItem Value="@("음식점업")">음식점업</MudSelectItem>
|
|
||||||
<MudSelectItem Value="@("프리랜서")">프리랜서</MudSelectItem>
|
|
||||||
<MudSelectItem Value="@("기타")">기타</MudSelectItem>
|
|
||||||
</MudSelect>
|
</MudSelect>
|
||||||
<MudSelect T="string" @bind-Value="profileForm.TaxRiskLevel" Label="위험도" Variant="Variant.Outlined" FullWidth="true" Class="mb-4">
|
<MudSelect T="string" @bind-Value="profileForm.TaxRiskLevel" Label="위험도" Variant="Variant.Outlined" FullWidth="true" Class="mb-4">
|
||||||
<MudSelectItem Value="@("low")">낮음</MudSelectItem>
|
@foreach (var level in riskLevels)
|
||||||
<MudSelectItem Value="@("normal")">보통</MudSelectItem>
|
{
|
||||||
<MudSelectItem Value="@("high")">높음</MudSelectItem>
|
<MudSelectItem Value="@level.CodeValue">@level.CodeName</MudSelectItem>
|
||||||
|
}
|
||||||
</MudSelect>
|
</MudSelect>
|
||||||
<MudDatePicker @bind-Date="profileForm.NextFilingDueDate" Label="다음 신고 예정일" Variant="Variant.Outlined" FullWidth="true" Class="mb-4" />
|
<MudDatePicker @bind-Date="profileForm.NextFilingDueDate" Label="다음 신고 예정일" Variant="Variant.Outlined" FullWidth="true" Class="mb-4" />
|
||||||
<MudTextField T="string" @bind-Value="profileForm.SpecialNotes" Label="특수 사항" Variant="Variant.Outlined" FullWidth="true" Lines="2" />
|
<MudTextField T="string" @bind-Value="profileForm.SpecialNotes" Label="특수 사항" Variant="Variant.Outlined" FullWidth="true" Lines="2" />
|
||||||
@@ -123,6 +120,8 @@ else
|
|||||||
private List<TaxProfile>? profiles;
|
private List<TaxProfile>? profiles;
|
||||||
private List<Client> clients = [];
|
private List<Client> clients = [];
|
||||||
private Dictionary<int, string> clientMap = new();
|
private Dictionary<int, string> clientMap = new();
|
||||||
|
private List<CommonCode> businessTypes = [];
|
||||||
|
private List<CommonCode> riskLevels = [];
|
||||||
private MudForm? form;
|
private MudForm? form;
|
||||||
private bool isDialogOpen;
|
private bool isDialogOpen;
|
||||||
private bool isEditMode;
|
private bool isEditMode;
|
||||||
@@ -153,6 +152,32 @@ else
|
|||||||
var (clientItems, _) = await ClientClient.GetPagedAsync(pageSize: 1000);
|
var (clientItems, _) = await ClientClient.GetPagedAsync(pageSize: 1000);
|
||||||
clients = clientItems.ToList();
|
clients = clientItems.ToList();
|
||||||
clientMap = clients.ToDictionary(c => c.Id, GetClientDisplayName);
|
clientMap = clients.ToDictionary(c => c.Id, GetClientDisplayName);
|
||||||
|
|
||||||
|
businessTypes = await CommonCodeClient.GetByGroupAsync("BUSINESS_TYPE");
|
||||||
|
if (businessTypes.Count == 0)
|
||||||
|
{
|
||||||
|
businessTypes = [
|
||||||
|
new() { CodeValue = "일반제조업", CodeName = "일반제조업" },
|
||||||
|
new() { CodeValue = "도소매업", CodeName = "도소매업" },
|
||||||
|
new() { CodeValue = "서비스업", CodeName = "서비스업" },
|
||||||
|
new() { CodeValue = "정보통신업", CodeName = "정보통신업" },
|
||||||
|
new() { CodeValue = "부동산업", CodeName = "부동산업" },
|
||||||
|
new() { CodeValue = "건설업", CodeName = "건설업" },
|
||||||
|
new() { CodeValue = "음식점업", CodeName = "음식점업" },
|
||||||
|
new() { CodeValue = "프리랜서", CodeName = "프리랜서" },
|
||||||
|
new() { CodeValue = "기타", CodeName = "기타" }
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
riskLevels = await CommonCodeClient.GetByGroupAsync("TAX_RISK_LEVEL");
|
||||||
|
if (riskLevels.Count == 0)
|
||||||
|
{
|
||||||
|
riskLevels = [
|
||||||
|
new() { CodeValue = "low", CodeName = "낮음" },
|
||||||
|
new() { CodeValue = "normal", CodeName = "보통" },
|
||||||
|
new() { CodeValue = "high", CodeName = "높음" }
|
||||||
|
];
|
||||||
|
}
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -0,0 +1,39 @@
|
|||||||
|
using Microsoft.AspNetCore.Authorization;
|
||||||
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
using TaxBaik.Application.Services;
|
||||||
|
|
||||||
|
namespace TaxBaik.Web.Controllers;
|
||||||
|
|
||||||
|
[ApiController]
|
||||||
|
[Route("api/[controller]")]
|
||||||
|
[Authorize]
|
||||||
|
public class CommonCodeController(CommonCodeService commonCodeService) : ControllerBase
|
||||||
|
{
|
||||||
|
[HttpGet]
|
||||||
|
public async Task<IActionResult> GetAllActive()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var codes = await commonCodeService.GetAllActiveAsync();
|
||||||
|
return Ok(codes);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
return StatusCode(500, new { error = "공통코드 조회 실패", message = ex.Message });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpGet("group/{group}")]
|
||||||
|
public async Task<IActionResult> GetByGroup(string group)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var codes = await commonCodeService.GetByGroupAsync(group);
|
||||||
|
return Ok(codes);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
return StatusCode(500, new { error = "그룹별 공통코드 조회 실패", message = ex.Message });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -256,6 +256,11 @@ builder.Services.AddHttpClient<IRevenueTrackingBrowserClient, RevenueTrackingBro
|
|||||||
client.BaseAddress = new Uri(apiBaseUrl);
|
client.BaseAddress = new Uri(apiBaseUrl);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
builder.Services.AddHttpClient<ICommonCodeBrowserClient, CommonCodeBrowserClient>(client =>
|
||||||
|
{
|
||||||
|
client.BaseAddress = new Uri(apiBaseUrl);
|
||||||
|
});
|
||||||
|
|
||||||
// UI & 캐시 (MudBlazor Theme Customization)
|
// UI & 캐시 (MudBlazor Theme Customization)
|
||||||
builder.Services.AddMudServices(config =>
|
builder.Services.AddMudServices(config =>
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -0,0 +1,56 @@
|
|||||||
|
namespace TaxBaik.Web.Services.AdminClients;
|
||||||
|
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Net.Http.Json;
|
||||||
|
using System.Text.Json;
|
||||||
|
using System.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using TaxBaik.Domain.Entities;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
|
||||||
|
public interface ICommonCodeBrowserClient
|
||||||
|
{
|
||||||
|
Task<List<CommonCode>> GetAllActiveAsync(CancellationToken ct = default);
|
||||||
|
Task<List<CommonCode>> GetByGroupAsync(string group, CancellationToken ct = default);
|
||||||
|
}
|
||||||
|
|
||||||
|
public class CommonCodeBrowserClient(HttpClient httpClient, ITokenStore tokenStore, ILogger<CommonCodeBrowserClient> logger) : ICommonCodeBrowserClient
|
||||||
|
{
|
||||||
|
private const string BaseUrl = "/api/commoncode";
|
||||||
|
|
||||||
|
private void EnsureAuthHeader()
|
||||||
|
{
|
||||||
|
if (!string.IsNullOrEmpty(tokenStore.AccessToken))
|
||||||
|
httpClient.DefaultRequestHeaders.Authorization = new("Bearer", tokenStore.AccessToken);
|
||||||
|
else
|
||||||
|
httpClient.DefaultRequestHeaders.Authorization = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<List<CommonCode>> GetAllActiveAsync(CancellationToken ct = default)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
EnsureAuthHeader();
|
||||||
|
return await httpClient.GetFromJsonAsync<List<CommonCode>>($"{BaseUrl}", ct) ?? [];
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
logger.LogError(ex, "Failed to get all active common codes");
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<List<CommonCode>> GetByGroupAsync(string group, CancellationToken ct = default)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
EnsureAuthHeader();
|
||||||
|
return await httpClient.GetFromJsonAsync<List<CommonCode>>($"{BaseUrl}/group/{group}", ct) ?? [];
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
logger.LogError(ex, "Failed to get common codes for group {Group}", group);
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,48 @@
|
|||||||
|
-- Create common_codes table
|
||||||
|
CREATE TABLE IF NOT EXISTS common_codes (
|
||||||
|
code_group VARCHAR(50) NOT NULL,
|
||||||
|
code_value VARCHAR(50) NOT NULL,
|
||||||
|
code_name VARCHAR(100) NOT NULL,
|
||||||
|
sort_order INT DEFAULT 0,
|
||||||
|
is_active BOOLEAN DEFAULT TRUE,
|
||||||
|
PRIMARY KEY (code_group, code_value)
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Seed data for BUSINESS_TYPE
|
||||||
|
INSERT INTO common_codes (code_group, code_value, code_name, sort_order) VALUES
|
||||||
|
('BUSINESS_TYPE', '일반제조업', '일반제조업', 10),
|
||||||
|
('BUSINESS_TYPE', '도소매업', '도소매업', 20),
|
||||||
|
('BUSINESS_TYPE', '서비스업', '서비스업', 30),
|
||||||
|
('BUSINESS_TYPE', '정보통신업', '정보통신업', 40),
|
||||||
|
('BUSINESS_TYPE', '부동산업', '부동산업', 50),
|
||||||
|
('BUSINESS_TYPE', '건설업', '건설업', 60),
|
||||||
|
('BUSINESS_TYPE', '음식점업', '음식점업', 70),
|
||||||
|
('BUSINESS_TYPE', '프리랜서', '프리랜서', 80),
|
||||||
|
('BUSINESS_TYPE', '기타', '기타', 90)
|
||||||
|
ON CONFLICT (code_group, code_value) DO NOTHING;
|
||||||
|
|
||||||
|
-- Seed data for TAX_RISK_LEVEL
|
||||||
|
INSERT INTO common_codes (code_group, code_value, code_name, sort_order) VALUES
|
||||||
|
('TAX_RISK_LEVEL', 'low', '낮음', 10),
|
||||||
|
('TAX_RISK_LEVEL', 'normal', '보통', 20),
|
||||||
|
('TAX_RISK_LEVEL', 'high', '높음', 30)
|
||||||
|
ON CONFLICT (code_group, code_value) DO NOTHING;
|
||||||
|
|
||||||
|
-- Seed data for FILING_TYPE
|
||||||
|
INSERT INTO common_codes (code_group, code_value, code_name, sort_order) VALUES
|
||||||
|
('FILING_TYPE', '종합소득세', '종합소득세', 10),
|
||||||
|
('FILING_TYPE', '부가가치세', '부가가치세', 20),
|
||||||
|
('FILING_TYPE', '법인세', '법인세', 30),
|
||||||
|
('FILING_TYPE', '원천세', '원천세', 40),
|
||||||
|
('FILING_TYPE', '양도소득세', '양도소득세', 50),
|
||||||
|
('FILING_TYPE', '상속/증여세', '상속/증여세', 60)
|
||||||
|
ON CONFLICT (code_group, code_value) DO NOTHING;
|
||||||
|
|
||||||
|
-- Seed data for SERVICE_TYPE
|
||||||
|
INSERT INTO common_codes (code_group, code_value, code_name, sort_order) VALUES
|
||||||
|
('SERVICE_TYPE', '개인 기장대리', '개인 기장대리', 10),
|
||||||
|
('SERVICE_TYPE', '법인 기장대리', '법인 기장대리', 20),
|
||||||
|
('SERVICE_TYPE', '세무조정', '세무조정', 30),
|
||||||
|
('SERVICE_TYPE', '세무컨설팅', '세무컨설팅', 40),
|
||||||
|
('SERVICE_TYPE', '불복청구', '불복청구', 50)
|
||||||
|
ON CONFLICT (code_group, code_value) DO NOTHING;
|
||||||
Reference in New Issue
Block a user