Compare commits
46 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 54c179b1eb | |||
| 488b8d11b7 | |||
| 65c5f19a2f | |||
| eaacbc8d7f | |||
| ac8a70a2ca | |||
| 203e674c3f | |||
| 0c014d0bdf | |||
| 904c0972ca | |||
| 7e75aeeec7 | |||
| b13eed7b7e | |||
| 4647b049b8 | |||
| 1a5ebb45bc | |||
| f197663101 | |||
| 70b57f1d4c | |||
| 428eeb6fd8 | |||
| dd68a237a1 | |||
| ef9fd523c6 | |||
| f2ab78dea2 | |||
| 1e0c0b7e1c | |||
| 1b173376ee | |||
| 1a7bc9e209 | |||
| 3be379431f | |||
| 682e2db3a3 | |||
| d9766cb5ef | |||
| 6bcb9effa8 | |||
| 186c6ef7a4 | |||
| c2e8e08f09 | |||
| 3f7cd7cd84 | |||
| 4b352df408 | |||
| a4b1234900 | |||
| a3c81c4f70 | |||
| 6e8b4e76ac | |||
| 5807e1b35e | |||
| 3e1097f585 | |||
| 917600a793 | |||
| 0d3615b44d | |||
| fc339ca9e7 | |||
| da1226994f | |||
| 6bc03ce3d9 | |||
| ecfbfc7cac | |||
| 46cb508bdf | |||
| ecabe8d9cc | |||
| 55c65810c1 | |||
| 7054d397e4 | |||
| 11fb596fc2 | |||
| ea9478f2f1 |
@@ -8,8 +8,8 @@
|
|||||||
Blazor → Service (서버) → DB
|
Blazor → Service (서버) → DB
|
||||||
|
|
||||||
✅ 현재: API-First (클라이언트-서버 분리)
|
✅ 현재: API-First (클라이언트-서버 분리)
|
||||||
Blazor (UI만) ← API (모든 로직) ← DB
|
Blazor (UI만, 사용자 액션 후 API 재조회) ← API (모든 로직) ← DB
|
||||||
SignalR (변경 알림만)
|
Blazor 데이터 변경 자동 push/broadcast 금지
|
||||||
```
|
```
|
||||||
|
|
||||||
### SOLID 기반 순차 마이그레이션 전략
|
### SOLID 기반 순차 마이그레이션 전략
|
||||||
@@ -61,10 +61,10 @@ _refreshTokenExpirationMinutes = 10080;
|
|||||||
|
|
||||||
**완료**: 2026-06-28 / 토큰 갱신 자동화 + 이중 토큰 패턴
|
**완료**: 2026-06-28 / 토큰 갱신 자동화 + 이중 토큰 패턴
|
||||||
|
|
||||||
#### Phase 6: SignalR 통합
|
#### Phase 6: Blazor 데이터 변경 SignalR 갱신 제거
|
||||||
- [ ] NotificationHub (변경 알림만)
|
- [x] NotificationHub 제거
|
||||||
- [ ] Blazor에서 구독
|
- [x] 데이터 변경용 INotificationService 제거
|
||||||
- [ ] 알림 후 API로 데이터 검증
|
- [x] Program.cs의 별도 AddSignalR/MapHub 등록 제거
|
||||||
|
|
||||||
#### Phase 7: 순차적 마이그레이션 ✅
|
#### Phase 7: 순차적 마이그레이션 ✅
|
||||||
- [x] Blog 페이지 → API 클라이언트
|
- [x] Blog 페이지 → API 클라이언트
|
||||||
@@ -136,11 +136,11 @@ _refreshTokenExpirationMinutes = 10080;
|
|||||||
- Status Color Chips (Error/Warning/Success)
|
- Status Color Chips (Error/Warning/Success)
|
||||||
- Client 링크 (상세 페이지 연동)
|
- Client 링크 (상세 페이지 연동)
|
||||||
|
|
||||||
### **Phase 6: SignalR 통합** ✅
|
### **Phase 6: Lite Blazor 운영 원칙** ✅
|
||||||
- NotificationHub (브로드캐스트만, 상태 관리 없음)
|
- Blazor에서 데이터 변경 시 SignalR publish/subscribe로 목록을 자동 갱신하지 않는다.
|
||||||
- INotificationService (이벤트 기반)
|
- NotificationHub와 데이터 변경용 INotificationService는 제거된 상태를 유지한다.
|
||||||
- 5개 알림 유형 (Inquiry, Client, Announcement, Filing, Status)
|
- Blazor Server의 기본 interactive 연결은 UI 구동에만 사용한다.
|
||||||
- Program.cs SignalR 등록
|
- 공지사항, 문의, 고객, 신고 등 도메인 CRUD 기능은 그대로 유지하고, 변경 전파 방식만 API 재조회로 제한한다.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -160,11 +160,11 @@ Repositories (데이터 계층)
|
|||||||
PostgreSQL Database
|
PostgreSQL Database
|
||||||
```
|
```
|
||||||
|
|
||||||
**Blazor Server SignalR**:
|
**Lite Blazor 데이터 갱신**:
|
||||||
- 자동 연결 (내장 Hub connection)
|
- Blazor Server 자동 연결은 컴포넌트 상호작용용 기본 회선으로만 사용한다.
|
||||||
- NotificationHub 클라이언트 그룹 (admins)
|
- 데이터 변경 알림용 별도 Hub, 그룹, broadcast, client subscription을 추가하지 않는다.
|
||||||
- 이벤트 기반 메시지 (상태 관리 없음)
|
- 저장/삭제/완료 같은 사용자 액션 이후 필요한 목록만 API로 다시 조회한다.
|
||||||
- 클라이언트는 알림 후 API로 데이터 검증
|
- 공지사항, 문의, 고객, 신고 등 도메인 CRUD 기능은 그대로 유지한다.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -182,10 +182,10 @@ PostgreSQL Database
|
|||||||
- [x] Phase 7-4: CRM & 세무관리 (5개 API, 5개 Blazor) - **2026-06-28 완료**
|
- [x] Phase 7-4: CRM & 세무관리 (5개 API, 5개 Blazor) - **2026-06-28 완료**
|
||||||
- [x] SOLID 원칙 전체 적용 (Single Responsibility, Dependency Inversion)
|
- [x] SOLID 원칙 전체 적용 (Single Responsibility, Dependency Inversion)
|
||||||
|
|
||||||
**실시간 알림 (Phase 6)**:
|
**Lite Blazor / 데이터 갱신 (Phase 6)**:
|
||||||
- [x] NotificationHub 구현
|
- [x] Blazor 데이터 변경 SignalR 자동 갱신 제거
|
||||||
- [x] Event-driven 알림 시스템
|
- [x] NotificationHub 제거
|
||||||
- [x] Scoped DI 등록
|
- [x] 데이터 변경용 INotificationService 제거
|
||||||
|
|
||||||
**Blazor 페이지 & UI 고도화 (Phase 7-4)**:
|
**Blazor 페이지 & UI 고도화 (Phase 7-4)**:
|
||||||
- [x] 5개 CRM/세무관리 Blazor 페이지
|
- [x] 5개 CRM/세무관리 Blazor 페이지
|
||||||
|
|||||||
@@ -33,6 +33,9 @@ public class ConsultingActivityService(IConsultingActivityRepository repository)
|
|||||||
public async Task<IEnumerable<ConsultingActivity>> GetByClientIdAsync(int clientId, CancellationToken ct = default) =>
|
public async Task<IEnumerable<ConsultingActivity>> GetByClientIdAsync(int clientId, CancellationToken ct = default) =>
|
||||||
await repository.GetByClientIdAsync(clientId, ct);
|
await repository.GetByClientIdAsync(clientId, ct);
|
||||||
|
|
||||||
|
public async Task<IEnumerable<ConsultingActivity>> GetAllAsync(CancellationToken ct = default) =>
|
||||||
|
await repository.GetAllAsync(ct);
|
||||||
|
|
||||||
public async Task<IEnumerable<ConsultingActivity>> GetPendingFollowupsAsync(CancellationToken ct = default) =>
|
public async Task<IEnumerable<ConsultingActivity>> GetPendingFollowupsAsync(CancellationToken ct = default) =>
|
||||||
await repository.GetPendingFollowupsAsync(ct);
|
await repository.GetPendingFollowupsAsync(ct);
|
||||||
|
|
||||||
|
|||||||
@@ -36,6 +36,9 @@ public class ContractService(IContractRepository repository)
|
|||||||
public async Task<Contract?> GetByIdAsync(int id, CancellationToken ct = default) =>
|
public async Task<Contract?> GetByIdAsync(int id, CancellationToken ct = default) =>
|
||||||
await repository.GetByIdAsync(id, ct);
|
await repository.GetByIdAsync(id, ct);
|
||||||
|
|
||||||
|
public async Task<IEnumerable<Contract>> GetAllAsync(CancellationToken ct = default) =>
|
||||||
|
await repository.GetAllAsync(ct);
|
||||||
|
|
||||||
public async Task<IEnumerable<Contract>> GetByClientIdAsync(int clientId, CancellationToken ct = default) =>
|
public async Task<IEnumerable<Contract>> GetByClientIdAsync(int clientId, CancellationToken ct = default) =>
|
||||||
await repository.GetByClientIdAsync(clientId, ct);
|
await repository.GetByClientIdAsync(clientId, ct);
|
||||||
|
|
||||||
|
|||||||
@@ -34,6 +34,9 @@ public class RevenueTrackingService(IRevenueTrackingRepository repository)
|
|||||||
public async Task<IEnumerable<RevenueTracking>> GetByClientIdAsync(int clientId, CancellationToken ct = default) =>
|
public async Task<IEnumerable<RevenueTracking>> GetByClientIdAsync(int clientId, CancellationToken ct = default) =>
|
||||||
await repository.GetByClientIdAsync(clientId, ct);
|
await repository.GetByClientIdAsync(clientId, ct);
|
||||||
|
|
||||||
|
public async Task<IEnumerable<RevenueTracking>> GetAllAsync(CancellationToken ct = default) =>
|
||||||
|
await repository.GetAllAsync(ct);
|
||||||
|
|
||||||
public async Task<IEnumerable<RevenueTracking>> GetPendingPaymentsAsync(CancellationToken ct = default) =>
|
public async Task<IEnumerable<RevenueTracking>> GetPendingPaymentsAsync(CancellationToken ct = default) =>
|
||||||
await repository.GetPendingPaymentsAsync(ct);
|
await repository.GetPendingPaymentsAsync(ct);
|
||||||
|
|
||||||
|
|||||||
@@ -33,6 +33,9 @@ public class TaxFilingScheduleService(ITaxFilingScheduleRepository repository)
|
|||||||
public async Task<TaxFilingSchedule?> GetByIdAsync(int id, CancellationToken ct = default) =>
|
public async Task<TaxFilingSchedule?> GetByIdAsync(int id, CancellationToken ct = default) =>
|
||||||
await repository.GetByIdAsync(id, ct);
|
await repository.GetByIdAsync(id, ct);
|
||||||
|
|
||||||
|
public async Task<IEnumerable<TaxFilingSchedule>> GetAllAsync(CancellationToken ct = default) =>
|
||||||
|
await repository.GetAllAsync(ct);
|
||||||
|
|
||||||
public async Task<IEnumerable<TaxFilingSchedule>> GetByClientIdAsync(int clientId, CancellationToken ct = default) =>
|
public async Task<IEnumerable<TaxFilingSchedule>> GetByClientIdAsync(int clientId, CancellationToken ct = default) =>
|
||||||
await repository.GetByClientIdAsync(clientId, ct);
|
await repository.GetByClientIdAsync(clientId, ct);
|
||||||
|
|
||||||
|
|||||||
@@ -31,6 +31,9 @@ public class TaxProfileService(ITaxProfileRepository repository)
|
|||||||
public async Task<TaxProfile?> GetByClientIdAsync(int clientId, CancellationToken ct = default) =>
|
public async Task<TaxProfile?> GetByClientIdAsync(int clientId, CancellationToken ct = default) =>
|
||||||
await repository.GetByClientIdAsync(clientId, ct);
|
await repository.GetByClientIdAsync(clientId, ct);
|
||||||
|
|
||||||
|
public async Task<IEnumerable<TaxProfile>> GetAllAsync(CancellationToken ct = default) =>
|
||||||
|
await repository.GetAllAsync(ct);
|
||||||
|
|
||||||
public async Task UpdateAsync(int profileId, string? businessType, string? accountingMethod,
|
public async Task UpdateAsync(int profileId, string? businessType, string? accountingMethod,
|
||||||
DateTime? nextFilingDueDate, string taxRiskLevel = "normal", CancellationToken ct = default)
|
DateTime? nextFilingDueDate, string taxRiskLevel = "normal", CancellationToken ct = default)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ using TaxBaik.Domain.Entities;
|
|||||||
public interface IConsultingActivityRepository
|
public interface IConsultingActivityRepository
|
||||||
{
|
{
|
||||||
Task<int> CreateAsync(ConsultingActivity activity, CancellationToken cancellationToken = default);
|
Task<int> CreateAsync(ConsultingActivity activity, CancellationToken cancellationToken = default);
|
||||||
|
Task<IEnumerable<ConsultingActivity>> GetAllAsync(CancellationToken cancellationToken = default);
|
||||||
Task<IEnumerable<ConsultingActivity>> GetByClientIdAsync(int clientId, CancellationToken cancellationToken = default);
|
Task<IEnumerable<ConsultingActivity>> GetByClientIdAsync(int clientId, CancellationToken cancellationToken = default);
|
||||||
Task<IEnumerable<ConsultingActivity>> GetPendingFollowupsAsync(CancellationToken cancellationToken = default);
|
Task<IEnumerable<ConsultingActivity>> GetPendingFollowupsAsync(CancellationToken cancellationToken = default);
|
||||||
Task<IEnumerable<ConsultingActivity>> GetByConsultantAsync(int consultantId, DateTime fromDate, CancellationToken cancellationToken = default);
|
Task<IEnumerable<ConsultingActivity>> GetByConsultantAsync(int consultantId, DateTime fromDate, CancellationToken cancellationToken = default);
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ using TaxBaik.Domain.Entities;
|
|||||||
public interface IContractRepository
|
public interface IContractRepository
|
||||||
{
|
{
|
||||||
Task<int> CreateAsync(Contract contract, CancellationToken cancellationToken = default);
|
Task<int> CreateAsync(Contract contract, CancellationToken cancellationToken = default);
|
||||||
|
Task<IEnumerable<Contract>> GetAllAsync(CancellationToken cancellationToken = default);
|
||||||
Task<Contract?> GetByIdAsync(int id, CancellationToken cancellationToken = default);
|
Task<Contract?> GetByIdAsync(int id, CancellationToken cancellationToken = default);
|
||||||
Task<IEnumerable<Contract>> GetByClientIdAsync(int clientId, CancellationToken cancellationToken = default);
|
Task<IEnumerable<Contract>> GetByClientIdAsync(int clientId, CancellationToken cancellationToken = default);
|
||||||
Task<IEnumerable<Contract>> GetActiveContractsAsync(CancellationToken cancellationToken = default);
|
Task<IEnumerable<Contract>> GetActiveContractsAsync(CancellationToken cancellationToken = default);
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ using TaxBaik.Domain.Entities;
|
|||||||
public interface IRevenueTrackingRepository
|
public interface IRevenueTrackingRepository
|
||||||
{
|
{
|
||||||
Task<int> CreateAsync(RevenueTracking revenue, CancellationToken cancellationToken = default);
|
Task<int> CreateAsync(RevenueTracking revenue, CancellationToken cancellationToken = default);
|
||||||
|
Task<IEnumerable<RevenueTracking>> GetAllAsync(CancellationToken cancellationToken = default);
|
||||||
Task<IEnumerable<RevenueTracking>> GetByClientIdAsync(int clientId, CancellationToken cancellationToken = default);
|
Task<IEnumerable<RevenueTracking>> GetByClientIdAsync(int clientId, CancellationToken cancellationToken = default);
|
||||||
Task<IEnumerable<RevenueTracking>> GetPendingPaymentsAsync(CancellationToken cancellationToken = default);
|
Task<IEnumerable<RevenueTracking>> GetPendingPaymentsAsync(CancellationToken cancellationToken = default);
|
||||||
Task<IEnumerable<RevenueTracking>> GetByDateRangeAsync(DateTime startDate, DateTime endDate, CancellationToken cancellationToken = default);
|
Task<IEnumerable<RevenueTracking>> GetByDateRangeAsync(DateTime startDate, DateTime endDate, CancellationToken cancellationToken = default);
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ using TaxBaik.Domain.Entities;
|
|||||||
public interface ITaxFilingScheduleRepository
|
public interface ITaxFilingScheduleRepository
|
||||||
{
|
{
|
||||||
Task<int> CreateAsync(TaxFilingSchedule schedule, CancellationToken cancellationToken = default);
|
Task<int> CreateAsync(TaxFilingSchedule schedule, CancellationToken cancellationToken = default);
|
||||||
|
Task<IEnumerable<TaxFilingSchedule>> GetAllAsync(CancellationToken cancellationToken = default);
|
||||||
Task<TaxFilingSchedule?> GetByIdAsync(int id, CancellationToken cancellationToken = default);
|
Task<TaxFilingSchedule?> GetByIdAsync(int id, CancellationToken cancellationToken = default);
|
||||||
Task<IEnumerable<TaxFilingSchedule>> GetByClientIdAsync(int clientId, CancellationToken cancellationToken = default);
|
Task<IEnumerable<TaxFilingSchedule>> GetByClientIdAsync(int clientId, CancellationToken cancellationToken = default);
|
||||||
Task<IEnumerable<TaxFilingSchedule>> GetUpcomingDuesAsync(int daysAhead = 30, CancellationToken cancellationToken = default);
|
Task<IEnumerable<TaxFilingSchedule>> GetUpcomingDuesAsync(int daysAhead = 30, CancellationToken cancellationToken = default);
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ using TaxBaik.Domain.Entities;
|
|||||||
public interface ITaxProfileRepository
|
public interface ITaxProfileRepository
|
||||||
{
|
{
|
||||||
Task<int> CreateAsync(TaxProfile profile, CancellationToken cancellationToken = default);
|
Task<int> CreateAsync(TaxProfile profile, CancellationToken cancellationToken = default);
|
||||||
|
Task<IEnumerable<TaxProfile>> GetAllAsync(CancellationToken cancellationToken = default);
|
||||||
Task<TaxProfile?> GetByClientIdAsync(int clientId, CancellationToken cancellationToken = default);
|
Task<TaxProfile?> GetByClientIdAsync(int clientId, CancellationToken cancellationToken = default);
|
||||||
Task UpdateAsync(TaxProfile profile, CancellationToken cancellationToken = default);
|
Task UpdateAsync(TaxProfile profile, CancellationToken cancellationToken = default);
|
||||||
Task<IEnumerable<TaxProfile>> GetByRiskLevelAsync(string riskLevel, CancellationToken cancellationToken = default);
|
Task<IEnumerable<TaxProfile>> GetByRiskLevelAsync(string riskLevel, CancellationToken cancellationToken = default);
|
||||||
|
|||||||
@@ -16,6 +16,14 @@ public class ConsultingActivityRepository(IDbConnectionFactory connectionFactory
|
|||||||
activity);
|
activity);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async Task<IEnumerable<ConsultingActivity>> GetAllAsync(CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
using var conn = Conn();
|
||||||
|
return await conn.QueryAsync<ConsultingActivity>(
|
||||||
|
@"SELECT id, client_id, activity_type, activity_date, activity_time, assigned_consultant, description, outcome, next_followup_date, notes, created_at, updated_at
|
||||||
|
FROM consulting_activities ORDER BY activity_date DESC");
|
||||||
|
}
|
||||||
|
|
||||||
public async Task<IEnumerable<ConsultingActivity>> GetByClientIdAsync(int clientId, CancellationToken cancellationToken = default)
|
public async Task<IEnumerable<ConsultingActivity>> GetByClientIdAsync(int clientId, CancellationToken cancellationToken = default)
|
||||||
{
|
{
|
||||||
using var conn = Conn();
|
using var conn = Conn();
|
||||||
|
|||||||
@@ -16,6 +16,14 @@ public class ContractRepository(IDbConnectionFactory connectionFactory) : BaseRe
|
|||||||
contract);
|
contract);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async Task<IEnumerable<Contract>> GetAllAsync(CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
using var conn = Conn();
|
||||||
|
return await conn.QueryAsync<Contract>(
|
||||||
|
@"SELECT id, client_id, contract_number, service_type, contract_date, start_date, end_date, monthly_fee, total_amount, payment_status, status, notes, created_at, updated_at
|
||||||
|
FROM contracts ORDER BY contract_date DESC");
|
||||||
|
}
|
||||||
|
|
||||||
public async Task<Contract?> GetByIdAsync(int id, CancellationToken cancellationToken = default)
|
public async Task<Contract?> GetByIdAsync(int id, CancellationToken cancellationToken = default)
|
||||||
{
|
{
|
||||||
using var conn = Conn();
|
using var conn = Conn();
|
||||||
|
|||||||
@@ -16,6 +16,14 @@ public class RevenueTrackingRepository(IDbConnectionFactory connectionFactory) :
|
|||||||
revenue);
|
revenue);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async Task<IEnumerable<RevenueTracking>> GetAllAsync(CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
using var conn = Conn();
|
||||||
|
return await conn.QueryAsync<RevenueTracking>(
|
||||||
|
@"SELECT id, client_id, invoice_number, invoice_date, service_type, amount, payment_status, payment_date, due_date, notes, created_at, updated_at
|
||||||
|
FROM revenue_tracking ORDER BY invoice_date DESC");
|
||||||
|
}
|
||||||
|
|
||||||
public async Task<IEnumerable<RevenueTracking>> GetByClientIdAsync(int clientId, CancellationToken cancellationToken = default)
|
public async Task<IEnumerable<RevenueTracking>> GetByClientIdAsync(int clientId, CancellationToken cancellationToken = default)
|
||||||
{
|
{
|
||||||
using var conn = Conn();
|
using var conn = Conn();
|
||||||
|
|||||||
@@ -16,6 +16,14 @@ public class TaxFilingScheduleRepository(IDbConnectionFactory connectionFactory)
|
|||||||
schedule);
|
schedule);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async Task<IEnumerable<TaxFilingSchedule>> GetAllAsync(CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
using var conn = Conn();
|
||||||
|
return await conn.QueryAsync<TaxFilingSchedule>(
|
||||||
|
@"SELECT id, client_id, filing_type, due_date, filing_year, status, assigned_to, completed_date, notes, created_at, updated_at
|
||||||
|
FROM tax_filing_schedules ORDER BY due_date DESC");
|
||||||
|
}
|
||||||
|
|
||||||
public async Task<TaxFilingSchedule?> GetByIdAsync(int id, CancellationToken cancellationToken = default)
|
public async Task<TaxFilingSchedule?> GetByIdAsync(int id, CancellationToken cancellationToken = default)
|
||||||
{
|
{
|
||||||
using var conn = Conn();
|
using var conn = Conn();
|
||||||
|
|||||||
@@ -20,6 +20,16 @@ public class TaxProfileRepository(IDbConnectionFactory connectionFactory) : Base
|
|||||||
profile);
|
profile);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async Task<IEnumerable<TaxProfile>> GetAllAsync(CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
using var conn = Conn();
|
||||||
|
return await conn.QueryAsync<TaxProfile>(
|
||||||
|
@"SELECT id, client_id, business_registration, business_type, establishment_date,
|
||||||
|
annual_revenue_range, employee_count, accounting_method, fiscal_year_end, last_filing_date,
|
||||||
|
next_filing_due_date, tax_risk_level, previous_audit_history, special_notes, created_at, updated_at
|
||||||
|
FROM tax_profiles ORDER BY id DESC");
|
||||||
|
}
|
||||||
|
|
||||||
public async Task<TaxProfile?> GetByClientIdAsync(int clientId, CancellationToken cancellationToken = default)
|
public async Task<TaxProfile?> GetByClientIdAsync(int clientId, CancellationToken cancellationToken = default)
|
||||||
{
|
{
|
||||||
using var conn = Conn();
|
using var conn = Conn();
|
||||||
|
|||||||
@@ -32,6 +32,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<MudThemeProvider @bind-IsDarkMode="isDarkMode" Theme="mudTheme" />
|
<MudThemeProvider @bind-IsDarkMode="isDarkMode" Theme="mudTheme" />
|
||||||
|
<MudPopoverProvider />
|
||||||
<MudDialogProvider />
|
<MudDialogProvider />
|
||||||
<MudSnackbarProvider />
|
<MudSnackbarProvider />
|
||||||
<Routes @rendermode="new InteractiveServerRenderMode(prerender: false)" />
|
<Routes @rendermode="new InteractiveServerRenderMode(prerender: false)" />
|
||||||
|
|||||||
@@ -90,11 +90,25 @@
|
|||||||
</MudPaper>
|
</MudPaper>
|
||||||
|
|
||||||
@code {
|
@code {
|
||||||
|
[CascadingParameter]
|
||||||
|
private Task<AuthenticationState>? AuthStateTask { get; set; }
|
||||||
|
|
||||||
private List<Announcement>? announcements;
|
private List<Announcement>? announcements;
|
||||||
|
|
||||||
protected override async Task OnInitializedAsync()
|
protected override async Task OnAfterRenderAsync(bool firstRender)
|
||||||
{
|
{
|
||||||
await LoadAsync();
|
if (firstRender)
|
||||||
|
{
|
||||||
|
if (AuthStateTask != null)
|
||||||
|
{
|
||||||
|
var authState = await AuthStateTask;
|
||||||
|
if (authState.User.Identity?.IsAuthenticated == true)
|
||||||
|
{
|
||||||
|
await LoadAsync();
|
||||||
|
StateHasChanged();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task LoadAsync()
|
private async Task LoadAsync()
|
||||||
|
|||||||
@@ -50,6 +50,9 @@
|
|||||||
</MudStack>
|
</MudStack>
|
||||||
|
|
||||||
@code {
|
@code {
|
||||||
|
[CascadingParameter]
|
||||||
|
private Task<AuthenticationState>? AuthStateTask { get; set; }
|
||||||
|
|
||||||
private List<TaxBaik.Domain.Entities.BlogPost> posts = [];
|
private List<TaxBaik.Domain.Entities.BlogPost> posts = [];
|
||||||
private bool isLoading = true;
|
private bool isLoading = true;
|
||||||
private int currentPage = 1;
|
private int currentPage = 1;
|
||||||
@@ -57,9 +60,20 @@
|
|||||||
private int totalPosts = 0;
|
private int totalPosts = 0;
|
||||||
private const int PageSize = 20;
|
private const int PageSize = 20;
|
||||||
|
|
||||||
protected override async Task OnInitializedAsync()
|
protected override async Task OnAfterRenderAsync(bool firstRender)
|
||||||
{
|
{
|
||||||
await LoadPosts();
|
if (firstRender)
|
||||||
|
{
|
||||||
|
if (AuthStateTask != null)
|
||||||
|
{
|
||||||
|
var authState = await AuthStateTask;
|
||||||
|
if (authState.User.Identity?.IsAuthenticated == true)
|
||||||
|
{
|
||||||
|
await LoadPosts();
|
||||||
|
StateHasChanged();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task LoadPosts()
|
private async Task LoadPosts()
|
||||||
|
|||||||
@@ -129,6 +129,9 @@
|
|||||||
</MudPaper>
|
</MudPaper>
|
||||||
|
|
||||||
@code {
|
@code {
|
||||||
|
[CascadingParameter]
|
||||||
|
private Task<AuthenticationState>? AuthStateTask { get; set; }
|
||||||
|
|
||||||
private List<Client>? clients;
|
private List<Client>? clients;
|
||||||
private string searchText = "";
|
private string searchText = "";
|
||||||
private string statusFilter = "";
|
private string statusFilter = "";
|
||||||
@@ -137,7 +140,21 @@
|
|||||||
private int totalPages;
|
private int totalPages;
|
||||||
private const int PageSize = 20;
|
private const int PageSize = 20;
|
||||||
|
|
||||||
protected override async Task OnInitializedAsync() => await LoadAsync();
|
protected override async Task OnAfterRenderAsync(bool firstRender)
|
||||||
|
{
|
||||||
|
if (firstRender)
|
||||||
|
{
|
||||||
|
if (AuthStateTask != null)
|
||||||
|
{
|
||||||
|
var authState = await AuthStateTask;
|
||||||
|
if (authState.User.Identity?.IsAuthenticated == true)
|
||||||
|
{
|
||||||
|
await LoadAsync();
|
||||||
|
StateHasChanged();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private async Task LoadAsync()
|
private async Task LoadAsync()
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -100,10 +100,17 @@
|
|||||||
<MudSelect T="int" @bind-Value="activityForm.ClientId" Label="고객" Required="true" Variant="Variant.Outlined" FullWidth="true" Class="mb-4">
|
<MudSelect T="int" @bind-Value="activityForm.ClientId" Label="고객" Required="true" Variant="Variant.Outlined" FullWidth="true" Class="mb-4">
|
||||||
@foreach (var client in clients)
|
@foreach (var client in clients)
|
||||||
{
|
{
|
||||||
<MudSelectItem Value="@client.Id">@client.CompanyName</MudSelectItem>
|
<MudSelectItem Value="@client.Id">@GetClientDisplayName(client)</MudSelectItem>
|
||||||
}
|
}
|
||||||
</MudSelect>
|
</MudSelect>
|
||||||
<MudTextField T="string" @bind-Value="activityForm.ActivityType" Label="활동 유형" Variant="Variant.Outlined" FullWidth="true" Class="mb-4" Required="true" />
|
<MudSelect T="string" @bind-Value="activityForm.ActivityType" Label="활동 유형" Variant="Variant.Outlined" FullWidth="true" Class="mb-4" Required="true">
|
||||||
|
<MudSelectItem Value="@("방문 상담")">방문 상담</MudSelectItem>
|
||||||
|
<MudSelectItem Value="@("전화 상담")">전화 상담</MudSelectItem>
|
||||||
|
<MudSelectItem Value="@("세무조사 대응 미팅")">세무조사 대응 미팅</MudSelectItem>
|
||||||
|
<MudSelectItem Value="@("카카오톡 상담")">카카오톡 상담</MudSelectItem>
|
||||||
|
<MudSelectItem Value="@("이메일 자료 접수")">이메일 자료 접수</MudSelectItem>
|
||||||
|
<MudSelectItem Value="@("기타")">기타</MudSelectItem>
|
||||||
|
</MudSelect>
|
||||||
<MudDatePicker @bind-Date="activityForm.ActivityDate" Label="활동일" Variant="Variant.Outlined" FullWidth="true" Class="mb-4" Required="true" />
|
<MudDatePicker @bind-Date="activityForm.ActivityDate" Label="활동일" Variant="Variant.Outlined" FullWidth="true" Class="mb-4" Required="true" />
|
||||||
<MudTextField T="string" @bind-Value="activityForm.Description" Label="설명" Variant="Variant.Outlined" FullWidth="true" Lines="3" Class="mb-4" Required="true" />
|
<MudTextField T="string" @bind-Value="activityForm.Description" Label="설명" Variant="Variant.Outlined" FullWidth="true" Lines="3" Class="mb-4" Required="true" />
|
||||||
<MudDatePicker @bind-Date="activityForm.NextFollowupDate" Label="다음 팔로업일" Variant="Variant.Outlined" FullWidth="true" Class="mb-4" />
|
<MudDatePicker @bind-Date="activityForm.NextFollowupDate" Label="다음 팔로업일" Variant="Variant.Outlined" FullWidth="true" Class="mb-4" />
|
||||||
@@ -116,6 +123,9 @@
|
|||||||
</MudDialog>
|
</MudDialog>
|
||||||
|
|
||||||
@code {
|
@code {
|
||||||
|
[CascadingParameter]
|
||||||
|
private Task<AuthenticationState>? AuthStateTask { get; set; }
|
||||||
|
|
||||||
private List<ConsultingActivity>? activities;
|
private List<ConsultingActivity>? activities;
|
||||||
private List<Client> clients = [];
|
private List<Client> clients = [];
|
||||||
private Dictionary<int, string> clientMap = new();
|
private Dictionary<int, string> clientMap = new();
|
||||||
@@ -124,9 +134,20 @@
|
|||||||
private ConsultingActivity? editingActivity;
|
private ConsultingActivity? editingActivity;
|
||||||
private ConsultingActivityForm activityForm = new();
|
private ConsultingActivityForm activityForm = new();
|
||||||
|
|
||||||
protected override async Task OnInitializedAsync()
|
protected override async Task OnAfterRenderAsync(bool firstRender)
|
||||||
{
|
{
|
||||||
await LoadData();
|
if (firstRender)
|
||||||
|
{
|
||||||
|
if (AuthStateTask != null)
|
||||||
|
{
|
||||||
|
var authState = await AuthStateTask;
|
||||||
|
if (authState.User.Identity?.IsAuthenticated == true)
|
||||||
|
{
|
||||||
|
await LoadData();
|
||||||
|
StateHasChanged();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task LoadData()
|
private async Task LoadData()
|
||||||
@@ -134,9 +155,9 @@
|
|||||||
try
|
try
|
||||||
{
|
{
|
||||||
activities = await ActivityClient.GetAllAsync();
|
activities = await ActivityClient.GetAllAsync();
|
||||||
var (clientItems, _) = await ClientClient.GetPagedAsync();
|
var (clientItems, _) = await ClientClient.GetPagedAsync(pageSize: 1000);
|
||||||
clients = clientItems.ToList();
|
clients = clientItems.ToList();
|
||||||
clientMap = clients.ToDictionary(c => c.Id, c => c.CompanyName ?? "");
|
clientMap = clients.ToDictionary(c => c.Id, GetClientDisplayName);
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
@@ -147,7 +168,11 @@
|
|||||||
private void OpenCreateDialog()
|
private void OpenCreateDialog()
|
||||||
{
|
{
|
||||||
editingActivity = null;
|
editingActivity = null;
|
||||||
activityForm = new ConsultingActivityForm { ActivityDate = DateTime.Now };
|
activityForm = new ConsultingActivityForm
|
||||||
|
{
|
||||||
|
ActivityDate = DateTime.Now,
|
||||||
|
ClientId = clients.FirstOrDefault()?.Id ?? 0
|
||||||
|
};
|
||||||
isDialogOpen = true;
|
isDialogOpen = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -167,6 +192,16 @@
|
|||||||
|
|
||||||
private async Task SaveActivity()
|
private async Task SaveActivity()
|
||||||
{
|
{
|
||||||
|
if (form != null)
|
||||||
|
{
|
||||||
|
await form.Validate();
|
||||||
|
if (!form.IsValid)
|
||||||
|
{
|
||||||
|
Snackbar.Add("필수 항목을 입력해주세요.", Severity.Warning);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
if (editingActivity == null)
|
if (editingActivity == null)
|
||||||
@@ -238,6 +273,12 @@
|
|||||||
activityForm = new();
|
activityForm = new();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static string GetClientDisplayName(Client client)
|
||||||
|
=> !string.IsNullOrWhiteSpace(client.CompanyName)
|
||||||
|
? client.CompanyName
|
||||||
|
: !string.IsNullOrWhiteSpace(client.Name)
|
||||||
|
? client.Name
|
||||||
|
: $"Client #{client.Id}";
|
||||||
private class ConsultingActivityForm
|
private class ConsultingActivityForm
|
||||||
{
|
{
|
||||||
public int ClientId { get; set; }
|
public int ClientId { get; set; }
|
||||||
|
|||||||
@@ -107,14 +107,21 @@
|
|||||||
</TitleContent>
|
</TitleContent>
|
||||||
<DialogContent>
|
<DialogContent>
|
||||||
<MudForm @ref="form">
|
<MudForm @ref="form">
|
||||||
<MudSelect T="int" @bind-Value="contractForm.ClientId" Label="고객" Required="true" Variant="Variant.Outlined" FullWidth="true" Class="mb-4">
|
<MudSelect T="int?" @bind-Value="contractForm.ClientId" Label="고객" Required="true" Variant="Variant.Outlined" FullWidth="true" Class="mb-4" RequiredError="고객을 선택하세요.">
|
||||||
@foreach (var client in clients)
|
@foreach (var client in clients)
|
||||||
{
|
{
|
||||||
<MudSelectItem Value="@client.Id">@client.CompanyName</MudSelectItem>
|
<MudSelectItem Value="@((int?)client.Id)">@GetClientDisplayName(client)</MudSelectItem>
|
||||||
}
|
}
|
||||||
</MudSelect>
|
</MudSelect>
|
||||||
<MudTextField T="string" @bind-Value="contractForm.ContractNumber" Label="계약번호" Variant="Variant.Outlined" FullWidth="true" Class="mb-4" Required="true" />
|
<MudTextField T="string" @bind-Value="contractForm.ContractNumber" Label="계약번호" Variant="Variant.Outlined" FullWidth="true" Class="mb-4" Required="true" />
|
||||||
<MudTextField T="string" @bind-Value="contractForm.ServiceType" Label="서비스 유형" Variant="Variant.Outlined" FullWidth="true" Class="mb-4" Required="true" />
|
<MudSelect T="string" @bind-Value="contractForm.ServiceType" Label="서비스 유형" Variant="Variant.Outlined" FullWidth="true" Class="mb-4" Required="true">
|
||||||
|
<MudSelectItem Value="@("개인 기장대리")">개인 기장대리</MudSelectItem>
|
||||||
|
<MudSelectItem Value="@("법인 기장대리")">법인 기장대리</MudSelectItem>
|
||||||
|
<MudSelectItem Value="@("세무조정 대행")">세무조정 대행</MudSelectItem>
|
||||||
|
<MudSelectItem Value="@("양도세 신고대리")">양도세 신고대리</MudSelectItem>
|
||||||
|
<MudSelectItem Value="@("상속·증여 자문")">상속·증여 자문</MudSelectItem>
|
||||||
|
<MudSelectItem Value="@("세무조사 대응")">세무조사 대응</MudSelectItem>
|
||||||
|
</MudSelect>
|
||||||
<MudDatePicker @bind-Date="contractForm.StartDate" Label="계약 시작일" Variant="Variant.Outlined" FullWidth="true" Class="mb-4" Required="true" />
|
<MudDatePicker @bind-Date="contractForm.StartDate" Label="계약 시작일" Variant="Variant.Outlined" FullWidth="true" Class="mb-4" Required="true" />
|
||||||
<MudNumericField T="decimal?" @bind-Value="contractForm.MonthlyFee" Label="월 수수료" Variant="Variant.Outlined" FullWidth="true" Class="mb-4" />
|
<MudNumericField T="decimal?" @bind-Value="contractForm.MonthlyFee" Label="월 수수료" Variant="Variant.Outlined" FullWidth="true" Class="mb-4" />
|
||||||
</MudForm>
|
</MudForm>
|
||||||
@@ -126,6 +133,9 @@
|
|||||||
</MudDialog>
|
</MudDialog>
|
||||||
|
|
||||||
@code {
|
@code {
|
||||||
|
[CascadingParameter]
|
||||||
|
private Task<AuthenticationState>? AuthStateTask { get; set; }
|
||||||
|
|
||||||
private List<Contract>? contracts;
|
private List<Contract>? contracts;
|
||||||
private List<Client> clients = [];
|
private List<Client> clients = [];
|
||||||
private Dictionary<int, string> clientMap = new();
|
private Dictionary<int, string> clientMap = new();
|
||||||
@@ -134,9 +144,20 @@
|
|||||||
private bool isDialogOpen;
|
private bool isDialogOpen;
|
||||||
private ContractForm contractForm = new();
|
private ContractForm contractForm = new();
|
||||||
|
|
||||||
protected override async Task OnInitializedAsync()
|
protected override async Task OnAfterRenderAsync(bool firstRender)
|
||||||
{
|
{
|
||||||
await LoadData();
|
if (firstRender)
|
||||||
|
{
|
||||||
|
if (AuthStateTask != null)
|
||||||
|
{
|
||||||
|
var authState = await AuthStateTask;
|
||||||
|
if (authState.User.Identity?.IsAuthenticated == true)
|
||||||
|
{
|
||||||
|
await LoadData();
|
||||||
|
StateHasChanged();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task LoadData()
|
private async Task LoadData()
|
||||||
@@ -144,9 +165,9 @@
|
|||||||
try
|
try
|
||||||
{
|
{
|
||||||
contracts = await ContractClient.GetAllAsync();
|
contracts = await ContractClient.GetAllAsync();
|
||||||
var (clientItems, _) = await ClientClient.GetPagedAsync();
|
var (clientItems, _) = await ClientClient.GetPagedAsync(pageSize: 1000);
|
||||||
clients = clientItems.ToList();
|
clients = clientItems.ToList();
|
||||||
clientMap = clients.ToDictionary(c => c.Id, c => c.CompanyName ?? "");
|
clientMap = clients.ToDictionary(c => c.Id, GetClientDisplayName);
|
||||||
mrr = await ContractClient.GetMonthlyRecurringRevenueAsync();
|
mrr = await ContractClient.GetMonthlyRecurringRevenueAsync();
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
@@ -157,16 +178,31 @@
|
|||||||
|
|
||||||
private void OpenCreateDialog()
|
private void OpenCreateDialog()
|
||||||
{
|
{
|
||||||
contractForm = new();
|
contractForm = new ContractForm
|
||||||
|
{
|
||||||
|
ClientId = clients.FirstOrDefault()?.Id,
|
||||||
|
StartDate = DateTime.Today
|
||||||
|
};
|
||||||
isDialogOpen = true;
|
isDialogOpen = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task SaveContract()
|
private async Task SaveContract()
|
||||||
{
|
{
|
||||||
|
if (form != null)
|
||||||
|
{
|
||||||
|
await form.Validate();
|
||||||
|
if (!form.IsValid)
|
||||||
|
{
|
||||||
|
Snackbar.Add("필수 항목을 입력해주세요.", Severity.Warning);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
|
if (contractForm.ClientId == null) return;
|
||||||
var newId = await ContractClient.CreateAsync(
|
var newId = await ContractClient.CreateAsync(
|
||||||
contractForm.ClientId,
|
contractForm.ClientId.Value,
|
||||||
contractForm.ContractNumber,
|
contractForm.ContractNumber,
|
||||||
contractForm.ServiceType,
|
contractForm.ServiceType,
|
||||||
contractForm.StartDate ?? DateTime.Now,
|
contractForm.StartDate ?? DateTime.Now,
|
||||||
@@ -217,9 +253,15 @@
|
|||||||
contractForm = new();
|
contractForm = new();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static string GetClientDisplayName(Client client)
|
||||||
|
=> !string.IsNullOrWhiteSpace(client.CompanyName)
|
||||||
|
? client.CompanyName
|
||||||
|
: !string.IsNullOrWhiteSpace(client.Name)
|
||||||
|
? client.Name
|
||||||
|
: $"Client #{client.Id}";
|
||||||
private class ContractForm
|
private class ContractForm
|
||||||
{
|
{
|
||||||
public int ClientId { get; set; }
|
public int? ClientId { get; set; }
|
||||||
public string ContractNumber { get; set; } = "";
|
public string ContractNumber { get; set; } = "";
|
||||||
public string ServiceType { get; set; } = "";
|
public string ServiceType { get; set; } = "";
|
||||||
public DateTime? StartDate { get; set; }
|
public DateTime? StartDate { get; set; }
|
||||||
|
|||||||
@@ -158,31 +158,45 @@
|
|||||||
</MudPaper>
|
</MudPaper>
|
||||||
|
|
||||||
@code {
|
@code {
|
||||||
|
[CascadingParameter]
|
||||||
|
private Task<AuthenticationState>? AuthStateTask { get; set; }
|
||||||
|
|
||||||
private AdminDashboardSummary summary = new(0, 0, 0, 0, []);
|
private AdminDashboardSummary summary = new(0, 0, 0, 0, []);
|
||||||
private List<Domain.Entities.TaxFiling> upcomingFilings = [];
|
private List<Domain.Entities.TaxFiling> upcomingFilings = [];
|
||||||
private string? errorMessage;
|
private string? errorMessage;
|
||||||
private bool isLoading = true;
|
private bool isLoading = true;
|
||||||
|
|
||||||
protected override async Task OnInitializedAsync()
|
protected override async Task OnAfterRenderAsync(bool firstRender)
|
||||||
{
|
{
|
||||||
try
|
if (firstRender)
|
||||||
{
|
{
|
||||||
// API 클라이언트 사용 (서비스 직접 호출 X)
|
if (AuthStateTask != null)
|
||||||
var summaryTask = DashboardClient.GetSummaryAsync();
|
{
|
||||||
var filingsTask = DashboardClient.GetUpcomingFilingsAsync(30);
|
var authState = await AuthStateTask;
|
||||||
|
if (authState.User.Identity?.IsAuthenticated == true)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
// API 클라이언트 사용 (서비스 직접 호출 X)
|
||||||
|
var summaryTask = DashboardClient.GetSummaryAsync();
|
||||||
|
var filingsTask = DashboardClient.GetUpcomingFilingsAsync(30);
|
||||||
|
|
||||||
await Task.WhenAll(summaryTask, filingsTask);
|
await Task.WhenAll(summaryTask, filingsTask);
|
||||||
summary = await summaryTask;
|
summary = await summaryTask;
|
||||||
upcomingFilings = (await filingsTask).ToList();
|
upcomingFilings = (await filingsTask).ToList();
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
errorMessage = "대시보드 데이터를 불러올 수 없습니다.";
|
errorMessage = "대시보드 데이터를 불러올 수 없습니다.";
|
||||||
Console.Error.WriteLine($"Dashboard error: {ex.Message}");
|
Console.Error.WriteLine($"Dashboard error: {ex.Message}");
|
||||||
}
|
}
|
||||||
finally
|
finally
|
||||||
{
|
{
|
||||||
isLoading = false;
|
isLoading = false;
|
||||||
|
StateHasChanged();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -95,9 +95,26 @@
|
|||||||
</MudPaper>
|
</MudPaper>
|
||||||
|
|
||||||
@code {
|
@code {
|
||||||
|
[CascadingParameter]
|
||||||
|
private Task<AuthenticationState>? AuthStateTask { get; set; }
|
||||||
|
|
||||||
private List<Faq>? faqs;
|
private List<Faq>? faqs;
|
||||||
|
|
||||||
protected override async Task OnInitializedAsync() => await LoadAsync();
|
protected override async Task OnAfterRenderAsync(bool firstRender)
|
||||||
|
{
|
||||||
|
if (firstRender)
|
||||||
|
{
|
||||||
|
if (AuthStateTask != null)
|
||||||
|
{
|
||||||
|
var authState = await AuthStateTask;
|
||||||
|
if (authState.User.Identity?.IsAuthenticated == true)
|
||||||
|
{
|
||||||
|
await LoadAsync();
|
||||||
|
StateHasChanged();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private async Task LoadAsync()
|
private async Task LoadAsync()
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -46,11 +46,31 @@ else
|
|||||||
</MudPaper>
|
</MudPaper>
|
||||||
|
|
||||||
@code {
|
@code {
|
||||||
|
[CascadingParameter]
|
||||||
|
private Task<AuthenticationState>? AuthStateTask { get; set; }
|
||||||
|
|
||||||
private bool isLoading = true;
|
private bool isLoading = true;
|
||||||
private IReadOnlyList<Domain.Entities.Inquiry> allInquiries = [];
|
private IReadOnlyList<Domain.Entities.Inquiry> allInquiries = [];
|
||||||
|
|
||||||
protected override async Task OnInitializedAsync()
|
protected override async Task OnAfterRenderAsync(bool firstRender)
|
||||||
{
|
{
|
||||||
|
if (firstRender)
|
||||||
|
{
|
||||||
|
if (AuthStateTask != null)
|
||||||
|
{
|
||||||
|
var authState = await AuthStateTask;
|
||||||
|
if (authState.User.Identity?.IsAuthenticated == true)
|
||||||
|
{
|
||||||
|
await LoadData();
|
||||||
|
StateHasChanged();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task LoadData()
|
||||||
|
{
|
||||||
|
isLoading = true;
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
var (items, _) = await InquiryClient.GetPagedAsync(1, 200);
|
var (items, _) = await InquiryClient.GetPagedAsync(1, 200);
|
||||||
|
|||||||
@@ -96,13 +96,19 @@
|
|||||||
<MudSelect T="int" @bind-Value="revenueForm.ClientId" Label="고객" Required="true" Variant="Variant.Outlined" FullWidth="true" Class="mb-4">
|
<MudSelect T="int" @bind-Value="revenueForm.ClientId" Label="고객" Required="true" Variant="Variant.Outlined" FullWidth="true" Class="mb-4">
|
||||||
@foreach (var client in clients)
|
@foreach (var client in clients)
|
||||||
{
|
{
|
||||||
<MudSelectItem Value="@client.Id">@client.CompanyName</MudSelectItem>
|
<MudSelectItem Value="@client.Id">@GetClientDisplayName(client)</MudSelectItem>
|
||||||
}
|
}
|
||||||
</MudSelect>
|
</MudSelect>
|
||||||
<MudTextField T="string" @bind-Value="revenueForm.InvoiceNumber" Label="청구번호" Variant="Variant.Outlined" FullWidth="true" Class="mb-4" Required="true" />
|
<MudTextField T="string" @bind-Value="revenueForm.InvoiceNumber" Label="청구번호" Variant="Variant.Outlined" FullWidth="true" Class="mb-4" Required="true" />
|
||||||
<MudDatePicker @bind-Date="revenueForm.InvoiceDate" Label="청구일" Variant="Variant.Outlined" FullWidth="true" Class="mb-4" Required="true" />
|
<MudDatePicker @bind-Date="revenueForm.InvoiceDate" Label="청구일" Variant="Variant.Outlined" FullWidth="true" Class="mb-4" Required="true" />
|
||||||
<MudNumericField T="decimal" @bind-Value="revenueForm.Amount" Label="청구액" Variant="Variant.Outlined" FullWidth="true" Class="mb-4" Required="true" />
|
<MudNumericField T="decimal" @bind-Value="revenueForm.Amount" Label="청구액" Variant="Variant.Outlined" FullWidth="true" Class="mb-4" Required="true" />
|
||||||
<MudTextField T="string" @bind-Value="revenueForm.ServiceType" Label="서비스 유형" Variant="Variant.Outlined" FullWidth="true" Class="mb-4" />
|
<MudSelect T="string" @bind-Value="revenueForm.ServiceType" Label="서비스 유형" Variant="Variant.Outlined" FullWidth="true" Class="mb-4">
|
||||||
|
<MudSelectItem Value="@("기장 수수료")">기장 수수료</MudSelectItem>
|
||||||
|
<MudSelectItem Value="@("세무조정료")">세무조정료</MudSelectItem>
|
||||||
|
<MudSelectItem Value="@("세무상담료")">세무상담료</MudSelectItem>
|
||||||
|
<MudSelectItem Value="@("신고 대행료")">신고 대행료</MudSelectItem>
|
||||||
|
<MudSelectItem Value="@("자문 수수료")">자문 수수료</MudSelectItem>
|
||||||
|
</MudSelect>
|
||||||
<MudDatePicker @bind-Date="revenueForm.DueDate" Label="납부예정일" Variant="Variant.Outlined" FullWidth="true" Class="mb-4" />
|
<MudDatePicker @bind-Date="revenueForm.DueDate" Label="납부예정일" Variant="Variant.Outlined" FullWidth="true" Class="mb-4" />
|
||||||
</MudForm>
|
</MudForm>
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
@@ -113,6 +119,9 @@
|
|||||||
</MudDialog>
|
</MudDialog>
|
||||||
|
|
||||||
@code {
|
@code {
|
||||||
|
[CascadingParameter]
|
||||||
|
private Task<AuthenticationState>? AuthStateTask { get; set; }
|
||||||
|
|
||||||
private List<RevenueTracking>? revenues;
|
private List<RevenueTracking>? revenues;
|
||||||
private List<Client> clients = [];
|
private List<Client> clients = [];
|
||||||
private Dictionary<int, string> clientMap = new();
|
private Dictionary<int, string> clientMap = new();
|
||||||
@@ -120,9 +129,20 @@
|
|||||||
private bool isDialogOpen;
|
private bool isDialogOpen;
|
||||||
private RevenueForm revenueForm = new();
|
private RevenueForm revenueForm = new();
|
||||||
|
|
||||||
protected override async Task OnInitializedAsync()
|
protected override async Task OnAfterRenderAsync(bool firstRender)
|
||||||
{
|
{
|
||||||
await LoadData();
|
if (firstRender)
|
||||||
|
{
|
||||||
|
if (AuthStateTask != null)
|
||||||
|
{
|
||||||
|
var authState = await AuthStateTask;
|
||||||
|
if (authState.User.Identity?.IsAuthenticated == true)
|
||||||
|
{
|
||||||
|
await LoadData();
|
||||||
|
StateHasChanged();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task LoadData()
|
private async Task LoadData()
|
||||||
@@ -130,9 +150,9 @@
|
|||||||
try
|
try
|
||||||
{
|
{
|
||||||
revenues = await RevenueClient.GetAllAsync();
|
revenues = await RevenueClient.GetAllAsync();
|
||||||
var (clientItems, _) = await ClientClient.GetPagedAsync();
|
var (clientItems, _) = await ClientClient.GetPagedAsync(pageSize: 1000);
|
||||||
clients = clientItems.ToList();
|
clients = clientItems.ToList();
|
||||||
clientMap = clients.ToDictionary(c => c.Id, c => c.CompanyName ?? "");
|
clientMap = clients.ToDictionary(c => c.Id, GetClientDisplayName);
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
@@ -142,12 +162,27 @@
|
|||||||
|
|
||||||
private void OpenCreateDialog()
|
private void OpenCreateDialog()
|
||||||
{
|
{
|
||||||
revenueForm = new();
|
revenueForm = new RevenueForm
|
||||||
|
{
|
||||||
|
ClientId = clients.FirstOrDefault()?.Id ?? 0,
|
||||||
|
InvoiceDate = DateTime.Today,
|
||||||
|
DueDate = DateTime.Today.AddDays(14)
|
||||||
|
};
|
||||||
isDialogOpen = true;
|
isDialogOpen = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task SaveRevenue()
|
private async Task SaveRevenue()
|
||||||
{
|
{
|
||||||
|
if (form != null)
|
||||||
|
{
|
||||||
|
await form.Validate();
|
||||||
|
if (!form.IsValid)
|
||||||
|
{
|
||||||
|
Snackbar.Add("필수 항목을 입력해주세요.", Severity.Warning);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
var newId = await RevenueClient.CreateAsync(
|
var newId = await RevenueClient.CreateAsync(
|
||||||
@@ -217,6 +252,12 @@
|
|||||||
revenueForm = new();
|
revenueForm = new();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static string GetClientDisplayName(Client client)
|
||||||
|
=> !string.IsNullOrWhiteSpace(client.CompanyName)
|
||||||
|
? client.CompanyName
|
||||||
|
: !string.IsNullOrWhiteSpace(client.Name)
|
||||||
|
? client.Name
|
||||||
|
: $"Client #{client.Id}";
|
||||||
private class RevenueForm
|
private class RevenueForm
|
||||||
{
|
{
|
||||||
public int ClientId { get; set; }
|
public int ClientId { get; set; }
|
||||||
|
|||||||
@@ -117,19 +117,29 @@
|
|||||||
</TitleContent>
|
</TitleContent>
|
||||||
<DialogContent>
|
<DialogContent>
|
||||||
<MudForm @ref="form">
|
<MudForm @ref="form">
|
||||||
<MudSelect T="int"
|
<MudSelect T="int?"
|
||||||
@bind-Value="scheduleForm.ClientId"
|
@bind-Value="scheduleForm.ClientId"
|
||||||
Label="고객"
|
Label="고객"
|
||||||
Required="true"
|
Required="true"
|
||||||
Variant="Variant.Outlined"
|
Variant="Variant.Outlined"
|
||||||
FullWidth="true"
|
FullWidth="true"
|
||||||
Class="mb-4">
|
Class="mb-4"
|
||||||
|
RequiredError="고객을 선택하세요.">
|
||||||
@foreach (var client in clients)
|
@foreach (var client in clients)
|
||||||
{
|
{
|
||||||
<MudSelectItem Value="@client.Id">@client.CompanyName</MudSelectItem>
|
<MudSelectItem Value="@((int?)client.Id)">@GetClientDisplayName(client)</MudSelectItem>
|
||||||
}
|
}
|
||||||
</MudSelect>
|
</MudSelect>
|
||||||
<MudTextField T="string" @bind-Value="scheduleForm.FilingType" Label="신고 유형" Variant="Variant.Outlined" FullWidth="true" Class="mb-4" Required="true" />
|
<MudSelect T="string" @bind-Value="scheduleForm.FilingType" Label="신고 유형" Variant="Variant.Outlined" FullWidth="true" Class="mb-4" Required="true">
|
||||||
|
<MudSelectItem Value="@("종합소득세")">종합소득세</MudSelectItem>
|
||||||
|
<MudSelectItem Value="@("부가가치세")">부가가치세</MudSelectItem>
|
||||||
|
<MudSelectItem Value="@("법인세")">법인세</MudSelectItem>
|
||||||
|
<MudSelectItem Value="@("원천세")">원천세</MudSelectItem>
|
||||||
|
<MudSelectItem Value="@("종합부동산세")">종합부동산세</MudSelectItem>
|
||||||
|
<MudSelectItem Value="@("양도소득세")">양도소득세</MudSelectItem>
|
||||||
|
<MudSelectItem Value="@("상속·증여세")">상속·증여세</MudSelectItem>
|
||||||
|
<MudSelectItem Value="@("세무조정")">세무조정</MudSelectItem>
|
||||||
|
</MudSelect>
|
||||||
<MudDatePicker @bind-Date="scheduleForm.DueDate" Label="마감일" Variant="Variant.Outlined" FullWidth="true" Class="mb-4" Required="true" />
|
<MudDatePicker @bind-Date="scheduleForm.DueDate" Label="마감일" Variant="Variant.Outlined" FullWidth="true" Class="mb-4" Required="true" />
|
||||||
<MudNumericField T="int" @bind-Value="scheduleForm.FilingYear" Label="신고연도" Variant="Variant.Outlined" FullWidth="true" Class="mb-4" Required="true" />
|
<MudNumericField T="int" @bind-Value="scheduleForm.FilingYear" Label="신고연도" Variant="Variant.Outlined" FullWidth="true" Class="mb-4" Required="true" />
|
||||||
</MudForm>
|
</MudForm>
|
||||||
@@ -141,23 +151,40 @@
|
|||||||
</MudDialog>
|
</MudDialog>
|
||||||
|
|
||||||
@code {
|
@code {
|
||||||
|
[CascadingParameter]
|
||||||
|
private Task<AuthenticationState>? AuthStateTask { get; set; }
|
||||||
|
|
||||||
private List<TaxFilingSchedule>? schedules;
|
private List<TaxFilingSchedule>? schedules;
|
||||||
private List<Client> clients = [];
|
private List<Client> clients = [];
|
||||||
private Dictionary<int, string> clientMap = new();
|
private Dictionary<int, string> clientMap = new();
|
||||||
private MudForm? form;
|
private MudForm? form;
|
||||||
private bool isDialogOpen;
|
private bool isDialogOpen;
|
||||||
private TaxFilingScheduleForm scheduleForm = new();
|
private TaxFilingScheduleForm scheduleForm = new();
|
||||||
|
|
||||||
protected override async Task OnInitializedAsync() => await LoadData();
|
protected override async Task OnAfterRenderAsync(bool firstRender)
|
||||||
|
{
|
||||||
|
if (firstRender)
|
||||||
|
{
|
||||||
|
if (AuthStateTask != null)
|
||||||
|
{
|
||||||
|
var authState = await AuthStateTask;
|
||||||
|
if (authState.User.Identity?.IsAuthenticated == true)
|
||||||
|
{
|
||||||
|
await LoadData();
|
||||||
|
StateHasChanged();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private async Task LoadData()
|
private async Task LoadData()
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
schedules = await TaxFilingClient.GetAllAsync();
|
schedules = await TaxFilingClient.GetAllAsync();
|
||||||
var (clientItems, _) = await ClientClient.GetPagedAsync();
|
var (clientItems, _) = await ClientClient.GetPagedAsync(pageSize: 1000);
|
||||||
clients = clientItems.ToList();
|
clients = clientItems.ToList();
|
||||||
clientMap = clients.ToDictionary(c => c.Id, c => c.CompanyName ?? "");
|
clientMap = clients.ToDictionary(c => c.Id, GetClientDisplayName);
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
@@ -167,16 +194,32 @@
|
|||||||
|
|
||||||
private void OpenCreateDialog()
|
private void OpenCreateDialog()
|
||||||
{
|
{
|
||||||
scheduleForm = new TaxFilingScheduleForm { FilingYear = DateTime.Now.Year };
|
scheduleForm = new TaxFilingScheduleForm
|
||||||
|
{
|
||||||
|
FilingYear = DateTime.Now.Year,
|
||||||
|
DueDate = DateTime.Today,
|
||||||
|
ClientId = clients.FirstOrDefault()?.Id
|
||||||
|
};
|
||||||
isDialogOpen = true;
|
isDialogOpen = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task SaveSchedule()
|
private async Task SaveSchedule()
|
||||||
{
|
{
|
||||||
|
if (form != null)
|
||||||
|
{
|
||||||
|
await form.Validate();
|
||||||
|
if (!form.IsValid)
|
||||||
|
{
|
||||||
|
Snackbar.Add("필수 항목을 입력해주세요.", Severity.Warning);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
|
if (scheduleForm.ClientId == null) return;
|
||||||
var newId = await TaxFilingClient.CreateAsync(
|
var newId = await TaxFilingClient.CreateAsync(
|
||||||
scheduleForm.ClientId,
|
scheduleForm.ClientId.Value,
|
||||||
scheduleForm.FilingType,
|
scheduleForm.FilingType,
|
||||||
scheduleForm.DueDate ?? DateTime.Today,
|
scheduleForm.DueDate ?? DateTime.Today,
|
||||||
scheduleForm.FilingYear);
|
scheduleForm.FilingYear);
|
||||||
@@ -243,9 +286,15 @@
|
|||||||
scheduleForm = new();
|
scheduleForm = new();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static string GetClientDisplayName(Client client)
|
||||||
|
=> !string.IsNullOrWhiteSpace(client.CompanyName)
|
||||||
|
? client.CompanyName
|
||||||
|
: !string.IsNullOrWhiteSpace(client.Name)
|
||||||
|
? client.Name
|
||||||
|
: $"Client #{client.Id}";
|
||||||
private class TaxFilingScheduleForm
|
private class TaxFilingScheduleForm
|
||||||
{
|
{
|
||||||
public int ClientId { get; set; }
|
public int? ClientId { get; set; }
|
||||||
public string FilingType { get; set; } = "";
|
public string FilingType { get; set; } = "";
|
||||||
public DateTime? DueDate { get; set; }
|
public DateTime? DueDate { get; set; }
|
||||||
public int FilingYear { get; set; } = DateTime.Now.Year;
|
public int FilingYear { get; set; } = DateTime.Now.Year;
|
||||||
|
|||||||
@@ -101,7 +101,7 @@
|
|||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
var (items, _) = await ClientClient.GetPagedAsync(1, 20, search: value);
|
var (items, _) = await ClientClient.GetPagedAsync(1, 100, search: value);
|
||||||
return items;
|
return items;
|
||||||
}
|
}
|
||||||
catch
|
catch
|
||||||
@@ -110,6 +110,12 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static string GetClientDisplayName(Client client)
|
||||||
|
=> !string.IsNullOrWhiteSpace(client.CompanyName)
|
||||||
|
? client.CompanyName
|
||||||
|
: !string.IsNullOrWhiteSpace(client.Name)
|
||||||
|
? client.Name
|
||||||
|
: $"Client #{client.Id}";
|
||||||
private async Task AddFiling()
|
private async Task AddFiling()
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
|
|||||||
@@ -84,13 +84,23 @@ else
|
|||||||
</TitleContent>
|
</TitleContent>
|
||||||
<DialogContent>
|
<DialogContent>
|
||||||
<MudForm @ref="form">
|
<MudForm @ref="form">
|
||||||
<MudSelect T="int" @bind-Value="profileForm.ClientId" Label="고객" Required="true" Variant="Variant.Outlined" FullWidth="true" Class="mb-4">
|
<MudSelect T="int?" @bind-Value="profileForm.ClientId" Label="고객" Required="true" Variant="Variant.Outlined" FullWidth="true" Class="mb-4" RequiredError="고객을 선택하세요.">
|
||||||
@foreach (var client in clients)
|
@foreach (var client in clients)
|
||||||
{
|
{
|
||||||
<MudSelectItem Value="@client.Id">@client.CompanyName</MudSelectItem>
|
<MudSelectItem Value="@((int?)client.Id)">@GetClientDisplayName(client)</MudSelectItem>
|
||||||
}
|
}
|
||||||
</MudSelect>
|
</MudSelect>
|
||||||
<MudTextField T="string" @bind-Value="profileForm.BusinessType" Label="사업 유형" Variant="Variant.Outlined" FullWidth="true" Class="mb-4" />
|
<MudSelect T="string" @bind-Value="profileForm.BusinessType" Label="사업 유형" Variant="Variant.Outlined" FullWidth="true" Class="mb-4" Required="true">
|
||||||
|
<MudSelectItem Value="@("일반제조업")">일반제조업</MudSelectItem>
|
||||||
|
<MudSelectItem Value="@("도소매업")">도소매업</MudSelectItem>
|
||||||
|
<MudSelectItem Value="@("서비스업")">서비스업</MudSelectItem>
|
||||||
|
<MudSelectItem Value="@("정보통신업")">정보통신업</MudSelectItem>
|
||||||
|
<MudSelectItem Value="@("부동산업")">부동산업</MudSelectItem>
|
||||||
|
<MudSelectItem Value="@("건설업")">건설업</MudSelectItem>
|
||||||
|
<MudSelectItem Value="@("음식점업")">음식점업</MudSelectItem>
|
||||||
|
<MudSelectItem Value="@("프리랜서")">프리랜서</MudSelectItem>
|
||||||
|
<MudSelectItem Value="@("기타")">기타</MudSelectItem>
|
||||||
|
</MudSelect>
|
||||||
<MudSelect T="string" @bind-Value="profileForm.TaxRiskLevel" Label="위험도" Variant="Variant.Outlined" FullWidth="true" Class="mb-4">
|
<MudSelect T="string" @bind-Value="profileForm.TaxRiskLevel" Label="위험도" Variant="Variant.Outlined" FullWidth="true" Class="mb-4">
|
||||||
<MudSelectItem Value="@("low")">낮음</MudSelectItem>
|
<MudSelectItem Value="@("low")">낮음</MudSelectItem>
|
||||||
<MudSelectItem Value="@("normal")">보통</MudSelectItem>
|
<MudSelectItem Value="@("normal")">보통</MudSelectItem>
|
||||||
@@ -107,6 +117,9 @@ else
|
|||||||
</MudDialog>
|
</MudDialog>
|
||||||
|
|
||||||
@code {
|
@code {
|
||||||
|
[CascadingParameter]
|
||||||
|
private Task<AuthenticationState>? AuthStateTask { get; set; }
|
||||||
|
|
||||||
private List<TaxProfile>? profiles;
|
private List<TaxProfile>? profiles;
|
||||||
private List<Client> clients = [];
|
private List<Client> clients = [];
|
||||||
private Dictionary<int, string> clientMap = new();
|
private Dictionary<int, string> clientMap = new();
|
||||||
@@ -116,9 +129,20 @@ else
|
|||||||
private TaxProfile? editingProfile;
|
private TaxProfile? editingProfile;
|
||||||
private TaxProfileForm profileForm = new();
|
private TaxProfileForm profileForm = new();
|
||||||
|
|
||||||
protected override async Task OnInitializedAsync()
|
protected override async Task OnAfterRenderAsync(bool firstRender)
|
||||||
{
|
{
|
||||||
await LoadData();
|
if (firstRender)
|
||||||
|
{
|
||||||
|
if (AuthStateTask != null)
|
||||||
|
{
|
||||||
|
var authState = await AuthStateTask;
|
||||||
|
if (authState.User.Identity?.IsAuthenticated == true)
|
||||||
|
{
|
||||||
|
await LoadData();
|
||||||
|
StateHasChanged();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task LoadData()
|
private async Task LoadData()
|
||||||
@@ -126,9 +150,9 @@ else
|
|||||||
try
|
try
|
||||||
{
|
{
|
||||||
profiles = await TaxProfileClient.GetAllAsync();
|
profiles = await TaxProfileClient.GetAllAsync();
|
||||||
var (clientItems, _) = await ClientClient.GetPagedAsync();
|
var (clientItems, _) = await ClientClient.GetPagedAsync(pageSize: 1000);
|
||||||
clients = clientItems.ToList();
|
clients = clientItems.ToList();
|
||||||
clientMap = clients.ToDictionary(c => c.Id, c => c.CompanyName ?? "");
|
clientMap = clients.ToDictionary(c => c.Id, GetClientDisplayName);
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
@@ -140,7 +164,12 @@ else
|
|||||||
{
|
{
|
||||||
isEditMode = false;
|
isEditMode = false;
|
||||||
editingProfile = null;
|
editingProfile = null;
|
||||||
profileForm = new();
|
profileForm = new TaxProfileForm
|
||||||
|
{
|
||||||
|
ClientId = clients.FirstOrDefault()?.Id,
|
||||||
|
TaxRiskLevel = "normal",
|
||||||
|
NextFilingDueDate = DateTime.Today.AddMonths(1)
|
||||||
|
};
|
||||||
isDialogOpen = true;
|
isDialogOpen = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -161,25 +190,43 @@ else
|
|||||||
|
|
||||||
private async Task SaveProfile()
|
private async Task SaveProfile()
|
||||||
{
|
{
|
||||||
|
if (form != null)
|
||||||
|
{
|
||||||
|
await form.Validate();
|
||||||
|
if (!form.IsValid)
|
||||||
|
{
|
||||||
|
Snackbar.Add("고객을 선택하세요.", Severity.Warning);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
if (isEditMode)
|
if (isEditMode && editingProfile != null)
|
||||||
{
|
{
|
||||||
await TaxProfileClient.UpdateAsync(
|
await TaxProfileClient.UpdateAsync(editingProfile.Id, profileForm.BusinessType,
|
||||||
editingProfile!.Id,
|
null, profileForm.NextFilingDueDate, profileForm.TaxRiskLevel);
|
||||||
profileForm.BusinessType,
|
Snackbar.Add("세무 프로필이 수정되었습니다.", Severity.Success);
|
||||||
null,
|
|
||||||
profileForm.NextFilingDueDate,
|
|
||||||
profileForm.TaxRiskLevel);
|
|
||||||
Snackbar.Add("세무 프로필이 업데이트되었습니다.", Severity.Success);
|
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
|
if (!profileForm.ClientId.HasValue)
|
||||||
|
{
|
||||||
|
Snackbar.Add("고객을 선택하세요.", Severity.Warning);
|
||||||
|
return;
|
||||||
|
}
|
||||||
var newId = await TaxProfileClient.CreateAsync(
|
var newId = await TaxProfileClient.CreateAsync(
|
||||||
profileForm.ClientId,
|
profileForm.ClientId.Value,
|
||||||
profileForm.BusinessType);
|
profileForm.BusinessType);
|
||||||
if (newId > 0)
|
if (newId > 0)
|
||||||
{
|
{
|
||||||
|
// 생성 후 상태 업데이트 처리
|
||||||
|
await TaxProfileClient.UpdateAsync(
|
||||||
|
newId,
|
||||||
|
profileForm.BusinessType,
|
||||||
|
null,
|
||||||
|
profileForm.NextFilingDueDate,
|
||||||
|
profileForm.TaxRiskLevel);
|
||||||
Snackbar.Add("세무 프로필이 추가되었습니다.", Severity.Success);
|
Snackbar.Add("세무 프로필이 추가되었습니다.", Severity.Success);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -232,9 +279,15 @@ else
|
|||||||
_ => Color.Default
|
_ => Color.Default
|
||||||
};
|
};
|
||||||
|
|
||||||
|
private static string GetClientDisplayName(Client client)
|
||||||
|
=> !string.IsNullOrWhiteSpace(client.CompanyName)
|
||||||
|
? client.CompanyName
|
||||||
|
: !string.IsNullOrWhiteSpace(client.Name)
|
||||||
|
? client.Name
|
||||||
|
: $"Client #{client.Id}";
|
||||||
private class TaxProfileForm
|
private class TaxProfileForm
|
||||||
{
|
{
|
||||||
public int ClientId { get; set; }
|
public int? ClientId { get; set; }
|
||||||
public string BusinessType { get; set; } = "";
|
public string BusinessType { get; set; } = "";
|
||||||
public string TaxRiskLevel { get; set; } = "normal";
|
public string TaxRiskLevel { get; set; } = "normal";
|
||||||
public DateTime? NextFilingDueDate { get; set; }
|
public DateTime? NextFilingDueDate { get; set; }
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ namespace TaxBaik.Web.Controllers;
|
|||||||
/// SOLID: Single Responsibility - 대시보드 데이터만 담당
|
/// SOLID: Single Responsibility - 대시보드 데이터만 담당
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[ApiController]
|
[ApiController]
|
||||||
[Route("api/[controller]")]
|
[Route("api/admin-dashboard")]
|
||||||
[Authorize]
|
[Authorize]
|
||||||
public class AdminDashboardController : ControllerBase
|
public class AdminDashboardController : ControllerBase
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -24,6 +24,20 @@ public class ConsultingActivityController(ConsultingActivityService service) : C
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[HttpGet]
|
||||||
|
public async Task<IActionResult> GetAll()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var activities = await service.GetAllAsync();
|
||||||
|
return Ok(activities);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
return StatusCode(500, new { error = "조회 실패", message = ex.Message });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
[HttpGet("{id:int}")]
|
[HttpGet("{id:int}")]
|
||||||
public async Task<IActionResult> GetById(int id)
|
public async Task<IActionResult> GetById(int id)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -24,6 +24,20 @@ public class ContractController(ContractService service) : ControllerBase
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[HttpGet]
|
||||||
|
public async Task<IActionResult> GetAll()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var contracts = await service.GetAllAsync();
|
||||||
|
return Ok(contracts);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
return StatusCode(500, new { error = "조회 실패", message = ex.Message });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
[HttpGet("{id:int}")]
|
[HttpGet("{id:int}")]
|
||||||
public async Task<IActionResult> GetById(int id)
|
public async Task<IActionResult> GetById(int id)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -24,6 +24,20 @@ public class RevenueTrackingController(RevenueTrackingService service) : Control
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[HttpGet]
|
||||||
|
public async Task<IActionResult> GetAll()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var revenues = await service.GetAllAsync();
|
||||||
|
return Ok(revenues);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
return StatusCode(500, new { error = "조회 실패", message = ex.Message });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
[HttpGet("{id:int}")]
|
[HttpGet("{id:int}")]
|
||||||
public async Task<IActionResult> GetById(int id)
|
public async Task<IActionResult> GetById(int id)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -24,6 +24,20 @@ public class TaxFilingScheduleController(TaxFilingScheduleService service) : Con
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[HttpGet]
|
||||||
|
public async Task<IActionResult> GetAll()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var schedules = await service.GetAllAsync();
|
||||||
|
return Ok(schedules);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
return StatusCode(500, new { error = "조회 실패", message = ex.Message });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
[HttpGet("{id:int}")]
|
[HttpGet("{id:int}")]
|
||||||
public async Task<IActionResult> GetById(int id)
|
public async Task<IActionResult> GetById(int id)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -24,6 +24,20 @@ public class TaxProfileController(TaxProfileService taxProfileService) : Control
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[HttpGet]
|
||||||
|
public async Task<IActionResult> GetAll()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var profiles = await taxProfileService.GetAllAsync();
|
||||||
|
return Ok(profiles);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
return StatusCode(500, new { error = "조회 실패", message = ex.Message });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
[HttpGet("client/{clientId:int}")]
|
[HttpGet("client/{clientId:int}")]
|
||||||
public async Task<IActionResult> GetByClientId(int clientId)
|
public async Task<IActionResult> GetByClientId(int clientId)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -1,87 +0,0 @@
|
|||||||
using Microsoft.AspNetCore.Authorization;
|
|
||||||
using Microsoft.AspNetCore.SignalR;
|
|
||||||
|
|
||||||
namespace TaxBaik.Web.Hubs;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Real-time notification hub for admin dashboard
|
|
||||||
/// SOLID: Single Responsibility - Only broadcasts change notifications
|
|
||||||
/// No state management - stateless broadcast pattern
|
|
||||||
/// </summary>
|
|
||||||
[Authorize]
|
|
||||||
public class NotificationHub : Hub
|
|
||||||
{
|
|
||||||
private const string AdminGroup = "admins";
|
|
||||||
|
|
||||||
public override async Task OnConnectedAsync()
|
|
||||||
{
|
|
||||||
await Groups.AddToGroupAsync(Context.ConnectionId, AdminGroup);
|
|
||||||
await base.OnConnectedAsync();
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Broadcast inquiry status changed to all connected admins
|
|
||||||
/// Clients should re-fetch from API to verify
|
|
||||||
/// </summary>
|
|
||||||
public async Task NotifyInquiryStatusChanged(int inquiryId, string newStatus)
|
|
||||||
{
|
|
||||||
await Clients.Group(AdminGroup).SendAsync("InquiryStatusChanged", new
|
|
||||||
{
|
|
||||||
InquiryId = inquiryId,
|
|
||||||
Status = newStatus,
|
|
||||||
ChangedAt = DateTime.UtcNow
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Broadcast inquiry submitted (new inquiry created)
|
|
||||||
/// </summary>
|
|
||||||
public async Task NotifyInquiryCreated(int inquiryId, string name)
|
|
||||||
{
|
|
||||||
await Clients.Group(AdminGroup).SendAsync("InquiryCreated", new
|
|
||||||
{
|
|
||||||
InquiryId = inquiryId,
|
|
||||||
Name = name,
|
|
||||||
CreatedAt = DateTime.UtcNow
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Broadcast client created
|
|
||||||
/// </summary>
|
|
||||||
public async Task NotifyClientCreated(int clientId, string name)
|
|
||||||
{
|
|
||||||
await Clients.Group(AdminGroup).SendAsync("ClientCreated", new
|
|
||||||
{
|
|
||||||
ClientId = clientId,
|
|
||||||
Name = name,
|
|
||||||
CreatedAt = DateTime.UtcNow
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Broadcast announcement published
|
|
||||||
/// </summary>
|
|
||||||
public async Task NotifyAnnouncementPublished(int announcementId, string title)
|
|
||||||
{
|
|
||||||
await Clients.Group(AdminGroup).SendAsync("AnnouncementPublished", new
|
|
||||||
{
|
|
||||||
AnnouncementId = announcementId,
|
|
||||||
Title = title,
|
|
||||||
PublishedAt = DateTime.UtcNow
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Broadcast tax filing completed
|
|
||||||
/// </summary>
|
|
||||||
public async Task NotifyFilingCompleted(int filingId, string filingType)
|
|
||||||
{
|
|
||||||
await Clients.Group(AdminGroup).SendAsync("FilingCompleted", new
|
|
||||||
{
|
|
||||||
FilingId = filingId,
|
|
||||||
FilingType = filingType,
|
|
||||||
CompletedAt = DateTime.UtcNow
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -28,6 +28,20 @@ public class TelegramSink : ILogEventSink
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Filter out harmless client disconnect and task cancellation exceptions
|
||||||
|
if (logEvent.Exception != null)
|
||||||
|
{
|
||||||
|
var exTypeName = logEvent.Exception.GetType().FullName ?? "";
|
||||||
|
var exMessage = logEvent.Exception.Message ?? "";
|
||||||
|
if (exTypeName.Contains("JSDisconnectedException") ||
|
||||||
|
exTypeName.Contains("TaskCanceledException") ||
|
||||||
|
exMessage.Contains("JavaScript interop calls cannot be issued") ||
|
||||||
|
exMessage.Contains("circuit has disconnected"))
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Emit is a synchronous method, so we dispatch the network call asynchronously
|
// Emit is a synchronous method, so we dispatch the network call asynchronously
|
||||||
Task.Run(async () =>
|
Task.Run(async () =>
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -3,21 +3,57 @@
|
|||||||
<head>
|
<head>
|
||||||
<meta charset="utf-8" />
|
<meta charset="utf-8" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
<title>@(ViewData["Title"] ?? "백원숙 세무회계")</title>
|
<title>@(ViewData["Title"] ?? "백원숙 세무회계 - 세무사 전문 상담")</title>
|
||||||
<meta name="description" content="@(ViewData["Description"] ?? "사업자 기장, 부동산 양도세·증여세, 종합소득세 전문 상담.")" />
|
<meta name="description" content="@(ViewData["Description"] ?? "백원숙 세무회계 - 사업자 기장, 부동산 양도세·증여세, 종합소득세 전문 상담. 맞춤형 세무 절세 컨설팅 제공.")" />
|
||||||
<meta property="og:title" content="@ViewData["Title"]" />
|
<meta name="keywords" content="백원숙 세무회계, 세무사, 사업자 기장, 양도소득세, 증여세, 상속세, 종합소득세, 절세 상담, 세무 대리" />
|
||||||
<meta property="og:description" content="@ViewData["Description"]" />
|
|
||||||
<meta property="og:image" content="@ViewData["OgImage"]" />
|
<!-- Open Graph / Facebook -->
|
||||||
<meta property="og:url" content="@ViewData["OgUrl"]" />
|
<meta property="og:type" content="website" />
|
||||||
|
<meta property="og:title" content="@(ViewData["Title"] ?? "백원숙 세무회계 - 세무사 전문 상담")" />
|
||||||
|
<meta property="og:description" content="@(ViewData["Description"] ?? "백원숙 세무회계 - 사업자 기장, 부동산 양도세·증여세, 종합소득세 전문 상담. 맞춤형 세무 절세 컨설팅 제공.")" />
|
||||||
|
<meta property="og:image" content="@(ViewData["OgImage"] ?? "http://178.104.200.7/taxbaik/images/og-image.jpg")" />
|
||||||
|
<meta property="og:url" content="@(ViewData["OgUrl"] ?? "http://178.104.200.7/taxbaik/")" />
|
||||||
|
|
||||||
|
<!-- Twitter -->
|
||||||
|
<meta property="twitter:card" content="summary_large_image" />
|
||||||
|
<meta property="twitter:title" content="@(ViewData["Title"] ?? "백원숙 세무회계 - 세무사 전문 상담")" />
|
||||||
|
<meta property="twitter:description" content="@(ViewData["Description"] ?? "백원숙 세무회계 - 사업자 기장, 부동산 양도세·증여세, 종합소득세 전문 상담. 맞춤형 세무 절세 컨설팅 제공.")" />
|
||||||
|
<meta property="twitter:image" content="@(ViewData["OgImage"] ?? "http://178.104.200.7/taxbaik/images/og-image.jpg")" />
|
||||||
|
|
||||||
|
<!-- 검색엔진 등록용 소유권 인증 메타 태그 (발급받으신 토큰이 있으면 아래 content에 넣어 주시면 됩니다) -->
|
||||||
|
<!-- <meta name="naver-site-verification" content="네이버_서치어드바이저_토큰_입력" /> -->
|
||||||
|
<!-- <meta name="google-site-verification" content="구글_서치콘솔_토큰_입력" /> -->
|
||||||
|
|
||||||
<meta name="robots" content="index, follow" />
|
<meta name="robots" content="index, follow" />
|
||||||
<meta name="theme-color" content="#C89D6E" />
|
<meta name="theme-color" content="#C89D6E" />
|
||||||
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
||||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
||||||
<link rel="dns-prefetch" href="https://cdn.jsdelivr.net" />
|
<link rel="dns-prefetch" href="https://cdn.jsdelivr.net" />
|
||||||
<link href="https://fonts.googleapis.com/css2?family=Noto+Sans+KR:wght@400;500;700&display=swap" rel="stylesheet" />
|
<link href="https://fonts.googleapis.com/css2?family=Noto+Sans+KR:wght@400;500;700&display=swap" rel="stylesheet" />
|
||||||
<link rel="canonical" href="@ViewData["CanonicalUrl"]" />
|
<link rel="canonical" href="@(ViewData["CanonicalUrl"] ?? "http://178.104.200.7/taxbaik/")" />
|
||||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet" />
|
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet" />
|
||||||
<link rel="stylesheet" href="~/css/site.css" asp-append-version="true" />
|
<link rel="stylesheet" href="~/css/site.css" asp-append-version="true" />
|
||||||
|
|
||||||
|
<!-- 구조화된 데이터 (JSON-LD Schema Markup) -->
|
||||||
|
<script type="application/ld+json">
|
||||||
|
{
|
||||||
|
"@@context": "https://schema.org",
|
||||||
|
"@@type": "ProfessionalService",
|
||||||
|
"name": "백원숙 세무회계",
|
||||||
|
"description": "사업자 기장, 부동산 양도세·증여세, 종합소득세 전문 상담 세무사",
|
||||||
|
"url": "http://178.104.200.7/taxbaik/",
|
||||||
|
"telephone": "010-4122-8268",
|
||||||
|
"email": "taxbaik5668@gmail.com",
|
||||||
|
"address": {
|
||||||
|
"@@type": "PostalAddress",
|
||||||
|
"addressCountry": "KR"
|
||||||
|
},
|
||||||
|
"sameAs": [
|
||||||
|
"https://www.instagram.com/taxtory5668/",
|
||||||
|
"http://pf.kakao.com/_xoxchTX"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
</script>
|
||||||
</head>
|
</head>
|
||||||
<body class="with-mobile-cta">
|
<body class="with-mobile-cta">
|
||||||
<partial name="_Header" />
|
<partial name="_Header" />
|
||||||
|
|||||||
+11
-30
@@ -52,9 +52,6 @@ builder.Services.AddControllers();
|
|||||||
builder.Services.AddProblemDetails();
|
builder.Services.AddProblemDetails();
|
||||||
builder.Services.AddHealthChecks();
|
builder.Services.AddHealthChecks();
|
||||||
|
|
||||||
// SignalR (Notifications only, no state management)
|
|
||||||
builder.Services.AddSignalR();
|
|
||||||
|
|
||||||
// Razor Pages + Blazor Server 통합
|
// Razor Pages + Blazor Server 통합
|
||||||
builder.Services.AddRazorPages();
|
builder.Services.AddRazorPages();
|
||||||
builder.Services.AddRazorComponents().AddInteractiveServerComponents();
|
builder.Services.AddRazorComponents().AddInteractiveServerComponents();
|
||||||
@@ -197,9 +194,6 @@ builder.Services.AddCascadingAuthenticationState();
|
|||||||
builder.Services.AddAuthorization();
|
builder.Services.AddAuthorization();
|
||||||
builder.Services.AddAuthorizationCore();
|
builder.Services.AddAuthorizationCore();
|
||||||
|
|
||||||
// Notifications (SignalR)
|
|
||||||
builder.Services.AddScoped<INotificationService, NotificationService>();
|
|
||||||
|
|
||||||
// Telegram Notification
|
// Telegram Notification
|
||||||
builder.Services.AddHttpClient<ITelegramNotificationService, TelegramNotificationService>();
|
builder.Services.AddHttpClient<ITelegramNotificationService, TelegramNotificationService>();
|
||||||
|
|
||||||
@@ -214,64 +208,53 @@ var apiBaseUrl = builder.Configuration["ApiClient:BaseUrl"]
|
|||||||
builder.Services.AddHttpClient<IAdminDashboardClient, AdminDashboardClient>(client =>
|
builder.Services.AddHttpClient<IAdminDashboardClient, AdminDashboardClient>(client =>
|
||||||
{
|
{
|
||||||
client.BaseAddress = new Uri(apiBaseUrl);
|
client.BaseAddress = new Uri(apiBaseUrl);
|
||||||
})
|
});
|
||||||
.AddHttpMessageHandler<TokenRefreshHandler>();
|
|
||||||
builder.Services.AddHttpClient<IInquiryBrowserClient, InquiryBrowserClient>(client =>
|
builder.Services.AddHttpClient<IInquiryBrowserClient, InquiryBrowserClient>(client =>
|
||||||
{
|
{
|
||||||
client.BaseAddress = new Uri(apiBaseUrl);
|
client.BaseAddress = new Uri(apiBaseUrl);
|
||||||
})
|
});
|
||||||
.AddHttpMessageHandler<TokenRefreshHandler>();
|
|
||||||
builder.Services.AddHttpClient<IClientBrowserClient, ClientBrowserClient>(client =>
|
builder.Services.AddHttpClient<IClientBrowserClient, ClientBrowserClient>(client =>
|
||||||
{
|
{
|
||||||
client.BaseAddress = new Uri(apiBaseUrl);
|
client.BaseAddress = new Uri(apiBaseUrl);
|
||||||
})
|
});
|
||||||
.AddHttpMessageHandler<TokenRefreshHandler>();
|
|
||||||
builder.Services.AddHttpClient<ITaxFilingBrowserClient, TaxFilingBrowserClient>(client =>
|
builder.Services.AddHttpClient<ITaxFilingBrowserClient, TaxFilingBrowserClient>(client =>
|
||||||
{
|
{
|
||||||
client.BaseAddress = new Uri(apiBaseUrl);
|
client.BaseAddress = new Uri(apiBaseUrl);
|
||||||
})
|
});
|
||||||
.AddHttpMessageHandler<TokenRefreshHandler>();
|
|
||||||
builder.Services.AddHttpClient<IFaqBrowserClient, FaqBrowserClient>(client =>
|
builder.Services.AddHttpClient<IFaqBrowserClient, FaqBrowserClient>(client =>
|
||||||
{
|
{
|
||||||
client.BaseAddress = new Uri(apiBaseUrl);
|
client.BaseAddress = new Uri(apiBaseUrl);
|
||||||
})
|
});
|
||||||
.AddHttpMessageHandler<TokenRefreshHandler>();
|
|
||||||
builder.Services.AddHttpClient<IAnnouncementBrowserClient, AnnouncementBrowserClient>(client =>
|
builder.Services.AddHttpClient<IAnnouncementBrowserClient, AnnouncementBrowserClient>(client =>
|
||||||
{
|
{
|
||||||
client.BaseAddress = new Uri(apiBaseUrl);
|
client.BaseAddress = new Uri(apiBaseUrl);
|
||||||
})
|
});
|
||||||
.AddHttpMessageHandler<TokenRefreshHandler>();
|
|
||||||
|
|
||||||
// Phase 5: Tax Accounting & CRM Browser Clients
|
// Phase 5: Tax Accounting & CRM Browser Clients
|
||||||
builder.Services.AddHttpClient<ITaxProfileBrowserClient, TaxProfileBrowserClient>(client =>
|
builder.Services.AddHttpClient<ITaxProfileBrowserClient, TaxProfileBrowserClient>(client =>
|
||||||
{
|
{
|
||||||
client.BaseAddress = new Uri(apiBaseUrl);
|
client.BaseAddress = new Uri(apiBaseUrl);
|
||||||
})
|
});
|
||||||
.AddHttpMessageHandler<TokenRefreshHandler>();
|
|
||||||
|
|
||||||
builder.Services.AddHttpClient<ITaxFilingScheduleBrowserClient, TaxFilingScheduleBrowserClient>(client =>
|
builder.Services.AddHttpClient<ITaxFilingScheduleBrowserClient, TaxFilingScheduleBrowserClient>(client =>
|
||||||
{
|
{
|
||||||
client.BaseAddress = new Uri(apiBaseUrl);
|
client.BaseAddress = new Uri(apiBaseUrl);
|
||||||
})
|
});
|
||||||
.AddHttpMessageHandler<TokenRefreshHandler>();
|
|
||||||
|
|
||||||
builder.Services.AddHttpClient<IConsultingActivityBrowserClient, ConsultingActivityBrowserClient>(client =>
|
builder.Services.AddHttpClient<IConsultingActivityBrowserClient, ConsultingActivityBrowserClient>(client =>
|
||||||
{
|
{
|
||||||
client.BaseAddress = new Uri(apiBaseUrl);
|
client.BaseAddress = new Uri(apiBaseUrl);
|
||||||
})
|
});
|
||||||
.AddHttpMessageHandler<TokenRefreshHandler>();
|
|
||||||
|
|
||||||
builder.Services.AddHttpClient<IContractBrowserClient, ContractBrowserClient>(client =>
|
builder.Services.AddHttpClient<IContractBrowserClient, ContractBrowserClient>(client =>
|
||||||
{
|
{
|
||||||
client.BaseAddress = new Uri(apiBaseUrl);
|
client.BaseAddress = new Uri(apiBaseUrl);
|
||||||
})
|
});
|
||||||
.AddHttpMessageHandler<TokenRefreshHandler>();
|
|
||||||
|
|
||||||
builder.Services.AddHttpClient<IRevenueTrackingBrowserClient, RevenueTrackingBrowserClient>(client =>
|
builder.Services.AddHttpClient<IRevenueTrackingBrowserClient, RevenueTrackingBrowserClient>(client =>
|
||||||
{
|
{
|
||||||
client.BaseAddress = new Uri(apiBaseUrl);
|
client.BaseAddress = new Uri(apiBaseUrl);
|
||||||
})
|
});
|
||||||
.AddHttpMessageHandler<TokenRefreshHandler>();
|
|
||||||
|
|
||||||
// UI & 캐시 (MudBlazor Theme Customization)
|
// UI & 캐시 (MudBlazor Theme Customization)
|
||||||
builder.Services.AddMudServices(config =>
|
builder.Services.AddMudServices(config =>
|
||||||
@@ -361,8 +344,6 @@ app.MapControllers();
|
|||||||
app.MapHealthChecks("/healthz");
|
app.MapHealthChecks("/healthz");
|
||||||
app.MapRazorPages();
|
app.MapRazorPages();
|
||||||
|
|
||||||
// SignalR Hub
|
|
||||||
app.MapHub<TaxBaik.Web.Hubs.NotificationHub>("/taxbaik/notifications");
|
|
||||||
// AllowAnonymous: JWT 미들웨어가 Blazor 셸 요청을 401로 차단하지 않도록 한다.
|
// AllowAnonymous: JWT 미들웨어가 Blazor 셸 요청을 401로 차단하지 않도록 한다.
|
||||||
// 인증은 Blazor AuthorizeRouteView → RedirectToLogin 에서 처리한다.
|
// 인증은 Blazor AuthorizeRouteView → RedirectToLogin 에서 처리한다.
|
||||||
app.MapRazorComponents<TaxBaik.Web.Components.Admin.App>()
|
app.MapRazorComponents<TaxBaik.Web.Components.Admin.App>()
|
||||||
|
|||||||
@@ -14,15 +14,24 @@ public interface IConsultingActivityBrowserClient
|
|||||||
Task DeleteAsync(int id, CancellationToken ct = default);
|
Task DeleteAsync(int id, CancellationToken ct = default);
|
||||||
}
|
}
|
||||||
|
|
||||||
public class ConsultingActivityBrowserClient(HttpClient httpClient, ILogger<ConsultingActivityBrowserClient> logger)
|
public class ConsultingActivityBrowserClient(HttpClient httpClient, ITokenStore tokenStore, ILogger<ConsultingActivityBrowserClient> logger)
|
||||||
: IConsultingActivityBrowserClient
|
: IConsultingActivityBrowserClient
|
||||||
{
|
{
|
||||||
private const string BaseUrl = "/api/consultingactivity";
|
private const string BaseUrl = "/api/consultingactivity";
|
||||||
|
|
||||||
|
private void EnsureAuthHeader()
|
||||||
|
{
|
||||||
|
if (!string.IsNullOrEmpty(tokenStore.AccessToken))
|
||||||
|
httpClient.DefaultRequestHeaders.Authorization = new("Bearer", tokenStore.AccessToken);
|
||||||
|
else
|
||||||
|
httpClient.DefaultRequestHeaders.Authorization = null;
|
||||||
|
}
|
||||||
|
|
||||||
public async Task<List<ConsultingActivity>> GetAllAsync(CancellationToken ct = default)
|
public async Task<List<ConsultingActivity>> GetAllAsync(CancellationToken ct = default)
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
|
EnsureAuthHeader();
|
||||||
return await httpClient.GetFromJsonAsync<List<ConsultingActivity>>($"{BaseUrl}", ct) ?? [];
|
return await httpClient.GetFromJsonAsync<List<ConsultingActivity>>($"{BaseUrl}", ct) ?? [];
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
@@ -36,6 +45,7 @@ public class ConsultingActivityBrowserClient(HttpClient httpClient, ILogger<Cons
|
|||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
|
EnsureAuthHeader();
|
||||||
return await httpClient.GetFromJsonAsync<List<ConsultingActivity>>($"{BaseUrl}/client/{clientId}", ct) ?? [];
|
return await httpClient.GetFromJsonAsync<List<ConsultingActivity>>($"{BaseUrl}/client/{clientId}", ct) ?? [];
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
@@ -49,6 +59,7 @@ public class ConsultingActivityBrowserClient(HttpClient httpClient, ILogger<Cons
|
|||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
|
EnsureAuthHeader();
|
||||||
var response = await httpClient.GetFromJsonAsync<JsonElement>($"{BaseUrl}/pending-followups", ct);
|
var response = await httpClient.GetFromJsonAsync<JsonElement>($"{BaseUrl}/pending-followups", ct);
|
||||||
if (response.TryGetProperty("data", out var data))
|
if (response.TryGetProperty("data", out var data))
|
||||||
return System.Text.Json.JsonSerializer.Deserialize<List<ConsultingActivity>>(data.GetRawText()) ?? [];
|
return System.Text.Json.JsonSerializer.Deserialize<List<ConsultingActivity>>(data.GetRawText()) ?? [];
|
||||||
@@ -66,6 +77,7 @@ public class ConsultingActivityBrowserClient(HttpClient httpClient, ILogger<Cons
|
|||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
|
EnsureAuthHeader();
|
||||||
var request = new { clientId, activityType, activityDate, description, consultantId, nextFollowupDate };
|
var request = new { clientId, activityType, activityDate, description, consultantId, nextFollowupDate };
|
||||||
var response = await httpClient.PostAsJsonAsync(BaseUrl, request, ct);
|
var response = await httpClient.PostAsJsonAsync(BaseUrl, request, ct);
|
||||||
response.EnsureSuccessStatusCode();
|
response.EnsureSuccessStatusCode();
|
||||||
@@ -83,6 +95,7 @@ public class ConsultingActivityBrowserClient(HttpClient httpClient, ILogger<Cons
|
|||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
|
EnsureAuthHeader();
|
||||||
var request = new { outcome, nextFollowupDate };
|
var request = new { outcome, nextFollowupDate };
|
||||||
var response = await httpClient.PutAsJsonAsync($"{BaseUrl}/{id}", request, ct);
|
var response = await httpClient.PutAsJsonAsync($"{BaseUrl}/{id}", request, ct);
|
||||||
response.EnsureSuccessStatusCode();
|
response.EnsureSuccessStatusCode();
|
||||||
@@ -97,6 +110,7 @@ public class ConsultingActivityBrowserClient(HttpClient httpClient, ILogger<Cons
|
|||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
|
EnsureAuthHeader();
|
||||||
var response = await httpClient.DeleteAsync($"{BaseUrl}/{id}", ct);
|
var response = await httpClient.DeleteAsync($"{BaseUrl}/{id}", ct);
|
||||||
response.EnsureSuccessStatusCode();
|
response.EnsureSuccessStatusCode();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,15 +16,24 @@ public interface IContractBrowserClient
|
|||||||
Task DeleteAsync(int id, CancellationToken ct = default);
|
Task DeleteAsync(int id, CancellationToken ct = default);
|
||||||
}
|
}
|
||||||
|
|
||||||
public class ContractBrowserClient(HttpClient httpClient, ILogger<ContractBrowserClient> logger)
|
public class ContractBrowserClient(HttpClient httpClient, ITokenStore tokenStore, ILogger<ContractBrowserClient> logger)
|
||||||
: IContractBrowserClient
|
: IContractBrowserClient
|
||||||
{
|
{
|
||||||
private const string BaseUrl = "/api/contract";
|
private const string BaseUrl = "/api/contract";
|
||||||
|
|
||||||
|
private void EnsureAuthHeader()
|
||||||
|
{
|
||||||
|
if (!string.IsNullOrEmpty(tokenStore.AccessToken))
|
||||||
|
httpClient.DefaultRequestHeaders.Authorization = new("Bearer", tokenStore.AccessToken);
|
||||||
|
else
|
||||||
|
httpClient.DefaultRequestHeaders.Authorization = null;
|
||||||
|
}
|
||||||
|
|
||||||
public async Task<List<Contract>> GetAllAsync(CancellationToken ct = default)
|
public async Task<List<Contract>> GetAllAsync(CancellationToken ct = default)
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
|
EnsureAuthHeader();
|
||||||
return await httpClient.GetFromJsonAsync<List<Contract>>($"{BaseUrl}", ct) ?? [];
|
return await httpClient.GetFromJsonAsync<List<Contract>>($"{BaseUrl}", ct) ?? [];
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
@@ -38,6 +47,7 @@ public class ContractBrowserClient(HttpClient httpClient, ILogger<ContractBrowse
|
|||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
|
EnsureAuthHeader();
|
||||||
return await httpClient.GetFromJsonAsync<Contract>($"{BaseUrl}/{id}", ct);
|
return await httpClient.GetFromJsonAsync<Contract>($"{BaseUrl}/{id}", ct);
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
@@ -51,6 +61,7 @@ public class ContractBrowserClient(HttpClient httpClient, ILogger<ContractBrowse
|
|||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
|
EnsureAuthHeader();
|
||||||
return await httpClient.GetFromJsonAsync<List<Contract>>($"{BaseUrl}/client/{clientId}", ct) ?? [];
|
return await httpClient.GetFromJsonAsync<List<Contract>>($"{BaseUrl}/client/{clientId}", ct) ?? [];
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
@@ -64,6 +75,7 @@ public class ContractBrowserClient(HttpClient httpClient, ILogger<ContractBrowse
|
|||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
|
EnsureAuthHeader();
|
||||||
var response = await httpClient.GetFromJsonAsync<JsonElement>($"{BaseUrl}/active", ct);
|
var response = await httpClient.GetFromJsonAsync<JsonElement>($"{BaseUrl}/active", ct);
|
||||||
if (response.TryGetProperty("data", out var data))
|
if (response.TryGetProperty("data", out var data))
|
||||||
return System.Text.Json.JsonSerializer.Deserialize<List<Contract>>(data.GetRawText()) ?? [];
|
return System.Text.Json.JsonSerializer.Deserialize<List<Contract>>(data.GetRawText()) ?? [];
|
||||||
@@ -80,6 +92,7 @@ public class ContractBrowserClient(HttpClient httpClient, ILogger<ContractBrowse
|
|||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
|
EnsureAuthHeader();
|
||||||
var response = await httpClient.GetFromJsonAsync<JsonElement>($"{BaseUrl}/expiring?daysAhead={daysAhead}", ct);
|
var response = await httpClient.GetFromJsonAsync<JsonElement>($"{BaseUrl}/expiring?daysAhead={daysAhead}", ct);
|
||||||
if (response.TryGetProperty("data", out var data))
|
if (response.TryGetProperty("data", out var data))
|
||||||
return System.Text.Json.JsonSerializer.Deserialize<List<Contract>>(data.GetRawText()) ?? [];
|
return System.Text.Json.JsonSerializer.Deserialize<List<Contract>>(data.GetRawText()) ?? [];
|
||||||
@@ -96,6 +109,7 @@ public class ContractBrowserClient(HttpClient httpClient, ILogger<ContractBrowse
|
|||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
|
EnsureAuthHeader();
|
||||||
var response = await httpClient.GetFromJsonAsync<JsonElement>($"{BaseUrl}/mrr", ct);
|
var response = await httpClient.GetFromJsonAsync<JsonElement>($"{BaseUrl}/mrr", ct);
|
||||||
if (response.TryGetProperty("mrr", out var mrrValue))
|
if (response.TryGetProperty("mrr", out var mrrValue))
|
||||||
return System.Text.Json.JsonSerializer.Deserialize<decimal>(mrrValue.GetRawText());
|
return System.Text.Json.JsonSerializer.Deserialize<decimal>(mrrValue.GetRawText());
|
||||||
@@ -113,6 +127,7 @@ public class ContractBrowserClient(HttpClient httpClient, ILogger<ContractBrowse
|
|||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
|
EnsureAuthHeader();
|
||||||
var request = new { clientId, contractNumber, serviceType, startDate, monthlyFee, totalAmount };
|
var request = new { clientId, contractNumber, serviceType, startDate, monthlyFee, totalAmount };
|
||||||
var response = await httpClient.PostAsJsonAsync(BaseUrl, request, ct);
|
var response = await httpClient.PostAsJsonAsync(BaseUrl, request, ct);
|
||||||
response.EnsureSuccessStatusCode();
|
response.EnsureSuccessStatusCode();
|
||||||
@@ -130,6 +145,7 @@ public class ContractBrowserClient(HttpClient httpClient, ILogger<ContractBrowse
|
|||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
|
EnsureAuthHeader();
|
||||||
var response = await httpClient.DeleteAsync($"{BaseUrl}/{id}", ct);
|
var response = await httpClient.DeleteAsync($"{BaseUrl}/{id}", ct);
|
||||||
response.EnsureSuccessStatusCode();
|
response.EnsureSuccessStatusCode();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,15 +16,24 @@ public interface IRevenueTrackingBrowserClient
|
|||||||
Task DeleteAsync(int id, CancellationToken ct = default);
|
Task DeleteAsync(int id, CancellationToken ct = default);
|
||||||
}
|
}
|
||||||
|
|
||||||
public class RevenueTrackingBrowserClient(HttpClient httpClient, ILogger<RevenueTrackingBrowserClient> logger)
|
public class RevenueTrackingBrowserClient(HttpClient httpClient, ITokenStore tokenStore, ILogger<RevenueTrackingBrowserClient> logger)
|
||||||
: IRevenueTrackingBrowserClient
|
: IRevenueTrackingBrowserClient
|
||||||
{
|
{
|
||||||
private const string BaseUrl = "/api/revenuetracking";
|
private const string BaseUrl = "/api/revenuetracking";
|
||||||
|
|
||||||
|
private void EnsureAuthHeader()
|
||||||
|
{
|
||||||
|
if (!string.IsNullOrEmpty(tokenStore.AccessToken))
|
||||||
|
httpClient.DefaultRequestHeaders.Authorization = new("Bearer", tokenStore.AccessToken);
|
||||||
|
else
|
||||||
|
httpClient.DefaultRequestHeaders.Authorization = null;
|
||||||
|
}
|
||||||
|
|
||||||
public async Task<List<RevenueTracking>> GetAllAsync(CancellationToken ct = default)
|
public async Task<List<RevenueTracking>> GetAllAsync(CancellationToken ct = default)
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
|
EnsureAuthHeader();
|
||||||
return await httpClient.GetFromJsonAsync<List<RevenueTracking>>($"{BaseUrl}", ct) ?? [];
|
return await httpClient.GetFromJsonAsync<List<RevenueTracking>>($"{BaseUrl}", ct) ?? [];
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
@@ -38,6 +47,7 @@ public class RevenueTrackingBrowserClient(HttpClient httpClient, ILogger<Revenue
|
|||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
|
EnsureAuthHeader();
|
||||||
return await httpClient.GetFromJsonAsync<List<RevenueTracking>>($"{BaseUrl}/client/{clientId}", ct) ?? [];
|
return await httpClient.GetFromJsonAsync<List<RevenueTracking>>($"{BaseUrl}/client/{clientId}", ct) ?? [];
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
@@ -51,6 +61,7 @@ public class RevenueTrackingBrowserClient(HttpClient httpClient, ILogger<Revenue
|
|||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
|
EnsureAuthHeader();
|
||||||
var response = await httpClient.GetFromJsonAsync<JsonElement>($"{BaseUrl}/pending", ct);
|
var response = await httpClient.GetFromJsonAsync<JsonElement>($"{BaseUrl}/pending", ct);
|
||||||
if (response.TryGetProperty("data", out var data))
|
if (response.TryGetProperty("data", out var data))
|
||||||
return System.Text.Json.JsonSerializer.Deserialize<List<RevenueTracking>>(data.GetRawText()) ?? [];
|
return System.Text.Json.JsonSerializer.Deserialize<List<RevenueTracking>>(data.GetRawText()) ?? [];
|
||||||
@@ -67,6 +78,7 @@ public class RevenueTrackingBrowserClient(HttpClient httpClient, ILogger<Revenue
|
|||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
|
EnsureAuthHeader();
|
||||||
var response = await httpClient.GetFromJsonAsync<JsonElement>($"{BaseUrl}/monthly?year={year}&month={month}", ct);
|
var response = await httpClient.GetFromJsonAsync<JsonElement>($"{BaseUrl}/monthly?year={year}&month={month}", ct);
|
||||||
if (response.TryGetProperty("data", out var data))
|
if (response.TryGetProperty("data", out var data))
|
||||||
return System.Text.Json.JsonSerializer.Deserialize<List<RevenueTracking>>(data.GetRawText()) ?? [];
|
return System.Text.Json.JsonSerializer.Deserialize<List<RevenueTracking>>(data.GetRawText()) ?? [];
|
||||||
@@ -83,6 +95,7 @@ public class RevenueTrackingBrowserClient(HttpClient httpClient, ILogger<Revenue
|
|||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
|
EnsureAuthHeader();
|
||||||
var response = await httpClient.GetFromJsonAsync<JsonElement>(
|
var response = await httpClient.GetFromJsonAsync<JsonElement>(
|
||||||
$"{BaseUrl}/total?startDate={startDate:yyyy-MM-dd}&endDate={endDate:yyyy-MM-dd}", ct);
|
$"{BaseUrl}/total?startDate={startDate:yyyy-MM-dd}&endDate={endDate:yyyy-MM-dd}", ct);
|
||||||
if (response.TryGetProperty("total", out var totalValue))
|
if (response.TryGetProperty("total", out var totalValue))
|
||||||
@@ -101,6 +114,7 @@ public class RevenueTrackingBrowserClient(HttpClient httpClient, ILogger<Revenue
|
|||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
|
EnsureAuthHeader();
|
||||||
var request = new { clientId, invoiceNumber, invoiceDate, amount, serviceType, dueDate };
|
var request = new { clientId, invoiceNumber, invoiceDate, amount, serviceType, dueDate };
|
||||||
var response = await httpClient.PostAsJsonAsync(BaseUrl, request, ct);
|
var response = await httpClient.PostAsJsonAsync(BaseUrl, request, ct);
|
||||||
response.EnsureSuccessStatusCode();
|
response.EnsureSuccessStatusCode();
|
||||||
@@ -118,6 +132,7 @@ public class RevenueTrackingBrowserClient(HttpClient httpClient, ILogger<Revenue
|
|||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
|
EnsureAuthHeader();
|
||||||
var request = new { paymentDate };
|
var request = new { paymentDate };
|
||||||
var response = await httpClient.PutAsJsonAsync($"{BaseUrl}/{id}/paid", request, ct);
|
var response = await httpClient.PutAsJsonAsync($"{BaseUrl}/{id}/paid", request, ct);
|
||||||
response.EnsureSuccessStatusCode();
|
response.EnsureSuccessStatusCode();
|
||||||
@@ -132,6 +147,7 @@ public class RevenueTrackingBrowserClient(HttpClient httpClient, ILogger<Revenue
|
|||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
|
EnsureAuthHeader();
|
||||||
var response = await httpClient.DeleteAsync($"{BaseUrl}/{id}", ct);
|
var response = await httpClient.DeleteAsync($"{BaseUrl}/{id}", ct);
|
||||||
response.EnsureSuccessStatusCode();
|
response.EnsureSuccessStatusCode();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,15 +15,24 @@ public interface ITaxFilingScheduleBrowserClient
|
|||||||
Task DeleteAsync(int id, CancellationToken ct = default);
|
Task DeleteAsync(int id, CancellationToken ct = default);
|
||||||
}
|
}
|
||||||
|
|
||||||
public class TaxFilingScheduleBrowserClient(HttpClient httpClient, ILogger<TaxFilingScheduleBrowserClient> logger)
|
public class TaxFilingScheduleBrowserClient(HttpClient httpClient, ITokenStore tokenStore, ILogger<TaxFilingScheduleBrowserClient> logger)
|
||||||
: ITaxFilingScheduleBrowserClient
|
: ITaxFilingScheduleBrowserClient
|
||||||
{
|
{
|
||||||
private const string BaseUrl = "/api/taxfilingschedule";
|
private const string BaseUrl = "/api/taxfilingschedule";
|
||||||
|
|
||||||
|
private void EnsureAuthHeader()
|
||||||
|
{
|
||||||
|
if (!string.IsNullOrEmpty(tokenStore.AccessToken))
|
||||||
|
httpClient.DefaultRequestHeaders.Authorization = new("Bearer", tokenStore.AccessToken);
|
||||||
|
else
|
||||||
|
httpClient.DefaultRequestHeaders.Authorization = null;
|
||||||
|
}
|
||||||
|
|
||||||
public async Task<List<TaxFilingSchedule>> GetAllAsync(CancellationToken ct = default)
|
public async Task<List<TaxFilingSchedule>> GetAllAsync(CancellationToken ct = default)
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
|
EnsureAuthHeader();
|
||||||
return await httpClient.GetFromJsonAsync<List<TaxFilingSchedule>>($"{BaseUrl}", ct) ?? [];
|
return await httpClient.GetFromJsonAsync<List<TaxFilingSchedule>>($"{BaseUrl}", ct) ?? [];
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
@@ -37,6 +46,7 @@ public class TaxFilingScheduleBrowserClient(HttpClient httpClient, ILogger<TaxFi
|
|||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
|
EnsureAuthHeader();
|
||||||
return await httpClient.GetFromJsonAsync<TaxFilingSchedule>($"{BaseUrl}/{id}", ct);
|
return await httpClient.GetFromJsonAsync<TaxFilingSchedule>($"{BaseUrl}/{id}", ct);
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
@@ -50,6 +60,7 @@ public class TaxFilingScheduleBrowserClient(HttpClient httpClient, ILogger<TaxFi
|
|||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
|
EnsureAuthHeader();
|
||||||
return await httpClient.GetFromJsonAsync<List<TaxFilingSchedule>>($"{BaseUrl}/client/{clientId}", ct) ?? [];
|
return await httpClient.GetFromJsonAsync<List<TaxFilingSchedule>>($"{BaseUrl}/client/{clientId}", ct) ?? [];
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
@@ -63,6 +74,7 @@ public class TaxFilingScheduleBrowserClient(HttpClient httpClient, ILogger<TaxFi
|
|||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
|
EnsureAuthHeader();
|
||||||
var response = await httpClient.GetFromJsonAsync<JsonElement>($"{BaseUrl}/upcoming?daysAhead={daysAhead}", ct);
|
var response = await httpClient.GetFromJsonAsync<JsonElement>($"{BaseUrl}/upcoming?daysAhead={daysAhead}", ct);
|
||||||
if (response.TryGetProperty("data", out var data))
|
if (response.TryGetProperty("data", out var data))
|
||||||
return System.Text.Json.JsonSerializer.Deserialize<List<TaxFilingSchedule>>(data.GetRawText()) ?? [];
|
return System.Text.Json.JsonSerializer.Deserialize<List<TaxFilingSchedule>>(data.GetRawText()) ?? [];
|
||||||
@@ -80,6 +92,7 @@ public class TaxFilingScheduleBrowserClient(HttpClient httpClient, ILogger<TaxFi
|
|||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
|
EnsureAuthHeader();
|
||||||
var request = new { clientId, filingType, dueDate, filingYear, assignedTo };
|
var request = new { clientId, filingType, dueDate, filingYear, assignedTo };
|
||||||
var response = await httpClient.PostAsJsonAsync(BaseUrl, request, ct);
|
var response = await httpClient.PostAsJsonAsync(BaseUrl, request, ct);
|
||||||
response.EnsureSuccessStatusCode();
|
response.EnsureSuccessStatusCode();
|
||||||
@@ -97,6 +110,7 @@ public class TaxFilingScheduleBrowserClient(HttpClient httpClient, ILogger<TaxFi
|
|||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
|
EnsureAuthHeader();
|
||||||
var response = await httpClient.PutAsJsonAsync($"{BaseUrl}/{id}/complete", new { }, ct);
|
var response = await httpClient.PutAsJsonAsync($"{BaseUrl}/{id}/complete", new { }, ct);
|
||||||
response.EnsureSuccessStatusCode();
|
response.EnsureSuccessStatusCode();
|
||||||
}
|
}
|
||||||
@@ -110,6 +124,7 @@ public class TaxFilingScheduleBrowserClient(HttpClient httpClient, ILogger<TaxFi
|
|||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
|
EnsureAuthHeader();
|
||||||
var response = await httpClient.DeleteAsync($"{BaseUrl}/{id}", ct);
|
var response = await httpClient.DeleteAsync($"{BaseUrl}/{id}", ct);
|
||||||
response.EnsureSuccessStatusCode();
|
response.EnsureSuccessStatusCode();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,14 +17,23 @@ public interface ITaxProfileBrowserClient
|
|||||||
Task DeleteAsync(int id, CancellationToken ct = default);
|
Task DeleteAsync(int id, CancellationToken ct = default);
|
||||||
}
|
}
|
||||||
|
|
||||||
public class TaxProfileBrowserClient(HttpClient httpClient, ILogger<TaxProfileBrowserClient> logger) : ITaxProfileBrowserClient
|
public class TaxProfileBrowserClient(HttpClient httpClient, ITokenStore tokenStore, ILogger<TaxProfileBrowserClient> logger) : ITaxProfileBrowserClient
|
||||||
{
|
{
|
||||||
private const string BaseUrl = "/api/taxprofile";
|
private const string BaseUrl = "/api/taxprofile";
|
||||||
|
|
||||||
|
private void EnsureAuthHeader()
|
||||||
|
{
|
||||||
|
if (!string.IsNullOrEmpty(tokenStore.AccessToken))
|
||||||
|
httpClient.DefaultRequestHeaders.Authorization = new("Bearer", tokenStore.AccessToken);
|
||||||
|
else
|
||||||
|
httpClient.DefaultRequestHeaders.Authorization = null;
|
||||||
|
}
|
||||||
|
|
||||||
public async Task<List<TaxProfile>> GetAllAsync(CancellationToken ct = default)
|
public async Task<List<TaxProfile>> GetAllAsync(CancellationToken ct = default)
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
|
EnsureAuthHeader();
|
||||||
return await httpClient.GetFromJsonAsync<List<TaxProfile>>($"{BaseUrl}", ct) ?? [];
|
return await httpClient.GetFromJsonAsync<List<TaxProfile>>($"{BaseUrl}", ct) ?? [];
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
@@ -38,6 +47,7 @@ public class TaxProfileBrowserClient(HttpClient httpClient, ILogger<TaxProfileBr
|
|||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
|
EnsureAuthHeader();
|
||||||
return await httpClient.GetFromJsonAsync<TaxProfile>($"{BaseUrl}/{id}", ct);
|
return await httpClient.GetFromJsonAsync<TaxProfile>($"{BaseUrl}/{id}", ct);
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
@@ -51,6 +61,7 @@ public class TaxProfileBrowserClient(HttpClient httpClient, ILogger<TaxProfileBr
|
|||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
|
EnsureAuthHeader();
|
||||||
return await httpClient.GetFromJsonAsync<List<TaxProfile>>($"{BaseUrl}/client/{clientId}", ct) ?? [];
|
return await httpClient.GetFromJsonAsync<List<TaxProfile>>($"{BaseUrl}/client/{clientId}", ct) ?? [];
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
@@ -64,6 +75,7 @@ public class TaxProfileBrowserClient(HttpClient httpClient, ILogger<TaxProfileBr
|
|||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
|
EnsureAuthHeader();
|
||||||
var response = await httpClient.GetFromJsonAsync<JsonElement>($"{BaseUrl}/high-risk", ct);
|
var response = await httpClient.GetFromJsonAsync<JsonElement>($"{BaseUrl}/high-risk", ct);
|
||||||
if (response.TryGetProperty("data", out var data))
|
if (response.TryGetProperty("data", out var data))
|
||||||
return System.Text.Json.JsonSerializer.Deserialize<List<TaxProfile>>(data.GetRawText()) ?? [];
|
return System.Text.Json.JsonSerializer.Deserialize<List<TaxProfile>>(data.GetRawText()) ?? [];
|
||||||
@@ -80,6 +92,7 @@ public class TaxProfileBrowserClient(HttpClient httpClient, ILogger<TaxProfileBr
|
|||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
|
EnsureAuthHeader();
|
||||||
var response = await httpClient.GetFromJsonAsync<JsonElement>($"{BaseUrl}/upcoming-filings?daysAhead={daysAhead}", ct);
|
var response = await httpClient.GetFromJsonAsync<JsonElement>($"{BaseUrl}/upcoming-filings?daysAhead={daysAhead}", ct);
|
||||||
if (response.TryGetProperty("data", out var data))
|
if (response.TryGetProperty("data", out var data))
|
||||||
return System.Text.Json.JsonSerializer.Deserialize<List<TaxProfile>>(data.GetRawText()) ?? [];
|
return System.Text.Json.JsonSerializer.Deserialize<List<TaxProfile>>(data.GetRawText()) ?? [];
|
||||||
@@ -97,6 +110,7 @@ public class TaxProfileBrowserClient(HttpClient httpClient, ILogger<TaxProfileBr
|
|||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
|
EnsureAuthHeader();
|
||||||
var request = new { clientId, businessType, businessRegistration, accountingMethod, establishmentDate };
|
var request = new { clientId, businessType, businessRegistration, accountingMethod, establishmentDate };
|
||||||
var response = await httpClient.PostAsJsonAsync(BaseUrl, request, ct);
|
var response = await httpClient.PostAsJsonAsync(BaseUrl, request, ct);
|
||||||
response.EnsureSuccessStatusCode();
|
response.EnsureSuccessStatusCode();
|
||||||
@@ -115,6 +129,7 @@ public class TaxProfileBrowserClient(HttpClient httpClient, ILogger<TaxProfileBr
|
|||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
|
EnsureAuthHeader();
|
||||||
var request = new { businessType, accountingMethod, nextFilingDueDate, taxRiskLevel };
|
var request = new { businessType, accountingMethod, nextFilingDueDate, taxRiskLevel };
|
||||||
var response = await httpClient.PutAsJsonAsync($"{BaseUrl}/{id}", request, ct);
|
var response = await httpClient.PutAsJsonAsync($"{BaseUrl}/{id}", request, ct);
|
||||||
response.EnsureSuccessStatusCode();
|
response.EnsureSuccessStatusCode();
|
||||||
@@ -129,6 +144,7 @@ public class TaxProfileBrowserClient(HttpClient httpClient, ILogger<TaxProfileBr
|
|||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
|
EnsureAuthHeader();
|
||||||
var response = await httpClient.DeleteAsync($"{BaseUrl}/{id}", ct);
|
var response = await httpClient.DeleteAsync($"{BaseUrl}/{id}", ct);
|
||||||
response.EnsureSuccessStatusCode();
|
response.EnsureSuccessStatusCode();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -33,10 +33,10 @@ public class AdminDashboardClient : IAdminDashboardClient
|
|||||||
|
|
||||||
private void EnsureAuthHeader()
|
private void EnsureAuthHeader()
|
||||||
{
|
{
|
||||||
if (!string.IsNullOrEmpty(_tokenStore.AccessToken) && !_http.DefaultRequestHeaders.Contains("Authorization"))
|
if (!string.IsNullOrEmpty(_tokenStore.AccessToken))
|
||||||
{
|
|
||||||
_http.DefaultRequestHeaders.Authorization = new("Bearer", _tokenStore.AccessToken);
|
_http.DefaultRequestHeaders.Authorization = new("Bearer", _tokenStore.AccessToken);
|
||||||
}
|
else
|
||||||
|
_http.DefaultRequestHeaders.Authorization = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<AdminDashboardSummary> GetSummaryAsync(CancellationToken ct = default)
|
public async Task<AdminDashboardSummary> GetSummaryAsync(CancellationToken ct = default)
|
||||||
|
|||||||
@@ -29,10 +29,10 @@ public class AnnouncementBrowserClient : IAnnouncementBrowserClient
|
|||||||
|
|
||||||
private void EnsureAuthHeader()
|
private void EnsureAuthHeader()
|
||||||
{
|
{
|
||||||
if (!string.IsNullOrEmpty(_tokenStore.AccessToken) && !_http.DefaultRequestHeaders.Contains("Authorization"))
|
if (!string.IsNullOrEmpty(_tokenStore.AccessToken))
|
||||||
{
|
|
||||||
_http.DefaultRequestHeaders.Authorization = new("Bearer", _tokenStore.AccessToken);
|
_http.DefaultRequestHeaders.Authorization = new("Bearer", _tokenStore.AccessToken);
|
||||||
}
|
else
|
||||||
|
_http.DefaultRequestHeaders.Authorization = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<IEnumerable<Announcement>> GetAllAsync(CancellationToken ct = default)
|
public async Task<IEnumerable<Announcement>> GetAllAsync(CancellationToken ct = default)
|
||||||
|
|||||||
@@ -34,10 +34,10 @@ public class ClientBrowserClient : IClientBrowserClient
|
|||||||
|
|
||||||
private void EnsureAuthHeader()
|
private void EnsureAuthHeader()
|
||||||
{
|
{
|
||||||
if (!string.IsNullOrEmpty(_tokenStore.AccessToken) && !_http.DefaultRequestHeaders.Contains("Authorization"))
|
if (!string.IsNullOrEmpty(_tokenStore.AccessToken))
|
||||||
{
|
|
||||||
_http.DefaultRequestHeaders.Authorization = new("Bearer", _tokenStore.AccessToken);
|
_http.DefaultRequestHeaders.Authorization = new("Bearer", _tokenStore.AccessToken);
|
||||||
}
|
else
|
||||||
|
_http.DefaultRequestHeaders.Authorization = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<(IEnumerable<Client> Items, int Total)> GetPagedAsync(
|
public async Task<(IEnumerable<Client> Items, int Total)> GetPagedAsync(
|
||||||
|
|||||||
@@ -32,21 +32,22 @@ public class CustomAuthenticationStateProvider : AuthenticationStateProvider
|
|||||||
// TokenStore가 비어있으면 localStorage에서 복원 (페이지 리로드 후)
|
// TokenStore가 비어있으면 localStorage에서 복원 (페이지 리로드 후)
|
||||||
if (string.IsNullOrEmpty(accessToken))
|
if (string.IsNullOrEmpty(accessToken))
|
||||||
{
|
{
|
||||||
accessToken = await _localStorage.GetItemAsStringAsync("accessToken");
|
var storedToken = await _localStorage.GetItemAsStringAsync("accessToken");
|
||||||
if (!string.IsNullOrEmpty(accessToken))
|
if (!string.IsNullOrEmpty(storedToken))
|
||||||
{
|
{
|
||||||
var refreshToken = await _localStorage.GetItemAsStringAsync("refreshToken");
|
var refreshToken = await _localStorage.GetItemAsStringAsync("refreshToken");
|
||||||
var ticksStr = await _localStorage.GetItemAsStringAsync("tokenExpiry");
|
var ticksStr = await _localStorage.GetItemAsStringAsync("tokenExpiry");
|
||||||
if (long.TryParse(ticksStr, out var ticks))
|
if (long.TryParse(ticksStr, out var ticks))
|
||||||
{
|
{
|
||||||
_tokenStore.AccessToken = accessToken;
|
_tokenStore.AccessToken = storedToken;
|
||||||
_tokenStore.RefreshToken = refreshToken;
|
_tokenStore.RefreshToken = refreshToken;
|
||||||
_tokenStore.TokenExpiryTicks = ticks;
|
_tokenStore.TokenExpiryTicks = ticks;
|
||||||
|
accessToken = storedToken;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (string.IsNullOrEmpty(accessToken))
|
if (string.IsNullOrEmpty(_tokenStore.AccessToken))
|
||||||
{
|
{
|
||||||
return new AuthenticationState(new ClaimsPrincipal(new ClaimsIdentity()));
|
return new AuthenticationState(new ClaimsPrincipal(new ClaimsIdentity()));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -28,10 +28,10 @@ public class FaqBrowserClient : IFaqBrowserClient
|
|||||||
|
|
||||||
private void EnsureAuthHeader()
|
private void EnsureAuthHeader()
|
||||||
{
|
{
|
||||||
if (!string.IsNullOrEmpty(_tokenStore.AccessToken) && !_http.DefaultRequestHeaders.Contains("Authorization"))
|
if (!string.IsNullOrEmpty(_tokenStore.AccessToken))
|
||||||
{
|
|
||||||
_http.DefaultRequestHeaders.Authorization = new("Bearer", _tokenStore.AccessToken);
|
_http.DefaultRequestHeaders.Authorization = new("Bearer", _tokenStore.AccessToken);
|
||||||
}
|
else
|
||||||
|
_http.DefaultRequestHeaders.Authorization = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<IEnumerable<Faq>> GetAllAsync(CancellationToken ct = default)
|
public async Task<IEnumerable<Faq>> GetAllAsync(CancellationToken ct = default)
|
||||||
|
|||||||
@@ -33,10 +33,10 @@ public class InquiryBrowserClient : IInquiryBrowserClient
|
|||||||
|
|
||||||
private void EnsureAuthHeader()
|
private void EnsureAuthHeader()
|
||||||
{
|
{
|
||||||
if (!string.IsNullOrEmpty(_tokenStore.AccessToken) && !_http.DefaultRequestHeaders.Contains("Authorization"))
|
if (!string.IsNullOrEmpty(_tokenStore.AccessToken))
|
||||||
{
|
|
||||||
_http.DefaultRequestHeaders.Authorization = new("Bearer", _tokenStore.AccessToken);
|
_http.DefaultRequestHeaders.Authorization = new("Bearer", _tokenStore.AccessToken);
|
||||||
}
|
else
|
||||||
|
_http.DefaultRequestHeaders.Authorization = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<(IEnumerable<Inquiry> Items, int Total)> GetPagedAsync(
|
public async Task<(IEnumerable<Inquiry> Items, int Total)> GetPagedAsync(
|
||||||
|
|||||||
@@ -1,72 +0,0 @@
|
|||||||
namespace TaxBaik.Web.Services;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Notification service for real-time admin updates
|
|
||||||
/// SOLID: Single Responsibility - Event notification only
|
|
||||||
/// Uses Blazor Server's built-in SignalR for real-time communication
|
|
||||||
/// </summary>
|
|
||||||
public interface INotificationService
|
|
||||||
{
|
|
||||||
event Func<int, string, Task>? OnInquiryStatusChanged;
|
|
||||||
event Func<int, string, Task>? OnInquiryCreated;
|
|
||||||
event Func<int, string, Task>? OnClientCreated;
|
|
||||||
event Func<int, string, Task>? OnAnnouncementPublished;
|
|
||||||
event Func<int, string, Task>? OnFilingCompleted;
|
|
||||||
|
|
||||||
Task TriggerInquiryStatusChanged(int inquiryId, string status);
|
|
||||||
Task TriggerInquiryCreated(int inquiryId, string name);
|
|
||||||
Task TriggerClientCreated(int clientId, string name);
|
|
||||||
Task TriggerAnnouncementPublished(int announcementId, string title);
|
|
||||||
Task TriggerFilingCompleted(int filingId, string filingType);
|
|
||||||
}
|
|
||||||
|
|
||||||
public class NotificationService : INotificationService
|
|
||||||
{
|
|
||||||
private readonly ILogger<NotificationService> _logger;
|
|
||||||
|
|
||||||
public NotificationService(ILogger<NotificationService> logger)
|
|
||||||
{
|
|
||||||
_logger = logger;
|
|
||||||
}
|
|
||||||
|
|
||||||
public event Func<int, string, Task>? OnInquiryStatusChanged;
|
|
||||||
public event Func<int, string, Task>? OnInquiryCreated;
|
|
||||||
public event Func<int, string, Task>? OnClientCreated;
|
|
||||||
public event Func<int, string, Task>? OnAnnouncementPublished;
|
|
||||||
public event Func<int, string, Task>? OnFilingCompleted;
|
|
||||||
|
|
||||||
public async Task TriggerInquiryStatusChanged(int inquiryId, string status)
|
|
||||||
{
|
|
||||||
_logger.LogInformation($"Inquiry {inquiryId} status changed to {status}");
|
|
||||||
if (OnInquiryStatusChanged != null)
|
|
||||||
await OnInquiryStatusChanged(inquiryId, status);
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task TriggerInquiryCreated(int inquiryId, string name)
|
|
||||||
{
|
|
||||||
_logger.LogInformation($"New inquiry {inquiryId} from {name}");
|
|
||||||
if (OnInquiryCreated != null)
|
|
||||||
await OnInquiryCreated(inquiryId, name);
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task TriggerClientCreated(int clientId, string name)
|
|
||||||
{
|
|
||||||
_logger.LogInformation($"New client {clientId}: {name}");
|
|
||||||
if (OnClientCreated != null)
|
|
||||||
await OnClientCreated(clientId, name);
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task TriggerAnnouncementPublished(int announcementId, string title)
|
|
||||||
{
|
|
||||||
_logger.LogInformation($"Announcement {announcementId} published: {title}");
|
|
||||||
if (OnAnnouncementPublished != null)
|
|
||||||
await OnAnnouncementPublished(announcementId, title);
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task TriggerFilingCompleted(int filingId, string filingType)
|
|
||||||
{
|
|
||||||
_logger.LogInformation($"Filing {filingId} ({filingType}) completed");
|
|
||||||
if (OnFilingCompleted != null)
|
|
||||||
await OnFilingCompleted(filingId, filingType);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -32,10 +32,10 @@ public class TaxFilingBrowserClient : ITaxFilingBrowserClient
|
|||||||
|
|
||||||
private void EnsureAuthHeader()
|
private void EnsureAuthHeader()
|
||||||
{
|
{
|
||||||
if (!string.IsNullOrEmpty(_tokenStore.AccessToken) && !_http.DefaultRequestHeaders.Contains("Authorization"))
|
if (!string.IsNullOrEmpty(_tokenStore.AccessToken))
|
||||||
{
|
|
||||||
_http.DefaultRequestHeaders.Authorization = new("Bearer", _tokenStore.AccessToken);
|
_http.DefaultRequestHeaders.Authorization = new("Bearer", _tokenStore.AccessToken);
|
||||||
}
|
else
|
||||||
|
_http.DefaultRequestHeaders.Authorization = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<IEnumerable<TaxFiling>> GetUpcomingAsync(int daysAhead = 30, CancellationToken ct = default)
|
public async Task<IEnumerable<TaxFiling>> GetUpcomingAsync(int daysAhead = 30, CancellationToken ct = default)
|
||||||
@@ -44,7 +44,7 @@ public class TaxFilingBrowserClient : ITaxFilingBrowserClient
|
|||||||
{
|
{
|
||||||
EnsureAuthHeader();
|
EnsureAuthHeader();
|
||||||
var result = await _http.GetFromJsonAsync<TaxFilingListResponse>(
|
var result = await _http.GetFromJsonAsync<TaxFilingListResponse>(
|
||||||
$"tax-filing/upcoming?daysAhead={daysAhead}", cancellationToken: ct);
|
$"taxfiling/upcoming?daysAhead={daysAhead}", cancellationToken: ct);
|
||||||
return result?.Data ?? [];
|
return result?.Data ?? [];
|
||||||
}
|
}
|
||||||
catch (HttpRequestException ex)
|
catch (HttpRequestException ex)
|
||||||
@@ -60,7 +60,7 @@ public class TaxFilingBrowserClient : ITaxFilingBrowserClient
|
|||||||
{
|
{
|
||||||
EnsureAuthHeader();
|
EnsureAuthHeader();
|
||||||
var result = await _http.GetFromJsonAsync<TaxFilingListResponse>(
|
var result = await _http.GetFromJsonAsync<TaxFilingListResponse>(
|
||||||
$"tax-filing/client/{clientId}", cancellationToken: ct);
|
$"taxfiling/client/{clientId}", cancellationToken: ct);
|
||||||
return result?.Data ?? [];
|
return result?.Data ?? [];
|
||||||
}
|
}
|
||||||
catch (HttpRequestException ex)
|
catch (HttpRequestException ex)
|
||||||
@@ -76,7 +76,7 @@ public class TaxFilingBrowserClient : ITaxFilingBrowserClient
|
|||||||
{
|
{
|
||||||
EnsureAuthHeader();
|
EnsureAuthHeader();
|
||||||
return await _http.GetFromJsonAsync<TaxFiling>(
|
return await _http.GetFromJsonAsync<TaxFiling>(
|
||||||
$"tax-filing/{id}", cancellationToken: ct);
|
$"taxfiling/{id}", cancellationToken: ct);
|
||||||
}
|
}
|
||||||
catch (HttpRequestException ex)
|
catch (HttpRequestException ex)
|
||||||
{
|
{
|
||||||
@@ -90,7 +90,7 @@ public class TaxFilingBrowserClient : ITaxFilingBrowserClient
|
|||||||
try
|
try
|
||||||
{
|
{
|
||||||
EnsureAuthHeader();
|
EnsureAuthHeader();
|
||||||
var response = await _http.PostAsJsonAsync("tax-filing", filing, cancellationToken: ct);
|
var response = await _http.PostAsJsonAsync("taxfiling", filing, cancellationToken: ct);
|
||||||
if (!response.IsSuccessStatusCode)
|
if (!response.IsSuccessStatusCode)
|
||||||
return null;
|
return null;
|
||||||
|
|
||||||
@@ -111,7 +111,7 @@ public class TaxFilingBrowserClient : ITaxFilingBrowserClient
|
|||||||
try
|
try
|
||||||
{
|
{
|
||||||
EnsureAuthHeader();
|
EnsureAuthHeader();
|
||||||
var response = await _http.PutAsJsonAsync($"tax-filing/{id}", filing, cancellationToken: ct);
|
var response = await _http.PutAsJsonAsync($"taxfiling/{id}", filing, cancellationToken: ct);
|
||||||
if (!response.IsSuccessStatusCode)
|
if (!response.IsSuccessStatusCode)
|
||||||
return null;
|
return null;
|
||||||
|
|
||||||
@@ -132,7 +132,7 @@ public class TaxFilingBrowserClient : ITaxFilingBrowserClient
|
|||||||
try
|
try
|
||||||
{
|
{
|
||||||
EnsureAuthHeader();
|
EnsureAuthHeader();
|
||||||
var response = await _http.DeleteAsync($"tax-filing/{id}", cancellationToken: ct);
|
var response = await _http.DeleteAsync($"taxfiling/{id}", cancellationToken: ct);
|
||||||
return response.IsSuccessStatusCode;
|
return response.IsSuccessStatusCode;
|
||||||
}
|
}
|
||||||
catch (HttpRequestException ex)
|
catch (HttpRequestException ex)
|
||||||
|
|||||||
@@ -10,12 +10,12 @@ using System.Text.Json;
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public class TokenRefreshHandler : DelegatingHandler
|
public class TokenRefreshHandler : DelegatingHandler
|
||||||
{
|
{
|
||||||
private readonly ITokenStore _tokenStore;
|
private readonly IServiceProvider _serviceProvider;
|
||||||
private readonly ILogger<TokenRefreshHandler> _logger;
|
private readonly ILogger<TokenRefreshHandler> _logger;
|
||||||
|
|
||||||
public TokenRefreshHandler(ITokenStore tokenStore, ILogger<TokenRefreshHandler> logger)
|
public TokenRefreshHandler(IServiceProvider serviceProvider, ILogger<TokenRefreshHandler> logger)
|
||||||
{
|
{
|
||||||
_tokenStore = tokenStore;
|
_serviceProvider = serviceProvider;
|
||||||
_logger = logger;
|
_logger = logger;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -23,10 +23,13 @@ public class TokenRefreshHandler : DelegatingHandler
|
|||||||
HttpRequestMessage request,
|
HttpRequestMessage request,
|
||||||
CancellationToken cancellationToken)
|
CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
|
// 최신 Scoped ITokenStore 실시간 해석 (Scope Capture 차단 및 기존 Blazor 회로 수명 공유)
|
||||||
|
var tokenStore = Microsoft.Extensions.DependencyInjection.ServiceProviderServiceExtensions.GetRequiredService<ITokenStore>(_serviceProvider);
|
||||||
|
|
||||||
// 요청에 access token 추가
|
// 요청에 access token 추가
|
||||||
if (!string.IsNullOrEmpty(_tokenStore.AccessToken))
|
if (!string.IsNullOrEmpty(tokenStore.AccessToken))
|
||||||
{
|
{
|
||||||
request.Headers.Authorization = new("Bearer", _tokenStore.AccessToken);
|
request.Headers.Authorization = new("Bearer", tokenStore.AccessToken);
|
||||||
}
|
}
|
||||||
|
|
||||||
var response = await base.SendAsync(request, cancellationToken);
|
var response = await base.SendAsync(request, cancellationToken);
|
||||||
@@ -34,15 +37,15 @@ public class TokenRefreshHandler : DelegatingHandler
|
|||||||
// 401 응답이면 토큰 갱신 시도
|
// 401 응답이면 토큰 갱신 시도
|
||||||
if (response.StatusCode == HttpStatusCode.Unauthorized)
|
if (response.StatusCode == HttpStatusCode.Unauthorized)
|
||||||
{
|
{
|
||||||
if (!string.IsNullOrEmpty(_tokenStore.RefreshToken))
|
if (!string.IsNullOrEmpty(tokenStore.RefreshToken))
|
||||||
{
|
{
|
||||||
var newTokenPair = await RefreshTokenAsync(_tokenStore.RefreshToken, request, cancellationToken);
|
var newTokenPair = await RefreshTokenAsync(tokenStore.RefreshToken, request, cancellationToken);
|
||||||
if (newTokenPair != null)
|
if (newTokenPair != null)
|
||||||
{
|
{
|
||||||
// TokenStore에 토큰 저장
|
// TokenStore에 토큰 저장
|
||||||
_tokenStore.AccessToken = newTokenPair.AccessToken;
|
tokenStore.AccessToken = newTokenPair.AccessToken;
|
||||||
_tokenStore.RefreshToken = newTokenPair.RefreshToken;
|
tokenStore.RefreshToken = newTokenPair.RefreshToken;
|
||||||
_tokenStore.TokenExpiryTicks = DateTime.UtcNow.AddSeconds(newTokenPair.ExpiresIn).Ticks;
|
tokenStore.TokenExpiryTicks = DateTime.UtcNow.AddSeconds(newTokenPair.ExpiresIn).Ticks;
|
||||||
|
|
||||||
// 새 토큰으로 재요청
|
// 새 토큰으로 재요청
|
||||||
request.Headers.Authorization = new("Bearer", newTokenPair.AccessToken);
|
request.Headers.Authorization = new("Bearer", newTokenPair.AccessToken);
|
||||||
@@ -51,7 +54,7 @@ public class TokenRefreshHandler : DelegatingHandler
|
|||||||
else
|
else
|
||||||
{
|
{
|
||||||
_logger.LogWarning("토큰 갱신 실패 - 로그아웃");
|
_logger.LogWarning("토큰 갱신 실패 - 로그아웃");
|
||||||
_tokenStore.Clear();
|
tokenStore.Clear();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,31 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
|
||||||
|
<!-- 메인 홈 -->
|
||||||
|
<url>
|
||||||
|
<loc>http://178.104.200.7/taxbaik/</loc>
|
||||||
|
<lastmod>2026-06-29</lastmod>
|
||||||
|
<changefreq>daily</changefreq>
|
||||||
|
<priority>1.0</priority>
|
||||||
|
</url>
|
||||||
|
<!-- 고객 포털 -->
|
||||||
|
<url>
|
||||||
|
<loc>http://178.104.200.7/taxbaik/portal</loc>
|
||||||
|
<lastmod>2026-06-29</lastmod>
|
||||||
|
<changefreq>weekly</changefreq>
|
||||||
|
<priority>0.8</priority>
|
||||||
|
</url>
|
||||||
|
<!-- 이용약관 -->
|
||||||
|
<url>
|
||||||
|
<loc>http://178.104.200.7/taxbaik/terms</loc>
|
||||||
|
<lastmod>2026-06-29</lastmod>
|
||||||
|
<changefreq>monthly</changefreq>
|
||||||
|
<priority>0.3</priority>
|
||||||
|
</url>
|
||||||
|
<!-- 개인정보처리방침 -->
|
||||||
|
<url>
|
||||||
|
<loc>http://178.104.200.7/taxbaik/privacy</loc>
|
||||||
|
<lastmod>2026-06-29</lastmod>
|
||||||
|
<changefreq>monthly</changefreq>
|
||||||
|
<priority>0.3</priority>
|
||||||
|
</url>
|
||||||
|
</urlset>
|
||||||
@@ -6,6 +6,8 @@ const password = process.env.E2E_ADMIN_PASSWORD;
|
|||||||
const baseUrl = (process.env.E2E_BASE_URL ?? 'http://178.104.200.7/taxbaik').replace(/\/$/, '');
|
const baseUrl = (process.env.E2E_BASE_URL ?? 'http://178.104.200.7/taxbaik').replace(/\/$/, '');
|
||||||
|
|
||||||
test.describe('admin CRM pages', () => {
|
test.describe('admin CRM pages', () => {
|
||||||
|
test.describe.configure({ mode: 'serial' });
|
||||||
|
|
||||||
test.beforeEach(async ({ page }) => {
|
test.beforeEach(async ({ page }) => {
|
||||||
test.skip(!password, 'E2E_ADMIN_PASSWORD is required.');
|
test.skip(!password, 'E2E_ADMIN_PASSWORD is required.');
|
||||||
await loginThroughAdminUi(page, baseUrl, username, password);
|
await loginThroughAdminUi(page, baseUrl, username, password);
|
||||||
@@ -121,4 +123,61 @@ test.describe('admin CRM pages', () => {
|
|||||||
|
|
||||||
expect(consoleErrors, 'no console errors during CRM navigation').toEqual([]);
|
expect(consoleErrors, 'no console errors during CRM navigation').toEqual([]);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('TaxProfiles form displays valid business type combo choices', async ({ page }) => {
|
||||||
|
await navigateInBlazor(page, `${baseUrl}/admin/tax-profiles`);
|
||||||
|
await expect(page.locator('.admin-grid, .mud-alert')).toBeVisible({ timeout: 15000 });
|
||||||
|
|
||||||
|
const addButton = page.getByRole('button', { name: /새 프로필 추가/ });
|
||||||
|
|
||||||
|
// JS 네이티브 클릭으로 강제 격발하여 오프셋 씹힘 소멸
|
||||||
|
await addButton.evaluate(el => (el as HTMLButtonElement).click());
|
||||||
|
|
||||||
|
// 대화상자(MudDialog) 자체의 노출 대기
|
||||||
|
await expect(page.locator('.mud-dialog')).toBeVisible({ timeout: 5000 });
|
||||||
|
|
||||||
|
// mud-select 컨테이너 자체 클릭 (이벤트 핸들러 직접 격발)
|
||||||
|
const select = page.locator('.mud-select').filter({ hasText: '사업 유형' }).first();
|
||||||
|
await select.evaluate(el => (el as HTMLDivElement).click());
|
||||||
|
|
||||||
|
// 활성화된 팝오버(.mud-popover-open) 내에서 텍스트 노출 검증
|
||||||
|
const popover = page.locator('.mud-popover-open');
|
||||||
|
await expect(popover.getByText('일반제조업')).toBeVisible({ timeout: 5000 });
|
||||||
|
await expect(popover.getByText('도소매업')).toBeVisible({ timeout: 5000 });
|
||||||
|
await expect(popover.getByText('서비스업')).toBeVisible({ timeout: 5000 });
|
||||||
|
});
|
||||||
|
|
||||||
|
test('TaxFilingSchedules form displays filing type combo choices', async ({ page }) => {
|
||||||
|
await navigateInBlazor(page, `${baseUrl}/admin/tax-filing-schedules`);
|
||||||
|
await expect(page.locator('.admin-grid, .mud-alert')).toBeVisible({ timeout: 15000 });
|
||||||
|
|
||||||
|
const addButton = page.getByRole('button', { name: /새 일정 추가/ });
|
||||||
|
await addButton.evaluate(el => (el as HTMLButtonElement).click());
|
||||||
|
|
||||||
|
await expect(page.locator('.mud-dialog')).toBeVisible({ timeout: 5000 });
|
||||||
|
|
||||||
|
const select = page.locator('.mud-select').filter({ hasText: '신고 유형' }).first();
|
||||||
|
await select.evaluate(el => (el as HTMLDivElement).click());
|
||||||
|
|
||||||
|
const popover = page.locator('.mud-popover-open');
|
||||||
|
await expect(popover.getByText('종합소득세')).toBeVisible({ timeout: 5000 });
|
||||||
|
await expect(popover.getByText('부가가치세')).toBeVisible({ timeout: 5000 });
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Contracts form displays service type combo choices', async ({ page }) => {
|
||||||
|
await navigateInBlazor(page, `${baseUrl}/admin/contracts`);
|
||||||
|
await expect(page.locator('.admin-grid, .mud-alert')).toBeVisible({ timeout: 15000 });
|
||||||
|
|
||||||
|
const addButton = page.getByRole('button', { name: /새 계약 추가/ });
|
||||||
|
await addButton.evaluate(el => (el as HTMLButtonElement).click());
|
||||||
|
|
||||||
|
await expect(page.locator('.mud-dialog')).toBeVisible({ timeout: 5000 });
|
||||||
|
|
||||||
|
const select = page.locator('.mud-select').filter({ hasText: '서비스 유형' }).first();
|
||||||
|
await select.evaluate(el => (el as HTMLDivElement).click());
|
||||||
|
|
||||||
|
const popover = page.locator('.mud-popover-open');
|
||||||
|
await expect(popover.getByText('개인 기장대리')).toBeVisible({ timeout: 5000 });
|
||||||
|
await expect(popover.getByText('법인 기장대리')).toBeVisible({ timeout: 5000 });
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -24,7 +24,14 @@ export async function getAdminToken(
|
|||||||
}
|
}
|
||||||
|
|
||||||
export async function installAdminToken(page: Page, token: string) {
|
export async function installAdminToken(page: Page, token: string) {
|
||||||
await page.addInitScript(value => localStorage.setItem('auth_token', value), token);
|
await page.addInitScript(value => {
|
||||||
|
localStorage.setItem('accessToken', value);
|
||||||
|
localStorage.setItem('refreshToken', 'ci-test-refresh-token');
|
||||||
|
// Calculate C# Ticks for 1 hour from now: (JS_ms * 10000) + 621355968000000000
|
||||||
|
const expiryMs = Date.now() + 3600 * 1000;
|
||||||
|
const ticks = (expiryMs * 10000) + 621355968000000000;
|
||||||
|
localStorage.setItem('tokenExpiry', ticks.toString());
|
||||||
|
}, token);
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function loginThroughAdminUi(
|
export async function loginThroughAdminUi(
|
||||||
@@ -51,6 +58,22 @@ export async function navigateInBlazor(page: Page, targetUrl: string) {
|
|||||||
|
|
||||||
window.location.href = url;
|
window.location.href = url;
|
||||||
}, targetUrl);
|
}, targetUrl);
|
||||||
|
|
||||||
|
// Wait until Blazor Server completes connection and hides the loading spinner overlay
|
||||||
|
await page.locator('#blazor-loading').waitFor({ state: 'hidden', timeout: 15000 }).catch(() => {});
|
||||||
|
|
||||||
|
// Give the SPA router a brief window to unmount the previous page and mount the loading spinner
|
||||||
|
await page.waitForTimeout(500);
|
||||||
|
|
||||||
|
// Also wait for MudBlazor's dynamic loading spinners to disappear (ensuring the grid is interactive)
|
||||||
|
const spinner = page.locator('.mud-progress-circular, .mud-progress-linear-bar');
|
||||||
|
try {
|
||||||
|
if (await spinner.count() > 0) {
|
||||||
|
await spinner.first().waitFor({ state: 'hidden', timeout: 10000 });
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
// Suppress timeout if the spinner was already gone or never showed up
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function findInquiryByName(
|
export async function findInquiryByName(
|
||||||
|
|||||||
Reference in New Issue
Block a user