홈화면 동물 프로필 카드 추가 / 수정 페이지 추가/ 수정, 추가에 따른 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 String? registrationNumber;
|
||||
final String? profileImageUrl;
|
||||
final double? weight; // 체중 (kg)
|
||||
final List<String> diseases;
|
||||
final List<String> pastDiseases;
|
||||
final List<String> healthConcerns;
|
||||
@ -29,6 +30,7 @@ class Pet {
|
||||
required this.isDateUnknown,
|
||||
this.registrationNumber,
|
||||
this.profileImageUrl,
|
||||
this.weight,
|
||||
required this.diseases,
|
||||
required this.pastDiseases,
|
||||
required this.healthConcerns,
|
||||
@ -48,6 +50,7 @@ class Pet {
|
||||
'isDateUnknown': isDateUnknown,
|
||||
'registrationNumber': registrationNumber,
|
||||
'profileImageUrl': profileImageUrl,
|
||||
'weight': weight,
|
||||
'diseases': diseases,
|
||||
'pastDiseases': pastDiseases,
|
||||
'healthConcerns': healthConcerns,
|
||||
@ -70,6 +73,7 @@ class Pet {
|
||||
isDateUnknown: map['isDateUnknown'] ?? false,
|
||||
registrationNumber: map['registrationNumber'],
|
||||
profileImageUrl: map['profileImageUrl'],
|
||||
weight: map['weight']?.toDouble(),
|
||||
diseases: List<String>.from(map['diseases'] ?? []),
|
||||
pastDiseases: List<String>.from(map['pastDiseases'] ?? []),
|
||||
healthConcerns: List<String>.from(map['healthConcerns'] ?? []),
|
||||
|
||||
@ -1,10 +1,11 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_screenutil/flutter_screenutil.dart';
|
||||
import 'package:flutter_svg/flutter_svg.dart';
|
||||
import 'pet_registration_screen.dart';
|
||||
import 'pet_form_screen.dart';
|
||||
import '../services/firestore_service.dart';
|
||||
import '../models/pet_model.dart';
|
||||
import '../theme/app_colors.dart';
|
||||
import '../widgets/home/pet_profile_card.dart';
|
||||
|
||||
class HomeScreen extends StatefulWidget {
|
||||
const HomeScreen({super.key});
|
||||
@ -24,124 +25,6 @@ class _HomeScreenState extends State<HomeScreen> {
|
||||
_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
|
||||
Widget build(BuildContext context) {
|
||||
if (_userId == null) {
|
||||
@ -182,8 +65,7 @@ class _HomeScreenState extends State<HomeScreen> {
|
||||
Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (context) =>
|
||||
const PetRegistrationScreen(),
|
||||
builder: (context) => const PetFormScreen(),
|
||||
),
|
||||
);
|
||||
},
|
||||
@ -233,19 +115,16 @@ class _HomeScreenState extends State<HomeScreen> {
|
||||
|
||||
// 등록된 반려동물이 있을 때
|
||||
// 선택된 펫이 없거나 리스트에 없으면 첫 번째 펫 선택
|
||||
if (_selectedPet == null ||
|
||||
!pets.any((p) => p.id == _selectedPet!.id)) {
|
||||
// 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;
|
||||
}
|
||||
// 등록된 반려동물이 있을 때
|
||||
Pet displayPet;
|
||||
|
||||
// To ensure _selectedPet is valid (e.g. after deletion), find it in the new list
|
||||
final displayPet = pets.firstWhere(
|
||||
(p) => p.id == _selectedPet?.id,
|
||||
orElse: () => pets.first,
|
||||
);
|
||||
// 선택된 펫이 없거나 리스트에 없으면 첫 번째 펫 선택 (State 변경 없이 화면 표시만 처리)
|
||||
if (_selectedPet != null &&
|
||||
pets.any((p) => p.id == _selectedPet!.id)) {
|
||||
displayPet = pets.firstWhere((p) => p.id == _selectedPet!.id);
|
||||
} else {
|
||||
displayPet = pets.first;
|
||||
}
|
||||
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
@ -255,8 +134,109 @@ class _HomeScreenState extends State<HomeScreen> {
|
||||
horizontal: 20.w,
|
||||
vertical: 20.h,
|
||||
),
|
||||
child: GestureDetector(
|
||||
onTap: () => _showPetSelectionModal(pets),
|
||||
child: PopupMenuButton<dynamic>(
|
||||
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(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
@ -301,15 +281,8 @@ class _HomeScreenState extends State<HomeScreen> {
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
child: Center(
|
||||
child: Text(
|
||||
'안녕, ${displayPet.name}!',
|
||||
style: TextStyle(
|
||||
fontFamily: 'SCDream',
|
||||
fontSize: 24.sp,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
child: SingleChildScrollView(
|
||||
child: Column(children: [PetProfileCard(pet: displayPet)]),
|
||||
),
|
||||
),
|
||||
],
|
||||
|
||||
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);
|
||||
_monthController.addListener(_updateState);
|
||||
_dayController.addListener(_updateState);
|
||||
_weightController.addListener(_updateState);
|
||||
}
|
||||
|
||||
void _updateState() {
|
||||
@ -79,6 +80,8 @@ class _PetRegistrationScreenState extends State<PetRegistrationScreen> {
|
||||
TextEditingController(); // 품종 컨트롤러
|
||||
final TextEditingController _genderController =
|
||||
TextEditingController(); // 성별 컨트롤러
|
||||
final TextEditingController _weightController =
|
||||
TextEditingController(); // 체중 컨트롤러
|
||||
|
||||
// 종 데이터 (Removed: Use PetData.breedsData)
|
||||
|
||||
@ -217,6 +220,7 @@ class _PetRegistrationScreenState extends State<PetRegistrationScreen> {
|
||||
_monthFocus.dispose();
|
||||
_dayFocus.dispose();
|
||||
_registrationNumberController.dispose();
|
||||
_weightController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@ -281,6 +285,12 @@ class _PetRegistrationScreenState extends State<PetRegistrationScreen> {
|
||||
registrationNumber: _registrationNumberController.text.isNotEmpty
|
||||
? _registrationNumberController.text
|
||||
: null,
|
||||
weight:
|
||||
_weightController
|
||||
.text
|
||||
.isNotEmpty // 체중 추가
|
||||
? double.tryParse(_weightController.text)
|
||||
: null,
|
||||
diseases: finalDiseases,
|
||||
pastDiseases: finalPastDiseases,
|
||||
healthConcerns: finalHealthConcerns,
|
||||
@ -1616,6 +1626,21 @@ class _PetRegistrationScreenState extends State<PetRegistrationScreen> {
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
|
||||
// 체중 입력
|
||||
_buildLabel('몸무게 (kg)', isRequired: false),
|
||||
_buildTextField(
|
||||
controller: _weightController,
|
||||
hint: '예: 4.5',
|
||||
suffix: 'kg',
|
||||
keyboardType: const TextInputType.numberWithOptions(
|
||||
decimal: true,
|
||||
),
|
||||
inputFormatters: [
|
||||
FilteringTextInputFormatter.allow(RegExp(r'^\d*\.?\d*')),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
|
||||
// 6. 질환 정보 (검색 아이콘 포함)
|
||||
_buildSearchField(
|
||||
'보유 질환',
|
||||
@ -1784,6 +1809,7 @@ class _PetRegistrationScreenState extends State<PetRegistrationScreen> {
|
||||
List<TextInputFormatter>? inputFormatters,
|
||||
FocusNode? focusNode,
|
||||
ValueChanged<String>? onChanged,
|
||||
String? suffix,
|
||||
}) {
|
||||
return TextField(
|
||||
controller: controller,
|
||||
@ -1805,6 +1831,13 @@ class _PetRegistrationScreenState extends State<PetRegistrationScreen> {
|
||||
fontSize: 14.sp,
|
||||
color: hintColor ?? Colors.grey,
|
||||
),
|
||||
suffixText: suffix,
|
||||
suffixStyle: TextStyle(
|
||||
fontFamily: 'SCDream',
|
||||
fontSize: 14.sp,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Colors.black,
|
||||
),
|
||||
enabledBorder: const UnderlineInputBorder(
|
||||
borderSide: BorderSide(color: Color(0xFFDDDDDD)),
|
||||
),
|
||||
|
||||
@ -58,6 +58,7 @@ class FirestoreService {
|
||||
isDateUnknown: pet.isDateUnknown,
|
||||
registrationNumber: pet.registrationNumber,
|
||||
profileImageUrl: imageUrl, // 이미지 URL 설정
|
||||
weight: pet.weight, // 체중 추가
|
||||
diseases: pet.diseases,
|
||||
pastDiseases: pet.pastDiseases,
|
||||
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 가져오기
|
||||
String? getCurrentUserId() {
|
||||
return _auth.currentUser?.uid;
|
||||
|
||||
@ -1,4 +1,6 @@
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/widgets.dart'; // For WidgetsBinding
|
||||
import 'package:flutter/scheduler.dart'; // For SchedulerPhase
|
||||
|
||||
class LogManager {
|
||||
static final LogManager _instance = LogManager._internal();
|
||||
@ -12,7 +14,15 @@ class LogManager {
|
||||
final timestamp = DateTime.now().toString().split(' ')[1].split('.')[0];
|
||||
final logMessage = "[$timestamp] $message";
|
||||
|
||||
// 빌드 중에 호출될 경우를 대비해 스케줄링
|
||||
if (WidgetsBinding.instance.schedulerPhase ==
|
||||
SchedulerPhase.persistentCallbacks) {
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
logs.value = [logMessage, ...logs.value];
|
||||
});
|
||||
} else {
|
||||
logs.value = [logMessage, ...logs.value];
|
||||
}
|
||||
} catch (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"
|
||||
source: hosted
|
||||
version: "0.2.2"
|
||||
intl:
|
||||
dependency: "direct dev"
|
||||
description:
|
||||
name: intl
|
||||
sha256: d6f56758b7d3014a48af9701c085700aac781a92a87a62b1333b46d8879661cf
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.19.0"
|
||||
js:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
||||
@ -50,6 +50,7 @@ dependencies:
|
||||
dev_dependencies:
|
||||
flutter_test:
|
||||
sdk: flutter
|
||||
intl: ^0.19.0
|
||||
|
||||
# The "flutter_lints" package below contains a set of recommended lints to
|
||||
# encourage good coding practices. The lint set provided by the package is
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user