608 lines
21 KiB
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), // 스크롤 테스트용 여백
|
|
],
|
|
),
|
|
),
|
|
),
|
|
);
|
|
},
|
|
);
|
|
}
|
|
}
|