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,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() {

View File

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

View File

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

View File

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