feat: 대시보드에 라이선스 만료 요약 및 Lookup 데이터 캐싱 시스템 구현
Some checks failed
Flutter Test & Quality Check / Test on macos-latest (push) Has been cancelled
Flutter Test & Quality Check / Test on ubuntu-latest (push) Has been cancelled
Flutter Test & Quality Check / Build APK (push) Has been cancelled

- 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:
JiWoong Sul
2025-08-08 14:42:20 +09:00
parent 740a691406
commit 844c7bd92f
15 changed files with 2105 additions and 4 deletions

View 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();
}
}