From d38bb98424d67e498f03be3c1c21ffe7602eda9d Mon Sep 17 00:00:00 2001 From: HuiChan Seo <78739194+seochan99@users.noreply.github.com> Date: Mon, 19 Feb 2024 21:25:38 +0900 Subject: [PATCH] =?UTF-8?q?=E2=9C=A8feat=20:=20=EB=8D=B0=EC=9D=BC=EB=A6=AC?= =?UTF-8?q?=EA=B8=B0=EB=A1=9D=20=EB=B3=B4=EA=B8=B0=20=EC=99=84=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/main_app.dart | 1 + lib/utilities/app_routes.dart | 5 + .../study/date_study_screen_viewmodel.dart | 61 ++++-- lib/viewModels/word/word_viewmodel.dart | 4 +- lib/views/study/date_study_screen.dart | 178 +++++++++++------- .../study/widget/study_row_card_widget.dart | 20 +- lib/views/word/widget/word_list_widget.dart | 3 + lib/views/word/word_screen.dart | 42 ++++- 8 files changed, 219 insertions(+), 95 deletions(-) diff --git a/lib/main_app.dart b/lib/main_app.dart index dc4b989..e60d734 100644 --- a/lib/main_app.dart +++ b/lib/main_app.dart @@ -43,6 +43,7 @@ class MainApp extends StatelessWidget { Routes.routes[1], Routes.routes[2], Routes.routes[3], + Routes.routes[4], ], ); } diff --git a/lib/utilities/app_routes.dart b/lib/utilities/app_routes.dart index e448225..931474d 100644 --- a/lib/utilities/app_routes.dart +++ b/lib/utilities/app_routes.dart @@ -3,6 +3,7 @@ import 'package:earlips/views/home/home_screen.dart'; import 'package:earlips/views/profile/profile_account/profile_account_screen.dart'; import 'package:earlips/views/profile/profile_language_setting/profile_language_setting.dart'; import 'package:earlips/views/profile/profile_screen.dart'; +import 'package:earlips/views/study/study_main.dart'; import 'package:get/get.dart'; abstract class Routes { @@ -20,6 +21,10 @@ abstract class Routes { name: HOME, page: () => HomeScreen(), ), + GetPage( + name: STUDY, + page: () => const StudyMain(), + ), GetPage( name: PROFILE, page: () => const ProfileScreen(), diff --git a/lib/viewModels/study/date_study_screen_viewmodel.dart b/lib/viewModels/study/date_study_screen_viewmodel.dart index b110d71..68df46a 100644 --- a/lib/viewModels/study/date_study_screen_viewmodel.dart +++ b/lib/viewModels/study/date_study_screen_viewmodel.dart @@ -1,24 +1,61 @@ -import 'package:flutter/material.dart'; +import 'package:cloud_firestore/cloud_firestore.dart'; +import 'package:firebase_auth/firebase_auth.dart'; +import 'package:intl/intl.dart'; class LearningSession { - final String type; // 세션 유형 (음소, 단어, 문장) - final DateTime createdDate; // 세션 생성 날짜 + final int type; // 세션 유형 (음소, 단어, 문장) + final String createdDate; // 세션 생성 날짜 final String text; // 세션과 관련된 텍스트 + final int index; // 세션 인덱스 - LearningSession({required this.type, required this.createdDate, required this.text}); + LearningSession( + {required this.type, + required this.createdDate, + required this.text, + required this.index}); } class DateStudyViewModel { final DateTime date; - + final FirebaseFirestore _firestore = FirebaseFirestore.instance; + final FirebaseAuth _auth = FirebaseAuth.instance; DateStudyViewModel({required this.date}); - List getSessions() { - // 실제 애플리케이션에서는 여기서 날짜에 따라 데이터를 조회하거나 필터링하는 로직을 구현할 수 있습니다. - return [ - LearningSession(type: '음소', createdDate: DateTime(2024, 2, 12), text: "가"), - LearningSession(type: '단어', createdDate: DateTime(2024, 2, 13), text: "가다"), - LearningSession(type: '문장', createdDate: DateTime(2024, 2, 14), text: "가다 보면 길이 있다."), - ]; + Future> getSessions() async { + // 파이어스토어에 저장된 날짜 형식에 맞게 날짜를 변환 + final uid = _auth.currentUser?.uid; + + String formattedDate = DateFormat('yyyyMMdd').format(date); + try { + // 해당 날짜의 daily record 문서를 가져옴 + DocumentSnapshot dailyRecordSnapshot = await _firestore + .collection('users') + .doc(uid) + .collection('daily_records') + .doc(formattedDate) + .get(); + + if (dailyRecordSnapshot.exists) { + // 해당 날짜의 세션 데이터를 가져옴 + List> sessionsData = + List>.from( + dailyRecordSnapshot.get('wordsList') ?? []); + // 세션 데이터를 LearningSession 객체로 변환 + List sessions = sessionsData.map((session) { + return LearningSession( + type: session['type'], + createdDate: formattedDate, + text: session['word'], + index: session['index'], + ); + }).toList(); + + return sessions; + } else { + return []; + } + } catch (error) { + return []; + } } } diff --git a/lib/viewModels/word/word_viewmodel.dart b/lib/viewModels/word/word_viewmodel.dart index cc26767..b9adef8 100644 --- a/lib/viewModels/word/word_viewmodel.dart +++ b/lib/viewModels/word/word_viewmodel.dart @@ -116,6 +116,7 @@ class WordViewModel extends GetxController { existingWordsList.add({ 'word': word.word, 'type': word.type, + 'index': currentIndex.value }); // 업데이트 @@ -123,12 +124,13 @@ class WordViewModel extends GetxController { .update(dailyRecordRef, {'wordsList': existingWordsList}); } } else { - // If the document doesn't exist, create a new one with the initial word + // 만약 daily record가 없다면 새로 생성 transaction.set(dailyRecordRef, { 'wordsList': [ { 'word': word.word, 'type': word.type, + 'index': currentIndex.value }, ], }); diff --git a/lib/views/study/date_study_screen.dart b/lib/views/study/date_study_screen.dart index 3efe831..b99466b 100644 --- a/lib/views/study/date_study_screen.dart +++ b/lib/views/study/date_study_screen.dart @@ -1,18 +1,19 @@ +import 'package:earlips/utilities/style/color_styles.dart'; +import 'package:earlips/viewModels/study/date_study_screen_viewmodel.dart'; import 'package:earlips/views/base/default_back_appbar.dart'; +import 'package:earlips/views/study/widget/study_row_card_widget.dart'; +import 'package:earlips/views/word/word_screen.dart'; import 'package:flutter/material.dart'; +import 'package:get/get.dart'; import 'package:intl/intl.dart'; -// ViewModel import 경로는 실제 프로젝트 구조에 따라 달라질 수 있습니다. -import 'package:earlips/viewModels/study/date_study_screen_viewmodel.dart'; class DateStudyScreen extends StatelessWidget { final DateTime date; + const DateStudyScreen({super.key, required this.date}); @override Widget build(BuildContext context) { - final viewModel = DateStudyViewModel(date: date); - final sessions = viewModel.getSessions(); - return Scaffold( appBar: PreferredSize( preferredSize: const Size.fromHeight(kToolbarHeight), @@ -20,77 +21,110 @@ class DateStudyScreen extends StatelessWidget { title: DateFormat('yyyy/MM/dd').format(date), ), ), - body: ListView.separated( - padding: const EdgeInsets.fromLTRB(25, 20, 25, 20), - itemCount: sessions.length, - itemBuilder: (context, index) { - var session = sessions[index]; - return Container( - decoration: BoxDecoration( - color: Colors.white, - borderRadius: BorderRadius.circular(15.0), - boxShadow: [ - BoxShadow( - color: Colors.grey.withOpacity(0.15), - spreadRadius: 0.1, - blurRadius: 10, - offset: const Offset(0, 2), - ), - ], - ), - child: Column( - children: [ - Container( - alignment: Alignment.centerLeft, - margin: const EdgeInsets.only(left: 20.0, top: 16.0), - child: _SmallCard(name: session.type)), // 세션 유형을 표시 - ListTile( - contentPadding: - const EdgeInsets.only(left: 20, right: 20, bottom: 30), - title: Container( - alignment: Alignment.center, - child: Text( - session.text, // 세션과 관련된 텍스트를 표시 - style: const TextStyle( - fontSize: 24.0, - fontWeight: FontWeight.bold, + body: FutureBuilder>( + future: DateStudyViewModel(date: date).getSessions(), + builder: (context, snapshot) { + if (snapshot.connectionState == ConnectionState.waiting) { + return const Center( + child: CircularProgressIndicator(), + ); + } else if (snapshot.hasError) { + return Center( + child: Text('Error: ${snapshot.error}'), + ); + } else if (!snapshot.hasData || snapshot.data!.isEmpty) { + return Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + const Text('등록된 학습 기록이 없습니다.'), + const SizedBox(height: 20), + // 학습하러가기 + ElevatedButton( + style: ButtonStyle( + padding: MaterialStateProperty.all( + const EdgeInsets.fromLTRB(20, 10, 20, 10), + ), + backgroundColor: + MaterialStateProperty.all(ColorSystem.main2), + ), + onPressed: () { + Get.back(); + }, + child: const Text( + '학습하러가기', + style: TextStyle( + color: Colors.white, + fontSize: 16.0, ), ), ), - onTap: () {}, - ), - ], - ), - ); + ], + ), + ); + } else { + return ListView.separated( + padding: const EdgeInsets.fromLTRB(25, 20, 25, 20), + itemCount: snapshot.data!.length, + itemBuilder: (context, index) { + var session = snapshot.data![index]; + return Container( + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(15.0), + boxShadow: [ + BoxShadow( + color: Colors.grey.withOpacity(0.15), + spreadRadius: 0.1, + blurRadius: 10, + offset: const Offset(0, 2), + ), + ], + ), + child: Column( + children: [ + Container( + alignment: Alignment.centerLeft, + margin: const EdgeInsets.only(left: 20.0, top: 16.0), + child: SmallCard(type: session.type), + ), + ListTile( + contentPadding: const EdgeInsets.only( + left: 20, right: 20, bottom: 30), + title: Container( + alignment: Alignment.center, + child: Text( + session.text, + style: const TextStyle( + fontSize: 24.0, + fontWeight: FontWeight.bold, + ), + ), + ), + onTap: () { + Get.to( + () => WordScreen( + // session type에 따라 다른 tttle + title: session.type == 0 + ? '음소' + : (session.type == 1 + ? '단어' + : (session.type == 2 + ? '문장' + : '문단')), + type: 0, + ), + arguments: session.index); + }, + ), + ], + ), + ); + }, + separatorBuilder: (context, index) => const SizedBox(height: 20), + ); + } }, - separatorBuilder: (context, index) => const SizedBox(height: 20), - ), - ); - } -} - -class _SmallCard extends StatelessWidget { - final String name; - - const _SmallCard({super.key, required this.name}); - - @override - Widget build(BuildContext context) { - return Container( - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(4.0), - color: const Color(0xFF1FA9DC), - ), - alignment: Alignment.center, - width: 50, - height: 24, - child: Text( - name, - style: const TextStyle( - color: Colors.white, - fontSize: 14, - fontWeight: FontWeight.w600, - ), ), ); } diff --git a/lib/views/study/widget/study_row_card_widget.dart b/lib/views/study/widget/study_row_card_widget.dart index c21cfc9..267bf29 100644 --- a/lib/views/study/widget/study_row_card_widget.dart +++ b/lib/views/study/widget/study_row_card_widget.dart @@ -1,9 +1,25 @@ import 'package:flutter/material.dart'; class SmallCard extends StatelessWidget { - final String name; + final int type; - const SmallCard({super.key, required this.name}); + const SmallCard({super.key, required this.type}); + + // type에 따라 name을 반환 + String get name { + switch (type) { + case 0: + return '음소'; + case 1: + return '단어'; + case 2: + return '문장'; + case 3: + return '문단'; + default: + return '음소'; + } + } @override Widget build(BuildContext context) { diff --git a/lib/views/word/widget/word_list_widget.dart b/lib/views/word/widget/word_list_widget.dart index 5c20c8d..d665fba 100644 --- a/lib/views/word/widget/word_list_widget.dart +++ b/lib/views/word/widget/word_list_widget.dart @@ -85,6 +85,9 @@ class _WordListState extends State { color: ColorSystem.gray5, ), ), + const SizedBox( + width: 5, + ), const Icon( Icons.check_circle_rounded, color: ColorSystem.green, diff --git a/lib/views/word/word_screen.dart b/lib/views/word/word_screen.dart index b2ad2ac..65c6674 100644 --- a/lib/views/word/word_screen.dart +++ b/lib/views/word/word_screen.dart @@ -13,11 +13,15 @@ class WordScreen extends StatelessWidget { const WordScreen({super.key, required this.title, required this.type}); + // argument로 받은 type에 따라 다른 wordViewModel을 사용 + @override Widget build(BuildContext context) { final wordViewModel = Get.put(WordViewModel( type: type, )); + var arg = Get.arguments; + wordViewModel.currentIndex.value = arg; final PageController pageController = PageController(initialPage: wordViewModel.currentIndex.value); @@ -82,7 +86,7 @@ class WordScreen extends StatelessWidget { ); } else { return Container( - child: CreateScriptPage(), + child: const CreateScriptPage(), ); } }, @@ -90,18 +94,29 @@ class WordScreen extends StatelessWidget { const Spacer(), // wordViewModel final String video로 영상 유튜브 링크를 바로 볼 수 있게 하기 ElevatedButton( + style: ButtonStyle( + padding: MaterialStateProperty.all( + const EdgeInsets.fromLTRB(30, 10, 30, 10), + ), + backgroundColor: MaterialStateProperty.all(ColorSystem.main2), + ), onPressed: () async { - await wordViewModel.markWordAsDone(wordViewModel - .wordList[wordViewModel.currentIndex.value].wordCard); - + // isLast Get.dialog( AlertDialog( title: const Text('학습 완료'), content: const Text('다음으로 넘어가려면 아래 버튼을 눌러주세요.'), actions: [ ElevatedButton( - onPressed: () { - Get.back(); + // button style + onPressed: () async { + await wordViewModel.markWordAsDone(wordViewModel + .wordList[wordViewModel.currentIndex.value] + .wordCard); + wordViewModel.currentIndex.value < + wordViewModel.wordList.length - 1 + ? Get.back() + : Get.offAllNamed('/'); if (wordViewModel.currentIndex.value < wordViewModel.wordList.length - 1) { pageController.animateToPage( @@ -113,13 +128,24 @@ class WordScreen extends StatelessWidget { wordViewModel.currentIndex.value + 1; } }, - child: const Text('다음으로 넘어가기'), + // 마지막 단어일 경우 홈으로 이동 + + child: Text(wordViewModel.currentIndex.value < + wordViewModel.wordList.length - 1 + ? '다음 단어' + : '홈으로 이동'), ), ], ), ); }, - child: const Text("학습 완료"), + child: const Text( + "학습 완료", + style: TextStyle( + color: ColorSystem.white, + fontSize: 16.0, + ), + ), ), const SizedBox(height: 80), ],