From 9d72a54d6e6207e0be749270e396f6ef329bfccd Mon Sep 17 00:00:00 2001 From: Seongyun Lee <79950005+seongyunlee@users.noreply.github.com> Date: Thu, 28 Sep 2023 00:13:50 +0900 Subject: [PATCH] Feat/get chat list improve (#64) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * ismymsg 추가 * 채팅 가져오기 학생 오류 수정 * dynamoDB 채팅 메시지 적재 로직 수정. * 메시지 보내는 과정 분기 * cherry pick 메시지 보내는 과정 분기 * offer service 채팅 보내기 프로토콜 변경 * offer service 채팅 보내기 프로토콜 변경 * offer service 채팅 보내기 프로토콜 변경 * offer service 채팅 보내기 프로토콜 변경 * offer service 채팅 보내기 프로토콜 변경 * offer service 채팅 보내기 프로토콜 변경 * console.log 추가 * console.log 추가 * console.log 추가 * console.log 추가 * console.log 추가 * console.log 추가 * chatting findOne 추가 --- src/chatting/chatting.controller.ts | 31 +------ src/chatting/chatting.repository.ts | 4 +- src/chatting/chatting.service.ts | 93 ++++++++------------ src/chatting/items/chat.list.ts | 5 -- src/offer/offer.module.ts | 3 +- src/offer/offer.service.ts | 29 +++++-- src/question/question.controller.ts | 8 ++ src/question/question.module.ts | 3 +- src/question/question.service.ts | 26 ++++-- src/socket/socket.gateway.ts | 128 +++++++++++++++++++++++----- src/tutoring/tutoring.service.ts | 3 +- 11 files changed, 199 insertions(+), 134 deletions(-) diff --git a/src/chatting/chatting.controller.ts b/src/chatting/chatting.controller.ts index 841f1f2..58ba596 100644 --- a/src/chatting/chatting.controller.ts +++ b/src/chatting/chatting.controller.ts @@ -2,7 +2,6 @@ import { AccessToken } from '../auth/entities/auth.entity'; import { ChattingService } from './chatting.service'; import { ChattingOperation } from './description/chatting.operation'; import { ChattingResponse } from './description/chatting.response'; -import { CreateChattingDto } from './dto/create-chatting.dto'; import { UpdateChattingDto } from './dto/update-chatting.dto'; import { Body, @@ -12,7 +11,6 @@ import { Headers, Param, Patch, - Post, } from '@nestjs/common'; import { ApiBearerAuth, @@ -34,39 +32,14 @@ export class ChattingController { return this.chattingService.getChatList(AccessToken.userId(headers)); } - @Post() - create( - @Headers() headers: Headers, - @Body() createChattingDto: CreateChattingDto, - ) { - /* - return this.chattingService.create( - AccessToken.userId(headers), - createChattingDto, - );*/ - } - - /* - @Post('send') - sendMessage( - @Headers() headers: Headers, - @Body() sendMessageDto: SendMessageDto, - ) { - return this.chattingService.sendMessage( - AccessToken.userId(headers), - sendMessageDto, - ); - } - */ - @Get() findAll() { return this.chattingService.findAll(); } @Get(':chattingRoomId') - findOne(@Param('chattingRoomId') id: string) { - return this.chattingService.findOne(id); + findOne(@Param('chattingRoomId') id: string, @Headers() headers: Headers) { + return this.chattingService.findOne(id, AccessToken.userId(headers)); } @Patch(':id') diff --git a/src/chatting/chatting.repository.ts b/src/chatting/chatting.repository.ts index 51fa237..151c5c0 100644 --- a/src/chatting/chatting.repository.ts +++ b/src/chatting/chatting.repository.ts @@ -62,12 +62,12 @@ export class ChattingRepository { roomId: string, senderId: string, format: string, - message?: any, + body?: any, ) { const newMessage = { sender: senderId, format: format, - body: JSON.stringify(message), + body: JSON.stringify(body), createdAt: new Date().toISOString(), }; return await this.chattingModel.update( diff --git a/src/chatting/chatting.service.ts b/src/chatting/chatting.service.ts index 04bdfb2..6981371 100644 --- a/src/chatting/chatting.service.ts +++ b/src/chatting/chatting.service.ts @@ -11,6 +11,7 @@ import { NestedChatRoomInfo, } from './items/chat.list'; import { Injectable } from '@nestjs/common'; +import { v4 as uuid } from 'uuid'; @Injectable() export class ChattingService { @@ -24,6 +25,8 @@ export class ChattingService { try { const userInfo = await this.userRepository.get(userId); + const insertedQuestions = new Set(); + const chatRooms: ChatRoom[] = await Promise.all( //Join Chatting & Question userInfo.participatingChattingRooms.map(async (roomId) => { @@ -37,70 +40,31 @@ export class ChattingService { }), ); - const chatLists = this.groupChatRoomByState(chatRooms); if (userInfo.role == 'student') { const questions = await this.questionRepository.getStudentPendingQuestions(userId); - const questionIds = []; for (let i = 0; i < questions.count; i++) { - questionIds.push(questions[i].id); + if (insertedQuestions.has(questions[i].id)) continue; + const question = questions[i]; + const questionRoom: ChatRoom = { + id: uuid(), + roomImage: question.problem.mainImage, + title: question.problem.description, + questionInfo: question, + status: ChattingStatus.pending, + isSelect: false, + questionId: question.id, + }; + chatRooms.push(questionRoom); } - chatLists.normalProposed = await this.groupNormalProposedForStudent( - chatLists.normalProposed, - questionIds, - ); } - return new Success('채팅방 목록을 불러왔습니다.', chatLists); + return new Success('채팅방 목록을 불러왔습니다.', chatRooms); } catch (error) { return new Fail(error.message); } } - async groupNormalProposedForStudent( - chatRooms: ChatRoom[], - pendingQuestionIds: string[], - ): Promise { - const result = {}; - chatRooms.forEach((chatRoom) => { - if (chatRoom.questionId in result) { - result[chatRoom.questionId].teachers.push(chatRoom); - } else { - const questionRoom: ChatRoom = { - teachers: [chatRoom], - isTeacherRoom: false, - roomImage: chatRoom.problemImage, - title: chatRoom.questionInfo.problem.description, - schoolSubject: chatRoom.schoolSubject, - schoolLevel: chatRoom.schoolLevel, - status: ChattingStatus.pending, - questionId: chatRoom.questionId, - problemImage: chatRoom.problemImage, - isSelect: false, - }; - result[chatRoom.questionId] = questionRoom; - } - }); - - for (const questionId of pendingQuestionIds) { - if (!(questionId in result)) { - const questionInfo = await this.questionRepository.getInfo(questionId); - result[questionId] = { - teachers: [], - isTeacherRoom: false, - roomImage: questionInfo.problem.mainImage, - title: questionInfo.problem.description, - isSelect: false, - status: ChattingStatus.pending, - questionId: questionId, - problemImage: questionInfo.problem.mainImage, - }; - } - } - - return Object.values(result); - } - groupChatRoomByState(chatRooms: ChatRoom[]): ChatList { const result: ChatList = { normalProposed: [], @@ -175,12 +139,8 @@ export class ChattingService { status: status, roomImage: opponentInfo.profileImage, questionId: questionInfo.id, - schoolSubject: questionInfo.problem.schoolSubject, - schoolLevel: questionInfo.problem.schoolLevel, - problemImage: questionInfo.problem.mainImage, isSelect: questionInfo.isSelect, opponentId: opponentInfo?.id, - isTeacherRoom: true, questionInfo: questionInfo, title: opponentInfo?.name, }; @@ -231,8 +191,25 @@ export class ChattingService { return await this.chattingRepository.findAll(); } - async findOne(chattingRoomId: string) { - return await this.chattingRepository.findOne(chattingRoomId); + async findOne(chattingRoomId: string, userId: string) { + try { + const room = await this.chattingRepository.findOne(chattingRoomId); + if (room.studentId == userId || room.teacherId == userId) { + const userInfo = await this.userRepository.get(userId); + const questionInfo = await this.questionRepository.getInfo( + room.questionId, + ); + const roomInfo = await this.makeChatItem( + { roomInfo: room, questionInfo }, + userInfo, + ); + return new Success('채팅방 정보를 불러왔습니다.', roomInfo); + } else { + return new Fail('해당 채팅방에 대한 권한이 없습니다.'); + } + } catch (error) { + return new Fail('해당 채팅방 정보가 없습니다.'); + } } update(id: number, updateChattingDto: UpdateChattingDto) { diff --git a/src/chatting/items/chat.list.ts b/src/chatting/items/chat.list.ts index 059985a..bdb4f2a 100644 --- a/src/chatting/items/chat.list.ts +++ b/src/chatting/items/chat.list.ts @@ -25,14 +25,9 @@ export interface ChatRoom { id?: string; title: string; roomImage: string; - problemImage?: string; opponentId?: string; - schoolSubject: string; - schoolLevel: string; isSelect: boolean; - isTeacherRoom: boolean; questionInfo?: Question; - teachers?: ChatRoom[]; questionId: string; } diff --git a/src/offer/offer.module.ts b/src/offer/offer.module.ts index 575e0ce..3110fd8 100644 --- a/src/offer/offer.module.ts +++ b/src/offer/offer.module.ts @@ -2,6 +2,7 @@ import { AgoraModule } from '../agora/agora.module'; import { ChattingRepository } from '../chatting/chatting.repository'; import { dynamooseModule } from '../config.dynamoose'; import { QuestionRepository } from '../question/question.repository'; +import { SocketModule } from '../socket/socket.module'; import { TutoringRepository } from '../tutoring/tutoring.repository'; import { UploadRepository } from '../upload/upload.repository'; import { UserRepository } from '../user/user.repository'; @@ -11,7 +12,7 @@ import { OfferService } from './offer.service'; import { Module } from '@nestjs/common'; @Module({ - imports: [dynamooseModule, AgoraModule], + imports: [dynamooseModule, AgoraModule, SocketModule], controllers: [OfferController], providers: [ OfferService, diff --git a/src/offer/offer.service.ts b/src/offer/offer.service.ts index 34107c2..1eda952 100644 --- a/src/offer/offer.service.ts +++ b/src/offer/offer.service.ts @@ -1,6 +1,7 @@ import { ChattingRepository } from '../chatting/chatting.repository'; import { QuestionRepository } from '../question/question.repository'; import { Fail, Success } from '../response'; +import { SocketGateway } from '../socket/socket.gateway'; import { UserRepository } from '../user/user.repository'; import { OfferRepository } from './offer.repository'; import { Injectable } from '@nestjs/common'; @@ -12,6 +13,7 @@ export class OfferService { private readonly userRepository: UserRepository, private readonly chattingRepository: ChattingRepository, private readonly questionRepository: QuestionRepository, + private readonly socketGateway: SocketGateway, ) {} async append(userId: string, questionId: string) { @@ -49,19 +51,29 @@ export class OfferService { const requestMessage = { text: '안녕하세요 선생님! 언제 수업 가능하신가요?', }; + console.log( + 'chatRoomId', + chatRoomId, + 'studentId', + studentId, + 'userId', + userId, + ); //TODO: redis pub/sub으로 변경 - await this.chattingRepository.sendMessage( - chatRoomId, + await this.socketGateway.sendMessageToBothUser( studentId, + userId, + chatRoomId, 'problem-image', - problemMessage, + JSON.stringify(problemMessage), ); - await this.chattingRepository.sendMessage( - chatRoomId, + await this.socketGateway.sendMessageToBothUser( studentId, + userId, + chatRoomId, 'text', - requestMessage, + JSON.stringify(requestMessage), ); return new Success('질문 대기열에 추가되었습니다.', { chatRoomId }); @@ -114,9 +126,10 @@ export class OfferService { offerTeacherId, ); //TODO: redis pub/sub으로 변경 - await this.chattingRepository.sendMessage( - teacherChatId, + await this.socketGateway.sendMessageToBothUser( userId, + offerTeacherId, + teacherChatId, 'text', '죄송합니다.\n다른 선생님과 수업을 진행하기로 했습니다.', ); diff --git a/src/question/question.controller.ts b/src/question/question.controller.ts index 7f979af..e6fb9d7 100644 --- a/src/question/question.controller.ts +++ b/src/question/question.controller.ts @@ -64,4 +64,12 @@ export class QuestionController { list() { return this.questionService.getPendingNormalQuestions(); } + + @ApiTags('Question') + @ApiBearerAuth('Authorization') + @ApiOperation(QuestionOperation.list) + @Get('question/info/:questionId') + getQuestionInfo(@Param('questionId') questionId: string) { + return this.questionService.getQuestionInfo(questionId); + } } diff --git a/src/question/question.module.ts b/src/question/question.module.ts index 23d8c20..fdeeacf 100644 --- a/src/question/question.module.ts +++ b/src/question/question.module.ts @@ -1,5 +1,6 @@ import { ChattingRepository } from '../chatting/chatting.repository'; import { dynamooseModule } from '../config.dynamoose'; +import { SocketModule } from '../socket/socket.module'; import { UploadRepository } from '../upload/upload.repository'; import { UserRepository } from '../user/user.repository'; import { QuestionController } from './question.controller'; @@ -8,7 +9,7 @@ import { QuestionService } from './question.service'; import { Module } from '@nestjs/common'; @Module({ - imports: [dynamooseModule], + imports: [dynamooseModule, SocketModule], controllers: [QuestionController], providers: [ QuestionService, diff --git a/src/question/question.service.ts b/src/question/question.service.ts index 7781333..45c5f02 100644 --- a/src/question/question.service.ts +++ b/src/question/question.service.ts @@ -1,5 +1,6 @@ import { ChattingRepository } from '../chatting/chatting.repository'; import { Fail, Success } from '../response'; +import { SocketGateway } from '../socket/socket.gateway'; import { UserRepository } from '../user/user.repository'; import { CreateNormalQuestionDto, @@ -16,6 +17,7 @@ export class QuestionService { private readonly questionRepository: QuestionRepository, private readonly chattingRepository: ChattingRepository, private readonly userRepository: UserRepository, + private readonly socketGateway: SocketGateway, ) {} /** @@ -94,18 +96,19 @@ export class QuestionService { startDateTime: createQuestionDto.requestTutoringStartTime, }; - //TODO: redis pub/sub으로 변경 - await this.chattingRepository.sendMessage( - chatRoomId, + await this.socketGateway.sendMessageToBothUser( userId, + teacherId, + chatRoomId, 'problem-image', - problemMessage, + JSON.stringify(problemMessage), ); - await this.chattingRepository.sendMessage( - chatRoomId, + await this.socketGateway.sendMessageToBothUser( userId, + teacherId, + chatRoomId, 'appoint-request', - requestMessage, + JSON.stringify(requestMessage), ); return new Success('질문이 생성되었습니다.', question); @@ -114,6 +117,15 @@ export class QuestionService { } } + async getQuestionInfo(questionId: string) { + try { + const info = await this.questionRepository.getInfo(questionId); + return new Success('질문 정보를 가져왔습니다.', info); + } catch (error) { + return new Fail(error.message); + } + } + async delete(userId: string, questionId: string) { try { await this.questionRepository.delete(userId, questionId); diff --git a/src/socket/socket.gateway.ts b/src/socket/socket.gateway.ts index 82ac698..821f01a 100644 --- a/src/socket/socket.gateway.ts +++ b/src/socket/socket.gateway.ts @@ -124,30 +124,12 @@ export class SocketGateway { .getUserFromAuthorization(client.handshake.headers) .then((user) => user.id); - // 메시지를 받을 사용자의 소켓 아이디를 가져옴 - const receiverSocketId = await this.redisRepository.get(receiverId); - const message: Message = { + console.log('test', sender, receiverId, chattingId, format, body); + // user에게 메시지 전송 + await this.sendMessageToUser( sender, - format, - body, - createdAt: new Date().toISOString(), - }; - - // 로컬 소켓 브로드캐스트 - client.broadcast - .to(receiverSocketId) - .emit('message', { chattingId, message }); - - // 레디스 Pub - await this.redisRepository.publish( - receiverSocketId, - JSON.stringify({ chattingId, message }), - ); - - // DynamoDB에 메시지 저장 - await this.chattingRepository.sendMessage( + receiverId, chattingId, - sender, format, body, ); @@ -161,6 +143,108 @@ export class SocketGateway { } } + /** + * 다른 사용자에게 메시지를 전송하는 메소드 + * @param senderId : 메시지를 보내는 사용자의 ID + * @param receiverId : 메시지를 받는 사용자의 ID + * @param chattingId : 메시지를 보내는 채팅방의 ID + * @param format : 메시지의 형식 (text, appoint-request , ...) + * @param body : 메시지의 내용 (JSON 형식 ex: { "text" : "안녕하세요" } ) + */ + async sendMessageToUser( + senderId: string, + receiverId: string, + chattingId: string, + format: string, + body: string, + ) { + const message: Message = { + sender: senderId, + format, + body, + createdAt: new Date().toISOString(), + }; + const receiverSocketId = await this.redisRepository.get(receiverId); + if (receiverSocketId != null) { + this.sendMessageToSocketClient(receiverSocketId, chattingId, message); + } else { + //FCM 메시지 보내기 + } + // TODO: 레디스 브로드캐스트 + // EC2서버에서 레디스가 잘 뿌려주는지 확인 필요 + await this.redisRepository.publish( + receiverSocketId, + JSON.stringify({ chattingId, message }), + ); + + // DynamoDB에 메시지 저장 + await this.chattingRepository.sendMessage( + chattingId, + senderId, + format, + body, + ); + } + + async sendMessageToBothUser( + senderId: string, + receiverId: string, + chattingId: string, + format: string, + body: string, + ) { + const message: Message = { + sender: senderId, + format, + body, + createdAt: new Date().toISOString(), + }; + const receiverSocketId = await this.redisRepository.get(receiverId); + + if (receiverSocketId != null) { + this.sendMessageToSocketClient(receiverSocketId, chattingId, message); + } else { + //TODO: FCM 메시지 보내기 + console.log('FCM 메시지 보내기'); + } + const senderSocketId = await this.redisRepository.get(senderId); + if (senderSocketId != null) { + this.sendMessageToSocketClient(senderSocketId, chattingId, message); + } else { + console.log('FCM 메시지 보내기'); + } + console.log( + 'sender-receiver', + senderId, + senderSocketId, + receiverId, + receiverSocketId, + ); + + // TODO: 레디스 브로드캐스트 + // EC2서버에서 레디스가 잘 뿌려주는지 확인 필요 + await this.redisRepository.publish( + receiverSocketId, + JSON.stringify({ chattingId, message }), + ); + + // DynamoDB에 메시지 저장 + await this.chattingRepository.sendMessage( + chattingId, + senderId, + format, + body, + ); + } + + sendMessageToSocketClient( + receiverSocketId: string, + chattingId: string, + message: Message, + ) { + this.server.to(receiverSocketId).emit('message', { chattingId, message }); + } + /** * 디버깅용 메소드 * 현재 서버에 접속한 클라이언트의 소켓 정보 및 레디스에 저장된 키 목록을 가져옴 diff --git a/src/tutoring/tutoring.service.ts b/src/tutoring/tutoring.service.ts index b4d0313..01c3daf 100644 --- a/src/tutoring/tutoring.service.ts +++ b/src/tutoring/tutoring.service.ts @@ -66,13 +66,14 @@ export class TutoringService { const tutoring = await this.tutoringRepository.get(question.tutoringId); + /* const userInfo = await this.userRepository.get(userId); - if (userInfo.role == 'student') { if (tutoring.status != 'going') { return new Fail('수업 시작 전입니다.'); } } + */ return new Success('과외 정보를 가져왔습니다.', { tutoring }); } catch (error) { return new Fail(error.message);