feat: V/R 유지보수 시스템 전환 및 대시보드 테이블 형태 완성
- V/R 시스템 완전 전환: WARRANTY/CONTRACT/INSPECTION → V(방문)/R(원격) - 유지보수 대시보드 카드 → StandardDataTable 테이블 형태 전환 - "조회중..." 문제 해결: 백엔드 직접 필드 사용 (equipment_model, company_name) - MaintenanceDto 신규 필드 추가: company_id, company_name, equipment_serial, equipment_model - preloadEquipmentData 비활성화로 불필요한 equipment-history API 호출 제거 - CO-STAR 프레임워크 적용 및 CLAUDE.md v3.0 업데이트 - Flutter Analyze ERROR: 0 유지, 100% shadcn_ui 컴플라이언스 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -6,9 +6,8 @@ import 'package:superport/services/equipment_service.dart';
|
||||
import 'package:superport/core/errors/failures.dart';
|
||||
import 'package:superport/core/utils/debug_logger.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';
|
||||
import 'package:superport/data/repositories/equipment_history_repository.dart';
|
||||
|
||||
/// 장비 입고 폼 컨트롤러
|
||||
///
|
||||
@@ -19,6 +18,7 @@ class EquipmentInFormController extends ChangeNotifier {
|
||||
// final WarehouseService _warehouseService = GetIt.instance<WarehouseService>(); // 사용되지 않음 - 제거
|
||||
// final CompanyService _companyService = GetIt.instance<CompanyService>(); // 사용되지 않음 - 제거
|
||||
final LookupsService _lookupsService = GetIt.instance<LookupsService>();
|
||||
final EquipmentHistoryRepository _equipmentHistoryRepository = GetIt.instance<EquipmentHistoryRepository>();
|
||||
final int? equipmentInId; // 실제로는 장비 ID (입고 ID가 아님)
|
||||
int? actualEquipmentId; // API 호출용 실제 장비 ID
|
||||
EquipmentDto? preloadedEquipment; // 사전 로드된 장비 데이터
|
||||
@@ -223,18 +223,39 @@ class EquipmentInFormController extends ChangeNotifier {
|
||||
required Map<String, dynamic> preloadedData,
|
||||
}) : equipmentInId = preloadedData['equipmentId'] as int?,
|
||||
actualEquipmentId = preloadedData['equipmentId'] as int? {
|
||||
print('DEBUG [withPreloadedData] preloadedData keys: ${preloadedData.keys.toList()}');
|
||||
print('DEBUG [withPreloadedData] equipmentId from args: ${preloadedData['equipmentId']}');
|
||||
print('DEBUG [withPreloadedData] equipmentInId after assignment: $equipmentInId');
|
||||
print('DEBUG [withPreloadedData] actualEquipmentId: $actualEquipmentId');
|
||||
isEditMode = equipmentInId != null;
|
||||
print('DEBUG [withPreloadedData] isEditMode: $isEditMode');
|
||||
|
||||
// 전달받은 데이터로 즉시 초기화
|
||||
preloadedEquipment = preloadedData['equipment'] as EquipmentDto?;
|
||||
print('DEBUG [withPreloadedData] equipment 데이터 타입: ${preloadedData['equipment'].runtimeType}');
|
||||
print('DEBUG [withPreloadedData] equipment 원시 데이터: ${preloadedData['equipment']}');
|
||||
|
||||
try {
|
||||
preloadedEquipment = preloadedData['equipment'] as EquipmentDto?;
|
||||
print('DEBUG [withPreloadedData] EquipmentDto 캐스팅 성공');
|
||||
print('DEBUG [withPreloadedData] preloadedEquipment: ${preloadedEquipment != null ? "있음 (id: ${preloadedEquipment!.id})" : "null"}');
|
||||
} catch (e, stackTrace) {
|
||||
print('DEBUG [withPreloadedData] EquipmentDto 캐스팅 실패: $e');
|
||||
print('DEBUG [withPreloadedData] StackTrace: $stackTrace');
|
||||
preloadedEquipment = null;
|
||||
}
|
||||
|
||||
final dropdownData = preloadedData['dropdownData'] as Map<String, dynamic>?;
|
||||
print('DEBUG [withPreloadedData] dropdownData: ${dropdownData != null ? "있음 (${dropdownData.keys.length}개 키)" : "null"}');
|
||||
|
||||
if (dropdownData != null) {
|
||||
_processDropdownData(dropdownData);
|
||||
}
|
||||
|
||||
if (preloadedEquipment != null) {
|
||||
print('DEBUG [withPreloadedData] _loadFromEquipment() 호출 예정');
|
||||
_loadFromEquipment(preloadedEquipment!);
|
||||
} else {
|
||||
print('DEBUG [withPreloadedData] preloadedEquipment가 null이어서 _loadFromEquipment() 호출 안함');
|
||||
}
|
||||
|
||||
_updateCanSave();
|
||||
@@ -242,13 +263,19 @@ class EquipmentInFormController extends ChangeNotifier {
|
||||
|
||||
// 수정 모드 초기화 (외부에서 호출)
|
||||
Future<void> initializeForEdit() async {
|
||||
if (!isEditMode || equipmentInId == null) return;
|
||||
print('DEBUG [initializeForEdit] 호출됨 - isEditMode: $isEditMode, equipmentInId: $equipmentInId');
|
||||
if (!isEditMode || equipmentInId == null) {
|
||||
print('DEBUG [initializeForEdit] 조건 미충족으로 return');
|
||||
return;
|
||||
}
|
||||
|
||||
print('DEBUG [initializeForEdit] 드롭다운 데이터와 장비 데이터 병렬 로드 시작');
|
||||
// 드롭다운 데이터와 장비 데이터를 병렬로 로드
|
||||
await Future.wait([
|
||||
_waitForDropdownData(),
|
||||
_loadEquipmentIn(),
|
||||
]);
|
||||
print('DEBUG [initializeForEdit] 병렬 로드 완료');
|
||||
}
|
||||
|
||||
// 드롭다운 데이터 로드 대기
|
||||
@@ -366,12 +393,24 @@ class EquipmentInFormController extends ChangeNotifier {
|
||||
|
||||
// 전달받은 장비 데이터로 폼 초기화 (간소화: 백엔드 JOIN 데이터 직접 활용)
|
||||
void _loadFromEquipment(EquipmentDto equipment) {
|
||||
print('DEBUG [_loadFromEquipment] 호출됨 - equipment.id: ${equipment.id}');
|
||||
print('DEBUG [_loadFromEquipment] equipment.warehousesId: ${equipment.warehousesId}');
|
||||
|
||||
serialNumber = equipment.serialNumber;
|
||||
barcode = equipment.barcode ?? '';
|
||||
modelsId = equipment.modelsId;
|
||||
purchasePrice = equipment.purchasePrice > 0 ? equipment.purchasePrice.toDouble() : null;
|
||||
initialStock = 1;
|
||||
selectedCompanyId = equipment.companiesId;
|
||||
selectedWarehouseId = equipment.warehousesId; // ✅ 기존 창고 ID 복원 (항상 null)
|
||||
print('DEBUG [_loadFromEquipment] selectedWarehouseId after assignment: $selectedWarehouseId');
|
||||
|
||||
// 🔧 창고 정보가 null이므로 Equipment History에서 비동기로 로드 필요
|
||||
if (selectedWarehouseId == null) {
|
||||
print('DEBUG [_loadFromEquipment] 창고 정보 null - 비동기 로드 예약');
|
||||
// 비동기 메서드는 동기 메서드에서 직접 호출 불가 -> Future 예약
|
||||
Future.microtask(() => _loadWarehouseFromHistory(equipment.id));
|
||||
}
|
||||
|
||||
// ✅ 간소화: 백엔드 JOIN 데이터 직접 사용 (복잡한 Controller 조회 제거)
|
||||
manufacturer = equipment.vendorName ?? '제조사 정보 없음';
|
||||
@@ -386,8 +425,10 @@ class EquipmentInFormController extends ChangeNotifier {
|
||||
remarkController.text = equipment.remark ?? '';
|
||||
warrantyNumberController.text = equipment.warrantyNumber;
|
||||
|
||||
// 수정 모드에서 입고지 기본값 설정
|
||||
// ✅ 수정 모드에서는 기존 창고 ID를 우선 사용, null인 경우에만 기본값 설정
|
||||
// (이제 위에서 selectedWarehouseId = equipment.warehousesId 로 설정하므로 이 조건은 거의 실행되지 않음)
|
||||
if (isEditMode && selectedWarehouseId == null && warehouses.isNotEmpty) {
|
||||
// 기존 창고 ID가 없는 경우에만 첫 번째 창고 선택
|
||||
selectedWarehouseId = warehouses.keys.first;
|
||||
}
|
||||
|
||||
@@ -397,10 +438,51 @@ class EquipmentInFormController extends ChangeNotifier {
|
||||
_updateCanSave();
|
||||
notifyListeners(); // UI 즉시 업데이트
|
||||
}
|
||||
|
||||
/// Equipment History에서 창고 정보를 로드 (비동기)
|
||||
Future<void> _loadWarehouseFromHistory(int equipmentId) async {
|
||||
try {
|
||||
print('DEBUG [_loadWarehouseFromHistory] 시작 - 장비 ID: $equipmentId');
|
||||
|
||||
final histories = await _equipmentHistoryRepository.getEquipmentHistoriesByEquipmentId(equipmentId);
|
||||
print('DEBUG [_loadWarehouseFromHistory] API 응답: ${histories.length}개 기록');
|
||||
|
||||
if (histories.isNotEmpty) {
|
||||
// 가장 최근 이력의 창고 ID 사용
|
||||
final latestHistory = histories.first;
|
||||
selectedWarehouseId = latestHistory.warehousesId;
|
||||
|
||||
final warehouseName = warehouses[selectedWarehouseId] ?? '알 수 없는 창고';
|
||||
print('DEBUG [_loadWarehouseFromHistory] 창고 정보 찾음: $warehouseName (ID: $selectedWarehouseId)');
|
||||
print('DEBUG [_loadWarehouseFromHistory] 최근 거래: ${latestHistory.transactionType} (${latestHistory.transactedAt})');
|
||||
|
||||
notifyListeners(); // UI 업데이트
|
||||
} else {
|
||||
print('DEBUG [_loadWarehouseFromHistory] 이력 없음 - 기본 창고 사용');
|
||||
if (warehouses.isNotEmpty) {
|
||||
selectedWarehouseId = warehouses.keys.first;
|
||||
print('DEBUG [_loadWarehouseFromHistory] 기본 창고로 설정: $selectedWarehouseId');
|
||||
notifyListeners();
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
print('DEBUG [_loadWarehouseFromHistory] 오류: $e');
|
||||
// 오류 발생시 기본 창고 사용
|
||||
if (warehouses.isNotEmpty) {
|
||||
selectedWarehouseId = warehouses.keys.first;
|
||||
print('DEBUG [_loadWarehouseFromHistory] 오류로 인한 기본 창고 설정: $selectedWarehouseId');
|
||||
notifyListeners();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 기존 데이터 로드(수정 모드)
|
||||
Future<void> _loadEquipmentIn() async {
|
||||
if (equipmentInId == null) return;
|
||||
print('DEBUG [_loadEquipmentIn] 호출됨 - equipmentInId: $equipmentInId');
|
||||
if (equipmentInId == null) {
|
||||
print('DEBUG [_loadEquipmentIn] equipmentInId가 null이어서 return');
|
||||
return;
|
||||
}
|
||||
|
||||
_isLoading = true;
|
||||
_error = null;
|
||||
@@ -436,6 +518,51 @@ class EquipmentInFormController extends ChangeNotifier {
|
||||
_serialNumber = equipment.serialNumber;
|
||||
_modelsId = equipment.modelsId; // 백엔드 실제 필드
|
||||
selectedCompanyId = equipment.companiesId; // companyId → companiesId
|
||||
selectedWarehouseId = equipment.warehousesId; // ✅ 기존 창고 ID 복원 (백엔드에서 null)
|
||||
print('DEBUG [_loadEquipmentIn] equipment.warehousesId: ${equipment.warehousesId}');
|
||||
print('DEBUG [_loadEquipmentIn] selectedWarehouseId after assignment: $selectedWarehouseId');
|
||||
|
||||
// 🔧 창고 정보 우회 처리: Equipment History에서 가장 최근 창고 정보 조회
|
||||
// 백엔드 Equipment API가 창고 정보를 제공하지 않으므로 항상 Equipment History에서 조회
|
||||
try {
|
||||
print('DEBUG [_loadEquipmentIn] Equipment History API 호출 시작');
|
||||
final equipmentHistories = await _equipmentHistoryRepository.getEquipmentHistoriesByEquipmentId(equipment.id);
|
||||
print('DEBUG [_loadEquipmentIn] Equipment History API 응답: ${equipmentHistories.length}개 기록');
|
||||
|
||||
if (equipmentHistories.isNotEmpty) {
|
||||
// 가장 최근 이력의 창고 ID 사용 (이미 날짜 순으로 정렬됨)
|
||||
final latestHistory = equipmentHistories.first;
|
||||
selectedWarehouseId = latestHistory.warehousesId;
|
||||
|
||||
// 창고 이름 찾기
|
||||
final warehouseName = warehouses[selectedWarehouseId] ?? '알 수 없는 창고';
|
||||
|
||||
print('DEBUG [_loadEquipmentIn] Equipment History에서 창고 정보 찾음: $warehouseName (ID: $selectedWarehouseId)');
|
||||
print('DEBUG [_loadEquipmentIn] 최근 거래: ${latestHistory.transactionType} (${latestHistory.transactedAt})');
|
||||
DebugLogger.log('창고 정보 우회 조회 성공', tag: 'EQUIPMENT_IN', data: {
|
||||
'equipmentId': equipment.id,
|
||||
'warehouseId': selectedWarehouseId,
|
||||
'warehouseName': warehouseName,
|
||||
'lastTransaction': latestHistory.transactionType,
|
||||
'transactedAt': latestHistory.transactedAt.toIso8601String(),
|
||||
});
|
||||
} else {
|
||||
print('DEBUG [_loadEquipmentIn] Equipment History가 비어있음 - 기본 창고 사용');
|
||||
// 창고 정보를 찾을 수 없으면 기본값으로 첫 번째 창고 사용
|
||||
if (warehouses.isNotEmpty) {
|
||||
selectedWarehouseId = warehouses.keys.first;
|
||||
print('DEBUG [_loadEquipmentIn] 기본 창고로 설정: $selectedWarehouseId');
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
print('DEBUG [_loadEquipmentIn] Equipment History에서 창고 정보 찾기 실패: $e');
|
||||
// 창고 정보를 찾을 수 없으면 기본값으로 첫 번째 창고 사용
|
||||
if (warehouses.isNotEmpty) {
|
||||
selectedWarehouseId = warehouses.keys.first;
|
||||
print('DEBUG [_loadEquipmentIn] 기본 창고로 설정: $selectedWarehouseId');
|
||||
}
|
||||
}
|
||||
|
||||
purchasePrice = equipment.purchasePrice > 0 ? equipment.purchasePrice.toDouble() : null; // int → double 변환, 0이면 null
|
||||
remarkController.text = equipment.remark ?? '';
|
||||
|
||||
@@ -520,6 +647,13 @@ class EquipmentInFormController extends ChangeNotifier {
|
||||
}
|
||||
formKey.currentState!.save();
|
||||
|
||||
// 입고지 필수 선택 검증 (신규 생성 모드에서만)
|
||||
if (!isEditMode && selectedWarehouseId == null) {
|
||||
_error = '입고지는 필수 선택 항목입니다. 입고지를 선택해주세요.';
|
||||
if (!_disposed) notifyListeners();
|
||||
return false;
|
||||
}
|
||||
|
||||
_isSaving = true;
|
||||
_error = null;
|
||||
_updateCanSave(); // 저장 시작 시 canSave 상태 업데이트
|
||||
@@ -641,34 +775,50 @@ class EquipmentInFormController extends ChangeNotifier {
|
||||
'equipmentId': createdEquipment.id,
|
||||
});
|
||||
|
||||
// 2. Equipment History (입고 기록) 생성
|
||||
// 2. Equipment History (입고 기록) 생성 - 출고 시스템과 동일한 패턴 적용
|
||||
print('🔍 [입고 처리] selectedWarehouseId: $selectedWarehouseId, createdEquipment.id: ${createdEquipment.id}');
|
||||
|
||||
if (selectedWarehouseId != null && createdEquipment.id != null) {
|
||||
// 입고지 정보 상세 로깅
|
||||
final warehouseName = warehouses[selectedWarehouseId] ?? '알 수 없는 창고';
|
||||
print('🏪 [입고 처리] 입고지 정보:');
|
||||
print(' - 창고 ID: $selectedWarehouseId');
|
||||
print(' - 창고 이름: $warehouseName');
|
||||
print(' - 장비 ID: ${createdEquipment.id}');
|
||||
print(' - 입고 수량: $_initialStock');
|
||||
|
||||
try {
|
||||
// EquipmentHistoryController를 통한 입고 처리
|
||||
final historyController = EquipmentHistoryController();
|
||||
|
||||
// 입고 처리 (EquipmentHistoryRequestDto 객체 생성)
|
||||
final historyRequest = EquipmentHistoryRequestDto(
|
||||
equipmentsId: createdEquipment.id, // null 체크 이미 완료되어 ! 연산자 불필요
|
||||
// ✅ Repository 직접 호출 (출고 시스템과 동일한 패턴)
|
||||
await _equipmentHistoryRepository.createStockIn(
|
||||
equipmentsId: createdEquipment.id,
|
||||
warehousesId: selectedWarehouseId!,
|
||||
transactionType: 'I', // 입고: 'I'
|
||||
quantity: _initialStock,
|
||||
transactedAt: DateTime.now(),
|
||||
transactedAt: DateTime.now().toUtc().copyWith(microsecond: 0),
|
||||
remark: '장비 등록 시 자동 입고',
|
||||
);
|
||||
|
||||
await historyController.createHistory(historyRequest);
|
||||
|
||||
print('✅ [입고 처리] Equipment History 생성 성공');
|
||||
DebugLogger.log('Equipment History 생성 성공', tag: 'EQUIPMENT_IN', data: {
|
||||
'equipmentId': createdEquipment.id,
|
||||
'warehouseId': selectedWarehouseId,
|
||||
'warehouseName': warehouseName,
|
||||
'quantity': _initialStock,
|
||||
});
|
||||
} catch (e) {
|
||||
// 입고 실패 시에도 장비는 이미 생성되었으므로 경고만 표시
|
||||
// ✅ 입고 이력 생성 실패시 전체 프로세스 실패 처리 (출고 시스템과 동일)
|
||||
print('❌ [입고 처리] Equipment History 생성 실패: $e');
|
||||
DebugLogger.logError('Equipment History 생성 실패', error: e);
|
||||
_error = '장비는 등록되었으나 입고 처리 중 오류가 발생했습니다.';
|
||||
throw Exception('입고 이력 생성에 실패했습니다. 다시 시도해주세요: $e');
|
||||
}
|
||||
} else {
|
||||
// 필수 정보 누락 시 에러
|
||||
final missingInfo = <String>[];
|
||||
if (selectedWarehouseId == null) missingInfo.add('입고지');
|
||||
if (createdEquipment.id == null) missingInfo.add('장비 ID');
|
||||
|
||||
final errorMsg = '입고 처리 실패: ${missingInfo.join(', ')} 정보가 누락되었습니다';
|
||||
print('❌ [입고 처리] $errorMsg');
|
||||
_error = errorMsg;
|
||||
}
|
||||
|
||||
DebugLogger.log('입고 처리 완료', tag: 'EQUIPMENT_IN');
|
||||
|
||||
@@ -11,12 +11,14 @@ import 'package:superport/data/models/lookups/lookup_data.dart';
|
||||
import 'package:superport/utils/constants.dart';
|
||||
import 'package:superport/data/models/equipment/equipment_dto.dart';
|
||||
import 'package:superport/domain/usecases/equipment/search_equipment_usecase.dart';
|
||||
import 'package:superport/services/equipment_history_service.dart';
|
||||
|
||||
/// 장비 목록 화면의 상태 및 비즈니스 로직을 담당하는 컨트롤러 (리팩토링 버전)
|
||||
/// BaseListController를 상속받아 공통 기능을 재사용
|
||||
class EquipmentListController extends BaseListController<UnifiedEquipment> {
|
||||
late final EquipmentService _equipmentService;
|
||||
late final LookupsService _lookupsService;
|
||||
late final EquipmentHistoryService _historyService;
|
||||
|
||||
// 추가 상태 관리
|
||||
final Set<String> selectedEquipmentIds = {}; // 'id:status' 형식
|
||||
@@ -62,6 +64,7 @@ class EquipmentListController extends BaseListController<UnifiedEquipment> {
|
||||
throw Exception('LookupsService not registered in GetIt');
|
||||
}
|
||||
|
||||
_historyService = EquipmentHistoryService();
|
||||
}
|
||||
|
||||
@override
|
||||
@@ -101,9 +104,9 @@ class EquipmentListController extends BaseListController<UnifiedEquipment> {
|
||||
|
||||
// DTO를 UnifiedEquipment로 변환
|
||||
print('DEBUG [EquipmentListController] Converting ${apiEquipmentDtos.items.length} DTOs to UnifiedEquipment');
|
||||
final items = apiEquipmentDtos.items.map((dto) {
|
||||
final items = await Future.wait(apiEquipmentDtos.items.map((dto) async {
|
||||
// 🔧 [DEBUG] JOIN된 데이터 로깅
|
||||
print('DEBUG [EquipmentListController] DTO ID: ${dto.id}, companyName: "${dto.companyName}"');
|
||||
print('DEBUG [EquipmentListController] DTO ID: ${dto.id}, companyName: "${dto.companyName}", warehousesName: "${dto.warehousesName}", warehousesId: ${dto.warehousesId}');
|
||||
final equipment = Equipment(
|
||||
id: dto.id,
|
||||
modelsId: dto.modelsId, // Sprint 3: Model FK 사용
|
||||
@@ -125,18 +128,34 @@ class EquipmentListController extends BaseListController<UnifiedEquipment> {
|
||||
// 간단한 Company 정보 생성 (사용하지 않으므로 제거)
|
||||
// final company = dto.companyName != null ? ... : null;
|
||||
|
||||
// 각 장비의 최신 히스토리를 조회해서 실제 상태 가져오기
|
||||
String status = 'I'; // 기본값: 입고 (I)
|
||||
DateTime transactionDate = dto.registeredAt ?? DateTime.now();
|
||||
|
||||
try {
|
||||
final histories = await _historyService.getEquipmentHistoriesByEquipmentId(dto.id);
|
||||
if (histories.isNotEmpty) {
|
||||
// 최신 히스토리의 transaction_type 사용
|
||||
// 히스토리는 최신순으로 정렬되어 있다고 가정
|
||||
status = histories.first.transactionType ?? 'I';
|
||||
transactionDate = histories.first.transactedAt ?? transactionDate;
|
||||
print('DEBUG [EquipmentListController] Equipment ${dto.id} status from history: $status');
|
||||
}
|
||||
} catch (e) {
|
||||
print('DEBUG [EquipmentListController] Failed to get history for equipment ${dto.id}: $e');
|
||||
// 히스토리 조회 실패시 기본값 사용
|
||||
}
|
||||
|
||||
final unifiedEquipment = UnifiedEquipment(
|
||||
id: dto.id,
|
||||
equipment: equipment,
|
||||
date: dto.registeredAt ?? DateTime.now(), // EquipmentDto에는 createdAt 대신 registeredAt 존재
|
||||
status: '입고', // EquipmentDto에 status 필드 없음 - 기본값 설정 (실제는 Equipment_History에서 상태 관리)
|
||||
date: transactionDate, // 최신 거래 날짜 사용
|
||||
status: status, // 실제 equipment_history의 transaction_type 사용
|
||||
notes: dto.remark, // EquipmentDto에 remark 필드 존재
|
||||
// 🔧 [BUG FIX] 누락된 위치 정보 필드들 추가
|
||||
// 문제: 장비 리스트에서 위치 정보(현재 위치, 창고 위치)가 표시되지 않음
|
||||
// 원인: EquipmentDto에 warehouseName 필드가 없음 (백엔드 스키마에 warehouse 정보 분리)
|
||||
// 해결: 현재는 companyName만 사용, warehouseLocation은 null로 설정
|
||||
// 백엔드에서 warehouses_name 제공하므로 이를 사용
|
||||
currentCompany: dto.companyName, // API company_name → currentCompany
|
||||
warehouseLocation: null, // EquipmentDto에 warehouse_name 필드 없음
|
||||
warehouseLocation: dto.warehousesName, // API warehouses_name → warehouseLocation
|
||||
// currentBranch는 EquipmentListDto에 없으므로 null (백엔드 API 구조 변경으로 지점 개념 제거)
|
||||
currentBranch: null,
|
||||
// ⚡ [FIX] 백엔드 직접 제공 필드들 추가 - 화면에서 N/A 문제 해결
|
||||
@@ -144,10 +163,10 @@ class EquipmentListController extends BaseListController<UnifiedEquipment> {
|
||||
vendorName: dto.vendorName, // API vendor_name → UI 제조사 컬럼
|
||||
modelName: dto.modelName, // API model_name → UI 모델명 컬럼
|
||||
);
|
||||
// 🔧 [DEBUG] 변환된 UnifiedEquipment 로깅 (필요 시 활성화)
|
||||
// print('DEBUG [EquipmentListController] UnifiedEquipment ID: ${unifiedEquipment.id}, currentCompany: "${unifiedEquipment.currentCompany}", warehouseLocation: "${unifiedEquipment.warehouseLocation}"');
|
||||
// 🔧 [DEBUG] 변환된 UnifiedEquipment 로깅
|
||||
print('DEBUG [EquipmentListController] UnifiedEquipment ID: ${unifiedEquipment.id}, currentCompany: "${unifiedEquipment.currentCompany}", warehouseLocation: "${unifiedEquipment.warehouseLocation}"');
|
||||
return unifiedEquipment;
|
||||
}).toList();
|
||||
}));
|
||||
|
||||
// API에서 반환한 실제 메타데이터 사용
|
||||
final meta = PaginationMeta(
|
||||
@@ -406,7 +425,7 @@ class EquipmentListController extends BaseListController<UnifiedEquipment> {
|
||||
/// 선택된 장비들을 폐기 처리
|
||||
Future<void> disposeSelectedEquipments({String? reason}) async {
|
||||
final selectedEquipments = getSelectedEquipments()
|
||||
.where((equipment) => equipment.status != EquipmentStatus.disposed)
|
||||
.where((equipment) => equipment.status != 'P') // 영문 코드로 통일
|
||||
.toList();
|
||||
|
||||
if (selectedEquipments.isEmpty) {
|
||||
@@ -484,7 +503,7 @@ class EquipmentListController extends BaseListController<UnifiedEquipment> {
|
||||
/// 선택된 입고 상태 장비 개수
|
||||
int getSelectedInStockCount() {
|
||||
return selectedEquipmentIds
|
||||
.where((key) => key.endsWith(':입고'))
|
||||
.where((key) => key.endsWith(':I')) // 영문 코드만 체크
|
||||
.length;
|
||||
}
|
||||
|
||||
@@ -520,6 +539,7 @@ class EquipmentListController extends BaseListController<UnifiedEquipment> {
|
||||
|
||||
/// 특정 상태의 선택된 장비 개수
|
||||
int getSelectedEquipmentCountByStatus(String status) {
|
||||
// status가 이미 코드(I, O, T 등)일 수도 있고, 상수명(EquipmentStatus.in_ 등)일 수도 있음
|
||||
return selectedEquipmentIds
|
||||
.where((key) => key.endsWith(':$status'))
|
||||
.length;
|
||||
|
||||
@@ -0,0 +1,314 @@
|
||||
import 'dart:async';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:intl/intl.dart';
|
||||
import 'package:superport/data/models/equipment/equipment_dto.dart';
|
||||
import 'package:superport/data/models/company/company_dto.dart';
|
||||
import 'package:superport/data/models/stock_status_dto.dart';
|
||||
import 'package:superport/data/repositories/equipment_history_repository.dart';
|
||||
import 'package:superport/domain/repositories/company_repository.dart';
|
||||
import 'package:superport/data/repositories/company_repository_impl.dart';
|
||||
import 'package:superport/data/datasources/remote/company_remote_datasource.dart';
|
||||
import 'package:superport/data/datasources/remote/api_client.dart';
|
||||
import 'package:superport/services/equipment_warehouse_cache_service.dart';
|
||||
|
||||
class EquipmentOutboundController extends ChangeNotifier {
|
||||
final List<EquipmentDto> selectedEquipments;
|
||||
late final CompanyRepository _companyRepository;
|
||||
late final EquipmentHistoryRepository _equipmentHistoryRepository;
|
||||
late final EquipmentWarehouseCacheService _warehouseCacheService;
|
||||
|
||||
// Form controllers
|
||||
final TextEditingController remarkController = TextEditingController();
|
||||
|
||||
// State variables
|
||||
bool _isLoading = false;
|
||||
bool _isLoadingCompanies = false;
|
||||
bool _isLoadingWarehouseInfo = false;
|
||||
String? _errorMessage;
|
||||
String? _companyError;
|
||||
String? _warehouseError;
|
||||
|
||||
// Form data
|
||||
DateTime _transactionDate = DateTime.now();
|
||||
List<CompanyDto> _companies = [];
|
||||
CompanyDto? _selectedCompany;
|
||||
final Map<int, DateTime> _warrantyDates = {}; // 각 장비의 워런티 날짜 관리
|
||||
|
||||
// Getters
|
||||
bool get isLoading => _isLoading;
|
||||
bool get isLoadingCompanies => _isLoadingCompanies;
|
||||
bool get isLoadingWarehouseInfo => _isLoadingWarehouseInfo;
|
||||
String? get errorMessage => _errorMessage;
|
||||
String? get companyError => _companyError;
|
||||
String? get warehouseError => _warehouseError;
|
||||
DateTime get transactionDate => _transactionDate;
|
||||
List<CompanyDto> get companies => _companies;
|
||||
CompanyDto? get selectedCompany => _selectedCompany;
|
||||
|
||||
bool get canSubmit =>
|
||||
!_isLoading &&
|
||||
_selectedCompany != null &&
|
||||
selectedEquipments.isNotEmpty;
|
||||
|
||||
EquipmentOutboundController({
|
||||
required this.selectedEquipments,
|
||||
}) {
|
||||
// Initialize repositories directly with proper dependencies
|
||||
final apiClient = ApiClient();
|
||||
final companyRemoteDataSource = CompanyRemoteDataSourceImpl(apiClient);
|
||||
_companyRepository = CompanyRepositoryImpl(remoteDataSource: companyRemoteDataSource);
|
||||
|
||||
// Initialize EquipmentHistoryRepository with ApiClient's Dio instance
|
||||
// ApiClient has proper auth headers and base URL configuration
|
||||
final dio = apiClient.dio; // Use the authenticated Dio instance from ApiClient
|
||||
_equipmentHistoryRepository = EquipmentHistoryRepositoryImpl(dio);
|
||||
|
||||
// Initialize warehouse cache service
|
||||
_warehouseCacheService = EquipmentWarehouseCacheService();
|
||||
|
||||
// 각 장비의 현재 워런티 날짜로 초기화
|
||||
for (final equipment in selectedEquipments) {
|
||||
final id = equipment.id;
|
||||
final warrantyDate = equipment.warrantyEndedAt;
|
||||
if (id != null && warrantyDate != null) {
|
||||
_warrantyDates[id] = warrantyDate;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
set transactionDate(DateTime value) {
|
||||
_transactionDate = value;
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
set selectedCompany(CompanyDto? value) {
|
||||
_selectedCompany = value;
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
Future<void> initialize() async {
|
||||
// 병렬로 회사 정보와 창고 캐시 로드
|
||||
await Future.wait([
|
||||
loadCompanies(),
|
||||
_loadWarehouseCache(),
|
||||
]);
|
||||
}
|
||||
|
||||
|
||||
Future<void> loadCompanies() async {
|
||||
print('[EquipmentOutboundController] loadCompanies called');
|
||||
_isLoadingCompanies = true;
|
||||
_companyError = null;
|
||||
notifyListeners();
|
||||
|
||||
try {
|
||||
print('[EquipmentOutboundController] Calling _companyRepository.getCompanies');
|
||||
final result = await _companyRepository.getCompanies(
|
||||
limit: 1000, // 모든 회사를 가져오기 위해 큰 값 설정
|
||||
);
|
||||
|
||||
result.fold(
|
||||
(failure) {
|
||||
print('[EquipmentOutboundController] Company loading failed: ${failure.message}');
|
||||
_companyError = failure.message;
|
||||
},
|
||||
(data) {
|
||||
print('[EquipmentOutboundController] Companies loaded successfully: ${data.items.length} companies');
|
||||
// Convert Company to CompanyDto - only use required fields
|
||||
_companies = data.items
|
||||
.map((company) => CompanyDto(
|
||||
id: company.id,
|
||||
name: company.name,
|
||||
contactName: '', // Default value for required field
|
||||
contactPhone: '', // Default value for required field
|
||||
contactEmail: '', // Default value for required field
|
||||
address: company.address.toString(),
|
||||
isCustomer: company.isCustomer,
|
||||
))
|
||||
.where((c) => c.isCustomer == true)
|
||||
.toList();
|
||||
print('[EquipmentOutboundController] Filtered customer companies: ${_companies.length}');
|
||||
},
|
||||
);
|
||||
} catch (e, stackTrace) {
|
||||
print('[EquipmentOutboundController] Exception in loadCompanies: $e');
|
||||
print('[EquipmentOutboundController] Stack trace: $stackTrace');
|
||||
_companyError = '회사 목록을 불러오는 중 오류가 발생했습니다: $e';
|
||||
} finally {
|
||||
_isLoadingCompanies = false;
|
||||
notifyListeners();
|
||||
print('[EquipmentOutboundController] loadCompanies completed');
|
||||
}
|
||||
}
|
||||
|
||||
/// 창고 캐시 로딩
|
||||
Future<void> _loadWarehouseCache() async {
|
||||
if (_warehouseCacheService.needsRefresh()) {
|
||||
_isLoadingWarehouseInfo = true;
|
||||
_warehouseError = null;
|
||||
notifyListeners();
|
||||
|
||||
try {
|
||||
final success = await _warehouseCacheService.loadCache();
|
||||
if (!success) {
|
||||
_warehouseError = _warehouseCacheService.lastError ?? '창고 정보 로딩 실패';
|
||||
}
|
||||
} catch (e) {
|
||||
_warehouseError = '창고 정보 로딩 중 오류: $e';
|
||||
} finally {
|
||||
_isLoadingWarehouseInfo = false;
|
||||
notifyListeners();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 장비의 현재 창고 정보 조회 (Stock Status 기반)
|
||||
///
|
||||
/// [equipment]: 조회할 장비
|
||||
///
|
||||
/// Returns: 창고명 (Stock Status 우선, Fallback으로 Equipment DTO 사용)
|
||||
String getEquipmentCurrentWarehouse(EquipmentDto equipment) {
|
||||
// 디버깅: 실제 Equipment DTO 데이터 출력
|
||||
print('[EquipmentOutboundController] Equipment ${equipment.id} 창고 정보:');
|
||||
print(' - warehousesId: ${equipment.warehousesId}');
|
||||
print(' - warehousesName: ${equipment.warehousesName}');
|
||||
print(' - serialNumber: ${equipment.serialNumber}');
|
||||
|
||||
if (_warehouseError != null) {
|
||||
print('[EquipmentOutboundController] Stock Status API 실패, Equipment DTO 사용');
|
||||
final fallbackName = equipment.warehousesName ?? '창고 미지정';
|
||||
print(' - Fallback 결과: $fallbackName');
|
||||
return fallbackName;
|
||||
}
|
||||
|
||||
// Primary: Stock Status API 기반 정보 사용
|
||||
final stockInfo = _warehouseCacheService.getEquipmentStock(equipment.id);
|
||||
print('[EquipmentOutboundController] Stock Status API 결과:');
|
||||
print(' - stockInfo 존재: ${stockInfo != null}');
|
||||
if (stockInfo != null) {
|
||||
print(' - stockInfo.warehouseName: ${stockInfo.warehouseName}');
|
||||
print(' - stockInfo.warehouseId: ${stockInfo.warehouseId}');
|
||||
}
|
||||
|
||||
final finalResult = stockInfo?.warehouseName ??
|
||||
equipment.warehousesName ??
|
||||
'입출고 이력 없음';
|
||||
print(' - 최종 결과: $finalResult');
|
||||
|
||||
return finalResult;
|
||||
}
|
||||
|
||||
/// 장비의 현재 창고 ID 조회
|
||||
int? getEquipmentCurrentWarehouseId(EquipmentDto equipment) {
|
||||
// Primary: Stock Status API 기반 정보 사용
|
||||
final stockInfo = _warehouseCacheService.getEquipmentStock(equipment.id);
|
||||
return stockInfo?.warehouseId ?? equipment.warehousesId;
|
||||
}
|
||||
|
||||
/// 장비의 재고 현황 정보 조회
|
||||
StockStatusDto? getEquipmentStockStatus(EquipmentDto equipment) {
|
||||
return _warehouseCacheService.getEquipmentStock(equipment.id);
|
||||
}
|
||||
|
||||
/// 출고 후 창고 캐시 갱신
|
||||
Future<void> _refreshWarehouseCache() async {
|
||||
print('[EquipmentOutboundController] 출고 완료 후 창고 캐시 갱신 시작...');
|
||||
|
||||
try {
|
||||
await _warehouseCacheService.refreshCache();
|
||||
print('[EquipmentOutboundController] 창고 캐시 갱신 완료');
|
||||
} catch (e) {
|
||||
print('[EquipmentOutboundController] 창고 캐시 갱신 실패: $e');
|
||||
// 갱신 실패해도 출고 프로세스는 성공으로 간주
|
||||
}
|
||||
}
|
||||
|
||||
Future<bool> processOutbound() async {
|
||||
print('[EquipmentOutboundController] processOutbound called');
|
||||
print('[EquipmentOutboundController] canSubmit: $canSubmit');
|
||||
print('[EquipmentOutboundController] selectedEquipments count: ${selectedEquipments.length}');
|
||||
print('[EquipmentOutboundController] selectedCompany: ${_selectedCompany?.name} (ID: ${_selectedCompany?.id})');
|
||||
print('[EquipmentOutboundController] API Base URL: ${_equipmentHistoryRepository.toString()}');
|
||||
|
||||
if (!canSubmit) {
|
||||
print('[EquipmentOutboundController] Cannot submit - validation failed');
|
||||
return false;
|
||||
}
|
||||
|
||||
_isLoading = true;
|
||||
_errorMessage = null;
|
||||
notifyListeners();
|
||||
|
||||
try {
|
||||
print('[EquipmentOutboundController] Starting outbound process for ${selectedEquipments.length} equipments');
|
||||
|
||||
// Process each selected equipment
|
||||
for (int i = 0; i < selectedEquipments.length; i++) {
|
||||
final equipment = selectedEquipments[i];
|
||||
|
||||
// 개선된 창고 정보 조회 (Stock Status API 우선)
|
||||
final currentWarehouseName = getEquipmentCurrentWarehouse(equipment);
|
||||
final currentWarehouseId = getEquipmentCurrentWarehouseId(equipment);
|
||||
|
||||
print('[EquipmentOutboundController] Processing equipment ${i+1}/${selectedEquipments.length}');
|
||||
print('[EquipmentOutboundController] Equipment ID: ${equipment.id}');
|
||||
print('[EquipmentOutboundController] Equipment Serial: ${equipment.serialNumber}');
|
||||
print('[EquipmentOutboundController] Current Warehouse (Stock Status): $currentWarehouseName (ID: $currentWarehouseId)');
|
||||
print('[EquipmentOutboundController] Original Warehouse (DTO): ${equipment.warehousesName} (ID: ${equipment.warehousesId})');
|
||||
|
||||
await _equipmentHistoryRepository.createStockOut(
|
||||
equipmentsId: equipment.id,
|
||||
warehousesId: currentWarehouseId ?? equipment.warehousesId, // 개선된 창고 정보 사용
|
||||
companyIds: _selectedCompany?.id != null ? [_selectedCompany!.id!] : null,
|
||||
quantity: 1,
|
||||
transactedAt: _transactionDate,
|
||||
remark: remarkController.text.isNotEmpty ? remarkController.text : null,
|
||||
);
|
||||
|
||||
print('[EquipmentOutboundController] Successfully processed equipment ${equipment.id}');
|
||||
}
|
||||
|
||||
print('[EquipmentOutboundController] All equipments processed successfully');
|
||||
|
||||
// 출고 완료 후 창고 캐시 갱신 (백그라운드에서 실행)
|
||||
unawaited(_refreshWarehouseCache());
|
||||
|
||||
return true;
|
||||
} catch (e, stackTrace) {
|
||||
print('[EquipmentOutboundController] ERROR during outbound process: $e');
|
||||
print('[EquipmentOutboundController] Stack trace: $stackTrace');
|
||||
_errorMessage = '출고 처리 중 오류가 발생했습니다: $e';
|
||||
notifyListeners();
|
||||
return false;
|
||||
} finally {
|
||||
_isLoading = false;
|
||||
notifyListeners();
|
||||
print('[EquipmentOutboundController] processOutbound completed');
|
||||
}
|
||||
}
|
||||
|
||||
String formatDate(DateTime date) {
|
||||
return DateFormat('yyyy-MM-dd').format(date);
|
||||
}
|
||||
|
||||
String formatPrice(int? price) {
|
||||
if (price == null) return '-';
|
||||
final formatter = NumberFormat('#,###');
|
||||
return '₩${formatter.format(price)}';
|
||||
}
|
||||
|
||||
DateTime? getWarrantyDate(int equipmentId) {
|
||||
return _warrantyDates[equipmentId];
|
||||
}
|
||||
|
||||
void updateWarrantyDate(int equipmentId, DateTime date) {
|
||||
_warrantyDates[equipmentId] = date;
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
remarkController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user