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'; import '../services/api_service.dart'; import '../services/auth_service.dart'; import '../widgets/pet_registration/input_formatters.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); _weightController.addListener(_updateState); _registrationNumberController.addListener(_updateState); // 추가 _loadMasterData(); } // API로부터 마스터 데이터 로드 Map>> _breedsData = {}; List _diseaseList = []; bool _isMasterDataLoaded = false; Future _loadMasterData() async { try { final apiData = await ApiService().getInitialData(); final groups = apiData['groups'] as List; final diseases = apiData['diseases'] as List; // Process Diseases _diseaseList = diseases.map((e) => e['name'] as String).toList(); if (!_diseaseList.contains('기타')) _diseaseList.add('기타'); // Process Groups -> Species -> Breeds _breedsData = {}; for (var group in groups) { Map> speciesMap = {}; for (var species in group['PetSpecies']) { List breeds = (species['PetBreeds'] as List) .map((b) => b['name'] as String) .toList(); if (!breeds.contains('기타(직접 입력)')) breeds.add('기타(직접 입력)'); speciesMap[species['name']] = breeds; } speciesMap['기타(직접 입력)'] = ['기타(직접 입력)']; _breedsData[group['name']] = speciesMap; } _breedsData['기타(직접 입력)'] = { '기타(직접 입력)': ['기타(직접 입력)'], }; if (mounted) { setState(() { _isMasterDataLoaded = true; // 마스터 데이터 로드 후 카테고리 정보 복원 시도 (초기화 시 못 찾았을 경우 대비) if (widget.petToEdit != null && (_currentMajorCategory == null || _currentMinorCategory == null)) { _restoreCategoryFromSpecies(_speciesController.text); } }); } } catch (e) { debugPrint('Error loading master data: $e'); // Fallback or retry logic can be added here // For now, allow fallback to PetData if empty, or just show loading } } 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; }); // 5. 카테고리 정보 복원 _restoreCategoryFromSpecies(pet.species); } void _restoreCategoryFromSpecies(String speciesName) { if (speciesName.isEmpty) return; final sourceData = _isMasterDataLoaded ? _breedsData : PetData.breedsData; // 이미 설정되어 있다면 패스 if (_currentMajorCategory != null && _currentMinorCategory != null) return; for (var major in sourceData.keys) { final minorMap = sourceData[major]; if (minorMap != null && minorMap.containsKey(speciesName)) { setState(() { _currentMajorCategory = major; _currentMinorCategory = speciesName; }); return; } } } void _parseAndSetDiseases( List source, Function(List, String, String) onSet, ) { List selected = []; String otherText = ''; for (var item in source) { if (item.trim().isEmpty || item == '[]' || item.contains('[')) continue; // Filter out bad data 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 Material( color: Colors.white, borderRadius: const BorderRadius.vertical(top: Radius.circular(20)), clipBehavior: Clip.hardEdge, child: Column( mainAxisSize: MainAxisSize.min, children: [ SizedBox(height: 20.h), Text( '프로필 사진 설정', style: TextStyle( fontFamily: 'SCDream', fontSize: 18.sp, fontWeight: FontWeight.bold, ), ), SizedBox(height: 10.h), Divider(color: const Color(0xFFDDDDDD), thickness: 1.h), SizedBox(height: 10.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) { // API 사용 (MySQL) await ApiService().updatePet( petId: int.parse(widget.petToEdit!.id), name: _nameController.text, species: _speciesController.text, breed: _breedController.text, gender: _selectedGender!, isNeutered: _isNeutered, birthDate: birthDate, isDateUnknown: _isDateUnknown, weight: _weightController.text.isNotEmpty ? double.tryParse(_weightController.text) : null, registrationNumber: _registrationNumberController.text.isNotEmpty ? _registrationNumberController.text : null, profileImage: _profileImage, diseases: finalDiseases, pastDiseases: finalPastDiseases, healthConcerns: finalHealthConcerns, ); 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, true); // Return true to indicate success } // 등록 모드 else { // API 사용 (MySQL) final userInfo = await AuthService().getUserInfo(); if (userInfo == null) { throw Exception('로그인 정보를 가져올 수 없습니다.'); } await ApiService().registerPet( userId: userInfo['id'], name: _nameController.text, species: _speciesController.text, breed: _breedController.text, gender: _selectedGender!, isNeutered: _isNeutered, birthDate: birthDate, isDateUnknown: _isDateUnknown, weight: _weightController.text.isNotEmpty ? double.tryParse(_weightController.text) : null, registrationNumber: _registrationNumberController.text.isNotEmpty ? _registrationNumberController.text : null, profileImage: _profileImage, diseases: finalDiseases, pastDiseases: finalPastDiseases, healthConcerns: finalHealthConcerns, ); if (!mounted) return; Navigator.pop(context, true); // Return true to indicate success } } 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: (_diseaseList.isEmpty ? PetData.diseaseList : _diseaseList) .where((e) => e != '기타(직접 입력)') .length, padding: const EdgeInsets.symmetric(vertical: 10), itemBuilder: (context, index) { final originalList = _diseaseList.isEmpty ? PetData.diseaseList : _diseaseList; final filteredList = originalList .where((e) => e != '기타(직접 입력)') .toList(); final disease = filteredList[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 == '기타' ? '기타(직접 입력)' : 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(), focusedBorder: const OutlineInputBorder( borderSide: BorderSide( color: AppColors.highlight, ), ), ), ), ), ], ); }, ), ), 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, ), ), ), ), ), ), ], ), ), ], ), ); }, ); }, ); } // 종 선택 모달 (대분류 -> 중분류 2단계) 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; final sourceMap = _isMasterDataLoaded ? _breedsData : PetData.breedsData; final sortedKeys = sourceMap.keys.toList() ..sort((a, b) { if (a == '기타(직접 입력)') return 1; if (b == '기타(직접 입력)') return -1; return (sourceMap[b]?.length ?? 0).compareTo( sourceMap[a]?.length ?? 0, ); }); return Container( height: 0.6.sh, // 높이 60%로 조정 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, color: Colors.black, ), ), // 닫기 버튼 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, color: Colors.black87, ), ), SizedBox(height: 20.h), TextField( controller: speciesInputController, autofocus: true, decoration: const InputDecoration( hintText: '예: 미어캣, 라쿤 등', border: OutlineInputBorder(), focusedBorder: OutlineInputBorder( borderSide: BorderSide( color: AppColors.highlight, ), ), ), ), const 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, elevation: 0, shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular( 12.r, ), ), ), child: Text( '완료', style: TextStyle( fontFamily: 'SCDream', fontSize: 16.sp, fontWeight: FontWeight.bold, color: Colors.white, ), ), ), ), SizedBox(height: bottomInset), ], ), ) : (selectedMajor == null ? ListView.builder( // 대분류 리스트 itemCount: sortedKeys.length, itemBuilder: (context, index) { final major = sortedKeys[index]; return ListTile( title: Text( major, style: const TextStyle( fontFamily: 'SCDream', fontSize: 16, ), ), trailing: const Icon( Icons.arrow_forward_ios, size: 16, color: Colors.grey, ), onTap: () { setModalState(() { if (major == '기타(직접 입력)') { showInput = true; } else { selectedMajor = major; } }); }, ); }, ) : ListView.builder( // 중분류 리스트 itemCount: _isMasterDataLoaded ? _breedsData[selectedMajor]!.length : PetData .breedsData[selectedMajor]! .length, itemBuilder: (context, index) { final minor = _isMasterDataLoaded ? _breedsData[selectedMajor]!.keys .elementAt(index) : PetData .breedsData[selectedMajor]! .keys .elementAt(index); return ListTile( title: Text( minor, style: const TextStyle( fontFamily: 'SCDream', fontSize: 16, ), ), trailing: minor == '기타(직접 입력)' ? const Icon( Icons.arrow_forward_ios, size: 16, color: Colors.grey, ) : null, onTap: () { if (minor == '기타(직접 입력)') { setModalState(() { showInput = true; }); } else { setState(() { // 최종 선택 반영 _currentMajorCategory = selectedMajor; _currentMinorCategory = minor; _speciesController.text = minor; _breedController .clear(); // 종 변경 시 품종 초기화 }); Navigator.pop(context); } }, ); }, )), ), ], ), ); }, ); }, ); } // 품종 선택 모달 (검색 가능) void _showBreedSelectionModal() { // 1. 종 선택 선행 확인 if (_speciesController.text.isEmpty) { ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: Row( children: [ const Icon(Icons.info_outline, 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), ), ); return; } // 2. 직접 입력 등 카테고리 정보가 없는 경우 -> 바로 직접 입력 모드로 if (_currentMajorCategory == null || _currentMinorCategory == null) { _showBreedDirectInputModal(); return; } // 3. 품종 리스트 가져오기 final Map> sourceData = _isMasterDataLoaded ? _breedsData[_currentMajorCategory]! : PetData.breedsData[_currentMajorCategory]!; final List originalList = sourceData[_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; void filterList(String query) { setModalState(() { searchText = query; if (query.isEmpty) { filteredList = List.from(originalList); } else { filteredList = originalList .where( (breed) => breed.toLowerCase().contains(query.toLowerCase()), ) .toList(); } }); } 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, size: 20.w, color: Colors.black, ), ) : const SizedBox(width: 20), Text( showInput ? '직접 입력' : '품종 선택', style: TextStyle( fontFamily: 'SCDream', fontSize: 18.sp, fontWeight: FontWeight.bold, color: Colors.black, ), ), 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, color: Colors.black87, ), ), SizedBox(height: 20.h), TextField( controller: manualInputController, autofocus: true, decoration: const InputDecoration( hintText: '예: 믹스, 시고르자브종 등', border: OutlineInputBorder(), focusedBorder: OutlineInputBorder( borderSide: BorderSide( color: AppColors.highlight, ), ), ), ), const Spacer(), SizedBox( width: double.infinity, height: 52.h, child: ElevatedButton( onPressed: () { if (manualInputController .text .isNotEmpty) { setState(() { _breedController.text = manualInputController.text; }); Navigator.pop(context); } }, style: ElevatedButton.styleFrom( backgroundColor: AppColors.highlight, elevation: 0, shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular( 12.r, ), ), ), child: Text( '완료', style: TextStyle( fontFamily: 'SCDream', fontSize: 16.sp, fontWeight: FontWeight.bold, color: Colors.white, ), ), ), ), SizedBox(height: bottomInset), ], ), ) : Column( children: [ // 검색창 Padding( padding: EdgeInsets.fromLTRB( 20.w, 10.h, 20.w, 10.h, ), child: TextField( controller: searchController, onChanged: filterList, decoration: InputDecoration( hintText: '품종 검색', prefixIcon: const Icon( Icons.search, color: Colors.grey, ), filled: true, fillColor: Colors.white, enabledBorder: OutlineInputBorder( borderRadius: BorderRadius.circular(12), borderSide: const BorderSide( color: AppColors.highlight, ), ), focusedBorder: OutlineInputBorder( borderRadius: BorderRadius.circular(12), borderSide: const BorderSide( color: AppColors.highlight, ), ), contentPadding: const EdgeInsets.symmetric( vertical: 0, horizontal: 16, ), ), ), ), // 리스트 Expanded( child: ListView.builder( itemCount: filteredList.length + 1, // 목록 + 직접입력 itemBuilder: (context, index) { if (index == filteredList.length) { // 마지막 아이템: 직접 입력 return ListTile( title: const Text( '기타(직접 입력)', style: TextStyle( fontFamily: 'SCDream', fontSize: 16, color: Colors.black87, ), ), trailing: const Icon( Icons.arrow_forward_ios, size: 16, color: Colors.grey, ), onTap: () { setModalState(() { showInput = true; }); }, ); } final breed = filteredList[index]; return ListTile( title: Text( breed, style: const TextStyle( fontFamily: 'SCDream', fontSize: 16, ), ), onTap: () { setState(() { _breedController.text = breed; }); Navigator.pop(context); }, ); }, ), ), ], ), ), ], ), ); }, ); }, ); } // 품종 직접 입력 모달 (카테고리 정보 없을 때) void _showBreedDirectInputModal() { showModalBottomSheet( context: context, backgroundColor: Colors.transparent, isScrollControlled: true, builder: (context) { final TextEditingController manualInputController = TextEditingController(); 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: Padding( padding: EdgeInsets.all(20.w), child: Column( children: [ // 네비게이션 Row( mainAxisAlignment: MainAxisAlignment.end, children: [ GestureDetector( onTap: () => Navigator.pop(context), child: Icon(Icons.close, size: 24.w, color: Colors.black), ), ], ), SizedBox(height: 20.h), Text( '반려동물의 품종을 직접 입력해주세요.', style: TextStyle( fontFamily: 'SCDream', fontSize: 16.sp, color: Colors.black87, ), ), SizedBox(height: 20.h), TextField( controller: manualInputController, autofocus: true, decoration: const InputDecoration( hintText: '예: 믹스, 시고르자브종 등', border: OutlineInputBorder(), focusedBorder: OutlineInputBorder( borderSide: BorderSide(color: AppColors.highlight), ), ), ), const Spacer(), SizedBox( width: double.infinity, height: 52.h, child: ElevatedButton( onPressed: () { if (manualInputController.text.isNotEmpty) { setState(() { _breedController.text = manualInputController.text; }); Navigator.pop(context); } }, style: ElevatedButton.styleFrom( backgroundColor: AppColors.highlight, elevation: 0, shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(12.r), ), ), child: Text( '완료', style: TextStyle( fontFamily: 'SCDream', fontSize: 16.sp, fontWeight: FontWeight.bold, color: Colors.white, ), ), ), ), SizedBox(height: MediaQuery.of(context).viewInsets.bottom), ], ), ), ); }, ); } // 성별 선택 모달 (남아/여아/기타 + 중성화) void _showGenderSelectionModal() { showModalBottomSheet( context: context, backgroundColor: Colors.transparent, isScrollControlled: true, builder: (context) { // 모달 내부 임시 상태 String? tempGender = _selectedGender; bool tempNeutered = _isNeutered; return StatefulBuilder( builder: (BuildContext context, StateSetter setModalState) { return Container( // height 제거 (내용물 크기에 맞춤) decoration: BoxDecoration( color: Colors.white, borderRadius: BorderRadius.vertical(top: Radius.circular(20.r)), ), child: Column( mainAxisSize: MainAxisSize.min, // 내용물만큼만 차지 children: [ // 상단 네비게이션바 Padding( padding: EdgeInsets.symmetric( horizontal: 16.w, vertical: 12.h, ), child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ SizedBox(width: 24.w), // 닫기 버튼과 대칭을 위한 여백 Text( '성별 선택', style: TextStyle( fontFamily: 'SCDream', fontSize: 18.sp, fontWeight: FontWeight.bold, color: Colors.black, ), ), GestureDetector( onTap: () => Navigator.pop(context), child: Icon( Icons.close, size: 24.w, color: Colors.black, ), ), ], ), ), Divider(color: const Color(0xFFEEEEEE), thickness: 1.h), SizedBox(height: 30.h), // 성별 선택 버튼 영역 (3개) Padding( padding: const EdgeInsets.symmetric(horizontal: 20), child: Row( children: [ Expanded( child: _buildGenderCard( '남아', Icons.male, tempGender == '남아', (val) => setModalState(() => tempGender = val), ), ), const SizedBox(width: 12), Expanded( child: _buildGenderCard( '여아', Icons.female, tempGender == '여아', (val) => setModalState(() => tempGender = val), ), ), const SizedBox(width: 12), Expanded( child: _buildGenderCard( '기타', Icons.question_mark, tempGender == '기타', (val) => setModalState(() => tempGender = val), ), ), ], ), ), const SizedBox(height: 30), // 중성화 여부 체크박스 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, size: 24.w, ), SizedBox(width: 8.w), Text( '중성화를 했어요', style: TextStyle( fontFamily: 'SCDream', fontSize: 16.sp, color: Colors.black87, ), ), ], ), ), SizedBox(height: 12.h), // 간격 축소 // 완료 버튼 Padding( padding: EdgeInsets.fromLTRB(20.w, 0, 20.w, 20.h), child: SizedBox( width: double.infinity, height: 52.h, child: ElevatedButton( onPressed: () { setState(() { _selectedGender = tempGender; _isNeutered = tempNeutered; if (_selectedGender != null) { if (_selectedGender == '기타') { _genderController.text = '기타'; } else { _genderController.text = '$_selectedGender${_isNeutered ? '(중성화)' : ''}'; } } }); Navigator.pop(context); }, style: ElevatedButton.styleFrom( backgroundColor: AppColors.highlight, elevation: 0, shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(12.r), ), ), child: const Text( '선택 완료', style: TextStyle( fontFamily: 'SCDream', fontSize: 16, fontWeight: FontWeight.bold, color: Colors.white, ), ), ), ), ), ], ), ); }, ); }, ); } Widget _buildGenderCard( String gender, IconData icon, bool isSelected, Function(String) onTap, ) { return GestureDetector( onTap: () => onTap(gender), child: Container( width: 120.w, height: 100.h, decoration: BoxDecoration( color: isSelected ? AppColors.highlight.withOpacity(0.1) : Colors.white, borderRadius: BorderRadius.circular(16.r), border: Border.all( color: isSelected ? AppColors.highlight : const Color(0xFFEEEEEE), width: 2.w, ), ), child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ Icon( icon, size: 40.w, color: isSelected ? AppColors.highlight : Colors.grey, ), SizedBox(height: 12.h), Text( gender, style: TextStyle( fontFamily: 'SCDream', fontSize: 16.sp, 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. 동물 등록 번호 _buildLabel('동물 등록 번호', isRequired: false), _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, ), ), ), ], ); } }