diff --git a/front/capstone_front/lib/main.dart b/front/capstone_front/lib/main.dart index 1a6ed5c8a9..a30395036d 100644 --- a/front/capstone_front/lib/main.dart +++ b/front/capstone_front/lib/main.dart @@ -38,6 +38,7 @@ import 'package:flutter_secure_storage/flutter_secure_storage.dart'; import 'package:fluttertoast/fluttertoast.dart'; import 'package:go_router/go_router.dart'; import 'package:provider/provider.dart'; +import 'package:shared_preferences/shared_preferences.dart'; // 앱에서 지원하는 언어 리스트 변수 final supportedLocales = [const Locale('en', 'US'), const Locale('ko', 'KR')]; @@ -61,6 +62,11 @@ Future setSetting() async { if (str == 'true') { _isLogin = true; } + // storage.write(key: "uuid", value: "GVxxcceRRFNcWS690xLo85I8pV03"); + final SharedPreferences prefs = await SharedPreferences.getInstance(); + prefs.remove("chatRoomData"); + prefs.remove("23EiDZdTsQXL6erpvmztnnekIKE3"); + prefs.remove("GVxxcceRRFNcWS690xLo85I8pV03"); } void initializeFirebase() async { diff --git a/front/capstone_front/lib/models/api_fail_response.dart b/front/capstone_front/lib/models/api_fail_response.dart index 050e65f268..eaedd97953 100644 --- a/front/capstone_front/lib/models/api_fail_response.dart +++ b/front/capstone_front/lib/models/api_fail_response.dart @@ -1,16 +1,13 @@ class ApiFailResponse { final bool success; final String message; - final String code; ApiFailResponse({ required this.success, required this.message, - required this.code, }); ApiFailResponse.fromJson(Map json) : success = json['success'] as bool, - message = json['message'] as String, - code = json['code'] as String; + message = json['message'] as String; } diff --git a/front/capstone_front/lib/models/chat_model.dart b/front/capstone_front/lib/models/chat_model.dart index 320e3c8b4e..fe07e3342d 100644 --- a/front/capstone_front/lib/models/chat_model.dart +++ b/front/capstone_front/lib/models/chat_model.dart @@ -3,10 +3,24 @@ class ChatModel { String content; String timestamp; + ChatModel({ + required this.id, + required this.content, + required this.timestamp, + }); + ChatModel.fromJson(Map json) : id = json['id'], content = json['content'], timestamp = json['timestamp']; + + Map toJson() { + return { + 'id': id, + 'content': content, + 'timestamp': timestamp, + }; + } } class ChatModelForChatList extends ChatModel { diff --git a/front/capstone_front/lib/models/chat_room_model.dart b/front/capstone_front/lib/models/chat_room_model.dart index 1611f35dcf..1e21c677fa 100644 --- a/front/capstone_front/lib/models/chat_room_model.dart +++ b/front/capstone_front/lib/models/chat_room_model.dart @@ -6,6 +6,15 @@ class ChatRoomModel { String chatRoomMessage; String chatRoomDate; + ChatRoomModel({ + required this.chatRoomId, + required this.userId, + required this.userName, + required this.lastMessageId, + required this.chatRoomMessage, + required this.chatRoomDate, + }); + ChatRoomModel.fromJson(Map json) : chatRoomId = json['chatRoomId'], userId = json['userId'], @@ -13,4 +22,15 @@ class ChatRoomModel { lastMessageId = json['lastMessageId'], chatRoomMessage = json['chatRoomMessage'], chatRoomDate = json['chatRoomDate']; + + Map toJson() { + return { + 'chatRoomId': chatRoomId, + 'userId': userId, + 'userName': userName, + 'lastMessageId': lastMessageId, + 'chatRoomMessage': chatRoomMessage, + 'chatRoomDate': chatRoomDate, + }; + } } diff --git a/front/capstone_front/lib/screens/helper/helper_chatting/helper_chatting_room.dart b/front/capstone_front/lib/screens/helper/helper_chatting/helper_chatting_room.dart index 8353fa655b..21353e6596 100644 --- a/front/capstone_front/lib/screens/helper/helper_chatting/helper_chatting_room.dart +++ b/front/capstone_front/lib/screens/helper/helper_chatting/helper_chatting_room.dart @@ -1,7 +1,15 @@ +import 'dart:convert'; +import 'dart:io'; + +import 'package:capstone_front/models/chat_model.dart'; +import 'package:capstone_front/models/chat_room_model.dart'; +import 'package:capstone_front/services/chat_service.dart'; import 'package:capstone_front/utils/bubble_painter1.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; +import 'package:flutter_secure_storage/flutter_secure_storage.dart'; +import 'package:shared_preferences/shared_preferences.dart'; class HelperChattingRoom extends StatefulWidget { const HelperChattingRoom({super.key}); @@ -11,16 +19,29 @@ class HelperChattingRoom extends StatefulWidget { } class _HelperChattingRoomState extends State { + FlutterSecureStorage storage = const FlutterSecureStorage(); final ScrollController _scrollController = ScrollController(); final TextEditingController _textController = TextEditingController(); final List _messages = []; final int _maxLines = 1; - void _sendMessage() { + String userId = "GVxxcceRRFNcWS690xLo85I8pV03"; + String userName = "partner"; + late int chatRoomId; + late int lastMessageId = 0; + late String lastMessage = ""; + late String chatRoomDate = ""; + late List messages = []; + late List chatRooms; + bool isActive = true; + + void _sendMessage() async { if (_textController.text.isNotEmpty) { + var content = _textController.text; + var newChat = await ChatService.sendChat(chatRoomId, content); + setState(() { - // Todo: _textController.text를 서버로 보내고 답변 받아오기 - _messages.insert(_messages.length, _textController.text); + messages.insert(messages.length, newChat); _textController.clear(); // 텍스트 입력 시 방금 입력된 텍스트가 보일 수 있도록 이동 @@ -29,10 +50,143 @@ class _HelperChattingRoomState extends State { duration: const Duration(milliseconds: 100), curve: Curves.easeInOut, ); + // TODO setState 안에서 이게 되나? + saveChatData(messages, userId); + }); + } + } + + // 채팅 데이터 저장 + Future saveChatData(List chats, String partner) async { + final SharedPreferences prefs = await SharedPreferences.getInstance(); + List chatData = + chats.map((chat) => json.encode(chat.toJson())).toList(); + + await prefs.setStringList(partner, chatData); + } + + // 채팅 데이터 불러오기 + Future> loadChatData(String partner) async { + final SharedPreferences prefs = await SharedPreferences.getInstance(); + List chatData = prefs.getStringList(partner) ?? []; + + return chatData + .map((chat) => ChatModel.fromJson(json.decode(chat))) + .toList(); + } + + // 채팅방 데이터 저장 + Future saveChatRoomData(List chatRooms) async { + final SharedPreferences prefs = await SharedPreferences.getInstance(); + List chatRoomData = + chatRooms.map((chatRoom) => json.encode(chatRoom.toJson())).toList(); + await prefs.setStringList('chatRoomData', chatRoomData); + } + +// 채팅방 데이터 불러오기 + Future> loadChatRoomData() async { + final SharedPreferences prefs = await SharedPreferences.getInstance(); + List chatRoomData = prefs.getStringList('chatRoomData') ?? []; + return chatRoomData + .map((chatRoom) => ChatRoomModel.fromJson(json.decode(chatRoom))) + .toList(); + } + + // 초기 채팅방을 설정함 + Future initializeChat() async { + var chatRoomList = await loadChatRoomData(); + + var flag = false; + for (var chatRoom in chatRoomList) { + // uuid로 이 사람과의 채팅이 처음인지 아닌지 판단 + print(chatRoom.userId); + print(1); + if (chatRoom.userId == userId) { + flag = true; + var chatHistory = await loadChatData(userId); + setState(() { + chatRoomId = chatRoom.chatRoomId; + lastMessageId = chatRoom.lastMessageId; + lastMessage = chatRoom.chatRoomMessage; + chatRoomDate = chatRoom.chatRoomDate; + messages = chatHistory; + }); + break; + } + } + + if (flag) { + var newChats = await ChatService.loadNewChats(chatRoomId, lastMessageId); + messages.addAll(newChats); + } else { + var newChatRoomId = await ChatService.connectChat(userId); + if (newChatRoomId == -1) { + // 이미 나와 상대와의 채팅방이 존재함 + } + var newChatRoom = ChatRoomModel( + chatRoomId: newChatRoomId, + userId: userId, + userName: userName, + lastMessageId: lastMessageId, + chatRoomMessage: lastMessage, + chatRoomDate: chatRoomDate, + ); + + setState(() { + chatRoomId = newChatRoomId; + lastMessageId = 0; + lastMessage = ""; + chatRoomDate = ""; + messages = []; + chatRoomList.add(newChatRoom); + saveChatRoomData(chatRoomList); }); } } + Future startPolling(int chatRoomId, int lastMessageId) async { + print('chatRoomId'); + print(chatRoomId); + try { + while (isActive) { + print(1); + try { + var newMessages = + await ChatService.pollingChat(chatRoomId, lastMessageId); + if (newMessages.isNotEmpty) { + print("GOT MESSAGE"); + setState(() { + messages.addAll(newMessages); + lastMessageId = messages.last.id; + }); + } + + await Future.delayed(const Duration(seconds: 3)); + } catch (e) { + print('Error while polling: $e'); + await Future.delayed(const Duration(seconds: 3)); + } + } + } catch (e) { + print('Unexpected error in polling loop: $e'); + } + } + + @override + void initState() { + super.initState(); + initializeChat().then((_) { + startPolling(chatRoomId, lastMessageId); + }); + super.initState(); + } + + @override + void dispose() { + isActive = false; + super.dispose(); + } + @override Widget build(BuildContext context) { return Scaffold( @@ -56,7 +210,7 @@ class _HelperChattingRoomState extends State { reverse: true, shrinkWrap: true, controller: _scrollController, - itemCount: _messages.length, + itemCount: messages.length, itemBuilder: (context, index) { return Padding( padding: const EdgeInsets.only( @@ -72,7 +226,7 @@ class _HelperChattingRoomState extends State { child: Padding( padding: const EdgeInsets.all(10.0), child: Text( - _messages[_messages.length - 1 - index], + messages[messages.length - 1 - index].content, style: const TextStyle(color: Colors.white), ), ), diff --git a/front/capstone_front/lib/services/chat_service.dart b/front/capstone_front/lib/services/chat_service.dart index 3fde5eea88..670a95bfa6 100644 --- a/front/capstone_front/lib/services/chat_service.dart +++ b/front/capstone_front/lib/services/chat_service.dart @@ -13,7 +13,7 @@ class ChatService { static Future connectChat(String userId) async { FlutterSecureStorage storage = const FlutterSecureStorage(); - final accessToken = storage.read(key: "accessToken"); + final accessToken = await storage.read(key: "accessToken"); final url = Uri.parse('$baseUrl/chat/connect/$userId'); final response = await http.post( @@ -26,7 +26,8 @@ class ChatService { final String decodedBody = utf8.decode(response.bodyBytes); final Map jsonMap = jsonDecode(decodedBody); - if (response.statusCode == 200) { + if (response.statusCode == 201 || response.statusCode == 200) { + // 201은 새로 생성된 경우, 200은 이미 존재했으나, 본인이 채팅리스트 화면에 가지 않아, 최신화가 되지 않은 상태라 모르는 경우 var apiSuccessResponse = ApiSuccessResponse.fromJson(jsonMap); return apiSuccessResponse.response['chat_room_id']; @@ -41,7 +42,7 @@ class ChatService { static Future> loadNewChats( int chatRoomId, int lastMessageId) async { FlutterSecureStorage storage = const FlutterSecureStorage(); - final accessToken = storage.read(key: "accessToken"); + final accessToken = await storage.read(key: "accessToken"); final url = Uri.parse('$baseUrl/chat/join/$chatRoomId/$lastMessageId'); final response = await http.get( @@ -69,14 +70,14 @@ class ChatService { var apiFailResponse = ApiFailResponse.fromJson(jsonMap); print(response.statusCode); print(apiFailResponse.message); - throw Exception("fail to connect chatroom"); + throw Exception("fail to load newChats"); } } static Future> pollingChat( int chatRoomId, int lastMessageId) async { FlutterSecureStorage storage = const FlutterSecureStorage(); - final accessToken = storage.read(key: "accessToken"); + final accessToken = await storage.read(key: "accessToken"); final url = Uri.parse('$baseUrl/chat/poll/$chatRoomId/$lastMessageId'); final response = await http.post( @@ -101,16 +102,17 @@ class ChatService { return chatInstances; } else { + print('여기'); var apiFailResponse = ApiFailResponse.fromJson(jsonMap); print(response.statusCode); print(apiFailResponse.message); - throw Exception("fail to connect chatroom"); + throw Exception("fail to poilling chats"); } } static Future sendChat(int chatRoomId, String content) async { FlutterSecureStorage storage = const FlutterSecureStorage(); - final accessToken = storage.read(key: "accessToken"); + final accessToken = await storage.read(key: "accessToken"); final url = Uri.parse('$baseUrl/chat/$chatRoomId'); final response = await http.post( @@ -119,14 +121,14 @@ class ChatService { 'Content-Type': 'application/json', 'Authorization': 'Bearer $accessToken', }, - body: { + body: jsonEncode({ "content": content, - }, + }), ); final String decodedBody = utf8.decode(response.bodyBytes); final Map jsonMap = jsonDecode(decodedBody); - if (response.statusCode == 200) { + if (response.statusCode == 201) { var apiSuccessResponse = ApiSuccessResponse.fromJson(jsonMap); var chatModel = ChatModel.fromJson(apiSuccessResponse.response); @@ -135,13 +137,13 @@ class ChatService { var apiFailResponse = ApiFailResponse.fromJson(jsonMap); print(response.statusCode); print(apiFailResponse.message); - throw Exception("fail to connect chatroom"); + throw Exception("fail to send chats"); } } static Future> loadChatRooms() async { FlutterSecureStorage storage = const FlutterSecureStorage(); - final accessToken = storage.read(key: "accessToken"); + final accessToken = await storage.read(key: "accessToken"); final url = Uri.parse('$baseUrl/chat/'); final response = await http.get( @@ -169,14 +171,14 @@ class ChatService { var apiFailResponse = ApiFailResponse.fromJson(jsonMap); print(response.statusCode); print(apiFailResponse.message); - throw Exception("fail to connect chatroom"); + throw Exception("fail to load chatRooms"); } } static Future> pollingChatList( Map myChatRooms) async { FlutterSecureStorage storage = const FlutterSecureStorage(); - final accessToken = storage.read(key: "accessToken"); + final accessToken = await storage.read(key: "accessToken"); final url = Uri.parse('$baseUrl/chat/'); final response = await http.post( @@ -208,7 +210,7 @@ class ChatService { var apiFailResponse = ApiFailResponse.fromJson(jsonMap); print(response.statusCode); print(apiFailResponse.message); - throw Exception("fail to connect chatroom"); + throw Exception("fail to polling chatList"); } } } diff --git a/front/capstone_front/pubspec.yaml b/front/capstone_front/pubspec.yaml index 85a401ddf8..764188e411 100644 --- a/front/capstone_front/pubspec.yaml +++ b/front/capstone_front/pubspec.yaml @@ -40,7 +40,7 @@ dependencies: firebase_core: ^2.27.1 firebase_auth: ^4.17.9 provider: ^6.1.2 - shared_preferences: ^2.2.2 + shared_preferences: ^2.2.3 logger: 1.4.0 fluttertoast: ^8.2.4 easy_localization: ^3.0.5