feat: CRM Phase 1-2 완성 + 시즌 시뮬레이터 + 개인정보처리방침/이용약관
- WBS-CRM-02: 상담 이력 (consultations 테이블 V008, ClientDetail.razor) - WBS-CRM-03: 문의→고객 전환 (V009 client_id FK, InquiryDetail 고객등록 버튼) - WBS-CRM-04: 신고 일정 캘린더 (tax_filings 테이블 V010, TaxFilingList.razor) - WBS-CRM-05: 문의 상태 5단계 확장 (V011, InquiryStatus enum, InquiryList 탭) - WBS-MKT-04: 시즌 시뮬레이터 어드민 페이지 (SeasonSimulator.razor) - WBS-UX-04: 개인정보처리방침 /taxbaik/privacy, 이용약관 /taxbaik/terms - Dashboard.razor 마감 임박 신고 위젯 추가 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -17,6 +17,8 @@ public static class DependencyInjection
|
||||
services.AddSingleton<SeasonalMarketingService>();
|
||||
services.AddScoped<ClientService>();
|
||||
services.AddScoped<FaqService>();
|
||||
services.AddScoped<ConsultationService>();
|
||||
services.AddScoped<TaxFilingService>();
|
||||
return services;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -64,6 +64,19 @@ public class ClientService(IClientRepository repository)
|
||||
await repository.UpdateAsync(client, ct);
|
||||
}
|
||||
|
||||
public async Task<int> CreateFromInquiryAsync(string name, string? phone, string? serviceType, CancellationToken ct = default)
|
||||
{
|
||||
var client = new Client
|
||||
{
|
||||
Name = name.Trim(),
|
||||
Phone = phone?.Trim(),
|
||||
ServiceType = serviceType,
|
||||
Status = "active",
|
||||
Source = "홈페이지 문의"
|
||||
};
|
||||
return await repository.CreateAsync(client, ct);
|
||||
}
|
||||
|
||||
public async Task DeleteAsync(int id, CancellationToken ct = default) =>
|
||||
await repository.DeleteAsync(id, ct);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
namespace TaxBaik.Application.Services;
|
||||
|
||||
using TaxBaik.Domain.Entities;
|
||||
using TaxBaik.Domain.Interfaces;
|
||||
|
||||
public class ConsultationService(IConsultationRepository repository)
|
||||
{
|
||||
public static readonly string[] Results =
|
||||
["상담 중", "계약 완료", "보류", "거절", "완료"];
|
||||
|
||||
public async Task<IEnumerable<Consultation>> GetByClientIdAsync(int clientId, CancellationToken ct = default) =>
|
||||
await repository.GetByClientIdAsync(clientId, ct);
|
||||
|
||||
public async Task<int> CreateAsync(Consultation consultation, CancellationToken ct = default)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(consultation.Summary))
|
||||
throw new ValidationException("상담 내용을 입력하세요.");
|
||||
if (consultation.ClientId <= 0)
|
||||
throw new ValidationException("고객을 선택하세요.");
|
||||
return await repository.CreateAsync(consultation, ct);
|
||||
}
|
||||
|
||||
public async Task DeleteAsync(int id, CancellationToken ct = default) =>
|
||||
await repository.DeleteAsync(id, ct);
|
||||
}
|
||||
@@ -60,6 +60,12 @@ public class InquiryService(
|
||||
public Task<int> CountByStatusAsync(string status, CancellationToken ct = default)
|
||||
=> repository.CountByStatusAsync(status, 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))
|
||||
|
||||
@@ -4,24 +4,37 @@ using TaxBaik.Domain.Enums;
|
||||
|
||||
public static class InquiryStatusMapper
|
||||
{
|
||||
public static readonly Dictionary<string, string> Labels = new()
|
||||
{
|
||||
["new"] = "신규",
|
||||
["consulting"] = "상담중",
|
||||
["contracted"] = "계약완료",
|
||||
["rejected"] = "거절",
|
||||
["closed"] = "종결",
|
||||
};
|
||||
|
||||
public static string ToStorageValue(InquiryStatus status) => status switch
|
||||
{
|
||||
InquiryStatus.New => "new",
|
||||
InquiryStatus.Contacted => "contacted",
|
||||
InquiryStatus.Completed => "completed",
|
||||
InquiryStatus.New => "new",
|
||||
InquiryStatus.Consulting => "consulting",
|
||||
InquiryStatus.Contracted => "contracted",
|
||||
InquiryStatus.Rejected => "rejected",
|
||||
InquiryStatus.Closed => "closed",
|
||||
_ => throw new ArgumentOutOfRangeException(nameof(status), status, null)
|
||||
};
|
||||
|
||||
public static bool TryParse(string? value, out InquiryStatus status)
|
||||
{
|
||||
status = value?.Trim().ToLowerInvariant() switch
|
||||
var key = value?.Trim().ToLowerInvariant();
|
||||
status = key switch
|
||||
{
|
||||
"new" => InquiryStatus.New,
|
||||
"contacted" => InquiryStatus.Contacted,
|
||||
"completed" => InquiryStatus.Completed,
|
||||
"new" => InquiryStatus.New,
|
||||
"consulting" => InquiryStatus.Consulting,
|
||||
"contracted" => InquiryStatus.Contracted,
|
||||
"rejected" => InquiryStatus.Rejected,
|
||||
"closed" => InquiryStatus.Closed,
|
||||
_ => default
|
||||
};
|
||||
|
||||
return value?.Trim().ToLowerInvariant() is "new" or "contacted" or "completed";
|
||||
return key is "new" or "consulting" or "contracted" or "rejected" or "closed";
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,50 @@
|
||||
namespace TaxBaik.Application.Services;
|
||||
|
||||
using TaxBaik.Domain.Entities;
|
||||
using TaxBaik.Domain.Interfaces;
|
||||
|
||||
public class TaxFilingService(ITaxFilingRepository repository)
|
||||
{
|
||||
public static readonly string[] FilingTypes =
|
||||
["부가가치세", "종합소득세", "법인세", "원천징수", "종합부동산세", "증여세", "상속세", "기타"];
|
||||
|
||||
public static readonly string[] Statuses =
|
||||
["pending", "filed", "overdue"];
|
||||
|
||||
public static readonly Dictionary<string, string> StatusLabels = new()
|
||||
{
|
||||
["pending"] = "신고 예정",
|
||||
["filed"] = "신고 완료",
|
||||
["overdue"] = "기한 초과",
|
||||
};
|
||||
|
||||
public async Task<IEnumerable<TaxFiling>> GetByClientIdAsync(int clientId, CancellationToken ct = default) =>
|
||||
await repository.GetByClientIdAsync(clientId, ct);
|
||||
|
||||
public async Task<IEnumerable<TaxFiling>> GetUpcomingAsync(int daysAhead = 30, CancellationToken ct = default) =>
|
||||
await repository.GetUpcomingAsync(daysAhead, ct);
|
||||
|
||||
public async Task<TaxFiling?> GetByIdAsync(int id, CancellationToken ct = default) =>
|
||||
await repository.GetByIdAsync(id, ct);
|
||||
|
||||
public async Task<int> CreateAsync(TaxFiling filing, CancellationToken ct = default)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(filing.FilingType))
|
||||
throw new ValidationException("신고 유형을 선택하세요.");
|
||||
if (filing.ClientId <= 0)
|
||||
throw new ValidationException("고객을 선택하세요.");
|
||||
if (filing.DueDate == default)
|
||||
throw new ValidationException("신고 기한을 입력하세요.");
|
||||
return await repository.CreateAsync(filing, ct);
|
||||
}
|
||||
|
||||
public async Task UpdateAsync(TaxFiling filing, CancellationToken ct = default)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(filing.FilingType))
|
||||
throw new ValidationException("신고 유형을 선택하세요.");
|
||||
await repository.UpdateAsync(filing, ct);
|
||||
}
|
||||
|
||||
public async Task DeleteAsync(int id, CancellationToken ct = default) =>
|
||||
await repository.DeleteAsync(id, ct);
|
||||
}
|
||||
Reference in New Issue
Block a user