import 'package:flutter/material.dart'; /// 드롭다운 기능이 있는 재사용 가능한 TextFormField 위젯 class CustomDropdownField extends StatefulWidget { final String label; final String hint; final bool required; final TextEditingController controller; final FocusNode focusNode; final List items; final Function(String) onChanged; final Function(String)? onFieldSubmitted; final String? Function(String)? getAutocompleteSuggestion; final VoidCallback onDropdownPressed; final LayerLink layerLink; final GlobalKey fieldKey; const CustomDropdownField({ Key? key, required this.label, required this.hint, required this.required, required this.controller, required this.focusNode, required this.items, required this.onChanged, this.onFieldSubmitted, this.getAutocompleteSuggestion, required this.onDropdownPressed, required this.layerLink, required this.fieldKey, }) : super(key: key); @override State createState() => _CustomDropdownFieldState(); } class _CustomDropdownFieldState extends State { bool _isProgrammaticChange = false; OverlayEntry? _overlayEntry; @override void dispose() { _removeDropdown(); super.dispose(); } void _showDropdown() { _removeDropdown(); final RenderBox renderBox = widget.fieldKey.currentContext!.findRenderObject() as RenderBox; final size = renderBox.size; _overlayEntry = OverlayEntry( builder: (context) => Positioned( width: size.width, child: CompositedTransformFollower( link: widget.layerLink, showWhenUnlinked: false, offset: const Offset(0, 45), child: Material( elevation: 4, borderRadius: BorderRadius.circular(4), child: Container( decoration: BoxDecoration( color: Colors.white, border: Border.all(color: Colors.grey.shade300), borderRadius: BorderRadius.circular(4), boxShadow: [ BoxShadow( color: Colors.grey.withValues(alpha: 0.3), spreadRadius: 1, blurRadius: 3, offset: const Offset(0, 1), ), ], ), constraints: const BoxConstraints(maxHeight: 200), child: SingleChildScrollView( child: Column( mainAxisSize: MainAxisSize.min, children: widget.items.map((item) { return GestureDetector( behavior: HitTestBehavior.opaque, onTap: () { setState(() { _isProgrammaticChange = true; widget.controller.text = item; }); widget.onChanged(item); WidgetsBinding.instance.addPostFrameCallback((_) { _isProgrammaticChange = false; }); _removeDropdown(); }, child: Container( padding: const EdgeInsets.symmetric( horizontal: 16, vertical: 12, ), width: double.infinity, child: Text(item), ), ); }).toList(), ), ), ), ), ), ), ); Overlay.of(context).insert(_overlayEntry!); } void _removeDropdown() { if (_overlayEntry != null) { _overlayEntry!.remove(); _overlayEntry = null; } } @override Widget build(BuildContext context) { return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ CompositedTransformTarget( link: widget.layerLink, child: TextFormField( key: widget.fieldKey, controller: widget.controller, focusNode: widget.focusNode, decoration: InputDecoration( labelText: widget.label, hintText: widget.hint, suffixIcon: IconButton( icon: const Icon(Icons.arrow_drop_down), onPressed: () { widget.onDropdownPressed(); _showDropdown(); }, ), ), onChanged: (value) { if (!_isProgrammaticChange) { widget.onChanged(value); } }, onFieldSubmitted: widget.onFieldSubmitted, ), ), // 자동완성 후보 표시 if (widget.getAutocompleteSuggestion != null) Builder( builder: (context) { final suggestion = widget.getAutocompleteSuggestion!(widget.controller.text); if (suggestion != null && suggestion.length > widget.controller.text.length) { return Padding( padding: const EdgeInsets.only(left: 12, top: 2), child: Text( suggestion, style: const TextStyle( color: Color(0xFF1976D2), fontWeight: FontWeight.bold, fontSize: 13, ), ), ); } return const SizedBox.shrink(); }, ), ], ); } }