Skip to content

Commit

Permalink
feat: โœจ Receive Chatroom Detail Informations (#188)
Browse files Browse the repository at this point in the history
* fix: chatmessage data structure changed to sorted set

* fix: chat_message_service.delete() deprecated due to unnecessary

* fix: chat_message hasn't responsibility about id

* chore: change lecttuce debug level to debug in test profile due to test

* fix: change chat_message_repository from jpa interface to concrete class

* test: modify chat_message_repository test

* test: add boundary value analysis test & verify sorting with same create_at time

* refactor: add repository interface

* rename: chat_message_service save to create

* feat: add functions into chat_message_service

* feat: not_found error code added in chat_member_error_code

* feat: add three type of find method in chat_member_service

* feat: add chat_member_res_detail dto

* fix: insert joined_at field in the chat_member_res.detail dto

* feat: add chat_message response dto

* feat: chat_room_res.room_with_participants dto

* feat: chat_room_with_paritipants mapper

* feat: impl chat_room_with_participants_search_service

* style: sperate from service login hard code to contants & delete annotation

* test: chat_room_with_participants_search_service test case

* feat: add chat_room_manager for authorization

* feat: add chat_room_usecase

* feat: add get_chatroom controller & api

* docs: add information about recent_messages field's sorted in the chat_room_res

* chore: delete lecttuce log

* test: chat_room detail integration test
  • Loading branch information
psychology50 authored Nov 2, 2024
1 parent 6aa5578 commit 4fb271b
Show file tree
Hide file tree
Showing 22 changed files with 1,250 additions and 199 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
import org.springframework.http.ResponseEntity;
import org.springframework.security.core.annotation.AuthenticationPrincipal;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestParam;

Expand All @@ -39,4 +40,9 @@ public interface ChatRoomApi {
})
@ApiResponse(responseCode = "200", description = "์ฑ„ํŒ…๋ฐฉ ๊ฒ€์ƒ‰ ์„ฑ๊ณต", content = @Content(schemaProperties = @SchemaProperty(name = "chatRooms", schema = @Schema(implementation = SliceResponseTemplate.class))))
ResponseEntity<?> searchChatRooms(@Validated ChatRoomReq.SearchQuery query, @AuthenticationPrincipal SecurityUserDetails user);

@Operation(summary = "์ฑ„ํŒ…๋ฐฉ ์กฐํšŒ", method = "GET", description = "์‚ฌ์šฉ์ž๊ฐ€ ๊ฐ€์ž…ํ•œ ์ฑ„ํŒ…๋ฐฉ ์ค‘ ํŠน์ • ์ฑ„ํŒ…๋ฐฉ์˜ ์ƒ์„ธ ์ •๋ณด๋ฅผ ์กฐํšŒํ•œ๋‹ค. ์ฑ„ํŒ…๋ฐฉ์˜ ์ƒ์„ธ ์ •๋ณด์—๋Š” ์ฑ„ํŒ…๋ฐฉ์˜ ์ฐธ์—ฌ์ž ๋ชฉ๋ก๊ณผ ์ตœ๊ทผ ์ฑ„ํŒ… ๋ฉ”์‹œ์ง€ ๋ชฉ๋ก ๋“ฑ์ด ํฌํ•จ๋œ๋‹ค.")
@Parameter(name = "chatRoomId", description = "์กฐํšŒํ•  ์ฑ„ํŒ…๋ฐฉ์˜ ์‹๋ณ„์ž", example = "1", required = true)
@ApiResponse(responseCode = "200", description = "์ฑ„ํŒ…๋ฐฉ ์กฐํšŒ ์„ฑ๊ณต", content = @Content(schemaProperties = @SchemaProperty(name = "chatRoom", schema = @Schema(implementation = ChatRoomRes.RoomWithParticipants.class))))
ResponseEntity<?> getChatRoom(@PathVariable("chatRoomId") Long chatRoomId, @AuthenticationPrincipal SecurityUserDetails user);
}
Original file line number Diff line number Diff line change
Expand Up @@ -49,4 +49,10 @@ public ResponseEntity<?> searchChatRooms(@Validated ChatRoomReq.SearchQuery quer

return ResponseEntity.ok(SuccessResponse.from(CHAT_ROOM, chatRoomUseCase.searchChatRooms(user.getUserId(), query.target(), pageable)));
}

@GetMapping("/{chatRoomId}")
@PreAuthorize("isAuthenticated() and @chatRoomManager.hasPermission(#user.getUserId(), #chatRoomId)")
public ResponseEntity<?> getChatRoom(@PathVariable("chatRoomId") Long chatRoomId, @AuthenticationPrincipal SecurityUserDetails user) {
return ResponseEntity.ok(SuccessResponse.from(CHAT_ROOM, chatRoomUseCase.getChatRoomWithParticipants(user.getUserId(), chatRoomId)));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
package kr.co.pennyway.api.apis.chat.dto;

import com.fasterxml.jackson.annotation.JsonFormat;
import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
import com.fasterxml.jackson.datatype.jsr310.ser.LocalDateTimeSerializer;
import io.swagger.v3.oas.annotations.media.Schema;
import kr.co.pennyway.domain.domains.member.domain.ChatMember;
import kr.co.pennyway.domain.domains.member.type.ChatMemberRole;

import java.time.LocalDateTime;

public final class ChatMemberRes {
@Schema(description = "์ฑ„ํŒ…๋ฐฉ ์ฐธ์—ฌ์ž ์ƒ์„ธ ์ •๋ณด")
public record Detail(
@Schema(description = "์ฑ„ํŒ…๋ฐฉ ์ฐธ์—ฌ์ž ID", type = "long")
Long id,
@Schema(description = "์ฑ„ํŒ…๋ฐฉ ์ฐธ์—ฌ์ž ์ด๋ฆ„")
String name,
@Schema(description = "์ฑ„ํŒ…๋ฐฉ ์ฐธ์—ฌ์ž ์—ญํ• ")
ChatMemberRole role,
@Schema(description = "์ฑ„ํŒ…๋ฐฉ ์ฐธ์—ฌ์ž ์•Œ๋ฆผ ์„ค์ • ์—ฌ๋ถ€. ๋‚ด ์ •๋ณด๋ฅผ ์กฐํšŒํ•  ๋•Œ๋งŒ ํฌํ•จ๋ฉ๋‹ˆ๋‹ค.")
@JsonInclude(JsonInclude.Include.NON_NULL)
Boolean notifyEnabled,
@Schema(description = "์ฑ„ํŒ…๋ฐฉ ๊ฐ€์ž…์ผ")
@JsonSerialize(using = LocalDateTimeSerializer.class)
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
LocalDateTime createdAt
) {
public static Detail from(ChatMember chatMember, boolean isContainNotifyEnabled) {
return new Detail(
chatMember.getId(),
chatMember.getName(),
chatMember.getRole(),
isContainNotifyEnabled ? chatMember.isNotifyEnabled() : null,
chatMember.getCreatedAt()
);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
package kr.co.pennyway.api.apis.chat.dto;

import com.fasterxml.jackson.annotation.JsonFormat;
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
import com.fasterxml.jackson.datatype.jsr310.ser.LocalDateTimeSerializer;
import io.swagger.v3.oas.annotations.media.Schema;
import kr.co.pennyway.domain.common.redis.message.domain.ChatMessage;
import kr.co.pennyway.domain.common.redis.message.type.MessageCategoryType;
import kr.co.pennyway.domain.common.redis.message.type.MessageContentType;

import java.time.LocalDateTime;

public final class ChatRes {
@Schema(description = "์ฑ„ํŒ… ๋ฉ”์‹œ์ง€ ์ƒ์„ธ ์ •๋ณด")
public record Detail(
@Schema(description = "์ฑ„ํŒ…๋ฐฉ ID", type = "long")
Long chatRoomId,
@Schema(description = "์ฑ„ํŒ… ID", type = "long")
Long chatId,
@Schema(description = "์ฑ„ํŒ… ๋‚ด์šฉ")
String content,
@Schema(description = "์ฑ„ํŒ… ๋‚ด์šฉ ํƒ€์ž…")
MessageContentType contentType,
@Schema(description = "์ฑ„ํŒ… ๋ฉ”์‹œ์ง€ ์นดํ…Œ๊ณ ๋ฆฌ ํƒ€์ž…")
MessageCategoryType categoryType,
@Schema(description = "์ฑ„ํŒ… ์ƒ์„ฑ์ผ")
@JsonSerialize(using = LocalDateTimeSerializer.class)
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
LocalDateTime createdAt,
@Schema(description = "์ฑ„ํŒ… ๋ณด๋‚ธ ์‚ฌ๋žŒ ID", type = "long")
Long senderId
) {
public static Detail from(ChatMessage message) {
return new Detail(
message.getChatRoomId(),
message.getChatId(),
message.getContent(),
message.getContentType(),
message.getCategoryType(),
message.getCreatedAt(),
message.getSender()
);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,10 @@
import com.fasterxml.jackson.datatype.jsr310.ser.LocalDateTimeSerializer;
import io.swagger.v3.oas.annotations.media.Schema;
import kr.co.pennyway.domain.domains.chatroom.domain.ChatRoom;
import lombok.Builder;

import java.time.LocalDateTime;
import java.util.List;
import java.util.Objects;
import java.util.Set;

Expand Down Expand Up @@ -63,4 +65,19 @@ public record Summary(
Set<Long> chatRoomIds
) {
}

@Schema(description = "์ฑ„ํŒ…๋ฐฉ ์ฐธ์—ฌ์ž ์ •๋ณด (๋ฐฉ์˜ ์ฐธ์—ฌ์ž + ์ตœ๊ทผ ๋ฉ”์‹œ์ง€)")
@Builder
public record RoomWithParticipants(
@Schema(description = "์ฑ„ํŒ…๋ฐฉ์—์„œ ๋‚ด ์ •๋ณด")
ChatMemberRes.Detail myInfo,
@Schema(description = "์ตœ๊ทผ์— ์ฑ„ํŒ… ๋ฉ”์‹œ์ง€๋ฅผ ๋ณด๋‚ธ ์ฐธ์—ฌ์ž์˜ ์ƒ์„ธ ์ •๋ณด ๋ชฉ๋ก")
List<ChatMemberRes.Detail> recentParticipants,
@Schema(description = "์ฑ„ํŒ…๋ฐฉ์—์„œ ๋‚ด ์ •๋ณด์™€ ์ตœ๊ทผ ํ™œ๋™์ž๋ฅผ ์ œ์™ธํ•œ ์ฐธ์—ฌ์ž ID ๋ชฉ๋ก")
List<Long> otherParticipantIds,
@Schema(description = "์ตœ๊ทผ ์ฑ„ํŒ… ์ด๋ ฅ. ๋ฉ”์‹œ์ง€๋Š” ์ตœ์‹ ์ˆœ์œผ๋กœ ์ •๋ ฌ๋˜์–ด ๋ฐ˜ํ™˜.")
List<ChatRes.Detail> recentMessages
) {

}
}
Original file line number Diff line number Diff line change
@@ -1,10 +1,14 @@
package kr.co.pennyway.api.apis.chat.mapper;

import kr.co.pennyway.api.apis.chat.dto.ChatMemberRes;
import kr.co.pennyway.api.apis.chat.dto.ChatRes;
import kr.co.pennyway.api.apis.chat.dto.ChatRoomRes;
import kr.co.pennyway.api.common.response.SliceResponseTemplate;
import kr.co.pennyway.common.annotation.Mapper;
import kr.co.pennyway.domain.common.redis.message.domain.ChatMessage;
import kr.co.pennyway.domain.domains.chatroom.domain.ChatRoom;
import kr.co.pennyway.domain.domains.chatroom.dto.ChatRoomDetail;
import kr.co.pennyway.domain.domains.member.domain.ChatMember;
import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Slice;

Expand Down Expand Up @@ -45,4 +49,21 @@ public static List<ChatRoomRes.Detail> toChatRoomResDetails(List<ChatRoomDetail>
public static ChatRoomRes.Detail toChatRoomResDetail(ChatRoom chatRoom, boolean isAdmin, int participantCount) {
return ChatRoomRes.Detail.from(chatRoom, isAdmin, participantCount);
}

public static ChatRoomRes.RoomWithParticipants toChatRoomResRoomWithParticipants(ChatMember myInfo, List<ChatMember> recentParticipants, List<Long> otherMemberIds, List<ChatMessage> chatMessages) {
List<ChatMemberRes.Detail> recentParticipantsRes = recentParticipants.stream()
.map(participant -> ChatMemberRes.Detail.from(participant, false))
.toList();

List<ChatRes.Detail> chatMessagesRes = chatMessages.stream()
.map(ChatRes.Detail::from)
.toList();

return ChatRoomRes.RoomWithParticipants.builder()
.myInfo(ChatMemberRes.Detail.from(myInfo, true))
.recentParticipants(recentParticipantsRes)
.otherParticipantIds(otherMemberIds)
.recentMessages(chatMessagesRes)
.build();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
package kr.co.pennyway.api.apis.chat.service;

import kr.co.pennyway.api.apis.chat.dto.ChatRoomRes;
import kr.co.pennyway.api.apis.chat.mapper.ChatRoomMapper;
import kr.co.pennyway.domain.common.redis.message.domain.ChatMessage;
import kr.co.pennyway.domain.common.redis.message.service.ChatMessageService;
import kr.co.pennyway.domain.domains.member.domain.ChatMember;
import kr.co.pennyway.domain.domains.member.exception.ChatMemberErrorCode;
import kr.co.pennyway.domain.domains.member.exception.ChatMemberErrorException;
import kr.co.pennyway.domain.domains.member.service.ChatMemberService;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.util.List;
import java.util.Set;
import java.util.stream.Collectors;

@Slf4j
@Service
@RequiredArgsConstructor
public class ChatRoomWithParticipantsSearchService {
private static final int MESSAGE_LIMIT = 15;

private final ChatMemberService chatMemberService;
private final ChatMessageService chatMessageService;

@Transactional(readOnly = true)
public ChatRoomRes.RoomWithParticipants execute(Long userId, Long chatRoomId) {
ChatMember myInfo = chatMemberService.readChatMember(userId, chatRoomId)
.orElseThrow(() -> new ChatMemberErrorException(ChatMemberErrorCode.NOT_FOUND));

List<ChatMessage> chatMessages = chatMessageService.readRecentMessages(chatRoomId, MESSAGE_LIMIT);

Set<Long> recentParticipantIds = chatMessages.stream()
.map(ChatMessage::getSender)
.filter(sender -> !sender.equals(userId))
.collect(Collectors.toSet());

List<ChatMember> recentParticipants = chatMemberService.readChatMembersByMemberIdIn(chatRoomId, recentParticipantIds);

recentParticipantIds.add(userId);
List<Long> otherMemberIds = chatMemberService.readChatMemberIdsByMemberIdNotIn(chatRoomId, recentParticipantIds);

return ChatRoomMapper.toChatRoomResRoomWithParticipants(myInfo, recentParticipants, otherMemberIds, chatMessages);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
import kr.co.pennyway.api.apis.chat.service.ChatMemberSearchService;
import kr.co.pennyway.api.apis.chat.service.ChatRoomSaveService;
import kr.co.pennyway.api.apis.chat.service.ChatRoomSearchService;
import kr.co.pennyway.api.apis.chat.service.ChatRoomWithParticipantsSearchService;
import kr.co.pennyway.api.common.response.SliceResponseTemplate;
import kr.co.pennyway.common.annotation.UseCase;
import kr.co.pennyway.domain.domains.chatroom.domain.ChatRoom;
Expand All @@ -22,6 +23,7 @@
public class ChatRoomUseCase {
private final ChatRoomSaveService chatRoomSaveService;
private final ChatRoomSearchService chatRoomSearchService;
private final ChatRoomWithParticipantsSearchService chatRoomWithParticipantsSearchService;

private final ChatMemberSearchService chatMemberSearchService;

Expand All @@ -37,6 +39,10 @@ public List<ChatRoomRes.Detail> getChatRooms(Long userId) {
return ChatRoomMapper.toChatRoomResDetails(chatRooms);
}

public ChatRoomRes.RoomWithParticipants getChatRoomWithParticipants(Long userId, Long chatRoomId) {
return chatRoomWithParticipantsSearchService.execute(userId, chatRoomId);
}

public ChatRoomRes.Summary readJoinedChatRoomIds(Long userId) {
Set<Long> chatRoomIds = chatMemberSearchService.readJoinedChatRoomIds(userId);

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
package kr.co.pennyway.api.common.security.authorization;

import kr.co.pennyway.domain.domains.member.service.ChatMemberService;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import org.springframework.transaction.annotation.Transactional;

@Slf4j
@Component("chatRoomManager")
@RequiredArgsConstructor
public class ChatRoomManager {
private final ChatMemberService chatMemberService;

/**
* ์‚ฌ์šฉ์ž๊ฐ€ ์ฑ„ํŒ…๋ฐฉ์— ๋Œ€ํ•œ ์ ‘๊ทผ ๊ถŒํ•œ์ด ์žˆ๋Š”์ง€ ํ™•์ธํ•œ๋‹ค.
*/
@Transactional(readOnly = true)
public boolean hasPermission(Long userId, Long chatRoomId) {
return chatMemberService.isExists(chatRoomId, userId);
}
}
Loading

0 comments on commit 4fb271b

Please sign in to comment.