import 'package:flutter/material.dart'; import 'package:flutter/foundation.dart'; // kDebugMode 사용 /// 드롭다운 + 자동완성 + 텍스트 입력을 모두 지원하는 공통 위젯 /// /// - 텍스트 입력 시 자동완성 추천 리스트 노출 /// - 드롭다운 버튼 클릭 시 전체 리스트 노출 /// - 직접 입력, 선택 모두 가능 /// - 재사용성 및 SRP 준수 class AutocompleteDropdownField 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 bool enabled; const AutocompleteDropdownField({ Key? key, required this.label, required this.value, required this.items, required this.onChanged, required this.onSelected, this.isRequired = false, this.hintText = '', this.enabled = true, }) : super(key: key); @override State createState() => _AutocompleteDropdownFieldState(); } class _AutocompleteDropdownFieldState extends State { late TextEditingController _controller; late final FocusNode _focusNode; late List _filteredItems; bool _showDropdown = false; // 위젯 고유 키 추가 (동적 값 기반 키 대신 고정된 ValueKey 사용) final GlobalKey _fieldKey = GlobalKey(); @override void initState() { super.initState(); _controller = TextEditingController(text: widget.value); if (kDebugMode) { print( '[AutocompleteDropdownField:initState] label=${widget.label}, value=${widget.value}', ); } _focusNode = FocusNode(); _filteredItems = List.from(widget.items); _controller.addListener(_onTextChanged); _focusNode.addListener(_handleFocusChange); } @override void didUpdateWidget(covariant AutocompleteDropdownField oldWidget) { super.didUpdateWidget(oldWidget); // 항상 부모의 value와 내부 컨트롤러를 동기화 (동기화 누락 방지) _controller.text = widget.value; if (kDebugMode) { print( '[AutocompleteDropdownField:didUpdateWidget] label=${widget.label}, value 동기화: widget.value=${widget.value}, controller.text=${_controller.text}', ); } if (widget.items != oldWidget.items) { _filteredItems = List.from(widget.items); } } @override void dispose() { _focusNode.dispose(); _controller.dispose(); super.dispose(); } void _handleFocusChange() { setState(() { // 포커스가 있고 필터링된 아이템이 있을 때만 드롭다운 표시 _showDropdown = _focusNode.hasFocus && _filteredItems.isNotEmpty; // 포커스가 없으면 드롭다운 닫기 if (!_focusNode.hasFocus) { _showDropdown = false; } }); if (kDebugMode) { print( '[AutocompleteDropdownField:_handleFocusChange] label=${widget.label}, hasFocus=${_focusNode.hasFocus}, showDropdown=$_showDropdown', ); } } void _onTextChanged() { final text = _controller.text; if (kDebugMode) { print( '[AutocompleteDropdownField:_onTextChanged] label=${widget.label}, text=$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) { if (kDebugMode) { print( '[AutocompleteDropdownField:_handleSelect] 선택값=$value, 이전 값=${_controller.text}', ); } // 1. 값 전달 (부모 콜백) widget.onChanged(value); // 입력값 변경 콜백 widget.onSelected(value); // 선택 콜백 // 2. 부모 setState 이후, 프레임이 끝난 뒤 드롭다운 닫기 (즉각 반영 보장) WidgetsBinding.instance.addPostFrameCallback((_) { setState(() { _controller.text = value; _controller.selection = TextSelection.collapsed(offset: value.length); _showDropdown = false; }); _focusNode.unfocus(); }); if (kDebugMode) { print( '[AutocompleteDropdownField:_handleSelect] 업데이트 완료: controller.text=${_controller.text}', ); } } void _toggleDropdown() { setState(() { _showDropdown = !_showDropdown && _filteredItems.isNotEmpty; if (_showDropdown) { _focusNode.requestFocus(); } }); } @override Widget build(BuildContext context) { return Stack( key: _fieldKey, // 고정된 키 사용 children: [ TextFormField( controller: _controller, focusNode: _focusNode, enabled: widget.enabled, decoration: InputDecoration( labelText: widget.label, hintText: widget.hintText, suffixIcon: Row( mainAxisSize: MainAxisSize.min, children: [ if (_controller.text.isNotEmpty) IconButton( icon: const Icon(Icons.clear), onPressed: widget.enabled ? () { setState(() { _controller.clear(); _filteredItems = List.from(widget.items); _showDropdown = _focusNode.hasFocus && _filteredItems.isNotEmpty; widget.onSelected(''); }); } : null, ), IconButton( icon: const Icon(Icons.arrow_drop_down), onPressed: widget.enabled ? _toggleDropdown : null, ), ], ), ), validator: (value) { if (widget.isRequired && (value == null || value.isEmpty)) { return '${widget.label}을(를) 입력해주세요'; } return null; }, onSaved: (value) { widget.onSelected(value ?? ''); }, ), if (_showDropdown) Positioned( left: 0, right: 0, top: 56, // TextFormField 높이만큼 아래로 child: Material( elevation: 4, borderRadius: BorderRadius.circular(4), child: ConstrainedBox( constraints: const BoxConstraints(maxHeight: 200), child: ListView.builder( shrinkWrap: true, itemCount: _filteredItems.length, itemBuilder: (context, index) { return GestureDetector( behavior: HitTestBehavior.opaque, onTap: () { if (kDebugMode) { print( '[AutocompleteDropdownField:GestureDetector:onTap] label=${widget.label}, 선택값=${_filteredItems[index]}', ); } _handleSelect(_filteredItems[index]); }, child: Container( padding: const EdgeInsets.symmetric( horizontal: 12, vertical: 8, ), child: Text(_filteredItems[index]), ), ); }, ), ), ), ), ], ); } }