1894 lines
74 KiB
Dart
1894 lines
74 KiB
Dart
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 Data
|
|
import '../widgets/pet_registration/selection_modal.dart'; // Import SelectionModal
|
|
import '../widgets/pet_registration/input_formatters.dart'; // Import InputFormatters
|
|
import '../services/firestore_service.dart';
|
|
import '../models/pet_model.dart';
|
|
|
|
class PetRegistrationScreen extends StatefulWidget {
|
|
const PetRegistrationScreen({super.key});
|
|
|
|
@override
|
|
State<PetRegistrationScreen> createState() => _PetRegistrationScreenState();
|
|
}
|
|
|
|
class _PetRegistrationScreenState extends State<PetRegistrationScreen> {
|
|
// 정확한 날짜를 몰라요 상태
|
|
bool _isDateUnknown = false;
|
|
|
|
final TextEditingController _monthController = TextEditingController();
|
|
final TextEditingController _yearController =
|
|
TextEditingController(); // Added back
|
|
final TextEditingController _dayController = TextEditingController();
|
|
final TextEditingController _registrationNumberController =
|
|
TextEditingController(); // Added Registration Number Controller
|
|
final FocusNode _yearFocus = FocusNode();
|
|
final FocusNode _monthFocus = FocusNode();
|
|
final FocusNode _dayFocus = FocusNode();
|
|
|
|
bool get _isFormValid {
|
|
// 1. 이름 확인
|
|
if (_nameController.text.trim().isEmpty) return false;
|
|
// 2. 종 확인
|
|
if (_speciesController.text.trim().isEmpty) return false;
|
|
// 3. 품종 확인
|
|
if (_breedController.text.trim().isEmpty) return false;
|
|
// 4. 성별 확인
|
|
if (_selectedGender == null) return false;
|
|
// 5. 생년월일 확인
|
|
if (!_isDateUnknown) {
|
|
if (_yearController.text.length != 4 ||
|
|
_monthController.text.length != 2 ||
|
|
_dayController.text.length != 2) {
|
|
return false;
|
|
}
|
|
}
|
|
return true;
|
|
}
|
|
|
|
@override
|
|
void initState() {
|
|
super.initState();
|
|
// 폼 상태 변경 감지를 위한 리스너 등록
|
|
_nameController.addListener(_updateState);
|
|
_speciesController.addListener(_updateState);
|
|
_breedController.addListener(_updateState);
|
|
_yearController.addListener(_updateState);
|
|
_monthController.addListener(_updateState);
|
|
_dayController.addListener(_updateState);
|
|
}
|
|
|
|
void _updateState() {
|
|
setState(() {});
|
|
}
|
|
|
|
// 보유 질환 데이터 (Removed: Use PetData.diseaseList)
|
|
|
|
// 각 항목별 선택 상태 및 컨트롤러
|
|
final TextEditingController _nameController =
|
|
TextEditingController(); // 이름 컨트롤러
|
|
final TextEditingController _speciesController =
|
|
TextEditingController(); // 종 컨트롤러
|
|
final TextEditingController _breedController =
|
|
TextEditingController(); // 품종 컨트롤러
|
|
final TextEditingController _genderController =
|
|
TextEditingController(); // 성별 컨트롤러
|
|
|
|
// 종 데이터 (Removed: Use PetData.breedsData)
|
|
|
|
// 선택된 종 정보 (품종 선택을 위해 필요)
|
|
String? _currentMajorCategory;
|
|
String? _currentMinorCategory;
|
|
|
|
String? _selectedGender; // '남아', '여아'
|
|
bool _isNeutered = false; // 중성화 여부
|
|
List<String> _selectedDiseases = [];
|
|
String _otherDiseaseText = ''; // 보유 질환 기타 텍스트
|
|
final TextEditingController _diseaseController = TextEditingController();
|
|
|
|
List<String> _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<String> _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();
|
|
super.dispose();
|
|
}
|
|
|
|
bool _isLoading = false;
|
|
|
|
Future<void> _registerPet() 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<String> finalDiseases = List.from(_selectedDiseases);
|
|
if (finalDiseases.contains('기타') && _otherDiseaseText.isNotEmpty) {
|
|
finalDiseases.remove('기타');
|
|
finalDiseases.add('기타($_otherDiseaseText)');
|
|
}
|
|
|
|
// 리스트 + 기타 텍스트 합치기 (과거 질환)
|
|
List<String> finalPastDiseases = List.from(_selectedPastDiseases);
|
|
if (finalPastDiseases.contains('기타') &&
|
|
_otherPastDiseaseText.isNotEmpty) {
|
|
finalPastDiseases.remove('기타');
|
|
finalPastDiseases.add('기타($_otherPastDiseaseText)');
|
|
}
|
|
|
|
// 리스트 + 기타 텍스트 합치기 (염려 건강)
|
|
List<String> finalHealthConcerns = List.from(_selectedHealthConcerns);
|
|
if (finalHealthConcerns.contains('기타') &&
|
|
_otherHealthConcernText.isNotEmpty) {
|
|
finalHealthConcerns.remove('기타');
|
|
finalHealthConcerns.add('기타($_otherHealthConcernText)');
|
|
}
|
|
|
|
final newPet = Pet(
|
|
id: firestoreService.generatePetId(),
|
|
ownerId: userId,
|
|
name: _nameController.text,
|
|
species: _speciesController.text, // 중분류 or 직접입력
|
|
breed: _breedController.text,
|
|
gender: _selectedGender!,
|
|
isNeutered: _isNeutered,
|
|
birthDate: birthDate,
|
|
isDateUnknown: _isDateUnknown,
|
|
registrationNumber: _registrationNumberController.text.isNotEmpty
|
|
? _registrationNumberController.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();
|
|
}
|
|
});
|
|
}
|
|
|
|
// 공통 선택 모달 (보유 질환, 과거 진단, 염려 건강) - Generic Selection Modal
|
|
void _showSelectionModal({
|
|
required String title,
|
|
required List<String> currentSelected,
|
|
required String currentOtherText,
|
|
required Function(List<String>, String) onComplete,
|
|
}) {
|
|
showModalBottomSheet(
|
|
context: context,
|
|
backgroundColor: Colors.transparent,
|
|
isScrollControlled: true,
|
|
builder: (context) {
|
|
// 모달 내부 임시 상태
|
|
List<String> tempSelected = List.from(currentSelected);
|
|
final TextEditingController otherInputController =
|
|
TextEditingController(text: currentOtherText);
|
|
|
|
return StatefulBuilder(
|
|
builder: (BuildContext context, StateSetter setModalState) {
|
|
// 키보드가 올라왔을 때를 대비한 Padding 처리
|
|
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(
|
|
key: const ValueKey('other_input'),
|
|
controller: otherInputController,
|
|
autofocus: true, // 입력창이 생기면 바로 포커스
|
|
style: TextStyle(
|
|
fontFamily: 'SCDream',
|
|
fontSize: 14.sp,
|
|
),
|
|
decoration: InputDecoration(
|
|
hintText: '직접 입력해 주세요',
|
|
isDense: true,
|
|
contentPadding: EdgeInsets.symmetric(
|
|
vertical: 10.h,
|
|
horizontal: 10.w,
|
|
),
|
|
border: const OutlineInputBorder(
|
|
borderSide: BorderSide(
|
|
color: Color(0xFFDDDDDD),
|
|
),
|
|
),
|
|
enabledBorder: const OutlineInputBorder(
|
|
borderSide: BorderSide(
|
|
color: Color(0xFFDDDDDD),
|
|
),
|
|
),
|
|
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;
|
|
|
|
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: PetData.breedsData.keys.length,
|
|
itemBuilder: (context, index) {
|
|
final major = PetData.breedsData.keys
|
|
.elementAt(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:
|
|
PetData.breedsData[selectedMajor]!.length,
|
|
itemBuilder: (context, index) {
|
|
final minor = 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(
|
|
const SnackBar(
|
|
content: Text('반려동물 종을 먼저 선택해주세요.'),
|
|
duration: Duration(seconds: 1),
|
|
),
|
|
);
|
|
return;
|
|
}
|
|
|
|
// 2. 직접 입력 등 카테고리 정보가 없는 경우 -> 바로 직접 입력 모드로
|
|
if (_currentMajorCategory == null || _currentMinorCategory == null) {
|
|
_showBreedDirectInputModal();
|
|
return;
|
|
}
|
|
|
|
// 3. 품종 리스트 가져오기
|
|
final List<String> originalList = PetData
|
|
.breedsData[_currentMajorCategory]![_currentMinorCategory]!
|
|
.where((e) => e != '기타(직접 입력)')
|
|
.toList();
|
|
// '기타(직접 입력)'은 리스트 마지막에 고정하거나 별도 처리, 여기서는 필터링 후 맨 뒤에 붙일 예정
|
|
|
|
showModalBottomSheet(
|
|
context: context,
|
|
isScrollControlled: true,
|
|
backgroundColor: Colors.transparent,
|
|
builder: (context) {
|
|
String searchText = '';
|
|
List<String> 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: const Color(0xFFF5F5F5),
|
|
border: OutlineInputBorder(
|
|
borderRadius: BorderRadius.circular(12),
|
|
borderSide: BorderSide.none,
|
|
),
|
|
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),
|
|
),
|
|
),
|
|
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) {
|
|
return Scaffold(
|
|
backgroundColor: Colors.white,
|
|
appBar: AppBar(
|
|
title: Text(
|
|
'반려동물 등록',
|
|
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,
|
|
)
|
|
: null,
|
|
),
|
|
child: _profileImage == 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),
|
|
// const SizedBox(height: 8),
|
|
_buildTextField(
|
|
controller: _registrationNumberController,
|
|
hint: '숫자만 입력',
|
|
keyboardType: TextInputType.number,
|
|
inputFormatters: [
|
|
FilteringTextInputFormatter.digitsOnly,
|
|
LengthLimitingTextInputFormatter(15),
|
|
],
|
|
),
|
|
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;
|
|
|
|
// 텍스트 필드 표시용 문자열 생성
|
|
List<String> 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<String> 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<String> 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) ? _registerPet : 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(
|
|
'반려동물 등록',
|
|
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 String hint,
|
|
TextAlign textAlign = TextAlign.start,
|
|
Color? hintColor,
|
|
bool enabled = true,
|
|
TextEditingController? controller,
|
|
TextInputType? keyboardType,
|
|
List<TextInputFormatter>? inputFormatters,
|
|
FocusNode? focusNode,
|
|
ValueChanged<String>? onChanged,
|
|
}) {
|
|
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,
|
|
),
|
|
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 _buildSelectionBox(String text, {required bool isRequired}) {
|
|
return Container(
|
|
padding: EdgeInsets.symmetric(horizontal: 16.w, vertical: 14.h),
|
|
decoration: BoxDecoration(
|
|
border: Border.all(color: const Color(0xFFDDDDDD)),
|
|
borderRadius: BorderRadius.circular(4.r),
|
|
),
|
|
child: Row(
|
|
children: [
|
|
if (isRequired)
|
|
Padding(
|
|
padding: EdgeInsets.only(right: 6.w),
|
|
child: Icon(Icons.circle, size: 4.w, color: Colors.red),
|
|
),
|
|
Expanded(
|
|
child: Text(
|
|
text,
|
|
style: TextStyle(
|
|
fontFamily: 'SCDream',
|
|
fontSize: 14.sp,
|
|
fontWeight: FontWeight.w500,
|
|
color: Colors.black87,
|
|
),
|
|
),
|
|
),
|
|
Icon(Icons.arrow_forward_ios, size: 14.w, color: Colors.grey),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
|
|
Widget _buildSearchField(
|
|
String label, {
|
|
TextEditingController? controller,
|
|
VoidCallback? onTap,
|
|
bool readOnly = false,
|
|
bool isRequired = false, // Added isRequired param
|
|
}) {
|
|
return Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
_buildLabel(
|
|
label,
|
|
isRequired: isRequired,
|
|
), // Use _buildLabel to show label with red dot
|
|
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,
|
|
),
|
|
),
|
|
),
|
|
],
|
|
);
|
|
}
|
|
}
|