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:
@@ -18,6 +18,8 @@ public static class DependencyInjection
|
||||
services.AddScoped<IAnnouncementRepository, AnnouncementRepository>();
|
||||
services.AddScoped<IClientRepository, ClientRepository>();
|
||||
services.AddScoped<IFaqRepository, FaqRepository>();
|
||||
services.AddScoped<IConsultationRepository, ConsultationRepository>();
|
||||
services.AddScoped<ITaxFilingRepository, TaxFilingRepository>();
|
||||
|
||||
return services;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
namespace TaxBaik.Infrastructure.Repositories;
|
||||
|
||||
using Dapper;
|
||||
using TaxBaik.Domain.Entities;
|
||||
using TaxBaik.Domain.Interfaces;
|
||||
|
||||
public class ConsultationRepository(IDbConnectionFactory connectionFactory) : BaseRepository(connectionFactory), IConsultationRepository
|
||||
{
|
||||
public async Task<IEnumerable<Consultation>> GetByClientIdAsync(int clientId, CancellationToken ct = default)
|
||||
{
|
||||
using var conn = Conn();
|
||||
return await conn.QueryAsync<Consultation>(
|
||||
@"SELECT id, client_id, consultation_date, service_type, summary, result, fee, created_at
|
||||
FROM consultations
|
||||
WHERE client_id = @ClientId
|
||||
ORDER BY consultation_date DESC, id DESC",
|
||||
new { ClientId = clientId });
|
||||
}
|
||||
|
||||
public async Task<int> CreateAsync(Consultation consultation, CancellationToken ct = default)
|
||||
{
|
||||
using var conn = Conn();
|
||||
return await conn.QueryFirstAsync<int>(
|
||||
@"INSERT INTO consultations (client_id, consultation_date, service_type, summary, result, fee, created_at)
|
||||
VALUES (@ClientId, @ConsultationDate, @ServiceType, @Summary, @Result, @Fee, NOW())
|
||||
RETURNING id",
|
||||
consultation);
|
||||
}
|
||||
|
||||
public async Task DeleteAsync(int id, CancellationToken ct = default)
|
||||
{
|
||||
using var conn = Conn();
|
||||
await conn.ExecuteAsync("DELETE FROM consultations WHERE id = @Id", new { Id = id });
|
||||
}
|
||||
}
|
||||
@@ -20,7 +20,9 @@ public class InquiryRepository(IDbConnectionFactory connectionFactory) : BaseRep
|
||||
{
|
||||
using var conn = Conn();
|
||||
return await conn.QueryFirstOrDefaultAsync<Inquiry>(
|
||||
"SELECT id, name, phone, email, service_type, message, status, ip_address, created_at FROM inquiries WHERE id = @Id",
|
||||
@"SELECT id, name, phone, email, service_type, message, status, ip_address,
|
||||
client_id, admin_memo, created_at, updated_at
|
||||
FROM inquiries WHERE id = @Id",
|
||||
new { Id = id });
|
||||
}
|
||||
|
||||
@@ -31,7 +33,8 @@ public class InquiryRepository(IDbConnectionFactory connectionFactory) : BaseRep
|
||||
var offset = (page - 1) * pageSize;
|
||||
|
||||
using var reader = await conn.QueryMultipleAsync(
|
||||
@"SELECT id, name, phone, email, service_type, message, status, ip_address, created_at
|
||||
@"SELECT id, name, phone, email, service_type, message, status, ip_address,
|
||||
client_id, admin_memo, created_at, updated_at
|
||||
FROM inquiries
|
||||
WHERE @Status::text IS NULL OR status = @Status
|
||||
ORDER BY created_at DESC
|
||||
@@ -74,6 +77,24 @@ public class InquiryRepository(IDbConnectionFactory connectionFactory) : BaseRep
|
||||
public async Task UpdateStatusAsync(int id, string status, CancellationToken cancellationToken = default)
|
||||
{
|
||||
using var conn = Conn();
|
||||
await conn.ExecuteAsync("UPDATE inquiries SET status = @Status WHERE id = @Id", new { Id = id, Status = status });
|
||||
await conn.ExecuteAsync(
|
||||
"UPDATE inquiries SET status = @Status, updated_at = NOW() WHERE id = @Id",
|
||||
new { Id = id, Status = status });
|
||||
}
|
||||
|
||||
public async Task UpdateAdminMemoAsync(int id, string? adminMemo, CancellationToken cancellationToken = default)
|
||||
{
|
||||
using var conn = Conn();
|
||||
await conn.ExecuteAsync(
|
||||
"UPDATE inquiries SET admin_memo = @AdminMemo, updated_at = NOW() WHERE id = @Id",
|
||||
new { Id = id, AdminMemo = adminMemo });
|
||||
}
|
||||
|
||||
public async Task LinkClientAsync(int inquiryId, int clientId, CancellationToken cancellationToken = default)
|
||||
{
|
||||
using var conn = Conn();
|
||||
await conn.ExecuteAsync(
|
||||
"UPDATE inquiries SET client_id = @ClientId, updated_at = NOW() WHERE id = @Id",
|
||||
new { Id = inquiryId, ClientId = clientId });
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,76 @@
|
||||
namespace TaxBaik.Infrastructure.Repositories;
|
||||
|
||||
using Dapper;
|
||||
using TaxBaik.Domain.Entities;
|
||||
using TaxBaik.Domain.Interfaces;
|
||||
|
||||
public class TaxFilingRepository(IDbConnectionFactory connectionFactory) : BaseRepository(connectionFactory), ITaxFilingRepository
|
||||
{
|
||||
private const string SelectColumns = @"
|
||||
tf.id, tf.client_id, c.name AS client_name, tf.filing_type, tf.due_date,
|
||||
tf.status, tf.memo, tf.created_at, tf.updated_at";
|
||||
|
||||
public async Task<IEnumerable<TaxFiling>> GetByClientIdAsync(int clientId, CancellationToken ct = default)
|
||||
{
|
||||
using var conn = Conn();
|
||||
return await conn.QueryAsync<TaxFiling>(
|
||||
$@"SELECT {SelectColumns}
|
||||
FROM tax_filings tf
|
||||
JOIN clients c ON c.id = tf.client_id
|
||||
WHERE tf.client_id = @ClientId
|
||||
ORDER BY tf.due_date ASC",
|
||||
new { ClientId = clientId });
|
||||
}
|
||||
|
||||
public async Task<IEnumerable<TaxFiling>> GetUpcomingAsync(int daysAhead, CancellationToken ct = default)
|
||||
{
|
||||
using var conn = Conn();
|
||||
return await conn.QueryAsync<TaxFiling>(
|
||||
$@"SELECT {SelectColumns}
|
||||
FROM tax_filings tf
|
||||
JOIN clients c ON c.id = tf.client_id
|
||||
WHERE tf.status = 'pending'
|
||||
AND tf.due_date <= CURRENT_DATE + @DaysAhead::int
|
||||
AND tf.due_date >= CURRENT_DATE
|
||||
ORDER BY tf.due_date ASC",
|
||||
new { DaysAhead = daysAhead });
|
||||
}
|
||||
|
||||
public async Task<TaxFiling?> GetByIdAsync(int id, CancellationToken ct = default)
|
||||
{
|
||||
using var conn = Conn();
|
||||
return await conn.QueryFirstOrDefaultAsync<TaxFiling>(
|
||||
$@"SELECT {SelectColumns}
|
||||
FROM tax_filings tf
|
||||
JOIN clients c ON c.id = tf.client_id
|
||||
WHERE tf.id = @Id",
|
||||
new { Id = id });
|
||||
}
|
||||
|
||||
public async Task<int> CreateAsync(TaxFiling filing, CancellationToken ct = default)
|
||||
{
|
||||
using var conn = Conn();
|
||||
return await conn.QueryFirstAsync<int>(
|
||||
@"INSERT INTO tax_filings (client_id, filing_type, due_date, status, memo, created_at, updated_at)
|
||||
VALUES (@ClientId, @FilingType, @DueDate, @Status, @Memo, NOW(), NOW())
|
||||
RETURNING id",
|
||||
filing);
|
||||
}
|
||||
|
||||
public async Task UpdateAsync(TaxFiling filing, CancellationToken ct = default)
|
||||
{
|
||||
using var conn = Conn();
|
||||
await conn.ExecuteAsync(
|
||||
@"UPDATE tax_filings
|
||||
SET filing_type = @FilingType, due_date = @DueDate, status = @Status,
|
||||
memo = @Memo, updated_at = NOW()
|
||||
WHERE id = @Id",
|
||||
filing);
|
||||
}
|
||||
|
||||
public async Task DeleteAsync(int id, CancellationToken ct = default)
|
||||
{
|
||||
using var conn = Conn();
|
||||
await conn.ExecuteAsync("DELETE FROM tax_filings WHERE id = @Id", new { Id = id });
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user