From 994a26a11e322582ea015c6c7d6957da435206a7 Mon Sep 17 00:00:00 2001 From: HyungJoon Date: Fri, 6 Dec 2024 23:26:20 +0900 Subject: [PATCH] feat: Notification Util --- client-app/lib/main.dart | 4 + .../article/detail/article_detail_screen.dart | 38 +++-- .../comment/adding/comment_adding_screen.dart | 104 ++++++++++++ .../comment_input_field.dart | 2 +- .../editing/medication_editing_screen.dart | 37 +++++ .../medication_content_for_editing_view.dart | 21 +++ ...medication_indicator_for_editing_view.dart | 5 + .../adding/question_adding_screen.dart | 150 ++++++++++-------- .../question_input_field.dart | 8 +- .../detail/question_detail_screen.dart | 36 +++-- .../medication_editing_view_model.dart | 20 +++ .../detail/question_detail_view_model.dart | 7 + 12 files changed, 341 insertions(+), 91 deletions(-) diff --git a/client-app/lib/main.dart b/client-app/lib/main.dart index 8a8f509..a293e78 100644 --- a/client-app/lib/main.dart +++ b/client-app/lib/main.dart @@ -34,6 +34,10 @@ Future onInitSystem() async { // Storage & Database await StorageFactory.onInit(); + + // Notification + await NotificationUtil.initialize(); + await NotificationUtil.setupRemoteNotification(); } Future onReadySystem() async { diff --git a/client-app/lib/presentation/view/article/detail/article_detail_screen.dart b/client-app/lib/presentation/view/article/detail/article_detail_screen.dart index ba4b75e..8576833 100644 --- a/client-app/lib/presentation/view/article/detail/article_detail_screen.dart +++ b/client-app/lib/presentation/view/article/detail/article_detail_screen.dart @@ -37,18 +37,32 @@ class ArticleDetailScreen extends GetView { top: false, bottom: true, child: Padding( - padding: const EdgeInsets.symmetric(horizontal: 20), - child: SingleChildScrollView( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - const ArticleDetailView(), - const CommentCardListView(), - SizedBox(height: GetPlatform.isAndroid ? 80 : 120), - ], - ), - ), - ), + padding: const EdgeInsets.symmetric(horizontal: 20), + child: RefreshIndicator( + onRefresh: viewModel.onRefresh, + child: LayoutBuilder( + builder: (BuildContext context, BoxConstraints constraints) { + return CustomScrollView( + physics: const AlwaysScrollableScrollPhysics(), + slivers: [ + SliverToBoxAdapter( + child: Container( + color: ColorSystem.white, + child: Column( + children: [ + const ArticleDetailView(), + const CommentCardListView(), + SizedBox( + height: GetPlatform.isAndroid ? 80 : 120), + ], + ), + ), + ), + ], + ); + }, + ), + )), ), ); } diff --git a/client-app/lib/presentation/view/comment/adding/comment_adding_screen.dart b/client-app/lib/presentation/view/comment/adding/comment_adding_screen.dart index 4e1389b..e062f6c 100644 --- a/client-app/lib/presentation/view/comment/adding/comment_adding_screen.dart +++ b/client-app/lib/presentation/view/comment/adding/comment_adding_screen.dart @@ -23,6 +23,110 @@ class CommentAddingScreen extends BaseScreen { @override Widget buildBody(BuildContext context) { return GestureDetector( + onTap: () { + FocusScope.of(context).unfocus(); + }, + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 20), + child: CustomScrollView( + slivers: [ + const SliverToBoxAdapter( + child: SizedBox(height: 32), + ), + const SliverToBoxAdapter( + child: CommentInputField(), + ), + const SliverToBoxAdapter( + child: SizedBox(height: 32), + ), + // SliverToBoxAdapter( + // child: Row( + // children: [ + // FilledButton( + // onPressed: () {}, + // style: FilledButton.styleFrom( + // // Size + // minimumSize: const Size(200, 32), + // fixedSize: const Size(200, 32), + // + // padding: EdgeInsets.zero, + // + // // Color + // backgroundColor: ColorSystem.neutral.shade300, + // foregroundColor: ColorSystem.white, + // + // disabledBackgroundColor: ColorSystem.neutral.shade300, + // + // // Border + // shape: const RoundedRectangleBorder( + // borderRadius: BorderRadius.all( + // Radius.circular(12), + // ), + // ), + // ), + // child: Center( + // child: Text( + // "커뮤니티 이용규칙 전체 보기", + // style: FontSystem.H5.copyWith( + // color: ColorSystem.neutral, + // height: 1.0, + // ), + // ), + // ), + // ), + // const Spacer(), + // ], + // ), + // ), + SliverToBoxAdapter( + child: Text( + "우아한은 건간을 위한 커뮤니티를 만들기 위해 커뮤니티 이용규칙을 제정하여 운영하고 있습니다. 위반 시 게시물이 삭제되고 서비스 이용이 일정 기간 제한될 수 있습니다." + "\n" + "\n" + "아래는 이 게시판에 해당하는 핵심 내용에 대한 요약 사항이며, 게시물 작성 전 커뮤니티 이용규칙 전문을 반드시 확인하시기 바랍니다.", + style: FontSystem.H6.copyWith( + color: ColorSystem.neutral.shade600, + ), + ), + ), + SliverFillRemaining( + hasScrollBody: false, + child: Column( + children: [ + const Spacer(), + const SizedBox(height: 32), + Obx(() { + VoidCallback? onPressed = viewModel.content.length > 10 + ? () { + viewModel.createComment().then((value) { + if (value) { + Get.back(); + } else { + Get.snackbar('알림', '댓글 작성에 실패했습니다.'); + } + }); + } + : null; + + return PrimaryFillButton( + width: Get.width, + height: 60, + content: '완료', + onPressed: onPressed, + ); + }), + SizedBox( + height: GetPlatform.isAndroid ? 20 : 40, + ), + ], + ), + ), + ], + ), + ), + ); + + GestureDetector( onTap: () => FocusScope.of(context).unfocus(), child: SingleChildScrollView( child: Padding( diff --git a/client-app/lib/presentation/view/comment/adding/widget/comment_input_field/comment_input_field.dart b/client-app/lib/presentation/view/comment/adding/widget/comment_input_field/comment_input_field.dart index 16764e4..66e2b8a 100644 --- a/client-app/lib/presentation/view/comment/adding/widget/comment_input_field/comment_input_field.dart +++ b/client-app/lib/presentation/view/comment/adding/widget/comment_input_field/comment_input_field.dart @@ -23,7 +23,7 @@ class CommentInputField extends BaseWidget { hasSuffixIcon: false, fillColor: ColorSystem.neutral.shade100, textInputType: TextInputType.multiline, - textInputAction: TextInputAction.search, + textInputAction: TextInputAction.done, onChangedCallBack: viewModel.updateContent, onClearCallBack: viewModel.updateContent, onSubmittedCallBack: () { diff --git a/client-app/lib/presentation/view/medication/editing/medication_editing_screen.dart b/client-app/lib/presentation/view/medication/editing/medication_editing_screen.dart index 89f76a4..38adec3 100644 --- a/client-app/lib/presentation/view/medication/editing/medication_editing_screen.dart +++ b/client-app/lib/presentation/view/medication/editing/medication_editing_screen.dart @@ -8,6 +8,7 @@ import 'package:wooahan/presentation/view/medication/editing/widget/medication_i import 'package:wooahan/presentation/view_model/medication/editing/medication_editing_view_model.dart'; import 'package:wooahan/presentation/widget/common/appbar/text_back_app_bar.dart'; import 'package:wooahan/presentation/widget/common/button/primary/primary_fill_button.dart'; +import 'package:wooahan/presentation/widget/common/dialog/confirm_dialog.dart'; class MedicationEditingScreen extends BaseScreen { const MedicationEditingScreen({super.key}); @@ -33,6 +34,42 @@ class MedicationEditingScreen extends BaseScreen { title: '복약 수정하기', backgroundColor: ColorSystem.white, onBackPress: Get.back, + actions: [ + GestureDetector( + onTap: () { + Get.dialog( + ConfirmDialog( + title: "복약 삭제", + content: '해당 약을 지우시겠어요?' + '\n' + '삭제 시 알림을 받을 수 없어요.', + onPressedCancel: Get.back, + onPressedApply: () { + viewModel.deleteAllApply().then((value) { + if (value) { + Get.back(); + } else { + Get.snackbar( + '알림', + '전체 삭제에 실패했어요. 다시 시도해주세요.', + backgroundColor: ColorSystem.primary, + colorText: ColorSystem.white, + ); + } + }); + }, + ), + ); + }, + child: Text( + '전체 삭제', + style: FontSystem.H6.copyWith( + color: ColorSystem.red, + ), + ), + ), + const SizedBox(width: 20), + ], preferredSize: const Size.fromHeight(64), ); } diff --git a/client-app/lib/presentation/view/medication/editing/widget/medication_content_for_editing/medication_content_for_editing_view.dart b/client-app/lib/presentation/view/medication/editing/widget/medication_content_for_editing/medication_content_for_editing_view.dart index 8e55786..ab6ae14 100644 --- a/client-app/lib/presentation/view/medication/editing/widget/medication_content_for_editing/medication_content_for_editing_view.dart +++ b/client-app/lib/presentation/view/medication/editing/widget/medication_content_for_editing/medication_content_for_editing_view.dart @@ -1,5 +1,7 @@ import 'package:flutter/material.dart'; import 'package:get/get.dart'; +import 'package:wooahan/app/config/color_system.dart'; +import 'package:wooahan/app/config/font_system.dart'; import 'package:wooahan/core/screen/base_widget.dart'; import 'package:wooahan/presentation/view/medication/editing/widget/medication_content_for_editing/component/editing_icon_button.dart'; import 'package:wooahan/presentation/view_model/medication/editing/medication_editing_view_model.dart'; @@ -28,6 +30,10 @@ class MedicationContentForEditingView return const SizedBox(width: 48); } + if (viewModel.modifiedMedicationList.isEmpty) { + return const SizedBox(width: 48); + } + if (viewModel.currentIndex != 0) { return EditingIconButton( icon: const Icon( @@ -49,6 +55,16 @@ class MedicationContentForEditingView return const Expanded(child: SizedBox()); } + if (viewModel.modifiedMedicationList.isEmpty) { + return Text( + '복약 중인 약이 없어요', + style: FontSystem.H6.copyWith( + height: 1.0, + color: ColorSystem.neutral.shade500, + ), + ); + } + return MedicationBasicDefaultItemView( state: viewModel.modifiedMedicationList[viewModel.currentIndex], onLongPress: () { @@ -100,6 +116,11 @@ class MedicationContentForEditingView if (viewModel.isInitLoading) { return const SizedBox(width: 48); } + + if (viewModel.modifiedMedicationList.isEmpty) { + return const SizedBox(width: 48); + } + if (viewModel.currentIndex != viewModel.modifiedMedicationList.length - 1) { return EditingIconButton( diff --git a/client-app/lib/presentation/view/medication/editing/widget/medication_indicator_for_editing/medication_indicator_for_editing_view.dart b/client-app/lib/presentation/view/medication/editing/widget/medication_indicator_for_editing/medication_indicator_for_editing_view.dart index 20c89bd..3de73fb 100644 --- a/client-app/lib/presentation/view/medication/editing/widget/medication_indicator_for_editing/medication_indicator_for_editing_view.dart +++ b/client-app/lib/presentation/view/medication/editing/widget/medication_indicator_for_editing/medication_indicator_for_editing_view.dart @@ -16,6 +16,11 @@ class MedicationIndicatorForEditingView if (viewModel.isInitLoading) { return const SizedBox(height: 16); } + + if (viewModel.modifiedMedicationList.isEmpty) { + return const SizedBox(height: 16); + } + return SizedBox( height: 16, child: Center( diff --git a/client-app/lib/presentation/view/question/adding/question_adding_screen.dart b/client-app/lib/presentation/view/question/adding/question_adding_screen.dart index cc44e63..a2b17d0 100644 --- a/client-app/lib/presentation/view/question/adding/question_adding_screen.dart +++ b/client-app/lib/presentation/view/question/adding/question_adding_screen.dart @@ -26,49 +26,60 @@ class QuestionAddingScreen extends BaseScreen { onTap: () { FocusScope.of(context).unfocus(); }, - child: SingleChildScrollView( - child: Padding( - padding: const EdgeInsets.symmetric(horizontal: 20), - child: Column( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - const SizedBox(height: 32), - const QuestionInputField(), - const SizedBox(height: 32), - FilledButton( - onPressed: () {}, - style: FilledButton.styleFrom( - // Size - minimumSize: const Size(212, 32), - fixedSize: const Size(212, 32), - - padding: EdgeInsets.zero, - - // Color - backgroundColor: ColorSystem.neutral.shade300, - foregroundColor: ColorSystem.white, - - disabledBackgroundColor: ColorSystem.neutral.shade300, - - // Border - shape: const RoundedRectangleBorder( - borderRadius: BorderRadius.all( - Radius.circular(12), - ), - ), - ), - child: Center( - child: Text( - "커뮤니티 이용규칙 전체 보기", - style: FontSystem.H5.copyWith( - color: ColorSystem.neutral, - height: 1.0, - ), - ), - ), - ), - Text( + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 20), + child: CustomScrollView( + slivers: [ + const SliverToBoxAdapter( + child: SizedBox(height: 32), + ), + const SliverToBoxAdapter( + child: QuestionInputField(), + ), + const SliverToBoxAdapter( + child: SizedBox(height: 32), + ), + // SliverToBoxAdapter( + // child: Row( + // children: [ + // FilledButton( + // onPressed: () {}, + // style: FilledButton.styleFrom( + // // Size + // minimumSize: const Size(200, 32), + // fixedSize: const Size(200, 32), + // + // padding: EdgeInsets.zero, + // + // // Color + // backgroundColor: ColorSystem.neutral.shade300, + // foregroundColor: ColorSystem.white, + // + // disabledBackgroundColor: ColorSystem.neutral.shade300, + // + // // Border + // shape: const RoundedRectangleBorder( + // borderRadius: BorderRadius.all( + // Radius.circular(12), + // ), + // ), + // ), + // child: Center( + // child: Text( + // "커뮤니티 이용규칙 전체 보기", + // style: FontSystem.H5.copyWith( + // color: ColorSystem.neutral, + // height: 1.0, + // ), + // ), + // ), + // ), + // const Spacer(), + // ], + // ), + // ), + SliverToBoxAdapter( + child: Text( "우아한은 건간을 위한 커뮤니티를 만들기 위해 커뮤니티 이용규칙을 제정하여 운영하고 있습니다. 위반 시 게시물이 삭제되고 서비스 이용이 일정 기간 제한될 수 있습니다." "\n" "\n" @@ -77,29 +88,40 @@ class QuestionAddingScreen extends BaseScreen { color: ColorSystem.neutral.shade600, ), ), - Obx(() { - VoidCallback? onPressed = viewModel.content.length > 10 - ? () { - viewModel.createQuestion().then((value) { - if (value) { - Get.back(); - } else { - Get.snackbar('알림', '질문 작성에 실패했습니다.'); + ), + SliverFillRemaining( + hasScrollBody: false, + child: Column( + children: [ + const Spacer(), + const SizedBox(height: 32), + Obx(() { + final onPressed = viewModel.content.length > 10 + ? () { + viewModel.createQuestion().then((value) { + if (value) { + Get.back(); + } else { + Get.snackbar('알림', '질문 작성에 실패했습니다.'); + } + }); } - }); - } - : null; + : null; - return PrimaryFillButton( - width: Get.width, - height: 60, - content: '완료', - onPressed: onPressed, - ); - }), - SizedBox(height: GetPlatform.isAndroid ? 20 : 40), - ], - ), + return PrimaryFillButton( + width: Get.width, + height: 60, + content: '완료', + onPressed: onPressed, + ); + }), + SizedBox( + height: GetPlatform.isAndroid ? 20 : 40, + ), + ], + ), + ), + ], ), ), ); diff --git a/client-app/lib/presentation/view/question/adding/widget/question_input_field/question_input_field.dart b/client-app/lib/presentation/view/question/adding/widget/question_input_field/question_input_field.dart index 0ca1d8e..828e542 100644 --- a/client-app/lib/presentation/view/question/adding/widget/question_input_field/question_input_field.dart +++ b/client-app/lib/presentation/view/question/adding/widget/question_input_field/question_input_field.dart @@ -15,13 +15,15 @@ class QuestionInputField extends BaseWidget { () => CustomInputTextField( textStyle: FontSystem.H6, enable: !viewModel.isLoading, - minLines: 6, - maxLines: 6, + minLines: 10, + maxLines: 10, + maxLength: 500, placeholder: '질문를 입력해주세요', + enabledCounter: true, hasSuffixIcon: false, fillColor: ColorSystem.neutral.shade200, textInputType: TextInputType.multiline, - textInputAction: TextInputAction.search, + textInputAction: TextInputAction.done, onChangedCallBack: viewModel.updateContent, onClearCallBack: viewModel.updateContent, onSubmittedCallBack: () { diff --git a/client-app/lib/presentation/view/question/detail/question_detail_screen.dart b/client-app/lib/presentation/view/question/detail/question_detail_screen.dart index bb622a7..cd19f26 100644 --- a/client-app/lib/presentation/view/question/detail/question_detail_screen.dart +++ b/client-app/lib/presentation/view/question/detail/question_detail_screen.dart @@ -45,17 +45,31 @@ class QuestionDetailScreen extends BaseScreen { @override Widget buildBody(BuildContext context) { - return Padding( - padding: const EdgeInsets.symmetric(horizontal: 20), - child: SingleChildScrollView( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - const QuestionDetailView(), - const AnswerCardListView(), - SizedBox(height: GetPlatform.isAndroid ? 80 : 120), - ], - ), + return RefreshIndicator( + onRefresh: viewModel.onRefresh, + child: LayoutBuilder( + builder: (BuildContext context, BoxConstraints constraints) { + return Padding( + padding: const EdgeInsets.symmetric(horizontal: 20), + child: CustomScrollView( + physics: const AlwaysScrollableScrollPhysics(), + slivers: [ + SliverToBoxAdapter( + child: Container( + color: ColorSystem.white, + child: Column( + children: [ + const QuestionDetailView(), + const AnswerCardListView(), + SizedBox(height: GetPlatform.isAndroid ? 80 : 120), + ], + ), + ), + ), + ], + ), + ); + }, ), ); } diff --git a/client-app/lib/presentation/view_model/medication/editing/medication_editing_view_model.dart b/client-app/lib/presentation/view_model/medication/editing/medication_editing_view_model.dart index 263d725..adc88ce 100644 --- a/client-app/lib/presentation/view_model/medication/editing/medication_editing_view_model.dart +++ b/client-app/lib/presentation/view_model/medication/editing/medication_editing_view_model.dart @@ -153,6 +153,26 @@ class MedicationEditingViewModel extends GetxController { _isEdited.value = !_equalsMedicationList(); } + Future deleteAllApply() async { + StateWrapper stateWrapper = + await _updateMedicationListUseCase.execute( + UpdateMedicationListCondition( + medicationList: [], + ), + ); + + if (!stateWrapper.success) { + return false; + } + + _modifiedMedicationList.clear(); + _currentIndex.value = 0; + + await Get.find().publishUpdateMedicationEvent(); + + return true; + } + Future confirmApply() async { StateWrapper stateWrapper = await _updateMedicationListUseCase.execute( diff --git a/client-app/lib/presentation/view_model/question/detail/question_detail_view_model.dart b/client-app/lib/presentation/view_model/question/detail/question_detail_view_model.dart index fb401eb..08bd4b6 100644 --- a/client-app/lib/presentation/view_model/question/detail/question_detail_view_model.dart +++ b/client-app/lib/presentation/view_model/question/detail/question_detail_view_model.dart @@ -68,6 +68,13 @@ class QuestionDetailViewModel extends GetxController { _isInitLoading.value = false; } + Future onRefresh() async { + await Future.wait([ + _fetchQuestionDetail(), + _fetchQuestionCommentList(), + ]); + } + Future _fetchQuestionDetail() async { StateWrapper state = await _readQuestionDetailUseCase.execute(