Skip to content

時間割をリファクタリング#427

Closed
kantacky wants to merge 99 commits intomainfrom
issue/425-fix-timetable
Closed

時間割をリファクタリング#427
kantacky wants to merge 99 commits intomainfrom
issue/425-fix-timetable

Conversation

@kantacky
Copy link
Member

@kantacky kantacky commented Jan 17, 2026

やったこと

  • 時間割をリファクタリング
    • ドメインモデルを追加
    • lib/feature/timetable/repository/timetable_repository.dart の処理を分離(コピー)して、HomeからこのRepositoryに依存しないように変更
      • データレイヤに処理を追加
        • lib/api/firebase/timetable_api.dart を追加
        • lib/api/db/course_db.dart を追加
      • lib/repository/timetable_repository.dart を追加
        • データレイヤから返却されたデータをドメインモデルに変換
    • Service, ViewModel, ViewState, Screen (v2) を追加
      • UI を Design System v2 を適用
  • Close 時間割に休講・補講ラベルが表示されない問題を修正 #425

確認したこと

  • 時間割が正しく表示されること
  • 休講・補講ラベルが正しく表示されること

UI 差分

Before After

メモ

  • テスト期間になってしまったのでデータレイヤのデバッグができない
  • 時刻表示はデザイン待ち
  • テストは今後書きます
  • バックエンド入れてからマージする?

- Introduced `TimetableCourseType` enum to represent course statuses (normal, cancelled, madeUp).
- Created `TimetableCourse` class with required fields including slot, lessonId, courseName, roomName, and type.
- Added `TimetableSlot` enum to define time slots with a method to retrieve the slot number.
- Established `Timetable` class to encapsulate timetable data with a date field.
- Updated import paths for room response and schedule response files.
- Created `RoomResponse` and `RoomScheduleResponse` classes with JSON serialization support using Freezed for better data handling in the Firebase API.
- Updated the `Timetable` class to require a list of `TimetableCourse` objects in addition to the existing date field for better representation of timetable data.
- Introduced `TimetableCourseResponse` class with JSON serialization support using Freezed.
- Added fields for lesson ID, title, resource IDs, and optional cancellation and supervision flags to enhance timetable data handling.
- Implemented `TimetableAPI` class to handle timetable data retrieval and filtering.
- Added methods to get date ranges, filter personal timetables, and load lesson schedules.
- Integrated JSON file reading for timetable and cancellation data to enhance functionality.
- Improved handling of personal timetable lists and lesson schedules for better user experience.
- Introduced `TimetableRepository` interface and its implementation `TimetableRepositoryImpl` for fetching timetable data.
- Implemented method to retrieve timetables, including room name mapping and course conversion from API responses.
- Enhanced error handling with debug logging for better troubleshooting.
@kantacky kantacky added this to the 260129_ milestone Jan 17, 2026
@kantacky kantacky self-assigned this Jan 17, 2026
@kantacky kantacky changed the base branch from main to update-design January 17, 2026 12:21
@kantacky kantacky changed the title Issue/425 時間割をリファクタリング Jan 17, 2026
… and consistency. Updated button disabled states and added new accent color variations in the color scheme.
…e new design system. Update button and text colors for improved consistency and accessibility.
@kantacky kantacky changed the title ホーム画面の時間割をリファクタリング 時間割をリファクタリング Jan 27, 2026
- Introduced a new data model for CancelLecture with additional fields.
- Removed outdated controllers and services related to course cancellation.
- Implemented a new CourseCancellationService to handle fetching and filtering of course cancellations.
- Updated CourseCancellationScreen and related view models to utilize the new service and data model.
- Enhanced the user interface for better interaction with course cancellation data.
- Introduced WeekPeriodRecord model to represent week period data.
- Updated CourseDB to include methods for fetching week period records.
- Refactored timetable services and screens to utilize WeekPeriodRecord instead of raw Map data.
- Enhanced type safety and readability in timetable-related functionalities.
… 'View' to 'Screen' and clarify the use of ConsumerStatefulWidget in Dotto. Adjusted descriptions related to state management and ViewModel responsibilities accordingly.
- Introduced SearchCourseDomainError enum to manage selection limit errors.
- Refactored SearchCourseScreen to use ConsumerStatefulWidget for improved state management.
- Implemented SearchCourseService for handling user preferences and course selection logic.
- Updated SearchCourseViewModel to integrate new service and manage state effectively.
- Enhanced UI components to reflect changes in course selection and error handling.
- Removed outdated SearchCourseUsecase and related controllers for cleaner architecture.
@kantacky kantacky marked this pull request as ready for review January 27, 2026 07:10
@kantacky kantacky requested review from a team, Copilot, hikaru-0602 and masaya-osuga January 27, 2026 07:10
@github-actions
Copy link
Contributor

github-actions bot commented Jan 27, 2026

3 Warnings
⚠️ このPRは大きすぎます (4875 lines)。小さなPRに分割することを検討してください。
⚠️ アプリコードが変更されていますが、テストが追加されていません。テストの追加を検討してください。
⚠️ 重要なファイルが変更されています: lib/main.dart
3 Messages
📖 削除されたファイル: lib/feature/home/home.dart, lib/feature/search_course/search_course_usecase.dart, lib/feature/timetable/controller/course_cancellation_controller.dart, lib/feature/timetable/controller/focused_timetable_date_controller.dart, lib/feature/timetable/controller/is_filtered_only_taking_course_cancellation_controller.dart, lib/feature/timetable/controller/personal_lesson_id_list_controller.dart, lib/feature/timetable/controller/selected_semester_controller.dart, lib/feature/timetable/controller/timetable_period_style_controller.dart, lib/feature/timetable/controller/timetable_view_style_controller.dart, lib/feature/timetable/controller/two_week_timetable_controller.dart, lib/feature/timetable/controller/week_period_all_records_controller.dart, lib/feature/timetable/domain/course_cancellation.dart, lib/feature/timetable/domain/timetable_course.dart, lib/feature/timetable/edit_timetable_screen.dart, lib/feature/timetable/repository/timetable_repository.dart, lib/feature/timetable/select_course_screen.dart, lib/feature/timetable/widget/my_page_timetable.dart, lib/feature/timetable/widget/timetable_is_over_selected_snack_bar.dart
📖 追加されたファイル: lib/data/db/course_db.dart, lib/data/db/model/course.dart, lib/data/db/model/week_period_record.dart, lib/data/firebase/timetable_api.dart, lib/data/github/contributor_api.dart, lib/data/json/model/cancel_lecture.dart, lib/data/json/model/one_week_schedule.dart, lib/data/json/model/sup_lecture.dart, lib/data/json/timetable_json.dart, lib/data/preference/timetable_preference.dart, lib/domain/timetable.dart, lib/domain/timetable_course.dart, lib/domain/timetable_course_type.dart, lib/feature/home/component/timetable_calendar_view.dart, lib/feature/home/component/timetable_view.dart, lib/feature/home/home_screen.dart, lib/feature/home/home_service.dart, lib/feature/home/home_viewmodel.dart, lib/feature/home/home_viewstate.dart, lib/feature/search_course/search_course_domain_error.dart, lib/feature/search_course/search_course_service.dart, lib/feature/timetable/screen/edit_timetable_screen.dart, lib/feature/timetable/screen/select_course_screen.dart, lib/feature/timetable/service/course_cancellation_service.dart, lib/feature/timetable/service/edit_timetable_service.dart, lib/feature/timetable/service/select_course_service.dart, lib/feature/timetable/viewmodel/course_cancellation_viewmodel.dart, lib/feature/timetable/viewmodel/edit_timetable_viewmodel.dart, lib/feature/timetable/viewmodel/select_course_viewmodel.dart, lib/feature/timetable/viewstate/course_cancellation_viewstate.dart, lib/feature/timetable/viewstate/edit_timetable_viewstate.dart, lib/feature/timetable/viewstate/select_course_viewstate.dart, lib/repository/timetable_repository.dart
📖 Dangerのチェックが完了しました! 🎉

Generated by 🚫 Danger

Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR refactors the timetable feature by introducing a layered architecture with domain models, a new repository layer, data access objects, and MVVM pattern components (Service, ViewModel, ViewState, Screen).

Changes:

  • Introduced domain models (Timetable, TimetableCourse, TimetableSlot, TimetableCourseType, Semester) to represent timetable data
  • Added new repository layer (lib/repository/timetable_repository.dart) with data synchronization logic between local storage and Firestore
  • Separated data access into dedicated modules (timetable_api.dart for Firestore, course_db.dart for SQLite, timetable_preference.dart for local preferences)
  • Implemented MVVM pattern for timetable-related screens with Service, ViewModel, and ViewState layers
  • Migrated to Design System v2 for UI components
  • Refactored multiple existing features (search course, home screen) to integrate with the new architecture

Reviewed changes

Copilot reviewed 80 out of 86 changed files in this pull request and generated 19 comments.

Show a summary per file
File Description
lib/repository/timetable_repository.dart New repository implementing timetable sync logic between Firestore and local storage with conflict resolution
lib/data/firebase/timetable_api.dart New Firebase API for timetable CRUD operations
lib/data/db/course_db.dart New database access layer for course data
lib/data/preference/timetable_preference.dart New preference layer for local timetable storage
lib/domain/* New domain models for timetable, courses, slots, and course types
lib/feature/timetable/service/* Service layer implementations for select course, edit timetable, and course cancellation
lib/feature/timetable/viewmodel/* ViewModel implementations following MVVM pattern
lib/feature/timetable/viewstate/* ViewState data classes for UI state management
lib/feature/timetable/screen/* Refactored screen implementations with Design System v2
lib/feature/home/* Refactored home screen with new service and ViewModel layers
lib/feature/search_course/* Updated to integrate with new timetable repository
lib/feature/setting/repository/settings_repository.dart Updated to use new timetable repository and handle sync conflicts
lib/helper/location_helper.dart Refactored from singleton to provider-based pattern
Comments suppressed due to low confidence (1)

lib/helper/location_helper.dart:14

  • The comment "TODO: Refactor" provides no context about what needs to be refactored. Since this was just converted from a singleton pattern to a provider-based pattern, consider either completing the refactoring or documenting what specific aspects still need improvement.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +465 to +467
bool _isSameDate(String dateString, DateTime target) {
final date = DateTime.parse(dateString);
return date.month == target.month && date.day == target.day;
Copy link

Copilot AI Jan 27, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The _isSameDate function only compares month and day, but doesn't check the year. This could cause issues when comparing dates across different years, for example, a cancellation from December 2025 could incorrectly match January 2026.

Copilot uses AI. Check for mistakes.
final class TimetableAPI {
static final FirebaseFirestore _db = FirebaseFirestore.instance;
static const String _collection = 'user_taking_course';
static const String _yearKey = '2025';
Copy link

Copilot AI Jan 27, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The hardcoded year key '2025' in the TimetableAPI will need to be updated every year. Consider making this dynamic based on the current academic year or making it configurable to avoid requiring code changes each year.

Suggested change
static const String _yearKey = '2025';
static String get _yearKey {
final now = DateTime.now();
final academicYear = now.month >= 4 ? now.year : now.year - 1;
return academicYear.toString();
}

Copilot uses AI. Check for mistakes.
Comment on lines +11 to +21
// TODO: ドメインモデルを作成
// TODO: TimetableRepositoryに移行
Future<List<WeekPeriodRecord>> getWeekPeriodAllRecords() async {
return CourseDB.getWeekPeriodAllRecords();
}

// TODO: ドメインモデルを作成
// TODO: TimetableRepositoryに移行
Future<List<int>> getPersonalLessonIdList() async {
return TimetablePreference.getPersonalTimetableList();
}
Copy link

Copilot AI Jan 27, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The TODO comments indicate that getWeekPeriodAllRecords and getPersonalLessonIdList should be moved to TimetableRepository with domain models, but they're currently calling data layer directly. This creates inconsistency in the architecture. Consider completing the migration or creating a tracking issue for these TODOs.

Copilot uses AI. Check for mistakes.
Comment on lines +152 to +211
if (diff > 300000) {
final firestoreSet = firestoreList.toSet();
final localSet = localList.toSet();

// 同じIDセットかチェック
if (!firestoreSet.containsAll(localSet) ||
!localSet.containsAll(firestoreSet)) {
// 競合検出
// 元の処理ではここでAlertDialogを表示して、
// 「アカウント側に多い科目」「ローカル側に多い科目」を表示し、
// ユーザーに「アカウント方を残す」「ローカル方を残す」を選択させていた。
// この選択UIはUI層で実装する必要がある。
return TimetableConflictDetected(
firestoreList: firestoreList,
localList: localList,
firestoreOnlyIds: firestoreSet.difference(localSet).toList(),
localOnlyIds: localSet.difference(firestoreSet).toList(),
);
}
}

// 競合なし、Firestoreのデータを採用
await TimetablePreference.savePersonalTimetableList(firestoreList);
return TimetableSynced(firestoreList);
}

@override
Future<List<int>> loadPersonalTimetableList() async {
final user = _currentUser;

if (user == null) {
return TimetablePreference.getPersonalTimetableList();
}

final firestoreData = await TimetableAPI.getUserTimetableData(user.uid);

if (firestoreData == null) {
// Firestoreにデータがない場合、ローカルデータをアップロード
final localList = await TimetablePreference.getPersonalTimetableList();
await TimetableAPI.saveUserTimetable(user.uid, localList);
await TimetablePreference.savePersonalTimetableList(localList);
return localList;
}

final firestoreList = firestoreData.lessonIds;
final localList = await TimetablePreference.getPersonalTimetableList();
final localLastUpdated =
await TimetablePreference.getLastUpdateTimestamp();
final firestoreLastUpdated =
firestoreData.lastUpdated.millisecondsSinceEpoch;
final diff = localLastUpdated - firestoreLastUpdated;

// ローカルが空の場合、Firestoreのデータを採用
if (localList.isEmpty) {
await TimetablePreference.savePersonalTimetableList(firestoreList);
return firestoreList;
}

// Firestoreが空、またはローカルが10分以上新しい場合、ローカルデータをアップロード
if (firestoreList.isEmpty || diff > 600000) {
Copy link

Copilot AI Jan 27, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The sync logic uses a 5-minute threshold (300000ms) in loadPersonalTimetableListOnLogin but a 10-minute threshold (600000ms) in loadPersonalTimetableList. This inconsistency could lead to confusing behavior where the same data is handled differently depending on which method is called. Consider using a consistent threshold or documenting why different thresholds are appropriate for different scenarios.

Copilot uses AI. Check for mistakes.
Comment on lines +67 to +96
final removeLessonIdList = <int>{};
var flag = false;

for (final record in targetWeekPeriod) {
final term = record.semester;
final week = record.week;
final period = record.period;

final selectedLessonList = weekPeriodAllRecords.where((record) {
return record.week == week &&
record.period == period &&
(record.semester == term || record.semester == 0) &&
personalLessonIdList.contains(record.lessonId);
}).toList();

if (selectedLessonList.length > 1) {
final removeLessonList = selectedLessonList.sublist(
2,
selectedLessonList.length,
);
if (removeLessonList.isNotEmpty) {
removeLessonIdList.addAll(
removeLessonList.map((e) => e.lessonId).toSet(),
);
}
flag = true;
}
}

return flag;
Copy link

Copilot AI Jan 27, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The isOverSelected method in SearchCourseService builds a removeLessonIdList set (lines 67, 88-90) but never uses it to actually remove the lessons. This differs from the similar method in SelectCourseService (lib/feature/timetable/service/select_course_service.dart:85-90) which does save the updated list. Either the removal logic is missing here, or the variable removeLessonIdList should be removed if it's not needed.

Copilot uses AI. Check for mistakes.
).removeCurrentSnackBar();
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('3科目以上選択することはできません'),
Copy link

Copilot AI Jan 27, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The error message says "3科目以上選択することはできません" (cannot select 3 or more courses), but the logic in isOverSelected checks if selectedLessonList.length > 1, which means it triggers when there are 2 or more courses. The sublist starts at index 2, which would only be reached if there are already 3 or more courses. However, the error message is inconsistent with the old message "1つのコマに3つ以上選択できません" which says "cannot select 3 or more in one slot". Please clarify the intended behavior and ensure consistency.

Copilot uses AI. Check for mistakes.
Comment on lines +496 to +503
try {
for (final date in dates) {
twoWeekLessonSchedule[date] = await _dailyLessonSchedule(date);
}
return twoWeekLessonSchedule;
} on Exception {
return twoWeekLessonSchedule;
}
Copy link

Copilot AI Jan 27, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The method handles exceptions by catching generic Exception and returning an empty timetable map. This silently swallows errors, making debugging difficult. Consider logging the error with more context or rethrowing it to allow proper error handling at a higher level.

Copilot uses AI. Check for mistakes.
Comment on lines +47 to +97
Future<bool> isOverSelected(int lessonId) async {
final weekPeriodAllRecords = await CourseDB.getWeekPeriodAllRecords();
final personalLessonIdList = await getPersonalLessonIdList();

final filterWeekPeriod = weekPeriodAllRecords
.where((element) => element.lessonId == lessonId)
.toList();
final targetWeekPeriod = filterWeekPeriod
.where((element) => element.semester != 0)
.toList();

// 開講時期が0(通年)の場合は前期・後期両方に展開
for (final element in filterWeekPeriod.where(
(element) => element.semester == 0,
)) {
final e1 = element.copyWith(semester: 10);
final e2 = element.copyWith(semester: 20);
targetWeekPeriod.addAll([e1, e2]);
}

final removeLessonIdList = <int>{};
var flag = false;

for (final record in targetWeekPeriod) {
final term = record.semester;
final week = record.week;
final period = record.period;

final selectedLessonList = weekPeriodAllRecords.where((record) {
return record.week == week &&
record.period == period &&
(record.semester == term || record.semester == 0) &&
personalLessonIdList.contains(record.lessonId);
}).toList();

if (selectedLessonList.length > 1) {
final removeLessonList = selectedLessonList.sublist(
2,
selectedLessonList.length,
);
if (removeLessonList.isNotEmpty) {
removeLessonIdList.addAll(
removeLessonList.map((e) => e.lessonId).toSet(),
);
}
flag = true;
}
}

return flag;
}
Copy link

Copilot AI Jan 27, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The SearchCourseService's isOverSelected method performs side effects (removes lessons from the list) while appearing to be a query method. This violates the principle of command-query separation. The method name suggests it only checks if something is over-selected, but it also modifies state by removing excess lessons. Consider separating the check and the removal into distinct methods, or rename the method to reflect that it performs cleanup.

Copilot uses AI. Check for mistakes.
Comment on lines +427 to +452
Future<void> _processSupplementaryLectures(
DateTime selectTime,
Map<int, Map<int, TimetableCourse>> periodData,
Map<String, int> lessonIdMap,
) async {
final supLectureData = await TimetableJSON.fetchSupLectures();

for (final supLecture in supLectureData) {
if (!_isSameDate(supLecture.date, selectTime)) {
continue;
}

final lessonName = supLecture.lessonName;
if (!lessonIdMap.containsKey(lessonName)) {
continue;
}

final lessonId = lessonIdMap[lessonName]!;
final existingCourse = periodData[supLecture.period]?[lessonId];

if (existingCourse != null) {
periodData[supLecture.period]![lessonId] = existingCourse.copyWith(
type: TimetableCourseType.madeUp,
);
}
}
Copy link

Copilot AI Jan 27, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The method processes supplementary lectures but doesn't handle the case where a supplementary lecture exists without a corresponding normal course in the same period. In this case, existingCourse would be null and the copyWith would fail. Consider handling supplementary lectures that occur in periods where the course normally doesn't have a class.

Copilot uses AI. Check for mistakes.
Comment on lines +16 to 30
return Row(
children: [
DottoButton(
onPressed: onCourseCancellationPressed,
type: DottoButtonType.text,
child: const Text('休講・補講'),
),
const Spacer(),
DottoButton(
onPressed: onEditTimetablePressed,
type: DottoButtonType.text,
child: const Text('時間割を編集'),
),
],
);
Copy link

Copilot AI Jan 27, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The conversion logic removes the padding wrapper that was in the original component. The original TimetableButtons had EdgeInsetsGeometry.symmetric(horizontal: 16) padding, but the new version doesn't. This could affect the layout when integrated into the home screen. Verify that this change is intentional and doesn't break the UI layout.

Copilot uses AI. Check for mistakes.
Copilot AI review requested due to automatic review settings February 2, 2026 13:40
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 81 out of 87 changed files in this pull request and generated 9 comments.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +38 to +42
return UserTimetableData(
lessonIds: lessonIds != null ? List<int>.from(lessonIds) : [],
lastUpdated:
timestamp?.toDate() ?? DateTime.fromMillisecondsSinceEpoch(0),
);
Copy link

Copilot AI Feb 2, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Inconsistent null-safety handling. The method getUserTimetableData can return null when the document doesn't exist, but timestamp?.toDate() at line 41 provides a fallback to epoch (DateTime.fromMillisecondsSinceEpoch(0)). However, this creates an incorrect last_updated time of 1970-01-01 instead of treating it as "never updated". Consider using DateTime.now() or making lastUpdated nullable to properly represent missing data.

Copilot uses AI. Check for mistakes.
Comment on lines +47 to +97
Future<bool> isOverSelected(int lessonId) async {
final weekPeriodAllRecords = await CourseDB.getWeekPeriodAllRecords();
final personalLessonIdList = await getPersonalLessonIdList();

final filterWeekPeriod = weekPeriodAllRecords
.where((element) => element.lessonId == lessonId)
.toList();
final targetWeekPeriod = filterWeekPeriod
.where((element) => element.semester != 0)
.toList();

// 開講時期が0(通年)の場合は前期・後期両方に展開
for (final element in filterWeekPeriod.where(
(element) => element.semester == 0,
)) {
final e1 = element.copyWith(semester: 10);
final e2 = element.copyWith(semester: 20);
targetWeekPeriod.addAll([e1, e2]);
}

final removeLessonIdList = <int>{};
var flag = false;

for (final record in targetWeekPeriod) {
final term = record.semester;
final week = record.week;
final period = record.period;

final selectedLessonList = weekPeriodAllRecords.where((record) {
return record.week == week &&
record.period == period &&
(record.semester == term || record.semester == 0) &&
personalLessonIdList.contains(record.lessonId);
}).toList();

if (selectedLessonList.length > 1) {
final removeLessonList = selectedLessonList.sublist(
2,
selectedLessonList.length,
);
if (removeLessonList.isNotEmpty) {
removeLessonIdList.addAll(
removeLessonList.map((e) => e.lessonId).toSet(),
);
}
flag = true;
}
}

return flag;
}
Copy link

Copilot AI Feb 2, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The isOverSelected method duplicates similar logic found in SelectCourseService.isOverSelected. Both methods perform the same conflict checking with semester expansion (semester 0 → 10 and 20). Consider extracting this common logic into a shared utility or service method to maintain consistency and reduce code duplication.

Copilot uses AI. Check for mistakes.
// 競合検出
// 元の処理ではここでAlertDialogを表示して、
// 「アカウント側に多い科目」「ローカル側に多い科目」を表示し、
// ユーザーに「アカウント方を残す」「ローカル方を残す」を選択させていた。
Copy link

Copilot AI Feb 2, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Typo in documentation comment: 'アカウント方' and 'ローカル方' should be 'アカウント側' and 'ローカル側' respectively for correct Japanese grammar.

Copilot uses AI. Check for mistakes.
// 競合検出
// 元の処理ではここでAlertDialogを表示して、
// 「アカウント側に多い科目」「ローカル側に多い科目」を表示し、
// ユーザーに「アカウント方を残す」「ローカル方を残す」を選択させていた。
Copy link

Copilot AI Feb 2, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Typo in documentation comment: 'ローカル方' should be 'ローカル側' for correct Japanese grammar.

Copilot uses AI. Check for mistakes.
Comment on lines +38 to +41
void onTimetablePeriodStyleChanged(TimetablePeriodStyle style) {
unawaited(_service.setTimetablePeriodStyle(style));
state = state.copyWith(timetablePeriodStyle: AsyncValue.data(style));
}
Copy link

Copilot AI Feb 2, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Missing error handling for unawaited async operations. The onTimetablePeriodStyleChanged method uses unawaited() to call _service.setTimetablePeriodStyle(style), but if this operation fails, the error will be silently ignored while the UI state is already updated. This could lead to inconsistent state between the UI and persisted preferences.

Copilot uses AI. Check for mistakes.
Comment on lines +465 to +468
bool _isSameDate(String dateString, DateTime target) {
final date = DateTime.parse(dateString);
return date.month == target.month && date.day == target.day;
}
Copy link

Copilot AI Feb 2, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The date comparison logic in _isSameDate only checks month and day, ignoring the year. This could cause incorrect matches when comparing dates across different years. For example, a cancellation from 2024-12-25 would incorrectly match with 2025-12-25.

Copilot uses AI. Check for mistakes.
Comment on lines +41 to +92
Future<bool> isOverSelected(int lessonId) async {
final weekPeriodAllRecords = await CourseDB.getWeekPeriodAllRecords();
final personalLessonIdList = await getPersonalLessonIdList();

final filterWeekPeriod = weekPeriodAllRecords
.where((element) => element.lessonId == lessonId)
.toList();
final targetWeekPeriod = filterWeekPeriod
.where((element) => element.semester != 0)
.toList();

for (final element in filterWeekPeriod.where(
(element) => element.semester == 0,
)) {
final e1 = element.copyWith(semester: 10);
final e2 = element.copyWith(semester: 20);
targetWeekPeriod.addAll([e1, e2]);
}

final removeLessonIdList = <int>{};
var flag = false;

for (final record in targetWeekPeriod) {
final selectedLessonList = weekPeriodAllRecords.where((r) {
return r.week == record.week &&
r.period == record.period &&
(r.semester == record.semester || r.semester == 0) &&
personalLessonIdList.contains(r.lessonId);
}).toList();

if (selectedLessonList.length > 1) {
final removeLessonList = selectedLessonList.sublist(
2,
selectedLessonList.length,
);
if (removeLessonList.isNotEmpty) {
removeLessonIdList.addAll(
removeLessonList.map((e) => e.lessonId).toSet(),
);
}
flag = true;
}
}

if (removeLessonIdList.isNotEmpty) {
final updatedList = personalLessonIdList
.where((id) => !removeLessonIdList.contains(id))
.toList();
await _savePersonalLessonIdList(updatedList);
}

return flag;
Copy link

Copilot AI Feb 2, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The method isOverSelected has a side effect that modifies the personal lesson ID list by removing courses when conflicts are detected. This behavior is not clear from the method name (which suggests it only checks/queries) and could lead to unexpected data loss. Consider separating the validation logic from the mutation logic, or rename the method to clarify its behavior (e.g., checkAndResolveOverSelection).

Copilot uses AI. Check for mistakes.
Comment on lines +112 to +114
const SnackBar(
content: Text('3科目以上選択することはできません'),
),
Copy link

Copilot AI Feb 2, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The error message text changed from '1つのコマに3つ以上選択できません' to '3科目以上選択することはできません'. While this improves clarity, the actual validation logic checks if (selectedLessonList.length > 1) which allows up to 2 courses, not 3. The error message should say '3科目以上' (3 or more) is accurate only if the limit is 2 courses per slot. Please verify this is the intended behavior and message.

Copilot uses AI. Check for mistakes.
Comment on lines +26 to +32
Future<void> onAppear() async {
_service.startBusPolling();
await Future.wait([
_refresh(),
_service.changeDirectionOnCurrentLocation(),
]);
}
Copy link

Copilot AI Feb 2, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The HomeViewModel's build method initializes selectedDate but then onAppear immediately refreshes the state without updating the date. However, in TimetableCalendarView at line 76, there's a shadowing bug where a new selectedDate is created. This compounded issue means the date selection feature may not work correctly. Verify that date selection properly updates the HomeViewModel's state.

Copilot uses AI. Check for mistakes.
Copilot AI review requested due to automatic review settings February 2, 2026 13:58
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 81 out of 87 changed files in this pull request and generated 14 comments.

Comments suppressed due to low confidence (1)

docs/onboarding/codebase/02_Architecture.md:48

  • ドキュメントとコードの不一致:アーキテクチャドキュメントでは「UseCase」という用語が使用されていますが、実際のコードでは「Service」(例:SelectCourseService, EditTimetableService, CourseCancellationService)が使用されています。

用語の一貫性を保つため、ドキュメントをコードに合わせて「Service」に更新するか、コードをドキュメントに合わせて「UseCase」に変更することを推奨します。

### UseCase

ViewModel と Repository の橋渡しを行います。

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +31 to +32
final List<Map<String, dynamic>> records = await database.rawQuery(
'SELECT 授業名 FROM sort WHERE LessonId in (${lessonIdList.join(",")})',
Copy link

Copilot AI Feb 2, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

SQLインジェクションの脆弱性があります。lessonIdList.join(",")を使用してクエリを構築しているため、悪意のある入力があった場合に問題が発生する可能性があります。

パラメータ化されたクエリを使用してください。

Copilot uses AI. Check for mistakes.
/// 日付が同じかどうかを判定
bool _isSameDate(String dateString, DateTime target) {
final date = DateTime.parse(dateString);
return date.month == target.month && date.day == target.day;
Copy link

Copilot AI Feb 2, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

日付比較のロジックに潜在的なバグがあります。年が異なる場合に正しく動作しません。例えば、12月31日と1月1日を比較した場合、年をチェックしていないため、誤って「同じ日付」と判定される可能性があります。

年も含めて比較するように修正してください。

Copilot uses AI. Check for mistakes.
}

// Firestoreが空、またはローカルが10分以上新しい場合、ローカルデータをアップロード
if (firestoreList.isEmpty || diff > 600000) {
Copy link

Copilot AI Feb 2, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

マジックナンバー(600000)が使用されています。10分を表すタイムスタンプ差分として使われていますが、定数として定義することで可読性が向上します。

例:const _localNewerThresholdMs = 600000; // 10 minutes

Copilot uses AI. Check for mistakes.
final now = DateTime.now();
final today = DateTime(now.year, now.month, now.day);
final monday = today.subtract(Duration(days: today.weekday - 1));
final selectedDate = DateTime(now.year, now.month, now.day);
Copy link

Copilot AI Feb 2, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

selectedDate変数が定義されていますが使用されていません。代わりにthis.selectedDateが使用されています。未使用の変数は削除してください。

Copilot uses AI. Check for mistakes.
Comment on lines +51 to +61
Future<void> changeDirectionOnCurrentLocation() async {
final locationHelper = ref.read(locationHelperProvider);
final position = await locationHelper.determinePosition();
if (position != null) {
final latitude = position.latitude;
if (latitude > 41.838770 && latitude < 41.845295) {
final longitude = position.longitude;
if (longitude > 140.765061 && longitude < 140.770368) {
ref.read(busIsToProvider.notifier).toggle();
}
}
Copy link

Copilot AI Feb 2, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

セキュリティ上の懸念:ハードコードされた緯度経度の範囲で位置情報をチェックしていますが、この範囲が正しいかどうか検証できません。また、この位置情報チェックの目的がコメントで説明されていないため、意図が不明確です。

位置情報の使用目的と、この座標範囲が何を表しているのかをコメントで明記してください。

Copilot uses AI. Check for mistakes.
Comment on lines +77 to +79
final dates = List.generate(
5,
(index) => monday.add(Duration(days: index)),
Copy link

Copilot AI Feb 2, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ハードコードされた日数(5日)が使用されています。月曜から金曜までの平日を表していると思われますが、定数として定義するか、DayOfWeek.weekdays.lengthを使用することで意図がより明確になります。

Copilot uses AI. Check for mistakes.
Comment on lines +43 to +44
final List<Map<String, dynamic>> records = await database.rawQuery(
'SELECT LessonId, 授業名 FROM sort WHERE LessonId in (${lessonIdList.join(",")})',
Copy link

Copilot AI Feb 2, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

SQLインジェクションの脆弱性があります。lessonIdList.join(",")を使用してクエリを構築しているため、悪意のある入力があった場合に問題が発生する可能性があります。

パラメータ化されたクエリを使用してください。例:WHERE LessonId IN (${lessonIdList.map((_) => '?').join(',')})と引数としてlessonIdListを渡す。

Copilot uses AI. Check for mistakes.
Comment on lines +108 to +111
} catch (e) {
debugPrint(e.toString());
rethrow;
}
Copy link

Copilot AI Feb 2, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

エラー処理が不十分です。_getTimetables()で例外が発生した場合、rethrowで再スローしていますが、呼び出し元で適切にハンドリングされていません。空のリストを返すなど、より明示的なエラーハンドリングを検討してください。

Copilot uses AI. Check for mistakes.
Comment on lines +55 to +57
final e1 = element.copyWith(semester: 10);
final e2 = element.copyWith(semester: 20);
targetWeekPeriod.addAll([e1, e2]);
Copy link

Copilot AI Feb 2, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

マジックナンバー(10, 20)が使用されています。これらは学期(前期・後期)を表す値のようですが、Semester enumのnumberプロパティ(10: 前期、20: 後期)を使用しているため、コメントまたは定数で明示的に説明することを推奨します。

Copilot uses AI. Check for mistakes.
Comment on lines +75 to +83
'SELECT * FROM week_period order by lessonId',
);
return records
.where((record) =>
record['week'] == week &&
record['period'] == period &&
(record['開講時期'] == semester || record['開講時期'] == 0))
.map(WeekPeriodRecord.fromMap)
.toList();
Copy link

Copilot AI Feb 2, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

パフォーマンスの懸念:getAvailableCoursesメソッドでは、全レコードを取得してからフィルタリングしています。データベースレベルでWHERE句を使用してフィルタリングする方が効率的です。

SQLクエリを修正して、WHERE句で条件を指定することを推奨します。

Suggested change
'SELECT * FROM week_period order by lessonId',
);
return records
.where((record) =>
record['week'] == week &&
record['period'] == period &&
(record['開講時期'] == semester || record['開講時期'] == 0))
.map(WeekPeriodRecord.fromMap)
.toList();
'SELECT * FROM week_period WHERE week = ? AND period = ? AND (開講時期 = ? OR 開講時期 = 0) ORDER BY lessonId',
[week, period, semester],
);
return records.map(WeekPeriodRecord.fromMap).toList();

Copilot uses AI. Check for mistakes.
@kantacky kantacky modified the milestones: 260129_1.6.0, 260312_2.0.0 Feb 7, 2026
@kantacky kantacky closed this Feb 18, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

時間割に休講・補講ラベルが表示されない問題を修正

2 participants