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

608 lines
21 KiB
Dart

import 'package:flutter/material.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
import 'package:flutter_svg/flutter_svg.dart';
import 'package:table_calendar/table_calendar.dart';
import 'package:intl/intl.dart';
import '../models/pet_model.dart';
import '../services/firestore_service.dart';
import '../theme/app_colors.dart';
class DailyCareScreen extends StatefulWidget {
const DailyCareScreen({super.key});
@override
State<DailyCareScreen> createState() => _DailyCareScreenState();
}
class _DailyCareScreenState extends State<DailyCareScreen> {
final FirestoreService _firestoreService = FirestoreService();
DateTime _focusedDay = DateTime.now();
DateTime _selectedDay = DateTime.now();
String? _userId;
Pet? _selectedPet;
final DraggableScrollableController _sheetController =
DraggableScrollableController();
@override
void dispose() {
_sheetController.dispose();
super.dispose();
}
DateTime _normalizeDate(DateTime date) {
return DateTime(date.year, date.month, date.day);
}
late final Map<DateTime, List<String>> _events;
@override
void initState() {
super.initState();
_userId = _firestoreService.getCurrentUserId();
final today = _normalizeDate(DateTime.now());
_events = {
today: [
'flower',
'flower',
'flower',
'flower',
'incomplete',
'flower',
'important',
'general',
'general',
], // 오늘: 9개 (3x3 테스트)
today.subtract(const Duration(days: 1)): [
'flower',
'flower',
'flower',
], // 어제: 완료3
today.subtract(const Duration(days: 2)): ['general'], // 2일전: 일반1
today.subtract(const Duration(days: 3)): [
'incomplete',
'flower',
], // 3일전: 실패1, 완료1
today.subtract(const Duration(days: 5)): [
'flower',
'flower',
'flower',
], // 5일전: 완료3
today.subtract(const Duration(days: 7)): [
'incomplete',
'incomplete',
], // 7일전: 실패2
today.subtract(const Duration(days: 10)): [
'important',
'general',
], // 10일전: 중요1, 일반1
};
}
List<String> _getEventsForDay(DateTime day) {
return _events[_normalizeDate(day)] ?? [];
}
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: Colors.white,
body: SafeArea(
child: Stack(
children: [
Column(
children: [
_buildHeader(),
SizedBox(height: 30.h), // Equal spacing 1
_buildCustomCalendarHeader(), // Custom Header
SizedBox(height: 30.h), // Equal spacing 2
Expanded(
child: Padding(
padding: EdgeInsets.symmetric(horizontal: 20.w),
child: _buildCalendar(),
),
),
],
),
_buildBottomSheet(),
],
),
),
floatingActionButton: FloatingActionButton(
heroTag: 'daily_care_fab',
onPressed: () {
// 일정 추가 로직 (추후 구현)
ScaffoldMessenger.of(
context,
).showSnackBar(const SnackBar(content: Text('일정 추가 기능은 준비 중입니다.')));
},
backgroundColor: AppColors.highlight,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(16.r),
),
child: const Icon(Icons.add, color: Colors.white),
),
);
}
Widget _buildHeader() {
return Padding(
padding: EdgeInsets.only(left: 20.w, right: 20.w, top: 10.h, bottom: 0),
child: StreamBuilder<List<Pet>>(
stream: _userId != null
? _firestoreService.getPets(_userId!)
: const Stream.empty(),
builder: (context, snapshot) {
if (!snapshot.hasData || snapshot.data!.isEmpty) {
return const SizedBox.shrink();
}
final pets = snapshot.data!;
if (_selectedPet == null ||
!pets.any((p) => p.id == _selectedPet!.id)) {
_selectedPet = pets.first;
}
return Row(
mainAxisAlignment: MainAxisAlignment.start,
children: [
InkWell(
onTap: () {},
child: Row(
children: [
CircleAvatar(
radius: 20.r,
backgroundColor: Colors.grey[200],
backgroundImage: _selectedPet!.profileImageUrl != null
? NetworkImage(_selectedPet!.profileImageUrl!)
: null,
child: _selectedPet!.profileImageUrl == null
? SvgPicture.asset(
'assets/icons/profile_icon.svg',
width: 20.w,
colorFilter: ColorFilter.mode(
Colors.grey[400]!,
BlendMode.srcIn,
),
)
: null,
),
SizedBox(width: 8.w),
Text(
_selectedPet!.name,
style: TextStyle(
fontFamily: 'SCDream',
fontSize: 18.sp,
fontWeight: FontWeight.bold,
),
),
Icon(Icons.keyboard_arrow_down, size: 24.w),
],
),
),
],
);
},
),
);
}
Widget _buildCustomCalendarHeader() {
return Padding(
padding: EdgeInsets.symmetric(
horizontal: 20.w,
), // Removed vertical padding
child: Stack(
alignment: Alignment.center,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
InkWell(
onTap: () {
setState(() {
_focusedDay = DateTime(
_focusedDay.year,
_focusedDay.month - 1,
);
});
},
child: SvgPicture.asset(
'assets/icons/left_icon.svg',
width: 12.w, // Changed to 12px
height: 12.w,
),
),
SizedBox(width: 30.w),
Text(
DateFormat('yyyy년 M월', 'ko_KR').format(_focusedDay),
style: TextStyle(
fontFamily: 'SCDream',
fontSize: 18.sp,
fontWeight: FontWeight.bold,
),
),
SizedBox(width: 30.w),
InkWell(
onTap: () {
setState(() {
_focusedDay = DateTime(
_focusedDay.year,
_focusedDay.month + 1,
);
});
},
child: SvgPicture.asset(
'assets/icons/right_icon.svg',
width: 12.w, // Changed to 12px
height: 12.w,
),
),
],
),
Positioned(
right: 0,
child: Container(
width: 40.w,
height: 40.w,
decoration: BoxDecoration(
color: Colors.black,
borderRadius: BorderRadius.circular(12.r),
),
child: const Icon(Icons.download, color: Colors.white, size: 20),
),
),
],
),
);
}
Widget _buildCalendar() {
return TableCalendar(
shouldFillViewport: false,
locale: 'ko_KR',
rowHeight: 85
.h, // Increased to fit 3 rows of icons (approx 40px + 32px top offset)
daysOfWeekHeight: 30.h,
firstDay: DateTime.utc(2020, 1, 1),
lastDay: DateTime.utc(2030, 12, 31),
focusedDay: _focusedDay,
selectedDayPredicate: (day) => isSameDay(_selectedDay, day),
headerVisible: false, // Hide default header
onDaySelected: (selectedDay, focusedDay) {
setState(() {
_selectedDay = selectedDay;
_focusedDay = focusedDay;
});
},
onPageChanged: (focusedDay) {
_focusedDay = focusedDay;
},
calendarStyle: CalendarStyle(
defaultTextStyle: TextStyle(
fontFamily: 'SCDream',
fontWeight: FontWeight.w500,
fontSize: 15.sp,
color: const Color(0xFF1f1f1f),
),
weekendTextStyle: TextStyle(
fontFamily: 'SCDream',
fontWeight: FontWeight.w500,
fontSize: 15.sp,
color: const Color(0xFF1f1f1f),
),
todayDecoration: const BoxDecoration(
color: Colors.transparent,
shape: BoxShape.circle,
),
todayTextStyle: const TextStyle(
color: AppColors.highlight,
fontWeight: FontWeight.bold,
),
selectedDecoration: const BoxDecoration(
color: Colors.transparent,
shape: BoxShape.circle,
),
selectedTextStyle: const TextStyle(
color: Colors.black,
fontWeight: FontWeight.bold,
decoration: TextDecoration.underline,
),
outsideDaysVisible: true, // Show outside days
),
eventLoader: _getEventsForDay, // 이벤트 로더
calendarBuilders: CalendarBuilders(
// 마커 커스텀 빌더
markerBuilder: (context, day, events) {
if (events.isEmpty) return null;
// 외부 날짜인지 확인 (투명도 적용을 위해)
// 주의: _focusedDay는 페이지가 바뀔 때만 업데이트되므로,
// 달력의 현재 페이지 월과 day의 월을 비교해야 함.
// 하지만 markerBuilder는 페이징 중에 다시 빌드될 수 있음.
bool isOutside = day.month != _focusedDay.month;
double opacity = isOutside ? 0.5 : 1.0;
// 이벤트가 있으면 아이콘 표시 (최대 9개 등 제한 가능)
return Positioned(
top: 32.h,
left: 0,
right: 0,
child: Opacity(
opacity: opacity,
child: Center(
child: Container(
width:
40.w, // Exact width: 12*3 + 2*2 = 40. Centers perfectly.
child: Wrap(
alignment: WrapAlignment.start, // Left aligned
spacing: 2.w, // Horizontal spacing
runSpacing: 2.h, // Vertical spacing
children: events.take(9).map((event) {
String iconPath =
'assets/icons/general_schedule_icon.svg';
switch (event) {
case 'flower':
iconPath = 'assets/icons/flower_icon.svg';
break;
case 'incomplete':
iconPath = 'assets/icons/incomplete_icon.svg';
break;
case 'important':
iconPath = 'assets/icons/important_schedule_icon.svg';
break;
case 'general':
iconPath = 'assets/icons/general_schedule_icon.svg';
break;
}
return SvgPicture.asset(
iconPath,
width: 12.w,
height: 12.w,
);
}).toList(),
),
),
),
),
);
},
// 요일 헤더 커스텀
dowBuilder: (context, day) {
final text = DateFormat.E('ko_KR').format(day);
// 요일 헤더는 '현재 달' 개념이 모호하므로(항상 보임) 기본 색상 사용
Color color = const Color(0xFF939393);
if (day.weekday == DateTime.sunday)
color = const Color(0xFFFF3F3F); // 일요일 #FF3F3F로 복귀
return Container(
// Removed bottom border to avoid double border with first row's top border
padding: EdgeInsets.only(bottom: 10.h),
alignment: Alignment.bottomCenter,
child: Text(
text,
style: TextStyle(
color: color,
fontFamily: 'SCDream',
fontSize: 12.sp,
fontWeight: FontWeight.w500,
),
),
);
},
// 외부 날짜 (지난달/다음달) 커스텀
outsideBuilder: (context, day, focusedDay) {
// 기본 색상에서 투명도 50% 적용
Color color = const Color(0xFF1F1F1F).withOpacity(0.5); // 평일 50%
if (day.weekday == DateTime.sunday) {
color = const Color(0xFFFF3F3F).withOpacity(0.5); // 일요일 50%
}
return Container(
decoration: const BoxDecoration(
border: Border(
top: BorderSide(
color: Color(0xFFE0E0E0),
width: 1,
), // Changed to top
),
),
padding: EdgeInsets.only(top: 8.h),
alignment: Alignment.topCenter,
child: Text(
day.day.toString().padLeft(2, '0'),
style: TextStyle(
fontFamily: 'SCDream',
fontSize: 15.sp,
fontWeight: FontWeight.w500,
color: color,
),
),
);
},
// 날짜 셀 커스텀
defaultBuilder: (context, day, focusedDay) {
// 현재 달 날짜 색상
Color color = const Color(0xFF1F1F1F);
if (day.weekday == DateTime.sunday) {
color = const Color(0xFFFF3F3F);
}
return Container(
decoration: const BoxDecoration(
border: Border(
top: BorderSide(
color: Color(0xFFE0E0E0),
width: 1,
), // Changed to top
),
),
padding: EdgeInsets.only(top: 8.h), // Move closer to top line
alignment: Alignment.topCenter, // Align to top
child: Text(
day.day.toString().padLeft(2, '0'),
style: TextStyle(
fontFamily: 'SCDream',
fontSize: 15.sp,
fontWeight: FontWeight.w500,
color: color,
),
),
);
},
// 오늘/선택 날짜 커스텀
prioritizedBuilder: (context, day, focusedDay) {
final isToday = isSameDay(day, DateTime.now());
final isSelected = isSameDay(day, _selectedDay);
// 우선순위가 없는 날짜는 null을 반환하여 default/outsideBuilder가 실행되도록 함
if (!isToday && !isSelected) return null;
final isSunday = day.weekday == DateTime.sunday;
// 텍스트 색상 결정
Color textColor = const Color(0xFF1F1F1F); // 기본 검정
if (isToday) {
textColor = const Color(0xFFFF9500); // 오늘: #FF9500 (유지)
} else if (isSunday) {
textColor = const Color(0xFFFF3F3F); // 일요일: #FF3F3F
}
// 배경 색상/데코레이션 결정 (선택된 경우)
BoxDecoration? innerDecoration;
if (isSelected) {
innerDecoration = BoxDecoration(
color: const Color(0xFFFFEDBC), // 선택 배경: #FFEDBC
borderRadius: BorderRadius.circular(6.r), // 모서리 덜 둥글게 (6)
);
}
return Container(
// 바깥 컨테이너: 행 구분선 담당
decoration: const BoxDecoration(
border: Border(
bottom: BorderSide(color: Color(0xFFE0E0E0), width: 1),
),
),
child: Container(
// 내부 컨테이너: 선택 배경 담당 (가로 여백 제거로 넓게)
margin: EdgeInsets.symmetric(vertical: 2.h),
decoration: innerDecoration,
alignment: Alignment.topCenter, // 상단 정렬
padding: EdgeInsets.only(top: 6.h), // 텍스트 위 여백
child: Text(
day.day.toString().padLeft(2, '0'),
style: TextStyle(
fontFamily: 'SCDream',
fontSize: 15.sp,
fontWeight: FontWeight.bold, // 오늘/선택은 강조
decoration: isSelected ? null : null,
color: textColor,
),
),
),
);
},
),
);
}
Widget _buildBottomSheet() {
return DraggableScrollableSheet(
controller: _sheetController, // 컨트롤러 연결
initialChildSize: 0.08,
minChildSize: 0.08,
maxChildSize: 0.8,
builder: (context, scrollController) {
return Container(
margin: EdgeInsets.symmetric(horizontal: 20.w),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.vertical(top: Radius.circular(24.r)),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.05), // Lighter shadow
blurRadius: 5, // Tighter blur
offset: const Offset(0, -3), // Closer offset
),
],
),
child: SingleChildScrollView(
controller: scrollController,
child: Padding(
padding: EdgeInsets.symmetric(horizontal: 20.w, vertical: 10.h),
child: Column(
children: [
// 헤더 (터치 영역)
GestureDetector(
onTap: () {
// 현재 높이에 따라 토글
if (_sheetController.size > 0.4) {
_sheetController.animateTo(
0.08,
duration: const Duration(milliseconds: 300),
curve: Curves.easeInOut,
);
} else {
_sheetController.animateTo(
0.8,
duration: const Duration(milliseconds: 300),
curve: Curves.easeInOut,
);
}
},
child: Container(
color: Colors.transparent, // 터치 영역 확장
width: double.infinity,
child: Column(
children: [
AnimatedBuilder(
animation: _sheetController,
builder: (context, child) {
// 0.08 ~ 0.8 사이 값에서 회전 각도 계산
// 0.4 이상이면 완전히 회전하도록 설정하거나, 비례해서 회전
// 간단하게 0.4 기준으로 상태 판단
final double angle =
_sheetController.isAttached &&
_sheetController.size > 0.4
? 3.14159
: 0.0;
return Transform.rotate(
angle: angle,
child: Icon(
Icons.keyboard_arrow_up_rounded,
color: Colors.grey[400],
size: 24.w,
),
);
},
),
SizedBox(height: 20.h),
],
),
),
),
Align(
alignment: Alignment.centerLeft,
child: Text(
'중요 일정 1',
style: TextStyle(
fontFamily: 'SCDream',
fontSize: 14.sp,
color: Colors.grey[600],
),
),
),
SizedBox(height: 50.h), // 스크롤 테스트용 여백
],
),
),
),
);
},
);
}
}