feat: 대시보드에 라이선스 만료 요약 및 Lookup 데이터 캐싱 시스템 구현
- License Expiry Summary API 연동 완료 - 30/60/90일 내 만료 예정 라이선스 요약 표시 - 대시보드 상단에 알림 카드로 통합 - 만료 임박 순서로 색상 구분 (빨강/주황/노랑) - Lookup 데이터 전역 캐싱 시스템 구축 - LookupService 및 RemoteDataSource 생성 - 전체 lookup 데이터 일괄 로드 및 캐싱 - 타입별 필터링 지원 - 새로운 모델 추가 - LicenseExpirySummary (Freezed) - LookupData, LookupCategory, LookupItem 모델 - CLAUDE.md 문서 업데이트 - 미사용 API 활용 계획 추가 - 구현 우선순위 정의 🤖 Generated with Claude Code Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -4,6 +4,7 @@ import 'package:superport/core/errors/failures.dart';
|
||||
import 'package:superport/data/datasources/remote/dashboard_remote_datasource.dart';
|
||||
import 'package:superport/data/models/dashboard/equipment_status_distribution.dart';
|
||||
import 'package:superport/data/models/dashboard/expiring_license.dart';
|
||||
import 'package:superport/data/models/dashboard/license_expiry_summary.dart';
|
||||
import 'package:superport/data/models/dashboard/overview_stats.dart';
|
||||
import 'package:superport/data/models/dashboard/recent_activity.dart';
|
||||
|
||||
@@ -12,6 +13,7 @@ abstract class DashboardService {
|
||||
Future<Either<Failure, List<RecentActivity>>> getRecentActivities();
|
||||
Future<Either<Failure, EquipmentStatusDistribution>> getEquipmentStatusDistribution();
|
||||
Future<Either<Failure, List<ExpiringLicense>>> getExpiringLicenses({int days = 30});
|
||||
Future<Either<Failure, LicenseExpirySummary>> getLicenseExpirySummary();
|
||||
}
|
||||
|
||||
@LazySingleton(as: DashboardService)
|
||||
@@ -39,4 +41,9 @@ class DashboardServiceImpl implements DashboardService {
|
||||
Future<Either<Failure, List<ExpiringLicense>>> getExpiringLicenses({int days = 30}) async {
|
||||
return await _remoteDataSource.getExpiringLicenses(days: days);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<Either<Failure, LicenseExpirySummary>> getLicenseExpirySummary() async {
|
||||
return await _remoteDataSource.getLicenseExpirySummary();
|
||||
}
|
||||
}
|
||||
165
lib/services/lookup_service.dart
Normal file
165
lib/services/lookup_service.dart
Normal file
@@ -0,0 +1,165 @@
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:injectable/injectable.dart';
|
||||
import 'package:superport/data/datasources/remote/lookup_remote_datasource.dart';
|
||||
import 'package:superport/data/models/lookups/lookup_data.dart';
|
||||
|
||||
@lazySingleton
|
||||
class LookupService extends ChangeNotifier {
|
||||
final LookupRemoteDataSource _dataSource;
|
||||
|
||||
LookupData? _lookupData;
|
||||
bool _isLoading = false;
|
||||
String? _error;
|
||||
DateTime? _lastFetchTime;
|
||||
|
||||
// 캐시 유효 시간 (30분)
|
||||
static const Duration _cacheValidDuration = Duration(minutes: 30);
|
||||
|
||||
LookupService(this._dataSource);
|
||||
|
||||
// Getters
|
||||
LookupData? get lookupData => _lookupData;
|
||||
bool get isLoading => _isLoading;
|
||||
String? get error => _error;
|
||||
bool get hasData => _lookupData != null;
|
||||
|
||||
// 캐시가 유효한지 확인
|
||||
bool get isCacheValid {
|
||||
if (_lastFetchTime == null) return false;
|
||||
return DateTime.now().difference(_lastFetchTime!) < _cacheValidDuration;
|
||||
}
|
||||
|
||||
// 장비 타입 목록
|
||||
List<LookupItem> get equipmentTypes => _lookupData?.equipmentTypes ?? [];
|
||||
|
||||
// 장비 상태 목록
|
||||
List<LookupItem> get equipmentStatuses => _lookupData?.equipmentStatuses ?? [];
|
||||
|
||||
// 라이선스 타입 목록
|
||||
List<LookupItem> get licenseTypes => _lookupData?.licenseTypes ?? [];
|
||||
|
||||
// 제조사 목록
|
||||
List<LookupItem> get manufacturers => _lookupData?.manufacturers ?? [];
|
||||
|
||||
// 사용자 역할 목록
|
||||
List<LookupItem> get userRoles => _lookupData?.userRoles ?? [];
|
||||
|
||||
// 회사 상태 목록
|
||||
List<LookupItem> get companyStatuses => _lookupData?.companyStatuses ?? [];
|
||||
|
||||
// 창고 타입 목록
|
||||
List<LookupItem> get warehouseTypes => _lookupData?.warehouseTypes ?? [];
|
||||
|
||||
// 전체 조회 데이터 로드
|
||||
Future<void> loadAllLookups({bool forceRefresh = false}) async {
|
||||
// 캐시가 유효하고 강제 새로고침이 아니면 캐시 사용
|
||||
if (!forceRefresh && isCacheValid && hasData) {
|
||||
return;
|
||||
}
|
||||
|
||||
_isLoading = true;
|
||||
_error = null;
|
||||
notifyListeners();
|
||||
|
||||
try {
|
||||
final result = await _dataSource.getAllLookups();
|
||||
|
||||
result.fold(
|
||||
(failure) {
|
||||
_error = failure.message;
|
||||
_isLoading = false;
|
||||
notifyListeners();
|
||||
},
|
||||
(data) {
|
||||
_lookupData = data;
|
||||
_lastFetchTime = DateTime.now();
|
||||
_error = null;
|
||||
_isLoading = false;
|
||||
notifyListeners();
|
||||
},
|
||||
);
|
||||
} catch (e) {
|
||||
_error = '조회 데이터 로드 중 오류가 발생했습니다: $e';
|
||||
_isLoading = false;
|
||||
notifyListeners();
|
||||
}
|
||||
}
|
||||
|
||||
// 특정 타입의 조회 데이터만 로드
|
||||
Future<Map<String, List<LookupItem>>?> loadLookupsByType(String type) async {
|
||||
try {
|
||||
final result = await _dataSource.getLookupsByType(type);
|
||||
|
||||
return result.fold(
|
||||
(failure) {
|
||||
_error = failure.message;
|
||||
notifyListeners();
|
||||
return null;
|
||||
},
|
||||
(data) {
|
||||
// 부분 업데이트 (필요한 경우)
|
||||
_updatePartialData(type, data);
|
||||
return data;
|
||||
},
|
||||
);
|
||||
} catch (e) {
|
||||
_error = '타입별 조회 데이터 로드 중 오류가 발생했습니다: $e';
|
||||
notifyListeners();
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
// 부분 데이터 업데이트
|
||||
void _updatePartialData(String type, Map<String, List<LookupItem>> data) {
|
||||
if (_lookupData == null) {
|
||||
// 전체 데이터가 없으면 부분 데이터만으로 초기화
|
||||
_lookupData = LookupData(
|
||||
equipmentTypes: data['equipment_types'] ?? [],
|
||||
equipmentStatuses: data['equipment_statuses'] ?? [],
|
||||
licenseTypes: data['license_types'] ?? [],
|
||||
manufacturers: data['manufacturers'] ?? [],
|
||||
userRoles: data['user_roles'] ?? [],
|
||||
companyStatuses: data['company_statuses'] ?? [],
|
||||
warehouseTypes: data['warehouse_types'] ?? [],
|
||||
);
|
||||
} else {
|
||||
// 기존 데이터의 특정 부분만 업데이트
|
||||
_lookupData = _lookupData!.copyWith(
|
||||
equipmentTypes: data['equipment_types'] ?? _lookupData!.equipmentTypes,
|
||||
equipmentStatuses: data['equipment_statuses'] ?? _lookupData!.equipmentStatuses,
|
||||
licenseTypes: data['license_types'] ?? _lookupData!.licenseTypes,
|
||||
manufacturers: data['manufacturers'] ?? _lookupData!.manufacturers,
|
||||
userRoles: data['user_roles'] ?? _lookupData!.userRoles,
|
||||
companyStatuses: data['company_statuses'] ?? _lookupData!.companyStatuses,
|
||||
warehouseTypes: data['warehouse_types'] ?? _lookupData!.warehouseTypes,
|
||||
);
|
||||
}
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
// 코드로 아이템 찾기
|
||||
LookupItem? findByCode(List<LookupItem> items, String code) {
|
||||
try {
|
||||
return items.firstWhere((item) => item.code == code);
|
||||
} catch (_) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
// 이름으로 아이템 찾기
|
||||
LookupItem? findByName(List<LookupItem> items, String name) {
|
||||
try {
|
||||
return items.firstWhere((item) => item.name == name);
|
||||
} catch (_) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
// 캐시 클리어
|
||||
void clearCache() {
|
||||
_lookupData = null;
|
||||
_lastFetchTime = null;
|
||||
_error = null;
|
||||
notifyListeners();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user