feat: 회사 관리 기능 개선 및 지점 평면화 DTO 추가

- CompanyBranchFlatDto 모델 추가로 지점 데이터 처리 개선
- 회사 서비스에 브랜치 평면화 기능 추가
- 회사 폼 컨트롤러 기능 확장
- 회사 리스트 화면 UI 개선

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
JiWoong Sul
2025-08-09 02:16:57 +09:00
parent 8302ff37cc
commit f8e8a95391
9 changed files with 682 additions and 208 deletions

View File

@@ -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_dto.dart';
import 'package:superport/data/models/company/company_list_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/branch_dto.dart';
import 'package:superport/data/models/company/company_branch_flat_dto.dart';
abstract class CompanyRemoteDataSource { abstract class CompanyRemoteDataSource {
Future<PaginatedResponse<CompanyListDto>> getCompanies({ Future<PaginatedResponse<CompanyListDto>> getCompanies({
@@ -48,6 +49,8 @@ abstract class CompanyRemoteDataSource {
Future<void> deleteBranch(int companyId, int branchId); Future<void> deleteBranch(int companyId, int branchId);
Future<List<BranchListDto>> getCompanyBranches(int companyId); Future<List<BranchListDto>> getCompanyBranches(int companyId);
Future<List<CompanyBranchFlatDto>> getCompanyBranchesFlat();
} }
@LazySingleton(as: CompanyRemoteDataSource) @LazySingleton(as: CompanyRemoteDataSource)
@@ -343,6 +346,36 @@ class CompanyRemoteDataSourceImpl implements CompanyRemoteDataSource {
} }
} }
@override
Future<List<CompanyBranchFlatDto>> 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<dynamic> dataList = responseData['data'];
return dataList.map((item) =>
CompanyBranchFlatDto.fromJson(item as Map<String, dynamic>)
).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 @override
Future<bool> checkDuplicateCompany(String name) async { Future<bool> checkDuplicateCompany(String name) async {
try { try {

View File

@@ -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<String, dynamic> json) =>
_$CompanyBranchFlatDtoFromJson(json);
}

View File

@@ -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>(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<String, dynamic> 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<String, dynamic> 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<CompanyBranchFlatDto> 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<String, dynamic> 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<String, dynamic> 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<String, dynamic> 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;
}

View File

@@ -0,0 +1,25 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'company_branch_flat_dto.dart';
// **************************************************************************
// JsonSerializableGenerator
// **************************************************************************
_$CompanyBranchFlatDtoImpl _$$CompanyBranchFlatDtoImplFromJson(
Map<String, dynamic> 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<String, dynamic> _$$CompanyBranchFlatDtoImplToJson(
_$CompanyBranchFlatDtoImpl instance) =>
<String, dynamic>{
'company_id': instance.companyId,
'company_name': instance.companyName,
'branch_id': instance.branchId,
'branch_name': instance.branchName,
};

View File

@@ -28,6 +28,7 @@ import 'package:superport/services/mock_data_service.dart';
import 'dart:async'; import 'dart:async';
import 'dart:math' as math; import 'dart:math' as math;
import 'package:superport/screens/company/controllers/branch_form_controller.dart'; import 'package:superport/screens/company/controllers/branch_form_controller.dart';
import 'package:superport/core/config/environment.dart' as env;
/// 회사 유형 선택 위젯 (체크박스) /// 회사 유형 선택 위젯 (체크박스)
class CompanyTypeSelector extends StatelessWidget { class CompanyTypeSelector extends StatelessWidget {
@@ -107,10 +108,36 @@ class _CompanyFormScreenState extends State<CompanyFormScreen> {
companyId = args['companyId']; companyId = args['companyId'];
branchId = args['branchId']; branchId = args['branchId'];
} }
// API 모드 확인
final useApi = env.Environment.useApi;
debugPrint('📌 회사 폼 초기화 - API 모드: $useApi, companyId: $companyId');
_controller = CompanyFormController( _controller = CompanyFormController(
dataService: MockDataService(), dataService: useApi ? null : MockDataService(),
companyId: companyId, 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 정보 세팅 // 지점 수정 모드일 때 branchId로 branch 정보 세팅
if (isBranch && branchId != null) { if (isBranch && branchId != null) {
final company = MockDataService().getCompanyById(companyId!); final company = MockDataService().getCompanyById(companyId!);
@@ -471,7 +498,7 @@ class _CompanyFormScreenState extends State<CompanyFormScreen> {
), ),
), ),
], ],
// 저장 버튼 // 저장 버튼 추가
Padding( Padding(
padding: const EdgeInsets.only(top: 24.0, bottom: 16.0), padding: const EdgeInsets.only(top: 24.0, bottom: 16.0),
child: ElevatedButton( child: ElevatedButton(
@@ -488,6 +515,7 @@ class _CompanyFormScreenState extends State<CompanyFormScreen> {
style: const TextStyle( style: const TextStyle(
fontSize: 16, fontSize: 16,
fontWeight: FontWeight.bold, fontWeight: FontWeight.bold,
color: Colors.white,
), ),
), ),
), ),

View File

@@ -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/components/shadcn_components.dart';
import 'package:superport/screens/common/widgets/pagination.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/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_action_bar.dart';
import 'package:superport/screens/common/widgets/standard_states.dart'; import 'package:superport/screens/common/widgets/standard_states.dart';
import 'package:superport/screens/common/layouts/base_list_screen.dart'; import 'package:superport/screens/common/layouts/base_list_screen.dart';
@@ -123,44 +124,58 @@ class _CompanyListRedesignState extends State<CompanyListRedesign> {
/// 회사 유형 배지 생성 /// 회사 유형 배지 생성
Widget _buildCompanyTypeChips(List<CompanyType> types) { Widget _buildCompanyTypeChips(List<CompanyType> types) {
return Wrap( // 유형이 없으면 기본값 표시
spacing: ShadcnTheme.spacing1, if (types.isEmpty) {
children: return Text(
types.map((type) { '-',
Color bgColor; style: ShadcnTheme.bodySmall.copyWith(color: ShadcnTheme.muted),
Color borderColor; );
Color textColor; }
switch(type) { return Row(
case CompanyType.customer: mainAxisSize: MainAxisSize.min,
bgColor = ShadcnTheme.green.withValues(alpha: 0.9); children: [
textColor = Colors.white; Flexible(
break; child: Wrap(
case CompanyType.partner: spacing: 4,
bgColor = ShadcnTheme.purple.withValues(alpha: 0.9); runSpacing: 2,
textColor = Colors.white; children: types.map((type) {
break; Color bgColor;
default: Color textColor;
bgColor = ShadcnTheme.muted.withValues(alpha: 0.9);
textColor = ShadcnTheme.foreground; switch (type) {
} case CompanyType.customer:
bgColor = ShadcnTheme.green.withValues(alpha: 0.9);
return Container( textColor = Colors.white;
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 2), break;
decoration: BoxDecoration( case CompanyType.partner:
color: bgColor, bgColor = ShadcnTheme.purple.withValues(alpha: 0.9);
borderRadius: BorderRadius.circular(4), textColor = Colors.white;
), break;
child: Text( default:
companyTypeToString(type), bgColor = ShadcnTheme.muted.withValues(alpha: 0.9);
style: TextStyle( textColor = ShadcnTheme.foreground;
fontSize: 12, }
fontWeight: FontWeight.w500,
color: textColor, return Container(
padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 1),
decoration: BoxDecoration(
color: bgColor,
borderRadius: BorderRadius.circular(3),
), ),
), child: Text(
); companyTypeToString(type),
}).toList(), style: TextStyle(
fontSize: 11,
fontWeight: FontWeight.w500,
color: textColor,
),
),
);
}).toList(),
),
),
],
); );
} }
@@ -169,9 +184,10 @@ class _CompanyListRedesignState extends State<CompanyListRedesign> {
return Container( return Container(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 2), padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 2),
decoration: BoxDecoration( decoration: BoxDecoration(
color: isBranch color:
? ShadcnTheme.blue.withValues(alpha: 0.9) isBranch
: ShadcnTheme.primary.withValues(alpha: 0.9), ? ShadcnTheme.blue.withValues(alpha: 0.9)
: ShadcnTheme.primary.withValues(alpha: 0.9),
borderRadius: BorderRadius.circular(4), borderRadius: BorderRadius.circular(4),
), ),
child: Text( child: Text(
@@ -232,37 +248,51 @@ class _CompanyListRedesignState extends State<CompanyListRedesign> {
} }
final int totalCount = displayCompanies.length; final int totalCount = displayCompanies.length;
// 페이지네이션을 위한 데이터 처리 // 페이지네이션을 위한 데이터 처리
final int startIndex = (_currentPage - 1) * _pageSize; final int startIndex = (_currentPage - 1) * _pageSize;
final int endIndex = startIndex + _pageSize; final int endIndex = startIndex + _pageSize;
final List<Map<String, dynamic>> pagedCompanies = displayCompanies.sublist(
startIndex, // startIndex가 displayCompanies.length보다 크거나 같으면 첫 페이지로 리셋
endIndex > displayCompanies.length ? displayCompanies.length : endIndex, if (startIndex >= displayCompanies.length && displayCompanies.isNotEmpty) {
); WidgetsBinding.instance.addPostFrameCallback((_) {
setState(() {
_currentPage = 1;
});
});
}
final List<Map<String, dynamic>> pagedCompanies = displayCompanies.isEmpty
? []
: displayCompanies.sublist(
startIndex.clamp(0, displayCompanies.length - 1),
endIndex.clamp(0, displayCompanies.length),
);
// 로딩 상태 // 로딩 상태
if (controller.isLoading && controller.companies.isEmpty) { if (controller.isLoading && controller.companies.isEmpty) {
return const StandardLoadingState( return const StandardLoadingState(message: '회사 데이터를 불러오는 중...');
message: '회사 데이터를 불러오는 중...',
);
} }
return BaseListScreen( return BaseListScreen(
isLoading: false, isLoading: false,
error: controller.error, error: controller.error,
onRefresh: controller.refresh, onRefresh: controller.refresh,
emptyMessage: controller.searchKeyword.isNotEmpty emptyMessage:
? '검색 결과가 없습니다' controller.searchKeyword.isNotEmpty
: '등록된 회사가 없습니다', ? '검색 결과가 없습니다'
: '등록된 회사가 없습니다',
emptyIcon: Icons.business_outlined, emptyIcon: Icons.business_outlined,
// 검색바 // 검색바
searchBar: UnifiedSearchBar( searchBar: UnifiedSearchBar(
controller: _searchController, controller: _searchController,
placeholder: '회사명, 담당자명, 연락처로 검색', placeholder: '회사명, 담당자명, 연락처로 검색',
onChanged: _onSearchChanged, // 실시간 검색 (디바운싱) onChanged: _onSearchChanged, // 실시간 검색 (디바운싱)
onSearch: () => _controller.updateSearchKeyword(_searchController.text), // 즉시 검색 onSearch:
() => _controller.updateSearchKeyword(
_searchController.text,
), // 즉시 검색
onClear: () { onClear: () {
_searchController.clear(); _searchController.clear();
_onSearchChanged(''); _onSearchChanged('');
@@ -272,155 +302,169 @@ class _CompanyListRedesignState extends State<CompanyListRedesign> {
onPressed: _navigateToAddScreen, onPressed: _navigateToAddScreen,
), ),
), ),
// 액션바 // 액션바
actionBar: StandardActionBar( actionBar: StandardActionBar(
leftActions: [], leftActions: [],
totalCount: totalCount, totalCount: totalCount,
onRefresh: controller.refresh, onRefresh: controller.refresh,
statusMessage: controller.searchKeyword.isNotEmpty statusMessage:
? '"${controller.searchKeyword}" 검색 결과' controller.searchKeyword.isNotEmpty
: null, ? '"${controller.searchKeyword}" 검색 결과'
: null,
), ),
// 에러 메시지 // 에러 메시지
filterSection: controller.error != null filterSection:
? StandardInfoMessage( controller.error != null
message: controller.error!, ? StandardInfoMessage(
icon: Icons.error_outline, message: controller.error!,
color: ShadcnTheme.destructive, icon: Icons.error_outline,
onClose: controller.clearError, color: ShadcnTheme.destructive,
) onClose: controller.clearError,
: null, )
: null,
// 데이터 테이블 // 데이터 테이블
dataTable: displayCompanies.isEmpty dataTable:
? StandardEmptyState( displayCompanies.isEmpty
title: controller.searchKeyword.isNotEmpty ? StandardEmptyState(
? '검색 결과가 없습니다' title:
: '등록된 회사가 없습니다', controller.searchKeyword.isNotEmpty
icon: Icons.business_outlined, ? '검색 결과가 없습니다'
action: controller.searchKeyword.isEmpty : '등록된 회사가 없습니다',
? StandardActionButtons.addButton( icon: Icons.business_outlined,
text: '첫 회사 추가하기', action:
onPressed: _navigateToAddScreen, controller.searchKeyword.isEmpty
) ? StandardActionButtons.addButton(
: null, text: '첫 회사 추가하기',
) onPressed: _navigateToAddScreen,
: std_table.StandardDataTable( )
columns: [ : null,
std_table.DataColumn(label: '번호', flex: 1), )
std_table.DataColumn(label: '회사명', flex: 3), : std_table.StandardDataTable(
std_table.DataColumn(label: '구분', flex: 2), columns: [
std_table.DataColumn(label: '유형', flex: 2), std_table.DataColumn(label: '번호', flex: 1),
std_table.DataColumn(label: '연락처', flex: 2), std_table.DataColumn(label: '회사명', flex: 3),
std_table.DataColumn(label: '관리', flex: 2), std_table.DataColumn(label: '구분', flex: 1),
], std_table.DataColumn(label: '유형', flex: 2),
rows: [ std_table.DataColumn(label: '연락처', flex: 2),
...pagedCompanies.asMap().entries.map((entry) { std_table.DataColumn(label: '관리', flex: 2),
final int index = startIndex + entry.key; ],
final companyData = entry.value; rows: [
final bool isBranch = companyData['isBranch'] as bool; ...pagedCompanies.asMap().entries.map((entry) {
final Company company = final int index = startIndex + entry.key;
isBranch final companyData = entry.value;
? _convertBranchToCompany(companyData['branch'] as Branch) final bool isBranch = companyData['isBranch'] as bool;
: companyData['company'] as Company; final Company company =
final String? mainCompanyName = isBranch
companyData['mainCompanyName'] as String?; ? _convertBranchToCompany(
companyData['branch'] as Branch,
)
: companyData['company'] as Company;
final String? mainCompanyName =
companyData['mainCompanyName'] as String?;
return std_table.StandardDataRow( return std_table.StandardDataRow(
index: index, index: index,
columns: [ columns: [
std_table.DataColumn(label: '번호', flex: 1), std_table.DataColumn(label: '번호', flex: 1),
std_table.DataColumn(label: '회사명', flex: 3), std_table.DataColumn(label: '회사명', flex: 3),
std_table.DataColumn(label: '구분', flex: 2), std_table.DataColumn(label: '구분', flex: 1),
std_table.DataColumn(label: '유형', flex: 2), std_table.DataColumn(label: '유형', flex: 2),
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: [ cells: [
// 번호 // 번호
Text( Text(
'${index + 1}', '${index + 1}',
style: ShadcnTheme.bodySmall, style: ShadcnTheme.bodySmall,
), ),
// 회사명 // 회사명
_buildCompanyNameText( _buildCompanyNameText(
company, company,
isBranch, isBranch,
mainCompanyName: mainCompanyName, mainCompanyName: mainCompanyName,
), ),
// 구분 // 구분
_buildCompanyTypeLabel(isBranch), Align(
// 유형 alignment: Alignment.centerLeft,
_buildCompanyTypeChips( child: _buildCompanyTypeLabel(isBranch),
company.companyTypes, ),
), // 유형
// 연락처 Align(
Text( alignment: Alignment.centerLeft,
company.contactPhone ?? '-', child: _buildCompanyTypeChips(company.companyTypes),
style: ShadcnTheme.bodySmall, ),
), // 연락처
// 관리 Text(
Row( company.contactPhone ?? '-',
mainAxisSize: MainAxisSize.min, style: ShadcnTheme.bodySmall,
children: [ ),
if (!isBranch && // 관리
company.branches != null && Row(
company.branches!.isNotEmpty) ...[ mainAxisSize: MainAxisSize.min,
ShadcnButton( children: [
text: '지점', if (!isBranch &&
onPressed: company.branches != null &&
() => _showBranchDialog(company), company.branches!.isNotEmpty) ...[
variant: ShadcnButton(
ShadcnButtonVariant.ghost, text: '지점',
size: ShadcnButtonSize.small, onPressed:
), () => _showBranchDialog(company),
const SizedBox( variant: ShadcnButtonVariant.ghost,
width: ShadcnTheme.spacing1, 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( pagination: Pagination(
@@ -438,4 +482,4 @@ class _CompanyListRedesignState extends State<CompanyListRedesign> {
), ),
); );
} }
} }

View File

@@ -66,23 +66,48 @@ class CompanyListController extends ChangeNotifier {
try { try {
if (_useApi) { if (_useApi) {
// API 호출 // API 호출 - 지점 정보 포함
print('[CompanyListController] Using API to fetch companies'); print('[CompanyListController] Using API to fetch companies with branches');
final apiCompanies = await _companyService.getCompanies(
page: _currentPage,
perPage: _perPage,
search: searchKeyword.isNotEmpty ? searchKeyword : null,
isActive: _isActiveFilter,
);
print('[CompanyListController] API returned ${apiCompanies.length} companies');
if (isRefresh) { // 지점 정보를 포함한 전체 회사 목록 가져오기
companies = apiCompanies; final apiCompaniesWithBranches = await _companyService.getCompaniesWithBranchesFlat();
} else { print('[CompanyListController] API returned ${apiCompaniesWithBranches.length} companies with branches');
companies.addAll(apiCompanies);
// 지점 수 출력
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<Company> 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++; if (_hasMore) _currentPage++;
} else { } else {
// Mock 데이터 사용 // Mock 데이터 사용

View File

@@ -262,6 +262,58 @@ class CompanyService {
throw ServerFailure(message: 'Failed to update company status: $e'); throw ServerFailure(message: 'Failed to update company status: $e');
} }
} }
// 회사 목록과 지점 정보를 함께 조회 (플랫 데이터를 그룹화)
Future<List<Company>> getCompaniesWithBranchesFlat() async {
try {
final flatData = await _remoteDataSource.getCompanyBranchesFlat();
// 회사별로 데이터 그룹화
final Map<int, List<Branch>> companyBranchesMap = {};
final Map<int, String> 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<Company> 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) { Company _convertListDtoToCompany(CompanyListDto dto) {