/// 일정 컨트롤러 (MVCS의 Controller 레이어) /// /// 비즈니스 로직과 상태 관리를 담당합니다. /// View는 이 Controller를 통해 데이터에 접근합니다. library; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:intl/intl.dart'; import '../models/schedule.dart'; import '../services/schedules_service.dart'; /// 일정 상태 class ScheduleState { final DateTime selectedDate; final List schedules; final bool isLoading; final String? error; // 달력용 월별 일정 캐시 (key: "yyyy-MM") final Map> calendarCache; const ScheduleState({ required this.selectedDate, this.schedules = const [], this.isLoading = false, this.error, this.calendarCache = const {}, }); /// 상태 복사 (불변성 유지) ScheduleState copyWith({ DateTime? selectedDate, List? schedules, bool? isLoading, String? error, Map>? calendarCache, }) { return ScheduleState( selectedDate: selectedDate ?? this.selectedDate, schedules: schedules ?? this.schedules, isLoading: isLoading ?? this.isLoading, error: error, calendarCache: calendarCache ?? this.calendarCache, ); } /// 선택된 날짜의 일정 목록 List get selectedDateSchedules { final dateStr = DateFormat('yyyy-MM-dd').format(selectedDate); return schedules.where((s) => s.date.split('T')[0] == dateStr).toList(); } /// 특정 날짜의 일정 (점 표시용, 최대 3개) /// 캐시에서 먼저 찾고, 없으면 현재 schedules에서 찾음 List 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 get daysInMonth { final year = selectedDate.year; final month = selectedDate.month; final lastDay = DateTime(year, month + 1, 0).day; return List.generate(lastDay, (i) => DateTime(year, month, i + 1)); } } /// 일정 컨트롤러 class ScheduleController extends Notifier { @override ScheduleState build() { // 초기 상태 final initialState = ScheduleState(selectedDate: DateTime.now()); // 초기 데이터 로드 Future.microtask(() => loadSchedules()); return initialState; } /// 월별 일정 로드 Future loadSchedules() async { state = state.copyWith(isLoading: true, error: null); try { final schedules = await getSchedules( state.selectedDate.year, state.selectedDate.month, ); // 현재 월 일정을 캐시에도 저장 final cacheKey = '${state.selectedDate.year}-${state.selectedDate.month.toString().padLeft(2, '0')}'; final newCache = Map>.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 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>.from(state.calendarCache); newCache[cacheKey] = schedules; state = state.copyWith(calendarCache: newCache); } catch (e) { // 에러는 무시 (달력 점 표시가 안될 뿐) } } /// 날짜 선택 void selectDate(DateTime date) { state = state.copyWith(selectedDate: date); } /// 월 변경 void changeMonth(int delta) { final newDate = DateTime( state.selectedDate.year, state.selectedDate.month + delta, 1, ); final today = DateTime.now(); // 이번 달이면 오늘 날짜, 다른 달이면 1일 선택 final selectedDay = (newDate.year == today.year && newDate.month == today.month) ? today.day : 1; state = state.copyWith( selectedDate: DateTime(newDate.year, newDate.month, selectedDay), ); loadSchedules(); } /// 특정 날짜로 이동 (달력에서 선택 시) void goToDate(DateTime date) { final currentMonth = state.selectedDate.month; final currentYear = state.selectedDate.year; state = state.copyWith(selectedDate: date); // 월이 변경되면 일정 다시 로드 if (date.month != currentMonth || date.year != currentYear) { loadSchedules(); } } /// 오늘 여부 bool isToday(DateTime date) { final today = DateTime.now(); return date.year == today.year && date.month == today.month && date.day == today.day; } /// 선택된 날짜 여부 bool isSelected(DateTime date) { return date.year == state.selectedDate.year && date.month == state.selectedDate.month && date.day == state.selectedDate.day; } } /// 일정 Provider final scheduleProvider = NotifierProvider( ScheduleController.new, ); /// 검색 상태 class SearchState { final String searchTerm; final List results; final bool isLoading; final bool isFetchingMore; final bool hasMore; final int offset; final String? error; const SearchState({ this.searchTerm = '', this.results = const [], this.isLoading = false, this.isFetchingMore = false, this.hasMore = true, this.offset = 0, this.error, }); SearchState copyWith({ String? searchTerm, List? results, bool? isLoading, bool? isFetchingMore, bool? hasMore, int? offset, String? error, }) { return SearchState( searchTerm: searchTerm ?? this.searchTerm, results: results ?? this.results, isLoading: isLoading ?? this.isLoading, isFetchingMore: isFetchingMore ?? this.isFetchingMore, hasMore: hasMore ?? this.hasMore, offset: offset ?? this.offset, error: error, ); } } /// 검색 컨트롤러 class ScheduleSearchController extends Notifier { static const int _pageSize = 20; @override SearchState build() { return const SearchState(); } /// 검색 실행 Future search(String query) async { if (query.trim().isEmpty) { state = const SearchState(); return; } state = SearchState(searchTerm: query, isLoading: true); try { final result = await searchSchedules(query, offset: 0, limit: _pageSize); state = state.copyWith( results: result.schedules, isLoading: false, hasMore: result.hasMore, offset: result.schedules.length, ); } catch (e) { state = state.copyWith(isLoading: false, error: e.toString()); } } /// 다음 페이지 로드 Future loadMore() async { if (state.isFetchingMore || !state.hasMore || state.searchTerm.isEmpty) { return; } state = state.copyWith(isFetchingMore: true); try { final result = await searchSchedules( state.searchTerm, offset: state.offset, limit: _pageSize, ); state = state.copyWith( results: [...state.results, ...result.schedules], isFetchingMore: false, hasMore: result.hasMore, offset: state.offset + result.schedules.length, ); } catch (e) { state = state.copyWith(isFetchingMore: false, error: e.toString()); } } /// 검색 초기화 void clear() { state = const SearchState(); } } /// 검색 Provider final searchProvider = NotifierProvider( ScheduleSearchController.new, );