refactor: 프로젝트 구조 개선 및 테스트 시스템 강화
주요 변경사항: - CLAUDE.md: 프로젝트 규칙 v2.0으로 업데이트, 아키텍처 명확화 - 불필요한 문서 제거: NEXT_TASKS.md, TEST_PROGRESS.md, test_results 파일들 - 테스트 시스템 개선: 실제 API 테스트 스위트 추가 (15개 새 테스트 파일) - License 관리: DTO 모델 개선, API 응답 처리 최적화 - 에러 처리: Interceptor 로직 강화, 상세 로깅 추가 - Company/User/Warehouse 테스트: 자동화 테스트 안정성 향상 - Phone Utils: 전화번호 포맷팅 로직 개선 - Overview Controller: 대시보드 데이터 로딩 최적화 - Analysis Options: Flutter 린트 규칙 추가 테스트 개선: - company_real_api_test.dart: 실제 API 회사 관리 테스트 - equipment_in/out_real_api_test.dart: 장비 입출고 API 테스트 - license_real_api_test.dart: 라이선스 관리 API 테스트 - user_real_api_test.dart: 사용자 관리 API 테스트 - warehouse_location_real_api_test.dart: 창고 위치 API 테스트 - filter_sort_test.dart: 필터링/정렬 기능 테스트 - pagination_test.dart: 페이지네이션 테스트 - interactive_search_test.dart: 검색 기능 테스트 - overview_dashboard_test.dart: 대시보드 통합 테스트 코드 품질: - 모든 서비스에 에러 처리 강화 - DTO 모델 null safety 개선 - 테스트 커버리지 확대 - 불필요한 로그 파일 제거로 리포지토리 정리 Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -6,6 +6,15 @@ import 'package:superport/models/license_model.dart';
|
||||
import 'package:superport/services/license_service.dart';
|
||||
import 'package:superport/services/mock_data_service.dart';
|
||||
|
||||
// 라이센스 상태 필터
|
||||
enum LicenseStatusFilter {
|
||||
all,
|
||||
active,
|
||||
inactive,
|
||||
expiringSoon, // 30일 이내
|
||||
expired,
|
||||
}
|
||||
|
||||
// 라이센스 목록 화면의 상태 및 비즈니스 로직을 담당하는 컨트롤러
|
||||
class LicenseListController extends ChangeNotifier {
|
||||
final bool useApi;
|
||||
@@ -26,9 +35,22 @@ class LicenseListController extends ChangeNotifier {
|
||||
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;
|
||||
|
||||
@@ -49,6 +71,18 @@ class LicenseListController extends ChangeNotifier {
|
||||
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 {
|
||||
@@ -67,6 +101,8 @@ class LicenseListController extends ChangeNotifier {
|
||||
|
||||
try {
|
||||
if (useApi && GetIt.instance.isRegistered<LicenseService>()) {
|
||||
debugPrint('📑 API 모드로 라이센스 로드 시작...');
|
||||
|
||||
// API 사용
|
||||
final fetchedLicenses = await _licenseService.getLicenses(
|
||||
page: _currentPage,
|
||||
@@ -75,21 +111,27 @@ class LicenseListController extends ChangeNotifier {
|
||||
companyId: _selectedCompanyId,
|
||||
licenseType: _licenseType,
|
||||
);
|
||||
|
||||
debugPrint('📑 API에서 ${fetchedLicenses.length}개 라이센스 받음');
|
||||
|
||||
if (isInitialLoad) {
|
||||
_licenses = fetchedLicenses;
|
||||
debugPrint('📑 초기 로드: _licenses에 ${_licenses.length}개 저장');
|
||||
} else {
|
||||
_licenses.addAll(fetchedLicenses);
|
||||
debugPrint('📑 추가 로드: _licenses에 총 ${_licenses.length}개');
|
||||
}
|
||||
|
||||
_hasMore = fetchedLicenses.length >= _pageSize;
|
||||
|
||||
// 전체 개수 조회
|
||||
debugPrint('📑 전체 개수 조회 시작...');
|
||||
_total = await _licenseService.getTotalLicenses(
|
||||
isActive: _isActive,
|
||||
companyId: _selectedCompanyId,
|
||||
licenseType: _licenseType,
|
||||
);
|
||||
debugPrint('📑 전체 개수: $_total');
|
||||
} else {
|
||||
// Mock 데이터 사용
|
||||
final allLicenses = mockDataService?.getAllLicenses() ?? [];
|
||||
@@ -124,11 +166,17 @@ class LicenseListController extends ChangeNotifier {
|
||||
_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();
|
||||
}
|
||||
}
|
||||
@@ -162,21 +210,56 @@ class LicenseListController extends ChangeNotifier {
|
||||
|
||||
// 검색 필터 적용
|
||||
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);
|
||||
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({
|
||||
@@ -309,6 +392,162 @@ class LicenseListController extends ChangeNotifier {
|
||||
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 {
|
||||
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개 라이선스 삭제 실패');
|
||||
}
|
||||
}
|
||||
|
||||
// 통계 업데이트
|
||||
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();
|
||||
|
||||
Reference in New Issue
Block a user