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 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 GetByIdAsync(int id, CancellationToken ct = default) => await repository.GetByIdAsync(id, ct); public async Task<(IEnumerable, int)> GetPagedAsync( int page, int pageSize, string? status = null, CancellationToken ct = default) => await repository.GetPagedAsync(NormalizePage(page), NormalizePageSize(pageSize), NormalizeOptionalStatus(status), ct); public Task CountAsync(CancellationToken ct = default) => repository.CountAsync(ct); public Task CountThisMonthAsync(CancellationToken ct = default) => repository.CountThisMonthAsync(ct); public Task CountByStatusAsync(string status, CancellationToken ct = default) => repository.CountByStatusAsync(status, ct); public Task CountByDateRangeAsync(DateTime startDate, DateTime endDate, CancellationToken ct = default) => repository.CountByDateRangeAsync(startDate, endDate, ct); public Task 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); } 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) { } }