고객사 목록 쿼리스트링 연동 및 공통 JSON 파서 도입
This commit is contained in:
@@ -1,4 +1,5 @@
|
||||
import 'package:superport_v2/core/common/models/paginated_result.dart';
|
||||
import 'package:superport_v2/core/common/utils/json_utils.dart';
|
||||
|
||||
import '../../domain/entities/customer.dart';
|
||||
|
||||
@@ -93,16 +94,16 @@ class CustomerDto {
|
||||
);
|
||||
|
||||
static PaginatedResult<Customer> parsePaginated(Map<String, dynamic>? json) {
|
||||
final items = (json?['items'] as List<dynamic>? ?? [])
|
||||
.whereType<Map<String, dynamic>>()
|
||||
final rawItems = JsonUtils.extractList(json, keys: const ['items']);
|
||||
final items = rawItems
|
||||
.map(CustomerDto.fromJson)
|
||||
.map((dto) => dto.toEntity())
|
||||
.toList();
|
||||
.toList(growable: false);
|
||||
return PaginatedResult<Customer>(
|
||||
items: items,
|
||||
page: json?['page'] as int? ?? 1,
|
||||
pageSize: json?['page_size'] as int? ?? items.length,
|
||||
total: json?['total'] as int? ?? items.length,
|
||||
page: JsonUtils.readInt(json, 'page', fallback: 1),
|
||||
pageSize: JsonUtils.readInt(json, 'page_size', fallback: items.length),
|
||||
total: JsonUtils.readInt(json, 'total', fallback: items.length),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,6 +9,8 @@ enum CustomerTypeFilter { all, partner, general }
|
||||
enum CustomerStatusFilter { all, activeOnly, inactiveOnly }
|
||||
|
||||
class CustomerController extends ChangeNotifier {
|
||||
static const int defaultPageSize = 20;
|
||||
|
||||
CustomerController({required CustomerRepository repository})
|
||||
: _repository = repository;
|
||||
|
||||
@@ -20,6 +22,7 @@ class CustomerController extends ChangeNotifier {
|
||||
String _query = '';
|
||||
CustomerTypeFilter _typeFilter = CustomerTypeFilter.all;
|
||||
CustomerStatusFilter _statusFilter = CustomerStatusFilter.all;
|
||||
int _pageSize = defaultPageSize;
|
||||
String? _errorMessage;
|
||||
|
||||
PaginatedResult<Customer>? get result => _result;
|
||||
@@ -28,6 +31,7 @@ class CustomerController extends ChangeNotifier {
|
||||
String get query => _query;
|
||||
CustomerTypeFilter get typeFilter => _typeFilter;
|
||||
CustomerStatusFilter get statusFilter => _statusFilter;
|
||||
int get pageSize => _pageSize;
|
||||
String? get errorMessage => _errorMessage;
|
||||
|
||||
Future<void> fetch({int page = 1}) async {
|
||||
@@ -60,13 +64,16 @@ class CustomerController extends ChangeNotifier {
|
||||
|
||||
final response = await _repository.list(
|
||||
page: page,
|
||||
pageSize: _result?.pageSize ?? 20,
|
||||
pageSize: _pageSize,
|
||||
query: _query.isEmpty ? null : _query,
|
||||
isPartner: isPartner,
|
||||
isGeneral: isGeneral,
|
||||
isActive: isActive,
|
||||
);
|
||||
_result = response;
|
||||
if (response.pageSize > 0 && response.pageSize != _pageSize) {
|
||||
_pageSize = response.pageSize;
|
||||
}
|
||||
} catch (e) {
|
||||
_errorMessage = e.toString();
|
||||
} finally {
|
||||
@@ -76,20 +83,37 @@ class CustomerController extends ChangeNotifier {
|
||||
}
|
||||
|
||||
void updateQuery(String value) {
|
||||
if (_query == value) {
|
||||
return;
|
||||
}
|
||||
_query = value;
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
void updateTypeFilter(CustomerTypeFilter filter) {
|
||||
if (_typeFilter == filter) {
|
||||
return;
|
||||
}
|
||||
_typeFilter = filter;
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
void updateStatusFilter(CustomerStatusFilter filter) {
|
||||
if (_statusFilter == filter) {
|
||||
return;
|
||||
}
|
||||
_statusFilter = filter;
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
void updatePageSize(int size) {
|
||||
if (size <= 0 || _pageSize == size) {
|
||||
return;
|
||||
}
|
||||
_pageSize = size;
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
Future<Customer?> create(CustomerInput input) async {
|
||||
_setSubmitting(true);
|
||||
try {
|
||||
|
||||
@@ -1,10 +1,13 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get_it/get_it.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import 'package:shadcn_ui/shadcn_ui.dart';
|
||||
|
||||
import 'package:superport_v2/core/constants/app_sections.dart';
|
||||
import 'package:superport_v2/widgets/app_layout.dart';
|
||||
import 'package:superport_v2/widgets/components/filter_bar.dart';
|
||||
import 'package:superport_v2/features/util/postal_search/presentation/models/postal_search_result.dart';
|
||||
import 'package:superport_v2/features/util/postal_search/presentation/widgets/postal_search_dialog.dart';
|
||||
|
||||
import '../../../../../core/config/environment.dart';
|
||||
import '../../../../../widgets/spec_page.dart';
|
||||
@@ -13,7 +16,9 @@ import '../../domain/repositories/customer_repository.dart';
|
||||
import '../controllers/customer_controller.dart';
|
||||
|
||||
class CustomerPage extends StatelessWidget {
|
||||
const CustomerPage({super.key});
|
||||
const CustomerPage({super.key, required this.routeUri});
|
||||
|
||||
final Uri routeUri;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
@@ -76,12 +81,14 @@ class CustomerPage extends StatelessWidget {
|
||||
);
|
||||
}
|
||||
|
||||
return const _CustomerEnabledPage();
|
||||
return _CustomerEnabledPage(routeUri: routeUri);
|
||||
}
|
||||
}
|
||||
|
||||
class _CustomerEnabledPage extends StatefulWidget {
|
||||
const _CustomerEnabledPage();
|
||||
const _CustomerEnabledPage({required this.routeUri});
|
||||
|
||||
final Uri routeUri;
|
||||
|
||||
@override
|
||||
State<_CustomerEnabledPage> createState() => _CustomerEnabledPageState();
|
||||
@@ -92,15 +99,22 @@ class _CustomerEnabledPageState extends State<_CustomerEnabledPage> {
|
||||
final TextEditingController _searchController = TextEditingController();
|
||||
final FocusNode _searchFocus = FocusNode();
|
||||
String? _lastError;
|
||||
bool _routeApplied = false;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_controller = CustomerController(repository: GetIt.I<CustomerRepository>())
|
||||
..addListener(_handleControllerUpdate);
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
_controller.fetch();
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
void didChangeDependencies() {
|
||||
super.didChangeDependencies();
|
||||
if (!_routeApplied) {
|
||||
_routeApplied = true;
|
||||
_applyRouteParameters();
|
||||
}
|
||||
}
|
||||
|
||||
void _handleControllerUpdate() {
|
||||
@@ -114,6 +128,26 @@ class _CustomerEnabledPageState extends State<_CustomerEnabledPage> {
|
||||
}
|
||||
}
|
||||
|
||||
void _applyRouteParameters() {
|
||||
final params = widget.routeUri.queryParameters;
|
||||
final query = params['q'] ?? '';
|
||||
final type = _typeFromParam(params['type']);
|
||||
final status = _statusFromParam(params['status']);
|
||||
final pageSizeParam = int.tryParse(params['page_size'] ?? '');
|
||||
final pageParam = int.tryParse(params['page'] ?? '');
|
||||
|
||||
_searchController.text = query;
|
||||
_controller.updateQuery(query);
|
||||
_controller.updateTypeFilter(type);
|
||||
_controller.updateStatusFilter(status);
|
||||
if (pageSizeParam != null && pageSizeParam > 0) {
|
||||
_controller.updatePageSize(pageSizeParam);
|
||||
}
|
||||
|
||||
final page = pageParam != null && pageParam > 0 ? pageParam : 1;
|
||||
_controller.fetch(page: page);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_controller.removeListener(_handleControllerUpdate);
|
||||
@@ -141,7 +175,8 @@ class _CustomerEnabledPageState extends State<_CustomerEnabledPage> {
|
||||
? false
|
||||
: (result.page * result.pageSize) < result.total;
|
||||
|
||||
final showReset = _searchController.text.isNotEmpty ||
|
||||
final showReset =
|
||||
_searchController.text.isNotEmpty ||
|
||||
_controller.typeFilter != CustomerTypeFilter.all ||
|
||||
_controller.statusFilter != CustomerStatusFilter.all;
|
||||
|
||||
@@ -232,7 +267,12 @@ class _CustomerEnabledPageState extends State<_CustomerEnabledPage> {
|
||||
_controller.updateStatusFilter(
|
||||
CustomerStatusFilter.all,
|
||||
);
|
||||
_controller.fetch(page: 1);
|
||||
_updateRoute(
|
||||
page: 1,
|
||||
queryOverride: '',
|
||||
typeOverride: CustomerTypeFilter.all,
|
||||
statusOverride: CustomerStatusFilter.all,
|
||||
);
|
||||
},
|
||||
child: const Text('초기화'),
|
||||
),
|
||||
@@ -259,7 +299,7 @@ class _CustomerEnabledPageState extends State<_CustomerEnabledPage> {
|
||||
size: ShadButtonSize.sm,
|
||||
onPressed: _controller.isLoading || currentPage <= 1
|
||||
? null
|
||||
: () => _controller.fetch(page: currentPage - 1),
|
||||
: () => _goToPage(currentPage - 1),
|
||||
child: const Text('이전'),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
@@ -267,7 +307,7 @@ class _CustomerEnabledPageState extends State<_CustomerEnabledPage> {
|
||||
size: ShadButtonSize.sm,
|
||||
onPressed: _controller.isLoading || !hasNext
|
||||
? null
|
||||
: () => _controller.fetch(page: currentPage + 1),
|
||||
: () => _goToPage(currentPage + 1),
|
||||
child: const Text('다음'),
|
||||
),
|
||||
],
|
||||
@@ -280,26 +320,24 @@ class _CustomerEnabledPageState extends State<_CustomerEnabledPage> {
|
||||
child: Center(child: CircularProgressIndicator()),
|
||||
)
|
||||
: customers.isEmpty
|
||||
? Padding(
|
||||
padding: const EdgeInsets.all(32),
|
||||
child: Text(
|
||||
'조건에 맞는 고객사가 없습니다.',
|
||||
style: theme.textTheme.muted,
|
||||
),
|
||||
)
|
||||
: _CustomerTable(
|
||||
customers: customers,
|
||||
onEdit: _controller.isSubmitting
|
||||
? null
|
||||
: (customer) =>
|
||||
_openCustomerForm(context, customer: customer),
|
||||
onDelete: _controller.isSubmitting
|
||||
? null
|
||||
: _confirmDelete,
|
||||
onRestore: _controller.isSubmitting
|
||||
? null
|
||||
: _restoreCustomer,
|
||||
),
|
||||
? Padding(
|
||||
padding: const EdgeInsets.all(32),
|
||||
child: Text(
|
||||
'조건에 맞는 고객사가 없습니다.',
|
||||
style: theme.textTheme.muted,
|
||||
),
|
||||
)
|
||||
: _CustomerTable(
|
||||
customers: customers,
|
||||
onEdit: _controller.isSubmitting
|
||||
? null
|
||||
: (customer) =>
|
||||
_openCustomerForm(context, customer: customer),
|
||||
onDelete: _controller.isSubmitting ? null : _confirmDelete,
|
||||
onRestore: _controller.isSubmitting
|
||||
? null
|
||||
: _restoreCustomer,
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
@@ -307,8 +345,103 @@ class _CustomerEnabledPageState extends State<_CustomerEnabledPage> {
|
||||
}
|
||||
|
||||
void _applyFilters() {
|
||||
_controller.updateQuery(_searchController.text.trim());
|
||||
_controller.fetch(page: 1);
|
||||
final keyword = _searchController.text.trim();
|
||||
_controller.updateQuery(keyword);
|
||||
_updateRoute(page: 1, queryOverride: keyword);
|
||||
}
|
||||
|
||||
void _goToPage(int page) {
|
||||
if (page < 1) {
|
||||
page = 1;
|
||||
}
|
||||
_updateRoute(page: page);
|
||||
}
|
||||
|
||||
void _updateRoute({
|
||||
required int page,
|
||||
String? queryOverride,
|
||||
CustomerTypeFilter? typeOverride,
|
||||
CustomerStatusFilter? statusOverride,
|
||||
int? pageSizeOverride,
|
||||
}) {
|
||||
final query = queryOverride ?? _controller.query;
|
||||
final type = typeOverride ?? _controller.typeFilter;
|
||||
final status = statusOverride ?? _controller.statusFilter;
|
||||
final pageSize = pageSizeOverride ?? _controller.pageSize;
|
||||
|
||||
final params = <String, String>{};
|
||||
if (query.isNotEmpty) {
|
||||
params['q'] = query;
|
||||
}
|
||||
final typeParam = _encodeType(type);
|
||||
if (typeParam != null) {
|
||||
params['type'] = typeParam;
|
||||
}
|
||||
final statusParam = _encodeStatus(status);
|
||||
if (statusParam != null) {
|
||||
params['status'] = statusParam;
|
||||
}
|
||||
if (page > 1) {
|
||||
params['page'] = page.toString();
|
||||
}
|
||||
if (pageSize > 0 && pageSize != CustomerController.defaultPageSize) {
|
||||
params['page_size'] = pageSize.toString();
|
||||
}
|
||||
|
||||
final uri = Uri(
|
||||
path: widget.routeUri.path,
|
||||
queryParameters: params.isEmpty ? null : params,
|
||||
);
|
||||
final newLocation = uri.toString();
|
||||
if (widget.routeUri.toString() == newLocation) {
|
||||
_controller.fetch(page: page);
|
||||
return;
|
||||
}
|
||||
GoRouter.of(context).go(newLocation);
|
||||
}
|
||||
|
||||
CustomerTypeFilter _typeFromParam(String? value) {
|
||||
switch (value) {
|
||||
case 'partner':
|
||||
return CustomerTypeFilter.partner;
|
||||
case 'general':
|
||||
return CustomerTypeFilter.general;
|
||||
default:
|
||||
return CustomerTypeFilter.all;
|
||||
}
|
||||
}
|
||||
|
||||
String? _encodeType(CustomerTypeFilter filter) {
|
||||
switch (filter) {
|
||||
case CustomerTypeFilter.all:
|
||||
return null;
|
||||
case CustomerTypeFilter.partner:
|
||||
return 'partner';
|
||||
case CustomerTypeFilter.general:
|
||||
return 'general';
|
||||
}
|
||||
}
|
||||
|
||||
CustomerStatusFilter _statusFromParam(String? value) {
|
||||
switch (value) {
|
||||
case 'active':
|
||||
return CustomerStatusFilter.activeOnly;
|
||||
case 'inactive':
|
||||
return CustomerStatusFilter.inactiveOnly;
|
||||
default:
|
||||
return CustomerStatusFilter.all;
|
||||
}
|
||||
}
|
||||
|
||||
String? _encodeStatus(CustomerStatusFilter filter) {
|
||||
switch (filter) {
|
||||
case CustomerStatusFilter.all:
|
||||
return null;
|
||||
case CustomerStatusFilter.activeOnly:
|
||||
return 'active';
|
||||
case CustomerStatusFilter.inactiveOnly:
|
||||
return 'inactive';
|
||||
}
|
||||
}
|
||||
|
||||
String _typeLabel(CustomerTypeFilter filter) {
|
||||
@@ -364,6 +497,17 @@ class _CustomerEnabledPageState extends State<_CustomerEnabledPage> {
|
||||
text: existing?.addressDetail ?? '',
|
||||
);
|
||||
final noteController = TextEditingController(text: existing?.note ?? '');
|
||||
final existingZipcode = existing?.zipcode;
|
||||
final selectedPostalNotifier = ValueNotifier<PostalSearchResult?>(
|
||||
existingZipcode == null
|
||||
? null
|
||||
: PostalSearchResult(
|
||||
zipcode: existingZipcode.zipcode,
|
||||
sido: existingZipcode.sido,
|
||||
sigungu: existingZipcode.sigungu,
|
||||
roadName: existingZipcode.roadName,
|
||||
),
|
||||
);
|
||||
final partnerNotifier = ValueNotifier<bool>(existing?.isPartner ?? false);
|
||||
final generalNotifier = ValueNotifier<bool>(existing?.isGeneral ?? true);
|
||||
final isActiveNotifier = ValueNotifier<bool>(existing?.isActive ?? true);
|
||||
@@ -378,6 +522,30 @@ class _CustomerEnabledPageState extends State<_CustomerEnabledPage> {
|
||||
final theme = ShadTheme.of(dialogContext);
|
||||
final materialTheme = Theme.of(dialogContext);
|
||||
final navigator = Navigator.of(dialogContext);
|
||||
Future<void> openPostalSearch() async {
|
||||
final keyword = zipcodeController.text.trim();
|
||||
final result = await showPostalSearchDialog(
|
||||
dialogContext,
|
||||
initialKeyword: keyword.isEmpty ? null : keyword,
|
||||
);
|
||||
if (result == null) {
|
||||
return;
|
||||
}
|
||||
zipcodeController
|
||||
..text = result.zipcode
|
||||
..selection = TextSelection.collapsed(
|
||||
offset: result.zipcode.length,
|
||||
);
|
||||
selectedPostalNotifier.value = result;
|
||||
if (result.fullAddress.isNotEmpty) {
|
||||
addressController
|
||||
..text = result.fullAddress
|
||||
..selection = TextSelection.collapsed(
|
||||
offset: addressController.text.length,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return Dialog(
|
||||
insetPadding: const EdgeInsets.all(24),
|
||||
clipBehavior: Clip.antiAlias,
|
||||
@@ -648,9 +816,49 @@ class _CustomerEnabledPageState extends State<_CustomerEnabledPage> {
|
||||
const SizedBox(height: 16),
|
||||
_FormField(
|
||||
label: '우편번호',
|
||||
child: ShadInput(
|
||||
controller: zipcodeController,
|
||||
placeholder: const Text('예: 06000'),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: ShadInput(
|
||||
controller: zipcodeController,
|
||||
placeholder: const Text('예: 06000'),
|
||||
keyboardType: TextInputType.number,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
ShadButton.outline(
|
||||
onPressed: saving.value
|
||||
? null
|
||||
: openPostalSearch,
|
||||
child: const Text('검색'),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
ValueListenableBuilder<PostalSearchResult?>(
|
||||
valueListenable: selectedPostalNotifier,
|
||||
builder: (_, selection, __) {
|
||||
if (selection == null) {
|
||||
return Text(
|
||||
'검색 버튼을 눌러 주소를 선택하세요.',
|
||||
style: theme.textTheme.small.copyWith(
|
||||
color: theme.colorScheme.mutedForeground,
|
||||
),
|
||||
);
|
||||
}
|
||||
final fullAddress = selection.fullAddress;
|
||||
return Text(
|
||||
fullAddress.isEmpty
|
||||
? '선택한 우편번호에 주소 정보가 없습니다.'
|
||||
: fullAddress,
|
||||
style: theme.textTheme.small,
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
@@ -704,6 +912,7 @@ class _CustomerEnabledPageState extends State<_CustomerEnabledPage> {
|
||||
zipcodeController.dispose();
|
||||
addressController.dispose();
|
||||
noteController.dispose();
|
||||
selectedPostalNotifier.dispose();
|
||||
partnerNotifier.dispose();
|
||||
generalNotifier.dispose();
|
||||
isActiveNotifier.dispose();
|
||||
|
||||
Reference in New Issue
Block a user