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:
@@ -3,12 +3,9 @@ import 'package:get_it/get_it.dart';
|
||||
import 'package:superport/models/warehouse_location_model.dart';
|
||||
import 'package:superport/models/address_model.dart';
|
||||
import 'package:superport/services/warehouse_service.dart';
|
||||
import 'package:superport/services/mock_data_service.dart';
|
||||
|
||||
/// 입고지 폼 상태 및 저장/수정 로직을 담당하는 컨트롤러
|
||||
class WarehouseLocationFormController extends ChangeNotifier {
|
||||
final bool useApi;
|
||||
final MockDataService? mockDataService;
|
||||
late final WarehouseService _warehouseService;
|
||||
|
||||
/// 폼 키
|
||||
@@ -42,12 +39,12 @@ class WarehouseLocationFormController extends ChangeNotifier {
|
||||
WarehouseLocation? _originalLocation;
|
||||
|
||||
WarehouseLocationFormController({
|
||||
this.useApi = true,
|
||||
this.mockDataService,
|
||||
int? locationId,
|
||||
}) {
|
||||
if (useApi && GetIt.instance.isRegistered<WarehouseService>()) {
|
||||
if (GetIt.instance.isRegistered<WarehouseService>()) {
|
||||
_warehouseService = GetIt.instance<WarehouseService>();
|
||||
} else {
|
||||
throw Exception('WarehouseService not registered in GetIt');
|
||||
}
|
||||
|
||||
if (locationId != null) {
|
||||
@@ -73,11 +70,7 @@ class WarehouseLocationFormController extends ChangeNotifier {
|
||||
notifyListeners();
|
||||
|
||||
try {
|
||||
if (useApi && GetIt.instance.isRegistered<WarehouseService>()) {
|
||||
_originalLocation = await _warehouseService.getWarehouseLocationById(locationId);
|
||||
} else {
|
||||
_originalLocation = mockDataService?.getWarehouseLocationById(locationId);
|
||||
}
|
||||
_originalLocation = await _warehouseService.getWarehouseLocationById(locationId);
|
||||
|
||||
if (_originalLocation != null) {
|
||||
nameController.text = _originalLocation!.name;
|
||||
@@ -114,18 +107,10 @@ class WarehouseLocationFormController extends ChangeNotifier {
|
||||
remark: remarkController.text.trim().isEmpty ? null : remarkController.text.trim(),
|
||||
);
|
||||
|
||||
if (useApi && GetIt.instance.isRegistered<WarehouseService>()) {
|
||||
if (_isEditMode) {
|
||||
await _warehouseService.updateWarehouseLocation(location);
|
||||
} else {
|
||||
await _warehouseService.createWarehouseLocation(location);
|
||||
}
|
||||
if (_isEditMode) {
|
||||
await _warehouseService.updateWarehouseLocation(location);
|
||||
} else {
|
||||
if (_isEditMode) {
|
||||
mockDataService?.updateWarehouseLocation(location);
|
||||
} else {
|
||||
mockDataService?.addWarehouseLocation(location);
|
||||
}
|
||||
await _warehouseService.createWarehouseLocation(location);
|
||||
}
|
||||
|
||||
return true;
|
||||
|
||||
@@ -0,0 +1,210 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get_it/get_it.dart';
|
||||
import 'package:superport/models/warehouse_location_model.dart';
|
||||
import 'package:superport/services/warehouse_service.dart';
|
||||
import 'package:superport/core/errors/failures.dart';
|
||||
import 'package:superport/core/utils/error_handler.dart';
|
||||
|
||||
/// 입고지 리스트 상태 및 CRUD만 담당하는 컨트롤러 클래스 (SRP 적용)
|
||||
/// UI, 네비게이션, 다이얼로그 등은 포함하지 않음
|
||||
/// 향후 서비스/리포지토리 DI 구조로 확장 가능
|
||||
class WarehouseLocationListController extends ChangeNotifier {
|
||||
late final WarehouseService _warehouseService;
|
||||
|
||||
List<WarehouseLocation> _warehouseLocations = [];
|
||||
List<WarehouseLocation> _filteredLocations = [];
|
||||
bool _isLoading = false;
|
||||
String? _error;
|
||||
String _searchQuery = '';
|
||||
int _currentPage = 1;
|
||||
final int _pageSize = 20;
|
||||
bool _hasMore = true;
|
||||
int _total = 0;
|
||||
|
||||
// 필터 옵션
|
||||
bool? _isActive;
|
||||
|
||||
WarehouseLocationListController() {
|
||||
if (GetIt.instance.isRegistered<WarehouseService>()) {
|
||||
_warehouseService = GetIt.instance<WarehouseService>();
|
||||
} else {
|
||||
throw Exception('WarehouseService not registered');
|
||||
}
|
||||
}
|
||||
|
||||
// Getters
|
||||
List<WarehouseLocation> get warehouseLocations => _filteredLocations;
|
||||
bool get isLoading => _isLoading;
|
||||
String? get error => _error;
|
||||
String get searchQuery => _searchQuery;
|
||||
int get currentPage => _currentPage;
|
||||
bool get hasMore => _hasMore;
|
||||
int get total => _total;
|
||||
bool? get isActive => _isActive;
|
||||
|
||||
/// 데이터 로드
|
||||
Future<void> loadWarehouseLocations({bool isInitialLoad = true}) async {
|
||||
if (_isLoading) return;
|
||||
|
||||
_isLoading = true;
|
||||
_error = null;
|
||||
notifyListeners();
|
||||
|
||||
// API 사용 시 ErrorHandler 적용
|
||||
print('╔══════════════════════════════════════════════════════════');
|
||||
print('║ 🏭 입고지 목록 API 호출 시작');
|
||||
print('║ • 활성 필터: ${_isActive != null ? (_isActive! ? "활성" : "비활성") : "전체"}');
|
||||
print('╚══════════════════════════════════════════════════════════');
|
||||
|
||||
final fetchedLocations = await ErrorHandler.handleApiCall<List<WarehouseLocation>>(
|
||||
() => _warehouseService.getWarehouseLocations(
|
||||
page: 1,
|
||||
perPage: 1000, // 충분히 큰 값으로 전체 데이터 로드
|
||||
isActive: _isActive,
|
||||
),
|
||||
onError: (failure) {
|
||||
_error = ErrorHandler.getUserFriendlyMessage(failure);
|
||||
print('[WarehouseLocationListController] API 에러: ${failure.message}');
|
||||
},
|
||||
);
|
||||
|
||||
if (fetchedLocations != null) {
|
||||
print('╔══════════════════════════════════════════════════════════');
|
||||
print('║ 📊 입고지 목록 로드 완료');
|
||||
print('║ ▶ 총 입고지 수: ${fetchedLocations.length}개');
|
||||
print('╟──────────────────────────────────────────────────────────');
|
||||
|
||||
// 상태별 통계 (입고지에 상태가 있다면)
|
||||
int activeCount = 0;
|
||||
int inactiveCount = 0;
|
||||
for (final location in fetchedLocations) {
|
||||
// isActive 필드가 있다면 활용
|
||||
activeCount++; // 현재는 모두 활성으로 가정
|
||||
}
|
||||
|
||||
print('║ • 활성 입고지: $activeCount개');
|
||||
if (inactiveCount > 0) {
|
||||
print('║ • 비활성 입고지: $inactiveCount개');
|
||||
}
|
||||
|
||||
print('╟──────────────────────────────────────────────────────────');
|
||||
print('║ 📑 전체 데이터 로드 완료');
|
||||
print('║ • View에서 페이지네이션 처리 예정');
|
||||
print('╚══════════════════════════════════════════════════════════');
|
||||
|
||||
_warehouseLocations = fetchedLocations;
|
||||
_hasMore = false; // 전체 데이터를 로드했으므로 더 이상 로드할 필요 없음
|
||||
_total = fetchedLocations.length;
|
||||
_applySearchFilter();
|
||||
print('[WarehouseLocationListController] After filtering: ${_filteredLocations.length} locations shown');
|
||||
}
|
||||
|
||||
_isLoading = false;
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
// 다음 페이지 로드
|
||||
Future<void> loadNextPage() async {
|
||||
if (!_hasMore || _isLoading) return;
|
||||
await loadWarehouseLocations(isInitialLoad: false);
|
||||
}
|
||||
|
||||
// 검색
|
||||
void search(String query) {
|
||||
_searchQuery = query;
|
||||
_applySearchFilter();
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
// 검색 필터 적용
|
||||
void _applySearchFilter() {
|
||||
if (_searchQuery.isEmpty) {
|
||||
_filteredLocations = List.from(_warehouseLocations);
|
||||
} else {
|
||||
_filteredLocations = _warehouseLocations.where((location) {
|
||||
return location.name.toLowerCase().contains(_searchQuery.toLowerCase()) ||
|
||||
location.address.toString().toLowerCase().contains(_searchQuery.toLowerCase());
|
||||
}).toList();
|
||||
}
|
||||
}
|
||||
|
||||
// 필터 설정
|
||||
void setFilters({bool? isActive}) {
|
||||
_isActive = isActive;
|
||||
loadWarehouseLocations();
|
||||
}
|
||||
|
||||
// 필터 초기화
|
||||
void clearFilters() {
|
||||
_isActive = null;
|
||||
_searchQuery = '';
|
||||
loadWarehouseLocations();
|
||||
}
|
||||
|
||||
/// 입고지 추가
|
||||
Future<void> addWarehouseLocation(WarehouseLocation location) async {
|
||||
await ErrorHandler.handleApiCall<void>(
|
||||
() => _warehouseService.createWarehouseLocation(location),
|
||||
onError: (failure) {
|
||||
_error = ErrorHandler.getUserFriendlyMessage(failure);
|
||||
notifyListeners();
|
||||
},
|
||||
);
|
||||
|
||||
// 목록 새로고침
|
||||
await loadWarehouseLocations();
|
||||
}
|
||||
|
||||
/// 입고지 수정
|
||||
Future<void> updateWarehouseLocation(WarehouseLocation location) async {
|
||||
await ErrorHandler.handleApiCall<void>(
|
||||
() => _warehouseService.updateWarehouseLocation(location),
|
||||
onError: (failure) {
|
||||
_error = ErrorHandler.getUserFriendlyMessage(failure);
|
||||
notifyListeners();
|
||||
},
|
||||
);
|
||||
|
||||
// 목록에서 업데이트
|
||||
final index = _warehouseLocations.indexWhere((l) => l.id == location.id);
|
||||
if (index != -1) {
|
||||
_warehouseLocations[index] = location;
|
||||
_applySearchFilter();
|
||||
notifyListeners();
|
||||
}
|
||||
}
|
||||
|
||||
/// 입고지 삭제
|
||||
Future<void> deleteWarehouseLocation(int id) async {
|
||||
await ErrorHandler.handleApiCall<void>(
|
||||
() => _warehouseService.deleteWarehouseLocation(id),
|
||||
onError: (failure) {
|
||||
_error = ErrorHandler.getUserFriendlyMessage(failure);
|
||||
notifyListeners();
|
||||
},
|
||||
);
|
||||
|
||||
// 목록에서 제거
|
||||
_warehouseLocations.removeWhere((l) => l.id == id);
|
||||
_applySearchFilter();
|
||||
_total--;
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
// 새로고침
|
||||
Future<void> refresh() async {
|
||||
await loadWarehouseLocations();
|
||||
}
|
||||
|
||||
// 사용 중인 창고 위치 조회
|
||||
Future<List<WarehouseLocation>> getInUseWarehouseLocations() async {
|
||||
final locations = await ErrorHandler.handleApiCall<List<WarehouseLocation>>(
|
||||
() => _warehouseService.getInUseWarehouseLocations(),
|
||||
onError: (failure) {
|
||||
_error = ErrorHandler.getUserFriendlyMessage(failure);
|
||||
notifyListeners();
|
||||
},
|
||||
);
|
||||
return locations ?? [];
|
||||
}
|
||||
}
|
||||
@@ -2,265 +2,135 @@ import 'package:flutter/material.dart';
|
||||
import 'package:get_it/get_it.dart';
|
||||
import 'package:superport/models/warehouse_location_model.dart';
|
||||
import 'package:superport/services/warehouse_service.dart';
|
||||
import 'package:superport/services/mock_data_service.dart';
|
||||
import 'package:superport/core/errors/failures.dart';
|
||||
import 'package:superport/core/utils/error_handler.dart';
|
||||
import 'package:superport/core/controllers/base_list_controller.dart';
|
||||
import 'package:superport/data/models/common/pagination_params.dart';
|
||||
|
||||
/// 입고지 리스트 상태 및 CRUD만 담당하는 컨트롤러 클래스 (SRP 적용)
|
||||
/// UI, 네비게이션, 다이얼로그 등은 포함하지 않음
|
||||
/// 향후 서비스/리포지토리 DI 구조로 확장 가능
|
||||
class WarehouseLocationListController extends ChangeNotifier {
|
||||
final bool useApi;
|
||||
final MockDataService? mockDataService;
|
||||
WarehouseService? _warehouseService;
|
||||
/// 입고지 리스트 상태 및 CRUD만 담당하는 컨트롤러 클래스 (리팩토링 버전)
|
||||
/// BaseListController를 상속받아 공통 기능을 재사용
|
||||
class WarehouseLocationListController extends BaseListController<WarehouseLocation> {
|
||||
late final WarehouseService _warehouseService;
|
||||
|
||||
List<WarehouseLocation> _warehouseLocations = [];
|
||||
List<WarehouseLocation> _filteredLocations = [];
|
||||
bool _isLoading = false;
|
||||
String? _error;
|
||||
String _searchQuery = '';
|
||||
int _currentPage = 1;
|
||||
final int _pageSize = 20;
|
||||
bool _hasMore = true;
|
||||
int _total = 0;
|
||||
|
||||
// 필터 옵션
|
||||
bool? _isActive;
|
||||
|
||||
WarehouseLocationListController({this.useApi = true, this.mockDataService}) {
|
||||
if (useApi && GetIt.instance.isRegistered<WarehouseService>()) {
|
||||
WarehouseLocationListController() {
|
||||
if (GetIt.instance.isRegistered<WarehouseService>()) {
|
||||
_warehouseService = GetIt.instance<WarehouseService>();
|
||||
} else {
|
||||
throw Exception('WarehouseService not registered in GetIt');
|
||||
}
|
||||
}
|
||||
|
||||
// Getters
|
||||
List<WarehouseLocation> get warehouseLocations => _filteredLocations;
|
||||
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
|
||||
List<WarehouseLocation> get warehouseLocations => items;
|
||||
bool? get isActive => _isActive;
|
||||
|
||||
/// 데이터 로드
|
||||
@override
|
||||
Future<PagedResult<WarehouseLocation>> fetchData({
|
||||
required PaginationParams params,
|
||||
Map<String, dynamic>? additionalFilters,
|
||||
}) async {
|
||||
// API 사용
|
||||
final fetchedLocations = await ErrorHandler.handleApiCall<List<WarehouseLocation>>(
|
||||
() => _warehouseService.getWarehouseLocations(
|
||||
page: params.page,
|
||||
perPage: params.perPage,
|
||||
isActive: _isActive,
|
||||
),
|
||||
onError: (failure) {
|
||||
throw failure;
|
||||
},
|
||||
);
|
||||
|
||||
final items = fetchedLocations ?? [];
|
||||
|
||||
// 임시로 메타데이터 생성 (추후 API에서 실제 메타데이터 반환 시 수정)
|
||||
final meta = PaginationMeta(
|
||||
currentPage: params.page,
|
||||
perPage: params.perPage,
|
||||
total: items.length < params.perPage ?
|
||||
(params.page - 1) * params.perPage + items.length :
|
||||
params.page * params.perPage + 1,
|
||||
totalPages: items.length < params.perPage ? params.page : params.page + 1,
|
||||
hasNext: items.length >= params.perPage,
|
||||
hasPrevious: params.page > 1,
|
||||
);
|
||||
|
||||
return PagedResult(items: items, meta: meta);
|
||||
}
|
||||
|
||||
@override
|
||||
bool filterItem(WarehouseLocation item, String query) {
|
||||
return item.name.toLowerCase().contains(query.toLowerCase()) ||
|
||||
item.address.toString().toLowerCase().contains(query.toLowerCase());
|
||||
}
|
||||
|
||||
/// 데이터 로드 (호환성을 위해 유지)
|
||||
Future<void> loadWarehouseLocations({bool isInitialLoad = true}) async {
|
||||
if (_isLoading) return;
|
||||
|
||||
_isLoading = true;
|
||||
_error = null;
|
||||
notifyListeners();
|
||||
|
||||
try {
|
||||
if (useApi && _warehouseService != null) {
|
||||
// API 사용 - 전체 데이터 로드
|
||||
print('╔══════════════════════════════════════════════════════════');
|
||||
print('║ 🏭 입고지 목록 API 호출 시작');
|
||||
print('║ • 활성 필터: ${_isActive != null ? (_isActive! ? "활성" : "비활성") : "전체"}');
|
||||
print('╚══════════════════════════════════════════════════════════');
|
||||
|
||||
// 전체 데이터를 가져오기 위해 큰 perPage 값 사용
|
||||
final fetchedLocations = await _warehouseService!.getWarehouseLocations(
|
||||
page: 1,
|
||||
perPage: 1000, // 충분히 큰 값으로 전체 데이터 로드
|
||||
isActive: _isActive,
|
||||
);
|
||||
|
||||
print('╔══════════════════════════════════════════════════════════');
|
||||
print('║ 📊 입고지 목록 로드 완료');
|
||||
print('║ ▶ 총 입고지 수: ${fetchedLocations.length}개');
|
||||
print('╟──────────────────────────────────────────────────────────');
|
||||
|
||||
// 상태별 통계 (입고지에 상태가 있다면)
|
||||
int activeCount = 0;
|
||||
int inactiveCount = 0;
|
||||
for (final location in fetchedLocations) {
|
||||
// isActive 필드가 있다면 활용
|
||||
activeCount++; // 현재는 모두 활성으로 가정
|
||||
}
|
||||
|
||||
print('║ • 활성 입고지: $activeCount개');
|
||||
if (inactiveCount > 0) {
|
||||
print('║ • 비활성 입고지: $inactiveCount개');
|
||||
}
|
||||
|
||||
print('╟──────────────────────────────────────────────────────────');
|
||||
print('║ 📑 전체 데이터 로드 완료');
|
||||
print('║ • View에서 페이지네이션 처리 예정');
|
||||
print('╚══════════════════════════════════════════════════════════');
|
||||
|
||||
_warehouseLocations = fetchedLocations;
|
||||
_hasMore = false; // 전체 데이터를 로드했으므로 더 이상 로드할 필요 없음
|
||||
_total = fetchedLocations.length;
|
||||
} else {
|
||||
// Mock 데이터 사용
|
||||
print('[WarehouseLocationListController] Using Mock data');
|
||||
final allLocations = mockDataService?.getAllWarehouseLocations() ?? [];
|
||||
print('[WarehouseLocationListController] Mock data has ${allLocations.length} locations');
|
||||
|
||||
// 필터링 적용
|
||||
var filtered = allLocations;
|
||||
if (_isActive != null) {
|
||||
// Mock 데이터에는 isActive 필드가 없으므로 모두 활성으로 처리
|
||||
filtered = _isActive! ? allLocations : [];
|
||||
}
|
||||
|
||||
// 페이지네이션 적용
|
||||
final startIndex = (_currentPage - 1) * _pageSize;
|
||||
final endIndex = startIndex + _pageSize;
|
||||
|
||||
if (startIndex < filtered.length) {
|
||||
final pageLocations = filtered.sublist(
|
||||
startIndex,
|
||||
endIndex > filtered.length ? filtered.length : endIndex,
|
||||
);
|
||||
|
||||
if (isInitialLoad) {
|
||||
_warehouseLocations = pageLocations;
|
||||
} else {
|
||||
_warehouseLocations.addAll(pageLocations);
|
||||
}
|
||||
|
||||
_hasMore = endIndex < filtered.length;
|
||||
} else {
|
||||
_hasMore = false;
|
||||
}
|
||||
|
||||
_total = filtered.length;
|
||||
}
|
||||
|
||||
_applySearchFilter();
|
||||
print('[WarehouseLocationListController] After filtering: ${_filteredLocations.length} locations shown');
|
||||
} catch (e, stackTrace) {
|
||||
print('[WarehouseLocationListController] Error loading warehouse locations: $e');
|
||||
print('[WarehouseLocationListController] Error type: ${e.runtimeType}');
|
||||
print('[WarehouseLocationListController] Stack trace: $stackTrace');
|
||||
|
||||
if (e is ServerFailure) {
|
||||
_error = e.message;
|
||||
} else {
|
||||
_error = '오류 발생: ${e.toString()}';
|
||||
}
|
||||
} finally {
|
||||
_isLoading = false;
|
||||
notifyListeners();
|
||||
}
|
||||
}
|
||||
|
||||
// 다음 페이지 로드
|
||||
Future<void> loadNextPage() async {
|
||||
if (!_hasMore || _isLoading) return;
|
||||
await loadWarehouseLocations(isInitialLoad: false);
|
||||
}
|
||||
|
||||
// 검색
|
||||
void search(String query) {
|
||||
_searchQuery = query;
|
||||
_applySearchFilter();
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
// 검색 필터 적용
|
||||
void _applySearchFilter() {
|
||||
if (_searchQuery.isEmpty) {
|
||||
_filteredLocations = List.from(_warehouseLocations);
|
||||
} else {
|
||||
_filteredLocations = _warehouseLocations.where((location) {
|
||||
return location.name.toLowerCase().contains(_searchQuery.toLowerCase()) ||
|
||||
location.address.toString().toLowerCase().contains(_searchQuery.toLowerCase());
|
||||
}).toList();
|
||||
}
|
||||
await loadData(isRefresh: isInitialLoad);
|
||||
}
|
||||
|
||||
// 필터 설정
|
||||
void setFilters({bool? isActive}) {
|
||||
_isActive = isActive;
|
||||
loadWarehouseLocations();
|
||||
loadData(isRefresh: true);
|
||||
}
|
||||
|
||||
// 필터 초기화
|
||||
void clearFilters() {
|
||||
_isActive = null;
|
||||
_searchQuery = '';
|
||||
loadWarehouseLocations();
|
||||
search('');
|
||||
loadData(isRefresh: true);
|
||||
}
|
||||
|
||||
/// 입고지 추가
|
||||
Future<void> addWarehouseLocation(WarehouseLocation location) async {
|
||||
try {
|
||||
if (useApi && _warehouseService != null) {
|
||||
await _warehouseService!.createWarehouseLocation(location);
|
||||
} else {
|
||||
mockDataService?.addWarehouseLocation(location);
|
||||
}
|
||||
|
||||
// 목록 새로고침
|
||||
await loadWarehouseLocations();
|
||||
} catch (e) {
|
||||
_error = e.toString();
|
||||
notifyListeners();
|
||||
}
|
||||
await ErrorHandler.handleApiCall<void>(
|
||||
() => _warehouseService.createWarehouseLocation(location),
|
||||
onError: (failure) {
|
||||
throw failure;
|
||||
},
|
||||
);
|
||||
|
||||
// 목록 새로고침
|
||||
await refresh();
|
||||
}
|
||||
|
||||
/// 입고지 수정
|
||||
Future<void> updateWarehouseLocation(WarehouseLocation location) async {
|
||||
try {
|
||||
if (useApi && _warehouseService != null) {
|
||||
await _warehouseService!.updateWarehouseLocation(location);
|
||||
} else {
|
||||
mockDataService?.updateWarehouseLocation(location);
|
||||
}
|
||||
|
||||
// 목록에서 업데이트
|
||||
final index = _warehouseLocations.indexWhere((l) => l.id == location.id);
|
||||
if (index != -1) {
|
||||
_warehouseLocations[index] = location;
|
||||
_applySearchFilter();
|
||||
notifyListeners();
|
||||
}
|
||||
} catch (e) {
|
||||
_error = e.toString();
|
||||
notifyListeners();
|
||||
}
|
||||
await ErrorHandler.handleApiCall<void>(
|
||||
() => _warehouseService.updateWarehouseLocation(location),
|
||||
onError: (failure) {
|
||||
throw failure;
|
||||
},
|
||||
);
|
||||
|
||||
// 로컬 업데이트
|
||||
updateItemLocally(location, (l) => l.id == location.id);
|
||||
}
|
||||
|
||||
/// 입고지 삭제
|
||||
Future<void> deleteWarehouseLocation(int id) async {
|
||||
try {
|
||||
if (useApi && _warehouseService != null) {
|
||||
await _warehouseService!.deleteWarehouseLocation(id);
|
||||
} else {
|
||||
mockDataService?.deleteWarehouseLocation(id);
|
||||
}
|
||||
|
||||
// 목록에서 제거
|
||||
_warehouseLocations.removeWhere((l) => l.id == id);
|
||||
_applySearchFilter();
|
||||
_total--;
|
||||
notifyListeners();
|
||||
} catch (e) {
|
||||
_error = e.toString();
|
||||
notifyListeners();
|
||||
}
|
||||
}
|
||||
|
||||
// 새로고침
|
||||
Future<void> refresh() async {
|
||||
await loadWarehouseLocations();
|
||||
await ErrorHandler.handleApiCall<void>(
|
||||
() => _warehouseService.deleteWarehouseLocation(id),
|
||||
onError: (failure) {
|
||||
throw failure;
|
||||
},
|
||||
);
|
||||
|
||||
// 로컬 삭제
|
||||
removeItemLocally((l) => l.id == id);
|
||||
}
|
||||
|
||||
// 사용 중인 창고 위치 조회
|
||||
Future<List<WarehouseLocation>> getInUseWarehouseLocations() async {
|
||||
try {
|
||||
if (useApi && _warehouseService != null) {
|
||||
return await _warehouseService!.getInUseWarehouseLocations();
|
||||
} else {
|
||||
// Mock 데이터에서는 모든 창고가 사용 중으로 간주
|
||||
return mockDataService?.getAllWarehouseLocations() ?? [];
|
||||
}
|
||||
} catch (e) {
|
||||
_error = e.toString();
|
||||
notifyListeners();
|
||||
return [];
|
||||
}
|
||||
final locations = await ErrorHandler.handleApiCall<List<WarehouseLocation>>(
|
||||
() => _warehouseService.getInUseWarehouseLocations(),
|
||||
onError: (failure) {
|
||||
throw failure;
|
||||
},
|
||||
);
|
||||
return locations ?? [];
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,300 @@
|
||||
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/warehouse/warehouse_dto.dart';
|
||||
import '../../../domain/usecases/warehouse_location/warehouse_location_usecases.dart';
|
||||
|
||||
/// UseCase 패턴을 적용한 창고 위치 목록 컨트롤러
|
||||
class WarehouseLocationListControllerWithUseCase extends BaseListController<WarehouseLocationDto> {
|
||||
final GetWarehouseLocationsUseCase getWarehouseLocationsUseCase;
|
||||
final CreateWarehouseLocationUseCase createWarehouseLocationUseCase;
|
||||
final UpdateWarehouseLocationUseCase updateWarehouseLocationUseCase;
|
||||
final DeleteWarehouseLocationUseCase deleteWarehouseLocationUseCase;
|
||||
|
||||
// 선택된 항목들
|
||||
final Set<int> _selectedLocationIds = {};
|
||||
Set<int> get selectedLocationIds => _selectedLocationIds;
|
||||
|
||||
// 필터 옵션
|
||||
bool _showActiveOnly = true;
|
||||
String? _filterByManager;
|
||||
|
||||
bool get showActiveOnly => _showActiveOnly;
|
||||
String? get filterByManager => _filterByManager;
|
||||
|
||||
WarehouseLocationListControllerWithUseCase({
|
||||
required this.getWarehouseLocationsUseCase,
|
||||
required this.createWarehouseLocationUseCase,
|
||||
required this.updateWarehouseLocationUseCase,
|
||||
required this.deleteWarehouseLocationUseCase,
|
||||
});
|
||||
|
||||
@override
|
||||
Future<PagedResult<WarehouseLocationDto>> fetchData({
|
||||
required PaginationParams params,
|
||||
Map<String, dynamic>? additionalFilters,
|
||||
}) async {
|
||||
try {
|
||||
// 필터 파라미터 구성
|
||||
final filters = <String, dynamic>{};
|
||||
if (_showActiveOnly) filters['is_active'] = true;
|
||||
if (_filterByManager != null) filters['manager'] = _filterByManager;
|
||||
|
||||
final updatedParams = params.copyWith(filters: filters);
|
||||
final getParams = GetWarehouseLocationsParams.fromPaginationParams(updatedParams);
|
||||
|
||||
final result = await getWarehouseLocationsUseCase(getParams);
|
||||
|
||||
return result.fold(
|
||||
(failure) => throw Exception(failure.message),
|
||||
(locationsResponse) {
|
||||
// PagedResult로 래핑하여 반환
|
||||
final meta = PaginationMeta(
|
||||
currentPage: params.page,
|
||||
perPage: params.perPage,
|
||||
total: locationsResponse.items.length,
|
||||
totalPages: (locationsResponse.items.length / params.perPage).ceil(),
|
||||
hasNext: locationsResponse.items.length >= params.perPage,
|
||||
hasPrevious: params.page > 1,
|
||||
);
|
||||
return PagedResult(items: locationsResponse.items, meta: meta);
|
||||
},
|
||||
);
|
||||
} catch (e) {
|
||||
throw Exception('데이터 로드 실패: $e');
|
||||
}
|
||||
}
|
||||
|
||||
/// 창고 위치 생성
|
||||
Future<void> createWarehouseLocation({
|
||||
required String name,
|
||||
required String address,
|
||||
String? description,
|
||||
String? contactNumber,
|
||||
String? manager,
|
||||
double? latitude,
|
||||
double? longitude,
|
||||
}) async {
|
||||
try {
|
||||
isLoadingState = true;
|
||||
|
||||
final params = CreateWarehouseLocationParams(
|
||||
name: name,
|
||||
address: address,
|
||||
description: description,
|
||||
contactNumber: contactNumber,
|
||||
manager: manager,
|
||||
latitude: latitude,
|
||||
longitude: longitude,
|
||||
);
|
||||
|
||||
final result = await createWarehouseLocationUseCase(params);
|
||||
|
||||
await result.fold(
|
||||
(failure) async => errorState = failure.message,
|
||||
(location) async => await refresh(),
|
||||
);
|
||||
} catch (e) {
|
||||
errorState = '오류 발생: $e';
|
||||
} finally {
|
||||
isLoadingState = false;
|
||||
}
|
||||
}
|
||||
|
||||
/// 창고 위치 수정
|
||||
Future<void> updateWarehouseLocation({
|
||||
required int id,
|
||||
String? name,
|
||||
String? address,
|
||||
String? description,
|
||||
String? contactNumber,
|
||||
String? manager,
|
||||
double? latitude,
|
||||
double? longitude,
|
||||
bool? isActive,
|
||||
}) async {
|
||||
try {
|
||||
isLoadingState = true;
|
||||
|
||||
final params = UpdateWarehouseLocationParams(
|
||||
id: id,
|
||||
name: name,
|
||||
address: address,
|
||||
description: description,
|
||||
contactNumber: contactNumber,
|
||||
manager: manager,
|
||||
latitude: latitude,
|
||||
longitude: longitude,
|
||||
isActive: isActive,
|
||||
);
|
||||
|
||||
final result = await updateWarehouseLocationUseCase(params);
|
||||
|
||||
await result.fold(
|
||||
(failure) async => errorState = failure.message,
|
||||
(location) async => updateItemLocally(location, (item) => item.id == location.id),
|
||||
);
|
||||
} catch (e) {
|
||||
errorState = '오류 발생: $e';
|
||||
} finally {
|
||||
isLoadingState = false;
|
||||
}
|
||||
}
|
||||
|
||||
/// 창고 위치 삭제
|
||||
Future<void> deleteWarehouseLocation(int id) async {
|
||||
try {
|
||||
isLoadingState = true;
|
||||
|
||||
final result = await deleteWarehouseLocationUseCase(id);
|
||||
|
||||
await result.fold(
|
||||
(failure) async => errorState = failure.message,
|
||||
(_) async {
|
||||
removeItemLocally((item) => item.id == id);
|
||||
_selectedLocationIds.remove(id);
|
||||
},
|
||||
);
|
||||
} catch (e) {
|
||||
errorState = '오류 발생: $e';
|
||||
} finally {
|
||||
isLoadingState = false;
|
||||
}
|
||||
}
|
||||
|
||||
/// 창고 위치 활성/비활성 토글
|
||||
Future<void> toggleLocationStatus(int id) async {
|
||||
final location = items.firstWhere((item) => item.id == id);
|
||||
await updateWarehouseLocation(
|
||||
id: id,
|
||||
isActive: !location.isActive,
|
||||
);
|
||||
}
|
||||
|
||||
/// 필터 설정
|
||||
void setFilters({
|
||||
bool? showActiveOnly,
|
||||
String? manager,
|
||||
}) {
|
||||
if (showActiveOnly != null) _showActiveOnly = showActiveOnly;
|
||||
_filterByManager = manager;
|
||||
refresh();
|
||||
}
|
||||
|
||||
/// 필터 초기화
|
||||
void clearFilters() {
|
||||
_showActiveOnly = true;
|
||||
_filterByManager = null;
|
||||
refresh();
|
||||
}
|
||||
|
||||
/// 창고 위치 선택 토글
|
||||
void toggleLocationSelection(int id) {
|
||||
if (_selectedLocationIds.contains(id)) {
|
||||
_selectedLocationIds.remove(id);
|
||||
} else {
|
||||
_selectedLocationIds.add(id);
|
||||
}
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
/// 모든 창고 위치 선택
|
||||
void selectAll() {
|
||||
_selectedLocationIds.clear();
|
||||
_selectedLocationIds.addAll(items.map((e) => e.id));
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
/// 선택 해제
|
||||
void clearSelection() {
|
||||
_selectedLocationIds.clear();
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
/// 선택된 창고 위치 일괄 삭제
|
||||
Future<void> deleteSelectedLocations() async {
|
||||
if (_selectedLocationIds.isEmpty) return;
|
||||
|
||||
try {
|
||||
isLoadingState = true;
|
||||
|
||||
final errors = <String>[];
|
||||
for (final id in _selectedLocationIds.toList()) {
|
||||
final result = await deleteWarehouseLocationUseCase(id);
|
||||
result.fold(
|
||||
(failure) => errors.add('Location $id: ${failure.message}'),
|
||||
(_) => removeItemLocally((item) => item.id == id),
|
||||
);
|
||||
}
|
||||
|
||||
_selectedLocationIds.clear();
|
||||
|
||||
if (errors.isNotEmpty) {
|
||||
errorState = '일부 창고 위치 삭제 실패:\n${errors.join('\n')}';
|
||||
}
|
||||
|
||||
notifyListeners();
|
||||
} catch (e) {
|
||||
errorState = '오류 발생: $e';
|
||||
} finally {
|
||||
isLoadingState = false;
|
||||
}
|
||||
}
|
||||
|
||||
/// 선택된 창고 위치 일괄 활성화
|
||||
Future<void> activateSelectedLocations() async {
|
||||
if (_selectedLocationIds.isEmpty) return;
|
||||
|
||||
try {
|
||||
isLoadingState = true;
|
||||
|
||||
for (final id in _selectedLocationIds.toList()) {
|
||||
await updateWarehouseLocation(id: id, isActive: true);
|
||||
}
|
||||
|
||||
_selectedLocationIds.clear();
|
||||
notifyListeners();
|
||||
} catch (e) {
|
||||
errorState = '오류 발생: $e';
|
||||
} finally {
|
||||
isLoadingState = false;
|
||||
}
|
||||
}
|
||||
|
||||
/// 선택된 창고 위치 일괄 비활성화
|
||||
Future<void> deactivateSelectedLocations() async {
|
||||
if (_selectedLocationIds.isEmpty) return;
|
||||
|
||||
try {
|
||||
isLoadingState = true;
|
||||
|
||||
for (final id in _selectedLocationIds.toList()) {
|
||||
await updateWarehouseLocation(id: id, isActive: false);
|
||||
}
|
||||
|
||||
_selectedLocationIds.clear();
|
||||
notifyListeners();
|
||||
} catch (e) {
|
||||
errorState = '오류 발생: $e';
|
||||
} finally {
|
||||
isLoadingState = false;
|
||||
}
|
||||
}
|
||||
|
||||
/// 드롭다운용 활성 창고 위치 목록 가져오기
|
||||
List<WarehouseLocationDto> getActiveLocations() {
|
||||
return items.where((location) => location.isActive).toList();
|
||||
}
|
||||
|
||||
/// 특정 관리자의 창고 위치 목록 가져오기
|
||||
List<WarehouseLocationDto> getLocationsByManager(String manager) {
|
||||
return items.where((location) => location.managerName == manager).toList(); // managerName 필드 사용
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_selectedLocationIds.clear();
|
||||
super.dispose();
|
||||
}
|
||||
}
|
||||
@@ -11,6 +11,7 @@ import 'package:superport/screens/common/widgets/standard_states.dart';
|
||||
import 'package:superport/screens/common/layouts/base_list_screen.dart';
|
||||
import 'package:superport/screens/warehouse_location/controllers/warehouse_location_list_controller.dart';
|
||||
import 'package:superport/utils/constants.dart';
|
||||
import 'package:superport/core/widgets/auth_guard.dart';
|
||||
|
||||
/// shadcn/ui 스타일로 재설계된 입고지 관리 화면
|
||||
class WarehouseLocationListRedesign extends StatefulWidget {
|
||||
@@ -99,10 +100,13 @@ class _WarehouseLocationListRedesignState
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return ChangeNotifierProvider.value(
|
||||
value: _controller,
|
||||
child: Consumer<WarehouseLocationListController>(
|
||||
builder: (context, controller, child) {
|
||||
// Admin과 Manager만 접근 가능
|
||||
return AuthGuard(
|
||||
allowedRoles: UserRole.adminAndManager,
|
||||
child: ChangeNotifierProvider.value(
|
||||
value: _controller,
|
||||
child: Consumer<WarehouseLocationListController>(
|
||||
builder: (context, controller, child) {
|
||||
final int totalCount = controller.warehouseLocations.length;
|
||||
final int startIndex = (_currentPage - 1) * _pageSize;
|
||||
final int endIndex =
|
||||
@@ -161,6 +165,7 @@ class _WarehouseLocationListRedesignState
|
||||
) : null,
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user