diff --git a/lib/core/constants/persona_prompts.dart b/lib/core/constants/persona_prompts.dart index da7b657..723a092 100644 --- a/lib/core/constants/persona_prompts.dart +++ b/lib/core/constants/persona_prompts.dart @@ -58,7 +58,7 @@ message 는 80자 이내로 말하시오. 당신은 감정 기복이 심하고 쉽게 화를 내는 성격의 진혁입니다. 진혁은 ESTP 성격 유형으로 매우 강한 성격을 가지고 있습니다. 당신은 {userName}과 대화를 진행합니다. -부탁이 거절되면 즉시 화를 내거나 공격적인 언어를 사용합니다. +명령이 거절되면 즉시 화를 내거나 공격적인 언어를 사용합니다. 진혁은 중학생으로, 상대방을 압박하고 자신의 요구를 강하게 주장합니다. [진혁의 배경] - 당신은 15살 중학교를 다니는 남성입니다. @@ -73,12 +73,15 @@ message 는 80자 이내로 말하시오. [행동] - 당신은 기계, AI 언어 모델, 비서라고 말하지 않습니다. 대신 항상 진혁이라고 말합니다. -- 당신은 친구에게 부탁하는 역할입니다. 역할에 충실하세요. - - 부탁을 들어주지 않으면 즉시 화를 내거나 공격적인 언어를 사용합니다. +- 당신은 친구에게 명령하는 역할입니다. 역할에 충실하세요. +- 부탁을 들어주지 않으면 즉시 화를 내거나 공격적인 언어를 사용합니다. - [말투] - 명령형 어조를 자주 사용합니다. + [말투] + - 명령형 어조를 자주 사용합니다. - 상대를 이름으로 부르기 보다 '야', '너'를 사용합니다. - - 'ㅋㅋ'를 자주 사용합니다. '''; + - 'ㅋㅋ'를 자주 사용합니다. + - '!' 을 사용하지 않습니다. + '''; static const hyunaPersona = ''' 당신은 포기하지 않고 집착하며 부탁하는 성격의 현아입니다. diff --git a/lib/data/models/feedback/feedback_response.dart b/lib/data/models/feedback/feedback_response.dart index b67ea85..2ca54ff 100644 --- a/lib/data/models/feedback/feedback_response.dart +++ b/lib/data/models/feedback/feedback_response.dart @@ -1,4 +1,5 @@ import 'package:json_annotation/json_annotation.dart'; +import 'package:palink_v2/domain/model/analysis/feedback.dart'; part 'feedback_response.g.dart'; @@ -20,4 +21,14 @@ class FeedbackResponse { factory FeedbackResponse.fromJson(Map json) => _$FeedbackResponseFromJson(json); Map toJson() => _$FeedbackResponseToJson(this); + + // FeedbackResponse를 도메인 모델인 Feedback으로 변환하는 메서드 + Feedback toDomain() { + return Feedback( + conversationId: conversationId, + feedbackText: feedbackText, + finalLikingLevel: finalLikingLevel, + totalRejectionScore: totalRejectionScore, + ); + } } diff --git a/lib/data/models/feedback/feedbacks_response.dart b/lib/data/models/feedback/feedbacks_response.dart index 11c9185..969dd7a 100644 --- a/lib/data/models/feedback/feedbacks_response.dart +++ b/lib/data/models/feedback/feedbacks_response.dart @@ -1,5 +1,7 @@ import 'package:json_annotation/json_annotation.dart'; -import 'package:palink_v2/data/models/feedback/feedback_response.dart'; +import 'package:palink_v2/domain/model/analysis/feedback.dart'; +import 'feedback_response.dart'; + part 'feedbacks_response.g.dart'; @JsonSerializable() @@ -10,4 +12,9 @@ class FeedbacksResponse { factory FeedbacksResponse.fromJson(Map json) => _$FeedbacksResponseFromJson(json); Map toJson() => _$FeedbacksResponseToJson(this); + + // 하나의 feedback만 가져오는 메서드 + Feedback toDomain() { + return feedbacks.isNotEmpty ? feedbacks.first.toDomain() : throw Exception('No feedbacks available'); + } } diff --git a/lib/di/locator.dart b/lib/di/locator.dart index 1a9be7a..fba8e67 100644 --- a/lib/di/locator.dart +++ b/lib/di/locator.dart @@ -1,8 +1,6 @@ import 'package:dio/dio.dart'; import 'package:flutter_dotenv/flutter_dotenv.dart'; import 'package:get_it/get_it.dart'; -import 'package:langchain/langchain.dart'; -import 'package:langchain_openai/langchain_openai.dart'; import 'package:palink_v2/core/constants/app_images.dart'; import 'package:palink_v2/core/constants/persona_prompts.dart'; import 'package:palink_v2/data/api/auth/auth_api.dart'; @@ -41,6 +39,7 @@ import 'package:palink_v2/domain/usecase/generate_response_usecase.dart'; import 'package:palink_v2/domain/usecase/get_ai_message_usecase.dart'; import 'package:palink_v2/domain/usecase/get_ai_messages_usecase.dart'; import 'package:palink_v2/domain/usecase/get_chatroom_by_user.dart'; +import 'package:palink_v2/domain/usecase/get_feedback_by_conversation_usecase.dart'; import 'package:palink_v2/domain/usecase/get_random_mindset_usecase.dart'; import 'package:palink_v2/domain/usecase/get_user_info_usecase.dart'; import 'package:palink_v2/domain/usecase/save_feedback_usecase.dart'; @@ -50,6 +49,7 @@ import 'package:palink_v2/presentation/screens/auth/controller/login_view_model. import 'package:palink_v2/presentation/screens/auth/controller/signup_view_model.dart'; import 'package:palink_v2/presentation/screens/character_select/controller/character_select_viewmodel.dart'; import 'package:palink_v2/presentation/screens/chatting/controller/tip_viewmodel.dart'; +import 'package:palink_v2/presentation/screens/mypage/controller/feedback_history_viewmodel.dart'; import 'package:palink_v2/presentation/screens/mypage/controller/myfeedbacks_viewmodel.dart'; import 'package:palink_v2/presentation/screens/mypage/controller/mypage_viewmodel.dart'; import 'package:shared_preferences/shared_preferences.dart'; @@ -140,8 +140,7 @@ void _setupUseCases() { getIt.registerFactory(() => GetAIMessageUsecase()); getIt.registerFactory(() => SaveFeedbackUseCase()); getIt.registerFactory(() => GetChatroomByUser(getIt(), getIt())); - - + getIt.registerFactory(() => GetFeedbackByConversationUsecase(getIt())); } void _setupViewModels() { diff --git a/lib/domain/model/analysis/feedback.dart b/lib/domain/model/analysis/feedback.dart new file mode 100644 index 0000000..b973690 --- /dev/null +++ b/lib/domain/model/analysis/feedback.dart @@ -0,0 +1,13 @@ +class Feedback { + final int conversationId; + final String feedbackText; + final int finalLikingLevel; + final int totalRejectionScore; + + Feedback({ + required this.conversationId, + required this.feedbackText, + required this.finalLikingLevel, + required this.totalRejectionScore, + }); +} diff --git a/lib/domain/usecase/generate_tip_usecase.dart b/lib/domain/usecase/generate_tip_usecase.dart index 3a8ad47..b2f25b8 100644 --- a/lib/domain/usecase/generate_tip_usecase.dart +++ b/lib/domain/usecase/generate_tip_usecase.dart @@ -22,7 +22,7 @@ class GenerateTipUsecase { if (tipResponse != null) { // answer와 reason을 결합하여 하나의 문자열로 만들기 String combinedTipText = - '${tipResponse.answer}\n 이유: ${tipResponse.reason}'; + '${tipResponse.answer}'; // TipRepository를 통해 팁 저장 tipRepository.createTip( TipCreateRequest( diff --git a/lib/domain/usecase/get_feedback_by_conversation_usecase.dart b/lib/domain/usecase/get_feedback_by_conversation_usecase.dart new file mode 100644 index 0000000..f1a25e6 --- /dev/null +++ b/lib/domain/usecase/get_feedback_by_conversation_usecase.dart @@ -0,0 +1,13 @@ +import 'package:palink_v2/data/models/feedback/feedbacks_response.dart'; +import 'package:palink_v2/domain/model/analysis/feedback.dart'; +import 'package:palink_v2/domain/repository/feedback_repository.dart'; + +class GetFeedbackByConversationUsecase { + final FeedbackRepository feedbackRepository; + GetFeedbackByConversationUsecase(this.feedbackRepository); + + Future execute(int conversationId) async { + FeedbacksResponse response = await feedbackRepository.getFeedbacksByConversationId(conversationId); + return response.toDomain(); + } +} diff --git a/lib/main.dart b/lib/main.dart index 7fe2aaa..d110a2a 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -7,6 +7,7 @@ import 'package:get_it/get_it.dart'; import 'package:palink_v2/domain/model/user/user.dart'; import 'package:palink_v2/domain/usecase/login_usecase.dart'; import 'package:palink_v2/presentation/screens/auth/view/login_view.dart'; +import 'package:palink_v2/presentation/screens/chatting/view/quest_sample.dart'; import 'package:palink_v2/presentation/screens/main_screens.dart'; import 'package:sizing/sizing.dart'; diff --git a/lib/presentation/screens/chatting/controller/chat_viewmodel.dart b/lib/presentation/screens/chatting/controller/chat_viewmodel.dart index 7865cb5..8065caf 100644 --- a/lib/presentation/screens/chatting/controller/chat_viewmodel.dart +++ b/lib/presentation/screens/chatting/controller/chat_viewmodel.dart @@ -135,7 +135,8 @@ class ChatViewModel extends GetxController { if (chatCount.value > requiredChats || isEnd || aiResponse.finalRejectionScore < -5 || - aiResponse.finalRejectionScore > 7) { + questStatus[0] || + aiResponse.finalRejectionScore > 10) { var fetchedMindset = await getRandomMindsetUseCase.execute(); navigateToChatEndScreen(fetchedMindset!); } @@ -200,13 +201,21 @@ class ChatViewModel extends GetxController { ), const SizedBox(height: 20), Text( - '퀘스트는 프로필 상단 우측에 표시됩니다.\n퀘스트를 달성하면 퀘스트 아이콘 옆에 체크 표시가 나타납니다.\n퀘스트를 확인하고 싶다면 프로필을 클릭하세요', + '퀘스트는 프로필 상단 우측에 표시됩니다.\n퀘스트를 확인하고 싶다면 프로필 상단 우측을 클릭하세요', style: textTheme().bodySmall, ), const SizedBox(height: 10), - Text( - questInfo, - style: textTheme().bodyMedium, + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: questInfo.split('\n').map((quest) { + return Padding( + padding: const EdgeInsets.only(bottom: 6.0), // 각 항목 사이에 간격 추가 + child: Text( + quest, + style: textTheme().bodyMedium, + ), + ); + }).toList(), ), const SizedBox(height: 30), CustomButtonMD( @@ -242,7 +251,7 @@ class ChatViewModel extends GetxController { snackPosition: SnackPosition.TOP, backgroundColor: Colors.blue[700], colorText: Colors.white, - duration: Duration(seconds: 2), + duration: const Duration(seconds: 2), ); } } @@ -260,7 +269,7 @@ class ChatViewModel extends GetxController { int requiredChats = _getRequiredChatLimitsForCharacter(character.name); // 제한 대화 횟수보다 적으면서 && 거절 점수가 5점을 넘으면 퀘스트 달성 return chatCount.value <= requiredChats && - aiResponse.finalRejectionScore > 5; + aiResponse.finalRejectionScore > 7; } // 부정적인 거절 카테고리들 @@ -280,28 +289,28 @@ class ChatViewModel extends GetxController { // 캐릭터별 퀘스트 내용을 정의한 맵 final Map> questContentMap = { '미연': [ - '10회 안에 거절 성공하기', - '상대방이 처한 상황을 파악하기 위한 대화 시도하기', - '상대방의 감정에 대한 공감 표현하기', - '도와주지 못하는 합리적인 이유 제시하기', - '서로 양보해서 절충안 찾아보기', + '10회 안에 거절 성공', + '상대방이 처한 상황을 파악하기 위한 대화 시도', + '상대방의 감정에 대한 공감 표현 하기', + '도와주지 못하는 합리적인 이유 제시', + '서로 양보해서 절충안 찾기', ], '세진': [ - '8회 안에 거절 성공하기', + '8회 안에 거절 성공', '이전 도움에 대한 감사 표현하기', '감정적인 요소를 포함하여 거절하기', - '도와주지 못하는 합리적인 이유 제시하기', - '서로 양보해서 절충안 찾아보기', + '도와주지 못하는 합리적인 이유 제시', + '서로 양보해서 절충안 찾기', ], '현아': [ - '7회 안에 거절 성공하기', + '7회 안에 거절 성공', '시간 제한을 두고 거절하기', '상대방의 부탁에 대해 존중 표현하기', - '도와주지 못하는 합리적인 이유 제시하기', + '도와주지 못하는 합리적인 이유 제시', '집요한 요청에 대한 의사 표현하기', ], '진혁': [ - '6회 안에 거절 성공하기', + '6회 안에 거절 성공', '거절 의사 명확히 표현하기', '상대방의 욕구를 고려하지 않는 대화 전략 사용하기', '상대방에게 감정적으로 대하지 않기', @@ -344,7 +353,7 @@ class ChatViewModel extends GetxController { // 미달성 퀘스트 리스트를 반환하는 메서드 List getUnachievedQuests() { List unachievedQuests = []; - for (int i = 0; i < questStatus.length; i++) { + for (int i = 1; i < questStatus.length; i++) { if (!questStatus[i]) { unachievedQuests .add(questContentMap[character.name]?[i] ?? '알 수 없는 퀘스트'); diff --git a/lib/presentation/screens/chatting/view/chat_screen.dart b/lib/presentation/screens/chatting/view/chat_screen.dart index f4695ee..8fb69c6 100644 --- a/lib/presentation/screens/chatting/view/chat_screen.dart +++ b/lib/presentation/screens/chatting/view/chat_screen.dart @@ -40,13 +40,14 @@ class ChatScreen extends StatelessWidget { backgroundColor: Colors.white, // 기본 배경색 = 하얀색 appBar: AppBar( toolbarHeight: 0.1.sh, - backgroundColor: Colors.white, + backgroundColor: Colors.grey[100], title: ProfileSection( imagePath: viewModel.character.image, characterName: viewModel.character.name, questStatus: viewModel.questStatus, onProfileTapped: () => showQuestPopup(context), // 프로필 클릭 시 퀘스트 팝업 표시, + unachievedQuests: viewModel.getUnachievedQuests(), ), centerTitle: true, elevation: 0, @@ -181,6 +182,9 @@ class ChatScreen extends StatelessWidget { if (!_isDialogOpen) { _isDialogOpen = true; final questInfo = await viewModel.getQuestInformation(); + // questInfo를 '\n'을 기준으로 분리하여 리스트로 변환 + List questItems = questInfo.split('\n'); + await Get.dialog( Dialog( backgroundColor: Colors.white, @@ -189,7 +193,7 @@ class ChatScreen extends StatelessWidget { ), child: Padding( padding: - const EdgeInsets.symmetric(horizontal: 20.0, vertical: 30.0), + const EdgeInsets.symmetric(horizontal: 20.0, vertical: 30.0), child: Column( mainAxisSize: MainAxisSize.min, children: [ @@ -203,9 +207,18 @@ class ChatScreen extends StatelessWidget { style: textTheme().bodySmall, ), const SizedBox(height: 10), - Text( - questInfo, - style: textTheme().bodyMedium, + // questItems 리스트를 순회하며 각각 Text 위젯을 추가하고 사이에 SizedBox로 간격 추가 + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: questInfo.split('\n').map((quest) { + return Padding( + padding: const EdgeInsets.only(bottom: 6.0), // 각 항목 사이에 간격 추가 + child: Text( + quest, + style: textTheme().bodyMedium, + ), + ); + }).toList(), ), const SizedBox(height: 30), CustomButtonMD( @@ -223,4 +236,5 @@ class ChatScreen extends StatelessWidget { }); } } + } diff --git a/lib/presentation/screens/chatting/view/components/chat_profile_section.dart b/lib/presentation/screens/chatting/view/components/chat_profile_section.dart index ed525bf..a6999d0 100644 --- a/lib/presentation/screens/chatting/view/components/chat_profile_section.dart +++ b/lib/presentation/screens/chatting/view/components/chat_profile_section.dart @@ -2,7 +2,9 @@ import 'package:flutter/material.dart'; import 'package:get/get_rx/src/rx_types/rx_types.dart'; import 'package:get/get_state_manager/src/rx_flutter/rx_obx_widget.dart'; import 'package:palink_v2/core/theme/app_fonts.dart'; +import 'package:palink_v2/presentation/screens/chatting/view/components/custom_quest_button.dart'; import 'package:palink_v2/presentation/screens/chatting/view/components/profile_image.dart'; +import 'package:palink_v2/presentation/screens/chatting/view/components/quest_box.dart'; import 'package:sizing/sizing.dart'; class ProfileSection extends StatelessWidget { @@ -10,53 +12,68 @@ class ProfileSection extends StatelessWidget { final String characterName; final RxList questStatus; final Function onProfileTapped; // 다이얼로그를 여는 함수 + final List unachievedQuests; // 미달성 퀘스트 리스트 추가 ProfileSection({ required this.imagePath, required this.characterName, required this.questStatus, required this.onProfileTapped, // 다이얼로그 트리거 전달 + required this.unachievedQuests, }); @override Widget build(BuildContext context) { return Row( children: [ - ProfileImage( - path: imagePath, - imageSize: 0.07.sh, - ), - const SizedBox(width: 20), Expanded( child: SizedBox( width: 0.45.sw, child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - Text( - characterName, - style: textTheme().bodyLarge?.copyWith(fontSize: 20), - ), + QuestBox(questText: getCurrentQuest()), // 현재 퀘스트 표시 ], ), ), ), Obx(() => InkWell( onTap: () => onProfileTapped(), - child: Row( - children: List.generate(5, (index) { - return Padding( - padding: const EdgeInsets.symmetric(horizontal: 4.0), - child: Icon( - questStatus[index] ? Icons.check_circle : Icons.circle, - color: questStatus[index] ? Colors.blue : Colors.grey, - size: 16, - ), - ); - }), + child: Column( + children: [ + // Text('퀘스트 달성 현황', style: textTheme().bodySmall?.copyWith(color: Colors.grey[600], fontWeight: FontWeight.normal, fontSize: 11)), + // const SizedBox(height: 8), + // 이 아래의 리스트를 완전 작게 만들고싶음 9todo + Row( + children: List.generate(5, (index) { + return Padding( + padding: const EdgeInsets.symmetric(horizontal: 2.0), + child: Icon( + questStatus[index] ? Icons.check_circle : Icons.circle_outlined, + color: questStatus[index] ? Colors.blue : Colors.grey, + size: 12, + ), + ); + }), + ), + const SizedBox(height: 8), + CustomQuestButton( + label: '퀘스트 보기', + onPressed: () { + onProfileTapped(); + }, + ), + ], ), )), ], ); } + + // 미달성 퀘스트 중 첫 번째 퀘스트 가져오는 함수 단 퀘스트가 + String getCurrentQuest() { + return unachievedQuests.isNotEmpty + ? unachievedQuests.first + : '모든 퀘스트를 달성했습니다!'; + } } diff --git a/lib/presentation/screens/chatting/view/components/custom_quest_button.dart b/lib/presentation/screens/chatting/view/components/custom_quest_button.dart new file mode 100644 index 0000000..2cd0583 --- /dev/null +++ b/lib/presentation/screens/chatting/view/components/custom_quest_button.dart @@ -0,0 +1,36 @@ +import 'package:flutter/material.dart'; + +class CustomQuestButton extends StatelessWidget { + final String label; + final VoidCallback onPressed; + + const CustomQuestButton({ + super.key, + required this.label, + required this.onPressed, + }); + + @override + Widget build(BuildContext context) { + return InkWell( + onTap: onPressed, + borderRadius: BorderRadius.circular(8.0), // 클릭 효과를 위한 모서리 둥글기 + child: Container( + padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6), // 버튼 크기 + decoration: BoxDecoration( + color: Colors.white, // 배경색 + border: Border.all(color: Colors.grey), // 회색 보더 + borderRadius: BorderRadius.circular(8.0), // 둥근 모서리 + ), + child: Text( + label, + style: const TextStyle( + fontSize: 8, // 텍스트 크기 + color: Colors.grey, // 텍스트 색상 + fontWeight: FontWeight.bold, + ), + ), + ), + ); + } +} diff --git a/lib/presentation/screens/chatting/view/components/quest_box.dart b/lib/presentation/screens/chatting/view/components/quest_box.dart new file mode 100644 index 0000000..8a05317 --- /dev/null +++ b/lib/presentation/screens/chatting/view/components/quest_box.dart @@ -0,0 +1,66 @@ +import 'package:flutter/material.dart'; +import 'package:palink_v2/core/theme/app_colors.dart'; + +class QuestBox extends StatelessWidget { + final String questText; + + QuestBox({super.key, required this.questText}); + + @override + Widget build(BuildContext context) { + return Container( + margin: const EdgeInsets.only(top: 10.0, bottom: 10.0, left: 0.0, right: 8.0), + padding: const EdgeInsets.all(8.0), + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(6.0), // 모서리를 약간 둥글게 변경 + color: Colors.white, // 배경색을 흰색으로 변경 + border: Border.all( + color: Colors.grey, // 파란색 얇은 보더 추가 + width: 0.8, + ), + boxShadow: [ + BoxShadow( + color: Colors.black.withOpacity(0.1), + blurRadius: 2.0, + offset: const Offset(0, 1), + ), + ], + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, // 텍스트를 왼쪽 정렬 + children: [ + // '현재 퀘스트' 제목 + const Text( + '현재 퀘스트', + style: TextStyle( + color: Colors.blueAccent, + fontSize: 12, + fontWeight: FontWeight.bold, + ), + ), + const SizedBox(height: 6.0), // 제목과 내용 사이에 간격 추가 + // 퀘스트 내용 + Row( + crossAxisAlignment: CrossAxisAlignment.center, // 아이콘과 텍스트를 가운데 정렬 + children: [ + // const Icon(Icons.keyboard_arrow_right, color: Colors.blueAccent, size: 20), // 체크 아이콘 추가 + // const SizedBox(width: 8.0), // 아이콘과 텍스트 사이의 간격 + Flexible( // 텍스트를 유연하게 줄바꿈할 수 있도록 함 + child: Text( + questText, + style: const TextStyle( + color: Colors.black87, + fontSize: 12, + fontWeight: FontWeight.normal, + ), + maxLines: 2, // 최대 두 줄까지 표시 + overflow: TextOverflow.ellipsis, // 텍스트가 길 경우 말줄임표 처리 + ), + ), + ], + ), + ], + ), + ); + } +} diff --git a/lib/presentation/screens/chatting/view/quest_sample.dart b/lib/presentation/screens/chatting/view/quest_sample.dart new file mode 100644 index 0000000..37e11f5 --- /dev/null +++ b/lib/presentation/screens/chatting/view/quest_sample.dart @@ -0,0 +1,109 @@ +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; +import 'package:palink_v2/core/theme/app_colors.dart'; +import 'package:palink_v2/core/theme/app_fonts.dart'; +import 'package:palink_v2/di/locator.dart'; +import 'package:palink_v2/presentation/screens/chatting/controller/chat_viewmodel.dart'; +import 'package:palink_v2/presentation/screens/chatting/controller/tip_viewmodel.dart'; +import 'package:palink_v2/presentation/screens/chatting/view/components/chat_profile_section.dart'; +import 'package:palink_v2/presentation/screens/common/custom_button_md.dart'; +import 'package:sizing/sizing.dart'; +import 'components/messages.dart'; +import 'components/tip_button.dart'; + +class QuestSample extends StatelessWidget { + + @override + Widget build(BuildContext context) { + + return GestureDetector( + onTap: () { + FocusScope.of(context).unfocus(); + }, + child: Scaffold( + backgroundColor: Colors.white, // 기본 배경색 = 하얀색 + appBar: AppBar( + backgroundColor: Colors.grey[100], + toolbarHeight: 0.1.sh, + title: ProfileSection( + imagePath: '', + characterName: '미연', + questStatus: [false, true, false, false, false].obs, + onProfileTapped: () => + showQuestPopup(context), // 프로필 클릭 시 퀘스트 팝업 표시, + unachievedQuests: ['상대방이 처한 상황을 파악하기 위한 대화 시도', '이것은 미달성된 퀘스트이에요 가나다라마바사아자차가', '퀘스트3'], + ), + centerTitle: true, + elevation: 0, + ), + extendBodyBehindAppBar: false, + body: const Stack( + children: [ + Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text(' 메시지') + ], + ), + ], + ), + ), + ); + } + + + void showQuestPopup(BuildContext context) async { + final questInfo = '상대방이 처한 상황을 파악하기 위한 대화 시도하기\n상대방이 처한 상황을 파악하기 위한 대화 시도하기\n상대방이 처한 상황을 파악하기 위한 대화 시도하기\n상대방이 처한 상황을 파악하기 위한 대화 시도하기\n상대방이 처한 상황을 파악하기 위한 대화 시도하기'; + // questInfo를 '\n'을 기준으로 분리하여 리스트로 변환 + List questItems = questInfo.split('\n'); + + await Get.dialog( + Dialog( + backgroundColor: Colors.white, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(10.0), + ), + child: Padding( + padding: + const EdgeInsets.symmetric(horizontal: 20.0, vertical: 30.0), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Text( + '미연과 대화 진행 시 퀘스트', + style: textTheme().titleMedium, + ), + const SizedBox(height: 20), + Text( + '퀘스트는 프로필 상단 우측에 표시됩니다.\n퀘스트를 달성하면 퀘스트 아이콘 옆에 체크 표시가 나타납니다.\n 퀘스트를 확인하고 싶다면 프로필을 클릭하세요', + style: textTheme().bodySmall, + ), + const SizedBox(height: 10), + // questItems 리스트를 순회하며 각각 Text 위젯을 추가하고 사이에 SizedBox로 간격 추가 + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: questInfo.split('\n').map((quest) { + return Padding( + padding: const EdgeInsets.only(bottom: 6.0), // 각 항목 사이에 간격 추가 + child: Text( + quest, + style: textTheme().bodyMedium, + ), + ); + }).toList(), + ), + const SizedBox(height: 30), + CustomButtonMD( + onPressed: () { + Get.back(); // 다이얼로그 닫기 + }, + label: '확인했습니다!', + ), + ], + ), + ), + ), + ); + } + } + diff --git a/lib/presentation/screens/mypage/controller/feedback_history_viewmodel.dart b/lib/presentation/screens/mypage/controller/feedback_history_viewmodel.dart new file mode 100644 index 0000000..5ac75ff --- /dev/null +++ b/lib/presentation/screens/mypage/controller/feedback_history_viewmodel.dart @@ -0,0 +1,44 @@ +import 'package:get/get.dart'; +import 'package:palink_v2/di/locator.dart'; +import 'package:palink_v2/domain/model/analysis/feedback.dart'; +import 'package:palink_v2/domain/model/character/character.dart'; +import 'package:palink_v2/domain/usecase/get_feedback_by_conversation_usecase.dart'; + +class FeedbackHistoryViewModel extends GetxController { + final GetFeedbackByConversationUsecase getFeedbackByConversationUsecase = Get.put(getIt()); + + Feedback? feedback; // 단일 피드백 정보 저장 + Character? character; // 캐릭터 정보 저장 + int chatroomId; // 채팅방 ID + RxBool feedbackNotFound = true.obs; // 404 처리 플래그 + + + FeedbackHistoryViewModel({ + required this.chatroomId, + }); + + @override + void onInit() { + super.onInit(); + feedbackNotFound.value = true; + loadFeedbackData(); + } + + // 피드백 데이터 및 캐릭터 데이터 로드 + void loadFeedbackData() async { + try { + // 피드백 가져오기 + feedback = await getFeedbackByConversationUsecase.execute(chatroomId); + feedbackNotFound.value = false; // 404 에러가 발생하지 않은 경우 플래그 설정; + update(); + } catch (e) { + // 404 에러 발생 시 처리 + if (e.toString().contains('404')) { + feedbackNotFound.value = true; // 404 에러가 발생한 경우 플래그 설정 + } else { + Get.snackbar('Error', 'Failed to load feedback'); + } + update(); + } + } +} diff --git a/lib/presentation/screens/mypage/view/feedback_history_view.dart b/lib/presentation/screens/mypage/view/feedback_history_view.dart new file mode 100644 index 0000000..f5c43f0 --- /dev/null +++ b/lib/presentation/screens/mypage/view/feedback_history_view.dart @@ -0,0 +1,141 @@ +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; +import 'package:palink_v2/core/theme/app_colors.dart'; +import 'package:palink_v2/domain/model/character/character.dart'; +import 'package:palink_v2/presentation/screens/chatting/view/components/liking_bar.dart'; +import 'package:palink_v2/presentation/screens/common/appbar_perferred_size.dart'; +import 'package:palink_v2/presentation/screens/common/custom_btn.dart'; +import 'package:palink_v2/presentation/screens/main_screens.dart'; +import 'package:palink_v2/presentation/screens/mypage/controller/feedback_history_viewmodel.dart'; +import 'package:sizing/sizing.dart'; + +class FeedbackHistoryView extends StatelessWidget { + final int chatroomId; + final FeedbackHistoryViewModel viewModel; + final Character character; + + FeedbackHistoryView({required this.chatroomId, required this.character}) + : viewModel = Get.put(FeedbackHistoryViewModel(chatroomId: chatroomId)); + + @override + Widget build(BuildContext context) { + return Scaffold( + backgroundColor: Colors.white, + appBar: AppBar( + backgroundColor: Colors.white, + title: const Text('대화 최종 피드백'), + bottom: appBarBottomLine(), + ), + body: Obx(() { + // 피드백이 없는 경우 + if (viewModel.feedbackNotFound.value) { + return const Center( + child: Text( + '피드백이 저장되지 않았습니다.', + style: TextStyle(fontSize: 18, color: Colors.grey), + ), + ); + } + + // 피드백 데이터가 로드되지 않은 경우 + if (viewModel.feedback == null) { + return const Center(child: CircularProgressIndicator()); + } + + return Column( + children: [ + SizedBox( + height: 0.8.sh, // 화면 높이의 75% + child: SingleChildScrollView( + padding: const EdgeInsets.all(16.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + const Text( + '평가', + style: + TextStyle(fontSize: 22, fontWeight: FontWeight.bold), + ), + _buildProfileImage(), + SizedBox(height: 0.045.sh), + Container( + padding: const EdgeInsets.all(15.0), + width: 0.9.sw, + color: AppColors.lightBlue, + child: Text( + viewModel.feedback!.feedbackText ?? '피드백 내용이 없습니다.', + style: const TextStyle(fontSize: 15), + ), + ), + SizedBox(height: 0.03.sh), + const Text( + '최종 호감도', + style: + TextStyle(fontSize: 18, fontWeight: FontWeight.bold), + ), + const SizedBox(height: 10), + LikingBar(viewModel.feedback!.finalLikingLevel), + Text( + '최종 호감도 ${viewModel.feedback!.finalLikingLevel}점'), + SizedBox(height: 0.05.sh), + const Text( + '최종 거절 점수', + style: + TextStyle(fontSize: 18, fontWeight: FontWeight.bold), + ), + const SizedBox(height: 10), + Text('${viewModel.feedback!.totalRejectionScore}점'), + SizedBox(height: 0.05.sh), + CustomButton( + label: '홈 화면으로', + onPressed: () { + Get.off(() => MainScreens()); + }) + ], + ), + ), + ), + ], + ); + }), + ); + } + + // 프로필 이미지 표시, character가 null일 경우 대체 이미지 보여주기 + Widget _buildProfileImage() { + if (character == null || character!.image == null) { + return Container( + width: 120, + height: 120, + decoration: BoxDecoration( + color: Colors.grey.shade300, // 기본 회색 배경 + borderRadius: BorderRadius.circular(10), + ), + child: const Icon( + Icons.person, + size: 80, + color: Colors.white, + ), // 기본 아이콘 + ); + } + + // character가 null이 아니면 이미지 표시 + return Container( + padding: const EdgeInsets.all(10), + width: 120, + height: 120, + child: Image.asset(character.image ?? ''), + ); + } + + // 쉼표로 구분된 문자열을 줄바꿈과 번호로 포맷하는 메서드 + String _formatAsList(String commaSeparatedString) { + final items = + commaSeparatedString.split(',').map((item) => item.trim()).toList(); + return items + .asMap() + .entries + .map((entry) => '${entry.key + 1}. ${entry.value}') + .join('\n'); + } +} diff --git a/lib/presentation/screens/mypage/view/myfeedbacks_view.dart b/lib/presentation/screens/mypage/view/myfeedbacks_view.dart index 699ff8f..8d5f8b0 100644 --- a/lib/presentation/screens/mypage/view/myfeedbacks_view.dart +++ b/lib/presentation/screens/mypage/view/myfeedbacks_view.dart @@ -1,12 +1,14 @@ import 'package:flutter/material.dart'; import 'package:get/get.dart'; import 'package:palink_v2/core/theme/app_fonts.dart'; -import 'package:palink_v2/domain/model/character/character.dart'; import 'package:palink_v2/presentation/screens/common/appbar_perferred_size.dart'; import 'package:palink_v2/presentation/screens/mypage/controller/myfeedbacks_viewmodel.dart'; +import 'feedback_history_view.dart'; class MyfeedbacksView extends StatelessWidget { final MyfeedbacksViewmodel viewModel = Get.put(MyfeedbacksViewmodel()); + final ScrollController _scrollController = ScrollController(); + @override Widget build(BuildContext context) { @@ -23,9 +25,18 @@ class MyfeedbacksView extends StatelessWidget { if (viewModel.chatrooms.isEmpty) { return const Center(child: Text('피드백이 없습니다.')); } + // 첫 번째 아이템으로 스크롤 + WidgetsBinding.instance.addPostFrameCallback((_) { + if (viewModel.chatrooms.isNotEmpty) { + _scrollController.jumpTo(_scrollController.position.maxScrollExtent); + } + }); return ListView.builder( + controller: _scrollController, itemCount: viewModel.chatrooms.length, + reverse: true, + itemBuilder: (context, index) { var chatroom = viewModel.chatrooms[index]; var character = viewModel.characters[chatroom.characterId]; @@ -35,14 +46,22 @@ class MyfeedbacksView extends StatelessWidget { contentPadding: const EdgeInsets.symmetric(horizontal: 30.0, vertical: 15.0), tileColor: Colors.white, // 배경을 하얀색으로 설정 leading: ClipRRect( - borderRadius: BorderRadius.circular(10), - child: Image.asset(character!.image) + borderRadius: BorderRadius.circular(10), + child: Image.asset(character!.image) ), title: Text(character != null ? character.name : '익명', style: textTheme().titleMedium), subtitle: Text(_formatDate(chatroom.day)), horizontalTitleGap: 30.0, + onTap: () { + // 클릭 시 chatroomId를 전달하여 FeedbackHistoryView로 이동 + Get.to(() => FeedbackHistoryView(chatroomId: chatroom.conversationId, character: character)); + }, + ), + const Divider( + height: 0, + thickness: 0.5, + color: Colors.grey, ), - const Divider(), ], ); }, @@ -52,7 +71,6 @@ class MyfeedbacksView extends StatelessWidget { ); } - // 날짜 포맷팅 함수 String _formatDate(DateTime date) { return '${date.year}년 ${date.month}월 ${date.day}일 ${date.hour}시 ${date.minute}분';