feat(schedule): 달력 UI 개선 및 검색 준비

- ExpandablePageView로 달력 높이 동적 조절 (월별 주 수에 따라)
- 데이트픽커 년도 변경 시 스와이프 애니메이션 추가
- 달력 월 변경 시 일정 점 비동기 업데이트 (캐시 기반)
- 모든 달력/데이트픽커 텍스트에 Pretendard 폰트 적용
- 데이트픽커 화살표 터치 영역 확대 및 ripple effect 추가
- 데이트픽커 펼침 시 툴바 좌우 화살표 숨김 (페이드 애니메이션)
- expandable_page_view 패키지 추가

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
caadiq 2026-01-13 20:37:00 +09:00
parent 895d9c26a3
commit c4cbdc7d33
4 changed files with 557 additions and 298 deletions

View file

@ -15,12 +15,15 @@ class ScheduleState {
final List<Schedule> schedules;
final bool isLoading;
final String? error;
// (key: "yyyy-MM")
final Map<String, List<Schedule>> calendarCache;
const ScheduleState({
required this.selectedDate,
this.schedules = const [],
this.isLoading = false,
this.error,
this.calendarCache = const {},
});
/// ( )
@ -29,12 +32,14 @@ class ScheduleState {
List<Schedule>? schedules,
bool? isLoading,
String? error,
Map<String, List<Schedule>>? calendarCache,
}) {
return ScheduleState(
selectedDate: selectedDate ?? this.selectedDate,
schedules: schedules ?? this.schedules,
isLoading: isLoading ?? this.isLoading,
error: error,
calendarCache: calendarCache ?? this.calendarCache,
);
}
@ -45,11 +50,29 @@ class ScheduleState {
}
/// ( , 3)
/// , schedules에서
List<Schedule> getDaySchedules(DateTime date) {
final dateStr = DateFormat('yyyy-MM-dd').format(date);
final cacheKey = DateFormat('yyyy-MM').format(date);
//
if (calendarCache.containsKey(cacheKey)) {
return calendarCache[cacheKey]!
.where((s) => s.date.split('T')[0] == dateStr)
.take(3)
.toList();
}
// schedules에서
return schedules.where((s) => s.date.split('T')[0] == dateStr).take(3).toList();
}
///
bool hasMonthCache(int year, int month) {
final cacheKey = '$year-${month.toString().padLeft(2, '0')}';
return calendarCache.containsKey(cacheKey);
}
///
List<DateTime> get daysInMonth {
final year = selectedDate.year;
@ -79,12 +102,38 @@ class ScheduleController extends Notifier<ScheduleState> {
state.selectedDate.year,
state.selectedDate.month,
);
state = state.copyWith(schedules: schedules, isLoading: false);
//
final cacheKey = '${state.selectedDate.year}-${state.selectedDate.month.toString().padLeft(2, '0')}';
final newCache = Map<String, List<Schedule>>.from(state.calendarCache);
newCache[cacheKey] = schedules;
state = state.copyWith(
schedules: schedules,
isLoading: false,
calendarCache: newCache,
);
} catch (e) {
state = state.copyWith(isLoading: false, error: e.toString());
}
}
/// (UI )
Future<void> loadCalendarMonth(int year, int month) async {
final cacheKey = '$year-${month.toString().padLeft(2, '0')}';
//
if (state.calendarCache.containsKey(cacheKey)) return;
try {
final schedules = await getSchedules(year, month);
//
final newCache = Map<String, List<Schedule>>.from(state.calendarCache);
newCache[cacheKey] = schedules;
state = state.copyWith(calendarCache: newCache);
} catch (e) {
// ( )
}
}
///
void selectDate(DateTime date) {
state = state.copyWith(selectedDate: date);

View file

@ -5,6 +5,7 @@ library;
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:expandable_page_view/expandable_page_view.dart';
import '../../core/constants.dart';
import '../../models/schedule.dart';
import '../../controllers/schedule_controller.dart';
@ -27,7 +28,8 @@ class ScheduleView extends ConsumerStatefulWidget {
ConsumerState<ScheduleView> createState() => _ScheduleViewState();
}
class _ScheduleViewState extends ConsumerState<ScheduleView> {
class _ScheduleViewState extends ConsumerState<ScheduleView>
with SingleTickerProviderStateMixin {
final ScrollController _dateScrollController = ScrollController();
final ScrollController _contentScrollController = ScrollController();
DateTime? _lastSelectedDate;
@ -38,13 +40,81 @@ class _ScheduleViewState extends ConsumerState<ScheduleView> {
bool _showYearMonthPicker = false;
int _yearRangeStart = (DateTime.now().year ~/ 12) * 12;
// PageView
late PageController _calendarPageController;
late PageController _yearPageController;
static const int _initialPage = 1000;
//
late AnimationController _calendarAnimController;
late Animation<double> _calendarAnimation;
@override
void initState() {
super.initState();
_calendarPageController = PageController(initialPage: _initialPage);
_yearPageController = PageController(initialPage: _initialPage);
_calendarAnimController = AnimationController(
duration: const Duration(milliseconds: 200),
vsync: this,
);
_calendarAnimation = CurvedAnimation(
parent: _calendarAnimController,
curve: Curves.easeOut,
reverseCurve: Curves.easeIn,
);
}
@override
void dispose() {
_dateScrollController.dispose();
_contentScrollController.dispose();
_calendarPageController.dispose();
_yearPageController.dispose();
_calendarAnimController.dispose();
super.dispose();
}
///
void _openCalendar(DateTime initialDate) {
final today = DateTime.now();
final monthDelta = (initialDate.year - today.year) * 12 + (initialDate.month - today.month);
_yearRangeStart = (initialDate.year ~/ 12) * 12;
// PageView
final baseYearRange = (today.year ~/ 12) * 12;
final yearPageDelta = (_yearRangeStart - baseYearRange) ~/ 12;
setState(() {
_calendarViewDate = initialDate;
_showCalendar = true;
});
// PageView
WidgetsBinding.instance.addPostFrameCallback((_) {
if (mounted && _calendarPageController.hasClients) {
_calendarPageController.jumpToPage(_initialPage + monthDelta);
}
if (mounted && _yearPageController.hasClients) {
_yearPageController.jumpToPage(_initialPage + yearPageDelta);
}
});
_calendarAnimController.forward();
}
///
void _closeCalendar() {
_calendarAnimController.reverse().then((_) {
if (mounted) {
setState(() {
_showCalendar = false;
_showYearMonthPicker = false;
});
}
});
}
///
void _scrollToSelectedDate(DateTime selectedDate) {
if (!_dateScrollController.hasClients) return;
@ -103,6 +173,10 @@ class _ScheduleViewState extends ConsumerState<ScheduleView> {
});
}
// +
final safeTop = MediaQuery.of(context).padding.top;
final overlayTop = safeTop + 56 + 80; // toolbar(56) + date selector(80)
return Stack(
children: [
//
@ -118,29 +192,29 @@ class _ScheduleViewState extends ConsumerState<ScheduleView> {
),
],
),
// ( )
//
if (_showCalendar) ...[
// ( )
// ( )
Positioned(
top: MediaQuery.of(context).padding.top + 56,
top: overlayTop,
left: 0,
right: 0,
bottom: 0,
child: GestureDetector(
onTap: () {
setState(() {
_showCalendar = false;
_showYearMonthPicker = false;
});
},
child: AnimatedBuilder(
animation: _calendarAnimation,
builder: (context, child) {
return GestureDetector(
onTap: _closeCalendar,
child: Container(
color: Colors.black.withValues(alpha: 0.4),
color: Colors.black.withValues(alpha: 0.4 * _calendarAnimation.value),
),
);
},
),
),
// ()
Positioned(
top: MediaQuery.of(context).padding.top + 56,
top: safeTop + 56,
left: 0,
right: 0,
child: _buildCalendarPopup(scheduleState, controller),
@ -154,6 +228,7 @@ class _ScheduleViewState extends ConsumerState<ScheduleView> {
Widget _buildToolbar(ScheduleState state, ScheduleController controller) {
// ,
final displayDate = _showCalendar ? _calendarViewDate : state.selectedDate;
final yearMonthText = '${displayDate.year}${displayDate.month}';
return Container(
color: Colors.white,
@ -167,44 +242,40 @@ class _ScheduleViewState extends ConsumerState<ScheduleView> {
//
IconButton(
onPressed: () {
setState(() {
if (_showCalendar) {
_showCalendar = false;
_showYearMonthPicker = false;
_closeCalendar();
} else {
_calendarViewDate = state.selectedDate;
_showCalendar = true;
_openCalendar(state.selectedDate);
}
});
},
icon: const Icon(Icons.calendar_today_outlined, size: 20),
color: _showCalendar ? AppColors.primary : AppColors.textSecondary,
),
//
IconButton(
// ( , )
AnimatedSwitcher(
duration: const Duration(milliseconds: 200),
child: !_showYearMonthPicker
? IconButton(
key: const ValueKey('prev_button'),
onPressed: () {
if (_showCalendar) {
setState(() {
_calendarViewDate = DateTime(
_calendarViewDate.year,
_calendarViewDate.month - 1,
1,
_calendarPageController.previousPage(
duration: const Duration(milliseconds: 300),
curve: Curves.easeInOut,
);
});
} else {
controller.changeMonth(-1);
}
},
icon: const Icon(Icons.chevron_left, size: 24),
color: AppColors.textPrimary,
)
: const SizedBox(key: ValueKey('prev_empty'), width: 48),
),
//
Expanded(
child: Stack(
alignment: Alignment.center,
children: [
// ( )
GestureDetector(
child: GestureDetector(
behavior: HitTestBehavior.opaque,
onTap: _showCalendar
? () {
setState(() {
@ -213,9 +284,22 @@ class _ScheduleViewState extends ConsumerState<ScheduleView> {
});
}
: null,
child: Text(
'${displayDate.year}${displayDate.month}',
child: Center(
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
// ( , )
AnimatedSwitcher(
duration: const Duration(milliseconds: 200),
child: _showCalendar
? const SizedBox(key: ValueKey('arrow_space'), width: 20)
: const SizedBox(key: ValueKey('no_space'), width: 0),
),
// ( )
Text(
yearMonthText,
style: TextStyle(
fontFamily: 'Pretendard',
fontSize: 16,
fontWeight: FontWeight.bold,
color: _showYearMonthPicker
@ -223,20 +307,16 @@ class _ScheduleViewState extends ConsumerState<ScheduleView> {
: AppColors.textPrimary,
),
),
),
// ( )
if (_showCalendar)
Positioned(
// +
left: MediaQuery.of(context).size.width / 2 - 48 + 52,
child: GestureDetector(
onTap: () {
setState(() {
_showYearMonthPicker = !_showYearMonthPicker;
_yearRangeStart = (_calendarViewDate.year ~/ 12) * 12;
});
},
child: Icon(
// ( )
AnimatedSwitcher(
duration: const Duration(milliseconds: 200),
child: _showCalendar
? Row(
key: ValueKey('dropdown_$_showYearMonthPicker'),
mainAxisSize: MainAxisSize.min,
children: [
const SizedBox(width: 2),
Icon(
_showYearMonthPicker
? Icons.keyboard_arrow_up
: Icons.keyboard_arrow_down,
@ -245,28 +325,35 @@ class _ScheduleViewState extends ConsumerState<ScheduleView> {
? AppColors.primary
: AppColors.textPrimary,
),
),
],
)
: const SizedBox(key: ValueKey('no_dropdown'), width: 0),
),
],
),
),
//
IconButton(
),
),
// ( , )
AnimatedSwitcher(
duration: const Duration(milliseconds: 200),
child: !_showYearMonthPicker
? IconButton(
key: const ValueKey('next_button'),
onPressed: () {
if (_showCalendar) {
setState(() {
_calendarViewDate = DateTime(
_calendarViewDate.year,
_calendarViewDate.month + 1,
1,
_calendarPageController.nextPage(
duration: const Duration(milliseconds: 300),
curve: Curves.easeInOut,
);
});
} else {
controller.changeMonth(1);
}
},
icon: const Icon(Icons.chevron_right, size: 24),
color: AppColors.textPrimary,
)
: const SizedBox(key: ValueKey('next_empty'), width: 48),
),
// (3 )
IconButton(
@ -285,17 +372,15 @@ class _ScheduleViewState extends ConsumerState<ScheduleView> {
///
Widget _buildCalendarPopup(ScheduleState state, ScheduleController controller) {
return TweenAnimationBuilder<double>(
duration: const Duration(milliseconds: 200),
curve: Curves.easeOut,
tween: Tween(begin: 0.0, end: 1.0),
builder: (context, value, child) {
return AnimatedBuilder(
animation: _calendarAnimation,
builder: (context, child) {
return ClipRect(
child: Align(
alignment: Alignment.topCenter,
heightFactor: value,
heightFactor: _calendarAnimation.value,
child: Opacity(
opacity: value,
opacity: _calendarAnimation.value,
child: child,
),
),
@ -303,14 +388,52 @@ class _ScheduleViewState extends ConsumerState<ScheduleView> {
},
child: Material(
color: Colors.white,
elevation: 8,
child: AnimatedCrossFade(
elevation: 0,
child: AnimatedSize(
duration: const Duration(milliseconds: 200),
curve: Curves.easeOut,
alignment: Alignment.topCenter,
child: AnimatedSwitcher(
duration: const Duration(milliseconds: 150),
crossFadeState: _showYearMonthPicker
? CrossFadeState.showSecond
: CrossFadeState.showFirst,
firstChild: _buildCalendarGrid(state, controller),
secondChild: _buildYearMonthPicker(),
switchInCurve: Curves.easeOut,
switchOutCurve: Curves.easeIn,
layoutBuilder: (currentChild, previousChildren) {
//
return Stack(
alignment: Alignment.topCenter,
children: [
...previousChildren,
if (currentChild != null) currentChild,
],
);
},
transitionBuilder: (child, animation) {
//
final isYearMonthPicker = child.key == const ValueKey('yearMonth');
if (isYearMonthPicker) {
return FadeTransition(
opacity: animation,
child: SlideTransition(
position: Tween<Offset>(
begin: const Offset(0, -0.05),
end: Offset.zero,
).animate(animation),
child: child,
),
);
}
return FadeTransition(opacity: animation, child: child);
},
child: _showYearMonthPicker
? KeyedSubtree(
key: const ValueKey('yearMonth'),
child: _buildYearMonthPicker(),
)
: KeyedSubtree(
key: const ValueKey('calendar'),
child: _buildCalendarGrid(state, controller),
),
),
),
),
);
@ -318,11 +441,12 @@ class _ScheduleViewState extends ConsumerState<ScheduleView> {
///
Widget _buildYearMonthPicker() {
final yearRange = List.generate(12, (i) => _yearRangeStart + i);
final today = DateTime.now();
return Padding(
padding: const EdgeInsets.all(16),
padding: const EdgeInsets.fromLTRB(16, 4, 16, 16),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
//
Row(
@ -330,26 +454,37 @@ class _ScheduleViewState extends ConsumerState<ScheduleView> {
children: [
IconButton(
onPressed: () {
setState(() {
_yearRangeStart -= 12;
});
_yearPageController.previousPage(
duration: const Duration(milliseconds: 300),
curve: Curves.easeInOut,
);
},
icon: const Icon(Icons.chevron_left, size: 18),
icon: const Icon(Icons.chevron_left, size: 20),
color: AppColors.textPrimary,
splashRadius: 20,
padding: EdgeInsets.zero,
constraints: const BoxConstraints(minWidth: 40, minHeight: 40),
),
Text(
'$_yearRangeStart - ${_yearRangeStart + 11}',
style: const TextStyle(
fontFamily: 'Pretendard',
fontSize: 14,
fontWeight: FontWeight.w600,
),
),
IconButton(
onPressed: () {
setState(() {
_yearRangeStart += 12;
});
_yearPageController.nextPage(
duration: const Duration(milliseconds: 300),
curve: Curves.easeInOut,
);
},
icon: const Icon(Icons.chevron_right, size: 18),
icon: const Icon(Icons.chevron_right, size: 20),
color: AppColors.textPrimary,
splashRadius: 20,
padding: EdgeInsets.zero,
constraints: const BoxConstraints(minWidth: 40, minHeight: 40),
),
],
),
@ -357,68 +492,51 @@ class _ScheduleViewState extends ConsumerState<ScheduleView> {
//
const Text(
'년도',
style: TextStyle(fontSize: 12, color: AppColors.textTertiary),
style: TextStyle(fontFamily: 'Pretendard', fontSize: 12, color: AppColors.textTertiary),
),
const SizedBox(height: 8),
//
GridView.builder(
shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(),
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 4,
mainAxisSpacing: 8,
crossAxisSpacing: 8,
childAspectRatio: 2,
),
itemCount: 12,
itemBuilder: (context, index) {
final year = yearRange[index];
final isSelected = year == _calendarViewDate.year;
return GestureDetector(
onTap: () {
const SizedBox(height: 4),
// (ExpandablePageView로 )
ExpandablePageView.builder(
controller: _yearPageController,
itemCount: _initialPage * 2,
onPageChanged: (page) {
final delta = page - _initialPage;
final baseYearRange = (today.year ~/ 12) * 12;
setState(() {
_calendarViewDate = DateTime(year, _calendarViewDate.month, 1);
_yearRangeStart = baseYearRange + (delta * 12);
});
},
child: Container(
decoration: BoxDecoration(
color: isSelected ? AppColors.primary : Colors.transparent,
borderRadius: BorderRadius.circular(8),
),
alignment: Alignment.center,
child: Text(
'$year',
style: TextStyle(
fontSize: 14,
fontWeight: isSelected ? FontWeight.w600 : FontWeight.w400,
color: isSelected ? Colors.white : AppColors.textPrimary,
),
),
),
);
itemBuilder: (context, page) {
final delta = page - _initialPage;
final baseYearRange = (today.year ~/ 12) * 12;
final rangeStart = baseYearRange + (delta * 12);
final yearRange = List.generate(12, (i) => rangeStart + i);
return _buildYearGrid(yearRange: yearRange, today: today);
},
),
const SizedBox(height: 16),
//
const Text(
'',
style: TextStyle(fontSize: 12, color: AppColors.textTertiary),
style: TextStyle(fontFamily: 'Pretendard', fontSize: 12, color: AppColors.textTertiary),
),
const SizedBox(height: 8),
const SizedBox(height: 4),
//
GridView.builder(
shrinkWrap: true,
padding: EdgeInsets.zero,
physics: const NeverScrollableScrollPhysics(),
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 4,
mainAxisSpacing: 8,
mainAxisSpacing: 6,
crossAxisSpacing: 8,
childAspectRatio: 2,
childAspectRatio: 2.0,
),
itemCount: 12,
itemBuilder: (context, index) {
final month = index + 1;
final isSelected = month == _calendarViewDate.month;
final isCurrentMonth = month == today.month && _calendarViewDate.year == today.year;
return GestureDetector(
onTap: () {
setState(() {
@ -426,19 +544,27 @@ class _ScheduleViewState extends ConsumerState<ScheduleView> {
_showYearMonthPicker = false;
});
},
child: Container(
child: AnimatedContainer(
duration: const Duration(milliseconds: 150),
curve: Curves.easeOut,
decoration: BoxDecoration(
color: isSelected ? AppColors.primary : Colors.transparent,
borderRadius: BorderRadius.circular(8),
),
alignment: Alignment.center,
child: Text(
'$month월',
child: AnimatedDefaultTextStyle(
duration: const Duration(milliseconds: 150),
style: TextStyle(
fontFamily: 'Pretendard',
fontSize: 14,
fontWeight: isSelected ? FontWeight.w600 : FontWeight.w400,
color: isSelected ? Colors.white : AppColors.textPrimary,
fontWeight: isSelected || isCurrentMonth ? FontWeight.w600 : FontWeight.w400,
color: isSelected
? Colors.white
: isCurrentMonth
? AppColors.primary
: AppColors.textPrimary,
),
child: Text('$month월'),
),
),
);
@ -449,31 +575,24 @@ class _ScheduleViewState extends ConsumerState<ScheduleView> {
);
}
///
Widget _buildCalendarGrid(ScheduleState state, ScheduleController controller) {
final year = _calendarViewDate.year;
final month = _calendarViewDate.month;
//
///
List<DateTime> _getMonthDays(int year, int month) {
final firstDay = DateTime(year, month, 1);
final lastDay = DateTime(year, month + 1, 0);
final startWeekday = firstDay.weekday % 7; // 0 =
final startWeekday = firstDay.weekday % 7;
final daysInMonth = lastDay.day;
//
final prevMonth = DateTime(year, month, 0);
final prevMonthDays = List.generate(
startWeekday,
(i) => DateTime(year, month - 1, prevMonth.day - startWeekday + 1 + i),
);
//
final currentMonthDays = List.generate(
daysInMonth,
(i) => DateTime(year, month, i + 1),
);
// ( )
final totalDays = prevMonthDays.length + currentMonthDays.length;
final remaining = (7 - (totalDays % 7)) % 7;
final nextMonthDays = List.generate(
@ -481,11 +600,69 @@ class _ScheduleViewState extends ConsumerState<ScheduleView> {
(i) => DateTime(year, month + 1, i + 1),
);
final allDays = [...prevMonthDays, ...currentMonthDays, ...nextMonthDays];
return [...prevMonthDays, ...currentMonthDays, ...nextMonthDays];
}
///
Widget _buildYearGrid({
required List<int> yearRange,
required DateTime today,
}) {
return GridView.builder(
shrinkWrap: true,
padding: EdgeInsets.zero,
physics: const NeverScrollableScrollPhysics(),
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 4,
mainAxisSpacing: 6,
crossAxisSpacing: 8,
childAspectRatio: 2.0,
),
itemCount: 12,
itemBuilder: (context, index) {
final year = yearRange[index];
final isSelected = year == _calendarViewDate.year;
final isCurrentYear = year == today.year;
return GestureDetector(
onTap: () {
setState(() {
_calendarViewDate = DateTime(year, _calendarViewDate.month, 1);
});
},
child: AnimatedContainer(
duration: const Duration(milliseconds: 150),
curve: Curves.easeOut,
decoration: BoxDecoration(
color: isSelected ? AppColors.primary : Colors.transparent,
borderRadius: BorderRadius.circular(8),
),
alignment: Alignment.center,
child: AnimatedDefaultTextStyle(
duration: const Duration(milliseconds: 150),
style: TextStyle(
fontFamily: 'Pretendard',
fontSize: 14,
fontWeight: isSelected || isCurrentYear ? FontWeight.w600 : FontWeight.w400,
color: isSelected
? Colors.white
: isCurrentYear
? AppColors.primary
: AppColors.textPrimary,
),
child: Text('$year'),
),
),
);
},
);
}
///
Widget _buildCalendarGrid(ScheduleState state, ScheduleController controller) {
return Padding(
padding: const EdgeInsets.all(16),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
//
Row(
@ -497,6 +674,7 @@ class _ScheduleViewState extends ConsumerState<ScheduleView> {
child: Text(
day,
style: TextStyle(
fontFamily: 'Pretendard',
fontSize: 12,
fontWeight: FontWeight.w500,
color: index == 0
@ -510,16 +688,92 @@ class _ScheduleViewState extends ConsumerState<ScheduleView> {
);
}).toList(),
),
const SizedBox(height: 8),
//
GridView.builder(
const SizedBox(height: 4),
// (ExpandablePageView로 )
ExpandablePageView.builder(
controller: _calendarPageController,
itemCount: _initialPage * 2, //
onPageChanged: (page) {
final delta = page - _initialPage;
final newDate = DateTime(
DateTime.now().year,
DateTime.now().month + delta,
1,
);
setState(() {
_calendarViewDate = newDate;
});
// ( )
controller.loadCalendarMonth(newDate.year, newDate.month);
},
itemBuilder: (context, page) {
final delta = page - _initialPage;
final targetDate = DateTime(
DateTime.now().year,
DateTime.now().month + delta,
1,
);
//
if (!state.hasMonthCache(targetDate.year, targetDate.month)) {
controller.loadCalendarMonth(targetDate.year, targetDate.month);
}
final allDays = _getMonthDays(targetDate.year, targetDate.month);
return _buildCalendarDaysGrid(
allDays: allDays,
month: targetDate.month,
state: state,
controller: controller,
);
},
),
const SizedBox(height: 12),
//
GestureDetector(
onTap: () {
final today = DateTime.now();
controller.goToDate(today);
if (_contentScrollController.hasClients) {
_contentScrollController.jumpTo(0);
}
_calendarPageController.jumpToPage(_initialPage);
_closeCalendar();
},
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 6),
decoration: BoxDecoration(
color: AppColors.primary.withValues(alpha: 0.1),
borderRadius: BorderRadius.circular(20),
),
child: const Text(
'오늘',
style: TextStyle(
fontSize: 12,
fontWeight: FontWeight.w600,
color: AppColors.primary,
),
),
),
),
],
),
);
}
///
Widget _buildCalendarDaysGrid({
required List<DateTime> allDays,
required int month,
required ScheduleState state,
required ScheduleController controller,
}) {
return GridView.builder(
shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(),
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 7,
mainAxisSpacing: 4,
crossAxisSpacing: 4,
childAspectRatio: 1,
mainAxisExtent: 46, // Container(36) + SizedBox(6) + (4)
),
itemCount: allDays.length,
itemBuilder: (context, index) {
@ -532,17 +786,11 @@ class _ScheduleViewState extends ConsumerState<ScheduleView> {
return GestureDetector(
onTap: () {
//
controller.goToDate(date);
//
if (_contentScrollController.hasClients) {
_contentScrollController.jumpTo(0);
}
//
setState(() {
_showCalendar = false;
_showYearMonthPicker = false;
});
_closeCalendar();
},
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
@ -567,10 +815,9 @@ class _ScheduleViewState extends ConsumerState<ScheduleView> {
child: Text(
'${date.day}',
style: TextStyle(
fontFamily: 'Pretendard',
fontSize: 14,
fontWeight: isSelected || isToday
? FontWeight.bold
: FontWeight.w400,
fontWeight: isSelected || isToday ? FontWeight.bold : FontWeight.w400,
color: !isCurrentMonth
? AppColors.textTertiary.withValues(alpha: 0.5)
: isSelected
@ -585,7 +832,6 @@ class _ScheduleViewState extends ConsumerState<ScheduleView> {
),
),
),
// ( )
SizedBox(
height: 6,
child: !isSelected && daySchedules.isNotEmpty
@ -609,58 +855,14 @@ class _ScheduleViewState extends ConsumerState<ScheduleView> {
),
);
},
),
const SizedBox(height: 12),
//
GestureDetector(
onTap: () {
final today = DateTime.now();
controller.goToDate(today);
if (_contentScrollController.hasClients) {
_contentScrollController.jumpTo(0);
}
setState(() {
_showCalendar = false;
_showYearMonthPicker = false;
});
},
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 6),
decoration: BoxDecoration(
color: AppColors.primary.withValues(alpha: 0.1),
borderRadius: BorderRadius.circular(20),
),
child: const Text(
'오늘',
style: TextStyle(
fontSize: 12,
fontWeight: FontWeight.w600,
color: AppColors.primary,
),
),
),
),
],
),
);
}
///
Widget _buildDateSelector(ScheduleState state, ScheduleController controller) {
return Container(
color: Colors.white,
child: Container(
height: 80,
decoration: BoxDecoration(
color: Colors.white,
boxShadow: [
BoxShadow(
color: Colors.black.withValues(alpha: 0.05),
blurRadius: 4,
offset: const Offset(0, 1),
),
],
),
child: ListView.builder(
controller: _dateScrollController,
scrollDirection: Axis.horizontal,
@ -742,7 +944,6 @@ class _ScheduleViewState extends ConsumerState<ScheduleView> {
);
},
),
),
);
}

View file

@ -169,6 +169,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "2.1.1"
expandable_page_view:
dependency: "direct main"
description:
name: expandable_page_view
sha256: "2d2c9e6fbbaa153f761054200c199eb69dc45948c8018b98a871212c67b60608"
url: "https://pub.dev"
source: hosted
version: "1.2.0"
fake_async:
dependency: transitive
description:

View file

@ -49,6 +49,7 @@ dependencies:
modal_bottom_sheet: ^3.0.0
video_player: ^2.9.2
chewie: ^1.8.5
expandable_page_view: ^1.0.17
dev_dependencies:
flutter_test: