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

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

@@ -198,6 +198,10 @@ class _InventoryEmployeeAutocompleteFieldState
placeholder: Text(widget.placeholder),
onChanged: (_) => widget.onChanged?.call(),
onSubmitted: (_) => onFieldSubmitted(),
onPressedOutside: (event) {
// 드롭다운 선택 중 포커스를 유지해 리스트가 즉시 닫히는 문제를 방지한다.
focusNode.requestFocus();
},
);
if (!_isSearching) {
return input;
@@ -219,61 +223,78 @@ class _InventoryEmployeeAutocompleteFieldState
},
optionsViewBuilder: (context, onSelected, options) {
if (options.isEmpty) {
return Align(
alignment: AlignmentDirectional.topStart,
child: Material(
elevation: 6,
color: theme.colorScheme.background,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
side: BorderSide(color: theme.colorScheme.border),
),
child: Padding(
padding: const EdgeInsets.symmetric(vertical: 16),
child: Center(
child: Text('일치하는 직원이 없습니다.', style: theme.textTheme.muted),
return Listener(
onPointerDown: (_) {
// 포커스가 사라지면 항목 선택 전에 닫히므로 즉시 복구한다.
if (!_focusNode.hasPrimaryFocus) {
_focusNode.requestFocus();
}
},
child: Align(
alignment: AlignmentDirectional.topStart,
child: Material(
elevation: 6,
color: theme.colorScheme.background,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
side: BorderSide(color: theme.colorScheme.border),
),
child: Padding(
padding: const EdgeInsets.symmetric(vertical: 16),
child: Center(
child: Text('일치하는 직원이 없습니다.', style: theme.textTheme.muted),
),
),
),
),
);
}
return Align(
alignment: AlignmentDirectional.topStart,
child: ConstrainedBox(
constraints: const BoxConstraints(maxHeight: 240, maxWidth: 360),
child: Material(
elevation: 6,
color: theme.colorScheme.background,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
side: BorderSide(color: theme.colorScheme.border),
),
child: ListView.builder(
padding: const EdgeInsets.symmetric(vertical: 6),
itemCount: options.length,
itemBuilder: (context, index) {
final suggestion = options.elementAt(index);
return InkWell(
onTap: () => onSelected(suggestion),
child: Padding(
padding: const EdgeInsets.symmetric(
horizontal: 12,
vertical: 10,
return Listener(
onPointerDown: (_) {
if (!_focusNode.hasPrimaryFocus) {
_focusNode.requestFocus();
}
},
child: Align(
alignment: AlignmentDirectional.topStart,
child: ConstrainedBox(
constraints: const BoxConstraints(maxHeight: 240, maxWidth: 360),
child: Material(
elevation: 6,
color: theme.colorScheme.background,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
side: BorderSide(color: theme.colorScheme.border),
),
child: ListView.builder(
padding: const EdgeInsets.symmetric(vertical: 6),
itemCount: options.length,
itemBuilder: (context, index) {
final suggestion = options.elementAt(index);
return InkWell(
onTap: () => onSelected(suggestion),
child: Padding(
padding: const EdgeInsets.symmetric(
horizontal: 12,
vertical: 10,
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(suggestion.name, style: theme.textTheme.p),
const SizedBox(height: 4),
Text(
'ID ${suggestion.id} · ${suggestion.employeeNo}',
style: theme.textTheme.muted.copyWith(
fontSize: 12,
),
),
],
),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(suggestion.name, style: theme.textTheme.p),
const SizedBox(height: 4),
Text(
'ID ${suggestion.id} · ${suggestion.employeeNo}',
style: theme.textTheme.muted.copyWith(fontSize: 12),
),
],
),
),
);
},
);
},
),
),
),
),

View File

@@ -239,6 +239,10 @@ class _InventoryProductAutocompleteFieldState
placeholder: const Text('제품명 검색'),
onChanged: (_) => widget.onChanged?.call(),
onSubmitted: (_) => onFieldSubmitted(),
onPressedOutside: (event) {
// 포커스를 유지해 항목 선택 전 오버레이가 닫히지 않도록 한다.
focusNode.requestFocus();
},
);
if (!_isSearching) {
return input;
@@ -260,12 +264,52 @@ class _InventoryProductAutocompleteFieldState
},
optionsViewBuilder: (context, onSelected, options) {
if (options.isEmpty) {
return Align(
return Listener(
onPointerDown: (_) {
if (!widget.productFocusNode.hasPrimaryFocus) {
widget.productFocusNode.requestFocus();
}
},
child: Align(
alignment: AlignmentDirectional.topStart,
child: ConstrainedBox(
constraints: BoxConstraints(
maxWidth: constraints.maxWidth,
maxHeight: 220,
),
child: Material(
elevation: 6,
color: theme.colorScheme.background,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
side: BorderSide(color: theme.colorScheme.border),
),
child: Center(
child: Padding(
padding: const EdgeInsets.symmetric(vertical: 20),
child: Text(
'검색 결과가 없습니다.',
style: theme.textTheme.muted,
),
),
),
),
),
),
);
}
return Listener(
onPointerDown: (_) {
if (!widget.productFocusNode.hasPrimaryFocus) {
widget.productFocusNode.requestFocus();
}
},
child: Align(
alignment: AlignmentDirectional.topStart,
child: ConstrainedBox(
constraints: BoxConstraints(
maxWidth: constraints.maxWidth,
maxHeight: 220,
maxHeight: 260,
),
child: Material(
elevation: 6,
@@ -274,61 +318,35 @@ class _InventoryProductAutocompleteFieldState
borderRadius: BorderRadius.circular(12),
side: BorderSide(color: theme.colorScheme.border),
),
child: Center(
child: Padding(
padding: const EdgeInsets.symmetric(vertical: 20),
child: Text(
'검색 결과가 없습니다.',
style: theme.textTheme.muted,
),
),
),
),
),
);
}
return Align(
alignment: AlignmentDirectional.topStart,
child: ConstrainedBox(
constraints: BoxConstraints(
maxWidth: constraints.maxWidth,
maxHeight: 260,
),
child: Material(
elevation: 6,
color: theme.colorScheme.background,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
side: BorderSide(color: theme.colorScheme.border),
),
child: ListView.builder(
padding: const EdgeInsets.symmetric(vertical: 6),
itemCount: options.length,
itemBuilder: (context, index) {
final option = options.elementAt(index);
return InkWell(
onTap: () => onSelected(option),
child: Padding(
padding: const EdgeInsets.symmetric(
horizontal: 12,
vertical: 10,
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(option.name, style: theme.textTheme.p),
const SizedBox(height: 4),
Text(
'${option.code} · ${option.vendorName} · ${option.unitName}',
style: theme.textTheme.muted.copyWith(
fontSize: 12,
child: ListView.builder(
padding: const EdgeInsets.symmetric(vertical: 6),
itemCount: options.length,
itemBuilder: (context, index) {
final option = options.elementAt(index);
return InkWell(
onTap: () => onSelected(option),
child: Padding(
padding: const EdgeInsets.symmetric(
horizontal: 12,
vertical: 10,
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(option.name, style: theme.textTheme.p),
const SizedBox(height: 4),
Text(
'${option.code} · ${option.vendorName} · ${option.unitName}',
style: theme.textTheme.muted.copyWith(
fontSize: 12,
),
),
),
],
],
),
),
),
);
},
);
},
),
),
),
),

View File

@@ -395,6 +395,10 @@ class _InventoryWarehouseSelectFieldState
enabled: widget.enabled,
readOnly: !widget.enabled,
placeholder: placeholder,
onPressedOutside: (event) {
// 포커스를 유지하지 않으면 드롭다운이 즉시 닫히므로 재요청한다.
focusNode.requestFocus();
},
),
if (_isSearching)
const Padding(
@@ -410,41 +414,60 @@ class _InventoryWarehouseSelectFieldState
},
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 Listener(
onPointerDown: (_) {
if (!_focusNode.hasPrimaryFocus) {
_focusNode.requestFocus();
}
},
child: 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 option = options.elementAt(index);
final isAll = option.id == -1;
final label = isAll ? widget.allLabel : _displayLabel(option);
return InkWell(
onTap: () => onSelected(option),
child: Padding(
padding: const EdgeInsets.symmetric(
horizontal: 12,
vertical: 10,
return Listener(
onPointerDown: (_) {
if (!_focusNode.hasPrimaryFocus) {
_focusNode.requestFocus();
}
},
child: 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 option = options.elementAt(index);
final isAll = option.id == -1;
final label = isAll
? widget.allLabel
: _displayLabel(option);
return InkWell(
onTap: () => onSelected(option),
child: Padding(
padding: const EdgeInsets.symmetric(
horizontal: 12,
vertical: 10,
),
child: Text(label),
),
child: Text(label),
),
);
},
);
},
),
),
),
),