import 'dart:io'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_svg/flutter_svg.dart'; import 'package:image_picker/image_picker.dart'; import 'package:flutter_screenutil/flutter_screenutil.dart'; import '../theme/app_colors.dart'; import '../data/pet_data.dart'; import '../services/firestore_service.dart'; import '../models/pet_model.dart'; class PetFormScreen extends StatefulWidget { final Pet? petToEdit; const PetFormScreen({super.key, this.petToEdit}); @override State createState() => _PetFormScreenState(); } class _PetFormScreenState extends State { // 정확한 날짜를 몰라요 상태 bool _isDateUnknown = false; final TextEditingController _monthController = TextEditingController(); final TextEditingController _yearController = TextEditingController(); final TextEditingController _dayController = TextEditingController(); final TextEditingController _registrationNumberController = TextEditingController(); final FocusNode _yearFocus = FocusNode(); final FocusNode _monthFocus = FocusNode(); final FocusNode _dayFocus = FocusNode(); bool get _isFormValid { if (_nameController.text.trim().isEmpty) return false; if (_speciesController.text.trim().isEmpty) return false; if (_breedController.text.trim().isEmpty) return false; if (_selectedGender == null) return false; if (!_isDateUnknown) { if (_yearController.text.length != 4 || _monthController.text.length != 2 || _dayController.text.length != 2) { return false; } } return true; } @override void initState() { super.initState(); // 수정 모드일 경우 초기값 설정 if (widget.petToEdit != null) { _initializeData(widget.petToEdit!); } // 폼 상태 변경 감지를 위한 리스너 등록 _nameController.addListener(_updateState); _speciesController.addListener(_updateState); _breedController.addListener(_updateState); _yearController.addListener(_updateState); _monthController.addListener(_updateState); _dayController.addListener(_updateState); _weightController.addListener(_updateState); _registrationNumberController.addListener(_updateState); // 추가 } void _initializeData(Pet pet) { // 1. 기본 정보 설정 _nameController.text = pet.name; _speciesController.text = pet.species; _breedController.text = pet.breed; _selectedGender = pet.gender; _isNeutered = pet.isNeutered; _isDateUnknown = pet.isDateUnknown; // 성별 텍스트 설정 if (_selectedGender == '기타') { _genderController.text = '기타'; } else { _genderController.text = '$_selectedGender${_isNeutered ? '(중성화)' : ''}'; } // 2. 날짜 설정 if (!_isDateUnknown && pet.birthDate != null) { _yearController.text = pet.birthDate!.year.toString(); _monthController.text = pet.birthDate!.month.toString().padLeft(2, '0'); _dayController.text = pet.birthDate!.day.toString().padLeft(2, '0'); } // 3. 등록번호 & 체중 if (pet.registrationNumber != null) { _registrationNumberController.text = pet.registrationNumber!; } if (pet.weight != null) { _weightController.text = pet.weight.toString(); } // 4. 질환 목록 파싱 _parseAndSetDiseases(pet.diseases, (s, o, text) { _selectedDiseases = s; _otherDiseaseText = o; _diseaseController.text = text; }); _parseAndSetDiseases(pet.pastDiseases, (s, o, text) { _selectedPastDiseases = s; _otherPastDiseaseText = o; _pastDiseaseController.text = text; }); _parseAndSetDiseases(pet.healthConcerns, (s, o, text) { _selectedHealthConcerns = s; _otherHealthConcernText = o; _healthConcernController.text = text; }); } void _parseAndSetDiseases( List source, Function(List, String, String) onSet, ) { List selected = []; String otherText = ''; for (var item in source) { if (item.startsWith('기타(') && item.endsWith(')')) { selected.add('기타'); otherText = item.substring(3, item.length - 1); } else { selected.add(item); } } // 화면 표시 텍스트 생성 List displayList = selected.where((e) => e != '기타').toList(); if (selected.contains('기타') && otherText.isNotEmpty) { displayList.add('기타($otherText)'); } else if (selected.contains('기타')) { displayList.add('기타'); } onSet(selected, otherText, displayList.join(', ')); } void _updateState() { setState(() {}); } final TextEditingController _nameController = TextEditingController(); final TextEditingController _speciesController = TextEditingController(); final TextEditingController _breedController = TextEditingController(); final TextEditingController _genderController = TextEditingController(); final TextEditingController _weightController = TextEditingController(); // 선택된 종 정보 (품종 선택을 위해 필요) String? _currentMajorCategory; String? _currentMinorCategory; String? _selectedGender; bool _isNeutered = false; List _selectedDiseases = []; String _otherDiseaseText = ''; final TextEditingController _diseaseController = TextEditingController(); List _selectedPastDiseases = []; File? _profileImage; final ImagePicker _picker = ImagePicker(); void _pickImage() { showModalBottomSheet( context: context, backgroundColor: Colors.transparent, builder: (context) { return Container( decoration: const BoxDecoration( color: Colors.white, borderRadius: BorderRadius.vertical(top: Radius.circular(20)), ), child: Column( mainAxisSize: MainAxisSize.min, children: [ SizedBox(height: 20.h), Text( '프로필 사진 설정', style: TextStyle( fontFamily: 'SCDream', fontSize: 18.sp, fontWeight: FontWeight.bold, ), ), SizedBox(height: 20.h), ListTile( leading: Icon( Icons.camera_alt, color: Colors.black, size: 24.w, ), title: Text( '카메라로 촬영', style: TextStyle(fontFamily: 'SCDream', fontSize: 16.sp), ), onTap: () async { Navigator.pop(context); final XFile? image = await _picker.pickImage( source: ImageSource.camera, ); if (image != null) setState(() => _profileImage = File(image.path)); }, ), ListTile( leading: Icon( Icons.photo_library, color: Colors.black, size: 24.w, ), title: Text( '갤러리에서 선택', style: TextStyle(fontFamily: 'SCDream', fontSize: 16.sp), ), onTap: () async { Navigator.pop(context); final XFile? image = await _picker.pickImage( source: ImageSource.gallery, ); if (image != null) setState(() => _profileImage = File(image.path)); }, ), ListTile( leading: Icon( Icons.delete_outline, color: Colors.black, size: 24.w, ), title: Text( '기본 이미지로 변경', style: TextStyle( fontFamily: 'SCDream', fontSize: 16.sp, color: Colors.black, ), ), onTap: () { Navigator.pop(context); setState(() => _profileImage = null); }, ), SizedBox(height: 20.h), ], ), ); }, ); } String _otherPastDiseaseText = ''; final TextEditingController _pastDiseaseController = TextEditingController(); List _selectedHealthConcerns = []; String _otherHealthConcernText = ''; final TextEditingController _healthConcernController = TextEditingController(); @override void dispose() { _nameController.dispose(); _speciesController.dispose(); _breedController.dispose(); _genderController.dispose(); _yearController.dispose(); _monthController.dispose(); _dayController.dispose(); _diseaseController.dispose(); _pastDiseaseController.dispose(); _healthConcernController.dispose(); _yearFocus.dispose(); _monthFocus.dispose(); _dayFocus.dispose(); _registrationNumberController.dispose(); _weightController.dispose(); super.dispose(); } bool _isLoading = false; Future _submitForm() async { setState(() => _isLoading = true); try { final firestoreService = FirestoreService(); final userId = firestoreService.getCurrentUserId(); if (userId == null) { throw Exception('로그인이 필요합니다.'); } DateTime? birthDate; if (!_isDateUnknown) { birthDate = DateTime( int.parse(_yearController.text), int.parse(_monthController.text), int.parse(_dayController.text), ); } List finalDiseases = List.from(_selectedDiseases); if (finalDiseases.contains('기타') && _otherDiseaseText.isNotEmpty) { finalDiseases.remove('기타'); finalDiseases.add('기타($_otherDiseaseText)'); } List finalPastDiseases = List.from(_selectedPastDiseases); if (finalPastDiseases.contains('기타') && _otherPastDiseaseText.isNotEmpty) { finalPastDiseases.remove('기타'); finalPastDiseases.add('기타($_otherPastDiseaseText)'); } List finalHealthConcerns = List.from(_selectedHealthConcerns); if (finalHealthConcerns.contains('기타') && _otherHealthConcernText.isNotEmpty) { finalHealthConcerns.remove('기타'); finalHealthConcerns.add('기타($_otherHealthConcernText)'); } // 수정 모드 if (widget.petToEdit != null) { final updatedPet = Pet( id: widget.petToEdit!.id, ownerId: widget.petToEdit!.ownerId, name: _nameController.text, species: _speciesController.text, breed: _breedController.text, gender: _selectedGender!, isNeutered: _isNeutered, birthDate: birthDate, isDateUnknown: _isDateUnknown, registrationNumber: _registrationNumberController.text.isNotEmpty ? _registrationNumberController.text : null, profileImageUrl: widget.petToEdit!.profileImageUrl, // Service에서 업데이트 처리 weight: _weightController.text.isNotEmpty ? double.tryParse(_weightController.text) : null, diseases: finalDiseases, pastDiseases: finalPastDiseases, healthConcerns: finalHealthConcerns, createdAt: widget.petToEdit!.createdAt, ); await firestoreService.updatePet(updatedPet, _profileImage); if (!mounted) return; ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: Row( children: [ const Icon(Icons.check_circle, color: Colors.white), SizedBox(width: 10.w), const Text( '수정이 완료되었습니다.', style: TextStyle(fontWeight: FontWeight.bold), ), ], ), backgroundColor: AppColors.highlight, behavior: SnackBarBehavior.floating, shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(10.r), ), margin: EdgeInsets.all(20.w), duration: const Duration(seconds: 2), ), ); Navigator.pop(context); } // 등록 모드 else { final newPet = Pet( id: firestoreService.generatePetId(), ownerId: userId, name: _nameController.text, species: _speciesController.text, breed: _breedController.text, gender: _selectedGender!, isNeutered: _isNeutered, birthDate: birthDate, isDateUnknown: _isDateUnknown, registrationNumber: _registrationNumberController.text.isNotEmpty ? _registrationNumberController.text : null, profileImageUrl: null, // Service에서 처리 weight: _weightController.text.isNotEmpty ? double.tryParse(_weightController.text) : null, diseases: finalDiseases, pastDiseases: finalPastDiseases, healthConcerns: finalHealthConcerns, createdAt: DateTime.now(), ); await firestoreService.registerPet(newPet, _profileImage); if (!mounted) return; Navigator.pushReplacementNamed(context, '/register_complete'); } } catch (e) { if (!mounted) return; ScaffoldMessenger.of( context, ).showSnackBar(SnackBar(content: Text('처리 실패: $e'))); } finally { if (mounted) setState(() => _isLoading = false); } } void _toggleDateUnknown() { setState(() { _isDateUnknown = !_isDateUnknown; if (_isDateUnknown) { _yearController.clear(); _monthController.clear(); _dayController.clear(); } }); } // --- UI Helpers & Modals (Copied and adapted from registration screen) --- // 공통 선택 모달 (보유 질환, 과거 진단, 염려 건강) - Generic Selection Modal void _showSelectionModal({ required String title, required List currentSelected, required String currentOtherText, required Function(List, String) onComplete, }) { showModalBottomSheet( context: context, backgroundColor: Colors.transparent, isScrollControlled: true, builder: (context) { List tempSelected = List.from(currentSelected); final TextEditingController otherInputController = TextEditingController(text: currentOtherText); return StatefulBuilder( builder: (BuildContext context, StateSetter setModalState) { final bottomInset = MediaQuery.of(context).viewInsets.bottom; return Container( height: 0.85.sh, margin: EdgeInsets.only(top: 50.h), decoration: BoxDecoration( color: Colors.white, borderRadius: BorderRadius.vertical(top: Radius.circular(20.r)), ), child: Column( children: [ SizedBox(height: 20.h), Text( title, style: TextStyle( fontFamily: 'SCDream', fontSize: 18.sp, fontWeight: FontWeight.bold, color: Colors.black, ), ), SizedBox(height: 10.h), Divider(color: const Color(0xFFEEEEEE), thickness: 1.h), Expanded( child: ListView.builder( itemCount: PetData.diseaseList.length, padding: const EdgeInsets.symmetric(vertical: 10), itemBuilder: (context, index) { final disease = PetData.diseaseList[index]; final isSelected = tempSelected.contains(disease); return Column( children: [ InkWell( onTap: () { setModalState(() { if (isSelected) tempSelected.remove(disease); else tempSelected.add(disease); }); }, child: Padding( padding: const EdgeInsets.symmetric( horizontal: 20, vertical: 16, ), child: Row( children: [ Icon( Icons.check, size: 20, color: isSelected ? AppColors.highlight : Colors.grey[300], ), const SizedBox(width: 12), Expanded( child: Text( disease, style: TextStyle( fontFamily: 'SCDream', fontSize: 16, fontWeight: isSelected ? FontWeight.bold : FontWeight.normal, color: isSelected ? AppColors.highlight : Colors.black, ), ), ), ], ), ), ), if (isSelected && disease == "기타") Padding( padding: EdgeInsets.fromLTRB( 52.w, 0, 20.w, 10.h, ), child: TextField( controller: otherInputController, autofocus: true, decoration: InputDecoration( hintText: '직접 입력해 주세요', isDense: true, contentPadding: EdgeInsets.symmetric( vertical: 10.h, horizontal: 10.w, ), border: const OutlineInputBorder(), ), ), ), ], ); }, ), ), Padding( padding: EdgeInsets.fromLTRB( 20.w, 20.h, 20.w, 20.h + bottomInset, ), child: Row( children: [ InkWell( onTap: () => setModalState(() { tempSelected.clear(); otherInputController.clear(); }), child: Container( height: 52.h, padding: EdgeInsets.symmetric(horizontal: 20.w), decoration: BoxDecoration( color: const Color(0xFF333333), borderRadius: BorderRadius.circular(12.r), ), child: Row( children: [ Icon( Icons.refresh, color: Colors.white, size: 20.w, ), SizedBox(width: 4.w), const Text( '초기화', style: TextStyle( fontFamily: 'SCDream', color: Colors.white, fontWeight: FontWeight.bold, ), ), ], ), ), ), SizedBox(width: 12.w), Expanded( child: InkWell( onTap: () { onComplete( tempSelected, otherInputController.text, ); Navigator.pop(context); }, child: Container( height: 52.h, decoration: BoxDecoration( color: AppColors.highlight, borderRadius: BorderRadius.circular(12.r), ), child: Center( child: Text( '선택 완료', style: TextStyle( fontFamily: 'SCDream', color: Colors.white, fontSize: 16.sp, fontWeight: FontWeight.bold, ), ), ), ), ), ), ], ), ), ], ), ); }, ); }, ); } void _showSpeciesSelectionModal() { showModalBottomSheet( context: context, backgroundColor: Colors.transparent, isScrollControlled: true, builder: (context) { String? selectedMajor; // 모달 내부 임시 상태 (대분류) bool showInput = false; // 직접 입력 창 표시 여부 final TextEditingController speciesInputController = TextEditingController(); return StatefulBuilder( builder: (BuildContext context, StateSetter setModalState) { final bottomInset = MediaQuery.of(context).viewInsets.bottom; return Container( height: 0.6.sh, margin: EdgeInsets.only(top: 50.h), decoration: BoxDecoration( color: Colors.white, borderRadius: BorderRadius.vertical(top: Radius.circular(20.r)), ), child: Column( children: [ Padding( padding: EdgeInsets.symmetric( horizontal: 16.w, vertical: 12.h, ), child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ (selectedMajor != null || showInput) ? GestureDetector( onTap: () => setModalState(() { if (showInput) showInput = false; else selectedMajor = null; }), child: Icon( Icons.arrow_back_ios, size: 20.w, color: Colors.black, ), ) : SizedBox(width: 20.w), Text( showInput ? '직접 입력' : (selectedMajor == null ? '대분류' : '중분류'), style: TextStyle( fontFamily: 'SCDream', fontSize: 18.sp, fontWeight: FontWeight.bold, ), ), GestureDetector( onTap: () => Navigator.pop(context), child: Icon( Icons.close, size: 24.w, color: Colors.black, ), ), ], ), ), Divider(color: const Color(0xFFEEEEEE), thickness: 1.h), Expanded( child: showInput ? Padding( padding: EdgeInsets.all(20.w), child: Column( children: [ Text( '반려동물의 종을 직접 입력해주세요.', style: TextStyle( fontFamily: 'SCDream', fontSize: 16.sp, ), ), SizedBox(height: 20.h), TextField( controller: speciesInputController, autofocus: true, decoration: const InputDecoration( hintText: '예: 미어캣', border: OutlineInputBorder(), ), ), Spacer(), SizedBox( width: double.infinity, height: 52.h, child: ElevatedButton( onPressed: () { if (speciesInputController .text .isNotEmpty) { setState(() { _speciesController.text = speciesInputController.text; _currentMajorCategory = null; _currentMinorCategory = null; _breedController.clear(); }); Navigator.pop(context); } }, style: ElevatedButton.styleFrom( backgroundColor: AppColors.highlight, shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular( 12.r, ), ), ), child: Text( '완료', style: TextStyle( fontFamily: 'SCDream', fontSize: 16.sp, fontWeight: FontWeight.bold, color: Colors.white, ), ), ), ), ], ), ) : ListView.builder( itemCount: selectedMajor == null ? PetData.breedsData.keys.length : PetData.breedsData[selectedMajor]!.length, itemBuilder: (context, index) { if (selectedMajor == null) { final major = PetData.breedsData.keys.elementAt( index, ); return ListTile( title: Text( major, style: TextStyle( fontFamily: 'SCDream', fontSize: 16, ), ), trailing: Icon( Icons.arrow_forward_ios, size: 16, color: Colors.grey, ), onTap: () => setModalState(() { if (major == '기타(직접 입력)') showInput = true; else selectedMajor = major; }), ); } else { final minor = PetData .breedsData[selectedMajor]! .keys .elementAt(index); return ListTile( title: Text( minor, style: TextStyle( fontFamily: 'SCDream', fontSize: 16, ), ), onTap: () { if (minor == '기타(직접 입력)') setModalState(() => showInput = true); else { setState(() { _currentMajorCategory = selectedMajor; _currentMinorCategory = minor; _speciesController.text = minor; _breedController.clear(); }); Navigator.pop(context); } }, ); } }, ), ), ], ), ); }, ); }, ); } void _showBreedSelectionModal() { if (_speciesController.text.isEmpty) { ScaffoldMessenger.of(context).showSnackBar( const SnackBar( content: Text('반려동물 종을 먼저 선택해주세요.'), duration: Duration(seconds: 1), ), ); return; } if (_currentMajorCategory == null || _currentMinorCategory == null) { _showBreedDirectInputModal(); return; } final List originalList = PetData .breedsData[_currentMajorCategory]![_currentMinorCategory]! .where((e) => e != '기타(직접 입력)') .toList(); showModalBottomSheet( context: context, isScrollControlled: true, backgroundColor: Colors.transparent, builder: (context) { String searchText = ''; List filteredList = List.from(originalList); TextEditingController searchController = TextEditingController(); bool showInput = false; final TextEditingController manualInputController = TextEditingController(); return StatefulBuilder( builder: (BuildContext context, StateSetter setModalState) { final bottomInset = MediaQuery.of(context).viewInsets.bottom; return Container( height: 0.85.sh, margin: EdgeInsets.only(top: 50.h), decoration: BoxDecoration( color: Colors.white, borderRadius: BorderRadius.vertical(top: Radius.circular(20.r)), ), child: Column( children: [ Padding( padding: EdgeInsets.symmetric( horizontal: 16.w, vertical: 12.h, ), child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ showInput ? GestureDetector( onTap: () => setModalState(() => showInput = false), child: Icon(Icons.arrow_back_ios), ) : SizedBox(width: 20), Text( showInput ? '직접 입력' : '품종 선택', style: TextStyle( fontSize: 18.sp, fontWeight: FontWeight.bold, ), ), GestureDetector( onTap: () => Navigator.pop(context), child: Icon(Icons.close), ), ], ), ), Divider(), Expanded( child: showInput ? Padding( padding: EdgeInsets.all(20.w), child: Column( children: [ TextField( controller: manualInputController, autofocus: true, decoration: InputDecoration( hintText: '예: 믹스', ), ), Spacer(), ElevatedButton( onPressed: () { if (manualInputController.text.isNotEmpty) { setState( () => _breedController.text = manualInputController.text, ); Navigator.pop(context); } }, child: Text('완료'), ), ], ), ) : Column( children: [ Padding( padding: EdgeInsets.all(10), child: TextField( controller: searchController, onChanged: (v) { setModalState(() { searchText = v; filteredList = v.isEmpty ? List.from(originalList) : originalList .where((b) => b.contains(v)) .toList(); }); }, decoration: InputDecoration( hintText: '검색', prefixIcon: Icon(Icons.search), filled: true, border: OutlineInputBorder( borderRadius: BorderRadius.circular(12), borderSide: BorderSide.none, ), ), ), ), Expanded( child: ListView.builder( itemCount: filteredList.length + 1, itemBuilder: (ctx, idx) { if (idx == filteredList.length) return ListTile( title: Text('기타(직접 입력)'), onTap: () => setModalState( () => showInput = true, ), ); return ListTile( title: Text(filteredList[idx]), onTap: () { setState( () => _breedController.text = filteredList[idx], ); Navigator.pop(context); }, ); }, ), ), ], ), ), ], ), ); }, ); }, ); } void _showBreedDirectInputModal() { showModalBottomSheet( context: context, isScrollControlled: true, backgroundColor: Colors.transparent, builder: (context) { final controller = TextEditingController(); return Container( height: 0.5.sh, decoration: BoxDecoration( color: Colors.white, borderRadius: BorderRadius.vertical(top: Radius.circular(20)), ), padding: EdgeInsets.all(20), child: Column( children: [ Text( '품종 입력', style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold), ), TextField( controller: controller, decoration: InputDecoration(hintText: '품종을 입력하세요'), ), ElevatedButton( onPressed: () { setState(() => _breedController.text = controller.text); Navigator.pop(context); }, child: Text('완료'), ), ], ), ); }, ); } void _showGenderSelectionModal() { showModalBottomSheet( context: context, backgroundColor: Colors.transparent, isScrollControlled: true, builder: (context) { String? tempGender = _selectedGender; bool tempNeutered = _isNeutered; return StatefulBuilder( builder: (context, setModalState) { return Container( decoration: BoxDecoration( color: Colors.white, borderRadius: BorderRadius.vertical(top: Radius.circular(20.r)), ), child: Column( mainAxisSize: MainAxisSize.min, children: [ Padding( padding: EdgeInsets.all(16), child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ SizedBox(width: 24), Text( '성별 선택', style: TextStyle( fontSize: 18, fontWeight: FontWeight.bold, ), ), GestureDetector( onTap: () => Navigator.pop(context), child: Icon(Icons.close), ), ], ), ), Divider(), Padding( padding: EdgeInsets.all(20), child: Row( children: [ Expanded( child: _buildGenderCard( '남아', Icons.male, tempGender == '남아', (v) => setModalState(() => tempGender = v), ), ), SizedBox(width: 12), Expanded( child: _buildGenderCard( '여아', Icons.female, tempGender == '여아', (v) => setModalState(() => tempGender = v), ), ), SizedBox(width: 12), Expanded( child: _buildGenderCard( '기타', Icons.question_mark, tempGender == '기타', (v) => setModalState(() => tempGender = v), ), ), ], ), ), GestureDetector( onTap: () => setModalState(() => tempNeutered = !tempNeutered), child: Row( mainAxisAlignment: MainAxisAlignment.center, children: [ Icon( tempNeutered ? Icons.check_box : Icons.check_box_outline_blank, color: tempNeutered ? AppColors.highlight : Colors.grey, ), SizedBox(width: 8), Text('중성화를 했어요'), ], ), ), Padding( padding: EdgeInsets.all(20), child: SizedBox( width: double.infinity, height: 52, child: ElevatedButton( onPressed: () { setState(() { _selectedGender = tempGender; _isNeutered = tempNeutered; _genderController.text = _selectedGender == '기타' ? '기타' : '$_selectedGender${_isNeutered ? "(중성화)" : ""}'; }); Navigator.pop(context); }, style: ElevatedButton.styleFrom( backgroundColor: AppColors.highlight, ), child: Text( '선택 완료', style: TextStyle( color: Colors.white, fontWeight: FontWeight.bold, ), ), ), ), ), ], ), ); }, ); }, ); } Widget _buildGenderCard( String gender, IconData icon, bool isSelected, Function(String) onTap, ) { return GestureDetector( onTap: () => onTap(gender), child: Container( height: 100, decoration: BoxDecoration( color: isSelected ? AppColors.highlight.withOpacity(0.1) : Colors.white, border: Border.all( color: isSelected ? AppColors.highlight : Color(0xFFEEEEEE), ), borderRadius: BorderRadius.circular(16), ), child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ Icon( icon, size: 40, color: isSelected ? AppColors.highlight : Colors.grey, ), SizedBox(height: 12), Text( gender, style: TextStyle( fontWeight: FontWeight.bold, color: isSelected ? AppColors.highlight : Colors.grey, ), ), ], ), ), ); } @override Widget build(BuildContext context) { final bool isEditMode = widget.petToEdit != null; return Scaffold( backgroundColor: Colors.white, appBar: AppBar( title: Text( isEditMode ? '반려동물 정보 수정' : '반려동물 등록', style: TextStyle( color: Color(0xFF1f1f1f), fontFamily: 'SCDream', fontWeight: FontWeight.w500, fontSize: 15.sp, ), ), centerTitle: true, backgroundColor: Colors.white, scrolledUnderElevation: 0, elevation: 0, leading: IconButton( icon: Icon(Icons.arrow_back_ios, color: Colors.black, size: 16.w), onPressed: () => Navigator.pop(context), ), ), body: SingleChildScrollView( padding: EdgeInsets.all(20.w), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ // 1. 프로필 이미지 영역 Center( child: GestureDetector( onTap: _pickImage, child: Stack( children: [ Container( width: 100.w, height: 100.w, decoration: BoxDecoration( color: const Color(0xFFF5F5F5), shape: BoxShape.circle, border: Border.all(color: const Color(0xFFEEEEEE)), image: _profileImage != null ? DecorationImage( image: FileImage(_profileImage!), fit: BoxFit.cover, ) : (isEditMode && widget.petToEdit!.profileImageUrl != null ? DecorationImage( image: NetworkImage( widget.petToEdit!.profileImageUrl!, ), fit: BoxFit.cover, ) : null), ), child: (_profileImage == null && (!isEditMode || widget.petToEdit!.profileImageUrl == null)) ? Center( child: SvgPicture.asset( 'assets/icons/profile_icon.svg', width: 40.w, colorFilter: ColorFilter.mode( Colors.grey[400]!, BlendMode.srcIn, ), ), ) : null, ), Positioned( bottom: 0, right: 0, child: Container( padding: EdgeInsets.all(6.w), decoration: BoxDecoration( color: Colors.white, shape: BoxShape.circle, border: Border.all(color: const Color(0xFFEEEEEE)), boxShadow: [ BoxShadow( color: Colors.black.withOpacity(0.05), blurRadius: 4.r, offset: Offset(0, 2.h), ), ], ), child: Icon( Icons.camera_alt, size: 16.w, color: Colors.black87, ), ), ), ], ), ), ), SizedBox(height: 30.h), // 2. 반려동물 이름 입력 _buildLabel('반려동물 이름 입력', isRequired: true), _buildTextField( controller: _nameController, hint: '이름 입력 (2~10글자/한글/영문/숫자)', inputFormatters: [ LengthLimitingTextInputFormatter(10), // 최대 10글자 제한 ], ), SizedBox(height: 20.h), // 3. 선택 박스들 (종, 품종, 성별) _buildSearchField( '반려동물 종 선택', controller: _speciesController, readOnly: true, onTap: _showSpeciesSelectionModal, isRequired: true, ), SizedBox(height: 20.h), _buildSearchField( '반려동물 품종 선택', controller: _breedController, readOnly: true, onTap: _showBreedSelectionModal, isRequired: true, ), SizedBox(height: 20.h), _buildSearchField( '반려동물 성별', controller: _genderController, readOnly: true, onTap: _showGenderSelectionModal, isRequired: true, ), SizedBox(height: 20.h), // 4. 생년월일 _buildLabel('반려동물 생년월일', isRequired: true), Row( children: [ Expanded( child: _buildTextField( controller: _yearController, focusNode: _yearFocus, hint: 'YYYY', textAlign: TextAlign.center, hintColor: _isDateUnknown ? const Color(0xFFC8C8C8) : const Color(0xFF7D7C7C), enabled: !_isDateUnknown, keyboardType: TextInputType.number, inputFormatters: [ FilteringTextInputFormatter.digitsOnly, LengthLimitingTextInputFormatter(4), ], onChanged: (value) { if (value.length == 4) { FocusScope.of(context).requestFocus(_monthFocus); } }, ), ), SizedBox(width: 12.w), Expanded( child: _buildTextField( controller: _monthController, focusNode: _monthFocus, hint: 'MM', textAlign: TextAlign.center, hintColor: _isDateUnknown ? const Color(0xFFC8C8C8) : const Color(0xFF7D7C7C), enabled: !_isDateUnknown, keyboardType: TextInputType.number, inputFormatters: [ FilteringTextInputFormatter.digitsOnly, LengthLimitingTextInputFormatter(2), DateRangeInputFormatter(min: 1, max: 12), ], onChanged: (value) { if (value.length == 2) { FocusScope.of(context).requestFocus(_dayFocus); } }, ), ), SizedBox(width: 12.w), Expanded( child: _buildTextField( controller: _dayController, focusNode: _dayFocus, hint: 'DD', textAlign: TextAlign.center, hintColor: _isDateUnknown ? const Color(0xFFC8C8C8) : const Color(0xFF7D7C7C), enabled: !_isDateUnknown, keyboardType: TextInputType.number, inputFormatters: [ FilteringTextInputFormatter.digitsOnly, LengthLimitingTextInputFormatter(2), DayInputFormatter( monthController: _monthController, yearController: _yearController, ), ], ), ), ], ), const SizedBox(height: 12), GestureDetector( onTap: _toggleDateUnknown, child: Container( width: double.infinity, padding: EdgeInsets.symmetric(vertical: 14.h), decoration: BoxDecoration( color: _isDateUnknown ? AppColors.subHighlight : AppColors.inactive, borderRadius: BorderRadius.circular(30.r), ), child: Center( child: Text( '정확한 날짜를 몰라요', style: TextStyle( fontFamily: 'SCDream', color: Colors.white, fontWeight: FontWeight.w500, fontSize: 14.sp, ), ), ), ), ), const SizedBox(height: 24), // 5. 동물 등록 번호 _buildTextField( controller: _registrationNumberController, hint: '숫자만 입력', keyboardType: TextInputType.number, inputFormatters: [ FilteringTextInputFormatter.digitsOnly, LengthLimitingTextInputFormatter(15), ], ), const SizedBox(height: 24), // 체중 입력 _buildLabel('몸무게 (kg)', isRequired: false), _buildTextField( controller: _weightController, hint: '예: 4.5', suffix: 'kg', keyboardType: const TextInputType.numberWithOptions( decimal: true, ), inputFormatters: [ FilteringTextInputFormatter.allow(RegExp(r'^\d*\.?\d*')), ], ), const SizedBox(height: 24), // 6. 질환 정보 _buildSearchField( '보유 질환', controller: _diseaseController, readOnly: true, onTap: () => _showSelectionModal( title: '보유 질환 선택', currentSelected: _selectedDiseases, currentOtherText: _otherDiseaseText, onComplete: (selected, otherText) { setState(() { _selectedDiseases = selected; _otherDiseaseText = otherText; _parseAndSetDiseases( selected, (s, o, t) => _diseaseController.text = t, ); // Reusing logic // Helper logic copy List displayList = selected .where((e) => e != '기타') .toList(); if (selected.contains('기타') && otherText.isNotEmpty) { displayList.add('기타($otherText)'); } else if (selected.contains('기타')) { displayList.add('기타'); } _diseaseController.text = displayList.join(', '); }); }, ), ), SizedBox(height: 20.h), _buildSearchField( '과거 진단받은 질병', controller: _pastDiseaseController, readOnly: true, onTap: () => _showSelectionModal( title: '과거 진단받은 질병 선택', currentSelected: _selectedPastDiseases, currentOtherText: _otherPastDiseaseText, onComplete: (selected, otherText) { setState(() { _selectedPastDiseases = selected; _otherPastDiseaseText = otherText; List displayList = selected .where((e) => e != '기타') .toList(); if (selected.contains('기타') && otherText.isNotEmpty) { displayList.add('기타($otherText)'); } else if (selected.contains('기타')) { displayList.add('기타'); } _pastDiseaseController.text = displayList.join(', '); }); }, ), ), SizedBox(height: 20.h), _buildSearchField( '염려되는 건강 문제', controller: _healthConcernController, readOnly: true, onTap: () => _showSelectionModal( title: '염려되는 건강 문제 선택', currentSelected: _selectedHealthConcerns, currentOtherText: _otherHealthConcernText, onComplete: (selected, otherText) { setState(() { _selectedHealthConcerns = selected; _otherHealthConcernText = otherText; List displayList = selected .where((e) => e != '기타') .toList(); if (selected.contains('기타') && otherText.isNotEmpty) { displayList.add('기타($otherText)'); } else if (selected.contains('기타')) { displayList.add('기타'); } _healthConcernController.text = displayList.join(', '); }); }, ), ), SizedBox(height: 40.h), // 7. 등록 버튼 SizedBox( width: double.infinity, height: 52.h, child: ElevatedButton( onPressed: (_isFormValid && !_isLoading) ? _submitForm : null, style: ElevatedButton.styleFrom( backgroundColor: AppColors.highlight, disabledBackgroundColor: AppColors.inactive, disabledForegroundColor: Colors.white, elevation: 0, shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(30.r), ), ), child: _isLoading ? SizedBox( width: 24.w, height: 24.w, child: const CircularProgressIndicator( color: Colors.white, strokeWidth: 2, ), ) : Text( isEditMode ? '수정 완료' : '반려동물 등록', style: TextStyle( fontFamily: 'SCDream', fontSize: 16.sp, fontWeight: FontWeight.bold, color: Colors.white, ), ), ), ), SizedBox(height: 20.h), ], ), ), ); } // Helper Widget: 라벨 (필수 표시 포함) Widget _buildLabel(String text, {bool isRequired = false}) { return Row( mainAxisSize: MainAxisSize.min, children: [ if (isRequired) Padding( padding: EdgeInsets.only(right: 4.w), child: Container( width: 4.w, height: 4.w, decoration: const BoxDecoration( color: Colors.red, shape: BoxShape.circle, ), ), ), Text( text, style: TextStyle( fontFamily: 'SCDream', fontSize: 14.sp, fontWeight: FontWeight.w500, color: Colors.black, ), ), ], ); } // Helper Widget: 텍스트 입력 필드 Widget _buildTextField({ required TextEditingController? controller, // made required for sanity required String hint, // modified signature slightly for ease TextAlign textAlign = TextAlign.start, Color? hintColor, bool enabled = true, TextInputType? keyboardType, List? inputFormatters, FocusNode? focusNode, ValueChanged? onChanged, String? suffix, }) { return TextField( controller: controller, focusNode: focusNode, onChanged: onChanged, enabled: enabled, textAlign: textAlign, keyboardType: keyboardType, inputFormatters: inputFormatters, style: TextStyle( fontFamily: 'SCDream', fontSize: 14.sp, color: AppColors.text, ), decoration: InputDecoration( hintText: hint, hintStyle: TextStyle( fontFamily: 'SCDream', fontSize: 14.sp, color: hintColor ?? Colors.grey, ), suffixText: suffix, suffixStyle: TextStyle( fontFamily: 'SCDream', fontSize: 14.sp, fontWeight: FontWeight.bold, color: Colors.black, ), enabledBorder: const UnderlineInputBorder( borderSide: BorderSide(color: Color(0xFFDDDDDD)), ), focusedBorder: const UnderlineInputBorder( borderSide: BorderSide(color: AppColors.highlight), ), contentPadding: EdgeInsets.symmetric(vertical: 8.h), isDense: true, ), ); } Widget _buildSearchField( String label, { TextEditingController? controller, VoidCallback? onTap, bool readOnly = false, bool isRequired = false, }) { return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ _buildLabel(label, isRequired: isRequired), TextField( controller: controller, onTap: onTap, readOnly: readOnly, style: TextStyle( fontFamily: 'SCDream', fontSize: 14.sp, overflow: TextOverflow.ellipsis, ), decoration: InputDecoration( isDense: true, contentPadding: EdgeInsets.symmetric(vertical: 8.h), enabledBorder: const UnderlineInputBorder( borderSide: BorderSide(color: Color(0xFFDDDDDD)), ), focusedBorder: const UnderlineInputBorder( borderSide: BorderSide(color: AppColors.highlight), ), suffixIcon: const Icon(Icons.search, color: Colors.black87), suffixIconConstraints: BoxConstraints( minWidth: 24.w, minHeight: 24.w, ), ), ), ], ); } } // --- Helper Classes (Restored) --- class DateRangeInputFormatter extends TextInputFormatter { final int min; final int max; DateRangeInputFormatter({required this.min, required this.max}); @override TextEditingValue formatEditUpdate( TextEditingValue oldValue, TextEditingValue newValue, ) { if (newValue.text.isEmpty) { return newValue; } final int? value = int.tryParse(newValue.text); if (value == null) { return oldValue; } if (value < min || value > max) { return oldValue; } return newValue; } } class DayInputFormatter extends TextInputFormatter { final TextEditingController monthController; final TextEditingController yearController; DayInputFormatter({ required this.monthController, required this.yearController, }); @override TextEditingValue formatEditUpdate( TextEditingValue oldValue, TextEditingValue newValue, ) { if (newValue.text.isEmpty) { return newValue; } final int? day = int.tryParse(newValue.text); if (day == null) { return oldValue; } int? month = int.tryParse(monthController.text); int? year = int.tryParse(yearController.text); if (month == null || month < 1 || month > 12) { // 월이 입력되지 않았거나 유효하지 않으면 31일까지 허용 if (day < 1 || day > 31) return oldValue; return newValue; } int maxDay = _getDaysInMonth(year, month); if (day < 1 || day > maxDay) { return oldValue; } return newValue; } int _getDaysInMonth(int? year, int month) { if (month == 2) { if (year != null && ((year % 4 == 0 && year % 100 != 0) || (year % 400 == 0))) { return 29; } return 28; } const daysInMonth = [0, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31]; return daysInMonth[month]; } }