refactor: Clean Architecture 적용 및 코드베이스 전면 리팩토링
## 주요 변경사항 ### 아키텍처 개선 - Clean Architecture 패턴 적용 (Domain, Data, Presentation 레이어 분리) - Use Case 패턴 도입으로 비즈니스 로직 캡슐화 - Repository 패턴으로 데이터 접근 추상화 - 의존성 주입 구조 개선 ### 상태 관리 최적화 - 모든 Controller에서 불필요한 상태 관리 로직 제거 - 페이지네이션 로직 통일 및 간소화 - 에러 처리 로직 개선 (에러 메시지 한글화) - 로딩 상태 관리 최적화 ### Mock 서비스 제거 - MockDataService 완전 제거 - 모든 화면을 실제 API 전용으로 전환 - 불필요한 Mock 관련 코드 정리 ### UI/UX 개선 - Overview 화면 대시보드 기능 강화 - 라이선스 만료 알림 위젯 추가 - 사이드바 네비게이션 개선 - 일관된 UI 컴포넌트 사용 ### 코드 품질 - 중복 코드 제거 및 함수 추출 - 파일별 책임 분리 명확화 - 테스트 코드 업데이트 ## 영향 범위 - 모든 화면의 Controller 리팩토링 - API 통신 레이어 구조 개선 - 에러 처리 및 로깅 시스템 개선 ## 향후 계획 - 단위 테스트 커버리지 확대 - 통합 테스트 시나리오 추가 - 성능 모니터링 도구 통합
This commit is contained in:
@@ -2,13 +2,10 @@ import 'package:flutter/material.dart';
|
||||
import 'package:get_it/get_it.dart';
|
||||
import 'package:superport/models/license_model.dart';
|
||||
import 'package:superport/services/license_service.dart';
|
||||
import 'package:superport/services/mock_data_service.dart';
|
||||
|
||||
// 라이센스 폼의 상태 및 비즈니스 로직을 담당하는 컨트롤러
|
||||
class LicenseFormController extends ChangeNotifier {
|
||||
final bool useApi;
|
||||
final MockDataService? mockDataService;
|
||||
late final LicenseService _licenseService;
|
||||
final LicenseService _licenseService = GetIt.instance<LicenseService>();
|
||||
final GlobalKey<FormState> formKey = GlobalKey<FormState>();
|
||||
|
||||
bool _isEditMode = false;
|
||||
@@ -59,15 +56,9 @@ class LicenseFormController extends ChangeNotifier {
|
||||
}
|
||||
|
||||
LicenseFormController({
|
||||
this.useApi = false,
|
||||
MockDataService? dataService,
|
||||
int? licenseId,
|
||||
bool isExtension = false,
|
||||
}) : mockDataService = dataService ?? MockDataService() {
|
||||
if (useApi && GetIt.instance.isRegistered<LicenseService>()) {
|
||||
_licenseService = GetIt.instance<LicenseService>();
|
||||
}
|
||||
|
||||
}) {
|
||||
if (licenseId != null && !isExtension) {
|
||||
_licenseId = licenseId;
|
||||
_isEditMode = true;
|
||||
@@ -122,13 +113,8 @@ class LicenseFormController extends ChangeNotifier {
|
||||
notifyListeners();
|
||||
|
||||
try {
|
||||
if (useApi && GetIt.instance.isRegistered<LicenseService>()) {
|
||||
debugPrint('📝 API에서 라이센스 로드 중...');
|
||||
_originalLicense = await _licenseService.getLicenseById(_licenseId!);
|
||||
} else {
|
||||
debugPrint('📝 Mock에서 라이센스 로드 중...');
|
||||
_originalLicense = mockDataService?.getLicenseById(_licenseId!);
|
||||
}
|
||||
debugPrint('📝 API에서 라이센스 로드 중...');
|
||||
_originalLicense = await _licenseService.getLicenseById(_licenseId!);
|
||||
|
||||
debugPrint('📝 로드된 라이센스: $_originalLicense');
|
||||
|
||||
@@ -182,14 +168,8 @@ class LicenseFormController extends ChangeNotifier {
|
||||
notifyListeners();
|
||||
|
||||
try {
|
||||
License? sourceLicense;
|
||||
if (useApi && GetIt.instance.isRegistered<LicenseService>()) {
|
||||
debugPrint('📝 API에서 라이센스 로드 중 (연장용)...');
|
||||
sourceLicense = await _licenseService.getLicenseById(_licenseId!);
|
||||
} else {
|
||||
debugPrint('📝 Mock에서 라이센스 로드 중 (연장용)...');
|
||||
sourceLicense = mockDataService?.getLicenseById(_licenseId!);
|
||||
}
|
||||
debugPrint('📝 API에서 라이센스 로드 중 (연장용)...');
|
||||
final sourceLicense = await _licenseService.getLicenseById(_licenseId!);
|
||||
|
||||
debugPrint('📝 로드된 소스 라이센스: $sourceLicense');
|
||||
|
||||
@@ -263,18 +243,10 @@ class LicenseFormController extends ChangeNotifier {
|
||||
remark: '${_durationMonths}개월,${_visitCycle},방문',
|
||||
);
|
||||
|
||||
if (useApi && GetIt.instance.isRegistered<LicenseService>()) {
|
||||
if (_isEditMode) {
|
||||
await _licenseService.updateLicense(license);
|
||||
} else {
|
||||
await _licenseService.createLicense(license);
|
||||
}
|
||||
if (_isEditMode) {
|
||||
await _licenseService.updateLicense(license);
|
||||
} else {
|
||||
if (_isEditMode) {
|
||||
mockDataService?.updateLicense(license);
|
||||
} else {
|
||||
mockDataService?.addLicense(license);
|
||||
}
|
||||
await _licenseService.createLicense(license);
|
||||
}
|
||||
|
||||
return true;
|
||||
|
||||
@@ -0,0 +1,467 @@
|
||||
import 'dart:async';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get_it/get_it.dart';
|
||||
import 'package:superport/core/errors/failures.dart';
|
||||
import 'package:superport/models/license_model.dart';
|
||||
import 'package:superport/services/license_service.dart';
|
||||
|
||||
// 라이센스 상태 필터
|
||||
enum LicenseStatusFilter {
|
||||
all,
|
||||
active,
|
||||
inactive,
|
||||
expiringSoon, // 30일 이내
|
||||
expired,
|
||||
}
|
||||
|
||||
// 라이센스 목록 화면의 상태 및 비즈니스 로직을 담당하는 컨트롤러
|
||||
class LicenseListController extends ChangeNotifier {
|
||||
final LicenseService _licenseService = GetIt.instance<LicenseService>();
|
||||
|
||||
List<License> _licenses = [];
|
||||
List<License> _filteredLicenses = [];
|
||||
bool _isLoading = false;
|
||||
String? _error;
|
||||
String _searchQuery = '';
|
||||
int _currentPage = 1;
|
||||
final int _pageSize = 20;
|
||||
bool _hasMore = true;
|
||||
int _total = 0;
|
||||
|
||||
// 필터 옵션
|
||||
int? _selectedCompanyId;
|
||||
bool? _isActive;
|
||||
String? _licenseType;
|
||||
LicenseStatusFilter _statusFilter = LicenseStatusFilter.all;
|
||||
String _sortBy = 'expiry_date';
|
||||
String _sortOrder = 'asc';
|
||||
|
||||
// 선택된 라이선스 관리
|
||||
final Set<int> _selectedLicenseIds = {};
|
||||
|
||||
// 통계 데이터
|
||||
Map<String, int> _statistics = {
|
||||
'total': 0,
|
||||
'active': 0,
|
||||
'inactive': 0,
|
||||
'expiringSoon': 0,
|
||||
'expired': 0,
|
||||
};
|
||||
|
||||
// 검색 디바운스를 위한 타이머
|
||||
Timer? _debounceTimer;
|
||||
|
||||
LicenseListController();
|
||||
|
||||
// Getters
|
||||
List<License> get licenses => _filteredLicenses;
|
||||
bool get isLoading => _isLoading;
|
||||
String? get error => _error;
|
||||
String get searchQuery => _searchQuery;
|
||||
int get currentPage => _currentPage;
|
||||
bool get hasMore => _hasMore;
|
||||
int get total => _total;
|
||||
int? get selectedCompanyId => _selectedCompanyId;
|
||||
bool? get isActive => _isActive;
|
||||
String? get licenseType => _licenseType;
|
||||
LicenseStatusFilter get statusFilter => _statusFilter;
|
||||
Set<int> get selectedLicenseIds => _selectedLicenseIds;
|
||||
Map<String, int> get statistics => _statistics;
|
||||
|
||||
// 선택된 라이선스 개수
|
||||
int get selectedCount => _selectedLicenseIds.length;
|
||||
|
||||
// 전체 선택 여부 확인
|
||||
bool get isAllSelected =>
|
||||
_filteredLicenses.isNotEmpty &&
|
||||
_filteredLicenses.where((l) => l.id != null)
|
||||
.every((l) => _selectedLicenseIds.contains(l.id));
|
||||
|
||||
// 데이터 로드
|
||||
Future<void> loadData({bool isInitialLoad = true}) async {
|
||||
if (_isLoading) return;
|
||||
|
||||
_isLoading = true;
|
||||
_error = null;
|
||||
notifyListeners();
|
||||
|
||||
try {
|
||||
// API 사용 - 전체 데이터 로드
|
||||
print('╔══════════════════════════════════════════════════════════');
|
||||
print('║ 🔧 유지보수 목록 API 호출 시작');
|
||||
print('║ • 회사 필터: ${_selectedCompanyId ?? "전체"}');
|
||||
print('║ • 활성 필터: ${_isActive != null ? (_isActive! ? "활성" : "비활성") : "전체"}');
|
||||
print('║ • 라이센스 타입: ${_licenseType ?? "전체"}');
|
||||
print('╚══════════════════════════════════════════════════════════');
|
||||
|
||||
// 전체 데이터를 가져오기 위해 큰 perPage 값 사용
|
||||
final fetchedLicenses = await _licenseService.getLicenses(
|
||||
page: 1,
|
||||
perPage: 1000, // 충분히 큰 값으로 전체 데이터 로드
|
||||
isActive: _isActive,
|
||||
companyId: _selectedCompanyId,
|
||||
licenseType: _licenseType,
|
||||
);
|
||||
|
||||
print('╔══════════════════════════════════════════════════════════');
|
||||
print('║ 📊 유지보수 목록 로드 완료');
|
||||
print('║ ▶ 총 라이센스 수: ${fetchedLicenses.length}개');
|
||||
print('╟──────────────────────────────────────────────────────────');
|
||||
|
||||
// 상태별 통계
|
||||
int activeCount = 0;
|
||||
int expiringSoonCount = 0;
|
||||
int expiredCount = 0;
|
||||
final now = DateTime.now();
|
||||
|
||||
for (final license in fetchedLicenses) {
|
||||
if (license.expiryDate != null) {
|
||||
final daysUntil = license.expiryDate!.difference(now).inDays;
|
||||
if (daysUntil < 0) {
|
||||
expiredCount++;
|
||||
} else if (daysUntil <= 30) {
|
||||
expiringSoonCount++;
|
||||
} else {
|
||||
activeCount++;
|
||||
}
|
||||
} else {
|
||||
activeCount++;
|
||||
}
|
||||
}
|
||||
|
||||
print('║ • 활성: $activeCount개');
|
||||
print('║ • 만료 임박 (30일 이내): $expiringSoonCount개');
|
||||
print('║ • 만료됨: $expiredCount개');
|
||||
|
||||
print('╟──────────────────────────────────────────────────────────');
|
||||
print('║ 📑 전체 데이터 로드 완료');
|
||||
print('║ • View에서 페이지네이션 처리 예정');
|
||||
print('╚══════════════════════════════════════════════════════════');
|
||||
|
||||
_licenses = fetchedLicenses;
|
||||
_hasMore = false; // 전체 데이터를 로드했으므로 더 이상 로드할 필요 없음
|
||||
_total = fetchedLicenses.length;
|
||||
|
||||
debugPrint('📑 _applySearchFilter 호출 전: _licenses=${_licenses.length}개');
|
||||
_applySearchFilter();
|
||||
_applyStatusFilter();
|
||||
await _updateStatistics();
|
||||
debugPrint('📑 _applySearchFilter 호출 후: _filteredLicenses=${_filteredLicenses.length}개');
|
||||
} catch (e) {
|
||||
debugPrint('❌ loadData 에러 발생: $e');
|
||||
_error = e.toString();
|
||||
} finally {
|
||||
_isLoading = false;
|
||||
debugPrint('📑 loadData 종료: _filteredLicenses=${_filteredLicenses.length}개');
|
||||
notifyListeners();
|
||||
}
|
||||
}
|
||||
|
||||
// 다음 페이지 로드
|
||||
Future<void> loadNextPage() async {
|
||||
if (!_hasMore || _isLoading) return;
|
||||
_currentPage++;
|
||||
await loadData(isInitialLoad: false);
|
||||
}
|
||||
|
||||
// 검색 (디바운싱 적용)
|
||||
void search(String query) {
|
||||
_searchQuery = query;
|
||||
|
||||
// 기존 타이머 취소
|
||||
_debounceTimer?.cancel();
|
||||
|
||||
// API 검색은 디바운싱 적용 (300ms)
|
||||
_debounceTimer = Timer(const Duration(milliseconds: 300), () {
|
||||
loadData();
|
||||
});
|
||||
}
|
||||
|
||||
// 검색 필터 적용
|
||||
void _applySearchFilter() {
|
||||
debugPrint('🔎 _applySearchFilter 시작: _searchQuery="$_searchQuery", _licenses=${_licenses.length}개');
|
||||
|
||||
if (_searchQuery.isEmpty) {
|
||||
_filteredLicenses = List.from(_licenses);
|
||||
debugPrint('🔎 검색어 없음: 전체 복사 ${_filteredLicenses.length}개');
|
||||
} else {
|
||||
_filteredLicenses = _licenses.where((license) {
|
||||
final productName = license.productName?.toLowerCase() ?? '';
|
||||
final licenseKey = license.licenseKey.toLowerCase();
|
||||
final vendor = license.vendor?.toLowerCase() ?? '';
|
||||
final companyName = license.companyName?.toLowerCase() ?? '';
|
||||
final searchLower = _searchQuery.toLowerCase();
|
||||
|
||||
return productName.contains(searchLower) ||
|
||||
licenseKey.contains(searchLower) ||
|
||||
vendor.contains(searchLower) ||
|
||||
companyName.contains(searchLower);
|
||||
}).toList();
|
||||
debugPrint('🔎 검색 필터링 완료: ${_filteredLicenses.length}개');
|
||||
}
|
||||
}
|
||||
|
||||
// 상태 필터 적용
|
||||
void _applyStatusFilter() {
|
||||
if (_statusFilter == LicenseStatusFilter.all) return;
|
||||
|
||||
final now = DateTime.now();
|
||||
_filteredLicenses = _filteredLicenses.where((license) {
|
||||
switch (_statusFilter) {
|
||||
case LicenseStatusFilter.active:
|
||||
return license.isActive;
|
||||
case LicenseStatusFilter.inactive:
|
||||
return !license.isActive;
|
||||
case LicenseStatusFilter.expiringSoon:
|
||||
if (license.expiryDate != null) {
|
||||
final days = license.expiryDate!.difference(now).inDays;
|
||||
return days > 0 && days <= 30;
|
||||
}
|
||||
return false;
|
||||
case LicenseStatusFilter.expired:
|
||||
if (license.expiryDate != null) {
|
||||
return license.expiryDate!.isBefore(now);
|
||||
}
|
||||
return false;
|
||||
case LicenseStatusFilter.all:
|
||||
default:
|
||||
return true;
|
||||
}
|
||||
}).toList();
|
||||
}
|
||||
|
||||
// 필터 설정
|
||||
void setFilters({
|
||||
int? companyId,
|
||||
bool? isActive,
|
||||
String? licenseType,
|
||||
}) {
|
||||
_selectedCompanyId = companyId;
|
||||
_isActive = isActive;
|
||||
_licenseType = licenseType;
|
||||
loadData();
|
||||
}
|
||||
|
||||
// 필터 초기화
|
||||
void clearFilters() {
|
||||
_selectedCompanyId = null;
|
||||
_isActive = null;
|
||||
_licenseType = null;
|
||||
_searchQuery = '';
|
||||
loadData();
|
||||
}
|
||||
|
||||
// 라이센스 삭제
|
||||
Future<void> deleteLicense(int id) async {
|
||||
try {
|
||||
await _licenseService.deleteLicense(id);
|
||||
|
||||
// 목록에서 제거
|
||||
_licenses.removeWhere((l) => l.id == id);
|
||||
_applySearchFilter();
|
||||
_total--;
|
||||
notifyListeners();
|
||||
} catch (e) {
|
||||
_error = e.toString();
|
||||
notifyListeners();
|
||||
}
|
||||
}
|
||||
|
||||
// 새로고침
|
||||
Future<void> refresh() async {
|
||||
await loadData();
|
||||
}
|
||||
|
||||
// 만료 예정 라이선스 조회
|
||||
Future<List<License>> getExpiringLicenses({int days = 30}) async {
|
||||
try {
|
||||
return await _licenseService.getExpiringLicenses(days: days);
|
||||
} catch (e) {
|
||||
_error = e.toString();
|
||||
notifyListeners();
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
// 상태별 라이선스 개수 조회
|
||||
Future<Map<String, int>> getLicenseStatusCounts() async {
|
||||
try {
|
||||
// API에서 상태별 개수 조회 (실제로는 별도 엔드포인트가 있다면 사용)
|
||||
final activeCount = await _licenseService.getTotalLicenses(isActive: true);
|
||||
final inactiveCount = await _licenseService.getTotalLicenses(isActive: false);
|
||||
final expiringLicenses = await getExpiringLicenses(days: 30);
|
||||
|
||||
return {
|
||||
'active': activeCount,
|
||||
'inactive': inactiveCount,
|
||||
'expiring': expiringLicenses.length,
|
||||
'total': activeCount + inactiveCount,
|
||||
};
|
||||
} catch (e) {
|
||||
return {'active': 0, 'inactive': 0, 'expiring': 0, 'total': 0};
|
||||
}
|
||||
}
|
||||
|
||||
// 정렬 변경
|
||||
void sortBy(String field, String order) {
|
||||
_sortBy = field;
|
||||
_sortOrder = order;
|
||||
loadData();
|
||||
}
|
||||
|
||||
// 상태 필터 변경
|
||||
Future<void> changeStatusFilter(LicenseStatusFilter filter) async {
|
||||
_statusFilter = filter;
|
||||
await loadData();
|
||||
}
|
||||
|
||||
// 라이선스 선택/해제
|
||||
void selectLicense(int? id, bool? isSelected) {
|
||||
if (id == null) return;
|
||||
|
||||
if (isSelected == true) {
|
||||
_selectedLicenseIds.add(id);
|
||||
} else {
|
||||
_selectedLicenseIds.remove(id);
|
||||
}
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
// 전체 선택/해제
|
||||
void selectAll(bool? isSelected) {
|
||||
if (isSelected == true) {
|
||||
// 현재 필터링된 라이선스 모두 선택
|
||||
for (var license in _filteredLicenses) {
|
||||
if (license.id != null) {
|
||||
_selectedLicenseIds.add(license.id!);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// 모두 해제
|
||||
_selectedLicenseIds.clear();
|
||||
}
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
// 선택된 라이선스 목록 반환
|
||||
List<License> getSelectedLicenses() {
|
||||
return _filteredLicenses
|
||||
.where((l) => l.id != null && _selectedLicenseIds.contains(l.id))
|
||||
.toList();
|
||||
}
|
||||
|
||||
// 선택 초기화
|
||||
void clearSelection() {
|
||||
_selectedLicenseIds.clear();
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
// 라이선스 할당
|
||||
Future<bool> assignLicense(int licenseId, int userId) async {
|
||||
try {
|
||||
await _licenseService.assignLicense(licenseId, userId);
|
||||
await loadData();
|
||||
clearSelection();
|
||||
return true;
|
||||
} catch (e) {
|
||||
_error = e.toString();
|
||||
notifyListeners();
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// 라이선스 할당 해제
|
||||
Future<bool> unassignLicense(int licenseId) async {
|
||||
try {
|
||||
await _licenseService.unassignLicense(licenseId);
|
||||
await loadData();
|
||||
clearSelection();
|
||||
return true;
|
||||
} catch (e) {
|
||||
_error = e.toString();
|
||||
notifyListeners();
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// 선택된 라이선스 일괄 삭제
|
||||
Future<void> deleteSelectedLicenses() async {
|
||||
if (_selectedLicenseIds.isEmpty) return;
|
||||
|
||||
final selectedIds = List<int>.from(_selectedLicenseIds);
|
||||
int successCount = 0;
|
||||
int failCount = 0;
|
||||
|
||||
for (var id in selectedIds) {
|
||||
try {
|
||||
await deleteLicense(id);
|
||||
successCount++;
|
||||
} catch (e) {
|
||||
failCount++;
|
||||
debugPrint('라이선스 $id 삭제 실패: $e');
|
||||
}
|
||||
}
|
||||
|
||||
_selectedLicenseIds.clear();
|
||||
await loadData();
|
||||
|
||||
if (successCount > 0) {
|
||||
debugPrint('✅ $successCount개 라이선스 삭제 완료');
|
||||
}
|
||||
if (failCount > 0) {
|
||||
debugPrint('❌ $failCount개 라이선스 삭제 실패');
|
||||
}
|
||||
}
|
||||
|
||||
// 통계 업데이트
|
||||
Future<void> _updateStatistics() async {
|
||||
try {
|
||||
final counts = await getLicenseStatusCounts();
|
||||
|
||||
final now = DateTime.now();
|
||||
int expiringSoonCount = 0;
|
||||
int expiredCount = 0;
|
||||
|
||||
for (var license in _licenses) {
|
||||
if (license.expiryDate != null) {
|
||||
final days = license.expiryDate!.difference(now).inDays;
|
||||
if (days <= 0) {
|
||||
expiredCount++;
|
||||
} else if (days <= 30) {
|
||||
expiringSoonCount++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
_statistics = {
|
||||
'total': counts['total'] ?? 0,
|
||||
'active': counts['active'] ?? 0,
|
||||
'inactive': counts['inactive'] ?? 0,
|
||||
'expiringSoon': expiringSoonCount,
|
||||
'expired': expiredCount,
|
||||
};
|
||||
} catch (e) {
|
||||
debugPrint('❌ 통계 업데이트 오류: $e');
|
||||
// 오류 발생 시 기본값 사용
|
||||
_statistics = {
|
||||
'total': _licenses.length,
|
||||
'active': 0,
|
||||
'inactive': 0,
|
||||
'expiringSoon': 0,
|
||||
'expired': 0,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// 만료일까지 남은 일수 계산
|
||||
int? getDaysUntilExpiry(License license) {
|
||||
if (license.expiryDate == null) return null;
|
||||
return license.expiryDate!.difference(DateTime.now()).inDays;
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_debounceTimer?.cancel();
|
||||
super.dispose();
|
||||
}
|
||||
}
|
||||
@@ -1,37 +1,28 @@
|
||||
import 'dart:async';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get_it/get_it.dart';
|
||||
import 'package:superport/core/errors/failures.dart';
|
||||
import 'package:superport/core/controllers/base_list_controller.dart';
|
||||
import 'package:superport/core/constants/app_constants.dart';
|
||||
import 'package:superport/core/utils/error_handler.dart';
|
||||
import 'package:superport/models/license_model.dart';
|
||||
import 'package:superport/services/license_service.dart';
|
||||
import 'package:superport/services/mock_data_service.dart';
|
||||
import 'package:superport/data/models/common/pagination_params.dart';
|
||||
|
||||
// 라이센스 상태 필터
|
||||
/// 라이센스 상태 필터
|
||||
enum LicenseStatusFilter {
|
||||
all,
|
||||
active,
|
||||
inactive,
|
||||
expiringSoon, // 30일 이내
|
||||
expiringSoon, // ${AppConstants.licenseExpiryWarningDays}일 이내
|
||||
expired,
|
||||
}
|
||||
|
||||
// 라이센스 목록 화면의 상태 및 비즈니스 로직을 담당하는 컨트롤러
|
||||
class LicenseListController extends ChangeNotifier {
|
||||
final bool useApi;
|
||||
final MockDataService? mockDataService;
|
||||
/// 라이센스 목록 화면의 상태 및 비즈니스 로직을 담당하는 컨트롤러 (리팩토링 버전)
|
||||
/// BaseListController를 상속받아 공통 기능을 재사용
|
||||
class LicenseListController extends BaseListController<License> {
|
||||
late final LicenseService _licenseService;
|
||||
|
||||
List<License> _licenses = [];
|
||||
List<License> _filteredLicenses = [];
|
||||
bool _isLoading = false;
|
||||
String? _error;
|
||||
String _searchQuery = '';
|
||||
int _currentPage = 1;
|
||||
final int _pageSize = 20;
|
||||
bool _hasMore = true;
|
||||
int _total = 0;
|
||||
|
||||
// 필터 옵션
|
||||
// 라이선스 특화 필터 상태
|
||||
int? _selectedCompanyId;
|
||||
bool? _isActive;
|
||||
String? _licenseType;
|
||||
@@ -54,207 +45,112 @@ class LicenseListController extends ChangeNotifier {
|
||||
// 검색 디바운스를 위한 타이머
|
||||
Timer? _debounceTimer;
|
||||
|
||||
LicenseListController({this.useApi = false, this.mockDataService}) {
|
||||
if (useApi && GetIt.instance.isRegistered<LicenseService>()) {
|
||||
_licenseService = GetIt.instance<LicenseService>();
|
||||
}
|
||||
}
|
||||
|
||||
// Getters
|
||||
List<License> get licenses => _filteredLicenses;
|
||||
bool get isLoading => _isLoading;
|
||||
String? get error => _error;
|
||||
String get searchQuery => _searchQuery;
|
||||
int get currentPage => _currentPage;
|
||||
bool get hasMore => _hasMore;
|
||||
int get total => _total;
|
||||
// Getters for license-specific properties
|
||||
List<License> get licenses => items;
|
||||
int? get selectedCompanyId => _selectedCompanyId;
|
||||
bool? get isActive => _isActive;
|
||||
String? get licenseType => _licenseType;
|
||||
LicenseStatusFilter get statusFilter => _statusFilter;
|
||||
Set<int> get selectedLicenseIds => _selectedLicenseIds;
|
||||
Map<String, int> get statistics => _statistics;
|
||||
|
||||
// 선택된 라이선스 개수
|
||||
int get selectedCount => _selectedLicenseIds.length;
|
||||
|
||||
// 전체 선택 여부 확인
|
||||
bool get isAllSelected =>
|
||||
_filteredLicenses.isNotEmpty &&
|
||||
_filteredLicenses.where((l) => l.id != null)
|
||||
items.isNotEmpty &&
|
||||
items.where((l) => l.id != null)
|
||||
.every((l) => _selectedLicenseIds.contains(l.id));
|
||||
|
||||
// 데이터 로드
|
||||
Future<void> loadData({bool isInitialLoad = true}) async {
|
||||
if (_isLoading) return;
|
||||
|
||||
_isLoading = true;
|
||||
_error = null;
|
||||
notifyListeners();
|
||||
|
||||
try {
|
||||
if (useApi && GetIt.instance.isRegistered<LicenseService>()) {
|
||||
// API 사용 - 전체 데이터 로드
|
||||
print('╔══════════════════════════════════════════════════════════');
|
||||
print('║ 🔧 유지보수 목록 API 호출 시작');
|
||||
print('║ • 회사 필터: ${_selectedCompanyId ?? "전체"}');
|
||||
print('║ • 활성 필터: ${_isActive != null ? (_isActive! ? "활성" : "비활성") : "전체"}');
|
||||
print('║ • 라이센스 타입: ${_licenseType ?? "전체"}');
|
||||
print('╚══════════════════════════════════════════════════════════');
|
||||
|
||||
// 전체 데이터를 가져오기 위해 큰 perPage 값 사용
|
||||
final fetchedLicenses = await _licenseService.getLicenses(
|
||||
page: 1,
|
||||
perPage: 1000, // 충분히 큰 값으로 전체 데이터 로드
|
||||
isActive: _isActive,
|
||||
companyId: _selectedCompanyId,
|
||||
licenseType: _licenseType,
|
||||
);
|
||||
|
||||
print('╔══════════════════════════════════════════════════════════');
|
||||
print('║ 📊 유지보수 목록 로드 완료');
|
||||
print('║ ▶ 총 라이센스 수: ${fetchedLicenses.length}개');
|
||||
print('╟──────────────────────────────────────────────────────────');
|
||||
|
||||
// 상태별 통계
|
||||
int activeCount = 0;
|
||||
int expiringSoonCount = 0;
|
||||
int expiredCount = 0;
|
||||
final now = DateTime.now();
|
||||
|
||||
for (final license in fetchedLicenses) {
|
||||
if (license.expiryDate != null) {
|
||||
final daysUntil = license.expiryDate!.difference(now).inDays;
|
||||
if (daysUntil < 0) {
|
||||
expiredCount++;
|
||||
} else if (daysUntil <= 30) {
|
||||
expiringSoonCount++;
|
||||
} else {
|
||||
activeCount++;
|
||||
}
|
||||
} else {
|
||||
activeCount++;
|
||||
}
|
||||
}
|
||||
|
||||
print('║ • 활성: $activeCount개');
|
||||
print('║ • 만료 임박 (30일 이내): $expiringSoonCount개');
|
||||
print('║ • 만료됨: $expiredCount개');
|
||||
|
||||
print('╟──────────────────────────────────────────────────────────');
|
||||
print('║ 📑 전체 데이터 로드 완료');
|
||||
print('║ • View에서 페이지네이션 처리 예정');
|
||||
print('╚══════════════════════════════════════════════════════════');
|
||||
|
||||
_licenses = fetchedLicenses;
|
||||
_hasMore = false; // 전체 데이터를 로드했으므로 더 이상 로드할 필요 없음
|
||||
_total = fetchedLicenses.length;
|
||||
} else {
|
||||
// Mock 데이터 사용
|
||||
final allLicenses = mockDataService?.getAllLicenses() ?? [];
|
||||
|
||||
// 필터링 적용
|
||||
var filtered = allLicenses;
|
||||
if (_selectedCompanyId != null) {
|
||||
filtered = filtered.where((l) => l.companyId == _selectedCompanyId).toList();
|
||||
}
|
||||
|
||||
// 페이지네이션 적용
|
||||
final startIndex = (_currentPage - 1) * _pageSize;
|
||||
final endIndex = startIndex + _pageSize;
|
||||
|
||||
if (startIndex < filtered.length) {
|
||||
final pageLicenses = filtered.sublist(
|
||||
startIndex,
|
||||
endIndex > filtered.length ? filtered.length : endIndex,
|
||||
);
|
||||
|
||||
if (isInitialLoad) {
|
||||
_licenses = pageLicenses;
|
||||
} else {
|
||||
_licenses.addAll(pageLicenses);
|
||||
}
|
||||
|
||||
_hasMore = endIndex < filtered.length;
|
||||
} else {
|
||||
_hasMore = false;
|
||||
}
|
||||
|
||||
_total = filtered.length;
|
||||
}
|
||||
|
||||
debugPrint('📑 _applySearchFilter 호출 전: _licenses=${_licenses.length}개');
|
||||
_applySearchFilter();
|
||||
_applyStatusFilter();
|
||||
await _updateStatistics();
|
||||
debugPrint('📑 _applySearchFilter 호출 후: _filteredLicenses=${_filteredLicenses.length}개');
|
||||
} catch (e) {
|
||||
debugPrint('❌ loadData 에러 발생: $e');
|
||||
_error = e.toString();
|
||||
} finally {
|
||||
_isLoading = false;
|
||||
debugPrint('📑 loadData 종료: _filteredLicenses=${_filteredLicenses.length}개');
|
||||
notifyListeners();
|
||||
LicenseListController() {
|
||||
if (GetIt.instance.isRegistered<LicenseService>()) {
|
||||
_licenseService = GetIt.instance<LicenseService>();
|
||||
} else {
|
||||
throw Exception('LicenseService not registered in GetIt');
|
||||
}
|
||||
}
|
||||
|
||||
// 다음 페이지 로드
|
||||
Future<void> loadNextPage() async {
|
||||
if (!_hasMore || _isLoading) return;
|
||||
_currentPage++;
|
||||
await loadData(isInitialLoad: false);
|
||||
@override
|
||||
Future<PagedResult<License>> fetchData({
|
||||
required PaginationParams params,
|
||||
Map<String, dynamic>? additionalFilters,
|
||||
}) async {
|
||||
// API 호출
|
||||
final fetchedLicenses = await ErrorHandler.handleApiCall(
|
||||
() => _licenseService.getLicenses(
|
||||
page: params.page,
|
||||
perPage: params.perPage,
|
||||
isActive: _isActive,
|
||||
companyId: _selectedCompanyId,
|
||||
licenseType: _licenseType,
|
||||
),
|
||||
onError: (failure) {
|
||||
throw failure;
|
||||
},
|
||||
);
|
||||
|
||||
if (fetchedLicenses == null) {
|
||||
return PagedResult(
|
||||
items: [],
|
||||
meta: PaginationMeta(
|
||||
currentPage: params.page,
|
||||
perPage: params.perPage,
|
||||
total: 0,
|
||||
totalPages: 0,
|
||||
hasNext: false,
|
||||
hasPrevious: false,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// 통계 업데이트
|
||||
await _updateStatistics(fetchedLicenses);
|
||||
|
||||
// 임시로 메타데이터 생성 (추후 API에서 실제 메타데이터 반환 시 수정)
|
||||
final meta = PaginationMeta(
|
||||
currentPage: params.page,
|
||||
perPage: params.perPage,
|
||||
total: fetchedLicenses.length < params.perPage ?
|
||||
(params.page - 1) * params.perPage + fetchedLicenses.length :
|
||||
params.page * params.perPage + 1,
|
||||
totalPages: fetchedLicenses.length < params.perPage ? params.page : params.page + 1,
|
||||
hasNext: fetchedLicenses.length >= params.perPage,
|
||||
hasPrevious: params.page > 1,
|
||||
);
|
||||
|
||||
return PagedResult(items: fetchedLicenses, meta: meta);
|
||||
}
|
||||
|
||||
// 검색 (디바운싱 적용)
|
||||
@override
|
||||
bool filterItem(License item, String query) {
|
||||
final q = query.toLowerCase();
|
||||
return (item.productName?.toLowerCase().contains(q) ?? false) ||
|
||||
(item.licenseKey.toLowerCase().contains(q)) ||
|
||||
(item.vendor?.toLowerCase().contains(q) ?? false) ||
|
||||
(item.companyName?.toLowerCase().contains(q) ?? false);
|
||||
}
|
||||
|
||||
/// BaseListController의 검색을 오버라이드하여 디바운싱 적용
|
||||
@override
|
||||
void search(String query) {
|
||||
_searchQuery = query;
|
||||
|
||||
// 기존 타이머 취소
|
||||
_debounceTimer?.cancel();
|
||||
|
||||
// Mock 데이터는 즉시 검색
|
||||
if (!useApi) {
|
||||
_applySearchFilter();
|
||||
notifyListeners();
|
||||
return;
|
||||
}
|
||||
|
||||
// API 검색은 디바운싱 적용 (300ms)
|
||||
_debounceTimer = Timer(const Duration(milliseconds: 300), () {
|
||||
loadData();
|
||||
// 디바운싱 적용 (300ms)
|
||||
_debounceTimer = Timer(AppConstants.licenseSearchDebounce, () {
|
||||
super.search(query);
|
||||
_applyStatusFilter();
|
||||
});
|
||||
}
|
||||
|
||||
// 검색 필터 적용
|
||||
void _applySearchFilter() {
|
||||
debugPrint('🔎 _applySearchFilter 시작: _searchQuery="$_searchQuery", _licenses=${_licenses.length}개');
|
||||
|
||||
if (_searchQuery.isEmpty) {
|
||||
_filteredLicenses = List.from(_licenses);
|
||||
debugPrint('🔎 검색어 없음: 전체 복사 ${_filteredLicenses.length}개');
|
||||
} else {
|
||||
_filteredLicenses = _licenses.where((license) {
|
||||
final productName = license.productName?.toLowerCase() ?? '';
|
||||
final licenseKey = license.licenseKey.toLowerCase();
|
||||
final vendor = license.vendor?.toLowerCase() ?? '';
|
||||
final companyName = license.companyName?.toLowerCase() ?? '';
|
||||
final searchLower = _searchQuery.toLowerCase();
|
||||
|
||||
return productName.contains(searchLower) ||
|
||||
licenseKey.contains(searchLower) ||
|
||||
vendor.contains(searchLower) ||
|
||||
companyName.contains(searchLower);
|
||||
}).toList();
|
||||
debugPrint('🔎 검색 필터링 완료: ${_filteredLicenses.length}개');
|
||||
}
|
||||
}
|
||||
|
||||
// 상태 필터 적용
|
||||
/// 상태 필터 적용 (BaseListController의 filtering과 추가로 동작)
|
||||
void _applyStatusFilter() {
|
||||
if (_statusFilter == LicenseStatusFilter.all) return;
|
||||
|
||||
final now = DateTime.now();
|
||||
_filteredLicenses = _filteredLicenses.where((license) {
|
||||
final currentItems = List<License>.from(items);
|
||||
|
||||
// 상태 필터 적용
|
||||
final filteredByStatus = currentItems.where((license) {
|
||||
switch (_statusFilter) {
|
||||
case LicenseStatusFilter.active:
|
||||
return license.isActive;
|
||||
@@ -276,9 +172,13 @@ class LicenseListController extends ChangeNotifier {
|
||||
return true;
|
||||
}
|
||||
}).toList();
|
||||
|
||||
// 직접 필터링된 결과를 적용 (BaseListController의 private 필드에 접근할 수 없으므로)
|
||||
// 대신 notifyListeners를 통해 UI 업데이트
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
// 필터 설정
|
||||
/// 필터 설정
|
||||
void setFilters({
|
||||
int? companyId,
|
||||
bool? isActive,
|
||||
@@ -287,135 +187,48 @@ class LicenseListController extends ChangeNotifier {
|
||||
_selectedCompanyId = companyId;
|
||||
_isActive = isActive;
|
||||
_licenseType = licenseType;
|
||||
loadData();
|
||||
loadData(isRefresh: true);
|
||||
}
|
||||
|
||||
// 필터 초기화
|
||||
/// 필터 초기화
|
||||
void clearFilters() {
|
||||
_selectedCompanyId = null;
|
||||
_isActive = null;
|
||||
_licenseType = null;
|
||||
_searchQuery = '';
|
||||
loadData();
|
||||
_statusFilter = LicenseStatusFilter.all;
|
||||
search(''); // BaseListController의 search 호출
|
||||
}
|
||||
|
||||
// 라이센스 삭제
|
||||
Future<void> deleteLicense(int id) async {
|
||||
try {
|
||||
if (useApi && GetIt.instance.isRegistered<LicenseService>()) {
|
||||
await _licenseService.deleteLicense(id);
|
||||
} else {
|
||||
mockDataService?.deleteLicense(id);
|
||||
}
|
||||
|
||||
// 목록에서 제거
|
||||
_licenses.removeWhere((l) => l.id == id);
|
||||
_applySearchFilter();
|
||||
_total--;
|
||||
notifyListeners();
|
||||
} catch (e) {
|
||||
_error = e.toString();
|
||||
notifyListeners();
|
||||
}
|
||||
/// 상태 필터 변경
|
||||
Future<void> changeStatusFilter(LicenseStatusFilter filter) async {
|
||||
_statusFilter = filter;
|
||||
_applyStatusFilter();
|
||||
}
|
||||
|
||||
// 새로고침
|
||||
Future<void> refresh() async {
|
||||
await loadData();
|
||||
}
|
||||
|
||||
// 만료 예정 라이선스 조회
|
||||
Future<List<License>> getExpiringLicenses({int days = 30}) async {
|
||||
try {
|
||||
if (useApi && GetIt.instance.isRegistered<LicenseService>()) {
|
||||
return await _licenseService.getExpiringLicenses(days: days);
|
||||
} else {
|
||||
// Mock 데이터에서 만료 예정 라이선스 필터링
|
||||
final now = DateTime.now();
|
||||
final allLicenses = mockDataService?.getAllLicenses() ?? [];
|
||||
|
||||
return allLicenses.where((license) {
|
||||
// 실제 License 모델에서 만료일 확인
|
||||
if (license.expiryDate != null) {
|
||||
final daysUntilExpiry = license.expiryDate!.difference(now).inDays;
|
||||
return daysUntilExpiry > 0 && daysUntilExpiry <= days;
|
||||
}
|
||||
return false;
|
||||
}).toList();
|
||||
}
|
||||
} catch (e) {
|
||||
_error = e.toString();
|
||||
notifyListeners();
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
// 상태별 라이선스 개수 조회
|
||||
Future<Map<String, int>> getLicenseStatusCounts() async {
|
||||
if (useApi && GetIt.instance.isRegistered<LicenseService>()) {
|
||||
try {
|
||||
// API에서 상태별 개수 조회 (실제로는 별도 엔드포인트가 있다면 사용)
|
||||
final activeCount = await _licenseService.getTotalLicenses(isActive: true);
|
||||
final inactiveCount = await _licenseService.getTotalLicenses(isActive: false);
|
||||
final expiringLicenses = await getExpiringLicenses(days: 30);
|
||||
|
||||
return {
|
||||
'active': activeCount,
|
||||
'inactive': inactiveCount,
|
||||
'expiring': expiringLicenses.length,
|
||||
'total': activeCount + inactiveCount,
|
||||
};
|
||||
} catch (e) {
|
||||
return {'active': 0, 'inactive': 0, 'expiring': 0, 'total': 0};
|
||||
}
|
||||
} else {
|
||||
// Mock 데이터에서 계산
|
||||
final allLicenses = mockDataService?.getAllLicenses() ?? [];
|
||||
final now = DateTime.now();
|
||||
|
||||
int activeCount = 0;
|
||||
int expiredCount = 0;
|
||||
int expiringCount = 0;
|
||||
|
||||
for (var license in allLicenses) {
|
||||
if (license.isActive) {
|
||||
activeCount++;
|
||||
|
||||
if (license.expiryDate != null) {
|
||||
final daysUntilExpiry = license.expiryDate!.difference(now).inDays;
|
||||
if (daysUntilExpiry <= 0) {
|
||||
expiredCount++;
|
||||
} else if (daysUntilExpiry <= 30) {
|
||||
expiringCount++;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
'active': activeCount,
|
||||
'inactive': allLicenses.length - activeCount,
|
||||
'expiring': expiringCount,
|
||||
'expired': expiredCount,
|
||||
'total': allLicenses.length,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// 정렬 변경
|
||||
/// 정렬 변경
|
||||
void sortBy(String field, String order) {
|
||||
_sortBy = field;
|
||||
_sortOrder = order;
|
||||
loadData();
|
||||
loadData(isRefresh: true);
|
||||
}
|
||||
|
||||
// 상태 필터 변경
|
||||
Future<void> changeStatusFilter(LicenseStatusFilter filter) async {
|
||||
_statusFilter = filter;
|
||||
await loadData();
|
||||
|
||||
/// 라이선스 삭제 (BaseListController의 기본 기능 활용)
|
||||
Future<void> deleteLicense(int id) async {
|
||||
await ErrorHandler.handleApiCall<void>(
|
||||
() => _licenseService.deleteLicense(id),
|
||||
onError: (failure) {
|
||||
throw failure;
|
||||
},
|
||||
);
|
||||
|
||||
// BaseListController의 removeItemLocally 활용
|
||||
removeItemLocally((l) => l.id == id);
|
||||
|
||||
// 선택 목록에서도 제거
|
||||
_selectedLicenseIds.remove(id);
|
||||
}
|
||||
|
||||
// 라이선스 선택/해제
|
||||
|
||||
/// 라이선스 선택/해제
|
||||
void selectLicense(int? id, bool? isSelected) {
|
||||
if (id == null) return;
|
||||
|
||||
@@ -427,11 +240,11 @@ class LicenseListController extends ChangeNotifier {
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
// 전체 선택/해제
|
||||
/// 전체 선택/해제
|
||||
void selectAll(bool? isSelected) {
|
||||
if (isSelected == true) {
|
||||
// 현재 필터링된 라이선스 모두 선택
|
||||
for (var license in _filteredLicenses) {
|
||||
for (var license in items) {
|
||||
if (license.id != null) {
|
||||
_selectedLicenseIds.add(license.id!);
|
||||
}
|
||||
@@ -443,131 +256,126 @@ class LicenseListController extends ChangeNotifier {
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
// 선택된 라이선스 목록 반환
|
||||
/// 선택된 라이선스 목록 반환
|
||||
List<License> getSelectedLicenses() {
|
||||
return _filteredLicenses
|
||||
return items
|
||||
.where((l) => l.id != null && _selectedLicenseIds.contains(l.id))
|
||||
.toList();
|
||||
}
|
||||
|
||||
// 선택 초기화
|
||||
/// 선택 초기화 (BaseListController에도 있지만 라이선스 특화)
|
||||
@override
|
||||
void clearSelection() {
|
||||
_selectedLicenseIds.clear();
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
// 라이선스 할당
|
||||
Future<bool> assignLicense(int licenseId, int userId) async {
|
||||
try {
|
||||
if (useApi && GetIt.instance.isRegistered<LicenseService>()) {
|
||||
await _licenseService.assignLicense(licenseId, userId);
|
||||
await loadData();
|
||||
clearSelection();
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
} catch (e) {
|
||||
_error = e.toString();
|
||||
notifyListeners();
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// 라이선스 할당 해제
|
||||
Future<bool> unassignLicense(int licenseId) async {
|
||||
try {
|
||||
if (useApi && GetIt.instance.isRegistered<LicenseService>()) {
|
||||
await _licenseService.unassignLicense(licenseId);
|
||||
await loadData();
|
||||
clearSelection();
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
} catch (e) {
|
||||
_error = e.toString();
|
||||
notifyListeners();
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// 선택된 라이선스 일괄 삭제
|
||||
|
||||
/// 선택된 라이선스 일괄 삭제
|
||||
Future<void> deleteSelectedLicenses() async {
|
||||
if (_selectedLicenseIds.isEmpty) return;
|
||||
|
||||
final selectedIds = List<int>.from(_selectedLicenseIds);
|
||||
int successCount = 0;
|
||||
int failCount = 0;
|
||||
|
||||
for (var id in selectedIds) {
|
||||
try {
|
||||
await deleteLicense(id);
|
||||
successCount++;
|
||||
} catch (e) {
|
||||
failCount++;
|
||||
debugPrint('라이선스 $id 삭제 실패: $e');
|
||||
}
|
||||
}
|
||||
|
||||
_selectedLicenseIds.clear();
|
||||
await loadData();
|
||||
|
||||
if (successCount > 0) {
|
||||
debugPrint('✅ $successCount개 라이선스 삭제 완료');
|
||||
}
|
||||
if (failCount > 0) {
|
||||
debugPrint('❌ $failCount개 라이선스 삭제 실패');
|
||||
for (final id in _selectedLicenseIds.toList()) {
|
||||
await deleteLicense(id);
|
||||
}
|
||||
clearSelection();
|
||||
}
|
||||
|
||||
// 통계 업데이트
|
||||
Future<void> _updateStatistics() async {
|
||||
try {
|
||||
final counts = await getLicenseStatusCounts();
|
||||
|
||||
final now = DateTime.now();
|
||||
int expiringSoonCount = 0;
|
||||
int expiredCount = 0;
|
||||
|
||||
for (var license in _licenses) {
|
||||
if (license.expiryDate != null) {
|
||||
final days = license.expiryDate!.difference(now).inDays;
|
||||
if (days <= 0) {
|
||||
expiredCount++;
|
||||
} else if (days <= 30) {
|
||||
expiringSoonCount++;
|
||||
}
|
||||
|
||||
/// 라이선스 생성
|
||||
Future<void> createLicense(License license) async {
|
||||
await ErrorHandler.handleApiCall<void>(
|
||||
() => _licenseService.createLicense(license),
|
||||
onError: (failure) {
|
||||
throw failure;
|
||||
},
|
||||
);
|
||||
|
||||
await refresh();
|
||||
}
|
||||
|
||||
/// 라이선스 수정
|
||||
Future<void> updateLicense(License license) async {
|
||||
await ErrorHandler.handleApiCall<void>(
|
||||
() => _licenseService.updateLicense(license),
|
||||
onError: (failure) {
|
||||
throw failure;
|
||||
},
|
||||
);
|
||||
|
||||
updateItemLocally(license, (l) => l.id == license.id);
|
||||
}
|
||||
|
||||
/// 라이선스 활성화/비활성화 토글
|
||||
Future<void> toggleLicenseStatus(int id) async {
|
||||
final license = items.firstWhere((l) => l.id == id);
|
||||
final updatedLicense = license.copyWith(isActive: !license.isActive);
|
||||
|
||||
await updateLicense(updatedLicense);
|
||||
}
|
||||
|
||||
/// 통계 데이터 업데이트
|
||||
Future<void> _updateStatistics(List<License> licenses) async {
|
||||
final now = DateTime.now();
|
||||
|
||||
_statistics = {
|
||||
'total': licenses.length,
|
||||
'active': licenses.where((l) => l.isActive).length,
|
||||
'inactive': licenses.where((l) => !l.isActive).length,
|
||||
'expiringSoon': licenses.where((l) {
|
||||
if (l.expiryDate != null) {
|
||||
final days = l.expiryDate!.difference(now).inDays;
|
||||
return days > 0 && days <= 30;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}).length,
|
||||
'expired': licenses.where((l) {
|
||||
if (l.expiryDate != null) {
|
||||
return l.expiryDate!.isBefore(now);
|
||||
}
|
||||
return false;
|
||||
}).length,
|
||||
};
|
||||
}
|
||||
|
||||
/// 라이선스 만료일별 그룹핑
|
||||
Map<String, List<License>> getLicensesByExpiryPeriod() {
|
||||
final now = DateTime.now();
|
||||
final Map<String, List<License>> grouped = {
|
||||
'이미 만료': [],
|
||||
'${AppConstants.licenseExpiryWarningDays}일 이내': [],
|
||||
'${AppConstants.licenseExpiryCautionDays}일 이내': [],
|
||||
'${AppConstants.licenseExpiryInfoDays}일 이내': [],
|
||||
'${AppConstants.licenseExpiryInfoDays}일 이후': [],
|
||||
};
|
||||
|
||||
for (final license in items) {
|
||||
if (license.expiryDate == null) continue;
|
||||
|
||||
_statistics = {
|
||||
'total': counts['total'] ?? 0,
|
||||
'active': counts['active'] ?? 0,
|
||||
'inactive': counts['inactive'] ?? 0,
|
||||
'expiringSoon': expiringSoonCount,
|
||||
'expired': expiredCount,
|
||||
};
|
||||
} catch (e) {
|
||||
debugPrint('❌ 통계 업데이트 오류: $e');
|
||||
// 오류 발생 시 기본값 사용
|
||||
_statistics = {
|
||||
'total': _licenses.length,
|
||||
'active': 0,
|
||||
'inactive': 0,
|
||||
'expiringSoon': 0,
|
||||
'expired': 0,
|
||||
};
|
||||
final days = license.expiryDate!.difference(now).inDays;
|
||||
|
||||
if (days < 0) {
|
||||
grouped['이미 만료']!.add(license);
|
||||
} else if (days <= AppConstants.licenseExpiryWarningDays) {
|
||||
grouped['${AppConstants.licenseExpiryWarningDays}일 이내']!.add(license);
|
||||
} else if (days <= AppConstants.licenseExpiryCautionDays) {
|
||||
grouped['${AppConstants.licenseExpiryCautionDays}일 이내']!.add(license);
|
||||
} else if (days <= AppConstants.licenseExpiryInfoDays) {
|
||||
grouped['${AppConstants.licenseExpiryInfoDays}일 이내']!.add(license);
|
||||
} else {
|
||||
grouped['${AppConstants.licenseExpiryInfoDays}일 이후']!.add(license);
|
||||
}
|
||||
}
|
||||
|
||||
return grouped;
|
||||
}
|
||||
|
||||
// 만료일까지 남은 일수 계산
|
||||
int? getDaysUntilExpiry(License license) {
|
||||
if (license.expiryDate == null) return null;
|
||||
return license.expiryDate!.difference(DateTime.now()).inDays;
|
||||
|
||||
/// 만료까지 남은 날짜 계산
|
||||
int getDaysUntilExpiry(DateTime? expiryDate) {
|
||||
if (expiryDate == null) return 999; // 만료일이 없으면 큰 숫자 반환
|
||||
final now = DateTime.now();
|
||||
return expiryDate.difference(now).inDays;
|
||||
}
|
||||
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_debounceTimer?.cancel();
|
||||
super.dispose();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,283 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import '../../../core/controllers/base_list_controller.dart';
|
||||
import '../../../core/utils/error_handler.dart';
|
||||
import '../../../data/models/common/pagination_params.dart';
|
||||
import '../../../data/models/license/license_dto.dart';
|
||||
import '../../../domain/usecases/license/license_usecases.dart';
|
||||
|
||||
/// UseCase 패턴을 적용한 라이선스 목록 컨트롤러
|
||||
class LicenseListControllerWithUseCase extends BaseListController<LicenseDto> {
|
||||
final GetLicensesUseCase getLicensesUseCase;
|
||||
final CreateLicenseUseCase createLicenseUseCase;
|
||||
final UpdateLicenseUseCase updateLicenseUseCase;
|
||||
final DeleteLicenseUseCase deleteLicenseUseCase;
|
||||
final CheckLicenseExpiryUseCase checkLicenseExpiryUseCase;
|
||||
|
||||
// 선택된 항목들
|
||||
final Set<int> _selectedLicenseIds = {};
|
||||
Set<int> get selectedLicenseIds => _selectedLicenseIds;
|
||||
|
||||
// 필터 옵션
|
||||
String? _filterByCompany;
|
||||
String? _filterByExpiry;
|
||||
DateTime? _filterStartDate;
|
||||
DateTime? _filterEndDate;
|
||||
|
||||
String? get filterByCompany => _filterByCompany;
|
||||
String? get filterByExpiry => _filterByExpiry;
|
||||
DateTime? get filterStartDate => _filterStartDate;
|
||||
DateTime? get filterEndDate => _filterEndDate;
|
||||
|
||||
// 만료 임박 라이선스 정보
|
||||
LicenseExpiryResult? _expiryResult;
|
||||
LicenseExpiryResult? get expiryResult => _expiryResult;
|
||||
|
||||
LicenseListControllerWithUseCase({
|
||||
required this.getLicensesUseCase,
|
||||
required this.createLicenseUseCase,
|
||||
required this.updateLicenseUseCase,
|
||||
required this.deleteLicenseUseCase,
|
||||
required this.checkLicenseExpiryUseCase,
|
||||
});
|
||||
|
||||
@override
|
||||
Future<PagedResult<LicenseDto>> fetchData({
|
||||
required PaginationParams params,
|
||||
Map<String, dynamic>? additionalFilters,
|
||||
}) async {
|
||||
try {
|
||||
// 필터 파라미터 구성
|
||||
final filters = <String, dynamic>{};
|
||||
if (_filterByCompany != null) filters['company_id'] = _filterByCompany;
|
||||
if (_filterByExpiry != null) filters['expiry'] = _filterByExpiry;
|
||||
if (_filterStartDate != null) filters['start_date'] = _filterStartDate!.toIso8601String();
|
||||
if (_filterEndDate != null) filters['end_date'] = _filterEndDate!.toIso8601String();
|
||||
|
||||
final updatedParams = params.copyWith(filters: filters);
|
||||
final getParams = GetLicensesParams.fromPaginationParams(updatedParams);
|
||||
|
||||
final result = await getLicensesUseCase(getParams);
|
||||
|
||||
return result.fold(
|
||||
(failure) => throw Exception(failure.message),
|
||||
(licenseResponse) {
|
||||
// PagedResult로 래핑하여 반환
|
||||
final meta = PaginationMeta(
|
||||
currentPage: params.page,
|
||||
perPage: params.perPage,
|
||||
total: licenseResponse.items.length, // 실제로는 서버에서 받아와야 함
|
||||
totalPages: (licenseResponse.items.length / params.perPage).ceil(),
|
||||
hasNext: licenseResponse.items.length >= params.perPage,
|
||||
hasPrevious: params.page > 1,
|
||||
);
|
||||
return PagedResult(items: licenseResponse.items, meta: meta);
|
||||
},
|
||||
);
|
||||
} catch (e) {
|
||||
throw Exception('데이터 로드 실패: $e');
|
||||
}
|
||||
}
|
||||
|
||||
/// 만료 임박 라이선스 체크
|
||||
Future<void> checkExpiringLicenses() async {
|
||||
try {
|
||||
final params = CheckLicenseExpiryParams(
|
||||
companyId: _filterByCompany != null ? int.tryParse(_filterByCompany!) : null,
|
||||
);
|
||||
|
||||
final result = await checkLicenseExpiryUseCase(params);
|
||||
|
||||
result.fold(
|
||||
(failure) => errorState = failure.message,
|
||||
(expiryResult) {
|
||||
_expiryResult = expiryResult;
|
||||
notifyListeners();
|
||||
},
|
||||
);
|
||||
} catch (e) {
|
||||
errorState = '라이선스 만료 체크 실패: $e';
|
||||
}
|
||||
}
|
||||
|
||||
/// 라이선스 생성
|
||||
Future<void> createLicense({
|
||||
required int equipmentId,
|
||||
required int companyId,
|
||||
required String licenseType,
|
||||
required DateTime startDate,
|
||||
required DateTime expiryDate,
|
||||
String? description,
|
||||
double? cost,
|
||||
}) async {
|
||||
try {
|
||||
isLoadingState = true;
|
||||
|
||||
final params = CreateLicenseParams(
|
||||
equipmentId: equipmentId,
|
||||
companyId: companyId,
|
||||
licenseType: licenseType,
|
||||
startDate: startDate,
|
||||
expiryDate: expiryDate,
|
||||
description: description,
|
||||
cost: cost,
|
||||
);
|
||||
|
||||
final result = await createLicenseUseCase(params);
|
||||
|
||||
await result.fold(
|
||||
(failure) async => errorState = failure.message,
|
||||
(license) async {
|
||||
await refresh();
|
||||
await checkExpiringLicenses();
|
||||
},
|
||||
);
|
||||
} catch (e) {
|
||||
errorState = '오류 생성: $e';
|
||||
} finally {
|
||||
isLoadingState = false;
|
||||
}
|
||||
}
|
||||
|
||||
/// 라이선스 수정
|
||||
Future<void> updateLicense({
|
||||
required int id,
|
||||
int? equipmentId,
|
||||
int? companyId,
|
||||
String? licenseType,
|
||||
DateTime? startDate,
|
||||
DateTime? expiryDate,
|
||||
String? description,
|
||||
double? cost,
|
||||
String? status,
|
||||
}) async {
|
||||
try {
|
||||
isLoadingState = true;
|
||||
|
||||
final params = UpdateLicenseParams(
|
||||
id: id,
|
||||
equipmentId: equipmentId,
|
||||
companyId: companyId,
|
||||
licenseType: licenseType,
|
||||
startDate: startDate,
|
||||
expiryDate: expiryDate,
|
||||
description: description,
|
||||
cost: cost,
|
||||
status: status,
|
||||
);
|
||||
|
||||
final result = await updateLicenseUseCase(params);
|
||||
|
||||
await result.fold(
|
||||
(failure) async => errorState = failure.message,
|
||||
(license) async {
|
||||
updateItemLocally(license, (item) => item.id == license.id);
|
||||
await checkExpiringLicenses();
|
||||
},
|
||||
);
|
||||
} catch (e) {
|
||||
errorState = '오류 생성: $e';
|
||||
} finally {
|
||||
isLoadingState = false;
|
||||
}
|
||||
}
|
||||
|
||||
/// 라이선스 삭제
|
||||
Future<void> deleteLicense(int id) async {
|
||||
try {
|
||||
isLoadingState = true;
|
||||
|
||||
final result = await deleteLicenseUseCase(id);
|
||||
|
||||
await result.fold(
|
||||
(failure) async => errorState = failure.message,
|
||||
(_) async {
|
||||
removeItemLocally((item) => item.id == id);
|
||||
_selectedLicenseIds.remove(id);
|
||||
await checkExpiringLicenses();
|
||||
},
|
||||
);
|
||||
} catch (e) {
|
||||
errorState = '오류 생성: $e';
|
||||
} finally {
|
||||
isLoadingState = false;
|
||||
}
|
||||
}
|
||||
|
||||
/// 필터 설정
|
||||
void setFilters({
|
||||
String? company,
|
||||
String? expiry,
|
||||
DateTime? startDate,
|
||||
DateTime? endDate,
|
||||
}) {
|
||||
_filterByCompany = company;
|
||||
_filterByExpiry = expiry;
|
||||
_filterStartDate = startDate;
|
||||
_filterEndDate = endDate;
|
||||
refresh();
|
||||
}
|
||||
|
||||
/// 필터 초기화
|
||||
void clearFilters() {
|
||||
_filterByCompany = null;
|
||||
_filterByExpiry = null;
|
||||
_filterStartDate = null;
|
||||
_filterEndDate = null;
|
||||
refresh();
|
||||
}
|
||||
|
||||
/// 라이선스 선택 토글
|
||||
void toggleLicenseSelection(int id) {
|
||||
if (_selectedLicenseIds.contains(id)) {
|
||||
_selectedLicenseIds.remove(id);
|
||||
} else {
|
||||
_selectedLicenseIds.add(id);
|
||||
}
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
/// 모든 라이선스 선택
|
||||
void selectAll() {
|
||||
_selectedLicenseIds.clear();
|
||||
_selectedLicenseIds.addAll(items.map((e) => e.id));
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
/// 선택 해제
|
||||
void clearSelection() {
|
||||
_selectedLicenseIds.clear();
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
/// 선택된 라이선스 일괄 삭제
|
||||
Future<void> deleteSelectedLicenses() async {
|
||||
if (_selectedLicenseIds.isEmpty) return;
|
||||
|
||||
try {
|
||||
isLoadingState = true;
|
||||
|
||||
for (final id in _selectedLicenseIds.toList()) {
|
||||
final result = await deleteLicenseUseCase(id);
|
||||
result.fold(
|
||||
(failure) => print('Failed to delete license $id: ${failure.message}'),
|
||||
(_) => removeItemLocally((item) => item.id == id),
|
||||
);
|
||||
}
|
||||
|
||||
_selectedLicenseIds.clear();
|
||||
await checkExpiringLicenses();
|
||||
notifyListeners();
|
||||
} catch (e) {
|
||||
errorState = '오류 생성: $e';
|
||||
} finally {
|
||||
isLoadingState = false;
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_selectedLicenseIds.clear();
|
||||
_expiryResult = null;
|
||||
super.dispose();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user