EDOC/Flutter 스터디 (2025-1)

[4.1~4.3] 일정 관리 앱 만들기 ~ 서버와 연동하기

SpruceMoon 2025. 6. 27. 23:48

17. 일정 관리 앱 만들기

17.1 패키지 의존성

먼저 pubspec.yaml에서 의존성을 추가합니다.

dependencies:
  flutter:
    sdk: flutter
  table_calendar: ^3.0.9
  intl: ^0.18.1

intl은 날짜 형식을 출력할 때 필요하고, table_calendar는 달력 위젯입니다.


17.2 TableCalendar 기본 위젯 구조

이제 달력을 앱에 띄워봅시다. 간단한 StatefulWidget으로 작성합니다.

class CalendarScreen extends StatefulWidget {
  const CalendarScreen({super.key});

  @override
  State<CalendarScreen> createState() => _CalendarScreenState();
}

class _CalendarScreenState extends State<CalendarScreen> {
  DateTime _focusedDay = DateTime.now();
  DateTime? _selectedDay;

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('일정 관리')),
      body: Column(
        children: [
          TableCalendar(
            focusedDay: _focusedDay,
            firstDay: DateTime.utc(2020, 1, 1),
            lastDay: DateTime.utc(2030, 12, 31),
            selectedDayPredicate: (day) => isSameDay(_selectedDay, day),
            onDaySelected: (selectedDay, focusedDay) {
              setState(() {
                _selectedDay = selectedDay;
                _focusedDay = focusedDay;
              });
            },
            calendarStyle: CalendarStyle(
              selectedDecoration: BoxDecoration(
                color: Colors.blue,
                shape: BoxShape.circle,
              ),
            ),
          ),
          Expanded(
            child: Center(
              child: Text(
                _selectedDay == null
                    ? '날짜를 선택해주세요'
                    : '${_selectedDay!.toLocal()}의 일정',
                style: const TextStyle(fontSize: 16),
              ),
            ),
          )
        ],
      ),
    );
  }
}

포인트

  • selectedDayPredicate는 선택된 날짜를 알려주는 역할
  • onDaySelected에서 setState를 통해 선택 날짜 업데이트
  • 하단 Expanded 위젯으로 선택된 날짜를 표시

이렇게만 해도 달력과 선택한 날짜를 연결할 수 있어요.


17.3 일정 표시와 리스트뷰 결합

실제로는 선택된 날짜에 맞춘 일정 목록을 띄워야 하죠.
예를 들어 ListView.builder를 활용해 아래에 동적으로 일정을 그리면 됩니다.

List<String> _dummySchedules = [
  '회의',
  '스터디',
  '헬스장',
  '쇼핑',
];

Expanded(
  child: ListView.builder(
    itemCount: _dummySchedules.length,
    itemBuilder: (context, index) {
      return ListTile(
        title: Text(_dummySchedules[index]),
      );
    },
  ),
),
  • 이후 18장에서 이 데이터를 DB로 교체할 예정이기 때문에,
    17장에서는 우선 하드코딩된 리스트로 테스트해도 충분합니다.

18장. 데이터베이스 적용하기

18.1 drift 패키지 설정

pubspec.yaml에 다음을 추가합니다.

dependencies:
  drift: ^2.13.0
  sqlite3_flutter_libs: ^0.5.17
  path_provider: ^2.1.1
  path: ^1.8.3
  get_it: ^7.6.4
  provider: ^6.0.5

dev_dependencies:
  drift_dev: ^2.13.0
  build_runner: ^2.4.6

18.2 DB 테이블 정의

drift_database.dart:

import 'package:drift/drift.dart';
import 'package:drift/native.dart';
import 'package:path_provider/path_provider.dart';
import 'package:path/path.dart' as p;
import 'dart:io';

part 'drift_database.g.dart';

class Schedules extends Table {
  IntColumn get id => integer().autoIncrement()();
  TextColumn get content => text()();
  DateTimeColumn get date => dateTime()();
}

@DriftDatabase(tables: [Schedules])
class LocalDatabase extends _$LocalDatabase {
  LocalDatabase() : super(_openConnection());

  @override
  int get schemaVersion => 1;

  Stream<List<Schedule>> watchSchedules(DateTime date) {
    return (select(schedules)
          ..where((tbl) => tbl.date.equals(date)))
        .watch();
  }

  Future<int> createSchedule(SchedulesCompanion data) =>
      into(schedules).insert(data);

  Future<int> removeSchedule(int id) =>
      (delete(schedules)..where((tbl) => tbl.id.equals(id))).go();
}

LazyDatabase _openConnection() {
  return LazyDatabase(() async {
    final dir = await getApplicationDocumentsDirectory();
    final file = File(p.join(dir.path, 'db.sqlite'));
    return NativeDatabase(file);
  });
}
  • Schedules 테이블에 id, content, date 3개의 컬럼
  • watchSchedules는 날짜별 일정 스트림
  • LazyDatabase는 실제 디스크에 파일로 저장

18.3 일정 CRUD 연동

예시:

ElevatedButton(
  onPressed: () {
    final newSchedule = SchedulesCompanion(
      content: Value('새로운 일정'),
      date: Value(_selectedDay!),
    );
    localDatabase.createSchedule(newSchedule);
  },
  child: const Text('일정 추가'),
),

삭제 기능 -> Dismissible을 쓰면 아주 직관적임.

Dismissible(
  key: ValueKey(schedule.id),
  background: Container(color: Colors.red),
  onDismissed: (_) => localDatabase.removeSchedule(schedule.id),
  child: ListTile(title: Text(schedule.content)),
)

19. 서버와 연동하기

19.1 Dio 기반 API 서비스 작성

- api_service.dart

import 'package:dio/dio.dart';

class ApiService {
  final Dio _dio = Dio(BaseOptions(baseUrl: 'https://api.example.com'));

  Future<list> fetchSchedules(DateTime date) async {
    final resp = await _dio.get('/schedules', queryParameters: {
      'date': date.toIso8601String(),
    });
    return (resp.data as List)
        .map((json) => ScheduleModel.fromJson(json))
        .toList();
  }

  Future createSchedule(DateTime date, String content) async {
    final resp = await _dio.post('/schedules', data: {
      'date': date.toIso8601String(),
      'content': content,
    });
    return ScheduleModel.fromJson(resp.data);
  }

  Future deleteSchedule(int id) async {
    await _dio.delete('/schedules/$id');
  }
}
</list

19.2 상태관리 Provider 설계

- ScheduleProvider 클래스 (예)

class ScheduleProvider extends ChangeNotifier {
  final ApiService apiService;
  List<ScheduleModel> schedules = [];

  ScheduleProvider(this.apiService);

  Future<void> load(DateTime date) async {
    schedules = await apiService.fetchSchedules(date);
    notifyListeners();
  }

  Future<void> add(DateTime date, String content) async {
    final newSchedule = await apiService.createSchedule(date, content);
    schedules.add(newSchedule);
    notifyListeners();
  }

  Future<void> remove(int id) async {
    await apiService.deleteSchedule(id);
    schedules.removeWhere((s) => s.id == id);
    notifyListeners();
  }
}
  • notifyListeners()로 UI 즉시 갱신
  • optimistic UI를 구현할 수도 있는데,
    예를 들어 추가할 때 바로 schedules.add 후에 서버 응답을 기다리는 식으로 구성하면 됩니다.

19.3 UI 연결

ChangeNotifierProvider를 이용해 앱에 공급:

ChangeNotifierProvider(
  create: (_) => ScheduleProvider(ApiService()),
  child: MyApp(),
)