일정관리 앱 만들기: 데이터베이스 적용하기

잠만보·2024년 11월 8일

사전 지식

드리프트 플러그인

드리프트 플러그인을 사용하면 직접 SQL을 작성하지 않고도 SQLite를 작성할 수 있다.

테이블을 클래스로 표현하고 쿼리를 다트 언어로 표현하면 드리프트가 자동으로 해당되는 테이블과 쿼리를 생성한다.

쿼리변환 예시

CREATE TABLE product(
  id INTEGER PRIMARY KEY AUTOINCREMENT,
  name VARCHAR NOT NULL,
  quantity INT DEFAULT 0,
  price INT NOT NULL
)

이런 SQL 문법을 아래처럼 바꿔준다.

import 'package:drift/drift.dart';

class Product extends Table {
  IntColumn get id => integer().autoIncrement()();
  TextColumn get name => test()();
  IntColumn get quantity => integer().withDefault(const Constant(0))();
  IntColumn get price => integer().nullable()();
}

Dismissible 위젯

Dismissible 위젯 은 위젯을 밀어서 삭제하는 기능을 제공한다.

밀어서 삭제하기를 구현하고 싶은 위젯을 Dismissible 위젯 으로 감싸고 onDismissedkey 매개변수 를 입력해주면 된다.

Dismissible(
	key: ObjectKey(schedule.id), // 유니크한 키 값
    direction: DismissDirection.endToStart, // 밀기 방향
    onDismissed: (DismissDirection direction){ // 밀기 실행했을 때 실행할 함수
    },
    child: Container(), // 밀어서 삭제하기 구현할 위젯
);

DismissDirecton 값

구현하기

드리프트로 테이블을 생성하고 테이블에 데이터 CRUD 기능을 만들것이다.

이후 이전에 구현해놨던 UI와 연동해서 로컬 데이터베이스를 사용하는 일정 앱을 완성하겠다.

모델 구현하기

테이블은 드리프트 패키지의 Table 클래스를 상속하면 선언할 수 있다.

그다음 자식 클래스에 열로 정의하고 싶은 값들을 게터로 선언해주면 된다.

import 'package:drift/drift.dart';

class Schedules extends Table {
  IntColumn get id => integer().autoIncrement()(); // PK, 정수 열
  TextColumn get content => text()();  // 내용, 글자 열
  DateTimeColumn get date => dateTime()(); // 일정 날짜, 날짜 열
  IntColumn get startTime => integer()(); // 시작 시간
  IntColumn get endTime => integer()(); // 종료 시간
}

테이블 관련 코드 생성하기

드리프트에 생성한 Schedules 테이블을 등록해주면 드리프트가 자동을 테이블과 관련된 기능을 코드로 생성한다.

이렇게 작성한 코드를 기반으로 자동으로 코드가 생성되는 걸 코드 생성 이라고 한다.

어떤 플러터 패키지든 코드 생성 을 사용하려면 part 파일 을 추가해 줘야 한다.

대부분 현재 파일 이름에 .g.dart 를 추가하는 형식이다.

import 'package:calendar_scheduler/model/schedule.dart';
import 'package:drift/drift.dart';

part 'drift_database.g.dart';

@DriftDatabase(
  tables: [
    Schedules,
  ],
)

class LocalDatabase extends _$LocalDatabase {}

드리프트 관련 쿼리를 작성할 클래스를 하나 작성하고 이 클래스의 이름 앞에 _$ 을 추가한 부모 클래스를 상속한다.

이 클래스는 현재 존재하지 않지만 코드 생성을 실행하면 생성된다.

코드 작성 이후 터미널을 열어 flutter pub run build_runner build 명령어를 실행해서 코드 생성을 진행한다.

쿼리 구현하기

1. SELECT 기능 구현

드리프트에서 SELECT 기능을 두 가지로 표현할 수 있다.

일회성으로 데이터를 가져오는 get() 함수 와 변화가 있을 때 마다 자동으로 데이터를 불러오는 watch() 함수 이다

get() 은 버튼을 눌렀을 때 처럼 특정 이벤트 발생 상황의 데이터를 가져올 때 사용되고

watch() 는 위젯처럼 데이터가 업데이트 될 때마다 새로운 값을 반영해 주어야 할 때 사용된다.

class LocalDatabase extends _$LocalDatabase {
  Stream<List<Schedule>> watchSchedules(DateTime date) =>
      (select(schedules)..where((tbl) => tbl.date.equals(date))).watch();
}

코드 생성을 통해 생성된 클래스에는 SQL SELECT 문에 해당하는 select() 함수가 주어진다.

select() 함수 가 반환하는 값은 watch() 또는 get() 함수를 실행할 수 있다.

그러나 모든 일정을 다 불러오는게 아니고 특정 날짜에 해당되는 일정만 불러오기 때문where 함수를 통해서 관련 일정을 먼저 필터링 해야 한다.

2. INSERT 기능 구현

마찬가지로 SQL INSERT 에 해당하는 insert() 함수가 제공된다.

특이한 점은 into() 함수 를 먼저 사용해서 어떤 테이블에 데이터를 넣을 지 지정해준 다음 insert() 함수 를 이어서 사용해야 한다.

추가적으로 코드 생성을 실행하면 모든 테이블의 Companion 클래스 가 생성된다.

데이터를 생성할 때는 꼭 생성된 Companion 클래스 를 통해서 값을 넣어줘야 하기 때문에 Schedules 테이블 에 해당되는 SchedulesCompanion 클래스 를 입력받아서 insert() 함수에 전달해준다.

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

3. DELETE 기능 구현

delete() 함수 를 사용해서 구현한다.

delete() 함수 에는 go() 함수 를 실행해줘야 삭제가 완료된다.

특정 ID에 해당되는 값만 삭제해야 하니 매개변수에 id 값을 입력받고 해당 id에 해당하는 일정만 삭제하자.

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

4. schemaVersion 값 지정

드리프트 DB 클래스는 필수로 schemaVersion 값 을 지정해 줘야 한다.

기본적으로 1부터 시작하고 테이블에 변화가 있을 때 마다 1씩 올려줘서 테이블 구조가 변경된다는걸 드리프트에 인지시켜주는 기능이다.

@override
  int get schemaVersion => 1;

5. DB 파일 생성 후 연동하기

드리프트 DB 객체는 부모 생성자에 LazyDatabase 를 필수로 넣어줘야 한다.

LazyDatabase 객체 에는 데이터베이스를 생성할 위치에 대한 정보를 입력해주면 된다.

import 'dart:io';

import 'package:calendar_scheduler/model/schedule.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;

part 'drift_database.g.dart';

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

  Stream<List<Schedule>> watchSchedules(DateTime date) =>
      (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();

  @override
  int get schemaVersion => 1;
}
// ===============================================================================
LazyDatabase _openConnection() {
  return LazyDatabase(() async {
    // DB 파일 저장할 폴더
    final dbFolder = await getApplicationDocumentsDirectory(); // path_provider 패키지에서 제공하는 getApplicationDocumentsDirectory() 함수를 사용하면 현재 앱에 배정되 폴더의 경로 받을 수 있음 
    final file = File(p.join(dbFolder.path, 'db.sqlite')); // 해당 폴더에 db.sqlite 라는 파일을 DB 파일로 사용할 것이다.
    return NativeDatabase(file);
  });
}

드리프트 초기화하기

import 'package:calendar_scheduler/database/drift_database.dart';
import 'package:calendar_scheduler/screen/home_screen.dart';
import 'package:flutter/material.dart';
import 'package:get_it/get_it.dart';
import 'package:intl/date_symbol_data_local.dart';

void main() async{
  WidgetsFlutterBinding.ensureInitialized(); // 플러터 프레임워크가 준비될 때 까지 대기

  await initializeDateFormatting(); // intl 패키지 초기화 (다국어화)
  
  final database = LocalDatabase(); // DB 생성
  
  GetIt.I.registerSingleton<LocalDatabase>(database); // GetIt에 DB 변수 주입

  runApp(
    MaterialApp(
      home: HomeScreen(),
    ),
  );
}

미리 정의해둔 LocalDatabase 클래스인스턴스 화 한다.

get_it 패키지의존성 주입 을 구현하는 플러그인 이다.

GetIt 으로 값을 한 번 등록해 두면 어디서든 처음에 주입한 값(같은 database 변수) 를 GetIt.I 를 통해서 프로젝트 어디에서든 사용할 수 있다.

일정 데이터 생성하기

1. Form 위젯 사용 준비

import 'package:calendar_scheduler/const/colors.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';

class CustomTextField extends StatelessWidget {
  final String label; // 텍스트 필드 제목
  final bool isTime; // 시간 선택하는 텍스트 필드인지 확인여부용
  //====================================================
  final FormFieldSetter<String> onSaved;
  final FormFieldValidator<String> validator;
  //====================================================
  const CustomTextField({
    super.key,
    required this.label,
    required this.isTime,
    //====================================================
    required this.onSaved,
    required this.validator,
    //====================================================
  });

  @override
  Widget build(BuildContext context) {
    return Column(
      // 세로로 텍스트와 텍스트 필드 배치
      crossAxisAlignment: CrossAxisAlignment.start,
      children: [
        // 라벨 텍스트
        Text(
          label,
          style: TextStyle(
            color: PRIMARY_COLOR,
            fontWeight: FontWeight.w600,
          ),
        ),

        Expanded(
          flex: isTime ? 0 : 1,
          child: TextFormField(
          //====================================================
            onSaved: onSaved,  // 폼 저장했을 때 실행할 함수
            validator: validator, // 폼 검증했을 때 실행할 함수
          //====================================================
            cursorColor: Colors.grey,
            // 커서 색상 변경
            maxLines: isTime ? 1 : null,
            // 시간 관련 텍스트 필드가 아니면 한 줄 이상 작성 가능
            expands: !isTime,
            // 시간 관련 텍스트 필드는 공간 최대 차지
            keyboardType:
                isTime ? TextInputType.number : TextInputType.multiline,
            // 시간 관련 텍스트 필드는 기본 숫자 키보드 아니면 일반 글자 키보드 보여주기
            inputFormatters: isTime
                ? [
                    FilteringTextInputFormatter.digitsOnly,
                    // 시간 관련 텍스트 필드는 숫자만 입력하도록 제한
                  ]
                : [],
            decoration: InputDecoration(
              border: InputBorder.none, // 테두리 삭제
              filled: true, // 배경색 저장
              fillColor: Colors.grey[300], // 배경색
              suffixText: isTime ? '시' : null, // 시간 관련 텍스트는 '시' 접미사 추가
            ),
          ),
        ),
      ],
    );
  }
}

validator 매개변수는 Form 위젯의 validate() 함수를 실행하면 실행되는 함수이고

onSaved 매개변수는 Form 위젯의 save() 함수를 실행하면 실행되는 함수이다.

2. ScheduleBottomSheet 에서 Form 위젯 사용

import 'dart:convert';

import 'package:calendar_scheduler/component/custom_text_field.dart';
import 'package:calendar_scheduler/const/colors.dart';
import 'package:flutter/material.dart';

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

  @override
  State<ScheduleBottomSheet> createState() => _ScheduleBottomSheetState();
}

class _ScheduleBottomSheetState extends State<ScheduleBottomSheet> {
  //================================================================
  final GlobalKey<FormState> formKey = GlobalKey(); // Form 키 생성

  int? startTime; // 시작 시간 저장 변수
  int? endTime; // 종료 시간 저장 변수
  String? content; // 일정 내용 저장 변수
  //================================================================
  
  @override
  Widget build(BuildContext context) {
    // 키보드 높이 가져오기
    final bottomInset = MediaQuery.of(context).viewInsets.bottom;

    return Form( // 텍스트 필드를 한번에 관리할 수 있는 폼
        key: formKey, // 폼을 조작할 키 값
        child: SafeArea(
          child: Container(
            height: MediaQuery.of(context).size.height / 2 +
                bottomInset, // 화면 반 높이에 키보드 높이 추가하기
            color: Colors.white,
            child: Padding(
              padding: EdgeInsets.only(
                  left: 8, right: 8, top: 8, bottom: bottomInset),
              child: Column(
                children: [
                  Row(
                    // 시작시간, 종료시간 필드는 가로로 배치
                    children: [
                      Expanded(
                        child: CustomTextField(
                          // 시작 시간 입력 필드
                          label: '시작 시간',
                          isTime: true,
                //================================================================
                          onSaved: (String? val) {
                            startTime = int.parse(val!); // 저장이 시작되면 startTime 변수에 텍스트 필드 값 저장
                          },
                          validator: timeValidator,
                            //================================================================
                        ),
                      ),
                      const SizedBox(width: 16.0),
                      Expanded(
                        child: CustomTextField(
                          // 종료 시간 입력 필드
                          label: '종료 시간',
                          isTime: true,
                          onSaved: (String? val) {
                            endTime = int.parse(val!); // 저장이 시작되면 endTime 변수에 텍스트 필드 값 저장
                          },
                          validator: timeValidator,
                        ),
                      ),
                    ],
                  ),
                  SizedBox(height: 8.0),
                  Expanded(
                    child: CustomTextField(
                      // 내용 입력 필드
                      label: '내용',
                      isTime: false,
                      onSaved: (String? val) {
                        content = val; // 저장이 시작되면 content 변수에 텍스트 필드 값 저장
                      },
                      validator: contentValidator,
                    ),
                  ),
                  SizedBox(
                    width: double.infinity,
                    child: ElevatedButton(
                      // 저장 버튼
                      onPressed: onSavePressed,
                      style: ElevatedButton.styleFrom(
                        backgroundColor: PRIMARY_COLOR,
                      ),
                      child: Text('저장'),
                    ),
                  ),
                ],
              ),
            ),
          ),
        ));
  }

  void onSavePressed() {}

//================================================================
  // 시간 값 검증
  String? timeValidator(String? val) {}

  // 내용 값 검증
  String? contentValidator(String? val) {}
}

3. validator 함수 완성하기

이 함수들의 첫번째 매개변수에는 입력된 값이 제공되며 에로가 있을 경우 해당 메시지를 String 으로 반환하고 에러가 없으면 null을 반환한다.

// 시간 값 검증
  String? timeValidator(String? val) {
    if (val == null) {
      return '값을 입력해 주세요';
    }
    int? number;

    try {
      number = int.parse(val);
    } catch (e) {
      return '숫자를 입력해주세요';
    }

    if (number < 0 || number > 24) {
      return '0시부터 24시 사이를 입력해주세요';
    }
    return null;
  }

  // 내용 값 검증
  String? contentValidator(String? val) {
    if (val == null || val.length == 0) {
      return '값을 입력해 주세요';
    }
    return null;
  }

Form의 validate()와 save() 실행해보기

void onSavePressed() {
    if(formKey.currentState!.validate()){ // 폼 검증하기
      formKey.currentState!.save(); // 폼 저장하기
      
      print(startTime); // 시작 시간 출력
      print(endTime); // 종료 시간 출력
      print(content); // 내용 출력
    }
  }

입력폼에 입력하고 저장을 누르면 내가 입력한 것이 순서대로 출력되는 것을 알 수 있다.

텍스트 필드에 입력된 값들이 변수에 잘 저장되고 있다는 뜻이다.

4. HomeScreen 으로부터 selectedDate 변수 입력받기

LocalDatabase 클래스 에 정의해 놓은 createSchedule() 함수 를 실행하려면 현재 선택된 날짜의 값ScheduleBottomSheet 위젯 에서 갖고 있어야 한다.

schedule_bottom_sheet

class ScheduleBottomSheet extends StatefulWidget {
  final DateTime selectedDate; // 선택한 날짜 상위 위젯에서 입력받기

  const ScheduleBottomSheet({super.key, required this.selectedDate});

  @override
  State<ScheduleBottomSheet> createState() => _ScheduleBottomSheetState();
}
.
.
.

HomeScreen

@override
  Widget build(BuildContext context) {
    return Scaffold(
      floatingActionButton: FloatingActionButton(
        // 새 일정 버튼
        backgroundColor: PRIMARY_COLOR,

        // 추가 버튼 눌렀을 때 동작할 콜백
        onPressed: () {
          showModalBottomSheet(
            // BottomSheet 열기
            context: context,
            isDismissible: true, // 배경 탭했을 때 BottomSheet 닫기
            builder: (_) => ScheduleBottomSheet(
              selectedDate: selectedDate, // 선택된 날짜 selectedDate 넘겨주기
            ),
            isScrollControlled: true,// 할 일 입력창 렌더링
          );
        },

5. 저장 버튼 눌렀을 때 DB에 일정 저장하기

GetIt 을 통해 미리 생성해둔 LocalDatabase 인스턴스를 가져온 후 createSchedule() 함수를 실행하겠다.

import 'dart:convert';

import 'package:calendar_scheduler/component/custom_text_field.dart';
import 'package:calendar_scheduler/const/colors.dart';
import 'package:calendar_scheduler/database/drift_database.dart';

import 'package:drift/drift.dart' hide Column; // material.dart 패키지의 Column 클래스와 중복되니 드리프트에서는 숨기기
import 'package:flutter/material.dart';
import 'package:get_it/get_it.dart';


.
.
.

  void onSavePressed() async {
    if (formKey.currentState!.validate()) {
      // 폼 검증하기
      formKey.currentState!.save(); // 폼 저장하기
      
      await GetIt.I<LocalDatabase>().createSchedule( // 일정 생성하기
        SchedulesCompanion(
          startTime: Value(startTime!),
          endTime: Value(endTime!),
          content: Value(content!),
          date: Value(widget.selectedDate),
        ),
      );
      Navigator.of(context).pop(); // 일정 생성 후 화면 뒤로가기
    }
  }

.
.
.

}

일정 데이터 생성 최종 테스트

저장 버튼 클릭하면 ScheduleBottomSheet 위젯이 닫힌다.

일정 데이터 읽기

미리 생성해둔 LocalDatabase 클래스watchSchedules() 함수 를 사용해서 달력에서 선택한 날짜에 해당되는 일정들을 불러와 화면에 반영하겠다.

@override
  Widget build(BuildContext context) {
    return Scaffold(
      floatingActionButton: FloatingActionButton(
        // 새 일정 버튼
        backgroundColor: PRIMARY_COLOR,

        // 추가 버튼 눌렀을 때 동작할 콜백
        onPressed: () {
          showModalBottomSheet(
            // BottomSheet 열기
            context: context,
            isDismissible: true, // 배경 탭했을 때 BottomSheet 닫기
            builder: (_) => ScheduleBottomSheet(
              selectedDate: selectedDate, // 선택된 날짜 selectedDate 넘겨주기
            ),
            isScrollControlled: true, // 할 일 입력창 렌더링
          );
        },
        child: Icon(
          // '+' 모양 아이콘
          Icons.add,
        ),
      ),
      body: SafeArea(
        // 시스템 UI 피해서 UI 구현
        child: Column(
          // 달력과 리스트를 세로로 배치
          children: [
            // 달력 위젯
            MainCalendar(
              selectedDate: selectedDate, // 선택된 날짜 전달하기
              onDaySelected: onDaySelected, // 날짜가 선택됬을 떄 실행할 함수
            ),
            SizedBox(
              height: 8.0,
            ),

            // 오늘 날짜와 할일 개수 보여주는 위젯
            TodayBanner(
              selectedDate: selectedDate,
              count: 0,
            ),
            SizedBox(
              height: 8.0,
            ),
            //==============================================================================
            Expanded( // 남는 공간 모두 차지
              child: StreamBuilder<List<Schedule>>( // 일정 정보가 Stream 으로 제공되기 때문에 StreamBuilder 사용
                stream: GetIt.I<LocalDatabase>().watchSchedules(selectedDate),
                builder: (context, snapshot) {
                  if (!snapshot.hasData) { // 데이터가 없을 때
                    return Container(); // 빈 컨테이너 리턴
                  }
                  return ListView.builder( // 화면에 보이는 값들만 렌더링하는 리스트
                    itemCount: snapshot.data!.length, // 리스트에 입력할 값들의 총 개수
                    itemBuilder: (context, index) { // 현재 index 에 해당되는 일정
                      final schedule = snapshot.data![index];
                      return Padding(
                        padding: const EdgeInsets.only(
                            bottom: 8.0, left: 8.0, right: 8.0),
                        child: ScheduleCard(
                          startTime: schedule.startTime,
                          endTime: schedule.endTime,
                          content: schedule.content,
                        ),
                      );
                    },
                  );
                },
              ),
            ),
            // 할일 위젯
          ],
        ),
      ),
    );
  }

전에 만들어 뒀던 일정이 보이는 것을 확인할 수 있다.

다른 날짜로 바꾸면 일정이 보이지 않는다.

일정 데이터 삭제하기

@override
  Widget build(BuildContext context) {
    return Scaffold(
      floatingActionButton: FloatingActionButton(
        // 새 일정 버튼
        backgroundColor: PRIMARY_COLOR,

        // 추가 버튼 눌렀을 때 동작할 콜백
        onPressed: () {
          showModalBottomSheet(
            // BottomSheet 열기
            context: context,
            isDismissible: true, // 배경 탭했을 때 BottomSheet 닫기
            builder: (_) => ScheduleBottomSheet(
              selectedDate: selectedDate, // 선택된 날짜 selectedDate 넘겨주기
            ),
            isScrollControlled: true, // 할 일 입력창 렌더링
          );
        },
        child: Icon(
          // '+' 모양 아이콘
          Icons.add,
        ),
      ),
      body: SafeArea(
        // 시스템 UI 피해서 UI 구현
        child: Column(
          // 달력과 리스트를 세로로 배치
          children: [
            // 달력 위젯
            MainCalendar(
              selectedDate: selectedDate, // 선택된 날짜 전달하기
              onDaySelected: onDaySelected, // 날짜가 선택됬을 떄 실행할 함수
            ),
            SizedBox(
              height: 8.0,
            ),

            // 오늘 날짜와 할일 개수 보여주는 위젯
            TodayBanner(
              selectedDate: selectedDate,
              count: 0,
            ),
            SizedBox(
              height: 8.0,
            ),
            Expanded(
              // 남는 공간 모두 차지
              child: StreamBuilder<List<Schedule>>(
                // 일정 정보가 Stream 으로 제공되기 때문에 StreamBuilder 사용
                stream: GetIt.I<LocalDatabase>().watchSchedules(selectedDate),
                builder: (context, snapshot) {
                  if (!snapshot.hasData) {
                    // 데이터가 없을 때
                    return Container(); // 빈 컨테이너 리턴
                  }
                  return ListView.builder(
                    // 화면에 보이는 값들만 렌더링하는 리스트
                    itemCount: snapshot.data!.length, // 리스트에 입력할 값들의 총 개수
                    itemBuilder: (context, index) {
                      // 현재 index 에 해당되는 일정
                      final schedule = snapshot.data![index];
                      // =========================================================================
                      return Dismissible(
                        key: ObjectKey(schedule.id), // 생성한 스케줄의 고유한 키 값
                        direction: DismissDirection.startToEnd, // 밀기 방향 (왼->오)
                        onDismissed: (DismissDirection direction) { // 밀기 했을 때 실행할 함수
                          GetIt.I<LocalDatabase>().removeSchedule(schedule.id); // 스케줄id 로 해당되는 스케줄 삭제
                        },
                        child: Padding(
                          padding: const EdgeInsets.only(
                              bottom: 8.0, left: 8.0, right: 8.0),
                          child: ScheduleCard(
                            startTime: schedule.startTime,
                            endTime: schedule.endTime,
                            content: schedule.content,
                          ),
                        ),
                      );
                    },
                  );
                },
              ),
            ),
            // 할일 위젯
          ],
        ),
      ),
    );
  }

생성한 일정을 -> 방향으로 밀면 지워지는 걸 볼 수 있다.

일정 개수 반영하기

이제 한 날짜에 몇개의 일정이 있는지 확인할 수 있도록 하겠다.

import 'package:calendar_scheduler/component/main_calendar.dart';
import 'package:calendar_scheduler/component/schedule_bottom_sheet.dart';
import 'package:calendar_scheduler/component/schedule_card.dart';
import 'package:calendar_scheduler/component/today_banner.dart';
import 'package:calendar_scheduler/const/colors.dart';
import 'package:calendar_scheduler/database/drift_database.dart';
import 'package:flutter/material.dart';
import 'package:get_it/get_it.dart';

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

  @override
  State<HomeScreen> createState() => _HomeScreenState();
}

class _HomeScreenState extends State<HomeScreen> {
  DateTime selectedDate = DateTime.utc(
    // 선택된 날짜를 관리할 변수
    DateTime.now().year,
    DateTime.now().month,
    DateTime.now().day,
  );

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      floatingActionButton: FloatingActionButton(
        // 새 일정 버튼
        backgroundColor: PRIMARY_COLOR,

        // 추가 버튼 눌렀을 때 동작할 콜백
        onPressed: () {
          showModalBottomSheet(
            // BottomSheet 열기
            context: context,
            isDismissible: true, // 배경 탭했을 때 BottomSheet 닫기
            builder: (_) => ScheduleBottomSheet(
              selectedDate: selectedDate, // 선택된 날짜 selectedDate 넘겨주기
            ),
            isScrollControlled: true, // 할 일 입력창 렌더링
          );
        },
        child: Icon(
          // '+' 모양 아이콘
          Icons.add,
        ),
      ),
      body: SafeArea(
        // 시스템 UI 피해서 UI 구현
        child: Column(
          // 달력과 리스트를 세로로 배치
          children: [
            // 달력 위젯
            MainCalendar(
              selectedDate: selectedDate, // 선택된 날짜 전달하기
              onDaySelected: onDaySelected, // 날짜가 선택됬을 떄 실행할 함수
            ),
            SizedBox(
              height: 8.0,
            ),
            //===========================================================================
            StreamBuilder<List<Schedule>>( // 일정 Stream 으로 받아오기
              stream: GetIt.I<LocalDatabase>().watchSchedules(selectedDate),
              builder: (context, snapshot) {
                return TodayBanner(
                  selectedDate: selectedDate,
                  count: snapshot.data?.length ?? 0, // 일정 개수 입력해주기
                );
              },
            ),
			//===========================================================================            
            // 오늘 날짜와 할일 개수 보여주는 위젯
            SizedBox(
              height: 8.0,
            ),
            Expanded(
              // 남는 공간 모두 차지
              child: StreamBuilder<List<Schedule>>(
                // 일정 정보가 Stream 으로 제공되기 때문에 StreamBuilder 사용
                stream: GetIt.I<LocalDatabase>().watchSchedules(selectedDate),
                builder: (context, snapshot) {
                  if (!snapshot.hasData) {
                    // 데이터가 없을 때
                    return Container(); // 빈 컨테이너 리턴
                  }
                  return ListView.builder(
                    // 화면에 보이는 값들만 렌더링하는 리스트
                    itemCount: snapshot.data!.length, // 리스트에 입력할 값들의 총 개수
                    itemBuilder: (context, index) {
                      // 현재 index 에 해당되는 일정
                      final schedule = snapshot.data![index];
                      return Dismissible(
                        key: ObjectKey(schedule.id), // 생성한 스케줄의 고유한 키 값
                        direction: DismissDirection.startToEnd, // 밀기 방향 (왼->오)
                        onDismissed: (DismissDirection direction) {
                          // 밀기 했을 때 실행할 함수
                          GetIt.I<LocalDatabase>().removeSchedule(
                              schedule.id); // 스케줄id 로 해당되는 스케줄 삭제
                        },
                        child: Padding(
                          padding: const EdgeInsets.only(
                              bottom: 8.0, left: 8.0, right: 8.0),
                          child: ScheduleCard(
                            startTime: schedule.startTime,
                            endTime: schedule.endTime,
                            content: schedule.content,
                          ),
                        ),
                      );
                    },
                  );
                },
              ),
            ),
            // 할일 위젯
          ],
        ),
      ),
    );
  }

일정을 생성하면 일정 개수가 늘어나는것을 볼 수 있고

삭제시 줄어드는 것도 볼 수 있다.

profile
아프지 말자 - (잘못된 정보, 수정 사항 있으면 언제든지 알려주시면 감사하겠습니다!)

0개의 댓글