import 'package:flutter/material.dart'; import 'package:superport/screens/common/custom_widgets.dart'; import 'package:superport/screens/common/theme_tailwind.dart'; import 'package:superport/utils/address_constants.dart'; import 'package:superport/models/address_model.dart'; /// 주소 입력 컴포넌트 /// /// 우편번호, 시/도 드롭다운, 상세주소로 구성된 주소 입력 폼입니다. /// 1행 3열 구조로 배치되어 있으며, 각 필드는 SRP 원칙에 따라 개별적으로 관리됩니다. class AddressInput extends StatefulWidget { /// 최초 우편번호 값 final String initialZipCode; /// 최초 시/도 값 final String initialRegion; /// 최초 상세 주소 값 final String initialDetailAddress; /// 주소가 변경될 때 호출되는 콜백 함수 /// zipCode, region, detailAddress를 매개변수로 전달합니다. final Function(String zipCode, String region, String detailAddress) onAddressChanged; /// 필수 입력 여부 final bool isRequired; const AddressInput({ Key? key, this.initialZipCode = '', this.initialRegion = '', this.initialDetailAddress = '', required this.onAddressChanged, this.isRequired = false, }) : super(key: key); @override State createState() => _AddressInputState(); /// Address 객체를 받아 읽기 전용으로 표시하는 위젯 static Widget readonly({required Address address}) { // 회사 리스트와 동일하게 address.toString() 사용, 스타일도 bodyStyle로 통일 return Text(address.toString(), style: AppThemeTailwind.bodyStyle); } } class _AddressInputState extends State { // 텍스트 컨트롤러 late TextEditingController _zipCodeController; late TextEditingController _detailAddressController; // 드롭다운 관련 변수 String _selectedRegion = ''; bool _showRegionDropdown = false; // 레이어 링크 (드롭다운 위치 조정용) final LayerLink _regionLayerLink = LayerLink(); // 오버레이 엔트리 (드롭다운 메뉴) OverlayEntry? _regionOverlayEntry; // 포커스 노드 final FocusNode _regionFocusNode = FocusNode(); @override void initState() { super.initState(); _zipCodeController = TextEditingController(text: widget.initialZipCode); _selectedRegion = widget.initialRegion; _detailAddressController = TextEditingController( text: widget.initialDetailAddress, ); // 컨트롤러 변경 리스너 등록 _zipCodeController.addListener(_notifyAddressChanged); _detailAddressController.addListener(_notifyAddressChanged); } @override void dispose() { _zipCodeController.dispose(); _detailAddressController.dispose(); _removeRegionOverlay(); _regionFocusNode.dispose(); super.dispose(); } /// 주소 변경을 상위 위젯에 알립니다. void _notifyAddressChanged() { widget.onAddressChanged( _zipCodeController.text, _selectedRegion, _detailAddressController.text, ); } /// 시/도 드롭다운을 토글합니다. void _toggleRegionDropdown() { setState(() { if (_showRegionDropdown) { _removeRegionOverlay(); } else { _showRegionDropdown = true; _showRegionOverlay(); } }); } /// 시/도 드롭다운 오버레이를 제거합니다. void _removeRegionOverlay() { _regionOverlayEntry?.remove(); _regionOverlayEntry = null; _showRegionDropdown = false; } /// 시/도 드롭다운 오버레이를 표시합니다. void _showRegionOverlay() { final RenderBox renderBox = context.findRenderObject() as RenderBox; final size = renderBox.size; final offset = renderBox.localToGlobal(Offset.zero); final availableHeight = MediaQuery.of(context).size.height - offset.dy - 100; final maxHeight = 300.0 < availableHeight ? 300.0 : availableHeight; _regionOverlayEntry = OverlayEntry( builder: (context) => Positioned( width: 200, child: CompositedTransformFollower( link: _regionLayerLink, 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.withOpacity(0.3), spreadRadius: 1, blurRadius: 3, offset: const Offset(0, 1), ), ], ), constraints: BoxConstraints(maxHeight: maxHeight), child: SingleChildScrollView( child: Column( mainAxisSize: MainAxisSize.min, children: [ ...KoreanRegions.topLevel.map( (region) => InkWell( onTap: () { setState(() { _selectedRegion = region; _removeRegionOverlay(); _notifyAddressChanged(); }); }, child: Container( padding: const EdgeInsets.symmetric( horizontal: 16, vertical: 12, ), width: double.infinity, height: 48, child: Text( region, style: AppThemeTailwind.bodyStyle.copyWith( fontSize: 16, ), ), ), ), ), ], ), ), ), ), ), ), ); Overlay.of(context).insert(_regionOverlayEntry!); } @override Widget build(BuildContext context) { return FormFieldWrapper( label: '주소', isRequired: widget.isRequired, child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ // 우편번호 입력 필드 (1열) Expanded( flex: 2, child: TextField( controller: _zipCodeController, decoration: InputDecoration( hintText: AddressLabels.zipCodeHint, labelText: AddressLabels.zipCode, border: const OutlineInputBorder(), ), ), ), const SizedBox(width: 8), // 시/도 선택 드롭다운 (2열) Expanded( flex: 3, child: CompositedTransformTarget( link: _regionLayerLink, child: InkWell( onTap: _toggleRegionDropdown, child: Container( padding: const EdgeInsets.symmetric( horizontal: 12, vertical: 0, ), height: 48, decoration: BoxDecoration( border: Border.all(color: Colors.grey.shade400), borderRadius: BorderRadius.circular(4), ), child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Text( _selectedRegion.isEmpty ? AddressLabels.regionHint : _selectedRegion, style: TextStyle( fontSize: 16, color: _selectedRegion.isEmpty ? Colors.grey.shade600 : Colors.black, ), ), const Icon(Icons.arrow_drop_down), ], ), ), ), ), ), const SizedBox(width: 8), // 상세 주소 입력 필드 (3열) Expanded( flex: 7, child: TextField( controller: _detailAddressController, decoration: InputDecoration( hintText: AddressLabels.detailHint, labelText: AddressLabels.detail, border: const OutlineInputBorder(), ), ), ), ], ), ], ), ); } }