diff --git a/assets/images/ic_splash.svg b/assets/images/ic_splash.svg new file mode 100644 index 0000000..d614fc8 --- /dev/null +++ b/assets/images/ic_splash.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/ios/Podfile.lock b/ios/Podfile.lock index 238dcf3..bafc646 100644 --- a/ios/Podfile.lock +++ b/ios/Podfile.lock @@ -32,4 +32,4 @@ SPEC CHECKSUMS: PODFILE CHECKSUM: 819463e6a0290f5a72f145ba7cde16e8b6ef0796 -COCOAPODS: 1.16.2 +COCOAPODS: 1.15.2 diff --git a/lib/component/bottom_navigation_bar.dart b/lib/component/bottom_navigation_bar.dart index 8c582d8..8c3ba06 100644 --- a/lib/component/bottom_navigation_bar.dart +++ b/lib/component/bottom_navigation_bar.dart @@ -48,13 +48,13 @@ class EasierDodamBottomNavigationBar extends StatelessWidget { context, icon: 'assets/images/ic_moon_plus.svg', label: '심야자습', - index: 3, + index: 2, ), _buildNavItem( context, icon: 'assets/images/ic_gear.svg', label: '설정', - index: 2, + index: 3, ), ], ), @@ -75,17 +75,14 @@ class EasierDodamBottomNavigationBar extends StatelessWidget { icon, height: isSelected ? 30 : 24, width: isSelected ? 30 : 24, - colorFilter: ColorFilter.mode( - isSelected ? Colors.blue : Colors.grey, - BlendMode.srcIn, - ), + color: isSelected ? Colors.blue : Colors.grey, ), const SizedBox(height: 4), Text( label, style: TextStyle( color: isSelected ? Colors.blue : Colors.grey, - fontSize: isSelected ? 13 : 12, + fontSize: isSelected ? 14 : 12, fontWeight: isSelected ? FontWeight.bold : FontWeight.normal, ), ), diff --git a/lib/component/modal_bottom_sheet_container.dart b/lib/component/modal_bottom_sheet_container.dart index 90b9cf6..5ff9b80 100644 --- a/lib/component/modal_bottom_sheet_container.dart +++ b/lib/component/modal_bottom_sheet_container.dart @@ -1,5 +1,4 @@ import 'package:easier_dodam/component/theme/color.dart'; -import 'package:easier_dodam/component/theme/style.dart'; import 'package:flutter/material.dart'; class ModalBottomSheetContainer extends StatelessWidget { diff --git a/lib/feature/logout/logout.dart b/lib/feature/logout/logout.dart index e9de9e9..58e73cc 100644 --- a/lib/feature/logout/logout.dart +++ b/lib/feature/logout/logout.dart @@ -10,6 +10,7 @@ import '../../feature/out/out_navigation.dart'; import '../../feature/out_sleeping/out_sleeping_navigation.dart'; import '../logout/logout_viewmodel.dart'; import 'item/setting_item.dart'; +import 'logout_navigation.dart'; class SettingScreen extends StatefulWidget { const SettingScreen({super.key}); @@ -33,9 +34,10 @@ class _SettingScreenState extends State { Navigator.pushReplacementNamed(context, outRoute); break; case 2: + Navigator.pushReplacementNamed(context, nightStudyRoute); break; case 3: - Navigator.pushReplacementNamed(context, nightStudyRoute); + Navigator.pushReplacementNamed(context, logoutRoute); break; } }); diff --git a/lib/feature/night_study/item/night_study_item.dart b/lib/feature/night_study/item/night_study_item.dart new file mode 100644 index 0000000..c411c8e --- /dev/null +++ b/lib/feature/night_study/item/night_study_item.dart @@ -0,0 +1,184 @@ +import 'package:flutter/cupertino.dart'; +import 'package:flutter/material.dart'; + +import '../../../component/theme/color.dart'; +import '../../../component/theme/style.dart'; +import '../../../utiles/utile.dart'; + +enum TagType { PENDING, APPROVE, REJECT } +enum PlaceType { PROGRAMMING_1, PROGRAMMING_2, PROGRAMMING_3 } + +extension PlaceTypeExtension on PlaceType { + String get name { + switch (this) { + case PlaceType.PROGRAMMING_1: + return "프로그래밍1실"; + case PlaceType.PROGRAMMING_2: + return "프로그래밍2실"; + case PlaceType.PROGRAMMING_3: + return "프로그래밍3실"; + } + } +} + + class NightStudyItem extends StatelessWidget { + + final TagType tagType; + final Function() onClickTrash; + final String? rejectReason; + final DateTime startAt; + final DateTime endAt; + + const NightStudyItem({ + super.key, + required this.tagType, + required this.onClickTrash, + this.rejectReason, + required this.startAt, + required this.endAt, + }); + + @override + Widget build(BuildContext context) { + + final hour = dateDifferenceInDays(DateTime.now(), endAt) ~/ 60; + final minute = dateDifferenceInDays(DateTime.now(), endAt) % 60; + + return Container( + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.all(Radius.circular(10)), + boxShadow: [ + BoxShadow( + color: Colors.black.withOpacity(0.08), + spreadRadius: 0, + blurRadius: 4, + offset: const Offset(0, 4), // changes position of shadow + ), + ], + ), + child: Padding( + padding: const EdgeInsets.symmetric( + vertical: 8, + horizontal: 12, + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.start, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Container( + decoration: BoxDecoration( + color: switch (tagType) { + TagType.PENDING => EasierDodamColors.gray600, + TagType.APPROVE => EasierDodamColors.primary300, + TagType.REJECT => EasierDodamColors.staticRed + }, + borderRadius: BorderRadius.circular(12), + ), + child: Padding( + padding: const EdgeInsets.symmetric( + vertical: 4.0, + horizontal: 10.0, + ), + child: Text( + switch (tagType) { + TagType.PENDING => "대기중", + TagType.APPROVE => "수락됨", + TagType.REJECT => "거절됨", + }, + style: EasierDodamStyles.label1.copyWith( + fontSize: 12.0, + color: EasierDodamColors.staticWhite, + ), + ), + ), + ), + Material( + color: EasierDodamColors.staticWhite, + child: InkWell( + onTap: onClickTrash, + child: SizedBox( + width: 24, + height: 24, + child: Image.asset("assets/images/ic_trash.png"), + ), + ), + ) + ], + ), + const SizedBox( + height: 12, + ), + Row( + crossAxisAlignment: CrossAxisAlignment.end, + children: [ + Text( + hour > 0 ? "$hour시간" : "$minute분", + style: EasierDodamStyles.label2, + ), + SizedBox( + width: 2, + ), + Text( + "남음", + style: EasierDodamStyles.label2.copyWith( + fontSize: 12.0, + ), + ) + ], + ), + const SizedBox( + height: 12, + ), + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Row( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Text( + "시작", + style: EasierDodamStyles.label2.copyWith( + fontSize: 12.0, + ), + ), + const SizedBox( + width: 2, + ), + Text( + "${startAt.year}-${startAt.month}-${startAt.day}일", + style: EasierDodamStyles.label2, + ), + ], + ), + Row( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Text( + "복귀", + style: EasierDodamStyles.label2.copyWith( + fontSize: 12.0, + ), + ), + const SizedBox( + width: 2, + ), + Text( + "${endAt.year}-${endAt.month}-${endAt.day}일", + style: EasierDodamStyles.label2, + ), + ], + ), + ], + ), + ], + ), + ), + ); + } +} diff --git a/lib/feature/night_study/item/night_study_preset_item.dart b/lib/feature/night_study/item/night_study_preset_item.dart new file mode 100644 index 0000000..c3f5808 --- /dev/null +++ b/lib/feature/night_study/item/night_study_preset_item.dart @@ -0,0 +1,111 @@ +import 'package:easier_dodam/feature/night_study/item/night_study_item.dart'; +import 'package:flutter/material.dart'; + +import '../../../component/theme/color.dart'; +import '../../../component/theme/style.dart'; + +class NightStudyPresetItem extends StatelessWidget { + final String presetTitle; + final PlaceType place; + final String content; + final bool doNeedPhone; + final String phoneReason; + final String startDate; + final String endDate; + final Function() onTrashClick; + final Function() onClickCreate; + + const NightStudyPresetItem({ + super.key, + required this.presetTitle, + required this.place, + required this.content, + required this.doNeedPhone, + required this.phoneReason, + required this.startDate, + required this.endDate, + required this.onTrashClick, + required this.onClickCreate, + }); + + @override + Widget build(BuildContext context) { + return Material( + color: EasierDodamColors.staticWhite, + child: InkWell( + onTap: onClickCreate, + child: Padding( + padding: EdgeInsets.all(8.0), + child: Row( + mainAxisAlignment: MainAxisAlignment.start, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + presetTitle, + style: EasierDodamStyles.body2 + .copyWith(color: EasierDodamColors.gray700), + ), + SizedBox( + height: 4, + ), + Text( + "사유 : $context", + style: EasierDodamStyles.body2 + .copyWith(color: EasierDodamColors.gray600), + ), + SizedBox( + height: 4, + ), + Row( + children: [ + Text( + "시작", + style: + EasierDodamStyles.body2.copyWith(fontSize: 12.0), + ), + Text( + " $startDate", + style: + EasierDodamStyles.body2.copyWith(fontSize: 14.0), + ), + Expanded(child: SizedBox()), + Text( + "종료", + style: + EasierDodamStyles.body2.copyWith(fontSize: 12.0), + ), + Text( + " $endDate", + style: + EasierDodamStyles.body2.copyWith(fontSize: 14.0), + ), + ], + ) + ], + ), + ), + SizedBox( + width: 4, + ), + SizedBox( + width: 24, + height: 24, + child: InkWell( + onTap: onTrashClick, + child: Image.asset( + "assets/images/ic_trash.png", + color: EasierDodamColors.gray700, + ), + ), + ) + ], + ), + ), + ), + ); + } +} \ No newline at end of file diff --git a/lib/feature/night_study/night_study.dart b/lib/feature/night_study/night_study.dart index bbd6f56..b93f477 100644 --- a/lib/feature/night_study/night_study.dart +++ b/lib/feature/night_study/night_study.dart @@ -1,11 +1,21 @@ import 'package:easier_dodam/component/appbar.dart'; import 'package:easier_dodam/component/bottom_navigation_bar.dart'; +import 'package:easier_dodam/component/modal_bottom_sheet_container.dart'; import 'package:easier_dodam/component/theme/color.dart'; import 'package:easier_dodam/component/theme/style.dart'; -import 'package:easier_dodam/feature/night_study/night_study_viewmodel.dart'; +import 'package:easier_dodam/feature/night_study/item/night_study_item.dart'; +import 'package:easier_dodam/feature/night_study/item/night_study_preset_item.dart'; +import 'package:easier_dodam/remote/night_study/response/night_study_response.dart'; import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; +import '../logout/logout_navigation.dart'; +import '../night_study_create/night_study_create_navigation.dart'; +import '../out/out_navigation.dart'; +import '../out_sleeping/out_sleeping_navigation.dart'; +import 'night_study_navigation.dart'; +import 'night_study_viewmodel.dart'; + class NightStudyScreen extends StatefulWidget { const NightStudyScreen({super.key}); @@ -14,36 +24,30 @@ class NightStudyScreen extends StatefulWidget { } class _NightStudyScreenState extends State { - final TextEditingController _reasonTextFieldController = TextEditingController(); + final TextEditingController _reasonTextFieldController = + TextEditingController(); - int _selectedIndex = 0; + int _selectedIndex = 2; void _onItemTapped(int index) { setState(() { _selectedIndex = index; }); - } - void _onPlusClick() { - showDialog( - context: context, - builder: (context) => AlertDialog( - title: Text("심자 추가"), - content: Text("심자를 추가하시겠습니까?"), - actions: [ - TextButton( - onPressed: () => Navigator.of(context).pop(), - child: Text("취소"), - ), - TextButton( - onPressed: () { - Navigator.of(context).pop(); - }, - child: Text("확인"), - ), - ], - ), - ); + switch (index) { + case 0: + Navigator.pushReplacementNamed(context, outSleepingRoute); + break; + case 1: + Navigator.pushReplacementNamed(context, outRoute); + break; + case 2: + Navigator.pushReplacementNamed(context, nightStudyRoute); + break; + case 3: + Navigator.pushReplacementNamed(context, logoutRoute); + break; + } } @override @@ -54,44 +58,294 @@ class _NightStudyScreenState extends State { @override Widget build(BuildContext context) { - return Scaffold( - appBar: PreferredSize( - preferredSize: Size.fromHeight(60), - child: EasierDodamDefaultAppbar( - title: '심자', - onPlusClick: _onPlusClick, + return Consumer( + builder: + (BuildContext context, NightStudyViewmodel viewModel, Widget? child) { + return Scaffold( + appBar: PreferredSize( + preferredSize: Size.fromHeight(60), + child: EasierDodamDefaultAppbar( + title: '심자', + onPlusClick: () { + showModalBottomSheet( + context: context, + builder: (context) { + return ChangeNotifierProvider.value( + value: viewModel, + child: Consumer( + builder: ( + BuildContext context, + NightStudyViewmodel value, + Widget? child, + ) { + return _bottomWidget(context, viewModel); + }, + ), + ); + }, + ); + }, + ), ), - ), - body: SafeArea( - child: ChangeNotifierProvider( - create: (_) => NightStudyViewmodel(), - child: Consumer( - builder: (context, provider, child) { - return Container( + body: SafeArea( + child: RefreshIndicator( + onRefresh: () async { + return viewModel.getMyNightStudies(); + }, + child: Consumer( + builder: (context, provider, child) { + return Container( + color: EasierDodamColors.staticWhite, + margin: const EdgeInsets.symmetric(horizontal: 16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + viewModel.isLoading ? "" : "현재 신청된 심자", + style: EasierDodamStyles.body1, + textAlign: TextAlign.start, + ), + _loading(viewModel.isLoading), + _notExitsNightStudy( + viewModel.nightStudyResponses.isEmpty && + !viewModel.isLoading, + () { + viewModel.getMyNightStudies(); + }, + ), + ..._nightStudyItemsView( + viewModel.nightStudyResponses, + viewModel.isLoading, + (item) => {viewModel.deleteMyOut(item.id)}, + ), + ], + ), + ); + }, + ), + ), + ), + bottomNavigationBar: SafeArea( + child: EasierDodamBottomNavigationBar( + selectedIndex: _selectedIndex, + onItemTapped: _onItemTapped, + ), + ), + ); + }); + } + + Widget _bottomWidget(BuildContext context, NightStudyViewmodel viewModel) { + return ModalBottomSheetContainer( + child: Padding( + padding: const EdgeInsets.symmetric( + horizontal: 16, + ), + child: SingleChildScrollView( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Container( + height: 44, + width: 380, + child: Padding( + padding: const EdgeInsets.symmetric( + horizontal: 8, + ), + child: Text("심자 신청하기", style: EasierDodamStyles.title1), + ), + ), + SizedBox( + height: 8, + ), + ...viewModel.nightStudyEntities + .map( + (data) => NightStudyPresetItem( + presetTitle: data.title, + place: data.place, + content: data.content, + doNeedPhone: data.doNeedPhone, + phoneReason: data.reasonForPhone, + startDate: "${data.startAt} ${data.startAt.minute}", + endDate: "${data.endAt.hour} ${data.endAt.minute}", + onTrashClick: () { + viewModel.removeEntity(data.id ?? 0); + }, + onClickCreate: () async { + Navigator.pop(context); + await viewModel.nightStudy(data); + }, + ), + ) + .toList(), + SizedBox( + height: 8, + ), + Divider( + height: 1, + color: EasierDodamColors.gray600, + ), + SizedBox( + height: 8, + ), + Material( //프리셋 생성 시작 color: EasierDodamColors.staticWhite, - margin: const EdgeInsets.symmetric(horizontal: 16), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - SizedBox(height: 23), - Text( - "현재 신청된 심자", - style: EasierDodamStyles.body1, - textAlign: TextAlign.start, + child: InkWell( + onTap: () { + Navigator.pushReplacementNamed( + context, + nightStudyCreateRoute, + ); + }, + child: Padding( + padding: const EdgeInsets.symmetric( + horizontal: 8, + ), + child: Row( + children: [ + Image.asset( + width: 24, + height: 24, + "assets/images/ic_plus.png", + color: EasierDodamColors.gray700, + ), + SizedBox( + width: 8, + ), + Text( + "새로운 프리셋 만들기", + style: EasierDodamStyles.body2, + ) + ], ), - SizedBox(), - // You can add more content here - ], + ), ), - ); - }, + ), + ], ), ), ), - bottomNavigationBar: EasierDodamBottomNavigationBar( - selectedIndex: _selectedIndex, - onItemTapped: _onItemTapped, - ), ); } -} \ No newline at end of file + + Widget _loading(bool isLoading) { + if (isLoading) { + return Center( + child: Container( + width: 50, + height: 50, + child: CircularProgressIndicator( + backgroundColor: EasierDodamColors.staticWhite, + color: EasierDodamColors.primary300, + ), + ), + ); + } else { + return SizedBox(); + } + } + + Widget _notExitsNightStudy(bool isExit, Function() onClick) { + if (isExit) { + return Container( + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.all(Radius.circular(10)), + boxShadow: [ + BoxShadow( + color: Colors.black.withOpacity(0.08), + spreadRadius: 0, + blurRadius: 4, + offset: const Offset(0, 4), + ), + ], + ), + child: Padding( + padding: const EdgeInsets.symmetric( + vertical: 8, + horizontal: 12, + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.center, + mainAxisAlignment: MainAxisAlignment.start, + children: [ + SizedBox( + width: 40, + height: 40, + child: Image.asset("assets/images/ic_happy.png"), + ), + SizedBox( + height: 12, + ), + Text( + "현재 신청된 심자가 없어요", + style: EasierDodamStyles.label1, + ), + SizedBox( + height: 12, + ), + Material( + color: EasierDodamColors.staticWhite, + child: InkWell( + onTap: onClick, + child: Container( + width: double.infinity, + height: 42, + decoration: BoxDecoration( + border: const Border.fromBorderSide( + BorderSide( + width: 1.0, color: EasierDodamColors.gray500), + ), + borderRadius: BorderRadius.circular(8.0), + ), + child: Center( + child: Text( + "새로고침", + style: EasierDodamStyles.label1, + ), + ), + ), + ), + ) + ], + ), + ), + ); + } else { + return SizedBox(); + } + } + + List _nightStudyItemsView( + List items, + bool isLoading, + Function(NightStudyResponse) onClickTrash, + ) { + if (isLoading) { + return List.empty(); + } + return items + .map((item) => Column( + children: [ + SizedBox( + height: 12, + ), + NightStudyItem( + tagType: switch (item.status) { + Status.ALLOWED => TagType.APPROVE, + Status.PENDING => TagType.PENDING, + Status.REJECTED => TagType.REJECT, + }, + onClickTrash: () { + onClickTrash(item); + }, + startAt: item.startAt, + endAt: item.endAt, + ), + ], + )) + .toList(); + } + +} + diff --git a/lib/feature/night_study/night_study_viewmodel.dart b/lib/feature/night_study/night_study_viewmodel.dart index fed089b..ac3e1cd 100644 --- a/lib/feature/night_study/night_study_viewmodel.dart +++ b/lib/feature/night_study/night_study_viewmodel.dart @@ -1,32 +1,87 @@ -import 'package:easier_dodam/remote/night_study/night_study_data_source.dart'; -import 'package:easier_dodam/remote/night_study/response/night_study_response.dart'; +import 'dart:async'; + +import 'package:easier_dodam/feature/night_study/item/night_study_item.dart'; import 'package:flutter/cupertino.dart'; +import '../../local/database_manager.dart'; +import '../../local/entity/night_study_entity.dart'; import '../../remote/core/base_response.dart'; +import '../../remote/night_study/night_study_data_source.dart'; +import '../../remote/night_study/response/night_study_response.dart'; -class NightStudyViewmodel with ChangeNotifier{ +class NightStudyViewmodel with ChangeNotifier { late NightStudyDataSource _nightStudyDataSource; - String _testState = ""; - String get testState => _testState; + bool _isLoading = false; + bool get isLoading => _isLoading; + + List _nightStudyEntities = List.empty(); + List get nightStudyEntities => _nightStudyEntities; + + List _nightStudyResponses = List.empty(); + List get nightStudyResponses => _nightStudyResponses; - NightStudyViewmodel(){ + StreamSubscription>? _nightStudyStreamSubscription; + + NightStudyViewmodel() { _nightStudyDataSource = NightStudyDataSource(); + getMyNightStudies(); + _getNightStudyEntities(); + } + + void _getNightStudyEntities() async { + final database = await DatabaseManager.getDatabase(); + _nightStudyStreamSubscription + = database.nightStudyDao.findAllEntitiesWithStream().listen((data) { + _nightStudyEntities = data; + notifyListeners(); + }); + } + + void removeEntity(int id) async { + final database = await DatabaseManager.getDatabase(); + await database.nightStudyDao.deleteNightStudyEntityById(id); + notifyListeners(); } - Future nightStudy( - String place, - String content, - bool doNeedPhone, - String reasonForPhone, - String startAt, - String endAt - ) async { - final BaseResponse response = - await _nightStudyDataSource.nightStudy(place, content, doNeedPhone, reasonForPhone, startAt, endAt); + Future getMyNightStudies() async { + setIsLoading(true); + final nightStudies = await _nightStudyDataSource.getMyNightStudies(); + setIsLoading(false); + _nightStudyResponses = nightStudies; notifyListeners(); - return true; } + void deleteMyOut(int id) async { + setIsLoading(true); + await _nightStudyDataSource.deleteMyNightStudy(id); + _nightStudyResponses.removeWhere((data) => data.id == id); + setIsLoading(false); + notifyListeners(); + } + + Future nightStudy(NightStudyEntity nightStudyEntity) async { + await _nightStudyDataSource.postNightStudy( + nightStudyEntity.place, + nightStudyEntity.content, + nightStudyEntity.doNeedPhone, + nightStudyEntity.reasonForPhone, + nightStudyEntity.startAt, + nightStudyEntity.endAt + ); + + getMyNightStudies(); + return true; + } + void setIsLoading(bool isLoading) { + _isLoading = isLoading; + notifyListeners(); + } + + @override + void dispose() { + _nightStudyStreamSubscription?.cancel(); + super.dispose(); + } } \ No newline at end of file diff --git a/lib/feature/night_study_create/item/night_study_create_time_item.dart b/lib/feature/night_study_create/item/night_study_create_time_item.dart new file mode 100644 index 0000000..edeb332 --- /dev/null +++ b/lib/feature/night_study_create/item/night_study_create_time_item.dart @@ -0,0 +1,55 @@ +import 'package:easier_dodam/component/theme/color.dart'; +import 'package:easier_dodam/component/theme/style.dart'; +import 'package:flutter/material.dart'; + +class NightStudyCreateTimeItem extends StatelessWidget { + final String title; + final String buttonText; + final Function() onButtonClick; + + const NightStudyCreateTimeItem( + {super.key, + required this.title, + required this.buttonText, + required this.onButtonClick}); + + @override + Widget build(BuildContext context) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + title, + style: EasierDodamStyles.label1.copyWith( + height: 1.5, + color: EasierDodamColors.gray500, + ), + ), + InkWell( + onTap: onButtonClick, + child: Container( + decoration: BoxDecoration( + border: const Border.fromBorderSide( + BorderSide(width: 1.0, color: EasierDodamColors.gray500), + ), + borderRadius: BorderRadius.circular(10.0), + ), + child: Padding( + padding: const EdgeInsets.symmetric( + vertical: 15, + horizontal: 34.5, + ), + child: Text( + buttonText, + style: EasierDodamStyles.label1.copyWith( + height: 1.5, + color: EasierDodamColors.staticBlack, + ), + ), + ), + ), + ) + ], + ); + } +} diff --git a/lib/feature/night_study_create/night_study_create.dart b/lib/feature/night_study_create/night_study_create.dart new file mode 100644 index 0000000..d2aec2f --- /dev/null +++ b/lib/feature/night_study_create/night_study_create.dart @@ -0,0 +1,224 @@ +import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; + +import '../../../component/preset_appbar.dart'; +import '../../../component/textfield.dart'; +import '../../../component/theme/color.dart'; +import '../../../component/theme/style.dart'; +import '../night_study/item/night_study_item.dart'; +import 'item/night_study_create_time_item.dart'; +import 'night_study_create_viewmodel.dart'; + +class NightStudyCreateScreen extends StatefulWidget { + const NightStudyCreateScreen({super.key}); + + @override + State createState() => _NightStudyCreateState(); +} + +class _NightStudyCreateState extends State { + final TextEditingController _titleTextFieldController = TextEditingController(); + final TextEditingController _contentTextFieldController = TextEditingController(); + final TextEditingController _reasonTextFieldController = TextEditingController(); + + DateTime startAt = DateTime.now(); + DateTime endAt = adjustDateTime(DateTime.now(), hoursToAdd: 3); + + PlaceType? selectedPlace; + bool doNeedPhone = false; + + @override + void dispose() { + _titleTextFieldController.dispose(); + _contentTextFieldController.dispose(); + _reasonTextFieldController.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return ChangeNotifierProvider( + create: (_) => NightStudyCreateViewModel(), + child: Consumer( + builder: (context, provider, child) { + return Scaffold( + appBar: PreferredSize( + preferredSize: const Size.fromHeight(60), + child: EasierDodamDefaultPresetAppbar( + title: "심자 프리셋 생성하기", + onLeftArrowClick: () { + Navigator.pop(context); + }, + ), + ), + body: SafeArea( + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 16), + child: Column( + children: [ + EasierDodamTextField( + labelText: "프리셋 제목", + hintText: "프리셋 제목을 입력해주세요.", + controller: _titleTextFieldController, + ), + const SizedBox(height: 12), + EasierDodamTextField( + labelText: "사유", + hintText: "사유를 입력해주세요.", + controller: _contentTextFieldController, + ), + const SizedBox(height: 12), + DropdownButton( + value: selectedPlace, + hint: const Text("장소를 선택하세요"), + items: PlaceType.values.map((place) { + return DropdownMenuItem( + value: place, + child: Text(place.name), + ); + }).toList(), + onChanged: (PlaceType? value) { + setState(() { + selectedPlace = value; + }); + }, + dropdownColor: EasierDodamColors.staticWhite, + ), + const SizedBox(height: 12), + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + const Text("휴대폰 필요 여부", style: TextStyle(fontSize: 16)), + Checkbox( + value: doNeedPhone, + onChanged: (value) { + setState(() { + doNeedPhone = value!; + if (!doNeedPhone) { + _reasonTextFieldController.clear(); + } + }); + }, + activeColor: EasierDodamColors.primary300, + checkColor: EasierDodamColors.staticWhite, + side: BorderSide( + color: EasierDodamColors.gray200, + width: 2.0, + ), + ), + ], + ), + if (doNeedPhone) + EasierDodamTextField( + hintText: "휴대폰이 필요한 이유를 입력해주세요.", + controller: _reasonTextFieldController, + ), + const SizedBox(height: 12), + Row( + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Expanded( + child: NightStudyCreateTimeItem( + title: "심자 시작 날짜", + buttonText: "${startAt.year}년 ${startAt.month}월 ${startAt.day}일", + onButtonClick: () async { + final date = await showDatePicker( + context: context, + initialDate: startAt, + firstDate: DateTime(2000), + lastDate: DateTime(2100), + ); + if (date != null) { + setState(() { + startAt = date; + if (startAt.isAfter(endAt)) { + endAt = startAt; + } + }); + } + }, + ), + ), + SizedBox(width: 8), // 간격 추가 + Expanded( + child: NightStudyCreateTimeItem( + title: "심자 종료 날짜", + buttonText: "${endAt.year}년 ${endAt.month}월 ${endAt.day}일", + onButtonClick: () async { + final date = await showDatePicker( + context: context, + initialDate: endAt, + firstDate: DateTime(2000), + lastDate: DateTime(2100), + ); + if (date != null) { + setState(() { + endAt = date; + }); + } + }, + ), + ), + ], + ), + const Spacer(), + SizedBox( + width: double.infinity, + height: 56.0, + child: MaterialButton( + height: 56.0, + color: EasierDodamColors.primary300, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(12.0), + ), + child: const Text( + "생성하기", + style: EasierDodamStyles.body2, + ), + textColor: EasierDodamColors.staticWhite, + onPressed: () async { + if (selectedPlace == null) { + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar(content: Text("장소를 선택해주세요.")), + ); + return; + } + if (doNeedPhone && _reasonTextFieldController.text.isEmpty) { + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar(content: Text("휴대폰 필요 사유를 입력해주세요.")), + ); + return; + } + await provider.createNightStudy( + title: _titleTextFieldController.text, + place: selectedPlace!, + content: _contentTextFieldController.text, + doNeedPhone: doNeedPhone, + reasonForPhone: _reasonTextFieldController.text, + startAt: startAt, + endAt: endAt, + ); + Navigator.pop(context); + }, + ), + ), + const SizedBox( + height: 8 + ), + ], + ), + ), + ), + ); + }, + ), + ); + } + + static DateTime adjustDateTime(DateTime time, {required int hoursToAdd}) { + final adjustedTime = time.add(Duration(hours: hoursToAdd)); + + return adjustedTime; + } +} \ No newline at end of file diff --git a/lib/feature/night_study_create/night_study_create_navigation.dart b/lib/feature/night_study_create/night_study_create_navigation.dart new file mode 100644 index 0000000..55938e3 --- /dev/null +++ b/lib/feature/night_study_create/night_study_create_navigation.dart @@ -0,0 +1 @@ +const nightStudyCreateRoute = "night_study_create"; \ No newline at end of file diff --git a/lib/feature/night_study_create/night_study_create_viewmodel.dart b/lib/feature/night_study_create/night_study_create_viewmodel.dart new file mode 100644 index 0000000..a3e93d3 --- /dev/null +++ b/lib/feature/night_study_create/night_study_create_viewmodel.dart @@ -0,0 +1,33 @@ +import 'package:easier_dodam/local/database_manager.dart'; +import 'package:easier_dodam/local/entity/night_study_entity.dart'; +import 'package:flutter/material.dart'; + +import '../night_study/item/night_study_item.dart'; + + +class NightStudyCreateViewModel with ChangeNotifier { + Future createNightStudy({ + required PlaceType place, + required String title, + required String content, + required bool doNeedPhone, + required String reasonForPhone, + required DateTime startAt, + required DateTime endAt + }) async { + final database = await DatabaseManager.getDatabase(); + await database.nightStudyDao.insertNightStudyEntity(NightStudyEntity( + title: title, + place: place, + content: content, + doNeedPhone: doNeedPhone, + reasonForPhone: reasonForPhone, + startAt: startAt, + endAt: endAt + )); + + print(await database.nightStudyDao.findAllEntities()); + + return true; + } +} diff --git a/lib/feature/out/out.dart b/lib/feature/out/out.dart index d2aa230..8380274 100644 --- a/lib/feature/out/out.dart +++ b/lib/feature/out/out.dart @@ -2,7 +2,9 @@ import 'package:easier_dodam/component/appbar.dart'; import 'package:easier_dodam/component/modal_bottom_sheet_container.dart'; import 'package:easier_dodam/component/theme/color.dart'; import 'package:easier_dodam/component/theme/style.dart'; +import 'package:easier_dodam/feature/night_study/night_study_navigation.dart'; import 'package:easier_dodam/feature/out/item/out_item.dart'; +import 'package:easier_dodam/feature/out/out_navigation.dart'; import 'package:easier_dodam/feature/out/out_viewmodel.dart'; import 'package:easier_dodam/feature/out_create/out_create_navigation.dart'; import 'package:easier_dodam/remote/out/response/out_response.dart'; @@ -10,6 +12,9 @@ import 'package:easier_dodam/utiles/utile.dart'; import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; +import '../../component/bottom_navigation_bar.dart'; +import '../logout/logout_navigation.dart'; +import '../out_sleeping/out_sleeping_navigation.dart'; import 'item/out_preset_item.dart'; class OutScreen extends StatefulWidget { @@ -20,6 +25,30 @@ class OutScreen extends StatefulWidget { } class _OutScreenState extends State with WidgetsBindingObserver { + + int _selectedIndex = 1; + + void _onItemTapped(int index) { + setState(() { + _selectedIndex = index; + }); + + switch (index) { + case 0: + Navigator.pushReplacementNamed(context, outSleepingRoute); + break; + case 1: + Navigator.pushReplacementNamed(context, outRoute); + break; + case 2: + Navigator.pushReplacementNamed(context, nightStudyRoute); + break; + case 3: + Navigator.pushReplacementNamed(context, logoutRoute); + break; + } + } + @override void initState() { super.initState(); @@ -109,6 +138,12 @@ class _OutScreenState extends State with WidgetsBindingObserver { ), ), ), + bottomNavigationBar: SafeArea( + child: EasierDodamBottomNavigationBar( + selectedIndex: _selectedIndex, + onItemTapped: _onItemTapped, + ), + ), ); }, ); diff --git a/lib/feature/out_sleeping/out_sleeping.dart b/lib/feature/out_sleeping/out_sleeping.dart index 8716989..98ef54b 100644 --- a/lib/feature/out_sleeping/out_sleeping.dart +++ b/lib/feature/out_sleeping/out_sleeping.dart @@ -5,6 +5,7 @@ import 'package:easier_dodam/component/theme/style.dart'; import 'package:easier_dodam/feature/logout/logout_navigation.dart'; import 'package:easier_dodam/feature/night_study/night_study_navigation.dart'; import 'package:easier_dodam/feature/out_sleeping/item/out_sleeping_item.dart'; +import 'package:easier_dodam/feature/out_sleeping/out_sleeping_navigation.dart'; import 'package:easier_dodam/feature/out_sleeping/out_sleeping_viewmodel.dart'; import 'package:easier_dodam/feature/out_sleeping_create/out_sleeping_create_navigation.dart'; import 'package:easier_dodam/remote/out_sleeping/response/out_sleeping_response.dart'; @@ -38,15 +39,16 @@ class _OutSleepingScreenState extends State switch (index) { case 0: + Navigator.pushReplacementNamed(context, outSleepingRoute); break; case 1: Navigator.pushReplacementNamed(context, outRoute); break; case 2: - Navigator.pushReplacementNamed(context, logoutRoute); + Navigator.pushReplacementNamed(context, nightStudyRoute); break; case 3: - Navigator.pushReplacementNamed(context, nightStudyRoute); + Navigator.pushReplacementNamed(context, logoutRoute); break; } }); diff --git a/lib/local/conventer/place_type_converter.dart b/lib/local/conventer/place_type_converter.dart new file mode 100644 index 0000000..bd6753a --- /dev/null +++ b/lib/local/conventer/place_type_converter.dart @@ -0,0 +1,15 @@ +import 'package:floor/floor.dart'; + +import '../../feature/night_study/item/night_study_item.dart'; + +class PlaceTypeConverter extends TypeConverter { + @override + PlaceType decode(String databaseValue) { + return PlaceType.values.firstWhere((e) => e.toString() == 'PlaceType.' + databaseValue); + } + + @override + String encode(PlaceType value) { + return value.toString().split('.').last; + } +} \ No newline at end of file diff --git a/lib/local/dao/nigh_study_dao.dart b/lib/local/dao/nigh_study_dao.dart new file mode 100644 index 0000000..5be7d4b --- /dev/null +++ b/lib/local/dao/nigh_study_dao.dart @@ -0,0 +1,32 @@ +import 'package:floor/floor.dart'; + +import '../entity/night_study_entity.dart'; + +@dao +abstract class NighStudyDao { + @Query('SELECT * FROM $_tableName WHERE id = :id') + Future findNightStudyEntityById(int id); + + @Query('SELECT * FROM $_tableName') + Future> findAllEntities(); + + @Query('SELECT * FROM $_tableName') + Stream> findAllEntitiesWithStream(); + + @Query('DELETE FROM $_tableName WHERE id = :id') + Future deleteNightStudyEntityById(int id); + + @insert + Future insertNightStudyEntity(NightStudyEntity nightStudyEntity); + + @update + Future updateNightStudyEntity(NightStudyEntity nightStudyEntity); + + @delete + Future deleteNightStudyEntity(NightStudyEntity nightStudyEntity); + + @Query("DELETE FROM night_study") + Future deleteAllNightStudyEntities(); + + static const String _tableName = "night_study"; +} \ No newline at end of file diff --git a/lib/local/dao/out_dao.dart b/lib/local/dao/out_dao.dart index 1d94913..9fd4035 100644 --- a/lib/local/dao/out_dao.dart +++ b/lib/local/dao/out_dao.dart @@ -1,5 +1,3 @@ -import 'dart:ffi'; - import 'package:easier_dodam/local/entity/out_entity.dart'; import 'package:floor/floor.dart'; diff --git a/lib/local/easier_dodam_database.dart b/lib/local/easier_dodam_database.dart index 189aefa..5675294 100644 --- a/lib/local/easier_dodam_database.dart +++ b/lib/local/easier_dodam_database.dart @@ -1,20 +1,24 @@ import 'dart:async'; -import 'package:easier_dodam/local/conventer/date_time_conveter.dart'; +import 'package:easier_dodam/local/dao/nigh_study_dao.dart'; import 'package:easier_dodam/local/dao/out_dao.dart'; -import 'package:easier_dodam/local/dao/out_sleeping_dao.dart'; import 'package:floor/floor.dart'; import 'package:sqflite/sqflite.dart' as sqflite; +import 'conventer/date_time_conveter.dart'; +import 'conventer/place_type_converter.dart'; import 'conventer/time_of_day_converter.dart'; +import 'dao/out_sleeping_dao.dart'; +import 'entity/night_study_entity.dart'; import 'entity/out_entity.dart'; import 'entity/out_sleeping_entity.dart'; part "easier_dodam_database.g.dart"; -@TypeConverters([TimeOfDayConverter, DateTimeConverter]) -@Database(version: 1, entities: [OutEntity, OutSleepingEntity]) +@TypeConverters([TimeOfDayConverter, DateTimeConverter, PlaceTypeConverter]) +@Database(version: 1, entities: [OutEntity, NightStudyEntity, OutSleepingEntity]) abstract class EasierDodamDatabase extends FloorDatabase { OutDao get outDao; + NighStudyDao get nightStudyDao; OutSleepingDao get outSleepingDao; } diff --git a/lib/local/easier_dodam_database.g.dart b/lib/local/easier_dodam_database.g.dart index 2f12fea..477e9d2 100644 --- a/lib/local/easier_dodam_database.g.dart +++ b/lib/local/easier_dodam_database.g.dart @@ -76,6 +76,8 @@ class _$EasierDodamDatabase extends EasierDodamDatabase { OutDao? _outDaoInstance; + NighStudyDao? _nightStudyDaoInstance; + OutSleepingDao? _outSleepingDaoInstance; Future open( @@ -101,6 +103,8 @@ class _$EasierDodamDatabase extends EasierDodamDatabase { onCreate: (database, version) async { await database.execute( 'CREATE TABLE IF NOT EXISTS `out` (`id` INTEGER PRIMARY KEY AUTOINCREMENT, `title` TEXT NOT NULL, `reason` TEXT NOT NULL, `startAt` TEXT NOT NULL, `endAt` TEXT NOT NULL)'); + await database.execute( + 'CREATE TABLE IF NOT EXISTS `night_study` (`id` INTEGER PRIMARY KEY AUTOINCREMENT, `title` TEXT NOT NULL, `place` TEXT NOT NULL, `content` TEXT NOT NULL, `doNeedPhone` INTEGER NOT NULL, `reasonForPhone` TEXT NOT NULL, `startAt` TEXT NOT NULL, `endAt` TEXT NOT NULL)'); await database.execute( 'CREATE TABLE IF NOT EXISTS `out_sleeping` (`id` INTEGER PRIMARY KEY AUTOINCREMENT, `title` TEXT NOT NULL, `reason` TEXT NOT NULL, `startAt` TEXT NOT NULL, `endAt` TEXT NOT NULL)'); @@ -115,6 +119,11 @@ class _$EasierDodamDatabase extends EasierDodamDatabase { return _outDaoInstance ??= _$OutDao(database, changeListener); } + @override + NighStudyDao get nightStudyDao { + return _nightStudyDaoInstance ??= _$NighStudyDao(database, changeListener); + } + @override OutSleepingDao get outSleepingDao { return _outSleepingDaoInstance ??= @@ -239,6 +248,142 @@ class _$OutDao extends OutDao { } } +class _$NighStudyDao extends NighStudyDao { + _$NighStudyDao( + this.database, + this.changeListener, + ) : _queryAdapter = QueryAdapter(database, changeListener), + _nightStudyEntityInsertionAdapter = InsertionAdapter( + database, + 'night_study', + (NightStudyEntity item) => { + 'id': item.id, + 'title': item.title, + 'place': _placeTypeConverter.encode(item.place), + 'content': item.content, + 'doNeedPhone': item.doNeedPhone ? 1 : 0, + 'reasonForPhone': item.reasonForPhone, + 'startAt': _dateTimeConverter.encode(item.startAt), + 'endAt': _dateTimeConverter.encode(item.endAt) + }, + changeListener), + _nightStudyEntityUpdateAdapter = UpdateAdapter( + database, + 'night_study', + ['id'], + (NightStudyEntity item) => { + 'id': item.id, + 'title': item.title, + 'place': _placeTypeConverter.encode(item.place), + 'content': item.content, + 'doNeedPhone': item.doNeedPhone ? 1 : 0, + 'reasonForPhone': item.reasonForPhone, + 'startAt': _dateTimeConverter.encode(item.startAt), + 'endAt': _dateTimeConverter.encode(item.endAt) + }, + changeListener), + _nightStudyEntityDeletionAdapter = DeletionAdapter( + database, + 'night_study', + ['id'], + (NightStudyEntity item) => { + 'id': item.id, + 'title': item.title, + 'place': _placeTypeConverter.encode(item.place), + 'content': item.content, + 'doNeedPhone': item.doNeedPhone ? 1 : 0, + 'reasonForPhone': item.reasonForPhone, + 'startAt': _dateTimeConverter.encode(item.startAt), + 'endAt': _dateTimeConverter.encode(item.endAt) + }, + changeListener); + + final sqflite.DatabaseExecutor database; + + final StreamController changeListener; + + final QueryAdapter _queryAdapter; + + final InsertionAdapter _nightStudyEntityInsertionAdapter; + + final UpdateAdapter _nightStudyEntityUpdateAdapter; + + final DeletionAdapter _nightStudyEntityDeletionAdapter; + + @override + Future findNightStudyEntityById(int id) async { + return _queryAdapter.query('SELECT * FROM night_study WHERE id = ?1', + mapper: (Map row) => NightStudyEntity( + id: row['id'] as int?, + title: row['title'] as String, + place: _placeTypeConverter.decode(row['place'] as String), + content: row['content'] as String, + doNeedPhone: (row['doNeedPhone'] as int) != 0, + reasonForPhone: row['reasonForPhone'] as String, + startAt: _dateTimeConverter.decode(row['startAt'] as String), + endAt: _dateTimeConverter.decode(row['endAt'] as String)), + arguments: [id]); + } + + @override + Future> findAllEntities() async { + return _queryAdapter.queryList('SELECT * FROM night_study', + mapper: (Map row) => NightStudyEntity( + id: row['id'] as int?, + title: row['title'] as String, + place: _placeTypeConverter.decode(row['place'] as String), + content: row['content'] as String, + doNeedPhone: (row['doNeedPhone'] as int) != 0, + reasonForPhone: row['reasonForPhone'] as String, + startAt: _dateTimeConverter.decode(row['startAt'] as String), + endAt: _dateTimeConverter.decode(row['endAt'] as String))); + } + + @override + Stream> findAllEntitiesWithStream() { + return _queryAdapter.queryListStream('SELECT * FROM night_study', + mapper: (Map row) => NightStudyEntity( + id: row['id'] as int?, + title: row['title'] as String, + place: _placeTypeConverter.decode(row['place'] as String), + content: row['content'] as String, + doNeedPhone: (row['doNeedPhone'] as int) != 0, + reasonForPhone: row['reasonForPhone'] as String, + startAt: _dateTimeConverter.decode(row['startAt'] as String), + endAt: _dateTimeConverter.decode(row['endAt'] as String)), + queryableName: 'night_study', + isView: false); + } + + @override + Future deleteNightStudyEntityById(int id) async { + await _queryAdapter.queryNoReturn('DELETE FROM night_study WHERE id = ?1', + arguments: [id]); + } + + @override + Future deleteAllNightStudyEntities() async { + await _queryAdapter.queryNoReturn('DELETE FROM night_study'); + } + + @override + Future insertNightStudyEntity(NightStudyEntity nightStudyEntity) async { + await _nightStudyEntityInsertionAdapter.insert( + nightStudyEntity, OnConflictStrategy.abort); + } + + @override + Future updateNightStudyEntity(NightStudyEntity nightStudyEntity) async { + await _nightStudyEntityUpdateAdapter.update( + nightStudyEntity, OnConflictStrategy.abort); + } + + @override + Future deleteNightStudyEntity(NightStudyEntity nightStudyEntity) async { + await _nightStudyEntityDeletionAdapter.delete(nightStudyEntity); + } +} + class _$OutSleepingDao extends OutSleepingDao { _$OutSleepingDao( this.database, @@ -363,3 +508,4 @@ class _$OutSleepingDao extends OutSleepingDao { // ignore_for_file: unused_element final _timeOfDayConverter = TimeOfDayConverter(); final _dateTimeConverter = DateTimeConverter(); +final _placeTypeConverter = PlaceTypeConverter(); diff --git a/lib/local/entity/night_study_entity.dart b/lib/local/entity/night_study_entity.dart new file mode 100644 index 0000000..80452a9 --- /dev/null +++ b/lib/local/entity/night_study_entity.dart @@ -0,0 +1,64 @@ +import 'package:floor/floor.dart'; + +import '../../feature/night_study/item/night_study_item.dart'; + +@Entity(tableName: "night_study") +class NightStudyEntity { + @PrimaryKey(autoGenerate: true) + final int? id; + + final String title; + + final PlaceType place; + + final String content; + + final bool doNeedPhone; + + final String reasonForPhone; + + final DateTime startAt; + + final DateTime endAt; + + NightStudyEntity({ + this.id, + required this.title, + required this.place, + required this.content, + required this.doNeedPhone, + required this.reasonForPhone, + required this.startAt, + required this.endAt, + }); + + @override + bool operator ==(Object other) => + identical(this, other) || + other is NightStudyEntity && + runtimeType == other.runtimeType && + id == other.id && + title == other.title && + place == other.place && + content == other.content && + doNeedPhone == other.doNeedPhone && + reasonForPhone == other.reasonForPhone && + startAt == other.startAt && + endAt == other.endAt; + + @override + int get hashCode => + id.hashCode ^ + title.hashCode ^ + place.hashCode ^ + content.hashCode ^ + doNeedPhone.hashCode ^ + reasonForPhone.hashCode ^ + startAt.hashCode ^ + endAt.hashCode; + + @override + String toString() { + return 'NightStudyEntity(id: $id, title: $title, place: $place, content: $content, doNeedPhone: $doNeedPhone, reasonForPhone: $reasonForPhone, startAt: $startAt, endAt: $endAt)'; + } +} \ No newline at end of file diff --git a/lib/local/storage_manager.dart b/lib/local/storage_manager.dart index 29bb478..bd1861a 100644 --- a/lib/local/storage_manager.dart +++ b/lib/local/storage_manager.dart @@ -43,6 +43,11 @@ class StorageManager { ); } + Future hasToken() async { + final tokenDto = await StorageManager.getUserToken(); + return tokenDto.accessToken != null && tokenDto.accessToken!.isNotEmpty; + } + static get _idKey => "id"; static get _pwKey => "pw"; static get _accessTokenKey => "accessToken"; diff --git a/lib/main.dart b/lib/main.dart index ad4ec95..eaac5e4 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -1,8 +1,10 @@ import 'package:easier_dodam/component/theme/color.dart'; +import 'package:easier_dodam/component/theme/style.dart'; import 'package:easier_dodam/feature/login/login_navigation.dart'; import 'package:easier_dodam/feature/logout/logout.dart'; import 'package:easier_dodam/feature/logout/logout_navigation.dart'; import 'package:easier_dodam/feature/logout/logout_viewmodel.dart'; +import 'package:easier_dodam/feature/night_study/night_study_viewmodel.dart'; import 'package:easier_dodam/feature/out/out.dart'; import 'package:easier_dodam/feature/out_create/out_create.dart'; import 'package:easier_dodam/feature/out_create/out_create_navigation.dart'; @@ -10,21 +12,29 @@ import 'package:easier_dodam/feature/out_sleeping/out_sleeping.dart'; import 'package:easier_dodam/feature/out_sleeping/out_sleeping_navigation.dart'; import 'package:easier_dodam/feature/out_sleeping/out_sleeping_viewmodel.dart'; import 'package:easier_dodam/feature/out_sleeping_create/out_sleeping_create.dart'; +import 'package:easier_dodam/remote/core/core_client.dart'; import 'package:flutter/material.dart'; import 'package:flutter_config/flutter_config.dart'; +import 'package:flutter_svg/svg.dart'; import 'package:provider/provider.dart'; import 'feature/login/login.dart'; import 'feature/night_study/night_study.dart'; import 'feature/night_study/night_study_navigation.dart'; +import 'feature/night_study_create/night_study_create.dart'; +import 'feature/night_study_create/night_study_create_navigation.dart'; import 'feature/out/out_navigation.dart'; import 'feature/out/out_viewmodel.dart'; import 'feature/out_sleeping_create/out_sleeping_create_navigation.dart'; +import 'local/storage_manager.dart'; void main() async { WidgetsFlutterBinding.ensureInitialized(); - await FlutterConfig.loadEnvVariables(); - + try { + await FlutterConfig.loadEnvVariables(); + } catch (e) { + debugPrint('Failed to load environment variables: $e'); + } runApp(const MyApp()); } @@ -33,37 +43,85 @@ class MyApp extends StatelessWidget { @override Widget build(BuildContext context) { - return MaterialApp( - title: 'Flutter Demo', - debugShowCheckedModeBanner: false, - theme: ThemeData( - colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple), - useMaterial3: true, - fontFamily: "Pretendard", - scaffoldBackgroundColor: EasierDodamColors.staticWhite, + return MultiProvider( + providers: [ + ChangeNotifierProvider(create: (_) => NightStudyViewmodel()), + ChangeNotifierProvider(create: (_) => OutViewModel()), + ], + child: MaterialApp( + title: 'Flutter Demo', + debugShowCheckedModeBanner: false, + theme: ThemeData( + colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple), + useMaterial3: true, + fontFamily: "Pretendard", + scaffoldBackgroundColor: EasierDodamColors.staticWhite, + ), + initialRoute: 'splash', + routes: { + 'splash': (context) => const SplashScreen(), + nightStudyRoute: (context) => NightStudyScreen(), + loginRoute: (context) => LoginScreen(), + logoutRoute: (context) => ChangeNotifierProvider( + create: (_) => SettingViewModel(), + child: SettingScreen(), + ), + outSleepingRoute: (context) => ChangeNotifierProvider( + create: (_) => OutSleepingViewModel(), + child: OutSleepingScreen(), + ), + outSleepingCreateRoute: (context) => OutSleepingCreateScreen(), + outRoute: (context) => ChangeNotifierProvider( + create: (_) => OutViewModel(), + child: OutScreen(), + ), + outCreateRoute: (context) => OutCreateScreen(), + nightStudyCreateRoute: (context) => NightStudyCreateScreen(), + }, ), - initialRoute: loginRoute, - routes: { - nightStudyRoute: (context) => NightStudyScreen(), - loginRoute: (context) => LoginScreen(), - logoutRoute: (context) => ChangeNotifierProvider( - create: (_) => SettingViewModel(), - child: SettingScreen(), - ), - outSleepingRoute: (context) => ChangeNotifierProvider( - create: (_) => OutSleepingViewModel(), - child: OutSleepingScreen(), - ), - outSleepingCreateRoute: (context) => OutSleepingCreateScreen(), - outRoute: (context) => ChangeNotifierProvider( - create: (_) => OutViewModel(), - child: OutScreen(), - ), - outCreateRoute: (context) => OutCreateScreen(), - "test": (context) => Scaffold( - body: Text("test"), - ) - }, ); } } + +class SplashScreen extends StatelessWidget { + const SplashScreen({super.key}); + + @override + Widget build(BuildContext context) { + Future.delayed(const Duration(seconds: 1), () async { + if (await StorageManager().hasToken()) { + Navigator.pushReplacementNamed(context, outSleepingRoute); + } else { + Navigator.pushReplacementNamed(context, loginRoute); + } + }); + + return Scaffold( + body: Container( + decoration: BoxDecoration( + color: EasierDodamColors.staticWhite, + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + SvgPicture.asset( + 'assets/images/ic_splash.svg', + width: 200, + ), + const SizedBox(height: 16), + Text( + "도담도담을 쉽게", + style: EasierDodamStyles.title1.copyWith(fontSize: 30, color: EasierDodamColors.staticBlack), + ), + const SizedBox(height: 16), + ], + ), + ], + ), + ), + ); + } +} \ No newline at end of file diff --git a/lib/remote/core/core_client.dart b/lib/remote/core/core_client.dart index ca07da5..a291c99 100644 --- a/lib/remote/core/core_client.dart +++ b/lib/remote/core/core_client.dart @@ -18,7 +18,7 @@ class CoreClient { bool sendToken = true, }) async { final headers = { - 'Content-Type': 'application/json', + 'Content-Type': 'application/json; charset=utf-8', }; if (sendToken) { headers["Authorization"] = @@ -30,7 +30,8 @@ class CoreClient { body: json.encode(body), headers: headers, ); - print(response.body); + final responseBody = utf8.decode(response.bodyBytes); + print(responseBody); if (response.statusCode == 401 && sendToken) { final accessToken = await _refresh(); @@ -70,8 +71,8 @@ class CoreClient { Uri.parse(url), headers: headers, ); - - print(response.body); + final responseBody = utf8.decode(response.bodyBytes); + print(responseBody); if (response.statusCode == 401 && sendToken) { final accessToken = await _refresh(); diff --git a/lib/remote/night_study/night_study_data_source.dart b/lib/remote/night_study/night_study_data_source.dart index ccde3e2..6f87cf1 100644 --- a/lib/remote/night_study/night_study_data_source.dart +++ b/lib/remote/night_study/night_study_data_source.dart @@ -4,28 +4,52 @@ import 'package:easier_dodam/remote/core/url.dart'; import 'package:easier_dodam/remote/night_study/request/night_study_request.dart'; import 'package:easier_dodam/remote/night_study/response/night_study_response.dart'; +import '../../feature/night_study/item/night_study_item.dart'; + class NightStudyDataSource{ - Future> nightStudy( - String place, + Future postNightStudy( + PlaceType place, String content, bool doNeedPhone, String reasonForPhone, - String startAt, - String endAt) async { - return await CoreClient.post( - url: EasierDodamUrl.NIGHT_STUDY, - body: NightStudyRequest( - place: place, - content: content, - doNeedPhone: doNeedPhone, - reasonForPhone: reasonForPhone, - endAt: endAt, - startAt: startAt - ).toJson(), - decoder: NightStudyResponse.fromJson, - sendToken: true + DateTime startAt, + DateTime endAt + ) async { + await CoreClient.post( + url: EasierDodamUrl.NIGHT_STUDY, + body: NightStudyRequest( + place: place.name.toString(), + content: content, + doNeedPhone: doNeedPhone, + reasonForPhone: reasonForPhone, + startAt: startAt, + endAt: endAt + ).toJson(), ); + return true; + } + + Future> getMyNightStudies() async { + final response = await CoreClient.get>( + url: EasierDodamUrl.NIGHT_STUDY_MY, + listDecoder: (data) { + return data + .map((item) => NightStudyResponse.fromJson(item as Map)) + .toList(); + }, + ); + return response.data; + } + + Future deleteMyNightStudy(int id) async { + final response = await CoreClient.delete( + url: EasierDodamUrl.NIGHT_STUDY + "/$id", + ); + + print(response.message); + + return true; } } \ No newline at end of file diff --git a/lib/remote/night_study/request/night_study_request.dart b/lib/remote/night_study/request/night_study_request.dart index 5900b05..5f3e54a 100644 --- a/lib/remote/night_study/request/night_study_request.dart +++ b/lib/remote/night_study/request/night_study_request.dart @@ -1,5 +1,7 @@ import 'package:json_annotation/json_annotation.dart'; +import '../../../feature/night_study/item/night_study_item.dart'; + part 'night_study_request.g.dart'; @JsonSerializable() @@ -8,8 +10,8 @@ class NightStudyRequest{ String content; bool doNeedPhone; String reasonForPhone; - String startAt; - String endAt; + DateTime startAt; + DateTime endAt; NightStudyRequest( { diff --git a/lib/remote/night_study/request/night_study_request.g.dart b/lib/remote/night_study/request/night_study_request.g.dart index 39de93e..441c280 100644 --- a/lib/remote/night_study/request/night_study_request.g.dart +++ b/lib/remote/night_study/request/night_study_request.g.dart @@ -12,8 +12,8 @@ NightStudyRequest _$NightStudyRequestFromJson(Map json) => content: json['content'] as String, doNeedPhone: json['doNeedPhone'] as bool, reasonForPhone: json['reasonForPhone'] as String, - endAt: json['endAt'] as String, - startAt: json['startAt'] as String, + endAt: DateTime.parse(json['endAt'] as String), + startAt: DateTime.parse(json['startAt'] as String), ); Map _$NightStudyRequestToJson(NightStudyRequest instance) => @@ -22,6 +22,6 @@ Map _$NightStudyRequestToJson(NightStudyRequest instance) => 'content': instance.content, 'doNeedPhone': instance.doNeedPhone, 'reasonForPhone': instance.reasonForPhone, - 'startAt': instance.startAt, - 'endAt': instance.endAt, + 'startAt': instance.startAt.toIso8601String(), + 'endAt': instance.endAt.toIso8601String(), }; diff --git a/lib/remote/night_study/response/night_study_response.dart b/lib/remote/night_study/response/night_study_response.dart index fe37e2c..de7746e 100644 --- a/lib/remote/night_study/response/night_study_response.dart +++ b/lib/remote/night_study/response/night_study_response.dart @@ -1,46 +1,45 @@ -import 'package:easier_dodam/remote/core/base_response.dart'; import 'package:easier_dodam/remote/student/reponse/student_response.dart'; import 'package:json_annotation/json_annotation.dart'; part 'night_study_response.g.dart'; +enum Status { ALLOWED, PENDING, REJECTED } + @JsonSerializable() -class NightStudyResponse extends BaseObject{ +class NightStudyResponse { int id; String content; - String allowCheck; - bool isPhone; - String reason; + Status status; + bool doNeedPhone; + String? reasonForPhone; StudentResponse student; + String? rejectReason; String place; - String startAt; - String endAt; - String createdAt; - String? checkedAt; + DateTime startAt; + DateTime endAt; + DateTime createdAt; + DateTime? modifiedAt; NightStudyResponse( { required this.id, required this.content, - required this.allowCheck, - required this.isPhone, - required this.reason, + required this.status, + required this.doNeedPhone, + this.reasonForPhone, required this.student, + this.rejectReason, required this.place, required this.startAt, required this.endAt, required this.createdAt, - this.checkedAt + this.modifiedAt } ); factory NightStudyResponse.fromJson(Map json) => _$NightStudyResponseFromJson(json); - @override - NightStudyResponse fromJson(json) { - return NightStudyResponse.fromJson(json); - } - + Map toJson() => _$NightStudyResponseToJson(this); } \ No newline at end of file diff --git a/lib/remote/night_study/response/night_study_response.g.dart b/lib/remote/night_study/response/night_study_response.g.dart index 80ade5e..7a520a1 100644 --- a/lib/remote/night_study/response/night_study_response.g.dart +++ b/lib/remote/night_study/response/night_study_response.g.dart @@ -10,29 +10,39 @@ NightStudyResponse _$NightStudyResponseFromJson(Map json) => NightStudyResponse( id: (json['id'] as num).toInt(), content: json['content'] as String, - allowCheck: json['allowCheck'] as String, - isPhone: json['isPhone'] as bool, - reason: json['reason'] as String, + status: $enumDecode(_$StatusEnumMap, json['status']), + doNeedPhone: json['doNeedPhone'] as bool, + reasonForPhone: json['reasonForPhone'] as String?, student: StudentResponse.fromJson(json['student'] as Map), + rejectReason: json['rejectReason'] as String?, place: json['place'] as String, - startAt: json['startAt'] as String, - endAt: json['endAt'] as String, - createdAt: json['createdAt'] as String, - checkedAt: json['checkedAt'] as String?, + startAt: DateTime.parse(json['startAt'] as String), + endAt: DateTime.parse(json['endAt'] as String), + createdAt: DateTime.parse(json['createdAt'] as String), + modifiedAt: json['modifiedAt'] == null + ? null + : DateTime.parse(json['modifiedAt'] as String), ); Map _$NightStudyResponseToJson(NightStudyResponse instance) => { 'id': instance.id, 'content': instance.content, - 'allowCheck': instance.allowCheck, - 'isPhone': instance.isPhone, - 'reason': instance.reason, + 'status': _$StatusEnumMap[instance.status]!, + 'doNeedPhone': instance.doNeedPhone, + 'reasonForPhone': instance.reasonForPhone, 'student': instance.student, + 'rejectReason': instance.rejectReason, 'place': instance.place, - 'startAt': instance.startAt, - 'endAt': instance.endAt, - 'createdAt': instance.createdAt, - 'checkedAt': instance.checkedAt, + 'startAt': instance.startAt.toIso8601String(), + 'endAt': instance.endAt.toIso8601String(), + 'createdAt': instance.createdAt.toIso8601String(), + 'modifiedAt': instance.modifiedAt?.toIso8601String(), }; + +const _$StatusEnumMap = { + Status.ALLOWED: 'ALLOWED', + Status.PENDING: 'PENDING', + Status.REJECTED: 'REJECTED', +}; diff --git a/lib/remote/student/reponse/student_response.dart b/lib/remote/student/reponse/student_response.dart index c5c5c85..d07b499 100644 --- a/lib/remote/student/reponse/student_response.dart +++ b/lib/remote/student/reponse/student_response.dart @@ -5,12 +5,13 @@ part 'student_response.g.dart'; @JsonSerializable() class StudentResponse extends BaseObject{ + int? id; String name; int grade; int room; int number; - StudentResponse({required this.name, required this.grade, required this.room, required this.number}); + StudentResponse({this.id, required this.name, required this.grade, required this.room, required this.number}); factory StudentResponse.fromJson(Map json) => _$StudentResponseFromJson(json); diff --git a/lib/remote/student/reponse/student_response.g.dart b/lib/remote/student/reponse/student_response.g.dart index 311246d..3b1e74e 100644 --- a/lib/remote/student/reponse/student_response.g.dart +++ b/lib/remote/student/reponse/student_response.g.dart @@ -8,6 +8,7 @@ part of 'student_response.dart'; StudentResponse _$StudentResponseFromJson(Map json) => StudentResponse( + id: (json['id'] as num?)?.toInt(), name: json['name'] as String, grade: (json['grade'] as num).toInt(), room: (json['room'] as num).toInt(), @@ -16,6 +17,7 @@ StudentResponse _$StudentResponseFromJson(Map json) => Map _$StudentResponseToJson(StudentResponse instance) => { + 'id': instance.id, 'name': instance.name, 'grade': instance.grade, 'room': instance.room, diff --git a/lib/utiles/utile.dart b/lib/utiles/utile.dart index 9d126ec..80f0c09 100644 --- a/lib/utiles/utile.dart +++ b/lib/utiles/utile.dart @@ -16,3 +16,10 @@ extension DateTimeToTimeOfDay on DateTime { return TimeOfDay.fromDateTime(this); } } + +int dateDifferenceInDays(DateTime start, DateTime end) { + final difference = end.difference(start); + + return difference.inDays; +} + diff --git a/pubspec.lock b/pubspec.lock index 83cc890..1d7fc13 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -295,10 +295,10 @@ packages: dependency: "direct main" description: name: flutter_svg - sha256: "578bd8c508144fdaffd4f77b8ef2d8c523602275cd697cc3db284dbd762ef4ce" + sha256: "936d9c1c010d3e234d1672574636f3352b4941ca3decaddd3cafaeb9ad49c471" url: "https://pub.dev" source: hosted - version: "2.0.14" + version: "2.0.15" flutter_test: dependency: "direct dev" description: flutter @@ -372,10 +372,10 @@ packages: dependency: transitive description: name: js - sha256: f2c445dce49627136094980615a031419f7f3eb393237e4ecd97ac15dea343f3 + sha256: c1b2e9b5ea78c45e1a0788d29606ba27dc5f71f019f32ca5140f61ef071838cf url: "https://pub.dev" source: hosted - version: "0.6.7" + version: "0.7.1" json_annotation: dependency: "direct main" description: @@ -388,10 +388,10 @@ packages: dependency: "direct main" description: name: json_serializable - sha256: ea1432d167339ea9b5bb153f0571d0039607a873d6e04e0117af043f14a1fd4b + sha256: c2fcb3920cf2b6ae6845954186420fca40bc0a8abcc84903b7801f17d7050d7c url: "https://pub.dev" source: hosted - version: "6.8.0" + version: "6.9.0" leak_tracker: dependency: transitive description: @@ -588,10 +588,10 @@ packages: dependency: transitive description: name: shelf_web_socket - sha256: "073c147238594ecd0d193f3456a5fe91c4b0abbcc68bf5cd95b36c4e194ac611" + sha256: cc36c297b52866d203dbf9332263c94becc2fe0ceaa9681d07b6ef9807023b67 url: "https://pub.dev" source: hosted - version: "2.0.0" + version: "2.0.1" sky_engine: dependency: transitive description: flutter @@ -641,26 +641,26 @@ packages: dependency: transitive description: name: sqflite_common - sha256: "4468b24876d673418a7b7147e5a08a715b4998a7ae69227acafaab762e0e5490" + sha256: "761b9740ecbd4d3e66b8916d784e581861fd3c3553eda85e167bc49fdb68f709" url: "https://pub.dev" source: hosted - version: "2.5.4+5" + version: "2.5.4+6" sqflite_common_ffi: dependency: transitive description: name: sqflite_common_ffi - sha256: d316908f1537725427ff2827a5c5f3b2c1bc311caed985fe3c9b10939c9e11ca + sha256: b8ba78c1b72a9ee6c2323b06af95d43fd13e03d90c8369cb454fd7f629a72588 url: "https://pub.dev" source: hosted - version: "2.3.4" + version: "2.3.4+3" sqflite_common_ffi_web: dependency: transitive description: name: sqflite_common_ffi_web - sha256: f540ad769e5fd31aabe77bfa6e774fdd36145a83e33cdc39239f310c5f8559c3 + sha256: "61ea702e7aba727f28be7ead00b84c19c745cd4a4934d0c41473303df11ac9ea" url: "https://pub.dev" source: hosted - version: "0.4.5+3" + version: "0.4.5+4" sqflite_darwin: dependency: transitive description: @@ -785,10 +785,10 @@ packages: dependency: transitive description: name: vector_graphics - sha256: "773c9522d66d523e1c7b25dfb95cc91c26a1e17b107039cfe147285e92de7878" + sha256: "27d5fefe86fb9aace4a9f8375b56b3c292b64d8c04510df230f849850d912cb7" url: "https://pub.dev" source: hosted - version: "1.1.14" + version: "1.1.15" vector_graphics_codec: dependency: transitive description: