c65742a0c7
TaxBaik CI/CD / build-and-deploy (push) Successful in 48s
Core Components: - Create reusable InquiryForm.razor component following SOLID principles - Implement InquiryCreate.razor for registering new inquiries (offline, phone) - Implement InquiryEdit.razor for modifying existing inquiries with delete - Add DeleteAsync method to InquiryRepository and InquiryService - Update InquiryList with 'Create' button and Edit link in table Architecture: - InquiryForm: Encapsulates form logic, can be reused for create/edit - Service Layer: All operations go through InquiryService for cache invalidation - Repository Pattern: Database operations isolated in InquiryRepository - UI Consistency: Both pages follow admin-page-hero pattern Features: - Admin can create inquiries from phone/offline consultations - Admin can modify inquiry details (name, phone, email, message, status, memo) - Admin can delete inquiries with confirmation dialog - All operations update dashboard cache - Status validation and error handling throughout Testing: - Updated FakeInquiryRepository in tests to implement DeleteAsync
118 lines
4.9 KiB
C#
118 lines
4.9 KiB
C#
namespace TaxBaik.Application.Services;
|
|
|
|
using System.Text.RegularExpressions;
|
|
using Microsoft.Extensions.Caching.Memory;
|
|
using TaxBaik.Domain.Entities;
|
|
using TaxBaik.Domain.Enums;
|
|
using TaxBaik.Domain.Interfaces;
|
|
|
|
public class InquiryService(
|
|
IInquiryRepository repository,
|
|
IInquiryNotificationService notificationService,
|
|
IMemoryCache memoryCache)
|
|
{
|
|
private static readonly Regex PhoneRegex = new(@"^01[0-9]-\d{3,4}-\d{4}$");
|
|
|
|
public async Task<int> SubmitAsync(
|
|
string name, string phone, string serviceType, string message,
|
|
string? email = null, string? ipAddress = null, CancellationToken ct = default)
|
|
{
|
|
if (string.IsNullOrWhiteSpace(name))
|
|
throw new ValidationException("이름을 입력하세요.");
|
|
|
|
if (!PhoneRegex.IsMatch(phone))
|
|
throw new ValidationException("올바른 전화번호를 입력하세요. (예: 010-1234-5678)");
|
|
|
|
if (string.IsNullOrWhiteSpace(message))
|
|
throw new ValidationException("문의 내용을 입력하세요.");
|
|
|
|
var inquiry = new Inquiry
|
|
{
|
|
Name = name.Trim(),
|
|
Phone = phone.Trim(),
|
|
Email = string.IsNullOrWhiteSpace(email) ? null : email.Trim(),
|
|
ServiceType = serviceType ?? "기타",
|
|
Message = message.Trim(),
|
|
IpAddress = ipAddress,
|
|
Status = InquiryStatusMapper.ToStorageValue(InquiryStatus.New),
|
|
CreatedAt = DateTime.UtcNow
|
|
};
|
|
|
|
var inquiryId = await repository.CreateAsync(inquiry, ct);
|
|
await notificationService.NotifyCreatedAsync(inquiryId, inquiry.Name, inquiry.Phone, inquiry.ServiceType, inquiry.Message, inquiry.IpAddress, inquiry.CreatedAt, ct);
|
|
memoryCache.Remove(AdminDashboardService.CacheKey);
|
|
return inquiryId;
|
|
}
|
|
|
|
public async Task<Inquiry?> GetByIdAsync(int id, CancellationToken ct = default) =>
|
|
await repository.GetByIdAsync(id, ct);
|
|
|
|
public async Task<(IEnumerable<Inquiry>, int)> GetPagedAsync(
|
|
int page, int pageSize, string? status = null, CancellationToken ct = default) =>
|
|
await repository.GetPagedAsync(NormalizePage(page), NormalizePageSize(pageSize), NormalizeOptionalStatus(status), ct);
|
|
|
|
public Task<int> CountAsync(CancellationToken ct = default)
|
|
=> repository.CountAsync(ct);
|
|
|
|
public Task<int> CountThisMonthAsync(CancellationToken ct = default)
|
|
=> repository.CountThisMonthAsync(ct);
|
|
|
|
public Task<int> CountByStatusAsync(string status, CancellationToken ct = default)
|
|
=> repository.CountByStatusAsync(status, ct);
|
|
|
|
public Task<int> CountByDateRangeAsync(DateTime startDate, DateTime endDate, CancellationToken ct = default)
|
|
=> repository.CountByDateRangeAsync(startDate, endDate, ct);
|
|
|
|
public Task<int> CountByStatusAndDateAsync(string status, DateTime startDate, DateTime endDate, CancellationToken ct = default)
|
|
=> repository.CountByStatusAndDateAsync(status, startDate, endDate, ct);
|
|
|
|
public async Task UpdateAdminMemoAsync(int id, string? adminMemo, CancellationToken ct = default) =>
|
|
await repository.UpdateAdminMemoAsync(id, adminMemo, ct);
|
|
|
|
public async Task LinkClientAsync(int inquiryId, int clientId, CancellationToken ct = default) =>
|
|
await repository.LinkClientAsync(inquiryId, clientId, ct);
|
|
|
|
public async Task UpdateStatusAsync(int id, string status, string? changedBy = null, CancellationToken ct = default)
|
|
{
|
|
if (!InquiryStatusMapper.TryParse(status, out var parsed))
|
|
throw new ValidationException("지원하지 않는 문의 상태입니다.");
|
|
|
|
var inquiry = await repository.GetByIdAsync(id, ct);
|
|
if (inquiry == null)
|
|
return;
|
|
|
|
var previousStatus = inquiry.Status;
|
|
var newStatus = InquiryStatusMapper.ToStorageValue(parsed);
|
|
|
|
await repository.UpdateStatusAsync(id, newStatus, ct);
|
|
await notificationService.NotifyStatusChangedAsync(id, inquiry.Name, inquiry.Phone, inquiry.ServiceType, previousStatus, newStatus, changedBy, ct);
|
|
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);
|
|
|
|
private static string? NormalizeOptionalStatus(string? status)
|
|
{
|
|
if (string.IsNullOrWhiteSpace(status))
|
|
return null;
|
|
|
|
if (!InquiryStatusMapper.TryParse(status, out var parsed))
|
|
throw new ValidationException("지원하지 않는 문의 상태입니다.");
|
|
|
|
return InquiryStatusMapper.ToStorageValue(parsed);
|
|
}
|
|
}
|
|
|
|
public class ValidationException : Exception
|
|
{
|
|
public ValidationException(string message) : base(message) { }
|
|
}
|