rup-project/app/lib/screens/pet_registration_screen.dart

1878 lines
73 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';
class PetRegistrationScreen extends StatefulWidget {
const PetRegistrationScreen({super.key});
@override
State<PetRegistrationScreen> createState() => _PetRegistrationScreenState();
}
class _PetRegistrationScreenState extends State<PetRegistrationScreen> {
// 정확한 날짜를 몰라요 상태
bool _isDateUnknown = false;
final TextEditingController _yearController = TextEditingController();
final TextEditingController _monthController = TextEditingController();
final TextEditingController _dayController = TextEditingController();
// 보유 질환 데이터
final List<String> _diseaseList = [
"피부질환",
"눈 질환",
"치아 / 구강 질환",
"뼈 / 관절 질환",
"생식기 / 비뇨기 질환",
"심장 / 혈관 질환",
"소화기 질환",
"호흡기 질환",
"내분비계 질환",
"뇌신경 질환",
"생식기 질환",
"귀 질환",
"코 질환",
"기타",
];
// 각 항목별 선택 상태 및 컨트롤러
final TextEditingController _nameController =
TextEditingController(); // 이름 컨트롤러
final TextEditingController _speciesController =
TextEditingController(); // 종 컨트롤러
final TextEditingController _breedController =
TextEditingController(); // 품종 컨트롤러
final TextEditingController _genderController =
TextEditingController(); // 성별 컨트롤러
// 종 데이터 (대분류 -> 중분류 -> 품종)
final Map<String, Map<String, List<String>>> _petData = {
"포유류": {
"강아지": [
"말티즈",
"푸들",
"포메라니안",
"믹스견",
"치와와",
"시츄",
"비숑 프리제",
"골든 리트리버",
"진돗개",
"웰시 코기",
"기타(직접 입력)",
],
"고양이": [
"코리안 숏헤어",
"페르시안",
"러시안 블루",
"",
"렉돌",
"스코티시 폴드",
"먼치킨",
"노르웨이 숲",
"믹스묘",
"기타(직접 입력)",
],
"햄스터": ["정글리안", "", "푸딩", "골든 햄스터", "로보로브스키", "기타(직접 입력)"],
"토끼": ["롭이어", "더치", "라이언 헤드", "드워프", "렉스", "기타(직접 입력)"],
"기니피그": ["잉글리쉬", "아비시니안", "페루비안", "실키", "기타(직접 입력)"],
"고슴도치": ["플라티나", "화이트 초코", "알비노", "핀토", "기타(직접 입력)"],
"기타": ["기타(직접 입력)"],
},
"파충류": {
"거북이": ["커먼 머스크 터틀", "레이저백", "육지거북", "붉은귀거북", "남생이", "기타(직접 입력)"],
"도마뱀": ["크레스티드 게코", "레오파드 게코", "비어디 드래곤", "블루텅 스킨크", "이구아나", "기타(직접 입력)"],
"": ["볼 파이톤", "콘 스네이크", "킹 스네이크", "밀크 스네이크", "기타(직접 입력)"],
"기타": ["기타(직접 입력)"],
},
"조류": {
"앵무새": [
"사랑앵무(잉꼬)",
"코카티엘(왕관앵무)",
"모란앵무",
"코뉴어",
"퀘이커",
"금강앵무",
"기타(직접 입력)",
],
"카나리아": ["옐로우 카나리아", "레드 카나리아", "보더 카나리아", "기타(직접 입력)"],
"핀치": ["문조", "십자매", "금화조", "호금조", "기타(직접 입력)"],
"기타": ["기타(직접 입력)"],
},
"어류": {
"금붕어": ["오란다", "유금", "단정", "진주린", "코메트", "기타(직접 입력)"],
"열대어": ["네온 테트라", "엔젤피쉬", "플래티", "몰리", "디스커스", "기타(직접 입력)"],
"구피": ["고정 구피", "막구피(믹스)", "기타(직접 입력)"],
"잉어": ["비단잉어", "향어", "기타(직접 입력)"],
"기타": ["기타(직접 입력)"],
},
"곤충": {
"장수풍뎅이": ["국산 장수풍뎅이", "헤라클레스 장수풍뎅이", "코카서스 장수풍뎅이", "기타(직접 입력)"],
"사슴벌레": ["넓적사슴벌레", "왕사슴벌레", "톱사슴벌레", "애사슴벌레", "기타(직접 입력)"],
"나비/나방": ["배추흰나비", "호랑나비", "누에나방", "기타(직접 입력)"],
"사마귀": ["왕사마귀", "사마귀", "넓적배사마귀", "기타(직접 입력)"],
"기타": ["기타(직접 입력)"],
},
"절지동물": {
"타란툴라(거미)": ["로즈헤어", "골든니", "화이트니", "핑크토", "기타(직접 입력)"],
"전갈": ["황제전갈", "극동전갈", "아시안 포레스트 전갈", "기타(직접 입력)"],
"지네": ["왕지네", "청지네", "기타(직접 입력)"],
"소라게": ["인도 소라게", "딸기 소라게", "바이오라센트", "기타(직접 입력)"],
"기타": ["기타(직접 입력)"],
},
"기타": {
"기타(직접 입력)": ["기타(직접 입력)"],
},
};
// 선택된 종 정보 (품종 선택을 위해 필요)
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);
});
}
},
),
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();
super.dispose();
}
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: _diseaseList.length,
padding: const EdgeInsets.symmetric(vertical: 10),
itemBuilder: (context, index) {
final disease = _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.keys.length,
itemBuilder: (context, index) {
final major = _petData.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[selectedMajor]!.length,
itemBuilder: (context, index) {
final minor = _petData[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[_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/profileicon.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),
SizedBox(height: 8.h),
_buildTextField(
controller: _nameController,
hint: '이름 입력 (2~10글자/영문/숫자/한글)',
inputFormatters: [
LengthLimitingTextInputFormatter(10), // 최대 10글자 제한
],
),
SizedBox(height: 24.h),
// 3. 선택 박스들 (종, 품종, 성별)
_buildSearchField(
'반려동물 종 선택',
controller: _speciesController,
readOnly: true,
onTap: _showSpeciesSelectionModal,
),
SizedBox(height: 12.h),
_buildSearchField(
'반려동물 품종 선택',
controller: _breedController,
readOnly: true,
onTap: _showBreedSelectionModal,
),
const SizedBox(height: 12),
_buildSearchField(
'반려동물 성별',
controller: _genderController,
readOnly: true,
onTap: _showGenderSelectionModal,
),
const SizedBox(height: 24),
// 4. 생년월일
_buildLabel('반려동물 생년월일', isRequired: true),
const SizedBox(height: 8),
Row(
children: [
Expanded(
child: _buildTextField(
controller: _yearController,
hint: 'YYYY',
textAlign: TextAlign.center,
hintColor: _isDateUnknown
? const Color(0xFFC8C8C8)
: const Color(0xFF7D7C7C),
enabled: !_isDateUnknown,
keyboardType: TextInputType.number,
inputFormatters: [
FilteringTextInputFormatter.digitsOnly,
LengthLimitingTextInputFormatter(4),
],
),
),
SizedBox(width: 12.w),
Expanded(
child: _buildTextField(
controller: _monthController,
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),
],
),
),
SizedBox(width: 12.w),
Expanded(
child: _buildTextField(
controller: _dayController,
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(
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(', ');
});
},
),
),
const SizedBox(height: 24),
_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(', ');
});
},
),
),
const SizedBox(height: 24),
_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: () {},
style: ElevatedButton.styleFrom(
backgroundColor: AppColors.highlight,
elevation: 0,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(30.r),
),
),
child: 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(
children: [
if (isRequired)
Padding(
padding: EdgeInsets.only(right: 4.w),
child: Icon(Icons.circle, size: 4.w, color: Colors.red),
),
Text(
text,
style: TextStyle(
fontFamily: 'SCDream',
fontSize: 13.sp,
fontWeight: FontWeight.bold,
color: Colors.black87,
),
),
],
);
}
// Helper Widget: 텍스트 입력 필드
Widget _buildTextField({
required String hint,
TextAlign textAlign = TextAlign.start,
Color? hintColor,
bool enabled = true,
TextEditingController? controller,
TextInputType? keyboardType,
List<TextInputFormatter>? inputFormatters,
}) {
return TextField(
controller: controller,
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,
}) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
label,
style: TextStyle(
fontFamily: 'SCDream',
fontSize: 13.sp,
fontWeight: FontWeight.bold,
color: Colors.black87,
),
),
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,
),
),
),
],
);
}
}
// 범위 제한 Formatter (월: 1~12)
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 < 0) {
return oldValue;
}
// 입력 중에는 자릿수 제한이 있으므로, max값 초과 여부만 확인
if (value > max) {
return oldValue;
}
return newValue;
}
}
// 일(Day) 입력 Formatter (월에 따라 28~31일 제한)
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 maxDay = 31;
final int? month = int.tryParse(monthController.text);
if (month != null) {
if (month == 2) {
maxDay = 29; // 기본 29일
// 윤년 계산
final int? year = int.tryParse(yearController.text);
if (year != null) {
bool isLeap = (year % 4 == 0 && year % 100 != 0) || (year % 400 == 0);
maxDay = isLeap ? 29 : 28;
}
} else if ([4, 6, 9, 11].contains(month)) {
maxDay = 30;
}
}
if (day > maxDay) {
return oldValue;
}
return newValue;
}
}