import 'package:flutter/material.dart'; /// 자동완성 텍스트 필드 위젯 /// /// 입력, 드롭다운, 포커스, 필터링, 선택 기능을 모두 포함한다. class AutocompleteTextField extends StatefulWidget { final String label; final String value; final List items; final bool isRequired; final String hintText; final void Function(String) onChanged; final void Function(String) onSelected; final FocusNode? focusNode; const AutocompleteTextField({ Key? key, required this.label, required this.value, required this.items, required this.onChanged, required this.onSelected, this.isRequired = false, this.hintText = '', this.focusNode, }) : super(key: key); @override State createState() => _AutocompleteTextFieldState(); } class _AutocompleteTextFieldState extends State { late final TextEditingController _controller; late final FocusNode _focusNode; late List _filteredItems; bool _showDropdown = false; @override void initState() { super.initState(); _controller = TextEditingController(text: widget.value); _focusNode = widget.focusNode ?? FocusNode(); _filteredItems = List.from(widget.items); _controller.addListener(_onTextChanged); _focusNode.addListener(() { setState(() { if (_focusNode.hasFocus) { _showDropdown = _filteredItems.isNotEmpty; } else { _showDropdown = false; } }); }); } @override void didUpdateWidget(covariant AutocompleteTextField oldWidget) { super.didUpdateWidget(oldWidget); if (widget.value != _controller.text) { _controller.text = widget.value; } if (widget.items != oldWidget.items) { _filteredItems = List.from(widget.items); } } @override void dispose() { if (widget.focusNode == null) { _focusNode.dispose(); } _controller.dispose(); super.dispose(); } // 입력값 변경 시 필터링 void _onTextChanged() { final text = _controller.text; setState(() { if (text.isEmpty) { _filteredItems = List.from(widget.items); } else { _filteredItems = widget.items .where( (item) => item.toLowerCase().contains(text.toLowerCase()), ) .toList(); // 시작 부분이 일치하는 항목 우선 정렬 _filteredItems.sort((a, b) { bool aStartsWith = a.toLowerCase().startsWith(text.toLowerCase()); bool bStartsWith = b.toLowerCase().startsWith(text.toLowerCase()); if (aStartsWith && !bStartsWith) return -1; if (!aStartsWith && bStartsWith) return 1; return a.compareTo(b); }); } _showDropdown = _filteredItems.isNotEmpty && _focusNode.hasFocus; widget.onChanged(text); }); } void _handleSelect(String value) { setState(() { _controller.text = value; _showDropdown = false; }); widget.onSelected(value); } @override Widget build(BuildContext context) { return Stack( children: [ TextFormField( controller: _controller, focusNode: _focusNode, decoration: InputDecoration( labelText: widget.label, hintText: widget.hintText, ), validator: (value) { if (widget.isRequired && (value == null || value.isEmpty)) { return '${widget.label}을(를) 입력해주세요'; } return null; }, onSaved: (value) { widget.onSelected(value ?? ''); }, ), if (_showDropdown) Positioned( top: 50, left: 0, right: 0, child: Container( decoration: BoxDecoration( color: Colors.white, border: Border.all(color: Colors.grey.shade300), borderRadius: BorderRadius.circular(4), boxShadow: [ BoxShadow( color: Colors.grey.withOpacity(0.3), blurRadius: 4, offset: const Offset(0, 2), ), ], ), constraints: const BoxConstraints(maxHeight: 200), child: ListView.builder( shrinkWrap: true, itemCount: _filteredItems.length, itemBuilder: (context, index) { return InkWell( onTap: () => _handleSelect(_filteredItems[index]), child: Container( padding: const EdgeInsets.symmetric( horizontal: 12, vertical: 8, ), child: Text(_filteredItems[index]), ), ); }, ), ), ), ], ); } }