고객사 목록 쿼리스트링 연동 및 공통 JSON 파서 도입

This commit is contained in:
JiWoong Sul
2025-09-25 20:13:46 +09:00
parent 8a6ad1e81b
commit 900990c46b
27 changed files with 1458 additions and 176 deletions

View File

@@ -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),
);
}
}

View File

@@ -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 {

View File

@@ -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();

View File

@@ -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/group.dart';
@@ -52,16 +53,16 @@ class GroupDto {
);
static PaginatedResult<Group> 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(GroupDto.fromJson)
.map((dto) => dto.toEntity())
.toList();
.toList(growable: false);
return PaginatedResult<Group>(
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),
);
}
}

View File

@@ -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/menu.dart';
@@ -64,16 +65,16 @@ class MenuDto {
);
static PaginatedResult<MenuItem> 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(MenuDto.fromJson)
.map((dto) => dto.toEntity())
.toList();
.toList(growable: false);
return PaginatedResult<MenuItem>(
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),
);
}
}

View File

@@ -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/product.dart';
@@ -96,16 +97,16 @@ class ProductDto {
);
static PaginatedResult<Product> 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(ProductDto.fromJson)
.map((dto) => dto.toEntity())
.toList();
.toList(growable: false);
return PaginatedResult<Product>(
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),
);
}
}

View File

@@ -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/uom.dart';
@@ -48,16 +49,16 @@ class UomDto {
);
static PaginatedResult<Uom> 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(UomDto.fromJson)
.map((dto) => dto.toEntity())
.toList();
.toList(growable: false);
return PaginatedResult<Uom>(
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),
);
}
}

View File

@@ -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/user.dart';
@@ -64,16 +65,16 @@ class UserDto {
static PaginatedResult<UserAccount> 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(UserDto.fromJson)
.map((dto) => dto.toEntity())
.toList();
.toList(growable: false);
return PaginatedResult<UserAccount>(
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),
);
}
}

View File

@@ -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/vendor.dart';
@@ -73,16 +74,16 @@ class VendorDto {
);
static PaginatedResult<Vendor> 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(VendorDto.fromJson)
.map((dto) => dto.toEntity())
.toList();
.toList(growable: false);
return PaginatedResult<Vendor>(
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),
);
}
}

View File

@@ -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/warehouse.dart';
@@ -75,16 +76,16 @@ class WarehouseDto {
);
static PaginatedResult<Warehouse> 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(WarehouseDto.fromJson)
.map((dto) => dto.toEntity())
.toList();
.toList(growable: false);
return PaginatedResult<Warehouse>(
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),
);
}
}

View File

@@ -5,6 +5,8 @@ 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';
@@ -137,7 +139,8 @@ class _WarehouseEnabledPageState extends State<_WarehouseEnabledPage> {
? false
: (result.page * result.pageSize) < result.total;
final showReset = _searchController.text.isNotEmpty ||
final showReset =
_searchController.text.isNotEmpty ||
_controller.statusFilter != WarehouseStatusFilter.all;
return AppLayout(
@@ -253,27 +256,25 @@ class _WarehouseEnabledPageState extends State<_WarehouseEnabledPage> {
child: Center(child: CircularProgressIndicator()),
)
: warehouses.isEmpty
? Padding(
padding: const EdgeInsets.all(32),
child: Text(
'조건에 맞는 창고가 없습니다.',
style: theme.textTheme.muted,
),
)
: _WarehouseTable(
warehouses: warehouses,
dateFormat: _dateFormat,
onEdit: _controller.isSubmitting
? null
: (warehouse) =>
_openWarehouseForm(context, warehouse: warehouse),
onDelete: _controller.isSubmitting
? null
: _confirmDelete,
onRestore: _controller.isSubmitting
? null
: _restoreWarehouse,
),
? Padding(
padding: const EdgeInsets.all(32),
child: Text(
'조건에 맞는 창고가 없습니다.',
style: theme.textTheme.muted,
),
)
: _WarehouseTable(
warehouses: warehouses,
dateFormat: _dateFormat,
onEdit: _controller.isSubmitting
? null
: (warehouse) =>
_openWarehouseForm(context, warehouse: warehouse),
onDelete: _controller.isSubmitting ? null : _confirmDelete,
onRestore: _controller.isSubmitting
? null
: _restoreWarehouse,
),
),
);
},
@@ -321,6 +322,17 @@ class _WarehouseEnabledPageState extends State<_WarehouseEnabledPage> {
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 isActiveNotifier = ValueNotifier<bool>(existing?.isActive ?? true);
final saving = ValueNotifier<bool>(false);
final codeError = ValueNotifier<String?>(null);
@@ -332,6 +344,30 @@ class _WarehouseEnabledPageState extends State<_WarehouseEnabledPage> {
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,
@@ -488,9 +524,49 @@ class _WarehouseEnabledPageState extends State<_WarehouseEnabledPage> {
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),
@@ -554,6 +630,7 @@ class _WarehouseEnabledPageState extends State<_WarehouseEnabledPage> {
zipcodeController.dispose();
addressController.dispose();
noteController.dispose();
selectedPostalNotifier.dispose();
isActiveNotifier.dispose();
saving.dispose();
codeError.dispose();
@@ -566,6 +643,7 @@ class _WarehouseEnabledPageState extends State<_WarehouseEnabledPage> {
zipcodeController.dispose();
addressController.dispose();
noteController.dispose();
selectedPostalNotifier.dispose();
isActiveNotifier.dispose();
saving.dispose();
codeError.dispose();