From 967a784d6ee949810f7fb993cec5bb2e9d2f4407 Mon Sep 17 00:00:00 2001 From: kjh2064 Date: Tue, 30 Jun 2026 22:24:04 +0900 Subject: [PATCH] feat: implement database-driven Common Code system for admin comboboxes --- TaxBaik.Application/DependencyInjection.cs | 1 + .../Services/CommonCodeService.cs | 20 +++++++ TaxBaik.Domain/Entities/CommonCode.cs | 10 ++++ .../Interfaces/ICommonCodeRepository.cs | 12 ++++ TaxBaik.Infrastructure/DependencyInjection.cs | 1 + .../Repositories/CommonCodeRepository.cs | 33 +++++++++++ .../Components/Admin/Pages/TaxProfiles.razor | 49 ++++++++++++---- .../Controllers/CommonCodeController.cs | 39 +++++++++++++ TaxBaik.Web/Program.cs | 5 ++ .../AdminClients/ICommonCodeBrowserClient.cs | 56 +++++++++++++++++++ db/migrations/V017__CreateCommonCodes.sql | 48 ++++++++++++++++ 11 files changed, 262 insertions(+), 12 deletions(-) create mode 100644 TaxBaik.Application/Services/CommonCodeService.cs create mode 100644 TaxBaik.Domain/Entities/CommonCode.cs create mode 100644 TaxBaik.Domain/Interfaces/ICommonCodeRepository.cs create mode 100644 TaxBaik.Infrastructure/Repositories/CommonCodeRepository.cs create mode 100644 TaxBaik.Web/Controllers/CommonCodeController.cs create mode 100644 TaxBaik.Web/Services/AdminClients/ICommonCodeBrowserClient.cs create mode 100644 db/migrations/V017__CreateCommonCodes.sql diff --git a/TaxBaik.Application/DependencyInjection.cs b/TaxBaik.Application/DependencyInjection.cs index 6c1ca3b..be0f65f 100644 --- a/TaxBaik.Application/DependencyInjection.cs +++ b/TaxBaik.Application/DependencyInjection.cs @@ -27,6 +27,7 @@ public static class DependencyInjection services.AddScoped(); services.AddScoped(); services.AddScoped(); + services.AddScoped(); return services; } } diff --git a/TaxBaik.Application/Services/CommonCodeService.cs b/TaxBaik.Application/Services/CommonCodeService.cs new file mode 100644 index 0000000..1f25338 --- /dev/null +++ b/TaxBaik.Application/Services/CommonCodeService.cs @@ -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> GetByGroupAsync(string codeGroup, CancellationToken ct = default) + { + return await commonCodeRepository.GetByGroupAsync(codeGroup, ct); + } + + public async Task> GetAllActiveAsync(CancellationToken ct = default) + { + return await commonCodeRepository.GetAllActiveAsync(ct); + } +} diff --git a/TaxBaik.Domain/Entities/CommonCode.cs b/TaxBaik.Domain/Entities/CommonCode.cs new file mode 100644 index 0000000..9d9d0ff --- /dev/null +++ b/TaxBaik.Domain/Entities/CommonCode.cs @@ -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; +} diff --git a/TaxBaik.Domain/Interfaces/ICommonCodeRepository.cs b/TaxBaik.Domain/Interfaces/ICommonCodeRepository.cs new file mode 100644 index 0000000..e805585 --- /dev/null +++ b/TaxBaik.Domain/Interfaces/ICommonCodeRepository.cs @@ -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> GetByGroupAsync(string codeGroup, CancellationToken ct = default); + Task> GetAllActiveAsync(CancellationToken ct = default); +} diff --git a/TaxBaik.Infrastructure/DependencyInjection.cs b/TaxBaik.Infrastructure/DependencyInjection.cs index 35e8482..1d4df22 100644 --- a/TaxBaik.Infrastructure/DependencyInjection.cs +++ b/TaxBaik.Infrastructure/DependencyInjection.cs @@ -27,6 +27,7 @@ public static class DependencyInjection services.AddScoped(); services.AddScoped(); services.AddScoped(); + services.AddScoped(); return services; } diff --git a/TaxBaik.Infrastructure/Repositories/CommonCodeRepository.cs b/TaxBaik.Infrastructure/Repositories/CommonCodeRepository.cs new file mode 100644 index 0000000..ea594e0 --- /dev/null +++ b/TaxBaik.Infrastructure/Repositories/CommonCodeRepository.cs @@ -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> GetByGroupAsync(string codeGroup, CancellationToken ct = default) + { + using var conn = Conn(); + return await conn.QueryAsync( + @"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> GetAllActiveAsync(CancellationToken ct = default) + { + using var conn = Conn(); + return await conn.QueryAsync( + @"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"); + } +} diff --git a/TaxBaik.Web/Components/Admin/Pages/TaxProfiles.razor b/TaxBaik.Web/Components/Admin/Pages/TaxProfiles.razor index ef88d0d..e456d1d 100644 --- a/TaxBaik.Web/Components/Admin/Pages/TaxProfiles.razor +++ b/TaxBaik.Web/Components/Admin/Pages/TaxProfiles.razor @@ -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 } - 일반제조업 - 도소매업 - 서비스업 - 정보통신업 - 부동산업 - 건설업 - 음식점업 - 프리랜서 - 기타 + @foreach (var type in businessTypes) + { + @type.CodeName + } - 낮음 - 보통 - 높음 + @foreach (var level in riskLevels) + { + @level.CodeName + } @@ -123,6 +120,8 @@ else private List? profiles; private List clients = []; private Dictionary clientMap = new(); + private List businessTypes = []; + private List 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) { diff --git a/TaxBaik.Web/Controllers/CommonCodeController.cs b/TaxBaik.Web/Controllers/CommonCodeController.cs new file mode 100644 index 0000000..4cdc09b --- /dev/null +++ b/TaxBaik.Web/Controllers/CommonCodeController.cs @@ -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 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 GetByGroup(string group) + { + try + { + var codes = await commonCodeService.GetByGroupAsync(group); + return Ok(codes); + } + catch (Exception ex) + { + return StatusCode(500, new { error = "그룹별 공통코드 조회 실패", message = ex.Message }); + } + } +} diff --git a/TaxBaik.Web/Program.cs b/TaxBaik.Web/Program.cs index 89308fe..e2b0567 100644 --- a/TaxBaik.Web/Program.cs +++ b/TaxBaik.Web/Program.cs @@ -256,6 +256,11 @@ builder.Services.AddHttpClient(client => +{ + client.BaseAddress = new Uri(apiBaseUrl); +}); + // UI & 캐시 (MudBlazor Theme Customization) builder.Services.AddMudServices(config => { diff --git a/TaxBaik.Web/Services/AdminClients/ICommonCodeBrowserClient.cs b/TaxBaik.Web/Services/AdminClients/ICommonCodeBrowserClient.cs new file mode 100644 index 0000000..0fa0498 --- /dev/null +++ b/TaxBaik.Web/Services/AdminClients/ICommonCodeBrowserClient.cs @@ -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> GetAllActiveAsync(CancellationToken ct = default); + Task> GetByGroupAsync(string group, CancellationToken ct = default); +} + +public class CommonCodeBrowserClient(HttpClient httpClient, ITokenStore tokenStore, ILogger 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> GetAllActiveAsync(CancellationToken ct = default) + { + try + { + EnsureAuthHeader(); + return await httpClient.GetFromJsonAsync>($"{BaseUrl}", ct) ?? []; + } + catch (Exception ex) + { + logger.LogError(ex, "Failed to get all active common codes"); + return []; + } + } + + public async Task> GetByGroupAsync(string group, CancellationToken ct = default) + { + try + { + EnsureAuthHeader(); + return await httpClient.GetFromJsonAsync>($"{BaseUrl}/group/{group}", ct) ?? []; + } + catch (Exception ex) + { + logger.LogError(ex, "Failed to get common codes for group {Group}", group); + return []; + } + } +} diff --git a/db/migrations/V017__CreateCommonCodes.sql b/db/migrations/V017__CreateCommonCodes.sql new file mode 100644 index 0000000..af0cd96 --- /dev/null +++ b/db/migrations/V017__CreateCommonCodes.sql @@ -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;