Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
46 changes: 46 additions & 0 deletions docs/plans/2026-03-09-todo-list-calendar-redesign.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
# 홈 화면 Todo Bank + Slidable 스와이프 액션 구현

**Goal:** 홈 화면 바텀시트에 todo bank 섹션을 추가하고, 할일 스와이프 액션을 flutter_slidable로 개선하여 날짜별 할일 관리 UX를 향상시킨다.

**Architecture:** 기존 HomeScreen 바텀시트에 todo bank 섹션 추가. TodoListScreen은 변경 없음. DismissibleTodoItem을 Dismissible에서 flutter_slidable의 Slidable로 교체.

**Tech Stack:** Flutter · Riverpod · flutter_slidable · table_calendar

**관련 이슈:** #51

---

## 구현 내용

### 1. Todo Bank (홈 화면 바텀시트)

**변경 파일:**
- `lib/features/todo/presentation/providers/todo_provider.dart` — `todosNotForDate` provider 추가
- `lib/features/todo/presentation/providers/todo_provider.g.dart` — 코드 생성
- `lib/features/home/presentation/screens/home_screen.dart` — `_buildTodoBankSection`, `_addTodoToDate` 추가

**동작:**
- 홈 화면 캘린더에서 날짜 선택 후, 해당 날짜에 배정되지 않은 할일 목록을 "할 일 추가" 섹션으로 표시
- 할일을 탭하면 선택된 날짜의 scheduledDates에 추가되어 즉시 해당 날짜 할일 목록에 나타남
- 다른 날짜를 선택하면 해당 날짜에 미배정된 할일이 다시 bank에 표시됨
- scheduledDates가 비어있는 할일(미지정)은 bank에 표시하지 않음

### 2. Slidable 스와이프 액션

**변경 파일:**
- `lib/features/todo/presentation/widgets/dismissible_todo_item.dart` — Dismissible → Slidable 교체
- `pubspec.yaml` / `pubspec.lock` — flutter_slidable 패키지 추가

**동작:**
- **좌→우 (startActionPane):** 카테고리 이동 — `primaryLight` 색상
- **우→좌 (endActionPane):**
- 날짜에서 제거 (contextDate가 있을 때만) — `accentGoldLight` 색상
- 삭제 — `error` 색상
- CustomSlidableAction + 투명 배경 + 아이콘/텍스트만 표시 (다크 우주 테마에 어울리는 미니멀 스타일)
- 타이머 연동 중인 할일은 제거/삭제 차단

### 3. 변경하지 않은 것

- TodoListScreen (카테고리 그리드 중심 레이아웃 유지)
- CategoryTodoScreen 및 라우팅
- 기존 할일 CRUD 로직

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

56 changes: 54 additions & 2 deletions lib/features/home/presentation/screens/home_screen.dart
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import '../../../../routes/route_paths.dart';
import '../../../timer/presentation/providers/study_stats_provider.dart';
import '../../../todo/domain/entities/todo_entity.dart';
import '../../../todo/presentation/providers/todo_provider.dart';
import '../../../../core/widgets/space/todo_item.dart';
import '../../../todo/presentation/widgets/dismissible_todo_item.dart';
import '../../../todo/presentation/widgets/todo_add_bottom_sheet.dart';
import '../widgets/space_calendar.dart';
Expand Down Expand Up @@ -346,6 +347,7 @@ class _HomeScreenState extends ConsumerState<HomeScreen> {
_selectedDay.day,
);
final todosForSelected = ref.watch(todosForDateProvider(selectedKey));
final bankTodos = ref.watch(todosNotForDateProvider(selectedKey));
final unscheduled = ref.watch(unscheduledTodosProvider);
final todosByDate = ref.watch(todosByDateMapProvider);
final dateLabel = DateFormat('M/d', 'ko_KR').format(_selectedDay);
Expand Down Expand Up @@ -421,16 +423,19 @@ class _HomeScreenState extends ConsumerState<HomeScreen> {
),
SizedBox(height: AppSpacing.s8),

if (todosForSelected.isEmpty)
if (todosForSelected.isEmpty && bankTodos.isEmpty)
Padding(
padding: AppPadding.horizontal20,
child: _buildEmptyTodoCard(),
)
else
else if (todosForSelected.isNotEmpty)
...todosForSelected.map(
(todo) => _buildTodoRow(todo, contextDate: _selectedDay),
),

// ── 할 일 추가 (todo bank) ──
_buildTodoBankSection(selectedKey, bankTodos),

// ── 카테고리 관리 버튼 ──
Padding(
padding: AppPadding.horizontal20,
Expand Down Expand Up @@ -485,6 +490,53 @@ class _HomeScreenState extends ConsumerState<HomeScreen> {
);
}

/// 할 일 추가 (todo bank): 선택된 날짜에 배정되지 않은 할일 목록
///
/// 탭하면 해당 날짜에 즉시 추가된다.
Widget _buildTodoBankSection(
DateTime selectedDate,
List<TodoEntity> bankTodos,
) {
if (bankTodos.isEmpty) return const SizedBox.shrink();

return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
SizedBox(height: AppSpacing.s16),
Padding(
padding: AppPadding.horizontal20,
child: _buildSectionTitle('할 일 추가'),
),
SizedBox(height: AppSpacing.s8),
...bankTodos.map(
(todo) => Padding(
padding: EdgeInsets.fromLTRB(20.w, 0, 20.w, AppSpacing.s8),
child: TodoItem(
title: todo.title,
subtitle: todo.studyTimeLabel,
isCompleted: false,
leading: Icon(
Icons.add_circle_outline_rounded,
color: AppColors.primary,
size: 24.w,
),
onToggle: () => _addTodoToDate(ref, todo, selectedDate),
onTap: () => _addTodoToDate(ref, todo, selectedDate),
Comment on lines +523 to +524
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

# First, find the home_screen.dart file and check its size
fd -t f "home_screen.dart" lib/features/home/presentation/screens/

# Then read the relevant sections

Repository: SpaceStudyShip/SpaceStudyShip-FE

Length of output: 131


🏁 Script executed:

# Get the file location and inspect lines around 523-524 and 532-534
cat -n lib/features/home/presentation/screens/home_screen.dart | sed -n '515,545p'

Repository: SpaceStudyShip/SpaceStudyShip-FE

Length of output: 1222


🏁 Script executed:

# Also search for the _addTodoToDate method definition
grep -n "_addTodoToDate" lib/features/home/presentation/screens/home_screen.dart

Repository: SpaceStudyShip/SpaceStudyShip-FE

Length of output: 297


🏁 Script executed:

# Search for the addDateToTodo mutation definition to verify it's async
grep -n "addDateToTodo" lib/features/home/presentation/screens/home_screen.dart | head -20

Repository: SpaceStudyShip/SpaceStudyShip-FE

Length of output: 154


🏁 Script executed:

# Find the todoListNotifierProvider
fd -t f "todo_list.*provider" lib/features/home/

Repository: SpaceStudyShip/SpaceStudyShip-FE

Length of output: 58


🏁 Script executed:

# Search for todoListNotifierProvider definition
rg "todoListNotifierProvider" lib/features/home/ -A 5 | head -40

Repository: SpaceStudyShip/SpaceStudyShip-FE

Length of output: 1237


🏁 Script executed:

# Search for addDateToTodo method definition
rg "addDateToTodo" lib/features/home/ -B 3 -A 10

Repository: SpaceStudyShip/SpaceStudyShip-FE

Length of output: 1270


🏁 Script executed:

# Search across entire lib directory for todoListNotifierProvider
rg "final todoListNotifierProvider" lib/ -A 10

Repository: SpaceStudyShip/SpaceStudyShip-FE

Length of output: 1117


🏁 Script executed:

# Search for the notifier class that has addDateToTodo method
rg "class.*Notifier.*extends" lib/features/home/ -A 20 | grep -A 20 "TodoList\|addDateToTodo"

Repository: SpaceStudyShip/SpaceStudyShip-FE

Length of output: 58


🏁 Script executed:

# Find all provider files in home feature
fd -t f "provider" lib/features/home/

Repository: SpaceStudyShip/SpaceStudyShip-FE

Length of output: 58


🏁 Script executed:

# List all files in home presentation directory
ls -la lib/features/home/presentation/

Repository: SpaceStudyShip/SpaceStudyShip-FE

Length of output: 296


🏁 Script executed:

# Find the source provider file (not generated)
fd -t f "todo_provider.dart" lib/features/todo/

Repository: SpaceStudyShip/SpaceStudyShip-FE

Length of output: 135


🏁 Script executed:

# Read the TodoListNotifier class implementation
rg "class TodoListNotifier" lib/features/todo/ -A 100 | head -150

Repository: SpaceStudyShip/SpaceStudyShip-FE

Length of output: 9215


🏁 Script executed:

# Search specifically for addDateToTodo method
rg "addDateToTodo" lib/features/todo/ -B 3 -A 15

Repository: SpaceStudyShip/SpaceStudyShip-FE

Length of output: 1747


🏁 Script executed:

# Check if there are other usages of _addTodoToDate at lines around 532-534
cat -n lib/features/home/presentation/screens/home_screen.dart | sed -n '525,540p'

Repository: SpaceStudyShip/SpaceStudyShip-FE

Length of output: 550


_addTodoToDate()Future<void>로 변경하고 await/try-catch로 에러 처리 필수

addDateToTodo()가 비동기 mutation인데 _addTodoToDate()void 반환형이고 await 없이 호출되므로, 저장 실패 시 에러가 처리되지 않은 채 비동기로 흘러갑니다. 이 헬퍼를 Future<void>로 변경하고 내부에서 await/try-catch로 사용자 피드백을 처리해 주세요.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@lib/features/home/presentation/screens/home_screen.dart` around lines 523 -
524, 함수 _addTodoToDate를 void에서 Future<void>로 변경하고 내부에서 addDateToTodo 비동기 호출을
await로 수행한 뒤 try-catch로 에러를 잡아 사용자 피드백(예: 스낵바/토스트/로깅)을 호출하도록 수정하세요; 또한 이 헬퍼를
호출하는 곳(onToggle/onTap 익명 콜백)도 async로 바꾸고 await _addTodoToDate(ref, todo,
selectedDate)로 호출하도록 업데이트해 비동기 실패가 누락되지 않게 하세요.

),
),
),
],
);
}

Future<void> _addTodoToDate(
WidgetRef ref,
TodoEntity todo,
DateTime date,
) async {
await ref.read(todoListNotifierProvider.notifier).addDateToTodo(todo, date);
}

Widget _buildEmptyTodoCard() {
return AppCard(
style: AppCardStyle.outlined,
Expand Down

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 3 additions & 0 deletions lib/features/todo/domain/entities/todo_entity.dart
Original file line number Diff line number Diff line change
Expand Up @@ -30,4 +30,7 @@ class TodoEntity with _$TodoEntity {
if (scheduledDates.isEmpty) return completedDates.isNotEmpty;
return scheduledDates.every((d) => isCompletedForDate(d));
}

String? get studyTimeLabel =>
actualMinutes != null && actualMinutes! > 0 ? '$actualMinutes분 공부' : null;
}
39 changes: 33 additions & 6 deletions lib/features/todo/presentation/providers/todo_provider.dart
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,20 @@ class TodoListNotifier extends _$TodoListNotifier {
}
}

/// 특정 날짜를 할일에 추가 (todo bank → 날짜 배정)
Future<void> addDateToTodo(TodoEntity todo, DateTime date) async {
final normalized = TodoEntity.normalizeDate(date);
if (todo.scheduledDates.any(
(d) => TodoEntity.normalizeDate(d) == normalized,
)) {
return;
}
final updated = todo.copyWith(
scheduledDates: [...todo.scheduledDates, normalized],
);
await updateTodo(updated);
}

Future<void> updateTodo(TodoEntity todo) async {
final previousState = state;
state = AsyncData(
Expand Down Expand Up @@ -291,12 +305,25 @@ class CategoryListNotifier extends _$CategoryListNotifier {
@riverpod
List<TodoEntity> todosForDate(Ref ref, DateTime date) {
final todos = ref.watch(todoListNotifierProvider).valueOrNull ?? [];
final normalizedDate = DateTime(date.year, date.month, date.day);
final normalizedDate = TodoEntity.normalizeDate(date);
return todos.where((t) {
return t.scheduledDates.any(
(d) => TodoEntity.normalizeDate(d) == normalizedDate,
);
}).toList();
}

// === 해당 날짜에 배정되지 않은 할일 (todo bank용) ===

@riverpod
List<TodoEntity> todosNotForDate(Ref ref, DateTime date) {
final todos = ref.watch(todoListNotifierProvider).valueOrNull ?? [];
final normalizedDate = TodoEntity.normalizeDate(date);
return todos.where((t) {
return t.scheduledDates.any((d) {
final scheduled = DateTime(d.year, d.month, d.day);
return scheduled == normalizedDate;
});
if (t.scheduledDates.isEmpty) return false;
return !t.scheduledDates.any(
(d) => TodoEntity.normalizeDate(d) == normalizedDate,
);
}).toList();
}

Expand All @@ -316,7 +343,7 @@ Map<DateTime, List<TodoEntity>> todosByDateMap(Ref ref) {
final map = <DateTime, List<TodoEntity>>{};
for (final todo in todos) {
for (final date in todo.scheduledDates) {
final key = DateTime(date.year, date.month, date.day);
final key = TodoEntity.normalizeDate(date);
map.putIfAbsent(key, () => []).add(todo);
}
}
Expand Down
Loading