feat: implement database-driven Common Code system for admin comboboxes
TaxBaik CI/CD / build-and-deploy (push) Successful in 1m48s

This commit is contained in:
2026-06-30 22:24:04 +09:00
parent 03809bbf26
commit 967a784d6e
11 changed files with 262 additions and 12 deletions
@@ -27,6 +27,7 @@ public static class DependencyInjection
services.AddScoped<RevenueTrackingService>();
services.AddScoped<TelegramReportService>();
services.AddScoped<PortalUserService>();
services.AddScoped<CommonCodeService>();
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);
}
}
+10
View File
@@ -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<IContractRepository, ContractRepository>();
services.AddScoped<IRevenueTrackingRepository, RevenueTrackingRepository>();
services.AddScoped<ICommonCodeRepository, CommonCodeRepository>();
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
@inject ITaxProfileBrowserClient TaxProfileClient
@inject IClientBrowserClient ClientClient
@inject ICommonCodeBrowserClient CommonCodeClient
@inject ISnackbar Snackbar
@inject IDialogService DialogService
@attribute [Authorize]
@@ -91,20 +92,16 @@ else
}
</MudSelect>
<MudSelect T="string" @bind-Value="profileForm.BusinessType" Label="사업 유형" Variant="Variant.Outlined" FullWidth="true" Class="mb-4" Required="true">
<MudSelectItem Value="@("일반제조업")">일반제조업</MudSelectItem>
<MudSelectItem Value="@("도소매업")">도소매업</MudSelectItem>
<MudSelectItem Value="@("서비스업")">서비스업</MudSelectItem>
<MudSelectItem Value="@("정보통신업")">정보통신업</MudSelectItem>
<MudSelectItem Value="@("부동산업")">부동산업</MudSelectItem>
<MudSelectItem Value="@("건설업")">건설업</MudSelectItem>
<MudSelectItem Value="@("음식점업")">음식점업</MudSelectItem>
<MudSelectItem Value="@("프리랜서")">프리랜서</MudSelectItem>
<MudSelectItem Value="@("기타")">기타</MudSelectItem>
@foreach (var type in businessTypes)
{
<MudSelectItem Value="@type.CodeValue">@type.CodeName</MudSelectItem>
}
</MudSelect>
<MudSelect T="string" @bind-Value="profileForm.TaxRiskLevel" Label="위험도" Variant="Variant.Outlined" FullWidth="true" Class="mb-4">
<MudSelectItem Value="@("low")">낮음</MudSelectItem>
<MudSelectItem Value="@("normal")">보통</MudSelectItem>
<MudSelectItem Value="@("high")">높음</MudSelectItem>
@foreach (var level in riskLevels)
{
<MudSelectItem Value="@level.CodeValue">@level.CodeName</MudSelectItem>
}
</MudSelect>
<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" />
@@ -123,6 +120,8 @@ else
private List<TaxProfile>? profiles;
private List<Client> clients = [];
private Dictionary<int, string> clientMap = new();
private List<CommonCode> businessTypes = [];
private List<CommonCode> riskLevels = [];
private MudForm? form;
private bool isDialogOpen;
private bool isEditMode;
@@ -153,6 +152,32 @@ else
var (clientItems, _) = await ClientClient.GetPagedAsync(pageSize: 1000);
clients = clientItems.ToList();
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)
{
@@ -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 });
}
}
}
+5
View File
@@ -256,6 +256,11 @@ builder.Services.AddHttpClient<IRevenueTrackingBrowserClient, RevenueTrackingBro
client.BaseAddress = new Uri(apiBaseUrl);
});
builder.Services.AddHttpClient<ICommonCodeBrowserClient, CommonCodeBrowserClient>(client =>
{
client.BaseAddress = new Uri(apiBaseUrl);
});
// UI & 캐시 (MudBlazor Theme Customization)
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 [];
}
}
}
+48
View File
@@ -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;