From f8e8a95391ad7fcd6920b38ddbe74db6d7f17660 Mon Sep 17 00:00:00 2001 From: JiWoong Sul Date: Sat, 9 Aug 2025 02:16:57 +0900 Subject: [PATCH] =?UTF-8?q?feat:=20=ED=9A=8C=EC=82=AC=20=EA=B4=80=EB=A6=AC?= =?UTF-8?q?=20=EA=B8=B0=EB=8A=A5=20=EA=B0=9C=EC=84=A0=20=EB=B0=8F=20?= =?UTF-8?q?=EC=A7=80=EC=A0=90=20=ED=8F=89=EB=A9=B4=ED=99=94=20DTO=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - CompanyBranchFlatDto 모델 추가로 지점 데이터 처리 개선 - 회사 서비스에 브랜치 평면화 기능 추가 - 회사 폼 컨트롤러 기능 확장 - 회사 리스트 화면 UI 개선 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude --- .../remote/company_remote_datasource.dart | 33 ++ .../company/company_branch_flat_dto.dart | 18 + .../company_branch_flat_dto.freezed.dart | 249 ++++++++++ .../company/company_branch_flat_dto.g.dart | 25 + lib/screens/company/company_form.dart | 32 +- .../company/company_list_redesign.dart | 428 ++++++++++-------- .../controllers/company_form_controller.dart | Bin 14738 -> 17893 bytes .../controllers/company_list_controller.dart | 53 ++- lib/services/company_service.dart | 52 +++ 9 files changed, 682 insertions(+), 208 deletions(-) create mode 100644 lib/data/models/company/company_branch_flat_dto.dart create mode 100644 lib/data/models/company/company_branch_flat_dto.freezed.dart create mode 100644 lib/data/models/company/company_branch_flat_dto.g.dart diff --git a/lib/data/datasources/remote/company_remote_datasource.dart b/lib/data/datasources/remote/company_remote_datasource.dart index 3ba0577..b46554b 100644 --- a/lib/data/datasources/remote/company_remote_datasource.dart +++ b/lib/data/datasources/remote/company_remote_datasource.dart @@ -9,6 +9,7 @@ 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'; +import 'package:superport/data/models/company/company_branch_flat_dto.dart'; abstract class CompanyRemoteDataSource { Future> getCompanies({ @@ -48,6 +49,8 @@ abstract class CompanyRemoteDataSource { Future deleteBranch(int companyId, int branchId); Future> getCompanyBranches(int companyId); + + Future> getCompanyBranchesFlat(); } @LazySingleton(as: CompanyRemoteDataSource) @@ -343,6 +346,36 @@ class CompanyRemoteDataSourceImpl implements CompanyRemoteDataSource { } } + @override + Future> getCompanyBranchesFlat() async { + try { + final response = await _apiClient.get('${ApiEndpoints.companies}/branches'); + + if (response.statusCode == 200) { + final responseData = response.data; + if (responseData != null && responseData['success'] == true && responseData['data'] != null) { + final List dataList = responseData['data']; + return dataList.map((item) => + CompanyBranchFlatDto.fromJson(item as Map) + ).toList(); + } else { + throw ApiException( + message: responseData?['error']?['message'] ?? 'Failed to load company branches', + statusCode: response.statusCode, + ); + } + } else { + throw ApiException( + message: 'Failed to load company branches', + statusCode: response.statusCode, + ); + } + } catch (e) { + if (e is ApiException) rethrow; + throw ApiException(message: e.toString()); + } + } + @override Future checkDuplicateCompany(String name) async { try { diff --git a/lib/data/models/company/company_branch_flat_dto.dart b/lib/data/models/company/company_branch_flat_dto.dart new file mode 100644 index 0000000..2b415ec --- /dev/null +++ b/lib/data/models/company/company_branch_flat_dto.dart @@ -0,0 +1,18 @@ +import 'package:freezed_annotation/freezed_annotation.dart'; + +part 'company_branch_flat_dto.freezed.dart'; +part 'company_branch_flat_dto.g.dart'; + +/// /companies/branches API의 평면화된 응답을 위한 DTO +@freezed +class CompanyBranchFlatDto with _$CompanyBranchFlatDto { + const factory CompanyBranchFlatDto({ + @JsonKey(name: 'company_id') required int companyId, + @JsonKey(name: 'company_name') required String companyName, + @JsonKey(name: 'branch_id') int? branchId, + @JsonKey(name: 'branch_name') String? branchName, + }) = _CompanyBranchFlatDto; + + factory CompanyBranchFlatDto.fromJson(Map json) => + _$CompanyBranchFlatDtoFromJson(json); +} \ No newline at end of file diff --git a/lib/data/models/company/company_branch_flat_dto.freezed.dart b/lib/data/models/company/company_branch_flat_dto.freezed.dart new file mode 100644 index 0000000..8dd08f8 --- /dev/null +++ b/lib/data/models/company/company_branch_flat_dto.freezed.dart @@ -0,0 +1,249 @@ +// coverage:ignore-file +// GENERATED CODE - DO NOT MODIFY BY HAND +// ignore_for_file: type=lint +// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark + +part of 'company_branch_flat_dto.dart'; + +// ************************************************************************** +// FreezedGenerator +// ************************************************************************** + +T _$identity(T value) => value; + +final _privateConstructorUsedError = UnsupportedError( + 'It seems like you constructed your class using `MyClass._()`. This constructor is only meant to be used by freezed and you are not supposed to need it nor use it.\nPlease check the documentation here for more information: https://github.com/rrousselGit/freezed#adding-getters-and-methods-to-our-models'); + +CompanyBranchFlatDto _$CompanyBranchFlatDtoFromJson(Map json) { + return _CompanyBranchFlatDto.fromJson(json); +} + +/// @nodoc +mixin _$CompanyBranchFlatDto { + @JsonKey(name: 'company_id') + int get companyId => throw _privateConstructorUsedError; + @JsonKey(name: 'company_name') + String get companyName => throw _privateConstructorUsedError; + @JsonKey(name: 'branch_id') + int? get branchId => throw _privateConstructorUsedError; + @JsonKey(name: 'branch_name') + String? get branchName => throw _privateConstructorUsedError; + + /// Serializes this CompanyBranchFlatDto to a JSON map. + Map toJson() => throw _privateConstructorUsedError; + + /// Create a copy of CompanyBranchFlatDto + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) + $CompanyBranchFlatDtoCopyWith get copyWith => + throw _privateConstructorUsedError; +} + +/// @nodoc +abstract class $CompanyBranchFlatDtoCopyWith<$Res> { + factory $CompanyBranchFlatDtoCopyWith(CompanyBranchFlatDto value, + $Res Function(CompanyBranchFlatDto) then) = + _$CompanyBranchFlatDtoCopyWithImpl<$Res, CompanyBranchFlatDto>; + @useResult + $Res call( + {@JsonKey(name: 'company_id') int companyId, + @JsonKey(name: 'company_name') String companyName, + @JsonKey(name: 'branch_id') int? branchId, + @JsonKey(name: 'branch_name') String? branchName}); +} + +/// @nodoc +class _$CompanyBranchFlatDtoCopyWithImpl<$Res, + $Val extends CompanyBranchFlatDto> + implements $CompanyBranchFlatDtoCopyWith<$Res> { + _$CompanyBranchFlatDtoCopyWithImpl(this._value, this._then); + + // ignore: unused_field + final $Val _value; + // ignore: unused_field + final $Res Function($Val) _then; + + /// Create a copy of CompanyBranchFlatDto + /// with the given fields replaced by the non-null parameter values. + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? companyId = null, + Object? companyName = null, + Object? branchId = freezed, + Object? branchName = freezed, + }) { + return _then(_value.copyWith( + companyId: null == companyId + ? _value.companyId + : companyId // ignore: cast_nullable_to_non_nullable + as int, + companyName: null == companyName + ? _value.companyName + : companyName // ignore: cast_nullable_to_non_nullable + as String, + branchId: freezed == branchId + ? _value.branchId + : branchId // ignore: cast_nullable_to_non_nullable + as int?, + branchName: freezed == branchName + ? _value.branchName + : branchName // ignore: cast_nullable_to_non_nullable + as String?, + ) as $Val); + } +} + +/// @nodoc +abstract class _$$CompanyBranchFlatDtoImplCopyWith<$Res> + implements $CompanyBranchFlatDtoCopyWith<$Res> { + factory _$$CompanyBranchFlatDtoImplCopyWith(_$CompanyBranchFlatDtoImpl value, + $Res Function(_$CompanyBranchFlatDtoImpl) then) = + __$$CompanyBranchFlatDtoImplCopyWithImpl<$Res>; + @override + @useResult + $Res call( + {@JsonKey(name: 'company_id') int companyId, + @JsonKey(name: 'company_name') String companyName, + @JsonKey(name: 'branch_id') int? branchId, + @JsonKey(name: 'branch_name') String? branchName}); +} + +/// @nodoc +class __$$CompanyBranchFlatDtoImplCopyWithImpl<$Res> + extends _$CompanyBranchFlatDtoCopyWithImpl<$Res, _$CompanyBranchFlatDtoImpl> + implements _$$CompanyBranchFlatDtoImplCopyWith<$Res> { + __$$CompanyBranchFlatDtoImplCopyWithImpl(_$CompanyBranchFlatDtoImpl _value, + $Res Function(_$CompanyBranchFlatDtoImpl) _then) + : super(_value, _then); + + /// Create a copy of CompanyBranchFlatDto + /// with the given fields replaced by the non-null parameter values. + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? companyId = null, + Object? companyName = null, + Object? branchId = freezed, + Object? branchName = freezed, + }) { + return _then(_$CompanyBranchFlatDtoImpl( + companyId: null == companyId + ? _value.companyId + : companyId // ignore: cast_nullable_to_non_nullable + as int, + companyName: null == companyName + ? _value.companyName + : companyName // ignore: cast_nullable_to_non_nullable + as String, + branchId: freezed == branchId + ? _value.branchId + : branchId // ignore: cast_nullable_to_non_nullable + as int?, + branchName: freezed == branchName + ? _value.branchName + : branchName // ignore: cast_nullable_to_non_nullable + as String?, + )); + } +} + +/// @nodoc +@JsonSerializable() +class _$CompanyBranchFlatDtoImpl implements _CompanyBranchFlatDto { + const _$CompanyBranchFlatDtoImpl( + {@JsonKey(name: 'company_id') required this.companyId, + @JsonKey(name: 'company_name') required this.companyName, + @JsonKey(name: 'branch_id') this.branchId, + @JsonKey(name: 'branch_name') this.branchName}); + + factory _$CompanyBranchFlatDtoImpl.fromJson(Map json) => + _$$CompanyBranchFlatDtoImplFromJson(json); + + @override + @JsonKey(name: 'company_id') + final int companyId; + @override + @JsonKey(name: 'company_name') + final String companyName; + @override + @JsonKey(name: 'branch_id') + final int? branchId; + @override + @JsonKey(name: 'branch_name') + final String? branchName; + + @override + String toString() { + return 'CompanyBranchFlatDto(companyId: $companyId, companyName: $companyName, branchId: $branchId, branchName: $branchName)'; + } + + @override + bool operator ==(Object other) { + return identical(this, other) || + (other.runtimeType == runtimeType && + other is _$CompanyBranchFlatDtoImpl && + (identical(other.companyId, companyId) || + other.companyId == companyId) && + (identical(other.companyName, companyName) || + other.companyName == companyName) && + (identical(other.branchId, branchId) || + other.branchId == branchId) && + (identical(other.branchName, branchName) || + other.branchName == branchName)); + } + + @JsonKey(includeFromJson: false, includeToJson: false) + @override + int get hashCode => + Object.hash(runtimeType, companyId, companyName, branchId, branchName); + + /// Create a copy of CompanyBranchFlatDto + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) + @override + @pragma('vm:prefer-inline') + _$$CompanyBranchFlatDtoImplCopyWith<_$CompanyBranchFlatDtoImpl> + get copyWith => + __$$CompanyBranchFlatDtoImplCopyWithImpl<_$CompanyBranchFlatDtoImpl>( + this, _$identity); + + @override + Map toJson() { + return _$$CompanyBranchFlatDtoImplToJson( + this, + ); + } +} + +abstract class _CompanyBranchFlatDto implements CompanyBranchFlatDto { + const factory _CompanyBranchFlatDto( + {@JsonKey(name: 'company_id') required final int companyId, + @JsonKey(name: 'company_name') required final String companyName, + @JsonKey(name: 'branch_id') final int? branchId, + @JsonKey(name: 'branch_name') final String? branchName}) = + _$CompanyBranchFlatDtoImpl; + + factory _CompanyBranchFlatDto.fromJson(Map json) = + _$CompanyBranchFlatDtoImpl.fromJson; + + @override + @JsonKey(name: 'company_id') + int get companyId; + @override + @JsonKey(name: 'company_name') + String get companyName; + @override + @JsonKey(name: 'branch_id') + int? get branchId; + @override + @JsonKey(name: 'branch_name') + String? get branchName; + + /// Create a copy of CompanyBranchFlatDto + /// with the given fields replaced by the non-null parameter values. + @override + @JsonKey(includeFromJson: false, includeToJson: false) + _$$CompanyBranchFlatDtoImplCopyWith<_$CompanyBranchFlatDtoImpl> + get copyWith => throw _privateConstructorUsedError; +} diff --git a/lib/data/models/company/company_branch_flat_dto.g.dart b/lib/data/models/company/company_branch_flat_dto.g.dart new file mode 100644 index 0000000..feafa04 --- /dev/null +++ b/lib/data/models/company/company_branch_flat_dto.g.dart @@ -0,0 +1,25 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'company_branch_flat_dto.dart'; + +// ************************************************************************** +// JsonSerializableGenerator +// ************************************************************************** + +_$CompanyBranchFlatDtoImpl _$$CompanyBranchFlatDtoImplFromJson( + Map json) => + _$CompanyBranchFlatDtoImpl( + companyId: (json['company_id'] as num).toInt(), + companyName: json['company_name'] as String, + branchId: (json['branch_id'] as num?)?.toInt(), + branchName: json['branch_name'] as String?, + ); + +Map _$$CompanyBranchFlatDtoImplToJson( + _$CompanyBranchFlatDtoImpl instance) => + { + 'company_id': instance.companyId, + 'company_name': instance.companyName, + 'branch_id': instance.branchId, + 'branch_name': instance.branchName, + }; diff --git a/lib/screens/company/company_form.dart b/lib/screens/company/company_form.dart index 199ff94..21a0533 100644 --- a/lib/screens/company/company_form.dart +++ b/lib/screens/company/company_form.dart @@ -28,6 +28,7 @@ import 'package:superport/services/mock_data_service.dart'; import 'dart:async'; import 'dart:math' as math; import 'package:superport/screens/company/controllers/branch_form_controller.dart'; +import 'package:superport/core/config/environment.dart' as env; /// 회사 유형 선택 위젯 (체크박스) class CompanyTypeSelector extends StatelessWidget { @@ -107,10 +108,36 @@ class _CompanyFormScreenState extends State { companyId = args['companyId']; branchId = args['branchId']; } + + // API 모드 확인 + final useApi = env.Environment.useApi; + debugPrint('📌 회사 폼 초기화 - API 모드: $useApi, companyId: $companyId'); + _controller = CompanyFormController( - dataService: MockDataService(), + dataService: useApi ? null : MockDataService(), companyId: companyId, + useApi: useApi, ); + + // 일반 회사 수정 모드일 때 데이터 로드 + if (!isBranch && companyId != null) { + debugPrint('📌 회사 데이터 로드 시작...'); + WidgetsBinding.instance.addPostFrameCallback((_) { + _controller.loadCompanyData().then((_) { + debugPrint('📌 회사 데이터 로드 완료, UI 갱신'); + if (mounted) { + setState(() { + debugPrint('📌 setState 호출됨'); + debugPrint('📌 nameController.text: "${_controller.nameController.text}"'); + debugPrint('📌 contactNameController.text: "${_controller.contactNameController.text}"'); + }); + } + }).catchError((error) { + debugPrint('❌ 회사 데이터 로드 실패: $error'); + }); + }); + } + // 지점 수정 모드일 때 branchId로 branch 정보 세팅 if (isBranch && branchId != null) { final company = MockDataService().getCompanyById(companyId!); @@ -471,7 +498,7 @@ class _CompanyFormScreenState extends State { ), ), ], - // 저장 버튼 + // 저장 버튼 추가 Padding( padding: const EdgeInsets.only(top: 24.0, bottom: 16.0), child: ElevatedButton( @@ -488,6 +515,7 @@ class _CompanyFormScreenState extends State { style: const TextStyle( fontSize: 16, fontWeight: FontWeight.bold, + color: Colors.white, ), ), ), diff --git a/lib/screens/company/company_list_redesign.dart b/lib/screens/company/company_list_redesign.dart index 3679b7c..af839c9 100644 --- a/lib/screens/company/company_list_redesign.dart +++ b/lib/screens/company/company_list_redesign.dart @@ -6,7 +6,8 @@ import 'package:superport/screens/common/theme_shadcn.dart'; import 'package:superport/screens/common/components/shadcn_components.dart'; import 'package:superport/screens/common/widgets/pagination.dart'; import 'package:superport/screens/common/widgets/unified_search_bar.dart'; -import 'package:superport/screens/common/widgets/standard_data_table.dart' as std_table; +import 'package:superport/screens/common/widgets/standard_data_table.dart' + as std_table; import 'package:superport/screens/common/widgets/standard_action_bar.dart'; import 'package:superport/screens/common/widgets/standard_states.dart'; import 'package:superport/screens/common/layouts/base_list_screen.dart'; @@ -123,44 +124,58 @@ class _CompanyListRedesignState extends State { /// 회사 유형 배지 생성 Widget _buildCompanyTypeChips(List types) { - return Wrap( - spacing: ShadcnTheme.spacing1, - children: - types.map((type) { - Color bgColor; - Color borderColor; - Color textColor; - - switch(type) { - case CompanyType.customer: - bgColor = ShadcnTheme.green.withValues(alpha: 0.9); - textColor = Colors.white; - break; - case CompanyType.partner: - bgColor = ShadcnTheme.purple.withValues(alpha: 0.9); - textColor = Colors.white; - break; - default: - bgColor = ShadcnTheme.muted.withValues(alpha: 0.9); - textColor = ShadcnTheme.foreground; - } - - return Container( - padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 2), - decoration: BoxDecoration( - color: bgColor, - borderRadius: BorderRadius.circular(4), - ), - child: Text( - companyTypeToString(type), - style: TextStyle( - fontSize: 12, - fontWeight: FontWeight.w500, - color: textColor, + // 유형이 없으면 기본값 표시 + if (types.isEmpty) { + return Text( + '-', + style: ShadcnTheme.bodySmall.copyWith(color: ShadcnTheme.muted), + ); + } + + return Row( + mainAxisSize: MainAxisSize.min, + children: [ + Flexible( + child: Wrap( + spacing: 4, + runSpacing: 2, + children: types.map((type) { + Color bgColor; + Color textColor; + + switch (type) { + case CompanyType.customer: + bgColor = ShadcnTheme.green.withValues(alpha: 0.9); + textColor = Colors.white; + break; + case CompanyType.partner: + bgColor = ShadcnTheme.purple.withValues(alpha: 0.9); + textColor = Colors.white; + break; + default: + bgColor = ShadcnTheme.muted.withValues(alpha: 0.9); + textColor = ShadcnTheme.foreground; + } + + return Container( + padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 1), + decoration: BoxDecoration( + color: bgColor, + borderRadius: BorderRadius.circular(3), ), - ), - ); - }).toList(), + child: Text( + companyTypeToString(type), + style: TextStyle( + fontSize: 11, + fontWeight: FontWeight.w500, + color: textColor, + ), + ), + ); + }).toList(), + ), + ), + ], ); } @@ -169,9 +184,10 @@ class _CompanyListRedesignState extends State { return Container( padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 2), decoration: BoxDecoration( - color: isBranch - ? ShadcnTheme.blue.withValues(alpha: 0.9) - : ShadcnTheme.primary.withValues(alpha: 0.9), + color: + isBranch + ? ShadcnTheme.blue.withValues(alpha: 0.9) + : ShadcnTheme.primary.withValues(alpha: 0.9), borderRadius: BorderRadius.circular(4), ), child: Text( @@ -232,37 +248,51 @@ class _CompanyListRedesignState extends State { } final int totalCount = displayCompanies.length; - + // 페이지네이션을 위한 데이터 처리 final int startIndex = (_currentPage - 1) * _pageSize; final int endIndex = startIndex + _pageSize; - final List> pagedCompanies = displayCompanies.sublist( - startIndex, - endIndex > displayCompanies.length ? displayCompanies.length : 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 - 1), + endIndex.clamp(0, displayCompanies.length), + ); // 로딩 상태 if (controller.isLoading && controller.companies.isEmpty) { - return const StandardLoadingState( - message: '회사 데이터를 불러오는 중...', - ); + return const StandardLoadingState(message: '회사 데이터를 불러오는 중...'); } return BaseListScreen( isLoading: false, error: controller.error, onRefresh: controller.refresh, - emptyMessage: controller.searchKeyword.isNotEmpty - ? '검색 결과가 없습니다' - : '등록된 회사가 없습니다', + emptyMessage: + controller.searchKeyword.isNotEmpty + ? '검색 결과가 없습니다' + : '등록된 회사가 없습니다', emptyIcon: Icons.business_outlined, - + // 검색바 searchBar: UnifiedSearchBar( controller: _searchController, placeholder: '회사명, 담당자명, 연락처로 검색', - onChanged: _onSearchChanged, // 실시간 검색 (디바운싱) - onSearch: () => _controller.updateSearchKeyword(_searchController.text), // 즉시 검색 + onChanged: _onSearchChanged, // 실시간 검색 (디바운싱) + onSearch: + () => _controller.updateSearchKeyword( + _searchController.text, + ), // 즉시 검색 onClear: () { _searchController.clear(); _onSearchChanged(''); @@ -272,155 +302,169 @@ class _CompanyListRedesignState extends State { onPressed: _navigateToAddScreen, ), ), - + // 액션바 actionBar: StandardActionBar( leftActions: [], totalCount: totalCount, onRefresh: controller.refresh, - statusMessage: controller.searchKeyword.isNotEmpty - ? '"${controller.searchKeyword}" 검색 결과' - : null, + statusMessage: + controller.searchKeyword.isNotEmpty + ? '"${controller.searchKeyword}" 검색 결과' + : null, ), // 에러 메시지 - filterSection: controller.error != null - ? StandardInfoMessage( - message: controller.error!, - icon: Icons.error_outline, - color: ShadcnTheme.destructive, - onClose: controller.clearError, - ) - : null, + filterSection: + controller.error != null + ? StandardInfoMessage( + message: controller.error!, + icon: Icons.error_outline, + color: ShadcnTheme.destructive, + onClose: controller.clearError, + ) + : null, // 데이터 테이블 - dataTable: displayCompanies.isEmpty - ? StandardEmptyState( - title: controller.searchKeyword.isNotEmpty - ? '검색 결과가 없습니다' - : '등록된 회사가 없습니다', - icon: Icons.business_outlined, - action: controller.searchKeyword.isEmpty - ? StandardActionButtons.addButton( - text: '첫 회사 추가하기', - onPressed: _navigateToAddScreen, - ) - : null, - ) - : std_table.StandardDataTable( - columns: [ - std_table.DataColumn(label: '번호', flex: 1), - std_table.DataColumn(label: '회사명', flex: 3), - std_table.DataColumn(label: '구분', flex: 2), - std_table.DataColumn(label: '유형', flex: 2), - std_table.DataColumn(label: '연락처', flex: 2), - std_table.DataColumn(label: '관리', flex: 2), - ], - rows: [ - ...pagedCompanies.asMap().entries.map((entry) { - final int index = startIndex + entry.key; - final companyData = entry.value; - final bool isBranch = companyData['isBranch'] as bool; - final Company company = - isBranch - ? _convertBranchToCompany(companyData['branch'] as Branch) - : companyData['company'] as Company; - final String? mainCompanyName = - companyData['mainCompanyName'] as String?; + dataTable: + displayCompanies.isEmpty + ? StandardEmptyState( + title: + controller.searchKeyword.isNotEmpty + ? '검색 결과가 없습니다' + : '등록된 회사가 없습니다', + icon: Icons.business_outlined, + action: + controller.searchKeyword.isEmpty + ? StandardActionButtons.addButton( + text: '첫 회사 추가하기', + onPressed: _navigateToAddScreen, + ) + : null, + ) + : std_table.StandardDataTable( + columns: [ + std_table.DataColumn(label: '번호', flex: 1), + std_table.DataColumn(label: '회사명', flex: 3), + std_table.DataColumn(label: '구분', flex: 1), + std_table.DataColumn(label: '유형', flex: 2), + std_table.DataColumn(label: '연락처', flex: 2), + std_table.DataColumn(label: '관리', flex: 2), + ], + rows: [ + ...pagedCompanies.asMap().entries.map((entry) { + final int index = startIndex + entry.key; + final companyData = entry.value; + final bool isBranch = companyData['isBranch'] as bool; + final Company company = + isBranch + ? _convertBranchToCompany( + companyData['branch'] as Branch, + ) + : companyData['company'] as Company; + final String? mainCompanyName = + companyData['mainCompanyName'] as String?; - return std_table.StandardDataRow( - index: index, - columns: [ - std_table.DataColumn(label: '번호', flex: 1), - std_table.DataColumn(label: '회사명', flex: 3), - std_table.DataColumn(label: '구분', flex: 2), - std_table.DataColumn(label: '유형', flex: 2), - std_table.DataColumn(label: '연락처', flex: 2), - std_table.DataColumn(label: '관리', flex: 2), - ], - cells: [ - // 번호 - Text( - '${index + 1}', - style: ShadcnTheme.bodySmall, - ), - // 회사명 - _buildCompanyNameText( - company, - isBranch, - mainCompanyName: mainCompanyName, - ), - // 구분 - _buildCompanyTypeLabel(isBranch), - // 유형 - _buildCompanyTypeChips( - company.companyTypes, - ), - // 연락처 - Text( - company.contactPhone ?? '-', - style: ShadcnTheme.bodySmall, - ), - // 관리 - Row( - mainAxisSize: MainAxisSize.min, - children: [ - if (!isBranch && - company.branches != null && - company.branches!.isNotEmpty) ...[ - ShadcnButton( - text: '지점', - onPressed: - () => _showBranchDialog(company), - variant: - ShadcnButtonVariant.ghost, - size: ShadcnButtonSize.small, - ), - const SizedBox( - width: ShadcnTheme.spacing1, + return std_table.StandardDataRow( + index: index, + columns: [ + std_table.DataColumn(label: '번호', flex: 1), + std_table.DataColumn(label: '회사명', flex: 3), + std_table.DataColumn(label: '구분', flex: 1), + std_table.DataColumn(label: '유형', flex: 2), + std_table.DataColumn(label: '연락처', flex: 2), + std_table.DataColumn(label: '관리', flex: 2), + ], + cells: [ + // 번호 + Text( + '${index + 1}', + style: ShadcnTheme.bodySmall, + ), + // 회사명 + _buildCompanyNameText( + company, + isBranch, + mainCompanyName: mainCompanyName, + ), + // 구분 + Align( + alignment: Alignment.centerLeft, + child: _buildCompanyTypeLabel(isBranch), + ), + // 유형 + Align( + alignment: Alignment.centerLeft, + child: _buildCompanyTypeChips(company.companyTypes), + ), + // 연락처 + Text( + company.contactPhone ?? '-', + style: ShadcnTheme.bodySmall, + ), + // 관리 + Row( + mainAxisSize: MainAxisSize.min, + children: [ + if (!isBranch && + company.branches != null && + company.branches!.isNotEmpty) ...[ + ShadcnButton( + text: '지점', + onPressed: + () => _showBranchDialog(company), + variant: ShadcnButtonVariant.ghost, + size: ShadcnButtonSize.small, + ), + const SizedBox(width: ShadcnTheme.spacing1), + ], + std_table.StandardActionButtons( + onEdit: + company.id != null + ? () { + if (isBranch) { + Navigator.pushNamed( + context, + '/company/edit', + arguments: { + 'companyId': + companyData['companyId'], + 'isBranch': true, + 'mainCompanyName': + mainCompanyName, + 'branchId': company.id, + }, + ).then((result) { + if (result == true) + controller.refresh(); + }); + } else { + Navigator.pushNamed( + context, + '/company/edit', + arguments: { + 'companyId': company.id, + 'isBranch': false, + }, + ).then((result) { + if (result == true) + controller.refresh(); + }); + } + } + : null, + onDelete: + (!isBranch && company.id != null) + ? () => _deleteCompany(company.id!) + : null, ), ], - std_table.StandardActionButtons( - onEdit: company.id != null - ? () { - if (isBranch) { - Navigator.pushNamed( - context, - '/company/edit', - arguments: { - 'companyId': companyData['companyId'], - 'isBranch': true, - 'mainCompanyName': mainCompanyName, - 'branchId': company.id, - }, - ).then((result) { - if (result == true) controller.refresh(); - }); - } else { - Navigator.pushNamed( - context, - '/company/edit', - arguments: { - 'companyId': company.id, - 'isBranch': false, - }, - ).then((result) { - if (result == true) controller.refresh(); - }); - } - } - : null, - onDelete: (!isBranch && company.id != null) - ? () => _deleteCompany(company.id!) - : null, - ), - ], - ), - ], - ); - }), - ], - ), + ), + ], + ); + }), + ], + ), // 페이지네이션 (항상 표시) pagination: Pagination( @@ -438,4 +482,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 0f89e473ded3dd3089882c6e70543b18f2266278..c4ba5d2e21b6c0a4507bcb8e67cfb91486b7ed17 100644 GIT binary patch delta 2430 zcma)7?N3u@6i<1TmbdlAQY!M4x8BZjq3$Jy7af^1VNpY}C4N|@xwaSSR@&NL+#1$7 zma@5l0d{Z$M;Syi#rXlfWYJ)v{sCkAxcEtz?2Gp{!GFN*Jonz-wrm*sp(UL2JLh+P z=jHh>{`Z7oZKK$51U8D#16$H;9SnxK5IGx(gvgnZINuiyb{pYwNuxMkvRDRQ6Jfoo zRW*`cGRTGEe9~hi`02`m@c*iVc+%uDTbA zfn?_H%D*m1(>Dm?SAEV5Mq54s<0&FcrY&XXlxD>HD!>Gchdy$ zu+3p0ut7Q-J>8mrlJHn02@Vn_faKrfV;6#c9tIez#uvd@a-Pk_p5ukSP)JRJW-!^E z4m}e(fAFCm-Drept&^Y^JIqBH#_u7$NQ(_~!@P$TB9t!U>4vbgp+;kyIEgkS3}?J` zZwC0YBj*NU!LY!%|4L8pD3Cm{l9{~GMOt__mWDZPuvuOP=biQNTX}gERajSfX428f z$d~5ir7I}&4b)eclkd3@2?h=mC^h$>$W1Gc3zua2Pee)_If{xL_zd zSp5OlF4_uCU!_k;bt2;bcvrcw4me?P8eA}BarJ6C(xv#1IO@1c(>V9aB-^nhuL zt63fOsrw{zC57rE2O?cwlhf;&)EbdfD_Al_y|(=d%iKz+78E4aMVeYh;kL!HC)q>; zBfnoPo$!gbdIwDA#~j9_VwC2dzC`#RcLx$TAh{C_Y2U^OZV2Y+u-n4wGIAgEkW9ixzy6V z)H89Z0Q{iUcCauly-uinW`0(>a|w?ck?+h&Ki^VRq_pS(ff}35hXWEG357^oTXxD> zeI3K5D}k zJDMSG^X>-a(^2$~y!p+$xkO%A%FJESRDk7!4Hxl}<>)Bq{ryB#JEdwMs>$^)W&0hwzyGk3d;lfjC8E6y(;13V(>_Vt6BsaGG)ADGlqabeJY&rga}Y zsWU;?UIsCH8+>mku#Uff+P!9!&8zdG>^<f{%@KZL*8r8uWUF z;EB_B5f?+*zmjCWS)Y5wjKf}N#sydBR59axR;cr-g+Pk!x}2Bh6sObs>#Ar9jd@ae z6gu(FnzV;rt s<2A$MeYJ48Jqgupv-k#EQKYEwQD@alWV_(&#Q>u3FbyC+_br$I3$~Gh`2YX_ delta 648 zcmZ8d&ubH55GBc`+WZhxQi;(dvyG5lX*SvwW6~z2+JaK;rJx6q^fUW4e2`?*-Hkse z0j(FYi0E93SRtTbrHA64L_x%h(2IEU=)r>|{tdd>4WV^T!_1ra-n?Jyu4nqr2M>Pq zthfO%%_T=~s%jNVO`Xm(7!<(Jnsm~K_q+-70rzrfSfeXVrcniu_k+77nyR07W!N$Z zOrKMjaYa?A2&+D@i`*v6fKpW@$_2I7AoaVm#2|vX6usubWH5|eJb7f9)kzh~&VGT^ zsD7F024>`=_~fv!WKRTfvQG*7ke0*$52YGu2m&)`O@u-bdd>Az-J(t10HVX9p|N^J zlnk|}B8b>di({D#bT?jgUT;8m{RMR1KZMTPtxkIrx@+6r=UbLE{yCJuujvTBi3h_y zHa!m~R;gq)I;qfB3Zv8U>|QUHHN|>%k!rVCnMxIE%v7uW6!!hp^5Mx;6u&1%y!ISJ zfe3EL3S2Kj^LNba!c)@+5kjMQHpCCL>>$jGiO&HWfq!Fyd3yYu+eST-MsQOaGrvi@ zp1~TwOuWN2IUn4UqD8$cr#ykaJr z1m|+cIjPxDa9%!!_j57a$vt)?3;CpDeknif9=I@{P6ZvRPlYr1V+t@_jGME?_JMy8 C%h5sr diff --git a/lib/screens/company/controllers/company_list_controller.dart b/lib/screens/company/controllers/company_list_controller.dart index a9a5473..cbeee14 100644 --- a/lib/screens/company/controllers/company_list_controller.dart +++ b/lib/screens/company/controllers/company_list_controller.dart @@ -66,23 +66,48 @@ class CompanyListController extends ChangeNotifier { try { if (_useApi) { - // API 호출 - print('[CompanyListController] Using API to fetch companies'); - final apiCompanies = await _companyService.getCompanies( - page: _currentPage, - perPage: _perPage, - search: searchKeyword.isNotEmpty ? searchKeyword : null, - isActive: _isActiveFilter, - ); - print('[CompanyListController] API returned ${apiCompanies.length} companies'); + // API 호출 - 지점 정보 포함 + print('[CompanyListController] Using API to fetch companies with branches'); - if (isRefresh) { - companies = apiCompanies; - } else { - companies.addAll(apiCompanies); + // 지점 정보를 포함한 전체 회사 목록 가져오기 + final apiCompaniesWithBranches = await _companyService.getCompaniesWithBranchesFlat(); + print('[CompanyListController] API returned ${apiCompaniesWithBranches.length} companies with branches'); + + // 지점 수 출력 + for (final company in apiCompaniesWithBranches) { + if (company.branches?.isNotEmpty ?? false) { + print('[CompanyListController] ${company.name} has ${company.branches?.length ?? 0} branches'); + } } - _hasMore = apiCompanies.length == _perPage; + // 검색어 필터 적용 (서버에서 필터링이 안 되므로 클라이언트에서 처리) + 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(); + } + + // 활성 상태 필터 적용 (현재 API에서 지원하지 않으므로 주석 처리) + // if (_isActiveFilter != null) { + // filteredApiCompanies = filteredApiCompanies.where((c) => c.isActive == _isActiveFilter).toList(); + // } + + // 페이지네이션 처리 (클라이언트 사이드) + final startIndex = (_currentPage - 1) * _perPage; + final endIndex = startIndex + _perPage; + final paginatedCompanies = filteredApiCompanies.skip(startIndex).take(_perPage).toList(); + + if (isRefresh) { + companies = paginatedCompanies; + } else { + companies.addAll(paginatedCompanies); + } + + _hasMore = endIndex < filteredApiCompanies.length; if (_hasMore) _currentPage++; } else { // Mock 데이터 사용 diff --git a/lib/services/company_service.dart b/lib/services/company_service.dart index a1ba2a3..97a2ca7 100644 --- a/lib/services/company_service.dart +++ b/lib/services/company_service.dart @@ -262,6 +262,58 @@ class CompanyService { throw ServerFailure(message: 'Failed to update company status: $e'); } } + + // 회사 목록과 지점 정보를 함께 조회 (플랫 데이터를 그룹화) + Future> getCompaniesWithBranchesFlat() async { + try { + final flatData = await _remoteDataSource.getCompanyBranchesFlat(); + + // 회사별로 데이터 그룹화 + final Map> companyBranchesMap = {}; + final Map companyNamesMap = {}; + + for (final item in flatData) { + companyNamesMap[item.companyId] = item.companyName; + + if (item.branchId != null && item.branchName != null) { + if (!companyBranchesMap.containsKey(item.companyId)) { + companyBranchesMap[item.companyId] = []; + } + companyBranchesMap[item.companyId]!.add( + Branch( + id: item.branchId, + companyId: item.companyId, + name: item.branchName!, + address: Address.fromFullAddress(''), // 주소 정보가 없으므로 빈 값 + ), + ); + } + } + + // Company 객체 생성 + final List companies = []; + for (final entry in companyNamesMap.entries) { + companies.add( + Company( + id: entry.key, + name: entry.value, + address: Address.fromFullAddress(''), // 주소 정보가 없으므로 빈 값 + companyTypes: [CompanyType.customer], // 기본값 + branches: companyBranchesMap[entry.key], // null 허용 + ), + ); + } + + return companies; + } on ApiException catch (e) { + debugPrint('[CompanyService] ApiException: ${e.message}'); + throw ServerFailure(message: e.message); + } catch (e, stackTrace) { + debugPrint('[CompanyService] Error loading companies with branches: $e'); + debugPrint('[CompanyService] Stack trace: $stackTrace'); + throw ServerFailure(message: 'Failed to fetch companies with branches: $e'); + } + } // 변환 헬퍼 메서드들 Company _convertListDtoToCompany(CompanyListDto dto) {