From cddde574503ecf7bda35807d381e68ff1ec18d1d Mon Sep 17 00:00:00 2001 From: JiWoong Sul Date: Sat, 9 Aug 2025 02:17:16 +0900 Subject: [PATCH] =?UTF-8?q?feat:=20=EC=9E=A5=EB=B9=84=20=EA=B4=80=EB=A6=AC?= =?UTF-8?q?=20=EA=B8=B0=EB=8A=A5=20=EA=B0=95=ED=99=94=20=EB=B0=8F=20?= =?UTF-8?q?=EC=9D=B4=EB=A0=A5=20=EC=B6=94=EC=A0=81=20=EA=B0=9C=EC=84=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - EquipmentHistoryDto 모델 확장 (상세 정보 추가) - 장비 이력 화면 UI/UX 개선 - 장비 입고 폼 검증 로직 강화 - 테스트 이력 화면 추가 - API 응답 처리 개선 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude --- .../remote/equipment_remote_datasource.dart | 67 +++++- .../equipment/equipment_history_dto.dart | 22 +- .../equipment_history_dto.freezed.dart | 118 +++++---- .../equipment/equipment_history_dto.g.dart | 46 ++-- .../equipment_in_form_controller.dart | 111 ++++++--- .../equipment/equipment_history_screen.dart | 194 +++++++++++---- lib/screens/equipment/equipment_in_form.dart | 223 ++++++++++++------ .../equipment/equipment_list_redesign.dart | 32 +-- .../equipment/test_history_screen.dart | 183 ++++++++++++++ 9 files changed, 738 insertions(+), 258 deletions(-) create mode 100644 lib/screens/equipment/test_history_screen.dart diff --git a/lib/data/datasources/remote/equipment_remote_datasource.dart b/lib/data/datasources/remote/equipment_remote_datasource.dart index d76dd5e..a91b797 100644 --- a/lib/data/datasources/remote/equipment_remote_datasource.dart +++ b/lib/data/datasources/remote/equipment_remote_datasource.dart @@ -224,18 +224,73 @@ class EquipmentRemoteDataSourceImpl implements EquipmentRemoteDataSource { 'per_page': perPage, }; + print('[API] Requesting equipment history: ${ApiEndpoints.equipment}/$equipmentId/history'); + print('[API] Query params: $queryParams'); + final response = await _apiClient.get( '${ApiEndpoints.equipment}/$equipmentId/history', queryParameters: queryParams, ); - if (response.data['success'] == true && response.data['data'] != null) { - final List data = response.data['data']; - return data.map((json) => EquipmentHistoryDto.fromJson(json)).toList(); + print('[API] Response status: ${response.statusCode}'); + print('[API] Response data type: ${response.data.runtimeType}'); + print('[API] Full response: ${response.data}'); + + // API 응답 구조 확인 + if (response.data == null) { + print('[API ERROR] Response data is null'); + throw ServerException(message: 'Empty response from server'); + } + + // 응답이 Map인지 확인 + if (response.data is! Map) { + print('[API ERROR] Response is not a Map: ${response.data.runtimeType}'); + throw ServerException(message: 'Invalid response format'); + } + + // success 필드 확인 + final success = response.data['success']; + print('[API] Success field: $success'); + + if (success == true) { + final responseData = response.data['data']; + print('[API] Data field type: ${responseData?.runtimeType}'); + + if (responseData == null) { + print('[API] No data field, returning empty list'); + return []; + } + + if (responseData is! List) { + print('[API ERROR] Data is not a List: ${responseData.runtimeType}'); + throw ServerException(message: 'Invalid data format'); + } + + final List data = responseData; + print('[API] History data count: ${data.length}'); + + if (data.isEmpty) { + print('[API] Empty history data'); + return []; + } + + print('[API] First history item: ${data.first}'); + + try { + final histories = data.map((json) { + print('[API] Parsing history item: $json'); + return EquipmentHistoryDto.fromJson(json); + }).toList(); + print('[API] Successfully parsed ${histories.length} history items'); + return histories; + } catch (e) { + print('[API ERROR] Failed to parse history data: $e'); + throw ServerException(message: 'Failed to parse history data: $e'); + } } else { - throw ServerException( - message: response.data['message'] ?? 'Failed to fetch equipment history', - ); + final errorMessage = response.data['message'] ?? response.data['error'] ?? 'Failed to fetch equipment history'; + print('[API ERROR] Request failed: $errorMessage'); + throw ServerException(message: errorMessage); } } on DioException catch (e) { throw ServerException( diff --git a/lib/data/models/equipment/equipment_history_dto.dart b/lib/data/models/equipment/equipment_history_dto.dart index b762a0a..0151066 100644 --- a/lib/data/models/equipment/equipment_history_dto.dart +++ b/lib/data/models/equipment/equipment_history_dto.dart @@ -7,17 +7,17 @@ part 'equipment_history_dto.g.dart'; class EquipmentHistoryDto with _$EquipmentHistoryDto { const factory EquipmentHistoryDto({ required int id, - required int equipmentId, - required String transactionType, + @JsonKey(name: 'equipment_id') required int equipmentId, + @JsonKey(name: 'transaction_type') required String transactionType, required int quantity, - required DateTime transactionDate, + @JsonKey(name: 'transaction_date') required DateTime transactionDate, String? remarks, - int? createdBy, - int? userId, - required DateTime createdAt, + @JsonKey(name: 'created_by') int? createdBy, + @JsonKey(name: 'user_id') int? userId, + @JsonKey(name: 'created_at') required DateTime createdAt, // 추가 정보 - String? userName, - String? performedBy, + @JsonKey(name: 'user_name') String? userName, + @JsonKey(name: 'performed_by') String? performedBy, }) = _EquipmentHistoryDto; factory EquipmentHistoryDto.fromJson(Map json) => @@ -27,11 +27,11 @@ class EquipmentHistoryDto with _$EquipmentHistoryDto { @freezed class CreateHistoryRequest with _$CreateHistoryRequest { const factory CreateHistoryRequest({ - required String transactionType, + @JsonKey(name: 'transaction_type') required String transactionType, required int quantity, - DateTime? transactionDate, + @JsonKey(name: 'transaction_date') DateTime? transactionDate, String? remarks, - int? userId, + @JsonKey(name: 'user_id') int? userId, }) = _CreateHistoryRequest; factory CreateHistoryRequest.fromJson(Map json) => diff --git a/lib/data/models/equipment/equipment_history_dto.freezed.dart b/lib/data/models/equipment/equipment_history_dto.freezed.dart index 763871c..361cb72 100644 --- a/lib/data/models/equipment/equipment_history_dto.freezed.dart +++ b/lib/data/models/equipment/equipment_history_dto.freezed.dart @@ -21,15 +21,23 @@ EquipmentHistoryDto _$EquipmentHistoryDtoFromJson(Map json) { /// @nodoc mixin _$EquipmentHistoryDto { int get id => throw _privateConstructorUsedError; + @JsonKey(name: 'equipment_id') int get equipmentId => throw _privateConstructorUsedError; + @JsonKey(name: 'transaction_type') String get transactionType => throw _privateConstructorUsedError; int get quantity => throw _privateConstructorUsedError; + @JsonKey(name: 'transaction_date') DateTime get transactionDate => throw _privateConstructorUsedError; String? get remarks => throw _privateConstructorUsedError; + @JsonKey(name: 'created_by') int? get createdBy => throw _privateConstructorUsedError; + @JsonKey(name: 'user_id') int? get userId => throw _privateConstructorUsedError; + @JsonKey(name: 'created_at') DateTime get createdAt => throw _privateConstructorUsedError; // 추가 정보 + @JsonKey(name: 'user_name') String? get userName => throw _privateConstructorUsedError; + @JsonKey(name: 'performed_by') String? get performedBy => throw _privateConstructorUsedError; /// Serializes this EquipmentHistoryDto to a JSON map. @@ -50,16 +58,16 @@ abstract class $EquipmentHistoryDtoCopyWith<$Res> { @useResult $Res call( {int id, - int equipmentId, - String transactionType, + @JsonKey(name: 'equipment_id') int equipmentId, + @JsonKey(name: 'transaction_type') String transactionType, int quantity, - DateTime transactionDate, + @JsonKey(name: 'transaction_date') DateTime transactionDate, String? remarks, - int? createdBy, - int? userId, - DateTime createdAt, - String? userName, - String? performedBy}); + @JsonKey(name: 'created_by') int? createdBy, + @JsonKey(name: 'user_id') int? userId, + @JsonKey(name: 'created_at') DateTime createdAt, + @JsonKey(name: 'user_name') String? userName, + @JsonKey(name: 'performed_by') String? performedBy}); } /// @nodoc @@ -148,16 +156,16 @@ abstract class _$$EquipmentHistoryDtoImplCopyWith<$Res> @useResult $Res call( {int id, - int equipmentId, - String transactionType, + @JsonKey(name: 'equipment_id') int equipmentId, + @JsonKey(name: 'transaction_type') String transactionType, int quantity, - DateTime transactionDate, + @JsonKey(name: 'transaction_date') DateTime transactionDate, String? remarks, - int? createdBy, - int? userId, - DateTime createdAt, - String? userName, - String? performedBy}); + @JsonKey(name: 'created_by') int? createdBy, + @JsonKey(name: 'user_id') int? userId, + @JsonKey(name: 'created_at') DateTime createdAt, + @JsonKey(name: 'user_name') String? userName, + @JsonKey(name: 'performed_by') String? performedBy}); } /// @nodoc @@ -239,16 +247,16 @@ class __$$EquipmentHistoryDtoImplCopyWithImpl<$Res> class _$EquipmentHistoryDtoImpl implements _EquipmentHistoryDto { const _$EquipmentHistoryDtoImpl( {required this.id, - required this.equipmentId, - required this.transactionType, + @JsonKey(name: 'equipment_id') required this.equipmentId, + @JsonKey(name: 'transaction_type') required this.transactionType, required this.quantity, - required this.transactionDate, + @JsonKey(name: 'transaction_date') required this.transactionDate, this.remarks, - this.createdBy, - this.userId, - required this.createdAt, - this.userName, - this.performedBy}); + @JsonKey(name: 'created_by') this.createdBy, + @JsonKey(name: 'user_id') this.userId, + @JsonKey(name: 'created_at') required this.createdAt, + @JsonKey(name: 'user_name') this.userName, + @JsonKey(name: 'performed_by') this.performedBy}); factory _$EquipmentHistoryDtoImpl.fromJson(Map json) => _$$EquipmentHistoryDtoImplFromJson(json); @@ -256,25 +264,33 @@ class _$EquipmentHistoryDtoImpl implements _EquipmentHistoryDto { @override final int id; @override + @JsonKey(name: 'equipment_id') final int equipmentId; @override + @JsonKey(name: 'transaction_type') final String transactionType; @override final int quantity; @override + @JsonKey(name: 'transaction_date') final DateTime transactionDate; @override final String? remarks; @override + @JsonKey(name: 'created_by') final int? createdBy; @override + @JsonKey(name: 'user_id') final int? userId; @override + @JsonKey(name: 'created_at') final DateTime createdAt; // 추가 정보 @override + @JsonKey(name: 'user_name') final String? userName; @override + @JsonKey(name: 'performed_by') final String? performedBy; @override @@ -344,15 +360,17 @@ class _$EquipmentHistoryDtoImpl implements _EquipmentHistoryDto { abstract class _EquipmentHistoryDto implements EquipmentHistoryDto { const factory _EquipmentHistoryDto( {required final int id, - required final int equipmentId, - required final String transactionType, + @JsonKey(name: 'equipment_id') required final int equipmentId, + @JsonKey(name: 'transaction_type') required final String transactionType, required final int quantity, + @JsonKey(name: 'transaction_date') required final DateTime transactionDate, final String? remarks, - final int? createdBy, - final int? userId, - required final DateTime createdAt, - final String? userName, + @JsonKey(name: 'created_by') final int? createdBy, + @JsonKey(name: 'user_id') final int? userId, + @JsonKey(name: 'created_at') required final DateTime createdAt, + @JsonKey(name: 'user_name') final String? userName, + @JsonKey(name: 'performed_by') final String? performedBy}) = _$EquipmentHistoryDtoImpl; factory _EquipmentHistoryDto.fromJson(Map json) = @@ -361,24 +379,32 @@ abstract class _EquipmentHistoryDto implements EquipmentHistoryDto { @override int get id; @override + @JsonKey(name: 'equipment_id') int get equipmentId; @override + @JsonKey(name: 'transaction_type') String get transactionType; @override int get quantity; @override + @JsonKey(name: 'transaction_date') DateTime get transactionDate; @override String? get remarks; @override + @JsonKey(name: 'created_by') int? get createdBy; @override + @JsonKey(name: 'user_id') int? get userId; @override + @JsonKey(name: 'created_at') DateTime get createdAt; // 추가 정보 @override + @JsonKey(name: 'user_name') String? get userName; @override + @JsonKey(name: 'performed_by') String? get performedBy; /// Create a copy of EquipmentHistoryDto @@ -395,10 +421,13 @@ CreateHistoryRequest _$CreateHistoryRequestFromJson(Map json) { /// @nodoc mixin _$CreateHistoryRequest { + @JsonKey(name: 'transaction_type') String get transactionType => throw _privateConstructorUsedError; int get quantity => throw _privateConstructorUsedError; + @JsonKey(name: 'transaction_date') DateTime? get transactionDate => throw _privateConstructorUsedError; String? get remarks => throw _privateConstructorUsedError; + @JsonKey(name: 'user_id') int? get userId => throw _privateConstructorUsedError; /// Serializes this CreateHistoryRequest to a JSON map. @@ -418,11 +447,11 @@ abstract class $CreateHistoryRequestCopyWith<$Res> { _$CreateHistoryRequestCopyWithImpl<$Res, CreateHistoryRequest>; @useResult $Res call( - {String transactionType, + {@JsonKey(name: 'transaction_type') String transactionType, int quantity, - DateTime? transactionDate, + @JsonKey(name: 'transaction_date') DateTime? transactionDate, String? remarks, - int? userId}); + @JsonKey(name: 'user_id') int? userId}); } /// @nodoc @@ -481,11 +510,11 @@ abstract class _$$CreateHistoryRequestImplCopyWith<$Res> @override @useResult $Res call( - {String transactionType, + {@JsonKey(name: 'transaction_type') String transactionType, int quantity, - DateTime? transactionDate, + @JsonKey(name: 'transaction_date') DateTime? transactionDate, String? remarks, - int? userId}); + @JsonKey(name: 'user_id') int? userId}); } /// @nodoc @@ -536,24 +565,27 @@ class __$$CreateHistoryRequestImplCopyWithImpl<$Res> @JsonSerializable() class _$CreateHistoryRequestImpl implements _CreateHistoryRequest { const _$CreateHistoryRequestImpl( - {required this.transactionType, + {@JsonKey(name: 'transaction_type') required this.transactionType, required this.quantity, - this.transactionDate, + @JsonKey(name: 'transaction_date') this.transactionDate, this.remarks, - this.userId}); + @JsonKey(name: 'user_id') this.userId}); factory _$CreateHistoryRequestImpl.fromJson(Map json) => _$$CreateHistoryRequestImplFromJson(json); @override + @JsonKey(name: 'transaction_type') final String transactionType; @override final int quantity; @override + @JsonKey(name: 'transaction_date') final DateTime? transactionDate; @override final String? remarks; @override + @JsonKey(name: 'user_id') final int? userId; @override @@ -601,24 +633,28 @@ class _$CreateHistoryRequestImpl implements _CreateHistoryRequest { abstract class _CreateHistoryRequest implements CreateHistoryRequest { const factory _CreateHistoryRequest( - {required final String transactionType, + {@JsonKey(name: 'transaction_type') required final String transactionType, required final int quantity, - final DateTime? transactionDate, + @JsonKey(name: 'transaction_date') final DateTime? transactionDate, final String? remarks, + @JsonKey(name: 'user_id') final int? userId}) = _$CreateHistoryRequestImpl; factory _CreateHistoryRequest.fromJson(Map json) = _$CreateHistoryRequestImpl.fromJson; @override + @JsonKey(name: 'transaction_type') String get transactionType; @override int get quantity; @override + @JsonKey(name: 'transaction_date') DateTime? get transactionDate; @override String? get remarks; @override + @JsonKey(name: 'user_id') int? get userId; /// Create a copy of CreateHistoryRequest diff --git a/lib/data/models/equipment/equipment_history_dto.g.dart b/lib/data/models/equipment/equipment_history_dto.g.dart index 60697ae..9a9280a 100644 --- a/lib/data/models/equipment/equipment_history_dto.g.dart +++ b/lib/data/models/equipment/equipment_history_dto.g.dart @@ -10,52 +10,52 @@ _$EquipmentHistoryDtoImpl _$$EquipmentHistoryDtoImplFromJson( Map json) => _$EquipmentHistoryDtoImpl( id: (json['id'] as num).toInt(), - equipmentId: (json['equipmentId'] as num).toInt(), - transactionType: json['transactionType'] as String, + equipmentId: (json['equipment_id'] as num).toInt(), + transactionType: json['transaction_type'] as String, quantity: (json['quantity'] as num).toInt(), - transactionDate: DateTime.parse(json['transactionDate'] as String), + transactionDate: DateTime.parse(json['transaction_date'] as String), remarks: json['remarks'] as String?, - createdBy: (json['createdBy'] as num?)?.toInt(), - userId: (json['userId'] as num?)?.toInt(), - createdAt: DateTime.parse(json['createdAt'] as String), - userName: json['userName'] as String?, - performedBy: json['performedBy'] as String?, + createdBy: (json['created_by'] as num?)?.toInt(), + userId: (json['user_id'] as num?)?.toInt(), + createdAt: DateTime.parse(json['created_at'] as String), + userName: json['user_name'] as String?, + performedBy: json['performed_by'] as String?, ); Map _$$EquipmentHistoryDtoImplToJson( _$EquipmentHistoryDtoImpl instance) => { 'id': instance.id, - 'equipmentId': instance.equipmentId, - 'transactionType': instance.transactionType, + 'equipment_id': instance.equipmentId, + 'transaction_type': instance.transactionType, 'quantity': instance.quantity, - 'transactionDate': instance.transactionDate.toIso8601String(), + 'transaction_date': instance.transactionDate.toIso8601String(), 'remarks': instance.remarks, - 'createdBy': instance.createdBy, - 'userId': instance.userId, - 'createdAt': instance.createdAt.toIso8601String(), - 'userName': instance.userName, - 'performedBy': instance.performedBy, + 'created_by': instance.createdBy, + 'user_id': instance.userId, + 'created_at': instance.createdAt.toIso8601String(), + 'user_name': instance.userName, + 'performed_by': instance.performedBy, }; _$CreateHistoryRequestImpl _$$CreateHistoryRequestImplFromJson( Map json) => _$CreateHistoryRequestImpl( - transactionType: json['transactionType'] as String, + transactionType: json['transaction_type'] as String, quantity: (json['quantity'] as num).toInt(), - transactionDate: json['transactionDate'] == null + transactionDate: json['transaction_date'] == null ? null - : DateTime.parse(json['transactionDate'] as String), + : DateTime.parse(json['transaction_date'] as String), remarks: json['remarks'] as String?, - userId: (json['userId'] as num?)?.toInt(), + userId: (json['user_id'] as num?)?.toInt(), ); Map _$$CreateHistoryRequestImplToJson( _$CreateHistoryRequestImpl instance) => { - 'transactionType': instance.transactionType, + 'transaction_type': instance.transactionType, 'quantity': instance.quantity, - 'transactionDate': instance.transactionDate?.toIso8601String(), + 'transaction_date': instance.transactionDate?.toIso8601String(), 'remarks': instance.remarks, - 'userId': instance.userId, + 'user_id': instance.userId, }; diff --git a/lib/screens/equipment/controllers/equipment_in_form_controller.dart b/lib/screens/equipment/controllers/equipment_in_form_controller.dart index 2733c4b..1809f85 100644 --- a/lib/screens/equipment/controllers/equipment_in_form_controller.dart +++ b/lib/screens/equipment/controllers/equipment_in_form_controller.dart @@ -18,7 +18,8 @@ class EquipmentInFormController extends ChangeNotifier { final EquipmentService _equipmentService = GetIt.instance(); final WarehouseService _warehouseService = GetIt.instance(); final CompanyService _companyService = GetIt.instance(); - final int? equipmentInId; + final int? equipmentInId; // 실제로는 장비 ID (입고 ID가 아님) + int? actualEquipmentId; // API 호출용 실제 장비 ID bool _isLoading = false; String? _error; @@ -85,9 +86,13 @@ class EquipmentInFormController extends ChangeNotifier { _loadWarehouseLocations(); _loadPartnerCompanies(); _loadWarrantyLicenses(); - if (isEditMode) { - _loadEquipmentIn(); - } + // 수정 모드일 때 초기 데이터 로드는 initializeForEdit() 메서드로 이동 + } + + // 수정 모드 초기화 (외부에서 호출) + Future initializeForEdit() async { + if (!isEditMode || equipmentInId == null) return; + await _loadEquipmentIn(); } // 제조사 목록 로드 @@ -185,7 +190,7 @@ class EquipmentInFormController extends ChangeNotifier { } // 기존 데이터 로드(수정 모드) - void _loadEquipmentIn() async { + Future _loadEquipmentIn() async { if (equipmentInId == null) return; _isLoading = true; @@ -194,50 +199,67 @@ class EquipmentInFormController extends ChangeNotifier { try { if (_useApi) { - // API에서 장비 정보 로드 - // 현재는 장비 정보만 가져올 수 있으므로, 일단 Mock 데이터와 병용 - final equipmentIn = dataService.getEquipmentInById(equipmentInId!); - if (equipmentIn != null && equipmentIn.equipment.id != null) { - try { - // API에서 최신 장비 정보 가져오기 - final equipment = await _equipmentService.getEquipment(equipmentIn.equipment.id!); - manufacturer = equipment.manufacturer; - name = equipment.name; - category = equipment.category; - subCategory = equipment.subCategory; - subSubCategory = equipment.subSubCategory; - serialNumber = equipment.serialNumber ?? ''; - barcode = equipment.barcode ?? ''; - quantity = equipment.quantity; - remarkController.text = equipment.remark ?? ''; - hasSerialNumber = serialNumber.isNotEmpty; - - // 워런티 정보 - warrantyLicense = equipment.warrantyLicense; - warrantyStartDate = equipment.warrantyStartDate ?? DateTime.now(); - warrantyEndDate = equipment.warrantyEndDate ?? DateTime.now().add(const Duration(days: 365)); - - // 입고 관련 정보는 아직 Mock 데이터 사용 - inDate = equipmentIn.inDate; - equipmentType = equipmentIn.type; - warehouseLocation = equipmentIn.warehouseLocation; - partnerCompany = equipmentIn.partnerCompany; - } catch (e) { - // API 실패 시 Mock 데이터 사용 + // equipmentInId는 실제로 장비 ID임 (입고 ID가 아님) + actualEquipmentId = equipmentInId; + + try { + // API에서 장비 정보 가져오기 + DebugLogger.log('장비 정보 로드 시작', tag: 'EQUIPMENT_IN', data: { + 'equipmentId': actualEquipmentId, + }); + + final equipment = await _equipmentService.getEquipmentDetail(actualEquipmentId!); + + DebugLogger.log('장비 정보 로드 성공', tag: 'EQUIPMENT_IN', data: { + 'equipment': equipment.toJson(), + }); + + // 장비 정보 설정 + manufacturer = equipment.manufacturer; + name = equipment.name; + category = equipment.category; + subCategory = equipment.subCategory; + subSubCategory = equipment.subSubCategory; + serialNumber = equipment.serialNumber ?? ''; + barcode = equipment.barcode ?? ''; + quantity = equipment.quantity; + remarkController.text = equipment.remark ?? ''; + hasSerialNumber = serialNumber.isNotEmpty; + + // 워런티 정보 + warrantyLicense = equipment.warrantyLicense; + warrantyStartDate = equipment.warrantyStartDate ?? DateTime.now(); + warrantyEndDate = equipment.warrantyEndDate ?? DateTime.now().add(const Duration(days: 365)); + + // 입고 관련 정보는 현재 API에서 제공하지 않으므로 기본값 사용 + inDate = equipment.inDate ?? DateTime.now(); + equipmentType = EquipmentType.new_; + // 창고 위치와 파트너사는 사용자가 수정 시 입력 + + } catch (e) { + DebugLogger.logError('장비 정보 로드 실패', error: e); + // API 실패 시 Mock 데이터 시도 + final equipmentIn = dataService.getEquipmentInById(equipmentInId!); + if (equipmentIn != null) { + actualEquipmentId = equipmentIn.equipment.id; _loadFromMockData(equipmentIn); + } else { + throw ServerFailure(message: '장비 정보를 찾을 수 없습니다.'); } - } else if (equipmentIn != null) { - _loadFromMockData(equipmentIn); } } else { // Mock 데이터 사용 final equipmentIn = dataService.getEquipmentInById(equipmentInId!); if (equipmentIn != null) { + actualEquipmentId = equipmentIn.equipment.id; _loadFromMockData(equipmentIn); + } else { + throw ServerFailure(message: '장비 정보를 찾을 수 없습니다.'); } } } catch (e) { - _error = 'Failed to load equipment: $e'; + _error = '장비 정보를 불러오는데 실패했습니다: $e'; + DebugLogger.logError('장비 로드 실패', error: e); } finally { _isLoading = false; notifyListeners(); @@ -356,7 +378,18 @@ class EquipmentInFormController extends ChangeNotifier { // API 호출 if (isEditMode) { // 수정 모드 - API로 장비 정보 업데이트 - await _equipmentService.updateEquipment(equipmentInId!, equipment); + if (actualEquipmentId == null) { + throw ServerFailure(message: '장비 ID가 없습니다.'); + } + + DebugLogger.log('장비 정보 업데이트 시작', tag: 'EQUIPMENT_IN', data: { + 'equipmentId': actualEquipmentId, + 'data': equipment.toJson(), + }); + + await _equipmentService.updateEquipment(actualEquipmentId!, equipment); + + DebugLogger.log('장비 정보 업데이트 성공', tag: 'EQUIPMENT_IN'); } else { // 생성 모드 try { diff --git a/lib/screens/equipment/equipment_history_screen.dart b/lib/screens/equipment/equipment_history_screen.dart index 2166592..02ad1fc 100644 --- a/lib/screens/equipment/equipment_history_screen.dart +++ b/lib/screens/equipment/equipment_history_screen.dart @@ -24,7 +24,8 @@ class EquipmentHistoryScreen extends StatefulWidget { class _EquipmentHistoryScreenState extends State { final EquipmentService _equipmentService = GetIt.instance(); List _histories = []; - bool _isLoading = true; + bool _isLoading = false; // 초기값을 false로 변경 + bool _isInitialLoad = true; // 초기 로딩 상태 추가 String? _error; int _currentPage = 1; final int _perPage = 20; @@ -34,7 +35,8 @@ class _EquipmentHistoryScreenState extends State { @override void initState() { super.initState(); - _loadHistory(); + print('[INIT] EquipmentHistoryScreen initialized for equipment ${widget.equipmentId}'); + _loadHistory(isInitialLoad: true); _scrollController.addListener(_onScroll); } @@ -50,27 +52,62 @@ class _EquipmentHistoryScreenState extends State { } } - Future _loadHistory({bool isRefresh = false}) async { + Future _loadHistory({bool isRefresh = false, bool isInitialLoad = false}) async { + print('[_loadHistory] Called - isRefresh: $isRefresh, isInitialLoad: $isInitialLoad, _isLoading: $_isLoading'); + if (isRefresh) { _currentPage = 1; _hasMore = true; _histories.clear(); } - if (!_hasMore || (!isRefresh && _isLoading)) return; + // 초기 로딩이 아닌 경우에만 중복 호출 방지 + if (!isInitialLoad && (!_hasMore || (!isRefresh && _isLoading))) { + print('[_loadHistory] Skipping - hasMore: $_hasMore, isLoading: $_isLoading'); + return; + } + if (!mounted) return; + setState(() { _isLoading = true; _error = null; + if (isInitialLoad) _isInitialLoad = false; }); try { + print('[DEBUG] ==== STARTING HISTORY LOAD ===='); + print('[DEBUG] Equipment ID: ${widget.equipmentId}'); + print('[DEBUG] Equipment Name: ${widget.equipmentName}'); + print('[DEBUG] Page: $_currentPage, PerPage: $_perPage'); + print('[DEBUG] Current time: ${DateTime.now()}'); + + // 타임아웃 설정 final histories = await _equipmentService.getEquipmentHistory( widget.equipmentId, page: _currentPage, perPage: _perPage, + ).timeout( + const Duration(seconds: 10), + onTimeout: () { + print('[ERROR] API call timeout after 10 seconds'); + throw Exception('API 호출 시간 초과 (10초)'); + }, ); + print('[DEBUG] API call completed successfully'); + print('[DEBUG] Received ${histories.length} history records'); + if (histories.isNotEmpty) { + print('[DEBUG] First history record: ${histories.first.toJson()}'); + } else { + print('[DEBUG] No history records found'); + } + + if (!mounted) { + print('[WARNING] Widget not mounted, skipping setState'); + return; + } + setState(() { if (isRefresh) { _histories = histories; @@ -80,17 +117,25 @@ class _EquipmentHistoryScreenState extends State { _hasMore = histories.length == _perPage; if (_hasMore) _currentPage++; _isLoading = false; + print('[DEBUG] State updated - Loading: false, Histories count: ${_histories.length}'); }); } on Failure catch (e) { + print('[ERROR] Failure loading history: ${e.message}'); + if (!mounted) return; setState(() { _error = e.message; _isLoading = false; }); - } catch (e) { + } catch (e, stackTrace) { + print('[ERROR] Unexpected error loading history: $e'); + print('[ERROR] Stack trace: $stackTrace'); + if (!mounted) return; setState(() { _error = '이력을 불러오는 중 오류가 발생했습니다: $e'; _isLoading = false; }); + } finally { + print('[DEBUG] ==== HISTORY LOAD COMPLETED ===='); } } @@ -183,6 +228,8 @@ class _EquipmentHistoryScreenState extends State { @override Widget build(BuildContext context) { + print('[BUILD] EquipmentHistoryScreen - Loading: $_isLoading, InitialLoad: $_isInitialLoad, Error: $_error, Histories: ${_histories.length}'); + return Scaffold( appBar: AppBar( title: Column( @@ -195,51 +242,110 @@ class _EquipmentHistoryScreenState extends State { ), ], ), + actions: [ + IconButton( + icon: const Icon(Icons.refresh), + onPressed: () => _loadHistory(isRefresh: true), + tooltip: '새로고침', + ), + ], ), - body: _isLoading && _histories.isEmpty - ? const Center(child: CircularProgressIndicator()) - : _error != null && _histories.isEmpty - ? Center( - child: Column( - mainAxisAlignment: MainAxisAlignment.center, + body: Builder( + builder: (context) { + print('[UI] Building body - Loading: $_isLoading, InitialLoad: $_isInitialLoad, Error: $_error, Histories: ${_histories.length}'); + + // 초기 로딩 또는 일반 로딩 중 + if ((_isInitialLoad || _isLoading) && _histories.isEmpty) { + return Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + const CircularProgressIndicator(), + const SizedBox(height: 16), + Text('장비 ID ${widget.equipmentId}의 이력을 불러오는 중...'), + const SizedBox(height: 8), + TextButton( + onPressed: () { + print('[USER] Cancel loading clicked'); + setState(() { + _isLoading = false; + _error = '사용자가 로딩을 취소했습니다'; + }); + }, + child: const Text('로딩 취소'), + ), + ], + ), + ); + } + + if (_error != null && _histories.isEmpty) { + return Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + const Icon(Icons.error_outline, size: 48, color: Colors.red), + const SizedBox(height: 16), + Text('오류 발생', style: Theme.of(context).textTheme.titleLarge), + const SizedBox(height: 8), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 32), + child: Text( + _error!, + textAlign: TextAlign.center, + style: const TextStyle(color: Colors.red), + ), + ), + const SizedBox(height: 16), + ElevatedButton.icon( + icon: const Icon(Icons.refresh), + label: const Text('다시 시도'), + onPressed: () => _loadHistory(isRefresh: true), + ), + ], + ), + ); + } + + return RefreshIndicator( + onRefresh: () => _loadHistory(isRefresh: true), + child: _histories.isEmpty + ? ListView( children: [ - Text(_error!, style: const TextStyle(color: Colors.red)), + const SizedBox(height: 200), + const Icon(Icons.history, size: 48, color: Colors.grey), const SizedBox(height: 16), - ElevatedButton( - onPressed: () => _loadHistory(isRefresh: true), - child: const Text('다시 시도'), + const Center( + child: Text( + '이력이 없습니다.', + style: TextStyle(fontSize: 16, color: Colors.grey), + ), + ), + const SizedBox(height: 8), + Center( + child: TextButton( + onPressed: () => _loadHistory(isRefresh: true), + child: const Text('새로고침'), + ), ), ], + ) + : ListView.builder( + controller: _scrollController, + itemCount: _histories.length + (_hasMore ? 1 : 0), + itemBuilder: (context, index) { + if (index == _histories.length) { + return const Padding( + padding: EdgeInsets.all(16.0), + child: Center(child: CircularProgressIndicator()), + ); + } + return _buildHistoryItem(_histories[index]); + }, ), - ) - : RefreshIndicator( - onRefresh: () => _loadHistory(isRefresh: true), - child: _histories.isEmpty - ? ListView( - children: const [ - SizedBox(height: 200), - Center( - child: Text( - '이력이 없습니다.', - style: TextStyle(fontSize: 16, color: Colors.grey), - ), - ), - ], - ) - : ListView.builder( - controller: _scrollController, - itemCount: _histories.length + (_hasMore ? 1 : 0), - itemBuilder: (context, index) { - if (index == _histories.length) { - return const Padding( - padding: EdgeInsets.all(16.0), - child: Center(child: CircularProgressIndicator()), - ); - } - return _buildHistoryItem(_histories[index]); - }, - ), - ), + ); + }, + ), ); } } \ No newline at end of file diff --git a/lib/screens/equipment/equipment_in_form.dart b/lib/screens/equipment/equipment_in_form.dart index 6b3ca56..a950585 100644 --- a/lib/screens/equipment/equipment_in_form.dart +++ b/lib/screens/equipment/equipment_in_form.dart @@ -2,15 +2,16 @@ import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:provider/provider.dart'; // import 'package:superport/models/equipment_unified_model.dart'; -import 'package:superport/screens/common/custom_widgets.dart'; +// import 'package:superport/screens/common/custom_widgets.dart' hide FormFieldWrapper; import 'package:superport/screens/common/theme_tailwind.dart'; +import 'package:superport/screens/common/templates/form_layout_template.dart'; import 'package:superport/services/mock_data_service.dart'; import 'package:superport/utils/constants.dart'; // import 'package:flutter_localizations/flutter_localizations.dart'; // import 'package:superport/screens/equipment/widgets/autocomplete_text_field.dart'; import 'controllers/equipment_in_form_controller.dart'; // import 'package:superport/screens/common/widgets/category_autocomplete_field.dart'; -import 'package:superport/screens/common/widgets/autocomplete_dropdown_field.dart'; +// import 'package:superport/screens/common/widgets/autocomplete_dropdown_field.dart'; import 'package:superport/screens/common/widgets/remark_input.dart'; class EquipmentInFormScreen extends StatefulWidget { @@ -66,6 +67,13 @@ class _EquipmentInFormScreenState extends State { OverlayEntry? _subSubCategoryOverlayEntry; final FocusNode _subSubCategoryFocusNode = FocusNode(); late TextEditingController _subSubCategoryController; + + // 추가 필드 컨트롤러들 + late TextEditingController _nameController; + late TextEditingController _serialNumberController; + late TextEditingController _barcodeController; + late TextEditingController _quantityController; + late TextEditingController _warrantyCodeController; // 프로그램적 입력란 변경 여부 플래그 bool _isProgrammaticPartnerChange = false; @@ -176,6 +184,16 @@ class _EquipmentInFormScreenState extends State { dataService: MockDataService(), equipmentInId: widget.equipmentInId, ); + + // 수정 모드일 때 데이터 로드 + if (_controller.isEditMode) { + WidgetsBinding.instance.addPostFrameCallback((_) async { + await _controller.initializeForEdit(); + // 데이터 로드 후 텍스트 컨트롤러 업데이트 + _updateTextControllers(); + }); + } + _manufacturerFocusNode = FocusNode(); _nameFieldFocusNode = FocusNode(); _partnerController = TextEditingController( @@ -202,6 +220,13 @@ class _EquipmentInFormScreenState extends State { _subSubCategoryController = TextEditingController( text: _controller.subSubCategory, ); + + // 추가 필드 컨트롤러 초기화 + _nameController = TextEditingController(text: _controller.name); + _serialNumberController = TextEditingController(text: _controller.serialNumber); + _barcodeController = TextEditingController(text: _controller.barcode); + _quantityController = TextEditingController(text: _controller.quantity.toString()); + _warrantyCodeController = TextEditingController(text: _controller.warrantyCode ?? ''); // 포커스 변경 리스너 추가 _partnerFocusNode.addListener(_onPartnerFocusChange); @@ -213,6 +238,24 @@ class _EquipmentInFormScreenState extends State { _subSubCategoryFocusNode.addListener(_onSubSubCategoryFocusChange); } + // 텍스트 컨트롤러 업데이트 메서드 + void _updateTextControllers() { + setState(() { + _manufacturerController.text = _controller.manufacturer; + _nameController.text = _controller.name; + _categoryController.text = _controller.category; + _subCategoryController.text = _controller.subCategory; + _subSubCategoryController.text = _controller.subSubCategory; + _serialNumberController.text = _controller.serialNumber; + _barcodeController.text = _controller.barcode; + _quantityController.text = _controller.quantity.toString(); + _warehouseController.text = _controller.warehouseLocation ?? ''; + _partnerController.text = _controller.partnerCompany ?? ''; + _warrantyCodeController.text = _controller.warrantyCode ?? ''; + _controller.remarkController.text = _controller.remarkController.text; + }); + } + @override void dispose() { _manufacturerFocusNode.dispose(); @@ -243,7 +286,15 @@ class _EquipmentInFormScreenState extends State { _subSubCategoryOverlayEntry?.remove(); _subSubCategoryFocusNode.dispose(); _subSubCategoryController.dispose(); - + + // 추가 컨트롤러 정리 + _nameController.dispose(); + _serialNumberController.dispose(); + _barcodeController.dispose(); + _quantityController.dispose(); + _warrantyCodeController.dispose(); + + _controller.dispose(); super.dispose(); } @@ -365,19 +416,23 @@ class _EquipmentInFormScreenState extends State { final success = await _controller.save(); // 로딩 다이얼로그 닫기 + if (!mounted) return; Navigator.pop(context); if (success) { // 성공 메시지 표시 + if (!mounted) return; ScaffoldMessenger.of(context).showSnackBar( SnackBar( - content: Text(_controller.isEditMode ? '장비 입고가 수정되었습니다.' : '장비 입고가 등록되었습니다.'), + content: Text(_controller.isEditMode ? '장비 정보가 수정되었습니다.' : '장비 입고가 등록되었습니다.'), backgroundColor: Colors.green, ), ); + if (!mounted) return; Navigator.pop(context, true); } else { // 에러 메시지 표시 + if (!mounted) return; ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: Text(_controller.error ?? '저장 중 오류가 발생했습니다.'), @@ -387,9 +442,11 @@ class _EquipmentInFormScreenState extends State { } } catch (e) { // 로딩 다이얼로그 닫기 + if (!mounted) return; Navigator.pop(context); // 예외 처리 + if (!mounted) return; ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: Text('오류: $e'), @@ -429,7 +486,7 @@ class _EquipmentInFormScreenState extends State { borderRadius: BorderRadius.circular(4), boxShadow: [ BoxShadow( - color: Colors.grey.withOpacity(0.3), + color: Colors.grey.withValues(alpha: 0.3), spreadRadius: 1, blurRadius: 3, offset: const Offset(0, 1), @@ -481,7 +538,7 @@ class _EquipmentInFormScreenState extends State { child: Text(item), ), ); - }).toList(), + }), ], ), ), @@ -533,7 +590,7 @@ class _EquipmentInFormScreenState extends State { borderRadius: BorderRadius.circular(4), boxShadow: [ BoxShadow( - color: Colors.grey.withOpacity(0.3), + color: Colors.grey.withValues(alpha: 0.3), spreadRadius: 1, blurRadius: 3, offset: const Offset(0, 1), @@ -585,7 +642,7 @@ class _EquipmentInFormScreenState extends State { child: Text(item), ), ); - }).toList(), + }), ], ), ), @@ -637,7 +694,7 @@ class _EquipmentInFormScreenState extends State { borderRadius: BorderRadius.circular(4), boxShadow: [ BoxShadow( - color: Colors.grey.withOpacity(0.3), + color: Colors.grey.withValues(alpha: 0.3), spreadRadius: 1, blurRadius: 3, offset: const Offset(0, 1), @@ -689,7 +746,7 @@ class _EquipmentInFormScreenState extends State { child: Text(item), ), ); - }).toList(), + }), ], ), ), @@ -741,7 +798,7 @@ class _EquipmentInFormScreenState extends State { borderRadius: BorderRadius.circular(4), boxShadow: [ BoxShadow( - color: Colors.grey.withOpacity(0.3), + color: Colors.grey.withValues(alpha: 0.3), spreadRadius: 1, blurRadius: 3, offset: const Offset(0, 1), @@ -793,7 +850,7 @@ class _EquipmentInFormScreenState extends State { child: Text(item), ), ); - }).toList(), + }), ], ), ), @@ -845,7 +902,7 @@ class _EquipmentInFormScreenState extends State { borderRadius: BorderRadius.circular(4), boxShadow: [ BoxShadow( - color: Colors.grey.withOpacity(0.3), + color: Colors.grey.withValues(alpha: 0.3), spreadRadius: 1, blurRadius: 3, offset: const Offset(0, 1), @@ -897,7 +954,7 @@ class _EquipmentInFormScreenState extends State { child: Text(item), ), ); - }).toList(), + }), ], ), ), @@ -949,7 +1006,7 @@ class _EquipmentInFormScreenState extends State { borderRadius: BorderRadius.circular(4), boxShadow: [ BoxShadow( - color: Colors.grey.withOpacity(0.3), + color: Colors.grey.withValues(alpha: 0.3), spreadRadius: 1, blurRadius: 3, offset: const Offset(0, 1), @@ -1001,7 +1058,7 @@ class _EquipmentInFormScreenState extends State { child: Text(item), ), ); - }).toList(), + }), ], ), ), @@ -1053,7 +1110,7 @@ class _EquipmentInFormScreenState extends State { borderRadius: BorderRadius.circular(4), boxShadow: [ BoxShadow( - color: Colors.grey.withOpacity(0.3), + color: Colors.grey.withValues(alpha: 0.3), spreadRadius: 1, blurRadius: 3, offset: const Offset(0, 1), @@ -1105,7 +1162,7 @@ class _EquipmentInFormScreenState extends State { child: Text(item), ), ); - }).toList(), + }), ], ), ), @@ -1141,9 +1198,27 @@ class _EquipmentInFormScreenState extends State { return ChangeNotifierProvider.value( value: _controller, child: Consumer( - builder: (context, controller, child) => GestureDetector( - // 화면의 다른 곳을 탭하면 모든 드롭다운 닫기 - onTap: () { + builder: (context, controller, child) { + // 수정 모드에서 로딩 중일 때 로딩 인디케이터 표시 + if (controller.isEditMode && controller.isLoading) { + return Scaffold( + backgroundColor: Theme.of(context).scaffoldBackgroundColor, + body: const Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + CircularProgressIndicator(), + SizedBox(height: 16), + Text('장비 정보를 불러오는 중...'), + ], + ), + ), + ); + } + + return GestureDetector( + // 화면의 다른 곳을 탭하면 모든 드롭다운 닫기 + onTap: () { // 현재 포커스된 위젯 포커스 해제 FocusScope.of(context).unfocus(); // 모든 드롭다운 닫기 @@ -1155,24 +1230,28 @@ class _EquipmentInFormScreenState extends State { _removeSubCategoryDropdown(); _removeSubSubCategoryDropdown(); }, - child: Scaffold( - appBar: AppBar( - title: Text(_controller.isEditMode ? '장비 입고 수정' : '장비 입고 등록'), - ), - body: Form( + child: FormLayoutTemplate( + title: _controller.isEditMode ? '장비 입고 수정' : '장비 입고 등록', + onSave: _controller.isLoading ? null : _saveEquipmentIn, + onCancel: () => Navigator.of(context).pop(), + saveButtonText: _controller.isEditMode ? '수정 완료' : '입고 등록', + isLoading: _controller.isSaving, + child: Form( key: _controller.formKey, child: SingleChildScrollView( - padding: const EdgeInsets.all(16), + padding: const EdgeInsets.all(UIConstants.formPadding), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ // 기본 정보 섹션 - Text('기본 정보', style: Theme.of(context).textTheme.titleMedium), - const SizedBox(height: 12), + FormSection( + title: '기본 정보', + subtitle: '입고할 장비의 기본 정보를 입력하세요', + children: [ // 장비 유형 선택 (라디오 버튼) FormFieldWrapper( label: '장비 유형', - isRequired: true, + required: true, child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ @@ -1244,7 +1323,7 @@ class _EquipmentInFormScreenState extends State { Expanded( child: FormFieldWrapper( label: '구매처', - isRequired: true, + required: true, child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ @@ -1288,7 +1367,7 @@ class _EquipmentInFormScreenState extends State { '[구매처:onFieldSubmitted] 자동완성 적용: "$suggestion"', ); _isProgrammaticPartnerChange = true; - _partnerController.text = suggestion!; + _partnerController.text = suggestion; _controller.partnerCompany = suggestion; // 커서를 맨 뒤로 이동 _partnerController @@ -1315,7 +1394,7 @@ class _EquipmentInFormScreenState extends State { top: 2, ), child: Text( - suggestion!, + suggestion, style: const TextStyle( color: Color(0xFF1976D2), fontWeight: FontWeight.bold, @@ -1331,7 +1410,7 @@ class _EquipmentInFormScreenState extends State { Expanded( child: FormFieldWrapper( label: '입고지', - isRequired: true, + required: true, child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ @@ -1383,7 +1462,7 @@ class _EquipmentInFormScreenState extends State { ); _isProgrammaticWarehouseChange = true; _warehouseController.text = - warehouseSuggestion!; + warehouseSuggestion; _controller.warehouseLocation = warehouseSuggestion; // 커서를 맨 뒤로 이동 @@ -1441,7 +1520,7 @@ class _EquipmentInFormScreenState extends State { Expanded( child: FormFieldWrapper( label: '제조사', - isRequired: true, + required: true, child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ @@ -1494,7 +1573,7 @@ class _EquipmentInFormScreenState extends State { ); _isProgrammaticManufacturerChange = true; _manufacturerController.text = - manufacturerSuggestion!; + manufacturerSuggestion; _controller.manufacturer = manufacturerSuggestion; // 커서를 맨 뒤로 이동 @@ -1548,7 +1627,7 @@ class _EquipmentInFormScreenState extends State { Expanded( child: FormFieldWrapper( label: '장비명', - isRequired: true, + required: true, child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ @@ -1601,7 +1680,7 @@ class _EquipmentInFormScreenState extends State { ); _isProgrammaticEquipmentNameChange = true; _equipmentNameController.text = - equipmentNameSuggestion!; + equipmentNameSuggestion; _controller.name = equipmentNameSuggestion; // 커서를 맨 뒤로 이동 @@ -1659,7 +1738,7 @@ class _EquipmentInFormScreenState extends State { Expanded( child: FormFieldWrapper( label: '대분류', - isRequired: true, + required: true, child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ @@ -1709,7 +1788,7 @@ class _EquipmentInFormScreenState extends State { ); _isProgrammaticCategoryChange = true; _categoryController.text = - categorySuggestion!; + categorySuggestion; _controller.category = categorySuggestion; // 커서를 맨 뒤로 이동 _categoryController @@ -1761,7 +1840,7 @@ class _EquipmentInFormScreenState extends State { Expanded( child: FormFieldWrapper( label: '중분류', - isRequired: true, + required: true, child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ @@ -1814,7 +1893,7 @@ class _EquipmentInFormScreenState extends State { ); _isProgrammaticSubCategoryChange = true; _subCategoryController.text = - subCategorySuggestion!; + subCategorySuggestion; _controller.subCategory = subCategorySuggestion; // 커서를 맨 뒤로 이동 @@ -1868,7 +1947,7 @@ class _EquipmentInFormScreenState extends State { Expanded( child: FormFieldWrapper( label: '소분류', - isRequired: true, + required: true, child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ @@ -1922,7 +2001,7 @@ class _EquipmentInFormScreenState extends State { _isProgrammaticSubSubCategoryChange = true; _subSubCategoryController.text = - subSubCategorySuggestion!; + subSubCategorySuggestion; _controller.subSubCategory = subSubCategorySuggestion; // 커서를 맨 뒤로 이동 @@ -1977,7 +2056,7 @@ class _EquipmentInFormScreenState extends State { // 시리얼 번호 유무 토글 FormFieldWrapper( label: '시리얼 번호', - isRequired: false, + required: false, child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ @@ -2017,7 +2096,7 @@ class _EquipmentInFormScreenState extends State { // 바코드 필드 FormFieldWrapper( label: '바코드', - isRequired: false, + required: false, child: TextFormField( initialValue: _controller.barcode, decoration: const InputDecoration( @@ -2031,7 +2110,7 @@ class _EquipmentInFormScreenState extends State { // 수량 필드 FormFieldWrapper( label: '수량', - isRequired: true, + required: true, child: TextFormField( initialValue: _controller.quantity.toString(), decoration: const InputDecoration(hintText: '수량을 입력하세요'), @@ -2055,7 +2134,7 @@ class _EquipmentInFormScreenState extends State { // 입고일 필드 FormFieldWrapper( label: '입고일', - isRequired: true, + required: true, child: InkWell( onTap: () async { final DateTime? picked = await showDatePicker( @@ -2111,7 +2190,7 @@ class _EquipmentInFormScreenState extends State { flex: 2, child: FormFieldWrapper( label: '워런티 라이센스', - isRequired: false, + required: false, child: TextFormField( initialValue: _controller.warrantyLicense ?? '', decoration: const InputDecoration( @@ -2130,7 +2209,7 @@ class _EquipmentInFormScreenState extends State { flex: 2, child: FormFieldWrapper( label: '워런티 코드', - isRequired: false, + required: false, child: TextFormField( initialValue: _controller.warrantyCode ?? '', decoration: const InputDecoration( @@ -2149,7 +2228,7 @@ class _EquipmentInFormScreenState extends State { flex: 1, child: FormFieldWrapper( label: '시작일', - isRequired: false, + required: false, child: InkWell( onTap: () async { final DateTime? picked = await showDatePicker( @@ -2199,7 +2278,7 @@ class _EquipmentInFormScreenState extends State { flex: 1, child: FormFieldWrapper( label: '종료일', - isRequired: false, + required: false, child: InkWell( onTap: () async { final DateTime? picked = await showDatePicker( @@ -2249,7 +2328,7 @@ class _EquipmentInFormScreenState extends State { flex: 1, child: FormFieldWrapper( label: '워런티 기간', - isRequired: false, + required: false, child: Container( width: double.infinity, padding: const EdgeInsets.symmetric( @@ -2281,35 +2360,23 @@ class _EquipmentInFormScreenState extends State { const SizedBox(height: 16), FormFieldWrapper( label: '비고', - isRequired: false, + required: false, child: RemarkInput( controller: _controller.remarkController, hint: '비고를 입력하세요', minLines: 4, ), ), - const SizedBox(height: 24), - SizedBox( - width: double.infinity, - child: ElevatedButton( - onPressed: _saveEquipmentIn, - style: AppThemeTailwind.primaryButtonStyle, - child: Padding( - padding: const EdgeInsets.all(12.0), - child: Text( - _controller.isEditMode ? '수정하기' : '등록하기', - style: const TextStyle(fontSize: 16), - ), - ), - ), - ), - ], - ), - ), - ), - ), - ), - ), - ); + ], // FormSection children 끝 + ), // FormSection 끝 + ], // Column children 끝 + ), // SingleChildScrollView child 끝 + ), // Form child 끝 + ), // FormLayoutTemplate child 끝 + ), // GestureDetector 끝 + ); + }, // Consumer builder 끝 + ), // Consumer 끝 + ); // ChangeNotifierProvider.value 끝 } } diff --git a/lib/screens/equipment/equipment_list_redesign.dart b/lib/screens/equipment/equipment_list_redesign.dart index ccc7d9d..6c35e0e 100644 --- a/lib/screens/equipment/equipment_list_redesign.dart +++ b/lib/screens/equipment/equipment_list_redesign.dart @@ -279,22 +279,22 @@ class _EquipmentListRedesignState extends State { /// 편집 핸들러 void _handleEdit(UnifiedEquipment equipment) async { - if (equipment.status == EquipmentStatus.in_) { - final result = await Navigator.pushNamed( - context, - Routes.equipmentInEdit, - arguments: equipment.id, - ); - if (result == true) { - setState(() { - _controller.loadData(); - }); - } - } else { - // 출고/대여 등은 별도 폼으로 이동 필요시 구현 - ScaffoldMessenger.of(context).showSnackBar( - const SnackBar(content: Text('해당 상태의 편집 기능은 준비 중입니다.')), - ); + // 디버그: 실제 상태 값 확인 + print('DEBUG: equipment.status = ${equipment.status}'); + print('DEBUG: equipment.id = ${equipment.id}'); + print('DEBUG: equipment.equipment.id = ${equipment.equipment.id}'); + + // 모든 상태의 장비 수정 가능 + // equipment.equipment.id를 사용해야 실제 장비 ID임 + final result = await Navigator.pushNamed( + context, + Routes.equipmentInEdit, + arguments: equipment.equipment.id ?? equipment.id, // 실제 장비 ID 전달 + ); + if (result == true) { + setState(() { + _controller.loadData(); + }); } } diff --git a/lib/screens/equipment/test_history_screen.dart b/lib/screens/equipment/test_history_screen.dart new file mode 100644 index 0000000..2202b2b --- /dev/null +++ b/lib/screens/equipment/test_history_screen.dart @@ -0,0 +1,183 @@ +import 'package:flutter/material.dart'; +import 'package:get_it/get_it.dart'; +import 'package:superport/services/equipment_service.dart'; +import 'package:superport/data/models/equipment/equipment_history_dto.dart'; + +class TestHistoryScreen extends StatefulWidget { + const TestHistoryScreen({Key? key}) : super(key: key); + + @override + State createState() => _TestHistoryScreenState(); +} + +class _TestHistoryScreenState extends State { + final EquipmentService _equipmentService = GetIt.instance(); + final TextEditingController _idController = TextEditingController(text: '1'); + List? _histories; + bool _isLoading = false; + String? _error; + + Future _testAddHistory() async { + setState(() { + _isLoading = true; + _error = null; + }); + + try { + final equipmentId = int.tryParse(_idController.text) ?? 1; + + // 테스트 이력 추가 + await _equipmentService.addEquipmentHistory( + equipmentId, + 'I', // transaction type + 10, // quantity + 'Test history added at ${DateTime.now()}', // remarks + ); + + // 이력 다시 조회 + await _testGetHistory(); + + } catch (e) { + setState(() { + _error = 'Error adding history: $e'; + _isLoading = false; + }); + } + } + + Future _testGetHistory() async { + setState(() { + _isLoading = true; + _error = null; + _histories = null; + }); + + try { + final equipmentId = int.tryParse(_idController.text) ?? 1; + + print('[TEST] Fetching history for equipment ID: $equipmentId'); + + final histories = await _equipmentService.getEquipmentHistory( + equipmentId, + page: 1, + perPage: 20, + ); + + print('[TEST] Received ${histories.length} history records'); + + setState(() { + _histories = histories; + _isLoading = false; + }); + } catch (e) { + print('[TEST ERROR] $e'); + setState(() { + _error = 'Error: $e'; + _isLoading = false; + }); + } + } + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: const Text('Equipment History Test'), + ), + body: Padding( + padding: const EdgeInsets.all(16.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Expanded( + child: TextField( + controller: _idController, + decoration: const InputDecoration( + labelText: 'Equipment ID', + border: OutlineInputBorder(), + ), + keyboardType: TextInputType.number, + ), + ), + const SizedBox(width: 8), + ElevatedButton( + onPressed: _isLoading ? null : _testGetHistory, + child: const Text('Get History'), + ), + const SizedBox(width: 8), + ElevatedButton( + onPressed: _isLoading ? null : _testAddHistory, + style: ElevatedButton.styleFrom( + backgroundColor: Colors.green, + ), + child: const Text('Add Test History'), + ), + ], + ), + const SizedBox(height: 16), + if (_isLoading) + const Center(child: CircularProgressIndicator()) + else if (_error != null) + Card( + color: Colors.red[50], + child: Padding( + padding: const EdgeInsets.all(16.0), + child: Text( + _error!, + style: const TextStyle(color: Colors.red), + ), + ), + ) + else if (_histories != null) + Expanded( + child: Card( + child: Padding( + padding: const EdgeInsets.all(16.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'Found ${_histories!.length} history records', + style: const TextStyle(fontWeight: FontWeight.bold), + ), + const Divider(), + Expanded( + child: _histories!.isEmpty + ? const Center(child: Text('No history found')) + : ListView.builder( + itemCount: _histories!.length, + itemBuilder: (context, index) { + final history = _histories![index]; + return Card( + margin: const EdgeInsets.symmetric(vertical: 4), + child: ListTile( + leading: CircleAvatar( + child: Text(history.transactionType), + ), + title: Text('Quantity: ${history.quantity}'), + subtitle: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text('Remarks: ${history.remarks ?? "N/A"}'), + Text('Date: ${history.transactionDate}'), + Text('User: ${history.userName ?? "Unknown"}'), + ], + ), + ), + ); + }, + ), + ), + ], + ), + ), + ), + ), + ], + ), + ), + ); + } +} \ No newline at end of file