diff --git a/src/main/java/gg/agit/konect/domain/chat/unified/controller/ChatApi.java b/src/main/java/gg/agit/konect/domain/chat/controller/ChatApi.java similarity index 81% rename from src/main/java/gg/agit/konect/domain/chat/unified/controller/ChatApi.java rename to src/main/java/gg/agit/konect/domain/chat/controller/ChatApi.java index 478eff8d5..dca9763d0 100644 --- a/src/main/java/gg/agit/konect/domain/chat/unified/controller/ChatApi.java +++ b/src/main/java/gg/agit/konect/domain/chat/controller/ChatApi.java @@ -1,4 +1,4 @@ -package gg.agit.konect.domain.chat.unified.controller; +package gg.agit.konect.domain.chat.controller; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.GetMapping; @@ -8,14 +8,13 @@ import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestParam; -import gg.agit.konect.domain.chat.direct.dto.ChatMessageSendRequest; -import gg.agit.konect.domain.chat.direct.dto.ChatRoomCreateRequest; -import gg.agit.konect.domain.chat.direct.dto.ChatRoomResponse; -import gg.agit.konect.domain.chat.unified.dto.UnifiedChatMuteResponse; -import gg.agit.konect.domain.chat.unified.dto.UnifiedChatMessageResponse; -import gg.agit.konect.domain.chat.unified.dto.UnifiedChatMessagesResponse; -import gg.agit.konect.domain.chat.unified.dto.UnifiedChatRoomsResponse; -import gg.agit.konect.domain.chat.unified.enums.ChatType; +import gg.agit.konect.domain.chat.dto.ChatMessageSendRequest; +import gg.agit.konect.domain.chat.dto.ChatRoomCreateRequest; +import gg.agit.konect.domain.chat.dto.ChatMessageDetailResponse; +import gg.agit.konect.domain.chat.dto.ChatMessagePageResponse; +import gg.agit.konect.domain.chat.dto.ChatMuteResponse; +import gg.agit.konect.domain.chat.dto.ChatRoomsSummaryResponse; +import gg.agit.konect.domain.chat.dto.ChatRoomResponse; import gg.agit.konect.global.auth.annotation.UserId; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.tags.Tag; @@ -54,7 +53,7 @@ ResponseEntity createOrGetChatRoom( - 최근 메시지가 있는 순서대로 정렬됩니다. """) @GetMapping("/rooms") - ResponseEntity getChatRooms( + ResponseEntity getChatRooms( @UserId Integer userId ); @@ -74,8 +73,7 @@ ResponseEntity getChatRooms( - FORBIDDEN_CHAT_ROOM_ACCESS (403): 채팅방에 접근할 권한이 없습니다. """) @GetMapping("/rooms/{chatRoomId}") - ResponseEntity getChatRoomMessages( - @RequestParam(name = "type") ChatType type, + ResponseEntity getChatRoomMessages( @RequestParam(name = "page", defaultValue = "1") Integer page, @RequestParam(name = "limit", defaultValue = "20", required = false) Integer limit, @PathVariable(value = "chatRoomId") Integer chatRoomId, @@ -97,8 +95,7 @@ ResponseEntity getChatRoomMessages( - FORBIDDEN_CHAT_ROOM_ACCESS (403): 채팅방에 접근할 권한이 없습니다. """) @PostMapping("/rooms/{chatRoomId}/messages") - ResponseEntity sendMessage( - @RequestParam(name = "type") ChatType type, + ResponseEntity sendMessage( @PathVariable(value = "chatRoomId") Integer chatRoomId, @Valid @RequestBody ChatMessageSendRequest request, @UserId Integer userId @@ -106,8 +103,7 @@ ResponseEntity sendMessage( @Operation(summary = "채팅 알림 기능 유무를 토글한다.") @PostMapping("/rooms/{chatRoomId}/mute") - ResponseEntity toggleGroupChatMute( - @RequestParam(name = "type") ChatType type, + ResponseEntity toggleChatMute( @PathVariable(value = "chatRoomId") Integer chatRoomId, @UserId Integer userId ); diff --git a/src/main/java/gg/agit/konect/domain/chat/controller/ChatController.java b/src/main/java/gg/agit/konect/domain/chat/controller/ChatController.java new file mode 100644 index 000000000..e42402b10 --- /dev/null +++ b/src/main/java/gg/agit/konect/domain/chat/controller/ChatController.java @@ -0,0 +1,74 @@ +package gg.agit.konect.domain.chat.controller; + +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.PathVariable; +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; + +import gg.agit.konect.domain.chat.dto.ChatMessageSendRequest; +import gg.agit.konect.domain.chat.dto.ChatMessageDetailResponse; +import gg.agit.konect.domain.chat.dto.ChatMessagePageResponse; +import gg.agit.konect.domain.chat.dto.ChatMuteResponse; +import gg.agit.konect.domain.chat.dto.ChatRoomCreateRequest; +import gg.agit.konect.domain.chat.dto.ChatRoomResponse; +import gg.agit.konect.domain.chat.dto.ChatRoomsSummaryResponse; +import gg.agit.konect.domain.chat.service.ChatService; +import gg.agit.konect.global.auth.annotation.UserId; +import jakarta.validation.Valid; +import lombok.RequiredArgsConstructor; + +@RestController +@RequiredArgsConstructor +@RequestMapping("/chats") +public class ChatController implements ChatApi { + + private final ChatService chatService; + + @Override + public ResponseEntity createOrGetChatRoom( + @Valid @RequestBody ChatRoomCreateRequest request, + @UserId Integer userId + ) { + ChatRoomResponse response = chatService.createOrGetChatRoom(userId, request); + return ResponseEntity.ok(response); + } + + @Override + public ResponseEntity getChatRooms( + @UserId Integer userId + ) { + ChatRoomsSummaryResponse response = chatService.getChatRooms(userId); + return ResponseEntity.ok(response); + } + + @Override + public ResponseEntity getChatRoomMessages( + @RequestParam(name = "page", defaultValue = "1") Integer page, + @RequestParam(name = "limit", defaultValue = "20", required = false) Integer limit, + @PathVariable(value = "chatRoomId") Integer chatRoomId, + @UserId Integer userId + ) { + ChatMessagePageResponse response = chatService.getMessages(userId, chatRoomId, page, limit); + return ResponseEntity.ok(response); + } + + @Override + public ResponseEntity sendMessage( + @PathVariable(value = "chatRoomId") Integer chatRoomId, + @Valid @RequestBody ChatMessageSendRequest request, + @UserId Integer userId + ) { + ChatMessageDetailResponse response = chatService.sendMessage(userId, chatRoomId, request); + return ResponseEntity.ok(response); + } + + @Override + public ResponseEntity toggleChatMute( + @PathVariable(value = "chatRoomId") Integer chatRoomId, + @UserId Integer userId + ) { + return ResponseEntity.ok(chatService.toggleMute(userId, chatRoomId)); + } +} diff --git a/src/main/java/gg/agit/konect/domain/chat/direct/dto/ChatMessageResponse.java b/src/main/java/gg/agit/konect/domain/chat/direct/dto/ChatMessageResponse.java deleted file mode 100644 index 26c9e3b74..000000000 --- a/src/main/java/gg/agit/konect/domain/chat/direct/dto/ChatMessageResponse.java +++ /dev/null @@ -1,42 +0,0 @@ -package gg.agit.konect.domain.chat.direct.dto; - -import static io.swagger.v3.oas.annotations.media.Schema.RequiredMode.REQUIRED; - -import java.time.LocalDateTime; - -import com.fasterxml.jackson.annotation.JsonFormat; - -import gg.agit.konect.domain.chat.direct.model.ChatMessage; -import io.swagger.v3.oas.annotations.media.Schema; - -public record ChatMessageResponse( - @Schema(description = "메시지 ID", example = "505", requiredMode = REQUIRED) - Integer messageId, - - @Schema(description = "발신자 ID", example = "1", requiredMode = REQUIRED) - Integer senderId, - - @Schema(description = "메시지 내용", example = "투명 케이스가 끼워져 있었어요!", requiredMode = REQUIRED) - String content, - - @Schema(description = "메시지 전송 시간", example = "2025.07.23 15:53:12.123", requiredMode = REQUIRED) - @JsonFormat(pattern = "yyyy.MM.dd HH:mm:ss.SSS") - LocalDateTime createdAt, - - @Schema(description = "읽음 여부", example = "false", requiredMode = REQUIRED) - Boolean isRead, - - @Schema(description = "내가 보낸 메시지 여부", example = "true", requiredMode = REQUIRED) - Boolean isMine -) { - public static ChatMessageResponse from(ChatMessage message, Integer currentUserId) { - return new ChatMessageResponse( - message.getId(), - message.getSender().getId(), - message.getContent(), - message.getCreatedAt(), - message.getIsRead(), - message.isSentBy(currentUserId) - ); - } -} diff --git a/src/main/java/gg/agit/konect/domain/chat/direct/dto/ChatMessagesResponse.java b/src/main/java/gg/agit/konect/domain/chat/direct/dto/ChatMessagesResponse.java deleted file mode 100644 index 77cfe27d5..000000000 --- a/src/main/java/gg/agit/konect/domain/chat/direct/dto/ChatMessagesResponse.java +++ /dev/null @@ -1,91 +0,0 @@ -package gg.agit.konect.domain.chat.direct.dto; - -import static io.swagger.v3.oas.annotations.media.Schema.RequiredMode.REQUIRED; - -import java.time.LocalDateTime; -import java.util.List; - -import org.springframework.data.domain.Page; - -import com.fasterxml.jackson.annotation.JsonFormat; - -import gg.agit.konect.domain.chat.direct.model.ChatMessage; -import gg.agit.konect.domain.user.enums.UserRole; -import io.swagger.v3.oas.annotations.media.Schema; - -public record ChatMessagesResponse( - @Schema(description = "조건에 해당하는 메시지 수", example = "100", requiredMode = REQUIRED) - Long totalCount, - - @Schema(description = "현재 페이지에서 조회된 메시지 수", example = "20", requiredMode = REQUIRED) - Integer currentCount, - - @Schema(description = "최대 페이지", example = "5", requiredMode = REQUIRED) - Integer totalPage, - - @Schema(description = "현재 페이지", example = "1", requiredMode = REQUIRED) - Integer currentPage, - - @Schema(description = "채팅 메시지 리스트", requiredMode = REQUIRED) - List messages -) { - public record InnerChatMessageResponse( - @Schema(description = "메시지 ID", example = "505", requiredMode = REQUIRED) - Integer messageId, - - @Schema(description = "발신자 ID", example = "1", requiredMode = REQUIRED) - Integer senderId, - - @Schema(description = "메시지 내용", example = "투명 케이스가 끼워져 있었어요!", requiredMode = REQUIRED) - String content, - - @Schema(description = "메시지 전송 시간", example = "2025.07.23 15:53:12.123", requiredMode = REQUIRED) - @JsonFormat(pattern = "yyyy.MM.dd HH:mm:ss.SSS") - LocalDateTime createdAt, - - @Schema(description = "읽음 여부", example = "false", requiredMode = REQUIRED) - Boolean isRead, - - @Schema(description = "내가 보낸 메시지 여부", example = "true", requiredMode = REQUIRED) - Boolean isMine - ) { - public static InnerChatMessageResponse from( - ChatMessage message, - Integer currentUserId, - Integer maskedAdminId - ) { - Integer senderId = resolveSenderId(message, maskedAdminId); - return new InnerChatMessageResponse( - message.getId(), - senderId, - message.getContent(), - message.getCreatedAt(), - message.getIsRead(), - message.isSentBy(currentUserId) - ); - } - - private static Integer resolveSenderId(ChatMessage message, Integer maskedAdminId) { - if (maskedAdminId != null && message.getSender().getRole() == UserRole.ADMIN) { - return maskedAdminId; - } - return message.getSender().getId(); - } - } - - public static ChatMessagesResponse from( - Page page, - Integer currentUserId, - Integer maskedAdminId - ) { - return new ChatMessagesResponse( - page.getTotalElements(), - page.getNumberOfElements(), - page.getTotalPages(), - page.getNumber() + 1, - page.stream() - .map(message -> InnerChatMessageResponse.from(message, currentUserId, maskedAdminId)) - .toList() - ); - } -} diff --git a/src/main/java/gg/agit/konect/domain/chat/direct/dto/ChatRoomsResponse.java b/src/main/java/gg/agit/konect/domain/chat/direct/dto/ChatRoomsResponse.java deleted file mode 100644 index e9ae442fb..000000000 --- a/src/main/java/gg/agit/konect/domain/chat/direct/dto/ChatRoomsResponse.java +++ /dev/null @@ -1,84 +0,0 @@ -package gg.agit.konect.domain.chat.direct.dto; - -import static io.swagger.v3.oas.annotations.media.Schema.RequiredMode.NOT_REQUIRED; -import static io.swagger.v3.oas.annotations.media.Schema.RequiredMode.REQUIRED; - -import java.time.LocalDateTime; -import java.util.List; -import java.util.Map; - -import com.fasterxml.jackson.annotation.JsonFormat; - -import gg.agit.konect.domain.chat.direct.model.ChatRoom; -import gg.agit.konect.domain.chat.direct.enums.ChatRoomType; -import gg.agit.konect.domain.user.model.User; -import io.swagger.v3.oas.annotations.media.Schema; - -public record ChatRoomsResponse( - @Schema(description = "채팅방 리스트", requiredMode = REQUIRED) - List chatRooms -) { - public record InnerChatRoomResponse( - @Schema(description = "채팅방 ID", example = "1", requiredMode = REQUIRED) - Integer chatRoomId, - - @Schema(description = "상대방 이름", example = "김혜준", requiredMode = REQUIRED) - String chatPartnerName, - - @Schema(description = "상대방 프로필 사진", example = "https://bcsdlab.com/static/img/logo.d89d9cc.png", requiredMode = REQUIRED) - String chatPartnerProfileImage, - - @Schema(description = "마지막 메시지", example = "지원 어디서 해요", requiredMode = NOT_REQUIRED) - String lastMessage, - - @Schema(description = "마지막 메시지 전송 시간", example = "2025.12.19 23:21", requiredMode = NOT_REQUIRED) - @JsonFormat(pattern = "yyyy.MM.dd HH:mm") - LocalDateTime lastSentTime, - - @Schema(description = "읽지 않은 메시지 개수", example = "12", requiredMode = REQUIRED) - Integer unreadCount, - - @Schema(description = "채팅방 타입", example = "NORMAL", requiredMode = REQUIRED) - ChatRoomType chatRoomType - ) { - public static InnerChatRoomResponse from( - ChatRoom chatRoom, - User currentUser, - Map unreadCountMap, - ChatRoomType chatRoomType - ) { - User chatPartner = chatRoom.getChatPartner(currentUser); - - return new InnerChatRoomResponse( - chatRoom.getId(), - chatPartner.getName(), - chatPartner.getImageUrl(), - chatRoom.getLastMessageContent(), - chatRoom.getLastMessageSentAt(), - unreadCountMap.getOrDefault(chatRoom.getId(), 0), - chatRoomType - ); - } - - public static InnerChatRoomResponse fromForAdmin( - ChatRoom chatRoom, - Map unreadCountMap - ) { - User nonAdminUser = chatRoom.getNonAdminUser(); - - return new InnerChatRoomResponse( - chatRoom.getId(), - nonAdminUser.getName(), - nonAdminUser.getImageUrl(), - chatRoom.getLastMessageContent(), - chatRoom.getLastMessageSentAt(), - unreadCountMap.getOrDefault(chatRoom.getId(), 0), - ChatRoomType.ADMIN - ); - } - } - - public static ChatRoomsResponse of(List chatRooms) { - return new ChatRoomsResponse(chatRooms); - } -} diff --git a/src/main/java/gg/agit/konect/domain/chat/direct/enums/ChatRoomType.java b/src/main/java/gg/agit/konect/domain/chat/direct/enums/ChatRoomType.java deleted file mode 100644 index d954a2714..000000000 --- a/src/main/java/gg/agit/konect/domain/chat/direct/enums/ChatRoomType.java +++ /dev/null @@ -1,6 +0,0 @@ -package gg.agit.konect.domain.chat.direct.enums; - -public enum ChatRoomType { - NORMAL, - ADMIN -} diff --git a/src/main/java/gg/agit/konect/domain/chat/direct/repository/ChatMessageRepository.java b/src/main/java/gg/agit/konect/domain/chat/direct/repository/ChatMessageRepository.java deleted file mode 100644 index 4bb7f84c4..000000000 --- a/src/main/java/gg/agit/konect/domain/chat/direct/repository/ChatMessageRepository.java +++ /dev/null @@ -1,83 +0,0 @@ -package gg.agit.konect.domain.chat.direct.repository; - -import java.util.List; - -import org.springframework.data.domain.Page; -import org.springframework.data.domain.Pageable; -import org.springframework.data.jpa.repository.Query; -import org.springframework.data.repository.Repository; -import org.springframework.data.repository.query.Param; - -import gg.agit.konect.domain.chat.direct.dto.UnreadMessageCount; -import gg.agit.konect.domain.chat.direct.model.ChatMessage; -import gg.agit.konect.domain.user.enums.UserRole; - -public interface ChatMessageRepository extends Repository { - - ChatMessage save(ChatMessage chatMessage); - - @Query(""" - SELECT new gg.agit.konect.domain.chat.direct.dto.UnreadMessageCount( - cm.chatRoom.id, - COUNT(cm) - ) - FROM ChatMessage cm - WHERE cm.chatRoom.id IN :chatRoomIds - AND cm.receiver.id = :receiverId - AND cm.isRead = false - GROUP BY cm.chatRoom.id - """) - List countUnreadMessagesByChatRoomIdsAndUserId( - @Param("chatRoomIds") List chatRoomIds, - @Param("receiverId") Integer receiverId - ); - - @Query(""" - SELECT cm - FROM ChatMessage cm - JOIN FETCH cm.sender - WHERE cm.chatRoom.id = :chatRoomId - ORDER BY cm.createdAt DESC - """) - Page findByChatRoomId(@Param("chatRoomId") Integer chatRoomId, Pageable pageable); - - @Query(""" - SELECT cm - FROM ChatMessage cm - WHERE cm.chatRoom.id = :chatRoomId - AND cm.receiver.id = :receiverId - AND cm.isRead = false - """) - List findUnreadMessagesByChatRoomIdAndUserId( - @Param("chatRoomId") Integer chatRoomId, - @Param("receiverId") Integer receiverId - ); - - @Query(""" - SELECT cm - FROM ChatMessage cm - WHERE cm.chatRoom.id = :chatRoomId - AND cm.receiver.role = :adminRole - AND cm.isRead = false - """) - List findUnreadMessagesForAdmin( - @Param("chatRoomId") Integer chatRoomId, - @Param("adminRole") UserRole adminRole - ); - - @Query(""" - SELECT new gg.agit.konect.domain.chat.direct.dto.UnreadMessageCount( - cm.chatRoom.id, - COUNT(cm) - ) - FROM ChatMessage cm - WHERE cm.chatRoom.id IN :chatRoomIds - AND cm.receiver.role = :adminRole - AND cm.isRead = false - GROUP BY cm.chatRoom.id - """) - List countUnreadMessagesForAdmin( - @Param("chatRoomIds") List chatRoomIds, - @Param("adminRole") UserRole adminRole - ); -} diff --git a/src/main/java/gg/agit/konect/domain/chat/direct/repository/ChatRoomRepository.java b/src/main/java/gg/agit/konect/domain/chat/direct/repository/ChatRoomRepository.java deleted file mode 100644 index 0c7683e6d..000000000 --- a/src/main/java/gg/agit/konect/domain/chat/direct/repository/ChatRoomRepository.java +++ /dev/null @@ -1,81 +0,0 @@ -package gg.agit.konect.domain.chat.direct.repository; - -import java.util.List; -import java.util.Optional; - -import org.springframework.data.jpa.repository.Query; -import org.springframework.data.repository.Repository; -import org.springframework.data.repository.query.Param; - -import gg.agit.konect.domain.chat.direct.model.ChatRoom; -import gg.agit.konect.domain.user.enums.UserRole; - -public interface ChatRoomRepository extends Repository { - - ChatRoom save(ChatRoom chatRoom); - - @Query(""" - SELECT DISTINCT cr - FROM ChatRoom cr - JOIN FETCH cr.sender - JOIN FETCH cr.receiver - WHERE cr.sender.id = :userId OR cr.receiver.id = :userId - ORDER BY cr.lastMessageSentAt DESC NULLS LAST, cr.id - """) - List findByUserId(@Param("userId") Integer userId); - - @Query(""" - SELECT cr - FROM ChatRoom cr - JOIN FETCH cr.sender - JOIN FETCH cr.receiver - WHERE cr.id = :chatRoomId - """) - Optional findById(@Param("chatRoomId") Integer chatRoomId); - - @Query(""" - SELECT cr - FROM ChatRoom cr - WHERE (cr.sender.id = :userId1 AND cr.receiver.id = :userId2) - OR (cr.sender.id = :userId2 AND cr.receiver.id = :userId1) - """) - Optional findByTwoUsers(@Param("userId1") Integer userId1, @Param("userId2") Integer userId2); - - @Query(""" - SELECT DISTINCT cr - FROM ChatRoom cr - JOIN FETCH cr.sender s - JOIN FETCH cr.receiver r - WHERE s.role = :adminRole OR r.role = :adminRole - ORDER BY cr.lastMessageSentAt DESC NULLS LAST, cr.id - """) - List findAllAdminChatRooms(@Param("adminRole") UserRole adminRole); - - @Query(""" - SELECT DISTINCT cr - FROM ChatRoom cr - JOIN FETCH cr.sender s - JOIN FETCH cr.receiver r - WHERE (s.role = :adminRole OR r.role = :adminRole) - AND EXISTS ( - SELECT 1 FROM ChatMessage cm - WHERE cm.chatRoom = cr - AND cm.sender.role != :adminRole - ) - ORDER BY cr.lastMessageSentAt DESC NULLS LAST, cr.id - """) - List findAdminChatRoomsWithUserReply(@Param("adminRole") UserRole adminRole); - - @Query(""" - SELECT cr - FROM ChatRoom cr - JOIN FETCH cr.sender s - JOIN FETCH cr.receiver r - WHERE (s.id = :userId AND r.role = :adminRole) - OR (s.role = :adminRole AND r.id = :userId) - """) - Optional findByUserIdAndAdminRole( - @Param("userId") Integer userId, - @Param("adminRole") UserRole adminRole - ); -} diff --git a/src/main/java/gg/agit/konect/domain/chat/direct/service/ChatService.java b/src/main/java/gg/agit/konect/domain/chat/direct/service/ChatService.java deleted file mode 100644 index ad4125cfc..000000000 --- a/src/main/java/gg/agit/konect/domain/chat/direct/service/ChatService.java +++ /dev/null @@ -1,238 +0,0 @@ -package gg.agit.konect.domain.chat.direct.service; - -import static gg.agit.konect.global.code.ApiResponseCode.CANNOT_CREATE_CHAT_ROOM_WITH_SELF; -import static gg.agit.konect.global.code.ApiResponseCode.NOT_FOUND_CHAT_ROOM; - -import java.util.ArrayList; -import java.util.Comparator; -import java.util.HashSet; -import java.util.List; -import java.util.Map; -import java.util.Set; -import java.util.stream.Collectors; - -import org.springframework.context.ApplicationEventPublisher; -import org.springframework.data.domain.Page; -import org.springframework.data.domain.PageRequest; -import org.springframework.stereotype.Service; -import org.springframework.transaction.annotation.Transactional; - -import gg.agit.konect.domain.chat.direct.dto.ChatMessageResponse; -import gg.agit.konect.domain.chat.direct.dto.ChatMessageSendRequest; -import gg.agit.konect.domain.chat.direct.dto.ChatMessagesResponse; -import gg.agit.konect.domain.chat.direct.dto.ChatRoomCreateRequest; -import gg.agit.konect.domain.chat.direct.dto.ChatRoomResponse; -import gg.agit.konect.domain.chat.direct.dto.ChatRoomsResponse; -import gg.agit.konect.domain.chat.direct.dto.UnreadMessageCount; -import gg.agit.konect.domain.chat.direct.enums.ChatRoomType; -import gg.agit.konect.domain.chat.direct.model.ChatMessage; -import gg.agit.konect.domain.chat.direct.model.ChatRoom; -import gg.agit.konect.domain.chat.direct.repository.ChatMessageRepository; -import gg.agit.konect.domain.chat.direct.repository.ChatRoomRepository; -import gg.agit.konect.domain.chat.event.AdminChatReceivedEvent; -import gg.agit.konect.domain.chat.unified.service.ChatPresenceService; -import gg.agit.konect.domain.notification.service.NotificationService; -import gg.agit.konect.domain.user.enums.UserRole; -import gg.agit.konect.domain.user.model.User; -import gg.agit.konect.domain.user.repository.UserRepository; -import gg.agit.konect.global.exception.CustomException; -import lombok.RequiredArgsConstructor; - -@Service -@RequiredArgsConstructor -@Transactional(readOnly = true) -public class ChatService { - - private final ChatRoomRepository chatRoomRepository; - private final ChatMessageRepository chatMessageRepository; - private final UserRepository userRepository; - private final ChatPresenceService chatPresenceService; - private final NotificationService notificationService; - private final ApplicationEventPublisher eventPublisher; - - @Transactional - public ChatRoomResponse createOrGetChatRoom(Integer currentUserId, ChatRoomCreateRequest request) { - User currentUser = userRepository.getById(currentUserId); - User targetUser = userRepository.getById(request.userId()); - - if (currentUser.getId().equals(targetUser.getId())) { - throw CustomException.of(CANNOT_CREATE_CHAT_ROOM_WITH_SELF); - } - - ChatRoom chatRoom = chatRoomRepository.findByTwoUsers(currentUser.getId(), targetUser.getId()) - .orElseGet(() -> { - ChatRoom newChatRoom = ChatRoom.of(currentUser, targetUser); - return chatRoomRepository.save(newChatRoom); - }); - - return ChatRoomResponse.from(chatRoom); - } - - public ChatRoomsResponse getChatRooms(Integer userId) { - User user = userRepository.getById(userId); - List chatRoomResponses = new ArrayList<>(); - - List personalChatRooms = chatRoomRepository.findByUserId(userId); - Map personalUnreadCountMap = getUnreadCountMap( - extractChatRoomIds(personalChatRooms), userId - ); - - Set addedChatRoomIds = new HashSet<>(); - if (user.getRole() == UserRole.ADMIN) { - List adminChatRooms = chatRoomRepository.findAdminChatRoomsWithUserReply(UserRole.ADMIN); - Map adminUnreadCountMap = getAdminUnreadCountMap( - extractChatRoomIds(adminChatRooms) - ); - - for (ChatRoom chatRoom : adminChatRooms) { - chatRoomResponses.add(ChatRoomsResponse.InnerChatRoomResponse.fromForAdmin( - chatRoom, adminUnreadCountMap - )); - - addedChatRoomIds.add(chatRoom.getId()); - } - } - - for (ChatRoom chatRoom : personalChatRooms) { - if (!addedChatRoomIds.contains(chatRoom.getId())) { - ChatRoomType type = isAdminChatRoom(chatRoom) ? ChatRoomType.ADMIN : ChatRoomType.NORMAL; - chatRoomResponses.add(ChatRoomsResponse.InnerChatRoomResponse.from( - chatRoom, user, personalUnreadCountMap, type - )); - } - } - - chatRoomResponses.sort(Comparator - .comparing(ChatRoomsResponse.InnerChatRoomResponse::lastSentTime, - Comparator.nullsLast(Comparator.reverseOrder())) - .thenComparing(ChatRoomsResponse.InnerChatRoomResponse::chatRoomId)); - - return ChatRoomsResponse.of(chatRoomResponses); - } - - private List extractChatRoomIds(List chatRooms) { - return chatRooms.stream() - .map(ChatRoom::getId) - .toList(); - } - - private Map getUnreadCountMap(List chatRoomIds, Integer userId) { - if (chatRoomIds.isEmpty()) { - return Map.of(); - } - - List unreadMessageCounts = chatMessageRepository.countUnreadMessagesByChatRoomIdsAndUserId( - chatRoomIds, userId - ); - - return unreadMessageCounts.stream() - .collect(Collectors.toMap( - UnreadMessageCount::chatRoomId, - unreadMessageCount -> unreadMessageCount.unreadCount().intValue() - )); - } - - private Map getAdminUnreadCountMap(List chatRoomIds) { - if (chatRoomIds.isEmpty()) { - return Map.of(); - } - - List unreadMessageCounts = chatMessageRepository.countUnreadMessagesForAdmin( - chatRoomIds, UserRole.ADMIN - ); - - return unreadMessageCounts.stream() - .collect(Collectors.toMap( - UnreadMessageCount::chatRoomId, - unreadMessageCount -> unreadMessageCount.unreadCount().intValue() - )); - } - - @Transactional - public ChatMessagesResponse getChatRoomMessages(Integer userId, Integer roomId, Integer page, Integer limit) { - ChatRoom chatRoom = chatRoomRepository.findById(roomId) - .orElseThrow(() -> CustomException.of(NOT_FOUND_CHAT_ROOM)); - User user = userRepository.getById(userId); - validateChatRoomAccess(user, chatRoom); - - chatPresenceService.recordPresence(roomId, userId); - markUnreadMessagesAsRead(user, roomId, chatRoom); - - PageRequest pageable = PageRequest.of(page - 1, limit); - Page messages = chatMessageRepository.findByChatRoomId(roomId, pageable); - - Integer maskedAdminId = getMaskedAdminId(user, chatRoom); - return ChatMessagesResponse.from(messages, userId, maskedAdminId); - } - - private Integer getMaskedAdminId(User user, ChatRoom chatRoom) { - if (user.getRole() == UserRole.ADMIN || !isAdminChatRoom(chatRoom)) { - return null; - } - return getAdminFromChatRoom(chatRoom).getId(); - } - - private User getAdminFromChatRoom(ChatRoom chatRoom) { - if (chatRoom.getSender().getRole() == UserRole.ADMIN) { - return chatRoom.getSender(); - } - return chatRoom.getReceiver(); - } - - private void markUnreadMessagesAsRead(User user, Integer roomId, ChatRoom chatRoom) { - List unreadMessages; - - if (user.getRole() == UserRole.ADMIN && isAdminChatRoom(chatRoom)) { - unreadMessages = chatMessageRepository.findUnreadMessagesForAdmin(roomId, UserRole.ADMIN); - } else { - unreadMessages = chatMessageRepository.findUnreadMessagesByChatRoomIdAndUserId(roomId, user.getId()); - } - - unreadMessages.forEach(ChatMessage::markAsRead); - } - - private boolean isAdminChatRoom(ChatRoom chatRoom) { - return chatRoom.getSender().getRole() == UserRole.ADMIN - || chatRoom.getReceiver().getRole() == UserRole.ADMIN; - } - - private void validateChatRoomAccess(User user, ChatRoom chatRoom) { - if (user.getRole() == UserRole.ADMIN && isAdminChatRoom(chatRoom)) { - return; - } - chatRoom.validateIsParticipant(user.getId()); - } - - @Transactional - public ChatMessageResponse sendMessage(Integer userId, Integer roomId, ChatMessageSendRequest request) { - ChatRoom chatRoom = chatRoomRepository.findById(roomId) - .orElseThrow(() -> CustomException.of(NOT_FOUND_CHAT_ROOM)); - User sender = userRepository.getById(userId); - validateChatRoomAccess(sender, chatRoom); - - User receiver = getMessageReceiver(sender, chatRoom); - - ChatMessage chatMessage = chatMessageRepository.save( - ChatMessage.of(chatRoom, sender, receiver, request.content()) - ); - chatRoom.updateLastMessage(chatMessage.getContent(), chatMessage.getCreatedAt()); - - notificationService.sendChatNotification(receiver.getId(), roomId, sender.getName(), request.content()); - publishAdminChatEventIfNeeded(receiver, sender, request.content()); - - return ChatMessageResponse.from(chatMessage, userId); - } - - private User getMessageReceiver(User sender, ChatRoom chatRoom) { - if (sender.getRole() == UserRole.ADMIN && isAdminChatRoom(chatRoom)) { - return chatRoom.getNonAdminUser(); - } - return chatRoom.getChatPartner(sender); - } - - private void publishAdminChatEventIfNeeded(User receiver, User sender, String content) { - if (receiver.getRole() == UserRole.ADMIN) { - eventPublisher.publishEvent(AdminChatReceivedEvent.of(sender.getId(), sender.getName(), content)); - } - } -} diff --git a/src/main/java/gg/agit/konect/domain/chat/unified/dto/UnifiedChatMessageResponse.java b/src/main/java/gg/agit/konect/domain/chat/dto/ChatMessageDetailResponse.java similarity index 93% rename from src/main/java/gg/agit/konect/domain/chat/unified/dto/UnifiedChatMessageResponse.java rename to src/main/java/gg/agit/konect/domain/chat/dto/ChatMessageDetailResponse.java index ac5f6ad25..aa5906633 100644 --- a/src/main/java/gg/agit/konect/domain/chat/unified/dto/UnifiedChatMessageResponse.java +++ b/src/main/java/gg/agit/konect/domain/chat/dto/ChatMessageDetailResponse.java @@ -1,4 +1,4 @@ -package gg.agit.konect.domain.chat.unified.dto; +package gg.agit.konect.domain.chat.dto; import static io.swagger.v3.oas.annotations.media.Schema.RequiredMode.NOT_REQUIRED; import static io.swagger.v3.oas.annotations.media.Schema.RequiredMode.REQUIRED; @@ -9,7 +9,7 @@ import io.swagger.v3.oas.annotations.media.Schema; -public record UnifiedChatMessageResponse( +public record ChatMessageDetailResponse( @Schema(description = "메시지 ID", example = "505", requiredMode = REQUIRED) Integer messageId, diff --git a/src/main/java/gg/agit/konect/domain/chat/group/dto/GroupChatMessagesResponse.java b/src/main/java/gg/agit/konect/domain/chat/dto/ChatMessagePageResponse.java similarity index 87% rename from src/main/java/gg/agit/konect/domain/chat/group/dto/GroupChatMessagesResponse.java rename to src/main/java/gg/agit/konect/domain/chat/dto/ChatMessagePageResponse.java index 00f59d4dd..5a6a4f0cb 100644 --- a/src/main/java/gg/agit/konect/domain/chat/group/dto/GroupChatMessagesResponse.java +++ b/src/main/java/gg/agit/konect/domain/chat/dto/ChatMessagePageResponse.java @@ -1,4 +1,4 @@ -package gg.agit.konect.domain.chat.group.dto; +package gg.agit.konect.domain.chat.dto; import static io.swagger.v3.oas.annotations.media.Schema.RequiredMode.NOT_REQUIRED; import static io.swagger.v3.oas.annotations.media.Schema.RequiredMode.REQUIRED; @@ -7,7 +7,7 @@ import io.swagger.v3.oas.annotations.media.Schema; -public record GroupChatMessagesResponse( +public record ChatMessagePageResponse( @Schema(description = "조건에 해당하는 메시지 수", example = "100", requiredMode = REQUIRED) Long totalCount, @@ -24,6 +24,6 @@ public record GroupChatMessagesResponse( Integer clubId, @Schema(description = "채팅 메시지 리스트", requiredMode = REQUIRED) - List messages + List messages ) { } diff --git a/src/main/java/gg/agit/konect/domain/chat/direct/dto/ChatMessageSendRequest.java b/src/main/java/gg/agit/konect/domain/chat/dto/ChatMessageSendRequest.java similarity index 92% rename from src/main/java/gg/agit/konect/domain/chat/direct/dto/ChatMessageSendRequest.java rename to src/main/java/gg/agit/konect/domain/chat/dto/ChatMessageSendRequest.java index c02bb88f0..c943c29ea 100644 --- a/src/main/java/gg/agit/konect/domain/chat/direct/dto/ChatMessageSendRequest.java +++ b/src/main/java/gg/agit/konect/domain/chat/dto/ChatMessageSendRequest.java @@ -1,4 +1,4 @@ -package gg.agit.konect.domain.chat.direct.dto; +package gg.agit.konect.domain.chat.dto; import static io.swagger.v3.oas.annotations.media.Schema.RequiredMode.REQUIRED; diff --git a/src/main/java/gg/agit/konect/domain/chat/unified/dto/UnifiedChatMuteResponse.java b/src/main/java/gg/agit/konect/domain/chat/dto/ChatMuteResponse.java similarity index 75% rename from src/main/java/gg/agit/konect/domain/chat/unified/dto/UnifiedChatMuteResponse.java rename to src/main/java/gg/agit/konect/domain/chat/dto/ChatMuteResponse.java index 2df7868cc..0f7544282 100644 --- a/src/main/java/gg/agit/konect/domain/chat/unified/dto/UnifiedChatMuteResponse.java +++ b/src/main/java/gg/agit/konect/domain/chat/dto/ChatMuteResponse.java @@ -1,10 +1,10 @@ -package gg.agit.konect.domain.chat.unified.dto; +package gg.agit.konect.domain.chat.dto; import static io.swagger.v3.oas.annotations.media.Schema.RequiredMode.REQUIRED; import io.swagger.v3.oas.annotations.media.Schema; -public record UnifiedChatMuteResponse( +public record ChatMuteResponse( @Schema(description = "채팅 알림 음소거 여부", example = "true", requiredMode = REQUIRED) Boolean isMuted ) { diff --git a/src/main/java/gg/agit/konect/domain/chat/direct/dto/ChatRoomCreateRequest.java b/src/main/java/gg/agit/konect/domain/chat/dto/ChatRoomCreateRequest.java similarity index 89% rename from src/main/java/gg/agit/konect/domain/chat/direct/dto/ChatRoomCreateRequest.java rename to src/main/java/gg/agit/konect/domain/chat/dto/ChatRoomCreateRequest.java index b0dd9bf7f..e4b75b9c1 100644 --- a/src/main/java/gg/agit/konect/domain/chat/direct/dto/ChatRoomCreateRequest.java +++ b/src/main/java/gg/agit/konect/domain/chat/dto/ChatRoomCreateRequest.java @@ -1,4 +1,4 @@ -package gg.agit.konect.domain.chat.direct.dto; +package gg.agit.konect.domain.chat.dto; import static io.swagger.v3.oas.annotations.media.Schema.RequiredMode.REQUIRED; diff --git a/src/main/java/gg/agit/konect/domain/chat/direct/dto/ChatRoomResponse.java b/src/main/java/gg/agit/konect/domain/chat/dto/ChatRoomResponse.java similarity index 79% rename from src/main/java/gg/agit/konect/domain/chat/direct/dto/ChatRoomResponse.java rename to src/main/java/gg/agit/konect/domain/chat/dto/ChatRoomResponse.java index 578a4e49b..26d84d28b 100644 --- a/src/main/java/gg/agit/konect/domain/chat/direct/dto/ChatRoomResponse.java +++ b/src/main/java/gg/agit/konect/domain/chat/dto/ChatRoomResponse.java @@ -1,8 +1,8 @@ -package gg.agit.konect.domain.chat.direct.dto; +package gg.agit.konect.domain.chat.dto; import static io.swagger.v3.oas.annotations.media.Schema.RequiredMode.REQUIRED; -import gg.agit.konect.domain.chat.direct.model.ChatRoom; +import gg.agit.konect.domain.chat.model.ChatRoom; import io.swagger.v3.oas.annotations.media.Schema; public record ChatRoomResponse( diff --git a/src/main/java/gg/agit/konect/domain/chat/unified/dto/UnifiedChatRoomResponse.java b/src/main/java/gg/agit/konect/domain/chat/dto/ChatRoomSummaryResponse.java similarity index 90% rename from src/main/java/gg/agit/konect/domain/chat/unified/dto/UnifiedChatRoomResponse.java rename to src/main/java/gg/agit/konect/domain/chat/dto/ChatRoomSummaryResponse.java index f8e016114..0688a20c7 100644 --- a/src/main/java/gg/agit/konect/domain/chat/unified/dto/UnifiedChatRoomResponse.java +++ b/src/main/java/gg/agit/konect/domain/chat/dto/ChatRoomSummaryResponse.java @@ -1,4 +1,4 @@ -package gg.agit.konect.domain.chat.unified.dto; +package gg.agit.konect.domain.chat.dto; import static io.swagger.v3.oas.annotations.media.Schema.RequiredMode.NOT_REQUIRED; import static io.swagger.v3.oas.annotations.media.Schema.RequiredMode.REQUIRED; @@ -7,10 +7,10 @@ import com.fasterxml.jackson.annotation.JsonFormat; -import gg.agit.konect.domain.chat.unified.enums.ChatType; +import gg.agit.konect.domain.chat.enums.ChatType; import io.swagger.v3.oas.annotations.media.Schema; -public record UnifiedChatRoomResponse( +public record ChatRoomSummaryResponse( @Schema(description = "채팅방 ID", example = "1", requiredMode = REQUIRED) Integer roomId, diff --git a/src/main/java/gg/agit/konect/domain/chat/unified/dto/UnifiedChatRoomsResponse.java b/src/main/java/gg/agit/konect/domain/chat/dto/ChatRoomsSummaryResponse.java similarity index 65% rename from src/main/java/gg/agit/konect/domain/chat/unified/dto/UnifiedChatRoomsResponse.java rename to src/main/java/gg/agit/konect/domain/chat/dto/ChatRoomsSummaryResponse.java index d97f6e37e..5193ebbad 100644 --- a/src/main/java/gg/agit/konect/domain/chat/unified/dto/UnifiedChatRoomsResponse.java +++ b/src/main/java/gg/agit/konect/domain/chat/dto/ChatRoomsSummaryResponse.java @@ -1,4 +1,4 @@ -package gg.agit.konect.domain.chat.unified.dto; +package gg.agit.konect.domain.chat.dto; import static io.swagger.v3.oas.annotations.media.Schema.RequiredMode.REQUIRED; @@ -6,8 +6,8 @@ import io.swagger.v3.oas.annotations.media.Schema; -public record UnifiedChatRoomsResponse( +public record ChatRoomsSummaryResponse( @Schema(description = "채팅방 리스트", requiredMode = REQUIRED) - List rooms + List rooms ) { } diff --git a/src/main/java/gg/agit/konect/domain/chat/direct/dto/UnreadMessageCount.java b/src/main/java/gg/agit/konect/domain/chat/dto/UnreadMessageCount.java similarity index 64% rename from src/main/java/gg/agit/konect/domain/chat/direct/dto/UnreadMessageCount.java rename to src/main/java/gg/agit/konect/domain/chat/dto/UnreadMessageCount.java index bbcfacb53..1fff130d2 100644 --- a/src/main/java/gg/agit/konect/domain/chat/direct/dto/UnreadMessageCount.java +++ b/src/main/java/gg/agit/konect/domain/chat/dto/UnreadMessageCount.java @@ -1,4 +1,4 @@ -package gg.agit.konect.domain.chat.direct.dto; +package gg.agit.konect.domain.chat.dto; public record UnreadMessageCount( Integer chatRoomId, diff --git a/src/main/java/gg/agit/konect/domain/chat/enums/ChatType.java b/src/main/java/gg/agit/konect/domain/chat/enums/ChatType.java new file mode 100644 index 000000000..10b398775 --- /dev/null +++ b/src/main/java/gg/agit/konect/domain/chat/enums/ChatType.java @@ -0,0 +1,6 @@ +package gg.agit.konect.domain.chat.enums; + +public enum ChatType { + DIRECT, + GROUP +} diff --git a/src/main/java/gg/agit/konect/domain/chat/group/dto/GroupChatMessageResponse.java b/src/main/java/gg/agit/konect/domain/chat/group/dto/GroupChatMessageResponse.java deleted file mode 100644 index 4bcc2978e..000000000 --- a/src/main/java/gg/agit/konect/domain/chat/group/dto/GroupChatMessageResponse.java +++ /dev/null @@ -1,34 +0,0 @@ -package gg.agit.konect.domain.chat.group.dto; - -import static io.swagger.v3.oas.annotations.media.Schema.RequiredMode.REQUIRED; - -import java.time.LocalDateTime; - -import com.fasterxml.jackson.annotation.JsonFormat; - -import io.swagger.v3.oas.annotations.media.Schema; - -public record GroupChatMessageResponse( - @Schema(description = "메시지 ID", example = "505", requiredMode = REQUIRED) - Integer messageId, - - @Schema(description = "발신자 ID", example = "1", requiredMode = REQUIRED) - Integer senderId, - - @Schema(description = "발신자 이름", example = "홍길동", requiredMode = REQUIRED) - String senderName, - - @Schema(description = "메시지 내용", example = "안녕하세요!", requiredMode = REQUIRED) - String content, - - @Schema(description = "메시지 전송 시간", example = "2025.07.23 15:53:12.123", requiredMode = REQUIRED) - @JsonFormat(pattern = "yyyy.MM.dd HH:mm:ss.SSS") - LocalDateTime createdAt, - - @Schema(description = "미확인 인원 수", example = "3", requiredMode = REQUIRED) - Integer unreadCount, - - @Schema(description = "내가 보낸 메시지 여부", example = "true", requiredMode = REQUIRED) - Boolean isMine -) { -} diff --git a/src/main/java/gg/agit/konect/domain/chat/group/dto/GroupChatRoomResponse.java b/src/main/java/gg/agit/konect/domain/chat/group/dto/GroupChatRoomResponse.java deleted file mode 100644 index e24a16a47..000000000 --- a/src/main/java/gg/agit/konect/domain/chat/group/dto/GroupChatRoomResponse.java +++ /dev/null @@ -1,11 +0,0 @@ -package gg.agit.konect.domain.chat.group.dto; - -import static io.swagger.v3.oas.annotations.media.Schema.RequiredMode.REQUIRED; - -import io.swagger.v3.oas.annotations.media.Schema; - -public record GroupChatRoomResponse( - @Schema(description = "단체 채팅방 ID", example = "1", requiredMode = REQUIRED) - Integer groupChatRoomId -) { -} diff --git a/src/main/java/gg/agit/konect/domain/chat/group/dto/GroupChatRoomsResponse.java b/src/main/java/gg/agit/konect/domain/chat/group/dto/GroupChatRoomsResponse.java deleted file mode 100644 index e308d2334..000000000 --- a/src/main/java/gg/agit/konect/domain/chat/group/dto/GroupChatRoomsResponse.java +++ /dev/null @@ -1,71 +0,0 @@ -package gg.agit.konect.domain.chat.group.dto; - -import static io.swagger.v3.oas.annotations.media.Schema.RequiredMode.NOT_REQUIRED; -import static io.swagger.v3.oas.annotations.media.Schema.RequiredMode.REQUIRED; - -import java.time.LocalDateTime; -import java.util.List; -import java.util.Map; - -import com.fasterxml.jackson.annotation.JsonFormat; - -import gg.agit.konect.domain.chat.group.model.GroupChatMessage; -import gg.agit.konect.domain.chat.group.model.GroupChatRoom; -import io.swagger.v3.oas.annotations.media.Schema; - -public record GroupChatRoomsResponse( - @Schema(description = "단체 채팅방 리스트", requiredMode = REQUIRED) - List groupChatRooms -) { - public record InnerGroupChatRoomResponse( - @Schema(description = "단체 채팅방 ID", example = "1", requiredMode = REQUIRED) - Integer groupChatRoomId, - - @Schema(description = "동아리 이름", example = "BCSD Lab", requiredMode = REQUIRED) - String clubName, - - @Schema(description = "동아리 이미지 URL", example = "https://bcsdlab.com/static/img/logo.d89d9cc.png", requiredMode = REQUIRED) - String clubImageUrl, - - @Schema(description = "마지막 메시지", example = "안녕하세요!", requiredMode = NOT_REQUIRED) - String lastMessage, - - @Schema(description = "마지막 메시지 전송 시간", example = "2025.12.19 23:21", requiredMode = NOT_REQUIRED) - @JsonFormat(pattern = "yyyy.MM.dd HH:mm") - LocalDateTime lastSentAt, - - @Schema(description = "읽지 않은 메시지 개수", example = "12", requiredMode = REQUIRED) - Integer unreadMessageCount - ) { - public static InnerGroupChatRoomResponse from( - GroupChatRoom room, - GroupChatMessage lastMessage, - Map unreadCountMap - ) { - return new InnerGroupChatRoomResponse( - room.getId(), - room.getClub().getName(), - room.getClub().getImageUrl(), - lastMessage != null ? lastMessage.getContent() : null, - lastMessage != null ? lastMessage.getCreatedAt() : null, - unreadCountMap.getOrDefault(room.getId(), 0) - ); - } - } - - public static GroupChatRoomsResponse from( - List rooms, - Map lastMessageMap, - Map unreadCountMap - ) { - return new GroupChatRoomsResponse( - rooms.stream() - .map(room -> InnerGroupChatRoomResponse.from( - room, - lastMessageMap.get(room.getId()), - unreadCountMap - )) - .toList() - ); - } -} diff --git a/src/main/java/gg/agit/konect/domain/chat/group/model/GroupChatMessage.java b/src/main/java/gg/agit/konect/domain/chat/group/model/GroupChatMessage.java deleted file mode 100644 index a0180c3e0..000000000 --- a/src/main/java/gg/agit/konect/domain/chat/group/model/GroupChatMessage.java +++ /dev/null @@ -1,66 +0,0 @@ -package gg.agit.konect.domain.chat.group.model; - -import static jakarta.persistence.FetchType.LAZY; -import static jakarta.persistence.GenerationType.IDENTITY; -import static lombok.AccessLevel.PROTECTED; - -import gg.agit.konect.domain.user.model.User; -import gg.agit.konect.global.model.BaseEntity; -import jakarta.persistence.Column; -import jakarta.persistence.Entity; -import jakarta.persistence.GeneratedValue; -import jakarta.persistence.Id; -import jakarta.persistence.JoinColumn; -import jakarta.persistence.ManyToOne; -import jakarta.persistence.Table; -import lombok.Builder; -import lombok.Getter; -import lombok.NoArgsConstructor; - -@Getter -@Entity -@Table(name = "group_chat_message") -@NoArgsConstructor(access = PROTECTED) -public class GroupChatMessage extends BaseEntity { - - @Id - @GeneratedValue(strategy = IDENTITY) - @Column(name = "id", nullable = false, updatable = false, unique = true) - private Integer id; - - @ManyToOne(fetch = LAZY) - @JoinColumn(name = "room_id", nullable = false) - private GroupChatRoom room; - - @ManyToOne(fetch = LAZY) - @JoinColumn(name = "sender_id", nullable = false) - private User sender; - - @Column(name = "content", nullable = false, length = 1000) - private String content; - - @Builder - private GroupChatMessage( - Integer id, - GroupChatRoom room, - User sender, - String content - ) { - this.id = id; - this.room = room; - this.sender = sender; - this.content = content; - } - - public static GroupChatMessage of(GroupChatRoom room, User sender, String content) { - return GroupChatMessage.builder() - .room(room) - .sender(sender) - .content(content) - .build(); - } - - public boolean isSentBy(Integer userId) { - return sender.getId().equals(userId); - } -} diff --git a/src/main/java/gg/agit/konect/domain/chat/group/model/GroupChatReadStatus.java b/src/main/java/gg/agit/konect/domain/chat/group/model/GroupChatReadStatus.java deleted file mode 100644 index daeb2d1e2..000000000 --- a/src/main/java/gg/agit/konect/domain/chat/group/model/GroupChatReadStatus.java +++ /dev/null @@ -1,76 +0,0 @@ -package gg.agit.konect.domain.chat.group.model; - -import static jakarta.persistence.FetchType.LAZY; -import static lombok.AccessLevel.PROTECTED; - -import java.time.LocalDateTime; - -import gg.agit.konect.domain.user.model.User; -import jakarta.persistence.Column; -import jakarta.persistence.Entity; -import jakarta.persistence.Id; -import jakarta.persistence.IdClass; -import jakarta.persistence.JoinColumn; -import jakarta.persistence.ManyToOne; -import jakarta.persistence.Table; -import lombok.Builder; -import lombok.Getter; -import lombok.NoArgsConstructor; - -@Getter -@Entity -@Table(name = "group_chat_read_status") -@IdClass(GroupChatReadStatusId.class) -@NoArgsConstructor(access = PROTECTED) -public class GroupChatReadStatus { - - @Id - @Column(name = "room_id", nullable = false, updatable = false) - private Integer roomId; - - @Id - @Column(name = "user_id", nullable = false, updatable = false) - private Integer userId; - - @ManyToOne(fetch = LAZY) - @JoinColumn(name = "room_id", nullable = false, insertable = false, updatable = false) - private GroupChatRoom room; - - @ManyToOne(fetch = LAZY) - @JoinColumn(name = "user_id", nullable = false, insertable = false, updatable = false) - private User user; - - @Column(name = "last_read_at", nullable = false) - private LocalDateTime lastReadAt; - - @Builder - private GroupChatReadStatus( - Integer roomId, - Integer userId, - GroupChatRoom room, - User user, - LocalDateTime lastReadAt - ) { - this.roomId = roomId; - this.userId = userId; - this.room = room; - this.user = user; - this.lastReadAt = lastReadAt; - } - - public static GroupChatReadStatus of(GroupChatRoom room, User user, LocalDateTime lastReadAt) { - return GroupChatReadStatus.builder() - .roomId(room.getId()) - .userId(user.getId()) - .room(room) - .user(user) - .lastReadAt(lastReadAt) - .build(); - } - - public void updateLastReadAt(LocalDateTime lastReadAt) { - if (this.lastReadAt.isBefore(lastReadAt)) { - this.lastReadAt = lastReadAt; - } - } -} diff --git a/src/main/java/gg/agit/konect/domain/chat/group/model/GroupChatReadStatusId.java b/src/main/java/gg/agit/konect/domain/chat/group/model/GroupChatReadStatusId.java deleted file mode 100644 index cdf1abc8f..000000000 --- a/src/main/java/gg/agit/konect/domain/chat/group/model/GroupChatReadStatusId.java +++ /dev/null @@ -1,16 +0,0 @@ -package gg.agit.konect.domain.chat.group.model; - -import java.io.Serializable; - -import lombok.AllArgsConstructor; -import lombok.EqualsAndHashCode; -import lombok.NoArgsConstructor; - -@EqualsAndHashCode -@NoArgsConstructor -@AllArgsConstructor -public class GroupChatReadStatusId implements Serializable { - - private Integer roomId; - private Integer userId; -} diff --git a/src/main/java/gg/agit/konect/domain/chat/group/model/GroupChatRoom.java b/src/main/java/gg/agit/konect/domain/chat/group/model/GroupChatRoom.java deleted file mode 100644 index e93c3de13..000000000 --- a/src/main/java/gg/agit/konect/domain/chat/group/model/GroupChatRoom.java +++ /dev/null @@ -1,46 +0,0 @@ -package gg.agit.konect.domain.chat.group.model; - -import static jakarta.persistence.FetchType.LAZY; -import static jakarta.persistence.GenerationType.IDENTITY; -import static lombok.AccessLevel.PROTECTED; - -import gg.agit.konect.domain.club.model.Club; -import gg.agit.konect.global.model.BaseEntity; -import jakarta.persistence.Column; -import jakarta.persistence.Entity; -import jakarta.persistence.GeneratedValue; -import jakarta.persistence.Id; -import jakarta.persistence.JoinColumn; -import jakarta.persistence.ManyToOne; -import jakarta.persistence.Table; -import lombok.Builder; -import lombok.Getter; -import lombok.NoArgsConstructor; - -@Getter -@Entity -@Table(name = "group_chat_room") -@NoArgsConstructor(access = PROTECTED) -public class GroupChatRoom extends BaseEntity { - - @Id - @GeneratedValue(strategy = IDENTITY) - @Column(name = "id", nullable = false, updatable = false, unique = true) - private Integer id; - - @ManyToOne(fetch = LAZY) - @JoinColumn(name = "club_id", nullable = false, unique = true) - private Club club; - - @Builder - private GroupChatRoom(Integer id, Club club) { - this.id = id; - this.club = club; - } - - public static GroupChatRoom of(Club club) { - return GroupChatRoom.builder() - .club(club) - .build(); - } -} diff --git a/src/main/java/gg/agit/konect/domain/chat/group/repository/GroupChatMessageRepository.java b/src/main/java/gg/agit/konect/domain/chat/group/repository/GroupChatMessageRepository.java deleted file mode 100644 index 9ab29aa1c..000000000 --- a/src/main/java/gg/agit/konect/domain/chat/group/repository/GroupChatMessageRepository.java +++ /dev/null @@ -1,72 +0,0 @@ -package gg.agit.konect.domain.chat.group.repository; - -import java.util.List; -import java.util.Optional; - -import org.springframework.data.domain.Page; -import org.springframework.data.domain.Pageable; -import org.springframework.data.jpa.repository.Query; -import org.springframework.data.repository.Repository; -import org.springframework.data.repository.query.Param; - -import gg.agit.konect.domain.chat.group.model.GroupChatMessage; - -public interface GroupChatMessageRepository extends Repository { - - GroupChatMessage save(GroupChatMessage message); - - Optional findById(Integer messageId); - - @Query(""" - SELECT m - FROM GroupChatMessage m - JOIN FETCH m.sender - WHERE m.room.id = :roomId - ORDER BY m.createdAt DESC - """) - Page findByRoomIdOrderByCreatedAtDesc( - @Param("roomId") Integer roomId, - Pageable pageable - ); - - @Query(""" - SELECT COUNT(m) - FROM GroupChatMessage m - WHERE m.room.id = :roomId - """) - long countByRoomId(@Param("roomId") Integer roomId); - - @Query(""" - SELECT m - FROM GroupChatMessage m - JOIN FETCH m.sender - WHERE m.id IN ( - SELECT MAX(m2.id) - FROM GroupChatMessage m2 - WHERE m2.room.id IN :roomIds - GROUP BY m2.room.id - ) - """) - List findLatestMessagesByRoomIds(@Param("roomIds") List roomIds); - - @Query(value = """ - SELECT r.id AS roomId, COUNT(m.id) AS unreadCount - FROM group_chat_room r - JOIN club_member cm - ON cm.club_id = r.club_id - AND cm.user_id = :userId - LEFT JOIN group_chat_read_status grs - ON grs.room_id = r.id - AND grs.user_id = :userId - LEFT JOIN group_chat_message m - ON m.room_id = r.id - AND m.sender_id <> :userId - AND m.created_at > COALESCE(grs.last_read_at, cm.created_at) - WHERE r.id IN (:roomIds) - GROUP BY r.id - """, nativeQuery = true) - List countUnreadByRoomIdsAndUserId( - @Param("roomIds") List roomIds, - @Param("userId") Integer userId - ); -} diff --git a/src/main/java/gg/agit/konect/domain/chat/group/repository/GroupChatReadStatusRepository.java b/src/main/java/gg/agit/konect/domain/chat/group/repository/GroupChatReadStatusRepository.java deleted file mode 100644 index d11e27c46..000000000 --- a/src/main/java/gg/agit/konect/domain/chat/group/repository/GroupChatReadStatusRepository.java +++ /dev/null @@ -1,18 +0,0 @@ -package gg.agit.konect.domain.chat.group.repository; - -import java.util.List; -import java.util.Optional; - -import org.springframework.data.repository.Repository; - -import gg.agit.konect.domain.chat.group.model.GroupChatReadStatus; -import gg.agit.konect.domain.chat.group.model.GroupChatReadStatusId; - -public interface GroupChatReadStatusRepository extends Repository { - - GroupChatReadStatus save(GroupChatReadStatus status); - - Optional findByRoomIdAndUserId(Integer roomId, Integer userId); - - List findByRoomId(Integer roomId); -} diff --git a/src/main/java/gg/agit/konect/domain/chat/group/repository/GroupChatRoomRepository.java b/src/main/java/gg/agit/konect/domain/chat/group/repository/GroupChatRoomRepository.java deleted file mode 100644 index a2324c8c8..000000000 --- a/src/main/java/gg/agit/konect/domain/chat/group/repository/GroupChatRoomRepository.java +++ /dev/null @@ -1,42 +0,0 @@ -package gg.agit.konect.domain.chat.group.repository; - -import java.util.List; -import java.util.Optional; - -import org.springframework.data.jpa.repository.Query; -import org.springframework.data.repository.Repository; -import org.springframework.data.repository.query.Param; - -import gg.agit.konect.domain.chat.group.model.GroupChatRoom; -import gg.agit.konect.global.code.ApiResponseCode; -import gg.agit.konect.global.exception.CustomException; - -public interface GroupChatRoomRepository extends Repository { - - GroupChatRoom save(GroupChatRoom room); - - Optional findById(Integer roomId); - - @Query(""" - SELECT r - FROM GroupChatRoom r - JOIN FETCH r.club c - JOIN ClubMember cm ON cm.club.id = c.id - WHERE cm.user.id = :userId - ORDER BY r.updatedAt DESC - """) - List findAllByUserId(@Param("userId") Integer userId); - - Optional findByClubId(Integer clubId); - - default GroupChatRoom getById(Integer roomId) { - return findById(roomId) - .orElseThrow(() -> CustomException.of(ApiResponseCode.NOT_FOUND_GROUP_CHAT_ROOM)); - } - - default GroupChatRoom getByClubId(Integer clubId) { - return findByClubId(clubId) - .orElseThrow(() -> CustomException.of(ApiResponseCode.NOT_FOUND_GROUP_CHAT_ROOM)); - } - -} diff --git a/src/main/java/gg/agit/konect/domain/chat/group/repository/GroupRoomUnreadCountProjection.java b/src/main/java/gg/agit/konect/domain/chat/group/repository/GroupRoomUnreadCountProjection.java deleted file mode 100644 index 1fb947c6f..000000000 --- a/src/main/java/gg/agit/konect/domain/chat/group/repository/GroupRoomUnreadCountProjection.java +++ /dev/null @@ -1,8 +0,0 @@ -package gg.agit.konect.domain.chat.group.repository; - -public interface GroupRoomUnreadCountProjection { - - Integer getRoomId(); - - Long getUnreadCount(); -} diff --git a/src/main/java/gg/agit/konect/domain/chat/group/service/GroupChatService.java b/src/main/java/gg/agit/konect/domain/chat/group/service/GroupChatService.java deleted file mode 100644 index 85f4f0252..000000000 --- a/src/main/java/gg/agit/konect/domain/chat/group/service/GroupChatService.java +++ /dev/null @@ -1,305 +0,0 @@ -package gg.agit.konect.domain.chat.group.service; - -import java.time.LocalDateTime; -import java.util.HashSet; -import java.util.List; -import java.util.Map; -import java.util.Set; -import java.util.stream.Collectors; - -import org.springframework.data.domain.Page; -import org.springframework.data.domain.PageRequest; -import org.springframework.stereotype.Service; -import org.springframework.transaction.annotation.Transactional; - -import gg.agit.konect.domain.chat.direct.model.ChatRoom; -import gg.agit.konect.domain.chat.direct.repository.ChatRoomRepository; -import gg.agit.konect.domain.chat.group.dto.GroupChatMessageResponse; -import gg.agit.konect.domain.chat.group.dto.GroupChatMessagesResponse; -import gg.agit.konect.domain.chat.group.dto.GroupChatRoomResponse; -import gg.agit.konect.domain.chat.group.dto.GroupChatRoomsResponse; -import gg.agit.konect.domain.chat.group.model.GroupChatMessage; -import gg.agit.konect.domain.chat.group.model.GroupChatReadStatus; -import gg.agit.konect.domain.chat.group.model.GroupChatRoom; -import gg.agit.konect.domain.chat.group.repository.GroupChatMessageRepository; -import gg.agit.konect.domain.chat.group.repository.GroupChatReadStatusRepository; -import gg.agit.konect.domain.chat.group.repository.GroupChatRoomRepository; -import gg.agit.konect.domain.chat.group.repository.GroupRoomUnreadCountProjection; -import gg.agit.konect.domain.chat.unified.enums.ChatType; -import gg.agit.konect.domain.chat.unified.service.ChatPresenceService; -import gg.agit.konect.domain.club.model.ClubMember; -import gg.agit.konect.domain.club.repository.ClubMemberRepository; -import gg.agit.konect.domain.notification.service.NotificationService; -import gg.agit.konect.domain.notification.enums.NotificationTargetType; -import gg.agit.konect.domain.notification.model.NotificationMuteSetting; -import gg.agit.konect.domain.notification.repository.NotificationMuteSettingRepository; -import gg.agit.konect.domain.user.model.User; -import gg.agit.konect.domain.user.repository.UserRepository; -import gg.agit.konect.global.code.ApiResponseCode; -import gg.agit.konect.global.exception.CustomException; -import lombok.RequiredArgsConstructor; - -@Service -@RequiredArgsConstructor -@Transactional(readOnly = true) -public class GroupChatService { - - private final GroupChatRoomRepository groupChatRoomRepository; - private final GroupChatMessageRepository groupChatMessageRepository; - private final GroupChatReadStatusRepository groupChatReadStatusRepository; - private final NotificationMuteSettingRepository notificationMuteSettingRepository; - private final ChatRoomRepository chatRoomRepository; - private final ClubMemberRepository clubMemberRepository; - private final UserRepository userRepository; - private final ChatPresenceService chatPresenceService; - private final NotificationService notificationService; - - public GroupChatRoomResponse getGroupChatRoom(Integer clubId, Integer userId) { - validateClubMember(clubId, userId); - - GroupChatRoom room = groupChatRoomRepository.getByClubId(clubId); - return new GroupChatRoomResponse(room.getId()); - } - - public GroupChatRoomsResponse getChatRooms(Integer userId) { - List rooms = groupChatRoomRepository.findAllByUserId(userId); - List roomIds = rooms.stream() - .map(GroupChatRoom::getId) - .toList(); - - Map lastMessageMap = getLastMessageMap(roomIds); - Map unreadCountMap = getUnreadCountMap(roomIds, userId); - - return GroupChatRoomsResponse.from(rooms, lastMessageMap, unreadCountMap); - } - - @Transactional - public GroupChatMessagesResponse getMessagesByRoomId(Integer roomId, Integer userId, Integer page, Integer limit) { - GroupChatRoom room = groupChatRoomRepository.getById(roomId); - return getMessages(room.getClub().getId(), userId, page, limit); - } - - @Transactional - public GroupChatMessageResponse sendMessageByRoomId(Integer roomId, Integer userId, String content) { - GroupChatRoom room = groupChatRoomRepository.getById(roomId); - return sendMessage(room.getClub().getId(), userId, content); - } - - @Transactional - public GroupChatMessagesResponse getMessages(Integer clubId, Integer userId, Integer page, Integer limit) { - validateClubMember(clubId, userId); - - GroupChatRoom room = groupChatRoomRepository.getByClubId(clubId); - Integer roomId = room.getId(); - - chatPresenceService.recordPresence(roomId, userId); - - updateLastReadAt(roomId, userId, LocalDateTime.now()); - - PageRequest pageable = PageRequest.of(page - 1, limit); - long totalCount = groupChatMessageRepository.countByRoomId(roomId); - Page messagePage = groupChatMessageRepository - .findByRoomIdOrderByCreatedAtDesc(roomId, pageable); - List messages = messagePage.getContent(); - - List members = clubMemberRepository.findAllByClubId(clubId); - Map lastReadAtMap = groupChatReadStatusRepository.findByRoomId(roomId).stream() - .collect(java.util.stream.Collectors.toMap( - GroupChatReadStatus::getUserId, - GroupChatReadStatus::getLastReadAt - )); - - List responseMessages = messages.stream() - .map(message -> { - Integer messageId = message.getId(); - Integer senderId = message.getSender().getId(); - String senderName = message.getSender().getName(); - String messageContent = message.getContent(); - LocalDateTime createdAt = message.getCreatedAt(); - - int unreadCount = getUnreadCount(message, members, lastReadAtMap); - boolean isMine = senderId.equals(userId); - - return new GroupChatMessageResponse( - messageId, - senderId, - senderName, - messageContent, - createdAt, - unreadCount, - isMine - ); - }) - .toList(); - - int totalPage = limit > 0 ? (int)Math.ceil((double)totalCount / (double)limit) : 0; - - return new GroupChatMessagesResponse( - totalCount, - responseMessages.size(), - totalPage, - page, - clubId, - responseMessages - ); - } - - @Transactional - public GroupChatMessageResponse sendMessage(Integer clubId, Integer userId, String content) { - validateClubMember(clubId, userId); - - GroupChatRoom room = groupChatRoomRepository.getByClubId(clubId); - Integer roomId = room.getId(); - User sender = userRepository.getById(userId); - - GroupChatMessage message = groupChatMessageRepository.save(GroupChatMessage.of(room, sender, content)); - updateLastReadAt(roomId, userId, message.getCreatedAt()); - - Integer messageId = message.getId(); - Integer senderId = sender.getId(); - String senderName = sender.getName(); - String clubName = room.getClub().getName(); - String messageContent = message.getContent(); - LocalDateTime createdAt = message.getCreatedAt(); - - List recipientUserIds = clubMemberRepository.findUserIdsByClubId(clubId); - Set mutedUserIds = getMutedUserIds(roomId); - List filteredRecipients = recipientUserIds.stream() - .filter(recipientId -> !mutedUserIds.contains(recipientId)) - .toList(); - - notificationService.sendGroupChatNotification( - roomId, - senderId, - clubName, - senderName, - messageContent, - filteredRecipients - ); - - int memberCount = recipientUserIds.size(); - int unreadCount = memberCount - 1; - return new GroupChatMessageResponse( - messageId, - senderId, - senderName, - messageContent, - createdAt, - unreadCount, - true - ); - } - - @Transactional - public Boolean toggleMute(Integer userId, ChatType type, Integer chatRoomId) { - NotificationTargetType targetType; - - if (type == ChatType.DIRECT) { - ChatRoom directRoom = chatRoomRepository.findById(chatRoomId) - .orElseThrow(() -> CustomException.of(ApiResponseCode.NOT_FOUND_CHAT_ROOM)); - directRoom.validateIsParticipant(userId); - targetType = NotificationTargetType.DIRECT_CHAT_ROOM; - } else { - GroupChatRoom groupRoom = groupChatRoomRepository.getById(chatRoomId); - validateClubMember(groupRoom.getClub().getId(), userId); - targetType = NotificationTargetType.GROUP_CHAT_ROOM; - } - - User user = userRepository.getById(userId); - - return notificationMuteSettingRepository.findByTargetTypeAndTargetIdAndUserId( - targetType, - chatRoomId, - userId - ) - .map(setting -> { - setting.toggleMute(); - notificationMuteSettingRepository.save(setting); - return setting.getIsMuted(); - }) - .orElseGet(() -> { - notificationMuteSettingRepository.save(NotificationMuteSetting.of( - targetType, - chatRoomId, - user, - true - )); - return true; - }); - } - - private void validateClubMember(Integer clubId, Integer userId) { - if (!clubMemberRepository.existsByClubIdAndUserId(clubId, userId)) { - throw CustomException.of(ApiResponseCode.FORBIDDEN_GROUP_CHAT_ACCESS); - } - } - - private void updateLastReadAt(Integer roomId, Integer userId, LocalDateTime lastReadAt) { - groupChatReadStatusRepository.findByRoomIdAndUserId(roomId, userId) - .ifPresentOrElse(status -> { - status.updateLastReadAt(lastReadAt); - }, () -> { - GroupChatRoom room = groupChatRoomRepository.getById(roomId); - User user = userRepository.getById(userId); - groupChatReadStatusRepository.save(GroupChatReadStatus.of(room, user, lastReadAt)); - }); - } - - private Set getMutedUserIds(Integer roomId) { - List mutedUserIds = notificationMuteSettingRepository.findByTargetTypeAndTargetIdAndIsMutedTrue( - NotificationTargetType.GROUP_CHAT_ROOM, - roomId - ) - .stream() - .map(setting -> setting.getUser().getId()) - .toList(); - - return new HashSet<>(mutedUserIds); - } - - private int getUnreadCount( - GroupChatMessage message, - List members, - Map lastReadAtMap - ) { - LocalDateTime messageCreatedAt = message.getCreatedAt(); - int unreadCount = 0; - - for (ClubMember member : members) { - if (member.getCreatedAt().isAfter(messageCreatedAt)) { - continue; - } - - LocalDateTime lastReadAt = lastReadAtMap.getOrDefault(member.getUser().getId(), member.getCreatedAt()); - if (lastReadAt.isBefore(messageCreatedAt)) { - unreadCount++; - } - } - - return unreadCount; - } - - private Map getLastMessageMap(List roomIds) { - if (roomIds.isEmpty()) { - return Map.of(); - } - - return groupChatMessageRepository.findLatestMessagesByRoomIds(roomIds).stream() - .collect(Collectors.toMap( - message -> message.getRoom().getId(), - message -> message - )); - } - - private Map getUnreadCountMap(List roomIds, Integer userId) { - if (roomIds.isEmpty()) { - return Map.of(); - } - - return groupChatMessageRepository.countUnreadByRoomIdsAndUserId(roomIds, userId).stream() - .collect(Collectors.toMap( - GroupRoomUnreadCountProjection::getRoomId, - unreadCount -> unreadCount.getUnreadCount().intValue() - )); - } -} diff --git a/src/main/java/gg/agit/konect/domain/chat/direct/model/ChatMessage.java b/src/main/java/gg/agit/konect/domain/chat/model/ChatMessage.java similarity index 75% rename from src/main/java/gg/agit/konect/domain/chat/direct/model/ChatMessage.java rename to src/main/java/gg/agit/konect/domain/chat/model/ChatMessage.java index d6bf656fb..025b56687 100644 --- a/src/main/java/gg/agit/konect/domain/chat/direct/model/ChatMessage.java +++ b/src/main/java/gg/agit/konect/domain/chat/model/ChatMessage.java @@ -1,8 +1,7 @@ -package gg.agit.konect.domain.chat.direct.model; +package gg.agit.konect.domain.chat.model; import static jakarta.persistence.FetchType.LAZY; import static jakarta.persistence.GenerationType.IDENTITY; -import static java.lang.Boolean.FALSE; import static lombok.AccessLevel.PROTECTED; import gg.agit.konect.domain.user.model.User; @@ -34,9 +33,6 @@ public class ChatMessage extends BaseEntity { @Column(name = "content", nullable = false, length = 1000) private String content; - @Column(name = "is_read", nullable = false) - private Boolean isRead = FALSE; - @ManyToOne(fetch = LAZY) @JoinColumn(name = "chat_room_id", nullable = false) private ChatRoom chatRoom; @@ -45,34 +41,24 @@ public class ChatMessage extends BaseEntity { @JoinColumn(name = "sender_id", nullable = false) private User sender; - @ManyToOne(fetch = LAZY) - @JoinColumn(name = "receiver_id", nullable = false) - private User receiver; - @Builder private ChatMessage( Integer id, String content, - Boolean isRead, ChatRoom chatRoom, - User sender, - User receiver + User sender ) { this.id = id; this.content = content; - this.isRead = isRead; this.chatRoom = chatRoom; this.sender = sender; - this.receiver = receiver; } - public static ChatMessage of(ChatRoom chatRoom, User sender, User receiver, String content) { + public static ChatMessage of(ChatRoom chatRoom, User sender, String content) { return ChatMessage.builder() .content(content) .chatRoom(chatRoom) .sender(sender) - .receiver(receiver) - .isRead(FALSE) .build(); } @@ -80,7 +66,4 @@ public boolean isSentBy(Integer userId) { return sender.getId().equals(userId); } - public void markAsRead() { - this.isRead = true; - } } diff --git a/src/main/java/gg/agit/konect/domain/chat/direct/model/ChatRoom.java b/src/main/java/gg/agit/konect/domain/chat/model/ChatRoom.java similarity index 59% rename from src/main/java/gg/agit/konect/domain/chat/direct/model/ChatRoom.java rename to src/main/java/gg/agit/konect/domain/chat/model/ChatRoom.java index 43e295f16..f437e57f4 100644 --- a/src/main/java/gg/agit/konect/domain/chat/direct/model/ChatRoom.java +++ b/src/main/java/gg/agit/konect/domain/chat/model/ChatRoom.java @@ -1,14 +1,13 @@ -package gg.agit.konect.domain.chat.direct.model; +package gg.agit.konect.domain.chat.model; import static gg.agit.konect.global.code.ApiResponseCode.CANNOT_CREATE_CHAT_ROOM_WITH_SELF; -import static gg.agit.konect.global.code.ApiResponseCode.FORBIDDEN_CHAT_ROOM_ACCESS; import static jakarta.persistence.FetchType.LAZY; import static jakarta.persistence.GenerationType.IDENTITY; import static lombok.AccessLevel.PROTECTED; import java.time.LocalDateTime; -import gg.agit.konect.domain.user.enums.UserRole; +import gg.agit.konect.domain.club.model.Club; import gg.agit.konect.domain.user.model.User; import gg.agit.konect.global.exception.CustomException; import gg.agit.konect.global.model.BaseEntity; @@ -41,25 +40,22 @@ public class ChatRoom extends BaseEntity { private LocalDateTime lastMessageSentAt; @ManyToOne(fetch = LAZY) - @JoinColumn(name = "sender_id") - private User sender; - - @ManyToOne(fetch = LAZY) - @JoinColumn(name = "receiver_id") - private User receiver; + @JoinColumn(name = "club_id") + private Club club; @Builder - private ChatRoom(Integer id, User sender, User receiver) { + private ChatRoom(Integer id, Club club) { this.id = id; - this.sender = sender; - this.receiver = receiver; + this.club = club; } - public static ChatRoom of(User sender, User receiver) { - validateIsNotSameParticipant(sender, receiver); + public static ChatRoom directOf() { + return ChatRoom.builder().build(); + } + + public static ChatRoom groupOf(Club club) { return ChatRoom.builder() - .sender(sender) - .receiver(receiver) + .club(club) .build(); } @@ -69,25 +65,12 @@ public static void validateIsNotSameParticipant(User sender, User receiver) { } } - public void validateIsParticipant(Integer userId) { - if (!isParticipant(userId)) { - throw CustomException.of(FORBIDDEN_CHAT_ROOM_ACCESS); - } - } - - public boolean isParticipant(Integer userId) { - return sender.getId().equals(userId) || receiver.getId().equals(userId); + public boolean isDirectRoom() { + return club == null; } - public User getChatPartner(User currentUser) { - return sender.getId().equals(currentUser.getId()) ? receiver : sender; - } - - public User getNonAdminUser() { - if (sender.getRole() != UserRole.ADMIN) { - return sender; - } - return receiver; + public boolean isGroupRoom() { + return club != null; } public void updateLastMessage(String lastMessageContent, LocalDateTime lastMessageSentAt) { diff --git a/src/main/java/gg/agit/konect/domain/chat/model/ChatRoomMember.java b/src/main/java/gg/agit/konect/domain/chat/model/ChatRoomMember.java new file mode 100644 index 000000000..de7706f24 --- /dev/null +++ b/src/main/java/gg/agit/konect/domain/chat/model/ChatRoomMember.java @@ -0,0 +1,82 @@ +package gg.agit.konect.domain.chat.model; + +import static jakarta.persistence.FetchType.LAZY; +import static lombok.AccessLevel.PROTECTED; + +import java.time.LocalDateTime; + +import gg.agit.konect.domain.user.model.User; +import gg.agit.konect.global.model.BaseEntity; +import jakarta.persistence.Column; +import jakarta.persistence.EmbeddedId; +import jakarta.persistence.Entity; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.ManyToOne; +import jakarta.persistence.MapsId; +import jakarta.persistence.Table; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Getter +@Entity +@Table(name = "chat_room_member") +@NoArgsConstructor(access = PROTECTED) +public class ChatRoomMember extends BaseEntity { + + @EmbeddedId + private ChatRoomMemberId id; + + @ManyToOne(fetch = LAZY) + @MapsId("chatRoomId") + @JoinColumn(name = "chat_room_id", nullable = false) + private ChatRoom chatRoom; + + @ManyToOne(fetch = LAZY) + @MapsId("userId") + @JoinColumn(name = "user_id", nullable = false) + private User user; + + @Column(name = "last_read_at", nullable = false) + private LocalDateTime lastReadAt; + + @Builder + private ChatRoomMember( + ChatRoomMemberId id, + ChatRoom chatRoom, + User user, + LocalDateTime lastReadAt + ) { + this.id = id; + this.chatRoom = chatRoom; + this.user = user; + this.lastReadAt = lastReadAt; + } + + public static ChatRoomMember of(ChatRoom chatRoom, User user, LocalDateTime lastReadAt) { + return ChatRoomMember.builder() + .id(new ChatRoomMemberId(chatRoom.getId(), user.getId())) + .chatRoom(chatRoom) + .user(user) + .lastReadAt(lastReadAt) + .build(); + } + + public Integer getChatRoomId() { + return id.getChatRoomId(); + } + + public Integer getUserId() { + return id.getUserId(); + } + + public void updateLastReadAt(LocalDateTime lastReadAt) { + if (lastReadAt == null) { + return; + } + + if (this.lastReadAt == null || this.lastReadAt.isBefore(lastReadAt)) { + this.lastReadAt = lastReadAt; + } + } +} diff --git a/src/main/java/gg/agit/konect/domain/chat/model/ChatRoomMemberId.java b/src/main/java/gg/agit/konect/domain/chat/model/ChatRoomMemberId.java new file mode 100644 index 000000000..8e068756b --- /dev/null +++ b/src/main/java/gg/agit/konect/domain/chat/model/ChatRoomMemberId.java @@ -0,0 +1,28 @@ +package gg.agit.konect.domain.chat.model; + +import java.io.Serial; +import java.io.Serializable; + +import jakarta.persistence.Column; +import jakarta.persistence.Embeddable; +import lombok.AllArgsConstructor; +import lombok.EqualsAndHashCode; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Getter +@Embeddable +@EqualsAndHashCode +@NoArgsConstructor +@AllArgsConstructor +public class ChatRoomMemberId implements Serializable { + + @Serial + private static final long serialVersionUID = 1L; + + @Column(name = "chat_room_id", nullable = false, updatable = false) + private Integer chatRoomId; + + @Column(name = "user_id", nullable = false, updatable = false) + private Integer userId; +} diff --git a/src/main/java/gg/agit/konect/domain/chat/repository/ChatMessageRepository.java b/src/main/java/gg/agit/konect/domain/chat/repository/ChatMessageRepository.java new file mode 100644 index 000000000..f1ac16dec --- /dev/null +++ b/src/main/java/gg/agit/konect/domain/chat/repository/ChatMessageRepository.java @@ -0,0 +1,95 @@ +package gg.agit.konect.domain.chat.repository; + +import java.util.List; + +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.Repository; +import org.springframework.data.repository.query.Param; + +import gg.agit.konect.domain.chat.dto.UnreadMessageCount; +import gg.agit.konect.domain.chat.model.ChatMessage; +import gg.agit.konect.domain.user.enums.UserRole; + +public interface ChatMessageRepository extends Repository { + + ChatMessage save(ChatMessage chatMessage); + + @Query(""" + SELECT new gg.agit.konect.domain.chat.dto.UnreadMessageCount( + crm.id.chatRoomId, + COUNT(cm) + ) + FROM ChatRoomMember crm + LEFT JOIN ChatMessage cm ON cm.chatRoom.id = crm.id.chatRoomId + AND cm.sender.id <> :receiverId + AND cm.createdAt > crm.lastReadAt + WHERE crm.id.chatRoomId IN :chatRoomIds + AND crm.id.userId = :receiverId + GROUP BY crm.id.chatRoomId + """) + List countUnreadMessagesByChatRoomIdsAndUserId( + @Param("chatRoomIds") List chatRoomIds, + @Param("receiverId") Integer receiverId + ); + + @Query(""" + SELECT cm + FROM ChatMessage cm + JOIN FETCH cm.sender + WHERE cm.chatRoom.id = :chatRoomId + ORDER BY cm.createdAt DESC + """) + Page findByChatRoomId(@Param("chatRoomId") Integer chatRoomId, Pageable pageable); + + @Query(""" + SELECT new gg.agit.konect.domain.chat.dto.UnreadMessageCount( + crm.id.chatRoomId, + COUNT(cm) + ) + FROM ChatRoomMember crm + JOIN crm.user u + LEFT JOIN ChatMessage cm ON cm.chatRoom.id = crm.id.chatRoomId + AND cm.sender.role != :adminRole + AND cm.createdAt > crm.lastReadAt + WHERE crm.id.chatRoomId IN :chatRoomIds + AND u.role = :adminRole + GROUP BY crm.id.chatRoomId + """) + List countUnreadMessagesForAdmin( + @Param("chatRoomIds") List chatRoomIds, + @Param("adminRole") UserRole adminRole + ); + + @Query(""" + SELECT COUNT(cm) > 0 + FROM ChatMessage cm + WHERE cm.chatRoom.id = :chatRoomId + AND cm.sender.role != :adminRole + """) + boolean existsUserReplyByRoomId( + @Param("chatRoomId") Integer chatRoomId, + @Param("adminRole") UserRole adminRole + ); + + @Query(""" + SELECT m + FROM ChatMessage m + JOIN FETCH m.sender + WHERE m.id IN ( + SELECT MAX(m2.id) + FROM ChatMessage m2 + WHERE m2.chatRoom.id IN :roomIds + GROUP BY m2.chatRoom.id + ) + """) + List findLatestMessagesByRoomIds(@Param("roomIds") List roomIds); + + @Query(""" + SELECT COUNT(m) + FROM ChatMessage m + WHERE m.chatRoom.id = :chatRoomId + """) + long countByChatRoomId(@Param("chatRoomId") Integer chatRoomId); +} diff --git a/src/main/java/gg/agit/konect/domain/chat/repository/ChatRoomMemberRepository.java b/src/main/java/gg/agit/konect/domain/chat/repository/ChatRoomMemberRepository.java new file mode 100644 index 000000000..f3f70dafd --- /dev/null +++ b/src/main/java/gg/agit/konect/domain/chat/repository/ChatRoomMemberRepository.java @@ -0,0 +1,103 @@ +package gg.agit.konect.domain.chat.repository; + +import java.time.LocalDateTime; +import java.util.List; +import java.util.Optional; + +import org.springframework.data.jpa.repository.Modifying; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.Repository; +import org.springframework.data.repository.query.Param; + +import gg.agit.konect.domain.chat.model.ChatRoomMember; +import gg.agit.konect.domain.chat.model.ChatRoomMemberId; + +public interface ChatRoomMemberRepository extends Repository { + + ChatRoomMember save(ChatRoomMember chatRoomMember); + + @Query(""" + SELECT crm + FROM ChatRoomMember crm + WHERE crm.id.chatRoomId = :chatRoomId + AND crm.id.userId = :userId + """) + Optional findByChatRoomIdAndUserId( + @Param("chatRoomId") Integer chatRoomId, + @Param("userId") Integer userId + ); + + @Query(""" + SELECT COUNT(crm) > 0 + FROM ChatRoomMember crm + WHERE crm.id.chatRoomId = :chatRoomId + AND crm.id.userId = :userId + """) + boolean existsByChatRoomIdAndUserId( + @Param("chatRoomId") Integer chatRoomId, + @Param("userId") Integer userId + ); + + @Query(""" + SELECT crm + FROM ChatRoomMember crm + WHERE crm.id.chatRoomId = :chatRoomId + """) + List findByChatRoomId(@Param("chatRoomId") Integer chatRoomId); + + @Query(""" + SELECT crm + FROM ChatRoomMember crm + JOIN FETCH crm.user + WHERE crm.id.chatRoomId IN :chatRoomIds + """) + List findByChatRoomIds(@Param("chatRoomIds") List chatRoomIds); + + @Query(""" + SELECT crm + FROM ChatRoomMember crm + WHERE crm.id.userId = :userId + """) + List findByUserId(@Param("userId") Integer userId); + + @Modifying(clearAutomatically = true) + @Query(""" + UPDATE ChatRoomMember crm + SET crm.lastReadAt = :lastReadAt + WHERE crm.id.chatRoomId = :chatRoomId + AND crm.id.userId = :userId + AND crm.lastReadAt < :lastReadAt + """) + int updateLastReadAtIfOlder( + @Param("chatRoomId") Integer chatRoomId, + @Param("userId") Integer userId, + @Param("lastReadAt") LocalDateTime lastReadAt + ); + + @Modifying(clearAutomatically = true) + @Query(""" + DELETE FROM ChatRoomMember crm + WHERE crm.id.chatRoomId = :chatRoomId + AND crm.id.userId = :userId + """) + int deleteByChatRoomIdAndUserId( + @Param("chatRoomId") Integer chatRoomId, + @Param("userId") Integer userId + ); + + @Query(value = """ + SELECT crm.chat_room_id AS roomId, COUNT(cm.id) AS unreadCount + FROM chat_room_member crm + LEFT JOIN chat_message cm + ON cm.chat_room_id = crm.chat_room_id + AND cm.sender_id <> :userId + AND cm.created_at > crm.last_read_at + WHERE crm.user_id = :userId + AND crm.chat_room_id IN (:roomIds) + GROUP BY crm.chat_room_id + """, nativeQuery = true) + List countUnreadByRoomIdsAndUserId( + @Param("roomIds") List roomIds, + @Param("userId") Integer userId + ); +} diff --git a/src/main/java/gg/agit/konect/domain/chat/repository/ChatRoomRepository.java b/src/main/java/gg/agit/konect/domain/chat/repository/ChatRoomRepository.java new file mode 100644 index 000000000..29c38ce3e --- /dev/null +++ b/src/main/java/gg/agit/konect/domain/chat/repository/ChatRoomRepository.java @@ -0,0 +1,66 @@ +package gg.agit.konect.domain.chat.repository; + +import java.util.List; +import java.util.Optional; + +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.Repository; +import org.springframework.data.repository.query.Param; + +import gg.agit.konect.domain.chat.model.ChatRoom; + +public interface ChatRoomRepository extends Repository { + + ChatRoom save(ChatRoom chatRoom); + + @Query(""" + SELECT DISTINCT cr + FROM ChatRoom cr + JOIN ChatRoomMember crm ON crm.id.chatRoomId = cr.id + LEFT JOIN FETCH cr.club + WHERE crm.id.userId = :userId + AND cr.club IS NULL + ORDER BY cr.lastMessageSentAt DESC NULLS LAST, cr.id + """) + List findByUserId(@Param("userId") Integer userId); + + @Query(""" + SELECT cr + FROM ChatRoom cr + LEFT JOIN FETCH cr.club + WHERE cr.id = :chatRoomId + """) + Optional findById(@Param("chatRoomId") Integer chatRoomId); + + @Query(""" + SELECT cr + FROM ChatRoom cr + JOIN ChatRoomMember crm + ON crm.id.chatRoomId = cr.id + WHERE cr.club IS NULL + GROUP BY cr + HAVING COUNT(crm) = 2 + AND SUM(CASE WHEN crm.id.userId = :userId1 THEN 1 ELSE 0 END) = 1 + AND SUM(CASE WHEN crm.id.userId = :userId2 THEN 1 ELSE 0 END) = 1 + """) + Optional findByTwoUsers(@Param("userId1") Integer userId1, @Param("userId2") Integer userId2); + + @Query(""" + SELECT cr + FROM ChatRoom cr + LEFT JOIN FETCH cr.club c + WHERE c.id = :clubId + """) + Optional findByClubId(@Param("clubId") Integer clubId); + + @Query(""" + SELECT DISTINCT cr + FROM ChatRoom cr + JOIN ChatRoomMember crm ON crm.id.chatRoomId = cr.id + LEFT JOIN FETCH cr.club + WHERE crm.id.userId = :userId + AND cr.club IS NOT NULL + ORDER BY cr.lastMessageSentAt DESC NULLS LAST, cr.id + """) + List findGroupRoomsByUserId(@Param("userId") Integer userId); +} diff --git a/src/main/java/gg/agit/konect/domain/chat/repository/RoomUnreadCountProjection.java b/src/main/java/gg/agit/konect/domain/chat/repository/RoomUnreadCountProjection.java new file mode 100644 index 000000000..62f7579e5 --- /dev/null +++ b/src/main/java/gg/agit/konect/domain/chat/repository/RoomUnreadCountProjection.java @@ -0,0 +1,8 @@ +package gg.agit.konect.domain.chat.repository; + +public interface RoomUnreadCountProjection { + + Integer getRoomId(); + + Long getUnreadCount(); +} diff --git a/src/main/java/gg/agit/konect/domain/chat/unified/service/ChatPresenceService.java b/src/main/java/gg/agit/konect/domain/chat/service/ChatPresenceService.java similarity index 97% rename from src/main/java/gg/agit/konect/domain/chat/unified/service/ChatPresenceService.java rename to src/main/java/gg/agit/konect/domain/chat/service/ChatPresenceService.java index 815b1aae7..956a4d852 100644 --- a/src/main/java/gg/agit/konect/domain/chat/unified/service/ChatPresenceService.java +++ b/src/main/java/gg/agit/konect/domain/chat/service/ChatPresenceService.java @@ -1,4 +1,4 @@ -package gg.agit.konect.domain.chat.unified.service; +package gg.agit.konect.domain.chat.service; import java.time.Duration; diff --git a/src/main/java/gg/agit/konect/domain/chat/service/ChatRoomMembershipService.java b/src/main/java/gg/agit/konect/domain/chat/service/ChatRoomMembershipService.java new file mode 100644 index 000000000..5e3d6bc30 --- /dev/null +++ b/src/main/java/gg/agit/konect/domain/chat/service/ChatRoomMembershipService.java @@ -0,0 +1,55 @@ +package gg.agit.konect.domain.chat.service; + +import java.time.LocalDateTime; +import java.util.Objects; + +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import gg.agit.konect.domain.chat.model.ChatRoom; +import gg.agit.konect.domain.chat.model.ChatRoomMember; +import gg.agit.konect.domain.chat.repository.ChatRoomMemberRepository; +import gg.agit.konect.domain.chat.repository.ChatRoomRepository; +import gg.agit.konect.domain.club.model.ClubMember; +import gg.agit.konect.domain.user.model.User; +import lombok.RequiredArgsConstructor; + +@Service +@RequiredArgsConstructor +@Transactional(readOnly = true) +public class ChatRoomMembershipService { + + private final ChatRoomRepository chatRoomRepository; + private final ChatRoomMemberRepository chatRoomMemberRepository; + + @Transactional + public void addClubMember(ClubMember clubMember) { + LocalDateTime baseline = Objects.requireNonNull(clubMember.getCreatedAt()); + ChatRoom room = chatRoomRepository.findByClubId(clubMember.getClub().getId()) + .orElseGet(() -> chatRoomRepository.save(ChatRoom.groupOf(clubMember.getClub()))); + ensureMember(room, clubMember.getUser(), baseline); + } + + @Transactional + public void addDirectMembers(ChatRoom room, User firstUser, User secondUser, LocalDateTime joinedAt) { + LocalDateTime baseline = Objects.requireNonNull(joinedAt); + ensureMember(room, firstUser, baseline); + ensureMember(room, secondUser, baseline); + } + + private void ensureMember(ChatRoom room, User user, LocalDateTime baseline) { + chatRoomMemberRepository.findByChatRoomIdAndUserId(room.getId(), user.getId()) + .ifPresentOrElse(member -> { + LocalDateTime lastReadAt = member.getLastReadAt(); + if (lastReadAt == null || lastReadAt.isBefore(baseline)) { + member.updateLastReadAt(baseline); + } + }, () -> chatRoomMemberRepository.save(ChatRoomMember.of(room, user, baseline))); + } + + @Transactional + public void removeClubMember(Integer clubId, Integer userId) { + chatRoomRepository.findByClubId(clubId) + .ifPresent(room -> chatRoomMemberRepository.deleteByChatRoomIdAndUserId(room.getId(), userId)); + } +} diff --git a/src/main/java/gg/agit/konect/domain/chat/service/ChatService.java b/src/main/java/gg/agit/konect/domain/chat/service/ChatService.java new file mode 100644 index 000000000..83bbe72bb --- /dev/null +++ b/src/main/java/gg/agit/konect/domain/chat/service/ChatService.java @@ -0,0 +1,724 @@ +package gg.agit.konect.domain.chat.service; + +import static gg.agit.konect.global.code.ApiResponseCode.CANNOT_CREATE_CHAT_ROOM_WITH_SELF; +import static gg.agit.konect.global.code.ApiResponseCode.FORBIDDEN_CHAT_ROOM_ACCESS; +import static gg.agit.konect.global.code.ApiResponseCode.NOT_FOUND_CHAT_ROOM; + +import java.time.LocalDateTime; +import java.util.ArrayList; +import java.util.Comparator; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Set; +import java.util.stream.Collectors; + +import org.springframework.context.ApplicationEventPublisher; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageRequest; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import gg.agit.konect.domain.chat.dto.ChatMessageDetailResponse; +import gg.agit.konect.domain.chat.dto.ChatMessagePageResponse; +import gg.agit.konect.domain.chat.dto.ChatMessageSendRequest; +import gg.agit.konect.domain.chat.dto.ChatMuteResponse; +import gg.agit.konect.domain.chat.dto.ChatRoomCreateRequest; +import gg.agit.konect.domain.chat.dto.ChatRoomResponse; +import gg.agit.konect.domain.chat.dto.ChatRoomSummaryResponse; +import gg.agit.konect.domain.chat.dto.ChatRoomsSummaryResponse; +import gg.agit.konect.domain.chat.dto.UnreadMessageCount; +import gg.agit.konect.domain.chat.enums.ChatType; +import gg.agit.konect.domain.chat.event.AdminChatReceivedEvent; +import gg.agit.konect.domain.chat.model.ChatMessage; +import gg.agit.konect.domain.chat.model.ChatRoom; +import gg.agit.konect.domain.chat.model.ChatRoomMember; +import gg.agit.konect.domain.chat.repository.ChatMessageRepository; +import gg.agit.konect.domain.chat.repository.ChatRoomMemberRepository; +import gg.agit.konect.domain.chat.repository.ChatRoomRepository; +import gg.agit.konect.domain.chat.repository.RoomUnreadCountProjection; +import gg.agit.konect.domain.club.model.Club; +import gg.agit.konect.domain.club.model.ClubMember; +import gg.agit.konect.domain.club.repository.ClubMemberRepository; +import gg.agit.konect.domain.notification.enums.NotificationTargetType; +import gg.agit.konect.domain.notification.model.NotificationMuteSetting; +import gg.agit.konect.domain.notification.repository.NotificationMuteSettingRepository; +import gg.agit.konect.domain.notification.service.NotificationService; +import gg.agit.konect.domain.user.enums.UserRole; +import gg.agit.konect.domain.user.model.User; +import gg.agit.konect.domain.user.repository.UserRepository; +import gg.agit.konect.global.code.ApiResponseCode; +import gg.agit.konect.global.exception.CustomException; +import lombok.RequiredArgsConstructor; + +@Service +@RequiredArgsConstructor +@Transactional(readOnly = true) +public class ChatService { + + private final ChatRoomRepository chatRoomRepository; + private final ChatMessageRepository chatMessageRepository; + private final ChatRoomMemberRepository chatRoomMemberRepository; + private final NotificationMuteSettingRepository notificationMuteSettingRepository; + private final ClubMemberRepository clubMemberRepository; + private final UserRepository userRepository; + private final ChatPresenceService chatPresenceService; + private final NotificationService notificationService; + private final ApplicationEventPublisher eventPublisher; + + @Transactional + public ChatRoomResponse createOrGetChatRoom(Integer currentUserId, ChatRoomCreateRequest request) { + User currentUser = userRepository.getById(currentUserId); + User targetUser = userRepository.getById(request.userId()); + + if (currentUser.getId().equals(targetUser.getId())) { + throw CustomException.of(CANNOT_CREATE_CHAT_ROOM_WITH_SELF); + } + ChatRoom.validateIsNotSameParticipant(currentUser, targetUser); + + ChatRoom chatRoom = chatRoomRepository.findByTwoUsers(currentUser.getId(), targetUser.getId()) + .orElseGet(() -> chatRoomRepository.save(ChatRoom.directOf())); + + LocalDateTime joinedAt = Objects.requireNonNull(chatRoom.getCreatedAt(), "chatRoom.createdAt must not be null"); + ensureRoomMember(chatRoom, currentUser, joinedAt); + ensureRoomMember(chatRoom, targetUser, joinedAt); + + return ChatRoomResponse.from(chatRoom); + } + + @Transactional + public ChatRoomsSummaryResponse getChatRooms(Integer userId) { + List directRooms = getDirectChatRooms(userId); + List clubRooms = getClubChatRooms(userId); + + List roomIds = new ArrayList<>(); + roomIds.addAll(directRooms.stream().map(ChatRoomSummaryResponse::roomId).toList()); + roomIds.addAll(clubRooms.stream().map(ChatRoomSummaryResponse::roomId).toList()); + + Map muteMap = getMuteMap(roomIds, userId); + List rooms = new ArrayList<>(); + + directRooms.forEach(room -> rooms.add(new ChatRoomSummaryResponse( + room.roomId(), + room.chatType(), + room.roomName(), + room.roomImageUrl(), + room.lastMessage(), + room.lastSentAt(), + room.unreadCount(), + muteMap.getOrDefault(room.roomId(), false) + ))); + + clubRooms.forEach(room -> rooms.add(new ChatRoomSummaryResponse( + room.roomId(), + room.chatType(), + room.roomName(), + room.roomImageUrl(), + room.lastMessage(), + room.lastSentAt(), + room.unreadCount(), + muteMap.getOrDefault(room.roomId(), false) + ))); + + rooms.sort( + Comparator.comparing(ChatRoomSummaryResponse::lastSentAt, Comparator.nullsLast(Comparator.reverseOrder())) + .thenComparing(ChatRoomSummaryResponse::roomId) + ); + + return new ChatRoomsSummaryResponse(rooms); + } + + @Transactional + public ChatMessagePageResponse getMessages(Integer userId, Integer roomId, Integer page, Integer limit) { + ChatRoom room = chatRoomRepository.findById(roomId) + .orElseThrow(() -> CustomException.of(NOT_FOUND_CHAT_ROOM)); + + if (room.isDirectRoom()) { + return getDirectChatRoomMessages(userId, roomId, page, limit); + } + + return getClubMessagesByRoomId(roomId, userId, page, limit); + } + + @Transactional + public ChatMessageDetailResponse sendMessage(Integer userId, Integer roomId, ChatMessageSendRequest request) { + ChatRoom room = chatRoomRepository.findById(roomId) + .orElseThrow(() -> CustomException.of(NOT_FOUND_CHAT_ROOM)); + + if (room.isDirectRoom()) { + return sendDirectMessage(userId, roomId, request); + } + + return sendClubMessageByRoomId(roomId, userId, request.content()); + } + + @Transactional + public ChatMuteResponse toggleMute(Integer userId, Integer roomId) { + ChatRoom room = chatRoomRepository.findById(roomId) + .orElseThrow(() -> CustomException.of(ApiResponseCode.NOT_FOUND_CHAT_ROOM)); + + if (room.isGroupRoom()) { + ClubMember member = clubMemberRepository.getByClubIdAndUserId(room.getClub().getId(), userId); + ensureRoomMember(room, member.getUser(), member.getCreatedAt()); + } else { + validateDirectAccess(room, userId); + } + + User user = userRepository.getById(userId); + Boolean isMuted = notificationMuteSettingRepository.findByTargetTypeAndTargetIdAndUserId( + NotificationTargetType.CHAT_ROOM, + roomId, + userId + ) + .map(setting -> { + setting.toggleMute(); + notificationMuteSettingRepository.save(setting); + return setting.getIsMuted(); + }) + .orElseGet(() -> { + notificationMuteSettingRepository.save(NotificationMuteSetting.of( + NotificationTargetType.CHAT_ROOM, + roomId, + user, + true + )); + return true; + }); + + return new ChatMuteResponse(isMuted); + } + + private List getDirectChatRooms(Integer userId) { + User user = userRepository.getById(userId); + List roomSummaries = new ArrayList<>(); + + List personalChatRooms = chatRoomRepository.findByUserId(userId); + Map> roomMembersMap = getRoomMembersMap(personalChatRooms); + Map personalUnreadCountMap = getUnreadCountMap(extractChatRoomIds(personalChatRooms), userId); + + Set addedChatRoomIds = new HashSet<>(); + if (user.getRole() == UserRole.ADMIN) { + Map adminUnreadCountMap = getAdminUnreadCountMap(extractChatRoomIds(personalChatRooms)); + + for (ChatRoom chatRoom : personalChatRooms) { + List members = roomMembersMap.getOrDefault(chatRoom.getId(), List.of()); + User nonAdminUser = findNonAdminMember(members); + if (nonAdminUser == null) { + continue; + } + if (!chatMessageRepository.existsUserReplyByRoomId(chatRoom.getId(), UserRole.ADMIN)) { + continue; + } + + roomSummaries.add(new ChatRoomSummaryResponse( + chatRoom.getId(), + ChatType.DIRECT, + nonAdminUser.getName(), + nonAdminUser.getImageUrl(), + chatRoom.getLastMessageContent(), + chatRoom.getLastMessageSentAt(), + adminUnreadCountMap.getOrDefault(chatRoom.getId(), 0), + false + )); + addedChatRoomIds.add(chatRoom.getId()); + } + } + + for (ChatRoom chatRoom : personalChatRooms) { + if (!addedChatRoomIds.contains(chatRoom.getId())) { + List members = roomMembersMap.getOrDefault(chatRoom.getId(), List.of()); + User chatPartner = findDirectPartner(members, user.getId()); + if (chatPartner == null) { + continue; + } + + roomSummaries.add(new ChatRoomSummaryResponse( + chatRoom.getId(), + ChatType.DIRECT, + chatPartner.getName(), + chatPartner.getImageUrl(), + chatRoom.getLastMessageContent(), + chatRoom.getLastMessageSentAt(), + personalUnreadCountMap.getOrDefault(chatRoom.getId(), 0), + false + )); + } + } + + roomSummaries.sort(Comparator + .comparing( + ChatRoomSummaryResponse::lastSentAt, + Comparator.nullsLast(Comparator.reverseOrder()) + ) + .thenComparing(ChatRoomSummaryResponse::roomId)); + + return roomSummaries; + } + + private ChatMessagePageResponse getDirectChatRoomMessages( + Integer userId, + Integer roomId, + Integer page, + Integer limit + ) { + ChatRoom chatRoom = getDirectRoom(roomId); + User user = userRepository.getById(userId); + validateDirectAccess(chatRoom, userId); + + ChatRoomMember member = getRoomMember(roomId, userId); + LocalDateTime readAt = LocalDateTime.now(); + chatPresenceService.recordPresence(roomId, userId); + member.updateLastReadAt(readAt); + + PageRequest pageable = PageRequest.of(page - 1, limit); + Page messages = chatMessageRepository.findByChatRoomId(roomId, pageable); + + Integer maskedAdminId = getMaskedAdminId(user, chatRoom); + List responseMessages = messages.getContent().stream() + .map(message -> { + Integer senderId = resolveDirectSenderId(message, maskedAdminId); + boolean isMine = message.isSentBy(userId); + boolean isRead = isMine || !message.getCreatedAt().isAfter(readAt); + return new ChatMessageDetailResponse( + message.getId(), + senderId, + null, + message.getContent(), + message.getCreatedAt(), + isRead, + null, + isMine + ); + }) + .toList(); + + return new ChatMessagePageResponse( + messages.getTotalElements(), + messages.getNumberOfElements(), + messages.getTotalPages(), + messages.getNumber() + 1, + null, + responseMessages + ); + } + + private ChatMessageDetailResponse sendDirectMessage( + Integer userId, + Integer roomId, + ChatMessageSendRequest request + ) { + ChatRoom chatRoom = getDirectRoom(roomId); + User sender = userRepository.getById(userId); + validateDirectAccess(chatRoom, userId); + + List members = chatRoomMemberRepository.findByChatRoomId(roomId); + User receiver = resolveMessageReceiver(sender, members); + + ChatMessage chatMessage = chatMessageRepository.save( + ChatMessage.of(chatRoom, sender, request.content()) + ); + chatRoom.updateLastMessage(chatMessage.getContent(), chatMessage.getCreatedAt()); + updateMemberLastReadAt(roomId, userId, chatMessage.getCreatedAt()); + + notificationService.sendChatNotification(receiver.getId(), roomId, sender.getName(), request.content()); + publishAdminChatEventIfNeeded(receiver, sender, request.content()); + + return new ChatMessageDetailResponse( + chatMessage.getId(), + chatMessage.getSender().getId(), + null, + chatMessage.getContent(), + chatMessage.getCreatedAt(), + true, + null, + true + ); + } + + private List getClubChatRooms(Integer userId) { + List memberships = clubMemberRepository.findAllByUserId(userId); + if (memberships.isEmpty()) { + return List.of(); + } + + Map membershipByClubId = memberships.stream() + .collect(Collectors.toMap(cm -> cm.getClub().getId(), cm -> cm, (a, b) -> a)); + + List rooms = memberships.stream() + .map(ClubMember::getClub) + .map(this::resolveOrCreateClubRoom) + .toList(); + + for (ChatRoom room : rooms) { + ClubMember member = membershipByClubId.get(room.getClub().getId()); + if (member != null) { + ensureRoomMember(room, member.getUser(), member.getCreatedAt()); + } + } + + List roomIds = rooms.stream().map(ChatRoom::getId).toList(); + Map lastMessageMap = getLastMessageMap(roomIds); + Map unreadCountMap = getRoomUnreadCountMap(roomIds, userId); + + return rooms.stream() + .map(room -> { + ChatMessage lastMessage = lastMessageMap.get(room.getId()); + return new ChatRoomSummaryResponse( + room.getId(), + ChatType.GROUP, + room.getClub().getName(), + room.getClub().getImageUrl(), + lastMessage != null ? lastMessage.getContent() : null, + lastMessage != null ? lastMessage.getCreatedAt() : null, + unreadCountMap.getOrDefault(room.getId(), 0), + false + ); + }) + .toList(); + } + + private ChatMessagePageResponse getClubMessagesByRoomId( + Integer roomId, + Integer userId, + Integer page, + Integer limit + ) { + ChatRoom room = getClubRoom(roomId); + ClubMember member = clubMemberRepository.getByClubIdAndUserId(room.getClub().getId(), userId); + ensureRoomMember(room, member.getUser(), member.getCreatedAt()); + + chatPresenceService.recordPresence(roomId, userId); + updateLastReadAt(roomId, userId, LocalDateTime.now()); + + PageRequest pageable = PageRequest.of(page - 1, limit); + long totalCount = chatMessageRepository.countByChatRoomId(roomId); + Page messagePage = chatMessageRepository.findByChatRoomId(roomId, pageable); + List messages = messagePage.getContent(); + List members = chatRoomMemberRepository.findByChatRoomId(roomId); + List sortedReadBaselines = toSortedReadBaselines(members); + + List responseMessages = messages.stream() + .map(message -> { + int unreadCount = countUnreadSince(message.getCreatedAt(), sortedReadBaselines); + return new ChatMessageDetailResponse( + message.getId(), + message.getSender().getId(), + message.getSender().getName(), + message.getContent(), + message.getCreatedAt(), + null, + unreadCount, + message.isSentBy(userId) + ); + }) + .toList(); + + int totalPage = limit > 0 ? (int)Math.ceil((double)totalCount / (double)limit) : 0; + return new ChatMessagePageResponse( + totalCount, + responseMessages.size(), + totalPage, + page, + room.getClub().getId(), + responseMessages + ); + } + + private ChatMessageDetailResponse sendClubMessageByRoomId(Integer roomId, Integer userId, String content) { + ChatRoom room = getClubRoom(roomId); + ClubMember member = clubMemberRepository.getByClubIdAndUserId(room.getClub().getId(), userId); + User sender = member.getUser(); + + ensureRoomMember(room, sender, member.getCreatedAt()); + + ChatMessage message = chatMessageRepository.save(ChatMessage.of(room, sender, content)); + room.updateLastMessage(message.getContent(), message.getCreatedAt()); + updateLastReadAt(roomId, userId, message.getCreatedAt()); + + List members = chatRoomMemberRepository.findByChatRoomId(roomId); + List recipientUserIds = members.stream().map(ChatRoomMember::getUserId).toList(); + List sortedReadBaselines = toSortedReadBaselines(members); + + notificationService.sendGroupChatNotification( + roomId, + sender.getId(), + room.getClub().getName(), + sender.getName(), + message.getContent(), + recipientUserIds + ); + + return new ChatMessageDetailResponse( + message.getId(), + sender.getId(), + sender.getName(), + message.getContent(), + message.getCreatedAt(), + null, + countUnreadSince(message.getCreatedAt(), sortedReadBaselines), + true + ); + } + + private Map getMuteMap(List roomIds, Integer userId) { + if (roomIds.isEmpty()) { + return Map.of(); + } + + List settings = notificationMuteSettingRepository + .findByTargetTypeAndTargetIdsAndUserId(NotificationTargetType.CHAT_ROOM, roomIds, userId); + + Map muteMap = new HashMap<>(); + for (NotificationMuteSetting setting : settings) { + Integer targetId = setting.getTargetId(); + if (targetId != null) { + muteMap.put(targetId, setting.getIsMuted()); + } + } + + return muteMap; + } + + private ChatRoom getDirectRoom(Integer roomId) { + ChatRoom chatRoom = chatRoomRepository.findById(roomId) + .orElseThrow(() -> CustomException.of(NOT_FOUND_CHAT_ROOM)); + + if (!chatRoom.isDirectRoom()) { + throw CustomException.of(NOT_FOUND_CHAT_ROOM); + } + + return chatRoom; + } + + private ChatRoom getClubRoom(Integer roomId) { + ChatRoom room = chatRoomRepository.findById(roomId) + .orElseThrow(() -> CustomException.of(ApiResponseCode.NOT_FOUND_CHAT_ROOM)); + if (!room.isGroupRoom() || room.getClub() == null) { + throw CustomException.of(ApiResponseCode.NOT_FOUND_GROUP_CHAT_ROOM); + } + return room; + } + + private ChatRoom resolveOrCreateClubRoom(Club club) { + return chatRoomRepository.findByClubId(club.getId()) + .orElseGet(() -> chatRoomRepository.save(ChatRoom.groupOf(club))); + } + + private List extractChatRoomIds(List chatRooms) { + return chatRooms.stream() + .map(ChatRoom::getId) + .toList(); + } + + private Map getUnreadCountMap(List chatRoomIds, Integer userId) { + if (chatRoomIds.isEmpty()) { + return Map.of(); + } + + List unreadMessageCounts = chatMessageRepository.countUnreadMessagesByChatRoomIdsAndUserId( + chatRoomIds, + userId + ); + + return unreadMessageCounts.stream() + .collect(Collectors.toMap( + UnreadMessageCount::chatRoomId, + unreadMessageCount -> unreadMessageCount.unreadCount().intValue() + )); + } + + private Map getAdminUnreadCountMap(List chatRoomIds) { + if (chatRoomIds.isEmpty()) { + return Map.of(); + } + + List unreadMessageCounts = chatMessageRepository.countUnreadMessagesForAdmin( + chatRoomIds, + UserRole.ADMIN + ); + + return unreadMessageCounts.stream() + .collect(Collectors.toMap( + UnreadMessageCount::chatRoomId, + unreadMessageCount -> unreadMessageCount.unreadCount().intValue() + )); + } + + private Integer getMaskedAdminId(User user, ChatRoom chatRoom) { + if (user.getRole() == UserRole.ADMIN) { + return null; + } + + List members = chatRoomMemberRepository.findByChatRoomId(chatRoom.getId()); + User adminUser = findAdminMember(members); + if (adminUser == null) { + return null; + } + + return adminUser.getId(); + } + + private void publishAdminChatEventIfNeeded(User receiver, User sender, String content) { + if (receiver.getRole() == UserRole.ADMIN) { + eventPublisher.publishEvent(AdminChatReceivedEvent.of(sender.getId(), sender.getName(), content)); + } + } + + private ChatRoomMember getRoomMember(Integer roomId, Integer userId) { + return chatRoomMemberRepository.findByChatRoomIdAndUserId(roomId, userId) + .orElseThrow(() -> CustomException.of(FORBIDDEN_CHAT_ROOM_ACCESS)); + } + + private void ensureRoomMember(ChatRoom room, User user, LocalDateTime joinedAt) { + chatRoomMemberRepository.findByChatRoomIdAndUserId(room.getId(), user.getId()) + .ifPresentOrElse(member -> { + LocalDateTime lastReadAt = member.getLastReadAt(); + if (lastReadAt == null || lastReadAt.isBefore(joinedAt)) { + member.updateLastReadAt(joinedAt); + } + }, () -> chatRoomMemberRepository.save(ChatRoomMember.of(room, user, joinedAt))); + } + + private void updateMemberLastReadAt(Integer roomId, Integer userId, LocalDateTime lastReadAt) { + int updated = chatRoomMemberRepository.updateLastReadAtIfOlder(roomId, userId, lastReadAt); + if (updated == 0) { + ChatRoom room = chatRoomRepository.findById(roomId) + .orElseThrow(() -> CustomException.of(NOT_FOUND_CHAT_ROOM)); + User user = userRepository.getById(userId); + ensureRoomMember(room, user, lastReadAt); + } + } + + private void updateLastReadAt(Integer roomId, Integer userId, LocalDateTime lastReadAt) { + int updated = chatRoomMemberRepository.updateLastReadAtIfOlder(roomId, userId, lastReadAt); + if (updated == 0) { + ChatRoom room = getClubRoom(roomId); + ClubMember member = clubMemberRepository.getByClubIdAndUserId(room.getClub().getId(), userId); + ensureRoomMember(room, member.getUser(), member.getCreatedAt()); + } + } + + private List toSortedReadBaselines(List members) { + List baselines = members.stream() + .map(ChatRoomMember::getLastReadAt) + .sorted() + .toList(); + return baselines; + } + + private int countUnreadSince(LocalDateTime messageCreatedAt, List sortedReadBaselines) { + int left = 0; + int right = sortedReadBaselines.size(); + + while (left < right) { + int mid = (left + right) >>> 1; + LocalDateTime baseline = sortedReadBaselines.get(mid); + + if (baseline.isBefore(messageCreatedAt)) { + left = mid + 1; + } else { + right = mid; + } + } + + return left; + } + + private Map getLastMessageMap(List roomIds) { + if (roomIds.isEmpty()) { + return Map.of(); + } + + return chatMessageRepository.findLatestMessagesByRoomIds(roomIds).stream() + .collect(Collectors.toMap(message -> message.getChatRoom().getId(), message -> message)); + } + + private Map getRoomUnreadCountMap(List roomIds, Integer userId) { + if (roomIds.isEmpty()) { + return Map.of(); + } + + Map unreadCountMap = new HashMap<>(); + List projections = chatRoomMemberRepository.countUnreadByRoomIdsAndUserId( + roomIds, + userId + ); + for (RoomUnreadCountProjection projection : projections) { + unreadCountMap.put(projection.getRoomId(), projection.getUnreadCount().intValue()); + } + + Set existingRoomIds = unreadCountMap.keySet(); + for (Integer roomId : roomIds) { + if (!existingRoomIds.contains(roomId)) { + unreadCountMap.put(roomId, 0); + } + } + + return unreadCountMap; + } + + private void validateDirectAccess(ChatRoom chatRoom, Integer userId) { + if (!chatRoomMemberRepository.existsByChatRoomIdAndUserId(chatRoom.getId(), userId)) { + throw CustomException.of(FORBIDDEN_CHAT_ROOM_ACCESS); + } + } + + private Integer resolveDirectSenderId(ChatMessage message, Integer maskedAdminId) { + if (maskedAdminId != null && message.getSender().getRole() == UserRole.ADMIN) { + return maskedAdminId; + } + return message.getSender().getId(); + } + + private Map> getRoomMembersMap(List rooms) { + if (rooms.isEmpty()) { + return Map.of(); + } + + List roomIds = rooms.stream().map(ChatRoom::getId).toList(); + return chatRoomMemberRepository.findByChatRoomIds(roomIds).stream() + .collect(Collectors.groupingBy(ChatRoomMember::getChatRoomId)); + } + + private User findDirectPartner(List members, Integer userId) { + return members.stream() + .map(ChatRoomMember::getUser) + .filter(memberUser -> !memberUser.getId().equals(userId)) + .findFirst() + .orElse(null); + } + + private User findNonAdminMember(List members) { + return members.stream() + .map(ChatRoomMember::getUser) + .filter(memberUser -> memberUser.getRole() != UserRole.ADMIN) + .findFirst() + .orElse(null); + } + + private User findAdminMember(List members) { + return members.stream() + .map(ChatRoomMember::getUser) + .filter(memberUser -> memberUser.getRole() == UserRole.ADMIN) + .findFirst() + .orElse(null); + } + + private User resolveMessageReceiver(User sender, List members) { + if (sender.getRole() == UserRole.ADMIN) { + User nonAdminUser = findNonAdminMember(members); + if (nonAdminUser != null) { + return nonAdminUser; + } + } + + User partner = findDirectPartner(members, sender.getId()); + if (partner == null) { + throw CustomException.of(FORBIDDEN_CHAT_ROOM_ACCESS); + } + return partner; + } + +} diff --git a/src/main/java/gg/agit/konect/domain/chat/unified/controller/ChatController.java b/src/main/java/gg/agit/konect/domain/chat/unified/controller/ChatController.java deleted file mode 100644 index 4be8177c8..000000000 --- a/src/main/java/gg/agit/konect/domain/chat/unified/controller/ChatController.java +++ /dev/null @@ -1,83 +0,0 @@ -package gg.agit.konect.domain.chat.unified.controller; - -import org.springframework.http.ResponseEntity; -import org.springframework.web.bind.annotation.PathVariable; -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; - -import gg.agit.konect.domain.chat.direct.dto.ChatMessageSendRequest; -import gg.agit.konect.domain.chat.direct.dto.ChatRoomCreateRequest; -import gg.agit.konect.domain.chat.direct.dto.ChatRoomResponse; -import gg.agit.konect.domain.chat.direct.service.ChatService; -import gg.agit.konect.domain.chat.unified.dto.UnifiedChatMuteResponse; -import gg.agit.konect.domain.chat.group.service.GroupChatService; -import gg.agit.konect.domain.chat.unified.dto.UnifiedChatMessageResponse; -import gg.agit.konect.domain.chat.unified.dto.UnifiedChatMessagesResponse; -import gg.agit.konect.domain.chat.unified.dto.UnifiedChatRoomsResponse; -import gg.agit.konect.domain.chat.unified.enums.ChatType; -import gg.agit.konect.domain.chat.unified.service.UnifiedChatService; -import gg.agit.konect.global.auth.annotation.UserId; -import jakarta.validation.Valid; -import lombok.RequiredArgsConstructor; - -@RestController -@RequiredArgsConstructor -@RequestMapping("/chats") -public class ChatController implements ChatApi { - - private final ChatService chatService; - private final GroupChatService groupChatService; - private final UnifiedChatService unifiedChatService; - - @Override - public ResponseEntity createOrGetChatRoom( - @Valid @RequestBody ChatRoomCreateRequest request, - @UserId Integer userId - ) { - ChatRoomResponse response = chatService.createOrGetChatRoom(userId, request); - return ResponseEntity.ok(response); - } - - @Override - public ResponseEntity getChatRooms( - @UserId Integer userId - ) { - UnifiedChatRoomsResponse response = unifiedChatService.getChatRooms(userId); - return ResponseEntity.ok(response); - } - - @Override - public ResponseEntity getChatRoomMessages( - @RequestParam(name = "type") ChatType type, - @RequestParam(name = "page", defaultValue = "1") Integer page, - @RequestParam(name = "limit", defaultValue = "30", required = false) Integer limit, - @PathVariable(value = "chatRoomId") Integer chatRoomId, - @UserId Integer userId - ) { - UnifiedChatMessagesResponse response = unifiedChatService.getMessages(userId, type, chatRoomId, page, limit); - return ResponseEntity.ok(response); - } - - @Override - public ResponseEntity sendMessage( - @RequestParam(name = "type") ChatType type, - @PathVariable(value = "chatRoomId") Integer chatRoomId, - @Valid @RequestBody ChatMessageSendRequest request, - @UserId Integer userId - ) { - UnifiedChatMessageResponse response = unifiedChatService.sendMessage(userId, type, chatRoomId, request); - return ResponseEntity.ok(response); - } - - @Override - public ResponseEntity toggleGroupChatMute( - @RequestParam(name = "type") ChatType type, - @PathVariable(value = "chatRoomId") Integer chatRoomId, - @UserId Integer userId - ) { - Boolean isMuted = groupChatService.toggleMute(userId, type, chatRoomId); - return ResponseEntity.ok(new UnifiedChatMuteResponse(isMuted)); - } -} diff --git a/src/main/java/gg/agit/konect/domain/chat/unified/dto/UnifiedChatMessagesResponse.java b/src/main/java/gg/agit/konect/domain/chat/unified/dto/UnifiedChatMessagesResponse.java deleted file mode 100644 index 14e246d1c..000000000 --- a/src/main/java/gg/agit/konect/domain/chat/unified/dto/UnifiedChatMessagesResponse.java +++ /dev/null @@ -1,29 +0,0 @@ -package gg.agit.konect.domain.chat.unified.dto; - -import static io.swagger.v3.oas.annotations.media.Schema.RequiredMode.NOT_REQUIRED; -import static io.swagger.v3.oas.annotations.media.Schema.RequiredMode.REQUIRED; - -import java.util.List; - -import io.swagger.v3.oas.annotations.media.Schema; - -public record UnifiedChatMessagesResponse( - @Schema(description = "조건에 해당하는 메시지 수", example = "100", requiredMode = REQUIRED) - Long totalCount, - - @Schema(description = "현재 페이지에서 조회된 메시지 수", example = "20", requiredMode = REQUIRED) - Integer currentCount, - - @Schema(description = "최대 페이지", example = "5", requiredMode = REQUIRED) - Integer totalPage, - - @Schema(description = "현재 페이지", example = "1", requiredMode = REQUIRED) - Integer currentPage, - - @Schema(description = "동아리 ID", example = "1", requiredMode = NOT_REQUIRED) - Integer clubId, - - @Schema(description = "채팅 메시지 리스트", requiredMode = REQUIRED) - List messages -) { -} diff --git a/src/main/java/gg/agit/konect/domain/chat/unified/enums/ChatType.java b/src/main/java/gg/agit/konect/domain/chat/unified/enums/ChatType.java deleted file mode 100644 index 2fa4f32f6..000000000 --- a/src/main/java/gg/agit/konect/domain/chat/unified/enums/ChatType.java +++ /dev/null @@ -1,6 +0,0 @@ -package gg.agit.konect.domain.chat.unified.enums; - -public enum ChatType { - DIRECT, - GROUP -} diff --git a/src/main/java/gg/agit/konect/domain/chat/unified/service/UnifiedChatService.java b/src/main/java/gg/agit/konect/domain/chat/unified/service/UnifiedChatService.java deleted file mode 100644 index ddbf577bb..000000000 --- a/src/main/java/gg/agit/konect/domain/chat/unified/service/UnifiedChatService.java +++ /dev/null @@ -1,207 +0,0 @@ -package gg.agit.konect.domain.chat.unified.service; - -import java.util.ArrayList; -import java.util.Comparator; -import java.util.HashMap; -import java.util.List; -import java.util.Map; - -import org.springframework.stereotype.Service; - -import gg.agit.konect.domain.chat.direct.dto.ChatMessageResponse; -import gg.agit.konect.domain.chat.direct.dto.ChatMessageSendRequest; -import gg.agit.konect.domain.chat.direct.dto.ChatMessagesResponse; -import gg.agit.konect.domain.chat.direct.dto.ChatRoomsResponse; -import gg.agit.konect.domain.chat.direct.service.ChatService; -import gg.agit.konect.domain.chat.group.dto.GroupChatMessageResponse; -import gg.agit.konect.domain.chat.group.dto.GroupChatMessagesResponse; -import gg.agit.konect.domain.chat.group.dto.GroupChatRoomsResponse; -import gg.agit.konect.domain.chat.group.service.GroupChatService; -import gg.agit.konect.domain.chat.unified.dto.UnifiedChatMessageResponse; -import gg.agit.konect.domain.chat.unified.dto.UnifiedChatMessagesResponse; -import gg.agit.konect.domain.chat.unified.dto.UnifiedChatRoomResponse; -import gg.agit.konect.domain.chat.unified.dto.UnifiedChatRoomsResponse; -import gg.agit.konect.domain.chat.unified.enums.ChatType; -import gg.agit.konect.domain.notification.enums.NotificationTargetType; -import gg.agit.konect.domain.notification.model.NotificationMuteSetting; -import gg.agit.konect.domain.notification.repository.NotificationMuteSettingRepository; -import lombok.RequiredArgsConstructor; - -@Service -@RequiredArgsConstructor -public class UnifiedChatService { - - private final ChatService chatService; - private final GroupChatService groupChatService; - private final NotificationMuteSettingRepository notificationMuteSettingRepository; - - public UnifiedChatRoomsResponse getChatRooms(Integer userId) { - ChatRoomsResponse directRooms = chatService.getChatRooms(userId); - GroupChatRoomsResponse groupRooms = groupChatService.getChatRooms(userId); - Map directMuteMap = getMuteMap( - NotificationTargetType.DIRECT_CHAT_ROOM, - directRooms.chatRooms().stream() - .map(ChatRoomsResponse.InnerChatRoomResponse::chatRoomId) - .toList(), - userId - ); - Map groupMuteMap = getMuteMap( - NotificationTargetType.GROUP_CHAT_ROOM, - groupRooms.groupChatRooms().stream() - .map(GroupChatRoomsResponse.InnerGroupChatRoomResponse::groupChatRoomId) - .toList(), - userId - ); - - List rooms = new ArrayList<>(); - - directRooms.chatRooms().forEach(room -> rooms.add(new UnifiedChatRoomResponse( - room.chatRoomId(), - ChatType.DIRECT, - room.chatPartnerName(), - room.chatPartnerProfileImage(), - room.lastMessage(), - room.lastSentTime(), - room.unreadCount(), - directMuteMap.getOrDefault(room.chatRoomId(), false) - ))); - - groupRooms.groupChatRooms().forEach(room -> rooms.add(new UnifiedChatRoomResponse( - room.groupChatRoomId(), - ChatType.GROUP, - room.clubName(), - room.clubImageUrl(), - room.lastMessage(), - room.lastSentAt(), - room.unreadMessageCount(), - groupMuteMap.getOrDefault(room.groupChatRoomId(), false) - ))); - - rooms.sort( - Comparator.comparing(UnifiedChatRoomResponse::lastSentAt, Comparator.nullsLast(Comparator.reverseOrder())) - .thenComparing(UnifiedChatRoomResponse::roomId) - ); - - return new UnifiedChatRoomsResponse(rooms); - } - - private Map getMuteMap( - NotificationTargetType targetType, - List roomIds, - Integer userId - ) { - if (roomIds.isEmpty()) { - return Map.of(); - } - - List settings = notificationMuteSettingRepository - .findByTargetTypeAndTargetIdsAndUserId(targetType, roomIds, userId); - - Map muteMap = new HashMap<>(); - for (NotificationMuteSetting setting : settings) { - Integer targetId = setting.getTargetId(); - if (targetId != null) { - muteMap.put(targetId, setting.getIsMuted()); - } - } - - return muteMap; - } - - public UnifiedChatMessagesResponse getMessages( - Integer userId, - ChatType chatType, - Integer roomId, - Integer page, - Integer limit - ) { - if (chatType == ChatType.DIRECT) { - ChatMessagesResponse response = chatService.getChatRoomMessages(userId, roomId, page, limit); - return toUnifiedResponse(response); - } - - GroupChatMessagesResponse response = groupChatService.getMessagesByRoomId(roomId, userId, page, limit); - return toUnifiedResponse(response); - } - - public UnifiedChatMessageResponse sendMessage( - Integer userId, - ChatType chatType, - Integer roomId, - ChatMessageSendRequest request - ) { - if (chatType == ChatType.DIRECT) { - ChatMessageResponse response = chatService.sendMessage(userId, roomId, request); - return new UnifiedChatMessageResponse( - response.messageId(), - response.senderId(), - null, - response.content(), - response.createdAt(), - response.isRead(), - null, - response.isMine() - ); - } - - GroupChatMessageResponse response = groupChatService.sendMessageByRoomId(roomId, userId, request.content()); - return new UnifiedChatMessageResponse( - response.messageId(), - response.senderId(), - response.senderName(), - response.content(), - response.createdAt(), - null, - response.unreadCount(), - response.isMine() - ); - } - - private UnifiedChatMessagesResponse toUnifiedResponse(ChatMessagesResponse response) { - List messages = response.messages().stream() - .map(message -> new UnifiedChatMessageResponse( - message.messageId(), - message.senderId(), - null, - message.content(), - message.createdAt(), - message.isRead(), - null, - message.isMine() - )) - .toList(); - - return new UnifiedChatMessagesResponse( - response.totalCount(), - response.currentCount(), - response.totalPage(), - response.currentPage(), - null, - messages - ); - } - - private UnifiedChatMessagesResponse toUnifiedResponse(GroupChatMessagesResponse response) { - List messages = response.messages().stream() - .map(message -> new UnifiedChatMessageResponse( - message.messageId(), - message.senderId(), - message.senderName(), - message.content(), - message.createdAt(), - null, - message.unreadCount(), - message.isMine() - )) - .toList(); - - return new UnifiedChatMessagesResponse( - response.totalCount(), - response.currentCount(), - response.totalPage(), - response.currentPage(), - response.clubId(), - messages - ); - } -} diff --git a/src/main/java/gg/agit/konect/domain/club/service/ClubApplicationService.java b/src/main/java/gg/agit/konect/domain/club/service/ClubApplicationService.java index 47e3c7a1b..f21d26782 100644 --- a/src/main/java/gg/agit/konect/domain/club/service/ClubApplicationService.java +++ b/src/main/java/gg/agit/konect/domain/club/service/ClubApplicationService.java @@ -18,6 +18,7 @@ import org.springframework.util.StringUtils; import gg.agit.konect.domain.bank.repository.BankRepository; +import gg.agit.konect.domain.chat.service.ChatRoomMembershipService; import gg.agit.konect.domain.club.dto.ClubApplicationAnswersResponse; import gg.agit.konect.domain.club.dto.ClubApplicationCondition; import gg.agit.konect.domain.club.dto.ClubApplicationsResponse; @@ -63,6 +64,7 @@ public class ClubApplicationService { private final BankRepository bankRepository; private final ClubPermissionValidator clubPermissionValidator; private final NotificationService notificationService; + private final ChatRoomMembershipService chatRoomMembershipService; public ClubAppliedClubsResponse getAppliedClubs(Integer userId) { List clubApplies = clubApplyRepository.findAllPendingByUserIdWithClub(userId); @@ -121,7 +123,8 @@ public void approveClubApplication(Integer clubId, Integer applicationId, Intege .isFeePaid(true) .build(); - clubMemberRepository.save(newMember); + ClubMember savedMember = clubMemberRepository.save(newMember); + chatRoomMembershipService.addClubMember(savedMember); clubApplyRepository.delete(clubApply); notificationService.sendClubApplicationApprovedNotification( diff --git a/src/main/java/gg/agit/konect/domain/club/service/ClubMemberManagementService.java b/src/main/java/gg/agit/konect/domain/club/service/ClubMemberManagementService.java index 10ee73400..acf2bba40 100644 --- a/src/main/java/gg/agit/konect/domain/club/service/ClubMemberManagementService.java +++ b/src/main/java/gg/agit/konect/domain/club/service/ClubMemberManagementService.java @@ -10,6 +10,7 @@ import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; +import gg.agit.konect.domain.chat.service.ChatRoomMembershipService; import gg.agit.konect.domain.club.dto.ClubPreMemberAddRequest; import gg.agit.konect.domain.club.dto.ClubPreMemberAddResponse; import gg.agit.konect.domain.club.dto.ClubPreMembersResponse; @@ -40,6 +41,7 @@ public class ClubMemberManagementService { private final ClubPreMemberRepository clubPreMemberRepository; private final ClubPermissionValidator clubPermissionValidator; private final UserRepository userRepository; + private final ChatRoomMembershipService chatRoomMembershipService; @Transactional public ClubMember changeMemberPosition( @@ -118,6 +120,7 @@ private ClubPreMemberAddResponse addDirectMember(Club club, User user, ClubPosit .build(); ClubMember savedMember = clubMemberRepository.save(clubMember); + chatRoomMembershipService.addClubMember(savedMember); return ClubPreMemberAddResponse.from(savedMember); } @@ -257,6 +260,7 @@ public void removeMember(Integer clubId, Integer targetUserId, Integer requester } clubMemberRepository.delete(target); + chatRoomMembershipService.removeClubMember(clubId, targetUserId); } private void validateNotSelf(Integer userId1, Integer userId2, ApiResponseCode errorCode) { diff --git a/src/main/java/gg/agit/konect/domain/club/service/ClubService.java b/src/main/java/gg/agit/konect/domain/club/service/ClubService.java index b9c47a1aa..07a14ccde 100644 --- a/src/main/java/gg/agit/konect/domain/club/service/ClubService.java +++ b/src/main/java/gg/agit/konect/domain/club/service/ClubService.java @@ -14,8 +14,9 @@ import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; -import gg.agit.konect.domain.chat.group.model.GroupChatRoom; -import gg.agit.konect.domain.chat.group.repository.GroupChatRoomRepository; +import gg.agit.konect.domain.chat.model.ChatRoom; +import gg.agit.konect.domain.chat.repository.ChatRoomRepository; +import gg.agit.konect.domain.chat.service.ChatRoomMembershipService; import gg.agit.konect.domain.club.dto.ClubBasicInfoUpdateRequest; import gg.agit.konect.domain.club.dto.ClubCondition; import gg.agit.konect.domain.club.dto.ClubCreateRequest; @@ -51,7 +52,8 @@ public class ClubService { private final ClubApplyRepository clubApplyRepository; private final UserRepository userRepository; private final ClubPermissionValidator clubPermissionValidator; - private final GroupChatRoomRepository groupChatRoomRepository; + private final ChatRoomRepository chatRoomRepository; + private final ChatRoomMembershipService chatRoomMembershipService; public ClubsResponse getClubs(ClubCondition condition, Integer userId) { User user = userRepository.getById(userId); @@ -107,7 +109,7 @@ public ClubDetailResponse createClub(Integer userId, ClubCreateRequest request) Club savedClub = clubRepository.save(club); - groupChatRoomRepository.save(GroupChatRoom.of(savedClub)); + chatRoomRepository.save(ChatRoom.groupOf(savedClub)); ClubMember president = ClubMember.builder() .club(savedClub) @@ -116,7 +118,8 @@ public ClubDetailResponse createClub(Integer userId, ClubCreateRequest request) .isFeePaid(false) .build(); - clubMemberRepository.save(president); + ClubMember savedPresident = clubMemberRepository.save(president); + chatRoomMembershipService.addClubMember(savedPresident); return getClubDetail(savedClub.getId(), userId); } diff --git a/src/main/java/gg/agit/konect/domain/notification/enums/NotificationTargetType.java b/src/main/java/gg/agit/konect/domain/notification/enums/NotificationTargetType.java index abe1ef11f..8e7efa152 100644 --- a/src/main/java/gg/agit/konect/domain/notification/enums/NotificationTargetType.java +++ b/src/main/java/gg/agit/konect/domain/notification/enums/NotificationTargetType.java @@ -1,6 +1,5 @@ package gg.agit.konect.domain.notification.enums; public enum NotificationTargetType { - DIRECT_CHAT_ROOM, - GROUP_CHAT_ROOM + CHAT_ROOM } diff --git a/src/main/java/gg/agit/konect/domain/notification/model/NotificationMuteSetting.java b/src/main/java/gg/agit/konect/domain/notification/model/NotificationMuteSetting.java index 6007cfb9b..d5ebab0e3 100644 --- a/src/main/java/gg/agit/konect/domain/notification/model/NotificationMuteSetting.java +++ b/src/main/java/gg/agit/konect/domain/notification/model/NotificationMuteSetting.java @@ -17,7 +17,6 @@ import jakarta.persistence.ManyToOne; import jakarta.persistence.Table; import jakarta.persistence.UniqueConstraint; -import lombok.Builder; import lombok.Getter; import lombok.NoArgsConstructor; @@ -51,7 +50,6 @@ public class NotificationMuteSetting extends BaseEntity { @Column(name = "is_muted", nullable = false) private Boolean isMuted; - @Builder private NotificationMuteSetting( Integer id, NotificationTargetType targetType, @@ -72,12 +70,7 @@ public static NotificationMuteSetting of( User user, Boolean isMuted ) { - return NotificationMuteSetting.builder() - .targetType(targetType) - .targetId(targetId) - .user(user) - .isMuted(isMuted) - .build(); + return new NotificationMuteSetting(null, targetType, targetId, user, isMuted); } public void toggleMute() { diff --git a/src/main/java/gg/agit/konect/domain/notification/service/NotificationService.java b/src/main/java/gg/agit/konect/domain/notification/service/NotificationService.java index b5eec80c1..8d18e4e03 100644 --- a/src/main/java/gg/agit/konect/domain/notification/service/NotificationService.java +++ b/src/main/java/gg/agit/konect/domain/notification/service/NotificationService.java @@ -19,7 +19,7 @@ import org.springframework.transaction.annotation.Transactional; import org.springframework.web.client.RestTemplate; -import gg.agit.konect.domain.chat.unified.service.ChatPresenceService; +import gg.agit.konect.domain.chat.service.ChatPresenceService; import gg.agit.konect.domain.notification.dto.NotificationTokenDeleteRequest; import gg.agit.konect.domain.notification.dto.NotificationTokenRegisterRequest; import gg.agit.konect.domain.notification.enums.NotificationTargetType; @@ -111,7 +111,7 @@ public void sendChatNotification(Integer receiverId, Integer roomId, String send } boolean isMuted = notificationMuteSettingRepository.findByTargetTypeAndTargetIdAndUserId( - NotificationTargetType.DIRECT_CHAT_ROOM, + NotificationTargetType.CHAT_ROOM, roomId, receiverId ) @@ -230,6 +230,23 @@ public void sendGroupChatNotification( continue; } + boolean isMuted = notificationMuteSettingRepository.findByTargetTypeAndTargetIdAndUserId( + NotificationTargetType.CHAT_ROOM, + roomId, + recipientId + ) + .map(setting -> Boolean.TRUE.equals(setting.getIsMuted())) + .orElse(false); + + if (isMuted) { + log.debug( + "Group chat muted, skipping notification: roomId={}, recipientId={}", + roomId, + recipientId + ); + continue; + } + List tokens = notificationDeviceTokenRepository.findTokensByUserId(recipientId); if (tokens.isEmpty()) { log.debug("No device tokens found for user: recipientId={}", recipientId); diff --git a/src/main/java/gg/agit/konect/domain/user/service/UserService.java b/src/main/java/gg/agit/konect/domain/user/service/UserService.java index 6fcdaaa5e..a74e084b9 100644 --- a/src/main/java/gg/agit/konect/domain/user/service/UserService.java +++ b/src/main/java/gg/agit/konect/domain/user/service/UserService.java @@ -4,17 +4,20 @@ import static gg.agit.konect.global.code.ApiResponseCode.CANNOT_DELETE_CLUB_PRESIDENT; import static gg.agit.konect.global.code.ApiResponseCode.CANNOT_DELETE_USER_WITH_UNPAID_FEE; +import java.time.LocalDateTime; import java.util.List; +import java.util.Objects; import org.springframework.context.ApplicationEventPublisher; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import org.springframework.util.StringUtils; -import gg.agit.konect.domain.chat.direct.model.ChatMessage; -import gg.agit.konect.domain.chat.direct.model.ChatRoom; -import gg.agit.konect.domain.chat.direct.repository.ChatMessageRepository; -import gg.agit.konect.domain.chat.direct.repository.ChatRoomRepository; +import gg.agit.konect.domain.chat.model.ChatMessage; +import gg.agit.konect.domain.chat.model.ChatRoom; +import gg.agit.konect.domain.chat.repository.ChatMessageRepository; +import gg.agit.konect.domain.chat.repository.ChatRoomRepository; +import gg.agit.konect.domain.chat.service.ChatRoomMembershipService; import gg.agit.konect.domain.club.model.ClubMember; import gg.agit.konect.domain.club.model.ClubPreMember; import gg.agit.konect.domain.club.repository.ClubMemberRepository; @@ -58,6 +61,7 @@ public class UserService { private final StudyTimeQueryService studyTimeQueryService; private final ApplicationEventPublisher applicationEventPublisher; private final AppleTokenRevocationService appleTokenRevocationService; + private final ChatRoomMembershipService chatRoomMembershipService; @Transactional public Integer signup(String email, String providerId, Provider provider, SignupRequest request) { @@ -115,11 +119,19 @@ private void sendWelcomeMessage(User newUser) { return; } + ChatRoom.validateIsNotSameParticipant(operator, newUser); + ChatRoom chatRoom = chatRoomRepository.findByTwoUsers(operator.getId(), newUser.getId()) - .orElseGet(() -> chatRoomRepository.save(ChatRoom.of(operator, newUser))); + .orElseGet(() -> chatRoomRepository.save(ChatRoom.directOf())); + + LocalDateTime joinedAt = Objects.requireNonNull( + chatRoom.getCreatedAt(), + "chatRoom.createdAt must not be null" + ); + chatRoomMembershipService.addDirectMembers(chatRoom, operator, newUser, joinedAt); ChatMessage chatMessage = chatMessageRepository.save( - ChatMessage.of(chatRoom, operator, newUser, DEFAULT_WELCOME_MESSAGE) + ChatMessage.of(chatRoom, operator, DEFAULT_WELCOME_MESSAGE) ); chatRoom.updateLastMessage(chatMessage.getContent(), chatMessage.getCreatedAt()); @@ -161,7 +173,8 @@ private void joinPreMembers(User user, Integer universityId, String studentNumbe .isFeePaid(false) .build(); - clubMemberRepository.save(clubMember); + ClubMember savedMember = clubMemberRepository.save(clubMember); + chatRoomMembershipService.addClubMember(savedMember); } clubPreMemberRepository.deleteAll(preMembers); @@ -170,7 +183,10 @@ private void joinPreMembers(User user, Integer universityId, String studentNumbe private void replaceCurrentPresident(Integer clubId, Integer newPresidentUserId) { clubMemberRepository.findPresidentByClubId(clubId) .filter(currentPresident -> !currentPresident.getId().getUserId().equals(newPresidentUserId)) - .ifPresent(clubMemberRepository::delete); + .ifPresent(currentPresident -> { + clubMemberRepository.delete(currentPresident); + chatRoomMembershipService.removeClubMember(clubId, currentPresident.getUser().getId()); + }); } public UserInfoResponse getUserInfo(Integer userId) { diff --git a/src/main/resources/db/migration/V26__add_unified_chat_room_table.sql b/src/main/resources/db/migration/V26__add_unified_chat_room_table.sql new file mode 100644 index 000000000..7abeead78 --- /dev/null +++ b/src/main/resources/db/migration/V26__add_unified_chat_room_table.sql @@ -0,0 +1,253 @@ +ALTER TABLE chat_room + ADD COLUMN club_id INT NULL; + +ALTER TABLE chat_room + ADD CONSTRAINT fk_chat_room_club_id FOREIGN KEY (club_id) REFERENCES club (id) ON DELETE CASCADE; + +CREATE UNIQUE INDEX uq_chat_room_group_club_id + ON chat_room (club_id); + +SET @receiver_fk_name = ( + SELECT kcu.CONSTRAINT_NAME + FROM information_schema.KEY_COLUMN_USAGE kcu + WHERE kcu.TABLE_SCHEMA = DATABASE() + AND kcu.TABLE_NAME = 'chat_message' + AND kcu.COLUMN_NAME = 'receiver_id' + AND kcu.REFERENCED_TABLE_NAME IS NOT NULL + LIMIT 1 +); + +SET @drop_receiver_fk_sql = IF( + @receiver_fk_name IS NULL, + 'SELECT 1', + CONCAT('ALTER TABLE chat_message DROP FOREIGN KEY `', @receiver_fk_name, '`') +); + +PREPARE drop_receiver_fk_stmt FROM @drop_receiver_fk_sql; +EXECUTE drop_receiver_fk_stmt; +DEALLOCATE PREPARE drop_receiver_fk_stmt; + +ALTER TABLE chat_message + DROP COLUMN receiver_id; + +ALTER TABLE chat_message + DROP COLUMN is_read; + +CREATE TABLE IF NOT EXISTS chat_room_member +( + chat_room_id INT NOT NULL, + user_id INT NOT NULL, + last_read_at TIMESTAMP NOT NULL, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP NOT NULL, + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP NOT NULL, + + PRIMARY KEY (chat_room_id, user_id), + FOREIGN KEY (chat_room_id) REFERENCES chat_room (id) ON DELETE CASCADE, + FOREIGN KEY (user_id) REFERENCES users (id) ON DELETE CASCADE +); + +CREATE INDEX idx_chat_room_member_user_id + ON chat_room_member (user_id, chat_room_id); + +CREATE INDEX idx_chat_room_member_room_last_read + ON chat_room_member (chat_room_id, last_read_at); + +INSERT INTO chat_room_member (chat_room_id, user_id, last_read_at, created_at, updated_at) +SELECT src.chat_room_id, + src.user_id, + src.last_read_at, + src.created_at, + src.updated_at +FROM ( + SELECT cr.id AS chat_room_id, + cr.sender_id AS user_id, + COALESCE(cr.last_message_sent_at, cr.created_at) AS last_read_at, + cr.created_at AS created_at, + NOW() AS updated_at + FROM chat_room cr + WHERE cr.sender_id IS NOT NULL + ) src +ON DUPLICATE KEY UPDATE + last_read_at = GREATEST(chat_room_member.last_read_at, src.last_read_at); + +INSERT INTO chat_room_member (chat_room_id, user_id, last_read_at, created_at, updated_at) +SELECT src.chat_room_id, + src.user_id, + src.last_read_at, + src.created_at, + src.updated_at +FROM ( + SELECT cr.id AS chat_room_id, + cr.receiver_id AS user_id, + COALESCE(cr.last_message_sent_at, cr.created_at) AS last_read_at, + cr.created_at AS created_at, + NOW() AS updated_at + FROM chat_room cr + WHERE cr.receiver_id IS NOT NULL + ) src +ON DUPLICATE KEY UPDATE + last_read_at = GREATEST(chat_room_member.last_read_at, src.last_read_at); + +SET @chat_room_sender_fk_name = ( + SELECT kcu.CONSTRAINT_NAME + FROM information_schema.KEY_COLUMN_USAGE kcu + WHERE kcu.TABLE_SCHEMA = DATABASE() + AND kcu.TABLE_NAME = 'chat_room' + AND kcu.COLUMN_NAME = 'sender_id' + AND kcu.REFERENCED_TABLE_NAME IS NOT NULL + LIMIT 1 +); + +SET @drop_chat_room_sender_fk_sql = IF( + @chat_room_sender_fk_name IS NULL, + 'SELECT 1', + CONCAT('ALTER TABLE chat_room DROP FOREIGN KEY `', @chat_room_sender_fk_name, '`') +); + +PREPARE drop_chat_room_sender_fk_stmt FROM @drop_chat_room_sender_fk_sql; +EXECUTE drop_chat_room_sender_fk_stmt; +DEALLOCATE PREPARE drop_chat_room_sender_fk_stmt; + +SET @chat_room_receiver_fk_name = ( + SELECT kcu.CONSTRAINT_NAME + FROM information_schema.KEY_COLUMN_USAGE kcu + WHERE kcu.TABLE_SCHEMA = DATABASE() + AND kcu.TABLE_NAME = 'chat_room' + AND kcu.COLUMN_NAME = 'receiver_id' + AND kcu.REFERENCED_TABLE_NAME IS NOT NULL + LIMIT 1 +); + +SET @drop_chat_room_receiver_fk_sql = IF( + @chat_room_receiver_fk_name IS NULL, + 'SELECT 1', + CONCAT('ALTER TABLE chat_room DROP FOREIGN KEY `', @chat_room_receiver_fk_name, '`') +); + +PREPARE drop_chat_room_receiver_fk_stmt FROM @drop_chat_room_receiver_fk_sql; +EXECUTE drop_chat_room_receiver_fk_stmt; +DEALLOCATE PREPARE drop_chat_room_receiver_fk_stmt; + +ALTER TABLE chat_room + DROP COLUMN sender_id, + DROP COLUMN receiver_id; + +INSERT INTO chat_room ( + last_message_content, + last_message_sent_at, + club_id, + created_at, + updated_at +) +SELECT gm.content, + gm.created_at, + gcr.club_id, + gcr.created_at, + gcr.updated_at +FROM group_chat_room gcr + LEFT JOIN ( + SELECT m.room_id, + m.content, + m.created_at + FROM group_chat_message m + JOIN ( + SELECT room_id, MAX(id) AS max_id + FROM group_chat_message + GROUP BY room_id + ) lm ON lm.max_id = m.id +) gm ON gm.room_id = gcr.id + LEFT JOIN chat_room cr ON cr.club_id = gcr.club_id +WHERE cr.id IS NULL; + +INSERT INTO chat_room_member (chat_room_id, user_id, last_read_at, created_at, updated_at) +SELECT src.chat_room_id, + src.user_id, + src.last_read_at, + src.created_at, + src.updated_at +FROM ( + SELECT cr.id AS chat_room_id, + cm.user_id AS user_id, + COALESCE(grs.last_read_at, cm.created_at) AS last_read_at, + cm.created_at AS created_at, + NOW() AS updated_at + FROM chat_room cr + JOIN club_member cm ON cm.club_id = cr.club_id + LEFT JOIN group_chat_room gcr ON gcr.club_id = cr.club_id + LEFT JOIN group_chat_read_status grs ON grs.room_id = gcr.id + AND grs.user_id = cm.user_id + WHERE cr.club_id IS NOT NULL + ) src +ON DUPLICATE KEY UPDATE + last_read_at = GREATEST(chat_room_member.last_read_at, src.last_read_at); + +INSERT INTO chat_message ( + chat_room_id, + sender_id, + content, + created_at, + updated_at +) +SELECT cr.id, + gm.sender_id, + gm.content, + gm.created_at, + gm.updated_at +FROM group_chat_message gm + JOIN group_chat_room gcr ON gcr.id = gm.room_id + JOIN chat_room cr ON cr.club_id = gcr.club_id + LEFT JOIN chat_message cm + ON cm.chat_room_id = cr.id + AND cm.sender_id = gm.sender_id + AND cm.content = gm.content + AND cm.created_at = gm.created_at +WHERE cm.id IS NULL; + +UPDATE chat_room cr + JOIN ( + SELECT cm.chat_room_id, + cm.content, + cm.created_at + FROM chat_message cm + JOIN ( + SELECT chat_room_id, MAX(id) AS max_id + FROM chat_message + GROUP BY chat_room_id + ) lm ON lm.max_id = cm.id +) lm ON lm.chat_room_id = cr.id +SET cr.last_message_content = lm.content, + cr.last_message_sent_at = lm.created_at, + cr.updated_at = GREATEST(cr.updated_at, lm.created_at) +WHERE cr.club_id IS NOT NULL; + +INSERT INTO notification_mute_setting (user_id, target_type, target_id, is_muted, created_at, updated_at) +SELECT nms.user_id, + 'CHAT_ROOM', + nms.target_id, + nms.is_muted, + nms.created_at, + nms.updated_at +FROM notification_mute_setting nms + LEFT JOIN notification_mute_setting existed + ON existed.user_id = nms.user_id + AND existed.target_type = 'CHAT_ROOM' + AND existed.target_id = nms.target_id +WHERE nms.target_type = 'DIRECT_CHAT_ROOM' + AND existed.id IS NULL; + +INSERT INTO notification_mute_setting (user_id, target_type, target_id, is_muted, created_at, updated_at) +SELECT nms.user_id, + 'CHAT_ROOM', + cr.id, + nms.is_muted, + nms.created_at, + nms.updated_at +FROM notification_mute_setting nms + JOIN group_chat_room gcr ON gcr.id = nms.target_id + JOIN chat_room cr ON cr.club_id = gcr.club_id + LEFT JOIN notification_mute_setting existed + ON existed.user_id = nms.user_id + AND existed.target_type = 'CHAT_ROOM' + AND existed.target_id = cr.id +WHERE nms.target_type = 'GROUP_CHAT_ROOM' + AND existed.id IS NULL;