Skip to content

Commit

Permalink
[Feat/#250] Enhance Chat Room API (#253)
Browse files Browse the repository at this point in the history
* [Feat/#250] Add Password Field

* [Feat/#250] Fix Room List Query Error

* [Feat/#250] Add isPrivate flag and lastSenderProfileImg to ChatRespones

* [Feat/#250] Implement Infinite Scrolling

* [Feat/#250] Enhance join and leave chat room logic

* [Feat/#250] Add authenticatedEndpoints to SecurityConfig

* [Feat/#250] Add static import
  • Loading branch information
ahnsugyeong authored May 11, 2024
1 parent c30688b commit 28b41a8
Show file tree
Hide file tree
Showing 25 changed files with 312 additions and 219 deletions.

This file was deleted.

Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package com.example.waggle.domain.chat.application.message;

import com.example.waggle.domain.chat.presentation.dto.MessageDto;

public interface ChatMessageCommandService {

String createChatMessage(MessageDto chatMessageDto);

}
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
package com.example.waggle.domain.chat.application.chatMessage;
package com.example.waggle.domain.chat.application.message;

import com.example.waggle.domain.chat.persistence.entity.ChatMessage;
import com.example.waggle.domain.chat.persistence.dao.ChatMessageRepository;
import com.example.waggle.domain.chat.presentation.dto.ChatMessageDto;
import com.example.waggle.domain.chat.presentation.dto.MessageDto;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;

Expand All @@ -13,7 +13,7 @@ public class ChatMessageCommandServiceImpl implements ChatMessageCommandService
private final ChatMessageRepository chatMessageRepository;

@Override
public String createChatMessage(ChatMessageDto chatMessageDto) {
public String createChatMessage(MessageDto chatMessageDto) {
ChatMessage chatMessage = chatMessageRepository.save(chatMessageDto.toEntity());
return chatMessage.getId();
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package com.example.waggle.domain.chat.application.chatMessage;
package com.example.waggle.domain.chat.application.message;

import com.example.waggle.domain.chat.persistence.entity.ChatMessage;
import com.example.waggle.domain.member.persistence.entity.Member;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package com.example.waggle.domain.chat.application.chatMessage;
package com.example.waggle.domain.chat.application.message;

import com.example.waggle.domain.chat.persistence.dao.ChatMessageRepository;
import com.example.waggle.domain.chat.persistence.dao.ChatRoomMemberRepository;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package com.example.waggle.domain.chat.application.chatRoom;
package com.example.waggle.domain.chat.application.room;

import com.example.waggle.domain.chat.presentation.dto.ChatRoomRequest;
import com.example.waggle.domain.member.persistence.entity.Member;
Expand All @@ -9,7 +9,7 @@ public interface ChatRoomCommandService {

Long createChatRoom(Member member, ChatRoomRequest request);

Long joinChatRoom(Member member, Long chatRoomId, String password);
boolean joinChatRoom(Member member, Long chatRoomId, String password);

void leaveChatRoom(Member member, Long chatRoomId);

Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package com.example.waggle.domain.chat.application.chatRoom;
package com.example.waggle.domain.chat.application.room;

import com.example.waggle.domain.chat.persistence.dao.ChatRoomMemberRepository;
import com.example.waggle.domain.chat.persistence.dao.ChatRoomRepository;
Expand Down Expand Up @@ -39,11 +39,20 @@ private ChatRoom buildChatRoom(Member member, ChatRoomRequest request) {
}

@Override
public Long joinChatRoom(Member member, Long chatRoomId, String password) {
public boolean joinChatRoom(Member member, Long chatRoomId, String password) {
ChatRoom chatRoom = chatRoomRepository.findById(chatRoomId)
.orElseThrow(() -> new ChatRoomHandler(ErrorStatus.CHAT_ROOM_NOT_FOUND));

boolean isAlreadyMember = chatRoomMemberRepository.findByChatRoomIdAndMemberId(chatRoom.getId(), member.getId())
.isPresent();

if (isAlreadyMember) {
return false;
}

validateChatRoomPassword(chatRoom, password);
return addMemberToChatRoom(member, chatRoom).getId();
addMemberToChatRoom(member, chatRoom);
return true;
}

private void validateChatRoomPassword(ChatRoom chatRoom, String password) {
Expand All @@ -60,6 +69,7 @@ private ChatRoomMember buildChatRoomMember(Member member, ChatRoom chatRoom) {
return ChatRoomMember.builder()
.chatRoom(chatRoom)
.member(member)
.lastAccessTime(LocalDateTime.now())
.build();
}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package com.example.waggle.domain.chat.application.chatRoom;
package com.example.waggle.domain.chat.application.room;

import com.example.waggle.domain.chat.persistence.entity.ChatRoom;
import com.example.waggle.domain.member.persistence.entity.Member;
Expand All @@ -21,4 +21,6 @@ public interface ChatRoomQueryService {

String getLastMessageContent(Long chatRoomId);

String getLastSenderProfileImgUrl(Long chatRoomId);

}
Original file line number Diff line number Diff line change
@@ -1,14 +1,19 @@
package com.example.waggle.domain.chat.application.chatRoom;
package com.example.waggle.domain.chat.application.room;

import com.example.waggle.domain.chat.persistence.dao.ChatMessageRepository;
import com.example.waggle.domain.chat.persistence.dao.ChatRoomMemberRepository;
import com.example.waggle.domain.chat.persistence.dao.ChatRoomRepository;
import com.example.waggle.domain.chat.persistence.entity.ChatMessage;
import com.example.waggle.domain.chat.persistence.entity.ChatRoom;
import com.example.waggle.domain.chat.persistence.entity.ChatRoomMember;
import com.example.waggle.domain.member.persistence.dao.MemberRepository;
import com.example.waggle.domain.member.persistence.entity.Member;
import com.example.waggle.exception.object.handler.ChatRoomHandler;
import com.example.waggle.exception.payload.code.ErrorStatus;
import com.example.waggle.global.util.MediaUtil;
import java.time.ZoneId;
import java.util.List;
import java.util.Optional;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.data.domain.Page;
Expand All @@ -17,9 +22,6 @@
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.time.ZoneId;
import java.util.List;

@Slf4j
@RequiredArgsConstructor
@Transactional(readOnly = true)
Expand All @@ -29,6 +31,7 @@ public class ChatRoomQueryServiceImpl implements ChatRoomQueryService {
private final ChatRoomRepository chatRoomRepository;
private final ChatMessageRepository chatMessageRepository;
private final ChatRoomMemberRepository chatRoomMemberRepository;
private final MemberRepository memberRepository;

@Override
public List<ChatRoom> getChatRooms() {
Expand Down Expand Up @@ -70,4 +73,31 @@ public String getLastMessageContent(Long chatRoomId) {
}
return pagedChatMessage.getContent().get(0).getContent();
}
}

@Override
public String getLastSenderProfileImgUrl(Long chatRoomId) {
return findLastChatMessageByChatRoomId(chatRoomId)
.map(ChatMessage::getSenderUserUrl)
.flatMap(this::findProfileImgUrlByUserUrl)
.orElse(MediaUtil.getDefaultMemberProfileImgUrl());
}

private Optional<ChatMessage> findLastChatMessageByChatRoomId(Long chatRoomId) {
PageRequest pageRequest = PageRequest.of(0, 1);
Page<ChatMessage> pagedChatMessage = chatMessageRepository.findByChatRoomIdSortedBySendTimeDesc(chatRoomId,
pageRequest);
return pagedChatMessage.getContent().stream().findFirst();
}

private Optional<String> findProfileImgUrlByUserUrl(String userUrl) {
return memberRepository.findByUserUrl(userUrl)
.map(member -> getValidProfileImgUrlOrDefault(member));
}

private String getValidProfileImgUrlOrDefault(Member member) {
return Optional.ofNullable(member.getProfileImgUrl())
.filter(url -> !url.isEmpty())
.map(MediaUtil::appendUri)
.orElse(MediaUtil.getDefaultMemberProfileImgUrl());
}
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
package com.example.waggle.domain.chat.persistence.entity;

public enum ChatMessageType {
ENTER, EXIT, TALK
JOIN, LEAVE, TALK
}
Original file line number Diff line number Diff line change
@@ -1,33 +1,41 @@
package com.example.waggle.domain.chat.presentation.controller;

import com.example.waggle.domain.chat.application.chatMessage.ChatMessageQueryService;
import com.example.waggle.domain.chat.application.chatRoom.ChatRoomCommandService;
import com.example.waggle.domain.chat.application.chatRoom.ChatRoomQueryService;
import com.example.waggle.domain.chat.application.message.ChatMessageQueryService;
import com.example.waggle.domain.chat.application.room.ChatRoomCommandService;
import com.example.waggle.domain.chat.application.room.ChatRoomQueryService;
import com.example.waggle.domain.chat.persistence.entity.ChatMessage;
import com.example.waggle.domain.chat.persistence.entity.ChatRoom;
import com.example.waggle.domain.chat.presentation.converter.ChatConverter;
import com.example.waggle.domain.chat.presentation.dto.ChatResponse;
import com.example.waggle.domain.chat.presentation.dto.ChatResponse.*;
import com.example.waggle.domain.chat.presentation.dto.ChatRoomRequest;
import com.example.waggle.domain.member.application.MemberQueryService;
import com.example.waggle.domain.member.persistence.entity.Member;
import com.example.waggle.exception.payload.code.ErrorStatus;
import com.example.waggle.exception.payload.dto.ApiResponseDto;
import com.example.waggle.global.annotation.api.ApiErrorCodeExample;
import com.example.waggle.global.annotation.auth.AuthUser;
import com.example.waggle.exception.payload.dto.ApiResponseDto;
import com.example.waggle.exception.payload.code.ErrorStatus;
import com.example.waggle.global.util.PageUtil;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.responses.ApiResponse;
import io.swagger.v3.oas.annotations.tags.Tag;
import java.time.LocalDateTime;
import java.util.List;
import java.util.stream.Collectors;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Pageable;
import org.springframework.web.bind.annotation.*;

import java.time.LocalDateTime;
import java.util.List;
import java.util.stream.Collectors;
import org.springframework.data.domain.Sort;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.PutMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

@Slf4j
@RequiredArgsConstructor
Expand All @@ -42,6 +50,8 @@ public class ChatApiController {
private final ChatMessageQueryService chatMessageQueryService;
private final MemberQueryService memberQueryService;

private static final Sort SORT_BY_CREATED_DATE_DESC = Sort.by("createdDate").descending();

@Operation(summary = "채팅방 생성 🔑", description = "사용자가 새로운 채팅방을 생성합니다. 생성 성공 시 채팅방의 고유 ID를 반환합니다.")
@ApiErrorCodeExample({
ErrorStatus._INTERNAL_SERVER_ERROR
Expand All @@ -57,12 +67,13 @@ public ApiResponseDto<Long> createChatRoom(@AuthUser Member member, @RequestBody
ErrorStatus.CHAT_ROOM_NOT_FOUND
})
@PostMapping("/rooms/{chatRoomId}/join")
public ApiResponseDto<Long> joinChatRoom(@AuthUser Member member, @PathVariable("chatRoomId") Long chatRoomId,
@RequestParam(value = "password", required = false) String password) {
public ApiResponseDto<ChatRoomJoinDto> joinChatRoom(@AuthUser Member member,
@PathVariable("chatRoomId") Long chatRoomId,
@RequestParam(value = "password", required = false) String password) {
LocalDateTime now = LocalDateTime.now();
chatRoomCommandService.joinChatRoom(member, chatRoomId, password);
Long updatedChatRoomMemberId = chatRoomCommandService.updateLastAccessTime(member, chatRoomId, now);
return ApiResponseDto.onSuccess(updatedChatRoomMemberId);
boolean hasJoined = chatRoomCommandService.joinChatRoom(member, chatRoomId, password);
chatRoomCommandService.updateLastAccessTime(member, chatRoomId, now);
return ApiResponseDto.onSuccess(ChatConverter.toChatRoomJoinDto(hasJoined));
}

@Operation(summary = "채팅방 수정 🔑", description = "채팅방 호스트가 채팅방을 수정합니다. 호스트만 채팅방 수정 권한을 가지며, 성공적으로 수정 시 채팅방의 ID를 반환합니다.")
Expand Down Expand Up @@ -103,25 +114,16 @@ public ApiResponseDto<Boolean> leaveChatRoom(@AuthUser Member member, @PathVaria
return ApiResponseDto.onSuccess(true);
}

@Operation(summary = "채팅방 목록 전체 조회 (페이징 X)", description = "모든 채팅방의 목록을 조회합니다. (페이징 X, 테스트용)")
@ApiErrorCodeExample({
ErrorStatus._INTERNAL_SERVER_ERROR
})
@GetMapping("/rooms/all")
public ApiResponseDto<ChatResponse.ChatRoomListDto> getChatRooms() {
return ApiResponseDto.onSuccess(ChatConverter.toChatRoomListDto(chatRoomQueryService.getChatRooms()));
}

@Operation(summary = "채팅방 목록 전체 조회 (페이징 O)", description = "채팅방 목록을 조회합니다. 페이지의 크기는 9입니다.")
@ApiErrorCodeExample({
ErrorStatus._INTERNAL_SERVER_ERROR
})
@GetMapping("/rooms/paged")
public ApiResponseDto<ChatResponse.ChatRoomListDto> getPagedChatRooms(
public ApiResponseDto<ChatRoomListDto> getPagedChatRooms(
@RequestParam(name = "currentPage", defaultValue = "0") int currentPage) {
Pageable pageable = PageRequest.of(currentPage, PageUtil.CHAT_ROOM_SIZE);
Pageable pageable = PageRequest.of(currentPage, PageUtil.CHAT_ROOM_SIZE, SORT_BY_CREATED_DATE_DESC);
return ApiResponseDto.onSuccess(
ChatConverter.toChatRoomListDto(chatRoomQueryService.getPagedChatRooms(pageable).getContent()));
ChatConverter.toChatRoomListDto(chatRoomQueryService.getPagedChatRooms(pageable)));
}

@Operation(summary = "특정 채팅방 조회", description = "특정 채팅방을 조회합니다.")
Expand All @@ -130,50 +132,55 @@ public ApiResponseDto<ChatResponse.ChatRoomListDto> getPagedChatRooms(
ErrorStatus.CHAT_ROOM_NOT_FOUND
})
@GetMapping("/rooms/{chatRoomId}")
public ApiResponseDto<ChatResponse.ChatRoomDetailDto> getChatRoom(@PathVariable("chatRoomId") Long chatRoomId) {
public ApiResponseDto<ChatRoomDetailDto> getChatRoom(@PathVariable("chatRoomId") Long chatRoomId) {
return ApiResponseDto.onSuccess(
ChatConverter.toChatRoomDetailDto(chatRoomQueryService.getChatRoomById(chatRoomId)));
}

@Operation(summary = "특정 회원이 참여중인 채팅방 목록 전체 조회 (페이징 O)", description = "회원이 참여중인 채팅방 목록을 조회합니다. 페이지의 크기는 9입니다.")
@Operation(summary = "특정 회원이 참여중인 채팅방 목록 전체 조회 (페이징 O) 🔑", description = "회원이 참여중인 채팅방 목록을 조회합니다. 페이지의 크기는 9입니다.")
@ApiErrorCodeExample({
ErrorStatus._INTERNAL_SERVER_ERROR
})
@GetMapping("/rooms/active")
public ApiResponseDto<ChatResponse.ActiveChatRoomListDto> getPagedChatRoomsByMember(@AuthUser Member member,
@RequestParam(name = "currentPage", defaultValue = "0") int currentPage) {
public ApiResponseDto<ActiveChatRoomListDto> getPagedChatRoomsByMember(@AuthUser Member member,
@RequestParam(name = "currentPage", defaultValue = "0") int currentPage) {
Pageable pageable = PageRequest.of(currentPage, PageUtil.CHAT_ROOM_SIZE);
Page<ChatRoom> chatRooms = chatRoomQueryService.getPagedActiveChatRoomsByMember(member, pageable);
List<ChatResponse.ActiveChatRoomDto> activeChatRooms = chatRooms.getContent().stream()
List<ActiveChatRoomDto> activeChatRooms = chatRooms.getContent().stream()
.map(room -> buildActiveChatRoomDto(member, room))
.collect(Collectors.toList());
return ApiResponseDto.onSuccess(ChatConverter.toActiveChatRoomList(activeChatRooms));
return ApiResponseDto.onSuccess(
ChatConverter.toActiveChatRoomList(activeChatRooms, PageUtil.countNextPage(chatRooms)));
}

private ChatResponse.ActiveChatRoomDto buildActiveChatRoomDto(Member member, ChatRoom room) {
private ActiveChatRoomDto buildActiveChatRoomDto(Member member, ChatRoom room) {
long unreadCount = chatRoomQueryService.getUnreadMessagesCount(member, room.getId());
String lastMessageContent = chatRoomQueryService.getLastMessageContent(room.getId());
return ChatConverter.toActiveChatRoomDto(room, unreadCount, lastMessageContent);
String lastSenderProfileImgUrl = chatRoomQueryService.getLastSenderProfileImgUrl(room.getId());
return ChatConverter.toActiveChatRoomDto(room, unreadCount, lastMessageContent, lastSenderProfileImgUrl);
}

@Operation(summary = "특정 채팅 내역 목록 조회 (페이징 O)", description = "특정 채팅 내역 목록을 조회합니다. 페이지의 크기는 20입니다.")
@Operation(summary = "특정 채팅 내역 목록 조회 (페이징 O) 🔑", description = "특정 채팅 내역 목록을 조회합니다. 페이지의 크기는 20입니다.")
@ApiErrorCodeExample({
ErrorStatus._INTERNAL_SERVER_ERROR
})
@GetMapping("/rooms/{chatRoomId}/messages")
public ApiResponseDto<ChatResponse.ChatMessageListDto> getPagedChatMessages(@AuthUser Member member,
@PathVariable("chatRoomId") Long chatRoomId,
@RequestParam(name = "currentPage", defaultValue = "0") int currentPage) {
public ApiResponseDto<ChatMessageListDto> getPagedChatMessages(@AuthUser Member member,
@PathVariable("chatRoomId") Long chatRoomId,
@RequestParam(name = "currentPage", defaultValue = "0") int currentPage) {
Pageable pageable = PageRequest.of(currentPage, PageUtil.CHAT_MESSAGE_SIZE);
Page<ChatMessage> chatMessages = chatMessageQueryService.getPagedChatMessages(member, chatRoomId,
pageable);
List<ChatResponse.ChatMessageDto> chatMessageList = chatMessages.getContent().stream()
List<ChatMessageDto> chatMessageList = chatMessages.getContent().stream()
.map(this::buildChatMessageDto)
.collect(Collectors.toList());
return ApiResponseDto.onSuccess(ChatConverter.toChatMessageListDto(chatMessageList));
LocalDateTime now = LocalDateTime.now();
chatRoomCommandService.updateLastAccessTime(member, chatRoomId, now); // TODO 채팅 종료 시 update 필요
return ApiResponseDto.onSuccess(
ChatConverter.toChatMessageListDto(chatMessageList, PageUtil.countNextPage(chatMessages)));
}

private ChatResponse.ChatMessageDto buildChatMessageDto(ChatMessage chatMessage) {
private ChatMessageDto buildChatMessageDto(ChatMessage chatMessage) {
Member sender = memberQueryService.getMemberByUserUrl(chatMessage.getSenderUserUrl());
return ChatConverter.toChatMessageDto(chatMessage, sender);
}
Expand Down
Loading

0 comments on commit 28b41a8

Please sign in to comment.