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

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

View File

@@ -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 ?? [];
}
}

View File

@@ -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 ?? [];
}
}
}

View File

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

View File

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