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:
+78
-26
@@ -53,6 +53,23 @@ Todo:
|
|||||||
- [x] 배포 완료 (`12070b7`)
|
- [x] 배포 완료 (`12070b7`)
|
||||||
- [ ] 배포 후 브라우저 아코디언 동작 확인
|
- [ ] 배포 후 브라우저 아코디언 동작 확인
|
||||||
|
|
||||||
|
## WBS-UX-04 개인정보처리방침·이용약관 페이지
|
||||||
|
|
||||||
|
목표: 법적 의무를 충족하고 방문자 신뢰를 높이는 정책 페이지를 제공한다.
|
||||||
|
|
||||||
|
성공 기준:
|
||||||
|
- `/taxbaik/privacy` 개인정보처리방침 페이지 정상 렌더링 (200)
|
||||||
|
- `/taxbaik/terms` 이용약관 페이지 정상 렌더링 (200)
|
||||||
|
- 푸터에 두 페이지 링크 표시
|
||||||
|
- 개인정보처리방침: 수집 항목, 이용 목적, 보유 기간, 파기 방법, 책임자 정보 포함
|
||||||
|
- 이용약관: 목적, 서비스 범위, 면책 조항, 저작권, 준거법 포함
|
||||||
|
|
||||||
|
Todo:
|
||||||
|
- [x] Privacy.cshtml + Privacy.cshtml.cs (Razor Page)
|
||||||
|
- [x] Terms.cshtml + Terms.cshtml.cs (Razor Page)
|
||||||
|
- [x] _Footer.cshtml에 링크 이미 존재 확인
|
||||||
|
- [ ] 배포 후 /taxbaik/privacy, /taxbaik/terms 접근 확인
|
||||||
|
|
||||||
## WBS-UX-03 FAQ 관리 (어드민 CRUD)
|
## WBS-UX-03 FAQ 관리 (어드민 CRUD)
|
||||||
|
|
||||||
목표: 세무사가 관리자 화면에서 FAQ 항목을 직접 등록·수정·삭제·순서 조정한다.
|
목표: 세무사가 관리자 화면에서 FAQ 항목을 직접 등록·수정·삭제·순서 조정한다.
|
||||||
@@ -117,6 +134,27 @@ Todo:
|
|||||||
- [x] CLAUDE.md 섹션 13 세무 캘린더 하네스
|
- [x] CLAUDE.md 섹션 13 세무 캘린더 하네스
|
||||||
- [ ] 배포 후 시즌 날짜 경계값 수동 확인
|
- [ ] 배포 후 시즌 날짜 경계값 수동 확인
|
||||||
|
|
||||||
|
## WBS-MKT-04 시즌 시뮬레이터 (어드민)
|
||||||
|
|
||||||
|
목표: 관리자가 날짜를 선택해 홈페이지 시즌 화면을 사전에 확인하고 콘텐츠 준비를 계획한다.
|
||||||
|
|
||||||
|
배경: 7개 시즌이 자동 전환되므로, 실제 날짜가 되기 전 미리 Hero 화면을 확인하는 도구가 필요하다.
|
||||||
|
|
||||||
|
성공 기준:
|
||||||
|
- 관리자 `/taxbaik/admin/season-simulator` 접근 가능
|
||||||
|
- 날짜 선택 시 해당 날짜의 Hero 섹션 미리보기 렌더링
|
||||||
|
- 각 시즌 버튼 클릭으로 해당 시즌 첫날로 즉시 이동
|
||||||
|
- 비시즌 날짜 선택 시 기본 Hero 미리보기 표시
|
||||||
|
- 연간 시즌 타임라인 테이블 표시
|
||||||
|
|
||||||
|
Todo:
|
||||||
|
- [x] SeasonSimulator.razor 어드민 페이지 구현
|
||||||
|
- [x] 날짜 선택 → 실시간 Hero 미리보기
|
||||||
|
- [x] 시즌 빠른 이동 버튼 (7개 시즌)
|
||||||
|
- [x] 연간 타임라인 테이블 (활성/비활성 구분)
|
||||||
|
- [x] MainLayout.razor 시즌 시뮬레이터 메뉴 추가 (홈페이지 그룹 하위)
|
||||||
|
- [ ] 배포 후 관리자에서 시뮬레이터 동작 확인
|
||||||
|
|
||||||
## WBS-MKT-02 관리자 공지사항 (Announcement)
|
## WBS-MKT-02 관리자 공지사항 (Announcement)
|
||||||
|
|
||||||
목표: 운영자가 홈페이지 최상단 배너를 등록·수정·삭제할 수 있다.
|
목표: 운영자가 홈페이지 최상단 배너를 등록·수정·삭제할 수 있다.
|
||||||
@@ -294,16 +332,17 @@ Todo:
|
|||||||
- 이력 없는 고객은 빈 목록 표시
|
- 이력 없는 고객은 빈 목록 표시
|
||||||
|
|
||||||
DB 스키마:
|
DB 스키마:
|
||||||
- `consultations` 테이블 (V007 마이그레이션)
|
- `consultations` 테이블 (V008 마이그레이션)
|
||||||
- 컬럼: id, client_id(FK), consultation_date, service_type, summary, result, fee, created_at
|
- 컬럼: id, client_id(FK), consultation_date, service_type, summary, result, fee, created_at
|
||||||
|
|
||||||
Todo:
|
Todo:
|
||||||
- [ ] V007__CreateConsultations.sql 마이그레이션
|
- [x] V008__CreateConsultations.sql 마이그레이션
|
||||||
- [ ] Consultation 엔티티 (Domain)
|
- [x] Consultation 엔티티 (Domain)
|
||||||
- [ ] IConsultationRepository 인터페이스 (Domain)
|
- [x] IConsultationRepository 인터페이스 (Domain)
|
||||||
- [ ] ConsultationRepository 구현 (Infrastructure)
|
- [x] ConsultationRepository 구현 (Infrastructure)
|
||||||
- [ ] ConsultationService 구현 (Application)
|
- [x] ConsultationService 구현 (Application)
|
||||||
- [ ] ClientDetail.razor (고객 상세 + 상담 이력 탭)
|
- [x] ClientDetail.razor (고객 상세 + 상담 이력 추가/삭제)
|
||||||
|
- [x] DI 등록 (Infrastructure + Application)
|
||||||
- [ ] 배포 후 고객 상세에서 상담 이력 추가 확인
|
- [ ] 배포 후 고객 상세에서 상담 이력 추가 확인
|
||||||
|
|
||||||
## WBS-CRM-03 문의 → 고객 전환 — Phase 1
|
## WBS-CRM-03 문의 → 고객 전환 — Phase 1
|
||||||
@@ -312,14 +351,18 @@ Todo:
|
|||||||
|
|
||||||
성공 기준:
|
성공 기준:
|
||||||
- 문의 상세에 "고객으로 등록" 버튼 표시
|
- 문의 상세에 "고객으로 등록" 버튼 표시
|
||||||
- 버튼 클릭 시 이름·연락처 자동 채워진 고객 생성 폼으로 이동
|
- 버튼 클릭 시 고객 카드 자동 생성 후 연결
|
||||||
- 이미 연결된 고객이 있으면 버튼 대신 고객 카드 링크 표시
|
- 이미 연결된 고객이 있으면 버튼 대신 고객 카드 링크 표시
|
||||||
- inquiries 테이블에 client_id 컬럼 추가
|
- inquiries 테이블에 client_id, admin_memo, updated_at 컬럼 추가
|
||||||
|
|
||||||
Todo:
|
Todo:
|
||||||
- [ ] inquiries 테이블에 client_id FK 컬럼 추가 (V008 마이그레이션)
|
- [x] V009__AddClientIdToInquiries.sql 마이그레이션
|
||||||
- [ ] InquiryDetail.razor에 "고객으로 등록" 버튼 추가
|
- [x] Inquiry 엔티티 client_id, admin_memo, updated_at 추가
|
||||||
- [ ] ClientEdit.razor에 inquiry_id 파라미터 지원 (자동 채우기)
|
- [x] IInquiryRepository.LinkClientAsync, UpdateAdminMemoAsync 추가
|
||||||
|
- [x] InquiryRepository 구현
|
||||||
|
- [x] InquiryService.LinkClientAsync, UpdateAdminMemoAsync 추가
|
||||||
|
- [x] ClientService.CreateFromInquiryAsync 추가
|
||||||
|
- [x] InquiryDetail.razor "고객으로 등록" 버튼 + 담당자 메모 추가
|
||||||
- [ ] 배포 후 문의 → 고객 전환 흐름 확인
|
- [ ] 배포 후 문의 → 고객 전환 흐름 확인
|
||||||
|
|
||||||
---
|
---
|
||||||
@@ -336,14 +379,19 @@ Todo:
|
|||||||
- 이번 달 마감 목록을 대시보드 위젯으로 표시
|
- 이번 달 마감 목록을 대시보드 위젯으로 표시
|
||||||
|
|
||||||
DB 스키마:
|
DB 스키마:
|
||||||
- `tax_filings` 테이블 (V009 마이그레이션)
|
- `tax_filings` 테이블 (V010 마이그레이션)
|
||||||
- 컬럼: id, client_id(FK), filing_type, due_date, status(pending/filed/overdue), memo
|
- 컬럼: id, client_id(FK), filing_type, due_date, status(pending/filed/overdue), memo
|
||||||
|
|
||||||
Todo:
|
Todo:
|
||||||
- [ ] V009__CreateTaxFilings.sql
|
- [x] V010__CreateTaxFilings.sql
|
||||||
- [ ] TaxFiling 엔티티, Repository, Service
|
- [x] TaxFiling 엔티티 (Domain)
|
||||||
- [ ] TaxFilingList.razor (관리자 신고 일정 화면)
|
- [x] ITaxFilingRepository, TaxFilingRepository 구현
|
||||||
- [ ] Dashboard.razor에 이번 달 마감 위젯 추가
|
- [x] TaxFilingService 구현 (Application)
|
||||||
|
- [x] TaxFilingList.razor (관리자 신고 일정 화면 + 상태별 탭)
|
||||||
|
- [x] FilingTable.razor (D-Day 강조, 완료 처리, 삭제)
|
||||||
|
- [x] Dashboard.razor에 30일 이내 마감 위젯 추가
|
||||||
|
- [x] MainLayout.razor 신고 일정 메뉴 추가
|
||||||
|
- [x] DI 등록
|
||||||
- [ ] 배포 후 신고 일정 등록 → D-Day 표시 확인
|
- [ ] 배포 후 신고 일정 등록 → D-Day 표시 확인
|
||||||
|
|
||||||
## WBS-CRM-05 문의 접수 현황 강화 — Phase 2
|
## WBS-CRM-05 문의 접수 현황 강화 — Phase 2
|
||||||
@@ -352,13 +400,16 @@ Todo:
|
|||||||
|
|
||||||
성공 기준:
|
성공 기준:
|
||||||
- 문의 상태: 신규/상담중/계약완료/거절/종결 5단계
|
- 문의 상태: 신규/상담중/계약완료/거절/종결 5단계
|
||||||
- 목록에서 상태 칩 필터로 빠른 분류
|
- 목록에서 상태 탭 필터로 빠른 분류
|
||||||
- 상태 변경 시 변경 일시 자동 기록
|
- 상태 변경 시 updated_at 자동 기록
|
||||||
|
|
||||||
Todo:
|
Todo:
|
||||||
- [ ] inquiries.status 컬럼 확장 (V010 마이그레이션)
|
- [x] V011__ExtendInquiryStatus.sql 마이그레이션 (contacted→consulting, completed→closed, admin_memo/updated_at 추가)
|
||||||
- [ ] InquiryList.razor 상태 필터 추가
|
- [x] InquiryStatus enum 5단계 확장
|
||||||
- [ ] InquiryDetail.razor 상태 변경 버튼 추가
|
- [x] InquiryStatusMapper 5단계 레이블 + TryParse 업데이트
|
||||||
|
- [x] InquiryList.razor 5단계 탭 (신규/상담중/계약완료/거절/종결)
|
||||||
|
- [x] InquiryDetail.razor 5단계 상태 버튼 + 색상 구분
|
||||||
|
- [x] Dashboard.razor 상태 레이블 5단계 반영
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -466,7 +517,8 @@ Todo:
|
|||||||
### 현재 검증 메모
|
### 현재 검증 메모
|
||||||
|
|
||||||
- `dotnet build TaxBaik.sln` 성공 (2026-06-27 기준, 경고 0 오류 0)
|
- `dotnet build TaxBaik.sln` 성공 (2026-06-27 기준, 경고 0 오류 0)
|
||||||
- 배포 커밋: `77a5c44` (FAQ 섹션 추가, 푸시 대기 중)
|
- 최종 배포 커밋: `9c96f15` (FAQ 관리 기능)
|
||||||
- WBS-MKT-01/02/03 구현 완료, 배포 후 시각 검증 필요
|
- WBS-MKT-01/02/03/04 구현 완료, 배포 후 시각 검증 필요
|
||||||
- WBS-CRM-01 구현 중 (Phase 1 고객 카드)
|
- WBS-UX-03/04 구현 완료
|
||||||
- WBS-CRM-02/03 Phase 1 구현 예정 (고객 카드 완료 후 순차 진행)
|
- WBS-CRM-01/02/03/04/05 구현 완료 (배포 후 검증 필요)
|
||||||
|
- WBS-CRM-06/07/08 (텔레그램·포털·소셜 로그인) Phase 3 미착수
|
||||||
|
|||||||
@@ -65,6 +65,22 @@ public class InquiryServiceTests
|
|||||||
inquiry.Status = status;
|
inquiry.Status = status;
|
||||||
return Task.CompletedTask;
|
return Task.CompletedTask;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public Task UpdateAdminMemoAsync(int id, string? adminMemo, CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
var inquiry = Inquiries.FirstOrDefault(x => x.Id == id);
|
||||||
|
if (inquiry != null)
|
||||||
|
inquiry.AdminMemo = adminMemo;
|
||||||
|
return Task.CompletedTask;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Task LinkClientAsync(int inquiryId, int clientId, CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
var inquiry = Inquiries.FirstOrDefault(x => x.Id == inquiryId);
|
||||||
|
if (inquiry != null)
|
||||||
|
inquiry.ClientId = clientId;
|
||||||
|
return Task.CompletedTask;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private sealed class FakeInquiryNotificationService : IInquiryNotificationService
|
private sealed class FakeInquiryNotificationService : IInquiryNotificationService
|
||||||
|
|||||||
@@ -17,6 +17,8 @@ public static class DependencyInjection
|
|||||||
services.AddSingleton<SeasonalMarketingService>();
|
services.AddSingleton<SeasonalMarketingService>();
|
||||||
services.AddScoped<ClientService>();
|
services.AddScoped<ClientService>();
|
||||||
services.AddScoped<FaqService>();
|
services.AddScoped<FaqService>();
|
||||||
|
services.AddScoped<ConsultationService>();
|
||||||
|
services.AddScoped<TaxFilingService>();
|
||||||
return services;
|
return services;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -64,6 +64,19 @@ public class ClientService(IClientRepository repository)
|
|||||||
await repository.UpdateAsync(client, ct);
|
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) =>
|
public async Task DeleteAsync(int id, CancellationToken ct = default) =>
|
||||||
await repository.DeleteAsync(id, ct);
|
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)
|
public Task<int> CountByStatusAsync(string status, CancellationToken ct = default)
|
||||||
=> repository.CountByStatusAsync(status, ct);
|
=> 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)
|
public async Task UpdateStatusAsync(int id, string status, string? changedBy = null, CancellationToken ct = default)
|
||||||
{
|
{
|
||||||
if (!InquiryStatusMapper.TryParse(status, out var parsed))
|
if (!InquiryStatusMapper.TryParse(status, out var parsed))
|
||||||
|
|||||||
@@ -4,24 +4,37 @@ using TaxBaik.Domain.Enums;
|
|||||||
|
|
||||||
public static class InquiryStatusMapper
|
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
|
public static string ToStorageValue(InquiryStatus status) => status switch
|
||||||
{
|
{
|
||||||
InquiryStatus.New => "new",
|
InquiryStatus.New => "new",
|
||||||
InquiryStatus.Contacted => "contacted",
|
InquiryStatus.Consulting => "consulting",
|
||||||
InquiryStatus.Completed => "completed",
|
InquiryStatus.Contracted => "contracted",
|
||||||
|
InquiryStatus.Rejected => "rejected",
|
||||||
|
InquiryStatus.Closed => "closed",
|
||||||
_ => throw new ArgumentOutOfRangeException(nameof(status), status, null)
|
_ => throw new ArgumentOutOfRangeException(nameof(status), status, null)
|
||||||
};
|
};
|
||||||
|
|
||||||
public static bool TryParse(string? value, out InquiryStatus status)
|
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,
|
"new" => InquiryStatus.New,
|
||||||
"contacted" => InquiryStatus.Contacted,
|
"consulting" => InquiryStatus.Consulting,
|
||||||
"completed" => InquiryStatus.Completed,
|
"contracted" => InquiryStatus.Contracted,
|
||||||
|
"rejected" => InquiryStatus.Rejected,
|
||||||
|
"closed" => InquiryStatus.Closed,
|
||||||
_ => default
|
_ => default
|
||||||
};
|
};
|
||||||
|
return key is "new" or "consulting" or "contracted" or "rejected" or "closed";
|
||||||
return value?.Trim().ToLowerInvariant() is "new" or "contacted" or "completed";
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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);
|
||||||
|
}
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
namespace TaxBaik.Domain.Entities;
|
||||||
|
|
||||||
|
public class Consultation
|
||||||
|
{
|
||||||
|
public int Id { get; set; }
|
||||||
|
public int ClientId { get; set; }
|
||||||
|
public DateTime ConsultationDate { get; set; }
|
||||||
|
public string? ServiceType { get; set; }
|
||||||
|
public string Summary { get; set; } = null!;
|
||||||
|
public string? Result { get; set; }
|
||||||
|
public decimal? Fee { get; set; }
|
||||||
|
public DateTime CreatedAt { get; set; }
|
||||||
|
}
|
||||||
@@ -10,5 +10,8 @@ public class Inquiry
|
|||||||
public string Message { get; set; } = null!;
|
public string Message { get; set; } = null!;
|
||||||
public string Status { get; set; } = "new";
|
public string Status { get; set; } = "new";
|
||||||
public string? IpAddress { get; set; }
|
public string? IpAddress { get; set; }
|
||||||
|
public int? ClientId { get; set; }
|
||||||
|
public string? AdminMemo { get; set; }
|
||||||
public DateTime CreatedAt { get; set; }
|
public DateTime CreatedAt { get; set; }
|
||||||
|
public DateTime? UpdatedAt { get; set; }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,15 @@
|
|||||||
|
namespace TaxBaik.Domain.Entities;
|
||||||
|
|
||||||
|
public class TaxFiling
|
||||||
|
{
|
||||||
|
public int Id { get; set; }
|
||||||
|
public int ClientId { get; set; }
|
||||||
|
public string FilingType { get; set; } = null!;
|
||||||
|
public DateTime DueDate { get; set; }
|
||||||
|
public string Status { get; set; } = "pending";
|
||||||
|
public string? Memo { get; set; }
|
||||||
|
public DateTime CreatedAt { get; set; }
|
||||||
|
public DateTime UpdatedAt { get; set; }
|
||||||
|
// join
|
||||||
|
public string? ClientName { get; set; }
|
||||||
|
}
|
||||||
@@ -3,6 +3,8 @@ namespace TaxBaik.Domain.Enums;
|
|||||||
public enum InquiryStatus
|
public enum InquiryStatus
|
||||||
{
|
{
|
||||||
New = 0,
|
New = 0,
|
||||||
Contacted = 1,
|
Consulting = 1,
|
||||||
Completed = 2
|
Contracted = 2,
|
||||||
|
Rejected = 3,
|
||||||
|
Closed = 4
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,10 @@
|
|||||||
|
namespace TaxBaik.Domain.Interfaces;
|
||||||
|
|
||||||
|
using TaxBaik.Domain.Entities;
|
||||||
|
|
||||||
|
public interface IConsultationRepository
|
||||||
|
{
|
||||||
|
Task<IEnumerable<Consultation>> GetByClientIdAsync(int clientId, CancellationToken ct = default);
|
||||||
|
Task<int> CreateAsync(Consultation consultation, CancellationToken ct = default);
|
||||||
|
Task DeleteAsync(int id, CancellationToken ct = default);
|
||||||
|
}
|
||||||
@@ -12,4 +12,6 @@ public interface IInquiryRepository
|
|||||||
Task<int> CountThisMonthAsync(CancellationToken cancellationToken = default);
|
Task<int> CountThisMonthAsync(CancellationToken cancellationToken = default);
|
||||||
Task<int> CountByStatusAsync(string status, CancellationToken cancellationToken = default);
|
Task<int> CountByStatusAsync(string status, CancellationToken cancellationToken = default);
|
||||||
Task UpdateStatusAsync(int id, string status, CancellationToken cancellationToken = default);
|
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);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,13 @@
|
|||||||
|
namespace TaxBaik.Domain.Interfaces;
|
||||||
|
|
||||||
|
using TaxBaik.Domain.Entities;
|
||||||
|
|
||||||
|
public interface ITaxFilingRepository
|
||||||
|
{
|
||||||
|
Task<IEnumerable<TaxFiling>> GetByClientIdAsync(int clientId, CancellationToken ct = default);
|
||||||
|
Task<IEnumerable<TaxFiling>> GetUpcomingAsync(int daysAhead, CancellationToken ct = default);
|
||||||
|
Task<TaxFiling?> GetByIdAsync(int id, CancellationToken ct = default);
|
||||||
|
Task<int> CreateAsync(TaxFiling filing, CancellationToken ct = default);
|
||||||
|
Task UpdateAsync(TaxFiling filing, CancellationToken ct = default);
|
||||||
|
Task DeleteAsync(int id, CancellationToken ct = default);
|
||||||
|
}
|
||||||
@@ -18,6 +18,8 @@ public static class DependencyInjection
|
|||||||
services.AddScoped<IAnnouncementRepository, AnnouncementRepository>();
|
services.AddScoped<IAnnouncementRepository, AnnouncementRepository>();
|
||||||
services.AddScoped<IClientRepository, ClientRepository>();
|
services.AddScoped<IClientRepository, ClientRepository>();
|
||||||
services.AddScoped<IFaqRepository, FaqRepository>();
|
services.AddScoped<IFaqRepository, FaqRepository>();
|
||||||
|
services.AddScoped<IConsultationRepository, ConsultationRepository>();
|
||||||
|
services.AddScoped<ITaxFilingRepository, TaxFilingRepository>();
|
||||||
|
|
||||||
return services;
|
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();
|
using var conn = Conn();
|
||||||
return await conn.QueryFirstOrDefaultAsync<Inquiry>(
|
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 });
|
new { Id = id });
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -31,7 +33,8 @@ public class InquiryRepository(IDbConnectionFactory connectionFactory) : BaseRep
|
|||||||
var offset = (page - 1) * pageSize;
|
var offset = (page - 1) * pageSize;
|
||||||
|
|
||||||
using var reader = await conn.QueryMultipleAsync(
|
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
|
FROM inquiries
|
||||||
WHERE @Status::text IS NULL OR status = @Status
|
WHERE @Status::text IS NULL OR status = @Status
|
||||||
ORDER BY created_at DESC
|
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)
|
public async Task UpdateStatusAsync(int id, string status, CancellationToken cancellationToken = default)
|
||||||
{
|
{
|
||||||
using var conn = Conn();
|
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 });
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -44,11 +44,13 @@
|
|||||||
<MudNavLink Href="/taxbaik/admin/dashboard" Match="NavLinkMatch.All" Icon="@Icons.Material.Filled.Dashboard">대시보드</MudNavLink>
|
<MudNavLink Href="/taxbaik/admin/dashboard" Match="NavLinkMatch.All" Icon="@Icons.Material.Filled.Dashboard">대시보드</MudNavLink>
|
||||||
<MudNavGroup Title="고객 관리" Icon="@Icons.Material.Filled.PeopleAlt" Expanded="true">
|
<MudNavGroup Title="고객 관리" Icon="@Icons.Material.Filled.PeopleAlt" Expanded="true">
|
||||||
<MudNavLink Href="/taxbaik/admin/clients" Icon="@Icons.Material.Filled.ContactPage">고객 카드</MudNavLink>
|
<MudNavLink Href="/taxbaik/admin/clients" Icon="@Icons.Material.Filled.ContactPage">고객 카드</MudNavLink>
|
||||||
|
<MudNavLink Href="/taxbaik/admin/tax-filings" Icon="@Icons.Material.Filled.CalendarMonth">신고 일정</MudNavLink>
|
||||||
</MudNavGroup>
|
</MudNavGroup>
|
||||||
<MudNavGroup Title="홈페이지" Icon="@Icons.Material.Filled.Home" Expanded="false">
|
<MudNavGroup Title="홈페이지" Icon="@Icons.Material.Filled.Home" Expanded="false">
|
||||||
<MudNavLink Href="/taxbaik/admin/announcements" Icon="@Icons.Material.Filled.Campaign">공지사항</MudNavLink>
|
<MudNavLink Href="/taxbaik/admin/announcements" Icon="@Icons.Material.Filled.Campaign">공지사항</MudNavLink>
|
||||||
<MudNavLink Href="/taxbaik/admin/faqs" Icon="@Icons.Material.Filled.QuestionAnswer">FAQ 관리</MudNavLink>
|
<MudNavLink Href="/taxbaik/admin/faqs" Icon="@Icons.Material.Filled.QuestionAnswer">FAQ 관리</MudNavLink>
|
||||||
<MudNavLink Href="/taxbaik/admin/blog" Icon="@Icons.Material.Filled.Article">블로그 관리</MudNavLink>
|
<MudNavLink Href="/taxbaik/admin/blog" Icon="@Icons.Material.Filled.Article">블로그 관리</MudNavLink>
|
||||||
|
<MudNavLink Href="/taxbaik/admin/season-simulator" Icon="@Icons.Material.Filled.Preview">시즌 시뮬레이터</MudNavLink>
|
||||||
</MudNavGroup>
|
</MudNavGroup>
|
||||||
<MudNavLink Href="/taxbaik/admin/inquiries" Icon="@Icons.Material.Filled.Forum">문의 관리</MudNavLink>
|
<MudNavLink Href="/taxbaik/admin/inquiries" Icon="@Icons.Material.Filled.Forum">문의 관리</MudNavLink>
|
||||||
<MudNavLink Href="/taxbaik/admin/settings" Icon="@Icons.Material.Filled.Tune">설정</MudNavLink>
|
<MudNavLink Href="/taxbaik/admin/settings" Icon="@Icons.Material.Filled.Tune">설정</MudNavLink>
|
||||||
|
|||||||
@@ -0,0 +1,236 @@
|
|||||||
|
@page "/admin/clients/{ClientId:int}"
|
||||||
|
@attribute [Authorize]
|
||||||
|
@using TaxBaik.Application.Services
|
||||||
|
@inject ClientService ClientService
|
||||||
|
@inject ConsultationService ConsultationService
|
||||||
|
@inject NavigationManager Navigation
|
||||||
|
@inject ISnackbar Snackbar
|
||||||
|
|
||||||
|
<PageTitle>고객 상세</PageTitle>
|
||||||
|
|
||||||
|
@if (client == null)
|
||||||
|
{
|
||||||
|
<MudText>고객을 찾을 수 없습니다.</MudText>
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
<MudStack Row="true" AlignItems="AlignItems.Center" Class="mb-4" Spacing="2">
|
||||||
|
<MudButton Variant="Variant.Outlined" Color="Color.Primary"
|
||||||
|
StartIcon="@Icons.Material.Filled.ArrowBack"
|
||||||
|
@onclick="@(() => Navigation.NavigateTo("/taxbaik/admin/clients"))">
|
||||||
|
목록으로
|
||||||
|
</MudButton>
|
||||||
|
<MudButton Variant="Variant.Outlined" Color="Color.Warning"
|
||||||
|
StartIcon="@Icons.Material.Filled.Edit"
|
||||||
|
Href="@($"/taxbaik/admin/clients/{ClientId}/edit")">
|
||||||
|
수정
|
||||||
|
</MudButton>
|
||||||
|
</MudStack>
|
||||||
|
|
||||||
|
<MudGrid>
|
||||||
|
<MudItem xs="12" md="5">
|
||||||
|
<MudPaper Class="pa-4" Elevation="1">
|
||||||
|
<MudText Typo="Typo.h6" Class="mb-3">고객 정보</MudText>
|
||||||
|
<MudGrid Spacing="2">
|
||||||
|
<MudItem xs="6">
|
||||||
|
<MudText Typo="Typo.subtitle2" Color="Color.Secondary">이름</MudText>
|
||||||
|
<MudText>@client.Name</MudText>
|
||||||
|
</MudItem>
|
||||||
|
<MudItem xs="6">
|
||||||
|
<MudText Typo="Typo.subtitle2" Color="Color.Secondary">상호</MudText>
|
||||||
|
<MudText>@(client.CompanyName ?? "-")</MudText>
|
||||||
|
</MudItem>
|
||||||
|
<MudItem xs="6">
|
||||||
|
<MudText Typo="Typo.subtitle2" Color="Color.Secondary">연락처</MudText>
|
||||||
|
<MudText>@(client.Phone ?? "-")</MudText>
|
||||||
|
</MudItem>
|
||||||
|
<MudItem xs="6">
|
||||||
|
<MudText Typo="Typo.subtitle2" Color="Color.Secondary">이메일</MudText>
|
||||||
|
<MudText>@(client.Email ?? "-")</MudText>
|
||||||
|
</MudItem>
|
||||||
|
<MudItem xs="6">
|
||||||
|
<MudText Typo="Typo.subtitle2" Color="Color.Secondary">서비스</MudText>
|
||||||
|
<MudText>@(client.ServiceType ?? "-")</MudText>
|
||||||
|
</MudItem>
|
||||||
|
<MudItem xs="6">
|
||||||
|
<MudText Typo="Typo.subtitle2" Color="Color.Secondary">사업자 유형</MudText>
|
||||||
|
<MudText>@(client.TaxType ?? "-")</MudText>
|
||||||
|
</MudItem>
|
||||||
|
<MudItem xs="6">
|
||||||
|
<MudText Typo="Typo.subtitle2" Color="Color.Secondary">유입 경로</MudText>
|
||||||
|
<MudText>@(client.Source ?? "-")</MudText>
|
||||||
|
</MudItem>
|
||||||
|
<MudItem xs="6">
|
||||||
|
<MudText Typo="Typo.subtitle2" Color="Color.Secondary">등록일</MudText>
|
||||||
|
<MudText>@client.CreatedAt.ToLocalTime().ToString("yyyy-MM-dd")</MudText>
|
||||||
|
</MudItem>
|
||||||
|
@if (!string.IsNullOrWhiteSpace(client.Memo))
|
||||||
|
{
|
||||||
|
<MudItem xs="12">
|
||||||
|
<MudText Typo="Typo.subtitle2" Color="Color.Secondary">메모</MudText>
|
||||||
|
<MudText Style="white-space: pre-wrap;">@client.Memo</MudText>
|
||||||
|
</MudItem>
|
||||||
|
}
|
||||||
|
</MudGrid>
|
||||||
|
</MudPaper>
|
||||||
|
</MudItem>
|
||||||
|
|
||||||
|
<MudItem xs="12" md="7">
|
||||||
|
<MudPaper Class="pa-4" Elevation="1">
|
||||||
|
<MudStack Row="true" AlignItems="AlignItems.Center" Justify="Justify.SpaceBetween" Class="mb-3">
|
||||||
|
<MudText Typo="Typo.h6">상담 이력</MudText>
|
||||||
|
<MudButton Variant="Variant.Filled" Color="Color.Primary"
|
||||||
|
Size="Size.Small"
|
||||||
|
OnClick="OpenAddConsultation">
|
||||||
|
+ 상담 추가
|
||||||
|
</MudButton>
|
||||||
|
</MudStack>
|
||||||
|
|
||||||
|
@if (showAddForm)
|
||||||
|
{
|
||||||
|
<MudPaper Class="pa-3 mb-3" Outlined="true">
|
||||||
|
<MudGrid Spacing="2">
|
||||||
|
<MudItem xs="12" sm="6">
|
||||||
|
<MudDatePicker @bind-Date="newDate" Label="상담일" DateFormat="yyyy-MM-dd" />
|
||||||
|
</MudItem>
|
||||||
|
<MudItem xs="12" sm="6">
|
||||||
|
<MudSelect T="string" @bind-Value="newServiceType" Label="서비스 분야">
|
||||||
|
@foreach (var t in ClientService.ServiceTypes)
|
||||||
|
{
|
||||||
|
<MudSelectItem Value="@t">@t</MudSelectItem>
|
||||||
|
}
|
||||||
|
</MudSelect>
|
||||||
|
</MudItem>
|
||||||
|
<MudItem xs="12">
|
||||||
|
<MudTextField T="string" @bind-Value="newSummary" Label="상담 내용 *"
|
||||||
|
Lines="3" Variant="Variant.Outlined" Required="true" />
|
||||||
|
</MudItem>
|
||||||
|
<MudItem xs="12" sm="6">
|
||||||
|
<MudSelect T="string" @bind-Value="newResult" Label="결과">
|
||||||
|
<MudSelectItem Value="@("")">-</MudSelectItem>
|
||||||
|
@foreach (var r in ConsultationService.Results)
|
||||||
|
{
|
||||||
|
<MudSelectItem Value="@r">@r</MudSelectItem>
|
||||||
|
}
|
||||||
|
</MudSelect>
|
||||||
|
</MudItem>
|
||||||
|
<MudItem xs="12" sm="6">
|
||||||
|
<MudNumericField T="decimal?" @bind-Value="newFee" Label="수임료 (원)"
|
||||||
|
Format="N0" />
|
||||||
|
</MudItem>
|
||||||
|
</MudGrid>
|
||||||
|
<MudStack Row="true" Class="mt-2" Spacing="2">
|
||||||
|
<MudButton Variant="Variant.Filled" Color="Color.Primary" OnClick="AddConsultation">저장</MudButton>
|
||||||
|
<MudButton Variant="Variant.Outlined" OnClick="@(() => showAddForm = false)">취소</MudButton>
|
||||||
|
</MudStack>
|
||||||
|
</MudPaper>
|
||||||
|
}
|
||||||
|
|
||||||
|
@if (consultations.Count == 0)
|
||||||
|
{
|
||||||
|
<MudText Color="Color.Secondary">상담 이력이 없습니다.</MudText>
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
<MudList T="string" Dense="true">
|
||||||
|
@foreach (var c in consultations)
|
||||||
|
{
|
||||||
|
<MudListItem>
|
||||||
|
<MudPaper Class="pa-3" Outlined="true" Style="width:100%">
|
||||||
|
<MudStack Row="true" AlignItems="AlignItems.Start" Justify="Justify.SpaceBetween">
|
||||||
|
<div>
|
||||||
|
<MudText Typo="Typo.caption" Color="Color.Secondary">
|
||||||
|
@c.ConsultationDate.ToString("yyyy-MM-dd")
|
||||||
|
@if (!string.IsNullOrEmpty(c.ServiceType)) { <text> · @c.ServiceType</text> }
|
||||||
|
</MudText>
|
||||||
|
<MudText Style="white-space: pre-wrap;" Class="mt-1">@c.Summary</MudText>
|
||||||
|
@if (!string.IsNullOrEmpty(c.Result))
|
||||||
|
{
|
||||||
|
<MudChip T="string" Size="Size.Small" Color="Color.Info" Class="mt-1">@c.Result</MudChip>
|
||||||
|
}
|
||||||
|
@if (c.Fee.HasValue)
|
||||||
|
{
|
||||||
|
<MudText Typo="Typo.caption" Color="Color.Secondary" Class="mt-1">
|
||||||
|
수임료: @c.Fee.Value.ToString("N0")원
|
||||||
|
</MudText>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
<MudIconButton Icon="@Icons.Material.Filled.Delete"
|
||||||
|
Size="Size.Small" Color="Color.Error"
|
||||||
|
OnClick="@(() => DeleteConsultation(c.Id))" />
|
||||||
|
</MudStack>
|
||||||
|
</MudPaper>
|
||||||
|
</MudListItem>
|
||||||
|
}
|
||||||
|
</MudList>
|
||||||
|
}
|
||||||
|
</MudPaper>
|
||||||
|
</MudItem>
|
||||||
|
</MudGrid>
|
||||||
|
|
||||||
|
@code {
|
||||||
|
[Parameter]
|
||||||
|
public int ClientId { get; set; }
|
||||||
|
|
||||||
|
private Domain.Entities.Client? client;
|
||||||
|
private List<Domain.Entities.Consultation> consultations = [];
|
||||||
|
|
||||||
|
private bool showAddForm;
|
||||||
|
private DateTime? newDate = DateTime.Today;
|
||||||
|
private string newServiceType = "";
|
||||||
|
private string newSummary = "";
|
||||||
|
private string newResult = "";
|
||||||
|
private decimal? newFee;
|
||||||
|
|
||||||
|
protected override async Task OnInitializedAsync()
|
||||||
|
{
|
||||||
|
await LoadAll();
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task LoadAll()
|
||||||
|
{
|
||||||
|
client = await ClientService.GetByIdAsync(ClientId);
|
||||||
|
consultations = (await ConsultationService.GetByClientIdAsync(ClientId)).ToList();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OpenAddConsultation()
|
||||||
|
{
|
||||||
|
showAddForm = true;
|
||||||
|
newDate = DateTime.Today;
|
||||||
|
newServiceType = "";
|
||||||
|
newSummary = "";
|
||||||
|
newResult = "";
|
||||||
|
newFee = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task AddConsultation()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var c = new Domain.Entities.Consultation
|
||||||
|
{
|
||||||
|
ClientId = ClientId,
|
||||||
|
ConsultationDate = newDate?.ToUniversalTime() ?? DateTime.UtcNow,
|
||||||
|
ServiceType = string.IsNullOrWhiteSpace(newServiceType) ? null : newServiceType,
|
||||||
|
Summary = newSummary,
|
||||||
|
Result = string.IsNullOrWhiteSpace(newResult) ? null : newResult,
|
||||||
|
Fee = newFee
|
||||||
|
};
|
||||||
|
await ConsultationService.CreateAsync(c);
|
||||||
|
showAddForm = false;
|
||||||
|
consultations = (await ConsultationService.GetByClientIdAsync(ClientId)).ToList();
|
||||||
|
Snackbar.Add("상담이 추가되었습니다.", Severity.Success);
|
||||||
|
}
|
||||||
|
catch (ValidationException ex)
|
||||||
|
{
|
||||||
|
Snackbar.Add(ex.Message, Severity.Error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task DeleteConsultation(int id)
|
||||||
|
{
|
||||||
|
await ConsultationService.DeleteAsync(id);
|
||||||
|
consultations = (await ConsultationService.GetByClientIdAsync(ClientId)).ToList();
|
||||||
|
Snackbar.Add("삭제되었습니다.", Severity.Info);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -2,6 +2,7 @@
|
|||||||
@attribute [Authorize]
|
@attribute [Authorize]
|
||||||
@using TaxBaik.Application.Services
|
@using TaxBaik.Application.Services
|
||||||
@inject AdminDashboardService DashboardService
|
@inject AdminDashboardService DashboardService
|
||||||
|
@inject TaxFilingService FilingService
|
||||||
|
|
||||||
<PageTitle>대시보드</PageTitle>
|
<PageTitle>대시보드</PageTitle>
|
||||||
|
|
||||||
@@ -50,6 +51,50 @@
|
|||||||
</MudItem>
|
</MudItem>
|
||||||
</MudGrid>
|
</MudGrid>
|
||||||
|
|
||||||
|
@if (upcomingFilings.Count > 0)
|
||||||
|
{
|
||||||
|
<MudPaper Class="admin-surface mt-4" Elevation="0">
|
||||||
|
<div class="admin-section-header">
|
||||||
|
<div>
|
||||||
|
<MudText Typo="Typo.h6">이번 달 마감 임박 신고</MudText>
|
||||||
|
<MudText Typo="Typo.body2">30일 이내 신고 예정 건</MudText>
|
||||||
|
</div>
|
||||||
|
<MudButton Variant="Variant.Outlined" Color="Color.Primary" Href="/taxbaik/admin/tax-filings">전체 일정 보기</MudButton>
|
||||||
|
</div>
|
||||||
|
<MudSimpleTable Striped="true" Dense="true" Class="admin-table">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>고객</th>
|
||||||
|
<th>신고 유형</th>
|
||||||
|
<th>기한</th>
|
||||||
|
<th>D-day</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
@foreach (var f in upcomingFilings)
|
||||||
|
{
|
||||||
|
var dday = (f.DueDate.Date - DateTime.Today).Days;
|
||||||
|
<tr>
|
||||||
|
<td>@f.ClientName</td>
|
||||||
|
<td>@f.FilingType</td>
|
||||||
|
<td>@f.DueDate.ToString("yyyy-MM-dd")</td>
|
||||||
|
<td>
|
||||||
|
@if (dday <= 7)
|
||||||
|
{
|
||||||
|
<MudChip T="string" Size="Size.Small" Color="Color.Error">D-@dday</MudChip>
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
<span>D-@dday</span>
|
||||||
|
}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
}
|
||||||
|
</tbody>
|
||||||
|
</MudSimpleTable>
|
||||||
|
</MudPaper>
|
||||||
|
}
|
||||||
|
|
||||||
<MudPaper Class="admin-surface mt-4" Elevation="0">
|
<MudPaper Class="admin-surface mt-4" Elevation="0">
|
||||||
<div class="admin-section-header">
|
<div class="admin-section-header">
|
||||||
<div>
|
<div>
|
||||||
@@ -76,8 +121,7 @@
|
|||||||
<td>@inquiry.Phone</td>
|
<td>@inquiry.Phone</td>
|
||||||
<td>@inquiry.ServiceType</td>
|
<td>@inquiry.ServiceType</td>
|
||||||
<td>
|
<td>
|
||||||
<MudChip Size="Size.Small"
|
<MudChip T="string" Size="Size.Small" Color="@StatusColor(inquiry.Status)">
|
||||||
Color="@(inquiry.Status == "new" ? Color.Warning : inquiry.Status == "contacted" ? Color.Info : Color.Success)">
|
|
||||||
@GetStatusLabel(inquiry.Status)
|
@GetStatusLabel(inquiry.Status)
|
||||||
</MudChip>
|
</MudChip>
|
||||||
</td>
|
</td>
|
||||||
@@ -90,17 +134,26 @@
|
|||||||
|
|
||||||
@code {
|
@code {
|
||||||
private AdminDashboardSummary summary = new(0, 0, 0, 0, []);
|
private AdminDashboardSummary summary = new(0, 0, 0, 0, []);
|
||||||
|
private List<Domain.Entities.TaxFiling> upcomingFilings = [];
|
||||||
|
|
||||||
protected override async Task OnInitializedAsync()
|
protected override async Task OnInitializedAsync()
|
||||||
{
|
{
|
||||||
summary = await DashboardService.GetSummaryAsync();
|
var summaryTask = DashboardService.GetSummaryAsync();
|
||||||
}
|
var filingsTask = FilingService.GetUpcomingAsync(30);
|
||||||
|
await Task.WhenAll(summaryTask, filingsTask);
|
||||||
|
summary = await summaryTask;
|
||||||
|
upcomingFilings = (await filingsTask).ToList();
|
||||||
|
}
|
||||||
|
|
||||||
private static string GetStatusLabel(string status) => status switch
|
private static string GetStatusLabel(string status) => InquiryStatusMapper.Labels.GetValueOrDefault(status, status);
|
||||||
|
|
||||||
|
private static Color StatusColor(string status) => status switch
|
||||||
{
|
{
|
||||||
"new" => "신규",
|
"new" => Color.Warning,
|
||||||
"contacted" => "연락함",
|
"consulting" => Color.Info,
|
||||||
"completed" => "완료",
|
"contracted" => Color.Success,
|
||||||
_ => status
|
"rejected" => Color.Error,
|
||||||
|
"closed" => Color.Dark,
|
||||||
|
_ => Color.Default
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
@attribute [Authorize]
|
@attribute [Authorize]
|
||||||
@using TaxBaik.Application.Services
|
@using TaxBaik.Application.Services
|
||||||
@inject InquiryService InquiryService
|
@inject InquiryService InquiryService
|
||||||
|
@inject ClientService ClientService
|
||||||
@inject NavigationManager Navigation
|
@inject NavigationManager Navigation
|
||||||
@inject ISnackbar Snackbar
|
@inject ISnackbar Snackbar
|
||||||
|
|
||||||
@@ -13,58 +14,91 @@
|
|||||||
Color="Color.Primary"
|
Color="Color.Primary"
|
||||||
StartIcon="@Icons.Material.Filled.ArrowBack"
|
StartIcon="@Icons.Material.Filled.ArrowBack"
|
||||||
@onclick="@(() => Navigation.NavigateTo("/taxbaik/admin/inquiries"))">
|
@onclick="@(() => Navigation.NavigateTo("/taxbaik/admin/inquiries"))">
|
||||||
문의 목록으로 돌아가기
|
문의 목록으로
|
||||||
</MudButton>
|
</MudButton>
|
||||||
|
|
||||||
<MudPaper Class="pa-4 mt-4" Elevation="1">
|
<MudGrid Class="mt-4">
|
||||||
<MudStack Row="true" AlignItems="AlignItems.Center" Justify="Justify.SpaceBetween" Class="mb-4">
|
<MudItem xs="12" md="8">
|
||||||
<MudText Typo="Typo.h5">문의 상세</MudText>
|
<MudPaper Class="pa-4" Elevation="1">
|
||||||
<MudButton Variant="Variant.Filled"
|
<MudText Typo="Typo.h6" Class="mb-3">문의 정보</MudText>
|
||||||
Color="Color.Primary"
|
<MudGrid>
|
||||||
StartIcon="@Icons.Material.Filled.List"
|
<MudItem xs="12" sm="6">
|
||||||
Href="/taxbaik/admin/inquiries">
|
<MudText Typo="Typo.subtitle2" Color="Color.Secondary">이름</MudText>
|
||||||
다른 문의도 보기
|
<MudText>@inquiry.Name</MudText>
|
||||||
</MudButton>
|
</MudItem>
|
||||||
</MudStack>
|
<MudItem xs="12" sm="6">
|
||||||
|
<MudText Typo="Typo.subtitle2" Color="Color.Secondary">연락처</MudText>
|
||||||
|
<MudText>@inquiry.Phone</MudText>
|
||||||
|
</MudItem>
|
||||||
|
<MudItem xs="12" sm="6">
|
||||||
|
<MudText Typo="Typo.subtitle2" Color="Color.Secondary">이메일</MudText>
|
||||||
|
<MudText>@(inquiry.Email ?? "-")</MudText>
|
||||||
|
</MudItem>
|
||||||
|
<MudItem xs="12" sm="6">
|
||||||
|
<MudText Typo="Typo.subtitle2" Color="Color.Secondary">분야</MudText>
|
||||||
|
<MudText>@inquiry.ServiceType</MudText>
|
||||||
|
</MudItem>
|
||||||
|
<MudItem xs="12">
|
||||||
|
<MudText Typo="Typo.subtitle2" Color="Color.Secondary">문의 내용</MudText>
|
||||||
|
<MudPaper Class="pa-3 mt-1" Outlined="true">
|
||||||
|
<MudText Style="white-space: pre-wrap;">@inquiry.Message</MudText>
|
||||||
|
</MudPaper>
|
||||||
|
</MudItem>
|
||||||
|
<MudItem xs="12">
|
||||||
|
<MudText Typo="Typo.subtitle2" Color="Color.Secondary">접수일시</MudText>
|
||||||
|
<MudText>@inquiry.CreatedAt.ToLocalTime().ToString("yyyy-MM-dd HH:mm")</MudText>
|
||||||
|
</MudItem>
|
||||||
|
</MudGrid>
|
||||||
|
</MudPaper>
|
||||||
|
|
||||||
<MudGrid>
|
<MudPaper Class="pa-4 mt-4" Elevation="1">
|
||||||
<MudItem xs="12" md="6">
|
<MudText Typo="Typo.h6" Class="mb-3">담당자 메모</MudText>
|
||||||
<MudText Typo="Typo.subtitle1">이름</MudText>
|
<MudTextField T="string" @bind-Value="adminMemo" Label="내부 메모 (고객에게 미노출)"
|
||||||
<MudText>@inquiry.Name</MudText>
|
Lines="4" Variant="Variant.Outlined" />
|
||||||
</MudItem>
|
<MudButton Class="mt-2" Variant="Variant.Filled" Color="Color.Primary"
|
||||||
<MudItem xs="12" md="6">
|
OnClick="SaveMemo">메모 저장</MudButton>
|
||||||
<MudText Typo="Typo.subtitle1">연락처</MudText>
|
</MudPaper>
|
||||||
<MudText>@inquiry.Phone</MudText>
|
</MudItem>
|
||||||
</MudItem>
|
|
||||||
<MudItem xs="12" md="6">
|
<MudItem xs="12" md="4">
|
||||||
<MudText Typo="Typo.subtitle1">이메일</MudText>
|
<MudPaper Class="pa-4" Elevation="1">
|
||||||
<MudText>@inquiry.Email</MudText>
|
<MudText Typo="Typo.h6" Class="mb-3">처리 상태</MudText>
|
||||||
</MudItem>
|
<MudStack Spacing="2">
|
||||||
<MudItem xs="12" md="6">
|
@foreach (var (key, label) in InquiryStatusMapper.Labels)
|
||||||
<MudText Typo="Typo.subtitle1">분야</MudText>
|
{
|
||||||
<MudText>@inquiry.ServiceType</MudText>
|
<MudButton Variant="@(inquiry.Status == key ? Variant.Filled : Variant.Outlined)"
|
||||||
</MudItem>
|
Color="@StatusColor(key)"
|
||||||
<MudItem xs="12">
|
FullWidth="true"
|
||||||
<MudText Typo="Typo.subtitle1">메시지</MudText>
|
OnClick="@(() => OnStatusChanged(key))">
|
||||||
<MudPaper Class="pa-3 mt-2" Outlined="true">
|
@label
|
||||||
<MudText Style="white-space: pre-wrap;">@inquiry.Message</MudText>
|
</MudButton>
|
||||||
</MudPaper>
|
}
|
||||||
</MudItem>
|
|
||||||
<MudItem xs="12">
|
|
||||||
<MudText Typo="Typo.subtitle1">상태</MudText>
|
|
||||||
<MudSelect T="string" Value="inquiry.Status" ValueChanged="@((string status) => OnStatusChanged(status))" Label="상태 변경">
|
|
||||||
<MudSelectItem Value="@("new")">신규</MudSelectItem>
|
|
||||||
<MudSelectItem Value="@("contacted")">연락함</MudSelectItem>
|
|
||||||
<MudSelectItem Value="@("completed")">완료</MudSelectItem>
|
|
||||||
</MudSelect>
|
|
||||||
<MudStack Row="true" Class="mt-3" Spacing="2">
|
|
||||||
<MudButton Variant="Variant.Outlined" Color="Color.Warning" OnClick="@(() => OnStatusChanged("new"))">신규</MudButton>
|
|
||||||
<MudButton Variant="Variant.Outlined" Color="Color.Info" OnClick="@(() => OnStatusChanged("contacted"))">연락함</MudButton>
|
|
||||||
<MudButton Variant="Variant.Outlined" Color="Color.Success" OnClick="@(() => OnStatusChanged("completed"))">완료</MudButton>
|
|
||||||
</MudStack>
|
</MudStack>
|
||||||
</MudItem>
|
</MudPaper>
|
||||||
</MudGrid>
|
|
||||||
</MudPaper>
|
@if (inquiry.ClientId == null)
|
||||||
|
{
|
||||||
|
<MudPaper Class="pa-4 mt-4" Elevation="1">
|
||||||
|
<MudText Typo="Typo.h6" Class="mb-3">고객 카드 생성</MudText>
|
||||||
|
<MudText Typo="Typo.body2" Class="mb-3">이 문의를 고객 카드로 등록합니다.</MudText>
|
||||||
|
<MudButton Variant="Variant.Filled" Color="Color.Success" FullWidth="true"
|
||||||
|
OnClick="ConvertToClient">
|
||||||
|
고객으로 등록
|
||||||
|
</MudButton>
|
||||||
|
</MudPaper>
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
<MudPaper Class="pa-4 mt-4" Elevation="1">
|
||||||
|
<MudText Typo="Typo.h6" Class="mb-3">연결된 고객</MudText>
|
||||||
|
<MudButton Variant="Variant.Outlined" Color="Color.Primary" FullWidth="true"
|
||||||
|
Href="@($"/taxbaik/admin/clients/{inquiry.ClientId}")">
|
||||||
|
고객 카드 보기
|
||||||
|
</MudButton>
|
||||||
|
</MudPaper>
|
||||||
|
}
|
||||||
|
</MudItem>
|
||||||
|
</MudGrid>
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
@@ -76,17 +110,17 @@ else
|
|||||||
public int InquiryId { get; set; }
|
public int InquiryId { get; set; }
|
||||||
|
|
||||||
private Domain.Entities.Inquiry? inquiry;
|
private Domain.Entities.Inquiry? inquiry;
|
||||||
|
private string adminMemo = "";
|
||||||
|
|
||||||
protected override async Task OnInitializedAsync()
|
protected override async Task OnInitializedAsync()
|
||||||
{
|
{
|
||||||
inquiry = await InquiryService.GetByIdAsync(InquiryId);
|
inquiry = await InquiryService.GetByIdAsync(InquiryId);
|
||||||
|
adminMemo = inquiry?.AdminMemo ?? "";
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task OnStatusChanged(string status)
|
private async Task OnStatusChanged(string status)
|
||||||
{
|
{
|
||||||
if (inquiry == null)
|
if (inquiry == null) return;
|
||||||
return;
|
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
await InquiryService.UpdateStatusAsync(inquiry.Id, status, "관리자");
|
await InquiryService.UpdateStatusAsync(inquiry.Id, status, "관리자");
|
||||||
@@ -98,4 +132,40 @@ else
|
|||||||
Snackbar.Add(ex.Message, Severity.Error);
|
Snackbar.Add(ex.Message, Severity.Error);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async Task SaveMemo()
|
||||||
|
{
|
||||||
|
if (inquiry == null) return;
|
||||||
|
await InquiryService.UpdateAdminMemoAsync(inquiry.Id, adminMemo);
|
||||||
|
inquiry.AdminMemo = adminMemo;
|
||||||
|
Snackbar.Add("메모가 저장되었습니다.", Severity.Success);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task ConvertToClient()
|
||||||
|
{
|
||||||
|
if (inquiry == null) return;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var clientId = await ClientService.CreateFromInquiryAsync(inquiry.Name, inquiry.Phone, inquiry.ServiceType);
|
||||||
|
await InquiryService.LinkClientAsync(inquiry.Id, clientId);
|
||||||
|
await InquiryService.UpdateStatusAsync(inquiry.Id, "consulting", "관리자");
|
||||||
|
inquiry.ClientId = clientId;
|
||||||
|
inquiry.Status = "consulting";
|
||||||
|
Snackbar.Add("고객 카드가 생성되었습니다.", Severity.Success);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Snackbar.Add($"오류: {ex.Message}", Severity.Error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private Color StatusColor(string status) => status switch
|
||||||
|
{
|
||||||
|
"new" => Color.Default,
|
||||||
|
"consulting" => Color.Info,
|
||||||
|
"contracted" => Color.Success,
|
||||||
|
"rejected" => Color.Error,
|
||||||
|
"closed" => Color.Dark,
|
||||||
|
_ => Color.Default
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -21,11 +21,17 @@
|
|||||||
<MudTabPanel Text="신규">
|
<MudTabPanel Text="신규">
|
||||||
<InquiryTable Status="new" />
|
<InquiryTable Status="new" />
|
||||||
</MudTabPanel>
|
</MudTabPanel>
|
||||||
<MudTabPanel Text="연락함">
|
<MudTabPanel Text="상담중">
|
||||||
<InquiryTable Status="contacted" />
|
<InquiryTable Status="consulting" />
|
||||||
</MudTabPanel>
|
</MudTabPanel>
|
||||||
<MudTabPanel Text="완료">
|
<MudTabPanel Text="계약완료">
|
||||||
<InquiryTable Status="completed" />
|
<InquiryTable Status="contracted" />
|
||||||
|
</MudTabPanel>
|
||||||
|
<MudTabPanel Text="거절">
|
||||||
|
<InquiryTable Status="rejected" />
|
||||||
|
</MudTabPanel>
|
||||||
|
<MudTabPanel Text="종결">
|
||||||
|
<InquiryTable Status="closed" />
|
||||||
</MudTabPanel>
|
</MudTabPanel>
|
||||||
</MudTabs>
|
</MudTabs>
|
||||||
</MudPaper>
|
</MudPaper>
|
||||||
|
|||||||
@@ -0,0 +1,211 @@
|
|||||||
|
@page "/admin/season-simulator"
|
||||||
|
@attribute [Authorize]
|
||||||
|
@using TaxBaik.Application.Seasonal
|
||||||
|
@using TaxBaik.Application.Services
|
||||||
|
|
||||||
|
<PageTitle>시즌 시뮬레이터</PageTitle>
|
||||||
|
|
||||||
|
<section class="admin-page-hero">
|
||||||
|
<div>
|
||||||
|
<MudText Typo="Typo.caption" Class="admin-eyebrow">Season Preview</MudText>
|
||||||
|
<MudText Typo="Typo.h4" Class="admin-page-title">시즌 시뮬레이터</MudText>
|
||||||
|
<MudText Typo="Typo.body2" Class="admin-page-subtitle">날짜를 선택해 홈페이지 시즌 화면이 어떻게 보이는지 미리 확인합니다.</MudText>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<MudGrid>
|
||||||
|
<MudItem xs="12" md="4">
|
||||||
|
<MudPaper Class="pa-4" Elevation="1">
|
||||||
|
<MudText Typo="Typo.h6" Class="mb-3">시뮬레이션 날짜</MudText>
|
||||||
|
<MudDatePicker @bind-Date="simulationDate" Label="날짜 선택" DateFormat="yyyy-MM-dd" PickerVariant="PickerVariant.Static" />
|
||||||
|
<MudDivider Class="my-3" />
|
||||||
|
<MudText Typo="Typo.subtitle2" Class="mb-2">연간 세무 캘린더</MudText>
|
||||||
|
@foreach (var season in TaxSeasonCalendar.Seasons)
|
||||||
|
{
|
||||||
|
<MudButton Variant="Variant.Outlined" Size="Size.Small" FullWidth="true"
|
||||||
|
Class="mb-1" Color="Color.Primary"
|
||||||
|
OnClick="@(() => JumpToSeason(season))">
|
||||||
|
@season.StartMonth/@season.StartDay — @season.Name
|
||||||
|
</MudButton>
|
||||||
|
}
|
||||||
|
</MudPaper>
|
||||||
|
</MudItem>
|
||||||
|
|
||||||
|
<MudItem xs="12" md="8">
|
||||||
|
<MudPaper Class="pa-4" Elevation="1">
|
||||||
|
<MudText Typo="Typo.h6" Class="mb-1">
|
||||||
|
@(simulationDate?.ToString("yyyy년 MM월 dd일") ?? "날짜를 선택하세요") 홈페이지 미리보기
|
||||||
|
</MudText>
|
||||||
|
@if (activeSeason != null)
|
||||||
|
{
|
||||||
|
<MudChip T="string" Color="Color.Warning" Size="Size.Small" Class="mb-3">
|
||||||
|
@activeSeason.Name 시즌 활성
|
||||||
|
</MudChip>
|
||||||
|
<MudDivider Class="mb-4" />
|
||||||
|
<!-- Hero 섹션 미리보기 -->
|
||||||
|
<div style="background: linear-gradient(135deg, #1a365d 0%, #2a4365 100%); border-radius: 12px; padding: 2rem; color: white; margin-bottom: 1.5rem;">
|
||||||
|
@if (activeSeason.DaysUntilDeadline <= 7 && activeSeason.DaysUntilDeadline >= 0)
|
||||||
|
{
|
||||||
|
<div style="background: #f59e0b; color: #1a202c; display: inline-block; padding: 4px 12px; border-radius: 20px; font-size: 0.8rem; font-weight: 700; margin-bottom: 1rem;">
|
||||||
|
D-@activeSeason.DaysUntilDeadline 마감 임박
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
<div style="font-size: 1.8rem; font-weight: 800; white-space: pre-line; margin-bottom: 0.5rem; line-height: 1.3;">
|
||||||
|
@activeSeason.HeroHeadline
|
||||||
|
</div>
|
||||||
|
<div style="font-size: 0.95rem; color: rgba(255,255,255,0.8); margin-bottom: 1.5rem;">
|
||||||
|
@activeSeason.HeroSubtext
|
||||||
|
</div>
|
||||||
|
<div style="display: flex; gap: 0.75rem; flex-wrap: wrap;">
|
||||||
|
<div style="background: #e53e3e; color: white; padding: 10px 20px; border-radius: 8px; font-weight: 700; font-size: 0.95rem;">
|
||||||
|
@activeSeason.CtaText
|
||||||
|
</div>
|
||||||
|
<div style="background: transparent; border: 2px solid rgba(255,255,255,0.5); color: white; padding: 10px 20px; border-radius: 8px; font-size: 0.95rem;">
|
||||||
|
서비스 안내
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<MudGrid Spacing="2">
|
||||||
|
<MudItem xs="6">
|
||||||
|
<MudText Typo="Typo.subtitle2" Color="Color.Secondary">활성 시즌 키</MudText>
|
||||||
|
<MudText><code>@activeSeason.Key</code></MudText>
|
||||||
|
</MudItem>
|
||||||
|
<MudItem xs="6">
|
||||||
|
<MudText Typo="Typo.subtitle2" Color="Color.Secondary">마감까지</MudText>
|
||||||
|
<MudText>
|
||||||
|
@if (activeSeason.DaysUntilDeadline >= 0)
|
||||||
|
{
|
||||||
|
<MudChip T="string" Size="Size.Small"
|
||||||
|
Color="@(activeSeason.DaysUntilDeadline <= 7 ? Color.Error : Color.Warning)">
|
||||||
|
D-@activeSeason.DaysUntilDeadline
|
||||||
|
</MudChip>
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
<span>마감 후 @(-activeSeason.DaysUntilDeadline)일</span>
|
||||||
|
}
|
||||||
|
</MudText>
|
||||||
|
</MudItem>
|
||||||
|
<MudItem xs="6">
|
||||||
|
<MudText Typo="Typo.subtitle2" Color="Color.Secondary">포커스 서비스</MudText>
|
||||||
|
<MudText>@activeSeason.FocusService</MudText>
|
||||||
|
</MudItem>
|
||||||
|
<MudItem xs="6">
|
||||||
|
<MudText Typo="Typo.subtitle2" Color="Color.Secondary">블로그 카테고리</MudText>
|
||||||
|
<MudText>@activeSeason.RelatedCategorySlug</MudText>
|
||||||
|
</MudItem>
|
||||||
|
<MudItem xs="12">
|
||||||
|
<MudText Typo="Typo.subtitle2" Color="Color.Secondary">긴박감 배지 문구</MudText>
|
||||||
|
<MudText><code>@activeSeason.UrgencyBadge</code></MudText>
|
||||||
|
</MudItem>
|
||||||
|
</MudGrid>
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
<MudAlert Severity="Severity.Info">
|
||||||
|
선택한 날짜(@(simulationDate?.ToString("MM월 dd일") ?? "-"))는 시즌 비활성 기간입니다.
|
||||||
|
홈페이지는 기본 Hero를 표시합니다.
|
||||||
|
</MudAlert>
|
||||||
|
<div style="background: linear-gradient(135deg, #1a365d 0%, #2a4365 100%); border-radius: 12px; padding: 2rem; color: white; margin-top: 1.5rem;">
|
||||||
|
<div style="font-size: 1.8rem; font-weight: 800; margin-bottom: 0.5rem;">
|
||||||
|
사업자 세금, 부동산,<br/>가족자산까지
|
||||||
|
</div>
|
||||||
|
<div style="font-size: 0.95rem; color: rgba(255,255,255,0.8); margin-bottom: 1.5rem;">
|
||||||
|
세무사·부동산중개사·보험설계사 자격 보유 | 온라인 맞춤 상담
|
||||||
|
</div>
|
||||||
|
<div style="background: #e53e3e; color: white; display: inline-block; padding: 10px 20px; border-radius: 8px; font-weight: 700;">
|
||||||
|
무료 상담 신청
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
</MudPaper>
|
||||||
|
|
||||||
|
<MudPaper Class="pa-4 mt-4" Elevation="1">
|
||||||
|
<MudText Typo="Typo.h6" Class="mb-3">연간 시즌 타임라인</MudText>
|
||||||
|
<MudSimpleTable Dense="true">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>기간</th>
|
||||||
|
<th>시즌</th>
|
||||||
|
<th>블로그 카테고리</th>
|
||||||
|
<th>상태</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
@foreach (var s in TaxSeasonCalendar.Seasons)
|
||||||
|
{
|
||||||
|
var isActive = activeSeason?.Key == s.Key;
|
||||||
|
<tr style="@(isActive ? "background: rgba(66,153,225,0.1);" : "")">
|
||||||
|
<td style="white-space: nowrap;">
|
||||||
|
@s.StartMonth/@s.StartDay ~ @s.EndMonth/@s.EndDay
|
||||||
|
</td>
|
||||||
|
<td>@s.Name</td>
|
||||||
|
<td><code style="font-size:0.8rem;">@s.RelatedCategorySlug</code></td>
|
||||||
|
<td>
|
||||||
|
@if (isActive)
|
||||||
|
{
|
||||||
|
<MudChip T="string" Size="Size.Small" Color="Color.Success">활성</MudChip>
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
<span style="color: #a0aec0; font-size: 0.85rem;">비활성</span>
|
||||||
|
}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
}
|
||||||
|
</tbody>
|
||||||
|
</MudSimpleTable>
|
||||||
|
</MudPaper>
|
||||||
|
</MudItem>
|
||||||
|
</MudGrid>
|
||||||
|
|
||||||
|
@code {
|
||||||
|
private DateTime? simulationDate = DateTime.Today;
|
||||||
|
private CurrentSeasonDto? activeSeason;
|
||||||
|
|
||||||
|
protected override void OnInitialized() => ComputeSeason();
|
||||||
|
|
||||||
|
private void ComputeSeason()
|
||||||
|
{
|
||||||
|
if (simulationDate == null) { activeSeason = null; return; }
|
||||||
|
var date = simulationDate.Value;
|
||||||
|
var season = TaxSeasonCalendar.Seasons.FirstOrDefault(s =>
|
||||||
|
{
|
||||||
|
var start = new DateTime(date.Year, s.StartMonth, s.StartDay);
|
||||||
|
var endYear = (s.EndMonth < s.StartMonth) ? date.Year + 1 : date.Year;
|
||||||
|
var end = new DateTime(endYear, s.EndMonth, s.EndDay);
|
||||||
|
return date >= start && date <= end;
|
||||||
|
});
|
||||||
|
|
||||||
|
if (season == null) { activeSeason = null; return; }
|
||||||
|
|
||||||
|
var endYearCalc = (season.EndMonth < season.StartMonth) ? date.Year + 1 : date.Year;
|
||||||
|
var deadline = new DateTime(endYearCalc, season.EndMonth, season.EndDay);
|
||||||
|
var ddays = (deadline.Date - date.Date).Days;
|
||||||
|
|
||||||
|
var badge = ddays <= 7 && ddays >= 0
|
||||||
|
? season.UrgencyBadge.Replace("{n}", ddays.ToString())
|
||||||
|
: season.UrgencyBadge;
|
||||||
|
|
||||||
|
activeSeason = new CurrentSeasonDto
|
||||||
|
{
|
||||||
|
Key = season.Key,
|
||||||
|
Name = season.Name,
|
||||||
|
HeroHeadline = season.HeroHeadline,
|
||||||
|
HeroSubtext = season.HeroSubtext,
|
||||||
|
UrgencyBadge = badge,
|
||||||
|
FocusService = season.FocusService,
|
||||||
|
RelatedCategorySlug = season.RelatedCategorySlug,
|
||||||
|
CtaText = season.CtaText,
|
||||||
|
DaysUntilDeadline = ddays,
|
||||||
|
Deadline = deadline
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private void JumpToSeason(TaxSeason season)
|
||||||
|
{
|
||||||
|
simulationDate = new DateTime(DateTime.Today.Year, season.StartMonth, season.StartDay);
|
||||||
|
ComputeSeason();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,80 @@
|
|||||||
|
@using TaxBaik.Application.Services
|
||||||
|
@inject TaxFilingService FilingService
|
||||||
|
@inject ISnackbar Snackbar
|
||||||
|
|
||||||
|
@if (Filings == null || Filings.Count == 0)
|
||||||
|
{
|
||||||
|
<MudText Class="pa-4" Color="Color.Secondary">항목이 없습니다.</MudText>
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
<MudTable Items="Filings" Hover="true" Dense="true" Class="mt-2">
|
||||||
|
<HeaderContent>
|
||||||
|
<MudTh>고객</MudTh>
|
||||||
|
<MudTh>신고 유형</MudTh>
|
||||||
|
<MudTh>기한</MudTh>
|
||||||
|
<MudTh>D-day</MudTh>
|
||||||
|
<MudTh>메모</MudTh>
|
||||||
|
<MudTh>처리</MudTh>
|
||||||
|
</HeaderContent>
|
||||||
|
<RowTemplate>
|
||||||
|
<MudTd>@context.ClientName</MudTd>
|
||||||
|
<MudTd>@context.FilingType</MudTd>
|
||||||
|
<MudTd>@context.DueDate.ToString("yyyy-MM-dd")</MudTd>
|
||||||
|
<MudTd>
|
||||||
|
@{
|
||||||
|
var dday = (context.DueDate.Date - DateTime.Today).Days;
|
||||||
|
}
|
||||||
|
@if (dday < 0)
|
||||||
|
{
|
||||||
|
<MudChip T="string" Size="Size.Small" Color="Color.Error">D+@(-dday)</MudChip>
|
||||||
|
}
|
||||||
|
else if (dday <= 7)
|
||||||
|
{
|
||||||
|
<MudChip T="string" Size="Size.Small" Color="Color.Warning">D-@dday</MudChip>
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
<MudText Typo="Typo.body2">D-@dday</MudText>
|
||||||
|
}
|
||||||
|
</MudTd>
|
||||||
|
<MudTd>@(context.Memo ?? "")</MudTd>
|
||||||
|
<MudTd>
|
||||||
|
@if (context.Status == "pending")
|
||||||
|
{
|
||||||
|
<MudButton Size="Size.Small" Variant="Variant.Filled" Color="Color.Success"
|
||||||
|
OnClick="@(() => MarkFiled(context))">완료</MudButton>
|
||||||
|
}
|
||||||
|
else if (context.Status == "filed")
|
||||||
|
{
|
||||||
|
<MudChip T="string" Size="Size.Small" Color="Color.Success">완료</MudChip>
|
||||||
|
}
|
||||||
|
<MudIconButton Icon="@Icons.Material.Filled.Delete" Size="Size.Small" Color="Color.Error"
|
||||||
|
OnClick="@(() => DeleteFiling(context.Id))" />
|
||||||
|
</MudTd>
|
||||||
|
</RowTemplate>
|
||||||
|
</MudTable>
|
||||||
|
}
|
||||||
|
|
||||||
|
@code {
|
||||||
|
[Parameter]
|
||||||
|
public List<Domain.Entities.TaxFiling>? Filings { get; set; }
|
||||||
|
|
||||||
|
[Parameter]
|
||||||
|
public EventCallback OnStatusChange { get; set; }
|
||||||
|
|
||||||
|
private async Task MarkFiled(Domain.Entities.TaxFiling filing)
|
||||||
|
{
|
||||||
|
filing.Status = "filed";
|
||||||
|
await FilingService.UpdateAsync(filing);
|
||||||
|
Snackbar.Add("신고 완료 처리되었습니다.", Severity.Success);
|
||||||
|
await OnStatusChange.InvokeAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task DeleteFiling(int id)
|
||||||
|
{
|
||||||
|
await FilingService.DeleteAsync(id);
|
||||||
|
Snackbar.Add("삭제되었습니다.", Severity.Info);
|
||||||
|
await OnStatusChange.InvokeAsync();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,126 @@
|
|||||||
|
@page "/admin/tax-filings"
|
||||||
|
@attribute [Authorize]
|
||||||
|
@using TaxBaik.Application.Services
|
||||||
|
@inject TaxFilingService FilingService
|
||||||
|
@inject ClientService ClientService
|
||||||
|
@inject ISnackbar Snackbar
|
||||||
|
|
||||||
|
<PageTitle>신고 일정 관리</PageTitle>
|
||||||
|
|
||||||
|
<section class="admin-page-hero">
|
||||||
|
<div>
|
||||||
|
<MudText Typo="Typo.caption" Class="admin-eyebrow">Tax Schedule</MudText>
|
||||||
|
<MudText Typo="Typo.h4" Class="admin-page-title">신고 일정</MudText>
|
||||||
|
<MudText Typo="Typo.body2" Class="admin-page-subtitle">고객별 세금 신고 마감일을 관리하고 완료 처리합니다.</MudText>
|
||||||
|
</div>
|
||||||
|
<MudButton Variant="Variant.Filled" Color="Color.Primary"
|
||||||
|
OnClick="@(() => showAddForm = !showAddForm)"
|
||||||
|
StartIcon="@Icons.Material.Filled.Add">
|
||||||
|
일정 추가
|
||||||
|
</MudButton>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
@if (showAddForm)
|
||||||
|
{
|
||||||
|
<MudPaper Class="pa-4 mb-4" Elevation="1">
|
||||||
|
<MudText Typo="Typo.h6" Class="mb-3">새 신고 일정</MudText>
|
||||||
|
<MudGrid Spacing="2">
|
||||||
|
<MudItem xs="12" sm="6" md="4">
|
||||||
|
<MudAutocomplete T="Domain.Entities.Client" @bind-Value="selectedClient"
|
||||||
|
Label="고객 검색 *"
|
||||||
|
SearchFunc="SearchClients"
|
||||||
|
ToStringFunc="@(c => c == null ? "" : $"{c.Name} {c.CompanyName ?? ""}")"
|
||||||
|
Variant="Variant.Outlined" />
|
||||||
|
</MudItem>
|
||||||
|
<MudItem xs="12" sm="6" md="4">
|
||||||
|
<MudSelect T="string" @bind-Value="newFilingType" Label="신고 유형 *" Variant="Variant.Outlined">
|
||||||
|
@foreach (var t in TaxFilingService.FilingTypes)
|
||||||
|
{
|
||||||
|
<MudSelectItem Value="@t">@t</MudSelectItem>
|
||||||
|
}
|
||||||
|
</MudSelect>
|
||||||
|
</MudItem>
|
||||||
|
<MudItem xs="12" sm="6" md="4">
|
||||||
|
<MudDatePicker @bind-Date="newDueDate" Label="신고 기한 *" DateFormat="yyyy-MM-dd" />
|
||||||
|
</MudItem>
|
||||||
|
<MudItem xs="12">
|
||||||
|
<MudTextField T="string" @bind-Value="newMemo" Label="메모" Variant="Variant.Outlined" />
|
||||||
|
</MudItem>
|
||||||
|
</MudGrid>
|
||||||
|
<MudStack Row="true" Class="mt-3" Spacing="2">
|
||||||
|
<MudButton Variant="Variant.Filled" Color="Color.Primary" OnClick="AddFiling">저장</MudButton>
|
||||||
|
<MudButton Variant="Variant.Outlined" OnClick="@(() => showAddForm = false)">취소</MudButton>
|
||||||
|
</MudStack>
|
||||||
|
</MudPaper>
|
||||||
|
}
|
||||||
|
|
||||||
|
<MudPaper Class="admin-surface" Elevation="0">
|
||||||
|
<MudTabs Rounded="true" Elevation="0" Class="admin-tabs">
|
||||||
|
<MudTabPanel Text="신고 예정">
|
||||||
|
<FilingTable Filings="@pending" OnStatusChange="Reload" />
|
||||||
|
</MudTabPanel>
|
||||||
|
<MudTabPanel Text="신고 완료">
|
||||||
|
<FilingTable Filings="@filed" OnStatusChange="Reload" />
|
||||||
|
</MudTabPanel>
|
||||||
|
<MudTabPanel Text="기한 초과">
|
||||||
|
<FilingTable Filings="@overdue" OnStatusChange="Reload" />
|
||||||
|
</MudTabPanel>
|
||||||
|
</MudTabs>
|
||||||
|
</MudPaper>
|
||||||
|
|
||||||
|
@code {
|
||||||
|
private List<Domain.Entities.TaxFiling> pending = [];
|
||||||
|
private List<Domain.Entities.TaxFiling> filed = [];
|
||||||
|
private List<Domain.Entities.TaxFiling> overdue = [];
|
||||||
|
|
||||||
|
private bool showAddForm;
|
||||||
|
private Domain.Entities.Client? selectedClient;
|
||||||
|
private string newFilingType = "";
|
||||||
|
private DateTime? newDueDate = DateTime.Today.AddDays(30);
|
||||||
|
private string newMemo = "";
|
||||||
|
|
||||||
|
protected override async Task OnInitializedAsync() => await Reload();
|
||||||
|
|
||||||
|
private async Task Reload()
|
||||||
|
{
|
||||||
|
var all = (await FilingService.GetUpcomingAsync(365)).ToList();
|
||||||
|
// Also get filed ones by fetching all
|
||||||
|
pending = all.Where(x => x.Status == "pending").ToList();
|
||||||
|
filed = all.Where(x => x.Status == "filed").ToList();
|
||||||
|
overdue = all.Where(x => x.Status == "overdue").ToList();
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task<IEnumerable<Domain.Entities.Client>> SearchClients(string value)
|
||||||
|
{
|
||||||
|
var (items, _) = await ClientService.GetPagedAsync(1, 20, search: value);
|
||||||
|
return items;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task AddFiling()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (selectedClient == null)
|
||||||
|
{
|
||||||
|
Snackbar.Add("고객을 선택하세요.", Severity.Warning);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
var filing = new Domain.Entities.TaxFiling
|
||||||
|
{
|
||||||
|
ClientId = selectedClient.Id,
|
||||||
|
FilingType = newFilingType,
|
||||||
|
DueDate = newDueDate?.ToUniversalTime() ?? DateTime.UtcNow,
|
||||||
|
Status = "pending",
|
||||||
|
Memo = string.IsNullOrWhiteSpace(newMemo) ? null : newMemo
|
||||||
|
};
|
||||||
|
await FilingService.CreateAsync(filing);
|
||||||
|
showAddForm = false;
|
||||||
|
Snackbar.Add("신고 일정이 추가되었습니다.", Severity.Success);
|
||||||
|
await Reload();
|
||||||
|
}
|
||||||
|
catch (ValidationException ex)
|
||||||
|
{
|
||||||
|
Snackbar.Add(ex.Message, Severity.Error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,77 @@
|
|||||||
|
@page
|
||||||
|
@model TaxBaik.Web.Pages.PrivacyModel
|
||||||
|
@{
|
||||||
|
ViewData["Title"] = "개인정보처리방침 | 백원숙 세무회계";
|
||||||
|
ViewData["Description"] = "백원숙 세무회계의 개인정보 수집·이용·보관에 관한 방침을 안내합니다.";
|
||||||
|
}
|
||||||
|
|
||||||
|
<div class="container py-5" style="max-width:800px">
|
||||||
|
<h1 class="h3 fw-bold mb-4">개인정보처리방침</h1>
|
||||||
|
<p class="text-muted mb-4">최종 수정일: 2026년 6월 27일</p>
|
||||||
|
|
||||||
|
<p>백원숙 세무회계(이하 "사무소")는 「개인정보 보호법」에 따라 고객의 개인정보를 보호하고, 관련 불만을 신속하게 처리하기 위해 아래와 같이 개인정보처리방침을 수립·공개합니다.</p>
|
||||||
|
|
||||||
|
<hr class="my-4" />
|
||||||
|
|
||||||
|
<h2 class="h5 fw-bold mt-4 mb-2">1. 수집하는 개인정보 항목</h2>
|
||||||
|
<ul>
|
||||||
|
<li><strong>필수:</strong> 성명, 연락처(전화번호)</li>
|
||||||
|
<li><strong>선택:</strong> 이메일 주소, 문의 내용</li>
|
||||||
|
<li><strong>자동 수집:</strong> 접속 IP 주소, 접속 일시</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<h2 class="h5 fw-bold mt-4 mb-2">2. 개인정보의 수집·이용 목적</h2>
|
||||||
|
<ul>
|
||||||
|
<li>세무·부동산·가족자산 상담 신청 접수 및 답변</li>
|
||||||
|
<li>서비스 이용 문의에 대한 회신</li>
|
||||||
|
<li>서비스 품질 향상을 위한 통계 분석 (비식별화 처리)</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<h2 class="h5 fw-bold mt-4 mb-2">3. 개인정보의 보유 및 이용 기간</h2>
|
||||||
|
<p>상담 완료 후 <strong>3년</strong>간 보관 후 파기합니다. 단, 관계 법령에 따라 보관이 필요한 경우 해당 기간 동안 보관합니다.</p>
|
||||||
|
<ul>
|
||||||
|
<li>전자상거래 기록: 5년 (전자상거래 등에서의 소비자 보호에 관한 법률)</li>
|
||||||
|
<li>세금계산서 관련 자료: 5년 (부가가치세법)</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<h2 class="h5 fw-bold mt-4 mb-2">4. 개인정보의 제3자 제공</h2>
|
||||||
|
<p>사무소는 원칙적으로 고객의 개인정보를 외부에 제공하지 않습니다. 다만, 다음의 경우에는 예외로 합니다.</p>
|
||||||
|
<ul>
|
||||||
|
<li>고객이 동의한 경우</li>
|
||||||
|
<li>법령의 규정에 의거하거나, 수사 목적으로 법령에 정해진 절차와 방법에 따라 수사기관이 요구하는 경우</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<h2 class="h5 fw-bold mt-4 mb-2">5. 개인정보의 파기</h2>
|
||||||
|
<p>보유 기간이 경과하거나 처리 목적이 달성된 경우 지체 없이 파기합니다.</p>
|
||||||
|
<ul>
|
||||||
|
<li><strong>전자 파일 형태:</strong> 복구 불가능한 방법으로 영구 삭제</li>
|
||||||
|
<li><strong>종이 문서:</strong> 분쇄기 파기 또는 소각</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<h2 class="h5 fw-bold mt-4 mb-2">6. 정보주체의 권리·의무</h2>
|
||||||
|
<p>고객은 언제든지 다음 권리를 행사할 수 있습니다.</p>
|
||||||
|
<ul>
|
||||||
|
<li>개인정보 열람 요구</li>
|
||||||
|
<li>오류 등이 있을 경우 정정 요구</li>
|
||||||
|
<li>삭제 요구</li>
|
||||||
|
<li>처리 정지 요구</li>
|
||||||
|
</ul>
|
||||||
|
<p>권리 행사는 아래 연락처로 서면, 전화, 이메일로 요청하시면 지체 없이 조치하겠습니다.</p>
|
||||||
|
|
||||||
|
<h2 class="h5 fw-bold mt-4 mb-2">7. 개인정보 보호책임자</h2>
|
||||||
|
<table class="table table-bordered mt-2" style="max-width:400px">
|
||||||
|
<tbody>
|
||||||
|
<tr><th>성명</th><td>백원숙</td></tr>
|
||||||
|
<tr><th>직책</th><td>세무사</td></tr>
|
||||||
|
<tr><th>연락처</th><td>010-4122-8268</td></tr>
|
||||||
|
<tr><th>이메일</th><td>taxbaik5668@gmail.com</td></tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
<h2 class="h5 fw-bold mt-4 mb-2">8. 개인정보 처리방침 변경</h2>
|
||||||
|
<p>이 개인정보처리방침은 2026년 6월 27일부터 적용되며, 변경 시 홈페이지 공지를 통해 안내합니다.</p>
|
||||||
|
|
||||||
|
<div class="mt-5">
|
||||||
|
<a href="/taxbaik" class="btn btn-outline-primary">홈으로 돌아가기</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
namespace TaxBaik.Web.Pages;
|
||||||
|
|
||||||
|
using Microsoft.AspNetCore.Mvc.RazorPages;
|
||||||
|
|
||||||
|
public class PrivacyModel : PageModel
|
||||||
|
{
|
||||||
|
public void OnGet() { }
|
||||||
|
}
|
||||||
@@ -0,0 +1,56 @@
|
|||||||
|
@page
|
||||||
|
@model TaxBaik.Web.Pages.TermsModel
|
||||||
|
@{
|
||||||
|
ViewData["Title"] = "이용약관 | 백원숙 세무회계";
|
||||||
|
ViewData["Description"] = "백원숙 세무회계 홈페이지 이용약관을 안내합니다.";
|
||||||
|
}
|
||||||
|
|
||||||
|
<div class="container py-5" style="max-width:800px">
|
||||||
|
<h1 class="h3 fw-bold mb-4">이용약관</h1>
|
||||||
|
<p class="text-muted mb-4">최종 수정일: 2026년 6월 27일 / 시행일: 2026년 6월 27일</p>
|
||||||
|
|
||||||
|
<h2 class="h5 fw-bold mt-4 mb-2">제1조 (목적)</h2>
|
||||||
|
<p>이 약관은 백원숙 세무회계(이하 "사무소")가 운영하는 홈페이지(taxbaik.kr, 이하 "사이트")에서 제공하는 온라인 상담 신청 및 정보 서비스 이용과 관련하여 사무소와 이용자의 권리·의무 및 책임사항을 규정함을 목적으로 합니다.</p>
|
||||||
|
|
||||||
|
<h2 class="h5 fw-bold mt-4 mb-2">제2조 (정의)</h2>
|
||||||
|
<ul>
|
||||||
|
<li>"서비스"란 사이트에서 제공하는 세무 정보 제공, 상담 신청, 블로그 콘텐츠 열람 등 일체의 서비스를 말합니다.</li>
|
||||||
|
<li>"이용자"란 사이트에 접속하여 서비스를 이용하는 자를 말합니다.</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<h2 class="h5 fw-bold mt-4 mb-2">제3조 (약관의 효력 및 변경)</h2>
|
||||||
|
<p>이 약관은 사이트 내 게시함으로써 효력이 발생합니다. 사무소는 필요한 경우 약관을 변경할 수 있으며, 변경된 약관은 사이트 공지 후 7일이 경과한 날로부터 효력이 발생합니다.</p>
|
||||||
|
|
||||||
|
<h2 class="h5 fw-bold mt-4 mb-2">제4조 (서비스 제공)</h2>
|
||||||
|
<p>사무소는 다음 서비스를 제공합니다.</p>
|
||||||
|
<ul>
|
||||||
|
<li>세무·부동산·가족자산 관련 정보 콘텐츠 제공</li>
|
||||||
|
<li>온라인 상담 신청 접수</li>
|
||||||
|
<li>시즌별 세무 정보 안내</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<h2 class="h5 fw-bold mt-4 mb-2">제5조 (서비스 이용 시 주의사항)</h2>
|
||||||
|
<ul>
|
||||||
|
<li>사이트에서 제공하는 세무 정보는 일반적인 안내 목적으로, 개별 사안에 대한 법적 효력을 갖지 않습니다.</li>
|
||||||
|
<li>구체적인 세무 처리는 반드시 전문가 상담을 통해 진행하시기 바랍니다.</li>
|
||||||
|
<li>이용자는 상담 신청 시 허위 정보를 제공해서는 안 됩니다.</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<h2 class="h5 fw-bold mt-4 mb-2">제6조 (면책 조항)</h2>
|
||||||
|
<p>사무소는 다음 사항에 대해 책임을 지지 않습니다.</p>
|
||||||
|
<ul>
|
||||||
|
<li>천재지변 또는 이에 준하는 불가항력으로 서비스를 제공할 수 없는 경우</li>
|
||||||
|
<li>이용자의 귀책 사유로 인한 서비스 이용 장애</li>
|
||||||
|
<li>사이트에서 제공하는 일반 정보를 개별 사안에 적용함으로써 발생하는 손해</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<h2 class="h5 fw-bold mt-4 mb-2">제7조 (저작권)</h2>
|
||||||
|
<p>사이트에 게시된 모든 콘텐츠(글, 이미지, 디자인 등)의 저작권은 사무소 또는 원저작자에게 있으며, 무단 복제·배포를 금합니다.</p>
|
||||||
|
|
||||||
|
<h2 class="h5 fw-bold mt-4 mb-2">제8조 (준거법 및 재판관할)</h2>
|
||||||
|
<p>이 약관에 관한 분쟁은 대한민국 법령을 적용하며, 관할 법원은 민사소송법에 따른 관할 법원으로 합니다.</p>
|
||||||
|
|
||||||
|
<div class="mt-5">
|
||||||
|
<a href="/taxbaik" class="btn btn-outline-primary">홈으로 돌아가기</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
namespace TaxBaik.Web.Pages;
|
||||||
|
|
||||||
|
using Microsoft.AspNetCore.Mvc.RazorPages;
|
||||||
|
|
||||||
|
public class TermsModel : PageModel
|
||||||
|
{
|
||||||
|
public void OnGet() { }
|
||||||
|
}
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
-- 상담 이력 테이블
|
||||||
|
CREATE TABLE IF NOT EXISTS consultations (
|
||||||
|
id SERIAL PRIMARY KEY,
|
||||||
|
client_id INT NOT NULL REFERENCES clients(id) ON DELETE CASCADE,
|
||||||
|
consultation_date DATE NOT NULL DEFAULT CURRENT_DATE,
|
||||||
|
service_type VARCHAR(50),
|
||||||
|
summary TEXT NOT NULL,
|
||||||
|
result VARCHAR(30), -- consulting, contracted, rejected, pending, completed
|
||||||
|
fee NUMERIC(12,0),
|
||||||
|
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_consultations_client ON consultations (client_id);
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
-- 문의 → 고객 연결 (문의에서 고객 카드 생성 시 연결)
|
||||||
|
ALTER TABLE inquiries ADD COLUMN IF NOT EXISTS client_id INT REFERENCES clients(id) ON DELETE SET NULL;
|
||||||
|
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_inquiries_client ON inquiries (client_id);
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
-- 고객별 세금 신고 일정
|
||||||
|
CREATE TABLE IF NOT EXISTS tax_filings (
|
||||||
|
id SERIAL PRIMARY KEY,
|
||||||
|
client_id INT NOT NULL REFERENCES clients(id) ON DELETE CASCADE,
|
||||||
|
filing_type VARCHAR(60) NOT NULL, -- 부가가치세, 종합소득세, 법인세, 원천징수, 종합부동산세, 기타
|
||||||
|
due_date DATE NOT NULL,
|
||||||
|
status VARCHAR(20) NOT NULL DEFAULT 'pending', -- pending, filed, overdue
|
||||||
|
memo TEXT,
|
||||||
|
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||||
|
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_tax_filings_client ON tax_filings (client_id);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_tax_filings_due_date ON tax_filings (due_date);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_tax_filings_status ON tax_filings (status);
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
-- 문의 상태 5단계로 확장 + 처리 메모 컬럼 추가
|
||||||
|
-- 기존: new, contacted, completed
|
||||||
|
-- 신규: new(신규), consulting(상담중), contracted(계약완료), rejected(거절), closed(종결)
|
||||||
|
|
||||||
|
UPDATE inquiries SET status = 'consulting' WHERE status = 'contacted';
|
||||||
|
UPDATE inquiries SET status = 'closed' WHERE status = 'completed';
|
||||||
|
|
||||||
|
ALTER TABLE inquiries ADD COLUMN IF NOT EXISTS admin_memo TEXT;
|
||||||
|
ALTER TABLE inquiries ADD COLUMN IF NOT EXISTS updated_at TIMESTAMPTZ DEFAULT NOW();
|
||||||
Reference in New Issue
Block a user