사용하지 않는 파일 정리 전 백업 (Phase 10 완료 후 상태)

This commit is contained in:
JiWoong Sul
2025-08-29 15:11:59 +09:00
parent a740ff10c8
commit d916b281a7
333 changed files with 53617 additions and 22574 deletions

View File

@@ -0,0 +1,414 @@
import 'package:flutter/material.dart';
import 'package:injectable/injectable.dart';
import '../../../data/models/equipment/equipment_dto.dart';
import '../../../data/models/company/company_dto.dart';
import '../../../data/models/model_dto.dart';
import '../../../domain/usecases/equipment/create_equipment_usecase.dart';
import '../../../domain/usecases/equipment/update_equipment_usecase.dart';
import '../../../domain/usecases/equipment/get_equipment_detail_usecase.dart';
import '../../../domain/usecases/company/get_companies_usecase.dart';
import '../../../domain/usecases/model_usecase.dart';
import '../../../core/errors/failures.dart';
/// 장비 폼 컨트롤러 (생성/수정)
/// Phase 8 - Clean Architecture 패턴, 복합 FK 지원
@injectable
class EquipmentFormController extends ChangeNotifier {
final CreateEquipmentUseCase _createEquipmentUseCase;
final UpdateEquipmentUseCase _updateEquipmentUseCase;
final GetEquipmentDetailUseCase _getEquipmentDetailUseCase;
final GetCompaniesUseCase _getCompaniesUseCase;
final ModelUseCase _modelUseCase;
EquipmentFormController(
this._createEquipmentUseCase,
this._updateEquipmentUseCase,
this._getEquipmentDetailUseCase,
this._getCompaniesUseCase,
this._modelUseCase,
);
// 상태 관리
bool _isLoading = false;
bool _isLoadingCompanies = false;
bool _isLoadingModels = false;
bool _isSaving = false;
String? _error;
// 폼 데이터
EquipmentDto? _currentEquipment;
int? _equipmentId; // 수정 모드일 때 사용
// 드롭다운 데이터
List<CompanyDto> _companies = [];
List<ModelDto> _models = [];
List<ModelDto> _filteredModels = [];
// 선택된 값
int? _selectedCompanyId;
int? _selectedModelId;
// 폼 컨트롤러들
final TextEditingController serialNumberController = TextEditingController();
final TextEditingController barcodeController = TextEditingController();
final TextEditingController purchasePriceController = TextEditingController();
final TextEditingController warrantyNumberController = TextEditingController();
final TextEditingController remarkController = TextEditingController();
// 날짜 필드들
DateTime? _purchasedAt;
DateTime _warrantyStartedAt = DateTime.now();
DateTime _warrantyEndedAt = DateTime.now().add(const Duration(days: 365));
// Getters
bool get isLoading => _isLoading;
bool get isLoadingCompanies => _isLoadingCompanies;
bool get isLoadingModels => _isLoadingModels;
bool get isSaving => _isSaving;
String? get error => _error;
EquipmentDto? get currentEquipment => _currentEquipment;
bool get isEditMode => _equipmentId != null;
List<CompanyDto> get companies => _companies;
List<ModelDto> get filteredModels => _filteredModels;
int? get selectedCompanyId => _selectedCompanyId;
int? get selectedModelId => _selectedModelId;
DateTime? get purchasedAt => _purchasedAt;
DateTime get warrantyStartedAt => _warrantyStartedAt;
DateTime get warrantyEndedAt => _warrantyEndedAt;
/// 초기화 (생성 모드)
Future<void> initializeForCreate() async {
_equipmentId = null;
_currentEquipment = null;
_clearForm();
await _loadInitialData();
}
/// 초기화 (수정 모드)
Future<void> initializeForEdit(int equipmentId) async {
_equipmentId = equipmentId;
_isLoading = true;
_error = null;
notifyListeners();
try {
await _loadInitialData();
await _loadEquipmentDetail(equipmentId);
} catch (e) {
_error = '장비 정보를 불러오는데 실패했습니다: $e';
} finally {
_isLoading = false;
notifyListeners();
}
}
/// 초기 데이터 로드 (회사, 모델)
Future<void> _loadInitialData() async {
await Future.wait([
_loadCompanies(),
_loadModels(),
]);
}
/// 회사 목록 로드
Future<void> _loadCompanies() async {
_isLoadingCompanies = true;
notifyListeners();
try {
final params = GetCompaniesParams(page: 1, perPage: 1000); // 모든 회사 가져오기
final result = await _getCompaniesUseCase(params);
result.fold(
(failure) {
_error = _getErrorMessage(failure);
},
(paginatedResponse) {
_companies = paginatedResponse.items
.cast<CompanyDto>() // 타입 캐스팅 추가
.where((company) => company.isActive)
.toList()
..sort((a, b) => a.name.compareTo(b.name));
},
);
} catch (e) {
_error = '회사 목록을 불러오는데 실패했습니다: $e';
} finally {
_isLoadingCompanies = false;
notifyListeners();
}
}
/// 모델 목록 로드
Future<void> _loadModels() async {
_isLoadingModels = true;
notifyListeners();
try {
_models = await _modelUseCase.getModels();
_filteredModels = _models;
} catch (e) {
_error = '모델 목록을 불러오는데 실패했습니다: $e';
} finally {
_isLoadingModels = false;
notifyListeners();
}
}
/// 장비 상세 정보 로드 (수정 모드)
Future<void> _loadEquipmentDetail(int equipmentId) async {
final result = await _getEquipmentDetailUseCase(equipmentId);
result.fold(
(failure) {
_error = _getErrorMessage(failure);
},
(equipment) {
_currentEquipment = equipment;
_populateForm(equipment);
},
);
}
/// 폼 필드에 데이터 채우기 (수정 모드)
void _populateForm(EquipmentDto equipment) {
_selectedCompanyId = equipment.companiesId;
_selectedModelId = equipment.modelsId;
serialNumberController.text = equipment.serialNumber;
barcodeController.text = equipment.barcode ?? '';
purchasePriceController.text = equipment.purchasePrice.toString();
warrantyNumberController.text = equipment.warrantyNumber;
remarkController.text = equipment.remark ?? '';
_purchasedAt = equipment.purchasedAt;
_warrantyStartedAt = equipment.warrantyStartedAt;
_warrantyEndedAt = equipment.warrantyEndedAt;
// 선택된 회사에 따라 모델 필터링
_filterModelsByCompany(_selectedCompanyId);
notifyListeners();
}
/// 회사 선택
void selectCompany(int? companyId) {
_selectedCompanyId = companyId;
_selectedModelId = null; // 모델 선택 초기화
_filterModelsByCompany(companyId);
notifyListeners();
}
/// 모델 선택
void selectModel(int? modelId) {
_selectedModelId = modelId;
notifyListeners();
}
/// 회사별 모델 필터링
void _filterModelsByCompany(int? companyId) {
if (companyId == null) {
_filteredModels = _models;
} else {
// 실제로는 vendor로 필터링해야 하지만,
// 현재 구조에서는 모든 모델을 보여주고 사용자가 선택하도록 함
_filteredModels = _models;
}
notifyListeners();
}
/// 구매일 선택
void setPurchasedAt(DateTime? date) {
_purchasedAt = date;
notifyListeners();
}
/// 워런티 시작일 선택
void setWarrantyStartedAt(DateTime date) {
_warrantyStartedAt = date;
// 시작일이 종료일보다 늦으면 종료일을 1년 후로 설정
if (_warrantyStartedAt.isAfter(_warrantyEndedAt)) {
_warrantyEndedAt = _warrantyStartedAt.add(const Duration(days: 365));
}
notifyListeners();
}
/// 워런티 종료일 선택
void setWarrantyEndedAt(DateTime date) {
_warrantyEndedAt = date;
notifyListeners();
}
/// 폼 유효성 검증
String? validateForm() {
if (_selectedCompanyId == null) {
return '회사를 선택해주세요.';
}
if (_selectedModelId == null) {
return '모델을 선택해주세요.';
}
if (serialNumberController.text.trim().isEmpty) {
return '시리얼 번호를 입력해주세요.';
}
if (warrantyNumberController.text.trim().isEmpty) {
return '워런티 번호를 입력해주세요.';
}
if (_warrantyStartedAt.isAfter(_warrantyEndedAt)) {
return '워런티 시작일이 종료일보다 늦을 수 없습니다.';
}
return null;
}
/// 장비 저장 (생성 또는 수정)
Future<bool> saveEquipment() async {
final validationError = validateForm();
if (validationError != null) {
_error = validationError;
notifyListeners();
return false;
}
_isSaving = true;
_error = null;
notifyListeners();
try {
if (isEditMode) {
return await _updateEquipment();
} else {
return await _createEquipment();
}
} catch (e) {
_error = '저장 중 오류가 발생했습니다: $e';
return false;
} finally {
_isSaving = false;
notifyListeners();
}
}
/// 장비 생성
Future<bool> _createEquipment() async {
final request = EquipmentRequestDto(
companiesId: _selectedCompanyId!,
modelsId: _selectedModelId!,
serialNumber: serialNumberController.text.trim(),
barcode: barcodeController.text.trim().isNotEmpty
? barcodeController.text.trim()
: null,
purchasedAt: _purchasedAt,
purchasePrice: int.tryParse(purchasePriceController.text) ?? 0,
warrantyNumber: warrantyNumberController.text.trim(),
warrantyStartedAt: _warrantyStartedAt,
warrantyEndedAt: _warrantyEndedAt,
remark: remarkController.text.trim().isNotEmpty
? remarkController.text.trim()
: null,
);
final result = await _createEquipmentUseCase(request);
return result.fold(
(failure) {
_error = _getErrorMessage(failure);
return false;
},
(equipment) {
_currentEquipment = equipment;
return true;
},
);
}
/// 장비 수정
Future<bool> _updateEquipment() async {
final request = EquipmentUpdateRequestDto(
companiesId: _selectedCompanyId!,
modelsId: _selectedModelId!,
serialNumber: serialNumberController.text.trim(),
barcode: barcodeController.text.trim().isNotEmpty
? barcodeController.text.trim()
: null,
purchasedAt: _purchasedAt,
purchasePrice: int.tryParse(purchasePriceController.text) ?? 0,
warrantyNumber: warrantyNumberController.text.trim(),
warrantyStartedAt: _warrantyStartedAt,
warrantyEndedAt: _warrantyEndedAt,
remark: remarkController.text.trim().isNotEmpty
? remarkController.text.trim()
: null,
);
final result = await _updateEquipmentUseCase(UpdateEquipmentParams(
id: _equipmentId!,
request: request,
));
return result.fold(
(failure) {
_error = _getErrorMessage(failure);
return false;
},
(equipment) {
_currentEquipment = equipment;
return true;
},
);
}
/// 폼 초기화
void _clearForm() {
_selectedCompanyId = null;
_selectedModelId = null;
serialNumberController.clear();
barcodeController.clear();
purchasePriceController.text = '0';
warrantyNumberController.clear();
remarkController.clear();
_purchasedAt = null;
_warrantyStartedAt = DateTime.now();
_warrantyEndedAt = DateTime.now().add(const Duration(days: 365));
_error = null;
}
/// 에러 메시지 변환
String _getErrorMessage(Failure failure) {
switch (failure.runtimeType) {
case ServerFailure:
return (failure as ServerFailure).message;
case NetworkFailure:
return '네트워크 연결을 확인해주세요.';
case ValidationFailure:
return (failure as ValidationFailure).message;
default:
return '알 수 없는 오류가 발생했습니다.';
}
}
/// 에러 초기화
void clearError() {
_error = null;
notifyListeners();
}
@override
void dispose() {
serialNumberController.dispose();
barcodeController.dispose();
purchasePriceController.dispose();
warrantyNumberController.dispose();
remarkController.dispose();
super.dispose();
}
}

View File

@@ -0,0 +1,358 @@
import 'package:flutter/material.dart';
import 'package:get_it/get_it.dart';
import '../../../data/models/equipment_history_dto.dart';
import '../../../domain/usecases/equipment_history_usecase.dart';
import '../../../utils/constants.dart';
class EquipmentHistoryController extends ChangeNotifier {
final EquipmentHistoryUseCase _useCase;
EquipmentHistoryController({
EquipmentHistoryUseCase? useCase,
}) : _useCase = useCase ?? GetIt.instance<EquipmentHistoryUseCase>();
// 상태 변수 (백엔드 스키마 기반)
List<EquipmentHistoryDto> _historyList = [];
bool _isLoading = false;
String? _error;
// 페이지네이션
int _currentPage = 1;
int _pageSize = PaginationConstants.defaultPageSize;
int _totalCount = 0;
// 필터 (백엔드 실제 필드만)
String _searchQuery = '';
String? _transactionType;
int? _warehouseId;
int? _equipmentId;
DateTime? _startDate;
DateTime? _endDate;
// Getters (백엔드 실제 데이터만)
List<EquipmentHistoryDto> get historyList => _historyList;
bool get isLoading => _isLoading;
String? get error => _error;
int get currentPage => _currentPage;
int get totalPages => (_totalCount / _pageSize).ceil();
int get totalCount => _totalCount;
String get searchQuery => _searchQuery;
// 간단한 통계 (백엔드 실제 데이터 기반)
int get totalTransactions => _historyList.length;
int get totalInQuantity => _historyList
.where((item) => item.transactionType == 'I')
.fold(0, (sum, item) => sum + item.quantity);
int get totalOutQuantity => _historyList
.where((item) => item.transactionType == 'O')
.fold(0, (sum, item) => sum + item.quantity);
// 재고 이력 조회
Future<void> loadHistory({bool refresh = false}) async {
if (refresh) {
_currentPage = 1;
_historyList.clear();
}
_isLoading = true;
_error = null;
notifyListeners();
try {
final result = await _useCase.getEquipmentHistories(
page: _currentPage,
pageSize: _pageSize,
transactionType: _transactionType,
warehousesId: _warehouseId,
equipmentsId: _equipmentId,
startDate: _startDate?.toIso8601String(),
endDate: _endDate?.toIso8601String(),
);
// result는 EquipmentHistoryListResponse 타입으로 Either가 아님
if (refresh) {
_historyList = result.items;
} else {
_historyList.addAll(result.items);
}
_totalCount = result.totalCount;
} catch (e) {
_error = e.toString();
} finally {
_isLoading = false;
notifyListeners();
}
}
// 백엔드 스키마 기반 단순 재고 조회 (EquipmentHistoryDto 기반)
// 특정 장비의 현재 재고량 계산 (입고량 - 출고량)
int calculateEquipmentStock(int equipmentId) {
final equipmentHistories = _historyList.where((h) => h.equipmentsId == equipmentId);
int totalIn = equipmentHistories
.where((h) => h.transactionType == 'I')
.fold(0, (sum, h) => sum + h.quantity);
int totalOut = equipmentHistories
.where((h) => h.transactionType == 'O')
.fold(0, (sum, h) => sum + h.quantity);
return totalIn - totalOut;
}
// 백엔드 스키마 기반 단순 필터링
void setFilters({
String? transactionType,
int? warehouseId,
int? equipmentId,
DateTime? startDate,
DateTime? endDate,
String? searchQuery,
}) {
_transactionType = transactionType;
_warehouseId = warehouseId;
_equipmentId = equipmentId;
_startDate = startDate;
_endDate = endDate;
if (searchQuery != null) _searchQuery = searchQuery;
_currentPage = 1;
loadHistory(refresh: true);
}
void clearFilters() {
_transactionType = null;
_warehouseId = null;
_equipmentId = null;
_startDate = null;
_endDate = null;
_searchQuery = '';
_currentPage = 1;
loadHistory(refresh: true);
}
// 백엔드 스키마 기반 단순 통계
Map<String, int> getStockSummary() {
int totalIn = _historyList
.where((h) => h.transactionType == 'I')
.fold(0, (sum, h) => sum + h.quantity);
int totalOut = _historyList
.where((h) => h.transactionType == 'O')
.fold(0, (sum, h) => sum + h.quantity);
return {
'totalStock': totalIn - totalOut,
'totalIn': totalIn,
'totalOut': totalOut,
};
}
// 입고 처리 (백엔드 스키마 기반)
Future<bool> createStockIn({
required int equipmentsId,
required int warehousesId,
required int quantity,
DateTime? transactedAt,
String? remark,
}) async {
_isLoading = true;
_error = null;
notifyListeners();
try {
final request = EquipmentHistoryRequestDto(
equipmentsId: equipmentsId,
warehousesId: warehousesId,
transactionType: 'I', // 입고
quantity: quantity,
transactedAt: transactedAt ?? DateTime.now(),
remark: remark,
);
await _useCase.createEquipmentHistory(request);
await loadHistory(refresh: true);
return true;
} catch (e) {
_error = e.toString();
return false;
} finally {
_isLoading = false;
notifyListeners();
}
}
// 출고 처리 (백엔드 스키마 기반)
Future<bool> createStockOut({
required int equipmentsId,
required int warehousesId,
required int quantity,
DateTime? transactedAt,
String? remark,
}) async {
_isLoading = true;
_error = null;
notifyListeners();
try {
final request = EquipmentHistoryRequestDto(
equipmentsId: equipmentsId,
warehousesId: warehousesId,
transactionType: 'O', // 출고
quantity: quantity,
transactedAt: transactedAt ?? DateTime.now(),
remark: remark,
);
await _useCase.createEquipmentHistory(request);
await loadHistory(refresh: true);
return true;
} catch (e) {
_error = e.toString();
return false;
} finally {
_isLoading = false;
notifyListeners();
}
}
// createHistory 메서드 (별칭)
Future<bool> createHistory(EquipmentHistoryRequestDto request) async {
_isLoading = true;
_error = null;
notifyListeners();
try {
await _useCase.createEquipmentHistory(request);
await loadHistory(refresh: true);
return true;
} catch (e) {
_error = e.toString();
return false;
} finally {
_isLoading = false;
notifyListeners();
}
}
// searchEquipmentHistories 메서드
Future<List<EquipmentHistoryDto>> searchEquipmentHistories({
String? query,
String? transactionType,
int? equipmentId,
int? warehouseId,
}) async {
try {
final result = await _useCase.getEquipmentHistories(
page: 1,
pageSize: 100,
transactionType: transactionType,
equipmentsId: equipmentId,
warehousesId: warehouseId,
);
List<EquipmentHistoryDto> histories = result.items;
// 간단한 텍스트 검색 (remark 필드 기반)
if (query != null && query.isNotEmpty) {
histories = histories.where((h) =>
h.remark?.toLowerCase().contains(query.toLowerCase()) ?? false
).toList();
}
return histories;
} catch (e) {
_error = e.toString();
notifyListeners();
return [];
}
}
// setSearchQuery 메서드
void setSearchQuery(String query) {
_searchQuery = query;
setFilters(searchQuery: query);
}
// loadInventoryStatus 메서드 (단순 재고 상태 로드)
Future<void> loadInventoryStatus() async {
await loadHistory(refresh: true);
}
// loadWarehouseStock 메서드 (창고별 재고 로드)
Future<void> loadWarehouseStock({int? warehouseId}) async {
setFilters(warehouseId: warehouseId);
}
// getAvailableStock 메서드
Future<int> getAvailableStock(int equipmentId, {int? warehouseId}) async {
try {
final result = await _useCase.getEquipmentHistories(
page: 1,
pageSize: 1000,
equipmentsId: equipmentId,
warehousesId: warehouseId,
);
int totalIn = result.items
.where((h) => h.transactionType == 'I')
.fold(0, (sum, h) => sum + h.quantity);
int totalOut = result.items
.where((h) => h.transactionType == 'O')
.fold(0, (sum, h) => sum + h.quantity);
return totalIn - totalOut;
} catch (e) {
return 0;
}
}
// getAvailableEquipments 메서드 (사용 가능한 장비 목록)
Future<List<int>> getAvailableEquipments({int? warehouseId}) async {
try {
final result = await _useCase.getEquipmentHistories(
page: 1,
pageSize: 1000,
warehousesId: warehouseId,
);
// 재고가 있는 장비 ID들만 반환
Map<int, int> stockMap = {};
for (var history in result.items) {
stockMap[history.equipmentsId] = (stockMap[history.equipmentsId] ?? 0) +
(history.transactionType == 'I' ? history.quantity : -history.quantity);
}
return stockMap.entries
.where((entry) => entry.value > 0)
.map((entry) => entry.key)
.toList();
} catch (e) {
return [];
}
}
// 페이지네이션 메서드들
Future<void> previousPage() async {
if (_currentPage > 1) {
_currentPage--;
await loadHistory();
}
}
Future<void> nextPage() async {
if (_currentPage < totalPages) {
_currentPage++;
await loadHistory();
}
}
// 에러 클리어
void clearError() {
_error = null;
notifyListeners();
}
@override
void dispose() {
_historyList.clear();
super.dispose();
}
}

View File

@@ -2,21 +2,20 @@ 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/warehouse_service.dart';
import 'package:superport/services/company_service.dart';
import 'package:superport/utils/constants.dart';
import 'package:superport/core/errors/failures.dart';
import 'package:superport/core/utils/debug_logger.dart';
import 'package:superport/core/utils/equipment_status_converter.dart';
import 'package:superport/core/services/lookups_service.dart';
import 'package:superport/screens/equipment/controllers/equipment_history_controller.dart';
import 'package:superport/data/models/equipment/equipment_dto.dart';
import 'package:superport/data/models/equipment_history_dto.dart';
/// 장비 입고 폼 컨트롤러
///
/// 폼의 전체 상태, 유효성, 저장, 데이터 로딩 등 비즈니스 로직을 담당한다.
class EquipmentInFormController extends ChangeNotifier {
final EquipmentService _equipmentService = GetIt.instance<EquipmentService>();
final WarehouseService _warehouseService = GetIt.instance<WarehouseService>();
final CompanyService _companyService = GetIt.instance<CompanyService>();
// final WarehouseService _warehouseService = GetIt.instance<WarehouseService>(); // 사용되지 않음 - 제거
// final CompanyService _companyService = GetIt.instance<CompanyService>(); // 사용되지 않음 - 제거
final LookupsService _lookupsService = GetIt.instance<LookupsService>();
final int? equipmentInId; // 실제로는 장비 ID (입고 ID가 아님)
int? actualEquipmentId; // API 호출용 실제 장비 ID
@@ -37,15 +36,15 @@ class EquipmentInFormController extends ChangeNotifier {
/// canSave 상태 업데이트 (UI 렌더링 문제 해결)
void _updateCanSave() {
final hasEquipmentNumber = _equipmentNumber.trim().isNotEmpty;
final hasManufacturer = _manufacturer.trim().isNotEmpty;
final hasEquipmentNumber = _serialNumber.trim().isNotEmpty;
final hasModelsId = _modelsId != null; // models_id 필수
final isNotSaving = !_isSaving;
final newCanSave = isNotSaving && hasEquipmentNumber && hasManufacturer;
final newCanSave = isNotSaving && hasEquipmentNumber && hasModelsId;
if (_canSave != newCanSave) {
_canSave = newCanSave;
print('🚀 [canSave 상태 변경] $_canSaveequipmentNumber: "$_equipmentNumber", manufacturer: "$_manufacturer"');
print('🚀 [canSave 상태 변경] $_canSaveserialNumber: "$_serialNumber", modelsId: $_modelsId');
notifyListeners(); // 명시적 UI 업데이트
}
}
@@ -54,24 +53,18 @@ class EquipmentInFormController extends ChangeNotifier {
final GlobalKey<FormState> formKey = GlobalKey<FormState>();
// 입력 상태 변수 (백엔드 API 구조에 맞게 수정)
String _equipmentNumber = ''; // 장비번호 (필수) - private으로 변경
String _manufacturer = ''; // 제조사 (필수) - private으로 변경
String _modelName = ''; // 모델명 - private으로 변경
String _serialNumber = ''; // 시리얼번호 - private으로 변경
String _category1 = ''; // 대분류 - private으로 변경
String _category2 = ''; // 중분류 - private으로 변경
String _category3 = ''; // 소분류 - private으로 변경
String _serialNumber = ''; // 장비번호 (필수) - private으로 변경
int? _modelsId; // 모델 ID (필수) - Vendor→Model cascade에서 선택
int? _vendorId; // 벤더 ID (UI용, API에는 전송 안함)
// Legacy 필드 (UI 호환성 유지용)
String _manufacturer = ''; // 제조사 (Legacy) - ModelDto에서 가져옴
String _name = ''; // 모델명 (Legacy) - ModelDto에서 가져옴
String _category1 = ''; // 대분류 (Legacy)
String _category2 = ''; // 중분류 (Legacy)
String _category3 = ''; // 소분류 (Legacy)
// Getters and Setters for reactive fields
String get equipmentNumber => _equipmentNumber;
set equipmentNumber(String value) {
if (_equipmentNumber != value) {
_equipmentNumber = value;
_updateCanSave(); // canSave 상태 업데이트
print('DEBUG [Controller] equipmentNumber updated: "$_equipmentNumber"');
}
}
String get serialNumber => _serialNumber;
set serialNumber(String value) {
if (_serialNumber != value) {
@@ -90,12 +83,12 @@ class EquipmentInFormController extends ChangeNotifier {
}
}
String get modelName => _modelName;
set modelName(String value) {
if (_modelName != value) {
_modelName = value;
String get name => _name;
set name(String value) {
if (_name != value) {
_name = value;
_updateCanSave(); // canSave 상태 업데이트
print('DEBUG [Controller] modelName updated: "$_modelName"');
print('DEBUG [Controller] name updated: "$_name"');
}
}
@@ -122,6 +115,37 @@ class EquipmentInFormController extends ChangeNotifier {
_updateCanSave(); // canSave 상태 업데이트
}
}
// 새로운 필드 getters/setters
int? get modelsId => _modelsId;
set modelsId(int? value) {
if (_modelsId != value) {
_modelsId = value;
_updateCanSave(); // canSave 상태 업데이트
print('DEBUG [Controller] modelsId updated: $_modelsId');
}
}
int? get vendorId => _vendorId;
set vendorId(int? value) {
if (_vendorId != value) {
_vendorId = value;
// vendor 변경 시 model 초기화
if (_modelsId != null) {
_modelsId = null;
_updateCanSave();
}
print('DEBUG [Controller] vendorId updated: $_vendorId');
}
}
// Vendor→Model 선택 콜백
void onVendorModelChanged(int? vendorId, int? modelId) {
_vendorId = vendorId;
_modelsId = modelId;
_updateCanSave();
notifyListeners();
}
DateTime? purchaseDate; // 구매일
double? purchasePrice; // 구매가격
@@ -139,6 +163,16 @@ class EquipmentInFormController extends ChangeNotifier {
int? selectedCompanyId; // 구매처 ID
int? selectedWarehouseId; // 입고지 ID
// 재고 초기화 필드
int _initialStock = 1; // 초기 재고 수량 (기본값: 1)
int get initialStock => _initialStock;
set initialStock(int value) {
if (_initialStock != value && value > 0) {
_initialStock = value;
notifyListeners();
}
}
// 창고 위치 전체 데이터 (이름-ID 매핑용)
Map<String, int> warehouseLocationMap = {};
@@ -147,9 +181,9 @@ class EquipmentInFormController extends ChangeNotifier {
// 수정불가 필드 목록 (수정 모드에서만 적용)
static const List<String> _readOnlyFields = [
'equipmentNumber', // 장비번호
'serialNumber', // 장비번호
'manufacturer', // 제조사
'modelName', // 모델명
'name', // 모델명
'serialNumber', // 시리얼번호
'purchaseDate', // 구매일
'purchasePrice', // 구매가격
@@ -259,45 +293,42 @@ class EquipmentInFormController extends ChangeNotifier {
print('DEBUG [_loadEquipmentIn] Equipment loaded successfully');
DebugLogger.log('장비 정보 로드 성공', tag: 'EQUIPMENT_IN', data: {
'equipmentId': equipment.id,
'manufacturer': equipment.manufacturer,
'equipmentNumber': equipment.equipmentNumber, // name → equipmentNumber
'companiesId': equipment.companiesId, // 백엔드 실제 필드
'serialNumber': equipment.serialNumber,
});
// 장비 정보 설정 (새로운 필드 구조)
// 장비 정보 설정 (백엔드 실제 필드 구조)
print('DEBUG [_loadEquipmentIn] Setting equipment data...');
print('DEBUG [_loadEquipmentIn] equipment.manufacturer="${equipment.manufacturer}"');
print('DEBUG [_loadEquipmentIn] equipment.equipmentNumber="${equipment.equipmentNumber}"');
print('DEBUG [_loadEquipmentIn] equipment.companiesId="${equipment.companiesId}"');
print('DEBUG [_loadEquipmentIn] equipment.serialNumber="${equipment.serialNumber}"');
// 새로운 필드 구조로 매핑 (setter 사용하여 notifyListeners 자동 호출)
_equipmentNumber = equipment.equipmentNumber ?? '';
manufacturer = equipment.manufacturer ?? '';
modelName = equipment.modelName ?? ''; // deprecated name 제거
// 백엔드 실제 필드로 매핑
_serialNumber = equipment.serialNumber ?? '';
category1 = equipment.category1 ?? ''; // deprecated category 제거
category2 = equipment.category2 ?? ''; // deprecated subCategory 제거
category3 = equipment.category3 ?? ''; // deprecated subSubCategory 제거
purchaseDate = equipment.purchaseDate;
purchasePrice = equipment.purchasePrice;
selectedCompanyId = equipment.companyId;
selectedWarehouseId = equipment.warehouseLocationId;
_modelsId = equipment.modelsId; // 백엔드 실제 필드
selectedCompanyId = equipment.companiesId; // companyId → companiesId
purchasePrice = equipment.purchasePrice.toDouble(); // int → double 변환
remarkController.text = equipment.remark ?? '';
print('DEBUG [_loadEquipmentIn] After setting - equipmentNumber="$_equipmentNumber", manufacturer="$_manufacturer", modelName="$_modelName"');
// Legacy 필드들은 기본값으로 설정 (UI 호환성)
manufacturer = ''; // 더 이상 백엔드에서 제공안함
name = '';
category1 = '';
category2 = '';
category3 = '';
print('DEBUG [_loadEquipmentIn] After setting - serialNumber="$_serialNumber", manufacturer="$_manufacturer", name="$_name"');
// 🔧 [DEBUG] UI 업데이트를 위한 중요 필드들 로깅
print('DEBUG [_loadEquipmentIn] purchaseDate: $purchaseDate, purchasePrice: $purchasePrice');
print('DEBUG [_loadEquipmentIn] selectedCompanyId: $selectedCompanyId, selectedWarehouseId: $selectedWarehouseId');
DebugLogger.log('장비 데이터 설정 완료', tag: 'EQUIPMENT_IN', data: {
'equipmentNumber': _equipmentNumber,
'manufacturer': _manufacturer,
'modelName': _modelName,
'category1': _category1,
'category2': _category2,
'category3': _category3,
'serialNumber': _serialNumber,
'companiesId': selectedCompanyId,
'modelsId': _modelsId,
'purchasePrice': purchasePrice,
});
print('DEBUG [EQUIPMENT_IN]: Equipment loaded - equipmentNumber: "$_equipmentNumber", manufacturer: "$_manufacturer", modelName: "$_modelName"');
print('DEBUG [EQUIPMENT_IN]: Equipment loaded - serialNumber: "$_serialNumber", companiesId: "$selectedCompanyId", modelsId: "$_modelsId"');
// 추가 필드들은 입고 시에는 필요하지 않으므로 생략
@@ -359,22 +390,18 @@ class EquipmentInFormController extends ChangeNotifier {
if (_manufacturer.isNotEmpty && !manufacturers.contains(_manufacturer)) {
manufacturers.add(_manufacturer);
}
if (_modelName.isNotEmpty && !equipmentNames.contains(_modelName)) {
equipmentNames.add(_modelName);
if (_name.isNotEmpty && !equipmentNames.contains(_name)) {
equipmentNames.add(_name);
}
// 백엔드 API 구조에 맞는 Equipment 객체 생성
final equipment = Equipment(
// Required 필드들
manufacturer: _manufacturer.trim(),
equipmentNumber: _equipmentNumber.trim(), // required
modelName: _modelName.trim().isEmpty ? _equipmentNumber.trim() : _modelName.trim(), // required
category1: _category1.trim().isEmpty ? 'N/A' : _category1.trim(), // required (nullable에서 non-nullable로)
category2: _category2.trim().isEmpty ? 'N/A' : _category2.trim(), // required (nullable에서 non-nullable로)
category3: _category3.trim().isEmpty ? 'N/A' : _category3.trim(), // required (nullable에서 non-nullable로)
modelsId: _modelsId, // 필수: Model ID (Vendor→Model cascade에서 선택)
equipmentNumber: _serialNumber.trim(), // required: 장비번호
serialNumber: _serialNumber.trim(), // required: 시리얼번호
quantity: 1, // required
// Optional 필드들
serialNumber: _serialNumber.trim().isEmpty ? null : _serialNumber.trim(),
purchaseDate: purchaseDate,
purchasePrice: purchasePrice,
inDate: purchaseDate ?? DateTime.now(),
@@ -393,13 +420,26 @@ class EquipmentInFormController extends ChangeNotifier {
DebugLogger.log('장비 정보 업데이트 시작', tag: 'EQUIPMENT_IN', data: {
'equipmentId': actualEquipmentId,
'equipmentNumber': equipment.equipmentNumber,
'manufacturer': equipment.manufacturer,
'modelName': equipment.modelName,
'serialNumber': equipment.serialNumber,
'modelsId': equipment.modelsId,
'companyId': equipment.companyId,
});
await _equipmentService.updateEquipment(actualEquipmentId!, equipment);
// Equipment 객체를 EquipmentUpdateRequestDto로 변환
final updateRequest = EquipmentUpdateRequestDto(
companiesId: selectedCompanyId ?? 0,
modelsId: _modelsId ?? 0,
serialNumber: _serialNumber,
barcode: null,
purchasedAt: null,
purchasePrice: purchasePrice?.toInt() ?? 0,
warrantyNumber: '',
warrantyStartedAt: DateTime.now(),
warrantyEndedAt: DateTime.now().add(Duration(days: 365)),
remark: remarkController.text.isNotEmpty ? remarkController.text : null,
);
await _equipmentService.updateEquipment(actualEquipmentId!, updateRequest);
DebugLogger.log('장비 정보 업데이트 성공', tag: 'EQUIPMENT_IN');
} else {
@@ -407,25 +447,67 @@ class EquipmentInFormController extends ChangeNotifier {
try {
// 1. 먼저 장비 생성
DebugLogger.log('장비 입고 시작', tag: 'EQUIPMENT_IN', data: {
'equipmentNumber': _equipmentNumber,
'manufacturer': _manufacturer,
'modelName': _modelName,
'serialNumber': _serialNumber,
'manufacturer': _manufacturer,
'name': _name,
'companiesId': selectedCompanyId,
});
final createdEquipment = await _equipmentService.createEquipment(equipment);
// Equipment 객체를 EquipmentRequestDto로 변환
final createRequest = EquipmentRequestDto(
companiesId: selectedCompanyId ?? 0,
modelsId: _modelsId ?? 0,
serialNumber: _serialNumber,
barcode: null,
purchasedAt: null,
purchasePrice: purchasePrice?.toInt() ?? 0,
warrantyNumber: '',
warrantyStartedAt: DateTime.now(),
warrantyEndedAt: DateTime.now().add(Duration(days: 365)),
remark: remarkController.text.isNotEmpty ? remarkController.text : null,
);
final createdEquipment = await _equipmentService.createEquipment(createRequest);
DebugLogger.log('장비 생성 성공', tag: 'EQUIPMENT_IN', data: {
'equipmentId': createdEquipment.id,
});
// 새로운 API에서는 장비 생성 시 입고 처리까지 한 번에 처리됨
// 2. Equipment History (입고 기록) 생성
if (selectedWarehouseId != null && createdEquipment.id != null) {
try {
// EquipmentHistoryController를 통한 입고 처리
final historyController = EquipmentHistoryController();
// 입고 처리 (EquipmentHistoryRequestDto 객체 생성)
final historyRequest = EquipmentHistoryRequestDto(
equipmentsId: createdEquipment.id, // null 체크 이미 완료되어 ! 연산자 불필요
warehousesId: selectedWarehouseId!,
transactionType: 'I', // 입고: 'I'
quantity: _initialStock,
transactedAt: DateTime.now(),
remark: '장비 등록 시 자동 입고',
);
await historyController.createHistory(historyRequest);
DebugLogger.log('Equipment History 생성 성공', tag: 'EQUIPMENT_IN', data: {
'equipmentId': createdEquipment.id,
'warehouseId': selectedWarehouseId,
'quantity': _initialStock,
});
} catch (e) {
// 입고 실패 시에도 장비는 이미 생성되었으므로 경고만 표시
DebugLogger.logError('Equipment History 생성 실패', error: e);
_error = '장비는 등록되었으나 입고 처리 중 오류가 발생했습니다.';
}
}
DebugLogger.log('입고 처리 성공', tag: 'EQUIPMENT_IN');
DebugLogger.log('입고 처리 완료', tag: 'EQUIPMENT_IN');
} catch (e) {
DebugLogger.logError('장비 입고 처리 실패', error: e);
throw e; // 에러를 상위로 전파하여 적절한 에러 메시지 표시
rethrow; // 에러를 상위로 전파하여 적절한 에러 메시지 표시
}
}

View File

@@ -5,12 +5,11 @@ import 'package:superport/services/equipment_service.dart';
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/data/models/common/pagination_params.dart';
import 'package:superport/core/services/lookups_service.dart';
import 'package:superport/data/models/lookups/lookup_data.dart';
import 'package:superport/utils/constants.dart';
import 'package:superport/data/models/equipment/equipment_dto.dart';
/// 장비 목록 화면의 상태 및 비즈니스 로직을 담당하는 컨트롤러 (리팩토링 버전)
/// BaseListController를 상속받아 공통 기능을 재사용
@@ -76,8 +75,8 @@ class EquipmentListController extends BaseListController<UnifiedEquipment> {
status: _statusFilter != null ?
EquipmentStatusConverter.clientToServer(_statusFilter) : null,
search: params.search,
companyId: _companyIdFilter,
includeInactive: _includeInactive,
// companyId: _companyIdFilter, // 비활성화: EquipmentService에서 지원하지 않음
// includeInactive: _includeInactive, // 비활성화: EquipmentService에서 지원하지 않음
),
onError: (failure) {
throw failure;
@@ -102,18 +101,13 @@ class EquipmentListController extends BaseListController<UnifiedEquipment> {
print('DEBUG [EquipmentListController] Converting ${apiEquipmentDtos.items.length} DTOs to UnifiedEquipment');
final items = apiEquipmentDtos.items.map((dto) {
// 🔧 [DEBUG] JOIN된 데이터 로깅
print('DEBUG [EquipmentListController] DTO ID: ${dto.id}, companyName: "${dto.companyName}", warehouseName: "${dto.warehouseName}"');
print('DEBUG [EquipmentListController] DTO ID: ${dto.id}, companyName: "${dto.companyName}"');
final equipment = Equipment(
id: dto.id,
manufacturer: dto.manufacturer ?? 'Unknown',
equipmentNumber: dto.equipmentNumber ?? 'Unknown', // name → equipmentNumber (required)
modelName: dto.modelName ?? dto.equipmentNumber ?? 'Unknown', // 새로운 필수 필드 (required)
// 🔧 [BUG FIX] 하드코딩 제거 - 백엔드 API에서 카테고리 정보 미제공 시 기본값 사용
// TODO: 백엔드 API에서 category1/2/3 필드 추가 필요
category1: 'N/A', // 백엔드에서 카테고리 정보 미제공 시 기본값
category2: 'N/A', // 백엔드에서 카테고리 정보 미제공 시 기본값
category3: 'N/A', // 백엔드에서 카테고리 정보 미제공 시 기본값
serialNumber: dto.serialNumber,
modelsId: dto.modelsId, // Sprint 3: Model FK 사용
model: null, // Equipment 생성자에서 ModelDto? 요구, 현재 DTO는 modelName(String) 제공
equipmentNumber: dto.serialNumber ?? 'Unknown', // 장비번호 (required)
serialNumber: dto.serialNumber ?? 'Unknown', // 시리얼번호 (required)
quantity: 1, // 기본 수량
);
@@ -123,15 +117,15 @@ class EquipmentListController extends BaseListController<UnifiedEquipment> {
final unifiedEquipment = UnifiedEquipment(
id: dto.id,
equipment: equipment,
date: dto.createdAt ?? DateTime.now(),
status: EquipmentStatusConverter.serverToClient(dto.status),
notes: null, // EquipmentListDto에 remark 필드 없음
date: dto.registeredAt ?? DateTime.now(), // EquipmentDto에는 createdAt 대신 registeredAt 존재
status: '입고', // EquipmentDto에 status 필드 없음 - 기본값 설정 (실제는 Equipment_History에서 상태 관리)
notes: dto.remark, // EquipmentDto에 remark 필드 존재
// 🔧 [BUG FIX] 누락된 위치 정보 필드들 추가
// 문제: 장비 리스트에서 위치 정보(현재 위치, 창고 위치)가 표시되지 않음
// 원인: UnifiedEquipment 생성 시 JOIN된 데이터(companyName, warehouseName) 누락
// 해결: EquipmentListDto의 JOIN된 데이터를 UnifiedEquipment 필드로 매핑
// 원인: EquipmentDto에 warehouseName 필드가 없음 (백엔드 스키마에 warehouse 정보 분리)
// 해결: 현재는 companyName만 사용, warehouseLocation은 null로 설정
currentCompany: dto.companyName, // API company_name → currentCompany
warehouseLocation: dto.warehouseName, // API warehouse_name → warehouseLocation
warehouseLocation: null, // EquipmentDto에 warehouse_name 필드 없음
// currentBranch는 EquipmentListDto에 없으므로 null (백엔드 API 구조 변경으로 지점 개념 제거)
currentBranch: null,
);
@@ -156,10 +150,9 @@ class EquipmentListController extends BaseListController<UnifiedEquipment> {
@override
bool filterItem(UnifiedEquipment item, String query) {
final q = query.toLowerCase();
return (item.equipment.equipmentNumber.toLowerCase().contains(q)) || // name → equipmentNumber
(item.equipment.modelName?.toLowerCase().contains(q) ?? false) || // 모델명 추가
(item.equipment.serialNumber?.toLowerCase().contains(q) ?? false) ||
(item.equipment.manufacturer.toLowerCase().contains(q)) ||
return (item.equipment.serialNumber?.toLowerCase().contains(q) ?? false) || // serialNumber 검색
(item.equipment.modelName.toLowerCase().contains(q)) || // Equipment.modelName getter 사용
(item.equipment.manufacturer.toLowerCase().contains(q)) || // Equipment.manufacturer getter 사용
(item.notes?.toLowerCase().contains(q) ?? false) ||
(item.status.toLowerCase().contains(q));
}
@@ -277,8 +270,8 @@ class EquipmentListController extends BaseListController<UnifiedEquipment> {
await ErrorHandler.handleApiCall<void>(
() => _equipmentService.changeEquipmentStatus(
id,
EquipmentStatusConverter.clientToServer(newStatus),
reason,
EquipmentStatusConverter.clientToServer(newStatus),
// reason 파라미터는 EquipmentService에서 지원하지 않음
),
onError: (failure) {
throw failure;
@@ -314,7 +307,7 @@ class EquipmentListController extends BaseListController<UnifiedEquipment> {
reason: reason ?? '폐기 처리',
);
} catch (e) {
failedEquipments.add('${equipment.equipment.manufacturer} ${equipment.equipment.equipmentNumber}'); // name → equipmentNumber
failedEquipments.add('${equipment.equipment.manufacturer} ${equipment.equipment.serialNumber}'); // name → serialNumber
}
}
@@ -327,8 +320,22 @@ class EquipmentListController extends BaseListController<UnifiedEquipment> {
/// 장비 정보 수정
Future<void> updateEquipment(int id, UnifiedEquipment equipment) async {
// Equipment → EquipmentUpdateRequestDto 변환 (실제 필드명으로 매핑)
final updateRequest = EquipmentUpdateRequestDto(
companiesId: equipment.equipment.currentCompanyId ?? equipment.equipment.companyId, // companiesId 필드 없음
modelsId: equipment.equipment.modelsId, // ✓ 있음
serialNumber: equipment.equipment.serialNumber, // ✓ 있음
barcode: equipment.equipment.barcode, // ✓ 있음
purchasedAt: equipment.equipment.purchaseDate, // purchasedAt → purchaseDate
purchasePrice: equipment.equipment.purchasePrice?.toInt(), // double? → int? 변환
warrantyNumber: equipment.equipment.warrantyLicense, // warrantyNumber → warrantyLicense
warrantyStartedAt: equipment.equipment.warrantyStartDate, // warrantyStartedAt → warrantyStartDate
warrantyEndedAt: equipment.equipment.warrantyEndDate, // warrantyEndedAt → warrantyEndDate
remark: equipment.notes, // UnifiedEquipment.notes → DTO.remark
);
await ErrorHandler.handleApiCall<void>(
() => _equipmentService.updateEquipment(id, equipment.equipment),
() => _equipmentService.updateEquipment(id, updateRequest),
onError: (failure) {
throw failure;
},

View File

@@ -4,16 +4,16 @@ import 'package:intl/intl.dart';
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/equipment_service.dart';
import 'package:superport/screens/equipment/controllers/equipment_history_controller.dart';
import 'package:superport/services/company_service.dart';
import 'package:superport/utils/constants.dart';
import 'package:superport/data/models/equipment_history_dto.dart';
/// 장비 출고 폼 컨트롤러
///
/// 폼의 전체 상태, 유효성, 저장, 데이터 로딩 등 비즈니스 로직을 담당한다.
class EquipmentOutFormController extends ChangeNotifier {
final EquipmentService _equipmentService = GetIt.instance<EquipmentService>();
// final EquipmentService _equipmentService = GetIt.instance<EquipmentService>(); // 사용되지 않음 - 제거
final CompanyService _companyService = GetIt.instance<CompanyService>();
int? equipmentOutId;
@@ -21,7 +21,7 @@ class EquipmentOutFormController extends ChangeNotifier {
bool isEditMode = false;
// 상태 관리
bool _isLoading = false;
final bool _isLoading = false;
String? _errorMessage;
// Getters
@@ -221,26 +221,34 @@ class EquipmentOutFormController extends ChangeNotifier {
} else {
// 생성 모드
if (selectedEquipments != null && selectedEquipments!.isNotEmpty) {
// 다중 장비 출고
// 다중 장비 출고 (새로운 EquipmentHistoryController 사용)
final historyController = EquipmentHistoryController();
for (var equipmentData in selectedEquipments!) {
final equipment = equipmentData['equipment'] as Equipment;
if (equipment.id != null) {
await equipmentService.equipmentOut(
equipmentId: equipment.id!,
final historyRequest = EquipmentHistoryRequestDto(
equipmentsId: equipment.id!,
warehousesId: 1, // 기본 창고 ID (실제로는 사용자 선택 필요)
transactionType: 'OUT',
quantity: equipment.quantity,
companyId: companyId,
notes: note ?? remarkController.text,
remark: note ?? remarkController.text,
);
await historyController.createHistory(historyRequest);
}
}
} else if (selectedEquipment != null && selectedEquipment!.id != null) {
// 단일 장비 출고
await equipmentService.equipmentOut(
equipmentId: selectedEquipment!.id!,
// 단일 장비 출고 (새로운 EquipmentHistoryController 사용)
final historyController = EquipmentHistoryController();
final historyRequest = EquipmentHistoryRequestDto(
equipmentsId: selectedEquipment!.id!,
warehousesId: 1, // 기본 창고 ID (실제로는 사용자 선택 필요)
transactionType: 'OUT',
quantity: selectedEquipment!.quantity,
companyId: companyId,
notes: note ?? remarkController.text,
remark: note ?? remarkController.text,
);
await historyController.createHistory(historyRequest);
}
}