이름 | 기능 |
---|---|
Authentication | 소셜, 이메일, 핸드폰 인증 등의 인증 기능 연동 |
App Check | 보안 기능으로 허가되지 않은 클라이언트가 API 요청을 해 리소스를 낭비하는 걸 막을 수 있다. |
Firestore | 실시간으로 클라이언트와 서버의 데이터를 연동할 수 있고 강력한 쿼리기능을 제공하는 NoSQL DB |
Realtime Database | Firestore와 비슷한 NoSQL로 빠른 속도와 효율에 초점이 맞춰져 있다. |
Sorage | 이미지, 오디오, 비디오 등 사용자 생성하는 콘텐츠를 저장할 수 있는 저장소 |
Hosting | 웹 앱, 정적 및 동적 콘텐츠, 마이크로서비스를 빠르고 안정적으로 호스팅 할 수 있다. |
Functions | 파이어베이스 또는 HTTPS 요청에 의해 서버 코드를 실행할 수 있는 기능, 트래픽에 따라 필요한만큼 자동으로 확장되기 때문에 인프라 관리가 필요 없다. |
Machine Learning | 간단한 몇 줄의 코드만으로 텍스트 인식, 이미지 라벨링, 물체 감지 및 추적, 얼굴 감지 및 윤곽 추적 등 머신러닝 기능 사용 |
Remote Config | 앱의 동작 모양과 기능을 앱을 새로 배포하지 않고도 변경하도록 편의성 기능 제공 |
Crashlytics | 앱에 충돌이 있을 경우 정확한 문제와 로그를 파악할 수 있는 기능 |
Performance | 앱 성능을 모니터링 할 수 있는 기능 |
Test Lab | 구글 데이터 센터에 실행되고 있는 여러 실제 프로덕션 기기를 사용해 앱을 테스트할 수 있다. |
App Distribution | 앱을 더 빠르고 쉽게 배로할 수 있다. |
Analytics | 앱의 트래픽과 사용자의 활동 등을 모니터링하고 분석할 수 있다. |
Messaging | 푸시 알림을 쉽게 설정할 수 있다. |
dependencies:
cloud_firestore: ^4.14.0
firebase_core: ^2.24.2
minSdkVersion을 19로 설정
// android/app/build.gradle
defaultConfig {
minSdkVersion 19
}
curl -sL https://firebase.tools | bash
firebase login
dart pub global activate flutterfire_cli
아래와 같은 경고 문구가 보이면 다시 아래 작업 후 다시 진행A web search for "configure windows path" will show you how
C:\Users\{윈도우 사용자이름}\AppData\Local\Pub\Cache\bin
을 추가하고 Visualstudio Code를 재실행 후 다시 FlutterFire CLI를 설치$HOME/.pub-cache/bin
을 추가firebase-tools 설치
npm install -g firebase-tools
프로젝트 연결
```bash
flutterfire configure -p <프로젝트 ID>
```
프로젝트가 실행이 완료되면 lib/firebase_options.dart파일이 생성된다.
파이어베이스 콘솔에서 프로젝트 설정을 선택
기본 GCP 리소스 위치를 가까운 지역으로 선택
왼쪽 메뉴에서 빌드 클릭 → Firestore Database 클릭
데이터베이스 만들기 버튼 클릭
위치 선택 (asia-northeast3) → 테스트모드에서 시작 선택 후 만들기
아래와 같은 에러가 나왔지만 Google Clout 콘솔로 이동 하니 정상적으로 생성이 됬다.
💡 Tips: 여기서 생성이 됬더라도 데이터베이스가 Datastore모드로 생성이 됬기때문에 정상적으로 사용이 불가능하다 따라서 Native모드로 변경하면 정상적으로 사용할 수 있다.
// lib/main.dart
import 'package:calendar_scheduler_firebase/firebase_options.dart';
import 'package:calendar_scheduler_firebase/screen/home_screen.dart';
import 'package:firebase_core/firebase_core.dart';
import 'package:flutter/material.dart';
import 'package:intl/date_symbol_data_local.dart';
void main() async {
WidgetsFlutterBinding.ensureInitialized();
// 파이어베이스 프로젝트 설정 함수
await Firebase.initializeApp(options: DefaultFirebaseOptions.currentPlatform);
// intl 패키지 초기화(다국어화)
await initializeDateFormatting("ko_kr", null);
// Provider 하위 위젯에 제공하기
runApp(MaterialApp(debugShowCheckedModeBanner: false, home: HomeScreen()));
}
// lib/component/schedule_bottom_sheet.dart
import 'package:calendar_scheduler_firebase/component/custom_text_field.dart';
import 'package:calendar_scheduler_firebase/const/colors.dart';
import 'package:calendar_scheduler_firebase/model/schedule_model.dart';
import 'package:cloud_firestore/cloud_firestore.dart';
import 'package:flutter/material.dart';
import 'package:uuid/uuid.dart';
class ScheduleBottomSheet extends StatefulWidget {
// 선택된 날짜 상위 위젯에서 입력받기
final DateTime selectedDate;
const ScheduleBottomSheet({required this.selectedDate, super.key});
State<ScheduleBottomSheet> createState() => _ScheduleBottomSheetState();
}
class _ScheduleBottomSheetState extends State<ScheduleBottomSheet> {
final GlobalKey<FormState> formKey = GlobalKey();
int? startTime; // 시작 시간 저장 변수
int? endTime; // 종료 시간 저장 변수
String? content; // 일정 내용 저장 변수
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 변수에 텍스트 필드 값 저장
startTime = int.parse(val!);
},
validator: timeValidator,
)),
SizedBox(
width: 16.0,
),
Expanded(
child: CustomTextField(
label: "종료 시간",
isTime: true,
onSaved: (String? val) {
// 저장이 실행되면 endTime 변수에 텍스트 필드 값 저장
endTime = int.parse(val!);
},
validator: timeValidator,
)),
],
),
const SizedBox(
height: 8.0,
),
Expanded(
child: CustomTextField(
label: "내용",
isTime: false,
onSaved: (String? val) {
// 저장이 실행되면 content 변수에 텍스트 필드 값 저장
content = val;
},
validator: contentValidator,
)),
SizedBox(
width: double.infinity,
child: ElevatedButton(
onPressed: () => onSavePressed(context),
style: ElevatedButton.styleFrom(
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(1)),
backgroundColor: PRIMARY_COLOR,
),
child: const Text(
"저장",
style: TextStyle(color: Colors.white),
),
),
)
],
),
),
),
),
);
}
void onSavePressed(BuildContext context) async {
if (formKey.currentState!.validate()) {
formKey.currentState!.save();
// 스케줄 모델 생성하기
final schedule = ScheduleModel(
id: Uuid().v4(),
content: content!,
date: widget.selectedDate,
startTime: startTime!,
endTime: endTime!);
// 스케줄 모델 파이어스토어에 삽입하기
await FirebaseFirestore.instance
.collection('schedule')
.doc(schedule.id)
.set(schedule.toJson());
// 일정 생성 후 화면 뒤로가기
Navigator.of(context).pop();
}
}
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;
}
}
// lib/screen/home_screen.dart
import 'package:calendar_scheduler_firebase/component/main_calendar.dart';
import 'package:calendar_scheduler_firebase/component/schedule_bottom_sheet.dart';
import 'package:calendar_scheduler_firebase/component/schedule_card.dart';
import 'package:calendar_scheduler_firebase/component/today_banner.dart';
import 'package:calendar_scheduler_firebase/const/colors.dart';
import 'package:calendar_scheduler_firebase/model/schedule_model.dart';
import 'package:cloud_firestore/cloud_firestore.dart';
import 'package:flutter/material.dart';
class HomeScreen extends StatefulWidget {
State<HomeScreen> createState() => _HomeScreenState();
}
class _HomeScreenState extends State<HomeScreen> {
DateTime selectedDate = DateTime.utc(
// ➋ 선택된 날짜를 관리할 변수
DateTime.now().year,
DateTime.now().month,
DateTime.now().day,
);
Widget build(BuildContext context) {
return Scaffold(
floatingActionButton: FloatingActionButton(
shape: const RoundedRectangleBorder(
borderRadius: BorderRadius.all(Radius.circular(30.0))),
backgroundColor: PRIMARY_COLOR,
onPressed: () {
showModalBottomSheet(
context: context,
isDismissible: true, // 배경 탭 했을때 화면닫기
builder: (_) => ScheduleBottomSheet(
selectedDate: selectedDate,
),
// BottomSheet의 높이를 화면의 최대 높이로 정의하고 스크롤 가능하게 변경
isScrollControlled: true);
},
child: const Icon(Icons.add),
),
body: SafeArea(
child: Column(
children: [
MainCalendar(
selectedDate: selectedDate,
onDaySelected: (selectedDate, focusedDate) =>
onDaySelected(selectedDate, focusedDate, context),
),
const SizedBox(
height: 8.0,
),
StreamBuilder<QuerySnapshot>(
// ListView에 적용했던 같은 쿼리
stream: FirebaseFirestore.instance
.collection(
'schedule',
)
.where("date",
isEqualTo:
"${selectedDate.year}${selectedDate.month.toString().padLeft(2, "0")}${selectedDate.day.toString().padLeft(2, "0")}")
.snapshots(),
builder: (context, snapshot) {
return TodayBanner(
selectedDate: selectedDate,
// ➊ 개수 가져오기
count: snapshot.data?.docs.length ?? 0,
);
},
),
const SizedBox(
height: 8.0,
),
Expanded(
child: StreamBuilder<QuerySnapshot>(
// ➊ 파이어스토어로부터 일정 정보 받아오기
stream: FirebaseFirestore.instance
.collection(
'schedule',
)
.where("date",
isEqualTo:
"${selectedDate.year}${selectedDate.month.toString().padLeft(2, "0")}${selectedDate.day.toString().padLeft(2, "0")}")
.snapshots(),
builder: (context, snapshot) {
// Stream을 가져오는 동안 에러가 났을 때 보여줄 화면
if (snapshot.hasError) {
return Center(
child: Text('일정 정보를 가져오지 못했습니다.'),
);
}
// 로딩 중일 때 보여줄 화면
if (snapshot.connectionState == ConnectionState.waiting) {
return Container();
}
final schedules = snapshot.data!.docs
.map((e) => ScheduleModel.formJson(
json: (e.data() as Map<String, dynamic>)))
.toList();
return ListView.builder(
itemCount: schedules.length,
itemBuilder: (context, index) {
final schedule = schedules[index];
return Dismissible(
key: ObjectKey(schedule.id),
direction: DismissDirection.startToEnd,
onDismissed: (DismissDirection direction) {
FirebaseFirestore.instance
.collection('schedule')
.doc(schedule.id)
.delete();
},
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,
),
),
);
},
);
},
),
),
],
)),
);
}
void onDaySelected(
DateTime selectedDate, DateTime focusedDate, BuildContext context) {
setState(() => this.selectedDate = selectedDate);
}
}