import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'dart:developer' as developer; import 'package:superport/screens/common/custom_widgets.dart'; import 'package:superport/utils/validators.dart'; import 'package:superport/utils/phone_utils.dart'; import 'dart:math' as math; /// 담당자 정보 위젯 /// /// 회사 및 지점의 담당자 정보를 입력받는 공통 위젯 /// SRP(단일 책임 원칙)에 따라 담당자 정보 입력 로직을 분리 class ContactInfoWidget extends StatefulWidget { /// 위젯 제목 final String title; /// 담당자 이름 컨트롤러 final TextEditingController contactNameController; /// 담당자 직책 컨트롤러 final TextEditingController contactPositionController; /// 담당자 전화번호 컨트롤러 final TextEditingController contactPhoneController; /// 담당자 이메일 컨트롤러 final TextEditingController contactEmailController; /// 직책 목록 final List positions; /// 선택된 전화번호 접두사 final String selectedPhonePrefix; /// 전화번호 접두사 목록 final List phonePrefixes; /// 직책 컴팩트 모드 (Row 또는 Column 레이아웃 결정) final bool compactMode; /// 전화번호 접두사 변경 콜백 final ValueChanged onPhonePrefixChanged; /// 담당자 이름 변경 콜백 final ValueChanged onContactNameChanged; /// 담당자 직책 변경 콜백 final ValueChanged onContactPositionChanged; /// 담당자 전화번호 변경 콜백 final ValueChanged onContactPhoneChanged; /// 담당자 이메일 변경 콜백 final ValueChanged onContactEmailChanged; const ContactInfoWidget({ Key? key, this.title = '담당자 정보', required this.contactNameController, required this.contactPositionController, required this.contactPhoneController, required this.contactEmailController, required this.positions, required this.selectedPhonePrefix, required this.phonePrefixes, required this.onPhonePrefixChanged, required this.onContactNameChanged, required this.onContactPositionChanged, required this.onContactPhoneChanged, required this.onContactEmailChanged, this.compactMode = false, }) : super(key: key); @override State createState() => _ContactInfoWidgetState(); } class _ContactInfoWidgetState extends State { bool _showPositionDropdown = false; bool _showPhonePrefixDropdown = false; final LayerLink _positionLayerLink = LayerLink(); final LayerLink _phonePrefixLayerLink = LayerLink(); OverlayEntry? _positionOverlayEntry; OverlayEntry? _phonePrefixOverlayEntry; final FocusNode _positionFocusNode = FocusNode(); final FocusNode _phonePrefixFocusNode = FocusNode(); @override void initState() { super.initState(); developer.log('ContactInfoWidget 초기화 완료', name: 'ContactInfoWidget'); _positionFocusNode.addListener(() { if (_positionFocusNode.hasFocus) { developer.log('직책 필드 포커스 얻음', name: 'ContactInfoWidget'); } else { developer.log('직책 필드 포커스 잃음', name: 'ContactInfoWidget'); } }); _phonePrefixFocusNode.addListener(() { if (_phonePrefixFocusNode.hasFocus) { developer.log('전화번호 접두사 필드 포커스 얻음', name: 'ContactInfoWidget'); } else { developer.log('전화번호 접두사 필드 포커스 잃음', name: 'ContactInfoWidget'); } }); } @override void dispose() { _removeAllOverlays(); _positionFocusNode.dispose(); _phonePrefixFocusNode.dispose(); super.dispose(); } void _togglePositionDropdown() { developer.log( '직책 드롭다운 토글: $_showPositionDropdown -> ${!_showPositionDropdown}', name: 'ContactInfoWidget', ); setState(() { if (_showPositionDropdown) { _removePositionOverlay(); } else { _showPositionDropdown = true; _showPhonePrefixDropdown = false; _removePhonePrefixOverlay(); _showPositionOverlay(); } }); } void _togglePhonePrefixDropdown() { developer.log( '전화번호 접두사 드롭다운 토글: $_showPhonePrefixDropdown -> ${!_showPhonePrefixDropdown}', name: 'ContactInfoWidget', ); setState(() { if (_showPhonePrefixDropdown) { _removePhonePrefixOverlay(); } else { _showPhonePrefixDropdown = true; _showPositionDropdown = false; _removePositionOverlay(); _showPhonePrefixOverlay(); } }); } void _removePositionOverlay() { _positionOverlayEntry?.remove(); _positionOverlayEntry = null; _showPositionDropdown = false; } void _removePhonePrefixOverlay() { _phonePrefixOverlayEntry?.remove(); _phonePrefixOverlayEntry = null; _showPhonePrefixDropdown = false; } void _removeAllOverlays() { _removePositionOverlay(); _removePhonePrefixOverlay(); } void _closeAllDropdowns() { if (_showPositionDropdown || _showPhonePrefixDropdown) { developer.log('모든 드롭다운 닫기', name: 'ContactInfoWidget'); setState(() { _removeAllOverlays(); }); } } void _showPositionOverlay() { 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 = math.min(300.0, availableHeight); _positionOverlayEntry = OverlayEntry( builder: (context) => Positioned( width: 200, child: CompositedTransformFollower( link: _positionLayerLink, 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: [ ...widget.positions.map( (position) => InkWell( onTap: () { developer.log( '직책 선택됨: $position', name: 'ContactInfoWidget', ); setState(() { widget.contactPositionController.text = position; widget.onContactPositionChanged(position); _removePositionOverlay(); }); }, child: Container( padding: const EdgeInsets.symmetric( horizontal: 16, vertical: 12, ), width: double.infinity, child: Text(position), ), ), ), InkWell( onTap: () { developer.log( '직책 기타(직접 입력) 선택됨', name: 'ContactInfoWidget', ); _removePositionOverlay(); widget.contactPositionController.clear(); widget.onContactPositionChanged(''); _positionFocusNode.requestFocus(); }, child: Container( padding: const EdgeInsets.symmetric( horizontal: 16, vertical: 12, ), width: double.infinity, child: const Text('기타 (직접 입력)'), ), ), ], ), ), ), ), ), ), ); Overlay.of(context).insert(_positionOverlayEntry!); } void _showPhonePrefixOverlay() { 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 = math.min(300.0, availableHeight); _phonePrefixOverlayEntry = OverlayEntry( builder: (context) => Positioned( width: 200, child: CompositedTransformFollower( link: _phonePrefixLayerLink, 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: [ ...widget.phonePrefixes.map( (prefix) => InkWell( onTap: () { developer.log( '전화번호 접두사 선택됨: $prefix', name: 'ContactInfoWidget', ); widget.onPhonePrefixChanged(prefix); setState(() { _removePhonePrefixOverlay(); }); }, child: Container( padding: const EdgeInsets.symmetric( horizontal: 16, vertical: 12, ), width: double.infinity, child: Text(prefix), ), ), ), InkWell( onTap: () { developer.log( '전화번호 접두사 직접 입력 선택됨', name: 'ContactInfoWidget', ); _removePhonePrefixOverlay(); _phonePrefixFocusNode.requestFocus(); }, child: Container( padding: const EdgeInsets.symmetric( horizontal: 16, vertical: 12, ), width: double.infinity, child: const Text('기타 (직접 입력)'), ), ), ], ), ), ), ), ), ), ); Overlay.of(context).insert(_phonePrefixOverlayEntry!); } @override Widget build(BuildContext context) { developer.log( 'ContactInfoWidget 빌드 시작: 직책 드롭다운=$_showPositionDropdown, 전화번호 접두사 드롭다운=$_showPhonePrefixDropdown', name: 'ContactInfoWidget', ); // 컴팩트 모드에 따라 다른 레이아웃 생성 return FormFieldWrapper( label: widget.title, child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: widget.compactMode ? _buildCompactLayout() : _buildDefaultLayout(), ), ); } // 기본 레이아웃 (한 줄에 모든 필드 표시) List _buildDefaultLayout() { return [ Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ // 담당자 이름 Expanded( flex: 3, child: TextFormField( controller: widget.contactNameController, decoration: const InputDecoration( hintText: '이름', contentPadding: EdgeInsets.symmetric( horizontal: 10, vertical: 15, ), ), onTap: () { developer.log('이름 필드 터치됨', name: 'ContactInfoWidget'); _closeAllDropdowns(); }, onChanged: widget.onContactNameChanged, ), ), const SizedBox(width: 8), // 담당자 직책 Expanded( flex: 2, child: CompositedTransformTarget( link: _positionLayerLink, child: Stack( children: [ TextFormField( controller: widget.contactPositionController, focusNode: _positionFocusNode, decoration: InputDecoration( hintText: '직책', contentPadding: const EdgeInsets.symmetric( horizontal: 10, vertical: 15, ), suffixIcon: IconButton( icon: const Icon(Icons.arrow_drop_down, size: 20), padding: EdgeInsets.zero, onPressed: () { developer.log( '직책 드롭다운 버튼 클릭됨', name: 'ContactInfoWidget', ); _togglePositionDropdown(); }, ), ), onTap: () { // 필드를 터치했을 때는 드롭다운을 열지 않고 직접 입력 모드로 진입 _closeAllDropdowns(); }, onChanged: widget.onContactPositionChanged, ), ], ), ), ), const SizedBox(width: 8), // 전화번호 접두사 Expanded( flex: 2, child: CompositedTransformTarget( link: _phonePrefixLayerLink, child: Stack( children: [ TextFormField( controller: TextEditingController( text: widget.selectedPhonePrefix, ), focusNode: _phonePrefixFocusNode, decoration: InputDecoration( hintText: '국가번호', contentPadding: const EdgeInsets.symmetric( horizontal: 10, vertical: 15, ), suffixIcon: IconButton( icon: const Icon(Icons.arrow_drop_down, size: 20), padding: EdgeInsets.zero, onPressed: () { developer.log( '전화번호 접두사 드롭다운 버튼 클릭됨', name: 'ContactInfoWidget', ); _togglePhonePrefixDropdown(); }, ), ), onTap: () { // 필드를 터치했을 때는 드롭다운을 열지 않고 직접 입력 모드로 진입 _closeAllDropdowns(); }, onChanged: (value) { if (value.isNotEmpty) { widget.onPhonePrefixChanged(value); } }, ), ], ), ), ), const SizedBox(width: 8), // 담당자 전화번호 Expanded( flex: 3, child: TextFormField( controller: widget.contactPhoneController, decoration: const InputDecoration( hintText: '전화번호', contentPadding: EdgeInsets.symmetric( horizontal: 10, vertical: 15, ), ), keyboardType: TextInputType.phone, inputFormatters: [ FilteringTextInputFormatter.digitsOnly, // 접두사에 따른 동적 포맷팅 TextInputFormatter.withFunction((oldValue, newValue) { final formatted = PhoneUtils.formatPhoneNumberByPrefix( widget.selectedPhonePrefix, newValue.text, ); return TextEditingValue( text: formatted, selection: TextSelection.collapsed(offset: formatted.length), ); }), ], onTap: () { developer.log('전화번호 필드 터치됨', name: 'ContactInfoWidget'); _closeAllDropdowns(); }, validator: validatePhoneNumber, onChanged: widget.onContactPhoneChanged, ), ), const SizedBox(width: 8), // 담당자 이메일 Expanded( flex: 6, child: TextFormField( controller: widget.contactEmailController, decoration: const InputDecoration( hintText: '이메일', contentPadding: EdgeInsets.symmetric( horizontal: 10, vertical: 15, ), ), keyboardType: TextInputType.emailAddress, onTap: () { developer.log('이메일 필드 터치됨', name: 'ContactInfoWidget'); _closeAllDropdowns(); }, validator: FormValidator.email(), onChanged: widget.onContactEmailChanged, ), ), ], ), ]; } // 컴팩트 레이아웃 (여러 줄에 필드 표시) List _buildCompactLayout() { return [ Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ // 담당자 이름 Expanded( child: TextFormField( controller: widget.contactNameController, decoration: const InputDecoration( hintText: '담당자 이름', contentPadding: EdgeInsets.symmetric( horizontal: 10, vertical: 15, ), ), onTap: () { developer.log('이름 필드 터치됨', name: 'ContactInfoWidget'); _closeAllDropdowns(); }, onChanged: widget.onContactNameChanged, ), ), const SizedBox(width: 16), // 담당자 직책 Expanded( child: CompositedTransformTarget( link: _positionLayerLink, child: InkWell( onTap: _togglePositionDropdown, child: Container( padding: const EdgeInsets.symmetric( horizontal: 12, vertical: 15, ), decoration: BoxDecoration( border: Border.all(color: Colors.grey.shade400), borderRadius: BorderRadius.circular(4), ), child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Expanded( child: Text( widget.contactPositionController.text.isEmpty ? '직책 선택' : widget.contactPositionController.text, overflow: TextOverflow.ellipsis, style: TextStyle( color: widget.contactPositionController.text.isEmpty ? Colors.grey.shade600 : Colors.black, ), ), ), const Icon(Icons.arrow_drop_down), ], ), ), ), ), ), ], ), const SizedBox(height: 16), Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ // 전화번호 (접두사 + 번호) Expanded( child: Row( children: [ // 전화번호 접두사 CompositedTransformTarget( link: _phonePrefixLayerLink, child: InkWell( onTap: _togglePhonePrefixDropdown, child: Container( width: 70, padding: const EdgeInsets.symmetric( horizontal: 8, vertical: 14, ), decoration: BoxDecoration( border: Border.all(color: Colors.grey.shade400), borderRadius: const BorderRadius.horizontal( left: Radius.circular(4), ), ), child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Text( widget.selectedPhonePrefix, style: const TextStyle(fontSize: 14), ), const Icon(Icons.arrow_drop_down, size: 18), ], ), ), ), ), // 전화번호 Expanded( child: TextFormField( controller: widget.contactPhoneController, decoration: const InputDecoration( hintText: '전화번호', border: OutlineInputBorder( borderRadius: BorderRadius.horizontal( left: Radius.zero, right: Radius.circular(4), ), ), contentPadding: EdgeInsets.symmetric( horizontal: 10, vertical: 15, ), ), inputFormatters: [ FilteringTextInputFormatter.digitsOnly, // 접두사에 따른 동적 포맷팅 TextInputFormatter.withFunction((oldValue, newValue) { final formatted = PhoneUtils.formatPhoneNumberByPrefix( widget.selectedPhonePrefix, newValue.text, ); return TextEditingValue( text: formatted, selection: TextSelection.collapsed(offset: formatted.length), ); }), ], keyboardType: TextInputType.phone, onTap: _closeAllDropdowns, onChanged: widget.onContactPhoneChanged, validator: validatePhoneNumber, ), ), ], ), ), const SizedBox(width: 16), // 이메일 Expanded( child: TextFormField( controller: widget.contactEmailController, decoration: const InputDecoration( hintText: '이메일 주소', contentPadding: EdgeInsets.symmetric( horizontal: 10, vertical: 15, ), ), keyboardType: TextInputType.emailAddress, onTap: _closeAllDropdowns, onChanged: widget.onContactEmailChanged, validator: FormValidator.email(), ), ), ], ), ]; } }