refactor: Clean Architecture 적용 및 코드베이스 전면 리팩토링
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

## 주요 변경사항

### 아키텍처 개선
- Clean Architecture 패턴 적용 (Domain, Data, Presentation 레이어 분리)
- Use Case 패턴 도입으로 비즈니스 로직 캡슐화
- Repository 패턴으로 데이터 접근 추상화
- 의존성 주입 구조 개선

### 상태 관리 최적화
- 모든 Controller에서 불필요한 상태 관리 로직 제거
- 페이지네이션 로직 통일 및 간소화
- 에러 처리 로직 개선 (에러 메시지 한글화)
- 로딩 상태 관리 최적화

### Mock 서비스 제거
- MockDataService 완전 제거
- 모든 화면을 실제 API 전용으로 전환
- 불필요한 Mock 관련 코드 정리

### UI/UX 개선
- Overview 화면 대시보드 기능 강화
- 라이선스 만료 알림 위젯 추가
- 사이드바 네비게이션 개선
- 일관된 UI 컴포넌트 사용

### 코드 품질
- 중복 코드 제거 및 함수 추출
- 파일별 책임 분리 명확화
- 테스트 코드 업데이트

## 영향 범위
- 모든 화면의 Controller 리팩토링
- API 통신 레이어 구조 개선
- 에러 처리 및 로깅 시스템 개선

## 향후 계획
- 단위 테스트 커버리지 확대
- 통합 테스트 시나리오 추가
- 성능 모니터링 도구 통합
This commit is contained in:
JiWoong Sul
2025-08-11 00:04:28 +09:00
parent 6b5d126990
commit 162fe08618
113 changed files with 11072 additions and 3319 deletions

View File

@@ -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;

View File

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

View File

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

View File

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