홈화면 동물 프로필 카드 추가 / 수정 페이지 추가/ 수정, 추가에 따른 db 업데이트
This commit is contained in:
parent
8dc2524ba6
commit
721b748703
BIN
app/assets/img/profile_card_background.png
Normal file
BIN
app/assets/img/profile_card_background.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 13 KiB |
@ -12,6 +12,7 @@ class Pet {
|
|||||||
final bool isDateUnknown;
|
final bool isDateUnknown;
|
||||||
final String? registrationNumber;
|
final String? registrationNumber;
|
||||||
final String? profileImageUrl;
|
final String? profileImageUrl;
|
||||||
|
final double? weight; // 체중 (kg)
|
||||||
final List<String> diseases;
|
final List<String> diseases;
|
||||||
final List<String> pastDiseases;
|
final List<String> pastDiseases;
|
||||||
final List<String> healthConcerns;
|
final List<String> healthConcerns;
|
||||||
@ -29,6 +30,7 @@ class Pet {
|
|||||||
required this.isDateUnknown,
|
required this.isDateUnknown,
|
||||||
this.registrationNumber,
|
this.registrationNumber,
|
||||||
this.profileImageUrl,
|
this.profileImageUrl,
|
||||||
|
this.weight,
|
||||||
required this.diseases,
|
required this.diseases,
|
||||||
required this.pastDiseases,
|
required this.pastDiseases,
|
||||||
required this.healthConcerns,
|
required this.healthConcerns,
|
||||||
@ -48,6 +50,7 @@ class Pet {
|
|||||||
'isDateUnknown': isDateUnknown,
|
'isDateUnknown': isDateUnknown,
|
||||||
'registrationNumber': registrationNumber,
|
'registrationNumber': registrationNumber,
|
||||||
'profileImageUrl': profileImageUrl,
|
'profileImageUrl': profileImageUrl,
|
||||||
|
'weight': weight,
|
||||||
'diseases': diseases,
|
'diseases': diseases,
|
||||||
'pastDiseases': pastDiseases,
|
'pastDiseases': pastDiseases,
|
||||||
'healthConcerns': healthConcerns,
|
'healthConcerns': healthConcerns,
|
||||||
@ -70,6 +73,7 @@ class Pet {
|
|||||||
isDateUnknown: map['isDateUnknown'] ?? false,
|
isDateUnknown: map['isDateUnknown'] ?? false,
|
||||||
registrationNumber: map['registrationNumber'],
|
registrationNumber: map['registrationNumber'],
|
||||||
profileImageUrl: map['profileImageUrl'],
|
profileImageUrl: map['profileImageUrl'],
|
||||||
|
weight: map['weight']?.toDouble(),
|
||||||
diseases: List<String>.from(map['diseases'] ?? []),
|
diseases: List<String>.from(map['diseases'] ?? []),
|
||||||
pastDiseases: List<String>.from(map['pastDiseases'] ?? []),
|
pastDiseases: List<String>.from(map['pastDiseases'] ?? []),
|
||||||
healthConcerns: List<String>.from(map['healthConcerns'] ?? []),
|
healthConcerns: List<String>.from(map['healthConcerns'] ?? []),
|
||||||
|
|||||||
@ -1,10 +1,11 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_screenutil/flutter_screenutil.dart';
|
import 'package:flutter_screenutil/flutter_screenutil.dart';
|
||||||
import 'package:flutter_svg/flutter_svg.dart';
|
import 'package:flutter_svg/flutter_svg.dart';
|
||||||
import 'pet_registration_screen.dart';
|
import 'pet_form_screen.dart';
|
||||||
import '../services/firestore_service.dart';
|
import '../services/firestore_service.dart';
|
||||||
import '../models/pet_model.dart';
|
import '../models/pet_model.dart';
|
||||||
import '../theme/app_colors.dart';
|
import '../theme/app_colors.dart';
|
||||||
|
import '../widgets/home/pet_profile_card.dart';
|
||||||
|
|
||||||
class HomeScreen extends StatefulWidget {
|
class HomeScreen extends StatefulWidget {
|
||||||
const HomeScreen({super.key});
|
const HomeScreen({super.key});
|
||||||
@ -24,124 +25,6 @@ class _HomeScreenState extends State<HomeScreen> {
|
|||||||
_userId = _firestoreService.getCurrentUserId();
|
_userId = _firestoreService.getCurrentUserId();
|
||||||
}
|
}
|
||||||
|
|
||||||
// 반려동물 선택 시 호출
|
|
||||||
void _selectPet(Pet pet) {
|
|
||||||
setState(() {
|
|
||||||
_selectedPet = pet;
|
|
||||||
});
|
|
||||||
Navigator.pop(context); // 모달 닫기
|
|
||||||
}
|
|
||||||
|
|
||||||
// 반려동물 선택 모달 표시
|
|
||||||
void _showPetSelectionModal(List<Pet> pets) {
|
|
||||||
showModalBottomSheet(
|
|
||||||
context: context,
|
|
||||||
backgroundColor: Colors.transparent,
|
|
||||||
isScrollControlled: true,
|
|
||||||
builder: (context) {
|
|
||||||
return Container(
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
color: Colors.white,
|
|
||||||
borderRadius: BorderRadius.vertical(top: Radius.circular(20.r)),
|
|
||||||
),
|
|
||||||
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),
|
|
||||||
// 반려동물 리스트
|
|
||||||
Flexible(
|
|
||||||
child: ListView.builder(
|
|
||||||
shrinkWrap: true,
|
|
||||||
itemCount: pets.length,
|
|
||||||
itemBuilder: (context, index) {
|
|
||||||
final pet = pets[index];
|
|
||||||
final isSelected = pet.id == _selectedPet?.id;
|
|
||||||
return ListTile(
|
|
||||||
leading: CircleAvatar(
|
|
||||||
radius: 24.r,
|
|
||||||
backgroundColor: Colors.grey[200],
|
|
||||||
backgroundImage: pet.profileImageUrl != null
|
|
||||||
? NetworkImage(pet.profileImageUrl!)
|
|
||||||
: null,
|
|
||||||
child: pet.profileImageUrl == null
|
|
||||||
? SvgPicture.asset(
|
|
||||||
'assets/icons/profile_icon.svg',
|
|
||||||
width: 24.w,
|
|
||||||
colorFilter: ColorFilter.mode(
|
|
||||||
Colors.grey[400]!,
|
|
||||||
BlendMode.srcIn,
|
|
||||||
),
|
|
||||||
)
|
|
||||||
: null,
|
|
||||||
),
|
|
||||||
title: Text(
|
|
||||||
pet.name,
|
|
||||||
style: TextStyle(
|
|
||||||
fontFamily: 'SCDream',
|
|
||||||
fontSize: 16.sp,
|
|
||||||
fontWeight: isSelected
|
|
||||||
? FontWeight.bold
|
|
||||||
: FontWeight.normal,
|
|
||||||
color: isSelected
|
|
||||||
? AppColors.highlight
|
|
||||||
: Colors.black,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
trailing: isSelected
|
|
||||||
? const Icon(Icons.check, color: AppColors.highlight)
|
|
||||||
: null,
|
|
||||||
onTap: () => _selectPet(pet),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
),
|
|
||||||
Divider(thickness: 1, color: Colors.grey[200]),
|
|
||||||
// 반려동물 추가 버튼
|
|
||||||
ListTile(
|
|
||||||
leading: Container(
|
|
||||||
width: 48.r,
|
|
||||||
height: 48.r,
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
color: Colors.grey[100],
|
|
||||||
shape: BoxShape.circle,
|
|
||||||
),
|
|
||||||
child: const Icon(Icons.add, color: Colors.black54),
|
|
||||||
),
|
|
||||||
title: Text(
|
|
||||||
'반려동물 추가하기',
|
|
||||||
style: TextStyle(
|
|
||||||
fontFamily: 'SCDream',
|
|
||||||
fontSize: 16.sp,
|
|
||||||
fontWeight: FontWeight.w500,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
onTap: () {
|
|
||||||
Navigator.pop(context);
|
|
||||||
Navigator.push(
|
|
||||||
context,
|
|
||||||
MaterialPageRoute(
|
|
||||||
builder: (context) => const PetRegistrationScreen(),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
SizedBox(height: 30.h),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
if (_userId == null) {
|
if (_userId == null) {
|
||||||
@ -182,8 +65,7 @@ class _HomeScreenState extends State<HomeScreen> {
|
|||||||
Navigator.push(
|
Navigator.push(
|
||||||
context,
|
context,
|
||||||
MaterialPageRoute(
|
MaterialPageRoute(
|
||||||
builder: (context) =>
|
builder: (context) => const PetFormScreen(),
|
||||||
const PetRegistrationScreen(),
|
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
@ -233,19 +115,16 @@ class _HomeScreenState extends State<HomeScreen> {
|
|||||||
|
|
||||||
// 등록된 반려동물이 있을 때
|
// 등록된 반려동물이 있을 때
|
||||||
// 선택된 펫이 없거나 리스트에 없으면 첫 번째 펫 선택
|
// 선택된 펫이 없거나 리스트에 없으면 첫 번째 펫 선택
|
||||||
if (_selectedPet == null ||
|
// 등록된 반려동물이 있을 때
|
||||||
!pets.any((p) => p.id == _selectedPet!.id)) {
|
Pet displayPet;
|
||||||
// We shouldn't update state directly in build, but for initialization it's tricky.
|
|
||||||
// Using the first pet as default display.
|
|
||||||
// Better approach: use a local variable for display, update state in callbacks.
|
|
||||||
_selectedPet = pets.first;
|
|
||||||
}
|
|
||||||
|
|
||||||
// To ensure _selectedPet is valid (e.g. after deletion), find it in the new list
|
// 선택된 펫이 없거나 리스트에 없으면 첫 번째 펫 선택 (State 변경 없이 화면 표시만 처리)
|
||||||
final displayPet = pets.firstWhere(
|
if (_selectedPet != null &&
|
||||||
(p) => p.id == _selectedPet?.id,
|
pets.any((p) => p.id == _selectedPet!.id)) {
|
||||||
orElse: () => pets.first,
|
displayPet = pets.firstWhere((p) => p.id == _selectedPet!.id);
|
||||||
);
|
} else {
|
||||||
|
displayPet = pets.first;
|
||||||
|
}
|
||||||
|
|
||||||
return Column(
|
return Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
@ -255,8 +134,109 @@ class _HomeScreenState extends State<HomeScreen> {
|
|||||||
horizontal: 20.w,
|
horizontal: 20.w,
|
||||||
vertical: 20.h,
|
vertical: 20.h,
|
||||||
),
|
),
|
||||||
child: GestureDetector(
|
child: PopupMenuButton<dynamic>(
|
||||||
onTap: () => _showPetSelectionModal(pets),
|
offset: Offset(0, 50.h), // 헤더 바로 아래에 위치하도록 조정
|
||||||
|
elevation: 3,
|
||||||
|
shape: RoundedRectangleBorder(
|
||||||
|
borderRadius: BorderRadius.circular(12.r),
|
||||||
|
),
|
||||||
|
color: Colors.white,
|
||||||
|
surfaceTintColor: Colors.white,
|
||||||
|
onSelected: (value) {
|
||||||
|
if (value is Pet) {
|
||||||
|
setState(() {
|
||||||
|
_selectedPet = value;
|
||||||
|
});
|
||||||
|
} else if (value == 'add_pet') {
|
||||||
|
Navigator.push(
|
||||||
|
context,
|
||||||
|
MaterialPageRoute(
|
||||||
|
builder: (context) => const PetFormScreen(),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
itemBuilder: (context) {
|
||||||
|
return [
|
||||||
|
...pets.map(
|
||||||
|
(pet) => PopupMenuItem<Pet>(
|
||||||
|
value: pet,
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
CircleAvatar(
|
||||||
|
radius: 16.r,
|
||||||
|
backgroundColor: Colors.grey[200],
|
||||||
|
backgroundImage: pet.profileImageUrl != null
|
||||||
|
? NetworkImage(pet.profileImageUrl!)
|
||||||
|
: null,
|
||||||
|
child: pet.profileImageUrl == null
|
||||||
|
? SvgPicture.asset(
|
||||||
|
'assets/icons/profile_icon.svg',
|
||||||
|
width: 16.w,
|
||||||
|
colorFilter: ColorFilter.mode(
|
||||||
|
Colors.grey[400]!,
|
||||||
|
BlendMode.srcIn,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
: null,
|
||||||
|
),
|
||||||
|
SizedBox(width: 10.w),
|
||||||
|
Text(
|
||||||
|
pet.name,
|
||||||
|
style: TextStyle(
|
||||||
|
fontFamily: 'SCDream',
|
||||||
|
fontSize: 14.sp,
|
||||||
|
fontWeight: pet.id == displayPet.id
|
||||||
|
? FontWeight.bold
|
||||||
|
: FontWeight.normal,
|
||||||
|
color: pet.id == displayPet.id
|
||||||
|
? AppColors.highlight
|
||||||
|
: Colors.black,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
if (pet.id == displayPet.id) ...[
|
||||||
|
const Spacer(),
|
||||||
|
const Icon(
|
||||||
|
Icons.check,
|
||||||
|
color: AppColors.highlight,
|
||||||
|
size: 16,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const PopupMenuDivider(),
|
||||||
|
PopupMenuItem<String>(
|
||||||
|
value: 'add_pet',
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
Container(
|
||||||
|
padding: EdgeInsets.all(4.w),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: Colors.grey[100],
|
||||||
|
shape: BoxShape.circle,
|
||||||
|
),
|
||||||
|
child: Icon(
|
||||||
|
Icons.add,
|
||||||
|
size: 16.w,
|
||||||
|
color: Colors.black54,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
SizedBox(width: 10.w),
|
||||||
|
Text(
|
||||||
|
'반려동물 추가하기',
|
||||||
|
style: TextStyle(
|
||||||
|
fontFamily: 'SCDream',
|
||||||
|
fontSize: 14.sp,
|
||||||
|
fontWeight: FontWeight.w500,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
];
|
||||||
|
},
|
||||||
child: Row(
|
child: Row(
|
||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
children: [
|
children: [
|
||||||
@ -301,15 +281,8 @@ class _HomeScreenState extends State<HomeScreen> {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
Expanded(
|
Expanded(
|
||||||
child: Center(
|
child: SingleChildScrollView(
|
||||||
child: Text(
|
child: Column(children: [PetProfileCard(pet: displayPet)]),
|
||||||
'안녕, ${displayPet.name}!',
|
|
||||||
style: TextStyle(
|
|
||||||
fontFamily: 'SCDream',
|
|
||||||
fontSize: 24.sp,
|
|
||||||
fontWeight: FontWeight.bold,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
|
|||||||
1156
app/lib/screens/pet_detail_screen.dart
Normal file
1156
app/lib/screens/pet_detail_screen.dart
Normal file
File diff suppressed because it is too large
Load Diff
1726
app/lib/screens/pet_form_screen.dart
Normal file
1726
app/lib/screens/pet_form_screen.dart
Normal file
File diff suppressed because it is too large
Load Diff
@ -62,6 +62,7 @@ class _PetRegistrationScreenState extends State<PetRegistrationScreen> {
|
|||||||
_yearController.addListener(_updateState);
|
_yearController.addListener(_updateState);
|
||||||
_monthController.addListener(_updateState);
|
_monthController.addListener(_updateState);
|
||||||
_dayController.addListener(_updateState);
|
_dayController.addListener(_updateState);
|
||||||
|
_weightController.addListener(_updateState);
|
||||||
}
|
}
|
||||||
|
|
||||||
void _updateState() {
|
void _updateState() {
|
||||||
@ -79,6 +80,8 @@ class _PetRegistrationScreenState extends State<PetRegistrationScreen> {
|
|||||||
TextEditingController(); // 품종 컨트롤러
|
TextEditingController(); // 품종 컨트롤러
|
||||||
final TextEditingController _genderController =
|
final TextEditingController _genderController =
|
||||||
TextEditingController(); // 성별 컨트롤러
|
TextEditingController(); // 성별 컨트롤러
|
||||||
|
final TextEditingController _weightController =
|
||||||
|
TextEditingController(); // 체중 컨트롤러
|
||||||
|
|
||||||
// 종 데이터 (Removed: Use PetData.breedsData)
|
// 종 데이터 (Removed: Use PetData.breedsData)
|
||||||
|
|
||||||
@ -217,6 +220,7 @@ class _PetRegistrationScreenState extends State<PetRegistrationScreen> {
|
|||||||
_monthFocus.dispose();
|
_monthFocus.dispose();
|
||||||
_dayFocus.dispose();
|
_dayFocus.dispose();
|
||||||
_registrationNumberController.dispose();
|
_registrationNumberController.dispose();
|
||||||
|
_weightController.dispose();
|
||||||
super.dispose();
|
super.dispose();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -281,6 +285,12 @@ class _PetRegistrationScreenState extends State<PetRegistrationScreen> {
|
|||||||
registrationNumber: _registrationNumberController.text.isNotEmpty
|
registrationNumber: _registrationNumberController.text.isNotEmpty
|
||||||
? _registrationNumberController.text
|
? _registrationNumberController.text
|
||||||
: null,
|
: null,
|
||||||
|
weight:
|
||||||
|
_weightController
|
||||||
|
.text
|
||||||
|
.isNotEmpty // 체중 추가
|
||||||
|
? double.tryParse(_weightController.text)
|
||||||
|
: null,
|
||||||
diseases: finalDiseases,
|
diseases: finalDiseases,
|
||||||
pastDiseases: finalPastDiseases,
|
pastDiseases: finalPastDiseases,
|
||||||
healthConcerns: finalHealthConcerns,
|
healthConcerns: finalHealthConcerns,
|
||||||
@ -1616,6 +1626,21 @@ class _PetRegistrationScreenState extends State<PetRegistrationScreen> {
|
|||||||
),
|
),
|
||||||
const SizedBox(height: 24),
|
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. 질환 정보 (검색 아이콘 포함)
|
// 6. 질환 정보 (검색 아이콘 포함)
|
||||||
_buildSearchField(
|
_buildSearchField(
|
||||||
'보유 질환',
|
'보유 질환',
|
||||||
@ -1784,6 +1809,7 @@ class _PetRegistrationScreenState extends State<PetRegistrationScreen> {
|
|||||||
List<TextInputFormatter>? inputFormatters,
|
List<TextInputFormatter>? inputFormatters,
|
||||||
FocusNode? focusNode,
|
FocusNode? focusNode,
|
||||||
ValueChanged<String>? onChanged,
|
ValueChanged<String>? onChanged,
|
||||||
|
String? suffix,
|
||||||
}) {
|
}) {
|
||||||
return TextField(
|
return TextField(
|
||||||
controller: controller,
|
controller: controller,
|
||||||
@ -1805,6 +1831,13 @@ class _PetRegistrationScreenState extends State<PetRegistrationScreen> {
|
|||||||
fontSize: 14.sp,
|
fontSize: 14.sp,
|
||||||
color: hintColor ?? Colors.grey,
|
color: hintColor ?? Colors.grey,
|
||||||
),
|
),
|
||||||
|
suffixText: suffix,
|
||||||
|
suffixStyle: TextStyle(
|
||||||
|
fontFamily: 'SCDream',
|
||||||
|
fontSize: 14.sp,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
color: Colors.black,
|
||||||
|
),
|
||||||
enabledBorder: const UnderlineInputBorder(
|
enabledBorder: const UnderlineInputBorder(
|
||||||
borderSide: BorderSide(color: Color(0xFFDDDDDD)),
|
borderSide: BorderSide(color: Color(0xFFDDDDDD)),
|
||||||
),
|
),
|
||||||
|
|||||||
@ -58,6 +58,7 @@ class FirestoreService {
|
|||||||
isDateUnknown: pet.isDateUnknown,
|
isDateUnknown: pet.isDateUnknown,
|
||||||
registrationNumber: pet.registrationNumber,
|
registrationNumber: pet.registrationNumber,
|
||||||
profileImageUrl: imageUrl, // 이미지 URL 설정
|
profileImageUrl: imageUrl, // 이미지 URL 설정
|
||||||
|
weight: pet.weight, // 체중 추가
|
||||||
diseases: pet.diseases,
|
diseases: pet.diseases,
|
||||||
pastDiseases: pet.pastDiseases,
|
pastDiseases: pet.pastDiseases,
|
||||||
healthConcerns: pet.healthConcerns,
|
healthConcerns: pet.healthConcerns,
|
||||||
@ -74,6 +75,57 @@ class FirestoreService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 반려동물 정보 수정
|
||||||
|
Future<void> updatePet(Pet pet, File? newImageFile) async {
|
||||||
|
try {
|
||||||
|
String? imageUrl = pet.profileImageUrl;
|
||||||
|
|
||||||
|
// 1. 새 이미지가 있다면 업로드 및 기존 이미지 삭제(선택사항)
|
||||||
|
if (newImageFile != null) {
|
||||||
|
// 기존 이미지가 있다면 삭제할 수도 있겠지만, 여기선 덮어쓰거나 새로 올림
|
||||||
|
// 간단히 새로 올리고 URL 교체
|
||||||
|
final String fileName =
|
||||||
|
'${pet.id}_${DateTime.now().millisecondsSinceEpoch}.jpg';
|
||||||
|
final Reference storageRef = _storage
|
||||||
|
.ref()
|
||||||
|
.child('pet_images')
|
||||||
|
.child(fileName);
|
||||||
|
|
||||||
|
LogManager().addLog('[Storage] Uploading new image...');
|
||||||
|
await storageRef.putFile(newImageFile);
|
||||||
|
imageUrl = await storageRef.getDownloadURL();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. Pet 객체 업데이트
|
||||||
|
Pet updatedPet = Pet(
|
||||||
|
id: pet.id, // ID 유지
|
||||||
|
ownerId: pet.ownerId, // Owner ID 유지
|
||||||
|
name: pet.name,
|
||||||
|
species: pet.species,
|
||||||
|
breed: pet.breed,
|
||||||
|
gender: pet.gender,
|
||||||
|
isNeutered: pet.isNeutered,
|
||||||
|
birthDate: pet.birthDate,
|
||||||
|
isDateUnknown: pet.isDateUnknown,
|
||||||
|
registrationNumber: pet.registrationNumber,
|
||||||
|
profileImageUrl: imageUrl,
|
||||||
|
weight: pet.weight,
|
||||||
|
diseases: pet.diseases,
|
||||||
|
pastDiseases: pet.pastDiseases,
|
||||||
|
healthConcerns: pet.healthConcerns,
|
||||||
|
createdAt: pet.createdAt, // 생성일 유지
|
||||||
|
);
|
||||||
|
|
||||||
|
// 3. Firestore 업데이트
|
||||||
|
await _db.collection('pets').doc(pet.id).set(updatedPet.toMap());
|
||||||
|
|
||||||
|
LogManager().addLog('[Firestore] Pet updated successfully: ${pet.id}');
|
||||||
|
} catch (e) {
|
||||||
|
LogManager().addLog('[Firestore] Error updating pet: $e');
|
||||||
|
throw Exception('반려동물 수정 실패: $e');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// 현재 로그인한 사용자의 ID 가져오기
|
// 현재 로그인한 사용자의 ID 가져오기
|
||||||
String? getCurrentUserId() {
|
String? getCurrentUserId() {
|
||||||
return _auth.currentUser?.uid;
|
return _auth.currentUser?.uid;
|
||||||
|
|||||||
@ -1,4 +1,6 @@
|
|||||||
import 'package:flutter/foundation.dart';
|
import 'package:flutter/foundation.dart';
|
||||||
|
import 'package:flutter/widgets.dart'; // For WidgetsBinding
|
||||||
|
import 'package:flutter/scheduler.dart'; // For SchedulerPhase
|
||||||
|
|
||||||
class LogManager {
|
class LogManager {
|
||||||
static final LogManager _instance = LogManager._internal();
|
static final LogManager _instance = LogManager._internal();
|
||||||
@ -12,7 +14,15 @@ class LogManager {
|
|||||||
final timestamp = DateTime.now().toString().split(' ')[1].split('.')[0];
|
final timestamp = DateTime.now().toString().split(' ')[1].split('.')[0];
|
||||||
final logMessage = "[$timestamp] $message";
|
final logMessage = "[$timestamp] $message";
|
||||||
|
|
||||||
|
// 빌드 중에 호출될 경우를 대비해 스케줄링
|
||||||
|
if (WidgetsBinding.instance.schedulerPhase ==
|
||||||
|
SchedulerPhase.persistentCallbacks) {
|
||||||
|
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||||
logs.value = [logMessage, ...logs.value];
|
logs.value = [logMessage, ...logs.value];
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
logs.value = [logMessage, ...logs.value];
|
||||||
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
print('LogManager Error: $e');
|
print('LogManager Error: $e');
|
||||||
}
|
}
|
||||||
|
|||||||
400
app/lib/widgets/home/pet_profile_card.dart
Normal file
400
app/lib/widgets/home/pet_profile_card.dart
Normal file
@ -0,0 +1,400 @@
|
|||||||
|
import 'dart:ui';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_screenutil/flutter_screenutil.dart';
|
||||||
|
import 'package:flutter_svg/flutter_svg.dart';
|
||||||
|
import 'package:intl/intl.dart';
|
||||||
|
import '../../models/pet_model.dart';
|
||||||
|
import '../../screens/pet_form_screen.dart';
|
||||||
|
|
||||||
|
class PetProfileCard extends StatelessWidget {
|
||||||
|
final Pet pet;
|
||||||
|
|
||||||
|
const PetProfileCard({super.key, required this.pet});
|
||||||
|
|
||||||
|
// 나이 계산 (만 나이 & 사람 나이 환산 - 단순 예시)
|
||||||
|
String _calculateAge(DateTime? birthDate) {
|
||||||
|
if (birthDate == null) return '알 수 없음';
|
||||||
|
final now = DateTime.now();
|
||||||
|
int age = now.year - birthDate.year;
|
||||||
|
if (now.month < birthDate.month ||
|
||||||
|
(now.month == birthDate.month && now.day < birthDate.day)) {
|
||||||
|
age--;
|
||||||
|
}
|
||||||
|
return '$age세';
|
||||||
|
}
|
||||||
|
|
||||||
|
// 사람 나이 환산 (강아지 기준 대략적 계산 - 소형견 기준 예시)
|
||||||
|
// 사람 나이 환산 (종별 계산법 적용)
|
||||||
|
String _calculateHumanAge(DateTime? birthDate, String species) {
|
||||||
|
if (birthDate == null) return '??세';
|
||||||
|
|
||||||
|
final now = DateTime.now();
|
||||||
|
int ageYears = now.year - birthDate.year;
|
||||||
|
// 생일이 안 지났으면 만 나이 적용
|
||||||
|
if (now.month < birthDate.month ||
|
||||||
|
(now.month == birthDate.month && now.day < birthDate.day)) {
|
||||||
|
ageYears--;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 개월 수 계산 (햄스터/토끼 등 단명 동물용)
|
||||||
|
int ageMonths =
|
||||||
|
(now.year - birthDate.year) * 12 + now.month - birthDate.month;
|
||||||
|
if (now.day < birthDate.day) {
|
||||||
|
ageMonths--;
|
||||||
|
}
|
||||||
|
|
||||||
|
int humanAge = 0;
|
||||||
|
|
||||||
|
switch (species) {
|
||||||
|
case '강아지':
|
||||||
|
// 강아지: 1년=15살, 2년=24살, 3년+=5살씩
|
||||||
|
if (ageYears < 1)
|
||||||
|
humanAge = (ageMonths * 1.25).round(); // 15/12
|
||||||
|
else if (ageYears == 1)
|
||||||
|
humanAge = 15;
|
||||||
|
else if (ageYears == 2)
|
||||||
|
humanAge = 24;
|
||||||
|
else
|
||||||
|
humanAge = 24 + (ageYears - 2) * 5;
|
||||||
|
break;
|
||||||
|
|
||||||
|
case '고양이':
|
||||||
|
// 고양이: 1년=15살, 2년=24살, 3년+=4살씩
|
||||||
|
if (ageYears < 1)
|
||||||
|
humanAge = (ageMonths * 1.25).round();
|
||||||
|
else if (ageYears == 1)
|
||||||
|
humanAge = 15;
|
||||||
|
else if (ageYears == 2)
|
||||||
|
humanAge = 24;
|
||||||
|
else
|
||||||
|
humanAge = 24 + (ageYears - 2) * 4;
|
||||||
|
break;
|
||||||
|
|
||||||
|
case '햄스터':
|
||||||
|
// 햄스터: 1개월=약 5살, 1년=58살, 2년=70살, その後
|
||||||
|
// 단순화: 개월당 5살로 치되, 1년차부터 보정
|
||||||
|
// 1개월~: month * 5
|
||||||
|
humanAge = ageMonths * 5;
|
||||||
|
// 1년(12개월) = 60 근사치. 2년(24개월) = 120 (너무 많음).
|
||||||
|
// 햄스터는 2~3년이 수명이므로 보정 필요
|
||||||
|
// 1년=58살, 2년=70살, 3년=100살
|
||||||
|
if (ageMonths >= 12 && ageMonths < 24) {
|
||||||
|
// 1년~2년 사이: 58 + ((month-12) * 1) -> 1년 70까지 천천히
|
||||||
|
// 58 + (ageMonths - 12); // 12개월=58, 23개월=69
|
||||||
|
humanAge = 58 + (ageMonths - 12);
|
||||||
|
} else if (ageMonths >= 24) {
|
||||||
|
// 2년 이상: 70 + (month-24)*2.5 (3년차에 100되도록)
|
||||||
|
humanAge = 70 + ((ageMonths - 24) * 2.5).round();
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case '토끼':
|
||||||
|
// 토끼: 6개월=16, 1년=21, 2년=28, 이후 +6
|
||||||
|
if (ageMonths < 6)
|
||||||
|
humanAge = (ageMonths * 2.6).round(); // 16/6
|
||||||
|
else if (ageMonths < 12)
|
||||||
|
humanAge = 16 + (ageMonths - 6); // 16~21
|
||||||
|
else if (ageYears == 1)
|
||||||
|
humanAge = 21;
|
||||||
|
else if (ageYears == 2)
|
||||||
|
humanAge = 28;
|
||||||
|
else
|
||||||
|
humanAge = 28 + (ageYears - 2) * 6;
|
||||||
|
break;
|
||||||
|
|
||||||
|
default:
|
||||||
|
// 기타 (기니피그, 앵무새, 파충류 등): 강아지 로직 fallback 혹은 1:1
|
||||||
|
// 일단 강아지 로직을 따르되, 사용자가 인지하게끔
|
||||||
|
// 여기서는 그냥 강아지와 동일하게 처리 (사용자 요청: 미적용 동물 알려달라함)
|
||||||
|
if (ageYears < 1)
|
||||||
|
humanAge = (ageMonths * 1.25).round();
|
||||||
|
else if (ageYears == 1)
|
||||||
|
humanAge = 15;
|
||||||
|
else if (ageYears == 2)
|
||||||
|
humanAge = 24;
|
||||||
|
else
|
||||||
|
humanAge = 24 + (ageYears - 2) * 5;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
return '$humanAge세';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Container(
|
||||||
|
margin: EdgeInsets.symmetric(horizontal: 20.w, vertical: 10.h),
|
||||||
|
padding: EdgeInsets.all(12.w),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
borderRadius: BorderRadius.circular(20.r),
|
||||||
|
image: const DecorationImage(
|
||||||
|
image: AssetImage('assets/img/profile_card_background.png'),
|
||||||
|
fit: BoxFit.cover,
|
||||||
|
),
|
||||||
|
boxShadow: [
|
||||||
|
BoxShadow(
|
||||||
|
color: Colors.black.withOpacity(0.15),
|
||||||
|
blurRadius: 10,
|
||||||
|
offset: const Offset(0, 4),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
child: IntrinsicHeight(
|
||||||
|
child: Row(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||||
|
children: [
|
||||||
|
// 왼쪽: 프로필 이미지
|
||||||
|
Container(
|
||||||
|
width: 120.w,
|
||||||
|
// height 제거 (stretch 되도록)
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
borderRadius: BorderRadius.circular(16.r),
|
||||||
|
color: Colors.grey[200],
|
||||||
|
image: pet.profileImageUrl != null
|
||||||
|
? DecorationImage(
|
||||||
|
image: NetworkImage(pet.profileImageUrl!),
|
||||||
|
fit: BoxFit.cover,
|
||||||
|
)
|
||||||
|
: null,
|
||||||
|
),
|
||||||
|
child: pet.profileImageUrl == null
|
||||||
|
? Center(
|
||||||
|
child: SvgPicture.asset(
|
||||||
|
'assets/icons/profile_icon.svg',
|
||||||
|
width: 40.w,
|
||||||
|
colorFilter: ColorFilter.mode(
|
||||||
|
Colors.grey[400]!,
|
||||||
|
BlendMode.srcIn,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
: null,
|
||||||
|
),
|
||||||
|
SizedBox(width: 10.w),
|
||||||
|
// 오른쪽: 정보 영역
|
||||||
|
Expanded(
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
// 이름 & 종
|
||||||
|
Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
|
children: [
|
||||||
|
Expanded(
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
Flexible(
|
||||||
|
child: Text(
|
||||||
|
pet.name,
|
||||||
|
style: TextStyle(
|
||||||
|
fontFamily: 'SCDream',
|
||||||
|
fontSize: 22.sp,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
color: Colors.black,
|
||||||
|
),
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
if (pet.gender == '남아' || pet.gender == '여아') ...[
|
||||||
|
SizedBox(width: 6.w),
|
||||||
|
Icon(
|
||||||
|
pet.gender == '남아' ? Icons.male : Icons.female,
|
||||||
|
color: pet.gender == '남아'
|
||||||
|
? Colors.blue
|
||||||
|
: Colors.pinkAccent,
|
||||||
|
size: 20.sp,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
// 상세 프로필 이동 (우측 상단)
|
||||||
|
// 상세 프로필 이동 (우측 상단)
|
||||||
|
Material(
|
||||||
|
color: Colors.transparent,
|
||||||
|
child: InkWell(
|
||||||
|
borderRadius: BorderRadius.circular(20),
|
||||||
|
onTap: () {
|
||||||
|
Navigator.push(
|
||||||
|
context,
|
||||||
|
MaterialPageRoute(
|
||||||
|
builder: (context) =>
|
||||||
|
PetFormScreen(petToEdit: pet),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
child: Padding(
|
||||||
|
padding: EdgeInsets.all(8.w),
|
||||||
|
child: Icon(
|
||||||
|
Icons.arrow_forward_ios,
|
||||||
|
size: 16.sp,
|
||||||
|
color: Colors.grey[400],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
SizedBox(height: 2.h),
|
||||||
|
Text(
|
||||||
|
pet.breed,
|
||||||
|
style: TextStyle(
|
||||||
|
fontFamily: 'SCDream',
|
||||||
|
fontSize: 14.sp,
|
||||||
|
color: Colors.grey[600],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
SizedBox(height: 8.h), // 간격 줄임 (12 -> 8)
|
||||||
|
// 정보 박스 (생일, 체중)
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
_buildInfoBox(
|
||||||
|
'생일',
|
||||||
|
pet.birthDate != null
|
||||||
|
? DateFormat('yy.MM.dd').format(pet.birthDate!)
|
||||||
|
: '??.??.??',
|
||||||
|
),
|
||||||
|
SizedBox(width: 8.w),
|
||||||
|
_buildInfoBox(
|
||||||
|
'체중',
|
||||||
|
pet.weight != null ? '${pet.weight}kg' : '--kg',
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
SizedBox(height: 8.h), // 간격 줄임 (12 -> 8)
|
||||||
|
// 나이 정보
|
||||||
|
FittedBox(
|
||||||
|
fit: BoxFit.scaleDown,
|
||||||
|
alignment: Alignment.centerLeft,
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
'나이 ',
|
||||||
|
style: TextStyle(
|
||||||
|
fontFamily: 'SCDream',
|
||||||
|
fontSize: 14.sp,
|
||||||
|
color: Colors.grey[600],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Text(
|
||||||
|
_calculateAge(pet.birthDate),
|
||||||
|
style: TextStyle(
|
||||||
|
fontFamily: 'SCDream',
|
||||||
|
fontSize: 15.sp,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
color: Colors.black,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Padding(
|
||||||
|
padding: EdgeInsets.symmetric(horizontal: 8.w),
|
||||||
|
child: Text(
|
||||||
|
'/',
|
||||||
|
style: TextStyle(color: Colors.grey[300]),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Text(
|
||||||
|
'사람나이 ',
|
||||||
|
style: TextStyle(
|
||||||
|
fontFamily: 'SCDream',
|
||||||
|
fontSize: 14.sp,
|
||||||
|
color: Colors.grey[600],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Text(
|
||||||
|
'사람 나이 환산 ${_calculateHumanAge(pet.birthDate, pet.species)}',
|
||||||
|
style: TextStyle(
|
||||||
|
fontFamily: 'SCDream',
|
||||||
|
fontSize: 12.sp,
|
||||||
|
color: Colors.white,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
SizedBox(height: 6.h),
|
||||||
|
|
||||||
|
// 등록번호
|
||||||
|
Row(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
'등록번호 ',
|
||||||
|
style: TextStyle(
|
||||||
|
fontFamily: 'SCDream',
|
||||||
|
fontSize: 14.sp,
|
||||||
|
color: Colors.grey[600],
|
||||||
|
height: 1.2, // 줄간격 살짝 조정
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Expanded(
|
||||||
|
child: FittedBox(
|
||||||
|
fit: BoxFit.scaleDown,
|
||||||
|
alignment: Alignment.centerLeft,
|
||||||
|
child: Text(
|
||||||
|
pet.registrationNumber?.isNotEmpty == true
|
||||||
|
? pet.registrationNumber!
|
||||||
|
: '--',
|
||||||
|
style: TextStyle(
|
||||||
|
fontFamily: 'SCDream',
|
||||||
|
fontSize: 15.sp,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
color: Colors.black,
|
||||||
|
height: 1.2,
|
||||||
|
),
|
||||||
|
maxLines: 1,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildInfoBox(String label, String value) {
|
||||||
|
return Expanded(
|
||||||
|
child: ClipRRect(
|
||||||
|
borderRadius: BorderRadius.circular(12.r),
|
||||||
|
child: BackdropFilter(
|
||||||
|
filter: ImageFilter.blur(sigmaX: 10, sigmaY: 10),
|
||||||
|
child: Container(
|
||||||
|
padding: EdgeInsets.all(8.w),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: Colors.white.withOpacity(0.4), // 반투명 흰색 배경
|
||||||
|
border: Border.all(color: const Color(0xFFEEEEEE), width: 1),
|
||||||
|
borderRadius: BorderRadius.circular(12.r),
|
||||||
|
),
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
label,
|
||||||
|
style: TextStyle(
|
||||||
|
fontFamily: 'SCDream',
|
||||||
|
fontSize: 12.sp,
|
||||||
|
color: Colors.grey[600], // 가독성을 위해 조금 진하게
|
||||||
|
),
|
||||||
|
),
|
||||||
|
SizedBox(height: 4.h),
|
||||||
|
Text(
|
||||||
|
value,
|
||||||
|
style: TextStyle(
|
||||||
|
fontFamily: 'SCDream',
|
||||||
|
fontSize: 15.sp,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
color: Colors.black,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -520,6 +520,14 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.2.2"
|
version: "0.2.2"
|
||||||
|
intl:
|
||||||
|
dependency: "direct dev"
|
||||||
|
description:
|
||||||
|
name: intl
|
||||||
|
sha256: d6f56758b7d3014a48af9701c085700aac781a92a87a62b1333b46d8879661cf
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "0.19.0"
|
||||||
js:
|
js:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
|||||||
@ -50,6 +50,7 @@ dependencies:
|
|||||||
dev_dependencies:
|
dev_dependencies:
|
||||||
flutter_test:
|
flutter_test:
|
||||||
sdk: flutter
|
sdk: flutter
|
||||||
|
intl: ^0.19.0
|
||||||
|
|
||||||
# The "flutter_lints" package below contains a set of recommended lints to
|
# The "flutter_lints" package below contains a set of recommended lints to
|
||||||
# encourage good coding practices. The lint set provided by the package is
|
# encourage good coding practices. The lint set provided by the package is
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user