Skip to content

Commit

Permalink
[feat #116] 채팅방 목록 조회 API (#121)
Browse files Browse the repository at this point in the history
* [feat] : 채팅 메시지 조회 DTO에 읽음 여부 필드 추가

* [test] : DTO에 읽음 여부 필드 추가 반영

* [feat] : 채팅방 목록 조회용 dto 추가

* [feat] : 채팅방 query repository 추가

* [test] : 채팅방 query repository 테스트

* [feat] : 채팅방 최근 메시지 DTO 추가

* [feat] : mongoDB로 각 채팅방별 최근 메시지 조회

* [test] : mongoDB로 각 채팅방별 최근 메시지 조회 테스트

* [feat] : 채팅방 목록 조회 DTO 추가

* [feat] : 채팅방 목록 조회 비즈니스 로직 추가

* [feat] : 채팅방 목록 조회 API 메서드 작성

* [test] : 채팅방 목록 조회 API 통합 테스트

* [style] : 코드 리포멧팅

* [feat] : 예외 메세지 수정

* [feat] : 채팅 메시지 요청 필드명 수정
  • Loading branch information
hyun2371 authored Sep 28, 2024
1 parent 66a1ce8 commit 67b4cee
Show file tree
Hide file tree
Showing 20 changed files with 400 additions and 17 deletions.
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
package com.dnd.gongmuin.chat.controller;

import java.util.List;

import org.springframework.data.domain.Pageable;
import org.springframework.http.ResponseEntity;
import org.springframework.security.core.annotation.AuthenticationPrincipal;
Expand All @@ -14,6 +16,7 @@
import com.dnd.gongmuin.chat.dto.response.AcceptChatResponse;
import com.dnd.gongmuin.chat.dto.response.ChatMessageResponse;
import com.dnd.gongmuin.chat.dto.response.ChatRoomDetailResponse;
import com.dnd.gongmuin.chat.dto.response.ChatRoomSimpleResponse;
import com.dnd.gongmuin.chat.dto.response.RejectChatResponse;
import com.dnd.gongmuin.chat.service.ChatRoomService;
import com.dnd.gongmuin.common.dto.PageResponse;
Expand Down Expand Up @@ -52,6 +55,15 @@ public ResponseEntity<ChatRoomDetailResponse> createChatRoom(
return ResponseEntity.ok(response);
}

@Operation(summary = "채팅방 목록 조회 API", description = "회원의 채팅방 목록을 조회한다.")
@GetMapping("/api/chat-rooms")
public ResponseEntity<List<ChatRoomSimpleResponse>> getChatRoomsByMember(
@AuthenticationPrincipal Member member
) {
List<ChatRoomSimpleResponse> response = chatRoomService.getChatRoomsByMember(member);
return ResponseEntity.ok(response);
}

@Operation(summary = "채팅방 조회 API", description = "채팅방 아이디로 채팅방을 조회한다.")
@GetMapping("/api/chat-rooms/{chatRoomId}")
public ResponseEntity<ChatRoomDetailResponse> createChatRoom(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ public static ChatMessageResponse toChatMessageResponse(
chatMessage.getMemberId(),
chatMessage.getContent(),
chatMessage.getType().getLabel(),
chatMessage.getIsRead(),
chatMessage.getCreatedAt().toString()
);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,6 @@ public record ChatMessageRequest(
String type,

@NotNull(message = "회원 아이디를 입력해주세요.")
Long memberId
Long senderId
) {
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ public record CreateChatRoomRequest(
@NotNull(message = "질문 게시글 아이디는 필수 입력 항목입니다.")
Long questionPostId,

@NotNull(message = "답변 아이디는 필수 입력 항목입니다.")
@NotNull(message = "답변자 아이디는 필수 입력 항목입니다.")
Long answererId
) {
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ public record ChatMessageResponse(
Long senderId,
String content,
String type,
boolean isRead,
String createdAt
) {
}
29 changes: 29 additions & 0 deletions src/main/java/com/dnd/gongmuin/chat/dto/response/ChatRoomInfo.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
package com.dnd.gongmuin.chat.dto.response;

import com.dnd.gongmuin.member.domain.JobGroup;
import com.querydsl.core.annotations.QueryProjection;

public record ChatRoomInfo(
Long chatRoomId,
Long partnerId,
String partnerNickname,
String partnerJobGroup,
int partnerProfileImageNo
) {
@QueryProjection
public ChatRoomInfo(
Long chatRoomId,
Long partnerId,
String partnerNickname,
JobGroup partnerJobGroup,
int partnerProfileImageNo
) {
this(
chatRoomId,
partnerId,
partnerNickname,
partnerJobGroup.getLabel(),
partnerProfileImageNo
);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package com.dnd.gongmuin.chat.dto.response;

import com.dnd.gongmuin.question_post.dto.response.MemberInfo;

public record ChatRoomSimpleResponse(
Long chatRoomId,
MemberInfo chatPartner,
String latestMessage,
String messageType,
String messageCreatedAt
) {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package com.dnd.gongmuin.chat.dto.response;

import java.time.LocalDateTime;

public record LatestChatMessage(
Long chatRoomId,
String content,
String type,
LocalDateTime createdAt
) {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
package com.dnd.gongmuin.chat.repository;

import java.util.List;

import org.springframework.data.domain.Sort;
import org.springframework.data.mongodb.core.MongoTemplate;
import org.springframework.data.mongodb.core.aggregation.Aggregation;
import org.springframework.data.mongodb.core.aggregation.AggregationOperation;
import org.springframework.data.mongodb.core.aggregation.AggregationResults;
import org.springframework.data.mongodb.core.query.Criteria;
import org.springframework.stereotype.Component;

import com.dnd.gongmuin.chat.dto.response.LatestChatMessage;

import lombok.RequiredArgsConstructor;

@RequiredArgsConstructor
@Component
public class ChatMessageQueryRepository {

private final MongoTemplate mongoTemplate;

public List<LatestChatMessage> findLatestChatByChatRoomIds(List<Long> chatRoomIds) {
AggregationOperation sort = Aggregation.sort(Sort.Direction.DESC, "createdAt");
AggregationOperation match = Aggregation.match(Criteria.where("chatRoomId").in(chatRoomIds));
AggregationOperation group = Aggregation.group("chatRoomId")
.first("createdAt").as("createdAt")
.first("content").as("content")
.first("type").as("type");
AggregationOperation project = Aggregation.project()
.and("_id").as("chatRoomId")
.and("createdAt").as("createdAt")
.and("content").as("content")
.and("type").as("type");
Aggregation aggregation = Aggregation.newAggregation(sort, match, group, project);
AggregationResults<LatestChatMessage> results = mongoTemplate.aggregate(aggregation, "chat_messages",
LatestChatMessage.class);
return results.getMappedResults();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package com.dnd.gongmuin.chat.repository;

import java.util.List;

import com.dnd.gongmuin.chat.dto.response.ChatRoomInfo;
import com.dnd.gongmuin.member.domain.Member;

public interface ChatRoomQueryRepository {
List<ChatRoomInfo> getChatRoomsByMember(Member member);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
package com.dnd.gongmuin.chat.repository;

import static com.dnd.gongmuin.chat.domain.QChatRoom.*;

import java.util.List;

import com.dnd.gongmuin.chat.dto.response.ChatRoomInfo;
import com.dnd.gongmuin.chat.dto.response.QChatRoomInfo;
import com.dnd.gongmuin.member.domain.Member;
import com.querydsl.core.types.dsl.CaseBuilder;
import com.querydsl.jpa.impl.JPAQueryFactory;

import lombok.RequiredArgsConstructor;

@RequiredArgsConstructor
public class ChatRoomQueryRepositoryImpl implements ChatRoomQueryRepository {
private final JPAQueryFactory queryFactory;

public List<ChatRoomInfo> getChatRoomsByMember(Member member) {
return queryFactory
.select(new QChatRoomInfo(
chatRoom.id,
new CaseBuilder()
.when(chatRoom.inquirer.id.eq(member.getId()))
.then(chatRoom.answerer.id)
.otherwise(chatRoom.inquirer.id),
new CaseBuilder()
.when(chatRoom.inquirer.id.eq(member.getId()))
.then(chatRoom.answerer.nickname)
.otherwise(chatRoom.inquirer.nickname),
new CaseBuilder()
.when(chatRoom.inquirer.id.eq(member.getId()))
.then(chatRoom.answerer.jobGroup)
.otherwise(chatRoom.inquirer.jobGroup),
new CaseBuilder()
.when(chatRoom.inquirer.id.eq(member.getId()))
.then(chatRoom.answerer.profileImageNo)
.otherwise(chatRoom.inquirer.profileImageNo)
))
.from(chatRoom)
.where(chatRoom.inquirer.id.eq(member.getId())
.or(chatRoom.answerer.id.eq(member.getId())))
.fetch();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,5 +4,5 @@

import com.dnd.gongmuin.chat.domain.ChatRoom;

public interface ChatRoomRepository extends JpaRepository<ChatRoom, Long> {
public interface ChatRoomRepository extends JpaRepository<ChatRoom, Long>, ChatRoomQueryRepository {
}
Original file line number Diff line number Diff line change
Expand Up @@ -24,10 +24,10 @@ public ChatMessageResponse saveChatMessage(
ChatMessageRequest request,
Long chatRoomId
) {
Long memberId = request.memberId();
Long senderId = request.senderId();
ChatMessage chatMessage = chatMessageRepository.save(
ChatMessageMapper.toChatMessage(request, chatRoomId, memberId));
log.info("chatRoomId = {}, memberId= {}, chatMessageId= {}", chatRoomId, memberId, chatMessage.getId());
ChatMessageMapper.toChatMessage(request, chatRoomId, senderId));
log.info("chatRoomId = {}, senderId= {}, chatMessageId= {}", chatRoomId, senderId, chatMessage.getId());
return ChatMessageMapper.toChatMessageResponse(chatMessage);
}
}
41 changes: 41 additions & 0 deletions src/main/java/com/dnd/gongmuin/chat/service/ChatRoomService.java
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
package com.dnd.gongmuin.chat.service;

import java.util.Comparator;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.stream.Collectors;

import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Slice;
Expand All @@ -14,8 +18,12 @@
import com.dnd.gongmuin.chat.dto.response.AcceptChatResponse;
import com.dnd.gongmuin.chat.dto.response.ChatMessageResponse;
import com.dnd.gongmuin.chat.dto.response.ChatRoomDetailResponse;
import com.dnd.gongmuin.chat.dto.response.ChatRoomInfo;
import com.dnd.gongmuin.chat.dto.response.ChatRoomSimpleResponse;
import com.dnd.gongmuin.chat.dto.response.LatestChatMessage;
import com.dnd.gongmuin.chat.dto.response.RejectChatResponse;
import com.dnd.gongmuin.chat.exception.ChatErrorCode;
import com.dnd.gongmuin.chat.repository.ChatMessageQueryRepository;
import com.dnd.gongmuin.chat.repository.ChatMessageRepository;
import com.dnd.gongmuin.chat.repository.ChatRoomRepository;
import com.dnd.gongmuin.common.dto.PageMapper;
Expand All @@ -26,6 +34,7 @@
import com.dnd.gongmuin.member.exception.MemberErrorCode;
import com.dnd.gongmuin.member.repository.MemberRepository;
import com.dnd.gongmuin.question_post.domain.QuestionPost;
import com.dnd.gongmuin.question_post.dto.response.MemberInfo;
import com.dnd.gongmuin.question_post.exception.QuestionPostErrorCode;
import com.dnd.gongmuin.question_post.repository.QuestionPostRepository;

Expand All @@ -36,6 +45,7 @@
public class ChatRoomService {

private final ChatMessageRepository chatMessageRepository;
private final ChatMessageQueryRepository chatMessageQueryRepository;
private final ChatRoomRepository chatRoomRepository;
private final MemberRepository memberRepository;
private final QuestionPostRepository questionPostRepository;
Expand Down Expand Up @@ -66,6 +76,37 @@ public ChatRoomDetailResponse createChatRoom(CreateChatRoomRequest request, Memb
);
}

@Transactional(readOnly = true)
public List<ChatRoomSimpleResponse> getChatRoomsByMember(Member member) {
// 회원 채팅방 정보 가져오기
List<ChatRoomInfo> chatRoomInfos = chatRoomRepository.getChatRoomsByMember(member);
// chatRoomId 리스트 추출
List<Long> chatRoomIds = chatRoomInfos.stream()
.map(ChatRoomInfo::chatRoomId)
.toList();
// 각 채팅방 최근 메시지 정보
List<LatestChatMessage> latestChatMessages = chatMessageQueryRepository.findLatestChatByChatRoomIds(
chatRoomIds);
// <chatRoomId, LatestMessage> -> 순서 보장 x
Map<Long, LatestChatMessage> messageMap = latestChatMessages.stream()
.collect(Collectors.toMap(LatestChatMessage::chatRoomId, message -> message));
// 최신순 정렬
return chatRoomInfos.stream()
.sorted(
Comparator.comparing((ChatRoomInfo info) -> messageMap.get(info.chatRoomId()).createdAt()).reversed())
.map(chatRoomInfo -> {
LatestChatMessage latestMessage = messageMap.get(chatRoomInfo.chatRoomId());
return new ChatRoomSimpleResponse(
chatRoomInfo.chatRoomId(),
new MemberInfo(chatRoomInfo.partnerId(), chatRoomInfo.partnerNickname(),
chatRoomInfo.partnerJobGroup(), chatRoomInfo.partnerProfileImageNo()),
latestMessage.content(),
latestMessage.type(),
latestMessage.createdAt().toString()
);
}).toList();
}

@Transactional(readOnly = true)
public ChatRoomDetailResponse getChatRoomById(Long chatRoomId, Member member) {
ChatRoom chatRoom = getChatRoomById(chatRoomId);
Expand Down
20 changes: 10 additions & 10 deletions src/main/java/com/dnd/gongmuin/member/domain/Member.java
Original file line number Diff line number Diff line change
Expand Up @@ -62,16 +62,6 @@ private Member(String nickname, String socialName, JobGroup jobGroup, JobCategor
this.role = role;
}

@Override
public String toString() {
return "MEMBER INFO{" +
"id=" + id +
", role='" + role + '\'' +
", jobGroup=" + jobGroup +
", socialEmail='" + socialEmail + '\'' +
'}';
}

public static Member of(String socialName, String socialEmail, int credit) {
return Member.builder()
.socialName(socialName)
Expand Down Expand Up @@ -103,6 +93,16 @@ public static Member of(String nickname, String socialName, JobGroup jobGroup, J
.build();
}

@Override
public String toString() {
return "MEMBER INFO{" +
"id=" + id +
", role='" + role + '\'' +
", jobGroup=" + jobGroup +
", socialEmail='" + socialEmail + '\'' +
'}';
}

public void updateSocialEmail(String socialEmail) {
this.socialEmail = socialEmail;
}
Expand Down
Loading

0 comments on commit 67b4cee

Please sign in to comment.