결재 및 마스터 모듈을 v4 API 계약에 맞게 조정
This commit is contained in:
@@ -18,6 +18,8 @@ import '../../../vendor/domain/repositories/vendor_repository.dart';
|
||||
import '../../domain/entities/product.dart';
|
||||
import '../../domain/repositories/product_repository.dart';
|
||||
import '../controllers/product_controller.dart';
|
||||
import '../widgets/uom_autocomplete_field.dart';
|
||||
import '../widgets/vendor_autocomplete_field.dart';
|
||||
|
||||
/// 제품 관리 페이지. 기능 플래그에 따라 사양/실제 화면을 전환한다.
|
||||
class ProductPage extends StatelessWidget {
|
||||
@@ -686,42 +688,27 @@ class _ProductEnabledPageState extends State<_ProductEnabledPage> {
|
||||
return ValueListenableBuilder<String?>(
|
||||
valueListenable: vendorError,
|
||||
builder: (_, errorText, __) {
|
||||
final vendorLabel = () {
|
||||
final vendor = existing?.vendor;
|
||||
if (vendor == null) {
|
||||
return null;
|
||||
}
|
||||
return '${vendor.vendorName} (${vendor.vendorCode})';
|
||||
}();
|
||||
return _FormField(
|
||||
label: '제조사',
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
ShadSelect<int?>(
|
||||
initialValue: value,
|
||||
onChanged: saving.value
|
||||
? null
|
||||
: (next) {
|
||||
vendorNotifier.value = next;
|
||||
vendorError.value = null;
|
||||
},
|
||||
options: _controller.vendorOptions
|
||||
.map(
|
||||
(vendor) => ShadOption<int?>(
|
||||
value: vendor.id,
|
||||
child: Text(vendor.vendorName),
|
||||
),
|
||||
)
|
||||
.toList(),
|
||||
VendorAutocompleteField(
|
||||
initialOptions: _controller.vendorOptions,
|
||||
selectedVendorId: value,
|
||||
initialLabel: vendorLabel,
|
||||
enabled: !isSaving,
|
||||
placeholder: const Text('제조사를 선택하세요'),
|
||||
selectedOptionBuilder: (context, selected) {
|
||||
if (selected == null) {
|
||||
return const Text('제조사를 선택하세요');
|
||||
}
|
||||
final vendor = _controller.vendorOptions
|
||||
.firstWhere(
|
||||
(v) => v.id == selected,
|
||||
orElse: () => Vendor(
|
||||
id: selected,
|
||||
vendorCode: '',
|
||||
vendorName: '',
|
||||
),
|
||||
);
|
||||
return Text(vendor.vendorName);
|
||||
onSelected: (id) {
|
||||
vendorNotifier.value = id;
|
||||
vendorError.value = null;
|
||||
},
|
||||
),
|
||||
if (errorText != null)
|
||||
@@ -748,39 +735,21 @@ class _ProductEnabledPageState extends State<_ProductEnabledPage> {
|
||||
return ValueListenableBuilder<String?>(
|
||||
valueListenable: uomError,
|
||||
builder: (_, errorText, __) {
|
||||
final uomLabel = existing?.uom?.uomName;
|
||||
return _FormField(
|
||||
label: '단위',
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
ShadSelect<int?>(
|
||||
initialValue: value,
|
||||
onChanged: saving.value
|
||||
? null
|
||||
: (next) {
|
||||
uomNotifier.value = next;
|
||||
uomError.value = null;
|
||||
},
|
||||
options: _controller.uomOptions
|
||||
.map(
|
||||
(uom) => ShadOption<int?>(
|
||||
value: uom.id,
|
||||
child: Text(uom.uomName),
|
||||
),
|
||||
)
|
||||
.toList(),
|
||||
UomAutocompleteField(
|
||||
initialOptions: _controller.uomOptions,
|
||||
selectedUomId: value,
|
||||
initialLabel: uomLabel,
|
||||
enabled: !isSaving,
|
||||
placeholder: const Text('단위를 선택하세요'),
|
||||
selectedOptionBuilder: (context, selected) {
|
||||
if (selected == null) {
|
||||
return const Text('단위를 선택하세요');
|
||||
}
|
||||
final uom = _controller.uomOptions
|
||||
.firstWhere(
|
||||
(u) => u.id == selected,
|
||||
orElse: () =>
|
||||
Uom(id: selected, uomName: ''),
|
||||
);
|
||||
return Text(uom.uomName);
|
||||
onSelected: (id) {
|
||||
uomNotifier.value = id;
|
||||
uomError.value = null;
|
||||
},
|
||||
),
|
||||
if (errorText != null)
|
||||
|
||||
@@ -0,0 +1,315 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get_it/get_it.dart';
|
||||
import 'package:shadcn_ui/shadcn_ui.dart';
|
||||
|
||||
import '../../../../../core/network/failure.dart';
|
||||
import '../../../uom/domain/entities/uom.dart';
|
||||
import '../../../uom/domain/repositories/uom_repository.dart';
|
||||
|
||||
/// 단위를 검색/선택할 수 있는 자동완성 입력 필드.
|
||||
class UomAutocompleteField extends StatefulWidget {
|
||||
const UomAutocompleteField({
|
||||
super.key,
|
||||
required this.initialOptions,
|
||||
this.selectedUomId,
|
||||
this.initialLabel,
|
||||
required this.onSelected,
|
||||
this.enabled = true,
|
||||
this.placeholder,
|
||||
});
|
||||
|
||||
final List<Uom> initialOptions;
|
||||
final int? selectedUomId;
|
||||
final String? initialLabel;
|
||||
final ValueChanged<int?> onSelected;
|
||||
final bool enabled;
|
||||
final Widget? placeholder;
|
||||
|
||||
@override
|
||||
State<UomAutocompleteField> createState() => _UomAutocompleteFieldState();
|
||||
}
|
||||
|
||||
class _UomAutocompleteFieldState extends State<UomAutocompleteField> {
|
||||
static const Duration _debounceDuration = Duration(milliseconds: 250);
|
||||
|
||||
UomRepository? get _repository =>
|
||||
GetIt.I.isRegistered<UomRepository>() ? GetIt.I<UomRepository>() : null;
|
||||
|
||||
final TextEditingController _controller = TextEditingController();
|
||||
final FocusNode _focusNode = FocusNode();
|
||||
final List<Uom> _baseOptions = [];
|
||||
final List<Uom> _suggestions = [];
|
||||
Uom? _selected;
|
||||
bool _isSearching = false;
|
||||
String? _error;
|
||||
Timer? _debounce;
|
||||
int _requestId = 0;
|
||||
bool _isApplyingText = false;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_controller.addListener(_handleTextChanged);
|
||||
_initializeOptions();
|
||||
}
|
||||
|
||||
@override
|
||||
void didUpdateWidget(covariant UomAutocompleteField oldWidget) {
|
||||
super.didUpdateWidget(oldWidget);
|
||||
if (!identical(widget.initialOptions, oldWidget.initialOptions)) {
|
||||
_baseOptions
|
||||
..clear()
|
||||
..addAll(widget.initialOptions);
|
||||
if (_controller.text.trim().isEmpty) {
|
||||
_resetSuggestions();
|
||||
}
|
||||
}
|
||||
if (widget.selectedUomId != oldWidget.selectedUomId) {
|
||||
_applySelectionFromId(widget.selectedUomId, label: widget.initialLabel);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_controller.removeListener(_handleTextChanged);
|
||||
_controller.dispose();
|
||||
_focusNode.dispose();
|
||||
_debounce?.cancel();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
void _initializeOptions() {
|
||||
_baseOptions
|
||||
..clear()
|
||||
..addAll(widget.initialOptions);
|
||||
_resetSuggestions();
|
||||
if (widget.selectedUomId != null) {
|
||||
_applySelectionFromId(widget.selectedUomId, label: widget.initialLabel);
|
||||
} else if (widget.initialLabel != null && widget.initialLabel!.isNotEmpty) {
|
||||
_isApplyingText = true;
|
||||
_controller
|
||||
..text = widget.initialLabel!
|
||||
..selection = TextSelection.collapsed(
|
||||
offset: widget.initialLabel!.length,
|
||||
);
|
||||
_isApplyingText = false;
|
||||
}
|
||||
}
|
||||
|
||||
void _resetSuggestions() {
|
||||
setState(() {
|
||||
_suggestions
|
||||
..clear()
|
||||
..addAll(_baseOptions);
|
||||
});
|
||||
}
|
||||
|
||||
void _handleTextChanged() {
|
||||
if (_isApplyingText) {
|
||||
return;
|
||||
}
|
||||
final keyword = _controller.text.trim();
|
||||
if (keyword.isEmpty) {
|
||||
_debounce?.cancel();
|
||||
_resetSuggestions();
|
||||
return;
|
||||
}
|
||||
|
||||
if (_selected != null && keyword != _selected!.uomName) {
|
||||
_selected = null;
|
||||
widget.onSelected(null);
|
||||
}
|
||||
_scheduleSearch(keyword);
|
||||
}
|
||||
|
||||
void _scheduleSearch(String keyword) {
|
||||
_debounce?.cancel();
|
||||
_debounce = Timer(_debounceDuration, () => _search(keyword));
|
||||
}
|
||||
|
||||
Future<void> _search(String keyword) async {
|
||||
final repository = _repository;
|
||||
if (repository == null) {
|
||||
setState(() {
|
||||
_error = '단위 데이터 소스를 찾을 수 없습니다.';
|
||||
});
|
||||
return;
|
||||
}
|
||||
final request = ++_requestId;
|
||||
setState(() {
|
||||
_isSearching = true;
|
||||
_error = null;
|
||||
});
|
||||
try {
|
||||
final result = await repository.list(
|
||||
page: 1,
|
||||
pageSize: 20,
|
||||
query: keyword,
|
||||
isActive: true,
|
||||
);
|
||||
if (!mounted || request != _requestId) {
|
||||
return;
|
||||
}
|
||||
final items = result.items.toList(growable: false);
|
||||
setState(() {
|
||||
_suggestions
|
||||
..clear()
|
||||
..addAll(items);
|
||||
_isSearching = false;
|
||||
});
|
||||
} catch (error) {
|
||||
if (!mounted || request != _requestId) {
|
||||
return;
|
||||
}
|
||||
setState(() {
|
||||
final failure = Failure.from(error);
|
||||
_error = failure.describe();
|
||||
_suggestions.clear();
|
||||
_isSearching = false;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
void _applySelectionFromId(int? id, {String? label}) {
|
||||
if (id == null) {
|
||||
_setSelection(null, notify: true);
|
||||
return;
|
||||
}
|
||||
final match = _findById(_baseOptions, id) ?? _findById(_suggestions, id);
|
||||
if (match != null) {
|
||||
_setSelection(match, notify: false);
|
||||
return;
|
||||
}
|
||||
if (label != null && label.isNotEmpty) {
|
||||
_isApplyingText = true;
|
||||
_controller
|
||||
..text = label
|
||||
..selection = TextSelection.collapsed(offset: label.length);
|
||||
_isApplyingText = false;
|
||||
}
|
||||
}
|
||||
|
||||
Uom? _findById(List<Uom> options, int id) {
|
||||
for (final option in options) {
|
||||
if (option.id == id) {
|
||||
return option;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
void _setSelection(Uom? uom, {bool notify = true}) {
|
||||
_selected = uom;
|
||||
if (uom != null && !_baseOptions.any((item) => item.id == uom.id)) {
|
||||
_baseOptions.add(uom);
|
||||
}
|
||||
_isApplyingText = true;
|
||||
if (uom == null) {
|
||||
_controller.clear();
|
||||
if (notify) {
|
||||
widget.onSelected(null);
|
||||
}
|
||||
} else {
|
||||
_controller
|
||||
..text = uom.uomName
|
||||
..selection = TextSelection.collapsed(offset: uom.uomName.length);
|
||||
if (notify) {
|
||||
widget.onSelected(uom.id);
|
||||
}
|
||||
}
|
||||
_isApplyingText = false;
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
if (_error != null && _baseOptions.isEmpty && _suggestions.isEmpty) {
|
||||
return ShadInput(
|
||||
readOnly: true,
|
||||
enabled: false,
|
||||
placeholder: Text(_error!),
|
||||
);
|
||||
}
|
||||
|
||||
return RawAutocomplete<Uom>(
|
||||
textEditingController: _controller,
|
||||
focusNode: _focusNode,
|
||||
optionsBuilder: (textEditingValue) {
|
||||
if (textEditingValue.text.trim().isEmpty) {
|
||||
if (_suggestions.isEmpty) {
|
||||
return _baseOptions;
|
||||
}
|
||||
return _baseOptions;
|
||||
}
|
||||
return _suggestions;
|
||||
},
|
||||
displayStringForOption: (option) => option.uomName,
|
||||
onSelected: (uom) => _setSelection(uom),
|
||||
fieldViewBuilder: (context, textController, focusNode, onFieldSubmitted) {
|
||||
final placeholder = widget.placeholder ?? const Text('단위를 선택하세요');
|
||||
return Stack(
|
||||
alignment: Alignment.centerRight,
|
||||
children: [
|
||||
ShadInput(
|
||||
controller: textController,
|
||||
focusNode: focusNode,
|
||||
enabled: widget.enabled,
|
||||
readOnly: !widget.enabled,
|
||||
placeholder: placeholder,
|
||||
),
|
||||
if (_isSearching)
|
||||
const Padding(
|
||||
padding: EdgeInsets.only(right: 12),
|
||||
child: SizedBox(
|
||||
width: 16,
|
||||
height: 16,
|
||||
child: CircularProgressIndicator(strokeWidth: 2),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
optionsViewBuilder: (context, onSelected, options) {
|
||||
if (options.isEmpty) {
|
||||
return Align(
|
||||
alignment: Alignment.topLeft,
|
||||
child: ConstrainedBox(
|
||||
constraints: const BoxConstraints(maxHeight: 200, minWidth: 220),
|
||||
child: ShadCard(
|
||||
padding: const EdgeInsets.symmetric(vertical: 12),
|
||||
child: const Center(child: Text('검색 결과가 없습니다.')),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
return Align(
|
||||
alignment: Alignment.topLeft,
|
||||
child: ConstrainedBox(
|
||||
constraints: const BoxConstraints(maxHeight: 240, minWidth: 220),
|
||||
child: ShadCard(
|
||||
padding: EdgeInsets.zero,
|
||||
child: ListView.builder(
|
||||
shrinkWrap: true,
|
||||
itemCount: options.length,
|
||||
itemBuilder: (context, index) {
|
||||
final uom = options.elementAt(index);
|
||||
return InkWell(
|
||||
onTap: () => onSelected(uom),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 12,
|
||||
vertical: 10,
|
||||
),
|
||||
child: Text(uom.uomName),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,329 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get_it/get_it.dart';
|
||||
import 'package:shadcn_ui/shadcn_ui.dart';
|
||||
|
||||
import '../../../../../core/network/failure.dart';
|
||||
import '../../../vendor/domain/entities/vendor.dart';
|
||||
import '../../../vendor/domain/repositories/vendor_repository.dart';
|
||||
|
||||
/// 공급업체를 검색해 선택할 수 있는 자동완성 입력 필드.
|
||||
class VendorAutocompleteField extends StatefulWidget {
|
||||
const VendorAutocompleteField({
|
||||
super.key,
|
||||
required this.initialOptions,
|
||||
this.selectedVendorId,
|
||||
this.initialLabel,
|
||||
required this.onSelected,
|
||||
this.enabled = true,
|
||||
this.placeholder,
|
||||
});
|
||||
|
||||
final List<Vendor> initialOptions;
|
||||
final int? selectedVendorId;
|
||||
final String? initialLabel;
|
||||
final ValueChanged<int?> onSelected;
|
||||
final bool enabled;
|
||||
final Widget? placeholder;
|
||||
|
||||
@override
|
||||
State<VendorAutocompleteField> createState() =>
|
||||
_VendorAutocompleteFieldState();
|
||||
}
|
||||
|
||||
class _VendorAutocompleteFieldState extends State<VendorAutocompleteField> {
|
||||
static const Duration _debounceDuration = Duration(milliseconds: 250);
|
||||
|
||||
VendorRepository? get _repository => GetIt.I.isRegistered<VendorRepository>()
|
||||
? GetIt.I<VendorRepository>()
|
||||
: null;
|
||||
|
||||
final TextEditingController _controller = TextEditingController();
|
||||
final FocusNode _focusNode = FocusNode();
|
||||
final List<Vendor> _baseOptions = [];
|
||||
final List<Vendor> _suggestions = [];
|
||||
Vendor? _selected;
|
||||
bool _isSearching = false;
|
||||
String? _error;
|
||||
Timer? _debounce;
|
||||
int _requestId = 0;
|
||||
bool _isApplyingText = false;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_controller.addListener(_handleTextChanged);
|
||||
_initializeOptions();
|
||||
}
|
||||
|
||||
@override
|
||||
void didUpdateWidget(covariant VendorAutocompleteField oldWidget) {
|
||||
super.didUpdateWidget(oldWidget);
|
||||
if (!identical(widget.initialOptions, oldWidget.initialOptions)) {
|
||||
_baseOptions
|
||||
..clear()
|
||||
..addAll(widget.initialOptions);
|
||||
if (_controller.text.trim().isEmpty) {
|
||||
_resetSuggestions();
|
||||
}
|
||||
}
|
||||
if (widget.selectedVendorId != oldWidget.selectedVendorId) {
|
||||
_applySelectionFromId(
|
||||
widget.selectedVendorId,
|
||||
label: widget.initialLabel,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_controller.removeListener(_handleTextChanged);
|
||||
_controller.dispose();
|
||||
_focusNode.dispose();
|
||||
_debounce?.cancel();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
void _initializeOptions() {
|
||||
_baseOptions
|
||||
..clear()
|
||||
..addAll(widget.initialOptions);
|
||||
_resetSuggestions();
|
||||
if (widget.selectedVendorId != null) {
|
||||
_applySelectionFromId(
|
||||
widget.selectedVendorId,
|
||||
label: widget.initialLabel,
|
||||
);
|
||||
} else if (widget.initialLabel != null && widget.initialLabel!.isNotEmpty) {
|
||||
_isApplyingText = true;
|
||||
_controller
|
||||
..text = widget.initialLabel!
|
||||
..selection = TextSelection.collapsed(
|
||||
offset: widget.initialLabel!.length,
|
||||
);
|
||||
_isApplyingText = false;
|
||||
}
|
||||
}
|
||||
|
||||
void _resetSuggestions() {
|
||||
setState(() {
|
||||
_suggestions
|
||||
..clear()
|
||||
..addAll(_baseOptions);
|
||||
});
|
||||
}
|
||||
|
||||
void _handleTextChanged() {
|
||||
if (_isApplyingText) {
|
||||
return;
|
||||
}
|
||||
final keyword = _controller.text.trim();
|
||||
if (keyword.isEmpty) {
|
||||
_debounce?.cancel();
|
||||
_resetSuggestions();
|
||||
return;
|
||||
}
|
||||
|
||||
if (_selected != null && keyword != _displayLabel(_selected!)) {
|
||||
_selected = null;
|
||||
widget.onSelected(null);
|
||||
}
|
||||
_scheduleSearch(keyword);
|
||||
}
|
||||
|
||||
void _scheduleSearch(String keyword) {
|
||||
_debounce?.cancel();
|
||||
_debounce = Timer(_debounceDuration, () => _search(keyword));
|
||||
}
|
||||
|
||||
Future<void> _search(String keyword) async {
|
||||
final repository = _repository;
|
||||
if (repository == null) {
|
||||
setState(() {
|
||||
_error = '공급업체 데이터 소스를 찾을 수 없습니다.';
|
||||
});
|
||||
return;
|
||||
}
|
||||
final request = ++_requestId;
|
||||
setState(() {
|
||||
_isSearching = true;
|
||||
_error = null;
|
||||
});
|
||||
try {
|
||||
final result = await repository.list(
|
||||
page: 1,
|
||||
pageSize: 20,
|
||||
query: keyword,
|
||||
isActive: true,
|
||||
);
|
||||
if (!mounted || request != _requestId) {
|
||||
return;
|
||||
}
|
||||
final items = result.items.toList(growable: false);
|
||||
setState(() {
|
||||
_suggestions
|
||||
..clear()
|
||||
..addAll(items);
|
||||
_isSearching = false;
|
||||
});
|
||||
} catch (error) {
|
||||
if (!mounted || request != _requestId) {
|
||||
return;
|
||||
}
|
||||
setState(() {
|
||||
final failure = Failure.from(error);
|
||||
_error = failure.describe();
|
||||
_suggestions.clear();
|
||||
_isSearching = false;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
void _applySelectionFromId(int? id, {String? label}) {
|
||||
if (id == null) {
|
||||
_setSelection(null, notify: true);
|
||||
return;
|
||||
}
|
||||
final match = _findById(_baseOptions, id) ?? _findById(_suggestions, id);
|
||||
if (match != null) {
|
||||
_setSelection(match, notify: false);
|
||||
return;
|
||||
}
|
||||
if (label != null && label.isNotEmpty) {
|
||||
_isApplyingText = true;
|
||||
_controller
|
||||
..text = label
|
||||
..selection = TextSelection.collapsed(offset: label.length);
|
||||
_isApplyingText = false;
|
||||
}
|
||||
}
|
||||
|
||||
Vendor? _findById(List<Vendor> options, int id) {
|
||||
for (final option in options) {
|
||||
if (option.id == id) {
|
||||
return option;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
void _setSelection(Vendor? vendor, {bool notify = true}) {
|
||||
_selected = vendor;
|
||||
if (vendor != null && !_baseOptions.any((item) => item.id == vendor.id)) {
|
||||
_baseOptions.add(vendor);
|
||||
}
|
||||
_isApplyingText = true;
|
||||
if (vendor == null) {
|
||||
_controller.clear();
|
||||
if (notify) {
|
||||
widget.onSelected(null);
|
||||
}
|
||||
} else {
|
||||
final label = _displayLabel(vendor);
|
||||
_controller
|
||||
..text = label
|
||||
..selection = TextSelection.collapsed(offset: label.length);
|
||||
if (notify) {
|
||||
widget.onSelected(vendor.id);
|
||||
}
|
||||
}
|
||||
_isApplyingText = false;
|
||||
}
|
||||
|
||||
String _displayLabel(Vendor vendor) {
|
||||
final code = vendor.vendorCode.isEmpty ? '' : ' (${vendor.vendorCode})';
|
||||
return '${vendor.vendorName}$code';
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
if (_error != null && _baseOptions.isEmpty && _suggestions.isEmpty) {
|
||||
return ShadInput(
|
||||
readOnly: true,
|
||||
enabled: false,
|
||||
placeholder: Text(_error!),
|
||||
);
|
||||
}
|
||||
|
||||
return RawAutocomplete<Vendor>(
|
||||
textEditingController: _controller,
|
||||
focusNode: _focusNode,
|
||||
optionsBuilder: (textEditingValue) {
|
||||
if (textEditingValue.text.trim().isEmpty) {
|
||||
if (_suggestions.isEmpty) {
|
||||
return _baseOptions;
|
||||
}
|
||||
return _baseOptions;
|
||||
}
|
||||
return _suggestions;
|
||||
},
|
||||
displayStringForOption: _displayLabel,
|
||||
onSelected: (vendor) => _setSelection(vendor),
|
||||
fieldViewBuilder: (context, textController, focusNode, onFieldSubmitted) {
|
||||
final placeholder = widget.placeholder ?? const Text('제조사를 선택하세요');
|
||||
return Stack(
|
||||
alignment: Alignment.centerRight,
|
||||
children: [
|
||||
ShadInput(
|
||||
controller: textController,
|
||||
focusNode: focusNode,
|
||||
enabled: widget.enabled,
|
||||
readOnly: !widget.enabled,
|
||||
placeholder: placeholder,
|
||||
),
|
||||
if (_isSearching)
|
||||
const Padding(
|
||||
padding: EdgeInsets.only(right: 12),
|
||||
child: SizedBox(
|
||||
width: 16,
|
||||
height: 16,
|
||||
child: CircularProgressIndicator(strokeWidth: 2),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
optionsViewBuilder: (context, onSelected, options) {
|
||||
if (options.isEmpty) {
|
||||
return Align(
|
||||
alignment: Alignment.topLeft,
|
||||
child: ConstrainedBox(
|
||||
constraints: const BoxConstraints(maxHeight: 200, minWidth: 260),
|
||||
child: ShadCard(
|
||||
padding: const EdgeInsets.symmetric(vertical: 12),
|
||||
child: const Center(child: Text('검색 결과가 없습니다.')),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
return Align(
|
||||
alignment: Alignment.topLeft,
|
||||
child: ConstrainedBox(
|
||||
constraints: const BoxConstraints(maxHeight: 240, minWidth: 260),
|
||||
child: ShadCard(
|
||||
padding: EdgeInsets.zero,
|
||||
child: ListView.builder(
|
||||
shrinkWrap: true,
|
||||
itemCount: options.length,
|
||||
itemBuilder: (context, index) {
|
||||
final vendor = options.elementAt(index);
|
||||
return InkWell(
|
||||
onTap: () => onSelected(vendor),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 12,
|
||||
vertical: 10,
|
||||
),
|
||||
child: Text(_displayLabel(vendor)),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user