diff --git a/ios/Podfile.lock b/ios/Podfile.lock index d84a2de..70e2f96 100644 --- a/ios/Podfile.lock +++ b/ios/Podfile.lock @@ -52,4 +52,4 @@ SPEC CHECKSUMS: PODFILE CHECKSUM: 819463e6a0290f5a72f145ba7cde16e8b6ef0796 -COCOAPODS: 1.14.3 +COCOAPODS: 1.15.2 diff --git a/lib/main.dart b/lib/main.dart index 529ed7d..ee0ea87 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -21,7 +21,7 @@ void main() async { final OnboardingViewModel viewmodel = Get.find(); final onboardingCompleted = await viewmodel.isOnboardingCompleted(); //온보딩 다시 하고싶을때 취소 - viewmodel.clearOnboardingStatus(); + // viewmodel.clearOnboardingStatus(); runApp(MainApp(initialRoute: "/", onboardingCompleted: onboardingCompleted)); } diff --git a/lib/models/home/chapter.dart b/lib/models/home/chapter.dart index cea0770..b00af83 100644 --- a/lib/models/home/chapter.dart +++ b/lib/models/home/chapter.dart @@ -2,6 +2,7 @@ class HomeChapter { final int chapterId; final String chapterNumber; final String chapterName; + final String? description; final DateTime chapterCreatedAt; final List subChapters; @@ -9,6 +10,7 @@ class HomeChapter { required this.chapterId, required this.chapterNumber, required this.chapterName, + this.description, required this.chapterCreatedAt, this.subChapters = const [], // 기본값을 빈 리스트로 설정 }); @@ -18,12 +20,11 @@ class HomeChapter { chapterId: json['chapterId'], chapterNumber: json['chapterNumber'], chapterName: json['chapterName'], + description: json['chapterDescription'] as String?, chapterCreatedAt: DateTime.parse(json['chapterCreatedAt']), subChapters: json['subChapters'] != null - ? (json['subChapters'] as List) - .map((subChapter) => HomeChapter.fromJson(subChapter)) - .toList() + ? (json['subChapters'] as List).map((subChapter) => HomeChapter.fromJson(subChapter)).toList() : [], // 'subChapters'가 null이면 빈 리스트 반환 ); } -} \ No newline at end of file +} diff --git a/lib/models/onboarding/onboarding_model.dart b/lib/models/onboarding/onboarding_model.dart index eea7ba6..a371d0d 100644 --- a/lib/models/onboarding/onboarding_model.dart +++ b/lib/models/onboarding/onboarding_model.dart @@ -1,4 +1,3 @@ - class OnUserModel { String name; String bornedAt; diff --git a/lib/services/chatting/chatting_service.dart b/lib/services/chatting/chatting_service.dart index 782bde7..407d49b 100644 --- a/lib/services/chatting/chatting_service.dart +++ b/lib/services/chatting/chatting_service.dart @@ -51,6 +51,23 @@ class ChattingService extends GetxService { } } + Future> getChaptersInfo() async { + final response = await http.get( + Uri.parse('$baseUrl/autobiographies/chapters?size=100'), // TODO: 추후 페이징 처리 필요 + headers: { + 'Content-Type': 'application/json; charset=UTF-8', + 'Authorization': 'Bearer $token', + }, + ); + + if (response.statusCode == 200) { + final Map data = json.decode(response.body); + return data; + } else { + throw Exception('자서전 정보를 불러오는 중 오류가 발생했습니다. 상태 코드: ${response.statusCode}'); + } + } + /// Autobiography가 생성되었는 지 확인 Future<(int?, int?)> checkAutobiography(int chapterId) async { // 전체 자서전 목록 조회 후, 해당 chapterId가 있는지 확인 @@ -106,34 +123,135 @@ class ChattingService extends GetxService { throw Exception('대화 저장 중 오류가 발생했습니다. 상태 코드: ${response.statusCode}'); } } catch (e) { - throw Exception('대화 저장 중 오류가 발생했습니다: $e'); + throw Exception('대화 저장 중 오류가 발생했습��다: $e'); } } - Future createAutobiography(HomeChapter chapter) async { + Future createAutobiography(HomeChapter chapter) async { + List interviewQuestions = await generateInterviewQuestions(chapter); + try { + print('질문 개수: ${interviewQuestions.length}'); + + print('요청 본문:'); + print(jsonEncode({ + 'title': chapter.chapterName, + 'content': chapter.description, + 'interviewQuestions': interviewQuestions + .map((question) => { + 'order': interviewQuestions.indexOf(question) + 1, + 'questionText': question, + }) + .toList(), + })); + final response = await http.post(Uri.parse('$baseUrl/autobiographies'), headers: { 'Content-Type': 'application/json; charset=UTF-8', 'Authorization': 'Bearer $token', }, body: jsonEncode({ - 'chapterId': chapter.chapterId, 'title': chapter.chapterName, - // 'content': chapter., + 'content': chapter.description, + 'interviewQuestions': interviewQuestions + .map((question) => { + 'order': interviewQuestions.indexOf(question) + 1, + 'questionText': question.length > 60 ? '${question.substring(0, 60)}...' : question, + }) + .toList(), })); if (response.statusCode == 201) { - final Map data = json.decode(response.body); - return data['autobiographyId'] as int; + // TODO: 로직 변경으로 인해 주석 처리, 노션 참고 + // if (response.body.isEmpty) { + // print('경고: 자서전 생성 성공. 그러나 응답 본문이 비어 있음. - chatting_service.dart, createAutobiography()'); + // final autobiographyId = await fetchAutobiographyId(chapter); + // print('응답 본문이 비어 있어 찾아온 자서전 ID: $autobiographyId'); + // return autobiographyId; + // } + // final Map data = json.decode(response.body); + print('자서전 생성 성공: ${utf8.decode(response.bodyBytes)}'); + // return data['autobiographyId'] as int; } else { + print(utf8.decode(response.bodyBytes)); throw Exception('자서전 생성 중 오류가 발생했습니다. 상태 코드: ${response.statusCode}'); } } catch (e) { + print(e); throw Exception('자서전 생성 중 오류가 발생했습니다: $e'); } } + Future> generateInterviewQuestions(HomeChapter chapter) async { + final url = '$aiUrl/interviews/interview-questions'; + await fetchUserInfo(); + final chaptersInfo = await getChaptersInfo(); + Map chapterInfo = {}; + Map subChapterInfo = {}; + + for (var c in chaptersInfo['results']) { + for (var subChapter in c['subChapters']) { + if (subChapter['chapterId'] == chapter.chapterId) { + chapterInfo = { + 'title': chapter.chapterName, + 'description': chapter.description ?? "", + }; + subChapterInfo = { + 'title': subChapter['chapterName'], + 'description': subChapter['chapterDescription'] ?? "", + }; + break; + } + } + if (chapterInfo.isNotEmpty) break; + } + if (chapterInfo.isEmpty || subChapterInfo.isEmpty) { + throw Exception('해당 챕터 정보를 찾을 수 없습니다.'); + } + + final body = jsonEncode({ + 'user_info': { + "user_name": userInfo['name'], + "date_of_birth": userInfo['bornedAt'], + "gender": userInfo['gender'], + // "has_children": userInfo['hasChildren'], // TODO: api 수정 후 다시 추가 + "occupation": bodyinfo["occupation"], + "education_level": bodyinfo["education_level"], + "marital_status": bodyinfo["marital_status"], + }, + 'chapter_info': { + 'title': chapterInfo['title'], + 'description': chapterInfo['description'], + }, + 'sub_chapter_info': { + 'title': subChapterInfo['title'], + 'description': subChapterInfo['description'], + } + }); + + try { + final response = await http.post( + Uri.parse(url), + headers: { + 'Content-Type': 'application/json; charset=UTF-8', + 'Authorization': 'Bearer $token', + }, + body: body, + ); + + if (response.statusCode == 200) { + final Map data = json.decode(utf8.decode(response.bodyBytes)); + print("성공적으로 인터뷰 질문 생성: $data"); + return List.from(data['interview_questions']); + } else { + print(utf8.decode(response.bodyBytes)); + throw Exception('질문 생성 중 오류가 발생했습니다. 상태 코드: ${response.statusCode}'); + } + } catch (e) { + throw Exception('질문 생성 중 오류가 발생했습니다: $e'); + } + } + // TODO: Pagination에 따른 Paging 구현 필요 Future> getConversations(int interviewId, int page, int size) async { try { @@ -165,7 +283,9 @@ class ChattingService extends GetxService { } } - Future getNextQuestion(List> conversations, List predefinedQuestions, HomeChapter chapter) async { + /// 사용자 정보 가져오기 + /// TODO: 추후 유저 정보를 전역에서 받아올 수 있게 되면, 삭제 필요 + Future fetchUserInfo() async { if (userInfo.isEmpty) { final response = await http.get(Uri.parse('$baseUrl/members/me'), headers: { 'Content-Type': 'application/json; charset=UTF-8', @@ -178,9 +298,18 @@ class ChattingService extends GetxService { userInfo['bornedAt'] = data['bornedAt']; userInfo['gender'] = data['gender']; userInfo['hasChildren'] = data['hasChildren']; + } else { + throw Exception('사용자 정보를 가져오는 데 실패했습니다. 상태 코드: ${response.statusCode}'); } - print("userInfo: $userInfo"); + + final Map responseData = json.decode(utf8.decode(response.bodyBytes)); + print('응답 데이터: $responseData'); } + } + + // TODO: 추후 온보딩 화면에서 받아오는 값으로 변경 필요 + Future getNextQuestion(List> conversations, List predefinedQuestions, HomeChapter chapter) async { + await fetchUserInfo(); final url = '$aiUrl/interviews/interview-chat'; final body = jsonEncode({ @@ -242,6 +371,65 @@ class ChattingService extends GetxService { }; }).toList(); } + + Future fetchAutobiographyId(HomeChapter chapter) async { + try { + final autobiographyResponse = await http.get( + Uri.parse('$baseUrl/autobiographies'), + headers: { + 'Authorization': 'Bearer $token', + 'Content-Type': 'application/json', + }, + ); + + if (autobiographyResponse.statusCode == 200) { + final Map autobiographyData = json.decode(autobiographyResponse.body); + final List results = autobiographyData['results']; + + // chapterId와 일치하는 자서전 찾기 + final matchingAutobiography = results.firstWhere( + (autobiography) => autobiography['chapterId'] == chapter.chapterId, + orElse: () => null, + ); + + if (matchingAutobiography != null) { + return matchingAutobiography['autobiographyId']; + } else { + print('경고: 해당 챕터에 대한 자서전을 찾을 수 없습니다.'); + return -1; + } + } else { + print('자서전 정보를 가져오는 데 실패했습니다. 상태 코드: ${autobiographyResponse.statusCode}'); + return -1; + } + } catch (e) { + print('자서전 ID를 가져오는 중 오류 발생: $e'); + return -1; + } + } + + /// 인터뷰 질문 index (사전 생성 질문) 다음으로 넘기기 + Future moveToNextQuestionIndex(int interviewId) async { + try { + final response = await http.post( + Uri.parse('$baseUrl/interviews/$interviewId/questions/current-question'), + headers: { + 'Authorization': 'Bearer $token', + 'Content-Type': 'application/json', + }, + ); + + if (response.statusCode == 200) { + print('사전 질문 인덱스를 다음으로 이동했습니다.'); + } else { + print('사전 질문 인덱스를 다음으로 이동하는데 실패했습니다. 상태 코드: ${response.statusCode}'); + throw Exception('다음 질문으로 이동 실패'); + } + } catch (e) { + print('사전 질문 인덱스를 다음으로 이동 중 오류 발생: $e'); + throw Exception('다음 질문으로 이동 중 오류 발생'); + } + } } List getLastTwo(List list) { diff --git a/lib/services/home/chapter_service.dart b/lib/services/home/chapter_service.dart index 814993b..3e98f8a 100644 --- a/lib/services/home/chapter_service.dart +++ b/lib/services/home/chapter_service.dart @@ -26,9 +26,7 @@ class HomeChapterService { print(token); if (response.statusCode == 200) { var data = jsonDecode(utf8.decode(response.bodyBytes)); - // currentChapterId = data['currentChapterId']; - //TODO: currentChapterId 상수값 변경 필요 - currentChapterId = 51; + currentChapterId = data['currentChapterId']; print('Current Chapter ID: $currentChapterId'); return (data['results'] as List).map((chapterJson) => HomeChapter.fromJson(chapterJson)).toList(); diff --git a/lib/services/onboarding/onboarding_service.dart b/lib/services/onboarding/onboarding_service.dart index c9d4ab3..2055fe5 100644 --- a/lib/services/onboarding/onboarding_service.dart +++ b/lib/services/onboarding/onboarding_service.dart @@ -68,9 +68,6 @@ class OnboardingApiService { /// 챕터 생성 Future createChapter() async { - if (chapterTimeline == null) throw Exception('chapterTimeline이 비어있습니다.'); - - // chapterTimeline 데이터를 변환 List> chapters = []; for (int i = 0; i < chapterTimeline.length; i++) { var chapter = chapterTimeline[i]; @@ -82,6 +79,7 @@ class OnboardingApiService { subchapters.add({ 'number': '${i + 1}.${j + 1}', 'name': event['event_title'], + 'description': event['event_description'], }); } // 메인 챕터 변환 @@ -89,6 +87,7 @@ class OnboardingApiService { 'number': '${i + 1}', 'name': chapter['chapter_title'], 'subchapters': subchapters, + 'description': chapter['description'], }); } diff --git a/lib/viewModels/chatting/chatting_viewmodel.dart b/lib/viewModels/chatting/chatting_viewmodel.dart index fff0885..412dcdc 100644 --- a/lib/viewModels/chatting/chatting_viewmodel.dart +++ b/lib/viewModels/chatting/chatting_viewmodel.dart @@ -15,9 +15,12 @@ class ChattingViewModel extends GetxController { final RxList chatBubbles = [].obs; final RxList conversations = [].obs; final RxBool isLoading = true.obs; + final RxBool isInterviewFinished = false.obs; // 사전에 생성한 질문 리스트 (예시) - List predefinedQuestions = []; + List predefinedQuestions = []; + int currentPredefinedQuestionIndex = 0; + int additionalQuestionCount = 0; int currentQuestionId = 1; HomeChapter? currentChapter; @@ -49,8 +52,14 @@ class ChattingViewModel extends GetxController { autobiographyId = autoid; interviewId = intid; - // TODO: 없으면 생성 (온보딩에서 생성 시 없을 수 없음) => 추후 온보딩과 함께 수정 필요 - autobiographyId ??= await _apiService.createAutobiography(currentChapter); + // autobiography 존재하지 않으면 생성 후 다시 체크 + if (autoid == null) { + await _apiService.createAutobiography(currentChapter); + (autoid, intid) = await _apiService.checkAutobiography(chapterId); + autobiographyId = autoid; + interviewId = intid; + } + if (autobiographyId == null || interviewId == null) { throw Exception('자서전 생성 실패'); } @@ -58,7 +67,11 @@ class ChattingViewModel extends GetxController { final loadedConversations = await _apiService.getConversations(interviewId!, page, size); final loadedQuestions = await _apiService.getInterview(interviewId!); conversations.value = loadedConversations; - predefinedQuestions = loadedQuestions['results']; + predefinedQuestions = loadedQuestions['results'].map((q) => q['questionText'] as String).toList(); + currentPredefinedQuestionIndex = loadedQuestions['currentQuestionId'] - loadedQuestions['results'][0]['questionId']; + additionalQuestionCount = conversations.where((conv) => conv.conversationType == 'AI').length % 3; // 추가질문 개수 - 질문의 개수에서 3을 나누어 나머지로 할당 + print('현재 사전질문 인덱스: $currentPredefinedQuestionIndex'); + print('현재 추가질문 개수: $additionalQuestionCount'); updateChatBubbles(); } catch (e) { Get.snackbar('오류', e.toString()); @@ -79,13 +92,20 @@ class ChattingViewModel extends GetxController { if (chatBubbles.isEmpty) { conversations.add(Conversation( conversationType: 'AI', - content: predefinedQuestions.first['questionText'], + content: predefinedQuestions.first, )); - chatBubbles.add(ChatBubble(isUser: false, message: predefinedQuestions.first['questionText'], isFinal: true)); - print("첫 질문 업데이트: ${predefinedQuestions.first['questionText']}"); + chatBubbles.add(ChatBubble(isUser: false, message: predefinedQuestions.first, isFinal: true)); + currentPredefinedQuestionIndex++; // 사전 정의 질문 카운트 up + saveChatBubbles(); + print("첫 질문 업데이트: ${predefinedQuestions.first}"); } } + void saveChatBubbles() async { + // API를 통해 서버에 대화 내용 저장 + await _apiService.saveConversation(conversations, interviewId!); + } + /// 채팅 화면 아래 버튼 state 변경 Future changeMicState() async { _micStateValue.value = (_micStateValue.value + 1) % 3; @@ -166,15 +186,32 @@ class ChattingViewModel extends GetxController { try { isLoading(true); - // 현재까지의 대화 내용을 JSON 형태로 변환 - conversations.sort((a, b) => a.timestamp.compareTo(b.timestamp)); // 시간순 정렬 + conversations.sort((a, b) => a.timestamp.compareTo(b.timestamp)); final conversationsJson = conversations.map((conv) => conv.toJson()).toList(); - print(conversationsJson); - final result = await _apiService.getNextQuestion(conversationsJson, predefinedQuestions, currentChapter!); + String nextQuestion; - final String nextQuestion = result; - // TODO: isPredefined에 따라 질문이 미리 정해진 경우 처리하여 진행도 계산. + if (additionalQuestionCount < 2) { + // 사용자 정의 질문 생성 + print("질문 생성, 추가 질문 count = $additionalQuestionCount"); + final result = await _apiService.getNextQuestion(conversationsJson, predefinedQuestions, currentChapter!); + nextQuestion = result; + additionalQuestionCount++; // 미리 준비한 질문 1개당 2개를 추가 질문 할 수 있도록 + } else { + print("질문 리스트 사용, 질문 index = $currentPredefinedQuestionIndex"); + // 미리 정의된 질문 사용 + if (currentPredefinedQuestionIndex + 1 < predefinedQuestions.length) { + nextQuestion = predefinedQuestions[currentPredefinedQuestionIndex]; + currentPredefinedQuestionIndex++; + additionalQuestionCount = 0; + _apiService.moveToNextQuestionIndex(interviewId!); + } else { + // 모든 질문이 끝난 경우 + isInterviewFinished.value = true; + Get.snackbar('알림', '모든 질문이 완료되었습니다.'); + return; + } + } if (nextQuestion.isNotEmpty) { conversations.add(Conversation( @@ -182,10 +219,7 @@ class ChattingViewModel extends GetxController { content: nextQuestion, )); updateChatBubbles(); - // await _apiService.saveConversation(conversations, interviewId!); - } else { - // 더 이상 질문이 없는 경우 처리 - Get.snackbar('알림', '모든 질문이 완료되었습니다.'); + saveChatBubbles(); } } catch (e) { Get.snackbar('오류', e.toString()); diff --git a/lib/viewModels/home/home_viewmodel.dart b/lib/viewModels/home/home_viewmodel.dart index 14ab990..1676c4e 100644 --- a/lib/viewModels/home/home_viewmodel.dart +++ b/lib/viewModels/home/home_viewmodel.dart @@ -28,8 +28,25 @@ class HomeViewModel extends GetxController { } Future fetchChapters() async { - var fetchedChapters = await chapterService.fetchChapters(0, 10); + var fetchedChapters = await chapterService.fetchChapters(0, 100); chapters.value = fetchedChapters; + // print('Chapters:'); + // for (var chapter in chapters) { + // print(' Chapter ID: ${chapter.chapterId}'); + // print(' Chapter Number: ${chapter.chapterNumber}'); + // print(' Chapter Name: ${chapter.chapterName}'); + // print(' Description: ${chapter.description}'); + // print(' Created At: ${chapter.chapterCreatedAt}'); + // print(' Sub Chapters:'); + // for (var subChapter in chapter.subChapters) { + // print(' Sub Chapter ID: ${subChapter.chapterId}'); + // print(' Sub Chapter Number: ${subChapter.chapterNumber}'); + // print(' Sub Chapter Name: ${subChapter.chapterName}'); + // print(' Sub Description: ${subChapter.description}'); + // print(' Sub Created At: ${subChapter.chapterCreatedAt}'); + // } + // print(' ---'); + // } } void setCurrentChapter() { @@ -39,7 +56,7 @@ class HomeViewModel extends GetxController { currentChapter.value = _findChapterById(currentId); if (currentChapter.value != null) { print("current chapterid: ${currentChapter.value?.chapterId}"); - print('Current Chapter: ${currentChapter.value?.chapterName}'); + print('Current ChapterName: ${currentChapter.value?.chapterName}'); } else { print('No matching chapter found for the currentChapterId.'); } diff --git a/lib/viewModels/onboarding/onboarding_viewmodel.dart b/lib/viewModels/onboarding/onboarding_viewmodel.dart index 8ea02cc..c71eb37 100644 --- a/lib/viewModels/onboarding/onboarding_viewmodel.dart +++ b/lib/viewModels/onboarding/onboarding_viewmodel.dart @@ -115,7 +115,7 @@ class OnboardingViewModel extends GetxController { final prefs = await SharedPreferences.getInstance(); print("Onboarding completed: ${prefs.getBool('onboardingCompleted')}"); return prefs.getBool('onboardingCompleted') ?? false; - // return true; // TODO: 시연용 임시 + // return true; // TODO: 다른 기기, 온보딩을 완료한 계정으로 수행 시 onboarding == null인 문제 } Future clearOnboardingStatus() async { diff --git a/lib/views/chatting/chatting_screen.dart b/lib/views/chatting/chatting_screen.dart index de92ed2..7df6198 100644 --- a/lib/views/chatting/chatting_screen.dart +++ b/lib/views/chatting/chatting_screen.dart @@ -20,7 +20,13 @@ class ChattingScreen extends BaseScreen { super.initViewModel(); // TODO: Null Checking viewModel.loadConversations(currentChapter!); - print('Chatting Screen Initialized: currentChapterId: $currentChapter'); + print('Chatting Screen Initialized: currentChapterId: ${currentChapter!.chapterId}'); + + ever(viewModel.isInterviewFinished, (finished) { + if (finished) { + _showFinishModal(); + } + }); } @override