refactor: 프로젝트 구조 개선 및 테스트 시스템 강화
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

주요 변경사항:
- 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:
JiWoong Sul
2025-08-07 17:16:30 +09:00
parent fe05094392
commit c8dd1ff815
79 changed files with 12558 additions and 9761 deletions

View File

@@ -256,10 +256,21 @@ class _CompanyFormScreenState extends State<CompanyFormScreen> {
if (mounted) {
Navigator.pop(context); // 로딩 다이얼로그 닫기
if (success) {
// 성공 메시지 표시
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(companyId != null ? '회사 정보가 수정되었습니다.' : '회사가 등록되었습니다.'),
backgroundColor: Colors.green,
),
);
// 리스트 화면으로 돌아가기
Navigator.pop(context, true);
} else {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('회사 저장에 실패했습니다.')),
const SnackBar(
content: Text('회사 저장에 실패했습니다.'),
backgroundColor: Colors.red,
),
);
}
}
@@ -267,7 +278,10 @@ class _CompanyFormScreenState extends State<CompanyFormScreen> {
if (mounted) {
Navigator.pop(context); // 로딩 다이얼로그 닫기
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('오류가 발생했습니다: $e')),
SnackBar(
content: Text('오류가 발생했습니다: $e'),
backgroundColor: Colors.red,
),
);
}
}

View File

@@ -506,7 +506,17 @@ class _ContactInfoWidgetState extends State<ContactInfoWidget> {
keyboardType: TextInputType.phone,
inputFormatters: [
FilteringTextInputFormatter.digitsOnly,
PhoneUtils.phoneInputFormatter,
// 접두사에 따른 동적 포맷팅
TextInputFormatter.withFunction((oldValue, newValue) {
final formatted = PhoneUtils.formatPhoneNumberByPrefix(
widget.selectedPhonePrefix,
newValue.text,
);
return TextEditingValue(
text: formatted,
selection: TextSelection.collapsed(offset: formatted.length),
);
}),
],
onTap: () {
developer.log('전화번호 필드 터치됨', name: 'ContactInfoWidget');
@@ -666,7 +676,17 @@ class _ContactInfoWidgetState extends State<ContactInfoWidget> {
),
inputFormatters: [
FilteringTextInputFormatter.digitsOnly,
PhoneUtils.phoneInputFormatter,
// 접두사에 따른 동적 포맷팅
TextInputFormatter.withFunction((oldValue, newValue) {
final formatted = PhoneUtils.formatPhoneNumberByPrefix(
widget.selectedPhonePrefix,
newValue.text,
);
return TextEditingValue(
text: formatted,
selection: TextSelection.collapsed(offset: formatted.length),
);
}),
],
keyboardType: TextInputType.phone,
onTap: _closeAllDropdowns,

View File

@@ -4,8 +4,11 @@ import 'package:superport/models/equipment_unified_model.dart';
import 'package:superport/models/company_model.dart';
import 'package:superport/services/equipment_service.dart';
import 'package:superport/services/mock_data_service.dart';
import 'package:superport/services/warehouse_service.dart';
import 'package:superport/services/company_service.dart';
import 'package:superport/utils/constants.dart';
import 'package:superport/core/errors/failures.dart';
import 'package:superport/core/utils/debug_logger.dart';
/// 장비 입고 폼 컨트롤러
///
@@ -13,6 +16,8 @@ import 'package:superport/core/errors/failures.dart';
class EquipmentInFormController extends ChangeNotifier {
final MockDataService dataService;
final EquipmentService _equipmentService = GetIt.instance<EquipmentService>();
final WarehouseService _warehouseService = GetIt.instance<WarehouseService>();
final CompanyService _companyService = GetIt.instance<CompanyService>();
final int? equipmentInId;
bool _isLoading = false;
@@ -55,6 +60,9 @@ class EquipmentInFormController extends ChangeNotifier {
List<String> categories = [];
List<String> subCategories = [];
List<String> subSubCategories = [];
// 창고 위치 전체 데이터 (이름-ID 매핑용)
Map<String, int> warehouseLocationMap = {};
// 편집 모드 여부
bool isEditMode = false;
@@ -108,19 +116,66 @@ class EquipmentInFormController extends ChangeNotifier {
}
// 입고지 목록 로드
void _loadWarehouseLocations() {
warehouseLocations =
dataService.getAllWarehouseLocations().map((e) => e.name).toList();
void _loadWarehouseLocations() async {
if (_useApi) {
try {
DebugLogger.log('입고지 목록 API 로드 시작', tag: 'EQUIPMENT_IN');
final locations = await _warehouseService.getWarehouseLocations();
warehouseLocations = locations.map((e) => e.name).toList();
// 이름-ID 매핑 저장
warehouseLocationMap = {for (var loc in locations) loc.name: loc.id};
DebugLogger.log('입고지 목록 로드 성공', tag: 'EQUIPMENT_IN', data: {
'count': warehouseLocations.length,
'locations': warehouseLocations,
'locationMap': warehouseLocationMap,
});
notifyListeners();
} catch (e) {
DebugLogger.logError('입고지 목록 로드 실패', error: e);
// 실패 시 Mock 데이터 사용
final mockLocations = dataService.getAllWarehouseLocations();
warehouseLocations = mockLocations.map((e) => e.name).toList();
warehouseLocationMap = {for (var loc in mockLocations) loc.name: loc.id};
notifyListeners();
}
} else {
final mockLocations = dataService.getAllWarehouseLocations();
warehouseLocations = mockLocations.map((e) => e.name).toList();
warehouseLocationMap = {for (var loc in mockLocations) loc.name: loc.id};
}
}
// 파트너사 목록 로드
void _loadPartnerCompanies() {
partnerCompanies =
dataService
.getAllCompanies()
.where((c) => c.companyTypes.contains(CompanyType.partner))
.map((c) => c.name)
.toList();
void _loadPartnerCompanies() async {
if (_useApi) {
try {
DebugLogger.log('파트너사 목록 API 로드 시작', tag: 'EQUIPMENT_IN');
final companies = await _companyService.getCompanies();
partnerCompanies = companies.map((c) => c.name).toList();
DebugLogger.log('파트너사 목록 로드 성공', tag: 'EQUIPMENT_IN', data: {
'count': partnerCompanies.length,
'companies': partnerCompanies,
});
notifyListeners();
} catch (e) {
DebugLogger.logError('파트너사 목록 로드 실패', error: e);
// 실패 시 Mock 데이터 사용
partnerCompanies =
dataService
.getAllCompanies()
.where((c) => c.companyTypes.contains(CompanyType.partner))
.map((c) => c.name)
.toList();
notifyListeners();
}
} else {
partnerCompanies =
dataService
.getAllCompanies()
.where((c) => c.companyTypes.contains(CompanyType.partner))
.map((c) => c.name)
.toList();
}
}
// 워런티 라이센스 목록 로드
@@ -304,30 +359,50 @@ class EquipmentInFormController extends ChangeNotifier {
await _equipmentService.updateEquipment(equipmentInId!, equipment);
} else {
// 생성 모드
// 1. 먼저 장비 생성
final createdEquipment = await _equipmentService.createEquipment(equipment);
// 2. 입고 처리 (warehouse location ID 필요)
int? warehouseLocationId;
if (warehouseLocation != null) {
// TODO: 창고 위치 ID 가져오기 - 현재는 목 데이터에서 찾기
try {
final warehouse = dataService.getAllWarehouseLocations().firstWhere(
(w) => w.name == warehouseLocation,
);
warehouseLocationId = warehouse.id;
} catch (e) {
// 창고를 찾을 수 없는 경우
warehouseLocationId = null;
try {
// 1. 먼저 장비 생성
DebugLogger.log('장비 생성 시작', tag: 'EQUIPMENT_IN', data: {
'manufacturer': manufacturer,
'name': name,
'serialNumber': serialNumber,
});
final createdEquipment = await _equipmentService.createEquipment(equipment);
DebugLogger.log('장비 생성 성공', tag: 'EQUIPMENT_IN', data: {
'equipmentId': createdEquipment.id,
});
// 2. 입고 처리 (warehouse location ID 필요)
int? warehouseLocationId;
if (warehouseLocation != null) {
// 저장된 매핑에서 ID 가져오기
warehouseLocationId = warehouseLocationMap[warehouseLocation];
if (warehouseLocationId == null) {
DebugLogger.logError('창고 위치 ID를 찾을 수 없음', error: 'Warehouse: $warehouseLocation');
}
}
DebugLogger.log('입고 처리 시작', tag: 'EQUIPMENT_IN', data: {
'equipmentId': createdEquipment.id,
'quantity': quantity,
'warehouseLocationId': warehouseLocationId,
});
await _equipmentService.equipmentIn(
equipmentId: createdEquipment.id!,
quantity: quantity,
warehouseLocationId: warehouseLocationId,
notes: remarkController.text.trim(),
);
DebugLogger.log('입고 처리 성공', tag: 'EQUIPMENT_IN');
} catch (e) {
DebugLogger.logError('장비 입고 처리 실패', error: e);
throw e; // 에러를 상위로 전파하여 적절한 에러 메시지 표시
}
await _equipmentService.equipmentIn(
equipmentId: createdEquipment.id!,
quantity: quantity,
warehouseLocationId: warehouseLocationId,
notes: remarkController.text.trim(),
);
}
} else {
// Mock 데이터 사용

View File

@@ -22,6 +22,7 @@ class EquipmentListController extends ChangeNotifier {
List<UnifiedEquipment> equipments = [];
String? selectedStatusFilter;
String searchKeyword = ''; // 검색어 추가
final Set<String> selectedEquipmentIds = {}; // 'id:status' 형식
bool _isLoading = false;
@@ -42,7 +43,7 @@ class EquipmentListController extends ChangeNotifier {
EquipmentListController({required this.dataService});
// 데이터 로드 및 상태 필터 적용
Future<void> loadData({bool isRefresh = false}) async {
Future<void> loadData({bool isRefresh = false, String? search}) async {
if (isRefresh) {
_currentPage = 1;
_hasMore = true;
@@ -69,6 +70,7 @@ class EquipmentListController extends ChangeNotifier {
page: _currentPage,
perPage: _perPage,
status: selectedStatusFilter != null ? EquipmentStatusConverter.clientToServer(selectedStatusFilter) : null,
search: search ?? searchKeyword,
);
DebugLogger.log('장비 목록 API 응답', tag: 'EQUIPMENT', data: {
@@ -137,6 +139,12 @@ class EquipmentListController extends ChangeNotifier {
selectedStatusFilter = status;
await loadData(isRefresh: true);
}
// 검색어 변경
Future<void> updateSearchKeyword(String keyword) async {
searchKeyword = keyword;
await loadData(isRefresh: true, search: keyword);
}
// 장비 선택/해제 (모든 상태 지원)
void selectEquipment(int? id, String status, bool? isSelected) {

View File

@@ -116,11 +116,12 @@ class _EquipmentListRedesignState extends State<EquipmentListRedesign> {
}
/// 검색 실행
void _onSearch() {
void _onSearch() async {
setState(() {
_appliedSearchKeyword = _searchController.text;
_currentPage = 1;
});
await _controller.updateSearchKeyword(_searchController.text);
}
/// 장비 선택/해제

View File

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

File diff suppressed because it is too large Load Diff

View File

@@ -52,12 +52,17 @@ class OverviewController extends ChangeNotifier {
// 데이터 로드
Future<void> loadData() async {
await Future.wait([
_loadOverviewStats(),
_loadRecentActivities(),
_loadEquipmentStatus(),
_loadExpiringLicenses(),
]);
try {
await Future.wait([
_loadOverviewStats(),
_loadRecentActivities(),
_loadEquipmentStatus(),
_loadExpiringLicenses(),
], eagerError: false); // 하나의 작업이 실패해도 다른 작업 계속 진행
} catch (e) {
DebugLogger.logError('대시보드 데이터 로드 중 오류', error: e);
// 개별 에러는 각 메서드에서 처리하므로 여기서는 로그만 남김
}
}
// 대시보드 데이터 로드 (loadData의 alias)
@@ -71,16 +76,55 @@ class OverviewController extends ChangeNotifier {
_statsError = null;
notifyListeners();
final result = await _dashboardService.getOverviewStats();
result.fold(
(failure) {
_statsError = failure.message;
},
(stats) {
_overviewStats = stats;
},
);
try {
final result = await _dashboardService.getOverviewStats();
result.fold(
(failure) {
_statsError = failure.message;
DebugLogger.logError('Overview 통계 로드 실패', error: failure.message);
// 실패 시 기본값 설정
_overviewStats = OverviewStats(
totalCompanies: 0,
activeCompanies: 0,
totalUsers: 0,
activeUsers: 0,
totalEquipment: 0,
availableEquipment: 0,
inUseEquipment: 0,
maintenanceEquipment: 0,
totalLicenses: 0,
activeLicenses: 0,
expiringLicensesCount: 0,
expiredLicensesCount: 0,
totalWarehouseLocations: 0,
activeWarehouseLocations: 0,
);
},
(stats) {
_overviewStats = stats;
},
);
} catch (e) {
_statsError = '통계 데이터를 불러올 수 없습니다';
_overviewStats = OverviewStats(
totalCompanies: 0,
activeCompanies: 0,
totalUsers: 0,
activeUsers: 0,
totalEquipment: 0,
availableEquipment: 0,
inUseEquipment: 0,
maintenanceEquipment: 0,
totalLicenses: 0,
activeLicenses: 0,
expiringLicensesCount: 0,
expiredLicensesCount: 0,
totalWarehouseLocations: 0,
activeWarehouseLocations: 0,
);
DebugLogger.logError('Overview 통계 로드 예외', error: e);
}
_isLoadingStats = false;
notifyListeners();
@@ -91,16 +135,24 @@ class OverviewController extends ChangeNotifier {
_activitiesError = null;
notifyListeners();
final result = await _dashboardService.getRecentActivities();
result.fold(
(failure) {
_activitiesError = failure.message;
},
(activities) {
_recentActivities = activities;
},
);
try {
final result = await _dashboardService.getRecentActivities();
result.fold(
(failure) {
_activitiesError = failure.message;
_recentActivities = []; // 실패 시 빈 리스트
DebugLogger.logError('최근 활동 로드 실패', error: failure.message);
},
(activities) {
_recentActivities = activities ?? [];
},
);
} catch (e) {
_activitiesError = '최근 활동을 불러올 수 없습니다';
_recentActivities = [];
DebugLogger.logError('최근 활동 로드 예외', error: e);
}
_isLoadingActivities = false;
notifyListeners();
@@ -113,23 +165,41 @@ class OverviewController extends ChangeNotifier {
DebugLogger.log('장비 상태 분포 로드 시작', tag: 'DASHBOARD');
final result = await _dashboardService.getEquipmentStatusDistribution();
result.fold(
(failure) {
_equipmentStatusError = failure.message;
DebugLogger.logError('장비 상태 분포 로드 실패', error: failure.message);
},
(status) {
_equipmentStatus = status;
DebugLogger.log('장비 상태 분포 로드 성공', tag: 'DASHBOARD', data: {
'available': status.available,
'inUse': status.inUse,
'maintenance': status.maintenance,
'disposed': status.disposed,
});
},
);
try {
final result = await _dashboardService.getEquipmentStatusDistribution();
result.fold(
(failure) {
_equipmentStatusError = failure.message;
DebugLogger.logError('장비 상태 분포 로드 실패', error: failure.message);
// 실패 시 기본값 설정
_equipmentStatus = EquipmentStatusDistribution(
available: 0,
inUse: 0,
maintenance: 0,
disposed: 0,
);
},
(status) {
_equipmentStatus = status;
DebugLogger.log('장비 상태 분포 로드 성공', tag: 'DASHBOARD', data: {
'available': status.available,
'inUse': status.inUse,
'maintenance': status.maintenance,
'disposed': status.disposed,
});
},
);
} catch (e) {
_equipmentStatusError = '장비 상태를 불러올 수 없습니다';
_equipmentStatus = EquipmentStatusDistribution(
available: 0,
inUse: 0,
maintenance: 0,
disposed: 0,
);
DebugLogger.logError('장비 상태 로드 예외', error: e);
}
_isLoadingEquipmentStatus = false;
notifyListeners();
@@ -140,16 +210,24 @@ class OverviewController extends ChangeNotifier {
_licensesError = null;
notifyListeners();
final result = await _dashboardService.getExpiringLicenses(days: 30);
result.fold(
(failure) {
_licensesError = failure.message;
},
(licenses) {
_expiringLicenses = licenses;
},
);
try {
final result = await _dashboardService.getExpiringLicenses(days: 30);
result.fold(
(failure) {
_licensesError = failure.message;
_expiringLicenses = []; // 실패 시 빈 리스트
DebugLogger.logError('만료 라이선스 로드 실패', error: failure.message);
},
(licenses) {
_expiringLicenses = licenses ?? [];
},
);
} catch (e) {
_licensesError = '라이선스 정보를 불러올 수 없습니다';
_expiringLicenses = [];
DebugLogger.logError('만료 라이선스 로드 예외', error: e);
}
_isLoadingLicenses = false;
notifyListeners();

View File

@@ -125,13 +125,13 @@ class _OverviewScreenRedesignState extends State<OverviewScreenRedesign> {
),
_buildStatCard(
'입고 장비',
'${_controller.overviewStats?.availableEquipment ?? 0}',
'${_controller.equipmentStatus?.available ?? 0}',
Icons.inventory,
ShadcnTheme.success,
),
_buildStatCard(
'출고 장비',
'${_controller.overviewStats?.inUseEquipment ?? 0}',
'${_controller.equipmentStatus?.inUse ?? 0}',
Icons.local_shipping,
ShadcnTheme.warning,
),
@@ -300,7 +300,7 @@ class _OverviewScreenRedesignState extends State<OverviewScreenRedesign> {
const SizedBox(height: 16),
Consumer<OverviewController>(
builder: (context, controller, child) {
final activities = controller.recentActivities ?? [];
final activities = controller.recentActivities;
if (activities.isEmpty) {
return Padding(
padding: const EdgeInsets.symmetric(vertical: 20),
@@ -435,15 +435,19 @@ class _OverviewScreenRedesignState extends State<OverviewScreenRedesign> {
Widget _buildActivityItem(dynamic activity) {
// 아이콘 매핑
IconData getActivityIcon(String type) {
switch (type) {
IconData getActivityIcon(String? type) {
switch (type?.toLowerCase()) {
case 'equipment_in':
case '장비 입고':
return Icons.inventory;
case 'equipment_out':
case '장비 출고':
return Icons.local_shipping;
case 'company':
case '회사':
return Icons.business;
case 'user':
case '사용자':
return Icons.person_add;
default:
return Icons.settings;
@@ -451,23 +455,31 @@ class _OverviewScreenRedesignState extends State<OverviewScreenRedesign> {
}
// 색상 매핑
Color getActivityColor(String type) {
switch (type) {
Color getActivityColor(String? type) {
switch (type?.toLowerCase()) {
case 'equipment_in':
case '장비 입고':
return ShadcnTheme.success;
case 'equipment_out':
case '장비 출고':
return ShadcnTheme.warning;
case 'company':
case '회사':
return ShadcnTheme.info;
case 'user':
case '사용자':
return ShadcnTheme.primary;
default:
return ShadcnTheme.mutedForeground;
}
}
final color = getActivityColor(activity.activityType);
final activityType = activity.activityType ?? '';
final color = getActivityColor(activityType);
final dateFormat = DateFormat('MM/dd HH:mm');
final timestamp = activity.timestamp ?? DateTime.now();
final entityName = activity.entityName ?? '이름 없음';
final description = activity.description ?? '설명 없음';
return Padding(
padding: const EdgeInsets.symmetric(vertical: 8),
@@ -480,7 +492,7 @@ class _OverviewScreenRedesignState extends State<OverviewScreenRedesign> {
borderRadius: BorderRadius.circular(6),
),
child: Icon(
getActivityIcon(activity.activityType),
getActivityIcon(activityType),
color: color,
size: 16,
),
@@ -491,18 +503,20 @@ class _OverviewScreenRedesignState extends State<OverviewScreenRedesign> {
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
activity.entityName,
entityName,
style: ShadcnTheme.bodyMedium,
overflow: TextOverflow.ellipsis,
),
Text(
activity.description,
description,
style: ShadcnTheme.bodySmall,
overflow: TextOverflow.ellipsis,
),
],
),
),
Text(
dateFormat.format(activity.timestamp),
dateFormat.format(timestamp),
style: ShadcnTheme.bodySmall,
),
],

View File

@@ -108,8 +108,32 @@ class _WarehouseLocationFormScreenState
? null
: () async {
setState(() {}); // 저장 중 상태 갱신
await _controller.save();
final success = await _controller.save();
setState(() {}); // 저장 완료 후 상태 갱신
if (success) {
// 성공 메시지 표시
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(_controller.isEditMode ? '입고지가 수정되었습니다' : '입고지가 추가되었습니다'),
backgroundColor: AppThemeTailwind.success,
),
);
// 리스트 화면으로 돌아가기
Navigator.of(context).pop(true);
}
} else {
// 실패 메시지 표시
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(_controller.error ?? '저장에 실패했습니다'),
backgroundColor: AppThemeTailwind.danger,
),
);
}
}
},
style: ElevatedButton.styleFrom(
backgroundColor: AppThemeTailwind.primary,