feat: V/R 유지보수 시스템 전환 및 대시보드 테이블 형태 완성
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

- 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:
JiWoong Sul
2025-09-05 14:33:20 +09:00
parent 2c20999025
commit 519e1883a3
46 changed files with 7804 additions and 1034 deletions

View File

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

View File

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

View File

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