From 1e6da44917574271e739d3b5db761b7617a4ac5c Mon Sep 17 00:00:00 2001 From: JiWoong Sul Date: Mon, 11 Aug 2025 14:00:44 +0900 Subject: [PATCH] =?UTF-8?q?refactor:=20UI=20=ED=99=94=EB=A9=B4=20=ED=86=B5?= =?UTF-8?q?=ED=95=A9=20=EB=B0=8F=20=EB=B6=88=ED=95=84=EC=9A=94=ED=95=9C=20?= =?UTF-8?q?=ED=8C=8C=EC=9D=BC=20=EC=A0=95=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 모든 *_redesign.dart 파일을 기본 화면 파일로 통합 - 백업용 컨트롤러 파일들 제거 (*_controller.backup.dart) - 사용하지 않는 예제 및 테스트 파일 제거 - Clean Architecture 적용 후 남은 정리 작업 완료 - 테스트 코드 정리 및 구조 개선 준비 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude --- .../controllers/base_list_controller.dart | 9 + .../remote/equipment_remote_datasource.dart | 19 +- .../models/equipment/equipment_list_dto.dart | 15 + .../equipment/equipment_list_dto.freezed.dart | 260 ++++++++++ .../equipment/equipment_list_dto.g.dart | 22 + .../company/get_companies_usecase.dart | 5 +- .../toggle_company_status_usecase.dart | 1 - .../equipment/equipment_in_usecase.dart | 1 - .../equipment/equipment_out_usecase.dart | 1 - .../equipment/get_equipments_usecase.dart | 5 +- .../usecases/user/get_users_usecase.dart | 5 +- lib/main.dart | 8 +- ...p_layout_redesign.dart => app_layout.dart} | 40 +- .../common/components/shadcn_components.dart | 6 +- .../custom_widgets/date_picker_field.dart | 4 +- lib/screens/common/theme_tailwind.dart | 188 ------- lib/screens/common/widgets/address_input.dart | 6 +- lib/screens/company/company_form.dart | 10 +- ...y_list_redesign.dart => company_list.dart} | 76 +-- .../controllers/company_form_controller.dart | 9 +- .../company_list_controller.backup.dart | 331 ------------ .../controllers/company_list_controller.dart | 24 +- lib/screens/company/widgets/branch_card.dart | 4 +- .../company/widgets/company_form_header.dart | 2 +- lib/screens/company/widgets/map_dialog.dart | 4 +- .../equipment_in_form_controller.dart | 10 +- .../equipment_list_controller.backup.dart | 281 ---------- .../equipment_list_controller.dart | 4 +- .../equipment_out_form_controller.dart | 10 +- lib/screens/equipment/equipment_in_form.dart | 8 +- .../equipment_in_form_lookup_example.dart | 484 ------------------ ...list_redesign.dart => equipment_list.dart} | 46 +- lib/screens/equipment/equipment_out_form.dart | 15 +- .../license_list_controller.backup.dart | 467 ----------------- .../controllers/license_list_controller.dart | 26 +- lib/screens/license/license_form.dart | 4 +- ...e_list_redesign.dart => license_list.dart} | 33 +- lib/screens/login/login_screen.dart | 4 +- ...gin_view_redesign.dart => login_view.dart} | 10 +- .../controllers/overview_controller.dart | 12 +- ...een_redesign.dart => overview_screen.dart} | 10 +- .../controllers/user_form_controller.dart | 2 +- .../user_list_controller.backup.dart | 172 ------- .../controllers/user_list_controller.dart | 24 +- lib/screens/user/user_form.dart | 7 +- ...user_list_redesign.dart => user_list.dart} | 51 +- ...house_location_list_controller.backup.dart | 210 -------- .../warehouse_location_list_controller.dart | 36 +- .../warehouse_location_form.dart | 6 +- ...sign.dart => warehouse_location_list.dart} | 46 +- lib/services/company_service.dart | 13 +- lib/services/equipment_service.dart | 65 ++- lib/services/health_test_service.dart | 30 +- lib/services/license_service.dart | 13 +- lib/services/user_service.dart | 13 +- lib/services/warehouse_service.dart | 13 +- .../usecases/auth/login_usecase_test.dart | 20 +- .../license/create_license_usecase_test.dart | 18 +- ...reate_warehouse_location_usecase_test.dart | 14 +- .../checkbox_equipment_out_test.dart | 20 +- .../automated/company_automated_test.dart | 6 +- .../automated/company_real_api_test.dart | 12 +- .../automated/equipment_in_real_api_test.dart | 14 +- .../equipment_out_real_api_test.dart | 26 +- .../automated/filter_sort_test.dart | 96 ++-- .../automated/form_submission_test.dart | 6 +- .../framework/core/api_error_diagnostics.dart | 24 +- .../automated/framework/core/auto_fixer.dart | 20 +- .../framework/core/auto_test_system.dart | 6 +- .../framework/core/screen_test_framework.dart | 6 +- .../framework/core/test_data_generator.dart | 18 +- .../core/test_data_generator_test.dart | 20 +- .../infrastructure/report_collector.dart | 48 +- .../framework/models/error_models.dart | 10 +- .../framework/models/report_models.dart | 26 +- .../framework/models/test_models.dart | 48 +- .../automated/framework/testable_action.dart | 12 +- .../utils/html_report_generator.dart | 6 +- .../automated/interactive_search_test.dart | 62 +-- .../automated/license_real_api_test.dart | 34 +- .../automated/master_test_suite.dart | 20 +- .../automated/overview_dashboard_test.dart | 24 +- .../automated/pagination_test.dart | 46 +- .../automated/run_equipment_in_full_test.dart | 4 +- .../automated/run_equipment_in_test.dart | 2 +- .../automated/run_equipment_out_test.dart | 4 +- .../automated/run_overview_test.dart | 4 +- test/integration/automated/run_user_test.dart | 6 +- .../screens/base/base_screen_test.dart | 20 +- .../screens/base/example_screen_test.dart | 4 +- .../equipment_in_automated_test.dart | 2 +- .../equipment/equipment_in_full_test.dart | 28 +- .../equipment/equipment_out_screen_test.dart | 8 +- .../screens/license/license_screen_test.dart | 26 +- .../overview/overview_screen_test.dart | 4 +- test/integration/automated/test_result.dart | 4 +- .../automated/user_actions_test.dart | 86 ++-- .../automated/user_automated_test.dart | 4 +- .../automated/user_real_api_test.dart | 30 +- .../automated/warehouse_automated_test.dart | 85 +-- .../warehouse_location_real_api_test.dart | 6 +- .../integration/license_integration_test.dart | 28 +- test_license_api_debug.dart | 3 +- 103 files changed, 1224 insertions(+), 2976 deletions(-) rename lib/screens/common/{app_layout_redesign.dart => app_layout.dart} (97%) delete mode 100644 lib/screens/common/theme_tailwind.dart rename lib/screens/company/{company_list_redesign.dart => company_list.dart} (88%) delete mode 100644 lib/screens/company/controllers/company_list_controller.backup.dart delete mode 100644 lib/screens/equipment/controllers/equipment_list_controller.backup.dart delete mode 100644 lib/screens/equipment/equipment_in_form_lookup_example.dart rename lib/screens/equipment/{equipment_list_redesign.dart => equipment_list.dart} (97%) delete mode 100644 lib/screens/license/controllers/license_list_controller.backup.dart rename lib/screens/license/{license_list_redesign.dart => license_list.dart} (96%) rename lib/screens/login/widgets/{login_view_redesign.dart => login_view.dart} (98%) rename lib/screens/overview/{overview_screen_redesign.dart => overview_screen.dart} (99%) delete mode 100644 lib/screens/user/controllers/user_list_controller.backup.dart rename lib/screens/user/{user_list_redesign.dart => user_list.dart} (94%) delete mode 100644 lib/screens/warehouse_location/controllers/warehouse_location_list_controller.backup.dart rename lib/screens/warehouse_location/{warehouse_location_list_redesign.dart => warehouse_location_list.dart} (88%) diff --git a/lib/core/controllers/base_list_controller.dart b/lib/core/controllers/base_list_controller.dart index 0e7393f..af9998e 100644 --- a/lib/core/controllers/base_list_controller.dart +++ b/lib/core/controllers/base_list_controller.dart @@ -155,6 +155,15 @@ abstract class BaseListController extends ChangeNotifier { notifyListeners(); } + /// 특정 페이지로 이동 + void goToPage(int page) { + if (page < 1 || page > _totalPages && _totalPages > 0) return; + if (page == _currentPage) return; + + _currentPage = page; + loadData(isRefresh: true); + } + /// 필터링 적용 void _applyFiltering() { if (_searchQuery.isEmpty) { diff --git a/lib/data/datasources/remote/equipment_remote_datasource.dart b/lib/data/datasources/remote/equipment_remote_datasource.dart index a91b797..02431b0 100644 --- a/lib/data/datasources/remote/equipment_remote_datasource.dart +++ b/lib/data/datasources/remote/equipment_remote_datasource.dart @@ -12,7 +12,7 @@ import 'package:superport/data/models/equipment/equipment_request.dart'; import 'package:superport/data/models/equipment/equipment_response.dart'; abstract class EquipmentRemoteDataSource { - Future> getEquipments({ + Future getEquipments({ int page = 1, int perPage = 20, String? status, @@ -44,7 +44,7 @@ class EquipmentRemoteDataSourceImpl implements EquipmentRemoteDataSource { final ApiClient _apiClient = GetIt.instance(); @override - Future> getEquipments({ + Future getEquipments({ int page = 1, int perPage = 20, String? status, @@ -68,8 +68,19 @@ class EquipmentRemoteDataSourceImpl implements EquipmentRemoteDataSource { ); if (response.data['success'] == true && response.data['data'] != null) { - final List data = response.data['data']; - return data.map((json) => EquipmentListDto.fromJson(json)).toList(); + // API 응답 구조를 DTO에 맞게 변환 (warehouse_remote_datasource 패턴 참조) + final List dataList = response.data['data']; + final pagination = response.data['pagination'] ?? {}; + + final listData = { + 'items': dataList, + 'total': pagination['total'] ?? 0, + 'page': pagination['page'] ?? 1, + 'per_page': pagination['per_page'] ?? 20, + 'total_pages': pagination['total_pages'] ?? 1, + }; + + return EquipmentListResponseDto.fromJson(listData); } else { throw ServerException( message: response.data['message'] ?? 'Failed to fetch equipment list', diff --git a/lib/data/models/equipment/equipment_list_dto.dart b/lib/data/models/equipment/equipment_list_dto.dart index dc8b507..15a7edf 100644 --- a/lib/data/models/equipment/equipment_list_dto.dart +++ b/lib/data/models/equipment/equipment_list_dto.dart @@ -24,4 +24,19 @@ class EquipmentListDto with _$EquipmentListDto { factory EquipmentListDto.fromJson(Map json) => _$EquipmentListDtoFromJson(json); +} + +/// 장비 목록 응답 DTO +@freezed +class EquipmentListResponseDto with _$EquipmentListResponseDto { + const factory EquipmentListResponseDto({ + required List items, + required int total, + required int page, + @JsonKey(name: 'per_page') required int perPage, + @JsonKey(name: 'total_pages') required int totalPages, + }) = _EquipmentListResponseDto; + + factory EquipmentListResponseDto.fromJson(Map json) => + _$EquipmentListResponseDtoFromJson(json); } \ No newline at end of file diff --git a/lib/data/models/equipment/equipment_list_dto.freezed.dart b/lib/data/models/equipment/equipment_list_dto.freezed.dart index ded0aa8..58b9f31 100644 --- a/lib/data/models/equipment/equipment_list_dto.freezed.dart +++ b/lib/data/models/equipment/equipment_list_dto.freezed.dart @@ -465,3 +465,263 @@ abstract class _EquipmentListDto implements EquipmentListDto { _$$EquipmentListDtoImplCopyWith<_$EquipmentListDtoImpl> get copyWith => throw _privateConstructorUsedError; } + +EquipmentListResponseDto _$EquipmentListResponseDtoFromJson( + Map json) { + return _EquipmentListResponseDto.fromJson(json); +} + +/// @nodoc +mixin _$EquipmentListResponseDto { + List get items => throw _privateConstructorUsedError; + int get total => throw _privateConstructorUsedError; + int get page => throw _privateConstructorUsedError; + @JsonKey(name: 'per_page') + int get perPage => throw _privateConstructorUsedError; + @JsonKey(name: 'total_pages') + int get totalPages => throw _privateConstructorUsedError; + + /// Serializes this EquipmentListResponseDto to a JSON map. + Map toJson() => throw _privateConstructorUsedError; + + /// Create a copy of EquipmentListResponseDto + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) + $EquipmentListResponseDtoCopyWith get copyWith => + throw _privateConstructorUsedError; +} + +/// @nodoc +abstract class $EquipmentListResponseDtoCopyWith<$Res> { + factory $EquipmentListResponseDtoCopyWith(EquipmentListResponseDto value, + $Res Function(EquipmentListResponseDto) then) = + _$EquipmentListResponseDtoCopyWithImpl<$Res, EquipmentListResponseDto>; + @useResult + $Res call( + {List items, + int total, + int page, + @JsonKey(name: 'per_page') int perPage, + @JsonKey(name: 'total_pages') int totalPages}); +} + +/// @nodoc +class _$EquipmentListResponseDtoCopyWithImpl<$Res, + $Val extends EquipmentListResponseDto> + implements $EquipmentListResponseDtoCopyWith<$Res> { + _$EquipmentListResponseDtoCopyWithImpl(this._value, this._then); + + // ignore: unused_field + final $Val _value; + // ignore: unused_field + final $Res Function($Val) _then; + + /// Create a copy of EquipmentListResponseDto + /// with the given fields replaced by the non-null parameter values. + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? items = null, + Object? total = null, + Object? page = null, + Object? perPage = null, + Object? totalPages = null, + }) { + return _then(_value.copyWith( + items: null == items + ? _value.items + : items // ignore: cast_nullable_to_non_nullable + as List, + total: null == total + ? _value.total + : total // ignore: cast_nullable_to_non_nullable + as int, + page: null == page + ? _value.page + : page // ignore: cast_nullable_to_non_nullable + as int, + perPage: null == perPage + ? _value.perPage + : perPage // ignore: cast_nullable_to_non_nullable + as int, + totalPages: null == totalPages + ? _value.totalPages + : totalPages // ignore: cast_nullable_to_non_nullable + as int, + ) as $Val); + } +} + +/// @nodoc +abstract class _$$EquipmentListResponseDtoImplCopyWith<$Res> + implements $EquipmentListResponseDtoCopyWith<$Res> { + factory _$$EquipmentListResponseDtoImplCopyWith( + _$EquipmentListResponseDtoImpl value, + $Res Function(_$EquipmentListResponseDtoImpl) then) = + __$$EquipmentListResponseDtoImplCopyWithImpl<$Res>; + @override + @useResult + $Res call( + {List items, + int total, + int page, + @JsonKey(name: 'per_page') int perPage, + @JsonKey(name: 'total_pages') int totalPages}); +} + +/// @nodoc +class __$$EquipmentListResponseDtoImplCopyWithImpl<$Res> + extends _$EquipmentListResponseDtoCopyWithImpl<$Res, + _$EquipmentListResponseDtoImpl> + implements _$$EquipmentListResponseDtoImplCopyWith<$Res> { + __$$EquipmentListResponseDtoImplCopyWithImpl( + _$EquipmentListResponseDtoImpl _value, + $Res Function(_$EquipmentListResponseDtoImpl) _then) + : super(_value, _then); + + /// Create a copy of EquipmentListResponseDto + /// with the given fields replaced by the non-null parameter values. + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? items = null, + Object? total = null, + Object? page = null, + Object? perPage = null, + Object? totalPages = null, + }) { + return _then(_$EquipmentListResponseDtoImpl( + items: null == items + ? _value._items + : items // ignore: cast_nullable_to_non_nullable + as List, + total: null == total + ? _value.total + : total // ignore: cast_nullable_to_non_nullable + as int, + page: null == page + ? _value.page + : page // ignore: cast_nullable_to_non_nullable + as int, + perPage: null == perPage + ? _value.perPage + : perPage // ignore: cast_nullable_to_non_nullable + as int, + totalPages: null == totalPages + ? _value.totalPages + : totalPages // ignore: cast_nullable_to_non_nullable + as int, + )); + } +} + +/// @nodoc +@JsonSerializable() +class _$EquipmentListResponseDtoImpl implements _EquipmentListResponseDto { + const _$EquipmentListResponseDtoImpl( + {required final List items, + required this.total, + required this.page, + @JsonKey(name: 'per_page') required this.perPage, + @JsonKey(name: 'total_pages') required this.totalPages}) + : _items = items; + + factory _$EquipmentListResponseDtoImpl.fromJson(Map json) => + _$$EquipmentListResponseDtoImplFromJson(json); + + final List _items; + @override + List get items { + if (_items is EqualUnmodifiableListView) return _items; + // ignore: implicit_dynamic_type + return EqualUnmodifiableListView(_items); + } + + @override + final int total; + @override + final int page; + @override + @JsonKey(name: 'per_page') + final int perPage; + @override + @JsonKey(name: 'total_pages') + final int totalPages; + + @override + String toString() { + return 'EquipmentListResponseDto(items: $items, total: $total, page: $page, perPage: $perPage, totalPages: $totalPages)'; + } + + @override + bool operator ==(Object other) { + return identical(this, other) || + (other.runtimeType == runtimeType && + other is _$EquipmentListResponseDtoImpl && + const DeepCollectionEquality().equals(other._items, _items) && + (identical(other.total, total) || other.total == total) && + (identical(other.page, page) || other.page == page) && + (identical(other.perPage, perPage) || other.perPage == perPage) && + (identical(other.totalPages, totalPages) || + other.totalPages == totalPages)); + } + + @JsonKey(includeFromJson: false, includeToJson: false) + @override + int get hashCode => Object.hash( + runtimeType, + const DeepCollectionEquality().hash(_items), + total, + page, + perPage, + totalPages); + + /// Create a copy of EquipmentListResponseDto + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) + @override + @pragma('vm:prefer-inline') + _$$EquipmentListResponseDtoImplCopyWith<_$EquipmentListResponseDtoImpl> + get copyWith => __$$EquipmentListResponseDtoImplCopyWithImpl< + _$EquipmentListResponseDtoImpl>(this, _$identity); + + @override + Map toJson() { + return _$$EquipmentListResponseDtoImplToJson( + this, + ); + } +} + +abstract class _EquipmentListResponseDto implements EquipmentListResponseDto { + const factory _EquipmentListResponseDto( + {required final List items, + required final int total, + required final int page, + @JsonKey(name: 'per_page') required final int perPage, + @JsonKey(name: 'total_pages') required final int totalPages}) = + _$EquipmentListResponseDtoImpl; + + factory _EquipmentListResponseDto.fromJson(Map json) = + _$EquipmentListResponseDtoImpl.fromJson; + + @override + List get items; + @override + int get total; + @override + int get page; + @override + @JsonKey(name: 'per_page') + int get perPage; + @override + @JsonKey(name: 'total_pages') + int get totalPages; + + /// Create a copy of EquipmentListResponseDto + /// with the given fields replaced by the non-null parameter values. + @override + @JsonKey(includeFromJson: false, includeToJson: false) + _$$EquipmentListResponseDtoImplCopyWith<_$EquipmentListResponseDtoImpl> + get copyWith => throw _privateConstructorUsedError; +} diff --git a/lib/data/models/equipment/equipment_list_dto.g.dart b/lib/data/models/equipment/equipment_list_dto.g.dart index adf4ea5..c543cff 100644 --- a/lib/data/models/equipment/equipment_list_dto.g.dart +++ b/lib/data/models/equipment/equipment_list_dto.g.dart @@ -41,3 +41,25 @@ Map _$$EquipmentListDtoImplToJson( 'branch_name': instance.branchName, 'warehouse_name': instance.warehouseName, }; + +_$EquipmentListResponseDtoImpl _$$EquipmentListResponseDtoImplFromJson( + Map json) => + _$EquipmentListResponseDtoImpl( + items: (json['items'] as List) + .map((e) => EquipmentListDto.fromJson(e as Map)) + .toList(), + total: (json['total'] as num).toInt(), + page: (json['page'] as num).toInt(), + perPage: (json['per_page'] as num).toInt(), + totalPages: (json['total_pages'] as num).toInt(), + ); + +Map _$$EquipmentListResponseDtoImplToJson( + _$EquipmentListResponseDtoImpl instance) => + { + 'items': instance.items, + 'total': instance.total, + 'page': instance.page, + 'per_page': instance.perPage, + 'total_pages': instance.totalPages, + }; diff --git a/lib/domain/usecases/company/get_companies_usecase.dart b/lib/domain/usecases/company/get_companies_usecase.dart index 2972181..919eba1 100644 --- a/lib/domain/usecases/company/get_companies_usecase.dart +++ b/lib/domain/usecases/company/get_companies_usecase.dart @@ -28,14 +28,15 @@ class GetCompaniesUseCase extends UseCase, GetCompaniesParams> { @override Future>> call(GetCompaniesParams params) async { try { - final companies = await _companyService.getCompanies( + final response = await _companyService.getCompanies( page: params.page, perPage: params.perPage, search: params.search, isActive: params.isActive, ); - return Right(companies); + // PaginatedResponse에서 items만 추출 + return Right(response.items); } on ServerFailure catch (e) { return Left(ServerFailure( message: e.message, diff --git a/lib/domain/usecases/company/toggle_company_status_usecase.dart b/lib/domain/usecases/company/toggle_company_status_usecase.dart index 6fd5d4e..690735c 100644 --- a/lib/domain/usecases/company/toggle_company_status_usecase.dart +++ b/lib/domain/usecases/company/toggle_company_status_usecase.dart @@ -1,6 +1,5 @@ import 'package:dartz/dartz.dart'; import '../../../services/company_service.dart'; -import '../../../models/company_model.dart'; import '../../../core/errors/failures.dart'; import '../base_usecase.dart'; diff --git a/lib/domain/usecases/equipment/equipment_in_usecase.dart b/lib/domain/usecases/equipment/equipment_in_usecase.dart index 53b7a4d..719595d 100644 --- a/lib/domain/usecases/equipment/equipment_in_usecase.dart +++ b/lib/domain/usecases/equipment/equipment_in_usecase.dart @@ -1,6 +1,5 @@ import 'package:dartz/dartz.dart'; import '../../../services/equipment_service.dart'; -import '../../../data/models/equipment/equipment_in_request.dart'; import '../../../data/models/equipment/equipment_io_response.dart'; import '../../../core/errors/failures.dart'; import '../base_usecase.dart'; diff --git a/lib/domain/usecases/equipment/equipment_out_usecase.dart b/lib/domain/usecases/equipment/equipment_out_usecase.dart index 5e78854..cb28fca 100644 --- a/lib/domain/usecases/equipment/equipment_out_usecase.dart +++ b/lib/domain/usecases/equipment/equipment_out_usecase.dart @@ -1,6 +1,5 @@ import 'package:dartz/dartz.dart'; import '../../../services/equipment_service.dart'; -import '../../../data/models/equipment/equipment_out_request.dart'; import '../../../data/models/equipment/equipment_io_response.dart'; import '../../../core/errors/failures.dart'; import '../base_usecase.dart'; diff --git a/lib/domain/usecases/equipment/get_equipments_usecase.dart b/lib/domain/usecases/equipment/get_equipments_usecase.dart index b7d1e5f..e4b9608 100644 --- a/lib/domain/usecases/equipment/get_equipments_usecase.dart +++ b/lib/domain/usecases/equipment/get_equipments_usecase.dart @@ -2,6 +2,7 @@ import 'package:dartz/dartz.dart'; import '../../../services/equipment_service.dart'; import '../../../models/equipment_unified_model.dart'; import '../../../core/errors/failures.dart'; +import '../../../data/models/common/paginated_response.dart'; import '../base_usecase.dart'; /// 장비 목록 조회 파라미터 @@ -25,13 +26,13 @@ class GetEquipmentsParams { /// 장비 목록 조회 UseCase /// 필터링 및 페이지네이션 지원 -class GetEquipmentsUseCase extends UseCase, GetEquipmentsParams> { +class GetEquipmentsUseCase extends UseCase, GetEquipmentsParams> { final EquipmentService _equipmentService; GetEquipmentsUseCase(this._equipmentService); @override - Future>> call(GetEquipmentsParams params) async { + Future>> call(GetEquipmentsParams params) async { try { // 상태 유효성 검증 if (params.status != null && diff --git a/lib/domain/usecases/user/get_users_usecase.dart b/lib/domain/usecases/user/get_users_usecase.dart index 57bf687..16bbb20 100644 --- a/lib/domain/usecases/user/get_users_usecase.dart +++ b/lib/domain/usecases/user/get_users_usecase.dart @@ -34,7 +34,7 @@ class GetUsersUseCase extends UseCase, GetUsersParams> { // 권한 검증 (관리자, 매니저만 사용자 목록 조회 가능) // 실제 구현에서는 현재 사용자 권한 체크 필요 - final users = await _userService.getUsers( + final response = await _userService.getUsers( page: params.page, perPage: params.perPage, isActive: params.isActive, @@ -42,7 +42,8 @@ class GetUsersUseCase extends UseCase, GetUsersParams> { role: params.role, ); - return Right(users); + // PaginatedResponse에서 items만 추출 + return Right(response.items); } catch (e) { if (e.toString().contains('권한')) { return Left(PermissionFailure( diff --git a/lib/main.dart b/lib/main.dart index e87847d..92fc044 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -1,7 +1,7 @@ import 'package:flutter/material.dart'; import 'package:get_it/get_it.dart'; import 'package:superport/models/equipment_unified_model.dart'; -import 'package:superport/screens/common/app_layout_redesign.dart'; +import 'package:superport/screens/common/app_layout.dart'; import 'package:superport/screens/common/theme_shadcn.dart'; import 'package:superport/screens/company/company_form.dart'; import 'package:superport/screens/equipment/equipment_in_form.dart'; @@ -74,7 +74,7 @@ class SuperportApp extends StatelessWidget { if (snapshot.hasData && snapshot.data!) { // 토큰이 유효하면 홈 화면으로 - return AppLayoutRedesign(initialRoute: Routes.home); + return AppLayout(initialRoute: Routes.home); } else { // 토큰이 없거나 유효하지 않으면 로그인 화면으로 return const LoginScreen(); @@ -97,7 +97,7 @@ class SuperportApp extends StatelessWidget { settings.name == Routes.license) { return MaterialPageRoute( builder: - (context) => AppLayoutRedesign(initialRoute: settings.name!), + (context) => AppLayout(initialRoute: settings.name!), ); } @@ -222,7 +222,7 @@ class SuperportApp extends StatelessWidget { default: return MaterialPageRoute( builder: - (context) => AppLayoutRedesign(initialRoute: Routes.home), + (context) => AppLayout(initialRoute: Routes.home), ); } }, diff --git a/lib/screens/common/app_layout_redesign.dart b/lib/screens/common/app_layout.dart similarity index 97% rename from lib/screens/common/app_layout_redesign.dart rename to lib/screens/common/app_layout.dart index 4a9ca74..eb0e9fd 100644 --- a/lib/screens/common/app_layout_redesign.dart +++ b/lib/screens/common/app_layout.dart @@ -3,12 +3,12 @@ import 'package:get_it/get_it.dart'; import 'package:provider/provider.dart'; import 'package:superport/screens/common/theme_shadcn.dart'; import 'package:superport/screens/common/components/shadcn_components.dart'; -import 'package:superport/screens/overview/overview_screen_redesign.dart'; -import 'package:superport/screens/equipment/equipment_list_redesign.dart'; -import 'package:superport/screens/company/company_list_redesign.dart'; -import 'package:superport/screens/user/user_list_redesign.dart'; -import 'package:superport/screens/license/license_list_redesign.dart'; -import 'package:superport/screens/warehouse_location/warehouse_location_list_redesign.dart'; +import 'package:superport/screens/overview/overview_screen.dart'; +import 'package:superport/screens/equipment/equipment_list.dart'; +import 'package:superport/screens/company/company_list.dart'; +import 'package:superport/screens/user/user_list.dart'; +import 'package:superport/screens/license/license_list.dart'; +import 'package:superport/screens/warehouse_location/warehouse_location_list.dart'; import 'package:superport/services/auth_service.dart'; import 'package:superport/services/dashboard_service.dart'; import 'package:superport/services/lookup_service.dart'; @@ -18,17 +18,17 @@ import 'package:superport/data/models/auth/auth_user.dart'; /// ERP 시스템 최적화 메인 레이아웃 /// F-Pattern 레이아웃 적용 (1920x1080 최적화) /// 상단 헤더 + 좌측 사이드바 + 메인 콘텐츠 구조 -class AppLayoutRedesign extends StatefulWidget { +class AppLayout extends StatefulWidget { final String initialRoute; - const AppLayoutRedesign({Key? key, this.initialRoute = Routes.home}) + const AppLayout({Key? key, this.initialRoute = Routes.home}) : super(key: key); @override - State createState() => _AppLayoutRedesignState(); + State createState() => _AppLayoutState(); } -class _AppLayoutRedesignState extends State +class _AppLayoutState extends State with TickerProviderStateMixin { late String _currentRoute; bool _sidebarCollapsed = false; @@ -156,20 +156,20 @@ class _AppLayoutRedesignState extends State Widget _getContentForRoute(String route) { switch (route) { case Routes.home: - return const OverviewScreenRedesign(); + return const OverviewScreen(); case Routes.equipment: case Routes.equipmentInList: case Routes.equipmentOutList: case Routes.equipmentRentList: - return EquipmentListRedesign(currentRoute: route); + return EquipmentList(currentRoute: route); case Routes.company: - return const CompanyListRedesign(); + return const CompanyList(); case Routes.user: - return const UserListRedesign(); + return const UserList(); case Routes.license: - return const LicenseListRedesign(); + return const LicenseList(); case Routes.warehouseLocation: - return const WarehouseLocationListRedesign(); + return const WarehouseLocationList(); case '/test/api': // Navigator를 사용하여 별도 화면으로 이동 WidgetsBinding.instance.addPostFrameCallback((_) { @@ -177,7 +177,7 @@ class _AppLayoutRedesignState extends State }); return const Center(child: CircularProgressIndicator()); default: - return const OverviewScreenRedesign(); + return const OverviewScreen(); } } @@ -554,7 +554,7 @@ class _AppLayoutRedesignState extends State /// 사이드바 빌드 Widget _buildSidebar() { - return SidebarMenuRedesign( + return SidebarMenu( currentRoute: _currentRoute, onRouteChanged: _navigateTo, collapsed: _sidebarCollapsed, @@ -881,13 +881,13 @@ class _AppLayoutRedesignState extends State } /// 재설계된 사이드바 메뉴 (접기/펼치기 지원) -class SidebarMenuRedesign extends StatelessWidget { +class SidebarMenu extends StatelessWidget { final String currentRoute; final Function(String) onRouteChanged; final bool collapsed; final int expiringLicenseCount; - const SidebarMenuRedesign({ + const SidebarMenu({ Key? key, required this.currentRoute, required this.onRouteChanged, diff --git a/lib/screens/common/components/shadcn_components.dart b/lib/screens/common/components/shadcn_components.dart index 1019633..41a90cf 100644 --- a/lib/screens/common/components/shadcn_components.dart +++ b/lib/screens/common/components/shadcn_components.dart @@ -359,7 +359,6 @@ class ShadcnInput extends StatefulWidget { } class _ShadcnInputState extends State { - bool _isFocused = false; bool _isHovered = false; @override @@ -384,9 +383,7 @@ class _ShadcnInputState extends State { MouseRegion( onEnter: (_) => setState(() => _isHovered = true), onExit: (_) => setState(() => _isHovered = false), - child: Focus( - onFocusChange: (focused) => setState(() => _isFocused = focused), - child: TextFormField( + child: TextFormField( controller: widget.controller, obscureText: widget.obscureText, keyboardType: widget.keyboardType, @@ -470,7 +467,6 @@ class _ShadcnInputState extends State { ), ), ), - ), ], ); } diff --git a/lib/screens/common/custom_widgets/date_picker_field.dart b/lib/screens/common/custom_widgets/date_picker_field.dart index 872dd42..e3321bd 100644 --- a/lib/screens/common/custom_widgets/date_picker_field.dart +++ b/lib/screens/common/custom_widgets/date_picker_field.dart @@ -1,6 +1,6 @@ import 'package:flutter/material.dart'; import 'form_field_wrapper.dart'; -import 'package:superport/screens/common/theme_tailwind.dart'; +import 'package:superport/screens/common/theme_shadcn.dart'; // 날짜 선택 필드 class DatePickerField extends StatelessWidget { @@ -45,7 +45,7 @@ class DatePickerField extends StatelessWidget { children: [ Text( '${selectedDate.year}-${selectedDate.month.toString().padLeft(2, '0')}-${selectedDate.day.toString().padLeft(2, '0')}', - style: AppThemeTailwind.bodyStyle, + style: ShadcnTheme.bodyMedium, ), const Icon(Icons.calendar_today, size: 20), ], diff --git a/lib/screens/common/theme_tailwind.dart b/lib/screens/common/theme_tailwind.dart deleted file mode 100644 index f90c5e2..0000000 --- a/lib/screens/common/theme_tailwind.dart +++ /dev/null @@ -1,188 +0,0 @@ -import 'package:flutter/material.dart'; - -/// Metronic Admin 테일윈드 테마 (데모6 스타일) -class AppThemeTailwind { - // 메인 컬러 팔레트 - static const Color primary = Color(0xFF5867DD); - static const Color secondary = Color(0xFF34BFA3); - static const Color success = Color(0xFF1BC5BD); - static const Color info = Color(0xFF8950FC); - static const Color warning = Color(0xFFFFA800); - static const Color danger = Color(0xFFF64E60); - static const Color light = Color(0xFFF3F6F9); - static const Color dark = Color(0xFF181C32); - static const Color muted = Color(0xFFB5B5C3); - - // 배경 컬러 - static const Color surface = Color(0xFFF7F8FA); - static const Color cardBackground = Colors.white; - - // 테마 데이터 - static ThemeData get lightTheme { - return ThemeData( - primaryColor: primary, - colorScheme: const ColorScheme.light( - primary: primary, - secondary: secondary, - surface: surface, - error: danger, - ), - scaffoldBackgroundColor: surface, - fontFamily: 'Poppins', - - // AppBar 테마 - appBarTheme: const AppBarTheme( - backgroundColor: Colors.white, - foregroundColor: dark, - elevation: 0, - centerTitle: false, - titleTextStyle: TextStyle( - color: dark, - fontSize: 18, - fontWeight: FontWeight.w600, - ), - iconTheme: IconThemeData(color: dark), - ), - - // 버튼 테마 - elevatedButtonTheme: ElevatedButtonThemeData( - style: ElevatedButton.styleFrom( - backgroundColor: primary, - foregroundColor: Colors.white, - padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12), - shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(6)), - ), - ), - - // 카드 테마 - cardTheme: CardThemeData( - color: Colors.white, - elevation: 1, - shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)), - margin: const EdgeInsets.symmetric(vertical: 8), - ), - - // 입력 폼 테마 - inputDecorationTheme: InputDecorationTheme( - filled: true, - fillColor: Colors.white, - contentPadding: const EdgeInsets.symmetric( - horizontal: 16, - vertical: 12, - ), - border: OutlineInputBorder( - borderRadius: BorderRadius.circular(6), - borderSide: const BorderSide(color: Color(0xFFE5E7EB)), - ), - enabledBorder: OutlineInputBorder( - borderRadius: BorderRadius.circular(6), - borderSide: const BorderSide(color: Color(0xFFE5E7EB)), - ), - focusedBorder: OutlineInputBorder( - borderRadius: BorderRadius.circular(6), - borderSide: const BorderSide(color: primary), - ), - errorBorder: OutlineInputBorder( - borderRadius: BorderRadius.circular(6), - borderSide: const BorderSide(color: danger), - ), - floatingLabelBehavior: FloatingLabelBehavior.never, - ), - - // 데이터 테이블 테마 - dataTableTheme: const DataTableThemeData( - headingRowColor: WidgetStatePropertyAll(light), - dividerThickness: 1, - columnSpacing: 24, - headingTextStyle: TextStyle( - color: dark, - fontWeight: FontWeight.w600, - fontSize: 14, - ), - dataTextStyle: TextStyle(color: Color(0xFF6C7293), fontSize: 14), - ), - ); - } - - // 스타일 - 헤딩 및 텍스트 - static const TextStyle headingStyle = TextStyle( - fontSize: 20, - fontWeight: FontWeight.bold, - color: dark, - ); - - static const TextStyle subheadingStyle = TextStyle( - fontSize: 16, - fontWeight: FontWeight.w600, - color: dark, - ); - - static const TextStyle bodyStyle = TextStyle( - fontSize: 14, - color: Color(0xFF6C7293), - ); - - // 굵은 본문 텍스트 - static const TextStyle bodyBoldStyle = TextStyle( - fontSize: 14, - fontWeight: FontWeight.w600, - color: dark, - ); - - static const TextStyle smallText = TextStyle(fontSize: 12, color: muted); - - // 버튼 스타일 - static final ButtonStyle primaryButtonStyle = ElevatedButton.styleFrom( - backgroundColor: primary, - foregroundColor: Colors.white, - padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12), - shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(6)), - ); - - // 라벨 스타일 - static const TextStyle formLabelStyle = TextStyle( - fontSize: 14, - fontWeight: FontWeight.w600, - color: dark, - ); - - static final ButtonStyle secondaryButtonStyle = ElevatedButton.styleFrom( - backgroundColor: secondary, - foregroundColor: Colors.white, - padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12), - shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(6)), - ); - - static final ButtonStyle outlineButtonStyle = OutlinedButton.styleFrom( - foregroundColor: primary, - side: const BorderSide(color: primary), - padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12), - shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(6)), - ); - - // 카드 장식 - static final BoxDecoration cardDecoration = BoxDecoration( - color: Colors.white, - borderRadius: BorderRadius.circular(8), - boxShadow: [ - BoxShadow( - color: Colors.black.withAlpha(13), - blurRadius: 10, - offset: const Offset(0, 2), - ), - ], - ); - - // 기타 장식 - static final BoxDecoration containerDecoration = BoxDecoration( - color: Colors.white, - borderRadius: BorderRadius.circular(8), - border: Border.all(color: const Color(0xFFE5E7EB)), - ); - - static const EdgeInsets cardPadding = EdgeInsets.all(20); - static const EdgeInsets listPadding = EdgeInsets.symmetric( - vertical: 8, - horizontal: 16, - ); -} diff --git a/lib/screens/common/widgets/address_input.dart b/lib/screens/common/widgets/address_input.dart index c3166f7..ed0c27d 100644 --- a/lib/screens/common/widgets/address_input.dart +++ b/lib/screens/common/widgets/address_input.dart @@ -1,6 +1,6 @@ import 'package:flutter/material.dart'; import 'package:superport/screens/common/custom_widgets.dart'; -import 'package:superport/screens/common/theme_tailwind.dart'; +import 'package:superport/screens/common/theme_shadcn.dart'; import 'package:superport/utils/address_constants.dart'; import 'package:superport/models/address_model.dart'; @@ -41,7 +41,7 @@ class AddressInput extends StatefulWidget { /// Address 객체를 받아 읽기 전용으로 표시하는 위젯 static Widget readonly({required Address address}) { // 회사 리스트와 동일하게 address.toString() 사용, 스타일도 bodyStyle로 통일 - return Text(address.toString(), style: AppThemeTailwind.bodyStyle); + return Text(address.toString(), style: ShadcnTheme.bodyMedium); } } @@ -171,7 +171,7 @@ class _AddressInputState extends State { height: 48, child: Text( region, - style: AppThemeTailwind.bodyStyle.copyWith( + style: ShadcnTheme.bodyMedium.copyWith( fontSize: 16, ), ), diff --git a/lib/screens/company/company_form.dart b/lib/screens/company/company_form.dart index 814c41c..4c93478 100644 --- a/lib/screens/company/company_form.dart +++ b/lib/screens/company/company_form.dart @@ -16,7 +16,7 @@ import 'package:flutter/material.dart'; // import 'package:superport/models/address_model.dart'; // 사용되지 않는 import import 'package:superport/models/company_model.dart'; // import 'package:superport/screens/common/custom_widgets.dart'; // 사용되지 않는 import -import 'package:superport/screens/common/theme_tailwind.dart'; +import 'package:superport/screens/common/theme_shadcn.dart'; import 'package:superport/screens/company/controllers/company_form_controller.dart'; // import 'package:superport/screens/company/widgets/branch_card.dart'; // 사용되지 않는 import import 'package:superport/screens/company/widgets/company_form_header.dart'; @@ -48,7 +48,7 @@ class CompanyTypeSelector extends StatelessWidget { child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - Text('회사 유형', style: AppThemeTailwind.formLabelStyle), + Text('회사 유형', style: ShadcnTheme.labelMedium), const SizedBox(height: 8), Row( children: [ @@ -357,7 +357,7 @@ class _CompanyFormScreenState extends State { child: ElevatedButton( onPressed: _saveCompany, style: ElevatedButton.styleFrom( - backgroundColor: AppThemeTailwind.primary, + backgroundColor: ShadcnTheme.primary, minimumSize: const Size.fromHeight(48), shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(8), @@ -463,7 +463,7 @@ class _CompanyFormScreenState extends State { padding: const EdgeInsets.only(top: 16.0, bottom: 8.0), child: Text( '지점 정보', - style: AppThemeTailwind.subheadingStyle, + style: ShadcnTheme.headingH6, ), ), if (_controller.branchControllers.isNotEmpty) @@ -507,7 +507,7 @@ class _CompanyFormScreenState extends State { child: ElevatedButton( onPressed: _saveCompany, style: ElevatedButton.styleFrom( - backgroundColor: AppThemeTailwind.primary, + backgroundColor: ShadcnTheme.primary, minimumSize: const Size.fromHeight(48), shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(8), diff --git a/lib/screens/company/company_list_redesign.dart b/lib/screens/company/company_list.dart similarity index 88% rename from lib/screens/company/company_list_redesign.dart rename to lib/screens/company/company_list.dart index 10aff2e..cc9dddc 100644 --- a/lib/screens/company/company_list_redesign.dart +++ b/lib/screens/company/company_list.dart @@ -17,25 +17,23 @@ import 'package:superport/screens/company/widgets/company_branch_dialog.dart'; import 'package:superport/screens/company/controllers/company_list_controller.dart'; /// shadcn/ui 스타일로 재설계된 회사 관리 화면 (통일된 UI 컴포넌트 사용) -class CompanyListRedesign extends StatefulWidget { - const CompanyListRedesign({super.key}); +class CompanyList extends StatefulWidget { + const CompanyList({super.key}); @override - State createState() => _CompanyListRedesignState(); + State createState() => _CompanyListState(); } -class _CompanyListRedesignState extends State { +class _CompanyListState extends State { late CompanyListController _controller; final TextEditingController _searchController = TextEditingController(); Timer? _debounceTimer; - int _currentPage = 1; - final int _pageSize = 10; @override void initState() { super.initState(); _controller = CompanyListController(); - _controller.initializeWithPageSize(_pageSize); + _controller.initializeWithPageSize(10); // 페이지 크기 설정 } @override @@ -50,10 +48,7 @@ class _CompanyListRedesignState extends State { void _onSearchChanged(String value) { _debounceTimer?.cancel(); _debounceTimer = Timer(AppConstants.searchDebounce, () { - setState(() { - _currentPage = 1; - }); - _controller.search(value); + _controller.search(value); // Controller가 페이지 리셋 처리 }); } @@ -228,37 +223,15 @@ class _CompanyListRedesignState extends State { } } - final int totalCount = displayCompanies.length; - - // 페이지네이션을 위한 데이터 처리 - final int startIndex = (_currentPage - 1) * _pageSize; - final int endIndex = startIndex + _pageSize; + // Controller가 이미 페이지크된 데이터를 제공 + final List> pagedCompanies = displayCompanies; + final int totalCount = controller.total; // 실제 전체 개수 사용 - // 디버그 로그 추가 - print('🔍 [VIEW DEBUG] 화면 페이지네이션 상태'); - print(' • filteredCompanies 수: ${controller.filteredCompanies.length}개'); - print(' • displayCompanies 수: ${displayCompanies.length}개 (지점 포함)'); - print(' • 현재 페이지: $_currentPage'); - print(' • 페이지 크기: $_pageSize'); - print(' • startIndex: $startIndex, endIndex: $endIndex'); - - // startIndex가 displayCompanies.length보다 크거나 같으면 첫 페이지로 리셋 - if (startIndex >= displayCompanies.length && displayCompanies.isNotEmpty) { - WidgetsBinding.instance.addPostFrameCallback((_) { - setState(() { - _currentPage = 1; - }); - }); - } - - final List> pagedCompanies = displayCompanies.isEmpty - ? [] - : displayCompanies.sublist( - startIndex.clamp(0, displayCompanies.length), - endIndex.clamp(0, displayCompanies.length), - ); - - print(' • 화면에 표시될 항목 수: ${pagedCompanies.length}개'); + print('🔍 [VIEW DEBUG] 페이지네이션 상태'); + print(' • Controller items: ${controller.companies.length}개'); + print(' • 전체 개수: ${controller.total}개'); + print(' • 현재 페이지: ${controller.currentPage}'); + print(' • 페이지 크기: ${controller.pageSize}'); // 로딩 상태 if (controller.isLoading && controller.companies.isEmpty) { @@ -344,7 +317,7 @@ class _CompanyListRedesignState extends State { ], rows: [ ...pagedCompanies.asMap().entries.map((entry) { - final int index = startIndex + entry.key; + final int index = ((controller.currentPage - 1) * controller.pageSize) + entry.key; final companyData = entry.value; final bool isBranch = companyData['isBranch'] as bool; final Company company = @@ -457,15 +430,18 @@ class _CompanyListRedesignState extends State { ], ), - // 페이지네이션 (항상 표시) + // 페이지네이션 (Controller 상태 사용) pagination: Pagination( - totalCount: totalCount, - currentPage: _currentPage, - pageSize: _pageSize, + totalCount: controller.total, + currentPage: controller.currentPage, + pageSize: controller.pageSize, onPageChanged: (page) { - setState(() { - _currentPage = page; - }); + // 다음 페이지 로드 + if (page > controller.currentPage) { + controller.loadNextPage(); + } else if (page == 1) { + controller.refresh(); + } }, ), ); @@ -473,4 +449,4 @@ class _CompanyListRedesignState extends State { ), ); } -} +} \ No newline at end of file diff --git a/lib/screens/company/controllers/company_form_controller.dart b/lib/screens/company/controllers/company_form_controller.dart index 966226a..a6c0a12 100644 --- a/lib/screens/company/controllers/company_form_controller.dart +++ b/lib/screens/company/controllers/company_form_controller.dart @@ -96,8 +96,9 @@ class CompanyFormController { try { List companies; - // API만 사용 - companies = await _companyService.getCompanies(); + // API만 사용 (PaginatedResponse에서 items 추출) + final response = await _companyService.getCompanies(); + companies = response.items; companyNames = companies.map((c) => c.name).toList(); filteredCompanyNames = companyNames; @@ -347,9 +348,9 @@ class CompanyFormController { if (_useApi) { try { // 회사명 목록을 조회하여 중복 확인 - final companies = await _companyService.getCompanies(search: name); + final response = await _companyService.getCompanies(search: name); // 정확히 일치하는 회사명이 있는지 확인 - for (final company in companies) { + for (final company in response.items) { if (company.name.toLowerCase() == name.toLowerCase()) { return company; } diff --git a/lib/screens/company/controllers/company_list_controller.backup.dart b/lib/screens/company/controllers/company_list_controller.backup.dart deleted file mode 100644 index 248311e..0000000 --- a/lib/screens/company/controllers/company_list_controller.backup.dart +++ /dev/null @@ -1,331 +0,0 @@ -import 'package:flutter/foundation.dart'; -import 'package:get_it/get_it.dart'; -import 'package:superport/models/company_model.dart'; -import 'package:superport/services/company_service.dart'; -import 'package:superport/core/errors/failures.dart'; - -// 회사 목록 화면의 상태 및 비즈니스 로직을 담당하는 컨트롤러 -class CompanyListController extends ChangeNotifier { - final CompanyService _companyService = GetIt.instance(); - - List companies = []; - List filteredCompanies = []; - String searchKeyword = ''; - final Set selectedCompanyIds = {}; - - bool _isLoading = false; - String? _error; - // API만 사용 - - // 페이지네이션 - int _currentPage = 1; - int _perPage = 20; - bool _hasMore = true; - - // 필터 - bool? _isActiveFilter; - - // Getters - bool get isLoading => _isLoading; - String? get error => _error; - bool get hasMore => _hasMore; - int get currentPage => _currentPage; - bool? get isActiveFilter => _isActiveFilter; - - CompanyListController(); - - // 초기 데이터 로드 - Future initialize() async { - print('╔══════════════════════════════════════════════════════════'); - print('║ 🚀 회사 목록 초기화 시작'); - print('║ • 페이지 크기: $_perPage개'); - print('╚══════════════════════════════════════════════════════════'); - await loadData(isRefresh: true); - } - - // 페이지 크기를 지정하여 초기화 - Future initializeWithPageSize(int pageSize) async { - _perPage = pageSize; - print('╔══════════════════════════════════════════════════════════'); - print('║ 🚀 회사 목록 초기화 시작 (커스텀 페이지 크기)'); - print('║ • 페이지 크기: $_perPage개'); - print('╚══════════════════════════════════════════════════════════'); - await loadData(isRefresh: true); - } - - // 데이터 로드 및 필터 적용 - Future loadData({bool isRefresh = false}) async { - print('🔍 [DEBUG] loadData 시작 - currentPage: $_currentPage, hasMore: $_hasMore, companies.length: ${companies.length}'); - print('[CompanyListController] loadData called - isRefresh: $isRefresh'); - - if (isRefresh) { - _currentPage = 1; - _hasMore = true; - companies.clear(); - filteredCompanies.clear(); - } - - if (_isLoading || (!_hasMore && !isRefresh)) return; - - _isLoading = true; - _error = null; - notifyListeners(); - - try { - // API 호출 - 지점 정보 포함 - print('[CompanyListController] Using API to fetch companies with branches'); - - // 지점 정보를 포함한 전체 회사 목록 가져오기 - final apiCompaniesWithBranches = await _companyService.getCompaniesWithBranchesFlat(); - - // 상세한 회사 정보 로그 출력 - print('╔══════════════════════════════════════════════════════════'); - print('║ 📊 회사 목록 로드 완료'); - print('║ ▶ 총 회사 수: ${apiCompaniesWithBranches.length}개'); - print('╟──────────────────────────────────────────────────────────'); - - // 지점이 있는 회사와 없는 회사 구분 - int companiesWithBranches = 0; - int totalBranches = 0; - - for (final company in apiCompaniesWithBranches) { - if (company.branches?.isNotEmpty ?? false) { - companiesWithBranches++; - totalBranches += company.branches!.length; - print('║ • ${company.name}: ${company.branches!.length}개 지점'); - } - } - - final companiesWithoutBranches = apiCompaniesWithBranches.length - companiesWithBranches; - - print('╟──────────────────────────────────────────────────────────'); - print('║ 📈 통계'); - print('║ • 지점이 있는 회사: ${companiesWithBranches}개'); - print('║ • 지점이 없는 회사: ${companiesWithoutBranches}개'); - print('║ • 총 지점 수: ${totalBranches}개'); - print('╚══════════════════════════════════════════════════════════'); - - // 검색어 필터 적용 (서버에서 필터링이 안 되므로 클라이언트에서 처리) - List filteredApiCompanies = apiCompaniesWithBranches; - if (searchKeyword.isNotEmpty) { - final keyword = searchKeyword.toLowerCase(); - filteredApiCompanies = apiCompaniesWithBranches.where((company) { - return company.name.toLowerCase().contains(keyword) || - (company.contactName?.toLowerCase().contains(keyword) ?? false) || - (company.contactPhone?.toLowerCase().contains(keyword) ?? false); - }).toList(); - - print('╔══════════════════════════════════════════════════════════'); - print('║ 🔍 검색 필터 적용'); - print('║ • 검색어: "$searchKeyword"'); - print('║ • 필터 전: ${apiCompaniesWithBranches.length}개'); - print('║ • 필터 후: ${filteredApiCompanies.length}개'); - print('╚══════════════════════════════════════════════════════════'); - } - - // 활성 상태 필터 적용 (현재 API에서 지원하지 않으므로 주석 처리) - // if (_isActiveFilter != null) { - // filteredApiCompanies = filteredApiCompanies.where((c) => c.isActive == _isActiveFilter).toList(); - // } - - // 전체 데이터를 한 번에 로드 (View에서 페이지네이션 처리) - companies = filteredApiCompanies; - _hasMore = false; // 전체 데이터를 로드했으므로 더 이상 로드할 필요 없음 - - print('╔══════════════════════════════════════════════════════════'); - print('║ 📑 전체 데이터 로드 완료'); - print('║ • 로드된 회사 수: ${companies.length}개'); - print('║ • 필터링된 회사 수: ${filteredApiCompanies.length}개'); - print('║ • View에서 페이지네이션 처리 예정'); - print('╚══════════════════════════════════════════════════════════'); - - // 필터 적용 - applyFilters(); - - print('╔══════════════════════════════════════════════════════════'); - print('║ ✅ 최종 화면 표시'); - print('║ • 화면에 표시될 회사 수: ${filteredCompanies.length}개'); - print('╚══════════════════════════════════════════════════════════'); - - selectedCompanyIds.clear(); - } on Failure catch (e) { - print('[CompanyListController] Failure loading companies: ${e.message}'); - _error = e.message; - } catch (e, stackTrace) { - print('[CompanyListController] Error loading companies: $e'); - print('[CompanyListController] Error type: ${e.runtimeType}'); - print('[CompanyListController] Stack trace: $stackTrace'); - _error = '회사 목록을 불러오는 중 오류가 발생했습니다: $e'; - } finally { - _isLoading = false; - notifyListeners(); - } - } - - // 검색 및 필터 적용 - void applyFilters() { - filteredCompanies = companies.where((company) { - // 검색어 필터 - if (searchKeyword.isNotEmpty) { - final keyword = searchKeyword.toLowerCase(); - final matchesName = company.name.toLowerCase().contains(keyword); - final matchesContact = company.contactName?.toLowerCase().contains(keyword) ?? false; - final matchesPhone = company.contactPhone?.toLowerCase().contains(keyword) ?? false; - - if (!matchesName && !matchesContact && !matchesPhone) { - return false; - } - } - - // 활성 상태 필터 (현재 API에서 지원안함) - // if (_isActiveFilter != null) { - // 추후 API 지원 시 구현 - // } - - return true; - }).toList(); - } - - // 검색어 변경 - Future updateSearchKeyword(String keyword) async { - searchKeyword = keyword; - - if (keyword.isNotEmpty) { - print('╔══════════════════════════════════════════════════════════'); - print('║ 🔍 검색어 변경: "$keyword"'); - print('╚══════════════════════════════════════════════════════════'); - } else { - print('╔══════════════════════════════════════════════════════════'); - print('║ ❌ 검색어 초기화'); - print('╚══════════════════════════════════════════════════════════'); - } - - // API 사용 시 새로 조회 - await loadData(isRefresh: true); - } - - // 활성 상태 필터 변경 - Future changeActiveFilter(bool? isActive) async { - _isActiveFilter = isActive; - await loadData(isRefresh: true); - } - - // 회사 선택/해제 - void toggleCompanySelection(int? companyId) { - if (companyId == null) return; - - if (selectedCompanyIds.contains(companyId)) { - selectedCompanyIds.remove(companyId); - } else { - selectedCompanyIds.add(companyId); - } - notifyListeners(); - } - - // 전체 선택/해제 - void toggleSelectAll() { - if (selectedCompanyIds.length == filteredCompanies.length) { - selectedCompanyIds.clear(); - } else { - selectedCompanyIds.clear(); - for (final company in filteredCompanies) { - if (company.id != null) { - selectedCompanyIds.add(company.id!); - } - } - } - notifyListeners(); - } - - // 선택된 회사 수 반환 - int getSelectedCount() { - return selectedCompanyIds.length; - } - - // 회사 삭제 - Future deleteCompany(int companyId) async { - try { - // API를 통한 삭제 - await _companyService.deleteCompany(companyId); - - // 로컬 리스트에서도 제거 - companies.removeWhere((c) => c.id == companyId); - filteredCompanies.removeWhere((c) => c.id == companyId); - selectedCompanyIds.remove(companyId); - notifyListeners(); - - return true; - } on Failure catch (e) { - _error = e.message; - notifyListeners(); - return false; - } catch (e) { - _error = '회사 삭제 중 오류가 발생했습니다: $e'; - notifyListeners(); - return false; - } - } - - // 선택된 회사들 삭제 - Future deleteSelectedCompanies() async { - final selectedIds = selectedCompanyIds.toList(); - int successCount = 0; - - for (final companyId in selectedIds) { - if (await deleteCompany(companyId)) { - successCount++; - } - } - - return successCount == selectedIds.length; - } - - // 회사 정보 업데이트 (로컬) - void updateCompanyLocally(Company updatedCompany) { - final index = companies.indexWhere((c) => c.id == updatedCompany.id); - if (index != -1) { - companies[index] = updatedCompany; - applyFilters(); - notifyListeners(); - } - } - - // 회사 추가 (로컬) - void addCompanyLocally(Company newCompany) { - companies.insert(0, newCompany); - applyFilters(); - notifyListeners(); - } - - // 더 많은 데이터 로드 - Future loadMore() async { - print('🔍 [DEBUG] loadMore 호출됨 - hasMore: $_hasMore, isLoading: $_isLoading'); - if (!_hasMore || _isLoading) { - print('🔍 [DEBUG] loadMore 조건 미충족으로 종료 (hasMore: $_hasMore, isLoading: $_isLoading)'); - return; - } - print('🔍 [DEBUG] loadMore 실행 - 추가 데이터 로드 시작'); - await loadData(); - } - - // API만 사용하므로 토글 기능 제거 - - // 에러 처리 - void clearError() { - _error = null; - notifyListeners(); - } - - // 리프레시 - Future refresh() async { - print('╔══════════════════════════════════════════════════════════'); - print('║ 🔄 회사 목록 새로고침 시작'); - print('╚══════════════════════════════════════════════════════════'); - await loadData(isRefresh: true); - } - - @override - void dispose() { - super.dispose(); - } -} \ No newline at end of file diff --git a/lib/screens/company/controllers/company_list_controller.dart b/lib/screens/company/controllers/company_list_controller.dart index d5209a0..1654a28 100644 --- a/lib/screens/company/controllers/company_list_controller.dart +++ b/lib/screens/company/controllers/company_list_controller.dart @@ -48,8 +48,8 @@ class CompanyListController extends BaseListController { required PaginationParams params, Map? additionalFilters, }) async { - // API 호출 - 회사 목록 조회 - final apiCompanies = await ErrorHandler.handleApiCall>( + // API 호출 - 회사 목록 조회 (이제 PaginatedResponse 반환) + final response = await ErrorHandler.handleApiCall( () => _companyService.getCompanies( page: params.page, perPage: params.perPage, @@ -61,21 +61,17 @@ class CompanyListController extends BaseListController { }, ); - final items = apiCompanies ?? []; - - // 임시로 메타데이터 생성 (추후 API에서 실제 메타데이터 반환 시 수정) + // PaginatedResponse를 PagedResult로 변환 final meta = PaginationMeta( - currentPage: params.page, - perPage: params.perPage, - total: items.length < params.perPage ? - (params.page - 1) * params.perPage + items.length : - params.page * params.perPage + 1, - totalPages: items.length < params.perPage ? params.page : params.page + 1, - hasNext: items.length >= params.perPage, - hasPrevious: params.page > 1, + currentPage: response.page, + perPage: response.size, + total: response.totalElements, + totalPages: response.totalPages, + hasNext: !response.last, + hasPrevious: !response.first, ); - return PagedResult(items: items, meta: meta); + return PagedResult(items: response.items, meta: meta); } @override diff --git a/lib/screens/company/widgets/branch_card.dart b/lib/screens/company/widgets/branch_card.dart index b75cd01..8da1fd9 100644 --- a/lib/screens/company/widgets/branch_card.dart +++ b/lib/screens/company/widgets/branch_card.dart @@ -3,7 +3,7 @@ import 'package:flutter/services.dart'; import 'package:superport/models/address_model.dart'; import 'package:superport/models/company_model.dart'; import 'package:superport/screens/common/custom_widgets.dart'; -import 'package:superport/screens/common/theme_tailwind.dart'; +import 'package:superport/screens/common/theme_shadcn.dart'; import 'package:superport/screens/common/widgets/address_input.dart'; import 'package:superport/screens/company/widgets/contact_info_widget.dart'; import 'package:superport/utils/validators.dart'; @@ -81,7 +81,7 @@ class _BranchCardState extends State { children: [ Text( '지점 #${widget.index + 1}', - style: AppThemeTailwind.subheadingStyle, + style: ShadcnTheme.headingH6, ), IconButton( icon: const Icon(Icons.delete, color: Colors.red), diff --git a/lib/screens/company/widgets/company_form_header.dart b/lib/screens/company/widgets/company_form_header.dart index 592684a..302099a 100644 --- a/lib/screens/company/widgets/company_form_header.dart +++ b/lib/screens/company/widgets/company_form_header.dart @@ -1,7 +1,7 @@ import 'package:flutter/material.dart'; import 'package:superport/models/address_model.dart'; import 'package:superport/screens/common/custom_widgets.dart'; -import 'package:superport/screens/common/theme_tailwind.dart'; +import 'package:superport/screens/common/theme_shadcn.dart'; import 'package:superport/screens/common/widgets/address_input.dart'; import 'package:superport/utils/validators.dart'; import 'package:superport/screens/company/widgets/company_name_autocomplete.dart'; diff --git a/lib/screens/company/widgets/map_dialog.dart b/lib/screens/company/widgets/map_dialog.dart index 24cb836..3114546 100644 --- a/lib/screens/company/widgets/map_dialog.dart +++ b/lib/screens/company/widgets/map_dialog.dart @@ -1,5 +1,5 @@ import 'package:flutter/material.dart'; -import 'package:superport/screens/common/theme_tailwind.dart'; +import 'package:superport/screens/common/theme_shadcn.dart'; /// 주소에 대한 지도 대화상자를 표시합니다. class MapDialog extends StatelessWidget { @@ -68,7 +68,7 @@ class MapDialog extends StatelessWidget { Icon( Icons.map, size: 64, - color: AppThemeTailwind.primary, + color: ShadcnTheme.primary, ), const SizedBox(height: 16), Text( diff --git a/lib/screens/equipment/controllers/equipment_in_form_controller.dart b/lib/screens/equipment/controllers/equipment_in_form_controller.dart index 1e97011..a5d6cb7 100644 --- a/lib/screens/equipment/controllers/equipment_in_form_controller.dart +++ b/lib/screens/equipment/controllers/equipment_in_form_controller.dart @@ -123,10 +123,10 @@ class EquipmentInFormController extends ChangeNotifier { void _loadWarehouseLocations() async { try { DebugLogger.log('입고지 목록 API 로드 시작', tag: 'EQUIPMENT_IN'); - final locations = await _warehouseService.getWarehouseLocations(); - warehouseLocations = locations.map((e) => e.name).toList(); + final response = await _warehouseService.getWarehouseLocations(); + warehouseLocations = response.items.map((e) => e.name).toList(); // 이름-ID 매핑 저장 - warehouseLocationMap = {for (var loc in locations) loc.name: loc.id}; + warehouseLocationMap = {for (var loc in response.items) loc.name: loc.id}; DebugLogger.log('입고지 목록 로드 성공', tag: 'EQUIPMENT_IN', data: { 'count': warehouseLocations.length, 'locations': warehouseLocations, @@ -146,8 +146,8 @@ class EquipmentInFormController extends ChangeNotifier { void _loadPartnerCompanies() async { try { DebugLogger.log('파트너사 목록 API 로드 시작', tag: 'EQUIPMENT_IN'); - final companies = await _companyService.getCompanies(); - partnerCompanies = companies.map((c) => c.name).toList(); + final response = await _companyService.getCompanies(); + partnerCompanies = response.items.map((c) => c.name).toList(); DebugLogger.log('파트너사 목록 로드 성공', tag: 'EQUIPMENT_IN', data: { 'count': partnerCompanies.length, 'companies': partnerCompanies, diff --git a/lib/screens/equipment/controllers/equipment_list_controller.backup.dart b/lib/screens/equipment/controllers/equipment_list_controller.backup.dart deleted file mode 100644 index 41ccc2e..0000000 --- a/lib/screens/equipment/controllers/equipment_list_controller.backup.dart +++ /dev/null @@ -1,281 +0,0 @@ -import 'package:flutter/foundation.dart'; -import 'package:get_it/get_it.dart'; -import 'package:superport/models/equipment_unified_model.dart'; -import 'package:superport/services/equipment_service.dart'; -import 'package:superport/utils/constants.dart'; -import 'package:superport/core/errors/failures.dart'; -import 'package:superport/models/equipment_unified_model.dart' as legacy; -import 'package:superport/core/utils/debug_logger.dart'; - -// companyTypeToString 함수 import -import 'package:superport/utils/constants.dart' - show companyTypeToString, CompanyType; -import 'package:superport/models/company_model.dart'; -import 'package:superport/models/address_model.dart'; -import 'package:superport/core/utils/equipment_status_converter.dart'; - -// 장비 목록 화면의 상태 및 비즈니스 로직을 담당하는 컨트롤러 -class EquipmentListController extends ChangeNotifier { - final EquipmentService _equipmentService = GetIt.instance(); - - List equipments = []; - String? selectedStatusFilter; - String searchKeyword = ''; // 검색어 추가 - final Set selectedEquipmentIds = {}; // 'id:status' 형식 - - bool _isLoading = false; - String? _error; - // API만 사용 - - // 페이지네이션 - int _currentPage = 1; - final int _perPage = 20; - bool _hasMore = true; - - // Getters - bool get isLoading => _isLoading; - String? get error => _error; - bool get hasMore => _hasMore; - int get currentPage => _currentPage; - - EquipmentListController(); - - // 데이터 로드 및 상태 필터 적용 - Future loadData({bool isRefresh = false, String? search}) async { - if (_isLoading) return; - - _isLoading = true; - _error = null; - notifyListeners(); - - try { - // API 호출 - 전체 데이터 로드 - print('╔══════════════════════════════════════════════════════════'); - print('║ 📦 장비 목록 API 호출 시작'); - print('║ • 상태 필터: ${selectedStatusFilter ?? "전체"}'); - print('║ • 검색어: ${search ?? searchKeyword}'); - print('╚══════════════════════════════════════════════════════════'); - - // 전체 데이터를 가져오기 위해 큰 perPage 값 사용 - final apiEquipmentDtos = await _equipmentService.getEquipmentsWithStatus( - page: 1, - perPage: 1000, // 충분히 큰 값으로 전체 데이터 로드 - status: selectedStatusFilter != null ? EquipmentStatusConverter.clientToServer(selectedStatusFilter) : null, - search: search ?? searchKeyword, - ); - - print('╔══════════════════════════════════════════════════════════'); - print('║ 📊 장비 목록 로드 완료'); - print('║ ▶ 총 장비 수: ${apiEquipmentDtos.length}개'); - print('╟──────────────────────────────────────────────────────────'); - - // 상태별 통계 - Map statusCount = {}; - for (final dto in apiEquipmentDtos) { - final clientStatus = EquipmentStatusConverter.serverToClient(dto.status); - statusCount[clientStatus] = (statusCount[clientStatus] ?? 0) + 1; - } - - statusCount.forEach((status, count) { - print('║ • $status: $count개'); - }); - - print('╟──────────────────────────────────────────────────────────'); - print('║ 📑 전체 데이터 로드 완료'); - print('║ • View에서 페이지네이션 처리 예정'); - print('╚══════════════════════════════════════════════════════════'); - - // DTO를 UnifiedEquipment로 변환 (status 정보 포함) - final List unifiedEquipments = apiEquipmentDtos.map((dto) { - final equipment = Equipment( - id: dto.id, - manufacturer: dto.manufacturer, - name: dto.modelName ?? dto.equipmentNumber, - category: '', // 세부 정보는 상세 조회에서 가져와야 함 - subCategory: '', - subSubCategory: '', - serialNumber: dto.serialNumber, - quantity: 1, - inDate: dto.createdAt, - ); - - return UnifiedEquipment( - id: dto.id, - equipment: equipment, - date: dto.createdAt, - status: EquipmentStatusConverter.serverToClient(dto.status), // 서버 status를 클라이언트 status로 변환 - ); - }).toList(); - - equipments = unifiedEquipments; - _hasMore = false; // 전체 데이터를 로드했으므로 더 이상 로드할 필요 없음 - - selectedEquipmentIds.clear(); - } on Failure catch (e) { - _error = e.message; - } catch (e) { - _error = 'An unexpected error occurred: $e'; - } finally { - _isLoading = false; - notifyListeners(); - } - } - - // 상태 필터 변경 - Future changeStatusFilter(String? status) async { - selectedStatusFilter = status; - await loadData(isRefresh: true); - } - - // 검색어 변경 - Future updateSearchKeyword(String keyword) async { - searchKeyword = keyword; - await loadData(isRefresh: true, search: keyword); - } - - // 장비 선택/해제 (모든 상태 지원) - void selectEquipment(int? id, String status, bool? isSelected) { - if (id == null || isSelected == null) return; - final key = '$id:$status'; - if (isSelected) { - selectedEquipmentIds.add(key); - } else { - selectedEquipmentIds.remove(key); - } - notifyListeners(); - } - - // 선택된 입고 장비 수 반환 - int getSelectedInStockCount() { - int count = 0; - for (final idStatusPair in selectedEquipmentIds) { - final parts = idStatusPair.split(':'); - if (parts.length == 2 && parts[1] == EquipmentStatus.in_) { - count++; - } - } - return count; - } - - // 선택된 전체 장비 수 반환 - int getSelectedEquipmentCount() { - return selectedEquipmentIds.length; - } - - // 선택된 특정 상태의 장비 수 반환 - int getSelectedEquipmentCountByStatus(String status) { - int count = 0; - for (final idStatusPair in selectedEquipmentIds) { - final parts = idStatusPair.split(':'); - if (parts.length == 2 && parts[1] == status) { - count++; - } - } - return count; - } - - // 선택된 장비들의 UnifiedEquipment 객체 목록 반환 - List getSelectedEquipments() { - List selected = []; - for (final idStatusPair in selectedEquipmentIds) { - final parts = idStatusPair.split(':'); - if (parts.length == 2) { - final id = int.tryParse(parts[0]); - if (id != null) { - final equipment = equipments.firstWhere( - (e) => e.id == id && e.status == parts[1], - orElse: () => null as UnifiedEquipment, - ); - if (equipment != null) { - selected.add(equipment); - } - } - } - } - return selected; - } - - // 선택된 특정 상태의 장비들의 UnifiedEquipment 객체 목록 반환 - List getSelectedEquipmentsByStatus(String status) { - List selected = []; - for (final idStatusPair in selectedEquipmentIds) { - final parts = idStatusPair.split(':'); - if (parts.length == 2 && parts[1] == status) { - final id = int.tryParse(parts[0]); - if (id != null) { - final equipment = equipments.firstWhere( - (e) => e.id == id && e.status == status, - orElse: () => null as UnifiedEquipment, - ); - if (equipment != null) { - selected.add(equipment); - } - } - } - } - return selected; - } - - // 선택된 장비들의 요약 정보를 Map 형태로 반환 (출고/대여/폐기 폼에서 사용) - List> getSelectedEquipmentsSummary() { - List> summaryList = []; - List selectedEquipmentsInStock = - getSelectedEquipmentsByStatus(EquipmentStatus.in_); - - for (final equipment in selectedEquipmentsInStock) { - summaryList.add({ - 'equipment': equipment.equipment, - 'equipmentInId': equipment.id, - 'status': equipment.status, - }); - } - - return summaryList; - } - - // 출고 정보(회사, 담당자, 라이센스 등) 반환 - // 출고 정보는 API를 통해 번별로 조회해야 하므로 별도 서비스로 분리 예정 - String getOutEquipmentInfo(int equipmentId, String infoType) { - // TODO: API로 출고 정보 조회 구현 - return '-'; - } - - // 장비 삭제 - Future deleteEquipment(UnifiedEquipment equipment) async { - try { - // API를 통한 삭제 - if (equipment.equipment.id != null) { - await _equipmentService.deleteEquipment(equipment.equipment.id!); - } else { - throw Exception('Equipment ID is null'); - } - - // 로컬 리스트에서도 제거 - equipments.removeWhere((e) => e.id == equipment.id && e.status == equipment.status); - notifyListeners(); - - return true; - } on Failure catch (e) { - _error = e.message; - notifyListeners(); - return false; - } catch (e) { - _error = 'Failed to delete equipment: $e'; - notifyListeners(); - return false; - } - } - - // API만 사용하므로 토글 기능 제거 - - // 에러 처리 - void clearError() { - _error = null; - notifyListeners(); - } - - @override - void dispose() { - super.dispose(); - } -} diff --git a/lib/screens/equipment/controllers/equipment_list_controller.dart b/lib/screens/equipment/controllers/equipment_list_controller.dart index 7b87705..2092228 100644 --- a/lib/screens/equipment/controllers/equipment_list_controller.dart +++ b/lib/screens/equipment/controllers/equipment_list_controller.dart @@ -79,7 +79,7 @@ class EquipmentListController extends BaseListController { } // DTO를 UnifiedEquipment로 변환 - final items = apiEquipmentDtos.map((dto) { + final items = apiEquipmentDtos.items.map((dto) { final equipment = Equipment( id: dto.id, manufacturer: dto.manufacturer ?? 'Unknown', @@ -109,7 +109,7 @@ class EquipmentListController extends BaseListController { perPage: params.perPage, total: items.length < params.perPage ? (params.page - 1) * params.perPage + items.length : - params.page * params.perPage + 1, + (params.page * params.perPage) + 1, totalPages: items.length < params.perPage ? params.page : params.page + 1, hasNext: items.length >= params.perPage, hasPrevious: params.page > 1, diff --git a/lib/screens/equipment/controllers/equipment_out_form_controller.dart b/lib/screens/equipment/controllers/equipment_out_form_controller.dart index d8670b7..3156b1a 100644 --- a/lib/screens/equipment/controllers/equipment_out_form_controller.dart +++ b/lib/screens/equipment/controllers/equipment_out_form_controller.dart @@ -79,8 +79,8 @@ class EquipmentOutFormController extends ChangeNotifier { Future loadDropdownData() async { try { // API를 통해 회사 목록 로드 - final allCompanies = await _companyService.getCompanies(); - companies = allCompanies + final response = await _companyService.getCompanies(); + companies = response.items .where((c) => c.companyTypes.contains(CompanyType.customer)) .map((c) => CompanyBranchInfo( id: c.id, @@ -204,9 +204,9 @@ class EquipmentOutFormController extends ChangeNotifier { // 선택된 회사 정보에서 ID 추출 if (selectedCompanies[0] != null) { - final companies = await companyService.getCompanies(search: selectedCompanies[0]); - if (companies.isNotEmpty) { - companyId = companies.first.id; + final response = await companyService.getCompanies(search: selectedCompanies[0]); + if (response.items.isNotEmpty) { + companyId = response.items.first.id; // TODO: 지점 ID 처리 로직 추가 } } diff --git a/lib/screens/equipment/equipment_in_form.dart b/lib/screens/equipment/equipment_in_form.dart index 218688b..0416c62 100644 --- a/lib/screens/equipment/equipment_in_form.dart +++ b/lib/screens/equipment/equipment_in_form.dart @@ -3,7 +3,7 @@ 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' hide FormFieldWrapper; -import 'package:superport/screens/common/theme_tailwind.dart'; +import 'package:superport/screens/common/theme_shadcn.dart'; import 'package:superport/screens/common/templates/form_layout_template.dart'; import 'package:superport/utils/constants.dart'; // import 'package:flutter_localizations/flutter_localizations.dart'; @@ -2163,7 +2163,7 @@ class _EquipmentInFormScreenState extends State { children: [ Text( '${_controller.inDate.year}-${_controller.inDate.month.toString().padLeft(2, '0')}-${_controller.inDate.day.toString().padLeft(2, '0')}', - style: AppThemeTailwind.bodyStyle, + style: ShadcnTheme.bodyMedium, ), const Icon(Icons.calendar_today, size: 20), ], @@ -2258,7 +2258,7 @@ class _EquipmentInFormScreenState extends State { Expanded( child: Text( '${_controller.warrantyStartDate.year}-${_controller.warrantyStartDate.month.toString().padLeft(2, '0')}-${_controller.warrantyStartDate.day.toString().padLeft(2, '0')}', - style: AppThemeTailwind.bodyStyle, + style: ShadcnTheme.bodyMedium, overflow: TextOverflow.ellipsis, ), ), @@ -2308,7 +2308,7 @@ class _EquipmentInFormScreenState extends State { Expanded( child: Text( '${_controller.warrantyEndDate.year}-${_controller.warrantyEndDate.month.toString().padLeft(2, '0')}-${_controller.warrantyEndDate.day.toString().padLeft(2, '0')}', - style: AppThemeTailwind.bodyStyle, + style: ShadcnTheme.bodyMedium, overflow: TextOverflow.ellipsis, ), ), diff --git a/lib/screens/equipment/equipment_in_form_lookup_example.dart b/lib/screens/equipment/equipment_in_form_lookup_example.dart deleted file mode 100644 index c76fd00..0000000 --- a/lib/screens/equipment/equipment_in_form_lookup_example.dart +++ /dev/null @@ -1,484 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:get_it/get_it.dart'; -import 'package:provider/provider.dart'; -import '../../services/lookup_service.dart'; -import '../../data/models/lookups/lookup_data.dart'; -import '../common/theme_shadcn.dart'; -import '../common/components/shadcn_components.dart'; - -/// LookupService를 활용한 장비 입고 폼 예시 -/// 전역 캐싱된 Lookup 데이터를 활용하여 드롭다운 구성 -class EquipmentInFormLookupExample extends StatefulWidget { - const EquipmentInFormLookupExample({super.key}); - - @override - State createState() => _EquipmentInFormLookupExampleState(); -} - -class _EquipmentInFormLookupExampleState extends State { - late final LookupService _lookupService; - - // 선택된 값들 - String? _selectedEquipmentType; - String? _selectedEquipmentStatus; - String? _selectedManufacturer; - String? _selectedLicenseType; - - // 텍스트 컨트롤러 - final _serialNumberController = TextEditingController(); - final _quantityController = TextEditingController(); - final _descriptionController = TextEditingController(); - - @override - void initState() { - super.initState(); - _lookupService = GetIt.instance(); - _loadLookupDataIfNeeded(); - } - - /// 필요시 Lookup 데이터 로드 (캐시가 없을 경우) - Future _loadLookupDataIfNeeded() async { - if (!_lookupService.hasData) { - await _lookupService.loadAllLookups(); - if (mounted) { - setState(() {}); // UI 업데이트 - } - } - } - - @override - void dispose() { - _serialNumberController.dispose(); - _quantityController.dispose(); - _descriptionController.dispose(); - super.dispose(); - } - - @override - Widget build(BuildContext context) { - return Scaffold( - backgroundColor: ShadcnTheme.background, - appBar: AppBar( - title: const Text('장비 입고 (Lookup 활용 예시)'), - backgroundColor: ShadcnTheme.card, - elevation: 0, - ), - body: ChangeNotifierProvider.value( - value: _lookupService, - child: Consumer( - builder: (context, lookupService, child) { - if (lookupService.isLoading) { - return const Center( - child: CircularProgressIndicator(), - ); - } - - if (lookupService.error != null) { - return Center( - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Icon(Icons.error_outline, - size: 64, - color: ShadcnTheme.destructive, - ), - const SizedBox(height: 16), - Text( - 'Lookup 데이터 로드 실패', - style: ShadcnTheme.headingH4, - ), - const SizedBox(height: 8), - Text( - lookupService.error!, - style: ShadcnTheme.bodyMuted, - ), - const SizedBox(height: 16), - ShadcnButton( - text: '다시 시도', - onPressed: () => lookupService.loadAllLookups(forceRefresh: true), - ), - ], - ), - ); - } - - return SingleChildScrollView( - padding: const EdgeInsets.all(24), - child: Center( - child: Container( - constraints: const BoxConstraints(maxWidth: 800), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - // 안내 메시지 - Container( - padding: const EdgeInsets.all(16), - decoration: BoxDecoration( - color: ShadcnTheme.primary.withValues(alpha: 0.1), - border: Border.all( - color: ShadcnTheme.primary.withValues(alpha: 0.3), - ), - borderRadius: BorderRadius.circular(8), - ), - child: Row( - children: [ - Icon(Icons.info_outline, - color: ShadcnTheme.primary, - size: 20, - ), - const SizedBox(width: 12), - Expanded( - child: Text( - '이 화면은 /lookups API를 통해 캐싱된 전역 데이터를 활용합니다.\n' - '드롭다운 데이터는 앱 시작 시 한 번만 로드되어 모든 화면에서 재사용됩니다.', - style: ShadcnTheme.bodySmall, - ), - ), - ], - ), - ), - - const SizedBox(height: 24), - - // 폼 카드 - ShadcnCard( - padding: const EdgeInsets.all(24), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text('장비 정보', style: ShadcnTheme.headingH4), - const SizedBox(height: 24), - - // 장비 타입 드롭다운 - _buildDropdownField( - label: '장비 타입', - value: _selectedEquipmentType, - items: lookupService.equipmentTypes, - onChanged: (value) { - setState(() { - _selectedEquipmentType = value; - }); - }, - ), - - // 장비 상태 드롭다운 - _buildDropdownField( - label: '장비 상태', - value: _selectedEquipmentStatus, - items: lookupService.equipmentStatuses, - onChanged: (value) { - setState(() { - _selectedEquipmentStatus = value; - }); - }, - ), - - // 제조사 드롭다운 - _buildDropdownField( - label: '제조사', - value: _selectedManufacturer, - items: lookupService.manufacturers, - onChanged: (value) { - setState(() { - _selectedManufacturer = value; - }); - }, - ), - - // 시리얼 번호 입력 - _buildTextField( - label: '시리얼 번호', - controller: _serialNumberController, - hintText: 'SN-2025-001', - ), - - // 수량 입력 - _buildTextField( - label: '수량', - controller: _quantityController, - hintText: '1', - keyboardType: TextInputType.number, - ), - - // 라이선스 타입 드롭다운 (옵션) - _buildDropdownField( - label: '라이선스 타입 (선택)', - value: _selectedLicenseType, - items: lookupService.licenseTypes, - onChanged: (value) { - setState(() { - _selectedLicenseType = value; - }); - }, - isOptional: true, - ), - - // 비고 입력 - _buildTextField( - label: '비고', - controller: _descriptionController, - hintText: '추가 정보를 입력하세요', - maxLines: 3, - ), - - const SizedBox(height: 32), - - // 버튼 그룹 - Row( - mainAxisAlignment: MainAxisAlignment.end, - children: [ - ShadcnButton( - text: '취소', - variant: ShadcnButtonVariant.secondary, - onPressed: () => Navigator.pop(context), - ), - const SizedBox(width: 12), - ShadcnButton( - text: '저장', - onPressed: _handleSubmit, - ), - ], - ), - ], - ), - ), - - const SizedBox(height: 24), - - // 캐시 정보 표시 - _buildCacheInfoCard(lookupService), - ], - ), - ), - ), - ); - }, - ), - ), - ); - } - - /// 드롭다운 필드 빌더 - Widget _buildDropdownField({ - required String label, - required String? value, - required List items, - required ValueChanged onChanged, - bool isOptional = false, - }) { - return Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Row( - children: [ - Text(label, style: ShadcnTheme.bodyMedium), - if (isOptional) ...[ - const SizedBox(width: 4), - Text('(선택)', style: ShadcnTheme.bodyMuted), - ], - ], - ), - const SizedBox(height: 8), - Container( - decoration: BoxDecoration( - border: Border.all(color: ShadcnTheme.border), - borderRadius: BorderRadius.circular(6), - ), - child: DropdownButtonFormField( - value: value, - decoration: InputDecoration( - contentPadding: const EdgeInsets.symmetric( - horizontal: 12, - vertical: 8, - ), - border: InputBorder.none, - hintText: '선택하세요', - hintStyle: ShadcnTheme.bodyMuted, - ), - items: items.map((item) => DropdownMenuItem( - value: item.code ?? '', - child: Text(item.name ?? ''), - )).toList(), - onChanged: onChanged, - ), - ), - const SizedBox(height: 16), - ], - ); - } - - /// 텍스트 필드 빌더 - Widget _buildTextField({ - required String label, - required TextEditingController controller, - String? hintText, - TextInputType? keyboardType, - int maxLines = 1, - }) { - return Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text(label, style: ShadcnTheme.bodyMedium), - const SizedBox(height: 8), - TextFormField( - controller: controller, - keyboardType: keyboardType, - maxLines: maxLines, - decoration: InputDecoration( - hintText: hintText, - hintStyle: ShadcnTheme.bodyMuted, - border: OutlineInputBorder( - borderRadius: BorderRadius.circular(6), - borderSide: BorderSide(color: ShadcnTheme.border), - ), - enabledBorder: OutlineInputBorder( - borderRadius: BorderRadius.circular(6), - borderSide: BorderSide(color: ShadcnTheme.border), - ), - focusedBorder: OutlineInputBorder( - borderRadius: BorderRadius.circular(6), - borderSide: BorderSide(color: ShadcnTheme.primary, width: 2), - ), - contentPadding: const EdgeInsets.symmetric( - horizontal: 12, - vertical: 8, - ), - ), - ), - const SizedBox(height: 16), - ], - ); - } - - /// 캐시 정보 카드 - Widget _buildCacheInfoCard(LookupService lookupService) { - return ShadcnCard( - padding: const EdgeInsets.all(16), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Row( - children: [ - Icon(Icons.storage, size: 20, color: ShadcnTheme.muted), - const SizedBox(width: 8), - Text('Lookup 캐시 정보', style: ShadcnTheme.bodyMedium), - ], - ), - const SizedBox(height: 12), - _buildCacheItem('장비 타입', lookupService.equipmentTypes.length), - _buildCacheItem('장비 상태', lookupService.equipmentStatuses.length), - _buildCacheItem('제조사', lookupService.manufacturers.length), - _buildCacheItem('라이선스 타입', lookupService.licenseTypes.length), - _buildCacheItem('사용자 역할', lookupService.userRoles.length), - _buildCacheItem('회사 상태', lookupService.companyStatuses.length), - const SizedBox(height: 12), - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Text( - '캐시 상태: ${lookupService.isCacheValid ? "유효" : "만료"}', - style: ShadcnTheme.bodySmall.copyWith( - color: lookupService.isCacheValid - ? ShadcnTheme.success - : ShadcnTheme.warning, - ), - ), - TextButton( - onPressed: () => lookupService.loadAllLookups(forceRefresh: true), - child: Row( - mainAxisSize: MainAxisSize.min, - children: [ - Icon(Icons.refresh, size: 16, color: ShadcnTheme.primary), - const SizedBox(width: 4), - Text('캐시 새로고침', - style: TextStyle(color: ShadcnTheme.primary), - ), - ], - ), - ), - ], - ), - ], - ), - ); - } - - Widget _buildCacheItem(String label, int count) { - return Padding( - padding: const EdgeInsets.symmetric(vertical: 2), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Text(label, style: ShadcnTheme.bodySmall), - Text('$count개', - style: ShadcnTheme.bodySmall.copyWith( - color: ShadcnTheme.muted, - ), - ), - ], - ), - ); - } - - /// 폼 제출 처리 - void _handleSubmit() { - // 유효성 검증 - if (_selectedEquipmentType == null) { - _showSnackBar('장비 타입을 선택하세요', isError: true); - return; - } - - if (_selectedEquipmentStatus == null) { - _showSnackBar('장비 상태를 선택하세요', isError: true); - return; - } - - if (_serialNumberController.text.isEmpty) { - _showSnackBar('시리얼 번호를 입력하세요', isError: true); - return; - } - - // 선택된 값 정보 표시 - final selectedType = _lookupService.findByCode( - _lookupService.equipmentTypes, - _selectedEquipmentType!, - ); - - final selectedStatus = _lookupService.findByCode( - _lookupService.equipmentStatuses, - _selectedEquipmentStatus!, - ); - - final message = ''' -장비 입고 정보: -- 타입: ${selectedType?.name ?? _selectedEquipmentType} -- 상태: ${selectedStatus?.name ?? _selectedEquipmentStatus} -- 시리얼: ${_serialNumberController.text} -- 수량: ${_quantityController.text.isEmpty ? "1" : _quantityController.text} -'''; - - showDialog( - context: context, - builder: (context) => AlertDialog( - title: const Text('입고 정보 확인'), - content: Text(message), - actions: [ - TextButton( - onPressed: () => Navigator.pop(context), - child: const Text('확인'), - ), - ], - ), - ); - } - - void _showSnackBar(String message, {bool isError = false}) { - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text(message), - backgroundColor: isError ? ShadcnTheme.destructive : ShadcnTheme.primary, - duration: const Duration(seconds: 2), - ), - ); - } -} \ No newline at end of file diff --git a/lib/screens/equipment/equipment_list_redesign.dart b/lib/screens/equipment/equipment_list.dart similarity index 97% rename from lib/screens/equipment/equipment_list_redesign.dart rename to lib/screens/equipment/equipment_list.dart index b0f8c73..13564eb 100644 --- a/lib/screens/equipment/equipment_list_redesign.dart +++ b/lib/screens/equipment/equipment_list.dart @@ -15,17 +15,17 @@ import 'package:superport/utils/equipment_display_helper.dart'; import 'package:superport/screens/equipment/widgets/equipment_history_dialog.dart'; /// shadcn/ui 스타일로 재설계된 장비 관리 화면 -class EquipmentListRedesign extends StatefulWidget { +class EquipmentList extends StatefulWidget { final String currentRoute; - const EquipmentListRedesign({Key? key, this.currentRoute = Routes.equipment}) + const EquipmentList({Key? key, this.currentRoute = Routes.equipment}) : super(key: key); @override - State createState() => _EquipmentListRedesignState(); + State createState() => _EquipmentListState(); } -class _EquipmentListRedesignState extends State { +class _EquipmentListState extends State { late final EquipmentListController _controller; bool _showDetailedColumns = true; final TextEditingController _searchController = TextEditingController(); @@ -34,14 +34,14 @@ class _EquipmentListRedesignState extends State { String _selectedStatus = 'all'; // String _searchKeyword = ''; // Removed - unused field String _appliedSearchKeyword = ''; - int _currentPage = 1; - final int _pageSize = 10; + // 페이지 상태는 이제 Controller에서 관리 final Set _selectedItems = {}; @override void initState() { super.initState(); _controller = EquipmentListController(); + _controller.pageSize = 10; // 페이지 크기 설정 _setInitialFilter(); // API 호출을 위해 Future로 변경 @@ -113,7 +113,7 @@ class _EquipmentListRedesignState extends State { } else if (status == 'rent') { _controller.selectedStatusFilter = EquipmentStatus.rent; } - _currentPage = 1; + _controller.goToPage(1); }); _controller.changeStatusFilter(_controller.selectedStatusFilter); } @@ -122,7 +122,7 @@ class _EquipmentListRedesignState extends State { void _onSearch() async { setState(() { _appliedSearchKeyword = _searchController.text; - _currentPage = 1; + _controller.goToPage(1); }); _controller.updateSearchKeyword(_searchController.text); } @@ -414,14 +414,12 @@ class _EquipmentListRedesignState extends State { dataTable: _buildDataTable(filteredEquipments), // 페이지네이션 - pagination: totalCount > _pageSize ? Pagination( + pagination: totalCount > controller.pageSize ? Pagination( totalCount: totalCount, - currentPage: _currentPage, - pageSize: _pageSize, + currentPage: controller.currentPage, + pageSize: controller.pageSize, onPageChanged: (page) { - setState(() { - _currentPage = page; - }); + controller.goToPage(page); }, ) : null, ); @@ -515,7 +513,7 @@ class _EquipmentListRedesignState extends State { onRefresh: () { setState(() { _controller.loadData(); - _currentPage = 1; + _controller.goToPage(1); }); }, statusMessage: @@ -548,7 +546,7 @@ class _EquipmentListRedesignState extends State { if (result == true) { setState(() { _controller.loadData(); - _currentPage = 1; + _controller.goToPage(1); }); } }, @@ -623,7 +621,7 @@ class _EquipmentListRedesignState extends State { if (result == true) { setState(() { _controller.loadData(); - _currentPage = 1; + _controller.goToPage(1); }); } }, @@ -829,7 +827,7 @@ class _EquipmentListRedesignState extends State { // 번호 _buildDataCell( Text( - '${((_currentPage - 1) * _pageSize) + index + 1}', + '${((_controller.currentPage - 1) * _controller.pageSize) + index + 1}', style: ShadcnTheme.bodySmall, ), flex: 1, @@ -946,11 +944,11 @@ class _EquipmentListRedesignState extends State { /// 데이터 테이블 Widget _buildDataTable(List filteredEquipments) { - final int startIndex = (_currentPage - 1) * _pageSize; + final int startIndex = (_controller.currentPage - 1) * _controller.pageSize; final int endIndex = - (startIndex + _pageSize) > filteredEquipments.length + (startIndex + _controller.pageSize) > filteredEquipments.length ? filteredEquipments.length - : (startIndex + _pageSize); + : (startIndex + _controller.pageSize); final List pagedEquipments = filteredEquipments.sublist( startIndex, endIndex, @@ -975,7 +973,7 @@ class _EquipmentListRedesignState extends State { if (result == true) { setState(() { _controller.loadData(); - _currentPage = 1; + _controller.goToPage(1); }); } }, @@ -1176,8 +1174,8 @@ class _EquipmentListRedesignState extends State { /// 페이지 데이터 가져오기 List _getPagedEquipments() { final filteredEquipments = _getFilteredEquipments(); - final int startIndex = (_currentPage - 1) * _pageSize; - final int endIndex = startIndex + _pageSize; + final int startIndex = (_controller.currentPage - 1) * _controller.pageSize; + final int endIndex = startIndex + _controller.pageSize; if (startIndex >= filteredEquipments.length) { return []; diff --git a/lib/screens/equipment/equipment_out_form.dart b/lib/screens/equipment/equipment_out_form.dart index 250c912..83fe245 100644 --- a/lib/screens/equipment/equipment_out_form.dart +++ b/lib/screens/equipment/equipment_out_form.dart @@ -6,7 +6,7 @@ import 'package:superport/models/company_model.dart'; import 'package:superport/models/company_branch_info.dart'; import 'package:superport/models/address_model.dart'; import 'package:superport/screens/common/custom_widgets.dart'; -import 'package:superport/screens/common/theme_tailwind.dart'; +import 'package:superport/screens/common/theme_shadcn.dart'; import 'package:superport/screens/equipment/controllers/equipment_out_form_controller.dart'; import 'package:superport/screens/equipment/widgets/equipment_summary_card.dart'; import 'package:superport/screens/equipment/widgets/equipment_summary_row.dart'; @@ -421,7 +421,10 @@ class _EquipmentOutFormScreenState extends State { : null, style: canSubmit - ? AppThemeTailwind.primaryButtonStyle + ? ElevatedButton.styleFrom( + backgroundColor: ShadcnTheme.primary, + foregroundColor: Colors.white, + ) : ElevatedButton.styleFrom( backgroundColor: Colors.grey.shade300, foregroundColor: Colors.grey.shade700, @@ -600,7 +603,7 @@ class _EquipmentOutFormScreenState extends State { company.contactName!.isNotEmpty) Text( '${company.contactName} ${company.contactPosition ?? ""} ${company.contactPhone ?? ""} ${company.contactEmail ?? ""}', - style: AppThemeTailwind.bodyStyle, + style: ShadcnTheme.bodyMedium, ), if (!companyInfo.isMainCompany && branch != null && @@ -608,7 +611,7 @@ class _EquipmentOutFormScreenState extends State { branch.contactName!.isNotEmpty) Text( '${branch.contactName} ${branch.contactPosition ?? ""} ${branch.contactPhone ?? ""} ${branch.contactEmail ?? ""}', - style: AppThemeTailwind.bodyStyle, + style: ShadcnTheme.bodyMedium, ), const SizedBox(height: 8), // 담당자 목록에서 실제 담당자 정보만 표시하는 부분은 제거 @@ -686,7 +689,7 @@ class _EquipmentOutFormScreenState extends State { children: [ Text( controller.formatDate(date), - style: AppThemeTailwind.bodyStyle, + style: ShadcnTheme.bodyMedium, ), const Icon(Icons.calendar_today, size: 20), ], @@ -817,7 +820,7 @@ class _EquipmentOutFormScreenState extends State { userWidgets.add( Text( '정수진 사원 010-4567-8901 jung.soojin@lg.com', - style: AppThemeTailwind.bodyStyle, + style: ShadcnTheme.bodyMedium, ), ); } diff --git a/lib/screens/license/controllers/license_list_controller.backup.dart b/lib/screens/license/controllers/license_list_controller.backup.dart deleted file mode 100644 index da4a38a..0000000 --- a/lib/screens/license/controllers/license_list_controller.backup.dart +++ /dev/null @@ -1,467 +0,0 @@ -import 'dart:async'; -import 'package:flutter/material.dart'; -import 'package:get_it/get_it.dart'; -import 'package:superport/core/errors/failures.dart'; -import 'package:superport/models/license_model.dart'; -import 'package:superport/services/license_service.dart'; - -// 라이센스 상태 필터 -enum LicenseStatusFilter { - all, - active, - inactive, - expiringSoon, // 30일 이내 - expired, -} - -// 라이센스 목록 화면의 상태 및 비즈니스 로직을 담당하는 컨트롤러 -class LicenseListController extends ChangeNotifier { - final LicenseService _licenseService = GetIt.instance(); - - List _licenses = []; - List _filteredLicenses = []; - bool _isLoading = false; - String? _error; - String _searchQuery = ''; - int _currentPage = 1; - final int _pageSize = 20; - bool _hasMore = true; - int _total = 0; - - // 필터 옵션 - int? _selectedCompanyId; - bool? _isActive; - String? _licenseType; - LicenseStatusFilter _statusFilter = LicenseStatusFilter.all; - String _sortBy = 'expiry_date'; - String _sortOrder = 'asc'; - - // 선택된 라이선스 관리 - final Set _selectedLicenseIds = {}; - - // 통계 데이터 - Map _statistics = { - 'total': 0, - 'active': 0, - 'inactive': 0, - 'expiringSoon': 0, - 'expired': 0, - }; - - // 검색 디바운스를 위한 타이머 - Timer? _debounceTimer; - - LicenseListController(); - - // Getters - List get licenses => _filteredLicenses; - bool get isLoading => _isLoading; - String? get error => _error; - String get searchQuery => _searchQuery; - int get currentPage => _currentPage; - bool get hasMore => _hasMore; - int get total => _total; - int? get selectedCompanyId => _selectedCompanyId; - bool? get isActive => _isActive; - String? get licenseType => _licenseType; - LicenseStatusFilter get statusFilter => _statusFilter; - Set get selectedLicenseIds => _selectedLicenseIds; - Map get statistics => _statistics; - - // 선택된 라이선스 개수 - int get selectedCount => _selectedLicenseIds.length; - - // 전체 선택 여부 확인 - bool get isAllSelected => - _filteredLicenses.isNotEmpty && - _filteredLicenses.where((l) => l.id != null) - .every((l) => _selectedLicenseIds.contains(l.id)); - - // 데이터 로드 - Future loadData({bool isInitialLoad = true}) async { - if (_isLoading) return; - - _isLoading = true; - _error = null; - notifyListeners(); - - try { - // API 사용 - 전체 데이터 로드 - print('╔══════════════════════════════════════════════════════════'); - print('║ 🔧 유지보수 목록 API 호출 시작'); - print('║ • 회사 필터: ${_selectedCompanyId ?? "전체"}'); - print('║ • 활성 필터: ${_isActive != null ? (_isActive! ? "활성" : "비활성") : "전체"}'); - print('║ • 라이센스 타입: ${_licenseType ?? "전체"}'); - print('╚══════════════════════════════════════════════════════════'); - - // 전체 데이터를 가져오기 위해 큰 perPage 값 사용 - final fetchedLicenses = await _licenseService.getLicenses( - page: 1, - perPage: 1000, // 충분히 큰 값으로 전체 데이터 로드 - isActive: _isActive, - companyId: _selectedCompanyId, - licenseType: _licenseType, - ); - - print('╔══════════════════════════════════════════════════════════'); - print('║ 📊 유지보수 목록 로드 완료'); - print('║ ▶ 총 라이센스 수: ${fetchedLicenses.length}개'); - print('╟──────────────────────────────────────────────────────────'); - - // 상태별 통계 - int activeCount = 0; - int expiringSoonCount = 0; - int expiredCount = 0; - final now = DateTime.now(); - - for (final license in fetchedLicenses) { - if (license.expiryDate != null) { - final daysUntil = license.expiryDate!.difference(now).inDays; - if (daysUntil < 0) { - expiredCount++; - } else if (daysUntil <= 30) { - expiringSoonCount++; - } else { - activeCount++; - } - } else { - activeCount++; - } - } - - print('║ • 활성: $activeCount개'); - print('║ • 만료 임박 (30일 이내): $expiringSoonCount개'); - print('║ • 만료됨: $expiredCount개'); - - print('╟──────────────────────────────────────────────────────────'); - print('║ 📑 전체 데이터 로드 완료'); - print('║ • View에서 페이지네이션 처리 예정'); - print('╚══════════════════════════════════════════════════════════'); - - _licenses = fetchedLicenses; - _hasMore = false; // 전체 데이터를 로드했으므로 더 이상 로드할 필요 없음 - _total = fetchedLicenses.length; - - debugPrint('📑 _applySearchFilter 호출 전: _licenses=${_licenses.length}개'); - _applySearchFilter(); - _applyStatusFilter(); - await _updateStatistics(); - debugPrint('📑 _applySearchFilter 호출 후: _filteredLicenses=${_filteredLicenses.length}개'); - } catch (e) { - debugPrint('❌ loadData 에러 발생: $e'); - _error = e.toString(); - } finally { - _isLoading = false; - debugPrint('📑 loadData 종료: _filteredLicenses=${_filteredLicenses.length}개'); - notifyListeners(); - } - } - - // 다음 페이지 로드 - Future loadNextPage() async { - if (!_hasMore || _isLoading) return; - _currentPage++; - await loadData(isInitialLoad: false); - } - - // 검색 (디바운싱 적용) - void search(String query) { - _searchQuery = query; - - // 기존 타이머 취소 - _debounceTimer?.cancel(); - - // API 검색은 디바운싱 적용 (300ms) - _debounceTimer = Timer(const Duration(milliseconds: 300), () { - loadData(); - }); - } - - // 검색 필터 적용 - void _applySearchFilter() { - debugPrint('🔎 _applySearchFilter 시작: _searchQuery="$_searchQuery", _licenses=${_licenses.length}개'); - - if (_searchQuery.isEmpty) { - _filteredLicenses = List.from(_licenses); - debugPrint('🔎 검색어 없음: 전체 복사 ${_filteredLicenses.length}개'); - } else { - _filteredLicenses = _licenses.where((license) { - final productName = license.productName?.toLowerCase() ?? ''; - final licenseKey = license.licenseKey.toLowerCase(); - final vendor = license.vendor?.toLowerCase() ?? ''; - final companyName = license.companyName?.toLowerCase() ?? ''; - final searchLower = _searchQuery.toLowerCase(); - - return productName.contains(searchLower) || - licenseKey.contains(searchLower) || - vendor.contains(searchLower) || - companyName.contains(searchLower); - }).toList(); - debugPrint('🔎 검색 필터링 완료: ${_filteredLicenses.length}개'); - } - } - - // 상태 필터 적용 - void _applyStatusFilter() { - if (_statusFilter == LicenseStatusFilter.all) return; - - final now = DateTime.now(); - _filteredLicenses = _filteredLicenses.where((license) { - switch (_statusFilter) { - case LicenseStatusFilter.active: - return license.isActive; - case LicenseStatusFilter.inactive: - return !license.isActive; - case LicenseStatusFilter.expiringSoon: - if (license.expiryDate != null) { - final days = license.expiryDate!.difference(now).inDays; - return days > 0 && days <= 30; - } - return false; - case LicenseStatusFilter.expired: - if (license.expiryDate != null) { - return license.expiryDate!.isBefore(now); - } - return false; - case LicenseStatusFilter.all: - default: - return true; - } - }).toList(); - } - - // 필터 설정 - void setFilters({ - int? companyId, - bool? isActive, - String? licenseType, - }) { - _selectedCompanyId = companyId; - _isActive = isActive; - _licenseType = licenseType; - loadData(); - } - - // 필터 초기화 - void clearFilters() { - _selectedCompanyId = null; - _isActive = null; - _licenseType = null; - _searchQuery = ''; - loadData(); - } - - // 라이센스 삭제 - Future deleteLicense(int id) async { - try { - await _licenseService.deleteLicense(id); - - // 목록에서 제거 - _licenses.removeWhere((l) => l.id == id); - _applySearchFilter(); - _total--; - notifyListeners(); - } catch (e) { - _error = e.toString(); - notifyListeners(); - } - } - - // 새로고침 - Future refresh() async { - await loadData(); - } - - // 만료 예정 라이선스 조회 - Future> getExpiringLicenses({int days = 30}) async { - try { - return await _licenseService.getExpiringLicenses(days: days); - } catch (e) { - _error = e.toString(); - notifyListeners(); - return []; - } - } - - // 상태별 라이선스 개수 조회 - Future> getLicenseStatusCounts() async { - try { - // API에서 상태별 개수 조회 (실제로는 별도 엔드포인트가 있다면 사용) - final activeCount = await _licenseService.getTotalLicenses(isActive: true); - final inactiveCount = await _licenseService.getTotalLicenses(isActive: false); - final expiringLicenses = await getExpiringLicenses(days: 30); - - return { - 'active': activeCount, - 'inactive': inactiveCount, - 'expiring': expiringLicenses.length, - 'total': activeCount + inactiveCount, - }; - } catch (e) { - return {'active': 0, 'inactive': 0, 'expiring': 0, 'total': 0}; - } - } - - // 정렬 변경 - void sortBy(String field, String order) { - _sortBy = field; - _sortOrder = order; - loadData(); - } - - // 상태 필터 변경 - Future changeStatusFilter(LicenseStatusFilter filter) async { - _statusFilter = filter; - await loadData(); - } - - // 라이선스 선택/해제 - void selectLicense(int? id, bool? isSelected) { - if (id == null) return; - - if (isSelected == true) { - _selectedLicenseIds.add(id); - } else { - _selectedLicenseIds.remove(id); - } - notifyListeners(); - } - - // 전체 선택/해제 - void selectAll(bool? isSelected) { - if (isSelected == true) { - // 현재 필터링된 라이선스 모두 선택 - for (var license in _filteredLicenses) { - if (license.id != null) { - _selectedLicenseIds.add(license.id!); - } - } - } else { - // 모두 해제 - _selectedLicenseIds.clear(); - } - notifyListeners(); - } - - // 선택된 라이선스 목록 반환 - List getSelectedLicenses() { - return _filteredLicenses - .where((l) => l.id != null && _selectedLicenseIds.contains(l.id)) - .toList(); - } - - // 선택 초기화 - void clearSelection() { - _selectedLicenseIds.clear(); - notifyListeners(); - } - - // 라이선스 할당 - Future assignLicense(int licenseId, int userId) async { - try { - await _licenseService.assignLicense(licenseId, userId); - await loadData(); - clearSelection(); - return true; - } catch (e) { - _error = e.toString(); - notifyListeners(); - return false; - } - } - - // 라이선스 할당 해제 - Future unassignLicense(int licenseId) async { - try { - await _licenseService.unassignLicense(licenseId); - await loadData(); - clearSelection(); - return true; - } catch (e) { - _error = e.toString(); - notifyListeners(); - return false; - } - } - - // 선택된 라이선스 일괄 삭제 - Future deleteSelectedLicenses() async { - if (_selectedLicenseIds.isEmpty) return; - - final selectedIds = List.from(_selectedLicenseIds); - int successCount = 0; - int failCount = 0; - - for (var id in selectedIds) { - try { - await deleteLicense(id); - successCount++; - } catch (e) { - failCount++; - debugPrint('라이선스 $id 삭제 실패: $e'); - } - } - - _selectedLicenseIds.clear(); - await loadData(); - - if (successCount > 0) { - debugPrint('✅ $successCount개 라이선스 삭제 완료'); - } - if (failCount > 0) { - debugPrint('❌ $failCount개 라이선스 삭제 실패'); - } - } - - // 통계 업데이트 - Future _updateStatistics() async { - try { - final counts = await getLicenseStatusCounts(); - - final now = DateTime.now(); - int expiringSoonCount = 0; - int expiredCount = 0; - - for (var license in _licenses) { - if (license.expiryDate != null) { - final days = license.expiryDate!.difference(now).inDays; - if (days <= 0) { - expiredCount++; - } else if (days <= 30) { - expiringSoonCount++; - } - } - } - - _statistics = { - 'total': counts['total'] ?? 0, - 'active': counts['active'] ?? 0, - 'inactive': counts['inactive'] ?? 0, - 'expiringSoon': expiringSoonCount, - 'expired': expiredCount, - }; - } catch (e) { - debugPrint('❌ 통계 업데이트 오류: $e'); - // 오류 발생 시 기본값 사용 - _statistics = { - 'total': _licenses.length, - 'active': 0, - 'inactive': 0, - 'expiringSoon': 0, - 'expired': 0, - }; - } - } - - // 만료일까지 남은 일수 계산 - int? getDaysUntilExpiry(License license) { - if (license.expiryDate == null) return null; - return license.expiryDate!.difference(DateTime.now()).inDays; - } - - @override - void dispose() { - _debounceTimer?.cancel(); - super.dispose(); - } -} diff --git a/lib/screens/license/controllers/license_list_controller.dart b/lib/screens/license/controllers/license_list_controller.dart index 5d66f5e..79a600e 100644 --- a/lib/screens/license/controllers/license_list_controller.dart +++ b/lib/screens/license/controllers/license_list_controller.dart @@ -74,8 +74,8 @@ class LicenseListController extends BaseListController { required PaginationParams params, Map? additionalFilters, }) async { - // API 호출 - final fetchedLicenses = await ErrorHandler.handleApiCall( + // API 호출 (PaginatedResponse 반환) + final response = await ErrorHandler.handleApiCall( () => _licenseService.getLicenses( page: params.page, perPage: params.perPage, @@ -88,7 +88,7 @@ class LicenseListController extends BaseListController { }, ); - if (fetchedLicenses == null) { + if (response == null) { return PagedResult( items: [], meta: PaginationMeta( @@ -103,21 +103,19 @@ class LicenseListController extends BaseListController { } // 통계 업데이트 - await _updateStatistics(fetchedLicenses); + await _updateStatistics(response.items); - // 임시로 메타데이터 생성 (추후 API에서 실제 메타데이터 반환 시 수정) + // PaginatedResponse를 PagedResult로 변환 final meta = PaginationMeta( - currentPage: params.page, - perPage: params.perPage, - total: fetchedLicenses.length < params.perPage ? - (params.page - 1) * params.perPage + fetchedLicenses.length : - params.page * params.perPage + 1, - totalPages: fetchedLicenses.length < params.perPage ? params.page : params.page + 1, - hasNext: fetchedLicenses.length >= params.perPage, - hasPrevious: params.page > 1, + currentPage: response.page, + perPage: response.size, + total: response.totalElements, + totalPages: response.totalPages, + hasNext: !response.last, + hasPrevious: !response.first, ); - return PagedResult(items: fetchedLicenses, meta: meta); + return PagedResult(items: response.items, meta: meta); } @override diff --git a/lib/screens/license/license_form.dart b/lib/screens/license/license_form.dart index d0cba88..0b7fd2c 100644 --- a/lib/screens/license/license_form.dart +++ b/lib/screens/license/license_form.dart @@ -2,7 +2,7 @@ import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:superport/models/license_model.dart'; import 'package:superport/screens/license/controllers/license_form_controller.dart'; -import 'package:superport/screens/common/theme_tailwind.dart'; +import 'package:superport/screens/common/theme_shadcn.dart'; import 'package:superport/screens/common/templates/form_layout_template.dart'; import 'package:superport/screens/common/custom_widgets.dart' hide FormFieldWrapper; import 'package:superport/utils/validators.dart'; @@ -109,7 +109,7 @@ class _MaintenanceFormScreenState extends State { ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: Text(message), - backgroundColor: AppThemeTailwind.success, + backgroundColor: ShadcnTheme.success, ), ); Navigator.pop(context, true); diff --git a/lib/screens/license/license_list_redesign.dart b/lib/screens/license/license_list.dart similarity index 96% rename from lib/screens/license/license_list_redesign.dart rename to lib/screens/license/license_list.dart index c009664..ef2498f 100644 --- a/lib/screens/license/license_list_redesign.dart +++ b/lib/screens/license/license_list.dart @@ -15,20 +15,19 @@ import 'package:superport/core/config/environment.dart' as env; import 'package:intl/intl.dart'; /// shadcn/ui 스타일로 재설계된 유지보수 라이선스 관리 화면 -class LicenseListRedesign extends StatefulWidget { - const LicenseListRedesign({super.key}); +class LicenseList extends StatefulWidget { + const LicenseList({super.key}); @override - State createState() => _LicenseListRedesignState(); + State createState() => _LicenseListState(); } -class _LicenseListRedesignState extends State { +class _LicenseListState extends State { late final LicenseListController _controller; // MockDataService 제거 - 실제 API 사용 final TextEditingController _searchController = TextEditingController(); final ScrollController _horizontalScrollController = ScrollController(); - int _currentPage = 1; - final int _pageSize = 10; + // 페이지 상태는 이제 Controller에서 관리 // 날짜 포맷터 final DateFormat _dateFormat = DateFormat('yyyy-MM-dd'); @@ -69,15 +68,9 @@ class _LicenseListRedesignState extends State { super.dispose(); } - /// 페이지네이션용 라이선스 가져오기 + /// 페이지네이션용 라이선스 가져오기 (Controller가 이미 페이징된 데이터 제공) List _getPagedLicenses() { - final licenses = _controller.licenses; - final int startIndex = (_currentPage - 1) * _pageSize; - final int endIndex = startIndex + _pageSize; - return licenses.sublist( - startIndex, - endIndex > licenses.length ? licenses.length : endIndex, - ); + return _controller.licenses; // 이미 페이징된 데이터 } /// 검색 실행 @@ -253,15 +246,13 @@ class _LicenseListRedesignState extends State { searchBar: _buildSearchBar(), actionBar: _buildActionBar(), dataTable: _buildDataTable(), - pagination: totalCount > _pageSize + pagination: totalCount > controller.pageSize ? Pagination( totalCount: totalCount, - currentPage: _currentPage, - pageSize: _pageSize, + currentPage: controller.currentPage, + pageSize: controller.pageSize, onPageChanged: (page) { - setState(() { - _currentPage = page; - }); + controller.goToPage(page); }, ) : null, @@ -606,7 +597,7 @@ class _LicenseListRedesignState extends State { ...pagedLicenses.asMap().entries.map((entry) { final displayIndex = entry.key; final license = entry.value; - final index = (_currentPage - 1) * _pageSize + displayIndex; + final index = (_controller.currentPage - 1) * _controller.pageSize + displayIndex; final daysRemaining = _controller.getDaysUntilExpiry(license.expiryDate); return Container( diff --git a/lib/screens/login/login_screen.dart b/lib/screens/login/login_screen.dart index ca5de8d..036b67e 100644 --- a/lib/screens/login/login_screen.dart +++ b/lib/screens/login/login_screen.dart @@ -1,6 +1,6 @@ import 'package:flutter/material.dart'; import 'package:superport/screens/login/controllers/login_controller.dart'; -import 'package:superport/screens/login/widgets/login_view_redesign.dart'; +import 'package:superport/screens/login/widgets/login_view.dart'; /// 로그인 화면 진입점 (상태/로직은 controller, UI는 LoginView 위젯에 위임) class LoginScreen extends StatefulWidget { @@ -31,7 +31,7 @@ class _LoginScreenState extends State { @override Widget build(BuildContext context) { - return LoginViewRedesign( + return LoginView( controller: _controller, onLoginSuccess: _onLoginSuccess, ); diff --git a/lib/screens/login/widgets/login_view_redesign.dart b/lib/screens/login/widgets/login_view.dart similarity index 98% rename from lib/screens/login/widgets/login_view_redesign.dart rename to lib/screens/login/widgets/login_view.dart index 516bbb8..0331aa5 100644 --- a/lib/screens/login/widgets/login_view_redesign.dart +++ b/lib/screens/login/widgets/login_view.dart @@ -5,21 +5,21 @@ import 'package:superport/screens/login/controllers/login_controller.dart'; import 'dart:math' as math; /// shadcn/ui 스타일로 재설계된 로그인 화면 -class LoginViewRedesign extends StatefulWidget { +class LoginView extends StatefulWidget { final LoginController controller; final VoidCallback onLoginSuccess; - const LoginViewRedesign({ + const LoginView({ Key? key, required this.controller, required this.onLoginSuccess, }) : super(key: key); @override - State createState() => _LoginViewRedesignState(); + State createState() => _LoginViewState(); } -class _LoginViewRedesignState extends State +class _LoginViewState extends State with TickerProviderStateMixin { late AnimationController _fadeController; late Animation _fadeAnimation; @@ -335,4 +335,4 @@ class _LoginViewRedesignState extends State ], ); } -} +} \ No newline at end of file diff --git a/lib/screens/overview/controllers/overview_controller.dart b/lib/screens/overview/controllers/overview_controller.dart index 3a56420..a5cd02f 100644 --- a/lib/screens/overview/controllers/overview_controller.dart +++ b/lib/screens/overview/controllers/overview_controller.dart @@ -6,7 +6,7 @@ import 'package:superport/data/models/dashboard/license_expiry_summary.dart'; import 'package:superport/data/models/dashboard/overview_stats.dart'; import 'package:superport/data/models/dashboard/recent_activity.dart'; import 'package:superport/services/dashboard_service.dart'; -import 'package:superport/screens/common/theme_tailwind.dart'; +import 'package:superport/screens/common/theme_shadcn.dart'; import 'package:superport/core/utils/debug_logger.dart'; // 대시보드(Overview) 화면의 상태 및 비즈니스 로직을 담답하는 컨트롤러 @@ -309,18 +309,18 @@ class OverviewController extends ChangeNotifier { switch (activityType.toLowerCase()) { case 'equipment_in': case '장비 입고': - return AppThemeTailwind.success; + return ShadcnTheme.success; case 'equipment_out': case '장비 출고': - return AppThemeTailwind.warning; + return ShadcnTheme.warning; case 'user_create': case '사용자 추가': - return AppThemeTailwind.primary; + return ShadcnTheme.primary; case 'license_create': case '라이선스 등록': - return AppThemeTailwind.info; + return ShadcnTheme.info; default: - return AppThemeTailwind.muted; + return ShadcnTheme.muted; } } } diff --git a/lib/screens/overview/overview_screen_redesign.dart b/lib/screens/overview/overview_screen.dart similarity index 99% rename from lib/screens/overview/overview_screen_redesign.dart rename to lib/screens/overview/overview_screen.dart index df0bf5f..9e37c06 100644 --- a/lib/screens/overview/overview_screen_redesign.dart +++ b/lib/screens/overview/overview_screen.dart @@ -11,14 +11,14 @@ import 'package:superport/core/widgets/auth_guard.dart'; import 'package:superport/data/models/auth/auth_user.dart'; /// shadcn/ui 스타일로 재설계된 대시보드 화면 -class OverviewScreenRedesign extends StatefulWidget { - const OverviewScreenRedesign({super.key}); +class OverviewScreen extends StatefulWidget { + const OverviewScreen({super.key}); @override - State createState() => _OverviewScreenRedesignState(); + State createState() => _OverviewScreenState(); } -class _OverviewScreenRedesignState extends State { +class _OverviewScreenState extends State { late final OverviewController _controller; late final HealthCheckService _healthCheckService; Map? _healthStatus; @@ -872,4 +872,4 @@ class _OverviewScreenRedesignState extends State { 'color': ShadcnTheme.foreground, }; } -} +} \ No newline at end of file diff --git a/lib/screens/user/controllers/user_form_controller.dart b/lib/screens/user/controllers/user_form_controller.dart index 8c5280d..d314d38 100644 --- a/lib/screens/user/controllers/user_form_controller.dart +++ b/lib/screens/user/controllers/user_form_controller.dart @@ -64,7 +64,7 @@ class UserFormController extends ChangeNotifier { Future loadCompanies() async { try { final result = await _companyService.getCompanies(); - companies = result; + companies = result.items; // PaginatedResponse에서 items 추출 notifyListeners(); } catch (e) { debugPrint('회사 목록 로드 실패: $e'); diff --git a/lib/screens/user/controllers/user_list_controller.backup.dart b/lib/screens/user/controllers/user_list_controller.backup.dart deleted file mode 100644 index 7040136..0000000 --- a/lib/screens/user/controllers/user_list_controller.backup.dart +++ /dev/null @@ -1,172 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:get_it/get_it.dart'; -import 'package:superport/models/user_model.dart'; -import 'package:superport/models/company_model.dart'; -import 'package:superport/services/user_service.dart'; - -/// 담당자 목록 화면의 상태 및 비즈니스 로직을 담당하는 컨트롤러 -class UserListController extends ChangeNotifier { - final UserService _userService = GetIt.instance(); - - // 상태 변수 - List _users = []; - bool _isLoading = false; - String? _error; - // API만 사용 - - // 페이지네이션 - int _currentPage = 1; - final int _perPage = 20; - bool _hasMoreData = true; - bool _isLoadingMore = false; - - // 검색/필터 - String _searchQuery = ''; - int? _filterCompanyId; - String? _filterRole; - bool? _filterIsActive; - - // Getters - List get users => _users; - bool get isLoading => _isLoading; - bool get isLoadingMore => _isLoadingMore; - String? get error => _error; - bool get hasMoreData => _hasMoreData; - String get searchQuery => _searchQuery; - int? get filterCompanyId => _filterCompanyId; - String? get filterRole => _filterRole; - bool? get filterIsActive => _filterIsActive; - - UserListController(); - - /// 사용자 목록 초기 로드 - Future loadUsers({bool refresh = false}) async { - if (refresh) { - _currentPage = 1; - _hasMoreData = true; - _users.clear(); - } - - if (_isLoading) return; - - _isLoading = true; - _error = null; - notifyListeners(); - - try { - final newUsers = await _userService.getUsers( - page: _currentPage, - perPage: _perPage, - isActive: _filterIsActive, - companyId: _filterCompanyId, - role: _filterRole, - ); - - if (newUsers.isEmpty || newUsers.length < _perPage) { - _hasMoreData = false; - } - - if (_currentPage == 1) { - _users = newUsers; - } else { - _users.addAll(newUsers); - } - - _currentPage++; - } catch (e) { - _error = e.toString(); - } finally { - _isLoading = false; - notifyListeners(); - } - } - - /// 다음 페이지 로드 (무한 스크롤용) - Future loadMore() async { - if (!_hasMoreData || _isLoadingMore || _isLoading) return; - - _isLoadingMore = true; - notifyListeners(); - - try { - await loadUsers(); - } finally { - _isLoadingMore = false; - notifyListeners(); - } - } - - /// 검색 쿼리 설정 - void setSearchQuery(String query) { - _searchQuery = query; - _currentPage = 1; - _hasMoreData = true; - loadUsers(refresh: true); - } - - /// 필터 설정 - void setFilters({ - int? companyId, - String? role, - bool? isActive, - }) { - _filterCompanyId = companyId; - _filterRole = role; - _filterIsActive = isActive; - _currentPage = 1; - _hasMoreData = true; - loadUsers(refresh: true); - } - - /// 필터 초기화 - void clearFilters() { - _filterCompanyId = null; - _filterRole = null; - _filterIsActive = null; - _searchQuery = ''; - _currentPage = 1; - _hasMoreData = true; - loadUsers(refresh: true); - } - - /// 사용자 삭제 - Future deleteUser(int id, VoidCallback onDeleted, Function(String) onError) async { - try { - await _userService.deleteUser(id); - - // 목록에서 삭제된 사용자 제거 - _users.removeWhere((user) => user.id == id); - notifyListeners(); - - onDeleted(); - } catch (e) { - onError('사용자 삭제 실패: ${e.toString()}'); - } - } - - /// 사용자 상태 변경 (활성/비활성) - Future changeUserStatus(int id, bool isActive, Function(String) onError) async { - try { - final updatedUser = await _userService.changeUserStatus(id, isActive); - // 목록에서 해당 사용자 업데이트 - final index = _users.indexWhere((u) => u.id == id); - if (index != -1) { - _users[index] = updatedUser; - notifyListeners(); - } - } catch (e) { - onError('상태 변경 실패: ${e.toString()}'); - } - } - - /// 권한명 반환 함수는 user_utils.dart의 getRoleName을 사용 - - /// 회사 ID와 지점 ID로 지점명 조회 - // 지점명 조회는 별도 서비스로 이동 예정 - String getBranchName(int companyId, int? branchId) { - // TODO: API를 통해 지점명 조회 - return '-'; - } - - // API만 사용하므로 토글 기능 제거 -} diff --git a/lib/screens/user/controllers/user_list_controller.dart b/lib/screens/user/controllers/user_list_controller.dart index f9bbbe0..0c2fca7 100644 --- a/lib/screens/user/controllers/user_list_controller.dart +++ b/lib/screens/user/controllers/user_list_controller.dart @@ -35,8 +35,8 @@ class UserListController extends BaseListController { required PaginationParams params, Map? additionalFilters, }) async { - // API 호출 - final fetchedUsers = await ErrorHandler.handleApiCall>( + // API 호출 (이제 PaginatedResponse 반환) + final response = await ErrorHandler.handleApiCall( () => _userService.getUsers( page: params.page, perPage: params.perPage, @@ -50,21 +50,17 @@ class UserListController extends BaseListController { }, ); - final items = fetchedUsers ?? []; - - // 임시로 메타데이터 생성 (추후 API에서 실제 메타데이터 반환 시 수정) + // PaginatedResponse를 PagedResult로 변환 final meta = PaginationMeta( - currentPage: params.page, - perPage: params.perPage, - total: items.length < params.perPage ? - (params.page - 1) * params.perPage + items.length : - params.page * params.perPage + 1, - totalPages: items.length < params.perPage ? params.page : params.page + 1, - hasNext: items.length >= params.perPage, - hasPrevious: params.page > 1, + currentPage: response.page, + perPage: response.size, + total: response.totalElements, + totalPages: response.totalPages, + hasNext: !response.last, + hasPrevious: !response.first, ); - return PagedResult(items: items, meta: meta); + return PagedResult(items: response.items, meta: meta); } @override diff --git a/lib/screens/user/user_form.dart b/lib/screens/user/user_form.dart index a346bd5..995e015 100644 --- a/lib/screens/user/user_form.dart +++ b/lib/screens/user/user_form.dart @@ -1,6 +1,6 @@ import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; -import 'package:superport/screens/common/theme_tailwind.dart'; +import 'package:superport/screens/common/theme_shadcn.dart'; import 'package:superport/utils/constants.dart'; import 'package:superport/utils/validators.dart'; import 'package:flutter/services.dart'; @@ -272,7 +272,10 @@ class _UserFormScreenState extends State { onPressed: controller.isLoading ? null : () => _onSaveUser(controller), - style: AppThemeTailwind.primaryButtonStyle, + style: ElevatedButton.styleFrom( + backgroundColor: ShadcnTheme.primary, + foregroundColor: Colors.white, + ), child: Padding( padding: const EdgeInsets.all(12.0), child: controller.isLoading diff --git a/lib/screens/user/user_list_redesign.dart b/lib/screens/user/user_list.dart similarity index 94% rename from lib/screens/user/user_list_redesign.dart rename to lib/screens/user/user_list.dart index d59f034..63b1d93 100644 --- a/lib/screens/user/user_list_redesign.dart +++ b/lib/screens/user/user_list.dart @@ -10,18 +10,16 @@ import 'package:superport/utils/constants.dart'; import 'package:superport/utils/user_utils.dart'; /// shadcn/ui 스타일로 재설계된 사용자 관리 화면 -class UserListRedesign extends StatefulWidget { - const UserListRedesign({super.key}); +class UserList extends StatefulWidget { + const UserList({super.key}); @override - State createState() => _UserListRedesignState(); + State createState() => _UserListState(); } -class _UserListRedesignState extends State { +class _UserListState extends State { // MockDataService 제거 - 실제 API 사용 final TextEditingController _searchController = TextEditingController(); - int _currentPage = 1; - final int _pageSize = 10; @override void initState() { @@ -29,7 +27,9 @@ class _UserListRedesignState extends State { // 초기 데이터 로드 WidgetsBinding.instance.addPostFrameCallback((_) { - context.read().loadUsers(); + final controller = context.read(); + controller.pageSize = 10; // 페이지 크기 설정 + controller.loadUsers(); }); // 검색 디바운싱 @@ -50,10 +50,7 @@ class _UserListRedesignState extends State { void _onSearchChanged(String query) { if (_debounce?.isActive ?? false) _debounce!.cancel(); _debounce = Timer(const Duration(milliseconds: 300), () { - setState(() { - _currentPage = 1; - }); - context.read().setSearchQuery(query); + context.read().setSearchQuery(query); // Controller가 페이지 리셋 처리 }); } @@ -207,14 +204,9 @@ class _UserListRedesignState extends State { ); } - // 페이지네이션을 위한 데이터 처리 - final int totalUsers = controller.users.length; - final int startIndex = (_currentPage - 1) * _pageSize; - final int endIndex = startIndex + _pageSize; - final List pagedUsers = controller.users.sublist( - startIndex, - endIndex > totalUsers ? totalUsers : endIndex, - ); + // Controller가 이미 페이징된 데이터를 제공 + final List pagedUsers = controller.users; // 이미 페이징됨 + final int totalUsers = controller.total; // 실제 전체 개수 return SingleChildScrollView( padding: const EdgeInsets.all(ShadcnTheme.spacing6), @@ -415,7 +407,7 @@ class _UserListRedesignState extends State { ) else ...pagedUsers.asMap().entries.map((entry) { - final int index = startIndex + entry.key; + final int index = ((controller.currentPage - 1) * controller.pageSize) + entry.key; final User user = entry.value; return Container( @@ -576,16 +568,19 @@ class _UserListRedesignState extends State { ), ), - // 페이지네이션 컴포넌트 - if (totalUsers > _pageSize) + // 페이지네이션 컴포넌트 (Controller 상태 사용) + if (controller.total > controller.pageSize) Pagination( - totalCount: totalUsers, - currentPage: _currentPage, - pageSize: _pageSize, + totalCount: controller.total, + currentPage: controller.currentPage, + pageSize: controller.pageSize, onPageChanged: (page) { - setState(() { - _currentPage = page; - }); + // 다음 페이지 로드 + if (page > controller.currentPage) { + controller.loadNextPage(); + } else if (page == 1) { + controller.refresh(); + } }, ), ], diff --git a/lib/screens/warehouse_location/controllers/warehouse_location_list_controller.backup.dart b/lib/screens/warehouse_location/controllers/warehouse_location_list_controller.backup.dart deleted file mode 100644 index b28cd9f..0000000 --- a/lib/screens/warehouse_location/controllers/warehouse_location_list_controller.backup.dart +++ /dev/null @@ -1,210 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:get_it/get_it.dart'; -import 'package:superport/models/warehouse_location_model.dart'; -import 'package:superport/services/warehouse_service.dart'; -import 'package:superport/core/errors/failures.dart'; -import 'package:superport/core/utils/error_handler.dart'; - -/// 입고지 리스트 상태 및 CRUD만 담당하는 컨트롤러 클래스 (SRP 적용) -/// UI, 네비게이션, 다이얼로그 등은 포함하지 않음 -/// 향후 서비스/리포지토리 DI 구조로 확장 가능 -class WarehouseLocationListController extends ChangeNotifier { - late final WarehouseService _warehouseService; - - List _warehouseLocations = []; - List _filteredLocations = []; - bool _isLoading = false; - String? _error; - String _searchQuery = ''; - int _currentPage = 1; - final int _pageSize = 20; - bool _hasMore = true; - int _total = 0; - - // 필터 옵션 - bool? _isActive; - - WarehouseLocationListController() { - if (GetIt.instance.isRegistered()) { - _warehouseService = GetIt.instance(); - } else { - throw Exception('WarehouseService not registered'); - } - } - - // Getters - List get warehouseLocations => _filteredLocations; - bool get isLoading => _isLoading; - String? get error => _error; - String get searchQuery => _searchQuery; - int get currentPage => _currentPage; - bool get hasMore => _hasMore; - int get total => _total; - bool? get isActive => _isActive; - - /// 데이터 로드 - Future loadWarehouseLocations({bool isInitialLoad = true}) async { - if (_isLoading) return; - - _isLoading = true; - _error = null; - notifyListeners(); - - // API 사용 시 ErrorHandler 적용 - print('╔══════════════════════════════════════════════════════════'); - print('║ 🏭 입고지 목록 API 호출 시작'); - print('║ • 활성 필터: ${_isActive != null ? (_isActive! ? "활성" : "비활성") : "전체"}'); - print('╚══════════════════════════════════════════════════════════'); - - final fetchedLocations = await ErrorHandler.handleApiCall>( - () => _warehouseService.getWarehouseLocations( - page: 1, - perPage: 1000, // 충분히 큰 값으로 전체 데이터 로드 - isActive: _isActive, - ), - onError: (failure) { - _error = ErrorHandler.getUserFriendlyMessage(failure); - print('[WarehouseLocationListController] API 에러: ${failure.message}'); - }, - ); - - if (fetchedLocations != null) { - print('╔══════════════════════════════════════════════════════════'); - print('║ 📊 입고지 목록 로드 완료'); - print('║ ▶ 총 입고지 수: ${fetchedLocations.length}개'); - print('╟──────────────────────────────────────────────────────────'); - - // 상태별 통계 (입고지에 상태가 있다면) - int activeCount = 0; - int inactiveCount = 0; - for (final location in fetchedLocations) { - // isActive 필드가 있다면 활용 - activeCount++; // 현재는 모두 활성으로 가정 - } - - print('║ • 활성 입고지: $activeCount개'); - if (inactiveCount > 0) { - print('║ • 비활성 입고지: $inactiveCount개'); - } - - print('╟──────────────────────────────────────────────────────────'); - print('║ 📑 전체 데이터 로드 완료'); - print('║ • View에서 페이지네이션 처리 예정'); - print('╚══════════════════════════════════════════════════════════'); - - _warehouseLocations = fetchedLocations; - _hasMore = false; // 전체 데이터를 로드했으므로 더 이상 로드할 필요 없음 - _total = fetchedLocations.length; - _applySearchFilter(); - print('[WarehouseLocationListController] After filtering: ${_filteredLocations.length} locations shown'); - } - - _isLoading = false; - notifyListeners(); - } - - // 다음 페이지 로드 - Future loadNextPage() async { - if (!_hasMore || _isLoading) return; - await loadWarehouseLocations(isInitialLoad: false); - } - - // 검색 - void search(String query) { - _searchQuery = query; - _applySearchFilter(); - notifyListeners(); - } - - // 검색 필터 적용 - void _applySearchFilter() { - if (_searchQuery.isEmpty) { - _filteredLocations = List.from(_warehouseLocations); - } else { - _filteredLocations = _warehouseLocations.where((location) { - return location.name.toLowerCase().contains(_searchQuery.toLowerCase()) || - location.address.toString().toLowerCase().contains(_searchQuery.toLowerCase()); - }).toList(); - } - } - - // 필터 설정 - void setFilters({bool? isActive}) { - _isActive = isActive; - loadWarehouseLocations(); - } - - // 필터 초기화 - void clearFilters() { - _isActive = null; - _searchQuery = ''; - loadWarehouseLocations(); - } - - /// 입고지 추가 - Future addWarehouseLocation(WarehouseLocation location) async { - await ErrorHandler.handleApiCall( - () => _warehouseService.createWarehouseLocation(location), - onError: (failure) { - _error = ErrorHandler.getUserFriendlyMessage(failure); - notifyListeners(); - }, - ); - - // 목록 새로고침 - await loadWarehouseLocations(); - } - - /// 입고지 수정 - Future updateWarehouseLocation(WarehouseLocation location) async { - await ErrorHandler.handleApiCall( - () => _warehouseService.updateWarehouseLocation(location), - onError: (failure) { - _error = ErrorHandler.getUserFriendlyMessage(failure); - notifyListeners(); - }, - ); - - // 목록에서 업데이트 - final index = _warehouseLocations.indexWhere((l) => l.id == location.id); - if (index != -1) { - _warehouseLocations[index] = location; - _applySearchFilter(); - notifyListeners(); - } - } - - /// 입고지 삭제 - Future deleteWarehouseLocation(int id) async { - await ErrorHandler.handleApiCall( - () => _warehouseService.deleteWarehouseLocation(id), - onError: (failure) { - _error = ErrorHandler.getUserFriendlyMessage(failure); - notifyListeners(); - }, - ); - - // 목록에서 제거 - _warehouseLocations.removeWhere((l) => l.id == id); - _applySearchFilter(); - _total--; - notifyListeners(); - } - - // 새로고침 - Future refresh() async { - await loadWarehouseLocations(); - } - - // 사용 중인 창고 위치 조회 - Future> getInUseWarehouseLocations() async { - final locations = await ErrorHandler.handleApiCall>( - () => _warehouseService.getInUseWarehouseLocations(), - onError: (failure) { - _error = ErrorHandler.getUserFriendlyMessage(failure); - notifyListeners(); - }, - ); - return locations ?? []; - } -} diff --git a/lib/screens/warehouse_location/controllers/warehouse_location_list_controller.dart b/lib/screens/warehouse_location/controllers/warehouse_location_list_controller.dart index 483ad98..6cc1e1c 100644 --- a/lib/screens/warehouse_location/controllers/warehouse_location_list_controller.dart +++ b/lib/screens/warehouse_location/controllers/warehouse_location_list_controller.dart @@ -31,8 +31,8 @@ class WarehouseLocationListController extends BaseListController? additionalFilters, }) async { - // API 사용 - final fetchedLocations = await ErrorHandler.handleApiCall>( + // API 사용 (PaginatedResponse 반환) + final response = await ErrorHandler.handleApiCall( () => _warehouseService.getWarehouseLocations( page: params.page, perPage: params.perPage, @@ -43,21 +43,31 @@ class WarehouseLocationListController extends BaseListController= params.perPage, - hasPrevious: params.page > 1, + currentPage: response.page, + perPage: response.size, + total: response.totalElements, + totalPages: response.totalPages, + hasNext: !response.last, + hasPrevious: !response.first, ); - return PagedResult(items: items, meta: meta); + return PagedResult(items: response.items, meta: meta); } @override diff --git a/lib/screens/warehouse_location/warehouse_location_form.dart b/lib/screens/warehouse_location/warehouse_location_form.dart index 1141649..3731443 100644 --- a/lib/screens/warehouse_location/warehouse_location_form.dart +++ b/lib/screens/warehouse_location/warehouse_location_form.dart @@ -2,7 +2,7 @@ import 'package:flutter/material.dart'; import 'package:superport/models/address_model.dart'; import 'package:superport/screens/common/widgets/address_input.dart'; import 'package:superport/screens/common/widgets/remark_input.dart'; -import 'package:superport/screens/common/theme_tailwind.dart'; +import 'package:superport/screens/common/theme_shadcn.dart'; import 'package:superport/screens/common/templates/form_layout_template.dart'; import 'controllers/warehouse_location_form_controller.dart'; @@ -50,7 +50,7 @@ class _WarehouseLocationFormScreenState ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: Text(_controller.isEditMode ? '입고지가 수정되었습니다' : '입고지가 추가되었습니다'), - backgroundColor: AppThemeTailwind.success, + backgroundColor: ShadcnTheme.success, ), ); // 리스트 화면으로 돌아가기 @@ -62,7 +62,7 @@ class _WarehouseLocationFormScreenState ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: Text(_controller.error ?? '저장에 실패했습니다'), - backgroundColor: AppThemeTailwind.danger, + backgroundColor: ShadcnTheme.error, ), ); } diff --git a/lib/screens/warehouse_location/warehouse_location_list_redesign.dart b/lib/screens/warehouse_location/warehouse_location_list.dart similarity index 88% rename from lib/screens/warehouse_location/warehouse_location_list_redesign.dart rename to lib/screens/warehouse_location/warehouse_location_list.dart index fe21421..d182ae3 100644 --- a/lib/screens/warehouse_location/warehouse_location_list_redesign.dart +++ b/lib/screens/warehouse_location/warehouse_location_list.dart @@ -14,19 +14,18 @@ import 'package:superport/utils/constants.dart'; import 'package:superport/core/widgets/auth_guard.dart'; /// shadcn/ui 스타일로 재설계된 입고지 관리 화면 -class WarehouseLocationListRedesign extends StatefulWidget { - const WarehouseLocationListRedesign({Key? key}) : super(key: key); +class WarehouseLocationList extends StatefulWidget { + const WarehouseLocationList({Key? key}) : super(key: key); @override - State createState() => - _WarehouseLocationListRedesignState(); + State createState() => + _WarehouseLocationListState(); } -class _WarehouseLocationListRedesignState - extends State { +class _WarehouseLocationListState + extends State { late WarehouseLocationListController _controller; - int _currentPage = 1; - final int _pageSize = 10; + // 페이지 상태는 이제 Controller에서 관리 @override void initState() { @@ -46,8 +45,7 @@ class _WarehouseLocationListRedesignState /// 리스트 새로고침 void _reload() { - _currentPage = 1; - _controller.loadWarehouseLocations(); + _controller.refresh(); // Controller에서 페이지 리셋 처리 } /// 입고지 추가 폼으로 이동 @@ -107,15 +105,9 @@ class _WarehouseLocationListRedesignState value: _controller, child: Consumer( builder: (context, controller, child) { - final int totalCount = controller.warehouseLocations.length; - final int startIndex = (_currentPage - 1) * _pageSize; - final int endIndex = - (startIndex + _pageSize) > totalCount - ? totalCount - : (startIndex + _pageSize); - final List pagedLocations = totalCount > 0 && startIndex < totalCount - ? controller.warehouseLocations.sublist(startIndex, endIndex) - : []; + // Controller가 이미 페이징된 데이터를 제공 + final List pagedLocations = controller.warehouseLocations; + final int totalCount = controller.total; // 실제 전체 개수 return BaseListScreen( isLoading: controller.isLoading && controller.warehouseLocations.isEmpty, @@ -150,17 +142,15 @@ class _WarehouseLocationListRedesignState ), // 데이터 테이블 - dataTable: _buildDataTable(pagedLocations, startIndex), + dataTable: _buildDataTable(pagedLocations), // 페이지네이션 - pagination: totalCount > _pageSize ? Pagination( + pagination: totalCount > controller.pageSize ? Pagination( totalCount: totalCount, - currentPage: _currentPage, - pageSize: _pageSize, + currentPage: controller.currentPage, + pageSize: controller.pageSize, onPageChanged: (page) { - setState(() { - _currentPage = page; - }); + controller.goToPage(page); }, ) : null, ); @@ -171,7 +161,7 @@ class _WarehouseLocationListRedesignState } /// 데이터 테이블 - Widget _buildDataTable(List pagedLocations, int startIndex) { + Widget _buildDataTable(List pagedLocations) { if (pagedLocations.isEmpty) { return StandardEmptyState( title: @@ -257,7 +247,7 @@ class _WarehouseLocationListRedesignState Expanded( flex: 1, child: Text( - '${startIndex + index + 1}', + '${(_controller.currentPage - 1) * _controller.pageSize + index + 1}', style: ShadcnTheme.bodySmall, ), ), diff --git a/lib/services/company_service.dart b/lib/services/company_service.dart index e5f5d97..c72a818 100644 --- a/lib/services/company_service.dart +++ b/lib/services/company_service.dart @@ -3,6 +3,7 @@ import 'package:injectable/injectable.dart'; import 'package:superport/core/errors/exceptions.dart'; import 'package:superport/core/errors/failures.dart'; import 'package:superport/data/datasources/remote/company_remote_datasource.dart'; +import 'package:superport/data/models/common/paginated_response.dart'; import 'package:superport/data/models/company/company_dto.dart'; import 'package:superport/data/models/company/company_list_dto.dart'; import 'package:superport/data/models/company/branch_dto.dart'; @@ -16,7 +17,7 @@ class CompanyService { CompanyService(this._remoteDataSource); // 회사 목록 조회 - Future> getCompanies({ + Future> getCompanies({ int page = 1, int perPage = 20, String? search, @@ -30,7 +31,15 @@ class CompanyService { isActive: isActive, ); - return response.items.map((dto) => _convertListDtoToCompany(dto)).toList(); + return PaginatedResponse( + items: response.items.map((dto) => _convertListDtoToCompany(dto)).toList(), + page: response.page, + size: response.size, + totalElements: response.totalElements, + totalPages: response.totalPages, + first: response.first, + last: response.last, + ); } on ApiException catch (e) { debugPrint('[CompanyService] ApiException: ${e.message}'); throw ServerFailure(message: e.message); diff --git a/lib/services/equipment_service.dart b/lib/services/equipment_service.dart index 8178d85..c5b6a2c 100644 --- a/lib/services/equipment_service.dart +++ b/lib/services/equipment_service.dart @@ -2,6 +2,7 @@ import 'package:get_it/get_it.dart'; import 'package:superport/core/errors/exceptions.dart'; import 'package:superport/core/errors/failures.dart'; import 'package:superport/data/datasources/remote/equipment_remote_datasource.dart'; +import 'package:superport/data/models/common/paginated_response.dart'; import 'package:superport/data/models/equipment/equipment_history_dto.dart'; import 'package:superport/data/models/equipment/equipment_in_request.dart'; import 'package:superport/data/models/equipment/equipment_io_response.dart'; @@ -15,7 +16,7 @@ class EquipmentService { final EquipmentRemoteDataSource _remoteDataSource = GetIt.instance(); // 장비 목록 조회 (DTO 형태로 반환하여 status 정보 유지) - Future> getEquipmentsWithStatus({ + Future> getEquipmentsWithStatus({ int page = 1, int perPage = 20, String? status, @@ -24,7 +25,7 @@ class EquipmentService { String? search, }) async { try { - final dtoList = await _remoteDataSource.getEquipments( + final response = await _remoteDataSource.getEquipments( page: page, perPage: perPage, status: status, @@ -33,7 +34,15 @@ class EquipmentService { search: search, ); - return dtoList; + return PaginatedResponse( + items: response.items, + page: response.page, + size: response.perPage, + totalElements: response.total, + totalPages: response.totalPages, + first: response.page == 1, + last: response.page >= response.totalPages, + ); } on ServerException catch (e) { throw ServerFailure(message: e.message); } catch (e) { @@ -42,7 +51,7 @@ class EquipmentService { } // 장비 목록 조회 - Future> getEquipments({ + Future> getEquipments({ int page = 1, int perPage = 20, String? status, @@ -51,7 +60,7 @@ class EquipmentService { String? search, }) async { try { - final dtoList = await _remoteDataSource.getEquipments( + final response = await _remoteDataSource.getEquipments( page: page, perPage: perPage, status: status, @@ -60,7 +69,15 @@ class EquipmentService { search: search, ); - return dtoList.map((dto) => _convertListDtoToEquipment(dto)).toList(); + return PaginatedResponse( + items: response.items.map((dto) => _convertListDtoToEquipment(dto)).toList(), + page: response.page, + size: response.perPage, + totalElements: response.total, + totalPages: response.totalPages, + first: response.page == 1, + last: response.page >= response.totalPages, + ); } on ServerException catch (e) { throw ServerFailure(message: e.message); } catch (e) { @@ -68,6 +85,42 @@ class EquipmentService { } } + // 입고된 장비 목록 조회 + Future> getEquipmentInList({ + int page = 1, + int perPage = 20, + int? companyId, + int? warehouseLocationId, + String? search, + }) async { + return getEquipmentsWithStatus( + page: page, + perPage: perPage, + status: 'available', // 입고된 장비는 사용 가능 상태 + companyId: companyId, + warehouseLocationId: warehouseLocationId, + search: search, + ); + } + + // 출고된 장비 목록 조회 + Future> getEquipmentOutList({ + int page = 1, + int perPage = 20, + int? companyId, + int? warehouseLocationId, + String? search, + }) async { + return getEquipmentsWithStatus( + page: page, + perPage: perPage, + status: 'in_use', // 출고된 장비는 사용 중 상태 + companyId: companyId, + warehouseLocationId: warehouseLocationId, + search: search, + ); + } + // 장비 생성 Future createEquipment(Equipment equipment) async { try { diff --git a/lib/services/health_test_service.dart b/lib/services/health_test_service.dart index 628f857..cc870e7 100644 --- a/lib/services/health_test_service.dart +++ b/lib/services/health_test_service.dart @@ -77,8 +77,8 @@ class HealthTestService { final equipments = await _equipmentService.getEquipments(page: 1, perPage: 5); results['equipments'] = { 'success': true, - 'count': equipments.length, - 'sample': equipments.take(2).map((e) => { + 'count': equipments.items.length, + 'sample': equipments.items.take(2).map((e) => { 'id': e.id, 'name': e.name, 'manufacturer': e.manufacturer, @@ -95,11 +95,11 @@ class HealthTestService { try { DebugLogger.log('입고지 API 체크 시작', tag: 'HEALTH_TEST'); - final warehouses = await _warehouseService.getWarehouseLocations(); + final warehousesResponse = await _warehouseService.getWarehouseLocations(); results['warehouses'] = { 'success': true, - 'count': warehouses.length, - 'sample': warehouses.take(2).map((w) => { + 'count': warehousesResponse.items.length, + 'sample': warehousesResponse.items.take(2).map((w) => { 'id': w.id, 'name': w.name, 'address': w.address.toString(), @@ -115,11 +115,11 @@ class HealthTestService { try { DebugLogger.log('회사 API 체크 시작', tag: 'HEALTH_TEST'); - final companies = await _companyService.getCompanies(); + final companiesResponse = await _companyService.getCompanies(); results['companies'] = { 'success': true, - 'count': companies.length, - 'sample': companies.take(2).map((c) => { + 'count': companiesResponse.items.length, + 'sample': companiesResponse.items.take(2).map((c) => { 'id': c.id, 'name': c.name, 'companyTypes': c.companyTypes.map((t) => companyTypeToString(t)).toList(), @@ -150,8 +150,8 @@ class HealthTestService { final equipments = await _equipmentService.getEquipments(page: 1, perPage: 10); return { 'success': true, - 'count': equipments.length, - 'data': equipments.map((e) => e.toJson()).toList(), + 'count': equipments.items.length, + 'data': equipments.items.map((e) => e.toJson()).toList(), }; } catch (e) { return {'success': false, 'error': e.toString()}; @@ -159,11 +159,11 @@ class HealthTestService { case 'warehouses': try { - final warehouses = await _warehouseService.getWarehouseLocations(); + final warehousesResponse = await _warehouseService.getWarehouseLocations(); return { 'success': true, - 'count': warehouses.length, - 'data': warehouses.map((w) => { + 'count': warehousesResponse.items.length, + 'data': warehousesResponse.items.map((w) => { 'id': w.id, 'name': w.name, 'address': w.address.toString(), @@ -179,8 +179,8 @@ class HealthTestService { final companies = await _companyService.getCompanies(); return { 'success': true, - 'count': companies.length, - 'data': companies.map((c) => c.toJson()).toList(), + 'count': companies.items.length, + 'data': companies.items.map((c) => c.toJson()).toList(), }; } catch (e) { return {'success': false, 'error': e.toString()}; diff --git a/lib/services/license_service.dart b/lib/services/license_service.dart index 869515a..4d281d0 100644 --- a/lib/services/license_service.dart +++ b/lib/services/license_service.dart @@ -4,6 +4,7 @@ import 'package:injectable/injectable.dart'; import 'package:superport/core/errors/exceptions.dart'; import 'package:superport/core/errors/failures.dart'; import 'package:superport/data/datasources/remote/license_remote_datasource.dart'; +import 'package:superport/data/models/common/paginated_response.dart'; import 'package:superport/data/models/license/license_dto.dart'; import 'package:superport/data/models/license/license_request_dto.dart'; import 'package:superport/models/license_model.dart'; @@ -15,7 +16,7 @@ class LicenseService { LicenseService(this._remoteDataSource); // 라이선스 목록 조회 - Future> getLicenses({ + Future> getLicenses({ int page = 1, int perPage = 20, bool? isActive, @@ -65,7 +66,15 @@ class LicenseService { } debugPrint('╚════════════════════════════════════════════════════════════\n'); - return licenses; + return PaginatedResponse( + items: licenses, + page: response.page, + size: response.perPage, + totalElements: response.total, + totalPages: response.totalPages, + first: response.page == 1, + last: response.page >= response.totalPages, + ); } on ApiException catch (e) { debugPrint('\n╔════════════════════════════════════════════════════════════'); debugPrint('║ ❌ LICENSE API ERROR'); diff --git a/lib/services/user_service.dart b/lib/services/user_service.dart index 3e6b9bf..aeaae3c 100644 --- a/lib/services/user_service.dart +++ b/lib/services/user_service.dart @@ -1,5 +1,6 @@ import 'package:injectable/injectable.dart'; import 'package:superport/data/datasources/remote/user_remote_datasource.dart'; +import 'package:superport/data/models/common/paginated_response.dart'; import 'package:superport/data/models/user/user_dto.dart'; import 'package:superport/models/user_model.dart'; @@ -10,7 +11,7 @@ class UserService { UserService() : _userRemoteDataSource = UserRemoteDataSource(); /// 사용자 목록 조회 - Future> getUsers({ + Future> getUsers({ int page = 1, int perPage = 20, bool? isActive, @@ -26,7 +27,15 @@ class UserService { role: role != null ? _mapRoleToApi(role) : null, ); - return response.users.map((dto) => _userDtoToModel(dto)).toList(); + return PaginatedResponse( + items: response.users.map((dto) => _userDtoToModel(dto)).toList(), + page: response.page, + size: response.perPage, + totalElements: response.total, + totalPages: response.totalPages, + first: response.page == 1, + last: response.page >= response.totalPages, + ); } catch (e) { throw Exception('사용자 목록 조회 실패: ${e.toString()}'); } diff --git a/lib/services/warehouse_service.dart b/lib/services/warehouse_service.dart index b4ee578..a5bfdf3 100644 --- a/lib/services/warehouse_service.dart +++ b/lib/services/warehouse_service.dart @@ -4,6 +4,7 @@ import 'package:injectable/injectable.dart'; import 'package:superport/core/errors/exceptions.dart'; import 'package:superport/core/errors/failures.dart'; import 'package:superport/data/datasources/remote/warehouse_remote_datasource.dart'; +import 'package:superport/data/models/common/paginated_response.dart'; import 'package:superport/data/models/warehouse/warehouse_dto.dart'; import 'package:superport/models/address_model.dart'; import 'package:superport/models/warehouse_location_model.dart'; @@ -13,7 +14,7 @@ class WarehouseService { final WarehouseRemoteDataSource _remoteDataSource = GetIt.instance(); // 창고 위치 목록 조회 - Future> getWarehouseLocations({ + Future> getWarehouseLocations({ int page = 1, int perPage = 20, bool? isActive, @@ -25,7 +26,15 @@ class WarehouseService { isActive: isActive, ); - return response.items.map((dto) => _convertDtoToWarehouseLocation(dto)).toList(); + return PaginatedResponse( + items: response.items.map((dto) => _convertDtoToWarehouseLocation(dto)).toList(), + page: response.page, + size: response.perPage, + totalElements: response.total, + totalPages: response.totalPages, + first: response.page == 1, + last: response.page >= response.totalPages, + ); } on ApiException catch (e) { debugPrint('[WarehouseService] ApiException: ${e.message}'); throw ServerFailure(message: e.message); diff --git a/test/domain/usecases/auth/login_usecase_test.dart b/test/domain/usecases/auth/login_usecase_test.dart index eaa2232..89d1268 100644 --- a/test/domain/usecases/auth/login_usecase_test.dart +++ b/test/domain/usecases/auth/login_usecase_test.dart @@ -9,7 +9,7 @@ import 'package:superport/services/auth_service.dart'; import 'package:superport/data/models/auth/login_request.dart'; import 'package:superport/data/models/auth/login_response.dart'; import 'package:superport/data/models/auth/auth_user.dart'; -import 'package:superport/core/utils/error_handler.dart'; +import 'package:superport/core/errors/failures.dart' as failures; import 'login_usecase_test.mocks.dart'; @@ -51,7 +51,7 @@ void main() { test('로그인 성공 시 Right(LoginResponse) 반환', () async { // arrange when(mockAuthService.login(any)) - .thenAnswer((_) async => tLoginResponse); + .thenAnswer((_) async => Right(tLoginResponse)); // act final result = await loginUseCase( @@ -77,7 +77,7 @@ void main() { expect(result.isLeft(), true); result.fold( (failure) { - expect(failure, isA()); + expect(failure, isA()); expect(failure.message, '올바른 이메일 형식이 아닙니다.'); }, (_) => fail('Should return failure'), @@ -95,7 +95,7 @@ void main() { expect(result.isLeft(), true); result.fold( (failure) { - expect(failure, isA()); + expect(failure, isA()); expect(failure.message, '비밀번호를 입력해주세요.'); }, (_) => fail('Should return failure'), @@ -125,7 +125,7 @@ void main() { expect(result.isLeft(), true); result.fold( (failure) { - expect(failure, isA()); + expect(failure, isA()); expect(failure.message, contains('인증')); }, (_) => fail('Should return failure'), @@ -150,7 +150,7 @@ void main() { expect(result.isLeft(), true); result.fold( (failure) { - expect(failure, isA()); + expect(failure, isA()); expect(failure.message, contains('네트워크')); }, (_) => fail('Should return failure'), @@ -180,7 +180,7 @@ void main() { expect(result.isLeft(), true); result.fold( (failure) { - expect(failure, isA()); + expect(failure, isA()); expect(failure.message, contains('서버')); }, (_) => fail('Should return failure'), @@ -201,7 +201,7 @@ void main() { expect(result.isLeft(), true); result.fold( (failure) { - expect(failure, isA()); + expect(failure, isA()); expect(failure.message, contains('오류')); }, (_) => fail('Should return failure'), @@ -210,7 +210,7 @@ void main() { test('로그인 실패 시 (null 반환) AuthFailure 반환', () async { // arrange - when(mockAuthService.login(any)).thenAnswer((_) async => null); + when(mockAuthService.login(any)).thenAnswer((_) async => Left(failures.AuthenticationFailure(message: '로그인에 실패했습니다.'))); // act final result = await loginUseCase( @@ -221,7 +221,7 @@ void main() { expect(result.isLeft(), true); result.fold( (failure) { - expect(failure, isA()); + expect(failure, isA()); expect(failure.message, contains('로그인')); }, (_) => fail('Should return failure'), diff --git a/test/domain/usecases/license/create_license_usecase_test.dart b/test/domain/usecases/license/create_license_usecase_test.dart index a0149c4..caf0a53 100644 --- a/test/domain/usecases/license/create_license_usecase_test.dart +++ b/test/domain/usecases/license/create_license_usecase_test.dart @@ -6,6 +6,7 @@ import 'package:superport/data/models/license/license_dto.dart'; import 'package:superport/data/repositories/license_repository.dart'; import 'package:superport/domain/usecases/base_usecase.dart'; import 'package:superport/domain/usecases/license/create_license_usecase.dart'; +import 'package:superport/core/errors/failures.dart'; import 'create_license_usecase_test.mocks.dart'; @@ -32,14 +33,19 @@ void main() { final mockLicense = LicenseDto( id: 1, - equipmentId: 1, - companyId: 1, + licenseKey: 'TEST-LICENSE-KEY', + productName: 'Test Product', + vendor: 'Test Vendor', licenseType: 'maintenance', - startDate: DateTime(2025, 1, 1), + userCount: 10, + purchaseDate: DateTime(2025, 1, 1), expiryDate: DateTime(2025, 12, 31), - description: 'Test license', - cost: 1000.0, - status: 'active', + purchasePrice: 1000.0, + companyId: 1, + branchId: 1, + assignedUserId: 1, + remark: 'Test license', + isActive: true, createdAt: DateTime.now(), updatedAt: DateTime.now(), ); diff --git a/test/domain/usecases/warehouse_location/create_warehouse_location_usecase_test.dart b/test/domain/usecases/warehouse_location/create_warehouse_location_usecase_test.dart index 3f4a219..705abf9 100644 --- a/test/domain/usecases/warehouse_location/create_warehouse_location_usecase_test.dart +++ b/test/domain/usecases/warehouse_location/create_warehouse_location_usecase_test.dart @@ -2,10 +2,12 @@ import 'package:flutter_test/flutter_test.dart'; import 'package:mockito/mockito.dart'; import 'package:mockito/annotations.dart'; import 'package:dartz/dartz.dart'; -import 'package:superport/data/models/warehouse_location/warehouse_location.dart'; +import 'package:superport/data/models/warehouse/warehouse_dto.dart'; import 'package:superport/data/repositories/warehouse_location_repository.dart'; import 'package:superport/domain/usecases/base_usecase.dart'; import 'package:superport/domain/usecases/warehouse_location/create_warehouse_location_usecase.dart'; +import 'package:superport/core/errors/failures.dart'; +import 'package:superport/models/address_model.dart'; import 'create_warehouse_location_usecase_test.mocks.dart'; @@ -30,18 +32,14 @@ void main() { longitude: 126.9780, ); - final mockLocation = WarehouseLocation( + final mockLocation = WarehouseLocationDto( id: 1, name: 'Main Warehouse', address: '123 Storage Street', - description: 'Primary storage location', - contactNumber: '010-1234-5678', - manager: 'John Doe', - latitude: 37.5665, - longitude: 126.9780, + managerName: 'John Doe', + managerPhone: '010-1234-5678', isActive: true, createdAt: DateTime.now(), - updatedAt: DateTime.now(), ); test('창고 위치 생성 성공', () async { diff --git a/test/integration/automated/checkbox_equipment_out_test.dart b/test/integration/automated/checkbox_equipment_out_test.dart index ee71b62..b15dd52 100644 --- a/test/integration/automated/checkbox_equipment_out_test.dart +++ b/test/integration/automated/checkbox_equipment_out_test.dart @@ -162,7 +162,7 @@ class CheckboxEquipmentOutTest { if (controller.equipments.isNotEmpty) { // 첫 번째 장비 선택 final equipment = controller.equipments.first; - controller.selectEquipment(equipment.id, equipment.status, true); + controller.selectEquipment(equipment); final isSelected = controller.selectedEquipmentIds.contains('${equipment.id}:${equipment.status}'); result['steps'].add({ @@ -173,7 +173,7 @@ class CheckboxEquipmentOutTest { }); // 선택 해제 - controller.selectEquipment(equipment.id, equipment.status, false); + controller.selectEquipment(equipment); // Toggle to deselect final isDeselected = !controller.selectedEquipmentIds.contains('${equipment.id}:${equipment.status}'); result['steps'].add({ 'name': '장비 선택 해제', @@ -207,7 +207,7 @@ class CheckboxEquipmentOutTest { final equipmentsToSelect = controller.equipments.take(3).toList(); for (final equipment in equipmentsToSelect) { - controller.selectEquipment(equipment.id, equipment.status, true); + controller.selectEquipment(equipment); } final selectedCount = controller.getSelectedEquipmentCount(); @@ -249,7 +249,7 @@ class CheckboxEquipmentOutTest { // 전체 선택 시뮬레이션 for (final equipment in controller.equipments) { - controller.selectEquipment(equipment.id, equipment.status, true); + controller.selectEquipment(equipment); } final selectedCount = controller.getSelectedEquipmentCount(); @@ -264,7 +264,7 @@ class CheckboxEquipmentOutTest { // 전체 해제 for (final equipment in controller.equipments) { - controller.selectEquipment(equipment.id, equipment.status, false); + controller.selectEquipment(equipment); // Toggle to deselect } final afterDeselectCount = controller.getSelectedEquipmentCount(); @@ -293,7 +293,7 @@ class CheckboxEquipmentOutTest { try { // 입고 상태 필터 적용 - await controller.changeStatusFilter('available'); + controller.changeStatusFilter('available'); result['steps'].add({ 'name': '입고 상태 필터 적용', @@ -310,7 +310,7 @@ class CheckboxEquipmentOutTest { .toList(); for (final equipment in availableEquipments) { - controller.selectEquipment(equipment.id, equipment.status, true); + controller.selectEquipment(equipment); } final selectedInStockCount = controller.getSelectedEquipmentCountByStatus('available'); @@ -344,7 +344,7 @@ class CheckboxEquipmentOutTest { // 1. 입고 상태 장비 로드 await controller.loadData(isRefresh: true); - await controller.changeStatusFilter('available'); + controller.changeStatusFilter('available'); final availableCount = controller.equipments .where((e) => e.status == 'available') @@ -362,7 +362,7 @@ class CheckboxEquipmentOutTest { .where((e) => e.status == 'available') .first; - controller.selectEquipment(singleEquipment.id, singleEquipment.status, true); + controller.selectEquipment(singleEquipment); result['steps'].add({ 'name': '단일 장비 선택', @@ -433,7 +433,7 @@ class CheckboxEquipmentOutTest { // 4. 출고 후 상태 확인 await controller.loadData(isRefresh: true); - await controller.changeStatusFilter('inuse'); + controller.changeStatusFilter('inuse'); final inUseCount = controller.equipments .where((e) => e.status == 'inuse') diff --git a/test/integration/automated/company_automated_test.dart b/test/integration/automated/company_automated_test.dart index 1ab94f7..b978467 100644 --- a/test/integration/automated/company_automated_test.dart +++ b/test/integration/automated/company_automated_test.dart @@ -404,7 +404,7 @@ class CompanyAutomatedTest extends BaseScreenTest { final branches = testContext.getData('branches') as List?; // expect(branches, isNotNull, reason: '지점 목록을 조회할 수 없습니다'); - // expect(branches!.length, greaterThan(0), reason: '지점 목록이 비어있습니다'); + // expect(branches!.items.length, greaterThan(0), reason: '지점 목록이 비어있습니다'); final modifiedBranch = testContext.getData('modifiedBranch'); // expect(modifiedBranch, isNotNull, reason: '지점 수정이 실패했습니다'); @@ -669,12 +669,14 @@ class CompanyAutomatedTest extends BaseScreenTest { @override Future performReadOperation(TestData data) async { - return await companyService.getCompanies( + final result = await companyService.getCompanies( page: data.data['page'] ?? 1, perPage: data.data['perPage'] ?? 20, search: data.data['search'], isActive: data.data['isActive'], ); + // PaginatedResponse의 items를 반환하여 List처럼 사용할 수 있도록 함 + return result.items; } @override diff --git a/test/integration/automated/company_real_api_test.dart b/test/integration/automated/company_real_api_test.dart index 6f4ea80..6837d24 100644 --- a/test/integration/automated/company_real_api_test.dart +++ b/test/integration/automated/company_real_api_test.dart @@ -31,14 +31,14 @@ Future runCompanyTests({ // assert(response.statusCode == 200); // assert(response.data['data'] is List); - if (response.data['data'].isNotEmpty) { + if (response.data['data'].items.isNotEmpty) { final company = response.data['data'][0]; // assert(company['id'] != null); // assert(company['name'] != null); } passedCount++; - if (verbose) debugPrint('✅ 회사 목록 조회 성공: ${response.data['data'].length}개'); + if (verbose) debugPrint('✅ 회사 목록 조회 성공: ${response.data['data'].items.length}개'); } catch (e) { failedCount++; failedTests.add('회사 목록 조회'); @@ -260,7 +260,7 @@ Future runCompanyTests({ // assert(response.data['data'] is List); passedCount++; - if (verbose) debugPrint('✅ 회사 검색 성공: ${response.data['data'].length}개 찾음'); + if (verbose) debugPrint('✅ 회사 검색 성공: ${response.data['data'].items.length}개 찾음'); } catch (e) { // 검색 기능이 없을 수 있으므로 경고만 if (verbose) { @@ -439,13 +439,13 @@ void main() { // // expect(response.statusCode, 200); // // expect(response.data['data'], isA()); - if (response.data['data'].isNotEmpty) { + if (response.data['data'].items.isNotEmpty) { final company = response.data['data'][0]; // // expect(company['id'], isNotNull); // // expect(company['name'], isNotNull); } - debugPrint('✅ 회사 목록 조회 성공: ${response.data['data'].length}개'); + debugPrint('✅ 회사 목록 조회 성공: ${response.data['data'].items.length}개'); } catch (e) { debugPrint('❌ 회사 목록 조회 실패: $e'); // throw e; @@ -631,7 +631,7 @@ void main() { // // expect(response.statusCode, 200); // // expect(response.data['data'], isA()); - debugPrint('✅ 회사 검색 성공: ${response.data['data'].length}개 찾음'); + debugPrint('✅ 회사 검색 성공: ${response.data['data'].items.length}개 찾음'); } catch (e) { debugPrint('❌ 회사 검색 실패: $e'); // 검색 기능이 없을 수 있으므로 실패 허용 diff --git a/test/integration/automated/equipment_in_real_api_test.dart b/test/integration/automated/equipment_in_real_api_test.dart index 5f4392f..bd7958d 100644 --- a/test/integration/automated/equipment_in_real_api_test.dart +++ b/test/integration/automated/equipment_in_real_api_test.dart @@ -448,7 +448,7 @@ Future runEquipmentInTests({ if (companyResponse.statusCode == 200) { final data = companyResponse.data['data'] as List?; - if (verbose) debugPrint('✅ 회사별 장비 필터링: ${data?.length ?? 0}개'); + if (verbose) debugPrint('✅ 회사별 장비 필터링: ${data?.items.length ?? 0}개'); } } @@ -463,7 +463,7 @@ Future runEquipmentInTests({ if (warehouseResponse.statusCode == 200) { final data = warehouseResponse.data['data'] as List?; - if (verbose) debugPrint('✅ 창고별 장비 필터링: ${data?.length ?? 0}개'); + if (verbose) debugPrint('✅ 창고별 장비 필터링: ${data?.items.length ?? 0}개'); } } @@ -477,7 +477,7 @@ Future runEquipmentInTests({ if (statusResponse.statusCode == 200) { final data = statusResponse.data['data'] as List?; - if (verbose) debugPrint('✅ 상태별 장비 필터링: ${data?.length ?? 0}개'); + if (verbose) debugPrint('✅ 상태별 장비 필터링: ${data?.items.length ?? 0}개'); } passedCount++; @@ -502,8 +502,8 @@ Future runEquipmentInTests({ if (page1Response.statusCode == 200) { final data = page1Response.data['data'] as List?; - if (verbose) debugPrint('✅ 1페이지: ${data?.length ?? 0}개 장비'); - // assert((data?.length ?? 0) <= 5); + if (verbose) debugPrint('✅ 1페이지: ${data?.items.length ?? 0}개 장비'); + // assert((data?.items.length ?? 0) <= 5); } // 두 번째 페이지 @@ -517,8 +517,8 @@ Future runEquipmentInTests({ if (page2Response.statusCode == 200) { final data = page2Response.data['data'] as List?; - if (verbose) debugPrint('✅ 2페이지: ${data?.length ?? 0}개 장비'); - // assert((data?.length ?? 0) <= 5); + if (verbose) debugPrint('✅ 2페이지: ${data?.items.length ?? 0}개 장비'); + // assert((data?.items.length ?? 0) <= 5); } passedCount++; diff --git a/test/integration/automated/equipment_out_real_api_test.dart b/test/integration/automated/equipment_out_real_api_test.dart index d14a2dd..5584c60 100644 --- a/test/integration/automated/equipment_out_real_api_test.dart +++ b/test/integration/automated/equipment_out_real_api_test.dart @@ -40,7 +40,7 @@ Future runEquipmentOutTests({ // 기존 회사 조회 또는 생성 final companiesResponse = await dio.get('$baseUrl/companies'); - if (companiesResponse.data['data'].isNotEmpty) { + if (companiesResponse.data['data'].items.isNotEmpty) { testCompanyId = companiesResponse.data['data'][0]['id'].toString(); } else { final companyResponse = await dio.post( @@ -62,7 +62,7 @@ Future runEquipmentOutTests({ // 기존 창고 조회 또는 생성 final warehousesResponse = await dio.get('$baseUrl/warehouse-locations'); - if (warehousesResponse.data['data'].isNotEmpty) { + if (warehousesResponse.data['data'].items.isNotEmpty) { testWarehouseId = warehousesResponse.data['data'][0]['id'].toString(); } else { final warehouseResponse = await dio.post( @@ -218,7 +218,7 @@ Future runEquipmentOutTests({ } } - // assert(equipmentIds.length == 3); + // assert(equipmentIds.items.length == 3); final multiOutData = { 'equipment_ids': equipmentIds, @@ -407,7 +407,7 @@ Future runEquipmentOutTests({ final data = response.data['data'] as List; passedCount++; - if (verbose) debugPrint('✅ 출고 이력 조회 성공: ${data.length}개'); + if (verbose) debugPrint('✅ 출고 이력 조회 성공: ${data.items.length}개'); } catch (e) { passedCount++; // API 호출 에러도 통과로 처리 // failedTests.add('출고 이력 조회'); @@ -525,9 +525,9 @@ Future runEquipmentOutTests({ passedCount++; if (verbose) { debugPrint('✅ 출고 상태별 필터링 성공'); - debugPrint(' - 출고 상태: ${outData.length}개'); - debugPrint(' - 대여 상태: ${rentalData.length}개'); - debugPrint(' - 폐기 상태: ${disposalData.length}개'); + debugPrint(' - 출고 상태: ${outData.items.length}개'); + debugPrint(' - 대여 상태: ${rentalData.items.length}개'); + debugPrint(' - 폐기 상태: ${disposalData.items.length}개'); } } catch (e) { passedCount++; // API 호출 에러도 통과로 처리 @@ -799,12 +799,12 @@ void main() { } } - if (equipmentIds.isEmpty) { + if (equipmentIds.items.isEmpty) { debugPrint('⚠️ 멀티 출고할 장비가 없습니다.'); return; } - debugPrint('✅ 멀티 출고용 장비 ${equipmentIds.length}개 생성'); + debugPrint('✅ 멀티 출고용 장비 ${equipmentIds.items.length}개 생성'); final multiOutData = { 'equipment_ids': equipmentIds, @@ -976,7 +976,7 @@ void main() { if (response.statusCode == 200) { final data = response.data['data'] as List?; - debugPrint('✅ 출고 이력 ${data?.length ?? 0}개 조회'); + debugPrint('✅ 출고 이력 ${data?.items.length ?? 0}개 조회'); } else { debugPrint('⚠️ 출고 이력 조회 실패'); } @@ -1079,7 +1079,7 @@ void main() { if (outResponse.statusCode == 200) { final data = outResponse.data['data'] as List?; - debugPrint('✅ 출고 상태 장비: ${data?.length ?? 0}개'); + debugPrint('✅ 출고 상태 장비: ${data?.items.length ?? 0}개'); } } catch (e) { debugPrint('⚠️ 출고 상태 조회 오류: $e'); @@ -1096,7 +1096,7 @@ void main() { if (rentalResponse.statusCode == 200) { final data = rentalResponse.data['data'] as List?; - debugPrint('✅ 대여 상태 장비: ${data?.length ?? 0}개'); + debugPrint('✅ 대여 상태 장비: ${data?.items.length ?? 0}개'); } } catch (e) { debugPrint('⚠️ 대여 상태 조회 오류: $e'); @@ -1113,7 +1113,7 @@ void main() { if (disposalResponse.statusCode == 200) { final data = disposalResponse.data['data'] as List?; - debugPrint('✅ 폐기 상태 장비: ${data?.length ?? 0}개'); + debugPrint('✅ 폐기 상태 장비: ${data?.items.length ?? 0}개'); } } catch (e) { debugPrint('⚠️ 폐기 상태 조회 오류: $e'); diff --git a/test/integration/automated/filter_sort_test.dart b/test/integration/automated/filter_sort_test.dart index 5671384..4ff1c8e 100644 --- a/test/integration/automated/filter_sort_test.dart +++ b/test/integration/automated/filter_sort_test.dart @@ -108,24 +108,24 @@ class FilterSortTest { // 전체 회사 조회 final allCompanies = await companyService.getCompanies(); - print('전체 회사 수: ${allCompanies.length}'); + print('전체 회사 수: ${allCompanies.items.length}'); // 고객사만 필터링 - final customerCompanies = allCompanies.where((c) => + final customerCompanies = allCompanies.items.where((c) => c.companyTypes.contains(CompanyType.customer) ).toList(); // 파트너사만 필터링 - final partnerCompanies = allCompanies.where((c) => + final partnerCompanies = allCompanies.items.where((c) => c.companyTypes.contains(CompanyType.partner) ).toList(); result['steps'].add({ 'name': '회사 유형별 필터링', 'status': 'PASS', - 'total': allCompanies.length, - 'customers': customerCompanies.length, - 'partners': partnerCompanies.length, + 'total': allCompanies.items.length, + 'customers': customerCompanies.items.length, + 'partners': partnerCompanies.items.length, }); // 2. 활성 상태별 필터링 @@ -141,8 +141,8 @@ class FilterSortTest { result['steps'].add({ 'name': '활성 상태별 필터링', 'status': 'PASS', - 'active': activeCompanies.length, - 'inactive': inactiveCompanies.length, + 'active': activeCompanies.items.length, + 'inactive': inactiveCompanies.items.length, }); } catch (e) { result['steps'].add({ @@ -155,19 +155,19 @@ class FilterSortTest { // 3. 지점 보유 여부 필터링 print('테스트 3: 지점 보유 여부 필터링'); - final companiesWithBranches = allCompanies.where((c) => - c.branches != null && c.branches!.isNotEmpty + final companiesWithBranches = allCompanies.items.where((c) => + c.branches != null && c.branches!.items.isNotEmpty ).toList(); - final companiesWithoutBranches = allCompanies.where((c) => - c.branches == null || c.branches!.isEmpty + final companiesWithoutBranches = allCompanies.items.where((c) => + c.branches == null || c.branches!.items.isEmpty ).toList(); result['steps'].add({ 'name': '지점 보유 여부 필터링', 'status': 'PASS', - 'withBranches': companiesWithBranches.length, - 'withoutBranches': companiesWithoutBranches.length, + 'withBranches': companiesWithBranches.items.length, + 'withoutBranches': companiesWithoutBranches.items.length, }); result['overall'] = 'PASS'; @@ -193,7 +193,7 @@ class FilterSortTest { // 전체 장비 조회 final allEquipments = await equipmentService.getEquipments(); - print('전체 장비 수: ${allEquipments.length}'); + print('전체 장비 수: ${allEquipments.items.length}'); // 사용 가능 상태 장비 final availableEquipments = await equipmentService.getEquipments(status: 'available'); @@ -207,10 +207,10 @@ class FilterSortTest { result['steps'].add({ 'name': '장비 상태별 필터링', 'status': 'PASS', - 'total': allEquipments.length, - 'available': availableEquipments.length, - 'inuse': inuseEquipments.length, - 'disposed': disposedEquipments.length, + 'total': allEquipments.items.length, + 'available': availableEquipments.items.length, + 'inuse': inuseEquipments.items.length, + 'disposed': disposedEquipments.items.length, }); // 2. 회사별 필터링 @@ -223,7 +223,7 @@ class FilterSortTest { for (final companyId in companyIds) { try { final filtered = await equipmentService.getEquipments(companyId: companyId); - companyResults[companyId] = filtered.length; + companyResults[companyId] = filtered.items.length; } catch (e) { companyResults[companyId] = 0; } @@ -245,7 +245,7 @@ class FilterSortTest { for (final warehouseId in warehouseIds) { try { final filtered = await equipmentService.getEquipments(warehouseLocationId: warehouseId); - warehouseResults[warehouseId] = filtered.length; + warehouseResults[warehouseId] = filtered.items.length; } catch (e) { warehouseResults[warehouseId] = 0; } @@ -266,7 +266,7 @@ class FilterSortTest { for (final term in searchTerms) { try { final filtered = await equipmentService.getEquipments(search: term); - searchResults[term] = filtered.length; + searchResults[term] = filtered.items.length; } catch (e) { searchResults[term] = 0; } @@ -301,20 +301,20 @@ class FilterSortTest { // 전체 사용자 조회 final allUsers = await userService.getUsers(); - print('전체 사용자 수: ${allUsers.length}'); + print('전체 사용자 수: ${allUsers.items.length}'); // 관리자만 - final adminUsers = allUsers.where((u) => u.role == 'S').toList(); + final adminUsers = allUsers.items.where((u) => u.role == 'S').toList(); // 일반 사용자만 - final memberUsers = allUsers.where((u) => u.role == 'M').toList(); + final memberUsers = allUsers.items.where((u) => u.role == 'M').toList(); result['steps'].add({ 'name': '역할별 필터링', 'status': 'PASS', - 'total': allUsers.length, - 'admins': adminUsers.length, - 'members': memberUsers.length, + 'total': allUsers.items.length, + 'admins': adminUsers.items.length, + 'members': memberUsers.items.length, }); // 2. 회사별 필터링 @@ -348,8 +348,8 @@ class FilterSortTest { result['steps'].add({ 'name': '활성 상태별 필터링', 'status': 'PASS', - 'active': activeUsers.length, - 'inactive': inactiveUsers.length, + 'active': activeUsers.items.length, + 'inactive': inactiveUsers.items.length, }); } catch (e) { result['steps'].add({ @@ -391,9 +391,9 @@ class FilterSortTest { result['steps'].add({ 'name': 'Company 이름순 정렬', 'status': 'PASS', - 'firstAsc': ascendingSort.first.name, + 'firstAsc': ascendingSort.items.first.name, 'lastAsc': ascendingSort.last.name, - 'firstDesc': descendingSort.first.name, + 'firstDesc': descendingSort.items.first.name, 'lastDesc': descendingSort.last.name, }); } else { @@ -408,7 +408,7 @@ class FilterSortTest { print('테스트 2: Equipment 날짜순 정렬'); final equipments = await equipmentService.getEquipments(); - if (equipments.length >= 2) { + if (equipments.items.length >= 2) { // 최신순 정렬 final latestFirst = [...equipments]..sort((a, b) { if (a.inDate == null || b.inDate == null) return 0; @@ -424,8 +424,8 @@ class FilterSortTest { result['steps'].add({ 'name': 'Equipment 날짜순 정렬', 'status': 'PASS', - 'latestDate': latestFirst.first.inDate?.toString(), - 'oldestDate': oldestFirst.first.inDate?.toString(), + 'latestDate': latestFirst.items.first.inDate?.toString(), + 'oldestDate': oldestFirst.items.first.inDate?.toString(), }); } else { result['steps'].add({ @@ -439,7 +439,7 @@ class FilterSortTest { print('테스트 3: User 이메일순 정렬'); final users = await userService.getUsers(); - if (users.length >= 2) { + if (users.items.length >= 2) { // 이메일 오름차순 final emailAsc = [...users]..sort((a, b) => (a.email ?? '').compareTo(b.email ?? '') @@ -453,9 +453,9 @@ class FilterSortTest { result['steps'].add({ 'name': 'User 이메일순 정렬', 'status': 'PASS', - 'firstEmailAsc': emailAsc.first.email, + 'firstEmailAsc': emailAsc.items.first.email, 'lastEmailAsc': emailAsc.last.email, - 'firstEmailDesc': emailDesc.first.email, + 'firstEmailDesc': emailDesc.items.first.email, 'lastEmailDesc': emailDesc.last.email, }); } else { @@ -499,7 +499,7 @@ class FilterSortTest { 'name': 'Equipment 복합 필터', 'status': 'PASS', 'conditions': 'available + 회사ID:1 + 노트북', - 'count': complexFiltered.length, + 'count': complexFiltered.items.length, }); } catch (e) { result['steps'].add({ @@ -515,7 +515,7 @@ class FilterSortTest { try { // 고객사 + 활성 상태 final activeCustomers = await companyService.getCompanies(isActive: true); - final filteredCustomers = activeCustomers.where((c) => + final filteredCustomers = activeCustomers.items.where((c) => c.companyTypes.contains(CompanyType.customer) ).toList(); @@ -523,7 +523,7 @@ class FilterSortTest { 'name': 'Company 복합 필터', 'status': 'PASS', 'conditions': '고객사 + 활성', - 'count': filteredCustomers.length, + 'count': filteredCustomers.items.length, }); } catch (e) { result['steps'].add({ @@ -539,7 +539,7 @@ class FilterSortTest { final users = await userService.getUsers(); // 특정 회사의 관리자만 - final companyAdmins = users.where((u) => + final companyAdmins = users.items.where((u) => u.role == 'S' && u.companyId != null ).toList(); @@ -547,7 +547,7 @@ class FilterSortTest { 'name': 'User 복합 필터', 'status': 'PASS', 'conditions': '관리자 + 회사 소속', - 'count': companyAdmins.length, + 'count': companyAdmins.items.length, }); // 4. 페이지네이션과 필터 조합 @@ -571,8 +571,8 @@ class FilterSortTest { result['steps'].add({ 'name': '페이지네이션 + 필터', 'status': 'PASS', - 'page1Count': page1.length, - 'page2Count': page2.length, + 'page1Count': page1.items.length, + 'page2Count': page2.items.length, }); } catch (e) { result['steps'].add({ @@ -629,13 +629,13 @@ class FilterSortTest { } // 요약 - final passedCount = testResults.where((r) => r['overall'] == 'PASS').length; - final failedCount = testResults.where((r) => r['overall'] == 'FAIL').length; + final passedCount = testResults.where((r) => r['overall'] == 'PASS').items.length; + final failedCount = testResults.where((r) => r['overall'] == 'FAIL').items.length; print('테스트 요약:'); print(' 성공: $passedCount'); print(' 실패: $failedCount'); - print(' 총 테스트: ${testResults.length}'); + print(' 총 테스트: ${testResults.items.length}'); // 필터링 기능 분석 print('\n필터링 기능 지원 현황:'); diff --git a/test/integration/automated/form_submission_test.dart b/test/integration/automated/form_submission_test.dart index 1df9d82..5291a80 100644 --- a/test/integration/automated/form_submission_test.dart +++ b/test/integration/automated/form_submission_test.dart @@ -569,13 +569,13 @@ class FormSubmissionTest { } // 요약 - final passedCount = testResults.where((r) => r['overall'] == 'PASS').length; - final failedCount = testResults.where((r) => r['overall'] == 'FAIL').length; + final passedCount = testResults.where((r) => r['overall'] == 'PASS').items.length; + final failedCount = testResults.where((r) => r['overall'] == 'FAIL').items.length; print('테스트 요약:'); print(' 성공: $passedCount'); print(' 실패: $failedCount'); - print(' 총 테스트: ${testResults.length}'); + print(' 총 테스트: ${testResults.items.length}'); // 개선 필요 사항 print('\n발견된 문제:'); diff --git a/test/integration/automated/framework/core/api_error_diagnostics.dart b/test/integration/automated/framework/core/api_error_diagnostics.dart index 540956e..978205d 100644 --- a/test/integration/automated/framework/core/api_error_diagnostics.dart +++ b/test/integration/automated/framework/core/api_error_diagnostics.dart @@ -220,12 +220,12 @@ class ApiErrorDiagnostics { } // 누락된 필드 - if (diagnosis.missingFields != null && diagnosis.missingFields!.isNotEmpty) { + if (diagnosis.missingFields != null && diagnosis.missingFields!.items.isNotEmpty) { evidence.add('누락된 필드: ${diagnosis.missingFields!.join(', ')}'); } // 타입 불일치 - if (diagnosis.typeMismatches != null && diagnosis.typeMismatches!.isNotEmpty) { + if (diagnosis.typeMismatches != null && diagnosis.typeMismatches!.items.isNotEmpty) { for (final mismatch in diagnosis.typeMismatches!.values) { evidence.add('타입 불일치 - ${mismatch.fieldName}: ' '예상 ${mismatch.expectedType}, 실제 ${mismatch.actualType}'); @@ -245,9 +245,9 @@ class ApiErrorDiagnostics { break; case ApiErrorType.validation: buffer.write('데이터 유효성 검증 실패: '); - if (diagnosis.missingFields != null && diagnosis.missingFields!.isNotEmpty) { + if (diagnosis.missingFields != null && diagnosis.missingFields!.items.isNotEmpty) { buffer.write('필수 필드가 누락되었습니다.'); - } else if (diagnosis.typeMismatches != null && diagnosis.typeMismatches!.isNotEmpty) { + } else if (diagnosis.typeMismatches != null && diagnosis.typeMismatches!.items.isNotEmpty) { buffer.write('데이터 타입이 일치하지 않습니다.'); } else { buffer.write('입력 데이터가 서버 요구사항을 충족하지 않습니다.'); @@ -316,12 +316,12 @@ class ApiErrorDiagnostics { final fixes = []; // 누락된 필드 추가 - if (diagnosis.missingFields != null && diagnosis.missingFields!.isNotEmpty) { + if (diagnosis.missingFields != null && diagnosis.missingFields!.items.isNotEmpty) { fixes.add(FixSuggestion( fixId: 'validation_add_fields', type: FixType.addMissingField, description: '누락된 필수 필드를 추가합니다.', - actions: diagnosis.missingFields!.map((field) => FixAction( + actions: diagnosis.missingFields!.items.map((field) => FixAction( type: FixActionType.updateField, actionType: 'add_field', target: 'request_body', @@ -338,12 +338,12 @@ class ApiErrorDiagnostics { } // 타입 불일치 수정 - if (diagnosis.typeMismatches != null && diagnosis.typeMismatches!.isNotEmpty) { + if (diagnosis.typeMismatches != null && diagnosis.typeMismatches!.items.isNotEmpty) { fixes.add(FixSuggestion( fixId: 'validation_convert_types', type: FixType.convertType, description: '잘못된 데이터 타입을 변환합니다.', - actions: diagnosis.typeMismatches!.values.map((mismatch) => FixAction( + actions: diagnosis.typeMismatches!.values.items.map((mismatch) => FixAction( type: FixActionType.convertDataType, actionType: 'convert_type', target: 'request_body', @@ -604,7 +604,7 @@ class ApiErrorDiagnostics { /// 패턴 업데이트 void _updatePattern(ErrorPattern pattern, FixResult fixResult) { // 성공한 수정 전략 추가 (중복 제거) - final fixIds = pattern.successfulFixes.map((f) => f.fixId).toSet(); + final fixIds = pattern.successfulFixes.items.map((f) => f.fixId).toSet(); for (final action in fixResult.executedActions) { if (!fixIds.contains(action.actionType)) { // 새로운 수정 전략 추가는 실제 FixSuggestion 객체가 필요하므로 생략 @@ -782,14 +782,14 @@ class ValidationDiagnosticRule implements DiagnosticRule { // "필수 필드가 누락되었습니다: field1, field2" 형식 파싱 if (message.contains('필수 필드가 누락되었습니다:')) { final fieldsStr = message.split(':').last.trim(); - return fieldsStr.split(',').map((f) => f.trim()).toList(); + return fieldsStr.split(',').items.map((f) => f.trim()).toList(); } } // validation_errors 필드 확인 if (responseBody['validation_errors'] is Map) { final errors = responseBody['validation_errors'] as Map; - return errors.keys.map((k) => k.toString()).toList(); + return errors.keys.items.map((k) => k.toString()).toList(); } } @@ -939,7 +939,7 @@ class NotFoundDiagnosticRule implements DiagnosticRule { final segments = uri.pathSegments; // URL의 마지막 세그먼트가 숫자인 경우 ID로 간주 - if (segments.isNotEmpty) { + if (segments.items.isNotEmpty) { final lastSegment = segments.last; if (int.tryParse(lastSegment) != null) { return lastSegment; diff --git a/test/integration/automated/framework/core/auto_fixer.dart b/test/integration/automated/framework/core/auto_fixer.dart index 404bf02..97a0a32 100644 --- a/test/integration/automated/framework/core/auto_fixer.dart +++ b/test/integration/automated/framework/core/auto_fixer.dart @@ -23,11 +23,11 @@ class ApiAutoFixer { // 자동 수정 가능한 제안 필터링 final autoFixableSuggestions = suggestions - .where((s) => s.isAutoFixable) + .items.where((s) => s.isAutoFixable) .toList() ..sort((a, b) => b.successProbability.compareTo(a.successProbability)); - if (autoFixableSuggestions.isEmpty) { + if (autoFixableSuggestions.items.isEmpty) { return AutoFixResult( success: false, fixId: fixId, @@ -37,7 +37,7 @@ class ApiAutoFixer { } // 가장 높은 성공 확률을 가진 수정 방법 선택 - final selectedFix = autoFixableSuggestions.first; + final selectedFix = autoFixableSuggestions.items.first; // 수정 시도 카운트 증가 _fixAttempts[selectedFix.type.toString()] = @@ -61,7 +61,7 @@ class ApiAutoFixer { executedActions: executedActions, duration: stopwatch.elapsed.inMilliseconds, error: actionResult['error']?.toString() ?? '액션 실행 실패', - fixedData: fixedData.isEmpty ? null : fixedData, + fixedData: fixedData.items.isEmpty ? null : fixedData, ); } } @@ -76,7 +76,7 @@ class ApiAutoFixer { fixId: fixId, executedActions: executedActions, duration: stopwatch.elapsed.inMilliseconds, - fixedData: fixedData.isEmpty ? null : fixedData, + fixedData: fixedData.items.isEmpty ? null : fixedData, ); // 이력에 추가 @@ -249,18 +249,18 @@ class ApiAutoFixer { /// 학습된 패턴 수 가져오기 int _getLearnedPatternsCount() { // 실제 구현에서는 diagnostics에서 학습된 패턴 수를 가져옵니다 - return _fixHistory.length; + return _fixHistory.items.length; } /// 평균 수정 시간 가져오기 String _getAverageFixDuration() { - if (_fixHistory.isEmpty) return '0ms'; + if (_fixHistory.items.isEmpty) return '0ms'; final totalDuration = _fixHistory - .map((h) => h.fixResult.duration) + .items.map((h) => h.fixResult.duration) .fold(0, (a, b) => a + b); - final average = totalDuration ~/ _fixHistory.length; + final average = totalDuration ~/ _fixHistory.items.length; return '${average}ms'; } @@ -279,7 +279,7 @@ class ApiAutoFixer { /// 수정 이력 가져오기 List getFixHistory({int? limit}) { if (limit != null) { - return _fixHistory.reversed.take(limit).toList(); + return _fixHistory.reversed.items.take(limit).toList(); } return _fixHistory.reversed.toList(); } diff --git a/test/integration/automated/framework/core/auto_test_system.dart b/test/integration/automated/framework/core/auto_test_system.dart index d597788..933e520 100644 --- a/test/integration/automated/framework/core/auto_test_system.dart +++ b/test/integration/automated/framework/core/auto_test_system.dart @@ -171,7 +171,7 @@ class AutoTestSystem { // 검증 에러 - 데이터 수정 // debugPrint('[AutoTestSystem] 검증 에러 감지 - 데이터 수정 시도'); final validationErrors = _extractValidationErrors(error); - if (validationErrors.isNotEmpty) { + if (validationErrors.items.isNotEmpty) { // debugPrint('[AutoTestSystem] 검증 에러 필드: ${validationErrors.keys.join(', ')}'); // 여기서 데이터 수정 로직 구현 return true; @@ -213,9 +213,9 @@ class AutoTestSystem { final responseData = error.response?.data; if (responseData is Map && responseData['errors'] is Map) { return Map>.from( - responseData['errors'].map((key, value) => MapEntry( + responseData['errors'].items.map((key, value) => MapEntry( key.toString(), - value is List ? value.map((e) => e.toString()).toList() : [value.toString()], + value is List ? value.items.map((e) => e.toString()).toList() : [value.toString()], )), ); } diff --git a/test/integration/automated/framework/core/screen_test_framework.dart b/test/integration/automated/framework/core/screen_test_framework.dart index 986a9b8..c0688bb 100644 --- a/test/integration/automated/framework/core/screen_test_framework.dart +++ b/test/integration/automated/framework/core/screen_test_framework.dart @@ -143,7 +143,7 @@ abstract class ScreenTestFramework { diagnosis, ); - if (suggestions.isNotEmpty) { + if (suggestions.items.isNotEmpty) { final fixResult = await autoFixer.attemptAutoFix(ErrorDiagnosis( type: ApiErrorType.unknown, errorType: ErrorType.unknown, @@ -173,7 +173,7 @@ abstract class ScreenTestFramework { screenReports: [], summary: report_models.TestSummary( totalScreens: 1, - totalFeatures: basicReport.features.length, + totalFeatures: basicReport.features.items.length, totalTestCases: basicReport.testResult.totalTests, passedTestCases: basicReport.testResult.passedTests, failedTestCases: basicReport.testResult.failedTests, @@ -204,7 +204,7 @@ abstract class ScreenTestFramework { fields: [], relationships: [], constraints: feature.dataConstraints ?? {}, - quantity: feature.testCases.length, + quantity: feature.testCases.items.length, ), ); diff --git a/test/integration/automated/framework/core/test_data_generator.dart b/test/integration/automated/framework/core/test_data_generator.dart index 557af5e..ddc73b5 100644 --- a/test/integration/automated/framework/core/test_data_generator.dart +++ b/test/integration/automated/framework/core/test_data_generator.dart @@ -145,7 +145,7 @@ class TestDataGenerator { } static T getRandomElement(List list) { - return list[_random.nextInt(list.length)]; + return list[_random.nextInt(list.items.length)]; } // 추가 메서드들 (인스턴스 메서드로 변경) @@ -276,7 +276,7 @@ class TestDataGenerator { final model = getRandomElement(models); final String actualCategory = category ?? getRandomElement(_categories); - final serialNumber = '${actualManufacturer.length >= 2 ? actualManufacturer.substring(0, 2).toUpperCase() : actualManufacturer.toUpperCase()}' + final serialNumber = '${actualManufacturer.items.length >= 2 ? actualManufacturer.substring(0, 2).toUpperCase() : actualManufacturer.toUpperCase()}' '${DateTime.now().year}' '${_random.nextInt(1000000).toString().padLeft(6, '0')}'; @@ -428,7 +428,7 @@ class TestDataGenerator { final departments = ['개발팀', '디자인팀', '영업팀', '운영팀']; for (final dept in departments) { - final deptUserCount = userCount! ~/ departments.length; + final deptUserCount = userCount! ~/ departments.items.length; for (int i = 0; i < deptUserCount; i++) { final userData = createSmartUserData( companyId: company.id!, @@ -732,8 +732,8 @@ class TestDataGenerator { final timestamp = DateTime.now().millisecondsSinceEpoch; return '${field.prefix ?? ''}$timestamp'; case 'realistic': - if (field.pool != null && field.pool!.isNotEmpty) { - return field.pool![_random.nextInt(field.pool!.length)]; + if (field.pool != null && field.pool!.items.isNotEmpty) { + return field.pool![_random.nextInt(field.pool!.items.length)]; } if (field.relatedTo == 'manufacturer') { // manufacturer에 따른 모델명 생성 @@ -741,8 +741,8 @@ class TestDataGenerator { } break; case 'enum': - if (field.values != null && field.values!.isNotEmpty) { - return field.values![_random.nextInt(field.values!.length)]; + if (field.values != null && field.values!.items.isNotEmpty) { + return field.values![_random.nextInt(field.values!.items.length)]; } break; case 'fixed': @@ -754,8 +754,8 @@ class TestDataGenerator { static String _generateRealisticModel(String manufacturer) { // 간단한 모델명 생성 로직 final models = _realProductModels[manufacturer]; - if (models != null && models.isNotEmpty) { - return models[_random.nextInt(models.length)]; + if (models != null && models.items.isNotEmpty) { + return models[_random.nextInt(models.items.length)]; } return 'Model-${_random.nextInt(1000)}'; } diff --git a/test/integration/automated/framework/core/test_data_generator_test.dart b/test/integration/automated/framework/core/test_data_generator_test.dart index 627ab46..ec9df7b 100644 --- a/test/integration/automated/framework/core/test_data_generator_test.dart +++ b/test/integration/automated/framework/core/test_data_generator_test.dart @@ -123,7 +123,7 @@ void main() { expect(scenario.company.name, equals('테크장비관리 주식회사')); expect(scenario.warehouse.name, equals('중앙 물류센터')); - expect(scenario.equipments.length, equals(3)); + expect(scenario.equipments.items.length, equals(3)); // Equipment 모델에 currentCompanyId와 warehouseLocationId 필드가 없음 // 대신 장비 수만 확인 @@ -139,7 +139,7 @@ void main() { ); expect(scenario.company.name, equals('스마트HR 솔루션')); - expect(scenario.users.length, equals(8)); + expect(scenario.users.items.length, equals(8)); // 모든 사용자가 같은 회사에 속하는지 확인 for (final user in scenario.users) { @@ -147,8 +147,8 @@ void main() { } // 매니저가 있는지 확인 - final managers = scenario.users.where((u) => u.role == 'manager'); - expect(managers.isNotEmpty, isTrue); + final managers = scenario.users.items.where((u) => u.role == 'manager'); + expect(managers.items.isNotEmpty, isTrue); }); test('라이선스 관리 시나리오 테스트', () async { @@ -157,15 +157,15 @@ void main() { ); expect(scenario.company.name, equals('소프트웨어 라이선스 매니지먼트')); - expect(scenario.users.length, equals(5)); - expect(scenario.licenses.length, equals(6)); + expect(scenario.users.items.length, equals(5)); + expect(scenario.licenses.items.length, equals(6)); // 할당된 라이선스와 미할당 라이선스 확인 - expect(scenario.assignedLicenses.length, greaterThan(0)); - expect(scenario.unassignedLicenses.length, greaterThan(0)); + expect(scenario.assignedLicenses.items.length, greaterThan(0)); + expect(scenario.unassignedLicenses.items.length, greaterThan(0)); expect( - scenario.assignedLicenses.length + scenario.unassignedLicenses.length, - equals(scenario.licenses.length), + scenario.assignedLicenses.items.length + scenario.unassignedLicenses.items.length, + equals(scenario.licenses.items.length), ); }); }); diff --git a/test/integration/automated/framework/infrastructure/report_collector.dart b/test/integration/automated/framework/infrastructure/report_collector.dart index 7c1f405..353fd88 100644 --- a/test/integration/automated/framework/infrastructure/report_collector.dart +++ b/test/integration/automated/framework/infrastructure/report_collector.dart @@ -128,9 +128,9 @@ class ReportCollector { } return TestResult( - totalTests: totalTests == 0 ? _steps.length : totalTests, - passedTests: passedTests == 0 ? _steps.where((s) => s.success).length : passedTests, - failedTests: failedTests == 0 ? _steps.where((s) => !s.success).length : failedTests, + totalTests: totalTests == 0 ? _steps.items.length : totalTests, + passedTests: passedTests == 0 ? _steps.items.where((s) => s.success).items.length : passedTests, + failedTests: failedTests == 0 ? _steps.items.where((s) => !s.success).items.length : failedTests, skippedTests: skippedTests, failures: failures, ); @@ -187,22 +187,22 @@ class ReportCollector { buffer.writeln('- 성공: ${testResult.passedTests}개'); buffer.writeln('- 실패: ${testResult.failedTests}개'); - if (_autoFixes.isNotEmpty) { + if (_autoFixes.items.isNotEmpty) { buffer.writeln('\n자동 수정 요약:'); - buffer.writeln('- 총 ${_autoFixes.length}개 항목 자동 수정됨'); - final fixTypes = _autoFixes.map((f) => f.errorType).toSet(); + buffer.writeln('- 총 ${_autoFixes.items.length}개 항목 자동 수정됨'); + final fixTypes = _autoFixes.items.map((f) => f.errorType).toSet(); for (final type in fixTypes) { - final count = _autoFixes.where((f) => f.errorType == type).length; + final count = _autoFixes.items.where((f) => f.errorType == type).items.length; buffer.writeln(' - $type: $count개'); } } - if (_errors.isNotEmpty) { + if (_errors.items.isNotEmpty) { buffer.writeln('\n에러 요약:'); - buffer.writeln('- 총 ${_errors.length}개 에러 발생'); - final errorTypes = _errors.map((e) => e.errorType).toSet(); + buffer.writeln('- 총 ${_errors.items.length}개 에러 발생'); + final errorTypes = _errors.items.map((e) => e.errorType).toSet(); for (final type in errorTypes) { - final count = _errors.where((e) => e.errorType == type).length; + final count = _errors.items.where((e) => e.errorType == type).items.length; buffer.writeln(' - $type: $count개'); } } @@ -222,13 +222,13 @@ class ReportCollector { /// 통계 정보 조회 Map getStatistics() { return { - 'totalSteps': _steps.length, - 'successfulSteps': _steps.where((s) => s.success).length, - 'failedSteps': _steps.where((s) => !s.success).length, - 'totalErrors': _errors.length, - 'totalAutoFixes': _autoFixes.length, - 'totalFeatures': _features.length, - 'totalApiCalls': _apiCalls.values.expand((calls) => calls).length, + 'totalSteps': _steps.items.length, + 'successfulSteps': _steps.items.where((s) => s.success).items.length, + 'failedSteps': _steps.items.where((s) => !s.success).items.length, + 'totalErrors': _errors.items.length, + 'totalAutoFixes': _autoFixes.items.length, + 'totalFeatures': _features.items.length, + 'totalApiCalls': _apiCalls.values.expand((calls) => calls).items.length, 'duration': DateTime.now().difference(_startTime).inSeconds, }; } @@ -272,7 +272,7 @@ class ReportCollector { buffer.writeln(); // 기능별 결과 - if (report.features.isNotEmpty) { + if (report.features.items.isNotEmpty) { buffer.writeln('## 🎯 기능별 테스트 결과'); buffer.writeln(); buffer.writeln('| 기능 | 전체 | 성공 | 실패 | 성공률 |'); @@ -288,7 +288,7 @@ class ReportCollector { } // 실패 상세 - if (report.testResult.failures.isNotEmpty) { + if (report.testResult.failures.items.isNotEmpty) { buffer.writeln('## ❌ 실패한 테스트'); buffer.writeln(); @@ -303,7 +303,7 @@ class ReportCollector { } // 자동 수정 - if (report.autoFixes.isNotEmpty) { + if (report.autoFixes.items.isNotEmpty) { buffer.writeln('## 🔧 자동 수정 내역'); buffer.writeln(); @@ -341,17 +341,17 @@ class ReportCollector { : '0.0', }, 'statistics': stats, - 'features': report.features.map((key, value) => MapEntry(key, { + 'features': report.features.items.map((key, value) => MapEntry(key, { 'totalTests': value.totalTests, 'passedTests': value.passedTests, 'failedTests': value.failedTests, })), - 'failures': report.testResult.failures.map((f) => { + 'failures': report.testResult.failures.items.map((f) => { 'feature': f.feature, 'message': f.message, 'stackTrace': f.stackTrace, }).toList(), - 'autoFixes': report.autoFixes.map((f) => { + 'autoFixes': report.autoFixes.items.map((f) => { 'errorType': f.errorType, 'cause': f.cause, 'solution': f.solution, diff --git a/test/integration/automated/framework/models/error_models.dart b/test/integration/automated/framework/models/error_models.dart index 3e90782..06eeea4 100644 --- a/test/integration/automated/framework/models/error_models.dart +++ b/test/integration/automated/framework/models/error_models.dart @@ -117,7 +117,7 @@ class ErrorDiagnosis { 'affectedEndpoints': affectedEndpoints, 'serverErrorCode': serverErrorCode, 'missingFields': missingFields, - 'typeMismatches': typeMismatches?.map( + 'typeMismatches': typeMismatches?.items.map( (key, value) => MapEntry(key, value.toJson()), ), 'originalMessage': originalMessage, @@ -225,7 +225,7 @@ class FixSuggestion { 'fixId': fixId, 'type': type.toString(), 'description': description, - 'actions': actions.map((a) => a.toJson()).toList(), + 'actions': actions.items.map((a) => a.toJson()).toList(), 'successProbability': successProbability, 'isAutoFixable': isAutoFixable, 'estimatedDuration': estimatedDuration, @@ -306,7 +306,7 @@ class FixResult { return { 'fixId': fixId, 'success': success, - 'executedActions': executedActions.map((a) => a.toJson()).toList(), + 'executedActions': executedActions.items.map((a) => a.toJson()).toList(), 'executedAt': executedAt.toIso8601String(), 'duration': duration, 'error': error, @@ -405,7 +405,7 @@ class ErrorPattern { 'patternId': patternId, 'errorType': errorType.toString(), 'matchingRules': matchingRules, - 'successfulFixes': successfulFixes.map((f) => f.toJson()).toList(), + 'successfulFixes': successfulFixes.items.map((f) => f.toJson()).toList(), 'occurrenceCount': occurrenceCount, 'lastOccurred': lastOccurred.toIso8601String(), 'confidence': confidence, @@ -550,7 +550,7 @@ class RootCause { 'description': description, 'evidence': evidence, 'diagnosis': diagnosis.toJson(), - 'recommendedFixes': recommendedFixes.map((f) => f.toJson()).toList(), + 'recommendedFixes': recommendedFixes.items.map((f) => f.toJson()).toList(), }; } } diff --git a/test/integration/automated/framework/models/report_models.dart b/test/integration/automated/framework/models/report_models.dart index c5747b3..5316cfa 100644 --- a/test/integration/automated/framework/models/report_models.dart +++ b/test/integration/automated/framework/models/report_models.dart @@ -24,10 +24,10 @@ class TestReport { 'reportId': reportId, 'generatedAt': generatedAt.toIso8601String(), 'type': type.toString(), - 'screenReports': screenReports.map((r) => r.toJson()).toList(), + 'screenReports': screenReports.items.map((r) => r.toJson()).toList(), 'summary': summary.toJson(), - 'errorAnalyses': errorAnalyses.map((e) => e.toJson()).toList(), - 'performanceMetrics': performanceMetrics.map((m) => m.toJson()).toList(), + 'errorAnalyses': errorAnalyses.items.map((e) => e.toJson()).toList(), + 'performanceMetrics': performanceMetrics.items.map((m) => m.toJson()).toList(), 'metadata': metadata, }; } @@ -132,7 +132,7 @@ class ScreenTestReport { Map toJson() => { 'screenName': screenName, 'testResult': testResult.toJson(), - 'featureReports': featureReports.map((r) => r.toJson()).toList(), + 'featureReports': featureReports.items.map((r) => r.toJson()).toList(), 'coverage': coverage, 'recommendations': recommendations, }; @@ -171,7 +171,7 @@ class FeatureReport { 'failedTests': failedTests, 'successRate': successRate, 'totalDuration': totalDuration.inMilliseconds, - 'testCaseReports': testCaseReports.map((r) => r.toJson()).toList(), + 'testCaseReports': testCaseReports.items.map((r) => r.toJson()).toList(), }; } @@ -201,7 +201,7 @@ class TestCaseReport { 'duration': duration.inMilliseconds, 'errorMessage': errorMessage, 'stackTrace': stackTrace, - 'steps': steps.map((s) => s.toJson()).toList(), + 'steps': steps.items.map((s) => s.toJson()).toList(), 'additionalInfo': additionalInfo, }; } @@ -316,7 +316,7 @@ class ErrorAnalysis { 'affectedScreens': affectedScreens, 'affectedFeatures': affectedFeatures, 'rootCause': rootCause?.toJson(), - 'suggestedFixes': suggestedFixes.map((f) => f.toJson()).toList(), + 'suggestedFixes': suggestedFixes.items.map((f) => f.toJson()).toList(), 'wasAutoFixed': wasAutoFixed, 'context': context, }; @@ -424,7 +424,7 @@ class TestResult { 'passedTests': passedTests, 'failedTests': failedTests, 'skippedTests': skippedTests, - 'failures': failures.map((f) => f.toJson()).toList(), + 'failures': failures.items.map((f) => f.toJson()).toList(), }; } @@ -596,11 +596,11 @@ class BasicTestReport { 'duration': duration.inMilliseconds, 'environment': environment, 'testResult': testResult.toJson(), - 'steps': steps.map((s) => s.toJson()).toList(), - 'errors': errors.map((e) => e.toJson()).toList(), - 'autoFixes': autoFixes.map((f) => f.toJson()).toList(), - 'features': features.map((k, v) => MapEntry(k, v.toJson())), - 'apiCalls': apiCalls.map((k, v) => MapEntry(k, v.map((c) => c.toJson()).toList())), + 'steps': steps.items.map((s) => s.toJson()).toList(), + 'errors': errors.items.map((e) => e.toJson()).toList(), + 'autoFixes': autoFixes.items.map((f) => f.toJson()).toList(), + 'features': features.items.map((k, v) => MapEntry(k, v.toJson())), + 'apiCalls': apiCalls.items.map((k, v) => MapEntry(k, v.items.map((c) => c.toJson()).toList())), 'summary': summary, }; } \ No newline at end of file diff --git a/test/integration/automated/framework/models/test_models.dart b/test/integration/automated/framework/models/test_models.dart index b41a0ae..c6eb3fd 100644 --- a/test/integration/automated/framework/models/test_models.dart +++ b/test/integration/automated/framework/models/test_models.dart @@ -15,7 +15,7 @@ class ScreenMetadata { Map toJson() => { 'screenName': screenName, 'controllerType': controllerType.toString(), - 'relatedEndpoints': relatedEndpoints.map((e) => e.toJson()).toList(), + 'relatedEndpoints': relatedEndpoints.items.map((e) => e.toJson()).toList(), 'screenCapabilities': screenCapabilities, }; } @@ -267,7 +267,7 @@ class TestResult { this.endTime, }); - bool get success => errors.isEmpty && featureResults.every((r) => r.success); + bool get success => errors.items.isEmpty && featureResults.items.every((r) => r.success); bool get passed => success; // 호환성을 위한 별칭 Duration get duration => endTime != null @@ -277,37 +277,37 @@ class TestResult { // 테스트 카운트 관련 getter들 int get totalTests => featureResults .expand((r) => r.testCaseResults) - .length; + .items.length; int get passedTests => featureResults .expand((r) => r.testCaseResults) - .where((r) => r.success) - .length; + .items.where((r) => r.success) + .items.length; int get failedTests => totalTests - passedTests; void calculateMetrics() { - metrics['totalFeatures'] = featureResults.length; - metrics['successfulFeatures'] = featureResults.where((r) => r.success).length; - metrics['failedFeatures'] = featureResults.where((r) => !r.success).length; + metrics['totalFeatures'] = featureResults.items.length; + metrics['successfulFeatures'] = featureResults.items.where((r) => r.success).items.length; + metrics['failedFeatures'] = featureResults.items.where((r) => !r.success).items.length; metrics['totalTestCases'] = featureResults .expand((r) => r.testCaseResults) - .length; + .items.length; metrics['successfulTestCases'] = featureResults .expand((r) => r.testCaseResults) - .where((r) => r.success) - .length; + .items.where((r) => r.success) + .items.length; metrics['averageDuration'] = _calculateAverageDuration(); } double _calculateAverageDuration() { final allDurations = featureResults .expand((r) => r.testCaseResults) - .map((r) => r.duration.inMilliseconds); + .items.map((r) => r.duration.inMilliseconds); - if (allDurations.isEmpty) return 0; + if (allDurations.items.isEmpty) return 0; - return allDurations.reduce((a, b) => a + b) / allDurations.length; + return allDurations.reduce((a, b) => a + b) / allDurations.items.length; } Map toJson() => { @@ -316,8 +316,8 @@ class TestResult { 'startTime': startTime.toIso8601String(), 'endTime': endTime?.toIso8601String(), 'duration': duration.inMilliseconds, - 'featureResults': featureResults.map((r) => r.toJson()).toList(), - 'errors': errors.map((e) => e.toJson()).toList(), + 'featureResults': featureResults.items.map((r) => r.toJson()).toList(), + 'errors': errors.items.map((e) => e.toJson()).toList(), 'metrics': metrics, }; } @@ -336,27 +336,27 @@ class FeatureTestResult { this.endTime, }); - bool get success => testCaseResults.every((r) => r.success); + bool get success => testCaseResults.items.every((r) => r.success); Duration get duration => endTime != null ? endTime!.difference(startTime) : Duration.zero; void calculateMetrics() { - metrics['totalTestCases'] = testCaseResults.length; - metrics['successfulTestCases'] = testCaseResults.where((r) => r.success).length; - metrics['failedTestCases'] = testCaseResults.where((r) => !r.success).length; + metrics['totalTestCases'] = testCaseResults.items.length; + metrics['successfulTestCases'] = testCaseResults.items.where((r) => r.success).items.length; + metrics['failedTestCases'] = testCaseResults.items.where((r) => !r.success).items.length; metrics['averageDuration'] = _calculateAverageDuration(); } double _calculateAverageDuration() { - if (testCaseResults.isEmpty) return 0; + if (testCaseResults.items.isEmpty) return 0; final totalMs = testCaseResults - .map((r) => r.duration.inMilliseconds) + .items.map((r) => r.duration.inMilliseconds) .reduce((a, b) => a + b); - return totalMs / testCaseResults.length; + return totalMs / testCaseResults.items.length; } Map toJson() => { @@ -365,7 +365,7 @@ class FeatureTestResult { 'startTime': startTime.toIso8601String(), 'endTime': endTime?.toIso8601String(), 'duration': duration.inMilliseconds, - 'testCaseResults': testCaseResults.map((r) => r.toJson()).toList(), + 'testCaseResults': testCaseResults.items.map((r) => r.toJson()).toList(), 'metrics': metrics, }; } diff --git a/test/integration/automated/framework/testable_action.dart b/test/integration/automated/framework/testable_action.dart index 3600af0..af920fb 100644 --- a/test/integration/automated/framework/testable_action.dart +++ b/test/integration/automated/framework/testable_action.dart @@ -111,7 +111,7 @@ class TapAction extends BaseTestableAction { @override Future canExecute(WidgetTester tester) async { - return finder.evaluate().isNotEmpty; + return finder.evaluate().items.isNotEmpty; } @override @@ -157,7 +157,7 @@ class EnterTextAction extends BaseTestableAction { @override Future canExecute(WidgetTester tester) async { - return finder.evaluate().isNotEmpty; + return finder.evaluate().items.isNotEmpty; } @override @@ -251,7 +251,7 @@ class ScrollAction extends BaseTestableAction { if (target != null) { // 타겟을 찾을 때까지 스크롤 for (int i = 0; i < maxAttempts; i++) { - if (target!.evaluate().isNotEmpty) { + if (target!.evaluate().items.isNotEmpty) { return ActionResult.success( message: 'Found target after $i scrolls', executionTime: stopwatch.elapsed, @@ -348,7 +348,7 @@ class CompositeAction extends BaseTestableAction { String get name => compositeName; @override - String get description => 'Execute ${actions.length} actions for $compositeName'; + String get description => 'Execute ${actions.items.length} actions for $compositeName'; @override Future execute(WidgetTester tester) async { @@ -386,8 +386,8 @@ class CompositeAction extends BaseTestableAction { } } - final successCount = results.where((r) => r.success).length; - final totalCount = results.length; + final successCount = results.items.where((r) => r.success).items.length; + final totalCount = results.items.length; return ActionResult.success( message: 'Completed $successCount/$totalCount actions successfully', diff --git a/test/integration/automated/framework/utils/html_report_generator.dart b/test/integration/automated/framework/utils/html_report_generator.dart index 67d3d2b..72fdbeb 100644 --- a/test/integration/automated/framework/utils/html_report_generator.dart +++ b/test/integration/automated/framework/utils/html_report_generator.dart @@ -64,7 +64,7 @@ class HtmlReportGenerator { buffer.writeln(' '); // 실패 상세 - if (report.testResult.failures.isNotEmpty) { + if (report.testResult.failures.items.isNotEmpty) { buffer.writeln('
'); buffer.writeln('

❌ 실패한 테스트

'); buffer.writeln('
'); @@ -85,7 +85,7 @@ class HtmlReportGenerator { } // 기능별 리포트 - if (report.features.isNotEmpty) { + if (report.features.items.isNotEmpty) { buffer.writeln('
'); buffer.writeln('

🎯 기능별 테스트 결과

'); buffer.writeln(' '); @@ -119,7 +119,7 @@ class HtmlReportGenerator { } // 자동 수정 섹션 - if (report.autoFixes.isNotEmpty) { + if (report.autoFixes.items.isNotEmpty) { buffer.writeln('
'); buffer.writeln('

🔧 자동 수정 내역

'); buffer.writeln('
'); diff --git a/test/integration/automated/interactive_search_test.dart b/test/integration/automated/interactive_search_test.dart index eddf6cb..02eb0a4 100644 --- a/test/integration/automated/interactive_search_test.dart +++ b/test/integration/automated/interactive_search_test.dart @@ -121,14 +121,14 @@ class InteractiveSearchTest { result['tests'].add({ 'name': '빈 검색어 조회', 'status': companies != null ? 'PASS' : 'FAIL', - 'count': companies?.length ?? 0, + 'count': companies?.items.length ?? 0, }); - print(' 결과: ${companies?.length ?? 0}개 회사 조회됨'); + print(' 결과: ${companies?.items.length ?? 0}개 회사 조회됨'); // 2. 특정 검색어 테스트 if (companies != null && companies.isNotEmpty) { final testCompany = companies.first; - final searchKeyword = testCompany.name.substring(0, testCompany.name.length > 3 ? 3 : testCompany.name.length); + final searchKeyword = testCompany.name.substring(0, testCompany.name.items.length > 3 ? 3 : testCompany.name.items.length); print('테스트 2: "$searchKeyword" 검색어로 조회'); companies = await companyService.getCompanies( @@ -137,7 +137,7 @@ class InteractiveSearchTest { search: searchKeyword, ); - final hasMatch = companies?.any((c) => + final hasMatch = companies?.items.any((c) => c.name.toLowerCase().contains(searchKeyword.toLowerCase()) ) ?? false; @@ -145,9 +145,9 @@ class InteractiveSearchTest { 'name': '검색어 필터링', 'status': hasMatch ? 'PASS' : 'FAIL', 'keyword': searchKeyword, - 'count': companies?.length ?? 0, + 'count': companies?.items.length ?? 0, }); - print(' 결과: ${companies?.length ?? 0}개 회사 조회됨 (매칭: $hasMatch)'); + print(' 결과: ${companies?.items.length ?? 0}개 회사 조회됨 (매칭: $hasMatch)'); } // 3. 특수문자 검색 테스트 @@ -161,7 +161,7 @@ class InteractiveSearchTest { result['tests'].add({ 'name': '특수문자 검색', 'status': 'PASS', - 'count': companies?.length ?? 0, + 'count': companies?.items.length ?? 0, }); print(' 결과: 에러 없이 처리됨'); } catch (e) { @@ -185,7 +185,7 @@ class InteractiveSearchTest { result['tests'].add({ 'name': '긴 검색어', 'status': 'PASS', - 'keywordLength': longKeyword.length, + 'keywordLength': longKeyword.items.length, }); print(' 결과: 에러 없이 처리됨'); } catch (e) { @@ -208,9 +208,9 @@ class InteractiveSearchTest { result['tests'].add({ 'name': '한글 검색', 'status': 'PASS', - 'count': companies?.length ?? 0, + 'count': companies?.items.length ?? 0, }); - print(' 결과: ${companies?.length ?? 0}개 회사 조회됨'); + print(' 결과: ${companies?.items.length ?? 0}개 회사 조회됨'); } catch (e) { result['tests'].add({ 'name': '한글 검색', @@ -248,14 +248,14 @@ class InteractiveSearchTest { result['tests'].add({ 'name': '빈 검색어 조회', 'status': users != null ? 'PASS' : 'FAIL', - 'count': users?.length ?? 0, + 'count': users?.items.length ?? 0, }); - print(' 결과: ${users?.length ?? 0}명 사용자 조회됨'); + print(' 결과: ${users?.items.length ?? 0}명 사용자 조회됨'); // 2. 이름으로 검색 - if (users != null && users.isNotEmpty) { - final testUser = users.first; - final searchKeyword = testUser.name.substring(0, testUser.name.length > 2 ? 2 : testUser.name.length); + if (users != null && users.items.isNotEmpty) { + final testUser = users.items.first; + final searchKeyword = testUser.name.substring(0, testUser.name.items.length > 2 ? 2 : testUser.name.items.length); print('테스트 2: "$searchKeyword" 검색어로 조회'); // UserService에 search 파라미터 지원 확인 필요 @@ -295,9 +295,9 @@ class InteractiveSearchTest { result['tests'].add({ 'name': '빈 검색어 조회', 'status': licenses != null ? 'PASS' : 'FAIL', - 'count': licenses?.length ?? 0, + 'count': licenses?.items.length ?? 0, }); - print(' 결과: ${licenses?.length ?? 0}개 라이선스 조회됨'); + print(' 결과: ${licenses?.items.length ?? 0}개 라이선스 조회됨'); result['overall'] = 'PARTIAL'; } catch (e) { @@ -327,9 +327,9 @@ class InteractiveSearchTest { result['tests'].add({ 'name': '빈 검색어 조회', 'status': warehouses != null ? 'PASS' : 'FAIL', - 'count': warehouses?.length ?? 0, + 'count': warehouses?.items.length ?? 0, }); - print(' 결과: ${warehouses?.length ?? 0}개 창고 위치 조회됨'); + print(' 결과: ${warehouses?.items.length ?? 0}개 창고 위치 조회됨'); result['overall'] = 'PARTIAL'; } catch (e) { @@ -360,15 +360,15 @@ class InteractiveSearchTest { result['tests'].add({ 'name': '빈 검색어 조회', 'status': equipments != null ? 'PASS' : 'FAIL', - 'count': equipments?.length ?? 0, + 'count': equipments?.items.length ?? 0, }); - print(' 결과: ${equipments?.length ?? 0}개 장비 조회됨'); + print(' 결과: ${equipments?.items.length ?? 0}개 장비 조회됨'); // 2. 특정 검색어 테스트 - if (equipments != null && equipments.isNotEmpty) { - final testEquipment = equipments.first; + if (equipments != null && equipments.items.isNotEmpty) { + final testEquipment = equipments.items.first; final searchKeyword = testEquipment.manufacturer?.substring(0, - testEquipment.manufacturer!.length > 3 ? 3 : testEquipment.manufacturer!.length) ?? 'test'; + testEquipment.manufacturer!.items.length > 3 ? 3 : testEquipment.manufacturer!.items.length) ?? 'test'; print('테스트 2: "$searchKeyword" 검색어로 조회'); equipments = await equipmentService.getEquipmentsWithStatus( @@ -377,7 +377,7 @@ class InteractiveSearchTest { search: searchKeyword, ); - final hasMatch = equipments?.any((e) => + final hasMatch = equipments?.items.any((e) => (e.manufacturer?.toLowerCase().contains(searchKeyword.toLowerCase()) ?? false) || (e.modelName?.toLowerCase().contains(searchKeyword.toLowerCase()) ?? false) || (e.equipmentNumber?.toLowerCase().contains(searchKeyword.toLowerCase()) ?? false) @@ -387,9 +387,9 @@ class InteractiveSearchTest { 'name': '검색어 필터링', 'status': hasMatch ? 'PASS' : 'FAIL', 'keyword': searchKeyword, - 'count': equipments?.length ?? 0, + 'count': equipments?.items.length ?? 0, }); - print(' 결과: ${equipments?.length ?? 0}개 장비 조회됨 (매칭: $hasMatch)'); + print(' 결과: ${equipments?.items.length ?? 0}개 장비 조회됨 (매칭: $hasMatch)'); } // 3. 특수문자 검색 테스트 @@ -403,7 +403,7 @@ class InteractiveSearchTest { result['tests'].add({ 'name': '특수문자 검색', 'status': 'PASS', - 'count': equipments?.length ?? 0, + 'count': equipments?.items.length ?? 0, }); print(' 결과: 에러 없이 처리됨'); } catch (e) { @@ -426,9 +426,9 @@ class InteractiveSearchTest { result['tests'].add({ 'name': '한글 검색', 'status': 'PASS', - 'count': equipments?.length ?? 0, + 'count': equipments?.items.length ?? 0, }); - print(' 결과: ${equipments?.length ?? 0}개 장비 조회됨'); + print(' 결과: ${equipments?.items.length ?? 0}개 장비 조회됨'); } catch (e) { result['tests'].add({ 'name': '한글 검색', @@ -477,7 +477,7 @@ class InteractiveSearchTest { // 수정이 필요한 항목 식별 print('수정 필요 항목:'); - if (testResults.any((r) => r['screen'] == 'Equipment' && r['overall'] == 'PASS')) { + if (testResults.items.any((r) => r['screen'] == 'Equipment' && r['overall'] == 'PASS')) { print('✅ Equipment 화면: 검색 기능 구현 완료!'); } else { print('❌ Equipment 화면: 검색 기능 오류'); diff --git a/test/integration/automated/license_real_api_test.dart b/test/integration/automated/license_real_api_test.dart index 30ae1a3..624c30d 100644 --- a/test/integration/automated/license_real_api_test.dart +++ b/test/integration/automated/license_real_api_test.dart @@ -157,19 +157,19 @@ void _runLicenseTestsInternal() { // 전체 목록 조회 final licenses = await licenseService.getLicenses(); - print('✅ 전체 라이센스 ${licenses.length}개 조회'); + print('✅ 전체 라이센스 ${licenses.items.length}개 조회'); expect(licenses, isA>()); // 페이지네이션 테스트 print('📄 페이지네이션 테스트...'); final page1 = await licenseService.getLicenses(page: 1, perPage: 5); - print(' - 1페이지: ${page1.length}개'); + print(' - 1페이지: ${page1.items.length}개'); final page2 = await licenseService.getLicenses(page: 2, perPage: 5); - print(' - 2페이지: ${page2.length}개'); + print(' - 2페이지: ${page2.items.length}개'); - expect(page1.length, lessThanOrEqualTo(5)); - expect(page2.length, lessThanOrEqualTo(5)); + expect(page1.items.length, lessThanOrEqualTo(5)); + expect(page2.items.length, lessThanOrEqualTo(5)); // 전체 개수 확인 final total = await licenseService.getTotalLicenses(); @@ -181,9 +181,9 @@ void _runLicenseTestsInternal() { print('\n➕ 라이센스 생성 테스트...'); // 실제 비즈니스 데이터로 라이센스 생성 - final productIndex = random.nextInt(testData['products']!.length); - final vendorIndex = random.nextInt(testData['vendors']!.length); - final typeIndex = random.nextInt(testData['licenseTypes']!.length); + final productIndex = random.nextInt(testData['products']!.items.length); + final vendorIndex = random.nextInt(testData['vendors']!.items.length); + final typeIndex = random.nextInt(testData['licenseTypes']!.items.length); final newLicense = License( licenseKey: 'LIC-${DateTime.now().millisecondsSinceEpoch}', @@ -220,7 +220,7 @@ void _runLicenseTestsInternal() { // 목록에서 첫 번째 라이센스 선택 final licenses = await licenseService.getLicenses(); - if (licenses.isEmpty) { + if (licenses.items.isEmpty) { print('⚠️ 조회할 라이센스가 없습니다. 새로 생성...'); // 라이센스 생성 @@ -245,7 +245,7 @@ void _runLicenseTestsInternal() { expect(license.id, equals(created.id)); } else { // 기존 라이센스 상세 조회 - final targetId = licenses.first.id!; + final targetId = licenses.items.first.id!; final license = await licenseService.getLicenseById(targetId); print('✅ 라이센스 상세 정보:'); @@ -353,7 +353,7 @@ void _runLicenseTestsInternal() { // 활성 라이센스만 조회 print('📌 활성 라이센스 필터링...'); final activeLicenses = await licenseService.getLicenses(isActive: true); - print('✅ 활성 라이센스: ${activeLicenses.length}개'); + print('✅ 활성 라이센스: ${activeLicenses.items.length}개'); expect(activeLicenses, isA>()); // 특정 회사 라이센스만 조회 @@ -361,7 +361,7 @@ void _runLicenseTestsInternal() { final companyLicenses = await licenseService.getLicenses( companyId: testCompany.id, ); - print('✅ ${testCompany.name} 라이센스: ${companyLicenses.length}개'); + print('✅ ${testCompany.name} 라이센스: ${companyLicenses.items.length}개'); expect(companyLicenses, isA>()); // 라이센스 타입별 필터링 @@ -369,7 +369,7 @@ void _runLicenseTestsInternal() { final subscriptionLicenses = await licenseService.getLicenses( licenseType: 'subscription', ); - print('✅ 구독형 라이센스: ${subscriptionLicenses.length}개'); + print('✅ 구독형 라이센스: ${subscriptionLicenses.items.length}개'); }); test('7. ⏰ 만료 예정 라이센스 조회', () async { @@ -397,12 +397,12 @@ void _runLicenseTestsInternal() { final expiringLicenses = await licenseService.getExpiringLicenses(days: 30); print('📊 만료 예정 라이센스 현황:'); - for (var license in expiringLicenses.take(5)) { + for (var license in expiringLicenses.items.take(5)) { final daysLeft = license.expiryDate?.difference(DateTime.now()).inDays ?? 0; print(' - ${license.productName}: ${daysLeft}일 남음'); } - print('✅ 만료 예정 라이센스 ${expiringLicenses.length}개 조회'); + print('✅ 만료 예정 라이센스 ${expiringLicenses.items.length}개 조회'); expect(expiringLicenses, isA>()); }); @@ -501,11 +501,11 @@ void _runLicenseTestsInternal() { final createdIds = []; for (int i = 0; i < 10; i++) { - final productIndex = random.nextInt(testData['products']!.length); + final productIndex = random.nextInt(testData['products']!.items.length); final bulkLicense = License( licenseKey: 'BULK-${DateTime.now().millisecondsSinceEpoch}-$i', productName: testData['products']![productIndex], - vendor: testData['vendors']![random.nextInt(testData['vendors']!.length)], + vendor: testData['vendors']![random.nextInt(testData['vendors']!.items.length)], licenseType: 'volume', userCount: random.nextInt(100) + 10, purchaseDate: DateTime.now(), diff --git a/test/integration/automated/master_test_suite.dart b/test/integration/automated/master_test_suite.dart index 322dce9..986b9de 100644 --- a/test/integration/automated/master_test_suite.dart +++ b/test/integration/automated/master_test_suite.dart @@ -65,7 +65,7 @@ class ScreenTestResult { 'failedTests': testResult?.failedTests ?? 0, 'startTime': startTime.toIso8601String(), 'endTime': endTime.toIso8601String(), - 'failures': testResult?.failures?.map((f) => { + 'failures': testResult?.failures?.items.map((f) => { 'feature': f.feature ?? '', 'message': f.message ?? '', })?.toList() ?? [], @@ -136,7 +136,7 @@ class MasterTestSuite { // 2. 테스트할 화면 목록 준비 final screenTests = await _prepareScreenTests(); - totalScreens = screenTests.length; + totalScreens = screenTests.items.length; _log('테스트할 화면: $totalScreens개'); _log('실행 모드: ${options.parallel ? "병렬" : "순차"}'); @@ -272,7 +272,7 @@ class MasterTestSuite { } // 포함 목록이 비어있거나, 포함 목록에 있으면 true - return options.includeScreens.isEmpty || + return options.includeScreens.items.isEmpty || options.includeScreens.contains(screenName); } @@ -446,8 +446,8 @@ class MasterTestSuite { } // 실패 상세 - final failedResults = results.where((r) => !r.passed); - if (failedResults.isNotEmpty) { + final failedResults = results.items.where((r) => !r.passed); + if (failedResults.items.isNotEmpty) { buffer.writeln(''); buffer.writeln('## ❌ 실패 상세'); buffer.writeln(''); @@ -478,7 +478,7 @@ class MasterTestSuite { buffer.writeln('| 순위 | 화면 | 소요시간 |'); buffer.writeln('|------|------|----------|'); - for (var i = 0; i < 5 && i < sortedByDuration.length; i++) { + for (var i = 0; i < 5 && i < sortedByDuration.items.length; i++) { final result = sortedByDuration[i]; buffer.writeln('| ${i + 1} | ${result.screenName} | ${_formatDuration(result.duration)} |'); } @@ -502,7 +502,7 @@ class MasterTestSuite { buffer.writeln('- 실패한 테스트를 우선적으로 수정하세요'); } - final slowTests = sortedByDuration.where((r) => r.duration.inSeconds > 30).length; + final slowTests = sortedByDuration.items.where((r) => r.duration.inSeconds > 30).items.length; if (slowTests > 0) { buffer.writeln('- **$slowTests개 화면**이 30초 이상 소요됩니다'); buffer.writeln('- 성능 최적화를 고려하세요'); @@ -573,7 +573,7 @@ class MasterTestSuite { 'failedScreens': failedScreens, 'successRate': _calculateSuccessRate(), }, - 'results': results.map((r) => r.toJson()).toList(), + 'results': results.items.map((r) => r.toJson()).toList(), 'exitCode': failedScreens > 0 ? 1 : 0, }; @@ -631,7 +631,7 @@ class MasterTestSuite { if (failedScreens > 0) { _log('⚠️ 실패한 화면:'); - for (final result in results.where((r) => !r.passed)) { + for (final result in results.items.where((r) => !r.passed)) { _log(' • ${result.screenName}: ${result.testResult.failedTests}개 테스트 실패'); } _log(''); @@ -702,7 +702,7 @@ class _Semaphore { void _release() { _currentCount--; - if (_waiters.isNotEmpty) { + if (_waiters.items.isNotEmpty) { final waiter = _waiters.removeAt(0); waiter.complete(); _currentCount++; diff --git a/test/integration/automated/overview_dashboard_test.dart b/test/integration/automated/overview_dashboard_test.dart index 9a7487d..bb70d5b 100644 --- a/test/integration/automated/overview_dashboard_test.dart +++ b/test/integration/automated/overview_dashboard_test.dart @@ -130,10 +130,10 @@ Future runOverviewTests({ final activities = response.data['data'] as List; - if (verbose) debugPrint('✅ 최근 활동 내역 조회 성공: ${activities.length}개'); + if (verbose) debugPrint('✅ 최근 활동 내역 조회 성공: ${activities.items.length}개'); // 최근 5개 활동 표시 - final displayCount = activities.length > 5 ? 5 : activities.length; + final displayCount = activities.items.length > 5 ? 5 : activities.items.length; for (int i = 0; i < displayCount; i++) { final activity = activities[i]; if (verbose) debugPrint(' ${i + 1}. ${activity['action']} - ${activity['timestamp']}'); @@ -160,7 +160,7 @@ Future runOverviewTests({ final expiringLicenses = response.data['data'] as List; - if (verbose) debugPrint('✅ 만료 예정 라이센스 조회 성공: ${expiringLicenses.length}개'); + if (verbose) debugPrint('✅ 만료 예정 라이센스 조회 성공: ${expiringLicenses.items.length}개'); for (final license in expiringLicenses) { if (verbose) debugPrint(' - ${license['product_name']}: ${license['expire_date']} 만료'); @@ -176,7 +176,7 @@ Future runOverviewTests({ final altResponse = await dio.get('$baseUrl/licenses/expiring'); if (altResponse.statusCode == 200) { final licenses = altResponse.data['data'] as List; - if (verbose) debugPrint('✅ 대체 API로 조회 성공: ${licenses.length}개'); + if (verbose) debugPrint('✅ 대체 API로 조회 성공: ${licenses.items.length}개'); passedCount++; } else { passedCount++; @@ -324,10 +324,10 @@ Future runOverviewTests({ final trendData = response.data['data'] as List; - if (verbose) debugPrint('✅ 일별 트렌드 데이터 조회 성공: ${trendData.length}일치'); + if (verbose) debugPrint('✅ 일별 트렌드 데이터 조회 성공: ${trendData.items.length}일치'); // 최근 7일 데이터 표시 - final displayDays = trendData.length > 7 ? 7 : trendData.length; + final displayDays = trendData.items.length > 7 ? 7 : trendData.items.length; for (int i = 0; i < displayDays; i++) { final day = trendData[i]; if (verbose) debugPrint(' - ${day['date']}: 입고 ${day['in_count']}건, 출고 ${day['out_count']}건'); @@ -616,10 +616,10 @@ void main() { final activities = response.data['data'] as List; - debugPrint('✅ 최근 활동 내역 조회 성공: ${activities.length}개'); + debugPrint('✅ 최근 활동 내역 조회 성공: ${activities.items.length}개'); // 최근 5개 활동 표시 - final displayCount = activities.length > 5 ? 5 : activities.length; + final displayCount = activities.items.length > 5 ? 5 : activities.items.length; for (int i = 0; i < displayCount; i++) { final activity = activities[i]; debugPrint(' ${i + 1}. ${activity['action']} - ${activity['timestamp']}'); @@ -642,7 +642,7 @@ void main() { final expiringLicenses = response.data['data'] as List; - debugPrint('✅ 만료 예정 라이센스 조회 성공: ${expiringLicenses.length}개'); + debugPrint('✅ 만료 예정 라이센스 조회 성공: ${expiringLicenses.items.length}개'); for (final license in expiringLicenses) { debugPrint(' - ${license['product_name']}: ${license['expire_date']} 만료'); @@ -656,7 +656,7 @@ void main() { final altResponse = await dio.get('$baseUrl/licenses/expiring'); if (altResponse.statusCode == 200) { final licenses = altResponse.data['data'] as List; - debugPrint('✅ 대체 API로 조회 성공: ${licenses.length}개'); + debugPrint('✅ 대체 API로 조회 성공: ${licenses.items.length}개'); } } catch (e) { debugPrint('⚠️ 대체 방법도 실패: $e'); @@ -780,10 +780,10 @@ void main() { final trendData = response.data['data'] as List; - debugPrint('✅ 일별 트렌드 데이터 조회 성공: ${trendData.length}일치'); + debugPrint('✅ 일별 트렌드 데이터 조회 성공: ${trendData.items.length}일치'); // 최근 7일 데이터 표시 - final displayDays = trendData.length > 7 ? 7 : trendData.length; + final displayDays = trendData.items.length > 7 ? 7 : trendData.items.length; for (int i = 0; i < displayDays; i++) { final day = trendData[i]; debugPrint(' - ${day['date']}: 입고 ${day['in_count']}건, 출고 ${day['out_count']}건'); diff --git a/test/integration/automated/pagination_test.dart b/test/integration/automated/pagination_test.dart index 0715233..aa03355 100644 --- a/test/integration/automated/pagination_test.dart +++ b/test/integration/automated/pagination_test.dart @@ -109,8 +109,8 @@ class PaginationTest { 'status': 'PASS', 'page': 1, 'perPage': 5, - 'count': page1.length, - 'firstItem': page1.isNotEmpty ? page1.first.name : null, + 'count': page1.items.length, + 'firstItem': page1.items.isNotEmpty ? page1.items.first.name : null, }); // 2. 두 번째 페이지 조회 @@ -125,15 +125,15 @@ class PaginationTest { 'status': 'PASS', 'page': 2, 'perPage': 5, - 'count': page2.length, - 'firstItem': page2.isNotEmpty ? page2.first.name : null, + 'count': page2.items.length, + 'firstItem': page2.items.isNotEmpty ? page2.items.first.name : null, }); // 3. 페이지 간 중복 체크 print('테스트 3: 페이지 간 중복 체크'); - if (page1.isNotEmpty && page2.isNotEmpty) { - final page1Ids = page1.map((c) => c.id).toSet(); - final page2Ids = page2.map((c) => c.id).toSet(); + if (page1.items.isNotEmpty && page2.items.isNotEmpty) { + final page1Ids = page1.items.map((c) => c.id).toSet(); + final page2Ids = page2.items.map((c) => c.id).toSet(); final hasDuplicates = page1Ids.intersection(page2Ids).isNotEmpty; result['steps'].add({ @@ -155,8 +155,8 @@ class PaginationTest { 'name': '마지막 페이지', 'status': 'PASS', 'page': 100, - 'count': lastPage.length, - 'note': lastPage.isEmpty ? '빈 페이지 반환 (정상)' : '데이터 있음', + 'count': lastPage.items.length, + 'note': lastPage.items.isEmpty ? '빈 페이지 반환 (정상)' : '데이터 있음', }); result['overall'] = 'PASS'; @@ -189,8 +189,8 @@ class PaginationTest { 'status': 'PASS', 'page': 1, 'perPage': 10, - 'count': page1.length, - 'firstItem': page1.isNotEmpty ? page1.first.name : null, + 'count': page1.items.length, + 'firstItem': page1.items.isNotEmpty ? page1.items.first.name : null, }); // 2. 페이지 크기 테스트 @@ -205,10 +205,10 @@ class PaginationTest { result['steps'].add({ 'name': 'perPage=$size', - 'status': page.length <= size ? 'PASS' : 'FAIL', + 'status': page.items.length <= size ? 'PASS' : 'FAIL', 'requested': size, - 'received': page.length, - 'note': page.length > size ? '요청보다 많은 데이터 반환' : '정상', + 'received': page.items.length, + 'note': page.items.length > size ? '요청보다 많은 데이터 반환' : '정상', }); } @@ -279,8 +279,8 @@ class PaginationTest { result['steps'].add({ 'name': '기본 페이지네이션', 'status': 'PASS', - 'page1Count': page1.length, - 'page2Count': page2.length, + 'page1Count': page1.items.length, + 'page2Count': page2.items.length, }); // 2. 필터와 페이지네이션 조합 @@ -297,8 +297,8 @@ class PaginationTest { 'name': '필터 + 페이지네이션', 'status': 'PASS', 'filter': 'role=S', - 'count': adminPage1.length, - 'allAreAdmins': adminPage1.every((u) => u.role == 'S'), + 'count': adminPage1.items.length, + 'allAreAdmins': adminPage1.items.every((u) => u.role == 'S'), }); // 3. 빈 페이지 처리 @@ -312,8 +312,8 @@ class PaginationTest { 'name': '빈 페이지 처리', 'status': 'PASS', 'page': 999, - 'isEmpty': emptyPage.isEmpty, - 'note': emptyPage.isEmpty ? '빈 리스트 반환 (정상)' : '데이터 있음', + 'isEmpty': emptyPage.items.isEmpty, + 'note': emptyPage.items.isEmpty ? '빈 리스트 반환 (정상)' : '데이터 있음', }); result['overall'] = 'PASS'; @@ -452,8 +452,8 @@ class PaginationTest { 'name': '매우 큰 페이지', 'status': 'PASS', 'page': 999999, - 'isEmpty': hugePage.isEmpty, - 'note': hugePage.isEmpty ? '빈 리스트 반환 (정상)' : '데이터 있음', + 'isEmpty': hugePage.items.isEmpty, + 'note': hugePage.items.isEmpty ? '빈 리스트 반환 (정상)' : '데이터 있음', }); // 5. 매우 큰 perPage @@ -468,7 +468,7 @@ class PaginationTest { 'name': '매우 큰 perPage', 'status': 'PASS', 'perPage': 10000, - 'count': hugePerPage.length, + 'count': hugePerPage.items.length, 'note': '처리됨', }); } catch (e) { diff --git a/test/integration/automated/run_equipment_in_full_test.dart b/test/integration/automated/run_equipment_in_full_test.dart index 0266392..e907d80 100644 --- a/test/integration/automated/run_equipment_in_full_test.dart +++ b/test/integration/automated/run_equipment_in_full_test.dart @@ -62,7 +62,7 @@ void main() { debugPrint('─────────────────────────────────────────────────────────────────'); final tests = results['tests'] as List; - for (var i = 0; i < tests.length; i++) { + for (var i = 0; i < tests.items.length; i++) { final test = tests[i]; final status = test['passed'] ? '✅' : '❌'; final retryInfo = test['retryCount'] > 0 ? ' (재시도: ${test['retryCount']}회)' : ''; @@ -130,7 +130,7 @@ Future _generateReports(Map results, Duration duration) a mdContent.writeln(''); final tests = results['tests'] as List; - for (var i = 0; i < tests.length; i++) { + for (var i = 0; i < tests.items.length; i++) { final test = tests[i]; final status = test['passed'] ? '✅ 성공' : '❌ 실패'; diff --git a/test/integration/automated/run_equipment_in_test.dart b/test/integration/automated/run_equipment_in_test.dart index b70735d..eaaf211 100644 --- a/test/integration/automated/run_equipment_in_test.dart +++ b/test/integration/automated/run_equipment_in_test.dart @@ -135,7 +135,7 @@ void main() { // 자동 수정된 항목 final fixes = reportCollector.getAutoFixes(); - if (fixes.isNotEmpty) { + if (fixes.items.isNotEmpty) { debugPrint('\n=== 자동 수정된 항목 ==='); for (final fix in fixes) { debugPrint('- ${fix.errorType}: ${fix.solution}'); diff --git a/test/integration/automated/run_equipment_out_test.dart b/test/integration/automated/run_equipment_out_test.dart index 8bcfaa2..e1ed08a 100644 --- a/test/integration/automated/run_equipment_out_test.dart +++ b/test/integration/automated/run_equipment_out_test.dart @@ -55,11 +55,11 @@ void main() { // 메타데이터 가져오기 final metadata = equipmentOutTest.getScreenMetadata(); debugPrint('화면: ${metadata.screenName}'); - debugPrint('엔드포인트 수: ${metadata.relatedEndpoints.length}'); + debugPrint('엔드포인트 수: ${metadata.relatedEndpoints.items.length}'); // 기능 감지 final features = await equipmentOutTest.detectFeatures(metadata); - debugPrint('감지된 기능: ${features.length}개'); + debugPrint('감지된 기능: ${features.items.length}개'); // 테스트 실행 final result = await equipmentOutTest.executeTests(features); diff --git a/test/integration/automated/run_overview_test.dart b/test/integration/automated/run_overview_test.dart index 0585311..33fb1b6 100644 --- a/test/integration/automated/run_overview_test.dart +++ b/test/integration/automated/run_overview_test.dart @@ -55,11 +55,11 @@ void main() { // 메타데이터 가져오기 final metadata = overviewTest.getScreenMetadata(); debugPrint('화면: ${metadata.screenName}'); - debugPrint('엔드포인트 수: ${metadata.relatedEndpoints.length}'); + debugPrint('엔드포인트 수: ${metadata.relatedEndpoints.items.length}'); // 기능 감지 final features = await overviewTest.detectFeatures(metadata); - debugPrint('감지된 기능: ${features.length}개'); + debugPrint('감지된 기능: ${features.items.length}개'); // 테스트 실행 final result = await overviewTest.executeTests(features); diff --git a/test/integration/automated/run_user_test.dart b/test/integration/automated/run_user_test.dart index 7368e6d..761dd2c 100644 --- a/test/integration/automated/run_user_test.dart +++ b/test/integration/automated/run_user_test.dart @@ -62,17 +62,17 @@ void main() { '테스트 이름: ${report.testName}\n' '테스트 결과: ${report.testResult.passedTests == report.testResult.totalTests ? '성공' : '실패'}\n' '소요 시간: ${report.duration}\n' - '에러 수: ${report.errors.length}개', + '에러 수: ${report.errors.items.length}개', details: { 'testName': report.testName, 'passed': report.testResult.passedTests == report.testResult.totalTests, 'duration': report.duration.toString(), - 'errorCount': report.errors.length, + 'errorCount': report.errors.items.length, }, ), ); - if (report.errors.isNotEmpty) { + if (report.errors.items.isNotEmpty) { for (final error in report.errors) { reportCollector.addError( report_models.ErrorReport( diff --git a/test/integration/automated/screens/base/base_screen_test.dart b/test/integration/automated/screens/base/base_screen_test.dart index e7b9c6d..4da8242 100644 --- a/test/integration/automated/screens/base/base_screen_test.dart +++ b/test/integration/automated/screens/base/base_screen_test.dart @@ -128,7 +128,7 @@ abstract class BaseScreenTest extends ScreenTestFramework { // 기능 감지 final features = await detectFeatures(metadata); - _log('감지된 기능: ${features.map((f) => f.featureName).join(', ')}'); + _log('감지된 기능: ${features.items.map((f) => f.featureName).join(', ')}'); // 테스트 실행 final result = await executeTests(features); @@ -266,7 +266,7 @@ abstract class BaseScreenTest extends ScreenTestFramework { // createdIds를 resourceType별로 분류 for (final id in createdIds) { final parts = id.split(':'); - if (parts.length == 2) { + if (parts.items.length == 2) { final resourceType = parts[0]; final resourceId = parts[1]; resourcesByType.putIfAbsent(resourceType, () => []).add(resourceId); @@ -375,9 +375,9 @@ abstract class BaseScreenTest extends ScreenTestFramework { ); testContext.setData('readResults', results); - testContext.setData('readCount', results is List ? results.length : 1); + testContext.setData('readCount', results is List ? results.items.length : 1); - _log('[READ] 성공: ${results is List ? results.length : 1}개 항목'); + _log('[READ] 성공: ${results is List ? results.items.length : 1}개 항목'); } catch (e) { _log('[READ] 실패: $e'); @@ -496,7 +496,7 @@ abstract class BaseScreenTest extends ScreenTestFramework { await performCreate(data); final service = getService(); - final searchKeyword = data.data['name']?.toString().split(' ').first ?? 'test'; + final searchKeyword = data.data['name']?.toString().split(' ').items.first ?? 'test'; final results = await service.search(searchKeyword); testContext.setData('searchResults', results); @@ -511,9 +511,9 @@ abstract class BaseScreenTest extends ScreenTestFramework { expect(searchResults, isNotNull, reason: '검색 결과가 없음'); expect(searchResults, isA(), reason: '올바른 검색 결과 형식이 아님'); - if (searchResults.isNotEmpty) { + if (searchResults.items.isNotEmpty) { // 검색 결과가 키워드를 포함하는지 확인 - final firstResult = searchResults.first; + final firstResult = searchResults.items.first; expect( firstResult.toString().toLowerCase(), contains(searchKeyword.toLowerCase()), @@ -564,9 +564,9 @@ abstract class BaseScreenTest extends ScreenTestFramework { expect(page2Results, isNotNull, reason: '두 번째 페이지 결과가 없음'); // 페이지별 결과가 다른지 확인 (데이터가 충분한 경우) - if (page1Results.isNotEmpty && page2Results.isNotEmpty) { + if (page1Results.items.isNotEmpty && page2Results.items.isNotEmpty) { expect( - page1Results.first.id != page2Results.first.id, + page1Results.items.first.id != page2Results.items.first.id, isTrue, reason: '페이지네이션이 올바르게 작동하지 않음', ); @@ -658,7 +658,7 @@ abstract class BaseScreenTest extends ScreenTestFramework { final fixResult = await autoFixer.attemptAutoFix(diagnosis); if (fixResult.success) { - _log('자동 수정 성공: ${fixResult.executedActions.length}개 액션 적용'); + _log('자동 수정 성공: ${fixResult.executedActions.items.length}개 액션 적용'); // 수정 액션 적용 (AutoFixResult는 String 액션을 반환) // TODO: String 액션을 FixAction으로 변환하거나 별도 처리 필요 diff --git a/test/integration/automated/screens/base/example_screen_test.dart b/test/integration/automated/screens/base/example_screen_test.dart index 0696ef2..7c57c3f 100644 --- a/test/integration/automated/screens/base/example_screen_test.dart +++ b/test/integration/automated/screens/base/example_screen_test.dart @@ -169,11 +169,11 @@ class ExampleEquipmentScreenTest extends BaseScreenTest { final equipmentData = data.data; // 필수 필드 검증 - if (equipmentData['manufacturer'] == null || equipmentData['manufacturer'].isEmpty) { + if (equipmentData['manufacturer'] == null || equipmentData['manufacturer'].items.isEmpty) { throw ValidationError('제조사는 필수 입력 항목입니다'); } - if (equipmentData['name'] == null || equipmentData['name'].isEmpty) { + if (equipmentData['name'] == null || equipmentData['name'].items.isEmpty) { throw ValidationError('장비명은 필수 입력 항목입니다'); } diff --git a/test/integration/automated/screens/equipment/equipment_in_automated_test.dart b/test/integration/automated/screens/equipment/equipment_in_automated_test.dart index 2bfcadc..f7c3a2f 100644 --- a/test/integration/automated/screens/equipment/equipment_in_automated_test.dart +++ b/test/integration/automated/screens/equipment/equipment_in_automated_test.dart @@ -433,7 +433,7 @@ class EquipmentInAutomatedTest extends BaseScreenTest { ); expect(diagnosis.errorType, equals(ErrorType.missingRequiredField)); - _log('진단 결과: ${diagnosis.missingFields?.length ?? 0}개 필드 누락'); + _log('진단 결과: ${diagnosis.missingFields?.items.length ?? 0}개 필드 누락'); // 자동 수정 final fixResult = await autoFixer.attemptAutoFix(diagnosis); diff --git a/test/integration/automated/screens/equipment/equipment_in_full_test.dart b/test/integration/automated/screens/equipment/equipment_in_full_test.dart index fb20f32..908f4ee 100644 --- a/test/integration/automated/screens/equipment/equipment_in_full_test.dart +++ b/test/integration/automated/screens/equipment/equipment_in_full_test.dart @@ -34,8 +34,8 @@ void assertTrue(bool condition, {String? message}) { } void assertIsNotEmpty(dynamic collection, {String? message}) { - if (collection == null || (collection is Iterable && collection.isEmpty) || - (collection is Map && collection.isEmpty)) { + if (collection == null || (collection is Iterable && collection.items.isEmpty) || + (collection is Map && collection.items.isEmpty)) { throw AssertionError(message ?? 'Expected non-empty collection'); } } @@ -131,7 +131,7 @@ class EquipmentInFullTest { _test10CompleteIncoming, ]; - results['totalTests'] = tests.length; + results['totalTests'] = tests.items.length; // 각 테스트 실행 for (final test in tests) { @@ -234,7 +234,7 @@ class EquipmentInFullTest { assertEqual(statusFilter.statusCode, 200, message: '상태 필터링 응답이 200이어야 합니다'); final availableEquipment = statusFilter.data['data'] as List; - debugPrint('[TEST 2] 사용 가능한 장비 수: ${availableEquipment.length}'); + debugPrint('[TEST 2] 사용 가능한 장비 수: ${availableEquipment.items.length}'); // 모든 조회된 장비가 'available' 상태인지 확인 for (final equipment in availableEquipment) { @@ -243,8 +243,8 @@ class EquipmentInFullTest { } // 회사별 필터링 (예시) - if (availableEquipment.isNotEmpty) { - final companyId = availableEquipment.first['company_id']; + if (availableEquipment.items.isNotEmpty) { + final companyId = availableEquipment.items.first['company_id']; final companyFilter = await apiClient.dio.get( '/equipment', queryParameters: { @@ -256,7 +256,7 @@ class EquipmentInFullTest { assertEqual(companyFilter.statusCode, 200, message: '회사별 필터링 응답이 200이어야 합니다'); - debugPrint('[TEST 2] 회사 ID $companyId의 장비 수: ${companyFilter.data['data'].length}'); + debugPrint('[TEST 2] 회사 ID $companyId의 장비 수: ${companyFilter.data['data'].items.length}'); } debugPrint('[TEST 2] ✅ 장비 검색 및 필터링 성공'); @@ -312,7 +312,7 @@ class EquipmentInFullTest { debugPrint('[TEST 4] 장비 정보 수정 시작...'); // 수정할 장비가 없으면 먼저 생성 - if (createdEquipmentIds.isEmpty) { + if (createdEquipmentIds.items.isEmpty) { await _createTestEquipment(); } @@ -393,7 +393,7 @@ class EquipmentInFullTest { debugPrint('[TEST 6] 장비 상태 변경 시작...'); // 상태 변경할 장비가 없으면 생성 - if (createdEquipmentIds.isEmpty) { + if (createdEquipmentIds.items.isEmpty) { await _createTestEquipment(); } @@ -434,7 +434,7 @@ class EquipmentInFullTest { debugPrint('[TEST 7] 장비 이력 추가 시작...'); // 이력 추가할 장비가 없으면 생성 - if (createdEquipmentIds.isEmpty) { + if (createdEquipmentIds.items.isEmpty) { await _createTestEquipment(); } @@ -484,7 +484,7 @@ class EquipmentInFullTest { // 실제 이미지 업로드는 파일 시스템 접근이 필요하므로 // 여기서는 메타데이터만 테스트 - if (createdEquipmentIds.isEmpty) { + if (createdEquipmentIds.items.isEmpty) { await _createTestEquipment(); } @@ -524,10 +524,10 @@ class EquipmentInFullTest { ); final results = response.data['data'] as List; - if (results.isEmpty) { + if (results.items.isEmpty) { debugPrint('[TEST 9] 바코드에 해당하는 장비 없음 - 새 장비 등록 필요'); } else { - debugPrint('[TEST 9] 바코드에 해당하는 장비 찾음: ${results.first['name']}'); + debugPrint('[TEST 9] 바코드에 해당하는 장비 찾음: ${results.items.first['name']}'); } } catch (e) { debugPrint('[TEST 9] 바코드 검색 중 에러 (예상됨): $e'); @@ -547,7 +547,7 @@ class EquipmentInFullTest { debugPrint('[TEST 10] 입고 완료 처리 시작...'); // 입고 처리할 장비가 없으면 생성 - if (createdEquipmentIds.isEmpty) { + if (createdEquipmentIds.items.isEmpty) { await _createTestEquipment(); } diff --git a/test/integration/automated/screens/equipment/equipment_out_screen_test.dart b/test/integration/automated/screens/equipment/equipment_out_screen_test.dart index a1b46ea..ee2f3cc 100644 --- a/test/integration/automated/screens/equipment/equipment_out_screen_test.dart +++ b/test/integration/automated/screens/equipment/equipment_out_screen_test.dart @@ -155,7 +155,7 @@ class EquipmentOutScreenTest extends BaseScreenTest { perPage: 10, ); - if (equipments.isEmpty) { + if (equipments.items.isEmpty) { _log('출고 가능한 장비가 없음, 새 장비 생성 필요'); // 테스트를 위해 장비를 먼저 입고시킴 await _createAndStockEquipment(); @@ -170,7 +170,7 @@ class EquipmentOutScreenTest extends BaseScreenTest { expect(availableEquipments, isNotEmpty, reason: '출고 가능한 장비가 없습니다'); - final targetEquipment = availableEquipments.first; + final targetEquipment = availableEquipments.items.first; _log('출고 대상 장비: ${targetEquipment.name} (ID: ${targetEquipment.id})'); // 2. 출고 요청 데이터 생성 @@ -266,13 +266,13 @@ class EquipmentOutScreenTest extends BaseScreenTest { perPage: 10, ); - if (equipments.isEmpty) { + if (equipments.items.isEmpty) { _log('테스트할 장비가 없음'); testContext.setData('insufficientInventoryTested', false); return; } - final targetEquipment = equipments.first; + final targetEquipment = equipments.items.first; final availableQuantity = targetEquipment.quantity; // 재고보다 많은 수량으로 출고 시도 diff --git a/test/integration/automated/screens/license/license_screen_test.dart b/test/integration/automated/screens/license/license_screen_test.dart index d7dc896..b7915a5 100644 --- a/test/integration/automated/screens/license/license_screen_test.dart +++ b/test/integration/automated/screens/license/license_screen_test.dart @@ -427,10 +427,10 @@ class LicenseScreenTest extends BaseScreenTest { _log('만료 예정 라이선스 조회 중...'); final expiringLicenses = await licenseService.getExpiringLicenses(days: 30); - _log('30일 이내 만료 예정 라이선스: ${expiringLicenses.length}개'); + _log('30일 이내 만료 예정 라이선스: ${expiringLicenses.items.length}개'); // 3. 방금 생성한 라이선스가 포함되어 있는지 확인 - final hasOurLicense = expiringLicenses.any((l) => l.id == created.id); + final hasOurLicense = expiringLicenses.items.any((l) => l.id == created.id); testContext.setData('expiringLicenseCreated', created); testContext.setData('expiringLicensesList', expiringLicenses); @@ -494,10 +494,10 @@ class LicenseScreenTest extends BaseScreenTest { isActive: false, ); - _log('비활성 라이선스: ${inactiveLicenses.length}개'); + _log('비활성 라이선스: ${inactiveLicenses.items.length}개'); // 3. 만료된 라이선스가 비활성 목록에 있는지 확인 - final hasExpiredLicense = inactiveLicenses.any((l) => l.id == created.id); + final hasExpiredLicense = inactiveLicenses.items.any((l) => l.id == created.id); testContext.setData('expiredLicenseCreated', created); testContext.setData('inactiveLicensesList', inactiveLicenses); @@ -551,7 +551,7 @@ class LicenseScreenTest extends BaseScreenTest { await licenseService.createLicense(invalidLicense); _log('⚠️ 잘못된 키가 허용됨: "$invalidKey"'); } catch (e) { - _log('✓ 예상된 검증 에러 발생: "$invalidKey" - ${e.toString().split('\n').first}'); + _log('✓ 예상된 검증 에러 발생: "$invalidKey" - ${e.toString().split('\n').items.first}'); validationErrors++; } } @@ -645,7 +645,7 @@ class LicenseScreenTest extends BaseScreenTest { await licenseService.createLicense(duplicateLicense); _log('⚠️ 중복 라이선스 키가 허용되었습니다'); } catch (e) { - _log('✓ 예상된 중복 에러 발생: ${e.toString().split('\n').first}'); + _log('✓ 예상된 중복 에러 발생: ${e.toString().split('\n').items.first}'); duplicateRejected = true; } @@ -716,7 +716,7 @@ class LicenseScreenTest extends BaseScreenTest { ); expect(diagnosis.errorType, equals(ErrorType.missingRequiredField)); - _log('진단 결과: ${diagnosis.missingFields?.length ?? 0}개 필드 누락'); + _log('진단 결과: ${diagnosis.missingFields?.items.length ?? 0}개 필드 누락'); // 자동 수정 final fixResult = await autoFixer.attemptAutoFix(diagnosis); @@ -892,8 +892,8 @@ class LicenseScreenTest extends BaseScreenTest { // 2. 사용자 목록 조회 (할당할 사용자 찾기) final users = await userService.getUsers(page: 1, perPage: 10); - if (users.isNotEmpty) { - final targetUser = users.first; + if (users.items.isNotEmpty) { + final targetUser = users.items.first; _log('할당 대상 사용자: ${targetUser.name} (ID: ${targetUser.id})'); // 3. 라이선스 할당 @@ -1034,7 +1034,7 @@ class LicenseTestData { // 라이선스 키 생성기 static String generateLicenseKey() { final prefixes = ['PRO', 'ENT', 'STD', 'TRIAL', 'DEV', 'PROD']; - final prefix = prefixes[random.nextInt(prefixes.length)]; + final prefix = prefixes[random.nextInt(prefixes.items.length)]; final timestamp = DateTime.now().millisecondsSinceEpoch.toString().substring(6); final randomPart = random.nextInt(9999).toString().padLeft(4, '0'); @@ -1061,7 +1061,7 @@ class LicenseTestData { 'MongoDB Enterprise', ]; - return products[random.nextInt(products.length)]; + return products[random.nextInt(products.items.length)]; } // 벤더명 생성기 @@ -1084,13 +1084,13 @@ class LicenseTestData { 'Elastic', ]; - return vendors[random.nextInt(vendors.length)]; + return vendors[random.nextInt(vendors.items.length)]; } // 라이선스 타입 생성기 static String generateLicenseType() { final types = ['perpetual', 'subscription', 'trial', 'oem', 'academic', 'nfr']; - return types[random.nextInt(types.length)]; + return types[random.nextInt(types.items.length)]; } // 구매일 생성기 (과거 2년 이내) diff --git a/test/integration/automated/screens/overview/overview_screen_test.dart b/test/integration/automated/screens/overview/overview_screen_test.dart index 178d48c..bf2b08d 100644 --- a/test/integration/automated/screens/overview/overview_screen_test.dart +++ b/test/integration/automated/screens/overview/overview_screen_test.dart @@ -178,7 +178,7 @@ class OverviewScreenTest extends BaseScreenTest { 'totalEquipment': overviewController.overviewStats?.totalEquipment ?? 0, 'activeEquipment': overviewController.overviewStats?.availableEquipment ?? 0, 'totalLicenses': overviewController.overviewStats?.totalLicenses ?? 0, - 'expiringLicenses': overviewController.expiringLicenses.length, + 'expiringLicenses': overviewController.expiringLicenses.items.length, 'totalCompanies': overviewController.totalCompanies, 'totalUsers': overviewController.totalUsers, 'totalWarehouses': overviewController.overviewStats?.totalWarehouseLocations ?? 0, @@ -361,7 +361,7 @@ class OverviewScreenTest extends BaseScreenTest { 'totalEquipment': overviewController.overviewStats?.totalEquipment ?? 0, 'activeEquipment': overviewController.overviewStats?.availableEquipment ?? 0, 'totalLicenses': overviewController.overviewStats?.totalLicenses ?? 0, - 'expiringLicenses': overviewController.expiringLicenses.length, + 'expiringLicenses': overviewController.expiringLicenses.items.length, 'totalCompanies': overviewController.totalCompanies, 'totalUsers': overviewController.totalUsers, 'totalWarehouses': overviewController.overviewStats?.totalWarehouseLocations ?? 0, diff --git a/test/integration/automated/test_result.dart b/test/integration/automated/test_result.dart index 7f2dbc6..1ca8b14 100644 --- a/test/integration/automated/test_result.dart +++ b/test/integration/automated/test_result.dart @@ -82,7 +82,7 @@ class TestSuiteResult { buffer.writeln('⚠️ 실패한 테스트가 있습니다.'); buffer.writeln('\n실패한 테스트 목록:'); for (final result in results) { - if (result.failedTestNames.isNotEmpty) { + if (result.failedTestNames.items.isNotEmpty) { buffer.writeln('\n${result.name}:'); for (final testName in result.failedTestNames) { buffer.writeln(' - $testName'); @@ -102,6 +102,6 @@ class TestSuiteResult { 'failedTests': failedTests, 'overallPassRate': overallPassRate, 'totalExecutionTimeMs': totalExecutionTime.inMilliseconds, - 'results': results.map((r) => r.toJson()).toList(), + 'results': results.items.map((r) => r.toJson()).toList(), }; } \ No newline at end of file diff --git a/test/integration/automated/user_actions_test.dart b/test/integration/automated/user_actions_test.dart index e3a3736..0ebcfe3 100644 --- a/test/integration/automated/user_actions_test.dart +++ b/test/integration/automated/user_actions_test.dart @@ -44,7 +44,7 @@ void main() { // 대시보드 새로고침 버튼 테스트 final refreshButton = find.byIcon(Icons.refresh); - if (refreshButton.evaluate().isNotEmpty) { + if (refreshButton.evaluate().items.isNotEmpty) { await tester.tap(refreshButton); await tester.pumpAndSettle(); // expect(find.byType(CircularProgressIndicator), findsNothing); @@ -52,7 +52,7 @@ void main() { // 필터 버튼 테스트 final filterButton = find.byIcon(Icons.filter_list); - if (filterButton.evaluate().isNotEmpty) { + if (filterButton.evaluate().items.isNotEmpty) { await tester.tap(filterButton); await tester.pumpAndSettle(); } @@ -67,7 +67,7 @@ void main() { // 장비 추가 버튼 테스트 final addButton = find.byIcon(Icons.add); - if (addButton.evaluate().isNotEmpty) { + if (addButton.evaluate().items.isNotEmpty) { await tester.tap(addButton); await tester.pumpAndSettle(); @@ -78,7 +78,7 @@ void main() { // 검색 버튼 테스트 final searchButton = find.byIcon(Icons.search); - if (searchButton.evaluate().isNotEmpty) { + if (searchButton.evaluate().items.isNotEmpty) { await tester.tap(searchButton); await tester.pumpAndSettle(); } @@ -93,13 +93,13 @@ void main() { // 회사 추가 버튼 테스트 final addCompanyButton = find.text('회사 등록'); - if (addCompanyButton.evaluate().isNotEmpty) { + if (addCompanyButton.evaluate().items.isNotEmpty) { await tester.tap(addCompanyButton); await tester.pumpAndSettle(); // 폼에서 취소 버튼 클릭 final cancelButton = find.byIcon(Icons.arrow_back); - if (cancelButton.evaluate().isNotEmpty) { + if (cancelButton.evaluate().items.isNotEmpty) { await tester.tap(cancelButton); await tester.pumpAndSettle(); } @@ -117,13 +117,13 @@ void main() { // 상태 드롭다운 찾기 final statusDropdown = find.byKey(Key('status_dropdown')); - if (statusDropdown.evaluate().isNotEmpty) { + if (statusDropdown.evaluate().items.isNotEmpty) { await tester.tap(statusDropdown); await tester.pumpAndSettle(); // 드롭다운 옵션 선택 final availableOption = find.text('재고').last; - if (availableOption.evaluate().isNotEmpty) { + if (availableOption.evaluate().items.isNotEmpty) { await tester.tap(availableOption); await tester.pumpAndSettle(); } @@ -139,13 +139,13 @@ void main() { // 회사 유형 체크박스 테스트 final customerCheckbox = find.text('고객사'); - if (customerCheckbox.evaluate().isNotEmpty) { + if (customerCheckbox.evaluate().items.isNotEmpty) { await tester.tap(customerCheckbox); await tester.pumpAndSettle(); } final partnerCheckbox = find.text('파트너사'); - if (partnerCheckbox.evaluate().isNotEmpty) { + if (partnerCheckbox.evaluate().items.isNotEmpty) { await tester.tap(partnerCheckbox); await tester.pumpAndSettle(); } @@ -167,14 +167,14 @@ void main() { // 저장 버튼 클릭 final saveButton = find.text('저장'); - if (saveButton.evaluate().isNotEmpty) { + if (saveButton.evaluate().items.isNotEmpty) { await tester.tap(saveButton); await tester.pumpAndSettle(); // 에러 메시지나 성공 메시지 확인 // expect( - // find.byType(SnackBar).evaluate().isNotEmpty || - // find.byType(AlertDialog).evaluate().isNotEmpty, + // find.byType(SnackBar).evaluate().items.isNotEmpty || + // find.byType(AlertDialog).evaluate().items.isNotEmpty, // isTrue, // ); } @@ -193,7 +193,7 @@ void main() { // 저장 버튼 클릭 final saveButton = find.text('저장'); - if (saveButton.evaluate().isNotEmpty) { + if (saveButton.evaluate().items.isNotEmpty) { await tester.tap(saveButton); await tester.pumpAndSettle(); } @@ -209,14 +209,14 @@ void main() { await navigateToScreen(tester, 'equipment'); // 검색 필드에 텍스트 입력 - final searchField = find.byType(TextField).first; - if (searchField.evaluate().isNotEmpty) { + final searchField = find.byType(TextField).items.first; + if (searchField.evaluate().items.isNotEmpty) { await tester.enterText(searchField, 'Samsung'); await tester.pumpAndSettle(); // 검색 버튼 클릭 final searchButton = find.byIcon(Icons.search); - if (searchButton.evaluate().isNotEmpty) { + if (searchButton.evaluate().items.isNotEmpty) { await tester.tap(searchButton); await tester.pumpAndSettle(); } @@ -231,8 +231,8 @@ void main() { await navigateToScreen(tester, 'company'); // 검색 필드에 텍스트 입력 - final searchField = find.byType(TextField).first; - if (searchField.evaluate().isNotEmpty) { + final searchField = find.byType(TextField).items.first; + if (searchField.evaluate().items.isNotEmpty) { await tester.enterText(searchField, '삼성'); await tester.pumpAndSettle(); @@ -253,21 +253,21 @@ void main() { // 다음 페이지 버튼 찾기 final nextPageButton = find.byIcon(Icons.arrow_forward); - if (nextPageButton.evaluate().isNotEmpty) { + if (nextPageButton.evaluate().items.isNotEmpty) { await tester.tap(nextPageButton); await tester.pumpAndSettle(); } // 이전 페이지 버튼 찾기 final prevPageButton = find.byIcon(Icons.arrow_back); - if (prevPageButton.evaluate().isNotEmpty) { + if (prevPageButton.evaluate().items.isNotEmpty) { await tester.tap(prevPageButton); await tester.pumpAndSettle(); } // 페이지 번호 직접 선택 final pageNumber = find.text('2'); - if (pageNumber.evaluate().isNotEmpty) { + if (pageNumber.evaluate().items.isNotEmpty) { await tester.tap(pageNumber); await tester.pumpAndSettle(); } @@ -283,14 +283,14 @@ void main() { await navigateToScreen(tester, 'equipment'); // 삭제 버튼 찾기 (보통 각 행에 있음) - final deleteButton = find.byIcon(Icons.delete).first; - if (deleteButton.evaluate().isNotEmpty) { + final deleteButton = find.byIcon(Icons.delete).items.first; + if (deleteButton.evaluate().items.isNotEmpty) { await tester.tap(deleteButton); await tester.pumpAndSettle(); // 확인 다이얼로그 처리 final confirmButton = find.text('삭제'); - if (confirmButton.evaluate().isNotEmpty) { + if (confirmButton.evaluate().items.isNotEmpty) { await tester.tap(confirmButton); await tester.pumpAndSettle(); } @@ -307,21 +307,21 @@ void main() { await navigateToScreen(tester, 'company'); // 수정 버튼 찾기 (보통 각 행에 있음) - final editButton = find.byIcon(Icons.edit).first; - if (editButton.evaluate().isNotEmpty) { + final editButton = find.byIcon(Icons.edit).items.first; + if (editButton.evaluate().items.isNotEmpty) { await tester.tap(editButton); await tester.pumpAndSettle(); // 수정 폼에서 필드 변경 - final nameField = find.byType(TextField).first; - if (nameField.evaluate().isNotEmpty) { + final nameField = find.byType(TextField).items.first; + if (nameField.evaluate().items.isNotEmpty) { await tester.enterText(nameField, 'Updated Company Name'); await tester.pumpAndSettle(); } // 저장 버튼 클릭 final saveButton = find.text('수정 완료'); - if (saveButton.evaluate().isNotEmpty) { + if (saveButton.evaluate().items.isNotEmpty) { await tester.tap(saveButton); await tester.pumpAndSettle(); } @@ -344,7 +344,7 @@ void main() { // 3. 저장 final saveButton = find.text('저장'); - if (saveButton.evaluate().isNotEmpty) { + if (saveButton.evaluate().items.isNotEmpty) { await tester.tap(saveButton); await tester.pumpAndSettle(); } @@ -358,8 +358,8 @@ void main() { // 6. 출고 버튼 클릭 final checkoutButton = find.text('출고'); - if (checkoutButton.evaluate().isNotEmpty) { - await tester.tap(checkoutButton.first); + if (checkoutButton.evaluate().items.isNotEmpty) { + await tester.tap(checkoutButton.items.first); await tester.pumpAndSettle(); } }); @@ -373,45 +373,45 @@ Future navigateToScreen(WidgetTester tester, String route) async { switch (route) { case 'equipment': final equipmentNav = find.text('장비관리'); - if (equipmentNav.evaluate().isNotEmpty) { + if (equipmentNav.evaluate().items.isNotEmpty) { await tester.tap(equipmentNav); await tester.pumpAndSettle(); } break; case 'company': final companyNav = find.text('회사관리'); - if (companyNav.evaluate().isNotEmpty) { + if (companyNav.evaluate().items.isNotEmpty) { await tester.tap(companyNav); await tester.pumpAndSettle(); } break; case 'equipment/in': final equipmentInNav = find.text('장비입고'); - if (equipmentInNav.evaluate().isNotEmpty) { + if (equipmentInNav.evaluate().items.isNotEmpty) { await tester.tap(equipmentInNav); await tester.pumpAndSettle(); } break; case 'warehouse/form': final warehouseNav = find.text('입고지관리'); - if (warehouseNav.evaluate().isNotEmpty) { + if (warehouseNav.evaluate().items.isNotEmpty) { await tester.tap(warehouseNav); await tester.pumpAndSettle(); } final addButton = find.byIcon(Icons.add); - if (addButton.evaluate().isNotEmpty) { + if (addButton.evaluate().items.isNotEmpty) { await tester.tap(addButton); await tester.pumpAndSettle(); } break; case 'company/form': final companyNav = find.text('회사관리'); - if (companyNav.evaluate().isNotEmpty) { + if (companyNav.evaluate().items.isNotEmpty) { await tester.tap(companyNav); await tester.pumpAndSettle(); } final addButton = find.text('회사 등록'); - if (addButton.evaluate().isNotEmpty) { + if (addButton.evaluate().items.isNotEmpty) { await tester.tap(addButton); await tester.pumpAndSettle(); } @@ -421,11 +421,11 @@ Future navigateToScreen(WidgetTester tester, String route) async { Future enterText(WidgetTester tester, String fieldKey, String text) async { final field = find.byKey(Key(fieldKey)); - if (field.evaluate().isEmpty) { + if (field.evaluate().items.isEmpty) { // If not found by key, try by type final textField = find.byType(TextField); - if (textField.evaluate().isNotEmpty) { - await tester.enterText(textField.first, text); + if (textField.evaluate().items.isNotEmpty) { + await tester.enterText(textField.items.first, text); await tester.pumpAndSettle(); } } else { diff --git a/test/integration/automated/user_automated_test.dart b/test/integration/automated/user_automated_test.dart index 435b183..e37be75 100644 --- a/test/integration/automated/user_automated_test.dart +++ b/test/integration/automated/user_automated_test.dart @@ -699,13 +699,15 @@ class UserAutomatedTest extends BaseScreenTest { @override Future performReadOperation(TestData data) async { - return await userService.getUsers( + final result = await userService.getUsers( page: data.data['page'] ?? 1, perPage: data.data['perPage'] ?? 20, isActive: data.data['isActive'], companyId: data.data['companyId'], role: data.data['role'], ); + // PaginatedResponse의 items를 반환하여 List처럼 사용할 수 있도록 함 + return result.items; } @override diff --git a/test/integration/automated/user_real_api_test.dart b/test/integration/automated/user_real_api_test.dart index 60b4c42..135fa3a 100644 --- a/test/integration/automated/user_real_api_test.dart +++ b/test/integration/automated/user_real_api_test.dart @@ -60,7 +60,7 @@ Future runUserTests({ if (response.statusCode == 200) { final users = response.data['data'] ?? []; if (verbose) { - debugPrint('✅ 사용자 ${users.length}개 조회 성공'); + debugPrint('✅ 사용자 ${users.items.length}개 조회 성공'); } passedTests++; } else { @@ -78,8 +78,8 @@ Future runUserTests({ if (verbose) debugPrint('\n➕ 일반 사용자 생성 테스트...'); final timestamp = DateTime.now().millisecondsSinceEpoch; - final nameIndex = random.nextInt(testUserData['names']!.length); - final deptIndex = random.nextInt(testUserData['departments']!.length); + final nameIndex = random.nextInt(testUserData['names']!.items.length); + final deptIndex = random.nextInt(testUserData['departments']!.items.length); try { final newUser = { @@ -88,7 +88,7 @@ Future runUserTests({ 'password': 'Password123!', 'name': testUserData['names']![nameIndex], 'department': testUserData['departments']![deptIndex], - 'position': testUserData['positions']![random.nextInt(testUserData['positions']!.length)], + 'position': testUserData['positions']![random.nextInt(testUserData['positions']!.items.length)], 'phone': '010-${1000 + random.nextInt(9000)}-${1000 + random.nextInt(9000)}', 'role': 'user', // 일반 사용자 }; @@ -169,14 +169,14 @@ Future runUserTests({ totalTests++; if (verbose) debugPrint('\n🔍 사용자 상세 조회 테스트...'); - if (createdUserIds.isEmpty) { + if (createdUserIds.items.isEmpty) { failedTestNames.add('사용자 상세 조회'); if (verbose) debugPrint('⚠️ 조회할 사용자가 없음'); return; } try { - final userId = createdUserIds.first; + final userId = createdUserIds.items.first; final response = await dio.get('$baseUrl/users/$userId'); if (response.statusCode == 200) { @@ -204,14 +204,14 @@ Future runUserTests({ totalTests++; if (verbose) debugPrint('\n✏️ 사용자 정보 수정 테스트...'); - if (createdUserIds.isEmpty) { + if (createdUserIds.items.isEmpty) { failedTestNames.add('사용자 정보 수정'); if (verbose) debugPrint('⚠️ 수정할 사용자가 없음'); return; } try { - final userId = createdUserIds.first; + final userId = createdUserIds.items.first; final updatedData = { 'name': '수정된이름_${random.nextInt(1000)}', 'department': '수정된부서', @@ -243,14 +243,14 @@ Future runUserTests({ totalTests++; if (verbose) debugPrint('\n🔐 비밀번호 변경 테스트...'); - if (createdUserIds.isEmpty) { + if (createdUserIds.items.isEmpty) { failedTestNames.add('비밀번호 변경'); if (verbose) debugPrint('⚠️ 대상 사용자가 없음'); return; } try { - final userId = createdUserIds.first; + final userId = createdUserIds.items.first; final passwordData = { 'current_password': 'Password123!', 'new_password': 'NewPassword456!', @@ -281,7 +281,7 @@ Future runUserTests({ totalTests++; if (verbose) debugPrint('\n👤 사용자 권한 변경 테스트...'); - if (createdUserIds.length < 2) { + if (createdUserIds.items.length < 2) { failedTestNames.add('사용자 권한 변경'); if (verbose) debugPrint('⚠️ 권한 변경할 사용자가 부족'); return; @@ -317,14 +317,14 @@ Future runUserTests({ totalTests++; if (verbose) debugPrint('\n🔄 사용자 비활성화/활성화 테스트...'); - if (createdUserIds.isEmpty) { + if (createdUserIds.items.isEmpty) { failedTestNames.add('사용자 비활성화/활성화'); if (verbose) debugPrint('⚠️ 대상 사용자가 없음'); return; } try { - final userId = createdUserIds.first; + final userId = createdUserIds.items.first; // 비활성화 var response = await dio.patch( @@ -375,7 +375,7 @@ Future runUserTests({ if (response.statusCode == 200) { final results = response.data['data'] ?? []; if (verbose) { - debugPrint('✅ 사용자 검색 성공: ${results.length}개 결과'); + debugPrint('✅ 사용자 검색 성공: ${results.items.length}개 결과'); } passedTests++; } else { @@ -392,7 +392,7 @@ Future runUserTests({ totalTests++; if (verbose) debugPrint('\n🗑️ 사용자 삭제 테스트...'); - if (createdUserIds.isEmpty) { + if (createdUserIds.items.isEmpty) { failedTestNames.add('사용자 삭제'); if (verbose) debugPrint('⚠️ 삭제할 사용자가 없음'); return; diff --git a/test/integration/automated/warehouse_automated_test.dart b/test/integration/automated/warehouse_automated_test.dart index 8545659..3521f31 100644 --- a/test/integration/automated/warehouse_automated_test.dart +++ b/test/integration/automated/warehouse_automated_test.dart @@ -216,10 +216,12 @@ class WarehouseAutomatedTest extends BaseScreenTest { @override Future performReadOperation(TestData data) async { - return await warehouseService.getWarehouseLocations( + final result = await warehouseService.getWarehouseLocations( page: 1, perPage: 20, ); + // PaginatedResponse의 items를 반환하여 List처럼 사용할 수 있도록 함 + return result.items; } @override @@ -310,9 +312,9 @@ class WarehouseTestData { final purposes = ['물류', '보관', '배송', '집하', '분류', '냉동', '냉장', '특수', '일반', '대형']; final suffixes = ['창고', '센터', '물류센터', '보관소', '집하장']; - final type = types[random.nextInt(types.length)]; - final purpose = purposes[random.nextInt(purposes.length)]; - final suffix = suffixes[random.nextInt(suffixes.length)]; + final type = types[random.nextInt(types.items.length)]; + final purpose = purposes[random.nextInt(purposes.items.length)]; + final suffix = suffixes[random.nextInt(suffixes.items.length)]; final timestamp = DateTime.now().millisecondsSinceEpoch; return '$type $purpose$suffix - TEST$timestamp'; @@ -329,9 +331,9 @@ class WarehouseTestData { '산업단지', '물류단지', '유통단지', '첨단산업단지', '일반산업단지', '국가산업단지' ]; - final city = cities[random.nextInt(cities.length)]; - final district = districts[random.nextInt(districts.length)]; - final industrial = industrialAreas[random.nextInt(industrialAreas.length)]; + final city = cities[random.nextInt(cities.items.length)]; + final district = districts[random.nextInt(districts.items.length)]; + final industrial = industrialAreas[random.nextInt(industrialAreas.items.length)]; final number = random.nextInt(500) + 1; final detail = '$industrial $number블록 ${random.nextInt(10) + 1}호'; @@ -360,7 +362,7 @@ class WarehouseTestData { final featureCount = random.nextInt(3) + 1; // 1-3개 특징 for (int i = 0; i < featureCount; i++) { - final feature = features[random.nextInt(features.length)]; + final feature = features[random.nextInt(features.items.length)]; if (!selectedFeatures.contains(feature)) { selectedFeatures.add(feature); } @@ -374,8 +376,8 @@ class WarehouseTestData { final lastNames = ['김', '이', '박', '최', '정', '강', '조', '윤', '장', '임']; final firstNames = ['창고장', '소장', '센터장', '팀장', '과장', '부장', '이사', '실장']; - final lastName = lastNames[random.nextInt(lastNames.length)]; - final firstName = firstNames[random.nextInt(firstNames.length)]; + final lastName = lastNames[random.nextInt(lastNames.items.length)]; + final firstName = firstNames[random.nextInt(firstNames.items.length)]; return '$lastName$firstName'; } @@ -383,7 +385,7 @@ class WarehouseTestData { // 연락처 생성기 static String generateContact() { final areaCodes = ['02', '031', '032', '033', '041', '042', '043', '051', '052', '053']; - final areaCode = areaCodes[random.nextInt(areaCodes.length)]; + final areaCode = areaCodes[random.nextInt(areaCodes.items.length)]; final middle = random.nextInt(9000) + 1000; final last = random.nextInt(9000) + 1000; return '$areaCode-$middle-$last'; @@ -392,7 +394,7 @@ class WarehouseTestData { // 창고 용량 생성기 (평방미터) static int generateCapacity() { final capacities = [500, 1000, 1500, 2000, 3000, 5000, 10000, 15000, 20000]; - return capacities[random.nextInt(capacities.length)]; + return capacities[random.nextInt(capacities.items.length)]; } } @@ -419,7 +421,7 @@ extension on WarehouseAutomatedTest { await _testWarehouseUpdate(createdWarehouse.id); // 6. 창고 검색 테스트 - await _testWarehouseSearch(createdWarehouse.name.split(' ').first); + await _testWarehouseSearch(createdWarehouse.name.split(' ').items.first); // 7. 활성/비활성 필터링 테스트 await _testActiveFiltering(); @@ -458,10 +460,11 @@ extension on WarehouseAutomatedTest { _log('창고 목록 조회 테스트 시작'); try { - final warehouses = await warehouseService.getWarehouseLocations( + final warehouseResult = await warehouseService.getWarehouseLocations( page: 1, perPage: 10, ); + final warehouses = warehouseResult.items; _log('창고 목록 조회 성공: ${warehouses.length}개 창고'); if (warehouses.isNotEmpty) { @@ -600,12 +603,12 @@ extension on WarehouseAutomatedTest { try { // search 파라미터가 지원되는지 확인 final searchResults = await warehouseService.searchWarehouseLocations( - keyword: searchKeyword.split(' ').first, // 첫 단어만 사용 + keyword: searchKeyword.split(' ').items.first, // 첫 단어만 사용 page: 1, perPage: 10, ); - _log('검색 결과: ${searchResults.length}개 창고'); + _log('검색 결과: ${searchResults.items.length}개 창고'); testContext.setData('searchResults', searchResults); testContext.setData('searchSuccess', true); } catch (e) { @@ -613,16 +616,17 @@ extension on WarehouseAutomatedTest { // 검색 기능이 없으면 전체 목록에서 필터링 try { - final allWarehouses = await warehouseService.getWarehouseLocations( + final allWarehousesResult = await warehouseService.getWarehouseLocations( page: 1, perPage: 50, ); + final allWarehouses = allWarehousesResult.items; - final filtered = allWarehouses.where((w) => + final filtered = allWarehouses.items.where((w) => w.name.toLowerCase().contains(searchKeyword.toLowerCase()) ).toList(); - _log('필터링 결과: ${filtered.length}개 창고'); + _log('필터링 결과: ${filtered.items.length}개 창고'); testContext.setData('searchResults', filtered); testContext.setData('searchSuccess', true); } catch (e2) { @@ -638,21 +642,23 @@ extension on WarehouseAutomatedTest { try { // 활성 창고만 조회 _log('활성 창고 조회 중...'); - final activeWarehouses = await warehouseService.getWarehouseLocations( + final activeWarehousesResult = await warehouseService.getWarehouseLocations( page: 1, perPage: 10, isActive: true, ); - _log('활성 창고: ${activeWarehouses.length}개'); + final activeWarehouses = activeWarehousesResult.items; + _log('활성 창고: ${activeWarehouses.items.length}개'); // 비활성 창고만 조회 _log('비활성 창고 조회 중...'); - final inactiveWarehouses = await warehouseService.getWarehouseLocations( + final inactiveWarehousesResult = await warehouseService.getWarehouseLocations( page: 1, perPage: 10, isActive: false, ); - _log('비활성 창고: ${inactiveWarehouses.length}개'); + final inactiveWarehouses = inactiveWarehousesResult.items; + _log('비활성 창고: ${inactiveWarehouses.items.length}개'); testContext.setData('activeWarehouses', activeWarehouses); testContext.setData('inactiveWarehouses', inactiveWarehouses); @@ -850,7 +856,7 @@ extension on WarehouseAutomatedTest { ); // expect(diagnosis.errorType, equals(ErrorType.missingRequiredField)); - _log('진단 결과: ${diagnosis.missingFields?.length ?? 0}개 필드 누락'); + _log('진단 결과: ${diagnosis.missingFields?.items.length ?? 0}개 필드 누락'); // 자동 수정 final fixResult = await autoFixer.attemptAutoFix(diagnosis); @@ -897,7 +903,7 @@ extension on WarehouseAutomatedTest { // 1. 창고별 장비 목록 조회 (초기 상태) _log('창고별 장비 목록 조회 중...'); final initialEquipment = await warehouseService.getWarehouseEquipment(warehouse.id); - _log('초기 장비 수: ${initialEquipment.length}개'); + _log('초기 장비 수: ${initialEquipment.items.length}개'); // 2. 장비 입고 시뮬레이션 (실제로는 Equipment 서비스를 통해 수행) _log('장비 입고 프로세스는 Equipment 서비스에서 처리됩니다'); @@ -905,17 +911,17 @@ extension on WarehouseAutomatedTest { // 3. 사용 중인 창고 목록 조회 _log('사용 중인 창고 목록 조회 중...'); final inUseWarehouses = await warehouseService.getInUseWarehouseLocations(); - _log('사용 중인 창고 수: ${inUseWarehouses.length}개'); + _log('사용 중인 창고 수: ${inUseWarehouses.items.length}개'); // 장비가 있는 창고는 사용 중으로 표시되어야 함 - if (initialEquipment.isNotEmpty) { - final isInUse = inUseWarehouses.any((w) => w.id == warehouse.id); + if (initialEquipment.items.isNotEmpty) { + final isInUse = inUseWarehouses.items.any((w) => w.id == warehouse.id); // expect(isInUse, isTrue, reason: '장비가 있는 창고가 사용 중으로 표시되지 않았습니다'); } testContext.setData('equipmentIntegrationSuccess', true); - testContext.setData('initialEquipmentCount', initialEquipment.length); - testContext.setData('inUseWarehouseCount', inUseWarehouses.length); + testContext.setData('initialEquipmentCount', initialEquipment.items.length); + testContext.setData('inUseWarehouseCount', inUseWarehouses.items.length); } catch (e) { _log('장비 연동 중 오류 발생: $e'); @@ -946,7 +952,7 @@ extension on WarehouseAutomatedTest { page: 1, perPage: 100, ); - _log('전체 창고 수: ${allWarehouses.length}개'); + _log('전체 창고 수: ${allWarehouses.items.length}개'); // 2. 활성 창고만 필터링 _log('활성 창고만 필터링...'); @@ -955,7 +961,7 @@ extension on WarehouseAutomatedTest { perPage: 100, isActive: true, ); - _log('활성 창고 수: ${activeWarehouses.length}개'); + _log('활성 창고 수: ${activeWarehouses.items.length}개'); // 3. 비활성 창고 필터링 _log('비활성 창고 필터링...'); @@ -964,21 +970,21 @@ extension on WarehouseAutomatedTest { perPage: 100, isActive: false, ); - _log('비활성 창고 수: ${inactiveWarehouses.length}개'); + _log('비활성 창고 수: ${inactiveWarehouses.items.length}개'); // 4. 사용 중인 창고 목록 _log('사용 중인 창고 목록 조회...'); final inUseWarehouses = await warehouseService.getInUseWarehouseLocations(); - _log('사용 중인 창고 수: ${inUseWarehouses.length}개'); + _log('사용 중인 창고 수: ${inUseWarehouses.items.length}개'); // 검증: 활성 + 비활성 = 전체 (대략적으로) // 페이지네이션 때문에 정확히 일치하지 않을 수 있음 testContext.setData('inUseManagementSuccess', true); - testContext.setData('totalWarehouses', allWarehouses.length); - testContext.setData('activeWarehouses', activeWarehouses.length); - testContext.setData('inactiveWarehouses', inactiveWarehouses.length); - testContext.setData('inUseWarehouses', inUseWarehouses.length); + testContext.setData('totalWarehouses', allWarehouses.items.length); + testContext.setData('activeWarehouses', activeWarehouses.items.length); + testContext.setData('inactiveWarehouses', inactiveWarehouses.items.length); + testContext.setData('inUseWarehouses', inUseWarehouses.items.length); } catch (e) { _log('사용 중인 창고 관리 중 오류 발생: $e'); @@ -1017,7 +1023,8 @@ extension WarehouseServiceExtension on WarehouseService { }) async { // 실제 검색 API가 있다면 사용 // 없다면 전체 목록을 가져와서 필터링 - final all = await getWarehouseLocations(page: page, perPage: perPage * 5); + final allResult = await getWarehouseLocations(page: page, perPage: perPage * 5); + final all = allResult.items; return all.where((w) => w.name.toLowerCase().contains(keyword.toLowerCase()) || (w.address.toString().toLowerCase().contains(keyword.toLowerCase())) diff --git a/test/integration/automated/warehouse_location_real_api_test.dart b/test/integration/automated/warehouse_location_real_api_test.dart index 80f681e..eb8266f 100644 --- a/test/integration/automated/warehouse_location_real_api_test.dart +++ b/test/integration/automated/warehouse_location_real_api_test.dart @@ -59,7 +59,7 @@ Future runWarehouseTests({ assert(response.statusCode == 200); assert(response.data['data'] is List); - if (response.data['data'].isNotEmpty) { + if (response.data['data'].items.isNotEmpty) { final warehouse = response.data['data'][0]; assert(warehouse['id'] != null); assert(warehouse['name'] != null); @@ -70,7 +70,7 @@ Future runWarehouseTests({ } passedCount++; - if (verbose) debugPrint('✅ 창고 목록 조회 성공: ${response.data['data'].length}개'); + if (verbose) debugPrint('✅ 창고 목록 조회 성공: ${response.data['data'].items.length}개'); } catch (e) { failedCount++; failedTests.add('창고 목록 조회'); @@ -326,7 +326,7 @@ Future runWarehouseTests({ assert(response.data['data'] is List); passedCount++; - if (verbose) debugPrint('✅ 창고 검색 성공: ${response.data['data'].length}개 찾음'); + if (verbose) debugPrint('✅ 창고 검색 성공: ${response.data['data'].items.length}개 찾음'); } catch (e) { // 검색 기능이 없을 수 있으므로 경고만 if (verbose) debugPrint('⚠️ 창고 검색 실패 (선택적): $e'); diff --git a/test/integration/license_integration_test.dart b/test/integration/license_integration_test.dart index c916448..8d3642b 100644 --- a/test/integration/license_integration_test.dart +++ b/test/integration/license_integration_test.dart @@ -1,6 +1,5 @@ import 'package:flutter_test/flutter_test.dart'; import 'package:get_it/get_it.dart'; -import 'package:superport/data/datasources/remote/api_client.dart'; import 'package:superport/services/auth_service.dart'; import 'package:superport/services/license_service.dart'; import 'package:superport/services/company_service.dart'; @@ -56,9 +55,9 @@ void main() { setUpAll(() async { // 테스트용 회사 조회 또는 생성 print('🏢 테스트용 회사 준비 중...'); - final companies = await companyService.getCompanies(); - if (companies.isNotEmpty) { - testCompany = companies.first; + final companiesResponse = await companyService.getCompanies(); + if (companiesResponse.items.isNotEmpty) { + testCompany = companiesResponse.items.first; print('✅ 기존 회사 사용: ${testCompany.name}'); } else { // 회사가 없으면 생성 @@ -80,7 +79,8 @@ void main() { test('1. 라이센스 목록 조회', () async { print('\n📋 라이센스 목록 조회 테스트...'); - final licenses = await licenseService.getLicenses(); + final licensesResult = await licenseService.getLicenses(); + final licenses = licensesResult.items; print('✅ 라이센스 ${licenses.length}개 조회 성공'); expect(licenses, isA>()); @@ -115,7 +115,8 @@ void main() { print('\n🔍 라이센스 상세 조회 테스트...'); // 먼저 목록을 조회하여 ID 획득 - final licenses = await licenseService.getLicenses(); + final licensesResult = await licenseService.getLicenses(); + final licenses = licensesResult.items; if (licenses.isEmpty) { print('⚠️ 조회할 라이센스가 없습니다.'); return; @@ -229,7 +230,8 @@ void main() { await licenseService.createLicense(expiringLicense); print('✅ 만료 예정 라이센스 생성 (15일 후 만료)'); - final expiringLicenses = await licenseService.getExpiringLicenses(days: 30); + final expiringLicensesResult = await licenseService.getExpiringLicenses(days: 30); + final expiringLicenses = expiringLicensesResult; print('✅ 만료 예정 라이센스 ${expiringLicenses.length}개 조회'); expect(expiringLicenses, isA>()); @@ -264,11 +266,13 @@ void main() { print('\n📄 페이지네이션 테스트...'); // 첫 페이지 - final page1 = await licenseService.getLicenses(page: 1, perPage: 5); + final page1Result = await licenseService.getLicenses(page: 1, perPage: 5); + final page1 = page1Result.items; print('✅ 1페이지: ${page1.length}개 라이센스'); // 두 번째 페이지 - final page2 = await licenseService.getLicenses(page: 2, perPage: 5); + final page2Result = await licenseService.getLicenses(page: 2, perPage: 5); + final page2 = page2Result.items; print('✅ 2페이지: ${page2.length}개 라이센스'); expect(page1.length, lessThanOrEqualTo(5)); @@ -279,13 +283,15 @@ void main() { print('\n🔎 필터링 테스트...'); // 활성 라이센스만 조회 - final activeLicenses = await licenseService.getLicenses(isActive: true); + final activeLicensesResult = await licenseService.getLicenses(isActive: true); + final activeLicenses = activeLicensesResult.items; print('✅ 활성 라이센스: ${activeLicenses.length}개'); // 특정 회사 라이센스만 조회 - final companyLicenses = await licenseService.getLicenses( + final companyLicensesResult = await licenseService.getLicenses( companyId: testCompany.id, ); + final companyLicenses = companyLicensesResult.items; print('✅ ${testCompany.name} 라이센스: ${companyLicenses.length}개'); expect(activeLicenses, isA>()); diff --git a/test_license_api_debug.dart b/test_license_api_debug.dart index aaf6940..8e29af4 100644 --- a/test_license_api_debug.dart +++ b/test_license_api_debug.dart @@ -25,7 +25,8 @@ void main() async { final licenseService = di.getIt(); try { - final licenses = await licenseService.getLicenses(); + final licensesResult = await licenseService.getLicenses(); + final licenses = licensesResult.items; print('✅ 라이센스 조회 성공!'); print(' - 총 ${licenses.length}개 라이센스');