Compare commits
3 Commits
df4c555dd1
...
1fc3b6c0a4
| Author | SHA1 | Date | |
|---|---|---|---|
| 1fc3b6c0a4 | |||
| da9f49c973 | |||
| 1839c2c3d1 |
@@ -1,6 +1,7 @@
|
|||||||
name: TaxBaik CI/CD
|
name: TaxBaik CI/CD
|
||||||
|
|
||||||
on:
|
on:
|
||||||
|
workflow_dispatch:
|
||||||
push:
|
push:
|
||||||
branches:
|
branches:
|
||||||
- master
|
- master
|
||||||
|
|||||||
@@ -0,0 +1,34 @@
|
|||||||
|
namespace TaxBaik.Application.Tests;
|
||||||
|
|
||||||
|
using TaxBaik.Web.Components.Admin.Shared;
|
||||||
|
using Xunit;
|
||||||
|
|
||||||
|
public class BusinessDayCalculatorTests
|
||||||
|
{
|
||||||
|
[Theory]
|
||||||
|
[InlineData(2026, 2, 14, 2026, 2, 19)]
|
||||||
|
[InlineData(2026, 8, 15, 2026, 8, 20)]
|
||||||
|
[InlineData(2026, 9, 24, 2026, 9, 29)]
|
||||||
|
[InlineData(2026, 10, 3, 2026, 10, 8)]
|
||||||
|
public void GetEffectiveDueDate_SkipsWeekendHolidayAndSubstituteHoliday(
|
||||||
|
int dueYear, int dueMonth, int dueDay,
|
||||||
|
int expectedYear, int expectedMonth, int expectedDay)
|
||||||
|
{
|
||||||
|
var effective = BusinessDayCalculator.GetEffectiveDueDate(new DateOnly(dueYear, dueMonth, dueDay));
|
||||||
|
|
||||||
|
Assert.Equal(new DateOnly(expectedYear, expectedMonth, expectedDay), effective);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[InlineData(2026, 2, 19, 0)]
|
||||||
|
[InlineData(2026, 2, 20, -1)]
|
||||||
|
[InlineData(2026, 2, 18, 1)]
|
||||||
|
public void GetDday_UsesEffectiveDueDate(
|
||||||
|
int refYear, int refMonth, int refDay,
|
||||||
|
int expectedDays)
|
||||||
|
{
|
||||||
|
var dday = BusinessDayCalculator.GetDday(new DateOnly(2026, 2, 14), new DateOnly(refYear, refMonth, refDay));
|
||||||
|
|
||||||
|
Assert.Equal(expectedDays, dday);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -18,5 +18,6 @@
|
|||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<ProjectReference Include="..\TaxBaik.Application\TaxBaik.Application.csproj" />
|
<ProjectReference Include="..\TaxBaik.Application\TaxBaik.Application.csproj" />
|
||||||
|
<ProjectReference Include="..\TaxBaik.Web\TaxBaik.Web.csproj" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
</Project>
|
</Project>
|
||||||
|
|||||||
@@ -8,6 +8,11 @@ namespace TaxBaik.Application.Services;
|
|||||||
|
|
||||||
public class CommonCodeService(ICommonCodeRepository commonCodeRepository)
|
public class CommonCodeService(ICommonCodeRepository commonCodeRepository)
|
||||||
{
|
{
|
||||||
|
public async Task<IEnumerable<string>> GetAllGroupsAsync(CancellationToken ct = default)
|
||||||
|
{
|
||||||
|
return await commonCodeRepository.GetAllGroupsAsync(ct);
|
||||||
|
}
|
||||||
|
|
||||||
public async Task<IEnumerable<CommonCode>> GetByGroupAsync(string codeGroup, CancellationToken ct = default)
|
public async Task<IEnumerable<CommonCode>> GetByGroupAsync(string codeGroup, CancellationToken ct = default)
|
||||||
{
|
{
|
||||||
return await commonCodeRepository.GetByGroupAsync(codeGroup, ct);
|
return await commonCodeRepository.GetByGroupAsync(codeGroup, ct);
|
||||||
@@ -17,4 +22,27 @@ public class CommonCodeService(ICommonCodeRepository commonCodeRepository)
|
|||||||
{
|
{
|
||||||
return await commonCodeRepository.GetAllActiveAsync(ct);
|
return await commonCodeRepository.GetAllActiveAsync(ct);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async Task<CommonCode?> GetAsync(string codeGroup, string codeValue, CancellationToken ct = default)
|
||||||
|
{
|
||||||
|
return await commonCodeRepository.GetAsync(codeGroup, codeValue, ct);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task UpsertAsync(CommonCode code, CancellationToken ct = default)
|
||||||
|
{
|
||||||
|
Normalize(code);
|
||||||
|
await commonCodeRepository.UpsertAsync(code, ct);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task DeleteAsync(string codeGroup, string codeValue, CancellationToken ct = default)
|
||||||
|
{
|
||||||
|
await commonCodeRepository.DeleteAsync(codeGroup.Trim(), codeValue.Trim(), ct);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void Normalize(CommonCode code)
|
||||||
|
{
|
||||||
|
code.CodeGroup = code.CodeGroup.Trim();
|
||||||
|
code.CodeValue = code.CodeValue.Trim();
|
||||||
|
code.CodeName = code.CodeName.Trim();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,6 +7,10 @@ namespace TaxBaik.Domain.Interfaces;
|
|||||||
|
|
||||||
public interface ICommonCodeRepository
|
public interface ICommonCodeRepository
|
||||||
{
|
{
|
||||||
|
Task<IEnumerable<string>> GetAllGroupsAsync(CancellationToken ct = default);
|
||||||
Task<IEnumerable<CommonCode>> GetByGroupAsync(string codeGroup, CancellationToken ct = default);
|
Task<IEnumerable<CommonCode>> GetByGroupAsync(string codeGroup, CancellationToken ct = default);
|
||||||
Task<IEnumerable<CommonCode>> GetAllActiveAsync(CancellationToken ct = default);
|
Task<IEnumerable<CommonCode>> GetAllActiveAsync(CancellationToken ct = default);
|
||||||
|
Task<CommonCode?> GetAsync(string codeGroup, string codeValue, CancellationToken ct = default);
|
||||||
|
Task UpsertAsync(CommonCode code, CancellationToken ct = default);
|
||||||
|
Task DeleteAsync(string codeGroup, string codeValue, CancellationToken ct = default);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,6 +10,13 @@ namespace TaxBaik.Infrastructure.Repositories;
|
|||||||
|
|
||||||
public class CommonCodeRepository(IDbConnectionFactory connectionFactory) : BaseRepository(connectionFactory), ICommonCodeRepository
|
public class CommonCodeRepository(IDbConnectionFactory connectionFactory) : BaseRepository(connectionFactory), ICommonCodeRepository
|
||||||
{
|
{
|
||||||
|
public async Task<IEnumerable<string>> GetAllGroupsAsync(CancellationToken ct = default)
|
||||||
|
{
|
||||||
|
using var conn = Conn();
|
||||||
|
return await conn.QueryAsync<string>(
|
||||||
|
"SELECT DISTINCT code_group FROM common_codes WHERE is_active = TRUE ORDER BY code_group");
|
||||||
|
}
|
||||||
|
|
||||||
public async Task<IEnumerable<CommonCode>> GetByGroupAsync(string codeGroup, CancellationToken ct = default)
|
public async Task<IEnumerable<CommonCode>> GetByGroupAsync(string codeGroup, CancellationToken ct = default)
|
||||||
{
|
{
|
||||||
using var conn = Conn();
|
using var conn = Conn();
|
||||||
@@ -30,4 +37,36 @@ public class CommonCodeRepository(IDbConnectionFactory connectionFactory) : Base
|
|||||||
WHERE is_active = TRUE
|
WHERE is_active = TRUE
|
||||||
ORDER BY code_group, sort_order");
|
ORDER BY code_group, sort_order");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async Task<CommonCode?> GetAsync(string codeGroup, string codeValue, CancellationToken ct = default)
|
||||||
|
{
|
||||||
|
using var conn = Conn();
|
||||||
|
return await conn.QuerySingleOrDefaultAsync<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 code_value = @CodeValue",
|
||||||
|
new { CodeGroup = codeGroup, CodeValue = codeValue });
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task UpsertAsync(CommonCode code, CancellationToken ct = default)
|
||||||
|
{
|
||||||
|
using var conn = Conn();
|
||||||
|
await conn.ExecuteAsync(
|
||||||
|
@"INSERT INTO common_codes (code_group, code_value, code_name, sort_order, is_active)
|
||||||
|
VALUES (@CodeGroup, @CodeValue, @CodeName, @SortOrder, @IsActive)
|
||||||
|
ON CONFLICT (code_group, code_value) DO UPDATE
|
||||||
|
SET code_name = EXCLUDED.code_name,
|
||||||
|
sort_order = EXCLUDED.sort_order,
|
||||||
|
is_active = EXCLUDED.is_active",
|
||||||
|
code);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task DeleteAsync(string codeGroup, string codeValue, CancellationToken ct = default)
|
||||||
|
{
|
||||||
|
using var conn = Conn();
|
||||||
|
await conn.ExecuteAsync(
|
||||||
|
@"DELETE FROM common_codes
|
||||||
|
WHERE code_group = @CodeGroup AND code_value = @CodeValue",
|
||||||
|
new { CodeGroup = codeGroup, CodeValue = codeValue });
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,8 +10,12 @@ using Microsoft.Extensions.Logging;
|
|||||||
|
|
||||||
public interface ICommonCodeBrowserClient
|
public interface ICommonCodeBrowserClient
|
||||||
{
|
{
|
||||||
|
Task<List<string>> GetGroupsAsync(CancellationToken ct = default);
|
||||||
Task<List<CommonCode>> GetAllActiveAsync(CancellationToken ct = default);
|
Task<List<CommonCode>> GetAllActiveAsync(CancellationToken ct = default);
|
||||||
Task<List<CommonCode>> GetByGroupAsync(string group, CancellationToken ct = default);
|
Task<List<CommonCode>> GetByGroupAsync(string group, CancellationToken ct = default);
|
||||||
|
Task<CommonCode?> GetAsync(string group, string value, CancellationToken ct = default);
|
||||||
|
Task<bool> UpsertAsync(CommonCode code, CancellationToken ct = default);
|
||||||
|
Task<bool> DeleteAsync(string group, string value, CancellationToken ct = default);
|
||||||
}
|
}
|
||||||
|
|
||||||
public class CommonCodeBrowserClient(HttpClient httpClient, ITokenStore tokenStore, ILogger<CommonCodeBrowserClient> logger) : ICommonCodeBrowserClient
|
public class CommonCodeBrowserClient(HttpClient httpClient, ITokenStore tokenStore, ILogger<CommonCodeBrowserClient> logger) : ICommonCodeBrowserClient
|
||||||
@@ -53,4 +57,62 @@ public class CommonCodeBrowserClient(HttpClient httpClient, ITokenStore tokenSto
|
|||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async Task<List<string>> GetGroupsAsync(CancellationToken ct = default)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
EnsureAuthHeader();
|
||||||
|
return await httpClient.GetFromJsonAsync<List<string>>($"{BaseUrl}/groups", ct) ?? [];
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
logger.LogError(ex, "Failed to get common code groups");
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<CommonCode?> GetAsync(string group, string value, CancellationToken ct = default)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
EnsureAuthHeader();
|
||||||
|
return await httpClient.GetFromJsonAsync<CommonCode>($"{BaseUrl}/{group}/{value}", ct);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
logger.LogError(ex, "Failed to get common code {Group}/{Value}", group, value);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<bool> UpsertAsync(CommonCode code, CancellationToken ct = default)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
EnsureAuthHeader();
|
||||||
|
var response = await httpClient.PostAsJsonAsync(BaseUrl, code, ct);
|
||||||
|
return response.IsSuccessStatusCode;
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
logger.LogError(ex, "Failed to upsert common code {Group}/{Value}", code.CodeGroup, code.CodeValue);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<bool> DeleteAsync(string group, string value, CancellationToken ct = default)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
EnsureAuthHeader();
|
||||||
|
var response = await httpClient.DeleteAsync($"{BaseUrl}/{group}/{value}", ct);
|
||||||
|
return response.IsSuccessStatusCode;
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
logger.LogError(ex, "Failed to delete common code {Group}/{Value}", group, value);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
@inject IJSRuntime JS
|
@inject IJSRuntime JS
|
||||||
@inject VersionInfo VersionInfo
|
@inject VersionInfo VersionInfo
|
||||||
@implements IDisposable
|
@implements IDisposable
|
||||||
@rendermode @(new InteractiveWebAssemblyRenderMode(prerender: false))
|
@rendermode @(new InteractiveWebAssemblyRenderMode(prerender: true))
|
||||||
|
|
||||||
<MudPopoverProvider />
|
<MudPopoverProvider />
|
||||||
<MudDialogProvider />
|
<MudDialogProvider />
|
||||||
@@ -88,6 +88,7 @@
|
|||||||
|
|
||||||
<MudNavLink Href="/taxbaik/admin/inquiries" Icon="@Icons.Material.Filled.Forum">문의 관리</MudNavLink>
|
<MudNavLink Href="/taxbaik/admin/inquiries" Icon="@Icons.Material.Filled.Forum">문의 관리</MudNavLink>
|
||||||
<MudNavLink Href="/taxbaik/admin/settings" Icon="@Icons.Material.Filled.Tune">설정</MudNavLink>
|
<MudNavLink Href="/taxbaik/admin/settings" Icon="@Icons.Material.Filled.Tune">설정</MudNavLink>
|
||||||
|
<MudNavLink Href="/taxbaik/admin/common-codes" Icon="@Icons.Material.Filled.Category">공통관리</MudNavLink>
|
||||||
</MudNavMenu>
|
</MudNavMenu>
|
||||||
|
|
||||||
<div class="admin-drawer-version">
|
<div class="admin-drawer-version">
|
||||||
@@ -127,7 +128,7 @@
|
|||||||
|
|
||||||
private void OnLocationChanged(object? sender, LocationChangedEventArgs args)
|
private void OnLocationChanged(object? sender, LocationChangedEventArgs args)
|
||||||
{
|
{
|
||||||
_ = InvokeAsync(() => JS.InvokeVoidAsync("taxbaikAdminSession.showLoading"));
|
_ = InvokeAsync(() => JS.InvokeVoidAsync("taxbaikAdminSession.hideLoading"));
|
||||||
}
|
}
|
||||||
|
|
||||||
private void ToggleDrawer()
|
private void ToggleDrawer()
|
||||||
|
|||||||
@@ -1,19 +1,16 @@
|
|||||||
@page "/admin/blog"
|
@page "/admin/blog"
|
||||||
@attribute [Authorize]
|
@attribute [Authorize]
|
||||||
@inject IApiClient ApiClient
|
@inject IBlogBrowserClient BlogClient
|
||||||
@inject ISnackbar Snackbar
|
@inject ISnackbar Snackbar
|
||||||
|
|
||||||
<PageTitle>블로그 관리</PageTitle>
|
<PageTitle>블로그 관리</PageTitle>
|
||||||
|
|
||||||
<section class="admin-page-hero">
|
<AdminPageHeader Title="블로그 관리" Eyebrow="Content" Subtitle="검색 유입 콘텐츠의 발행 상태와 성과를 관리합니다.">
|
||||||
<div>
|
<ChildContent>
|
||||||
<MudText Typo="Typo.caption" Class="admin-eyebrow">Content</MudText>
|
<MudButton Variant="Variant.Filled" Color="Color.Primary" StartIcon="@Icons.Material.Filled.EditNote"
|
||||||
<MudText Typo="Typo.h4" Class="admin-page-title">블로그 관리</MudText>
|
Href="/taxbaik/admin/blog/create">새 포스트 작성</MudButton>
|
||||||
<MudText Typo="Typo.body2" Class="admin-page-subtitle">검색 유입 콘텐츠의 발행 상태와 성과를 관리합니다.</MudText>
|
</ChildContent>
|
||||||
</div>
|
</AdminPageHeader>
|
||||||
<MudButton Variant="Variant.Filled" Color="Color.Primary" StartIcon="@Icons.Material.Filled.EditNote"
|
|
||||||
Href="/taxbaik/admin/blog/create">새 포스트 작성</MudButton>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<div class="d-flex pa-4 gap-4 align-center">
|
<div class="d-flex pa-4 gap-4 align-center">
|
||||||
<MudTextField @bind-Value="searchQuery" Placeholder="블로그 제목 또는 본문 검색..." Adornment="Adornment.Start"
|
<MudTextField @bind-Value="searchQuery" Placeholder="블로그 제목 또는 본문 검색..." Adornment="Adornment.Start"
|
||||||
@@ -58,7 +55,7 @@
|
|||||||
[CascadingParameter]
|
[CascadingParameter]
|
||||||
private Task<AuthenticationState>? AuthStateTask { get; set; }
|
private Task<AuthenticationState>? AuthStateTask { get; set; }
|
||||||
|
|
||||||
private List<TaxBaik.Domain.Entities.BlogPost> posts = [];
|
private List<TaxBaik.Application.DTOs.BlogPostResponseDto> posts = [];
|
||||||
private string searchQuery = "";
|
private string searchQuery = "";
|
||||||
private bool isLoading = true;
|
private bool isLoading = true;
|
||||||
private int currentPage = 1;
|
private int currentPage = 1;
|
||||||
@@ -66,23 +63,19 @@
|
|||||||
private int totalPosts = 0;
|
private int totalPosts = 0;
|
||||||
private const int PageSize = 20;
|
private const int PageSize = 20;
|
||||||
|
|
||||||
private IEnumerable<TaxBaik.Domain.Entities.BlogPost> FilteredPosts => posts?
|
private IEnumerable<TaxBaik.Application.DTOs.BlogPostResponseDto> FilteredPosts => posts
|
||||||
.Where(p => string.IsNullOrEmpty(searchQuery) ||
|
.Where(p => string.IsNullOrEmpty(searchQuery) ||
|
||||||
p.Title.Contains(searchQuery, StringComparison.OrdinalIgnoreCase) ||
|
p.Title.Contains(searchQuery, StringComparison.OrdinalIgnoreCase) ||
|
||||||
(p.Content != null && p.Content.Contains(searchQuery, StringComparison.OrdinalIgnoreCase))) ?? Enumerable.Empty<TaxBaik.Domain.Entities.BlogPost>();
|
(p.Content != null && p.Content.Contains(searchQuery, StringComparison.OrdinalIgnoreCase)));
|
||||||
|
|
||||||
protected override async Task OnAfterRenderAsync(bool firstRender)
|
protected override async Task OnInitializedAsync()
|
||||||
{
|
{
|
||||||
if (firstRender)
|
if (AuthStateTask != null)
|
||||||
{
|
{
|
||||||
if (AuthStateTask != null)
|
var authState = await AuthStateTask;
|
||||||
|
if (authState.User.Identity?.IsAuthenticated == true)
|
||||||
{
|
{
|
||||||
var authState = await AuthStateTask;
|
await LoadPosts();
|
||||||
if (authState.User.Identity?.IsAuthenticated == true)
|
|
||||||
{
|
|
||||||
await LoadPosts();
|
|
||||||
StateHasChanged();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -92,9 +85,9 @@
|
|||||||
isLoading = true;
|
isLoading = true;
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
var result = await ApiClient.GetAsync<PagedBlogResponse>($"blog/admin?page={currentPage}&pageSize={PageSize}");
|
var result = await BlogClient.GetAdminPagedAsync(currentPage, PageSize);
|
||||||
posts = result?.Data ?? [];
|
posts = result.Items.ToList();
|
||||||
totalPosts = result?.Total ?? 0;
|
totalPosts = result.Total;
|
||||||
totalPages = Math.Max(1, (int)Math.Ceiling(totalPosts / (double)PageSize));
|
totalPages = Math.Max(1, (int)Math.Ceiling(totalPosts / (double)PageSize));
|
||||||
}
|
}
|
||||||
catch
|
catch
|
||||||
@@ -124,21 +117,21 @@
|
|||||||
await LoadPosts();
|
await LoadPosts();
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task TogglePublish(TaxBaik.Domain.Entities.BlogPost post, bool isPublished)
|
private async Task TogglePublish(TaxBaik.Application.DTOs.BlogPostResponseDto post, bool isPublished)
|
||||||
{
|
{
|
||||||
var previous = post.IsPublished;
|
var previous = post.IsPublished;
|
||||||
post.IsPublished = isPublished;
|
post.IsPublished = isPublished;
|
||||||
var result = await ApiClient.PutAsync<TaxBaik.Domain.Entities.BlogPost>($"blog/{post.Id}", new
|
var result = await BlogClient.UpdateAsync(post.Id, new TaxBaik.Application.DTOs.CreateBlogPostDto
|
||||||
{
|
{
|
||||||
post.Title,
|
Title = post.Title,
|
||||||
post.Content,
|
Content = post.Content,
|
||||||
post.CategoryId,
|
CategoryId = post.CategoryId,
|
||||||
post.Tags,
|
Tags = post.Tags,
|
||||||
post.SeoTitle,
|
SeoTitle = post.SeoTitle,
|
||||||
post.SeoDescription,
|
SeoDescription = post.SeoDescription,
|
||||||
post.ThumbnailUrl,
|
ThumbnailUrl = post.ThumbnailUrl,
|
||||||
IsPublished = isPublished,
|
IsPublished = isPublished,
|
||||||
post.AuthorId
|
AuthorId = post.AuthorId
|
||||||
});
|
});
|
||||||
|
|
||||||
if (result == null)
|
if (result == null)
|
||||||
@@ -153,14 +146,13 @@
|
|||||||
|
|
||||||
private async Task DeletePost(int postId)
|
private async Task DeletePost(int postId)
|
||||||
{
|
{
|
||||||
await ApiClient.DeleteAsync($"blog/{postId}");
|
var deleted = await BlogClient.DeleteAsync(postId);
|
||||||
|
if (!deleted)
|
||||||
|
{
|
||||||
|
Snackbar.Add("포스트 삭제에 실패했습니다.", Severity.Error);
|
||||||
|
return;
|
||||||
|
}
|
||||||
Snackbar.Add("포스트가 삭제되었습니다.", Severity.Success);
|
Snackbar.Add("포스트가 삭제되었습니다.", Severity.Success);
|
||||||
await LoadPosts();
|
await LoadPosts();
|
||||||
}
|
}
|
||||||
|
|
||||||
private class PagedBlogResponse
|
|
||||||
{
|
|
||||||
public List<TaxBaik.Domain.Entities.BlogPost> Data { get; set; } = [];
|
|
||||||
public int Total { get; set; }
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,177 @@
|
|||||||
|
@page "/admin/common-codes"
|
||||||
|
@using TaxBaik.Web.Services.AdminClients
|
||||||
|
@using TaxBaik.Domain.Entities
|
||||||
|
@attribute [Authorize]
|
||||||
|
@inject ICommonCodeBrowserClient CommonCodeClient
|
||||||
|
@inject ISnackbar Snackbar
|
||||||
|
|
||||||
|
<PageTitle>공통관리</PageTitle>
|
||||||
|
|
||||||
|
<section class="admin-page-hero">
|
||||||
|
<div>
|
||||||
|
<MudText Typo="Typo.caption" Class="admin-eyebrow">System</MudText>
|
||||||
|
<MudText Typo="Typo.h4" Class="admin-page-title">공통관리</MudText>
|
||||||
|
<MudText Typo="Typo.body2" Class="admin-page-subtitle">공통코드 그룹과 항목을 일관된 기준으로 관리합니다.</MudText>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<MudGrid Spacing="2">
|
||||||
|
<MudItem XS="12" MD="4">
|
||||||
|
<MudPaper Class="admin-surface pa-4" Elevation="0">
|
||||||
|
<MudText Typo="Typo.h6" Class="mb-3">그룹</MudText>
|
||||||
|
<MudSelect T="string" Value="@selectedGroup" ValueChanged="OnGroupChanged" Label="코드 그룹" Variant="Variant.Outlined" FullWidth="true">
|
||||||
|
@foreach (var group in groups)
|
||||||
|
{
|
||||||
|
<MudSelectItem Value="@group">@group</MudSelectItem>
|
||||||
|
}
|
||||||
|
</MudSelect>
|
||||||
|
<MudButton Class="mt-3" Variant="Variant.Filled" Color="Color.Primary" OnClick="PrepareCreate">새 코드 추가</MudButton>
|
||||||
|
</MudPaper>
|
||||||
|
</MudItem>
|
||||||
|
|
||||||
|
<MudItem XS="12" MD="8">
|
||||||
|
<MudPaper Class="admin-surface pa-4" Elevation="0">
|
||||||
|
@if (isLoading)
|
||||||
|
{
|
||||||
|
<MudProgressLinear Indeterminate="true" />
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
<MudTable Items="@codes" Dense="true" Hover="true">
|
||||||
|
<HeaderContent>
|
||||||
|
<MudTh>그룹</MudTh>
|
||||||
|
<MudTh>값</MudTh>
|
||||||
|
<MudTh>이름</MudTh>
|
||||||
|
<MudTh>순서</MudTh>
|
||||||
|
<MudTh>상태</MudTh>
|
||||||
|
<MudTh>작업</MudTh>
|
||||||
|
</HeaderContent>
|
||||||
|
<RowTemplate>
|
||||||
|
<MudTd>@context.CodeGroup</MudTd>
|
||||||
|
<MudTd>@context.CodeValue</MudTd>
|
||||||
|
<MudTd>@context.CodeName</MudTd>
|
||||||
|
<MudTd>@context.SortOrder</MudTd>
|
||||||
|
<MudTd>@(context.IsActive ? "활성" : "비활성")</MudTd>
|
||||||
|
<MudTd>
|
||||||
|
<MudButton Size="Size.Small" Variant="Variant.Text" OnClick="@(() => EditCode(context))">수정</MudButton>
|
||||||
|
<MudButton Size="Size.Small" Variant="Variant.Text" Color="Color.Error" OnClick="@(() => DeleteCode(context))">삭제</MudButton>
|
||||||
|
</MudTd>
|
||||||
|
</RowTemplate>
|
||||||
|
</MudTable>
|
||||||
|
|
||||||
|
<MudDivider Class="my-4" />
|
||||||
|
|
||||||
|
<MudForm @ref="form">
|
||||||
|
<MudTextField @bind-Value="editModel.CodeGroup" Label="그룹" Variant="Variant.Outlined" FullWidth="true" Required="true" Disabled="@(!isCreateMode)" Class="mb-3" />
|
||||||
|
<MudTextField @bind-Value="editModel.CodeValue" Label="값" Variant="Variant.Outlined" FullWidth="true" Required="true" Disabled="@(!isCreateMode)" Class="mb-3" />
|
||||||
|
<MudTextField @bind-Value="editModel.CodeName" Label="이름" Variant="Variant.Outlined" FullWidth="true" Required="true" Class="mb-3" />
|
||||||
|
<MudNumericField T="int" @bind-Value="editModel.SortOrder" Label="순서" Variant="Variant.Outlined" FullWidth="true" Class="mb-3" />
|
||||||
|
<MudSwitch @bind-Checked="editModel.IsActive" Color="Color.Primary">활성</MudSwitch>
|
||||||
|
<div class="d-flex gap-2 mt-4">
|
||||||
|
<MudButton Variant="Variant.Filled" Color="Color.Primary" OnClick="SaveCode">저장</MudButton>
|
||||||
|
<MudButton Variant="Variant.Outlined" OnClick="PrepareCreate">초기화</MudButton>
|
||||||
|
</div>
|
||||||
|
</MudForm>
|
||||||
|
}
|
||||||
|
</MudPaper>
|
||||||
|
</MudItem>
|
||||||
|
</MudGrid>
|
||||||
|
|
||||||
|
@code {
|
||||||
|
private List<string> groups = [];
|
||||||
|
private List<CommonCode> codes = [];
|
||||||
|
private string selectedGroup = "";
|
||||||
|
private bool isLoading = true;
|
||||||
|
private MudForm? form;
|
||||||
|
private CommonCode editModel = new();
|
||||||
|
private bool isCreateMode = true;
|
||||||
|
|
||||||
|
protected override async Task OnInitializedAsync()
|
||||||
|
{
|
||||||
|
groups = await CommonCodeClient.GetGroupsAsync();
|
||||||
|
selectedGroup = groups.FirstOrDefault() ?? "";
|
||||||
|
await LoadCodes();
|
||||||
|
PrepareCreate();
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task OnGroupChanged(string value)
|
||||||
|
{
|
||||||
|
selectedGroup = value;
|
||||||
|
await LoadCodes();
|
||||||
|
PrepareCreate();
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task LoadCodes()
|
||||||
|
{
|
||||||
|
isLoading = true;
|
||||||
|
codes = string.IsNullOrWhiteSpace(selectedGroup)
|
||||||
|
? []
|
||||||
|
: await CommonCodeClient.GetByGroupAsync(selectedGroup);
|
||||||
|
isLoading = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void PrepareCreate()
|
||||||
|
{
|
||||||
|
isCreateMode = true;
|
||||||
|
editModel = new CommonCode
|
||||||
|
{
|
||||||
|
CodeGroup = selectedGroup,
|
||||||
|
IsActive = true
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private void EditCode(CommonCode code)
|
||||||
|
{
|
||||||
|
isCreateMode = false;
|
||||||
|
editModel = new CommonCode
|
||||||
|
{
|
||||||
|
CodeGroup = code.CodeGroup,
|
||||||
|
CodeValue = code.CodeValue,
|
||||||
|
CodeName = code.CodeName,
|
||||||
|
SortOrder = code.SortOrder,
|
||||||
|
IsActive = code.IsActive
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task SaveCode()
|
||||||
|
{
|
||||||
|
if (form != null)
|
||||||
|
{
|
||||||
|
await form.Validate();
|
||||||
|
if (!form.IsValid)
|
||||||
|
{
|
||||||
|
Snackbar.Add("필수 항목을 입력하세요.", Severity.Warning);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (editModel.CodeValue.Contains(' '))
|
||||||
|
{
|
||||||
|
Snackbar.Add("code_value에는 공백을 넣을 수 없습니다.", Severity.Error);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!await CommonCodeClient.UpsertAsync(editModel))
|
||||||
|
{
|
||||||
|
Snackbar.Add("저장 실패", Severity.Error);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Snackbar.Add("저장되었습니다.", Severity.Success);
|
||||||
|
await LoadCodes();
|
||||||
|
PrepareCreate();
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task DeleteCode(CommonCode code)
|
||||||
|
{
|
||||||
|
if (!await CommonCodeClient.DeleteAsync(code.CodeGroup, code.CodeValue))
|
||||||
|
{
|
||||||
|
Snackbar.Add("삭제 실패", Severity.Error);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Snackbar.Add("삭제되었습니다.", Severity.Success);
|
||||||
|
await LoadCodes();
|
||||||
|
PrepareCreate();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
@page "/admin/contracts"
|
@page "/admin/contracts"
|
||||||
@using TaxBaik.Web.Services.AdminClients
|
@using TaxBaik.Web.Services.AdminClients
|
||||||
|
@using TaxBaik.Web.Components.Admin.Shared
|
||||||
@inject IContractBrowserClient ContractClient
|
@inject IContractBrowserClient ContractClient
|
||||||
@inject IClientBrowserClient ClientClient
|
@inject IClientBrowserClient ClientClient
|
||||||
@inject ISnackbar Snackbar
|
@inject ISnackbar Snackbar
|
||||||
@@ -122,14 +123,7 @@ else
|
|||||||
}
|
}
|
||||||
</MudSelect>
|
</MudSelect>
|
||||||
<MudTextField T="string" @bind-Value="contractForm.ContractNumber" Label="계약번호" Variant="Variant.Outlined" FullWidth="@true" Class="mb-3" Required="true" />
|
<MudTextField T="string" @bind-Value="contractForm.ContractNumber" Label="계약번호" Variant="Variant.Outlined" FullWidth="@true" Class="mb-3" Required="true" />
|
||||||
<MudSelect T="string" @bind-Value="contractForm.ServiceType" Label="서비스 유형" Variant="Variant.Outlined" FullWidth="@true" Class="mb-3" Required="true">
|
<CommonCodeSelect @bind-Value="contractForm.ServiceType" Group="CONTRACT_SERVICE_TYPE" Label="서비스 유형" Class="mb-3" Required="true" />
|
||||||
<MudSelectItem Value="@("개인 기장대리")">개인 기장대리</MudSelectItem>
|
|
||||||
<MudSelectItem Value="@("법인 기장대리")">법인 기장대리</MudSelectItem>
|
|
||||||
<MudSelectItem Value="@("세무조정 대행")">세무조정 대행</MudSelectItem>
|
|
||||||
<MudSelectItem Value="@("양도세 신고대리")">양도세 신고대리</MudSelectItem>
|
|
||||||
<MudSelectItem Value="@("상속·증여 자문")">상속·증여 자문</MudSelectItem>
|
|
||||||
<MudSelectItem Value="@("세무조사 대응")">세무조사 대응</MudSelectItem>
|
|
||||||
</MudSelect>
|
|
||||||
<MudDatePicker @bind-Date="contractForm.StartDate" Label="계약 시작일" Variant="Variant.Outlined" FullWidth="@true" Class="mb-3" Required="true" />
|
<MudDatePicker @bind-Date="contractForm.StartDate" Label="계약 시작일" Variant="Variant.Outlined" FullWidth="@true" Class="mb-3" Required="true" />
|
||||||
<MudNumericField T="decimal?" @bind-Value="contractForm.MonthlyFee" Label="월 수수료" Variant="Variant.Outlined" FullWidth="@true" Class="mb-4" />
|
<MudNumericField T="decimal?" @bind-Value="contractForm.MonthlyFee" Label="월 수수료" Variant="Variant.Outlined" FullWidth="@true" Class="mb-4" />
|
||||||
|
|
||||||
@@ -162,19 +156,15 @@ else
|
|||||||
private Contract? selectedContract;
|
private Contract? selectedContract;
|
||||||
private ContractForm contractForm = new();
|
private ContractForm contractForm = new();
|
||||||
|
|
||||||
protected override async Task OnAfterRenderAsync(bool firstRender)
|
protected override async Task OnInitializedAsync()
|
||||||
{
|
{
|
||||||
if (firstRender)
|
if (AuthStateTask != null)
|
||||||
{
|
{
|
||||||
if (AuthStateTask != null)
|
var authState = await AuthStateTask;
|
||||||
|
if (authState.User.Identity?.IsAuthenticated == true)
|
||||||
{
|
{
|
||||||
var authState = await AuthStateTask;
|
await LoadData();
|
||||||
if (authState.User.Identity?.IsAuthenticated == true)
|
PrepareCreate();
|
||||||
{
|
|
||||||
await LoadData();
|
|
||||||
PrepareCreate();
|
|
||||||
StateHasChanged();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
@page "/admin/dashboard"
|
@page "/admin/dashboard"
|
||||||
@attribute [Authorize]
|
@attribute [Authorize]
|
||||||
@using TaxBaik.Web.Services
|
@using TaxBaik.Web.Services
|
||||||
|
@using TaxBaik.Web.Components.Admin.Shared
|
||||||
@inject IAdminDashboardClient DashboardClient
|
@inject IAdminDashboardClient DashboardClient
|
||||||
@inject NavigationManager Nav
|
@inject NavigationManager Nav
|
||||||
|
|
||||||
@@ -95,7 +96,8 @@
|
|||||||
<tbody>
|
<tbody>
|
||||||
@foreach (var f in upcomingFilings)
|
@foreach (var f in upcomingFilings)
|
||||||
{
|
{
|
||||||
var dday = (f.DueDate.Date - DateTime.Today).Days;
|
var dday = BusinessDayCalculator.GetDday(DateOnly.FromDateTime(f.DueDate));
|
||||||
|
var effectiveDueDate = BusinessDayCalculator.GetEffectiveDueDate(DateOnly.FromDateTime(f.DueDate));
|
||||||
<tr>
|
<tr>
|
||||||
<td>
|
<td>
|
||||||
<MudLink Href="@($"/taxbaik/admin/clients/{f.ClientId}")" Underline="Underline.Hover" Color="Color.Primary" Class="font-weight-bold">
|
<MudLink Href="@($"/taxbaik/admin/clients/{f.ClientId}")" Underline="Underline.Hover" Color="Color.Primary" Class="font-weight-bold">
|
||||||
@@ -103,7 +105,7 @@
|
|||||||
</MudLink>
|
</MudLink>
|
||||||
</td>
|
</td>
|
||||||
<td>@f.FilingType</td>
|
<td>@f.FilingType</td>
|
||||||
<td>@f.DueDate.ToString("yyyy-MM-dd")</td>
|
<td>@effectiveDueDate.ToDateTime(TimeOnly.MinValue).ToString("yyyy-MM-dd")</td>
|
||||||
<td>
|
<td>
|
||||||
@if (dday < 0)
|
@if (dday < 0)
|
||||||
{
|
{
|
||||||
@@ -175,35 +177,30 @@
|
|||||||
private string? errorMessage;
|
private string? errorMessage;
|
||||||
private bool isLoading = true;
|
private bool isLoading = true;
|
||||||
|
|
||||||
protected override async Task OnAfterRenderAsync(bool firstRender)
|
protected override async Task OnInitializedAsync()
|
||||||
{
|
{
|
||||||
if (firstRender)
|
if (AuthStateTask != null)
|
||||||
{
|
{
|
||||||
if (AuthStateTask != null)
|
var authState = await AuthStateTask;
|
||||||
|
if (authState.User.Identity?.IsAuthenticated == true)
|
||||||
{
|
{
|
||||||
var authState = await AuthStateTask;
|
try
|
||||||
if (authState.User.Identity?.IsAuthenticated == true)
|
|
||||||
{
|
{
|
||||||
try
|
var summaryTask = DashboardClient.GetSummaryAsync();
|
||||||
{
|
var filingsTask = DashboardClient.GetUpcomingFilingsAsync(30);
|
||||||
// API 클라이언트 사용 (서비스 직접 호출 X)
|
|
||||||
var summaryTask = DashboardClient.GetSummaryAsync();
|
|
||||||
var filingsTask = DashboardClient.GetUpcomingFilingsAsync(30);
|
|
||||||
|
|
||||||
await Task.WhenAll(summaryTask, filingsTask);
|
await Task.WhenAll(summaryTask, filingsTask);
|
||||||
summary = await summaryTask;
|
summary = await summaryTask;
|
||||||
upcomingFilings = (await filingsTask).ToList();
|
upcomingFilings = (await filingsTask).ToList();
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
errorMessage = "대시보드 데이터를 불러올 수 없습니다.";
|
errorMessage = "대시보드 데이터를 불러올 수 없습니다.";
|
||||||
Console.Error.WriteLine($"Dashboard error: {ex.Message}");
|
Console.Error.WriteLine($"Dashboard error: {ex.Message}");
|
||||||
}
|
}
|
||||||
finally
|
finally
|
||||||
{
|
{
|
||||||
isLoading = false;
|
isLoading = false;
|
||||||
StateHasChanged();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,15 +5,12 @@
|
|||||||
|
|
||||||
<PageTitle>문의 관리</PageTitle>
|
<PageTitle>문의 관리</PageTitle>
|
||||||
|
|
||||||
<section class="admin-page-hero">
|
<AdminPageHeader Title="문의 관리" Eyebrow="Customer Requests" Subtitle="상담 요청을 상태별로 확인하고 후속 조치를 기록합니다.">
|
||||||
<div>
|
<ChildContent>
|
||||||
<MudText Typo="Typo.caption" Class="admin-eyebrow">Customer Requests</MudText>
|
<MudButton Variant="Variant.Filled" Color="Color.Primary" StartIcon="@Icons.Material.Filled.Add"
|
||||||
<MudText Typo="Typo.h4" Class="admin-page-title">문의 관리</MudText>
|
Href="/taxbaik/admin/inquiries/create">새 문의 등록</MudButton>
|
||||||
<MudText Typo="Typo.body2" Class="admin-page-subtitle">상담 요청을 상태별로 확인하고 후속 조치를 기록합니다.</MudText>
|
</ChildContent>
|
||||||
</div>
|
</AdminPageHeader>
|
||||||
<MudButton Variant="Variant.Filled" Color="Color.Primary" StartIcon="@Icons.Material.Filled.Add"
|
|
||||||
Href="/taxbaik/admin/inquiries/create">새 문의 등록</MudButton>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<MudPaper Class="admin-surface" Elevation="0">
|
<MudPaper Class="admin-surface" Elevation="0">
|
||||||
@if (isLoading)
|
@if (isLoading)
|
||||||
@@ -52,18 +49,14 @@ else
|
|||||||
private bool isLoading = true;
|
private bool isLoading = true;
|
||||||
private IReadOnlyList<Domain.Entities.Inquiry> allInquiries = [];
|
private IReadOnlyList<Domain.Entities.Inquiry> allInquiries = [];
|
||||||
|
|
||||||
protected override async Task OnAfterRenderAsync(bool firstRender)
|
protected override async Task OnInitializedAsync()
|
||||||
{
|
{
|
||||||
if (firstRender)
|
if (AuthStateTask != null)
|
||||||
{
|
{
|
||||||
if (AuthStateTask != null)
|
var authState = await AuthStateTask;
|
||||||
|
if (authState.User.Identity?.IsAuthenticated == true)
|
||||||
{
|
{
|
||||||
var authState = await AuthStateTask;
|
await LoadData();
|
||||||
if (authState.User.Identity?.IsAuthenticated == true)
|
|
||||||
{
|
|
||||||
await LoadData();
|
|
||||||
StateHasChanged();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
@page "/admin/tax-filing-schedules"
|
@page "/admin/tax-filing-schedules"
|
||||||
@using TaxBaik.Web.Services.AdminClients
|
@using TaxBaik.Web.Services.AdminClients
|
||||||
|
@using TaxBaik.Domain.Entities
|
||||||
|
@using TaxBaik.Web.Components.Admin.Shared
|
||||||
@inject ITaxFilingScheduleBrowserClient TaxFilingClient
|
@inject ITaxFilingScheduleBrowserClient TaxFilingClient
|
||||||
@inject IClientBrowserClient ClientClient
|
@inject IClientBrowserClient ClientClient
|
||||||
@inject ISnackbar Snackbar
|
@inject ISnackbar Snackbar
|
||||||
@@ -61,11 +63,12 @@ else
|
|||||||
<TemplateColumn Title="마감일">
|
<TemplateColumn Title="마감일">
|
||||||
<CellTemplate>
|
<CellTemplate>
|
||||||
@{
|
@{
|
||||||
var daysLeft = (context.Item.DueDate.Date - DateTime.Today).Days;
|
var effectiveDueDate = BusinessDayCalculator.GetEffectiveDueDate(DateOnly.FromDateTime(context.Item.DueDate));
|
||||||
|
var daysLeft = BusinessDayCalculator.GetDday(DateOnly.FromDateTime(context.Item.DueDate));
|
||||||
var statusColor = daysLeft < 0 ? Color.Error : daysLeft <= 7 ? Color.Warning : Color.Success;
|
var statusColor = daysLeft < 0 ? Color.Error : daysLeft <= 7 ? Color.Warning : Color.Success;
|
||||||
}
|
}
|
||||||
<MudChip Size="Size.Small" Color="@statusColor" Variant="Variant.Filled">
|
<MudChip Size="Size.Small" Color="@statusColor" Variant="Variant.Filled">
|
||||||
@context.Item.DueDate.ToString("yyyy-MM-dd")
|
@effectiveDueDate.ToDateTime(TimeOnly.MinValue).ToString("yyyy-MM-dd")
|
||||||
@if (daysLeft >= 0)
|
@if (daysLeft >= 0)
|
||||||
{
|
{
|
||||||
<span class="ms-1">(D-@daysLeft)</span>
|
<span class="ms-1">(D-@daysLeft)</span>
|
||||||
@@ -139,16 +142,7 @@ else
|
|||||||
<MudSelectItem Value="@((int?)client.Id)">@GetClientDisplayName(client)</MudSelectItem>
|
<MudSelectItem Value="@((int?)client.Id)">@GetClientDisplayName(client)</MudSelectItem>
|
||||||
}
|
}
|
||||||
</MudSelect>
|
</MudSelect>
|
||||||
<MudSelect T="string" @bind-Value="scheduleForm.FilingType" Label="신고 유형" Variant="Variant.Outlined" FullWidth="@true" Class="mb-3" Required="true">
|
<CommonCodeSelect @bind-Value="scheduleForm.FilingType" Group="FILING_TYPE" Label="신고 유형" Class="mb-3" 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>
|
|
||||||
</MudSelect>
|
|
||||||
<MudDatePicker @bind-Date="scheduleForm.DueDate" Label="마감일" Variant="Variant.Outlined" FullWidth="@true" Class="mb-3" Required="true" />
|
<MudDatePicker @bind-Date="scheduleForm.DueDate" Label="마감일" Variant="Variant.Outlined" FullWidth="@true" Class="mb-3" Required="true" />
|
||||||
<MudNumericField T="int" @bind-Value="scheduleForm.FilingYear" Label="신고연도" Variant="Variant.Outlined" FullWidth="@true" Class="mb-4" Required="true" />
|
<MudNumericField T="int" @bind-Value="scheduleForm.FilingYear" Label="신고연도" Variant="Variant.Outlined" FullWidth="@true" Class="mb-4" Required="true" />
|
||||||
|
|
||||||
@@ -224,7 +218,8 @@ else
|
|||||||
{
|
{
|
||||||
FilingYear = DateTime.Now.Year,
|
FilingYear = DateTime.Now.Year,
|
||||||
DueDate = DateTime.Today,
|
DueDate = DateTime.Today,
|
||||||
ClientId = clients.FirstOrDefault()?.Id
|
ClientId = clients.FirstOrDefault()?.Id,
|
||||||
|
FilingType = string.Empty
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
@page "/admin/tax-profiles"
|
@page "/admin/tax-profiles"
|
||||||
@using TaxBaik.Web.Services.AdminClients
|
@using TaxBaik.Web.Services.AdminClients
|
||||||
|
@using TaxBaik.Web.Components.Admin.Shared
|
||||||
@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]
|
||||||
@@ -100,18 +100,8 @@ else
|
|||||||
<MudSelectItem Value="@((int?)client.Id)">@GetClientDisplayName(client)</MudSelectItem>
|
<MudSelectItem Value="@((int?)client.Id)">@GetClientDisplayName(client)</MudSelectItem>
|
||||||
}
|
}
|
||||||
</MudSelect>
|
</MudSelect>
|
||||||
<MudSelect T="string" @bind-Value="profileForm.BusinessType" Label="사업 유형" Variant="Variant.Outlined" FullWidth="@true" Class="mb-3" Required="true">
|
<CommonCodeSelect @bind-Value="profileForm.BusinessType" Group="BUSINESS_TYPE" Label="사업 유형" Class="mb-3" Required="true" />
|
||||||
@foreach (var type in businessTypes)
|
<CommonCodeSelect @bind-Value="profileForm.TaxRiskLevel" Group="TAX_RISK_LEVEL" Label="위험도" Class="mb-3" />
|
||||||
{
|
|
||||||
<MudSelectItem Value="@type.CodeValue">@type.CodeName</MudSelectItem>
|
|
||||||
}
|
|
||||||
</MudSelect>
|
|
||||||
<MudSelect T="string" @bind-Value="profileForm.TaxRiskLevel" Label="위험도" Variant="Variant.Outlined" FullWidth="@true" Class="mb-3">
|
|
||||||
@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-3" />
|
<MudDatePicker @bind-Date="profileForm.NextFilingDueDate" Label="다음 신고 예정일" Variant="Variant.Outlined" FullWidth="@true" Class="mb-3" />
|
||||||
<MudTextField T="string" @bind-Value="profileForm.SpecialNotes" Label="특수 사항" Variant="Variant.Outlined" FullWidth="@true" Lines="3" Class="mb-4" />
|
<MudTextField T="string" @bind-Value="profileForm.SpecialNotes" Label="특수 사항" Variant="Variant.Outlined" FullWidth="@true" Lines="3" Class="mb-4" />
|
||||||
|
|
||||||
@@ -135,26 +125,21 @@ 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 List<CommonCode> riskLevels = [];
|
||||||
private MudForm? form;
|
private MudForm? form;
|
||||||
private bool isEditMode;
|
private bool isEditMode;
|
||||||
private TaxProfile? selectedProfile;
|
private TaxProfile? selectedProfile;
|
||||||
private TaxProfileForm profileForm = new();
|
private TaxProfileForm profileForm = new();
|
||||||
|
|
||||||
protected override async Task OnAfterRenderAsync(bool firstRender)
|
protected override async Task OnInitializedAsync()
|
||||||
{
|
{
|
||||||
if (firstRender)
|
if (AuthStateTask != null)
|
||||||
{
|
{
|
||||||
if (AuthStateTask != null)
|
var authState = await AuthStateTask;
|
||||||
|
if (authState.User.Identity?.IsAuthenticated == true)
|
||||||
{
|
{
|
||||||
var authState = await AuthStateTask;
|
await LoadData();
|
||||||
if (authState.User.Identity?.IsAuthenticated == true)
|
PrepareCreate();
|
||||||
{
|
|
||||||
await LoadData();
|
|
||||||
PrepareCreate();
|
|
||||||
StateHasChanged();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -168,31 +153,6 @@ else
|
|||||||
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,88 @@
|
|||||||
|
namespace TaxBaik.Web.Components.Admin.Shared;
|
||||||
|
|
||||||
|
public static class BusinessDayCalculator
|
||||||
|
{
|
||||||
|
private sealed record HolidayWindow(DateOnly Start, DateOnly End)
|
||||||
|
{
|
||||||
|
public IEnumerable<DateOnly> Dates()
|
||||||
|
{
|
||||||
|
for (var date = Start; date <= End; date = date.AddDays(1))
|
||||||
|
{
|
||||||
|
yield return date;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static readonly HolidayWindow[] HolidayWindows =
|
||||||
|
{
|
||||||
|
new(new DateOnly(2026, 1, 1), new DateOnly(2026, 1, 1)),
|
||||||
|
new(new DateOnly(2026, 2, 16), new DateOnly(2026, 2, 18)),
|
||||||
|
new(new DateOnly(2026, 3, 1), new DateOnly(2026, 3, 2)),
|
||||||
|
new(new DateOnly(2026, 5, 5), new DateOnly(2026, 5, 5)),
|
||||||
|
new(new DateOnly(2026, 6, 6), new DateOnly(2026, 6, 6)),
|
||||||
|
new(new DateOnly(2026, 8, 15), new DateOnly(2026, 8, 17)),
|
||||||
|
new(new DateOnly(2026, 9, 24), new DateOnly(2026, 9, 26)),
|
||||||
|
new(new DateOnly(2026, 10, 3), new DateOnly(2026, 10, 5)),
|
||||||
|
new(new DateOnly(2026, 10, 9), new DateOnly(2026, 10, 9)),
|
||||||
|
new(new DateOnly(2026, 12, 25), new DateOnly(2026, 12, 25))
|
||||||
|
};
|
||||||
|
|
||||||
|
private static readonly HashSet<DateOnly> HolidayDates = BuildHolidayDates();
|
||||||
|
|
||||||
|
public static DateOnly GetEffectiveDueDate(DateOnly dueDate)
|
||||||
|
{
|
||||||
|
var effectiveDate = dueDate;
|
||||||
|
while (!IsBusinessDay(effectiveDate))
|
||||||
|
{
|
||||||
|
effectiveDate = effectiveDate.AddDays(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
return effectiveDate;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static int GetDday(DateOnly dueDate, DateOnly? referenceDate = null)
|
||||||
|
{
|
||||||
|
var today = referenceDate ?? DateOnly.FromDateTime(DateTime.Today);
|
||||||
|
var effectiveDueDate = GetEffectiveDueDate(dueDate);
|
||||||
|
return effectiveDueDate.DayNumber - today.DayNumber;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static bool IsBusinessDay(DateOnly date)
|
||||||
|
=> date.DayOfWeek is not DayOfWeek.Saturday and not DayOfWeek.Sunday
|
||||||
|
&& !HolidayDates.Contains(date);
|
||||||
|
|
||||||
|
private static HashSet<DateOnly> BuildHolidayDates()
|
||||||
|
{
|
||||||
|
var holidays = new HashSet<DateOnly>();
|
||||||
|
|
||||||
|
foreach (var window in HolidayWindows)
|
||||||
|
{
|
||||||
|
foreach (var date in window.Dates())
|
||||||
|
{
|
||||||
|
holidays.Add(date);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 주말과 연속 공휴일 뒤에 붙는 대체휴일을 다음 영업일로 자동 확장한다.
|
||||||
|
foreach (var window in HolidayWindows)
|
||||||
|
{
|
||||||
|
foreach (var date in window.Dates())
|
||||||
|
{
|
||||||
|
if (date.DayOfWeek is not DayOfWeek.Saturday and not DayOfWeek.Sunday)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
var substitute = date.AddDays(1);
|
||||||
|
while (substitute.DayOfWeek is DayOfWeek.Saturday or DayOfWeek.Sunday || holidays.Contains(substitute))
|
||||||
|
{
|
||||||
|
substitute = substitute.AddDays(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
holidays.Add(substitute);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return holidays;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,56 @@
|
|||||||
|
@using TaxBaik.Domain.Entities
|
||||||
|
@using TaxBaik.Web.Services.AdminClients
|
||||||
|
@inject ICommonCodeBrowserClient CommonCodeClient
|
||||||
|
|
||||||
|
<MudSelect T="string"
|
||||||
|
Value="Value"
|
||||||
|
ValueChanged="ValueChanged"
|
||||||
|
Label="@Label"
|
||||||
|
Variant="@Variant"
|
||||||
|
FullWidth="@FullWidth"
|
||||||
|
Class="@Class"
|
||||||
|
Required="@Required"
|
||||||
|
Clearable="@Clearable"
|
||||||
|
Disabled="@Disabled">
|
||||||
|
@if (!string.IsNullOrWhiteSpace(Placeholder))
|
||||||
|
{
|
||||||
|
<MudSelectItem Value="@string.Empty">@Placeholder</MudSelectItem>
|
||||||
|
}
|
||||||
|
@foreach (var item in items)
|
||||||
|
{
|
||||||
|
<MudSelectItem Value="@item.CodeValue">@item.CodeName</MudSelectItem>
|
||||||
|
}
|
||||||
|
</MudSelect>
|
||||||
|
|
||||||
|
@code {
|
||||||
|
[Parameter] public string? Value { get; set; }
|
||||||
|
[Parameter] public EventCallback<string?> ValueChanged { get; set; }
|
||||||
|
[Parameter] public string Group { get; set; } = string.Empty;
|
||||||
|
[Parameter] public string Label { get; set; } = string.Empty;
|
||||||
|
[Parameter] public Variant Variant { get; set; } = Variant.Outlined;
|
||||||
|
[Parameter] public bool FullWidth { get; set; } = true;
|
||||||
|
[Parameter] public string? Class { get; set; }
|
||||||
|
[Parameter] public bool Required { get; set; }
|
||||||
|
[Parameter] public bool Clearable { get; set; }
|
||||||
|
[Parameter] public bool Disabled { get; set; }
|
||||||
|
[Parameter] public string? Placeholder { get; set; }
|
||||||
|
|
||||||
|
private List<CommonCode> items = [];
|
||||||
|
|
||||||
|
protected override async Task OnParametersSetAsync()
|
||||||
|
{
|
||||||
|
var normalizedGroup = Group?.Trim() ?? string.Empty;
|
||||||
|
if (!string.Equals(normalizedGroup, _loadedGroup, StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
_loadedGroup = normalizedGroup;
|
||||||
|
items = string.IsNullOrWhiteSpace(normalizedGroup)
|
||||||
|
? []
|
||||||
|
: (await CommonCodeClient.GetByGroupAsync(normalizedGroup))
|
||||||
|
.OrderBy(x => x.SortOrder)
|
||||||
|
.ThenBy(x => x.CodeName)
|
||||||
|
.ToList();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private string? _loadedGroup;
|
||||||
|
}
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
using Microsoft.AspNetCore.Authorization;
|
using Microsoft.AspNetCore.Authorization;
|
||||||
using Microsoft.AspNetCore.Mvc;
|
using Microsoft.AspNetCore.Mvc;
|
||||||
using TaxBaik.Application.Services;
|
using TaxBaik.Application.Services;
|
||||||
|
using TaxBaik.Domain.Entities;
|
||||||
|
|
||||||
namespace TaxBaik.Web.Controllers;
|
namespace TaxBaik.Web.Controllers;
|
||||||
|
|
||||||
@@ -36,4 +37,44 @@ public class CommonCodeController(CommonCodeService commonCodeService) : Control
|
|||||||
return StatusCode(500, new { error = "그룹별 공통코드 조회 실패", message = ex.Message });
|
return StatusCode(500, new { error = "그룹별 공통코드 조회 실패", message = ex.Message });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[HttpGet("groups")]
|
||||||
|
public async Task<IActionResult> GetGroups()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var groups = await commonCodeService.GetAllGroupsAsync();
|
||||||
|
return Ok(groups);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
return StatusCode(500, new { error = "공통코드 그룹 조회 실패", message = ex.Message });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpGet("{group}/{value}")]
|
||||||
|
public async Task<IActionResult> Get(string group, string value)
|
||||||
|
{
|
||||||
|
var code = await commonCodeService.GetAsync(group, value);
|
||||||
|
return code is null ? NotFound() : Ok(code);
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpPost]
|
||||||
|
public async Task<IActionResult> Upsert([FromBody] CommonCode code)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(code.CodeGroup) || string.IsNullOrWhiteSpace(code.CodeValue) || string.IsNullOrWhiteSpace(code.CodeName))
|
||||||
|
return BadRequest(new { error = "코드 그룹, 값, 이름은 필수입니다." });
|
||||||
|
if (code.CodeValue.Contains(' '))
|
||||||
|
return BadRequest(new { error = "code_value에는 공백을 사용할 수 없습니다." });
|
||||||
|
|
||||||
|
await commonCodeService.UpsertAsync(code);
|
||||||
|
return Ok(code);
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpDelete("{group}/{value}")]
|
||||||
|
public async Task<IActionResult> Delete(string group, string value)
|
||||||
|
{
|
||||||
|
await commonCodeService.DeleteAsync(group, value);
|
||||||
|
return NoContent();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user