diff --git a/TaxBaik.Application.Tests/InquiryServiceTests.cs b/TaxBaik.Application.Tests/InquiryServiceTests.cs index 8a04ab1..59374f7 100644 --- a/TaxBaik.Application.Tests/InquiryServiceTests.cs +++ b/TaxBaik.Application.Tests/InquiryServiceTests.cs @@ -87,6 +87,14 @@ public class InquiryServiceTests inquiry.ClientId = clientId; return Task.CompletedTask; } + + public Task DeleteAsync(int id, CancellationToken cancellationToken = default) + { + var inquiry = Inquiries.FirstOrDefault(x => x.Id == id); + if (inquiry != null) + Inquiries.Remove(inquiry); + return Task.CompletedTask; + } } private sealed class FakeInquiryNotificationService : IInquiryNotificationService diff --git a/TaxBaik.Application/Services/InquiryService.cs b/TaxBaik.Application/Services/InquiryService.cs index 542d7aa..b5303e5 100644 --- a/TaxBaik.Application/Services/InquiryService.cs +++ b/TaxBaik.Application/Services/InquiryService.cs @@ -89,6 +89,12 @@ public class InquiryService( memoryCache.Remove(AdminDashboardService.CacheKey); } + public async Task DeleteAsync(int id, CancellationToken ct = default) + { + await repository.DeleteAsync(id, ct); + memoryCache.Remove(AdminDashboardService.CacheKey); + } + private static int NormalizePage(int page) => Math.Max(1, page); private static int NormalizePageSize(int pageSize) => Math.Clamp(pageSize, 1, 100); diff --git a/TaxBaik.Domain/Interfaces/IInquiryRepository.cs b/TaxBaik.Domain/Interfaces/IInquiryRepository.cs index 1900ed5..1b3c092 100644 --- a/TaxBaik.Domain/Interfaces/IInquiryRepository.cs +++ b/TaxBaik.Domain/Interfaces/IInquiryRepository.cs @@ -16,4 +16,5 @@ public interface IInquiryRepository Task UpdateStatusAsync(int id, string status, CancellationToken cancellationToken = default); Task UpdateAdminMemoAsync(int id, string? adminMemo, CancellationToken cancellationToken = default); Task LinkClientAsync(int inquiryId, int clientId, CancellationToken cancellationToken = default); + Task DeleteAsync(int id, CancellationToken cancellationToken = default); } diff --git a/TaxBaik.Infrastructure/Repositories/InquiryRepository.cs b/TaxBaik.Infrastructure/Repositories/InquiryRepository.cs index e146c79..3238ffa 100644 --- a/TaxBaik.Infrastructure/Repositories/InquiryRepository.cs +++ b/TaxBaik.Infrastructure/Repositories/InquiryRepository.cs @@ -119,4 +119,10 @@ public class InquiryRepository(IDbConnectionFactory connectionFactory) : BaseRep "UPDATE inquiries SET client_id = @ClientId, updated_at = NOW() WHERE id = @Id", new { Id = inquiryId, ClientId = clientId }); } + + public async Task DeleteAsync(int id, CancellationToken cancellationToken = default) + { + using var conn = Conn(); + await conn.ExecuteAsync("DELETE FROM inquiries WHERE id = @Id", new { Id = id }); + } } diff --git a/TaxBaik.Web/Components/Admin/Forms/InquiryForm.razor b/TaxBaik.Web/Components/Admin/Forms/InquiryForm.razor new file mode 100644 index 0000000..4de00e4 --- /dev/null +++ b/TaxBaik.Web/Components/Admin/Forms/InquiryForm.razor @@ -0,0 +1,100 @@ +@using TaxBaik.Application.DTOs +@using TaxBaik.Application.Services + + + + + + + + + + 사업자세무 + 부동산세금 + 가족자산 + 기타 + + + + + + 신규 + 상담중 + 계약완료 + 거절 + 종결 + + + + + + + @ButtonText + + 취소 + + + +@code { + [Parameter, EditorRequired] + public string ButtonText { get; set; } = "저장"; + + [Parameter] + public EventCallback OnSubmit { get; set; } + + [Parameter] + public EventCallback OnCancel { get; set; } + + [Parameter] + public InquiryFormModel? InitialData { get; set; } + + private MudForm? form; + private InquiryFormModel model = new(); + + protected override void OnInitialized() + { + if (InitialData != null) + { + model = new InquiryFormModel + { + Name = InitialData.Name, + Phone = InitialData.Phone, + Email = InitialData.Email, + ServiceType = InitialData.ServiceType, + Message = InitialData.Message, + Status = InitialData.Status, + AdminMemo = InitialData.AdminMemo + }; + } + } + + private async Task HandleSubmit() + { + if (form == null) + return; + + await form.Validate(); + if (!form.IsValid) + return; + + await OnSubmit.InvokeAsync(model); + } + + public class InquiryFormModel + { + public string Name { get; set; } = ""; + public string Phone { get; set; } = ""; + public string? Email { get; set; } + public string ServiceType { get; set; } = "기타"; + public string Message { get; set; } = ""; + public string Status { get; set; } = "new"; + public string? AdminMemo { get; set; } + } +} diff --git a/TaxBaik.Web/Components/Admin/InquiryTable.razor b/TaxBaik.Web/Components/Admin/InquiryTable.razor index bc94837..edf43bb 100644 --- a/TaxBaik.Web/Components/Admin/InquiryTable.razor +++ b/TaxBaik.Web/Components/Admin/InquiryTable.razor @@ -26,7 +26,9 @@ @inquiry.CreatedAt.ToString("yyyy-MM-dd") 문의 내용 확인 + Href="@($"/taxbaik/admin/inquiries/{inquiry.Id}")">보기 + 수정 } diff --git a/TaxBaik.Web/Components/Admin/Pages/Inquiries/InquiryCreate.razor b/TaxBaik.Web/Components/Admin/Pages/Inquiries/InquiryCreate.razor new file mode 100644 index 0000000..dbe453e --- /dev/null +++ b/TaxBaik.Web/Components/Admin/Pages/Inquiries/InquiryCreate.razor @@ -0,0 +1,55 @@ +@page "/admin/inquiries/create" +@attribute [Authorize] +@using TaxBaik.Application.DTOs +@using TaxBaik.Application.Services +@using TaxBaik.Web.Components.Admin.Forms +@inject InquiryService InquiryService +@inject NavigationManager Navigation +@inject ISnackbar Snackbar + +문의 등록 + + + + Customer Relations + 새 문의 등록 + 고객 문의를 등록합니다. (전화, 오프라인 등) + + 취소 + + + + + + +@code { + private void GoBack() + { + Navigation.NavigateTo("/taxbaik/admin/inquiries"); + } + + private async Task HandleCreate(InquiryForm.InquiryFormModel model) + { + try + { + await InquiryService.SubmitAsync( + model.Name, + model.Phone, + model.ServiceType, + model.Message, + model.Email, + ipAddress: "admin-registered"); + + Snackbar.Add("문의가 등록되었습니다.", Severity.Success); + Navigation.NavigateTo("/taxbaik/admin/inquiries"); + } + catch (ValidationException ex) + { + Snackbar.Add(ex.Message, Severity.Error); + } + catch (Exception ex) + { + Snackbar.Add($"등록 실패: {ex.Message}", Severity.Error); + } + } +} diff --git a/TaxBaik.Web/Components/Admin/Pages/Inquiries/InquiryEdit.razor b/TaxBaik.Web/Components/Admin/Pages/Inquiries/InquiryEdit.razor new file mode 100644 index 0000000..5eb11ea --- /dev/null +++ b/TaxBaik.Web/Components/Admin/Pages/Inquiries/InquiryEdit.razor @@ -0,0 +1,143 @@ +@page "/admin/inquiries/{id:int}/edit" +@attribute [Authorize] +@using TaxBaik.Application.DTOs +@using TaxBaik.Application.Services +@using TaxBaik.Web.Components.Admin.Forms +@inject InquiryService InquiryService +@inject NavigationManager Navigation +@inject ISnackbar Snackbar +@inject IDialogService DialogService + +문의 수정 + + + + Customer Relations + 문의 수정 + 고객 문의 정보를 수정합니다. + + 취소 + + +@if (isLoading) +{ + +} +else if (inquiry == null) +{ + 문의를 찾을 수 없습니다. +} +else +{ + + + + + + + 문의 삭제 + + +} + +@code { + [Parameter] + public int Id { get; set; } + + private Domain.Entities.Inquiry? inquiry; + private InquiryForm.InquiryFormModel? formModel; + private bool isLoading = true; + + protected override async Task OnInitializedAsync() + { + try + { + inquiry = await InquiryService.GetByIdAsync(Id); + if (inquiry != null) + { + formModel = new InquiryForm.InquiryFormModel + { + Name = inquiry.Name, + Phone = inquiry.Phone, + Email = inquiry.Email, + ServiceType = inquiry.ServiceType, + Message = inquiry.Message, + Status = inquiry.Status, + AdminMemo = inquiry.AdminMemo + }; + } + } + catch (Exception ex) + { + Snackbar.Add($"문의 로드 실패: {ex.Message}", Severity.Error); + } + finally + { + isLoading = false; + } + } + + private void GoBack() + { + Navigation.NavigateTo("/taxbaik/admin/inquiries"); + } + + private async Task HandleUpdate(InquiryForm.InquiryFormModel model) + { + if (inquiry == null) + return; + + try + { + inquiry.Name = model.Name; + inquiry.Phone = model.Phone; + inquiry.Email = model.Email; + inquiry.ServiceType = model.ServiceType; + inquiry.Message = model.Message; + inquiry.AdminMemo = model.AdminMemo; + + if (inquiry.Status != model.Status) + { + await InquiryService.UpdateStatusAsync(inquiry.Id, model.Status); + } + + await InquiryService.UpdateAdminMemoAsync(inquiry.Id, model.AdminMemo); + + Snackbar.Add("문의가 수정되었습니다.", Severity.Success); + Navigation.NavigateTo("/taxbaik/admin/inquiries"); + } + catch (ValidationException ex) + { + Snackbar.Add(ex.Message, Severity.Error); + } + catch (Exception ex) + { + Snackbar.Add($"수정 실패: {ex.Message}", Severity.Error); + } + } + + private async Task DeleteInquiry() + { + if (inquiry == null) + return; + + var result = await DialogService.ShowMessageBox( + "문의 삭제", + "정말 삭제하시겠습니까? 이 작업은 취소할 수 없습니다.", + "삭제", "취소"); + + if (result != true) + return; + + try + { + await InquiryService.DeleteAsync(inquiry.Id); + Snackbar.Add("문의가 삭제되었습니다.", Severity.Success); + Navigation.NavigateTo("/taxbaik/admin/inquiries"); + } + catch (Exception ex) + { + Snackbar.Add($"삭제 실패: {ex.Message}", Severity.Error); + } + } +} diff --git a/TaxBaik.Web/Components/Admin/Pages/Inquiries/InquiryList.razor b/TaxBaik.Web/Components/Admin/Pages/Inquiries/InquiryList.razor index 1ee1016..4a42eab 100644 --- a/TaxBaik.Web/Components/Admin/Pages/Inquiries/InquiryList.razor +++ b/TaxBaik.Web/Components/Admin/Pages/Inquiries/InquiryList.razor @@ -11,6 +11,8 @@ 문의 관리 상담 요청을 상태별로 확인하고 후속 조치를 기록합니다. + 새 문의 등록