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,7 +3,6 @@ import 'package:get_it/get_it.dart';
|
||||
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';
|
||||
@@ -14,7 +13,6 @@ import 'package:superport/core/utils/debug_logger.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>();
|
||||
@@ -24,7 +22,7 @@ class EquipmentInFormController extends ChangeNotifier {
|
||||
bool _isLoading = false;
|
||||
String? _error;
|
||||
bool _isSaving = false;
|
||||
bool _useApi = true; // Feature flag
|
||||
// API만 사용
|
||||
|
||||
// Getters
|
||||
bool get isLoading => _isLoading;
|
||||
@@ -76,7 +74,7 @@ class EquipmentInFormController extends ChangeNotifier {
|
||||
|
||||
final TextEditingController remarkController = TextEditingController();
|
||||
|
||||
EquipmentInFormController({required this.dataService, this.equipmentInId}) {
|
||||
EquipmentInFormController({this.equipmentInId}) {
|
||||
isEditMode = equipmentInId != null;
|
||||
_loadManufacturers();
|
||||
_loadEquipmentNames();
|
||||
@@ -95,91 +93,71 @@ class EquipmentInFormController extends ChangeNotifier {
|
||||
await _loadEquipmentIn();
|
||||
}
|
||||
|
||||
// 제조사 목록 로드
|
||||
// 자동완성 데이터는 API를 통해 로드해야 하지만, 현재는 빈 목록으로 설정
|
||||
void _loadManufacturers() {
|
||||
manufacturers = dataService.getAllManufacturers();
|
||||
// TODO: API를 통해 제조사 목록 로드
|
||||
manufacturers = [];
|
||||
}
|
||||
|
||||
// 장비명 목록 로드
|
||||
void _loadEquipmentNames() {
|
||||
equipmentNames = dataService.getAllEquipmentNames();
|
||||
// TODO: API를 통해 장비명 목록 로드
|
||||
equipmentNames = [];
|
||||
}
|
||||
|
||||
// 카테고리 목록 로드
|
||||
void _loadCategories() {
|
||||
categories = dataService.getAllCategories();
|
||||
// TODO: API를 통해 카테고리 목록 로드
|
||||
categories = [];
|
||||
}
|
||||
|
||||
// 서브카테고리 목록 로드
|
||||
void _loadSubCategories() {
|
||||
subCategories = dataService.getAllSubCategories();
|
||||
// TODO: API를 통해 서브카테고리 목록 로드
|
||||
subCategories = [];
|
||||
}
|
||||
|
||||
// 서브서브카테고리 목록 로드
|
||||
void _loadSubSubCategories() {
|
||||
subSubCategories = dataService.getAllSubSubCategories();
|
||||
// TODO: API를 통해 서브서브카테고리 목록 로드
|
||||
subSubCategories = [];
|
||||
}
|
||||
|
||||
// 입고지 목록 로드
|
||||
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};
|
||||
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);
|
||||
// API 실패 시 빈 목록
|
||||
warehouseLocations = [];
|
||||
warehouseLocationMap = {};
|
||||
notifyListeners();
|
||||
}
|
||||
}
|
||||
|
||||
// 파트너사 목록 로드
|
||||
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();
|
||||
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);
|
||||
// API 실패 시 빈 목록
|
||||
partnerCompanies = [];
|
||||
notifyListeners();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -198,12 +176,11 @@ class EquipmentInFormController extends ChangeNotifier {
|
||||
notifyListeners();
|
||||
|
||||
try {
|
||||
if (_useApi) {
|
||||
// equipmentInId는 실제로 장비 ID임 (입고 ID가 아님)
|
||||
actualEquipmentId = equipmentInId;
|
||||
|
||||
try {
|
||||
// API에서 장비 정보 가져오기
|
||||
// equipmentInId는 실제로 장빔 ID임 (입고 ID가 아님)
|
||||
actualEquipmentId = equipmentInId;
|
||||
|
||||
try {
|
||||
// API에서 장비 정보 가져오기
|
||||
DebugLogger.log('장비 정보 로드 시작', tag: 'EQUIPMENT_IN', data: {
|
||||
'equipmentId': actualEquipmentId,
|
||||
});
|
||||
@@ -238,25 +215,8 @@ class EquipmentInFormController extends ChangeNotifier {
|
||||
|
||||
} catch (e) {
|
||||
DebugLogger.logError('장비 정보 로드 실패', error: e);
|
||||
// API 실패 시 Mock 데이터 시도
|
||||
final equipmentIn = dataService.getEquipmentInById(equipmentInId!);
|
||||
if (equipmentIn != null) {
|
||||
actualEquipmentId = equipmentIn.equipment.id;
|
||||
_loadFromMockData(equipmentIn);
|
||||
} else {
|
||||
throw ServerFailure(message: '장비 정보를 찾을 수 없습니다.');
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Mock 데이터 사용
|
||||
final equipmentIn = dataService.getEquipmentInById(equipmentInId!);
|
||||
if (equipmentIn != null) {
|
||||
actualEquipmentId = equipmentIn.equipment.id;
|
||||
_loadFromMockData(equipmentIn);
|
||||
} else {
|
||||
throw ServerFailure(message: '장비 정보를 찾을 수 없습니다.');
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
_error = '장비 정보를 불러오는데 실패했습니다: $e';
|
||||
DebugLogger.logError('장비 로드 실패', error: e);
|
||||
@@ -266,28 +226,6 @@ class EquipmentInFormController extends ChangeNotifier {
|
||||
}
|
||||
}
|
||||
|
||||
void _loadFromMockData(EquipmentIn equipmentIn) {
|
||||
manufacturer = equipmentIn.equipment.manufacturer;
|
||||
name = equipmentIn.equipment.name;
|
||||
category = equipmentIn.equipment.category;
|
||||
subCategory = equipmentIn.equipment.subCategory;
|
||||
subSubCategory = equipmentIn.equipment.subSubCategory;
|
||||
serialNumber = equipmentIn.equipment.serialNumber ?? '';
|
||||
barcode = equipmentIn.equipment.barcode ?? '';
|
||||
quantity = equipmentIn.equipment.quantity;
|
||||
inDate = equipmentIn.inDate;
|
||||
hasSerialNumber = serialNumber.isNotEmpty;
|
||||
equipmentType = equipmentIn.type;
|
||||
warehouseLocation = equipmentIn.warehouseLocation;
|
||||
partnerCompany = equipmentIn.partnerCompany;
|
||||
remarkController.text = equipmentIn.remark ?? '';
|
||||
|
||||
// 워런티 정보 로드
|
||||
warrantyLicense = equipmentIn.partnerCompany;
|
||||
warrantyStartDate = equipmentIn.inDate;
|
||||
warrantyEndDate = equipmentIn.inDate.add(const Duration(days: 365));
|
||||
warrantyCode = null;
|
||||
}
|
||||
|
||||
// 워런티 기간 계산
|
||||
String getWarrantyPeriodSummary() {
|
||||
@@ -374,9 +312,8 @@ class EquipmentInFormController extends ChangeNotifier {
|
||||
// 워런티 코드 저장 필요시 여기에 추가
|
||||
);
|
||||
|
||||
if (_useApi) {
|
||||
// API 호출
|
||||
if (isEditMode) {
|
||||
// API 호출
|
||||
if (isEditMode) {
|
||||
// 수정 모드 - API로 장비 정보 업데이트
|
||||
if (actualEquipmentId == null) {
|
||||
throw ServerFailure(message: '장비 ID가 없습니다.');
|
||||
@@ -437,35 +374,6 @@ class EquipmentInFormController extends ChangeNotifier {
|
||||
throw e; // 에러를 상위로 전파하여 적절한 에러 메시지 표시
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Mock 데이터 사용
|
||||
if (isEditMode) {
|
||||
final equipmentIn = dataService.getEquipmentInById(equipmentInId!);
|
||||
if (equipmentIn != null) {
|
||||
final updatedEquipmentIn = EquipmentIn(
|
||||
id: equipmentIn.id,
|
||||
equipment: equipment,
|
||||
inDate: inDate,
|
||||
status: equipmentIn.status,
|
||||
type: equipmentType,
|
||||
warehouseLocation: warehouseLocation,
|
||||
partnerCompany: partnerCompany,
|
||||
remark: remarkController.text.trim(),
|
||||
);
|
||||
dataService.updateEquipmentIn(updatedEquipmentIn);
|
||||
}
|
||||
} else {
|
||||
final newEquipmentIn = EquipmentIn(
|
||||
equipment: equipment,
|
||||
inDate: inDate,
|
||||
type: equipmentType,
|
||||
warehouseLocation: warehouseLocation,
|
||||
partnerCompany: partnerCompany,
|
||||
remark: remarkController.text.trim(),
|
||||
);
|
||||
dataService.addEquipmentIn(newEquipmentIn);
|
||||
}
|
||||
}
|
||||
|
||||
// 저장 후 리스트 재로딩 (중복 방지 및 최신화)
|
||||
_loadManufacturers();
|
||||
@@ -498,11 +406,7 @@ class EquipmentInFormController extends ChangeNotifier {
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
// API 사용 여부 토글 (테스트용)
|
||||
void toggleApiUsage() {
|
||||
_useApi = !_useApi;
|
||||
notifyListeners();
|
||||
}
|
||||
// API만 사용하므로 토글 기능 제거
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
|
||||
@@ -0,0 +1,281 @@
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:get_it/get_it.dart';
|
||||
import 'package:superport/models/equipment_unified_model.dart';
|
||||
import 'package:superport/services/equipment_service.dart';
|
||||
import 'package:superport/utils/constants.dart';
|
||||
import 'package:superport/core/errors/failures.dart';
|
||||
import 'package:superport/models/equipment_unified_model.dart' as legacy;
|
||||
import 'package:superport/core/utils/debug_logger.dart';
|
||||
|
||||
// companyTypeToString 함수 import
|
||||
import 'package:superport/utils/constants.dart'
|
||||
show companyTypeToString, CompanyType;
|
||||
import 'package:superport/models/company_model.dart';
|
||||
import 'package:superport/models/address_model.dart';
|
||||
import 'package:superport/core/utils/equipment_status_converter.dart';
|
||||
|
||||
// 장비 목록 화면의 상태 및 비즈니스 로직을 담당하는 컨트롤러
|
||||
class EquipmentListController extends ChangeNotifier {
|
||||
final EquipmentService _equipmentService = GetIt.instance<EquipmentService>();
|
||||
|
||||
List<UnifiedEquipment> equipments = [];
|
||||
String? selectedStatusFilter;
|
||||
String searchKeyword = ''; // 검색어 추가
|
||||
final Set<String> selectedEquipmentIds = {}; // 'id:status' 형식
|
||||
|
||||
bool _isLoading = false;
|
||||
String? _error;
|
||||
// API만 사용
|
||||
|
||||
// 페이지네이션
|
||||
int _currentPage = 1;
|
||||
final int _perPage = 20;
|
||||
bool _hasMore = true;
|
||||
|
||||
// Getters
|
||||
bool get isLoading => _isLoading;
|
||||
String? get error => _error;
|
||||
bool get hasMore => _hasMore;
|
||||
int get currentPage => _currentPage;
|
||||
|
||||
EquipmentListController();
|
||||
|
||||
// 데이터 로드 및 상태 필터 적용
|
||||
Future<void> loadData({bool isRefresh = false, String? search}) async {
|
||||
if (_isLoading) return;
|
||||
|
||||
_isLoading = true;
|
||||
_error = null;
|
||||
notifyListeners();
|
||||
|
||||
try {
|
||||
// API 호출 - 전체 데이터 로드
|
||||
print('╔══════════════════════════════════════════════════════════');
|
||||
print('║ 📦 장비 목록 API 호출 시작');
|
||||
print('║ • 상태 필터: ${selectedStatusFilter ?? "전체"}');
|
||||
print('║ • 검색어: ${search ?? searchKeyword}');
|
||||
print('╚══════════════════════════════════════════════════════════');
|
||||
|
||||
// 전체 데이터를 가져오기 위해 큰 perPage 값 사용
|
||||
final apiEquipmentDtos = await _equipmentService.getEquipmentsWithStatus(
|
||||
page: 1,
|
||||
perPage: 1000, // 충분히 큰 값으로 전체 데이터 로드
|
||||
status: selectedStatusFilter != null ? EquipmentStatusConverter.clientToServer(selectedStatusFilter) : null,
|
||||
search: search ?? searchKeyword,
|
||||
);
|
||||
|
||||
print('╔══════════════════════════════════════════════════════════');
|
||||
print('║ 📊 장비 목록 로드 완료');
|
||||
print('║ ▶ 총 장비 수: ${apiEquipmentDtos.length}개');
|
||||
print('╟──────────────────────────────────────────────────────────');
|
||||
|
||||
// 상태별 통계
|
||||
Map<String, int> statusCount = {};
|
||||
for (final dto in apiEquipmentDtos) {
|
||||
final clientStatus = EquipmentStatusConverter.serverToClient(dto.status);
|
||||
statusCount[clientStatus] = (statusCount[clientStatus] ?? 0) + 1;
|
||||
}
|
||||
|
||||
statusCount.forEach((status, count) {
|
||||
print('║ • $status: $count개');
|
||||
});
|
||||
|
||||
print('╟──────────────────────────────────────────────────────────');
|
||||
print('║ 📑 전체 데이터 로드 완료');
|
||||
print('║ • View에서 페이지네이션 처리 예정');
|
||||
print('╚══════════════════════════════════════════════════════════');
|
||||
|
||||
// DTO를 UnifiedEquipment로 변환 (status 정보 포함)
|
||||
final List<UnifiedEquipment> unifiedEquipments = apiEquipmentDtos.map((dto) {
|
||||
final equipment = Equipment(
|
||||
id: dto.id,
|
||||
manufacturer: dto.manufacturer,
|
||||
name: dto.modelName ?? dto.equipmentNumber,
|
||||
category: '', // 세부 정보는 상세 조회에서 가져와야 함
|
||||
subCategory: '',
|
||||
subSubCategory: '',
|
||||
serialNumber: dto.serialNumber,
|
||||
quantity: 1,
|
||||
inDate: dto.createdAt,
|
||||
);
|
||||
|
||||
return UnifiedEquipment(
|
||||
id: dto.id,
|
||||
equipment: equipment,
|
||||
date: dto.createdAt,
|
||||
status: EquipmentStatusConverter.serverToClient(dto.status), // 서버 status를 클라이언트 status로 변환
|
||||
);
|
||||
}).toList();
|
||||
|
||||
equipments = unifiedEquipments;
|
||||
_hasMore = false; // 전체 데이터를 로드했으므로 더 이상 로드할 필요 없음
|
||||
|
||||
selectedEquipmentIds.clear();
|
||||
} on Failure catch (e) {
|
||||
_error = e.message;
|
||||
} catch (e) {
|
||||
_error = 'An unexpected error occurred: $e';
|
||||
} finally {
|
||||
_isLoading = false;
|
||||
notifyListeners();
|
||||
}
|
||||
}
|
||||
|
||||
// 상태 필터 변경
|
||||
Future<void> changeStatusFilter(String? status) async {
|
||||
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) {
|
||||
if (id == null || isSelected == null) return;
|
||||
final key = '$id:$status';
|
||||
if (isSelected) {
|
||||
selectedEquipmentIds.add(key);
|
||||
} else {
|
||||
selectedEquipmentIds.remove(key);
|
||||
}
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
// 선택된 입고 장비 수 반환
|
||||
int getSelectedInStockCount() {
|
||||
int count = 0;
|
||||
for (final idStatusPair in selectedEquipmentIds) {
|
||||
final parts = idStatusPair.split(':');
|
||||
if (parts.length == 2 && parts[1] == EquipmentStatus.in_) {
|
||||
count++;
|
||||
}
|
||||
}
|
||||
return count;
|
||||
}
|
||||
|
||||
// 선택된 전체 장비 수 반환
|
||||
int getSelectedEquipmentCount() {
|
||||
return selectedEquipmentIds.length;
|
||||
}
|
||||
|
||||
// 선택된 특정 상태의 장비 수 반환
|
||||
int getSelectedEquipmentCountByStatus(String status) {
|
||||
int count = 0;
|
||||
for (final idStatusPair in selectedEquipmentIds) {
|
||||
final parts = idStatusPair.split(':');
|
||||
if (parts.length == 2 && parts[1] == status) {
|
||||
count++;
|
||||
}
|
||||
}
|
||||
return count;
|
||||
}
|
||||
|
||||
// 선택된 장비들의 UnifiedEquipment 객체 목록 반환
|
||||
List<UnifiedEquipment> getSelectedEquipments() {
|
||||
List<UnifiedEquipment> selected = [];
|
||||
for (final idStatusPair in selectedEquipmentIds) {
|
||||
final parts = idStatusPair.split(':');
|
||||
if (parts.length == 2) {
|
||||
final id = int.tryParse(parts[0]);
|
||||
if (id != null) {
|
||||
final equipment = equipments.firstWhere(
|
||||
(e) => e.id == id && e.status == parts[1],
|
||||
orElse: () => null as UnifiedEquipment,
|
||||
);
|
||||
if (equipment != null) {
|
||||
selected.add(equipment);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return selected;
|
||||
}
|
||||
|
||||
// 선택된 특정 상태의 장비들의 UnifiedEquipment 객체 목록 반환
|
||||
List<UnifiedEquipment> getSelectedEquipmentsByStatus(String status) {
|
||||
List<UnifiedEquipment> selected = [];
|
||||
for (final idStatusPair in selectedEquipmentIds) {
|
||||
final parts = idStatusPair.split(':');
|
||||
if (parts.length == 2 && parts[1] == status) {
|
||||
final id = int.tryParse(parts[0]);
|
||||
if (id != null) {
|
||||
final equipment = equipments.firstWhere(
|
||||
(e) => e.id == id && e.status == status,
|
||||
orElse: () => null as UnifiedEquipment,
|
||||
);
|
||||
if (equipment != null) {
|
||||
selected.add(equipment);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return selected;
|
||||
}
|
||||
|
||||
// 선택된 장비들의 요약 정보를 Map 형태로 반환 (출고/대여/폐기 폼에서 사용)
|
||||
List<Map<String, dynamic>> getSelectedEquipmentsSummary() {
|
||||
List<Map<String, dynamic>> summaryList = [];
|
||||
List<UnifiedEquipment> selectedEquipmentsInStock =
|
||||
getSelectedEquipmentsByStatus(EquipmentStatus.in_);
|
||||
|
||||
for (final equipment in selectedEquipmentsInStock) {
|
||||
summaryList.add({
|
||||
'equipment': equipment.equipment,
|
||||
'equipmentInId': equipment.id,
|
||||
'status': equipment.status,
|
||||
});
|
||||
}
|
||||
|
||||
return summaryList;
|
||||
}
|
||||
|
||||
// 출고 정보(회사, 담당자, 라이센스 등) 반환
|
||||
// 출고 정보는 API를 통해 번별로 조회해야 하므로 별도 서비스로 분리 예정
|
||||
String getOutEquipmentInfo(int equipmentId, String infoType) {
|
||||
// TODO: API로 출고 정보 조회 구현
|
||||
return '-';
|
||||
}
|
||||
|
||||
// 장비 삭제
|
||||
Future<bool> deleteEquipment(UnifiedEquipment equipment) async {
|
||||
try {
|
||||
// API를 통한 삭제
|
||||
if (equipment.equipment.id != null) {
|
||||
await _equipmentService.deleteEquipment(equipment.equipment.id!);
|
||||
} else {
|
||||
throw Exception('Equipment ID is null');
|
||||
}
|
||||
|
||||
// 로컬 리스트에서도 제거
|
||||
equipments.removeWhere((e) => e.id == equipment.id && e.status == equipment.status);
|
||||
notifyListeners();
|
||||
|
||||
return true;
|
||||
} on Failure catch (e) {
|
||||
_error = e.message;
|
||||
notifyListeners();
|
||||
return false;
|
||||
} catch (e) {
|
||||
_error = 'Failed to delete equipment: $e';
|
||||
notifyListeners();
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// API만 사용하므로 토글 기능 제거
|
||||
|
||||
// 에러 처리
|
||||
void clearError() {
|
||||
_error = null;
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
super.dispose();
|
||||
}
|
||||
}
|
||||
@@ -1,338 +1,315 @@
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get_it/get_it.dart';
|
||||
import 'package:superport/models/equipment_unified_model.dart';
|
||||
import 'package:superport/services/equipment_service.dart';
|
||||
import 'package:superport/services/mock_data_service.dart';
|
||||
import 'package:superport/utils/constants.dart';
|
||||
import 'package:superport/core/errors/failures.dart';
|
||||
import 'package:superport/models/equipment_unified_model.dart' as legacy;
|
||||
import 'package:superport/core/utils/debug_logger.dart';
|
||||
|
||||
// companyTypeToString 함수 import
|
||||
import 'package:superport/utils/constants.dart'
|
||||
show companyTypeToString, CompanyType;
|
||||
import 'package:superport/core/utils/error_handler.dart';
|
||||
import 'package:superport/core/controllers/base_list_controller.dart';
|
||||
import 'package:superport/core/utils/equipment_status_converter.dart';
|
||||
import 'package:superport/models/company_model.dart';
|
||||
import 'package:superport/models/address_model.dart';
|
||||
import 'package:superport/core/utils/equipment_status_converter.dart';
|
||||
import 'package:superport/data/models/common/pagination_params.dart';
|
||||
|
||||
// 장비 목록 화면의 상태 및 비즈니스 로직을 담당하는 컨트롤러
|
||||
class EquipmentListController extends ChangeNotifier {
|
||||
final MockDataService dataService;
|
||||
final EquipmentService _equipmentService = GetIt.instance<EquipmentService>();
|
||||
/// 장비 목록 화면의 상태 및 비즈니스 로직을 담당하는 컨트롤러 (리팩토링 버전)
|
||||
/// BaseListController를 상속받아 공통 기능을 재사용
|
||||
class EquipmentListController extends BaseListController<UnifiedEquipment> {
|
||||
late final EquipmentService _equipmentService;
|
||||
|
||||
List<UnifiedEquipment> equipments = [];
|
||||
String? selectedStatusFilter;
|
||||
String searchKeyword = ''; // 검색어 추가
|
||||
// 추가 상태 관리
|
||||
final Set<String> selectedEquipmentIds = {}; // 'id:status' 형식
|
||||
|
||||
bool _isLoading = false;
|
||||
String? _error;
|
||||
bool _useApi = true; // Feature flag for API usage
|
||||
|
||||
// 페이지네이션
|
||||
int _currentPage = 1;
|
||||
final int _perPage = 20;
|
||||
bool _hasMore = true;
|
||||
// 필터
|
||||
String? _statusFilter;
|
||||
String? _categoryFilter;
|
||||
int? _companyIdFilter;
|
||||
String? _selectedStatusFilter;
|
||||
|
||||
// Getters
|
||||
bool get isLoading => _isLoading;
|
||||
String? get error => _error;
|
||||
bool get hasMore => _hasMore;
|
||||
int get currentPage => _currentPage;
|
||||
|
||||
EquipmentListController({required this.dataService});
|
||||
|
||||
// 데이터 로드 및 상태 필터 적용
|
||||
Future<void> loadData({bool isRefresh = false, String? search}) async {
|
||||
if (_isLoading) return;
|
||||
|
||||
_isLoading = true;
|
||||
_error = null;
|
||||
notifyListeners();
|
||||
|
||||
try {
|
||||
if (_useApi) {
|
||||
// API 호출 - 전체 데이터 로드
|
||||
print('╔══════════════════════════════════════════════════════════');
|
||||
print('║ 📦 장비 목록 API 호출 시작');
|
||||
print('║ • 상태 필터: ${selectedStatusFilter ?? "전체"}');
|
||||
print('║ • 검색어: ${search ?? searchKeyword}');
|
||||
print('╚══════════════════════════════════════════════════════════');
|
||||
|
||||
// 전체 데이터를 가져오기 위해 큰 perPage 값 사용
|
||||
final apiEquipmentDtos = await _equipmentService.getEquipmentsWithStatus(
|
||||
page: 1,
|
||||
perPage: 1000, // 충분히 큰 값으로 전체 데이터 로드
|
||||
status: selectedStatusFilter != null ? EquipmentStatusConverter.clientToServer(selectedStatusFilter) : null,
|
||||
search: search ?? searchKeyword,
|
||||
);
|
||||
|
||||
print('╔══════════════════════════════════════════════════════════');
|
||||
print('║ 📊 장비 목록 로드 완료');
|
||||
print('║ ▶ 총 장비 수: ${apiEquipmentDtos.length}개');
|
||||
print('╟──────────────────────────────────────────────────────────');
|
||||
|
||||
// 상태별 통계
|
||||
Map<String, int> statusCount = {};
|
||||
for (final dto in apiEquipmentDtos) {
|
||||
final clientStatus = EquipmentStatusConverter.serverToClient(dto.status);
|
||||
statusCount[clientStatus] = (statusCount[clientStatus] ?? 0) + 1;
|
||||
}
|
||||
|
||||
statusCount.forEach((status, count) {
|
||||
print('║ • $status: $count개');
|
||||
});
|
||||
|
||||
print('╟──────────────────────────────────────────────────────────');
|
||||
print('║ 📑 전체 데이터 로드 완료');
|
||||
print('║ • View에서 페이지네이션 처리 예정');
|
||||
print('╚══════════════════════════════════════════════════════════');
|
||||
|
||||
// DTO를 UnifiedEquipment로 변환 (status 정보 포함)
|
||||
final List<UnifiedEquipment> unifiedEquipments = apiEquipmentDtos.map((dto) {
|
||||
final equipment = Equipment(
|
||||
id: dto.id,
|
||||
manufacturer: dto.manufacturer,
|
||||
name: dto.modelName ?? dto.equipmentNumber,
|
||||
category: '', // 세부 정보는 상세 조회에서 가져와야 함
|
||||
subCategory: '',
|
||||
subSubCategory: '',
|
||||
serialNumber: dto.serialNumber,
|
||||
quantity: 1,
|
||||
inDate: dto.createdAt,
|
||||
);
|
||||
|
||||
return UnifiedEquipment(
|
||||
id: dto.id,
|
||||
equipment: equipment,
|
||||
date: dto.createdAt,
|
||||
status: EquipmentStatusConverter.serverToClient(dto.status), // 서버 status를 클라이언트 status로 변환
|
||||
);
|
||||
}).toList();
|
||||
|
||||
equipments = unifiedEquipments;
|
||||
_hasMore = false; // 전체 데이터를 로드했으므로 더 이상 로드할 필요 없음
|
||||
} else {
|
||||
// Mock 데이터 사용
|
||||
equipments = dataService.getAllEquipments();
|
||||
if (selectedStatusFilter != null) {
|
||||
equipments =
|
||||
equipments.where((e) => e.status == selectedStatusFilter).toList();
|
||||
}
|
||||
_hasMore = false;
|
||||
}
|
||||
|
||||
selectedEquipmentIds.clear();
|
||||
} on Failure catch (e) {
|
||||
_error = e.message;
|
||||
} catch (e) {
|
||||
_error = 'An unexpected error occurred: $e';
|
||||
} finally {
|
||||
_isLoading = false;
|
||||
notifyListeners();
|
||||
}
|
||||
}
|
||||
|
||||
// 상태 필터 변경
|
||||
Future<void> changeStatusFilter(String? status) async {
|
||||
selectedStatusFilter = status;
|
||||
await loadData(isRefresh: true);
|
||||
}
|
||||
List<UnifiedEquipment> get equipments => items;
|
||||
String? get statusFilter => _statusFilter;
|
||||
String? get categoryFilter => _categoryFilter;
|
||||
int? get companyIdFilter => _companyIdFilter;
|
||||
String? get selectedStatusFilter => _selectedStatusFilter;
|
||||
|
||||
// 검색어 변경
|
||||
Future<void> updateSearchKeyword(String keyword) async {
|
||||
searchKeyword = keyword;
|
||||
await loadData(isRefresh: true, search: keyword);
|
||||
// Setters
|
||||
set selectedStatusFilter(String? value) {
|
||||
_selectedStatusFilter = value;
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
// 장비 선택/해제 (모든 상태 지원)
|
||||
void selectEquipment(int? id, String status, bool? isSelected) {
|
||||
if (id == null || isSelected == null) return;
|
||||
final key = '$id:$status';
|
||||
if (isSelected) {
|
||||
selectedEquipmentIds.add(key);
|
||||
EquipmentListController() {
|
||||
if (GetIt.instance.isRegistered<EquipmentService>()) {
|
||||
_equipmentService = GetIt.instance<EquipmentService>();
|
||||
} else {
|
||||
selectedEquipmentIds.remove(key);
|
||||
throw Exception('EquipmentService not registered in GetIt');
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Future<PagedResult<UnifiedEquipment>> fetchData({
|
||||
required PaginationParams params,
|
||||
Map<String, dynamic>? additionalFilters,
|
||||
}) async {
|
||||
// API 호출
|
||||
final apiEquipmentDtos = await ErrorHandler.handleApiCall(
|
||||
() => _equipmentService.getEquipmentsWithStatus(
|
||||
page: params.page,
|
||||
perPage: params.perPage,
|
||||
status: _statusFilter != null ?
|
||||
EquipmentStatusConverter.clientToServer(_statusFilter) : null,
|
||||
search: params.search,
|
||||
companyId: _companyIdFilter,
|
||||
),
|
||||
onError: (failure) {
|
||||
throw failure;
|
||||
},
|
||||
);
|
||||
|
||||
if (apiEquipmentDtos == null) {
|
||||
return PagedResult(
|
||||
items: [],
|
||||
meta: PaginationMeta(
|
||||
currentPage: params.page,
|
||||
perPage: params.perPage,
|
||||
total: 0,
|
||||
totalPages: 0,
|
||||
hasNext: false,
|
||||
hasPrevious: false,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// DTO를 UnifiedEquipment로 변환
|
||||
final items = apiEquipmentDtos.map((dto) {
|
||||
final equipment = Equipment(
|
||||
id: dto.id,
|
||||
manufacturer: dto.manufacturer ?? 'Unknown',
|
||||
name: dto.modelName ?? dto.equipmentNumber ?? 'Unknown',
|
||||
category: 'Equipment', // 임시 카테고리
|
||||
subCategory: 'General', // 임시 서브카테고리
|
||||
subSubCategory: 'Standard', // 임시 서브서브카테고리
|
||||
serialNumber: dto.serialNumber,
|
||||
quantity: 1, // 기본 수량
|
||||
);
|
||||
|
||||
// 간단한 Company 정보 생성 (사용하지 않으므로 제거)
|
||||
// final company = dto.companyName != null ? ... : null;
|
||||
|
||||
return UnifiedEquipment(
|
||||
id: dto.id,
|
||||
equipment: equipment,
|
||||
date: dto.createdAt ?? DateTime.now(),
|
||||
status: EquipmentStatusConverter.serverToClient(dto.status),
|
||||
notes: null, // EquipmentListDto에 remark 필드 없음
|
||||
);
|
||||
}).toList();
|
||||
|
||||
// 임시로 메타데이터 생성 (추후 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(UnifiedEquipment item, String query) {
|
||||
final q = query.toLowerCase();
|
||||
return (item.equipment.name.toLowerCase().contains(q)) ||
|
||||
(item.equipment.serialNumber?.toLowerCase().contains(q) ?? false) ||
|
||||
(item.equipment.manufacturer.toLowerCase().contains(q)) ||
|
||||
(item.notes?.toLowerCase().contains(q) ?? false) ||
|
||||
(item.status.toLowerCase().contains(q));
|
||||
}
|
||||
|
||||
/// 장비 선택/선택 해제
|
||||
void toggleSelection(UnifiedEquipment equipment) {
|
||||
final equipmentKey = '${equipment.equipment.id}:${equipment.status}';
|
||||
if (selectedEquipmentIds.contains(equipmentKey)) {
|
||||
selectedEquipmentIds.remove(equipmentKey);
|
||||
} else {
|
||||
selectedEquipmentIds.add(equipmentKey);
|
||||
}
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
// 선택된 입고 장비 수 반환
|
||||
int getSelectedInStockCount() {
|
||||
int count = 0;
|
||||
for (final idStatusPair in selectedEquipmentIds) {
|
||||
final parts = idStatusPair.split(':');
|
||||
if (parts.length == 2 && parts[1] == EquipmentStatus.in_) {
|
||||
count++;
|
||||
}
|
||||
}
|
||||
return count;
|
||||
/// 모든 선택 해제
|
||||
void clearSelection() {
|
||||
selectedEquipmentIds.clear();
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
// 선택된 전체 장비 수 반환
|
||||
/// 선택된 장비 정보 가져오기
|
||||
Map<String, List<UnifiedEquipment>> getSelectedEquipmentsByStatus() {
|
||||
final Map<String, List<UnifiedEquipment>> groupedEquipments = {};
|
||||
|
||||
for (final equipment in items) {
|
||||
final equipmentKey = '${equipment.equipment.id}:${equipment.status}';
|
||||
if (selectedEquipmentIds.contains(equipmentKey)) {
|
||||
if (!groupedEquipments.containsKey(equipment.status)) {
|
||||
groupedEquipments[equipment.status] = [];
|
||||
}
|
||||
groupedEquipments[equipment.status]!.add(equipment);
|
||||
}
|
||||
}
|
||||
|
||||
return groupedEquipments;
|
||||
}
|
||||
|
||||
/// 필터 설정
|
||||
void setFilters({
|
||||
String? status,
|
||||
String? category,
|
||||
int? companyId,
|
||||
}) {
|
||||
_statusFilter = status;
|
||||
_categoryFilter = category;
|
||||
_companyIdFilter = companyId;
|
||||
loadData(isRefresh: true);
|
||||
}
|
||||
|
||||
/// 상태 필터 변경
|
||||
void filterByStatus(String? status) {
|
||||
_statusFilter = status;
|
||||
loadData(isRefresh: true);
|
||||
}
|
||||
|
||||
/// 카테고리 필터 변경
|
||||
void filterByCategory(String? category) {
|
||||
_categoryFilter = category;
|
||||
loadData(isRefresh: true);
|
||||
}
|
||||
|
||||
/// 회사 필터 변경
|
||||
void filterByCompany(int? companyId) {
|
||||
_companyIdFilter = companyId;
|
||||
loadData(isRefresh: true);
|
||||
}
|
||||
|
||||
/// 필터 초기화
|
||||
void clearFilters() {
|
||||
_statusFilter = null;
|
||||
_categoryFilter = null;
|
||||
_companyIdFilter = null;
|
||||
search('');
|
||||
loadData(isRefresh: true);
|
||||
}
|
||||
|
||||
/// 장비 삭제
|
||||
Future<void> deleteEquipment(int id, String status) async {
|
||||
await ErrorHandler.handleApiCall<void>(
|
||||
() => _equipmentService.deleteEquipment(id),
|
||||
);
|
||||
|
||||
removeItemLocally((e) => e.equipment.id == id && e.status == status);
|
||||
|
||||
// 선택 목록에서도 제거
|
||||
final equipmentKey = '$id:$status';
|
||||
selectedEquipmentIds.remove(equipmentKey);
|
||||
}
|
||||
|
||||
/// 선택된 장비 일괄 삭제
|
||||
Future<void> deleteSelectedEquipments() async {
|
||||
final selectedGroups = getSelectedEquipmentsByStatus();
|
||||
|
||||
for (final entry in selectedGroups.entries) {
|
||||
for (final equipment in entry.value) {
|
||||
if (equipment.equipment.id != null) {
|
||||
await deleteEquipment(equipment.equipment.id!, equipment.status);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
clearSelection();
|
||||
}
|
||||
|
||||
/// 장비 상태 변경 (임시 구현 - API가 지원하지 않음)
|
||||
Future<void> updateEquipmentStatus(int id, String currentStatus, String newStatus) async {
|
||||
debugPrint('장비 상태 변경: $id, $currentStatus -> $newStatus');
|
||||
// TODO: 실제 API가 장비 상태 변경을 지원할 때 구현
|
||||
// 현재는 새로고침만 수행
|
||||
await refresh();
|
||||
}
|
||||
|
||||
/// 장비 정보 수정
|
||||
Future<void> updateEquipment(int id, UnifiedEquipment equipment) async {
|
||||
await ErrorHandler.handleApiCall<void>(
|
||||
() => _equipmentService.updateEquipment(id, equipment.equipment),
|
||||
onError: (failure) {
|
||||
throw failure;
|
||||
},
|
||||
);
|
||||
|
||||
updateItemLocally(equipment, (e) =>
|
||||
e.equipment.id == equipment.equipment.id &&
|
||||
e.status == equipment.status
|
||||
);
|
||||
}
|
||||
|
||||
/// 상태 필터 변경
|
||||
void changeStatusFilter(String? status) {
|
||||
_selectedStatusFilter = status;
|
||||
_statusFilter = status;
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
/// 검색 키워드 업데이트
|
||||
void updateSearchKeyword(String keyword) {
|
||||
search(keyword); // BaseListController의 search 메서드 사용
|
||||
}
|
||||
|
||||
/// 장비 선택 (토글 선택을 위한 별칭)
|
||||
void selectEquipment(UnifiedEquipment equipment) {
|
||||
toggleSelection(equipment);
|
||||
}
|
||||
|
||||
/// 선택된 입고 상태 장비 개수
|
||||
int getSelectedInStockCount() {
|
||||
return selectedEquipmentIds
|
||||
.where((key) => key.endsWith(':입고'))
|
||||
.length;
|
||||
}
|
||||
|
||||
/// 선택된 장비들 가져오기
|
||||
List<UnifiedEquipment> getSelectedEquipments() {
|
||||
return items.where((equipment) {
|
||||
final equipmentKey = '${equipment.equipment.id}:${equipment.status}';
|
||||
return selectedEquipmentIds.contains(equipmentKey);
|
||||
}).toList();
|
||||
}
|
||||
|
||||
/// 선택된 장비들 요약 정보
|
||||
String getSelectedEquipmentsSummary() {
|
||||
final selectedEquipments = getSelectedEquipments();
|
||||
if (selectedEquipments.isEmpty) return '선택된 장비가 없습니다';
|
||||
|
||||
final Map<String, int> statusCounts = {};
|
||||
for (final equipment in selectedEquipments) {
|
||||
statusCounts[equipment.status] = (statusCounts[equipment.status] ?? 0) + 1;
|
||||
}
|
||||
|
||||
final summaryParts = statusCounts.entries
|
||||
.map((entry) => '${entry.key}: ${entry.value}개')
|
||||
.toList();
|
||||
|
||||
return summaryParts.join(', ');
|
||||
}
|
||||
|
||||
/// 선택된 장비 총 개수
|
||||
int getSelectedEquipmentCount() {
|
||||
return selectedEquipmentIds.length;
|
||||
}
|
||||
|
||||
// 선택된 특정 상태의 장비 수 반환
|
||||
/// 특정 상태의 선택된 장비 개수
|
||||
int getSelectedEquipmentCountByStatus(String status) {
|
||||
int count = 0;
|
||||
for (final idStatusPair in selectedEquipmentIds) {
|
||||
final parts = idStatusPair.split(':');
|
||||
if (parts.length == 2 && parts[1] == status) {
|
||||
count++;
|
||||
}
|
||||
}
|
||||
return count;
|
||||
return selectedEquipmentIds
|
||||
.where((key) => key.endsWith(':$status'))
|
||||
.length;
|
||||
}
|
||||
|
||||
// 선택된 장비들의 UnifiedEquipment 객체 목록 반환
|
||||
List<UnifiedEquipment> getSelectedEquipments() {
|
||||
List<UnifiedEquipment> selected = [];
|
||||
for (final idStatusPair in selectedEquipmentIds) {
|
||||
final parts = idStatusPair.split(':');
|
||||
if (parts.length == 2) {
|
||||
final id = int.tryParse(parts[0]);
|
||||
if (id != null) {
|
||||
final equipment = equipments.firstWhere(
|
||||
(e) => e.id == id && e.status == parts[1],
|
||||
orElse: () => null as UnifiedEquipment,
|
||||
);
|
||||
if (equipment != null) {
|
||||
selected.add(equipment);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return selected;
|
||||
}
|
||||
|
||||
// 선택된 특정 상태의 장비들의 UnifiedEquipment 객체 목록 반환
|
||||
List<UnifiedEquipment> getSelectedEquipmentsByStatus(String status) {
|
||||
List<UnifiedEquipment> selected = [];
|
||||
for (final idStatusPair in selectedEquipmentIds) {
|
||||
final parts = idStatusPair.split(':');
|
||||
if (parts.length == 2 && parts[1] == status) {
|
||||
final id = int.tryParse(parts[0]);
|
||||
if (id != null) {
|
||||
final equipment = equipments.firstWhere(
|
||||
(e) => e.id == id && e.status == status,
|
||||
orElse: () => null as UnifiedEquipment,
|
||||
);
|
||||
if (equipment != null) {
|
||||
selected.add(equipment);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return selected;
|
||||
}
|
||||
|
||||
// 선택된 장비들의 요약 정보를 Map 형태로 반환 (출고/대여/폐기 폼에서 사용)
|
||||
List<Map<String, dynamic>> getSelectedEquipmentsSummary() {
|
||||
List<Map<String, dynamic>> summaryList = [];
|
||||
List<UnifiedEquipment> selectedEquipmentsInStock =
|
||||
getSelectedEquipmentsByStatus(EquipmentStatus.in_);
|
||||
|
||||
for (final equipment in selectedEquipmentsInStock) {
|
||||
summaryList.add({
|
||||
'equipment': equipment.equipment,
|
||||
'equipmentInId': equipment.id,
|
||||
'status': equipment.status,
|
||||
});
|
||||
}
|
||||
|
||||
return summaryList;
|
||||
}
|
||||
|
||||
// 출고 정보(회사, 담당자, 라이센스 등) 반환
|
||||
String getOutEquipmentInfo(int equipmentId, String infoType) {
|
||||
final equipmentOut = dataService.getEquipmentOutById(equipmentId);
|
||||
if (equipmentOut != null) {
|
||||
switch (infoType) {
|
||||
case 'company':
|
||||
final company = equipmentOut.company ?? '-';
|
||||
if (company != '-') {
|
||||
final companyObj = dataService.getAllCompanies().firstWhere(
|
||||
(c) => c.name == company,
|
||||
orElse:
|
||||
() => Company(
|
||||
name: company,
|
||||
address: Address(),
|
||||
companyTypes: [CompanyType.customer], // 기본값 고객사
|
||||
),
|
||||
);
|
||||
// 여러 유형 중 첫 번째만 표시 (대표 유형)
|
||||
final typeText =
|
||||
companyObj.companyTypes.isNotEmpty
|
||||
? companyTypeToString(companyObj.companyTypes.first)
|
||||
: '-';
|
||||
return '$company (${typeText})';
|
||||
}
|
||||
return company;
|
||||
case 'manager':
|
||||
return equipmentOut.manager ?? '-';
|
||||
case 'license':
|
||||
return equipmentOut.license ?? '-';
|
||||
default:
|
||||
return '-';
|
||||
}
|
||||
}
|
||||
return '-';
|
||||
}
|
||||
|
||||
// 장비 삭제
|
||||
Future<bool> deleteEquipment(UnifiedEquipment equipment) async {
|
||||
try {
|
||||
if (_useApi) {
|
||||
// API를 통한 삭제
|
||||
if (equipment.equipment.id != null) {
|
||||
await _equipmentService.deleteEquipment(equipment.equipment.id!);
|
||||
} else {
|
||||
throw Exception('Equipment ID is null');
|
||||
}
|
||||
} else {
|
||||
// Mock 데이터 삭제
|
||||
if (equipment.status == EquipmentStatus.in_) {
|
||||
dataService.deleteEquipmentIn(equipment.id!);
|
||||
} else if (equipment.status == EquipmentStatus.out) {
|
||||
dataService.deleteEquipmentOut(equipment.id!);
|
||||
} else if (equipment.status == EquipmentStatus.rent) {
|
||||
// TODO: 대여 상태 삭제 구현
|
||||
throw UnimplementedError('Rent status deletion not implemented');
|
||||
}
|
||||
}
|
||||
|
||||
// 로컬 리스트에서도 제거
|
||||
equipments.removeWhere((e) => e.id == equipment.id && e.status == equipment.status);
|
||||
notifyListeners();
|
||||
|
||||
return true;
|
||||
} on Failure catch (e) {
|
||||
_error = e.message;
|
||||
notifyListeners();
|
||||
return false;
|
||||
} catch (e) {
|
||||
_error = 'Failed to delete equipment: $e';
|
||||
notifyListeners();
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// API 사용 여부 토글 (테스트용)
|
||||
void toggleApiUsage() {
|
||||
_useApi = !_useApi;
|
||||
loadData(isRefresh: true);
|
||||
}
|
||||
|
||||
// 에러 처리
|
||||
void clearError() {
|
||||
_error = null;
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
super.dispose();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -5,7 +5,6 @@ import 'package:superport/models/equipment_unified_model.dart';
|
||||
import 'package:superport/models/company_model.dart';
|
||||
import 'package:superport/models/company_branch_info.dart';
|
||||
import 'package:superport/models/address_model.dart';
|
||||
import 'package:superport/services/mock_data_service.dart';
|
||||
import 'package:superport/services/equipment_service.dart';
|
||||
import 'package:superport/services/company_service.dart';
|
||||
import 'package:superport/utils/constants.dart';
|
||||
@@ -14,7 +13,8 @@ import 'package:superport/utils/constants.dart';
|
||||
///
|
||||
/// 폼의 전체 상태, 유효성, 저장, 데이터 로딩 등 비즈니스 로직을 담당한다.
|
||||
class EquipmentOutFormController extends ChangeNotifier {
|
||||
final MockDataService dataService;
|
||||
final EquipmentService _equipmentService = GetIt.instance<EquipmentService>();
|
||||
final CompanyService _companyService = GetIt.instance<CompanyService>();
|
||||
int? equipmentOutId;
|
||||
|
||||
// 편집 모드 여부
|
||||
@@ -62,7 +62,6 @@ class EquipmentOutFormController extends ChangeNotifier {
|
||||
final TextEditingController remarkController = TextEditingController();
|
||||
|
||||
EquipmentOutFormController({
|
||||
required this.dataService,
|
||||
this.equipmentOutId,
|
||||
}) {
|
||||
isEditMode = equipmentOutId != null;
|
||||
@@ -77,22 +76,32 @@ class EquipmentOutFormController extends ChangeNotifier {
|
||||
}
|
||||
|
||||
// 드롭다운 데이터 로드
|
||||
void loadDropdownData() {
|
||||
// 회사 목록 로드 (출고처 가능한 회사만)
|
||||
companies = dataService.getAllCompanies()
|
||||
.where((c) => c.companyTypes.contains(CompanyType.customer))
|
||||
.map((c) => CompanyBranchInfo(
|
||||
id: c.id,
|
||||
name: c.name,
|
||||
originalName: c.name,
|
||||
isMainCompany: true,
|
||||
companyId: c.id,
|
||||
Future<void> loadDropdownData() async {
|
||||
try {
|
||||
// API를 통해 회사 목록 로드
|
||||
final allCompanies = await _companyService.getCompanies();
|
||||
companies = allCompanies
|
||||
.where((c) => c.companyTypes.contains(CompanyType.customer))
|
||||
.map((c) => CompanyBranchInfo(
|
||||
id: c.id,
|
||||
name: c.name,
|
||||
originalName: c.name,
|
||||
isMainCompany: true,
|
||||
companyId: c.id,
|
||||
branchId: null,
|
||||
))
|
||||
.toList();
|
||||
|
||||
// 라이선스 목록 로드
|
||||
licenses = dataService.getAllLicenses().map((l) => l.name).toList();
|
||||
|
||||
// TODO: 라이선스 목록도 API로 로드
|
||||
licenses = []; // 임시로 빈 목록
|
||||
|
||||
notifyListeners();
|
||||
} catch (e) {
|
||||
debugPrint('드롭다운 데이터 로드 실패: $e');
|
||||
companies = [];
|
||||
licenses = [];
|
||||
notifyListeners();
|
||||
}
|
||||
}
|
||||
|
||||
// 선택된 장비로 초기화
|
||||
@@ -109,23 +118,10 @@ class EquipmentOutFormController extends ChangeNotifier {
|
||||
return;
|
||||
}
|
||||
|
||||
// Mock 데이터에서 회사별 담당자 목록 가져오기
|
||||
final company = dataService.getAllCompanies().firstWhere(
|
||||
(c) => c.name == selectedCompanies[index],
|
||||
orElse: () => Company(
|
||||
name: '',
|
||||
companyTypes: [],
|
||||
),
|
||||
);
|
||||
|
||||
if (company.name.isNotEmpty && company.contactName != null && company.contactName!.isNotEmpty) {
|
||||
// 회사의 담당자 정보
|
||||
hasManagersPerCompany[index] = true;
|
||||
filteredManagersPerCompany[index] = [company.contactName!];
|
||||
} else {
|
||||
hasManagersPerCompany[index] = false;
|
||||
filteredManagersPerCompany[index] = ['없음'];
|
||||
}
|
||||
// TODO: API를 통해 회사별 담당자 목록 로드
|
||||
// 현재는 임시로 빈 목록 사용
|
||||
hasManagersPerCompany[index] = false;
|
||||
filteredManagersPerCompany[index] = [];
|
||||
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
@@ -5,7 +5,6 @@ import 'package:provider/provider.dart';
|
||||
// import 'package:superport/screens/common/custom_widgets.dart' hide FormFieldWrapper;
|
||||
import 'package:superport/screens/common/theme_tailwind.dart';
|
||||
import 'package:superport/screens/common/templates/form_layout_template.dart';
|
||||
import 'package:superport/services/mock_data_service.dart';
|
||||
import 'package:superport/utils/constants.dart';
|
||||
// import 'package:flutter_localizations/flutter_localizations.dart';
|
||||
// import 'package:superport/screens/equipment/widgets/autocomplete_text_field.dart';
|
||||
@@ -181,7 +180,6 @@ class _EquipmentInFormScreenState extends State<EquipmentInFormScreen> {
|
||||
void initState() {
|
||||
super.initState();
|
||||
_controller = EquipmentInFormController(
|
||||
dataService: MockDataService(),
|
||||
equipmentInId: widget.equipmentInId,
|
||||
);
|
||||
|
||||
|
||||
484
lib/screens/equipment/equipment_in_form_lookup_example.dart
Normal file
484
lib/screens/equipment/equipment_in_form_lookup_example.dart
Normal file
@@ -0,0 +1,484 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get_it/get_it.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import '../../services/lookup_service.dart';
|
||||
import '../../data/models/lookups/lookup_data.dart';
|
||||
import '../common/theme_shadcn.dart';
|
||||
import '../common/components/shadcn_components.dart';
|
||||
|
||||
/// LookupService를 활용한 장비 입고 폼 예시
|
||||
/// 전역 캐싱된 Lookup 데이터를 활용하여 드롭다운 구성
|
||||
class EquipmentInFormLookupExample extends StatefulWidget {
|
||||
const EquipmentInFormLookupExample({super.key});
|
||||
|
||||
@override
|
||||
State<EquipmentInFormLookupExample> createState() => _EquipmentInFormLookupExampleState();
|
||||
}
|
||||
|
||||
class _EquipmentInFormLookupExampleState extends State<EquipmentInFormLookupExample> {
|
||||
late final LookupService _lookupService;
|
||||
|
||||
// 선택된 값들
|
||||
String? _selectedEquipmentType;
|
||||
String? _selectedEquipmentStatus;
|
||||
String? _selectedManufacturer;
|
||||
String? _selectedLicenseType;
|
||||
|
||||
// 텍스트 컨트롤러
|
||||
final _serialNumberController = TextEditingController();
|
||||
final _quantityController = TextEditingController();
|
||||
final _descriptionController = TextEditingController();
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_lookupService = GetIt.instance<LookupService>();
|
||||
_loadLookupDataIfNeeded();
|
||||
}
|
||||
|
||||
/// 필요시 Lookup 데이터 로드 (캐시가 없을 경우)
|
||||
Future<void> _loadLookupDataIfNeeded() async {
|
||||
if (!_lookupService.hasData) {
|
||||
await _lookupService.loadAllLookups();
|
||||
if (mounted) {
|
||||
setState(() {}); // UI 업데이트
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_serialNumberController.dispose();
|
||||
_quantityController.dispose();
|
||||
_descriptionController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
backgroundColor: ShadcnTheme.background,
|
||||
appBar: AppBar(
|
||||
title: const Text('장비 입고 (Lookup 활용 예시)'),
|
||||
backgroundColor: ShadcnTheme.card,
|
||||
elevation: 0,
|
||||
),
|
||||
body: ChangeNotifierProvider.value(
|
||||
value: _lookupService,
|
||||
child: Consumer<LookupService>(
|
||||
builder: (context, lookupService, child) {
|
||||
if (lookupService.isLoading) {
|
||||
return const Center(
|
||||
child: CircularProgressIndicator(),
|
||||
);
|
||||
}
|
||||
|
||||
if (lookupService.error != null) {
|
||||
return Center(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Icon(Icons.error_outline,
|
||||
size: 64,
|
||||
color: ShadcnTheme.destructive,
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Text(
|
||||
'Lookup 데이터 로드 실패',
|
||||
style: ShadcnTheme.headingH4,
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
lookupService.error!,
|
||||
style: ShadcnTheme.bodyMuted,
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
ShadcnButton(
|
||||
text: '다시 시도',
|
||||
onPressed: () => lookupService.loadAllLookups(forceRefresh: true),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
return SingleChildScrollView(
|
||||
padding: const EdgeInsets.all(24),
|
||||
child: Center(
|
||||
child: Container(
|
||||
constraints: const BoxConstraints(maxWidth: 800),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
// 안내 메시지
|
||||
Container(
|
||||
padding: const EdgeInsets.all(16),
|
||||
decoration: BoxDecoration(
|
||||
color: ShadcnTheme.primary.withValues(alpha: 0.1),
|
||||
border: Border.all(
|
||||
color: ShadcnTheme.primary.withValues(alpha: 0.3),
|
||||
),
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
Icon(Icons.info_outline,
|
||||
color: ShadcnTheme.primary,
|
||||
size: 20,
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: Text(
|
||||
'이 화면은 /lookups API를 통해 캐싱된 전역 데이터를 활용합니다.\n'
|
||||
'드롭다운 데이터는 앱 시작 시 한 번만 로드되어 모든 화면에서 재사용됩니다.',
|
||||
style: ShadcnTheme.bodySmall,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
const SizedBox(height: 24),
|
||||
|
||||
// 폼 카드
|
||||
ShadcnCard(
|
||||
padding: const EdgeInsets.all(24),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text('장비 정보', style: ShadcnTheme.headingH4),
|
||||
const SizedBox(height: 24),
|
||||
|
||||
// 장비 타입 드롭다운
|
||||
_buildDropdownField(
|
||||
label: '장비 타입',
|
||||
value: _selectedEquipmentType,
|
||||
items: lookupService.equipmentTypes,
|
||||
onChanged: (value) {
|
||||
setState(() {
|
||||
_selectedEquipmentType = value;
|
||||
});
|
||||
},
|
||||
),
|
||||
|
||||
// 장비 상태 드롭다운
|
||||
_buildDropdownField(
|
||||
label: '장비 상태',
|
||||
value: _selectedEquipmentStatus,
|
||||
items: lookupService.equipmentStatuses,
|
||||
onChanged: (value) {
|
||||
setState(() {
|
||||
_selectedEquipmentStatus = value;
|
||||
});
|
||||
},
|
||||
),
|
||||
|
||||
// 제조사 드롭다운
|
||||
_buildDropdownField(
|
||||
label: '제조사',
|
||||
value: _selectedManufacturer,
|
||||
items: lookupService.manufacturers,
|
||||
onChanged: (value) {
|
||||
setState(() {
|
||||
_selectedManufacturer = value;
|
||||
});
|
||||
},
|
||||
),
|
||||
|
||||
// 시리얼 번호 입력
|
||||
_buildTextField(
|
||||
label: '시리얼 번호',
|
||||
controller: _serialNumberController,
|
||||
hintText: 'SN-2025-001',
|
||||
),
|
||||
|
||||
// 수량 입력
|
||||
_buildTextField(
|
||||
label: '수량',
|
||||
controller: _quantityController,
|
||||
hintText: '1',
|
||||
keyboardType: TextInputType.number,
|
||||
),
|
||||
|
||||
// 라이선스 타입 드롭다운 (옵션)
|
||||
_buildDropdownField(
|
||||
label: '라이선스 타입 (선택)',
|
||||
value: _selectedLicenseType,
|
||||
items: lookupService.licenseTypes,
|
||||
onChanged: (value) {
|
||||
setState(() {
|
||||
_selectedLicenseType = value;
|
||||
});
|
||||
},
|
||||
isOptional: true,
|
||||
),
|
||||
|
||||
// 비고 입력
|
||||
_buildTextField(
|
||||
label: '비고',
|
||||
controller: _descriptionController,
|
||||
hintText: '추가 정보를 입력하세요',
|
||||
maxLines: 3,
|
||||
),
|
||||
|
||||
const SizedBox(height: 32),
|
||||
|
||||
// 버튼 그룹
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.end,
|
||||
children: [
|
||||
ShadcnButton(
|
||||
text: '취소',
|
||||
variant: ShadcnButtonVariant.secondary,
|
||||
onPressed: () => Navigator.pop(context),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
ShadcnButton(
|
||||
text: '저장',
|
||||
onPressed: _handleSubmit,
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
const SizedBox(height: 24),
|
||||
|
||||
// 캐시 정보 표시
|
||||
_buildCacheInfoCard(lookupService),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// 드롭다운 필드 빌더
|
||||
Widget _buildDropdownField({
|
||||
required String label,
|
||||
required String? value,
|
||||
required List<LookupItem> items,
|
||||
required ValueChanged<String?> onChanged,
|
||||
bool isOptional = false,
|
||||
}) {
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Text(label, style: ShadcnTheme.bodyMedium),
|
||||
if (isOptional) ...[
|
||||
const SizedBox(width: 4),
|
||||
Text('(선택)', style: ShadcnTheme.bodyMuted),
|
||||
],
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Container(
|
||||
decoration: BoxDecoration(
|
||||
border: Border.all(color: ShadcnTheme.border),
|
||||
borderRadius: BorderRadius.circular(6),
|
||||
),
|
||||
child: DropdownButtonFormField<String>(
|
||||
value: value,
|
||||
decoration: InputDecoration(
|
||||
contentPadding: const EdgeInsets.symmetric(
|
||||
horizontal: 12,
|
||||
vertical: 8,
|
||||
),
|
||||
border: InputBorder.none,
|
||||
hintText: '선택하세요',
|
||||
hintStyle: ShadcnTheme.bodyMuted,
|
||||
),
|
||||
items: items.map((item) => DropdownMenuItem(
|
||||
value: item.code ?? '',
|
||||
child: Text(item.name ?? ''),
|
||||
)).toList(),
|
||||
onChanged: onChanged,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
/// 텍스트 필드 빌더
|
||||
Widget _buildTextField({
|
||||
required String label,
|
||||
required TextEditingController controller,
|
||||
String? hintText,
|
||||
TextInputType? keyboardType,
|
||||
int maxLines = 1,
|
||||
}) {
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(label, style: ShadcnTheme.bodyMedium),
|
||||
const SizedBox(height: 8),
|
||||
TextFormField(
|
||||
controller: controller,
|
||||
keyboardType: keyboardType,
|
||||
maxLines: maxLines,
|
||||
decoration: InputDecoration(
|
||||
hintText: hintText,
|
||||
hintStyle: ShadcnTheme.bodyMuted,
|
||||
border: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(6),
|
||||
borderSide: BorderSide(color: ShadcnTheme.border),
|
||||
),
|
||||
enabledBorder: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(6),
|
||||
borderSide: BorderSide(color: ShadcnTheme.border),
|
||||
),
|
||||
focusedBorder: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(6),
|
||||
borderSide: BorderSide(color: ShadcnTheme.primary, width: 2),
|
||||
),
|
||||
contentPadding: const EdgeInsets.symmetric(
|
||||
horizontal: 12,
|
||||
vertical: 8,
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
/// 캐시 정보 카드
|
||||
Widget _buildCacheInfoCard(LookupService lookupService) {
|
||||
return ShadcnCard(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Icon(Icons.storage, size: 20, color: ShadcnTheme.muted),
|
||||
const SizedBox(width: 8),
|
||||
Text('Lookup 캐시 정보', style: ShadcnTheme.bodyMedium),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
_buildCacheItem('장비 타입', lookupService.equipmentTypes.length),
|
||||
_buildCacheItem('장비 상태', lookupService.equipmentStatuses.length),
|
||||
_buildCacheItem('제조사', lookupService.manufacturers.length),
|
||||
_buildCacheItem('라이선스 타입', lookupService.licenseTypes.length),
|
||||
_buildCacheItem('사용자 역할', lookupService.userRoles.length),
|
||||
_buildCacheItem('회사 상태', lookupService.companyStatuses.length),
|
||||
const SizedBox(height: 12),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text(
|
||||
'캐시 상태: ${lookupService.isCacheValid ? "유효" : "만료"}',
|
||||
style: ShadcnTheme.bodySmall.copyWith(
|
||||
color: lookupService.isCacheValid
|
||||
? ShadcnTheme.success
|
||||
: ShadcnTheme.warning,
|
||||
),
|
||||
),
|
||||
TextButton(
|
||||
onPressed: () => lookupService.loadAllLookups(forceRefresh: true),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Icon(Icons.refresh, size: 16, color: ShadcnTheme.primary),
|
||||
const SizedBox(width: 4),
|
||||
Text('캐시 새로고침',
|
||||
style: TextStyle(color: ShadcnTheme.primary),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildCacheItem(String label, int count) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 2),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text(label, style: ShadcnTheme.bodySmall),
|
||||
Text('$count개',
|
||||
style: ShadcnTheme.bodySmall.copyWith(
|
||||
color: ShadcnTheme.muted,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// 폼 제출 처리
|
||||
void _handleSubmit() {
|
||||
// 유효성 검증
|
||||
if (_selectedEquipmentType == null) {
|
||||
_showSnackBar('장비 타입을 선택하세요', isError: true);
|
||||
return;
|
||||
}
|
||||
|
||||
if (_selectedEquipmentStatus == null) {
|
||||
_showSnackBar('장비 상태를 선택하세요', isError: true);
|
||||
return;
|
||||
}
|
||||
|
||||
if (_serialNumberController.text.isEmpty) {
|
||||
_showSnackBar('시리얼 번호를 입력하세요', isError: true);
|
||||
return;
|
||||
}
|
||||
|
||||
// 선택된 값 정보 표시
|
||||
final selectedType = _lookupService.findByCode(
|
||||
_lookupService.equipmentTypes,
|
||||
_selectedEquipmentType!,
|
||||
);
|
||||
|
||||
final selectedStatus = _lookupService.findByCode(
|
||||
_lookupService.equipmentStatuses,
|
||||
_selectedEquipmentStatus!,
|
||||
);
|
||||
|
||||
final message = '''
|
||||
장비 입고 정보:
|
||||
- 타입: ${selectedType?.name ?? _selectedEquipmentType}
|
||||
- 상태: ${selectedStatus?.name ?? _selectedEquipmentStatus}
|
||||
- 시리얼: ${_serialNumberController.text}
|
||||
- 수량: ${_quantityController.text.isEmpty ? "1" : _quantityController.text}
|
||||
''';
|
||||
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (context) => AlertDialog(
|
||||
title: const Text('입고 정보 확인'),
|
||||
content: Text(message),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.pop(context),
|
||||
child: const Text('확인'),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _showSnackBar(String message, {bool isError = false}) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(message),
|
||||
backgroundColor: isError ? ShadcnTheme.destructive : ShadcnTheme.primary,
|
||||
duration: const Duration(seconds: 2),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -9,7 +9,6 @@ import 'package:superport/screens/common/widgets/standard_data_table.dart' as st
|
||||
import 'package:superport/screens/common/widgets/standard_states.dart';
|
||||
import 'package:superport/screens/common/layouts/base_list_screen.dart';
|
||||
import 'package:superport/screens/equipment/controllers/equipment_list_controller.dart';
|
||||
import 'package:superport/services/mock_data_service.dart';
|
||||
import 'package:superport/models/equipment_unified_model.dart';
|
||||
import 'package:superport/utils/constants.dart';
|
||||
import 'package:superport/utils/equipment_display_helper.dart';
|
||||
@@ -42,7 +41,7 @@ class _EquipmentListRedesignState extends State<EquipmentListRedesign> {
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_controller = EquipmentListController(dataService: MockDataService());
|
||||
_controller = EquipmentListController();
|
||||
_setInitialFilter();
|
||||
|
||||
// API 호출을 위해 Future로 변경
|
||||
@@ -116,7 +115,7 @@ class _EquipmentListRedesignState extends State<EquipmentListRedesign> {
|
||||
}
|
||||
_currentPage = 1;
|
||||
});
|
||||
await _controller.changeStatusFilter(_controller.selectedStatusFilter);
|
||||
_controller.changeStatusFilter(_controller.selectedStatusFilter);
|
||||
}
|
||||
|
||||
/// 검색 실행
|
||||
@@ -125,13 +124,26 @@ class _EquipmentListRedesignState extends State<EquipmentListRedesign> {
|
||||
_appliedSearchKeyword = _searchController.text;
|
||||
_currentPage = 1;
|
||||
});
|
||||
await _controller.updateSearchKeyword(_searchController.text);
|
||||
_controller.updateSearchKeyword(_searchController.text);
|
||||
}
|
||||
|
||||
/// 장비 선택/해제
|
||||
void _onEquipmentSelected(int? id, String status, bool? isSelected) {
|
||||
if (id == null) return;
|
||||
|
||||
// UnifiedEquipment를 찾아서 선택/해제
|
||||
UnifiedEquipment? equipment;
|
||||
try {
|
||||
equipment = _controller.items.firstWhere(
|
||||
(e) => e.equipment.id == id && e.status == status,
|
||||
);
|
||||
} catch (e) {
|
||||
// 해당하는 장비를 찾지 못함
|
||||
return;
|
||||
}
|
||||
|
||||
setState(() {
|
||||
_controller.selectEquipment(id, status, isSelected);
|
||||
_controller.selectEquipment(equipment!);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -140,7 +152,7 @@ class _EquipmentListRedesignState extends State<EquipmentListRedesign> {
|
||||
setState(() {
|
||||
final equipments = _getFilteredEquipments();
|
||||
for (final equipment in equipments) {
|
||||
_controller.selectEquipment(equipment.id, equipment.status, value);
|
||||
_controller.selectEquipment(equipment);
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -234,7 +246,7 @@ class _EquipmentListRedesignState extends State<EquipmentListRedesign> {
|
||||
return;
|
||||
}
|
||||
|
||||
final selectedEquipmentsSummary = _controller.getSelectedEquipmentsSummary();
|
||||
final selectedEquipments = _controller.getSelectedEquipments();
|
||||
|
||||
showDialog(
|
||||
context: context,
|
||||
@@ -245,12 +257,12 @@ class _EquipmentListRedesignState extends State<EquipmentListRedesign> {
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text('선택한 ${selectedEquipmentsSummary.length}개 장비를 폐기하시겠습니까?'),
|
||||
Text('선택한 ${selectedEquipments.length}개 장비를 폐기하시겠습니까?'),
|
||||
const SizedBox(height: 16),
|
||||
const Text('폐기할 장비 목록:', style: TextStyle(fontWeight: FontWeight.bold)),
|
||||
const SizedBox(height: 8),
|
||||
...selectedEquipmentsSummary.map((equipmentData) {
|
||||
final equipment = equipmentData['equipment'] as Equipment;
|
||||
...selectedEquipments.map((unifiedEquipment) {
|
||||
final equipment = unifiedEquipment.equipment;
|
||||
return Padding(
|
||||
padding: const EdgeInsets.only(bottom: 8.0),
|
||||
child: Text(
|
||||
@@ -328,26 +340,15 @@ class _EquipmentListRedesignState extends State<EquipmentListRedesign> {
|
||||
);
|
||||
|
||||
// Controller를 통한 삭제 처리
|
||||
final success = await _controller.deleteEquipment(equipment);
|
||||
await _controller.deleteEquipment(equipment.equipment.id!, equipment.status);
|
||||
|
||||
// 로딩 다이얼로그 닫기
|
||||
if (mounted) Navigator.pop(context);
|
||||
|
||||
if (success) {
|
||||
if (mounted) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(content: Text('장비가 삭제되었습니다.')),
|
||||
);
|
||||
}
|
||||
} else {
|
||||
if (mounted) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(_controller.error ?? '삭제 중 오류가 발생했습니다.'),
|
||||
backgroundColor: Colors.red,
|
||||
),
|
||||
);
|
||||
}
|
||||
if (mounted) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(content: Text('장비가 삭제되었습니다.')),
|
||||
);
|
||||
}
|
||||
},
|
||||
child: const Text('삭제', style: TextStyle(color: Colors.red)),
|
||||
|
||||
@@ -7,7 +7,6 @@ import 'package:superport/models/company_branch_info.dart';
|
||||
import 'package:superport/models/address_model.dart';
|
||||
import 'package:superport/screens/common/custom_widgets.dart';
|
||||
import 'package:superport/screens/common/theme_tailwind.dart';
|
||||
import 'package:superport/services/mock_data_service.dart';
|
||||
import 'package:superport/screens/equipment/controllers/equipment_out_form_controller.dart';
|
||||
import 'package:superport/screens/equipment/widgets/equipment_summary_card.dart';
|
||||
import 'package:superport/screens/equipment/widgets/equipment_summary_row.dart';
|
||||
@@ -37,7 +36,7 @@ class _EquipmentOutFormScreenState extends State<EquipmentOutFormScreen> {
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_controller = EquipmentOutFormController(dataService: MockDataService());
|
||||
_controller = EquipmentOutFormController();
|
||||
_controller.isEditMode = widget.equipmentOutId != null;
|
||||
_controller.equipmentOutId = widget.equipmentOutId;
|
||||
_controller.selectedEquipment = widget.selectedEquipment;
|
||||
@@ -550,9 +549,9 @@ class _EquipmentOutFormScreenState extends State<EquipmentOutFormScreen> {
|
||||
Branch? branch;
|
||||
|
||||
if (companyInfo.companyId != null) {
|
||||
company = controller.dataService.getCompanyById(
|
||||
companyInfo.companyId!,
|
||||
);
|
||||
// TODO: 실제 CompanyService를 통해 회사 정보 가져오기
|
||||
// company = await _companyService.getCompanyById(companyInfo.companyId!);
|
||||
company = null; // 임시로 null 처리
|
||||
if (!companyInfo.isMainCompany &&
|
||||
companyInfo.branchId != null &&
|
||||
company != null) {
|
||||
|
||||
Reference in New Issue
Block a user