자동완성 포커스 유지 및 디버그 로그 추가

This commit is contained in:
JiWoong Sul
2025-10-22 14:31:45 +09:00
parent f4dc83d441
commit cefcfaac0d
8 changed files with 486 additions and 256 deletions

View File

@@ -155,11 +155,21 @@ class _ApprovalApproverAutocompleteFieldState
_applyManualEntry(textController.text); _applyManualEntry(textController.text);
onFieldSubmitted(); onFieldSubmitted();
}, },
onPressedOutside: (event) {
// 드롭다운에서 항목을 고르기 전에 포커스를 잃지 않도록 한다.
focusNode.requestFocus();
},
); );
}, },
optionsViewBuilder: (context, onSelected, options) { optionsViewBuilder: (context, onSelected, options) {
if (options.isEmpty) { if (options.isEmpty) {
return Align( return Listener(
onPointerDown: (_) {
if (!_focusNode.hasPrimaryFocus) {
_focusNode.requestFocus();
}
},
child: Align(
alignment: AlignmentDirectional.topStart, alignment: AlignmentDirectional.topStart,
child: ConstrainedBox( child: ConstrainedBox(
constraints: BoxConstraints( constraints: BoxConstraints(
@@ -184,9 +194,16 @@ class _ApprovalApproverAutocompleteFieldState
), ),
), ),
), ),
),
); );
} }
return Align( return Listener(
onPointerDown: (_) {
if (!_focusNode.hasPrimaryFocus) {
_focusNode.requestFocus();
}
},
child: Align(
alignment: AlignmentDirectional.topStart, alignment: AlignmentDirectional.topStart,
child: ConstrainedBox( child: ConstrainedBox(
constraints: BoxConstraints( constraints: BoxConstraints(
@@ -234,6 +251,7 @@ class _ApprovalApproverAutocompleteFieldState
), ),
), ),
), ),
),
); );
}, },
); );

View File

@@ -198,6 +198,10 @@ class _InventoryEmployeeAutocompleteFieldState
placeholder: Text(widget.placeholder), placeholder: Text(widget.placeholder),
onChanged: (_) => widget.onChanged?.call(), onChanged: (_) => widget.onChanged?.call(),
onSubmitted: (_) => onFieldSubmitted(), onSubmitted: (_) => onFieldSubmitted(),
onPressedOutside: (event) {
// 드롭다운 선택 중 포커스를 유지해 리스트가 즉시 닫히는 문제를 방지한다.
focusNode.requestFocus();
},
); );
if (!_isSearching) { if (!_isSearching) {
return input; return input;
@@ -219,7 +223,14 @@ class _InventoryEmployeeAutocompleteFieldState
}, },
optionsViewBuilder: (context, onSelected, options) { optionsViewBuilder: (context, onSelected, options) {
if (options.isEmpty) { if (options.isEmpty) {
return Align( return Listener(
onPointerDown: (_) {
// 포커스가 사라지면 항목 선택 전에 닫히므로 즉시 복구한다.
if (!_focusNode.hasPrimaryFocus) {
_focusNode.requestFocus();
}
},
child: Align(
alignment: AlignmentDirectional.topStart, alignment: AlignmentDirectional.topStart,
child: Material( child: Material(
elevation: 6, elevation: 6,
@@ -235,9 +246,16 @@ class _InventoryEmployeeAutocompleteFieldState
), ),
), ),
), ),
),
); );
} }
return Align( return Listener(
onPointerDown: (_) {
if (!_focusNode.hasPrimaryFocus) {
_focusNode.requestFocus();
}
},
child: Align(
alignment: AlignmentDirectional.topStart, alignment: AlignmentDirectional.topStart,
child: ConstrainedBox( child: ConstrainedBox(
constraints: const BoxConstraints(maxHeight: 240, maxWidth: 360), constraints: const BoxConstraints(maxHeight: 240, maxWidth: 360),
@@ -267,7 +285,9 @@ class _InventoryEmployeeAutocompleteFieldState
const SizedBox(height: 4), const SizedBox(height: 4),
Text( Text(
'ID ${suggestion.id} · ${suggestion.employeeNo}', 'ID ${suggestion.id} · ${suggestion.employeeNo}',
style: theme.textTheme.muted.copyWith(fontSize: 12), style: theme.textTheme.muted.copyWith(
fontSize: 12,
),
), ),
], ],
), ),
@@ -277,6 +297,7 @@ class _InventoryEmployeeAutocompleteFieldState
), ),
), ),
), ),
),
); );
}, },
); );

View File

@@ -239,6 +239,10 @@ class _InventoryProductAutocompleteFieldState
placeholder: const Text('제품명 검색'), placeholder: const Text('제품명 검색'),
onChanged: (_) => widget.onChanged?.call(), onChanged: (_) => widget.onChanged?.call(),
onSubmitted: (_) => onFieldSubmitted(), onSubmitted: (_) => onFieldSubmitted(),
onPressedOutside: (event) {
// 포커스를 유지해 항목 선택 전 오버레이가 닫히지 않도록 한다.
focusNode.requestFocus();
},
); );
if (!_isSearching) { if (!_isSearching) {
return input; return input;
@@ -260,7 +264,13 @@ class _InventoryProductAutocompleteFieldState
}, },
optionsViewBuilder: (context, onSelected, options) { optionsViewBuilder: (context, onSelected, options) {
if (options.isEmpty) { if (options.isEmpty) {
return Align( return Listener(
onPointerDown: (_) {
if (!widget.productFocusNode.hasPrimaryFocus) {
widget.productFocusNode.requestFocus();
}
},
child: Align(
alignment: AlignmentDirectional.topStart, alignment: AlignmentDirectional.topStart,
child: ConstrainedBox( child: ConstrainedBox(
constraints: BoxConstraints( constraints: BoxConstraints(
@@ -285,9 +295,16 @@ class _InventoryProductAutocompleteFieldState
), ),
), ),
), ),
),
); );
} }
return Align( return Listener(
onPointerDown: (_) {
if (!widget.productFocusNode.hasPrimaryFocus) {
widget.productFocusNode.requestFocus();
}
},
child: Align(
alignment: AlignmentDirectional.topStart, alignment: AlignmentDirectional.topStart,
child: ConstrainedBox( child: ConstrainedBox(
constraints: BoxConstraints( constraints: BoxConstraints(
@@ -332,6 +349,7 @@ class _InventoryProductAutocompleteFieldState
), ),
), ),
), ),
),
); );
}, },
); );

View File

@@ -395,6 +395,10 @@ class _InventoryWarehouseSelectFieldState
enabled: widget.enabled, enabled: widget.enabled,
readOnly: !widget.enabled, readOnly: !widget.enabled,
placeholder: placeholder, placeholder: placeholder,
onPressedOutside: (event) {
// 포커스를 유지하지 않으면 드롭다운이 즉시 닫히므로 재요청한다.
focusNode.requestFocus();
},
), ),
if (_isSearching) if (_isSearching)
const Padding( const Padding(
@@ -410,18 +414,34 @@ class _InventoryWarehouseSelectFieldState
}, },
optionsViewBuilder: (context, onSelected, options) { optionsViewBuilder: (context, onSelected, options) {
if (options.isEmpty) { if (options.isEmpty) {
return Align( return Listener(
onPointerDown: (_) {
if (!_focusNode.hasPrimaryFocus) {
_focusNode.requestFocus();
}
},
child: Align(
alignment: Alignment.topLeft, alignment: Alignment.topLeft,
child: ConstrainedBox( child: ConstrainedBox(
constraints: const BoxConstraints(maxHeight: 200, minWidth: 260), constraints: const BoxConstraints(
maxHeight: 200,
minWidth: 260,
),
child: ShadCard( child: ShadCard(
padding: const EdgeInsets.symmetric(vertical: 12), padding: const EdgeInsets.symmetric(vertical: 12),
child: const Center(child: Text('검색 결과가 없습니다.')), child: const Center(child: Text('검색 결과가 없습니다.')),
), ),
), ),
),
); );
} }
return Align( return Listener(
onPointerDown: (_) {
if (!_focusNode.hasPrimaryFocus) {
_focusNode.requestFocus();
}
},
child: Align(
alignment: Alignment.topLeft, alignment: Alignment.topLeft,
child: ConstrainedBox( child: ConstrainedBox(
constraints: const BoxConstraints(maxHeight: 240, minWidth: 260), constraints: const BoxConstraints(maxHeight: 240, minWidth: 260),
@@ -433,7 +453,9 @@ class _InventoryWarehouseSelectFieldState
itemBuilder: (context, index) { itemBuilder: (context, index) {
final option = options.elementAt(index); final option = options.elementAt(index);
final isAll = option.id == -1; final isAll = option.id == -1;
final label = isAll ? widget.allLabel : _displayLabel(option); final label = isAll
? widget.allLabel
: _displayLabel(option);
return InkWell( return InkWell(
onTap: () => onSelected(option), onTap: () => onSelected(option),
child: Padding( child: Padding(
@@ -448,6 +470,7 @@ class _InventoryWarehouseSelectFieldState
), ),
), ),
), ),
),
); );
}, },
); );

View File

@@ -104,6 +104,7 @@ class ProductController extends ChangeNotifier {
_isLoadingLookups = true; _isLoadingLookups = true;
notifyListeners(); notifyListeners();
try { try {
debugPrint('[ProductController] 드롭다운 데이터 조회 시작');
final vendors = await fetchAllPaginatedItems<Vendor>( final vendors = await fetchAllPaginatedItems<Vendor>(
request: (page, pageSize) => request: (page, pageSize) =>
_vendorRepository.list(page: page, pageSize: pageSize), _vendorRepository.list(page: page, pageSize: pageSize),
@@ -114,9 +115,16 @@ class ProductController extends ChangeNotifier {
); );
_vendorOptions = vendors; _vendorOptions = vendors;
_uomOptions = uoms; _uomOptions = uoms;
debugPrint(
'[ProductController] 드롭다운 데이터 조회 완료 '
'(vendors=${vendors.length}, uoms=${uoms.length})',
);
} catch (error) { } catch (error) {
final failure = Failure.from(error); final failure = Failure.from(error);
_errorMessage = failure.describe(); _errorMessage = failure.describe();
debugPrint(
'[ProductController] 드롭다운 데이터 조회 실패: ${_errorMessage ?? '알 수 없는 오류'}',
);
} finally { } finally {
_isLoadingLookups = false; _isLoadingLookups = false;
notifyListeners(); notifyListeners();

View File

@@ -633,8 +633,7 @@ class _ProductEnabledPageState extends State<_ProductEnabledPage> {
return ShadButton.ghost( return ShadButton.ghost(
onPressed: isSaving onPressed: isSaving
? null ? null
: () => : () => Navigator.of(context, rootNavigator: true).pop(false),
Navigator.of(context, rootNavigator: true).pop(false),
child: const Text('취소'), child: const Text('취소'),
); );
}, },
@@ -739,6 +738,9 @@ class _ProductEnabledPageState extends State<_ProductEnabledPage> {
enabled: !isSaving, enabled: !isSaving,
placeholder: const Text('제조사를 선택하세요'), placeholder: const Text('제조사를 선택하세요'),
onSelected: (id) { onSelected: (id) {
debugPrint(
'[ProductForm] 제조사 선택 -> id=$id',
);
vendorNotifier.value = id; vendorNotifier.value = id;
vendorError.value = null; vendorError.value = null;
}, },
@@ -780,6 +782,7 @@ class _ProductEnabledPageState extends State<_ProductEnabledPage> {
enabled: !isSaving, enabled: !isSaving,
placeholder: const Text('단위를 선택하세요'), placeholder: const Text('단위를 선택하세요'),
onSelected: (id) { onSelected: (id) {
debugPrint('[ProductForm] 단위 선택 -> id=$id');
uomNotifier.value = id; uomNotifier.value = id;
uomError.value = null; uomError.value = null;
}, },
@@ -867,8 +870,7 @@ class _ProductEnabledPageState extends State<_ProductEnabledPage> {
title: '제품 삭제', title: '제품 삭제',
description: '"${product.productName}" 제품을 삭제하시겠습니까?', description: '"${product.productName}" 제품을 삭제하시겠습니까?',
primaryAction: ShadButton.destructive( primaryAction: ShadButton.destructive(
onPressed: () => onPressed: () => Navigator.of(context, rootNavigator: true).pop(true),
Navigator.of(context, rootNavigator: true).pop(true),
child: const Text('삭제'), child: const Text('삭제'),
), ),
secondaryAction: ShadButton.ghost( secondaryAction: ShadButton.ghost(

View File

@@ -52,6 +52,7 @@ class _UomAutocompleteFieldState extends State<UomAutocompleteField> {
void initState() { void initState() {
super.initState(); super.initState();
_controller.addListener(_handleTextChanged); _controller.addListener(_handleTextChanged);
_focusNode.addListener(_handleFocusChange);
_initializeOptions(); _initializeOptions();
} }
@@ -75,7 +76,9 @@ class _UomAutocompleteFieldState extends State<UomAutocompleteField> {
void dispose() { void dispose() {
_controller.removeListener(_handleTextChanged); _controller.removeListener(_handleTextChanged);
_controller.dispose(); _controller.dispose();
_focusNode.dispose(); _focusNode
..removeListener(_handleFocusChange)
..dispose();
_debounce?.cancel(); _debounce?.cancel();
super.dispose(); super.dispose();
} }
@@ -96,6 +99,7 @@ class _UomAutocompleteFieldState extends State<UomAutocompleteField> {
); );
_isApplyingText = false; _isApplyingText = false;
} }
_log('초기 옵션 준비 완료 (base=${_baseOptions.length})');
} }
void _resetSuggestions() { void _resetSuggestions() {
@@ -104,6 +108,7 @@ class _UomAutocompleteFieldState extends State<UomAutocompleteField> {
..clear() ..clear()
..addAll(_baseOptions); ..addAll(_baseOptions);
}); });
_log('입력 초기화 -> 제안 목록 리셋 (count=${_suggestions.length})');
} }
void _handleTextChanged() { void _handleTextChanged() {
@@ -127,6 +132,7 @@ class _UomAutocompleteFieldState extends State<UomAutocompleteField> {
void _scheduleSearch(String keyword) { void _scheduleSearch(String keyword) {
_debounce?.cancel(); _debounce?.cancel();
_debounce = Timer(_debounceDuration, () => _search(keyword)); _debounce = Timer(_debounceDuration, () => _search(keyword));
_log('검색 예약: "$keyword"');
} }
Future<void> _search(String keyword) async { Future<void> _search(String keyword) async {
@@ -143,6 +149,7 @@ class _UomAutocompleteFieldState extends State<UomAutocompleteField> {
_error = null; _error = null;
}); });
try { try {
_log('단위 검색 시작: "$keyword"');
final result = await repository.list( final result = await repository.list(
page: 1, page: 1,
pageSize: 20, pageSize: 20,
@@ -159,6 +166,7 @@ class _UomAutocompleteFieldState extends State<UomAutocompleteField> {
..addAll(items); ..addAll(items);
_isSearching = false; _isSearching = false;
}); });
_log('단위 검색 완료: ${items.length}');
} catch (error) { } catch (error) {
if (!mounted || request != _requestId) { if (!mounted || request != _requestId) {
return; return;
@@ -169,6 +177,7 @@ class _UomAutocompleteFieldState extends State<UomAutocompleteField> {
_suggestions.clear(); _suggestions.clear();
_isSearching = false; _isSearching = false;
}); });
_log('단위 검색 실패: ${_error ?? '알 수 없는 오류'}');
} }
} }
@@ -208,6 +217,11 @@ class _UomAutocompleteFieldState extends State<UomAutocompleteField> {
} }
_applyControllerText(uom?.uomName ?? ''); _applyControllerText(uom?.uomName ?? '');
}); });
if (uom == null) {
_log('선택 해제됨');
} else {
_log('항목 선택: id=${uom.id}, label=${uom.uomName}');
}
if (notify) { if (notify) {
widget.onSelected(uom?.id); widget.onSelected(uom?.id);
} }
@@ -249,6 +263,13 @@ class _UomAutocompleteFieldState extends State<UomAutocompleteField> {
enabled: widget.enabled, enabled: widget.enabled,
readOnly: !widget.enabled, readOnly: !widget.enabled,
placeholder: placeholder, placeholder: placeholder,
onPressedOutside: (event) {
_log(
'입력 외부 포인터 감지 -> 포커스 유지 시도 '
'pos=${event.localPosition} kind=${event.kind}',
);
focusNode.requestFocus();
},
), ),
if (_isSearching) if (_isSearching)
const Padding( const Padding(
@@ -264,18 +285,45 @@ class _UomAutocompleteFieldState extends State<UomAutocompleteField> {
}, },
optionsViewBuilder: (context, onSelected, options) { optionsViewBuilder: (context, onSelected, options) {
if (options.isEmpty) { if (options.isEmpty) {
return Align( return Listener(
onPointerDown: (event) {
_log(
'오버레이 포인터 다운 (빈 목록) '
'pos=${event.localPosition} kind=${event.kind}',
);
if (!_focusNode.hasPrimaryFocus) {
_log('포커스 복구 시도 (빈 목록)');
_focusNode.requestFocus();
}
},
child: Align(
alignment: Alignment.topLeft, alignment: Alignment.topLeft,
child: ConstrainedBox( child: ConstrainedBox(
constraints: const BoxConstraints(maxHeight: 200, minWidth: 220), constraints: const BoxConstraints(
maxHeight: 200,
minWidth: 220,
),
child: ShadCard( child: ShadCard(
padding: const EdgeInsets.symmetric(vertical: 12), padding: const EdgeInsets.symmetric(vertical: 12),
child: const Center(child: Text('검색 결과가 없습니다.')), child: const Center(child: Text('검색 결과가 없습니다.')),
), ),
), ),
),
); );
} }
return Align( _log('드롭다운 리스트 표시: ${options.length}');
return Listener(
onPointerDown: (event) {
_log(
'오버레이 포인터 다운 pos=${event.localPosition} '
'kind=${event.kind}',
);
if (!_focusNode.hasPrimaryFocus) {
_log('포커스 복구 시도');
_focusNode.requestFocus();
}
},
child: Align(
alignment: Alignment.topLeft, alignment: Alignment.topLeft,
child: ConstrainedBox( child: ConstrainedBox(
constraints: const BoxConstraints(maxHeight: 240, minWidth: 220), constraints: const BoxConstraints(maxHeight: 240, minWidth: 220),
@@ -286,8 +334,16 @@ class _UomAutocompleteFieldState extends State<UomAutocompleteField> {
itemCount: options.length, itemCount: options.length,
itemBuilder: (context, index) { itemBuilder: (context, index) {
final uom = options.elementAt(index); final uom = options.elementAt(index);
return InkWell( return GestureDetector(
onTap: () => onSelected(uom), behavior: HitTestBehavior.opaque,
onTapDown: (_) =>
_log('항목 탭다운: id=${uom.id} index=$index'),
onTapCancel: () =>
_log('항목 탭 취소: id=${uom.id} index=$index'),
onTap: () {
_log('항목 탭 -> onSelected 호출: id=${uom.id}');
onSelected(uom);
},
child: Padding( child: Padding(
padding: const EdgeInsets.symmetric( padding: const EdgeInsets.symmetric(
horizontal: 12, horizontal: 12,
@@ -300,6 +356,7 @@ class _UomAutocompleteFieldState extends State<UomAutocompleteField> {
), ),
), ),
), ),
),
); );
}, },
); );
@@ -310,5 +367,18 @@ class _UomAutocompleteFieldState extends State<UomAutocompleteField> {
_controller.text = value; _controller.text = value;
_controller.selection = TextSelection.collapsed(offset: value.length); _controller.selection = TextSelection.collapsed(offset: value.length);
_isApplyingText = false; _isApplyingText = false;
_log('입력 필드 텍스트 적용: "$value"');
}
void _handleFocusChange() {
if (_focusNode.hasFocus) {
_log('포커스 획득 -> 드롭다운 진입');
} else {
_log('포커스 해제 -> 드롭다운 종료');
}
}
void _log(String message) {
debugPrint('[UomAutocomplete] $message');
} }
} }

View File

@@ -54,6 +54,7 @@ class _VendorAutocompleteFieldState extends State<VendorAutocompleteField> {
void initState() { void initState() {
super.initState(); super.initState();
_controller.addListener(_handleTextChanged); _controller.addListener(_handleTextChanged);
_focusNode.addListener(_handleFocusChange);
_initializeOptions(); _initializeOptions();
} }
@@ -80,7 +81,9 @@ class _VendorAutocompleteFieldState extends State<VendorAutocompleteField> {
void dispose() { void dispose() {
_controller.removeListener(_handleTextChanged); _controller.removeListener(_handleTextChanged);
_controller.dispose(); _controller.dispose();
_focusNode.dispose(); _focusNode
..removeListener(_handleFocusChange)
..dispose();
_debounce?.cancel(); _debounce?.cancel();
super.dispose(); super.dispose();
} }
@@ -104,6 +107,7 @@ class _VendorAutocompleteFieldState extends State<VendorAutocompleteField> {
); );
_isApplyingText = false; _isApplyingText = false;
} }
_log('초기 옵션 준비 완료 (base=${_baseOptions.length})');
} }
void _resetSuggestions() { void _resetSuggestions() {
@@ -112,6 +116,7 @@ class _VendorAutocompleteFieldState extends State<VendorAutocompleteField> {
..clear() ..clear()
..addAll(_baseOptions); ..addAll(_baseOptions);
}); });
_log('입력 초기화 -> 제안 목록 리셋 (count=${_suggestions.length})');
} }
void _handleTextChanged() { void _handleTextChanged() {
@@ -135,6 +140,7 @@ class _VendorAutocompleteFieldState extends State<VendorAutocompleteField> {
void _scheduleSearch(String keyword) { void _scheduleSearch(String keyword) {
_debounce?.cancel(); _debounce?.cancel();
_debounce = Timer(_debounceDuration, () => _search(keyword)); _debounce = Timer(_debounceDuration, () => _search(keyword));
_log('검색 예약: "$keyword"');
} }
Future<void> _search(String keyword) async { Future<void> _search(String keyword) async {
@@ -151,6 +157,7 @@ class _VendorAutocompleteFieldState extends State<VendorAutocompleteField> {
_error = null; _error = null;
}); });
try { try {
_log('공급업체 검색 시작: "$keyword"');
final result = await repository.list( final result = await repository.list(
page: 1, page: 1,
pageSize: 20, pageSize: 20,
@@ -167,6 +174,7 @@ class _VendorAutocompleteFieldState extends State<VendorAutocompleteField> {
..addAll(items); ..addAll(items);
_isSearching = false; _isSearching = false;
}); });
_log('공급업체 검색 완료: ${items.length}');
} catch (error) { } catch (error) {
if (!mounted || request != _requestId) { if (!mounted || request != _requestId) {
return; return;
@@ -177,6 +185,7 @@ class _VendorAutocompleteFieldState extends State<VendorAutocompleteField> {
_suggestions.clear(); _suggestions.clear();
_isSearching = false; _isSearching = false;
}); });
_log('공급업체 검색 실패: ${_error ?? '알 수 없는 오류'}');
} }
} }
@@ -220,6 +229,11 @@ class _VendorAutocompleteFieldState extends State<VendorAutocompleteField> {
_applyControllerText(_displayLabel(vendor)); _applyControllerText(_displayLabel(vendor));
} }
}); });
if (vendor == null) {
_log('선택 해제됨');
} else {
_log('항목 선택: id=${vendor.id}, label=${_displayLabel(vendor)}');
}
if (notify) { if (notify) {
widget.onSelected(vendor?.id); widget.onSelected(vendor?.id);
} }
@@ -266,6 +280,13 @@ class _VendorAutocompleteFieldState extends State<VendorAutocompleteField> {
enabled: widget.enabled, enabled: widget.enabled,
readOnly: !widget.enabled, readOnly: !widget.enabled,
placeholder: placeholder, placeholder: placeholder,
onPressedOutside: (event) {
_log(
'입력 외부 포인터 감지 -> 포커스 유지 시도 '
'pos=${event.localPosition} kind=${event.kind}',
);
focusNode.requestFocus();
},
), ),
if (_isSearching) if (_isSearching)
const Padding( const Padding(
@@ -281,18 +302,45 @@ class _VendorAutocompleteFieldState extends State<VendorAutocompleteField> {
}, },
optionsViewBuilder: (context, onSelected, options) { optionsViewBuilder: (context, onSelected, options) {
if (options.isEmpty) { if (options.isEmpty) {
return Align( return Listener(
onPointerDown: (event) {
_log(
'오버레이 포인터 다운 (빈 목록) '
'pos=${event.localPosition} kind=${event.kind}',
);
if (!_focusNode.hasPrimaryFocus) {
_log('포커스 복구 시도 (빈 목록)');
_focusNode.requestFocus();
}
},
child: Align(
alignment: Alignment.topLeft, alignment: Alignment.topLeft,
child: ConstrainedBox( child: ConstrainedBox(
constraints: const BoxConstraints(maxHeight: 200, minWidth: 260), constraints: const BoxConstraints(
maxHeight: 200,
minWidth: 260,
),
child: ShadCard( child: ShadCard(
padding: const EdgeInsets.symmetric(vertical: 12), padding: const EdgeInsets.symmetric(vertical: 12),
child: const Center(child: Text('검색 결과가 없습니다.')), child: const Center(child: Text('검색 결과가 없습니다.')),
), ),
), ),
),
); );
} }
return Align( _log('드롭다운 리스트 표시: ${options.length}');
return Listener(
onPointerDown: (event) {
_log(
'오버레이 포인터 다운 pos=${event.localPosition} '
'kind=${event.kind}',
);
if (!_focusNode.hasPrimaryFocus) {
_log('포커스 복구 시도');
_focusNode.requestFocus();
}
},
child: Align(
alignment: Alignment.topLeft, alignment: Alignment.topLeft,
child: ConstrainedBox( child: ConstrainedBox(
constraints: const BoxConstraints(maxHeight: 240, minWidth: 260), constraints: const BoxConstraints(maxHeight: 240, minWidth: 260),
@@ -303,8 +351,16 @@ class _VendorAutocompleteFieldState extends State<VendorAutocompleteField> {
itemCount: options.length, itemCount: options.length,
itemBuilder: (context, index) { itemBuilder: (context, index) {
final vendor = options.elementAt(index); final vendor = options.elementAt(index);
return InkWell( return GestureDetector(
onTap: () => onSelected(vendor), behavior: HitTestBehavior.opaque,
onTapDown: (_) =>
_log('항목 탭다운: id=${vendor.id} index=$index'),
onTapCancel: () =>
_log('항목 탭 취소: id=${vendor.id} index=$index'),
onTap: () {
_log('항목 탭 -> onSelected 호출: id=${vendor.id}');
onSelected(vendor);
},
child: Padding( child: Padding(
padding: const EdgeInsets.symmetric( padding: const EdgeInsets.symmetric(
horizontal: 12, horizontal: 12,
@@ -317,6 +373,7 @@ class _VendorAutocompleteFieldState extends State<VendorAutocompleteField> {
), ),
), ),
), ),
),
); );
}, },
); );
@@ -327,5 +384,18 @@ class _VendorAutocompleteFieldState extends State<VendorAutocompleteField> {
_controller.text = value; _controller.text = value;
_controller.selection = TextSelection.collapsed(offset: value.length); _controller.selection = TextSelection.collapsed(offset: value.length);
_isApplyingText = false; _isApplyingText = false;
_log('입력 필드 텍스트 적용: "$value"');
}
void _handleFocusChange() {
if (_focusNode.hasFocus) {
_log('포커스 획득 -> 드롭다운 진입');
} else {
_log('포커스 해제 -> 드롭다운 종료');
}
}
void _log(String message) {
debugPrint('[VendorAutocomplete] $message');
} }
} }